diff --git a/package.json b/package.json index a1126ce263c..fe9143c0756 100644 --- a/package.json +++ b/package.json @@ -259,7 +259,7 @@ "redux-thunk": "2.3.0", "reselect": "4.0.0", "rst2html": "github:thoward/rst2html#990cb89", - "rxjs": "6.4.0", + "rxjs": "6.5.4", "search-query-parser": "1.5.2", "slate": "0.47.8", "slate-plain-serializer": "0.7.10", diff --git a/packages/grafana-data/package.json b/packages/grafana-data/package.json index 43985e53b3b..3f2a5a5c5e9 100644 --- a/packages/grafana-data/package.json +++ b/packages/grafana-data/package.json @@ -39,7 +39,7 @@ "rollup-plugin-terser": "4.0.4", "rollup-plugin-typescript2": "0.19.3", "rollup-plugin-visualizer": "0.9.2", - "rxjs": "6.4.0", + "rxjs": "6.5.4", "sinon": "1.17.6", "typescript": "3.7.2" } diff --git a/packages/grafana-runtime/src/services/backendSrv.ts b/packages/grafana-runtime/src/services/backendSrv.ts index 59038a7b274..35ba108c0f5 100644 --- a/packages/grafana-runtime/src/services/backendSrv.ts +++ b/packages/grafana-runtime/src/services/backendSrv.ts @@ -43,10 +43,8 @@ export interface BackendSrv { let singletonInstance: BackendSrv; -export function setBackendSrv(instance: BackendSrv) { +export const setBackendSrv = (instance: BackendSrv) => { singletonInstance = instance; -} +}; -export function getBackendSrv(): BackendSrv { - return singletonInstance; -} +export const getBackendSrv = (): BackendSrv => singletonInstance; diff --git a/public/app/core/components/Select/DashboardPicker.tsx b/public/app/core/components/Select/DashboardPicker.tsx index 924f4141751..b8af8400d50 100644 --- a/public/app/core/components/Select/DashboardPicker.tsx +++ b/public/app/core/components/Select/DashboardPicker.tsx @@ -2,7 +2,7 @@ import React, { PureComponent } from 'react'; import { AsyncSelect } from '@grafana/ui'; import { SelectableValue } from '@grafana/data'; import { debounce } from 'lodash'; -import { getBackendSrv } from 'app/core/services/backend_srv'; +import { backendSrv } from 'app/core/services/backend_srv'; import { DashboardSearchHit, DashboardDTO } from 'app/types'; export interface Props { @@ -33,20 +33,16 @@ export class DashboardPicker extends PureComponent { getDashboards = (query = '') => { this.setState({ isLoading: true }); - return getBackendSrv() - .search({ type: 'dash-db', query }) - .then((result: DashboardSearchHit[]) => { - const dashboards = result.map((item: DashboardSearchHit) => { - return { - id: item.id, - value: item.id, - label: `${item.folderTitle ? item.folderTitle : 'General'}/${item.title}`, - }; - }); + return backendSrv.search({ type: 'dash-db', query }).then((result: DashboardSearchHit[]) => { + const dashboards = result.map((item: DashboardSearchHit) => ({ + id: item.id, + value: item.id, + label: `${item.folderTitle ? item.folderTitle : 'General'}/${item.title}`, + })); - this.setState({ isLoading: false }); - return dashboards; - }); + this.setState({ isLoading: false }); + return dashboards; + }); }; render() { diff --git a/public/app/core/components/Select/TeamPicker.test.tsx b/public/app/core/components/Select/TeamPicker.test.tsx index 8ee09461c7f..e78acec93df 100644 --- a/public/app/core/components/Select/TeamPicker.test.tsx +++ b/public/app/core/components/Select/TeamPicker.test.tsx @@ -3,7 +3,7 @@ import React from 'react'; import renderer from 'react-test-renderer'; import { TeamPicker } from './TeamPicker'; -jest.mock('app/core/services/backend_srv', () => ({ +jest.mock('@grafana/runtime', () => ({ getBackendSrv: () => { return { get: () => { diff --git a/public/app/core/components/Select/TeamPicker.tsx b/public/app/core/components/Select/TeamPicker.tsx index c17216c7939..2ad1825e254 100644 --- a/public/app/core/components/Select/TeamPicker.tsx +++ b/public/app/core/components/Select/TeamPicker.tsx @@ -2,7 +2,7 @@ import React, { Component } from 'react'; import _ from 'lodash'; import { AsyncSelect } from '@grafana/ui'; import { debounce } from 'lodash'; -import { getBackendSrv } from 'app/core/services/backend_srv'; +import { getBackendSrv } from '@grafana/runtime'; export interface Team { id: number; @@ -35,27 +35,28 @@ export class TeamPicker extends Component { } search(query?: string) { - const backendSrv = getBackendSrv(); this.setState({ isLoading: true }); if (_.isNil(query)) { query = ''; } - return backendSrv.get(`/api/teams/search?perpage=100&page=1&query=${query}`).then((result: any) => { - const teams = result.teams.map((team: any) => { - return { - id: team.id, - value: team.id, - label: team.name, - name: team.name, - imgUrl: team.avatarUrl, - }; - }); + return getBackendSrv() + .get(`/api/teams/search?perpage=100&page=1&query=${query}`) + .then((result: any) => { + const teams = result.teams.map((team: any) => { + return { + id: team.id, + value: team.id, + label: team.name, + name: team.name, + imgUrl: team.avatarUrl, + }; + }); - this.setState({ isLoading: false }); - return teams; - }); + this.setState({ isLoading: false }); + return teams; + }); } render() { diff --git a/public/app/core/components/Select/UserPicker.test.tsx b/public/app/core/components/Select/UserPicker.test.tsx index b1dddb48cff..5e148d55429 100644 --- a/public/app/core/components/Select/UserPicker.test.tsx +++ b/public/app/core/components/Select/UserPicker.test.tsx @@ -3,14 +3,8 @@ import React from 'react'; import renderer from 'react-test-renderer'; import { UserPicker } from './UserPicker'; -jest.mock('app/core/services/backend_srv', () => ({ - getBackendSrv: () => { - return { - get: () => { - return Promise.resolve([]); - }, - }; - }, +jest.mock('@grafana/runtime', () => ({ + getBackendSrv: () => ({ get: jest.fn().mockResolvedValue([]) }), })); describe('UserPicker', () => { diff --git a/public/app/core/components/Select/UserPicker.tsx b/public/app/core/components/Select/UserPicker.tsx index 5f8a96f65cd..c3545a193fe 100644 --- a/public/app/core/components/Select/UserPicker.tsx +++ b/public/app/core/components/Select/UserPicker.tsx @@ -7,7 +7,7 @@ import { AsyncSelect } from '@grafana/ui'; // Utils & Services import { debounce } from 'lodash'; -import { getBackendSrv } from 'app/core/services/backend_srv'; +import { getBackendSrv } from '@grafana/runtime'; // Types import { User } from 'app/types'; @@ -36,14 +36,13 @@ export class UserPicker extends Component { } search(query?: string) { - const backendSrv = getBackendSrv(); this.setState({ isLoading: true }); if (_.isNil(query)) { query = ''; } - return backendSrv + return getBackendSrv() .get(`/api/org/users/lookup?query=${query}&limit=10`) .then((result: any) => { return result.map((user: any) => ({ diff --git a/public/app/core/components/SharedPreferences/SharedPreferences.tsx b/public/app/core/components/SharedPreferences/SharedPreferences.tsx index 8d5ff6cb34b..3aec9c8c0c4 100644 --- a/public/app/core/components/SharedPreferences/SharedPreferences.tsx +++ b/public/app/core/components/SharedPreferences/SharedPreferences.tsx @@ -3,7 +3,7 @@ import React, { PureComponent } from 'react'; import { FormLabel, Select } from '@grafana/ui'; import { DashboardSearchHit, DashboardSearchHitType } from 'app/types'; -import { getBackendSrv } from 'app/core/services/backend_srv'; +import { backendSrv } from 'app/core/services/backend_srv'; export interface Props { resourceUri: string; @@ -29,7 +29,7 @@ const timezones = [ ]; export class SharedPreferences extends PureComponent { - backendSrv = getBackendSrv(); + backendSrv = backendSrv; constructor(props: Props) { super(props); @@ -43,8 +43,8 @@ export class SharedPreferences extends PureComponent { } async componentDidMount() { - const prefs = await this.backendSrv.get(`/api/${this.props.resourceUri}/preferences`); - const dashboards = await this.backendSrv.search({ starred: true }); + const prefs = await backendSrv.get(`/api/${this.props.resourceUri}/preferences`); + const dashboards = await backendSrv.search({ starred: true }); const defaultDashboardHit: DashboardSearchHit = { id: 0, title: 'Default', @@ -62,7 +62,7 @@ export class SharedPreferences extends PureComponent { }; if (prefs.homeDashboardId > 0 && !dashboards.find(d => d.id === prefs.homeDashboardId)) { - const missing = await this.backendSrv.search({ dashboardIds: [prefs.homeDashboardId] }); + const missing = await backendSrv.search({ dashboardIds: [prefs.homeDashboardId] }); if (missing && missing.length > 0) { dashboards.push(missing[0]); } @@ -81,7 +81,7 @@ export class SharedPreferences extends PureComponent { const { homeDashboardId, theme, timezone } = this.state; - await this.backendSrv.put(`/api/${this.props.resourceUri}/preferences`, { + await backendSrv.put(`/api/${this.props.resourceUri}/preferences`, { homeDashboardId, theme, timezone, diff --git a/public/app/core/components/dashboard_selector.ts b/public/app/core/components/dashboard_selector.ts index f312c77f6a8..e1f8c4c6c63 100644 --- a/public/app/core/components/dashboard_selector.ts +++ b/public/app/core/components/dashboard_selector.ts @@ -1,5 +1,5 @@ import coreModule from 'app/core/core_module'; -import { BackendSrv } from '../services/backend_srv'; +import { backendSrv } from '../services/backend_srv'; const template = ` @@ -9,13 +9,10 @@ export class DashboardSelectorCtrl { model: any; options: any; - /** @ngInject */ - constructor(private backendSrv: BackendSrv) {} - $onInit() { this.options = [{ value: 0, text: 'Default' }]; - return this.backendSrv.search({ starred: true }).then(res => { + return backendSrv.search({ starred: true }).then(res => { res.forEach(dash => { this.options.push({ value: dash.id, text: dash.title }); }); diff --git a/public/app/core/components/manage_dashboards/manage_dashboards.ts b/public/app/core/components/manage_dashboards/manage_dashboards.ts index b7b3cc0b6bf..81e56d6f425 100644 --- a/public/app/core/components/manage_dashboards/manage_dashboards.ts +++ b/public/app/core/components/manage_dashboards/manage_dashboards.ts @@ -3,9 +3,10 @@ import _ from 'lodash'; import coreModule from 'app/core/core_module'; import appEvents from 'app/core/app_events'; import { SearchSrv } from 'app/core/services/search_srv'; -import { BackendSrv } from 'app/core/services/backend_srv'; +import { backendSrv } from 'app/core/services/backend_srv'; import { ContextSrv } from 'app/core/services/context_srv'; import { CoreEvents } from 'app/types'; +import { promiseToDigest } from '../../utils/promiseToDigest'; export interface Section { id: number; @@ -69,12 +70,7 @@ export class ManageDashboardsCtrl { hasEditPermissionInFolders: boolean; /** @ngInject */ - constructor( - private $scope: IScope, - private backendSrv: BackendSrv, - private searchSrv: SearchSrv, - private contextSrv: ContextSrv - ) { + constructor(private $scope: IScope, private searchSrv: SearchSrv, private contextSrv: ContextSrv) { this.isEditor = this.contextSrv.isEditor; this.hasEditPermissionInFolders = this.contextSrv.hasEditPermissionInFolders; @@ -108,10 +104,10 @@ export class ManageDashboardsCtrl { .then(() => { if (!this.folderUid) { this.$scope.$digest(); - return; + return undefined; } - return this.backendSrv.getFolderByUid(this.folderUid).then((folder: any) => { + return backendSrv.getFolderByUid(this.folderUid).then((folder: any) => { this.canSave = folder.canSave; if (!this.canSave) { this.hasEditPermissionInFolders = false; @@ -216,9 +212,11 @@ export class ManageDashboardsCtrl { } private deleteFoldersAndDashboards(folderUids: string[], dashboardUids: string[]) { - this.backendSrv.deleteFoldersAndDashboards(folderUids, dashboardUids).then(() => { - this.refreshList(); - }); + promiseToDigest(this.$scope)( + backendSrv.deleteFoldersAndDashboards(folderUids, dashboardUids).then(() => { + this.refreshList(); + }) + ); } getDashboardsToMove() { diff --git a/public/app/core/components/search/search_results.ts b/public/app/core/components/search/search_results.ts index 72530f6d3fb..48a4a4ef0f6 100644 --- a/public/app/core/components/search/search_results.ts +++ b/public/app/core/components/search/search_results.ts @@ -1,9 +1,11 @@ import _ from 'lodash'; +import { ILocationService, IScope } from 'angular'; import { e2e } from '@grafana/e2e'; import coreModule from '../../core_module'; import appEvents from 'app/core/app_events'; import { CoreEvents } from 'app/types'; +import { promiseToDigest } from '../../utils/promiseToDigest'; export class SearchResultsCtrl { results: any; @@ -14,7 +16,7 @@ export class SearchResultsCtrl { selectors: typeof e2e.pages.Dashboards.selectors; /** @ngInject */ - constructor(private $location: any) { + constructor(private $location: ILocationService, private $scope: IScope) { this.selectors = e2e.pages.Dashboards.selectors; } @@ -24,19 +26,21 @@ export class SearchResultsCtrl { this.onFolderExpanding(); } - section.toggle(section).then((f: any) => { - if (this.editable && f.expanded) { - if (f.items) { - _.each(f.items, i => { - i.checked = f.checked; - }); + promiseToDigest(this.$scope)( + section.toggle(section).then((f: any) => { + if (this.editable && f.expanded) { + if (f.items) { + _.each(f.items, i => { + i.checked = f.checked; + }); - if (this.onSelectionChanged) { - this.onSelectionChanged(); + if (this.onSelectionChanged) { + this.onSelectionChanged(); + } } } - } - }); + }) + ); } } diff --git a/public/app/core/controllers/invited_ctrl.ts b/public/app/core/controllers/invited_ctrl.ts index dba074dc890..f50d6b0f1bd 100644 --- a/public/app/core/controllers/invited_ctrl.ts +++ b/public/app/core/controllers/invited_ctrl.ts @@ -1,9 +1,11 @@ import coreModule from '../core_module'; import config from 'app/core/config'; +import { getBackendSrv } from '@grafana/runtime'; +import { promiseToDigest } from '../utils/promiseToDigest'; export class InvitedCtrl { /** @ngInject */ - constructor($scope: any, $routeParams: any, contextSrv: any, backendSrv: any) { + constructor($scope: any, $routeParams: any, contextSrv: any) { contextSrv.sidemenu = false; $scope.formModel = {}; @@ -17,15 +19,19 @@ export class InvitedCtrl { }; $scope.init = () => { - backendSrv.get('/api/user/invite/' + $routeParams.code).then((invite: any) => { - $scope.formModel.name = invite.name; - $scope.formModel.email = invite.email; - $scope.formModel.username = invite.email; - $scope.formModel.inviteCode = $routeParams.code; + promiseToDigest($scope)( + getBackendSrv() + .get('/api/user/invite/' + $routeParams.code) + .then((invite: any) => { + $scope.formModel.name = invite.name; + $scope.formModel.email = invite.email; + $scope.formModel.username = invite.email; + $scope.formModel.inviteCode = $routeParams.code; - $scope.greeting = invite.name || invite.email || invite.username; - $scope.invitedBy = invite.invitedBy; - }); + $scope.greeting = invite.name || invite.email || invite.username; + $scope.invitedBy = invite.invitedBy; + }) + ); }; $scope.submit = () => { @@ -33,9 +39,11 @@ export class InvitedCtrl { return; } - backendSrv.post('/api/user/invite/complete', $scope.formModel).then(() => { - window.location.href = config.appSubUrl + '/'; - }); + getBackendSrv() + .post('/api/user/invite/complete', $scope.formModel) + .then(() => { + window.location.href = config.appSubUrl + '/'; + }); }; $scope.init(); diff --git a/public/app/core/controllers/reset_password_ctrl.ts b/public/app/core/controllers/reset_password_ctrl.ts index cc5f0a78088..ff132a54764 100644 --- a/public/app/core/controllers/reset_password_ctrl.ts +++ b/public/app/core/controllers/reset_password_ctrl.ts @@ -1,11 +1,12 @@ import coreModule from '../core_module'; import config from 'app/core/config'; -import { BackendSrv } from '../services/backend_srv'; import { AppEvents } from '@grafana/data'; +import { getBackendSrv } from '@grafana/runtime'; +import { promiseToDigest } from '../utils/promiseToDigest'; export class ResetPasswordCtrl { /** @ngInject */ - constructor($scope: any, backendSrv: BackendSrv, $location: any) { + constructor($scope: any, $location: any) { $scope.formModel = {}; $scope.mode = 'send'; $scope.ldapEnabled = config.ldapEnabled; @@ -31,9 +32,14 @@ export class ResetPasswordCtrl { if (!$scope.sendResetForm.$valid) { return; } - backendSrv.post('/api/user/password/send-reset-email', $scope.formModel).then(() => { - $scope.mode = 'email-sent'; - }); + + promiseToDigest($scope)( + getBackendSrv() + .post('/api/user/password/send-reset-email', $scope.formModel) + .then(() => { + $scope.mode = 'email-sent'; + }) + ); }; $scope.submitReset = () => { @@ -46,9 +52,11 @@ export class ResetPasswordCtrl { return; } - backendSrv.post('/api/user/password/reset', $scope.formModel).then(() => { - $location.path('login'); - }); + getBackendSrv() + .post('/api/user/password/reset', $scope.formModel) + .then(() => { + $location.path('login'); + }); }; } } diff --git a/public/app/core/controllers/signup_ctrl.ts b/public/app/core/controllers/signup_ctrl.ts index 7d3a3378fa8..d3cb01d58fd 100644 --- a/public/app/core/controllers/signup_ctrl.ts +++ b/public/app/core/controllers/signup_ctrl.ts @@ -1,9 +1,11 @@ import config from 'app/core/config'; import coreModule from '../core_module'; +import { getBackendSrv } from '@grafana/runtime/src/services'; +import { promiseToDigest } from '../utils/promiseToDigest'; export class SignUpCtrl { /** @ngInject */ - constructor(private $scope: any, private backendSrv: any, $location: any, contextSrv: any) { + constructor(private $scope: any, $location: any, contextSrv: any) { contextSrv.sidemenu = false; $scope.ctrl = this; @@ -34,10 +36,14 @@ export class SignUpCtrl { }, }; - backendSrv.get('/api/user/signup/options').then((options: any) => { - $scope.verifyEmailEnabled = options.verifyEmailEnabled; - $scope.autoAssignOrg = options.autoAssignOrg; - }); + promiseToDigest($scope)( + getBackendSrv() + .get('/api/user/signup/options') + .then((options: any) => { + $scope.verifyEmailEnabled = options.verifyEmailEnabled; + $scope.autoAssignOrg = options.autoAssignOrg; + }) + ); } submit() { @@ -45,13 +51,15 @@ export class SignUpCtrl { return; } - this.backendSrv.post('/api/user/signup/step2', this.$scope.formModel).then((rsp: any) => { - if (rsp.code === 'redirect-to-select-org') { - window.location.href = config.appSubUrl + '/profile/select-org?signup=1'; - } else { - window.location.href = config.appSubUrl + '/'; - } - }); + getBackendSrv() + .post('/api/user/signup/step2', this.$scope.formModel) + .then((rsp: any) => { + if (rsp.code === 'redirect-to-select-org') { + window.location.href = config.appSubUrl + '/profile/select-org?signup=1'; + } else { + window.location.href = config.appSubUrl + '/'; + } + }); } } diff --git a/public/app/core/services/backend_srv.ts b/public/app/core/services/backend_srv.ts index 7e1bb6c62f9..92b07988072 100644 --- a/public/app/core/services/backend_srv.ts +++ b/public/app/core/services/backend_srv.ts @@ -1,57 +1,119 @@ -import _ from 'lodash'; -import angular from 'angular'; -import coreModule from 'app/core/core_module'; +import omitBy from 'lodash/omitBy'; +import { from, merge, MonoTypeOperatorFunction, Observable, Subject, throwError } from 'rxjs'; +import { catchError, filter, map, mergeMap, retryWhen, share, takeUntil, tap } from 'rxjs/operators'; +import { fromFetch } from 'rxjs/fetch'; +import { BackendSrv as BackendService, BackendSrvRequest } from '@grafana/runtime'; +import { AppEvents } from '@grafana/data'; + import appEvents from 'app/core/app_events'; import config from 'app/core/config'; import { DashboardModel } from 'app/features/dashboard/state/DashboardModel'; import { DashboardSearchHit } from 'app/types/search'; -import { ContextSrv } from './context_srv'; -import { FolderInfo, DashboardDTO, CoreEvents } from 'app/types'; -import { BackendSrv as BackendService, getBackendSrv as getBackendService, BackendSrvRequest } from '@grafana/runtime'; -import { AppEvents } from '@grafana/data'; +import { CoreEvents, DashboardDTO, FolderInfo } from 'app/types'; +import { ContextSrv, contextSrv } from './context_srv'; +import { coreModule } from 'app/core/core_module'; +import { Emitter } from '../utils/emitter'; export interface DatasourceRequestOptions { retry?: number; method?: string; requestId?: string; - timeout?: angular.IPromise; + timeout?: Promise; url?: string; - headers?: { [key: string]: any }; + headers?: Record; silent?: boolean; - data?: { [key: string]: any }; + data?: Record; +} + +interface FetchResponseProps { + message?: string; +} + +interface ErrorResponseProps extends FetchResponseProps { + status?: string; + error?: string | any; +} + +export interface FetchResponse { + status: number; + statusText: string; + ok: boolean; + data: T; +} + +interface SuccessResponse extends FetchResponseProps, Record {} + +interface DataSourceSuccessResponse { + data: T; +} + +interface ErrorResponse { + status: number; + statusText?: string; + isHandled?: boolean; + data: T | string; + cancelled?: boolean; +} + +function serializeParams(data: Record): 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; + appEvents: Emitter; + contextSrv: ContextSrv; + logout: () => void; } export class BackendSrv implements BackendService { - private inFlightRequests: { [key: string]: Array> } = {}; + private inFlightRequests: Subject = new Subject(); private HTTP_REQUEST_CANCELED = -1; private noBackendCache: boolean; + private dependencies: BackendSrvDependencies = { + fromFetch: fromFetch, + appEvents: appEvents, + contextSrv: contextSrv, + logout: () => { + window.location.href = config.appSubUrl + '/logout'; + }, + }; - /** @ngInject */ - constructor( - private $http: any, - private $q: angular.IQService, - private $timeout: angular.ITimeoutService, - private contextSrv: ContextSrv - ) {} - - get(url: string, params?: any) { - return this.request({ method: 'GET', url, params }); + constructor(deps?: BackendSrvDependencies) { + if (deps) { + this.dependencies = { + ...this.dependencies, + ...deps, + }; + } } - delete(url: string) { - return this.request({ method: 'DELETE', url }); + async get(url: string, params?: any) { + return await this.request({ method: 'GET', url, params }); } - post(url: string, data?: any) { - return this.request({ method: 'POST', url, data }); + async delete(url: string) { + return await this.request({ method: 'DELETE', url }); } - patch(url: string, data: any) { - return this.request({ method: 'PATCH', url, data }); + async post(url: string, data?: any) { + return await this.request({ method: 'POST', url, data }); } - put(url: string, data: any) { - return this.request({ method: 'PUT', url, data }); + async patch(url: string, data: any) { + return await this.request({ method: 'PATCH', url, data }); + } + + async put(url: string, data: any) { + return await this.request({ method: 'PUT', url, data }); } withNoBackendCache(callback: any) { @@ -61,18 +123,18 @@ export class BackendSrv implements BackendService { }); } - requestErrorHandler(err: any) { + requestErrorHandler = (err: ErrorResponse) => { if (err.isHandled) { return; } - let data = err.data || { message: 'Unexpected error' }; - if (_.isString(data)) { + let data = err.data ?? { message: 'Unexpected error' }; + if (typeof data === 'string') { data = { message: data }; } if (err.status === 422) { - appEvents.emit(AppEvents.alertWarning, ['Validation failed', data.message]); + this.dependencies.appEvents.emit(AppEvents.alertWarning, ['Validation failed', data.message]); throw data; } @@ -84,170 +146,133 @@ export class BackendSrv implements BackendService { message = 'Error'; } - appEvents.emit(err.status < 500 ? AppEvents.alertWarning : AppEvents.alertError, [message, description]); + this.dependencies.appEvents.emit(err.status < 500 ? AppEvents.alertWarning : AppEvents.alertError, [ + message, + description, + ]); } throw data; - } + }; - request(options: BackendSrvRequest) { - options.retry = options.retry || 0; - const requestIsLocal = !options.url.match(/^http/); - const firstAttempt = options.retry === 0; + async request(options: BackendSrvRequest): Promise { + options = this.parseRequestOptions(options, this.dependencies.contextSrv.user?.orgId); - if (requestIsLocal) { - if (this.contextSrv.user && this.contextSrv.user.orgId) { - options.headers = options.headers || {}; - options.headers['X-Grafana-Org-Id'] = this.contextSrv.user.orgId; - } - - if (options.url.indexOf('/') === 0) { - options.url = options.url.substring(1); - } - } - - return this.$http(options).then( - (results: any) => { - if (options.method !== 'GET') { - if (results && results.data.message) { - if (options.showSuccessAlert !== false) { - appEvents.emit(AppEvents.alertSuccess, [results.data.message]); - } - } + const fromFetchStream = this.getFromFetchStream(options); + const failureStream = fromFetchStream.pipe(this.toFailureStream(options)); + const successStream = fromFetchStream.pipe( + filter(response => response.ok === true), + map(response => { + const fetchSuccessResponse: SuccessResponse = response.data; + return fetchSuccessResponse; + }), + tap(response => { + if (options.method !== 'GET' && response?.message && options.showSuccessAlert !== false) { + this.dependencies.appEvents.emit(AppEvents.alertSuccess, [response.message]); } - return results.data; - }, - (err: any) => { - // handle unauthorized - if (err.status === 401 && this.contextSrv.user.isSignedIn && firstAttempt) { - return this.loginPing() - .then(() => { - options.retry = 1; - return this.request(options); - }) - .catch((err: any) => { - if (err.status === 401) { - window.location.href = config.appSubUrl + '/logout'; - throw err; - } - }); - } - - this.$timeout(this.requestErrorHandler.bind(this, err), 50); - throw err; - } + }) ); - } - addCanceler(requestId: string, canceler: angular.IDeferred) { - if (requestId in this.inFlightRequests) { - this.inFlightRequests[requestId].push(canceler); - } else { - this.inFlightRequests[requestId] = [canceler]; - } + return merge(successStream, failureStream) + .pipe( + catchError((err: ErrorResponse) => { + if (err.status === 401) { + this.dependencies.logout(); + return throwError(err); + } + + // this setTimeout hack enables any caller catching this err to set isHandled to true + setTimeout(() => this.requestErrorHandler(err), 50); + return throwError(err); + }) + ) + .toPromise(); } resolveCancelerIfExists(requestId: string) { - const cancelers = this.inFlightRequests[requestId]; - if (!_.isUndefined(cancelers) && cancelers.length) { - cancelers[0].resolve(); - } + this.inFlightRequests.next(requestId); } - datasourceRequest(options: BackendSrvRequest) { - let canceler: angular.IDeferred = null; - options.retry = options.retry || 0; - - // A requestID is provided by the datasource as a unique identifier for a - // particular query. If the requestID exists, the promise it is keyed to - // is canceled, canceling the previous datasource request if it is still - // in-flight. - const requestId = options.requestId; - - if (requestId) { - this.resolveCancelerIfExists(requestId); - // create new canceler - canceler = this.$q.defer(); - options.timeout = canceler.promise; - this.addCanceler(requestId, canceler); + async datasourceRequest(options: BackendSrvRequest): Promise { + // A requestId is provided by the datasource as a unique identifier for a + // particular query. Every observable below has a takeUntil that subscribes to this.inFlightRequests and + // will cancel/unsubscribe that observable when a new datasourceRequest with the same requestId is made + if (options.requestId) { + this.inFlightRequests.next(options.requestId); } - const requestIsLocal = !options.url.match(/^http/); - const firstAttempt = options.retry === 0; + options = this.parseDataSourceRequestOptions( + options, + this.dependencies.contextSrv.user?.orgId, + this.noBackendCache + ); - if (requestIsLocal) { - if (this.contextSrv.user && this.contextSrv.user.orgId) { - options.headers = options.headers || {}; - options.headers['X-Grafana-Org-Id'] = this.contextSrv.user.orgId; - } - - if (options.url.indexOf('/') === 0) { - options.url = options.url.substring(1); - } - - if (options.headers && options.headers.Authorization) { - options.headers['X-DS-Authorization'] = options.headers.Authorization; - delete options.headers.Authorization; - } - - if (this.noBackendCache) { - options.headers['X-Grafana-NoCache'] = 'true'; - } - } - - return this.$http(options) - .then((response: any) => { + const fromFetchStream = this.getFromFetchStream(options); + const failureStream = fromFetchStream.pipe(this.toFailureStream(options)); + const successStream = fromFetchStream.pipe( + filter(response => response.ok === true), + map(response => { + const { data } = response; + const fetchSuccessResponse: DataSourceSuccessResponse = { data }; + return fetchSuccessResponse; + }), + tap(res => { if (!options.silent) { - appEvents.emit(CoreEvents.dsRequestResponse, response); + this.dependencies.appEvents.emit(CoreEvents.dsRequestResponse, res); } - return response; }) - .catch((err: any) => { - if (err.status === this.HTTP_REQUEST_CANCELED) { - throw { err, cancelled: true }; - } + ); - // handle unauthorized for backend requests - if (requestIsLocal && firstAttempt && err.status === 401) { - return this.loginPing() - .then(() => { - options.retry = 1; - if (canceler) { - canceler.resolve(); - } - return this.datasourceRequest(options); - }) - .catch((err: any) => { - if (err.status === 401) { - window.location.href = config.appSubUrl + '/logout'; - throw err; - } + return merge(successStream, failureStream) + .pipe( + catchError((err: ErrorResponse) => { + if (err.status === this.HTTP_REQUEST_CANCELED) { + return throwError({ + err, + cancelled: true, }); - } + } - // populate error obj on Internal Error - if (_.isString(err.data) && err.status === 500) { - err.data = { - error: err.statusText, - response: err.data, - }; - } + if (err.status === 401) { + this.dependencies.logout(); + return throwError(err); + } - // for Prometheus - if (err.data && !err.data.message && _.isString(err.data.error)) { - err.data.message = err.data.error; - } - if (!options.silent) { - appEvents.emit(CoreEvents.dsRequestError, err); - } - throw err; - }) - .finally(() => { - // clean up - if (options.requestId) { - this.inFlightRequests[options.requestId].shift(); - } - }); + // populate error obj on Internal Error + if (typeof err.data === 'string' && err.status === 500) { + err.data = { + error: err.statusText, + response: err.data, + }; + } + + // for Prometheus + if (err.data && !err.data.message && typeof err.data.error === 'string') { + err.data.message = err.data.error; + } + + if (!options.silent) { + this.dependencies.appEvents.emit(CoreEvents.dsRequestError, err); + } + + return throwError(err); + }), + takeUntil( + this.inFlightRequests.pipe( + filter(requestId => { + let cancelRequest = false; + if (options && options.requestId && options.requestId === requestId) { + // when a new requestId is started it will be published to inFlightRequests + // if a previous long running request that hasn't finished yet has the same requestId + // we need to cancel that request + cancelRequest = true; + } + return cancelRequest; + }) + ) + ) + ) + .toPromise(); } loginPing() { @@ -309,7 +334,7 @@ export class BackendSrv implements BackendService { tasks.push(this.createTask(this.deleteDashboard.bind(this), true, dashboardUid, true)); } - return this.executeInOrder(tasks, []); + return this.executeInOrder(tasks); } moveDashboards(dashboardUids: string[], toFolder: FolderInfo) { @@ -319,82 +344,184 @@ export class BackendSrv implements BackendService { tasks.push(this.createTask(this.moveDashboard.bind(this), true, uid, toFolder)); } - return this.executeInOrder(tasks, []).then((result: any) => { + return this.executeInOrder(tasks).then((result: any) => { return { totalCount: result.length, - successCount: _.filter(result, { succeeded: true }).length, - alreadyInFolderCount: _.filter(result, { alreadyInFolder: true }).length, + successCount: result.filter((res: any) => res.succeeded).length, + alreadyInFolderCount: result.filter((res: any) => res.alreadyInFolder).length, }; }); } - private moveDashboard(uid: string, toFolder: FolderInfo) { - const deferred = this.$q.defer(); + private async moveDashboard(uid: string, toFolder: FolderInfo) { + const fullDash: DashboardDTO = await this.getDashboardByUid(uid); + const model = new DashboardModel(fullDash.dashboard, fullDash.meta); - this.getDashboardByUid(uid).then((fullDash: DashboardDTO) => { - const model = new DashboardModel(fullDash.dashboard, fullDash.meta); + if ((!fullDash.meta.folderId && toFolder.id === 0) || fullDash.meta.folderId === toFolder.id) { + return { alreadyInFolder: true }; + } - if ((!fullDash.meta.folderId && toFolder.id === 0) || fullDash.meta.folderId === toFolder.id) { - deferred.resolve({ alreadyInFolder: true }); - return; + const clone = model.getSaveModelClone(); + const options = { + folderId: toFolder.id, + overwrite: false, + }; + + try { + await this.saveDashboard(clone, options); + return { succeeded: true }; + } catch (err) { + if (err.data?.status !== 'plugin-dashboard') { + return { succeeded: false }; } - const clone = model.getSaveModelClone(); - const options = { - folderId: toFolder.id, - overwrite: false, - }; + err.isHandled = true; + options.overwrite = true; - this.saveDashboard(clone, options) - .then(() => { - deferred.resolve({ succeeded: true }); - }) - .catch((err: any) => { - if (err.data && err.data.status === 'plugin-dashboard') { - err.isHandled = true; - options.overwrite = true; - - this.saveDashboard(clone, options) - .then(() => { - deferred.resolve({ succeeded: true }); - }) - .catch((err: any) => { - deferred.resolve({ succeeded: false }); - }); - } else { - deferred.resolve({ succeeded: false }); - } - }); - }); - - return deferred.promise; + try { + await this.saveDashboard(clone, options); + return { succeeded: true }; + } catch (e) { + return { succeeded: false }; + } + } } - private createTask(fn: Function, ignoreRejections: boolean, ...args: any[]) { - return (result: any) => { - return fn - .apply(null, args) - .then((res: any) => { - return Array.prototype.concat(result, [res]); - }) - .catch((err: any) => { - if (ignoreRejections) { - return result; - } + private createTask(fn: (...args: any[]) => Promise, ignoreRejections: boolean, ...args: any[]) { + return async (result: any) => { + try { + const res = await fn(...args); + return Array.prototype.concat(result, [res]); + } catch (err) { + if (ignoreRejections) { + return result; + } - throw err; - }); + throw err; + } }; } - private executeInOrder(tasks: any[], initialValue: any[]) { - return tasks.reduce(this.$q.when, initialValue); + private executeInOrder(tasks: any[]) { + return tasks.reduce((acc, task) => { + return Promise.resolve(acc).then(task); + }, []); } + + private parseRequestOptions = (options: BackendSrvRequest, orgId?: number): BackendSrvRequest => { + options.retry = options.retry ?? 0; + const requestIsLocal = !options.url.match(/^http/); + + if (requestIsLocal) { + if (orgId) { + options.headers = options.headers ?? {}; + options.headers['X-Grafana-Org-Id'] = orgId; + } + + if (options.url.startsWith('/')) { + options.url = options.url.substring(1); + } + + if (options.url.endsWith('/')) { + options.url = options.url.slice(0, -1); + } + } + + return options; + }; + + private parseDataSourceRequestOptions = ( + options: BackendSrvRequest, + orgId?: number, + noBackendCache?: boolean + ): BackendSrvRequest => { + options.retry = options.retry ?? 0; + const requestIsLocal = !options.url.match(/^http/); + + if (requestIsLocal) { + if (orgId) { + options.headers = options.headers || {}; + options.headers['X-Grafana-Org-Id'] = orgId; + } + + if (options.url.startsWith('/')) { + options.url = options.url.substring(1); + } + + if (options.headers?.Authorization) { + options.headers['X-DS-Authorization'] = options.headers.Authorization; + delete options.headers.Authorization; + } + + if (noBackendCache) { + options.headers['X-Grafana-NoCache'] = 'true'; + } + } + + return options; + }; + + private parseUrlFromOptions = (options: BackendSrvRequest): string => { + const cleanParams = omitBy(options.params, v => v === undefined || (v && v.length === 0)); + const serializedParams = serializeParams(cleanParams); + return options.params && serializedParams.length ? `${options.url}?${serializedParams}` : options.url; + }; + + private parseInitFromOptions = (options: BackendSrvRequest): RequestInit => ({ + method: options.method, + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json, text/plain, */*', + ...options.headers, + }, + body: JSON.stringify(options.data), + }); + + private getFromFetchStream = (options: BackendSrvRequest) => { + const url = this.parseUrlFromOptions(options); + const init = this.parseInitFromOptions(options); + return this.dependencies.fromFetch(url, init).pipe( + mergeMap(async response => { + const { status, statusText, ok } = response; + const textData = await response.text(); // this could be just a string, prometheus requests for instance + let data; + try { + data = JSON.parse(textData); // majority of the requests this will be something that can be parsed + } catch { + data = textData; + } + const fetchResponse: FetchResponse = { status, statusText, ok, data }; + return fetchResponse; + }), + share() // sharing this so we can split into success and failure and then merge back + ); + }; + + private toFailureStream = (options: BackendSrvRequest): MonoTypeOperatorFunction => 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) => + attempts.pipe( + mergeMap((error, i) => { + const firstAttempt = i === 0 && options.retry === 0; + + if (error.status === 401 && this.dependencies.contextSrv.user.isSignedIn && firstAttempt) { + return from(this.loginPing()); + } + + return throwError(error); + }) + ) + ) + ); } -coreModule.service('backendSrv', BackendSrv); - +coreModule.factory('backendSrv', () => backendSrv); // Used for testing and things that really need BackendSrv -export function getBackendSrv(): BackendSrv { - return getBackendService() as BackendSrv; -} +export const backendSrv = new BackendSrv(); +export const getBackendSrv = (): BackendSrv => backendSrv; diff --git a/public/app/core/services/echo/backends/PerformanceBackend.ts b/public/app/core/services/echo/backends/PerformanceBackend.ts index 575a5b57a38..77d0df69b60 100644 --- a/public/app/core/services/echo/backends/PerformanceBackend.ts +++ b/public/app/core/services/echo/backends/PerformanceBackend.ts @@ -43,7 +43,7 @@ export class PerformanceBackend implements EchoBackend { + return backendSrv.search({ dashboardIds: dashIds }).then(result => { return dashIds .map(orderId => { return _.find(result, { id: orderId }); @@ -78,7 +77,7 @@ export class SearchSrv { return Promise.resolve(); } - return this.backendSrv.search({ starred: true, limit: 30 }).then(result => { + return backendSrv.search({ starred: true, limit: 30 }).then(result => { if (result.length > 0) { sections['starred'] = { title: 'Starred', @@ -116,7 +115,7 @@ export class SearchSrv { } promises.push( - this.backendSrv.search(query).then(results => { + backendSrv.search(query).then(results => { return this.handleSearchResult(sections, results); }) ); @@ -197,14 +196,14 @@ export class SearchSrv { folderIds: [section.id], }; - return this.backendSrv.search(query).then(results => { + return backendSrv.search(query).then(results => { section.items = results; return Promise.resolve(section); }); } getDashboardTags() { - return this.backendSrv.get('/api/dashboards/tags'); + return backendSrv.get('/api/dashboards/tags'); } } diff --git a/public/app/core/specs/OrgSwitcher.test.tsx b/public/app/core/specs/OrgSwitcher.test.tsx index 6d92221bba6..181b00b90ea 100644 --- a/public/app/core/specs/OrgSwitcher.test.tsx +++ b/public/app/core/specs/OrgSwitcher.test.tsx @@ -1,16 +1,13 @@ import React from 'react'; -// @ts-ignore -import { getBackendSrv } from '@grafana/runtime/src/services/backendSrv'; import { OrgSwitcher } from '../components/OrgSwitcher'; import { shallow } from 'enzyme'; import { OrgRole } from '@grafana/data'; -const getMock = jest.fn(() => Promise.resolve([])); -const postMock = jest.fn(); +const postMock = jest.fn().mockImplementation(jest.fn()); -jest.mock('@grafana/runtime/src/services/backendSrv', () => ({ +jest.mock('@grafana/runtime', () => ({ getBackendSrv: () => ({ - get: getMock, + get: jest.fn().mockResolvedValue([]), post: postMock, }), })); diff --git a/public/app/core/specs/backend_srv.test.ts b/public/app/core/specs/backend_srv.test.ts index 28802fb013b..e6938398738 100644 --- a/public/app/core/specs/backend_srv.test.ts +++ b/public/app/core/specs/backend_srv.test.ts @@ -1,32 +1,532 @@ -import angular from 'angular'; -import { BackendSrv } from 'app/core/services/backend_srv'; -import { ContextSrv } from '../services/context_srv'; -jest.mock('app/core/store'); +import { BackendSrv, getBackendSrv } from '../services/backend_srv'; +import { Emitter } from '../utils/emitter'; +import { ContextSrv, User } from '../services/context_srv'; +import { Observable, of } from 'rxjs'; +import { AppEvents } from '@grafana/data'; +import { CoreEvents } from '../../types'; +import { delay } from 'rxjs/operators'; -describe('backend_srv', () => { - const _httpBackend = (options: any) => { - if (options.url === 'gateway-error') { - return Promise.reject({ status: 502 }); - } - return Promise.resolve({}); +const getTestContext = (overides?: object) => { + const defaults = { + data: { test: 'hello world' }, + ok: true, + status: 200, + statusText: 'Ok', + isSignedIn: true, + orgId: 1337, + }; + const props = { ...defaults, ...overides }; + const textMock = jest.fn().mockResolvedValue(JSON.stringify(props.data)); + const fromFetchMock = jest.fn().mockImplementation(() => { + return of({ + ok: props.ok, + status: props.status, + statusText: props.statusText, + text: textMock, + }); + }); + const appEventsMock: Emitter = ({ + emit: jest.fn(), + } as any) as Emitter; + const user: User = ({ + isSignedIn: props.isSignedIn, + orgId: props.orgId, + } as any) as User; + const contextSrvMock: ContextSrv = ({ + user, + } as any) as ContextSrv; + const logoutMock = jest.fn(); + const parseRequestOptionsMock = jest.fn().mockImplementation(options => options); + const parseDataSourceRequestOptionsMock = jest.fn().mockImplementation(options => options); + const parseUrlFromOptionsMock = jest.fn().mockImplementation(() => 'parseUrlFromOptionsMock'); + const parseInitFromOptionsMock = jest.fn().mockImplementation(() => 'parseInitFromOptionsMock'); + + const backendSrv = new BackendSrv({ + fromFetch: fromFetchMock, + appEvents: appEventsMock, + contextSrv: contextSrvMock, + logout: logoutMock, + }); + + backendSrv['parseRequestOptions'] = parseRequestOptionsMock; + backendSrv['parseDataSourceRequestOptions'] = parseDataSourceRequestOptionsMock; + backendSrv['parseUrlFromOptions'] = parseUrlFromOptionsMock; + backendSrv['parseInitFromOptions'] = parseInitFromOptionsMock; + + const expectCallChain = (options: any) => { + expect(parseUrlFromOptionsMock).toHaveBeenCalledTimes(1); + expect(parseUrlFromOptionsMock).toHaveBeenCalledWith(options); + expect(parseInitFromOptionsMock).toHaveBeenCalledTimes(1); + expect(parseInitFromOptionsMock).toHaveBeenCalledWith(options); + expect(fromFetchMock).toHaveBeenCalledTimes(1); + expect(fromFetchMock).toHaveBeenCalledWith('parseUrlFromOptionsMock', 'parseInitFromOptionsMock'); }; - const _backendSrv = new BackendSrv( - _httpBackend, - {} as angular.IQService, - {} as angular.ITimeoutService, - {} as ContextSrv - ); + const expectRequestCallChain = (options: any) => { + expect(parseRequestOptionsMock).toHaveBeenCalledTimes(1); + expect(parseRequestOptionsMock).toHaveBeenCalledWith(options, 1337); + expectCallChain(options); + }; - describe('when handling errors', () => { - it('should return the http status code', async () => { - try { - await _backendSrv.datasourceRequest({ - url: 'gateway-error', - }); - } catch (err) { - expect(err.status).toBe(502); + const expectDataSourceRequestCallChain = (options: any) => { + expect(parseDataSourceRequestOptionsMock).toHaveBeenCalledTimes(1); + expect(parseDataSourceRequestOptionsMock).toHaveBeenCalledWith(options, 1337, undefined); + expectCallChain(options); + }; + + return { + backendSrv, + fromFetchMock, + appEventsMock, + contextSrvMock, + textMock, + logoutMock, + parseRequestOptionsMock, + parseDataSourceRequestOptionsMock, + parseUrlFromOptionsMock, + parseInitFromOptionsMock, + expectRequestCallChain, + expectDataSourceRequestCallChain, + }; +}; + +describe('backendSrv', () => { + describe('parseRequestOptions', () => { + it.each` + retry | url | orgId | expected + ${undefined} | ${'http://localhost:3000/api/dashboard'} | ${undefined} | ${{ retry: 0, url: 'http://localhost:3000/api/dashboard' }} + ${1} | ${'http://localhost:3000/api/dashboard'} | ${1} | ${{ retry: 1, url: 'http://localhost:3000/api/dashboard' }} + ${undefined} | ${'api/dashboard'} | ${undefined} | ${{ retry: 0, url: 'api/dashboard' }} + ${undefined} | ${'/api/dashboard'} | ${undefined} | ${{ retry: 0, url: 'api/dashboard' }} + ${undefined} | ${'/api/dashboard/'} | ${undefined} | ${{ retry: 0, url: 'api/dashboard' }} + ${1} | ${'/api/dashboard/'} | ${undefined} | ${{ retry: 1, url: 'api/dashboard' }} + ${undefined} | ${'/api/dashboard/'} | ${1} | ${{ retry: 0, url: 'api/dashboard', headers: { 'X-Grafana-Org-Id': 1 } }} + ${1} | ${'/api/dashboard/'} | ${1} | ${{ retry: 1, url: 'api/dashboard', headers: { 'X-Grafana-Org-Id': 1 } }} + `( + "when called with retry: '$retry', url: '$url' and orgId: '$orgId' then result should be '$expected'", + ({ retry, url, orgId, expected }) => { + expect(getBackendSrv()['parseRequestOptions']({ retry, url }, orgId)).toEqual(expected); } + ); + }); + + describe('parseDataSourceRequestOptions', () => { + it.each` + retry | url | headers | orgId | noBackendCache | expected + ${undefined} | ${'http://localhost:3000/api/dashboard'} | ${undefined} | ${undefined} | ${undefined} | ${{ retry: 0, url: 'http://localhost:3000/api/dashboard' }} + ${1} | ${'http://localhost:3000/api/dashboard'} | ${{ Authorization: 'Some Auth' }} | ${1} | ${true} | ${{ retry: 1, url: 'http://localhost:3000/api/dashboard', headers: { Authorization: 'Some Auth' } }} + ${undefined} | ${'api/dashboard'} | ${undefined} | ${undefined} | ${undefined} | ${{ retry: 0, url: 'api/dashboard' }} + ${undefined} | ${'/api/dashboard'} | ${undefined} | ${undefined} | ${undefined} | ${{ retry: 0, url: 'api/dashboard' }} + ${undefined} | ${'/api/dashboard/'} | ${undefined} | ${undefined} | ${undefined} | ${{ retry: 0, url: 'api/dashboard/' }} + ${undefined} | ${'/api/dashboard/'} | ${{ Authorization: 'Some Auth' }} | ${undefined} | ${undefined} | ${{ retry: 0, url: 'api/dashboard/', headers: { 'X-DS-Authorization': 'Some Auth' } }} + ${undefined} | ${'/api/dashboard/'} | ${{ Authorization: 'Some Auth' }} | ${1} | ${undefined} | ${{ retry: 0, url: 'api/dashboard/', headers: { 'X-DS-Authorization': 'Some Auth', 'X-Grafana-Org-Id': 1 } }} + ${undefined} | ${'/api/dashboard/'} | ${{ Authorization: 'Some Auth' }} | ${1} | ${true} | ${{ retry: 0, url: 'api/dashboard/', headers: { 'X-DS-Authorization': 'Some Auth', 'X-Grafana-Org-Id': 1, 'X-Grafana-NoCache': 'true' } }} + ${1} | ${'/api/dashboard/'} | ${undefined} | ${undefined} | ${undefined} | ${{ retry: 1, url: 'api/dashboard/' }} + ${1} | ${'/api/dashboard/'} | ${{ Authorization: 'Some Auth' }} | ${undefined} | ${undefined} | ${{ retry: 1, url: 'api/dashboard/', headers: { 'X-DS-Authorization': 'Some Auth' } }} + ${1} | ${'/api/dashboard/'} | ${{ Authorization: 'Some Auth' }} | ${1} | ${undefined} | ${{ retry: 1, url: 'api/dashboard/', headers: { 'X-DS-Authorization': 'Some Auth', 'X-Grafana-Org-Id': 1 } }} + ${1} | ${'/api/dashboard/'} | ${{ Authorization: 'Some Auth' }} | ${1} | ${true} | ${{ retry: 1, url: 'api/dashboard/', headers: { 'X-DS-Authorization': 'Some Auth', 'X-Grafana-Org-Id': 1, 'X-Grafana-NoCache': 'true' } }} + `( + "when called with retry: '$retry', url: '$url', headers: '$headers', orgId: '$orgId' and noBackendCache: '$noBackendCache' then result should be '$expected'", + ({ retry, url, headers, orgId, noBackendCache, expected }) => { + expect( + getBackendSrv()['parseDataSourceRequestOptions']({ retry, url, headers }, orgId, noBackendCache) + ).toEqual(expected); + } + ); + }); + + describe('parseUrlFromOptions', () => { + it.each` + params | url | expected + ${undefined} | ${'api/dashboard'} | ${'api/dashboard'} + ${{ key: 'value' }} | ${'api/dashboard'} | ${'api/dashboard?key=value'} + ${{ key: undefined }} | ${'api/dashboard'} | ${'api/dashboard'} + ${{ firstKey: 'first value', secondValue: 'second value' }} | ${'api/dashboard'} | ${'api/dashboard?firstKey=first%20value&secondValue=second%20value'} + ${{ firstKey: 'first value', secondValue: undefined }} | ${'api/dashboard'} | ${'api/dashboard?firstKey=first%20value'} + ${{ id: [1, 2, 3] }} | ${'api/dashboard'} | ${'api/dashboard?id=1&id=2&id=3'} + ${{ id: [] }} | ${'api/dashboard'} | ${'api/dashboard'} + `( + "when called with params: '$params' and url: '$url' then result should be '$expected'", + ({ params, url, expected }) => { + expect(getBackendSrv()['parseUrlFromOptions']({ params, url })).toEqual(expected); + } + ); + }); + + describe('parseInitFromOptions', () => { + it.each` + method | headers | data | expected + ${undefined} | ${undefined} | ${undefined} | ${{ method: undefined, headers: { 'Content-Type': 'application/json', Accept: 'application/json, text/plain, */*' }, body: undefined }} + ${'GET'} | ${undefined} | ${undefined} | ${{ method: 'GET', headers: { 'Content-Type': 'application/json', Accept: 'application/json, text/plain, */*' }, body: undefined }} + ${'GET'} | ${{ Auth: 'Some Auth' }} | ${undefined} | ${{ method: 'GET', headers: { 'Content-Type': 'application/json', Accept: 'application/json, text/plain, */*', Auth: 'Some Auth' }, body: undefined }} + ${'GET'} | ${{ Auth: 'Some Auth' }} | ${{ data: { test: 'Some data' } }} | ${{ method: 'GET', headers: { 'Content-Type': 'application/json', Accept: 'application/json, text/plain, */*', Auth: 'Some Auth' }, body: '{"data":{"test":"Some data"}}' }} + ${'GET'} | ${{ Auth: 'Some Auth' }} | ${'some data'} | ${{ method: 'GET', headers: { 'Content-Type': 'application/json', Accept: 'application/json, text/plain, */*', Auth: 'Some Auth' }, body: '"some data"' }} + `( + "when called with method: '$method', headers: '$headers' and data: '$data' then result should be '$expected'", + ({ method, headers, data, expected }) => { + expect(getBackendSrv()['parseInitFromOptions']({ method, headers, data, url: '' })).toEqual(expected); + } + ); + }); + + describe('request', () => { + describe('when making a successful call and conditions for showSuccessAlert are not favorable', () => { + it('then it should return correct result and not emit anything', async () => { + const { backendSrv, appEventsMock, expectRequestCallChain } = getTestContext({ + data: { message: 'A message' }, + }); + const url = '/api/dashboard/'; + const result = await backendSrv.request({ url, method: 'DELETE', showSuccessAlert: false }); + expect(result).toEqual({ message: 'A message' }); + expect(appEventsMock.emit).not.toHaveBeenCalled(); + expectRequestCallChain({ url, method: 'DELETE', showSuccessAlert: false }); + }); + }); + + describe('when making a successful call and conditions for showSuccessAlert are favorable', () => { + it('then it should emit correct message', async () => { + const { backendSrv, appEventsMock, expectRequestCallChain } = getTestContext({ + data: { message: 'A message' }, + }); + const url = '/api/dashboard/'; + const result = await backendSrv.request({ url, method: 'DELETE', showSuccessAlert: true }); + expect(result).toEqual({ message: 'A message' }); + expect(appEventsMock.emit).toHaveBeenCalledTimes(1); + expect(appEventsMock.emit).toHaveBeenCalledWith(AppEvents.alertSuccess, ['A message']); + expectRequestCallChain({ url, method: 'DELETE', showSuccessAlert: true }); + }); + }); + + describe('when making an unsuccessful call and conditions for retry are favorable and loginPing does not throw', () => { + it('then it should retry', async () => { + jest.useFakeTimers(); + const { backendSrv, appEventsMock, logoutMock, expectRequestCallChain } = getTestContext({ + ok: false, + status: 401, + statusText: 'UnAuthorized', + data: { message: 'UnAuthorized' }, + }); + backendSrv.loginPing = jest + .fn() + .mockResolvedValue({ ok: true, status: 200, statusText: 'OK', data: { message: 'Ok' } }); + const url = '/api/dashboard/'; + // it would be better if we could simulate that after the call to loginPing everything is successful but as + // our fromFetchMock returns ok:false the second time this retries it will still be ok:false going into the + // mergeMap in toFailureStream + await backendSrv.request({ url, method: 'GET', retry: 0 }).catch(error => { + expect(error.status).toBe(401); + expect(error.statusText).toBe('UnAuthorized'); + expect(error.data).toEqual({ message: 'UnAuthorized' }); + expect(appEventsMock.emit).not.toHaveBeenCalled(); + expect(backendSrv.loginPing).toHaveBeenCalledTimes(1); + expect(logoutMock).toHaveBeenCalledTimes(1); + expectRequestCallChain({ url, method: 'GET', retry: 0 }); + jest.advanceTimersByTime(50); + expect(appEventsMock.emit).not.toHaveBeenCalled(); + }); + }); + }); + + describe('when making an unsuccessful call and conditions for retry are favorable and retry throws', () => { + it('then it throw error', async () => { + jest.useFakeTimers(); + const { backendSrv, appEventsMock, logoutMock, expectRequestCallChain } = getTestContext({ + ok: false, + status: 401, + statusText: 'UnAuthorized', + data: { message: 'UnAuthorized' }, + }); + backendSrv.loginPing = jest + .fn() + .mockRejectedValue({ status: 403, statusText: 'Forbidden', data: { message: 'Forbidden' } }); + const url = '/api/dashboard/'; + await backendSrv + .request({ url, method: 'GET', retry: 0 }) + .catch(error => { + expect(error.status).toBe(403); + expect(error.statusText).toBe('Forbidden'); + expect(error.data).toEqual({ message: 'Forbidden' }); + expect(appEventsMock.emit).not.toHaveBeenCalled(); + expect(backendSrv.loginPing).toHaveBeenCalledTimes(1); + expect(logoutMock).not.toHaveBeenCalled(); + expectRequestCallChain({ url, method: 'GET', retry: 0 }); + jest.advanceTimersByTime(50); + }) + .catch(error => { + expect(error).toEqual({ message: 'Forbidden' }); + expect(appEventsMock.emit).toHaveBeenCalledTimes(1); + expect(appEventsMock.emit).toHaveBeenCalledWith(AppEvents.alertWarning, ['Forbidden', '']); + }); + }); + }); + + describe('when making an unsuccessful 422 call', () => { + it('then it should emit Validation failed message', async () => { + jest.useFakeTimers(); + const { backendSrv, appEventsMock, logoutMock, expectRequestCallChain } = getTestContext({ + ok: false, + status: 422, + statusText: 'Unprocessable Entity', + data: { message: 'Unprocessable Entity' }, + }); + const url = '/api/dashboard/'; + await backendSrv + .request({ url, method: 'GET' }) + .catch(error => { + expect(error.status).toBe(422); + expect(error.statusText).toBe('Unprocessable Entity'); + expect(error.data).toEqual({ message: 'Unprocessable Entity' }); + expect(appEventsMock.emit).not.toHaveBeenCalled(); + expect(logoutMock).not.toHaveBeenCalled(); + expectRequestCallChain({ url, method: 'GET' }); + jest.advanceTimersByTime(50); + }) + .catch(error => { + expect(error).toEqual({ message: 'Unprocessable Entity' }); + expect(appEventsMock.emit).toHaveBeenCalledTimes(1); + expect(appEventsMock.emit).toHaveBeenCalledWith(AppEvents.alertWarning, [ + 'Validation failed', + 'Unprocessable Entity', + ]); + }); + }); + }); + + describe('when making an unsuccessful call and we handle the error', () => { + it('then it should not emit message', async () => { + jest.useFakeTimers(); + const { backendSrv, appEventsMock, logoutMock, expectRequestCallChain } = getTestContext({ + ok: false, + status: 404, + statusText: 'Not found', + data: { message: 'Not found' }, + }); + const url = '/api/dashboard/'; + await backendSrv.request({ url, method: 'GET' }).catch(error => { + expect(error.status).toBe(404); + expect(error.statusText).toBe('Not found'); + expect(error.data).toEqual({ message: 'Not found' }); + expect(appEventsMock.emit).not.toHaveBeenCalled(); + expect(logoutMock).not.toHaveBeenCalled(); + expectRequestCallChain({ url, method: 'GET' }); + error.isHandled = true; + jest.advanceTimersByTime(50); + expect(appEventsMock.emit).not.toHaveBeenCalled(); + }); + }); + }); + }); + + describe('datasourceRequest', () => { + describe('when making a successful call and silent is true', () => { + it('then it should not emit message', async () => { + const { backendSrv, appEventsMock, expectDataSourceRequestCallChain } = getTestContext(); + const url = 'http://www.some.url.com/'; + const result = await backendSrv.datasourceRequest({ url, silent: true }); + expect(result).toEqual({ data: { test: 'hello world' } }); + expect(appEventsMock.emit).not.toHaveBeenCalled(); + expectDataSourceRequestCallChain({ url, silent: true }); + }); + }); + + describe('when making a successful call and silent is not defined', () => { + it('then it should not emit message', async () => { + const { backendSrv, appEventsMock, expectDataSourceRequestCallChain } = getTestContext(); + const url = 'http://www.some.url.com/'; + const result = await backendSrv.datasourceRequest({ url }); + expect(result).toEqual({ data: { test: 'hello world' } }); + expect(appEventsMock.emit).toHaveBeenCalledTimes(1); + expect(appEventsMock.emit).toHaveBeenCalledWith(CoreEvents.dsRequestResponse, { + data: { test: 'hello world' }, + }); + expectDataSourceRequestCallChain({ url }); + }); + }); + + describe('when called with the same requestId twice', () => { + it('then it should cancel the first call and the first call should be unsubscribed', async () => { + const { backendSrv, fromFetchMock } = getTestContext(); + const unsubscribe = jest.fn(); + const slowData = { message: 'Slow Request' }; + const slowFetch = new Observable(subscriber => { + subscriber.next({ + ok: true, + status: 200, + statusText: 'Ok', + text: () => Promise.resolve(JSON.stringify(slowData)), + }); + return unsubscribe; + }).pipe(delay(10000)); + const fastData = { message: 'Fast Request' }; + const fastFetch = of({ + ok: true, + status: 200, + statusText: 'Ok', + text: () => Promise.resolve(JSON.stringify(fastData)), + }); + fromFetchMock.mockImplementationOnce(() => slowFetch); + fromFetchMock.mockImplementation(() => fastFetch); + const options = { + url: '/api/dashboard/', + requestId: 'A', + }; + const slowRequest = backendSrv.datasourceRequest(options); + const fastResponse = await backendSrv.datasourceRequest(options); + expect(fastResponse).toEqual({ data: { message: 'Fast Request' } }); + + const slowResponse = await slowRequest; + expect(slowResponse).toEqual(undefined); + expect(unsubscribe).toHaveBeenCalledTimes(1); + }); + }); + + describe('when making an unsuccessful call and conditions for retry are favorable and loginPing does not throw', () => { + it('then it should retry', async () => { + const { backendSrv, appEventsMock, logoutMock, expectDataSourceRequestCallChain } = getTestContext({ + ok: false, + status: 401, + statusText: 'UnAuthorized', + data: { message: 'UnAuthorized' }, + }); + backendSrv.loginPing = jest + .fn() + .mockResolvedValue({ ok: true, status: 200, statusText: 'OK', data: { message: 'Ok' } }); + const url = '/api/dashboard/'; + // it would be better if we could simulate that after the call to loginPing everything is successful but as + // our fromFetchMock returns ok:false the second time this retries it will still be ok:false going into the + // mergeMap in toFailureStream + await backendSrv.datasourceRequest({ url, method: 'GET', retry: 0 }).catch(error => { + expect(error.status).toBe(401); + expect(error.statusText).toBe('UnAuthorized'); + expect(error.data).toEqual({ message: 'UnAuthorized' }); + expect(appEventsMock.emit).not.toHaveBeenCalled(); + expect(backendSrv.loginPing).toHaveBeenCalledTimes(1); + expect(logoutMock).toHaveBeenCalledTimes(1); + expectDataSourceRequestCallChain({ url, method: 'GET', retry: 0 }); + }); + }); + }); + + describe('when making an unsuccessful call and conditions for retry are favorable and retry throws', () => { + it('then it throw error', async () => { + const { backendSrv, appEventsMock, logoutMock, expectDataSourceRequestCallChain } = getTestContext({ + ok: false, + status: 401, + statusText: 'UnAuthorized', + data: { message: 'UnAuthorized' }, + }); + backendSrv.loginPing = jest + .fn() + .mockRejectedValue({ status: 403, statusText: 'Forbidden', data: { message: 'Forbidden' } }); + const url = '/api/dashboard/'; + await backendSrv.datasourceRequest({ url, method: 'GET', retry: 0 }).catch(error => { + expect(error.status).toBe(403); + expect(error.statusText).toBe('Forbidden'); + expect(error.data).toEqual({ message: 'Forbidden' }); + expect(appEventsMock.emit).toHaveBeenCalledTimes(1); + expect(appEventsMock.emit).toHaveBeenCalledWith(CoreEvents.dsRequestError, { + data: { message: 'Forbidden' }, + status: 403, + statusText: 'Forbidden', + }); + expect(backendSrv.loginPing).toHaveBeenCalledTimes(1); + expect(logoutMock).not.toHaveBeenCalled(); + expectDataSourceRequestCallChain({ url, method: 'GET', retry: 0 }); + }); + }); + }); + + describe('when making a HTTP_REQUEST_CANCELED call', () => { + it('then it should throw cancelled error', async () => { + const { backendSrv, appEventsMock, logoutMock, expectDataSourceRequestCallChain } = getTestContext({ + ok: false, + status: -1, + statusText: 'HTTP_REQUEST_CANCELED', + data: { message: 'HTTP_REQUEST_CANCELED' }, + }); + const url = '/api/dashboard/'; + await backendSrv.datasourceRequest({ url, method: 'GET' }).catch(error => { + expect(error).toEqual({ + err: { + status: -1, + statusText: 'HTTP_REQUEST_CANCELED', + data: { message: 'HTTP_REQUEST_CANCELED' }, + }, + cancelled: true, + }); + expect(appEventsMock.emit).not.toHaveBeenCalled(); + expect(logoutMock).not.toHaveBeenCalled(); + expectDataSourceRequestCallChain({ url, method: 'GET' }); + }); + }); + }); + + describe('when making an Internal Error call', () => { + it('then it should throw cancelled error', async () => { + const { backendSrv, appEventsMock, logoutMock, expectDataSourceRequestCallChain } = getTestContext({ + ok: false, + status: 500, + statusText: 'Internal Server Error', + data: 'Internal Server Error', + }); + const url = '/api/dashboard/'; + await backendSrv.datasourceRequest({ url, method: 'GET' }).catch(error => { + expect(error).toEqual({ + status: 500, + statusText: 'Internal Server Error', + data: { + error: 'Internal Server Error', + response: 'Internal Server Error', + message: 'Internal Server Error', + }, + }); + expect(appEventsMock.emit).toHaveBeenCalledTimes(1); + expect(appEventsMock.emit).toHaveBeenCalledWith(CoreEvents.dsRequestError, { + status: 500, + statusText: 'Internal Server Error', + data: { + error: 'Internal Server Error', + response: 'Internal Server Error', + message: 'Internal Server Error', + }, + }); + expect(logoutMock).not.toHaveBeenCalled(); + expectDataSourceRequestCallChain({ url, method: 'GET' }); + }); + }); + }); + + describe('when formatting prometheus error', () => { + it('then it should throw cancelled error', async () => { + const { backendSrv, appEventsMock, logoutMock, expectDataSourceRequestCallChain } = getTestContext({ + ok: false, + status: 403, + statusText: 'Forbidden', + data: { error: 'Forbidden' }, + }); + const url = '/api/dashboard/'; + await backendSrv.datasourceRequest({ url, method: 'GET' }).catch(error => { + expect(error).toEqual({ + status: 403, + statusText: 'Forbidden', + data: { + error: 'Forbidden', + message: 'Forbidden', + }, + }); + expect(appEventsMock.emit).toHaveBeenCalledTimes(1); + expect(appEventsMock.emit).toHaveBeenCalledWith(CoreEvents.dsRequestError, { + status: 403, + statusText: 'Forbidden', + data: { + error: 'Forbidden', + message: 'Forbidden', + }, + }); + expect(logoutMock).not.toHaveBeenCalled(); + expectDataSourceRequestCallChain({ url, method: 'GET' }); + }); + }); }); }); }); diff --git a/public/app/core/specs/manage_dashboards.test.ts b/public/app/core/specs/manage_dashboards.test.ts index cda72459398..9c179cc14f3 100644 --- a/public/app/core/specs/manage_dashboards.test.ts +++ b/public/app/core/specs/manage_dashboards.test.ts @@ -6,7 +6,6 @@ import { FoldersAndDashboardUids, } from 'app/core/components/manage_dashboards/manage_dashboards'; import { SearchSrv } from 'app/core/services/search_srv'; -import { BackendSrv } from '../services/backend_srv'; import { ContextSrv } from '../services/context_srv'; const mockSection = (overides?: object): Section => { @@ -593,7 +592,6 @@ function createCtrlWithStubs(searchResponse: any, tags?: any) { return new ManageDashboardsCtrl( { $digest: jest.fn() } as any, - {} as BackendSrv, searchSrvStub as SearchSrv, { isEditor: true } as ContextSrv ); diff --git a/public/app/core/specs/search_results.test.ts b/public/app/core/specs/search_results.test.ts index 5db527e885f..9b6277ae7e9 100644 --- a/public/app/core/specs/search_results.test.ts +++ b/public/app/core/specs/search_results.test.ts @@ -1,5 +1,7 @@ +import { ILocationService, IScope } from 'angular'; + import { SearchResultsCtrl } from '../components/search/search_results'; -import { beforeEach, afterEach } from 'test/lib/common'; +import { afterEach, beforeEach } from 'test/lib/common'; import appEvents from 'app/core/app_events'; import { CoreEvents } from 'app/types'; @@ -11,13 +13,15 @@ jest.mock('app/core/app_events', () => { describe('SearchResultsCtrl', () => { let ctrl: any; + const $location = {} as ILocationService; + const $scope = ({ $evalAsync: jest.fn() } as any) as IScope; describe('when checking an item that is not checked', () => { const item = { checked: false }; let selectionChanged = false; beforeEach(() => { - ctrl = new SearchResultsCtrl({}); + ctrl = new SearchResultsCtrl($location, $scope); ctrl.onSelectionChanged = () => (selectionChanged = true); ctrl.toggleSelection(item); }); @@ -36,7 +40,7 @@ describe('SearchResultsCtrl', () => { let selectionChanged = false; beforeEach(() => { - ctrl = new SearchResultsCtrl({}); + ctrl = new SearchResultsCtrl($location, $scope); ctrl.onSelectionChanged = () => (selectionChanged = true); ctrl.toggleSelection(item); }); @@ -54,7 +58,7 @@ describe('SearchResultsCtrl', () => { let selectedTag: any = null; beforeEach(() => { - ctrl = new SearchResultsCtrl({}); + ctrl = new SearchResultsCtrl($location, $scope); ctrl.onTagSelected = (tag: any) => (selectedTag = tag); ctrl.selectTag('tag-test'); }); @@ -68,7 +72,7 @@ describe('SearchResultsCtrl', () => { let folderExpanded = false; beforeEach(() => { - ctrl = new SearchResultsCtrl({}); + ctrl = new SearchResultsCtrl($location, $scope); ctrl.onFolderExpanding = () => { folderExpanded = true; }; @@ -90,7 +94,7 @@ describe('SearchResultsCtrl', () => { let folderExpanded = false; beforeEach(() => { - ctrl = new SearchResultsCtrl({}); + ctrl = new SearchResultsCtrl($location, $scope); ctrl.onFolderExpanding = () => { folderExpanded = true; }; @@ -110,12 +114,12 @@ describe('SearchResultsCtrl', () => { describe('when clicking on a link in search result', () => { const dashPath = 'dashboard/path'; - const $location = { path: () => dashPath }; + const $location = ({ path: () => dashPath } as any) as ILocationService; const appEventsMock = appEvents as any; describe('with the same url as current path', () => { beforeEach(() => { - ctrl = new SearchResultsCtrl($location); + ctrl = new SearchResultsCtrl($location, $scope); const item = { url: dashPath }; ctrl.onItemClick(item); }); @@ -128,7 +132,7 @@ describe('SearchResultsCtrl', () => { describe('with a different url than current path', () => { beforeEach(() => { - ctrl = new SearchResultsCtrl($location); + ctrl = new SearchResultsCtrl($location, $scope); const item = { url: 'another/path' }; ctrl.onItemClick(item); }); diff --git a/public/app/core/specs/search_srv.test.ts b/public/app/core/specs/search_srv.test.ts index 42b509b3e79..8290d4147cf 100644 --- a/public/app/core/specs/search_srv.test.ts +++ b/public/app/core/specs/search_srv.test.ts @@ -1,9 +1,8 @@ import { SearchSrv } from 'app/core/services/search_srv'; -import { BackendSrvMock } from 'test/mocks/backend_srv'; import impressionSrv from 'app/core/services/impression_srv'; import { contextSrv } from 'app/core/services/context_srv'; import { beforeEach } from 'test/lib/common'; -import { BackendSrv } from '../services/backend_srv'; +import { backendSrv } from '../services/backend_srv'; jest.mock('app/core/store', () => { return { @@ -19,29 +18,32 @@ jest.mock('app/core/services/impression_srv', () => { }); describe('SearchSrv', () => { - let searchSrv: SearchSrv, backendSrvMock: BackendSrvMock; + let searchSrv: SearchSrv; + const searchMock = jest.spyOn(backendSrv, 'search'); // will use the mock in __mocks__ beforeEach(() => { - backendSrvMock = new BackendSrvMock(); - searchSrv = new SearchSrv(backendSrvMock as BackendSrv); + searchSrv = new SearchSrv(); contextSrv.isSignedIn = true; impressionSrv.getDashboardOpened = jest.fn().mockReturnValue([]); + jest.clearAllMocks(); }); describe('With recent dashboards', () => { let results: any; beforeEach(() => { - backendSrvMock.search = jest - .fn() - .mockReturnValueOnce( - Promise.resolve([ - { id: 2, title: 'second but first' }, - { id: 1, title: 'first but second' }, - ]) - ) - .mockReturnValue(Promise.resolve([])); + searchMock.mockImplementation( + jest + .fn() + .mockReturnValueOnce( + Promise.resolve([ + { id: 2, title: 'second but first' }, + { id: 1, title: 'first but second' }, + ]) + ) + .mockReturnValue(Promise.resolve([])) + ); impressionSrv.getDashboardOpened = jest.fn().mockReturnValue([1, 2]); @@ -63,15 +65,17 @@ describe('SearchSrv', () => { let results: any; beforeEach(() => { - backendSrvMock.search = jest - .fn() - .mockReturnValueOnce( - Promise.resolve([ - { id: 2, title: 'two' }, - { id: 1, title: 'one' }, - ]) - ) - .mockReturnValue(Promise.resolve([])); + searchMock.mockImplementation( + jest + .fn() + .mockReturnValueOnce( + Promise.resolve([ + { id: 2, title: 'two' }, + { id: 1, title: 'one' }, + ]) + ) + .mockReturnValue(Promise.resolve([])) + ); impressionSrv.getDashboardOpened = jest.fn().mockReturnValue([4, 5, 1, 2, 3]); @@ -92,7 +96,7 @@ describe('SearchSrv', () => { let results: any; beforeEach(() => { - backendSrvMock.search = jest.fn().mockReturnValue(Promise.resolve([{ id: 1, title: 'starred' }])); + searchMock.mockImplementation(jest.fn().mockReturnValue(Promise.resolve([{ id: 1, title: 'starred' }]))); return searchSrv.search({ query: '' }).then(res => { results = res; @@ -109,15 +113,17 @@ describe('SearchSrv', () => { let results: any; beforeEach(() => { - backendSrvMock.search = jest - .fn() - .mockReturnValueOnce( - Promise.resolve([ - { id: 1, title: 'starred and recent', isStarred: true }, - { id: 2, title: 'recent' }, - ]) - ) - .mockReturnValue(Promise.resolve([{ id: 1, title: 'starred and recent' }])); + searchMock.mockImplementation( + jest + .fn() + .mockReturnValueOnce( + Promise.resolve([ + { id: 1, title: 'starred and recent', isStarred: true }, + { id: 2, title: 'recent' }, + ]) + ) + .mockReturnValue(Promise.resolve([{ id: 1, title: 'starred and recent' }])) + ); impressionSrv.getDashboardOpened = jest.fn().mockReturnValue([1, 2]); return searchSrv.search({ query: '' }).then(res => { @@ -140,35 +146,37 @@ describe('SearchSrv', () => { let results: any; beforeEach(() => { - backendSrvMock.search = jest - .fn() - .mockReturnValueOnce(Promise.resolve([])) - .mockReturnValue( - Promise.resolve([ - { - title: 'folder1', - type: 'dash-folder', - id: 1, - }, - { - title: 'dash with no folder', - type: 'dash-db', - id: 2, - }, - { - title: 'dash in folder1 1', - type: 'dash-db', - id: 3, - folderId: 1, - }, - { - title: 'dash in folder1 2', - type: 'dash-db', - id: 4, - folderId: 1, - }, - ]) - ); + searchMock.mockImplementation( + jest + .fn() + .mockReturnValueOnce(Promise.resolve([])) + .mockReturnValue( + Promise.resolve([ + { + title: 'folder1', + type: 'dash-folder', + id: 1, + }, + { + title: 'dash with no folder', + type: 'dash-db', + id: 2, + }, + { + title: 'dash in folder1 1', + type: 'dash-db', + id: 3, + folderId: 1, + }, + { + title: 'dash in folder1 2', + type: 'dash-db', + id: 4, + folderId: 1, + }, + ]) + ) + ); return searchSrv.search({ query: '' }).then(res => { results = res; @@ -188,23 +196,25 @@ describe('SearchSrv', () => { let results: any; beforeEach(() => { - backendSrvMock.search = jest.fn().mockReturnValue( - Promise.resolve([ - { - id: 2, - title: 'dash with no folder', - type: 'dash-db', - }, - { - id: 3, - title: 'dash in folder1 1', - type: 'dash-db', - folderId: 1, - folderUid: 'uid', - folderTitle: 'folder1', - folderUrl: '/dashboards/f/uid/folder1', - }, - ]) + searchMock.mockImplementation( + jest.fn().mockReturnValue( + Promise.resolve([ + { + id: 2, + title: 'dash with no folder', + type: 'dash-db', + }, + { + id: 3, + title: 'dash in folder1 1', + type: 'dash-db', + folderId: 1, + folderUid: 'uid', + folderTitle: 'folder1', + folderUrl: '/dashboards/f/uid/folder1', + }, + ]) + ) ); return searchSrv.search({ query: 'search' }).then(res => { @@ -213,7 +223,7 @@ describe('SearchSrv', () => { }); it('should not specify folder ids', () => { - expect(backendSrvMock.search.mock.calls[0][0].folderIds).toHaveLength(0); + expect(searchMock.mock.calls[0][0].folderIds).toHaveLength(0); }); it('should group results by folder', () => { @@ -228,27 +238,25 @@ describe('SearchSrv', () => { describe('with tags', () => { beforeEach(() => { - backendSrvMock.search = jest.fn(); - backendSrvMock.search.mockReturnValue(Promise.resolve([])); + searchMock.mockImplementation(jest.fn().mockReturnValue(Promise.resolve([]))); return searchSrv.search({ tag: ['atag'] }).then(() => {}); }); it('should send tags query to backend search', () => { - expect(backendSrvMock.search.mock.calls[0][0].tag).toHaveLength(1); + expect(searchMock.mock.calls[0][0].tag).toHaveLength(1); }); }); describe('with starred', () => { beforeEach(() => { - backendSrvMock.search = jest.fn(); - backendSrvMock.search.mockReturnValue(Promise.resolve([])); + searchMock.mockImplementation(jest.fn().mockReturnValue(Promise.resolve([]))); return searchSrv.search({ starred: true }).then(() => {}); }); it('should send starred query to backend search', () => { - expect(backendSrvMock.search.mock.calls[0][0].starred).toEqual(true); + expect(searchMock.mock.calls[0][0].starred).toEqual(true); }); }); @@ -256,8 +264,7 @@ describe('SearchSrv', () => { let getRecentDashboardsCalled = false; beforeEach(() => { - backendSrvMock.search = jest.fn(); - backendSrvMock.search.mockReturnValue(Promise.resolve([])); + searchMock.mockImplementation(jest.fn().mockReturnValue(Promise.resolve([]))); searchSrv['getRecentDashboards'] = () => { getRecentDashboardsCalled = true; @@ -276,8 +283,7 @@ describe('SearchSrv', () => { let getStarredCalled = false; beforeEach(() => { - backendSrvMock.search = jest.fn(); - backendSrvMock.search.mockReturnValue(Promise.resolve([])); + searchMock.mockImplementation(jest.fn().mockReturnValue(Promise.resolve([]))); impressionSrv.getDashboardOpened = jest.fn().mockReturnValue([]); searchSrv['getStarred'] = () => { diff --git a/public/app/features/admin/AdminEditOrgCtrl.ts b/public/app/features/admin/AdminEditOrgCtrl.ts index 107c506742d..7b795173129 100644 --- a/public/app/features/admin/AdminEditOrgCtrl.ts +++ b/public/app/features/admin/AdminEditOrgCtrl.ts @@ -1,28 +1,32 @@ -import { BackendSrv } from 'app/core/services/backend_srv'; +import { getBackendSrv } from '@grafana/runtime'; import { NavModelSrv } from 'app/core/core'; +import { promiseToDigest } from 'app/core/utils/promiseToDigest'; export default class AdminEditOrgCtrl { /** @ngInject */ - constructor($scope: any, $routeParams: any, backendSrv: BackendSrv, $location: any, navModelSrv: NavModelSrv) { + constructor($scope: any, $routeParams: any, $location: any, navModelSrv: NavModelSrv) { $scope.init = () => { $scope.navModel = navModelSrv.getNav('admin', 'global-orgs', 0); if ($routeParams.id) { - $scope.getOrg($routeParams.id); - $scope.getOrgUsers($routeParams.id); + promiseToDigest($scope)(Promise.all([$scope.getOrg($routeParams.id), $scope.getOrgUsers($routeParams.id)])); } }; $scope.getOrg = (id: number) => { - backendSrv.get('/api/orgs/' + id).then((org: any) => { - $scope.org = org; - }); + return getBackendSrv() + .get('/api/orgs/' + id) + .then((org: any) => { + $scope.org = org; + }); }; $scope.getOrgUsers = (id: number) => { - backendSrv.get('/api/orgs/' + id + '/users').then((orgUsers: any) => { - $scope.orgUsers = orgUsers; - }); + return getBackendSrv() + .get('/api/orgs/' + id + '/users') + .then((orgUsers: any) => { + $scope.orgUsers = orgUsers; + }); }; $scope.update = () => { @@ -30,19 +34,25 @@ export default class AdminEditOrgCtrl { return; } - backendSrv.put('/api/orgs/' + $scope.org.id, $scope.org).then(() => { - $location.path('/admin/orgs'); - }); + promiseToDigest($scope)( + getBackendSrv() + .put('/api/orgs/' + $scope.org.id, $scope.org) + .then(() => { + $location.path('/admin/orgs'); + }) + ); }; $scope.updateOrgUser = (orgUser: any) => { - backendSrv.patch('/api/orgs/' + orgUser.orgId + '/users/' + orgUser.userId, orgUser); + getBackendSrv().patch('/api/orgs/' + orgUser.orgId + '/users/' + orgUser.userId, orgUser); }; $scope.removeOrgUser = (orgUser: any) => { - backendSrv.delete('/api/orgs/' + orgUser.orgId + '/users/' + orgUser.userId).then(() => { - $scope.getOrgUsers($scope.org.id); - }); + promiseToDigest($scope)( + getBackendSrv() + .delete('/api/orgs/' + orgUser.orgId + '/users/' + orgUser.userId) + .then(() => $scope.getOrgUsers($scope.org.id)) + ); }; $scope.init(); diff --git a/public/app/features/admin/AdminEditUserCtrl.ts b/public/app/features/admin/AdminEditUserCtrl.ts index 9aeb0f7e0ea..64140ac002e 100644 --- a/public/app/features/admin/AdminEditUserCtrl.ts +++ b/public/app/features/admin/AdminEditUserCtrl.ts @@ -1,19 +1,14 @@ import _ from 'lodash'; -import { BackendSrv } from 'app/core/services/backend_srv'; +import { getBackendSrv } from '@grafana/runtime'; import { NavModelSrv } from 'app/core/core'; import { User } from 'app/core/services/context_srv'; import { UserSession, Scope, CoreEvents, AppEventEmitter } from 'app/types'; import { dateTime } from '@grafana/data'; +import { promiseToDigest } from 'app/core/utils/promiseToDigest'; export default class AdminEditUserCtrl { /** @ngInject */ - constructor( - $scope: Scope & AppEventEmitter, - $routeParams: any, - backendSrv: BackendSrv, - $location: any, - navModelSrv: NavModelSrv - ) { + constructor($scope: Scope & AppEventEmitter, $routeParams: any, $location: any, navModelSrv: NavModelSrv) { $scope.user = {}; $scope.sessions = []; $scope.newOrg = { name: '', role: 'Editor' }; @@ -22,60 +17,74 @@ export default class AdminEditUserCtrl { $scope.init = () => { if ($routeParams.id) { - $scope.getUser($routeParams.id); - $scope.getUserSessions($routeParams.id); - $scope.getUserOrgs($routeParams.id); + promiseToDigest($scope)( + Promise.all([ + $scope.getUser($routeParams.id), + $scope.getUserSessions($routeParams.id), + $scope.getUserOrgs($routeParams.id), + ]) + ); } }; $scope.getUser = (id: number) => { - backendSrv.get('/api/users/' + id).then((user: User) => { - $scope.user = user; - $scope.user_id = id; - $scope.permissions.isGrafanaAdmin = user.isGrafanaAdmin; - }); + return getBackendSrv() + .get('/api/users/' + id) + .then((user: User) => { + $scope.user = user; + $scope.user_id = id; + $scope.permissions.isGrafanaAdmin = user.isGrafanaAdmin; + }); }; $scope.getUserSessions = (id: number) => { - backendSrv.get('/api/admin/users/' + id + '/auth-tokens').then((sessions: UserSession[]) => { - sessions.reverse(); + return getBackendSrv() + .get('/api/admin/users/' + id + '/auth-tokens') + .then((sessions: UserSession[]) => { + sessions.reverse(); - $scope.sessions = sessions.map((session: UserSession) => { - return { - id: session.id, - isActive: session.isActive, - seenAt: dateTime(session.seenAt).fromNow(), - createdAt: dateTime(session.createdAt).format('MMMM DD, YYYY'), - clientIp: session.clientIp, - browser: session.browser, - browserVersion: session.browserVersion, - os: session.os, - osVersion: session.osVersion, - device: session.device, - }; - }); - }); - }; - - $scope.revokeUserSession = (tokenId: number) => { - backendSrv - .post('/api/admin/users/' + $scope.user_id + '/revoke-auth-token', { - authTokenId: tokenId, - }) - .then(() => { - $scope.sessions = $scope.sessions.filter((session: UserSession) => { - if (session.id === tokenId) { - return false; - } - return true; + $scope.sessions = sessions.map((session: UserSession) => { + return { + id: session.id, + isActive: session.isActive, + seenAt: dateTime(session.seenAt).fromNow(), + createdAt: dateTime(session.createdAt).format('MMMM DD, YYYY'), + clientIp: session.clientIp, + browser: session.browser, + browserVersion: session.browserVersion, + os: session.os, + osVersion: session.osVersion, + device: session.device, + }; }); }); }; + $scope.revokeUserSession = (tokenId: number) => { + promiseToDigest($scope)( + getBackendSrv() + .post('/api/admin/users/' + $scope.user_id + '/revoke-auth-token', { + authTokenId: tokenId, + }) + .then(() => { + $scope.sessions = $scope.sessions.filter((session: UserSession) => { + if (session.id === tokenId) { + return false; + } + return true; + }); + }) + ); + }; + $scope.revokeAllUserSessions = (tokenId: number) => { - backendSrv.post('/api/admin/users/' + $scope.user_id + '/logout').then(() => { - $scope.sessions = []; - }); + promiseToDigest($scope)( + getBackendSrv() + .post('/api/admin/users/' + $scope.user_id + '/logout') + .then(() => { + $scope.sessions = []; + }) + ); }; $scope.setPassword = () => { @@ -84,15 +93,19 @@ export default class AdminEditUserCtrl { } const payload = { password: $scope.password }; - backendSrv.put('/api/admin/users/' + $scope.user_id + '/password', payload).then(() => { - $location.path('/admin/users'); - }); + + promiseToDigest($scope)( + getBackendSrv() + .put('/api/admin/users/' + $scope.user_id + '/password', payload) + .then(() => { + $location.path('/admin/users'); + }) + ); }; $scope.updatePermissions = () => { const payload = $scope.permissions; - - backendSrv.put('/api/admin/users/' + $scope.user_id + '/permissions', payload); + getBackendSrv().put('/api/admin/users/' + $scope.user_id + '/permissions', payload); }; $scope.create = () => { @@ -100,15 +113,21 @@ export default class AdminEditUserCtrl { return; } - backendSrv.post('/api/admin/users', $scope.user).then(() => { - $location.path('/admin/users'); - }); + promiseToDigest($scope)( + getBackendSrv() + .post('/api/admin/users', $scope.user) + .then(() => { + $location.path('/admin/users'); + }) + ); }; $scope.getUserOrgs = (id: number) => { - backendSrv.get('/api/users/' + id + '/orgs').then((orgs: any) => { - $scope.orgs = orgs; - }); + return getBackendSrv() + .get('/api/users/' + id + '/orgs') + .then((orgs: any) => { + $scope.orgs = orgs; + }); }; $scope.update = () => { @@ -116,20 +135,27 @@ export default class AdminEditUserCtrl { return; } - backendSrv.put('/api/users/' + $scope.user_id, $scope.user).then(() => { - $location.path('/admin/users'); - }); + promiseToDigest($scope)( + getBackendSrv() + .put('/api/users/' + $scope.user_id, $scope.user) + .then(() => { + $location.path('/admin/users'); + }) + ); }; $scope.updateOrgUser = (orgUser: { orgId: string }) => { - backendSrv.patch('/api/orgs/' + orgUser.orgId + '/users/' + $scope.user_id, orgUser).then(() => {}); + promiseToDigest($scope)( + getBackendSrv().patch('/api/orgs/' + orgUser.orgId + '/users/' + $scope.user_id, orgUser) + ); }; $scope.removeOrgUser = (orgUser: { orgId: string }) => { - backendSrv.delete('/api/orgs/' + orgUser.orgId + '/users/' + $scope.user_id).then(() => { - $scope.getUser($scope.user_id); - $scope.getUserOrgs($scope.user_id); - }); + promiseToDigest($scope)( + getBackendSrv() + .delete('/api/orgs/' + orgUser.orgId + '/users/' + $scope.user_id) + .then(() => Promise.all([$scope.getUser($scope.user_id), $scope.getUserOrgs($scope.user_id)])) + ); }; $scope.orgsSearchCache = []; @@ -140,10 +166,14 @@ export default class AdminEditUserCtrl { return; } - backendSrv.get('/api/orgs', { query: '' }).then((result: any) => { - $scope.orgsSearchCache = result; - callback(_.map(result, 'name')); - }); + promiseToDigest($scope)( + getBackendSrv() + .get('/api/orgs', { query: '' }) + .then((result: any) => { + $scope.orgsSearchCache = result; + callback(_.map(result, 'name')); + }) + ); }; $scope.addOrgUser = () => { @@ -161,10 +191,11 @@ export default class AdminEditUserCtrl { $scope.newOrg.loginOrEmail = $scope.user.login; - backendSrv.post('/api/orgs/' + orgInfo.id + '/users/', $scope.newOrg).then(() => { - $scope.getUser($scope.user_id); - $scope.getUserOrgs($scope.user_id); - }); + promiseToDigest($scope)( + getBackendSrv() + .post('/api/orgs/' + orgInfo.id + '/users/', $scope.newOrg) + .then(() => Promise.all([$scope.getUser($scope.user_id), $scope.getUserOrgs($scope.user_id)])) + ); }; $scope.deleteUser = (user: any) => { @@ -174,9 +205,13 @@ export default class AdminEditUserCtrl { icon: 'fa-trash', yesText: 'Delete', onConfirm: () => { - backendSrv.delete('/api/admin/users/' + user.id).then(() => { - $location.path('/admin/users'); - }); + promiseToDigest($scope)( + getBackendSrv() + .delete('/api/admin/users/' + user.id) + .then(() => { + $location.path('/admin/users'); + }) + ); }, }); }; @@ -192,9 +227,10 @@ export default class AdminEditUserCtrl { } const actionEndpoint = user.isDisabled ? '/enable' : '/disable'; - backendSrv.post('/api/admin/users/' + user.id + actionEndpoint).then(() => { - $scope.init(); - }); + + getBackendSrv() + .post('/api/admin/users/' + user.id + actionEndpoint) + .then(() => $scope.init()); }; $scope.init(); diff --git a/public/app/features/admin/AdminListOrgsCtrl.ts b/public/app/features/admin/AdminListOrgsCtrl.ts index 6aec34dabd0..635a1824453 100644 --- a/public/app/features/admin/AdminListOrgsCtrl.ts +++ b/public/app/features/admin/AdminListOrgsCtrl.ts @@ -1,19 +1,19 @@ -import { BackendSrv } from 'app/core/services/backend_srv'; +import { getBackendSrv } from '@grafana/runtime'; import { NavModelSrv } from 'app/core/core'; import { Scope, CoreEvents, AppEventEmitter } from 'app/types'; +import { promiseToDigest } from 'app/core/utils/promiseToDigest'; export default class AdminListOrgsCtrl { /** @ngInject */ - constructor($scope: Scope & AppEventEmitter, backendSrv: BackendSrv, navModelSrv: NavModelSrv) { - $scope.init = () => { + constructor($scope: Scope & AppEventEmitter, navModelSrv: NavModelSrv) { + $scope.init = async () => { $scope.navModel = navModelSrv.getNav('admin', 'global-orgs', 0); - $scope.getOrgs(); + await $scope.getOrgs(); }; - $scope.getOrgs = () => { - backendSrv.get('/api/orgs').then((orgs: any) => { - $scope.orgs = orgs; - }); + $scope.getOrgs = async () => { + const orgs = await promiseToDigest($scope)(getBackendSrv().get('/api/orgs')); + $scope.orgs = orgs; }; $scope.deleteOrg = (org: any) => { @@ -23,10 +23,9 @@ export default class AdminListOrgsCtrl { text2: 'All dashboards for this organization will be removed!', icon: 'fa-trash', yesText: 'Delete', - onConfirm: () => { - backendSrv.delete('/api/orgs/' + org.id).then(() => { - $scope.getOrgs(); - }); + onConfirm: async () => { + await getBackendSrv().delete('/api/orgs/' + org.id); + await $scope.getOrgs(); }, }); }; diff --git a/public/app/features/admin/AdminListUsersCtrl.ts b/public/app/features/admin/AdminListUsersCtrl.ts index 87571d39cc9..9dc750e2422 100644 --- a/public/app/features/admin/AdminListUsersCtrl.ts +++ b/public/app/features/admin/AdminListUsersCtrl.ts @@ -1,6 +1,8 @@ import { getTagColorsFromName } from '@grafana/ui'; -import { BackendSrv } from 'app/core/services/backend_srv'; +import { getBackendSrv } from '@grafana/runtime'; import { NavModelSrv } from 'app/core/core'; +import { Scope } from 'app/types/angular'; +import { promiseToDigest } from 'app/core/utils/promiseToDigest'; export default class AdminListUsersCtrl { users: any; @@ -13,29 +15,31 @@ export default class AdminListUsersCtrl { navModel: any; /** @ngInject */ - constructor(private backendSrv: BackendSrv, navModelSrv: NavModelSrv) { + constructor(private $scope: Scope, navModelSrv: NavModelSrv) { this.navModel = navModelSrv.getNav('admin', 'global-users', 0); this.query = ''; this.getUsers(); } getUsers() { - this.backendSrv - .get(`/api/users/search?perpage=${this.perPage}&page=${this.page}&query=${this.query}`) - .then((result: any) => { - this.users = result.users; - this.page = result.page; - this.perPage = result.perPage; - this.totalPages = Math.ceil(result.totalCount / result.perPage); - this.showPaging = this.totalPages > 1; - this.pages = []; + promiseToDigest(this.$scope)( + getBackendSrv() + .get(`/api/users/search?perpage=${this.perPage}&page=${this.page}&query=${this.query}`) + .then((result: any) => { + this.users = result.users; + this.page = result.page; + this.perPage = result.perPage; + this.totalPages = Math.ceil(result.totalCount / result.perPage); + this.showPaging = this.totalPages > 1; + this.pages = []; - for (let i = 1; i < this.totalPages + 1; i++) { - this.pages.push({ page: i, current: i === this.page }); - } + for (let i = 1; i < this.totalPages + 1; i++) { + this.pages.push({ page: i, current: i === this.page }); + } - this.addUsersAuthLabels(); - }); + this.addUsersAuthLabels(); + }) + ); } navigateToPage(page: any) { diff --git a/public/app/features/admin/AdminSettings.tsx b/public/app/features/admin/AdminSettings.tsx index d9c91e63e76..fec22e74410 100644 --- a/public/app/features/admin/AdminSettings.tsx +++ b/public/app/features/admin/AdminSettings.tsx @@ -9,8 +9,6 @@ import { StoreState } from 'app/types'; import { getNavModel } from 'app/core/selectors/navModel'; import Page from 'app/core/components/Page/Page'; -const backendSrv = getBackendSrv(); - type Settings = { [key: string]: { [key: string]: string } }; interface Props { @@ -29,7 +27,7 @@ export class AdminSettings extends React.PureComponent { }; async componentDidMount() { - const settings: Settings = await backendSrv.get('/api/admin/settings'); + const settings: Settings = await getBackendSrv().get('/api/admin/settings'); this.setState({ settings, isLoading: false, diff --git a/public/app/features/alerting/AlertTabCtrl.ts b/public/app/features/alerting/AlertTabCtrl.ts index b969d8269ca..96b1cab5a70 100644 --- a/public/app/features/alerting/AlertTabCtrl.ts +++ b/public/app/features/alerting/AlertTabCtrl.ts @@ -5,7 +5,7 @@ import { QueryPart } from 'app/core/components/query_part/query_part'; import alertDef from './state/alertDef'; import config from 'app/core/config'; import appEvents from 'app/core/app_events'; -import { BackendSrv } from 'app/core/services/backend_srv'; +import { getBackendSrv } from '@grafana/runtime'; import { DashboardSrv } from '../dashboard/services/DashboardSrv'; import DatasourceSrv from '../plugins/datasource_srv'; import { DataQuery, DataSourceApi } from '@grafana/data'; @@ -13,6 +13,7 @@ import { PanelModel } from 'app/features/dashboard/state'; import { getDefaultCondition } from './getAlertingValidationMessage'; import { CoreEvents } from 'app/types'; import kbn from 'app/core/utils/kbn'; +import { promiseToDigest } from 'app/core/utils/promiseToDigest'; export class AlertTabCtrl { panel: PanelModel; @@ -39,7 +40,6 @@ export class AlertTabCtrl { /** @ngInject */ constructor( private $scope: any, - private backendSrv: BackendSrv, private dashboardSrv: DashboardSrv, private uiSegmentSrv: any, private datasourceSrv: DatasourceSrv @@ -78,25 +78,31 @@ export class AlertTabCtrl { this.alertNotifications = []; this.alertHistory = []; - return this.backendSrv.get('/api/alert-notifications/lookup').then((res: any) => { - this.notifications = res; + return promiseToDigest(this.$scope)( + getBackendSrv() + .get('/api/alert-notifications/lookup') + .then((res: any) => { + this.notifications = res; - this.initModel(); - this.validateModel(); - }); + this.initModel(); + this.validateModel(); + }) + ); } getAlertHistory() { - this.backendSrv - .get(`/api/annotations?dashboardId=${this.panelCtrl.dashboard.id}&panelId=${this.panel.id}&limit=50&type=alert`) - .then((res: any) => { - this.alertHistory = _.map(res, ah => { - ah.time = this.dashboardSrv.getCurrent().formatDate(ah.time, 'MMM D, YYYY HH:mm:ss'); - ah.stateModel = alertDef.getStateDisplayModel(ah.newState); - ah.info = alertDef.getAlertAnnotationInfo(ah); - return ah; - }); - }); + promiseToDigest(this.$scope)( + getBackendSrv() + .get(`/api/annotations?dashboardId=${this.panelCtrl.dashboard.id}&panelId=${this.panel.id}&limit=50&type=alert`) + .then((res: any) => { + this.alertHistory = _.map(res, ah => { + ah.time = this.dashboardSrv.getCurrent().formatDate(ah.time, 'MMM D, YYYY HH:mm:ss'); + ah.stateModel = alertDef.getStateDisplayModel(ah.newState); + ah.info = alertDef.getAlertAnnotationInfo(ah); + return ah; + }); + }) + ); } getNotificationIcon(type: string): string { @@ -459,15 +465,17 @@ export class AlertTabCtrl { icon: 'fa-trash', yesText: 'Yes', onConfirm: () => { - this.backendSrv - .post('/api/annotations/mass-delete', { - dashboardId: this.panelCtrl.dashboard.id, - panelId: this.panel.id, - }) - .then(() => { - this.alertHistory = []; - this.panelCtrl.refresh(); - }); + promiseToDigest(this.$scope)( + getBackendSrv() + .post('/api/annotations/mass-delete', { + dashboardId: this.panelCtrl.dashboard.id, + panelId: this.panel.id, + }) + .then(() => { + this.alertHistory = []; + this.panelCtrl.refresh(); + }) + ); }, }); } diff --git a/public/app/features/alerting/NotificationsEditCtrl.ts b/public/app/features/alerting/NotificationsEditCtrl.ts index ccbd9dcdc88..7ea0137ae29 100644 --- a/public/app/features/alerting/NotificationsEditCtrl.ts +++ b/public/app/features/alerting/NotificationsEditCtrl.ts @@ -1,7 +1,9 @@ import _ from 'lodash'; import { appEvents, coreModule, NavModelSrv } from 'app/core/core'; -import { BackendSrv } from 'app/core/services/backend_srv'; +import { getBackendSrv } from '@grafana/runtime'; import { AppEvents } from '@grafana/data'; +import { IScope } from 'angular'; +import { promiseToDigest } from '../../core/utils/promiseToDigest'; export class AlertNotificationEditCtrl { theForm: any; @@ -28,8 +30,8 @@ export class AlertNotificationEditCtrl { /** @ngInject */ constructor( + private $scope: IScope, private $routeParams: any, - private backendSrv: BackendSrv, private $location: any, private $templateCache: any, navModelSrv: NavModelSrv @@ -41,33 +43,37 @@ export class AlertNotificationEditCtrl { return ['1m', '5m', '10m', '15m', '30m', '1h']; }; - this.backendSrv - .get(`/api/alert-notifiers`) - .then((notifiers: any) => { - this.notifiers = notifiers; + promiseToDigest(this.$scope)( + getBackendSrv() + .get(`/api/alert-notifiers`) + .then((notifiers: any) => { + this.notifiers = notifiers; - // add option templates - for (const notifier of this.notifiers) { - this.$templateCache.put(this.getNotifierTemplateId(notifier.type), notifier.optionsTemplate); - } + // add option templates + for (const notifier of this.notifiers) { + this.$templateCache.put(this.getNotifierTemplateId(notifier.type), notifier.optionsTemplate); + } - if (!this.$routeParams.id) { - this.navModel.breadcrumbs.push({ text: 'New channel' }); - this.navModel.node = { text: 'New channel' }; - return _.defaults(this.model, this.defaults); - } + if (!this.$routeParams.id) { + this.navModel.breadcrumbs.push({ text: 'New channel' }); + this.navModel.node = { text: 'New channel' }; + return _.defaults(this.model, this.defaults); + } - return this.backendSrv.get(`/api/alert-notifications/${this.$routeParams.id}`).then((result: any) => { - this.navModel.breadcrumbs.push({ text: result.name }); - this.navModel.node = { text: result.name }; - result.settings = _.defaults(result.settings, this.defaults.settings); - return result; - }); - }) - .then((model: any) => { - this.model = model; - this.notifierTemplateId = this.getNotifierTemplateId(this.model.type); - }); + return getBackendSrv() + .get(`/api/alert-notifications/${this.$routeParams.id}`) + .then((result: any) => { + this.navModel.breadcrumbs.push({ text: result.name }); + this.navModel.node = { text: result.name }; + result.settings = _.defaults(result.settings, this.defaults.settings); + return result; + }); + }) + .then((model: any) => { + this.model = model; + this.notifierTemplateId = this.getNotifierTemplateId(this.model.type); + }) + ); } save() { @@ -76,37 +82,45 @@ export class AlertNotificationEditCtrl { } if (this.model.id) { - this.backendSrv - .put(`/api/alert-notifications/${this.model.id}`, this.model) - .then((res: any) => { - this.model = res; - appEvents.emit(AppEvents.alertSuccess, ['Notification updated']); - }) - .catch((err: any) => { - if (err.data && err.data.error) { - appEvents.emit(AppEvents.alertError, [err.data.error]); - } - }); + promiseToDigest(this.$scope)( + getBackendSrv() + .put(`/api/alert-notifications/${this.model.id}`, this.model) + .then((res: any) => { + this.model = res; + appEvents.emit(AppEvents.alertSuccess, ['Notification updated']); + }) + .catch((err: any) => { + if (err.data && err.data.error) { + appEvents.emit(AppEvents.alertError, [err.data.error]); + } + }) + ); } else { - this.backendSrv - .post(`/api/alert-notifications`, this.model) - .then((res: any) => { - appEvents.emit(AppEvents.alertSuccess, ['Notification created']); - this.$location.path('alerting/notifications'); - }) - .catch((err: any) => { - if (err.data && err.data.error) { - appEvents.emit(AppEvents.alertError, [err.data.error]); - } - }); + promiseToDigest(this.$scope)( + getBackendSrv() + .post(`/api/alert-notifications`, this.model) + .then((res: any) => { + appEvents.emit(AppEvents.alertSuccess, ['Notification created']); + this.$location.path('alerting/notifications'); + }) + .catch((err: any) => { + if (err.data && err.data.error) { + appEvents.emit(AppEvents.alertError, [err.data.error]); + } + }) + ); } } deleteNotification() { - this.backendSrv.delete(`/api/alert-notifications/${this.model.id}`).then((res: any) => { - this.model = res; - this.$location.path('alerting/notifications'); - }); + promiseToDigest(this.$scope)( + getBackendSrv() + .delete(`/api/alert-notifications/${this.model.id}`) + .then((res: any) => { + this.model = res; + this.$location.path('alerting/notifications'); + }) + ); } getNotifierTemplateId(type: string) { @@ -130,7 +144,7 @@ export class AlertNotificationEditCtrl { settings: this.model.settings, }; - this.backendSrv.post(`/api/alert-notifications/test`, payload); + promiseToDigest(this.$scope)(getBackendSrv().post(`/api/alert-notifications/test`, payload)); } } diff --git a/public/app/features/alerting/NotificationsListCtrl.ts b/public/app/features/alerting/NotificationsListCtrl.ts index d6612f039a7..5842cdcfa2f 100644 --- a/public/app/features/alerting/NotificationsListCtrl.ts +++ b/public/app/features/alerting/NotificationsListCtrl.ts @@ -1,28 +1,39 @@ +import { IScope } from 'angular'; +import { getBackendSrv } from '@grafana/runtime'; + import { coreModule, NavModelSrv } from 'app/core/core'; -import { BackendSrv } from 'app/core/services/backend_srv'; +import { promiseToDigest } from '../../core/utils/promiseToDigest'; export class AlertNotificationsListCtrl { notifications: any; navModel: any; /** @ngInject */ - constructor(private backendSrv: BackendSrv, navModelSrv: NavModelSrv) { + constructor(private $scope: IScope, navModelSrv: NavModelSrv) { this.loadNotifications(); this.navModel = navModelSrv.getNav('alerting', 'channels', 0); } loadNotifications() { - this.backendSrv.get(`/api/alert-notifications`).then((result: any) => { - this.notifications = result; - }); + promiseToDigest(this.$scope)( + getBackendSrv() + .get(`/api/alert-notifications`) + .then((result: any) => { + this.notifications = result; + }) + ); } deleteNotification(id: number) { - this.backendSrv.delete(`/api/alert-notifications/${id}`).then(() => { - this.notifications = this.notifications.filter((notification: any) => { - return notification.id !== id; - }); - }); + promiseToDigest(this.$scope)( + getBackendSrv() + .delete(`/api/alert-notifications/${id}`) + .then(() => { + this.notifications = this.notifications.filter((notification: any) => { + return notification.id !== id; + }); + }) + ); } } diff --git a/public/app/features/alerting/TestRuleResult.test.tsx b/public/app/features/alerting/TestRuleResult.test.tsx index 5ba4e8bef66..acaff7f86af 100644 --- a/public/app/features/alerting/TestRuleResult.test.tsx +++ b/public/app/features/alerting/TestRuleResult.test.tsx @@ -3,11 +3,16 @@ import { TestRuleResult, Props } from './TestRuleResult'; import { DashboardModel } from '../dashboard/state'; import { shallow } from 'enzyme'; -jest.mock('@grafana/runtime/src/services/backendSrv', () => ({ - getBackendSrv: () => ({ - post: jest.fn(), - }), -})); +jest.mock('@grafana/runtime', () => { + const original = jest.requireActual('@grafana/runtime'); + + return { + ...original, + getBackendSrv: () => ({ + post: jest.fn(), + }), + }; +}); const setup = (propOverrides?: object) => { const props: Props = { diff --git a/public/app/features/alerting/TestRuleResult.tsx b/public/app/features/alerting/TestRuleResult.tsx index e2c103e85ff..db8390f0c6f 100644 --- a/public/app/features/alerting/TestRuleResult.tsx +++ b/public/app/features/alerting/TestRuleResult.tsx @@ -4,7 +4,7 @@ import { LoadingPlaceholder, JSONFormatter } from '@grafana/ui'; import appEvents from 'app/core/app_events'; import { CopyToClipboard } from 'app/core/components/CopyToClipboard/CopyToClipboard'; import { DashboardModel } from '../dashboard/state/DashboardModel'; -import { getBackendSrv, BackendSrv } from '@grafana/runtime'; +import { getBackendSrv } from '@grafana/runtime'; import { AppEvents } from '@grafana/data'; export interface Props { @@ -27,12 +27,6 @@ export class TestRuleResult extends PureComponent { formattedJson: any; clipboard: any; - backendSrv: BackendSrv = null; - - constructor(props: Props) { - super(props); - this.backendSrv = getBackendSrv(); - } componentDidMount() { this.testRule(); @@ -43,7 +37,7 @@ export class TestRuleResult extends PureComponent { const payload = { dashboard: dashboard.getSaveModelClone(), panelId }; this.setState({ isLoading: true }); - const testRuleResponse = await this.backendSrv.post(`/api/alerts/test`, payload); + const testRuleResponse = await getBackendSrv().post(`/api/alerts/test`, payload); this.setState({ isLoading: false, testRuleResponse }); } diff --git a/public/app/features/annotations/annotations_srv.ts b/public/app/features/annotations/annotations_srv.ts index 678f86d6ed7..d70198e56af 100644 --- a/public/app/features/annotations/annotations_srv.ts +++ b/public/app/features/annotations/annotations_srv.ts @@ -1,6 +1,6 @@ // Libaries -import angular from 'angular'; -import _ from 'lodash'; +import flattenDeep from 'lodash/flattenDeep'; +import cloneDeep from 'lodash/cloneDeep'; // Components import './editor_ctrl'; @@ -11,25 +11,16 @@ import { dedupAnnotations } from './events_processing'; // Types import { DashboardModel } from '../dashboard/state/DashboardModel'; -import DatasourceSrv from '../plugins/datasource_srv'; -import { BackendSrv } from 'app/core/services/backend_srv'; -import { TimeSrv } from '../dashboard/services/TimeSrv'; import { DataSourceApi, PanelEvents, AnnotationEvent, AppEvents, PanelModel, TimeRange } from '@grafana/data'; -import { GrafanaRootScope } from 'app/routes/GrafanaCtrl'; +import { appEvents } from 'app/core/core'; +import { getBackendSrv, getDataSourceSrv } from '@grafana/runtime'; +import { getTimeSrv } from '../dashboard/services/TimeSrv'; export class AnnotationsSrv { globalAnnotationsPromise: any; alertStatesPromise: any; datasourcePromises: any; - /** @ngInject */ - constructor( - private $rootScope: GrafanaRootScope, - private datasourceSrv: DatasourceSrv, - private backendSrv: BackendSrv, - private timeSrv: TimeSrv - ) {} - init(dashboard: DashboardModel) { // always clearPromiseCaches when loading new dashboard this.clearPromiseCaches(); @@ -47,10 +38,10 @@ export class AnnotationsSrv { return Promise.all([this.getGlobalAnnotations(options), this.getAlertStates(options)]) .then(results => { // combine the annotations and flatten results - let annotations: AnnotationEvent[] = _.flattenDeep(results[0]); + let annotations: AnnotationEvent[] = flattenDeep(results[0]); // filter out annotations that do not belong to requesting panel - annotations = _.filter(annotations, item => { + annotations = annotations.filter(item => { // if event has panel id and query is of type dashboard then panel and requesting panel id must match if (item.panelId && item.source.type === 'dashboard') { return item.panelId === options.panel.id; @@ -61,7 +52,7 @@ export class AnnotationsSrv { annotations = dedupAnnotations(annotations); // look for alert state for this panel - const alertState: any = _.find(results[1], { panelId: options.panel.id }); + const alertState: any = results[1].find((res: any) => res.panelId === options.panel.id); return { annotations: annotations, @@ -73,7 +64,7 @@ export class AnnotationsSrv { err.message = err.data.message; } console.log('AnnotationSrv.query error', err); - this.$rootScope.appEvent(AppEvents.alertError, ['Annotation Query Failed', err.message || err]); + appEvents.emit(AppEvents.alertError, ['Annotation Query Failed', err.message || err]); return []; }); } @@ -96,7 +87,7 @@ export class AnnotationsSrv { return this.alertStatesPromise; } - this.alertStatesPromise = this.backendSrv.get('/api/alerts/states-for-dashboard', { + this.alertStatesPromise = getBackendSrv().get('/api/alerts/states-for-dashboard', { dashboardId: options.dashboard.id, }); return this.alertStatesPromise; @@ -109,7 +100,7 @@ export class AnnotationsSrv { return this.globalAnnotationsPromise; } - const range = this.timeSrv.timeRange(); + const range = getTimeSrv().timeRange(); const promises = []; const dsPromises = []; @@ -121,7 +112,7 @@ export class AnnotationsSrv { if (annotation.snapshotData) { return this.translateQueryResult(annotation, annotation.snapshotData); } - const datasourcePromise = this.datasourceSrv.get(annotation.datasource); + const datasourcePromise = getDataSourceSrv().get(annotation.datasource); dsPromises.push(datasourcePromise); promises.push( datasourcePromise @@ -137,7 +128,7 @@ export class AnnotationsSrv { .then(results => { // store response in annotation object if this is a snapshot call if (dashboard.snapshot) { - annotation.snapshotData = angular.copy(results); + annotation.snapshotData = cloneDeep(results); } // translate result return this.translateQueryResult(annotation, results); @@ -151,26 +142,26 @@ export class AnnotationsSrv { saveAnnotationEvent(annotation: AnnotationEvent) { this.globalAnnotationsPromise = null; - return this.backendSrv.post('/api/annotations', annotation); + return getBackendSrv().post('/api/annotations', annotation); } updateAnnotationEvent(annotation: AnnotationEvent) { this.globalAnnotationsPromise = null; - return this.backendSrv.put(`/api/annotations/${annotation.id}`, annotation); + return getBackendSrv().put(`/api/annotations/${annotation.id}`, annotation); } deleteAnnotationEvent(annotation: AnnotationEvent) { this.globalAnnotationsPromise = null; const deleteUrl = `/api/annotations/${annotation.id}`; - return this.backendSrv.delete(deleteUrl); + return getBackendSrv().delete(deleteUrl); } translateQueryResult(annotation: any, results: any) { // if annotation has snapshotData // make clone and remove it if (annotation.snapshotData) { - annotation = angular.copy(annotation); + annotation = cloneDeep(annotation); delete annotation.snapshotData; } diff --git a/public/app/features/annotations/specs/annotations_srv.test.ts b/public/app/features/annotations/specs/annotations_srv.test.ts index 7086c2b5ed3..5329a1450aa 100644 --- a/public/app/features/annotations/specs/annotations_srv.test.ts +++ b/public/app/features/annotations/specs/annotations_srv.test.ts @@ -1,11 +1,7 @@ import { AnnotationsSrv } from '../annotations_srv'; describe('AnnotationsSrv', () => { - const $rootScope: any = { - onAppEvent: jest.fn(), - }; - - const annotationsSrv = new AnnotationsSrv($rootScope, null, null, null); + const annotationsSrv = new AnnotationsSrv(); describe('When translating the query result', () => { const annotationSource = { diff --git a/public/app/features/api-keys/state/actions.ts b/public/app/features/api-keys/state/actions.ts index f572999292a..c367331e362 100644 --- a/public/app/features/api-keys/state/actions.ts +++ b/public/app/features/api-keys/state/actions.ts @@ -25,7 +25,7 @@ export function loadApiKeys(includeExpired: boolean): ThunkResult { export function deleteApiKey(id: number, includeExpired: boolean): ThunkResult { return async dispatch => { getBackendSrv() - .delete('/api/auth/keys/' + id) - .then(dispatch(loadApiKeys(includeExpired))); + .delete(`/api/auth/keys/${id}`) + .then(() => dispatch(loadApiKeys(includeExpired))); }; } diff --git a/public/app/features/dashboard/components/DashLinks/DashLinksContainerCtrl.ts b/public/app/features/dashboard/components/DashLinks/DashLinksContainerCtrl.ts index a0cbfd60897..ce5ae36d418 100644 --- a/public/app/features/dashboard/components/DashLinks/DashLinksContainerCtrl.ts +++ b/public/app/features/dashboard/components/DashLinks/DashLinksContainerCtrl.ts @@ -2,11 +2,12 @@ import angular from 'angular'; import _ from 'lodash'; import { iconMap } from './DashLinksEditorCtrl'; import { LinkSrv } from 'app/features/panel/panellinks/link_srv'; -import { BackendSrv } from 'app/core/services/backend_srv'; +import { backendSrv } from 'app/core/services/backend_srv'; import { DashboardSrv } from '../../services/DashboardSrv'; import { PanelEvents } from '@grafana/data'; import { CoreEvents } from 'app/types'; import { GrafanaRootScope } from 'app/routes/GrafanaCtrl'; +import { promiseToDigest } from '../../../../core/utils/promiseToDigest'; export type DashboardLink = { tags: any; target: string; keepTime: any; includeVars: any }; @@ -94,13 +95,7 @@ function dashLink($compile: any, $sanitize: any, linkSrv: LinkSrv) { export class DashLinksContainerCtrl { /** @ngInject */ - constructor( - $scope: any, - $rootScope: GrafanaRootScope, - backendSrv: BackendSrv, - dashboardSrv: DashboardSrv, - linkSrv: LinkSrv - ) { + constructor($scope: any, $rootScope: GrafanaRootScope, dashboardSrv: DashboardSrv, linkSrv: LinkSrv) { const currentDashId = dashboardSrv.getCurrent().id; function buildLinks(linkDef: any) { @@ -154,26 +149,28 @@ export class DashLinksContainerCtrl { } $scope.searchDashboards = (link: DashboardLink, limit: any) => { - return backendSrv.search({ tag: link.tags, limit: limit }).then(results => { - return _.reduce( - results, - (memo, dash) => { - // do not add current dashboard - if (dash.id !== currentDashId) { - memo.push({ - title: dash.title, - url: dash.url, - target: link.target === '_self' ? '' : link.target, - icon: 'fa fa-th-large', - keepTime: link.keepTime, - includeVars: link.includeVars, - }); - } - return memo; - }, - [] - ); - }); + return promiseToDigest($scope)( + backendSrv.search({ tag: link.tags, limit: limit }).then(results => { + return _.reduce( + results, + (memo, dash) => { + // do not add current dashboard + if (dash.id !== currentDashId) { + memo.push({ + title: dash.title, + url: dash.url, + target: link.target === '_self' ? '' : link.target, + icon: 'fa fa-th-large', + keepTime: link.keepTime, + includeVars: link.includeVars, + }); + } + return memo; + }, + [] + ); + }) + ); }; $scope.fillDropdown = (link: { searchHits: any }) => { diff --git a/public/app/features/dashboard/components/DashboardSettings/SettingsCtrl.ts b/public/app/features/dashboard/components/DashboardSettings/SettingsCtrl.ts index 4947290b794..5fffadf472f 100644 --- a/public/app/features/dashboard/components/DashboardSettings/SettingsCtrl.ts +++ b/public/app/features/dashboard/components/DashboardSettings/SettingsCtrl.ts @@ -1,15 +1,17 @@ -import { appEvents, contextSrv, coreModule } from 'app/core/core'; -import { DashboardModel } from '../../state/DashboardModel'; import $ from 'jquery'; import _ from 'lodash'; -import angular, { ILocationService } from 'angular'; +import angular, { ILocationService, IScope } from 'angular'; +import { e2e } from '@grafana/e2e'; + +import { appEvents, contextSrv, coreModule } from 'app/core/core'; +import { DashboardModel } from '../../state/DashboardModel'; import config from 'app/core/config'; -import { BackendSrv } from 'app/core/services/backend_srv'; +import { backendSrv } from 'app/core/services/backend_srv'; import { DashboardSrv } from '../../services/DashboardSrv'; import { CoreEvents } from 'app/types'; import { GrafanaRootScope } from 'app/routes/GrafanaCtrl'; import { AppEvents } from '@grafana/data'; -import { e2e } from '@grafana/e2e'; +import { promiseToDigest } from '../../../../core/utils/promiseToDigest'; export class SettingsCtrl { dashboard: DashboardModel; @@ -26,11 +28,10 @@ export class SettingsCtrl { /** @ngInject */ constructor( - private $scope: any, + private $scope: IScope & Record, private $route: any, private $location: ILocationService, private $rootScope: GrafanaRootScope, - private backendSrv: BackendSrv, private dashboardSrv: DashboardSrv ) { // temp hack for annotations and variables editors @@ -234,10 +235,12 @@ export class SettingsCtrl { } deleteDashboardConfirmed() { - this.backendSrv.deleteDashboard(this.dashboard.uid, false).then(() => { - appEvents.emit(AppEvents.alertSuccess, ['Dashboard Deleted', this.dashboard.title + ' has been deleted']); - this.$location.url('/'); - }); + promiseToDigest(this.$scope)( + backendSrv.deleteDashboard(this.dashboard.uid, false).then(() => { + appEvents.emit(AppEvents.alertSuccess, ['Dashboard Deleted', this.dashboard.title + ' has been deleted']); + this.$location.url('/'); + }) + ); } onFolderChange(folder: { id: number; title: string }) { diff --git a/public/app/features/dashboard/components/FolderPicker/FolderPickerCtrl.ts b/public/app/features/dashboard/components/FolderPicker/FolderPickerCtrl.ts index ef4d7b192dd..d52f96c4917 100644 --- a/public/app/features/dashboard/components/FolderPicker/FolderPickerCtrl.ts +++ b/public/app/features/dashboard/components/FolderPicker/FolderPickerCtrl.ts @@ -1,10 +1,13 @@ import _ from 'lodash'; +import { IScope } from 'angular'; +import { AppEvents } from '@grafana/data'; + import coreModule from 'app/core/core_module'; import appEvents from 'app/core/app_events'; -import { BackendSrv } from 'app/core/services/backend_srv'; +import { backendSrv } from 'app/core/services/backend_srv'; import { ValidationSrv } from 'app/features/manage-dashboards'; import { ContextSrv } from 'app/core/services/context_srv'; -import { AppEvents } from '@grafana/data'; +import { promiseToDigest } from '../../../../core/utils/promiseToDigest'; export class FolderPickerCtrl { initialTitle: string; @@ -28,7 +31,7 @@ export class FolderPickerCtrl { dashboardId?: number; /** @ngInject */ - constructor(private backendSrv: BackendSrv, private validationSrv: ValidationSrv, private contextSrv: ContextSrv) { + constructor(private validationSrv: ValidationSrv, private contextSrv: ContextSrv, private $scope: IScope) { this.isEditor = this.contextSrv.isEditor; if (!this.labelClass) { @@ -45,33 +48,35 @@ export class FolderPickerCtrl { permission: 'Edit', }; - return this.backendSrv.get('api/search', params).then((result: any) => { - if ( - this.isEditor && - (query === '' || - query.toLowerCase() === 'g' || - query.toLowerCase() === 'ge' || - query.toLowerCase() === 'gen' || - query.toLowerCase() === 'gene' || - query.toLowerCase() === 'gener' || - query.toLowerCase() === 'genera' || - query.toLowerCase() === 'general') - ) { - result.unshift({ title: this.rootName, id: 0 }); - } + return promiseToDigest(this.$scope)( + backendSrv.get('api/search', params).then((result: any) => { + if ( + this.isEditor && + (query === '' || + query.toLowerCase() === 'g' || + query.toLowerCase() === 'ge' || + query.toLowerCase() === 'gen' || + query.toLowerCase() === 'gene' || + query.toLowerCase() === 'gener' || + query.toLowerCase() === 'genera' || + query.toLowerCase() === 'general') + ) { + result.unshift({ title: this.rootName, id: 0 }); + } - if (this.isEditor && this.enableCreateNew && query === '') { - result.unshift({ title: '-- New Folder --', id: -1 }); - } + if (this.isEditor && this.enableCreateNew && query === '') { + result.unshift({ title: '-- New Folder --', id: -1 }); + } - if (this.enableReset && query === '' && this.initialTitle !== '') { - result.unshift({ title: this.initialTitle, id: null }); - } + if (this.enableReset && query === '' && this.initialTitle !== '') { + result.unshift({ title: this.initialTitle, id: null }); + } - return _.map(result, item => { - return { text: item.title, value: item.id }; - }); - }); + return _.map(result, item => { + return { text: item.title, value: item.id }; + }); + }) + ); } onFolderChange(option: { value: number; text: string }) { @@ -105,13 +110,15 @@ export class FolderPickerCtrl { evt.preventDefault(); } - return this.backendSrv.createFolder({ title: this.newFolderName }).then((result: { title: string; id: number }) => { - appEvents.emit(AppEvents.alertSuccess, ['Folder Created', 'OK']); + return promiseToDigest(this.$scope)( + backendSrv.createFolder({ title: this.newFolderName }).then((result: { title: string; id: number }) => { + appEvents.emit(AppEvents.alertSuccess, ['Folder Created', 'OK']); - this.closeCreateFolder(); - this.folder = { text: result.title, value: result.id }; - this.onFolderChange(this.folder); - }); + this.closeCreateFolder(); + this.folder = { text: result.title, value: result.id }; + this.onFolderChange(this.folder); + }) + ); } cancelCreateFolder(evt: any) { diff --git a/public/app/features/dashboard/components/SaveModals/SaveDashboardModalCtrl.ts b/public/app/features/dashboard/components/SaveModals/SaveDashboardModalCtrl.ts index 0c5f4801692..8e85fa51e0a 100644 --- a/public/app/features/dashboard/components/SaveModals/SaveDashboardModalCtrl.ts +++ b/public/app/features/dashboard/components/SaveModals/SaveDashboardModalCtrl.ts @@ -96,7 +96,7 @@ export class SaveDashboardModalCtrl { this.selectors = e2e.pages.SaveDashboardModal.selectors; } - save() { + save(): void | Promise { if (!this.saveForm.$valid) { return; } diff --git a/public/app/features/dashboard/components/ShareModal/ShareSnapshotCtrl.ts b/public/app/features/dashboard/components/ShareModal/ShareSnapshotCtrl.ts index 7d81d85eaf9..de76be9c0ce 100644 --- a/public/app/features/dashboard/components/ShareModal/ShareSnapshotCtrl.ts +++ b/public/app/features/dashboard/components/ShareModal/ShareSnapshotCtrl.ts @@ -1,18 +1,19 @@ -import angular, { ILocationService } from 'angular'; +import angular, { ILocationService, IScope } from 'angular'; import _ from 'lodash'; -import { BackendSrv } from 'app/core/services/backend_srv'; +import { getBackendSrv } from '@grafana/runtime'; + import { TimeSrv } from '../../services/TimeSrv'; import { DashboardModel } from '../../state/DashboardModel'; import { PanelModel } from '../../state/PanelModel'; import { GrafanaRootScope } from 'app/routes/GrafanaCtrl'; +import { promiseToDigest } from '../../../../core/utils/promiseToDigest'; export class ShareSnapshotCtrl { /** @ngInject */ constructor( - $scope: any, + $scope: IScope & Record, $rootScope: GrafanaRootScope, $location: ILocationService, - backendSrv: BackendSrv, $timeout: any, timeSrv: TimeSrv ) { @@ -38,10 +39,14 @@ export class ShareSnapshotCtrl { ]; $scope.init = () => { - backendSrv.get('/api/snapshot/shared-options').then((options: { [x: string]: any }) => { - $scope.sharingButtonText = options['externalSnapshotName']; - $scope.externalEnabled = options['externalEnabled']; - }); + promiseToDigest($scope)( + getBackendSrv() + .get('/api/snapshot/shared-options') + .then((options: { [x: string]: any }) => { + $scope.sharingButtonText = options['externalSnapshotName']; + $scope.externalEnabled = options['externalEnabled']; + }) + ); }; $scope.apiUrl = '/api/snapshots'; @@ -75,16 +80,20 @@ export class ShareSnapshotCtrl { external: external, }; - backendSrv.post($scope.apiUrl, cmdData).then( - (results: { deleteUrl: any; url: any }) => { - $scope.loading = false; - $scope.deleteUrl = results.deleteUrl; - $scope.snapshotUrl = results.url; - $scope.step = 2; - }, - () => { - $scope.loading = false; - } + promiseToDigest($scope)( + getBackendSrv() + .post($scope.apiUrl, cmdData) + .then( + (results: { deleteUrl: any; url: any }) => { + $scope.loading = false; + $scope.deleteUrl = results.deleteUrl; + $scope.snapshotUrl = results.url; + $scope.step = 2; + }, + () => { + $scope.loading = false; + } + ) ); }; @@ -152,9 +161,13 @@ export class ShareSnapshotCtrl { }; $scope.deleteSnapshot = () => { - backendSrv.get($scope.deleteUrl).then(() => { - $scope.step = 3; - }); + promiseToDigest($scope)( + getBackendSrv() + .get($scope.deleteUrl) + .then(() => { + $scope.step = 3; + }) + ); }; } } diff --git a/public/app/features/dashboard/components/VersionHistory/HistoryListCtrl.test.ts b/public/app/features/dashboard/components/VersionHistory/HistoryListCtrl.test.ts index d5479f369a9..8ebc0bbf79d 100644 --- a/public/app/features/dashboard/components/VersionHistory/HistoryListCtrl.test.ts +++ b/public/app/features/dashboard/components/VersionHistory/HistoryListCtrl.test.ts @@ -1,6 +1,8 @@ import _ from 'lodash'; +import { IScope } from 'angular'; + import { HistoryListCtrl } from './HistoryListCtrl'; -import { versions, compare, restore } from './__mocks__/history'; +import { compare, restore, versions } from './__mocks__/history'; import { CoreEvents } from 'app/types'; describe('HistoryListCtrl', () => { @@ -12,6 +14,7 @@ describe('HistoryListCtrl', () => { let historySrv: any; let $rootScope: any; + const $scope: IScope = ({ $evalAsync: jest.fn() } as any) as IScope; let historyListCtrl: any; beforeEach(() => { historySrv = { @@ -28,7 +31,7 @@ describe('HistoryListCtrl', () => { beforeEach(() => { historySrv.getHistoryList = jest.fn(() => Promise.resolve({})); - historyListCtrl = new HistoryListCtrl({}, $rootScope, {} as any, historySrv, {}); + historyListCtrl = new HistoryListCtrl({}, $rootScope, {} as any, historySrv, $scope); historyListCtrl.dashboard = { id: 2, @@ -84,7 +87,7 @@ describe('HistoryListCtrl', () => { beforeEach(async () => { historySrv.getHistoryList = jest.fn(() => Promise.reject(new Error('HistoryListError'))); - historyListCtrl = new HistoryListCtrl({}, $rootScope, {} as any, historySrv, {}); + historyListCtrl = new HistoryListCtrl({}, $rootScope, {} as any, historySrv, $scope); await historyListCtrl.getLog(); }); @@ -127,7 +130,7 @@ describe('HistoryListCtrl', () => { historySrv.getHistoryList = jest.fn(() => Promise.resolve(versionsResponse)); historySrv.calculateDiff = jest.fn(() => Promise.resolve({})); - historyListCtrl = new HistoryListCtrl({}, $rootScope, {} as any, historySrv, {}); + historyListCtrl = new HistoryListCtrl({}, $rootScope, {} as any, historySrv, $scope); historyListCtrl.dashboard = { id: 2, @@ -260,7 +263,7 @@ describe('HistoryListCtrl', () => { historySrv.getHistoryList = jest.fn(() => Promise.resolve(versionsResponse)); historySrv.restoreDashboard = jest.fn(() => Promise.resolve()); - historyListCtrl = new HistoryListCtrl({}, $rootScope, {} as any, historySrv, {}); + historyListCtrl = new HistoryListCtrl({}, $rootScope, {} as any, historySrv, $scope); historyListCtrl.dashboard = { id: 1, @@ -279,7 +282,7 @@ describe('HistoryListCtrl', () => { beforeEach(async () => { historySrv.getHistoryList = jest.fn(() => Promise.resolve(versionsResponse)); historySrv.restoreDashboard = jest.fn(() => Promise.resolve()); - historyListCtrl = new HistoryListCtrl({}, $rootScope, {} as any, historySrv, {}); + historyListCtrl = new HistoryListCtrl({}, $rootScope, {} as any, historySrv, $scope); historySrv.restoreDashboard = jest.fn(() => Promise.reject(new Error('RestoreError'))); historyListCtrl.restoreConfirm(RESTORE_ID); await historyListCtrl.getLog(); diff --git a/public/app/features/dashboard/components/VersionHistory/HistoryListCtrl.ts b/public/app/features/dashboard/components/VersionHistory/HistoryListCtrl.ts index 91bfb05d7be..0218d9bb68a 100644 --- a/public/app/features/dashboard/components/VersionHistory/HistoryListCtrl.ts +++ b/public/app/features/dashboard/components/VersionHistory/HistoryListCtrl.ts @@ -1,12 +1,13 @@ import _ from 'lodash'; -import angular, { ILocationService } from 'angular'; +import angular, { ILocationService, IScope } from 'angular'; import locationUtil from 'app/core/utils/location_util'; import { DashboardModel } from '../../state/DashboardModel'; -import { HistoryListOpts, RevisionsModel, CalculateDiffOptions, HistorySrv } from './HistorySrv'; -import { dateTime, toUtc, DateTimeInput, AppEvents } from '@grafana/data'; +import { CalculateDiffOptions, HistoryListOpts, HistorySrv, RevisionsModel } from './HistorySrv'; +import { AppEvents, dateTime, DateTimeInput, toUtc } from '@grafana/data'; import { GrafanaRootScope } from 'app/routes/GrafanaCtrl'; import { CoreEvents } from 'app/types'; +import { promiseToDigest } from '../../../../core/utils/promiseToDigest'; export class HistoryListCtrl { appending: boolean; @@ -30,7 +31,7 @@ export class HistoryListCtrl { private $rootScope: GrafanaRootScope, private $location: ILocationService, private historySrv: HistorySrv, - public $scope: any + public $scope: IScope ) { this.appending = false; this.diff = 'basic'; @@ -108,18 +109,20 @@ export class HistoryListCtrl { diffType: diff, }; - return this.historySrv - .calculateDiff(options) - .then((response: any) => { - // @ts-ignore - this.delta[this.diff] = response; - }) - .catch(() => { - this.mode = 'list'; - }) - .finally(() => { - this.loading = false; - }); + return promiseToDigest(this.$scope)( + this.historySrv + .calculateDiff(options) + .then((response: any) => { + // @ts-ignore + this.delta[this.diff] = response; + }) + .catch(() => { + this.mode = 'list'; + }) + .finally(() => { + this.loading = false; + }) + ); } getLog(append = false) { @@ -130,25 +133,27 @@ export class HistoryListCtrl { start: this.start, }; - return this.historySrv - .getHistoryList(this.dashboard, options) - .then((revisions: any) => { - // set formatted dates & default values - for (const rev of revisions) { - rev.createdDateString = this.formatDate(rev.created); - rev.ageString = this.formatBasicDate(rev.created); - rev.checked = false; - } + return promiseToDigest(this.$scope)( + this.historySrv + .getHistoryList(this.dashboard, options) + .then((revisions: any) => { + // set formatted dates & default values + for (const rev of revisions) { + rev.createdDateString = this.formatDate(rev.created); + rev.ageString = this.formatBasicDate(rev.created); + rev.checked = false; + } - this.revisions = append ? this.revisions.concat(revisions) : revisions; - }) - .catch((err: any) => { - this.loading = false; - }) - .finally(() => { - this.loading = false; - this.appending = false; - }); + this.revisions = append ? this.revisions.concat(revisions) : revisions; + }) + .catch((err: any) => { + this.loading = false; + }) + .finally(() => { + this.loading = false; + this.appending = false; + }) + ); } isLastPage() { @@ -183,17 +188,19 @@ export class HistoryListCtrl { restoreConfirm(version: number) { this.loading = true; - return this.historySrv - .restoreDashboard(this.dashboard, version) - .then((response: any) => { - this.$location.url(locationUtil.stripBaseFromUrl(response.url)).replace(); - this.$route.reload(); - this.$rootScope.appEvent(AppEvents.alertSuccess, ['Dashboard restored', 'Restored from version ' + version]); - }) - .catch(() => { - this.mode = 'list'; - this.loading = false; - }); + return promiseToDigest(this.$scope)( + this.historySrv + .restoreDashboard(this.dashboard, version) + .then((response: any) => { + this.$location.url(locationUtil.stripBaseFromUrl(response.url)).replace(); + this.$route.reload(); + this.$rootScope.appEvent(AppEvents.alertSuccess, ['Dashboard restored', 'Restored from version ' + version]); + }) + .catch(() => { + this.mode = 'list'; + this.loading = false; + }) + ); } } diff --git a/public/app/features/dashboard/components/VersionHistory/HistorySrv.test.ts b/public/app/features/dashboard/components/VersionHistory/HistorySrv.test.ts index c09563e1635..ba069c5b49f 100644 --- a/public/app/features/dashboard/components/VersionHistory/HistorySrv.test.ts +++ b/public/app/features/dashboard/components/VersionHistory/HistorySrv.test.ts @@ -1,27 +1,41 @@ import { versions, restore } from './__mocks__/history'; import { HistorySrv } from './HistorySrv'; import { DashboardModel } from '../../state/DashboardModel'; + +const getMock = jest.fn().mockResolvedValue({}); +const postMock = jest.fn().mockResolvedValue({}); + jest.mock('app/core/store'); +jest.mock('@grafana/runtime', () => { + const original = jest.requireActual('@grafana/runtime'); + + return { + ...original, + getBackendSrv: () => ({ + post: postMock, + get: getMock, + }), + }; +}); describe('historySrv', () => { const versionsResponse = versions(); const restoreResponse = restore; - const backendSrv: any = { - get: jest.fn(() => Promise.resolve({})), - post: jest.fn(() => Promise.resolve({})), - }; - - let historySrv = new HistorySrv(backendSrv); + let historySrv = new HistorySrv(); const dash = new DashboardModel({ id: 1 }); const emptyDash = new DashboardModel({}); const historyListOpts = { limit: 10, start: 0 }; + beforeEach(() => { + jest.clearAllMocks(); + }); + describe('getHistoryList', () => { it('should return a versions array for the given dashboard id', () => { - backendSrv.get = jest.fn(() => Promise.resolve(versionsResponse)); - historySrv = new HistorySrv(backendSrv); + getMock.mockImplementation(() => Promise.resolve(versionsResponse)); + historySrv = new HistorySrv(); return historySrv.getHistoryList(dash, historyListOpts).then((versions: any) => { expect(versions).toEqual(versionsResponse); @@ -44,15 +58,15 @@ describe('historySrv', () => { describe('restoreDashboard', () => { it('should return a success response given valid parameters', () => { const version = 6; - backendSrv.post = jest.fn(() => Promise.resolve(restoreResponse(version))); - historySrv = new HistorySrv(backendSrv); + postMock.mockImplementation(() => Promise.resolve(restoreResponse(version))); + historySrv = new HistorySrv(); return historySrv.restoreDashboard(dash, version).then((response: any) => { expect(response).toEqual(restoreResponse(version)); }); }); it('should return an empty object when not given an id', async () => { - historySrv = new HistorySrv(backendSrv); + historySrv = new HistorySrv(); const rsp = await historySrv.restoreDashboard(emptyDash, 6); expect(rsp).toEqual({}); }); diff --git a/public/app/features/dashboard/components/VersionHistory/HistorySrv.ts b/public/app/features/dashboard/components/VersionHistory/HistorySrv.ts index f62f73e4347..b32e2e191c5 100644 --- a/public/app/features/dashboard/components/VersionHistory/HistorySrv.ts +++ b/public/app/features/dashboard/components/VersionHistory/HistorySrv.ts @@ -1,7 +1,7 @@ import _ from 'lodash'; import coreModule from 'app/core/core_module'; import { DashboardModel } from '../../state/DashboardModel'; -import { BackendSrv } from 'app/core/services/backend_srv'; +import { getBackendSrv } from '@grafana/runtime'; export interface HistoryListOpts { limit: number; @@ -32,23 +32,20 @@ export interface DiffTarget { } export class HistorySrv { - /** @ngInject */ - constructor(private backendSrv: BackendSrv) {} - getHistoryList(dashboard: DashboardModel, options: HistoryListOpts) { const id = dashboard && dashboard.id ? dashboard.id : void 0; - return id ? this.backendSrv.get(`api/dashboards/id/${id}/versions`, options) : Promise.resolve([]); + return id ? getBackendSrv().get(`api/dashboards/id/${id}/versions`, options) : Promise.resolve([]); } calculateDiff(options: CalculateDiffOptions) { - return this.backendSrv.post('api/dashboards/calculate-diff', options); + return getBackendSrv().post('api/dashboards/calculate-diff', options); } restoreDashboard(dashboard: DashboardModel, version: number) { const id = dashboard && dashboard.id ? dashboard.id : void 0; const url = `api/dashboards/id/${id}/restore`; - return id && _.isNumber(version) ? this.backendSrv.post(url, { version }) : Promise.resolve({}); + return id && _.isNumber(version) ? getBackendSrv().post(url, { version }) : Promise.resolve({}); } } diff --git a/public/app/features/dashboard/panel_editor/QueriesTab.tsx b/public/app/features/dashboard/panel_editor/QueriesTab.tsx index 3d3f82e1d1a..3fe4671a754 100644 --- a/public/app/features/dashboard/panel_editor/QueriesTab.tsx +++ b/public/app/features/dashboard/panel_editor/QueriesTab.tsx @@ -11,7 +11,7 @@ import { PanelOptionsGroup, TransformationsEditor, AlphaNotice } from '@grafana/ import { QueryEditorRows } from './QueryEditorRows'; // Services import { getDatasourceSrv } from 'app/features/plugins/datasource_srv'; -import { getBackendSrv } from 'app/core/services/backend_srv'; +import { backendSrv } from 'app/core/services/backend_srv'; import config from 'app/core/config'; // Types import { PanelModel } from '../state/PanelModel'; @@ -48,7 +48,7 @@ interface State { export class QueriesTab extends PureComponent { datasources: DataSourceSelectItem[] = getDatasourceSrv().getMetricSources(); - backendSrv = getBackendSrv(); + backendSrv = backendSrv; querySubscription: Unsubscribable; state: State = { diff --git a/public/app/features/dashboard/services/DashboardLoaderSrv.ts b/public/app/features/dashboard/services/DashboardLoaderSrv.ts index d2c90461bbd..ea3db8dff51 100644 --- a/public/app/features/dashboard/services/DashboardLoaderSrv.ts +++ b/public/app/features/dashboard/services/DashboardLoaderSrv.ts @@ -6,7 +6,7 @@ import $ from 'jquery'; import kbn from 'app/core/utils/kbn'; import { dateMath, AppEvents } from '@grafana/data'; import impressionSrv from 'app/core/services/impression_srv'; -import { BackendSrv } from 'app/core/services/backend_srv'; +import { backendSrv } from 'app/core/services/backend_srv'; import { DashboardSrv } from './DashboardSrv'; import DatasourceSrv from 'app/features/plugins/datasource_srv'; import { UrlQueryValue } from '@grafana/runtime'; @@ -15,7 +15,6 @@ import { GrafanaRootScope } from 'app/routes/GrafanaCtrl'; export class DashboardLoaderSrv { /** @ngInject */ constructor( - private backendSrv: BackendSrv, private dashboardSrv: DashboardSrv, private datasourceSrv: DatasourceSrv, private $http: any, @@ -46,11 +45,11 @@ export class DashboardLoaderSrv { if (type === 'script') { promise = this._loadScriptedDashboard(slug); } else if (type === 'snapshot') { - promise = this.backendSrv.get('/api/snapshots/' + slug).catch(() => { + promise = backendSrv.get('/api/snapshots/' + slug).catch(() => { return this._dashboardLoadFailed('Snapshot not found', true); }); } else { - promise = this.backendSrv + promise = backendSrv .getDashboardByUid(uid) .then((result: any) => { if (result.meta.isFolder) { diff --git a/public/app/features/dashboard/services/DashboardSrv.ts b/public/app/features/dashboard/services/DashboardSrv.ts index bebcd1aac13..e71a8b73d45 100644 --- a/public/app/features/dashboard/services/DashboardSrv.ts +++ b/public/app/features/dashboard/services/DashboardSrv.ts @@ -1,14 +1,15 @@ +import { ILocationService } from 'angular'; +import { AppEvents, PanelEvents } from '@grafana/data'; + import coreModule from 'app/core/core_module'; import { appEvents } from 'app/core/app_events'; import locationUtil from 'app/core/utils/location_util'; import { DashboardModel } from '../state/DashboardModel'; import { removePanel } from '../utils/panel'; -import { DashboardMeta, CoreEvents } from 'app/types'; +import { CoreEvents, DashboardMeta } from 'app/types'; import { GrafanaRootScope } from 'app/routes/GrafanaCtrl'; -import { BackendSrv } from 'app/core/services/backend_srv'; -import { ILocationService } from 'angular'; -import { AppEvents } from '@grafana/data'; -import { PanelEvents } from '@grafana/data'; +import { backendSrv } from 'app/core/services/backend_srv'; +import { promiseToDigest } from '../../../core/utils/promiseToDigest'; interface DashboardSaveOptions { folderId?: number; @@ -21,11 +22,7 @@ export class DashboardSrv { dashboard: DashboardModel; /** @ngInject */ - constructor( - private backendSrv: BackendSrv, - private $rootScope: GrafanaRootScope, - private $location: ILocationService - ) { + constructor(private $rootScope: GrafanaRootScope, private $location: ILocationService) { appEvents.on(CoreEvents.saveDashboard, this.saveDashboard.bind(this), $rootScope); appEvents.on(PanelEvents.panelChangeView, this.onPanelChangeView); appEvents.on(CoreEvents.removePanel, this.onRemovePanel); @@ -167,10 +164,12 @@ export class DashboardSrv { save(clone: any, options?: DashboardSaveOptions) { options.folderId = options.folderId >= 0 ? options.folderId : this.dashboard.meta.folderId || clone.folderId; - return this.backendSrv - .saveDashboard(clone, options) - .then((data: any) => this.postSave(data)) - .catch(this.handleSaveDashboardError.bind(this, clone, { folderId: options.folderId })); + return promiseToDigest(this.$rootScope)( + backendSrv + .saveDashboard(clone, options) + .then((data: any) => this.postSave(data)) + .catch(this.handleSaveDashboardError.bind(this, clone, { folderId: options.folderId })) + ); } saveDashboard( @@ -228,13 +227,17 @@ export class DashboardSrv { let promise; if (isStarred) { - promise = this.backendSrv.delete('/api/user/stars/dashboard/' + dashboardId).then(() => { - return false; - }); + promise = promiseToDigest(this.$rootScope)( + backendSrv.delete('/api/user/stars/dashboard/' + dashboardId).then(() => { + return false; + }) + ); } else { - promise = this.backendSrv.post('/api/user/stars/dashboard/' + dashboardId).then(() => { - return true; - }); + promise = promiseToDigest(this.$rootScope)( + backendSrv.post('/api/user/stars/dashboard/' + dashboardId).then(() => { + return true; + }) + ); } return promise.then((res: boolean) => { diff --git a/public/app/features/dashboard/state/initDashboard.ts b/public/app/features/dashboard/state/initDashboard.ts index 672d42d23fa..8795e7c4e62 100644 --- a/public/app/features/dashboard/state/initDashboard.ts +++ b/public/app/features/dashboard/state/initDashboard.ts @@ -1,6 +1,6 @@ // Services & Utils import { createErrorNotification } from 'app/core/copy/appNotification'; -import { getBackendSrv } from 'app/core/services/backend_srv'; +import { backendSrv } from 'app/core/services/backend_srv'; import { DashboardSrv } from 'app/features/dashboard/services/DashboardSrv'; import { DashboardLoaderSrv } from 'app/features/dashboard/services/DashboardLoaderSrv'; import { TimeSrv } from 'app/features/dashboard/services/TimeSrv'; @@ -38,7 +38,7 @@ export interface InitDashboardArgs { } async function redirectToNewUrl(slug: string, dispatch: ThunkDispatch, currentPath: string) { - const res = await getBackendSrv().getDashboardBySlug(slug); + const res = await backendSrv.getDashboardBySlug(slug); if (res) { let newUrl = res.meta.url; @@ -62,7 +62,7 @@ async function fetchDashboard( switch (args.routeInfo) { case DashboardRouteInfo.Home: { // load home dash - const dashDTO: DashboardDTO = await getBackendSrv().get('/api/dashboards/home'); + const dashDTO: DashboardDTO = await backendSrv.get('/api/dashboards/home'); // if user specified a custom home dashboard redirect to that if (dashDTO.redirectUri) { diff --git a/public/app/features/dashboard/state/runRequest.ts b/public/app/features/dashboard/state/runRequest.ts index e97260d76b0..09da05e1244 100644 --- a/public/app/features/dashboard/state/runRequest.ts +++ b/public/app/features/dashboard/state/runRequest.ts @@ -3,7 +3,7 @@ import { Observable, of, timer, merge, from } from 'rxjs'; import { flatten, map as lodashMap, isArray, isString } from 'lodash'; import { map, catchError, takeUntil, mapTo, share, finalize, tap } from 'rxjs/operators'; // Utils & Services -import { getBackendSrv } from 'app/core/services/backend_srv'; +import { backendSrv } from 'app/core/services/backend_srv'; // Types import { DataSourceApi, @@ -136,7 +136,7 @@ export function runRequest(datasource: DataSourceApi, request: DataQueryRequest) function cancelNetworkRequestsOnUnsubscribe(req: DataQueryRequest) { return () => { - getBackendSrv().resolveCancelerIfExists(req.requestId); + backendSrv.resolveCancelerIfExists(req.requestId); }; } diff --git a/public/app/features/datasources/settings/DataSourceSettingsPage.tsx b/public/app/features/datasources/settings/DataSourceSettingsPage.tsx index 72a5803428b..6c6e6c53adb 100644 --- a/public/app/features/datasources/settings/DataSourceSettingsPage.tsx +++ b/public/app/features/datasources/settings/DataSourceSettingsPage.tsx @@ -11,7 +11,7 @@ import BasicSettings from './BasicSettings'; import ButtonRow from './ButtonRow'; // Services & Utils import appEvents from 'app/core/app_events'; -import { getBackendSrv } from 'app/core/services/backend_srv'; +import { backendSrv } from 'app/core/services/backend_srv'; import { getDatasourceSrv } from 'app/features/plugins/datasource_srv'; // Actions & selectors import { getDataSource, getDataSourceMeta } from '../state/selectors'; @@ -145,7 +145,7 @@ export class DataSourceSettingsPage extends PureComponent { this.setState({ isTesting: true, testingMessage: 'Testing...', testingStatus: 'info' }); - getBackendSrv().withNoBackendCache(async () => { + backendSrv.withNoBackendCache(async () => { try { const result = await dsApi.testDatasource(); diff --git a/public/app/features/folders/CreateFolderCtrl.ts b/public/app/features/folders/CreateFolderCtrl.ts index 14aaeb26b3d..bc7d5f8fb47 100644 --- a/public/app/features/folders/CreateFolderCtrl.ts +++ b/public/app/features/folders/CreateFolderCtrl.ts @@ -1,10 +1,12 @@ +import { ILocationService, IScope } from 'angular'; +import { AppEvents } from '@grafana/data'; + import appEvents from 'app/core/app_events'; import locationUtil from 'app/core/utils/location_util'; -import { BackendSrv } from 'app/core/services/backend_srv'; -import { ILocationService } from 'angular'; +import { backendSrv } from 'app/core/services/backend_srv'; import { ValidationSrv } from 'app/features/manage-dashboards'; import { NavModelSrv } from 'app/core/nav_model_srv'; -import { AppEvents } from '@grafana/data'; +import { promiseToDigest } from '../../core/utils/promiseToDigest'; export default class CreateFolderCtrl { title = ''; @@ -15,10 +17,10 @@ export default class CreateFolderCtrl { /** @ngInject */ constructor( - private backendSrv: BackendSrv, private $location: ILocationService, private validationSrv: ValidationSrv, - navModelSrv: NavModelSrv + navModelSrv: NavModelSrv, + private $scope: IScope ) { this.navModel = navModelSrv.getNav('dashboards', 'manage-dashboards', 0); } @@ -28,23 +30,27 @@ export default class CreateFolderCtrl { return; } - return this.backendSrv.createFolder({ title: this.title }).then((result: any) => { - appEvents.emit(AppEvents.alertSuccess, ['Folder Created', 'OK']); - this.$location.url(locationUtil.stripBaseFromUrl(result.url)); - }); + promiseToDigest(this.$scope)( + backendSrv.createFolder({ title: this.title }).then((result: any) => { + appEvents.emit(AppEvents.alertSuccess, ['Folder Created', 'OK']); + this.$location.url(locationUtil.stripBaseFromUrl(result.url)); + }) + ); } titleChanged() { this.titleTouched = true; - this.validationSrv - .validateNewFolderName(this.title) - .then(() => { - this.hasValidationError = false; - }) - .catch(err => { - this.hasValidationError = true; - this.validationError = err.message; - }); + promiseToDigest(this.$scope)( + this.validationSrv + .validateNewFolderName(this.title) + .then(() => { + this.hasValidationError = false; + }) + .catch(err => { + this.hasValidationError = true; + this.validationError = err.message; + }) + ); } } diff --git a/public/app/features/folders/FolderDashboardsCtrl.ts b/public/app/features/folders/FolderDashboardsCtrl.ts index 619dced9fa1..96b2b9d6d5d 100644 --- a/public/app/features/folders/FolderDashboardsCtrl.ts +++ b/public/app/features/folders/FolderDashboardsCtrl.ts @@ -1,7 +1,9 @@ +import { ILocationService, IScope } from 'angular'; + import { FolderPageLoader } from './services/FolderPageLoader'; import locationUtil from 'app/core/utils/location_util'; import { NavModelSrv } from 'app/core/core'; -import { ILocationService } from 'angular'; +import { promiseToDigest } from '../../core/utils/promiseToDigest'; export default class FolderDashboardsCtrl { navModel: any; @@ -10,23 +12,25 @@ export default class FolderDashboardsCtrl { /** @ngInject */ constructor( - private backendSrv: any, navModelSrv: NavModelSrv, private $routeParams: any, - $location: ILocationService + $location: ILocationService, + private $scope: IScope ) { if (this.$routeParams.uid) { this.uid = $routeParams.uid; - const loader = new FolderPageLoader(this.backendSrv); + const loader = new FolderPageLoader(); - loader.load(this, this.uid, 'manage-folder-dashboards').then((folder: any) => { - const url = locationUtil.stripBaseFromUrl(folder.url); + promiseToDigest(this.$scope)( + loader.load(this, this.uid, 'manage-folder-dashboards').then((folder: any) => { + const url = locationUtil.stripBaseFromUrl(folder.url); - if (url !== $location.path()) { - $location.path(url).replace(); - } - }); + if (url !== $location.path()) { + $location.path(url).replace(); + } + }) + ); } } } diff --git a/public/app/features/folders/services/FolderPageLoader.ts b/public/app/features/folders/services/FolderPageLoader.ts index 3f353d0f63c..a53272f48b8 100644 --- a/public/app/features/folders/services/FolderPageLoader.ts +++ b/public/app/features/folders/services/FolderPageLoader.ts @@ -1,8 +1,6 @@ -import { BackendSrv } from 'app/core/services/backend_srv'; +import { backendSrv } from 'app/core/services/backend_srv'; export class FolderPageLoader { - constructor(private backendSrv: BackendSrv) {} - load(ctrl: any, uid: any, activeChildId: any) { ctrl.navModel = { main: { @@ -38,7 +36,7 @@ export class FolderPageLoader { }, }; - return this.backendSrv.getFolderByUid(uid).then((folder: any) => { + return backendSrv.getFolderByUid(uid).then((folder: any) => { ctrl.folderId = folder.id; const folderTitle = folder.title; const folderUrl = folder.url; diff --git a/public/app/features/folders/state/actions.ts b/public/app/features/folders/state/actions.ts index 3790a8cb771..188f6deb38a 100644 --- a/public/app/features/folders/state/actions.ts +++ b/public/app/features/folders/state/actions.ts @@ -1,6 +1,5 @@ import { AppEvents } from '@grafana/data'; - -import { getBackendSrv } from 'app/core/services/backend_srv'; +import { backendSrv } from 'app/core/services/backend_srv'; import { FolderState, ThunkResult } from 'app/types'; import { DashboardAcl, DashboardAclUpdateDTO, NewDashboardAclItem, PermissionLevel } from 'app/types/acl'; @@ -11,7 +10,7 @@ import { loadFolder, loadFolderPermissions } from './reducers'; export function getFolderByUid(uid: string): ThunkResult { return async dispatch => { - const folder = await getBackendSrv().getFolderByUid(uid); + const folder = await backendSrv.getFolderByUid(uid); dispatch(loadFolder(folder)); dispatch(updateNavIndex(buildNavModel(folder))); }; @@ -19,7 +18,7 @@ export function getFolderByUid(uid: string): ThunkResult { export function saveFolder(folder: FolderState): ThunkResult { return async dispatch => { - const res = await getBackendSrv().put(`/api/folders/${folder.uid}`, { + const res = await backendSrv.put(`/api/folders/${folder.uid}`, { title: folder.title, version: folder.version, }); @@ -33,14 +32,14 @@ export function saveFolder(folder: FolderState): ThunkResult { export function deleteFolder(uid: string): ThunkResult { return async dispatch => { - await getBackendSrv().deleteFolder(uid, true); + await backendSrv.deleteFolder(uid, true); dispatch(updateLocation({ path: `dashboards` })); }; } export function getFolderPermissions(uid: string): ThunkResult { return async dispatch => { - const permissions = await getBackendSrv().get(`/api/folders/${uid}/permissions`); + const permissions = await backendSrv.get(`/api/folders/${uid}/permissions`); dispatch(loadFolderPermissions(permissions)); }; } @@ -74,7 +73,7 @@ export function updateFolderPermission(itemToUpdate: DashboardAcl, level: Permis itemsToUpdate.push(updated); } - await getBackendSrv().post(`/api/folders/${folder.uid}/permissions`, { items: itemsToUpdate }); + await backendSrv.post(`/api/folders/${folder.uid}/permissions`, { items: itemsToUpdate }); await dispatch(getFolderPermissions(folder.uid)); }; } @@ -91,7 +90,7 @@ export function removeFolderPermission(itemToDelete: DashboardAcl): ThunkResult< itemsToUpdate.push(toUpdateItem(item)); } - await getBackendSrv().post(`/api/folders/${folder.uid}/permissions`, { items: itemsToUpdate }); + await backendSrv.post(`/api/folders/${folder.uid}/permissions`, { items: itemsToUpdate }); await dispatch(getFolderPermissions(folder.uid)); }; } @@ -115,7 +114,7 @@ export function addFolderPermission(newItem: NewDashboardAclItem): ThunkResult { const ctx: any = {}; + jest.spyOn(backendSrv, 'getDashboardByUid').mockImplementation(() => Promise.resolve([])); + jest.spyOn(backendSrv, 'search').mockImplementation(() => Promise.resolve([])); + const getMock = jest.spyOn(backendSrv, 'get'); let navModelSrv: any; - let backendSrv: any; let validationSrv: any; beforeEach(() => { @@ -13,17 +16,13 @@ describe('DashboardImportCtrl', () => { getNav: () => {}, }; - backendSrv = { - search: jest.fn().mockReturnValue(Promise.resolve([])), - getDashboardByUid: jest.fn().mockReturnValue(Promise.resolve([])), - get: jest.fn(), - }; - validationSrv = { validateNewDashboardName: jest.fn().mockReturnValue(Promise.resolve()), }; - ctx.ctrl = new DashboardImportCtrl(backendSrv, validationSrv, navModelSrv, {} as any, {} as any); + ctx.ctrl = new DashboardImportCtrl(validationSrv, navModelSrv, {} as any, {} as any); + + jest.clearAllMocks(); }); describe('when uploading json', () => { @@ -61,16 +60,12 @@ describe('DashboardImportCtrl', () => { beforeEach(() => { ctx.ctrl.gnetUrl = 'http://grafana.com/dashboards/123'; // setup api mock - backendSrv.get = jest.fn(() => { - return Promise.resolve({ - json: {}, - }); - }); + getMock.mockImplementation(() => Promise.resolve({ json: {} })); return ctx.ctrl.checkGnetDashboard(); }); it('should call gnet api with correct dashboard id', () => { - expect(backendSrv.get.mock.calls[0][0]).toBe('api/gnet/dashboards/123'); + expect(getMock.mock.calls[0][0]).toBe('api/gnet/dashboards/123'); }); }); @@ -78,16 +73,12 @@ describe('DashboardImportCtrl', () => { beforeEach(() => { ctx.ctrl.gnetUrl = '2342'; // setup api mock - backendSrv.get = jest.fn(() => { - return Promise.resolve({ - json: {}, - }); - }); + getMock.mockImplementation(() => Promise.resolve({ json: {} })); return ctx.ctrl.checkGnetDashboard(); }); it('should call gnet api with correct dashboard id', () => { - expect(backendSrv.get.mock.calls[0][0]).toBe('api/gnet/dashboards/2342'); + expect(getMock.mock.calls[0][0]).toBe('api/gnet/dashboards/2342'); }); }); }); diff --git a/public/app/features/manage-dashboards/DashboardImportCtrl.ts b/public/app/features/manage-dashboards/DashboardImportCtrl.ts index 55cc428535d..4aa7f74ee53 100644 --- a/public/app/features/manage-dashboards/DashboardImportCtrl.ts +++ b/public/app/features/manage-dashboards/DashboardImportCtrl.ts @@ -1,10 +1,10 @@ import _ from 'lodash'; import config from 'app/core/config'; import locationUtil from 'app/core/utils/location_util'; -import { BackendSrv } from '@grafana/runtime'; import { ValidationSrv } from './services/ValidationSrv'; import { NavModelSrv } from 'app/core/core'; import { ILocationService } from 'angular'; +import { backendSrv } from 'app/core/services/backend_srv'; export class DashboardImportCtrl { navModel: any; @@ -32,7 +32,6 @@ export class DashboardImportCtrl { /** @ngInject */ constructor( - private backendSrv: BackendSrv, private validationSrv: ValidationSrv, navModelSrv: NavModelSrv, private $location: ILocationService, @@ -145,7 +144,7 @@ export class DashboardImportCtrl { return; } - this.backendSrv + backendSrv // @ts-ignore .getDashboardByUid(this.dash.uid) .then((res: any) => { @@ -185,7 +184,7 @@ export class DashboardImportCtrl { }; }); - return this.backendSrv + return backendSrv .post('api/dashboards/import', { dashboard: this.dash, overwrite: true, @@ -224,7 +223,7 @@ export class DashboardImportCtrl { this.gnetError = 'Could not find dashboard'; } - return this.backendSrv + return backendSrv .get('api/gnet/dashboards/' + dashboardId) .then(res => { this.gnetInfo = res; diff --git a/public/app/features/manage-dashboards/SnapshotListCtrl.ts b/public/app/features/manage-dashboards/SnapshotListCtrl.ts index 67f9d38255f..707d2d0898a 100644 --- a/public/app/features/manage-dashboards/SnapshotListCtrl.ts +++ b/public/app/features/manage-dashboards/SnapshotListCtrl.ts @@ -1,9 +1,11 @@ import _ from 'lodash'; +import { ILocationService, IScope } from 'angular'; +import { getBackendSrv } from '@grafana/runtime'; + import { NavModelSrv } from 'app/core/core'; -import { ILocationService } from 'angular'; -import { BackendSrv } from '@grafana/runtime'; import { GrafanaRootScope } from 'app/routes/GrafanaCtrl'; import { CoreEvents } from 'app/types'; +import { promiseToDigest } from '../../core/utils/promiseToDigest'; export class SnapshotListCtrl { navModel: any; @@ -12,27 +14,35 @@ export class SnapshotListCtrl { /** @ngInject */ constructor( private $rootScope: GrafanaRootScope, - private backendSrv: BackendSrv, navModelSrv: NavModelSrv, - private $location: ILocationService + private $location: ILocationService, + private $scope: IScope ) { this.navModel = navModelSrv.getNav('dashboards', 'snapshots', 0); - this.backendSrv.get('/api/dashboard/snapshots').then((result: any) => { - const baseUrl = this.$location.absUrl().replace($location.url(), ''); - this.snapshots = result.map((snapshot: any) => ({ - ...snapshot, - url: snapshot.externalUrl || `${baseUrl}/dashboard/snapshot/${snapshot.key}`, - })); - }); + promiseToDigest(this.$scope)( + getBackendSrv() + .get('/api/dashboard/snapshots') + .then((result: any) => { + const baseUrl = this.$location.absUrl().replace($location.url(), ''); + this.snapshots = result.map((snapshot: any) => ({ + ...snapshot, + url: snapshot.externalUrl || `${baseUrl}/dashboard/snapshot/${snapshot.key}`, + })); + }) + ); } removeSnapshotConfirmed(snapshot: any) { _.remove(this.snapshots, { key: snapshot.key }); - this.backendSrv.delete('/api/snapshots/' + snapshot.key).then( - () => {}, - () => { - this.snapshots.push(snapshot); - } + promiseToDigest(this.$scope)( + getBackendSrv() + .delete('/api/snapshots/' + snapshot.key) + .then( + () => {}, + () => { + this.snapshots.push(snapshot); + } + ) ); } diff --git a/public/app/features/manage-dashboards/components/MoveToFolderModal/MoveToFolderCtrl.ts b/public/app/features/manage-dashboards/components/MoveToFolderModal/MoveToFolderCtrl.ts index f4c24e28100..7bbe094e938 100644 --- a/public/app/features/manage-dashboards/components/MoveToFolderModal/MoveToFolderCtrl.ts +++ b/public/app/features/manage-dashboards/components/MoveToFolderModal/MoveToFolderCtrl.ts @@ -1,6 +1,6 @@ import coreModule from 'app/core/core_module'; import appEvents from 'app/core/app_events'; -import { BackendSrv } from 'app/core/services/backend_srv'; +import { backendSrv } from 'app/core/services/backend_srv'; import { AppEvents } from '@grafana/data'; export class MoveToFolderCtrl { @@ -10,15 +10,12 @@ export class MoveToFolderCtrl { afterSave: any; isValidFolderSelection = true; - /** @ngInject */ - constructor(private backendSrv: BackendSrv) {} - onFolderChange(folder: any) { this.folder = folder; } save() { - return this.backendSrv.moveDashboards(this.dashboards, this.folder).then((result: any) => { + return backendSrv.moveDashboards(this.dashboards, this.folder).then((result: any) => { if (result.successCount > 0) { const header = `Dashboard${result.successCount === 1 ? '' : 's'} Moved`; const msg = `${result.successCount} dashboard${result.successCount === 1 ? '' : 's'} moved to ${ diff --git a/public/app/features/manage-dashboards/services/ValidationSrv.ts b/public/app/features/manage-dashboards/services/ValidationSrv.ts index 6f1477d53e1..da50c42aed5 100644 --- a/public/app/features/manage-dashboards/services/ValidationSrv.ts +++ b/public/app/features/manage-dashboards/services/ValidationSrv.ts @@ -1,5 +1,5 @@ import coreModule from 'app/core/core_module'; -import { BackendSrv } from 'app/core/services/backend_srv'; +import { backendSrv } from 'app/core/services/backend_srv'; const hitTypes = { FOLDER: 'dash-folder', @@ -9,9 +9,6 @@ const hitTypes = { export class ValidationSrv { rootName = 'general'; - /** @ngInject */ - constructor(private backendSrv: BackendSrv) {} - validateNewDashboardName(folderId: any, name: string) { return this.validate(folderId, name, 'A dashboard in this folder with the same name already exists'); } @@ -39,8 +36,8 @@ export class ValidationSrv { } const promises = []; - promises.push(this.backendSrv.search({ type: hitTypes.FOLDER, folderIds: [folderId], query: name })); - promises.push(this.backendSrv.search({ type: hitTypes.DASHBOARD, folderIds: [folderId], query: name })); + promises.push(backendSrv.search({ type: hitTypes.FOLDER, folderIds: [folderId], query: name })); + promises.push(backendSrv.search({ type: hitTypes.DASHBOARD, folderIds: [folderId], query: name })); return Promise.all(promises).then(res => { let hits: any[] = []; diff --git a/public/app/features/org/NewOrgCtrl.ts b/public/app/features/org/NewOrgCtrl.ts index 5d1a42c455e..0694873c819 100644 --- a/public/app/features/org/NewOrgCtrl.ts +++ b/public/app/features/org/NewOrgCtrl.ts @@ -1,20 +1,24 @@ import angular from 'angular'; import config from 'app/core/config'; -import { BackendSrv } from 'app/core/services/backend_srv'; +import { getBackendSrv } from '@grafana/runtime'; import { NavModelSrv } from 'app/core/core'; export class NewOrgCtrl { /** @ngInject */ - constructor($scope: any, $http: any, backendSrv: BackendSrv, navModelSrv: NavModelSrv) { + constructor($scope: any, $http: any, navModelSrv: NavModelSrv) { $scope.navModel = navModelSrv.getNav('admin', 'global-orgs', 0); $scope.newOrg = { name: '' }; $scope.createOrg = () => { - backendSrv.post('/api/orgs/', $scope.newOrg).then((result: any) => { - backendSrv.post('/api/user/using/' + result.orgId).then(() => { - window.location.href = config.appSubUrl + '/org'; + getBackendSrv() + .post('/api/orgs/', $scope.newOrg) + .then((result: any) => { + getBackendSrv() + .post('/api/user/using/' + result.orgId) + .then(() => { + window.location.href = config.appSubUrl + '/org'; + }); }); - }); }; } } diff --git a/public/app/features/org/SelectOrgCtrl.ts b/public/app/features/org/SelectOrgCtrl.ts index 5d56baf3152..b42aa33d0fe 100644 --- a/public/app/features/org/SelectOrgCtrl.ts +++ b/public/app/features/org/SelectOrgCtrl.ts @@ -1,10 +1,11 @@ import angular from 'angular'; import config from 'app/core/config'; -import { BackendSrv } from 'app/core/services/backend_srv'; +import { getBackendSrv } from '@grafana/runtime'; +import { promiseToDigest } from 'app/core/utils/promiseToDigest'; export class SelectOrgCtrl { /** @ngInject */ - constructor($scope: any, backendSrv: BackendSrv, contextSrv: any) { + constructor($scope: any, contextSrv: any) { contextSrv.sidemenu = false; $scope.navModel = { @@ -20,15 +21,21 @@ export class SelectOrgCtrl { }; $scope.getUserOrgs = () => { - backendSrv.get('/api/user/orgs').then((orgs: any) => { - $scope.orgs = orgs; - }); + promiseToDigest($scope)( + getBackendSrv() + .get('/api/user/orgs') + .then((orgs: any) => { + $scope.orgs = orgs; + }) + ); }; $scope.setUsingOrg = (org: any) => { - backendSrv.post('/api/user/using/' + org.orgId).then(() => { - window.location.href = config.appSubUrl + '/'; - }); + getBackendSrv() + .post('/api/user/using/' + org.orgId) + .then(() => { + window.location.href = config.appSubUrl + '/'; + }); }; $scope.init(); diff --git a/public/app/features/org/UserInviteCtrl.ts b/public/app/features/org/UserInviteCtrl.ts index d86a8716a3d..1cd4bca496c 100644 --- a/public/app/features/org/UserInviteCtrl.ts +++ b/public/app/features/org/UserInviteCtrl.ts @@ -1,7 +1,8 @@ import coreModule from 'app/core/core_module'; -import { BackendSrv } from 'app/core/services/backend_srv'; +import { getBackendSrv } from '@grafana/runtime'; import { NavModelSrv } from 'app/core/core'; -import { ILocationService } from 'angular'; +import { ILocationService, IScope } from 'angular'; +import { promiseToDigest } from 'app/core/utils/promiseToDigest'; export class UserInviteCtrl { navModel: any; @@ -9,7 +10,7 @@ export class UserInviteCtrl { inviteForm: any; /** @ngInject */ - constructor(private backendSrv: BackendSrv, navModelSrv: NavModelSrv, private $location: ILocationService) { + constructor(private $scope: IScope, navModelSrv: NavModelSrv, private $location: ILocationService) { this.navModel = navModelSrv.getNav('cfg', 'users', 0); this.invite = { @@ -25,9 +26,13 @@ export class UserInviteCtrl { return; } - return this.backendSrv.post('/api/org/invites', this.invite).then(() => { - this.$location.path('org/users/'); - }); + promiseToDigest(this.$scope)( + getBackendSrv() + .post('/api/org/invites', this.invite) + .then(() => { + this.$location.path('org/users/'); + }) + ); } } diff --git a/public/app/features/panel/panellinks/module.ts b/public/app/features/panel/panellinks/module.ts index 06ced91eb1d..384ac8d96d5 100644 --- a/public/app/features/panel/panellinks/module.ts +++ b/public/app/features/panel/panellinks/module.ts @@ -1,7 +1,7 @@ import angular from 'angular'; import _ from 'lodash'; import './link_srv'; -import { BackendSrv } from 'app/core/services/backend_srv'; +import { backendSrv } from 'app/core/services/backend_srv'; function panelLinksEditor() { return { @@ -17,7 +17,7 @@ function panelLinksEditor() { export class PanelLinksEditorCtrl { /** @ngInject */ - constructor($scope: any, backendSrv: BackendSrv) { + constructor($scope: any) { $scope.panel.links = $scope.panel.links || []; $scope.addLink = () => { diff --git a/public/app/features/playlist/playlist_edit_ctrl.ts b/public/app/features/playlist/playlist_edit_ctrl.ts index de77a51648b..e7fd1f42317 100644 --- a/public/app/features/playlist/playlist_edit_ctrl.ts +++ b/public/app/features/playlist/playlist_edit_ctrl.ts @@ -1,10 +1,11 @@ import _ from 'lodash'; import coreModule from '../../core/core_module'; -import { ILocationService } from 'angular'; -import { BackendSrv } from 'app/core/services/backend_srv'; +import { ILocationService, IScope } from 'angular'; +import { getBackendSrv } from '@grafana/runtime'; import { NavModelSrv } from 'app/core/nav_model_srv'; import { AppEventEmitter } from 'app/types'; import { AppEvents } from '@grafana/data'; +import { promiseToDigest } from '../../core/utils/promiseToDigest'; export interface PlaylistItem { value: any; @@ -29,8 +30,7 @@ export class PlaylistEditCtrl { /** @ngInject */ constructor( - private $scope: AppEventEmitter, - private backendSrv: BackendSrv, + private $scope: IScope & AppEventEmitter, private $location: ILocationService, $route: any, navModelSrv: NavModelSrv @@ -41,13 +41,21 @@ export class PlaylistEditCtrl { if ($route.current.params.id) { const playlistId = $route.current.params.id; - backendSrv.get('/api/playlists/' + playlistId).then((result: any) => { - this.playlist = result; - }); + promiseToDigest(this.$scope)( + getBackendSrv() + .get('/api/playlists/' + playlistId) + .then((result: any) => { + this.playlist = result; + }) + ); - backendSrv.get('/api/playlists/' + playlistId + '/items').then((result: any) => { - this.playlistItems = result; - }); + promiseToDigest(this.$scope)( + getBackendSrv() + .get('/api/playlists/' + playlistId + '/items') + .then((result: any) => { + this.playlistItems = result; + }) + ); } } @@ -99,8 +107,8 @@ export class PlaylistEditCtrl { playlist.items = playlistItems; savePromise = playlist.id - ? this.backendSrv.put('/api/playlists/' + playlist.id, playlist) - : this.backendSrv.post('/api/playlists', playlist); + ? promiseToDigest(this.$scope)(getBackendSrv().put('/api/playlists/' + playlist.id, playlist)) + : promiseToDigest(this.$scope)(getBackendSrv().post('/api/playlists', playlist)); savePromise.then( () => { diff --git a/public/app/features/playlist/playlist_search.ts b/public/app/features/playlist/playlist_search.ts index a9307df2442..931f0d44b23 100644 --- a/public/app/features/playlist/playlist_search.ts +++ b/public/app/features/playlist/playlist_search.ts @@ -1,5 +1,8 @@ +import { IScope, ITimeoutService } from 'angular'; + import coreModule from '../../core/core_module'; -import { BackendSrv } from 'app/core/services/backend_srv'; +import { backendSrv } from 'app/core/services/backend_srv'; +import { promiseToDigest } from '../../core/utils/promiseToDigest'; export class PlaylistSearchCtrl { query: any; @@ -8,7 +11,7 @@ export class PlaylistSearchCtrl { searchStarted: any; /** @ngInject */ - constructor($timeout: any, private backendSrv: BackendSrv) { + constructor(private $scope: IScope, $timeout: ITimeoutService) { this.query = { query: '', tag: [], starred: false, limit: 20 }; $timeout(() => { @@ -22,12 +25,14 @@ export class PlaylistSearchCtrl { this.tagsMode = false; const prom: any = {}; - prom.promise = this.backendSrv.search(this.query).then(result => { - return { - dashboardResult: result, - tagResult: [], - }; - }); + prom.promise = promiseToDigest(this.$scope)( + backendSrv.search(this.query).then(result => { + return { + dashboardResult: result, + tagResult: [], + }; + }) + ); this.searchStarted(prom); } @@ -52,12 +57,14 @@ export class PlaylistSearchCtrl { getTags() { const prom: any = {}; - prom.promise = this.backendSrv.get('/api/dashboards/tags').then((result: any) => { - return { - dashboardResult: [], - tagResult: result, - } as any; - }); + prom.promise = promiseToDigest(this.$scope)( + backendSrv.get('/api/dashboards/tags').then((result: any) => { + return { + dashboardResult: [], + tagResult: result, + } as any; + }) + ); this.searchStarted(prom); } diff --git a/public/app/features/playlist/playlist_srv.ts b/public/app/features/playlist/playlist_srv.ts index 01542823701..f15adecb09e 100644 --- a/public/app/features/playlist/playlist_srv.ts +++ b/public/app/features/playlist/playlist_srv.ts @@ -9,6 +9,7 @@ import locationUtil from 'app/core/utils/location_util'; import kbn from 'app/core/utils/kbn'; import { store } from 'app/store/store'; import { CoreEvents } from 'app/types'; +import { getBackendSrv } from '@grafana/runtime'; export const queryParamsToPreserve: { [key: string]: boolean } = { kiosk: true, @@ -28,7 +29,7 @@ export class PlaylistSrv { isPlaying: boolean; /** @ngInject */ - constructor(private $location: any, private $timeout: any, private backendSrv: any) {} + constructor(private $location: any, private $timeout: any) {} next() { this.$timeout.cancel(this.cancelPromise); @@ -89,13 +90,17 @@ export class PlaylistSrv { appEvents.emit(CoreEvents.playlistStarted); - return this.backendSrv.get(`/api/playlists/${playlistId}`).then((playlist: any) => { - return this.backendSrv.get(`/api/playlists/${playlistId}/dashboards`).then((dashboards: any) => { - this.dashboards = dashboards; - this.interval = kbn.interval_to_ms(playlist.interval); - this.next(); + return getBackendSrv() + .get(`/api/playlists/${playlistId}`) + .then((playlist: any) => { + return getBackendSrv() + .get(`/api/playlists/${playlistId}/dashboards`) + .then((dashboards: any) => { + this.dashboards = dashboards; + this.interval = kbn.interval_to_ms(playlist.interval); + this.next(); + }); }); - }); } stop() { diff --git a/public/app/features/playlist/playlists_ctrl.ts b/public/app/features/playlist/playlists_ctrl.ts index 19da03b2614..f3ce7b41c5a 100644 --- a/public/app/features/playlist/playlists_ctrl.ts +++ b/public/app/features/playlist/playlists_ctrl.ts @@ -1,37 +1,47 @@ +import { IScope } from 'angular'; import _ from 'lodash'; -import coreModule from '../../core/core_module'; -import { BackendSrv } from '@grafana/runtime'; -import { NavModelSrv } from 'app/core/nav_model_srv'; -import { CoreEvents } from 'app/types'; import { AppEvents } from '@grafana/data'; +import { getBackendSrv } from '@grafana/runtime'; + +import coreModule from '../../core/core_module'; +import { NavModelSrv } from 'app/core/nav_model_srv'; +import { AppEventEmitter, CoreEvents } from 'app/types'; +import { promiseToDigest } from '../../core/utils/promiseToDigest'; export class PlaylistsCtrl { playlists: any; navModel: any; /** @ngInject */ - constructor(private $scope: any, private backendSrv: BackendSrv, navModelSrv: NavModelSrv) { + constructor(private $scope: IScope & AppEventEmitter, navModelSrv: NavModelSrv) { this.navModel = navModelSrv.getNav('dashboards', 'playlists', 0); - - backendSrv.get('/api/playlists').then((result: any) => { - this.playlists = result.map((item: any) => { - item.startUrl = `playlists/play/${item.id}`; - return item; - }); - }); + promiseToDigest($scope)( + getBackendSrv() + .get('/api/playlists') + .then((result: any) => { + this.playlists = result.map((item: any) => { + item.startUrl = `playlists/play/${item.id}`; + return item; + }); + }) + ); } removePlaylistConfirmed(playlist: any) { _.remove(this.playlists, { id: playlist.id }); - this.backendSrv.delete('/api/playlists/' + playlist.id).then( - () => { - this.$scope.appEvent(AppEvents.alertSuccess, ['Playlist deleted']); - }, - () => { - this.$scope.appEvent(AppEvents.alertError, ['Unable to delete playlist']); - this.playlists.push(playlist); - } + promiseToDigest(this.$scope)( + getBackendSrv() + .delete('/api/playlists/' + playlist.id) + .then( + () => { + this.$scope.appEvent(AppEvents.alertSuccess, ['Playlist deleted']); + }, + () => { + this.$scope.appEvent(AppEvents.alertError, ['Unable to delete playlist']); + this.playlists.push(playlist); + } + ) ); } diff --git a/public/app/features/playlist/specs/playlist_edit_ctrl.test.ts b/public/app/features/playlist/specs/playlist_edit_ctrl.test.ts index 11554fa5bd2..18ef61d78a1 100644 --- a/public/app/features/playlist/specs/playlist_edit_ctrl.test.ts +++ b/public/app/features/playlist/specs/playlist_edit_ctrl.test.ts @@ -10,7 +10,7 @@ describe('PlaylistEditCtrl', () => { }, }; - ctx = new PlaylistEditCtrl(null, null, null, { current: { params: {} } }, navModelSrv); + ctx = new PlaylistEditCtrl(null, null, { current: { params: {} } }, navModelSrv); ctx.dashboardresult = [ { id: 2, title: 'dashboard: 2' }, diff --git a/public/app/features/playlist/specs/playlist_srv.test.ts b/public/app/features/playlist/specs/playlist_srv.test.ts index a62a921e0f0..570e99d2a78 100644 --- a/public/app/features/playlist/specs/playlist_srv.test.ts +++ b/public/app/features/playlist/specs/playlist_srv.test.ts @@ -3,6 +3,18 @@ import configureMockStore from 'redux-mock-store'; import { PlaylistSrv } from '../playlist_srv'; import { setStore } from 'app/store/store'; +const getMock = jest.fn(); + +jest.mock('@grafana/runtime', () => { + const original = jest.requireActual('@grafana/runtime'); + return { + ...original, + getBackendSrv: () => ({ + get: getMock, + }), + }; +}); + const mockStore = configureMockStore(); setStore( @@ -14,19 +26,6 @@ setStore( const dashboards = [{ url: 'dash1' }, { url: 'dash2' }]; const createPlaylistSrv = (): [PlaylistSrv, { url: jest.MockInstance }] => { - const mockBackendSrv = { - get: jest.fn(url => { - switch (url) { - case '/api/playlists/1': - return Promise.resolve({ interval: '1s' }); - case '/api/playlists/1/dashboards': - return Promise.resolve(dashboards); - default: - throw new Error(`Unexpected url=${url}`); - } - }), - }; - const mockLocation = { url: jest.fn(), search: () => ({}), @@ -36,7 +35,7 @@ const createPlaylistSrv = (): [PlaylistSrv, { url: jest.MockInstance } const mockTimeout = jest.fn(); (mockTimeout as any).cancel = jest.fn(); - return [new PlaylistSrv(mockLocation, mockTimeout, mockBackendSrv), mockLocation]; + return [new PlaylistSrv(mockLocation, mockTimeout), mockLocation]; }; const mockWindowLocation = (): [jest.MockInstance, () => void] => { @@ -67,6 +66,20 @@ describe('PlaylistSrv', () => { const initialUrl = 'http://localhost/playlist'; beforeEach(() => { + jest.clearAllMocks(); + getMock.mockImplementation( + jest.fn(url => { + switch (url) { + case '/api/playlists/1': + return Promise.resolve({ interval: '1s' }); + case '/api/playlists/1/dashboards': + return Promise.resolve(dashboards); + default: + throw new Error(`Unexpected url=${url}`); + } + }) + ); + [srv] = createPlaylistSrv(); [hrefMock, unmockLocation] = mockWindowLocation(); diff --git a/public/app/features/plugins/PluginDashboards.tsx b/public/app/features/plugins/PluginDashboards.tsx index ab9e6b8d7ce..5dbebc5c7a6 100644 --- a/public/app/features/plugins/PluginDashboards.tsx +++ b/public/app/features/plugins/PluginDashboards.tsx @@ -3,7 +3,7 @@ import React, { PureComponent } from 'react'; import extend from 'lodash/extend'; import { PluginDashboard } from 'app/types'; -import { getBackendSrv } from 'app/core/services/backend_srv'; +import { getBackendSrv } from '@grafana/runtime'; import { appEvents } from 'app/core/core'; import DashboardsTable from 'app/features/datasources/DashboardsTable'; import { AppEvents, PluginMeta, DataSourceApi } from '@grafana/data'; diff --git a/public/app/features/plugins/PluginSettingsCache.ts b/public/app/features/plugins/PluginSettingsCache.ts index b3a295bbda8..1d285f1cde3 100644 --- a/public/app/features/plugins/PluginSettingsCache.ts +++ b/public/app/features/plugins/PluginSettingsCache.ts @@ -1,4 +1,4 @@ -import { getBackendSrv } from 'app/core/services/backend_srv'; +import { getBackendSrv } from '@grafana/runtime'; import { PluginMeta } from '@grafana/data'; type PluginCache = { diff --git a/public/app/features/plugins/datasource_srv.ts b/public/app/features/plugins/datasource_srv.ts index 2546b76134e..d6cd3ee0c75 100644 --- a/public/app/features/plugins/datasource_srv.ts +++ b/public/app/features/plugins/datasource_srv.ts @@ -1,5 +1,5 @@ // Libraries -import _ from 'lodash'; +import sortBy from 'lodash/sortBy'; import coreModule from 'app/core/core_module'; // Services & Utils @@ -17,7 +17,7 @@ import { GrafanaRootScope } from 'app/routes/GrafanaCtrl'; import { expressionDatasource } from 'app/features/expressions/ExpressionDatasource'; export class DatasourceSrv implements DataSourceService { - datasources: Record; + datasources: Record = {}; /** @ngInject */ constructor( @@ -38,7 +38,7 @@ export class DatasourceSrv implements DataSourceService { } // Interpolation here is to support template variable in data source selection - name = this.templateSrv.replace(name, scopedVars, (value: any[], variable: any) => { + name = this.templateSrv.replace(name, scopedVars, (value: any[]) => { if (Array.isArray(value)) { return value[0]; } @@ -56,7 +56,7 @@ export class DatasourceSrv implements DataSourceService { return this.loadDatasource(name); } - loadDatasource(name: string): Promise> { + async loadDatasource(name: string): Promise> { // Expression Datasource (not a real datasource) if (name === expressionDatasource.name) { this.datasources[name] = expressionDatasource as any; @@ -68,32 +68,31 @@ export class DatasourceSrv implements DataSourceService { return Promise.reject({ message: `Datasource named ${name} was not found` }); } - return importDataSourcePlugin(dsConfig.meta) - .then(dsPlugin => { - // check if its in cache now - if (this.datasources[name]) { - return this.datasources[name]; - } + try { + const dsPlugin = await importDataSourcePlugin(dsConfig.meta); + // check if its in cache now + if (this.datasources[name]) { + return this.datasources[name]; + } - // If there is only one constructor argument it is instanceSettings - const useAngular = dsPlugin.DataSourceClass.length !== 1; - const instance: DataSourceApi = useAngular - ? this.$injector.instantiate(dsPlugin.DataSourceClass, { - instanceSettings: dsConfig, - }) - : new dsPlugin.DataSourceClass(dsConfig); + // If there is only one constructor argument it is instanceSettings + const useAngular = dsPlugin.DataSourceClass.length !== 1; + const instance: DataSourceApi = useAngular + ? this.$injector.instantiate(dsPlugin.DataSourceClass, { + instanceSettings: dsConfig, + }) + : new dsPlugin.DataSourceClass(dsConfig); - instance.components = dsPlugin.components; - instance.meta = dsConfig.meta; + instance.components = dsPlugin.components; + instance.meta = dsConfig.meta; - // store in instance cache - this.datasources[name] = instance; - return instance; - }) - .catch(err => { - this.$rootScope.appEvent(AppEvents.alertError, [dsConfig.name + ' plugin failed', err.toString()]); - return undefined; - }); + // store in instance cache + this.datasources[name] = instance; + return instance; + } catch (err) { + this.$rootScope.appEvent(AppEvents.alertError, [dsConfig.name + ' plugin failed', err.toString()]); + return Promise.reject({ message: `Datasource named ${name} was not found` }); + } } getAll() { @@ -103,7 +102,7 @@ export class DatasourceSrv implements DataSourceService { getExternal() { const datasources = this.getAll().filter(ds => !ds.meta.builtIn); - return _.sortBy(datasources, ['name']); + return sortBy(datasources, ['name']); } getAnnotationSources() { @@ -111,8 +110,8 @@ export class DatasourceSrv implements DataSourceService { this.addDataSourceVariables(sources); - _.each(config.datasources, value => { - if (value.meta && value.meta.annotations) { + Object.values(config.datasources).forEach(value => { + if (value.meta?.annotations) { sources.push(value); } }); @@ -123,8 +122,8 @@ export class DatasourceSrv implements DataSourceService { getMetricSources(options?: { skipVariables?: boolean }) { const metricSources: DataSourceSelectItem[] = []; - _.each(config.datasources, (value, key) => { - if (value.meta && value.meta.metrics) { + Object.entries(config.datasources).forEach(([key, value]) => { + if (value.meta?.metrics) { let metricSource = { value: key, name: key, meta: value.meta, sort: key }; //Make sure grafana and mixed are sorted at the bottom @@ -164,29 +163,22 @@ export class DatasourceSrv implements DataSourceService { addDataSourceVariables(list: any[]) { // look for data source variables - for (let i = 0; i < this.templateSrv.variables.length; i++) { - const variable = this.templateSrv.variables[i]; - if (variable.type !== 'datasource') { - continue; - } + this.templateSrv.variables + .filter(variable => variable.type === 'datasource') + .forEach(variable => { + const first = variable.current.value === 'default' ? config.defaultDatasource : variable.current.value; + const ds = config.datasources[first]; - let first = variable.current.value; - if (first === 'default') { - first = config.defaultDatasource; - } - - const ds = config.datasources[first]; - - if (ds) { - const key = `$${variable.name}`; - list.push({ - name: key, - value: key, - meta: ds.meta, - sort: key, - }); - } - } + if (ds) { + const key = `$${variable.name}`; + list.push({ + name: key, + value: key, + meta: ds.meta, + sort: key, + }); + } + }); } } diff --git a/public/app/features/plugins/wrappers/AppConfigWrapper.tsx b/public/app/features/plugins/wrappers/AppConfigWrapper.tsx index 67ecf02b33b..0e01c60e3f9 100644 --- a/public/app/features/plugins/wrappers/AppConfigWrapper.tsx +++ b/public/app/features/plugins/wrappers/AppConfigWrapper.tsx @@ -5,9 +5,8 @@ import extend from 'lodash/extend'; import { Button } from '@grafana/ui'; import { PluginMeta, AppPlugin, deprecationWarning } from '@grafana/data'; +import { AngularComponent, getAngularLoader, getBackendSrv } from '@grafana/runtime'; -import { AngularComponent, getAngularLoader } from '@grafana/runtime'; -import { getBackendSrv } from 'app/core/services/backend_srv'; import { css } from 'emotion'; interface Props { diff --git a/public/app/features/profile/ProfileCtrl.ts b/public/app/features/profile/ProfileCtrl.ts index 87fb073d00f..6193735e7bd 100644 --- a/public/app/features/profile/ProfileCtrl.ts +++ b/public/app/features/profile/ProfileCtrl.ts @@ -1,62 +1,70 @@ import { coreModule, NavModelSrv } from 'app/core/core'; import { dateTime } from '@grafana/data'; import { UserSession } from 'app/types'; -import { BackendSrv } from 'app/core/services/backend_srv'; +import { getBackendSrv } from '@grafana/runtime'; +import { promiseToDigest } from 'app/core/utils/promiseToDigest'; +import { IScope } from 'angular'; export class ProfileCtrl { sessions: object[] = []; navModel: any; /** @ngInject */ - constructor(private backendSrv: BackendSrv, navModelSrv: NavModelSrv) { + constructor(private $scope: IScope, navModelSrv: NavModelSrv) { this.getUserSessions(); this.navModel = navModelSrv.getNav('profile', 'profile-settings', 0); } getUserSessions() { - this.backendSrv.get('/api/user/auth-tokens').then((sessions: UserSession[]) => { - sessions.reverse(); + promiseToDigest(this.$scope)( + getBackendSrv() + .get('/api/user/auth-tokens') + .then((sessions: UserSession[]) => { + sessions.reverse(); - const found = sessions.findIndex((session: UserSession) => { - return session.isActive; - }); + const found = sessions.findIndex((session: UserSession) => { + return session.isActive; + }); - if (found) { - const now = sessions[found]; - sessions.splice(found, found); - sessions.unshift(now); - } + if (found) { + const now = sessions[found]; + sessions.splice(found, found); + sessions.unshift(now); + } - this.sessions = sessions.map((session: UserSession) => { - return { - id: session.id, - isActive: session.isActive, - seenAt: dateTime(session.seenAt).fromNow(), - createdAt: dateTime(session.createdAt).format('MMMM DD, YYYY'), - clientIp: session.clientIp, - browser: session.browser, - browserVersion: session.browserVersion, - os: session.os, - osVersion: session.osVersion, - device: session.device, - }; - }); - }); + this.sessions = sessions.map((session: UserSession) => { + return { + id: session.id, + isActive: session.isActive, + seenAt: dateTime(session.seenAt).fromNow(), + createdAt: dateTime(session.createdAt).format('MMMM DD, YYYY'), + clientIp: session.clientIp, + browser: session.browser, + browserVersion: session.browserVersion, + os: session.os, + osVersion: session.osVersion, + device: session.device, + }; + }); + }) + ); } revokeUserSession(tokenId: number) { - this.backendSrv - .post('/api/user/revoke-auth-token', { - authTokenId: tokenId, - }) - .then(() => { - this.sessions = this.sessions.filter((session: UserSession) => { - if (session.id === tokenId) { - return false; - } - return true; - }); - }); + promiseToDigest(this.$scope)( + getBackendSrv() + .post('/api/user/revoke-auth-token', { + authTokenId: tokenId, + }) + .then(() => { + this.sessions = this.sessions.filter((session: UserSession) => { + if (session.id === tokenId) { + return false; + } + return true; + }); + }) + ); } } diff --git a/public/app/features/templating/all.ts b/public/app/features/templating/all.ts index b872fa6cd4a..9431c3d036c 100644 --- a/public/app/features/templating/all.ts +++ b/public/app/features/templating/all.ts @@ -11,9 +11,7 @@ import { ConstantVariable } from './constant_variable'; import { AdhocVariable } from './adhoc_variable'; import { TextBoxVariable } from './TextBoxVariable'; -coreModule.factory('templateSrv', () => { - return templateSrv; -}); +coreModule.factory('templateSrv', () => templateSrv); export { VariableSrv, diff --git a/public/app/plugins/datasource/cloudwatch/components/QueryEditor.test.tsx b/public/app/plugins/datasource/cloudwatch/components/QueryEditor.test.tsx index 08e594edbc6..cb411da9625 100644 --- a/public/app/plugins/datasource/cloudwatch/components/QueryEditor.test.tsx +++ b/public/app/plugins/datasource/cloudwatch/components/QueryEditor.test.tsx @@ -31,7 +31,7 @@ const setup = () => { ), ]); - const datasource = new CloudWatchDatasource(instanceSettings, {} as any, templateSrv as any, {} as any); + const datasource = new CloudWatchDatasource(instanceSettings, templateSrv as any, {} as any); datasource.metricFindQuery = async () => [{ value: 'test', label: 'test' }]; const props: Props = { diff --git a/public/app/plugins/datasource/cloudwatch/datasource.ts b/public/app/plugins/datasource/cloudwatch/datasource.ts index 2eeb5b06c80..23dadaecaa7 100644 --- a/public/app/plugins/datasource/cloudwatch/datasource.ts +++ b/public/app/plugins/datasource/cloudwatch/datasource.ts @@ -15,7 +15,7 @@ import { DataQueryRequest, DataSourceInstanceSettings, } from '@grafana/data'; -import { BackendSrv } from 'app/core/services/backend_srv'; +import { getBackendSrv } from '@grafana/runtime'; import { TemplateSrv } from 'app/features/templating/template_srv'; import { TimeSrv } from 'app/features/dashboard/services/TimeSrv'; import { ThrottlingErrorMessage } from './components/ThrottlingErrorMessage'; @@ -48,7 +48,6 @@ export default class CloudWatchDatasource extends DataSourceApi, - private backendSrv: BackendSrv, private templateSrv: TemplateSrv, private timeSrv: TimeSrv ) { @@ -193,7 +192,7 @@ export default class CloudWatchDatasource extends DataSourceApi { return this.awsRequest('/api/tsdb/query', request) .then((res: any) => { if (!res.results) { @@ -530,9 +529,11 @@ export default class CloudWatchDatasource extends DataSourceApi { - return result.data; - }); + return getBackendSrv() + .datasourceRequest(options) + .then((result: any) => { + return result.data; + }); } getDefaultRegion() { diff --git a/public/app/plugins/datasource/cloudwatch/specs/datasource.test.ts b/public/app/plugins/datasource/cloudwatch/specs/datasource.test.ts index bee6ae28763..bff7304c9ae 100644 --- a/public/app/plugins/datasource/cloudwatch/specs/datasource.test.ts +++ b/public/app/plugins/datasource/cloudwatch/specs/datasource.test.ts @@ -7,10 +7,17 @@ import { CustomVariable } from 'app/features/templating/all'; import _ from 'lodash'; import { CloudWatchQuery } from '../types'; import { DataSourceInstanceSettings } from '@grafana/data'; -import { BackendSrv } from 'app/core/services/backend_srv'; +import { backendSrv } from 'app/core/services/backend_srv'; // will use the version in __mocks__ import { TimeSrv } from 'app/features/dashboard/services/TimeSrv'; +jest.mock('@grafana/runtime', () => ({ + ...jest.requireActual('@grafana/runtime'), + getBackendSrv: () => backendSrv, +})); + describe('CloudWatchDatasource', () => { + const datasourceRequestMock = jest.spyOn(backendSrv, 'datasourceRequest'); + const instanceSettings = { jsonData: { defaultRegion: 'us-east-1' }, name: 'TestDatasource', @@ -29,14 +36,14 @@ describe('CloudWatchDatasource', () => { }; }, } as TimeSrv; - const backendSrv = {} as BackendSrv; + const ctx = { - backendSrv, templateSrv, } as any; beforeEach(() => { - ctx.ds = new CloudWatchDatasource(instanceSettings, backendSrv, templateSrv, timeSrv); + ctx.ds = new CloudWatchDatasource(instanceSettings, templateSrv, timeSrv); + jest.clearAllMocks(); }); describe('When performing CloudWatch query', () => { @@ -86,7 +93,7 @@ describe('CloudWatchDatasource', () => { }; beforeEach(() => { - ctx.backendSrv.datasourceRequest = jest.fn(params => { + datasourceRequestMock.mockImplementation(params => { requestParams = params.data; return Promise.resolve({ data: response }); }); @@ -174,7 +181,7 @@ describe('CloudWatchDatasource', () => { describe('a correct cloudwatch url should be built for each time series in the response', () => { beforeEach(() => { - ctx.backendSrv.datasourceRequest = jest.fn(params => { + datasourceRequestMock.mockImplementation(params => { requestParams = params.data; return Promise.resolve({ data: response }); }); @@ -291,7 +298,7 @@ describe('CloudWatchDatasource', () => { dispatch: jest.fn(), } as any); - ctx.backendSrv.datasourceRequest = jest.fn(() => { + datasourceRequestMock.mockImplementation(() => { return Promise.reject(backendErrorResponse); }); }); @@ -310,10 +317,10 @@ describe('CloudWatchDatasource', () => { describe('when regions query is used', () => { beforeEach(() => { - ctx.backendSrv.datasourceRequest = jest.fn(() => { + datasourceRequestMock.mockImplementation(() => { return Promise.resolve({}); }); - ctx.ds = new CloudWatchDatasource(instanceSettings, backendSrv, templateSrv, timeSrv); + ctx.ds = new CloudWatchDatasource(instanceSettings, templateSrv, timeSrv); ctx.ds.doMetricQueryRequest = jest.fn(() => []); }); describe('and region param is left out', () => { @@ -441,7 +448,7 @@ describe('CloudWatchDatasource', () => { }; beforeEach(() => { - ctx.backendSrv.datasourceRequest = jest.fn(params => { + datasourceRequestMock.mockImplementation(params => { return Promise.resolve({ data: response }); }); }); @@ -511,7 +518,7 @@ describe('CloudWatchDatasource', () => { ), ]); - ctx.backendSrv.datasourceRequest = jest.fn(params => { + datasourceRequestMock.mockImplementation(params => { requestParams = params.data; return Promise.resolve({ data: {} }); }); @@ -643,7 +650,7 @@ describe('CloudWatchDatasource', () => { scenario.setup = async (setupCallback: any) => { beforeEach(async () => { await setupCallback(); - ctx.backendSrv.datasourceRequest = jest.fn(args => { + datasourceRequestMock.mockImplementation(args => { scenario.request = args.data; return Promise.resolve({ data: scenario.requestResponse }); }); diff --git a/public/app/plugins/datasource/elasticsearch/datasource.test.ts b/public/app/plugins/datasource/elasticsearch/datasource.test.ts index 3a8f6151d51..b2dad0c81e2 100644 --- a/public/app/plugins/datasource/elasticsearch/datasource.test.ts +++ b/public/app/plugins/datasource/elasticsearch/datasource.test.ts @@ -3,16 +3,23 @@ import { dateMath, Field } from '@grafana/data'; import _ from 'lodash'; import { ElasticDatasource } from './datasource'; import { toUtc, dateTime } from '@grafana/data'; -import { BackendSrv } from 'app/core/services/backend_srv'; +import { backendSrv } from 'app/core/services/backend_srv'; // will use the version in __mocks__ import { TimeSrv } from 'app/features/dashboard/services/TimeSrv'; import { TemplateSrv } from 'app/features/templating/template_srv'; import { DataSourceInstanceSettings } from '@grafana/data'; import { ElasticsearchOptions } from './types'; +jest.mock('@grafana/runtime', () => ({ + ...jest.requireActual('@grafana/runtime'), + getBackendSrv: () => backendSrv, +})); + describe('ElasticDatasource', function(this: any) { - const backendSrv: any = { - datasourceRequest: jest.fn(), - }; + const datasourceRequestMock = jest.spyOn(backendSrv, 'datasourceRequest'); + + beforeEach(() => { + jest.clearAllMocks(); + }); const $rootScope = { $on: jest.fn(), @@ -45,17 +52,11 @@ describe('ElasticDatasource', function(this: any) { const ctx = { $rootScope, - backendSrv, } as any; function createDatasource(instanceSettings: DataSourceInstanceSettings) { instanceSettings.jsonData = instanceSettings.jsonData || ({} as ElasticsearchOptions); - ctx.ds = new ElasticDatasource( - instanceSettings, - backendSrv as BackendSrv, - templateSrv as TemplateSrv, - timeSrv as TimeSrv - ); + ctx.ds = new ElasticDatasource(instanceSettings, templateSrv as TemplateSrv, timeSrv as TimeSrv); } describe('When testing datasource with index pattern', () => { @@ -69,7 +70,7 @@ describe('ElasticDatasource', function(this: any) { it('should translate index pattern to current day', () => { let requestOptions: any; - ctx.backendSrv.datasourceRequest = jest.fn(options => { + datasourceRequestMock.mockImplementation(options => { requestOptions = options; return Promise.resolve({ data: {} }); }); @@ -91,7 +92,7 @@ describe('ElasticDatasource', function(this: any) { jsonData: { interval: 'Daily', esVersion: 2 } as ElasticsearchOptions, } as DataSourceInstanceSettings); - ctx.backendSrv.datasourceRequest = jest.fn(options => { + datasourceRequestMock.mockImplementation(options => { requestOptions = options; return Promise.resolve({ data: { @@ -165,7 +166,7 @@ describe('ElasticDatasource', function(this: any) { } as ElasticsearchOptions, } as DataSourceInstanceSettings); - ctx.backendSrv.datasourceRequest = jest.fn(options => { + datasourceRequestMock.mockImplementation(options => { return Promise.resolve(logsResponse); }); @@ -225,7 +226,7 @@ describe('ElasticDatasource', function(this: any) { jsonData: { esVersion: 2 } as ElasticsearchOptions, } as DataSourceInstanceSettings); - ctx.backendSrv.datasourceRequest = jest.fn(options => { + datasourceRequestMock.mockImplementation(options => { requestOptions = options; return Promise.resolve({ data: { responses: [] } }); }); @@ -266,7 +267,7 @@ describe('ElasticDatasource', function(this: any) { jsonData: { esVersion: 50 } as ElasticsearchOptions, } as DataSourceInstanceSettings); - ctx.backendSrv.datasourceRequest = jest.fn(options => { + datasourceRequestMock.mockImplementation(options => { return Promise.resolve({ data: { metricbeat: { @@ -362,7 +363,7 @@ describe('ElasticDatasource', function(this: any) { jsonData: { esVersion: 70 } as ElasticsearchOptions, } as DataSourceInstanceSettings); - ctx.backendSrv.datasourceRequest = jest.fn(options => { + datasourceRequestMock.mockImplementation(options => { return Promise.resolve({ data: { 'genuine.es7._mapping.response': { @@ -515,7 +516,7 @@ describe('ElasticDatasource', function(this: any) { jsonData: { esVersion: 5 } as ElasticsearchOptions, } as DataSourceInstanceSettings); - ctx.backendSrv.datasourceRequest = jest.fn(options => { + datasourceRequestMock.mockImplementation(options => { requestOptions = options; return Promise.resolve({ data: { responses: [] } }); }); @@ -558,7 +559,7 @@ describe('ElasticDatasource', function(this: any) { jsonData: { esVersion: 5 } as ElasticsearchOptions, } as DataSourceInstanceSettings); - ctx.backendSrv.datasourceRequest = jest.fn(options => { + datasourceRequestMock.mockImplementation(options => { requestOptions = options; return Promise.resolve({ data: { diff --git a/public/app/plugins/datasource/elasticsearch/datasource.ts b/public/app/plugins/datasource/elasticsearch/datasource.ts index f89a61b3546..dc52713a88a 100644 --- a/public/app/plugins/datasource/elasticsearch/datasource.ts +++ b/public/app/plugins/datasource/elasticsearch/datasource.ts @@ -12,7 +12,7 @@ import { IndexPattern } from './index_pattern'; import { ElasticQueryBuilder } from './query_builder'; import { toUtc } from '@grafana/data'; import * as queryDef from './query_def'; -import { BackendSrv } from 'app/core/services/backend_srv'; +import { getBackendSrv } from '@grafana/runtime'; import { TemplateSrv } from 'app/features/templating/template_srv'; import { TimeSrv } from 'app/features/dashboard/services/TimeSrv'; import { DataLinkConfig, ElasticsearchOptions, ElasticsearchQuery } from './types'; @@ -36,7 +36,6 @@ export class ElasticDatasource extends DataSourceApi, - private backendSrv: BackendSrv, private templateSrv: TemplateSrv, private timeSrv: TimeSrv ) { @@ -86,7 +85,7 @@ export class ElasticDatasource extends DataSourceApi { const annotation = options.annotation; const timeField = annotation.timeField || '@timestamp'; const timeEndField = annotation.timeEndField || null; @@ -511,20 +510,20 @@ export class ElasticDatasource extends DataSourceApi ({ + ...jest.requireActual('@grafana/runtime'), + getBackendSrv: () => backendSrv, +})); describe('AppInsightsDatasource', () => { + const datasourceRequestMock = jest.spyOn(backendSrv, 'datasourceRequest'); + const ctx: any = { - backendSrv: {}, templateSrv: new TemplateSrv(), }; beforeEach(() => { + jest.clearAllMocks(); ctx.instanceSettings = { jsonData: { appInsightsAppId: '3ad4400f-ea7d-465d-a8fb-43fb20555d85' }, url: 'http://appinsightsapi', }; - ctx.ds = new Datasource(ctx.instanceSettings, ctx.backendSrv, ctx.templateSrv); + ctx.ds = new Datasource(ctx.instanceSettings, ctx.templateSrv); }); describe('When performing testDatasource', () => { @@ -38,9 +46,9 @@ describe('AppInsightsDatasource', () => { }; beforeEach(() => { - ctx.backendSrv.datasourceRequest = () => { + datasourceRequestMock.mockImplementation(() => { return Promise.resolve({ data: response, status: 200 }); - }; + }); }); it('should return success status', () => { @@ -63,9 +71,9 @@ describe('AppInsightsDatasource', () => { }; beforeEach(() => { - ctx.backendSrv.datasourceRequest = () => { + datasourceRequestMock.mockImplementation(() => { return Promise.reject(error); - }; + }); }); it('should return error status and a detailed error message', () => { @@ -91,9 +99,9 @@ describe('AppInsightsDatasource', () => { }; beforeEach(() => { - ctx.backendSrv.datasourceRequest = () => { + datasourceRequestMock.mockImplementation(() => { return Promise.reject(error); - }; + }); }); it('should return error status and a detailed error message', () => { @@ -151,7 +159,7 @@ describe('AppInsightsDatasource', () => { }; beforeEach(() => { - ctx.backendSrv.datasourceRequest = (options: any) => { + datasourceRequestMock.mockImplementation((options: any) => { expect(options.url).toContain('/api/tsdb/query'); expect(options.data.queries.length).toBe(1); expect(options.data.queries[0].refId).toBe('A'); @@ -160,7 +168,7 @@ describe('AppInsightsDatasource', () => { expect(options.data.queries[0].appInsights.valueColumn).toEqual('max'); expect(options.data.queries[0].appInsights.segmentColumn).toBeUndefined(); return Promise.resolve({ data: response, status: 200 }); - }; + }); }); it('should return a list of datapoints', () => { @@ -194,7 +202,7 @@ describe('AppInsightsDatasource', () => { beforeEach(() => { options.targets[0].appInsights.segmentColumn = 'partition'; - ctx.backendSrv.datasourceRequest = (options: any) => { + datasourceRequestMock.mockImplementation((options: any) => { expect(options.url).toContain('/api/tsdb/query'); expect(options.data.queries.length).toBe(1); expect(options.data.queries[0].refId).toBe('A'); @@ -203,7 +211,7 @@ describe('AppInsightsDatasource', () => { expect(options.data.queries[0].appInsights.valueColumn).toEqual('max'); expect(options.data.queries[0].appInsights.segmentColumn).toEqual('partition'); return Promise.resolve({ data: response, status: 200 }); - }; + }); }); it('should return a list of datapoints', () => { @@ -257,14 +265,14 @@ describe('AppInsightsDatasource', () => { }; beforeEach(() => { - ctx.backendSrv.datasourceRequest = (options: any) => { + datasourceRequestMock.mockImplementation((options: any) => { expect(options.url).toContain('/api/tsdb/query'); expect(options.data.queries.length).toBe(1); expect(options.data.queries[0].refId).toBe('A'); expect(options.data.queries[0].appInsights.rawQueryString).toBeUndefined(); expect(options.data.queries[0].appInsights.metricName).toBe('exceptions/server'); return Promise.resolve({ data: response, status: 200 }); - }; + }); }); it('should return a single datapoint', () => { @@ -300,14 +308,14 @@ describe('AppInsightsDatasource', () => { beforeEach(() => { options.targets[0].appInsights.timeGrain = 'PT30M'; - ctx.backendSrv.datasourceRequest = (options: any) => { + datasourceRequestMock.mockImplementation((options: any) => { expect(options.url).toContain('/api/tsdb/query'); expect(options.data.queries[0].refId).toBe('A'); expect(options.data.queries[0].appInsights.rawQueryString).toBeUndefined(); expect(options.data.queries[0].appInsights.metricName).toBe('exceptions/server'); expect(options.data.queries[0].appInsights.timeGrain).toBe('PT30M'); return Promise.resolve({ data: response, status: 200 }); - }; + }); }); it('should return a list of datapoints', () => { @@ -355,13 +363,13 @@ describe('AppInsightsDatasource', () => { beforeEach(() => { options.targets[0].appInsights.dimension = 'client/city'; - ctx.backendSrv.datasourceRequest = (options: any) => { + datasourceRequestMock.mockImplementation((options: any) => { expect(options.url).toContain('/api/tsdb/query'); expect(options.data.queries[0].appInsights.rawQueryString).toBeUndefined(); expect(options.data.queries[0].appInsights.metricName).toBe('exceptions/server'); expect(options.data.queries[0].appInsights.dimension).toBe('client/city'); return Promise.resolve({ data: response, status: 200 }); - }; + }); }); it('should return a list of datapoints', () => { @@ -397,10 +405,10 @@ describe('AppInsightsDatasource', () => { }; beforeEach(() => { - ctx.backendSrv.datasourceRequest = (options: { url: string }) => { + datasourceRequestMock.mockImplementation((options: { url: string }) => { expect(options.url).toContain('/metrics/metadata'); return Promise.resolve({ data: response, status: 200 }); - }; + }); }); it('should return a list of metric names', () => { @@ -435,10 +443,10 @@ describe('AppInsightsDatasource', () => { }; beforeEach(() => { - ctx.backendSrv.datasourceRequest = (options: { url: string }) => { + datasourceRequestMock.mockImplementation((options: { url: string }) => { expect(options.url).toContain('/metrics/metadata'); return Promise.resolve({ data: response, status: 200 }); - }; + }); }); it('should return a list of group bys', () => { @@ -463,10 +471,10 @@ describe('AppInsightsDatasource', () => { }; beforeEach(() => { - ctx.backendSrv.datasourceRequest = (options: { url: string }) => { + datasourceRequestMock.mockImplementation((options: { url: string }) => { expect(options.url).toContain('/metrics/metadata'); return Promise.resolve({ data: response, status: 200 }); - }; + }); }); it('should return a list of metric names', () => { @@ -501,10 +509,10 @@ describe('AppInsightsDatasource', () => { }; beforeEach(() => { - ctx.backendSrv.datasourceRequest = (options: { url: string }) => { + datasourceRequestMock.mockImplementation((options: { url: string }) => { expect(options.url).toContain('/metrics/metadata'); return Promise.resolve({ data: response, status: 200 }); - }; + }); }); it('should return a list of group bys', () => { diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/app_insights/app_insights_datasource.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/app_insights/app_insights_datasource.ts index b61ab0efacb..98023112a83 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/app_insights/app_insights_datasource.ts +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/app_insights/app_insights_datasource.ts @@ -1,6 +1,6 @@ import { TimeSeries, toDataFrame } from '@grafana/data'; import { DataQueryRequest, DataQueryResponseData, DataSourceInstanceSettings } from '@grafana/data'; -import { BackendSrv } from 'app/core/services/backend_srv'; +import { getBackendSrv } from '@grafana/runtime'; import { TemplateSrv } from 'app/features/templating/template_srv'; import _ from 'lodash'; @@ -21,11 +21,7 @@ export default class AppInsightsDatasource { logAnalyticsColumns: { [key: string]: LogAnalyticsColumn[] } = {}; /** @ngInject */ - constructor( - instanceSettings: DataSourceInstanceSettings, - private backendSrv: BackendSrv, - private templateSrv: TemplateSrv - ) { + constructor(instanceSettings: DataSourceInstanceSettings, private templateSrv: TemplateSrv) { this.id = instanceSettings.id; this.applicationId = instanceSettings.jsonData.appInsightsAppId; this.baseUrl = `/appinsights/${this.version}/apps/${this.applicationId}`; @@ -119,7 +115,7 @@ export default class AppInsightsDatasource { return; } - const { data } = await this.backendSrv.datasourceRequest({ + const { data } = await getBackendSrv().datasourceRequest({ url: '/api/tsdb/query', method: 'POST', data: { @@ -228,8 +224,8 @@ export default class AppInsightsDatasource { }); } - doRequest(url: any, maxRetries = 1) { - return this.backendSrv + doRequest(url: any, maxRetries = 1): Promise { + return getBackendSrv() .datasourceRequest({ url: this.url + url, method: 'GET', diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_log_analytics/azure_log_analytics_datasource.test.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_log_analytics/azure_log_analytics_datasource.test.ts index 18fb82f23cd..5203dbe4bdc 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_log_analytics/azure_log_analytics_datasource.test.ts +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_log_analytics/azure_log_analytics_datasource.test.ts @@ -4,10 +4,22 @@ import FakeSchemaData from './__mocks__/schema'; import { TemplateSrv } from 'app/features/templating/template_srv'; import { KustoSchema } from '../types'; import { toUtc } from '@grafana/data'; +import { backendSrv } from 'app/core/services/backend_srv'; // will use the version in __mocks__ + +jest.mock('@grafana/runtime', () => ({ + ...jest.requireActual('@grafana/runtime'), + getBackendSrv: () => backendSrv, +})); describe('AzureLogAnalyticsDatasource', () => { + const datasourceRequestMock = jest.spyOn(backendSrv, 'datasourceRequest'); + + beforeEach(() => { + jest.clearAllMocks(); + datasourceRequestMock.mockImplementation(jest.fn()); + }); + const ctx: any = { - backendSrv: {}, templateSrv: new TemplateSrv(), }; @@ -17,7 +29,7 @@ describe('AzureLogAnalyticsDatasource', () => { url: 'http://azureloganalyticsapi', }; - ctx.ds = new AzureMonitorDatasource(ctx.instanceSettings, ctx.backendSrv, ctx.templateSrv); + ctx.ds = new AzureMonitorDatasource(ctx.instanceSettings, ctx.templateSrv); }); describe('When the config option "Same as Azure Monitor" has been chosen', () => { @@ -56,9 +68,9 @@ describe('AzureLogAnalyticsDatasource', () => { ctx.instanceSettings.jsonData.tenantId = 'xxx'; ctx.instanceSettings.jsonData.clientId = 'xxx'; ctx.instanceSettings.jsonData.azureLogAnalyticsSameAs = true; - ctx.ds = new AzureMonitorDatasource(ctx.instanceSettings, ctx.backendSrv, ctx.templateSrv); + ctx.ds = new AzureMonitorDatasource(ctx.instanceSettings, ctx.templateSrv); - ctx.backendSrv.datasourceRequest = (options: { url: string }) => { + datasourceRequestMock.mockImplementation((options: { url: string }) => { if (options.url.indexOf('Microsoft.OperationalInsights/workspaces') > -1) { workspacesUrl = options.url; return Promise.resolve({ data: workspaceResponse, status: 200 }); @@ -66,7 +78,7 @@ describe('AzureLogAnalyticsDatasource', () => { azureLogAnalyticsUrl = options.url; return Promise.resolve({ data: tableResponseWithOneColumn, status: 200 }); } - }; + }); await ctx.ds.metricFindQuery('workspace("aworkspace").AzureActivity | distinct Category'); }); @@ -94,9 +106,7 @@ describe('AzureLogAnalyticsDatasource', () => { ctx.instanceSettings.jsonData.logAnalyticsSubscriptionId = 'xxx'; ctx.instanceSettings.jsonData.logAnalyticsTenantId = 'xxx'; ctx.instanceSettings.jsonData.logAnalyticsClientId = 'xxx'; - ctx.backendSrv.datasourceRequest = () => { - return Promise.reject(error); - }; + datasourceRequestMock.mockImplementation(() => Promise.reject(error)); }); it('should return error status and a detailed error message', () => { @@ -166,10 +176,10 @@ describe('AzureLogAnalyticsDatasource', () => { describe('in time series format', () => { describe('and the data is valid (has time, metric and value columns)', () => { beforeEach(() => { - ctx.backendSrv.datasourceRequest = (options: { url: string }) => { + datasourceRequestMock.mockImplementation((options: { url: string }) => { expect(options.url).toContain('query=AzureActivity'); return Promise.resolve({ data: response, status: 200 }); - }; + }); }); it('should return a list of datapoints', () => { @@ -205,10 +215,11 @@ describe('AzureLogAnalyticsDatasource', () => { }, ], }; - ctx.backendSrv.datasourceRequest = (options: { url: string }) => { + + datasourceRequestMock.mockImplementation((options: { url: string }) => { expect(options.url).toContain('query=AzureActivity'); return Promise.resolve({ data: invalidResponse, status: 200 }); - }; + }); }); it('should throw an exception', () => { @@ -222,10 +233,10 @@ describe('AzureLogAnalyticsDatasource', () => { describe('in tableformat', () => { beforeEach(() => { options.targets[0].azureLogAnalytics.resultFormat = 'table'; - ctx.backendSrv.datasourceRequest = (options: { url: string }) => { + datasourceRequestMock.mockImplementation((options: { url: string }) => { expect(options.url).toContain('query=AzureActivity'); return Promise.resolve({ data: response, status: 200 }); - }; + }); }); it('should return a list of columns and rows', () => { @@ -249,10 +260,10 @@ describe('AzureLogAnalyticsDatasource', () => { describe('When performing getSchema', () => { beforeEach(() => { - ctx.backendSrv.datasourceRequest = (options: { url: string }) => { + datasourceRequestMock.mockImplementation((options: { url: string }) => { expect(options.url).toContain('metadata'); return Promise.resolve({ data: FakeSchemaData.getlogAnalyticsFakeMetadata(), status: 200 }); - }; + }); }); it('should return a schema with a table and rows', () => { @@ -302,13 +313,13 @@ describe('AzureLogAnalyticsDatasource', () => { let queryResults: any[]; beforeEach(async () => { - ctx.backendSrv.datasourceRequest = (options: { url: string }) => { + datasourceRequestMock.mockImplementation((options: { url: string }) => { if (options.url.indexOf('Microsoft.OperationalInsights/workspaces') > -1) { return Promise.resolve({ data: workspaceResponse, status: 200 }); } else { return Promise.resolve({ data: tableResponseWithOneColumn, status: 200 }); } - }; + }); queryResults = await ctx.ds.metricFindQuery('workspace("aworkspace").AzureActivity | distinct Category'); }); @@ -364,13 +375,13 @@ describe('AzureLogAnalyticsDatasource', () => { let annotationResults: any[]; beforeEach(async () => { - ctx.backendSrv.datasourceRequest = (options: { url: string }) => { + datasourceRequestMock.mockImplementation((options: { url: string }) => { if (options.url.indexOf('Microsoft.OperationalInsights/workspaces') > -1) { return Promise.resolve({ data: workspaceResponse, status: 200 }); } else { return Promise.resolve({ data: tableResponse, status: 200 }); } - }; + }); annotationResults = await ctx.ds.annotationQuery({ annotation: { diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_log_analytics/azure_log_analytics_datasource.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_log_analytics/azure_log_analytics_datasource.ts index c40f34197c5..903a898ea0e 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_log_analytics/azure_log_analytics_datasource.ts +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_log_analytics/azure_log_analytics_datasource.ts @@ -3,7 +3,7 @@ import LogAnalyticsQuerystringBuilder from '../log_analytics/querystring_builder import ResponseParser from './response_parser'; import { AzureMonitorQuery, AzureDataSourceJsonData } from '../types'; import { DataQueryRequest, DataSourceInstanceSettings } from '@grafana/data'; -import { BackendSrv } from 'app/core/services/backend_srv'; +import { getBackendSrv } from '@grafana/runtime'; import { TemplateSrv } from 'app/features/templating/template_srv'; export default class AzureLogAnalyticsDatasource { @@ -18,7 +18,6 @@ export default class AzureLogAnalyticsDatasource { /** @ngInject */ constructor( private instanceSettings: DataSourceInstanceSettings, - private backendSrv: BackendSrv, private templateSrv: TemplateSrv ) { this.id = instanceSettings.id; @@ -230,8 +229,8 @@ export default class AzureLogAnalyticsDatasource { }); } - doRequest(url: string, maxRetries = 1) { - return this.backendSrv + doRequest(url: string, maxRetries = 1): Promise { + return getBackendSrv() .datasourceRequest({ url: this.url + url, method: 'GET', diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/azure_monitor_datasource.test.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/azure_monitor_datasource.test.ts index 9445aa435db..bd3d64e5b43 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/azure_monitor_datasource.test.ts +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/azure_monitor_datasource.test.ts @@ -2,21 +2,28 @@ import AzureMonitorDatasource from '../datasource'; import { TemplateSrv } from 'app/features/templating/template_srv'; import { toUtc, DataFrame } from '@grafana/data'; +import { backendSrv } from 'app/core/services/backend_srv'; // will use the version in __mocks__ + +jest.mock('@grafana/runtime', () => ({ + ...jest.requireActual('@grafana/runtime'), + getBackendSrv: () => backendSrv, +})); describe('AzureMonitorDatasource', () => { const ctx: any = { - backendSrv: {}, templateSrv: new TemplateSrv(), }; + const datasourceRequestMock = jest.spyOn(backendSrv, 'datasourceRequest'); beforeEach(() => { + jest.clearAllMocks(); ctx.instanceSettings = { url: 'http://azuremonitor.com', jsonData: { subscriptionId: '9935389e-9122-4ef9-95f9-1513dd24753f' }, cloudName: 'azuremonitor', }; - ctx.ds = new AzureMonitorDatasource(ctx.instanceSettings, ctx.backendSrv, ctx.templateSrv); + ctx.ds = new AzureMonitorDatasource(ctx.instanceSettings, ctx.templateSrv); }); describe('When performing testDatasource', () => { @@ -35,9 +42,7 @@ describe('AzureMonitorDatasource', () => { beforeEach(() => { ctx.instanceSettings.jsonData.tenantId = 'xxx'; ctx.instanceSettings.jsonData.clientId = 'xxx'; - ctx.backendSrv.datasourceRequest = () => { - return Promise.reject(error); - }; + datasourceRequestMock.mockImplementation(() => Promise.reject(error)); }); it('should return error status and a detailed error message', () => { @@ -62,9 +67,7 @@ describe('AzureMonitorDatasource', () => { beforeEach(() => { ctx.instanceSettings.jsonData.tenantId = 'xxx'; ctx.instanceSettings.jsonData.clientId = 'xxx'; - ctx.backendSrv.datasourceRequest = () => { - return Promise.resolve({ data: response, status: 200 }); - }; + datasourceRequestMock.mockImplementation(() => Promise.resolve({ data: response, status: 200 })); }); it('should return success status', () => { @@ -124,10 +127,10 @@ describe('AzureMonitorDatasource', () => { }; beforeEach(() => { - ctx.backendSrv.datasourceRequest = (options: { url: string }) => { + datasourceRequestMock.mockImplementation((options: { url: string }) => { expect(options.url).toContain('/api/tsdb/query'); return Promise.resolve({ data: response, status: 200 }); - }; + }); }); it('should return a list of datapoints', () => { @@ -157,9 +160,7 @@ describe('AzureMonitorDatasource', () => { }; beforeEach(() => { - ctx.backendSrv.datasourceRequest = () => { - return Promise.resolve(response); - }; + datasourceRequestMock.mockImplementation((options: { url: string }) => Promise.resolve(response)); }); it('should return a list of subscriptions', () => { @@ -183,9 +184,7 @@ describe('AzureMonitorDatasource', () => { }; beforeEach(() => { - ctx.backendSrv.datasourceRequest = () => { - return Promise.resolve(response); - }; + datasourceRequestMock.mockImplementation(() => Promise.resolve(response)); }); it('should return a list of resource groups', () => { @@ -209,10 +208,10 @@ describe('AzureMonitorDatasource', () => { }; beforeEach(() => { - ctx.backendSrv.datasourceRequest = (options: { url: string }) => { + datasourceRequestMock.mockImplementation((options: { url: string }) => { expect(options.url).toContain('11112222-eeee-4949-9b2d-9106972f9123'); return Promise.resolve(response); - }; + }); }); it('should return a list of resource groups', () => { @@ -243,12 +242,12 @@ describe('AzureMonitorDatasource', () => { }; beforeEach(() => { - ctx.backendSrv.datasourceRequest = (options: { url: string }) => { + datasourceRequestMock.mockImplementation((options: { url: string }) => { const baseUrl = 'http://azuremonitor.com/azuremonitor/subscriptions/9935389e-9122-4ef9-95f9-1513dd24753f/resourceGroups'; expect(options.url).toBe(baseUrl + '/nodesapp/resources?api-version=2018-01-01'); return Promise.resolve(response); - }; + }); }); it('should return a list of namespaces', () => { @@ -277,12 +276,12 @@ describe('AzureMonitorDatasource', () => { }; beforeEach(() => { - ctx.backendSrv.datasourceRequest = (options: { url: string }) => { + datasourceRequestMock.mockImplementation((options: { url: string }) => { const baseUrl = 'http://azuremonitor.com/azuremonitor/subscriptions/11112222-eeee-4949-9b2d-9106972f9123/resourceGroups'; expect(options.url).toBe(baseUrl + '/nodesapp/resources?api-version=2018-01-01'); return Promise.resolve(response); - }; + }); }); it('should return a list of namespaces', () => { @@ -315,12 +314,12 @@ describe('AzureMonitorDatasource', () => { }; beforeEach(() => { - ctx.backendSrv.datasourceRequest = (options: { url: string }) => { + datasourceRequestMock.mockImplementation((options: { url: string }) => { const baseUrl = 'http://azuremonitor.com/azuremonitor/subscriptions/9935389e-9122-4ef9-95f9-1513dd24753f/resourceGroups'; expect(options.url).toBe(baseUrl + '/nodeapp/resources?api-version=2018-01-01'); return Promise.resolve(response); - }; + }); }); it('should return a list of resource names', () => { @@ -353,12 +352,12 @@ describe('AzureMonitorDatasource', () => { }; beforeEach(() => { - ctx.backendSrv.datasourceRequest = (options: { url: string }) => { + datasourceRequestMock.mockImplementation((options: { url: string }) => { const baseUrl = 'http://azuremonitor.com/azuremonitor/subscriptions/11112222-eeee-4949-9b2d-9106972f9123/resourceGroups'; expect(options.url).toBe(baseUrl + '/nodeapp/resources?api-version=2018-01-01'); return Promise.resolve(response); - }; + }); }); it('should return a list of resource names', () => { @@ -397,7 +396,7 @@ describe('AzureMonitorDatasource', () => { }; beforeEach(() => { - ctx.backendSrv.datasourceRequest = (options: { url: string }) => { + datasourceRequestMock.mockImplementation((options: { url: string }) => { const baseUrl = 'http://azuremonitor.com/azuremonitor/subscriptions/9935389e-9122-4ef9-95f9-1513dd24753f/resourceGroups'; expect(options.url).toBe( @@ -406,7 +405,7 @@ describe('AzureMonitorDatasource', () => { 'metricdefinitions?api-version=2018-01-01&metricnamespace=default' ); return Promise.resolve(response); - }; + }); }); it('should return a list of metric names', () => { @@ -446,7 +445,7 @@ describe('AzureMonitorDatasource', () => { }; beforeEach(() => { - ctx.backendSrv.datasourceRequest = (options: { url: string }) => { + datasourceRequestMock.mockImplementation((options: { url: string }) => { const baseUrl = 'http://azuremonitor.com/azuremonitor/subscriptions/11112222-eeee-4949-9b2d-9106972f9123/resourceGroups'; expect(options.url).toBe( @@ -455,7 +454,7 @@ describe('AzureMonitorDatasource', () => { 'metricdefinitions?api-version=2018-01-01&metricnamespace=default' ); return Promise.resolve(response); - }; + }); }); it('should return a list of metric names', () => { @@ -497,7 +496,7 @@ describe('AzureMonitorDatasource', () => { }; beforeEach(() => { - ctx.backendSrv.datasourceRequest = (options: { url: string }) => { + datasourceRequestMock.mockImplementation((options: { url: string }) => { const baseUrl = 'http://azuremonitor.com/azuremonitor/subscriptions/9935389e-9122-4ef9-95f9-1513dd24753f/resourceGroups'; expect(options.url).toBe( @@ -505,7 +504,7 @@ describe('AzureMonitorDatasource', () => { '/nodeapp/providers/Microsoft.Compute/virtualMachines/rn/providers/microsoft.insights/metricNamespaces?api-version=2017-12-01-preview' ); return Promise.resolve(response); - }; + }); }); it('should return a list of metric names', () => { @@ -545,7 +544,7 @@ describe('AzureMonitorDatasource', () => { }; beforeEach(() => { - ctx.backendSrv.datasourceRequest = (options: { url: string }) => { + datasourceRequestMock.mockImplementation((options: { url: string }) => { const baseUrl = 'http://azuremonitor.com/azuremonitor/subscriptions/11112222-eeee-4949-9b2d-9106972f9123/resourceGroups'; expect(options.url).toBe( @@ -553,7 +552,7 @@ describe('AzureMonitorDatasource', () => { '/nodeapp/providers/Microsoft.Compute/virtualMachines/rn/providers/microsoft.insights/metricNamespaces?api-version=2017-12-01-preview' ); return Promise.resolve(response); - }; + }); }); it('should return a list of metric namespaces', () => { @@ -601,9 +600,7 @@ describe('AzureMonitorDatasource', () => { }; beforeEach(() => { - ctx.backendSrv.datasourceRequest = () => { - return Promise.resolve(response); - }; + datasourceRequestMock.mockImplementation(() => Promise.resolve(response)); }); it('should return list of Resource Groups', () => { @@ -625,9 +622,7 @@ describe('AzureMonitorDatasource', () => { }; beforeEach(() => { - ctx.backendSrv.datasourceRequest = () => { - return Promise.resolve(response); - }; + datasourceRequestMock.mockImplementation(() => Promise.resolve(response)); }); it('should return list of Resource Groups', () => { @@ -674,12 +669,12 @@ describe('AzureMonitorDatasource', () => { }; beforeEach(() => { - ctx.backendSrv.datasourceRequest = (options: { url: string }) => { + datasourceRequestMock.mockImplementation((options: { url: string }) => { const baseUrl = 'http://azuremonitor.com/azuremonitor/subscriptions/9935389e-9122-4ef9-95f9-1513dd24753f/resourceGroups'; expect(options.url).toBe(baseUrl + '/nodesapp/resources?api-version=2018-01-01'); return Promise.resolve(response); - }; + }); }); it('should return list of Metric Definitions with no duplicates and no unsupported namespaces', () => { @@ -725,12 +720,12 @@ describe('AzureMonitorDatasource', () => { }; beforeEach(() => { - ctx.backendSrv.datasourceRequest = (options: { url: string }) => { + datasourceRequestMock.mockImplementation((options: { url: string }) => { const baseUrl = 'http://azuremonitor.com/azuremonitor/subscriptions/9935389e-9122-4ef9-95f9-1513dd24753f/resourceGroups'; expect(options.url).toBe(baseUrl + '/nodeapp/resources?api-version=2018-01-01'); return Promise.resolve(response); - }; + }); }); it('should return list of Resource Names', () => { @@ -763,12 +758,12 @@ describe('AzureMonitorDatasource', () => { }; beforeEach(() => { - ctx.backendSrv.datasourceRequest = (options: { url: string }) => { + datasourceRequestMock.mockImplementation((options: { url: string }) => { const baseUrl = 'http://azuremonitor.com/azuremonitor/subscriptions/9935389e-9122-4ef9-95f9-1513dd24753f/resourceGroups'; expect(options.url).toBe(baseUrl + '/nodeapp/resources?api-version=2018-01-01'); return Promise.resolve(response); - }; + }); }); it('should return list of Resource Names', () => { @@ -828,7 +823,7 @@ describe('AzureMonitorDatasource', () => { }; beforeEach(() => { - ctx.backendSrv.datasourceRequest = (options: { url: string }) => { + datasourceRequestMock.mockImplementation((options: { url: string }) => { const baseUrl = 'http://azuremonitor.com/azuremonitor/subscriptions/9935389e-9122-4ef9-95f9-1513dd24753f/resourceGroups/nodeapp'; const expected = @@ -837,7 +832,7 @@ describe('AzureMonitorDatasource', () => { '/providers/microsoft.insights/metricdefinitions?api-version=2018-01-01&metricnamespace=default'; expect(options.url).toBe(expected); return Promise.resolve(response); - }; + }); }); it('should return list of Metric Definitions', () => { @@ -900,7 +895,7 @@ describe('AzureMonitorDatasource', () => { }; beforeEach(() => { - ctx.backendSrv.datasourceRequest = (options: { url: string }) => { + datasourceRequestMock.mockImplementation((options: { url: string }) => { const baseUrl = 'http://azuremonitor.com/azuremonitor/subscriptions/9935389e-9122-4ef9-95f9-1513dd24753f/resourceGroups/nodeapp'; const expected = @@ -909,7 +904,7 @@ describe('AzureMonitorDatasource', () => { '/providers/microsoft.insights/metricdefinitions?api-version=2018-01-01&metricnamespace=default'; expect(options.url).toBe(expected); return Promise.resolve(response); - }; + }); }); it('should return Aggregation metadata for a Metric', () => { @@ -974,7 +969,7 @@ describe('AzureMonitorDatasource', () => { }; beforeEach(() => { - ctx.backendSrv.datasourceRequest = (options: { url: string }) => { + datasourceRequestMock.mockImplementation((options: { url: string }) => { const baseUrl = 'http://azuremonitor.com/azuremonitor/subscriptions/9935389e-9122-4ef9-95f9-1513dd24753f/resourceGroups/nodeapp'; const expected = @@ -983,7 +978,7 @@ describe('AzureMonitorDatasource', () => { '/providers/microsoft.insights/metricdefinitions?api-version=2018-01-01&metricnamespace=default'; expect(options.url).toBe(expected); return Promise.resolve(response); - }; + }); }); it('should return dimensions for a Metric that has dimensions', () => { diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/azure_monitor_datasource.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/azure_monitor_datasource.ts index 6b2b9be34a7..0f9d19f321e 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/azure_monitor_datasource.ts +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/azure_monitor_datasource.ts @@ -12,8 +12,8 @@ import { import { DataQueryRequest, DataQueryResponseData, DataSourceInstanceSettings } from '@grafana/data'; import { TimeSeries, toDataFrame } from '@grafana/data'; -import { BackendSrv } from 'app/core/services/backend_srv'; import { TemplateSrv } from 'app/features/templating/template_srv'; +import { getBackendSrv } from '@grafana/runtime'; export default class AzureMonitorDatasource { apiVersion = '2018-01-01'; @@ -31,7 +31,6 @@ export default class AzureMonitorDatasource { /** @ngInject */ constructor( private instanceSettings: DataSourceInstanceSettings, - private backendSrv: BackendSrv, private templateSrv: TemplateSrv ) { this.id = instanceSettings.id; @@ -108,7 +107,7 @@ export default class AzureMonitorDatasource { return Promise.resolve([]); } - const { data } = await this.backendSrv.datasourceRequest({ + const { data } = await getBackendSrv().datasourceRequest({ url: '/api/tsdb/query', method: 'POST', data: { @@ -434,8 +433,8 @@ export default class AzureMonitorDatasource { return field && field.length > 0; } - doRequest(url: string, maxRetries = 1) { - return this.backendSrv + doRequest(url: string, maxRetries = 1): Promise { + return getBackendSrv() .datasourceRequest({ url: this.url + url, method: 'GET', diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/ConfigEditor.tsx b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/ConfigEditor.tsx index 7cb30b1145f..e97ba25f4dd 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/ConfigEditor.tsx +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/ConfigEditor.tsx @@ -10,7 +10,7 @@ import { import { MonitorConfig } from './MonitorConfig'; import { AnalyticsConfig } from './AnalyticsConfig'; import { TemplateSrv } from 'app/features/templating/template_srv'; -import { getBackendSrv, BackendSrv } from 'app/core/services/backend_srv'; +import { getBackendSrv } from '@grafana/runtime'; import { InsightsConfig } from './InsightsConfig'; import ResponseParser from '../azure_monitor/response_parser'; import { AzureDataSourceJsonData, AzureDataSourceSecureJsonData, AzureDataSourceSettings } from '../types'; @@ -38,7 +38,6 @@ export class ConfigEditor extends PureComponent { logAnalyticsSubscriptionId: '', }; - this.backendSrv = getBackendSrv(); this.templateSrv = new TemplateSrv(); if (this.props.options.id) { updateDatasourcePluginOption(this.props, 'url', '/api/datasources/proxy/' + this.props.options.id); @@ -46,7 +45,6 @@ export class ConfigEditor extends PureComponent { } initPromise: CancelablePromise = null; - backendSrv: BackendSrv = null; templateSrv: TemplateSrv = null; componentDidMount() { @@ -157,7 +155,7 @@ export class ConfigEditor extends PureComponent { }; onLoadSubscriptions = async (type?: string) => { - await this.backendSrv + await getBackendSrv() .put(`/api/datasources/${this.props.options.id}`, this.props.options) .then((result: AzureDataSourceSettings) => { updateDatasourcePluginOption(this.props, 'version', result.version); @@ -173,7 +171,7 @@ export class ConfigEditor extends PureComponent { loadSubscriptions = async (route?: string) => { const url = `/${route || this.props.options.jsonData.cloudName}/subscriptions?api-version=2019-03-01`; - const result = await this.backendSrv.datasourceRequest({ + const result = await getBackendSrv().datasourceRequest({ url: this.props.options.url + url, method: 'GET', }); @@ -198,7 +196,7 @@ export class ConfigEditor extends PureComponent { azureMonitorUrl + `/${subscriptionId}/providers/Microsoft.OperationalInsights/workspaces?api-version=2017-04-26-preview`; - const result = await this.backendSrv.datasourceRequest({ + const result = await getBackendSrv().datasourceRequest({ url: this.props.options.url + workspaceListUrl, method: 'GET', }); diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/datasource.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/datasource.ts index f3b7591ff12..d8e8f72c0d5 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/datasource.ts +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/datasource.ts @@ -4,7 +4,6 @@ import AppInsightsDatasource from './app_insights/app_insights_datasource'; import AzureLogAnalyticsDatasource from './azure_log_analytics/azure_log_analytics_datasource'; import { AzureMonitorQuery, AzureDataSourceJsonData } from './types'; import { DataSourceApi, DataQueryRequest, DataSourceInstanceSettings } from '@grafana/data'; -import { BackendSrv } from 'app/core/services/backend_srv'; import { TemplateSrv } from 'app/features/templating/template_srv'; export default class Datasource extends DataSourceApi { @@ -13,20 +12,12 @@ export default class Datasource extends DataSourceApi, - private backendSrv: BackendSrv, - private templateSrv: TemplateSrv - ) { + constructor(instanceSettings: DataSourceInstanceSettings, private templateSrv: TemplateSrv) { super(instanceSettings); - this.azureMonitorDatasource = new AzureMonitorDatasource(instanceSettings, this.backendSrv, this.templateSrv); - this.appInsightsDatasource = new AppInsightsDatasource(instanceSettings, this.backendSrv, this.templateSrv); + this.azureMonitorDatasource = new AzureMonitorDatasource(instanceSettings, this.templateSrv); + this.appInsightsDatasource = new AppInsightsDatasource(instanceSettings, this.templateSrv); - this.azureLogAnalyticsDatasource = new AzureLogAnalyticsDatasource( - instanceSettings, - this.backendSrv, - this.templateSrv - ); + this.azureLogAnalyticsDatasource = new AzureLogAnalyticsDatasource(instanceSettings, this.templateSrv); } async query(options: DataQueryRequest) { diff --git a/public/app/plugins/datasource/grafana/datasource.ts b/public/app/plugins/datasource/grafana/datasource.ts index e5808930741..81ef99d96dc 100644 --- a/public/app/plugins/datasource/grafana/datasource.ts +++ b/public/app/plugins/datasource/grafana/datasource.ts @@ -1,13 +1,13 @@ import _ from 'lodash'; -import { BackendSrv } from 'app/core/services/backend_srv'; import { TemplateSrv } from 'app/features/templating/template_srv'; +import { getBackendSrv } from '@grafana/runtime'; class GrafanaDatasource { /** @ngInject */ - constructor(private backendSrv: BackendSrv, private templateSrv: TemplateSrv) {} + constructor(private templateSrv: TemplateSrv) {} query(options: any) { - return this.backendSrv + return getBackendSrv() .get('/api/tsdb/testdata/random-walk', { from: options.range.from.valueOf(), to: options.range.to.valueOf(), @@ -76,7 +76,7 @@ class GrafanaDatasource { params.tags = tags; } - return this.backendSrv.get('/api/annotations', params); + return getBackendSrv().get('/api/annotations', params); } } diff --git a/public/app/plugins/datasource/grafana/specs/datasource.test.ts b/public/app/plugins/datasource/grafana/specs/datasource.test.ts index ede87d2a8b2..bb6dc2085e5 100644 --- a/public/app/plugins/datasource/grafana/specs/datasource.test.ts +++ b/public/app/plugins/datasource/grafana/specs/datasource.test.ts @@ -1,25 +1,37 @@ -import { GrafanaDatasource } from '../datasource'; -// @ts-ignore -import q from 'q'; import { dateTime } from '@grafana/data'; +import { backendSrv } from 'app/core/services/backend_srv'; // will use the version in __mocks__ +import { GrafanaDatasource } from '../datasource'; + +jest.mock('@grafana/runtime', () => ({ + ...jest.requireActual('@grafana/runtime'), + getBackendSrv: () => backendSrv, +})); describe('grafana data source', () => { + const getMock = jest.spyOn(backendSrv, 'get'); + + beforeEach(() => { + jest.clearAllMocks(); + }); + describe('when executing an annotations query', () => { let calledBackendSrvParams: any; - const backendSrvStub = { - get: (url: string, options: any) => { + let templateSrvStub: any; + let ds: GrafanaDatasource; + beforeEach(() => { + getMock.mockImplementation((url: string, options: any) => { calledBackendSrvParams = options; - return q.resolve([]); - }, - }; + return Promise.resolve([]); + }); - const templateSrvStub = { - replace: (val: string) => { - return val.replace('$var2', 'replaced__delimiter__replaced2').replace('$var', 'replaced'); - }, - }; + templateSrvStub = { + replace: (val: string) => { + return val.replace('$var2', 'replaced__delimiter__replaced2').replace('$var', 'replaced'); + }, + }; - const ds = new GrafanaDatasource(backendSrvStub as any, templateSrvStub as any); + ds = new GrafanaDatasource(templateSrvStub as any); + }); describe('with tags that have template variables', () => { const options = setupAnnotationQueryOptions({ tags: ['tag1:$var'] }); diff --git a/public/app/plugins/datasource/graphite/datasource.ts b/public/app/plugins/datasource/graphite/datasource.ts index 1a79bf8749d..648930bf231 100644 --- a/public/app/plugins/datasource/graphite/datasource.ts +++ b/public/app/plugins/datasource/graphite/datasource.ts @@ -10,7 +10,7 @@ import { } from '@grafana/data'; import { isVersionGtOrEq, SemVersion } from 'app/core/utils/version'; import gfunc from './gfunc'; -import { BackendSrv } from 'app/core/services/backend_srv'; +import { getBackendSrv } from '@grafana/runtime'; import { TemplateSrv } from 'app/features/templating/template_srv'; //Types import { GraphiteOptions, GraphiteQuery, GraphiteType } from './types'; @@ -30,7 +30,7 @@ export class GraphiteDatasource extends DataSourceApi ({ + ...jest.requireActual('@grafana/runtime'), + getBackendSrv: () => backendSrv, +})); describe('graphiteDatasource', () => { + const datasourceRequestMock = jest.spyOn(backendSrv, 'datasourceRequest'); + const ctx: any = { - backendSrv: {}, // @ts-ignore templateSrv: new TemplateSrv(), instanceSettings: { url: 'url', name: 'graphiteProd', jsonData: {} }, }; beforeEach(() => { + jest.clearAllMocks(); ctx.instanceSettings.url = '/api/datasources/proxy/1'; - // @ts-ignore - ctx.ds = new GraphiteDatasource(ctx.instanceSettings, ctx.backendSrv, ctx.templateSrv); + ctx.ds = new GraphiteDatasource(ctx.instanceSettings, ctx.templateSrv); }); describe('When querying graphite with one target using query editor target spec', () => { @@ -31,7 +38,7 @@ describe('graphiteDatasource', () => { let requestOptions: any; beforeEach(async () => { - ctx.backendSrv.datasourceRequest = (options: any) => { + datasourceRequestMock.mockImplementation((options: any) => { requestOptions = options; return Promise.resolve({ data: [ @@ -44,7 +51,7 @@ describe('graphiteDatasource', () => { }, ], }); - }; + }); await ctx.ds.query(query).then((data: any) => { results = data; @@ -115,10 +122,9 @@ describe('graphiteDatasource', () => { }; beforeEach(async () => { - ctx.backendSrv.datasourceRequest = (options: any) => { + datasourceRequestMock.mockImplementation((options: any) => { return Promise.resolve(response); - }; - + }); await ctx.ds.annotationQuery(options).then((data: any) => { results = data; }); @@ -145,9 +151,9 @@ describe('graphiteDatasource', () => { ], }; beforeEach(() => { - ctx.backendSrv.datasourceRequest = (options: any) => { + datasourceRequestMock.mockImplementation((options: any) => { return Promise.resolve(response); - }; + }); ctx.ds.annotationQuery(options).then((data: any) => { results = data; @@ -263,12 +269,12 @@ describe('graphiteDatasource', () => { let requestOptions: any; beforeEach(() => { - ctx.backendSrv.datasourceRequest = (options: any) => { + datasourceRequestMock.mockImplementation((options: any) => { requestOptions = options; return Promise.resolve({ data: ['backend_01', 'backend_02'], }); - }; + }); }); it('should generate tags query', () => { @@ -390,7 +396,6 @@ describe('graphiteDatasource', () => { function accessScenario(name: string, url: string, fn: any) { describe('access scenario ' + name, () => { const ctx: any = { - backendSrv: {}, // @ts-ignore templateSrv: new TemplateSrv(), instanceSettings: { url: 'url', name: 'graphiteProd', jsonData: {} }, @@ -405,8 +410,7 @@ function accessScenario(name: string, url: string, fn: any) { it('tracing headers should be added', () => { ctx.instanceSettings.url = url; - // @ts-ignore - const ds = new GraphiteDatasource(ctx.instanceSettings, ctx.backendSrv, ctx.templateSrv); + const ds = new GraphiteDatasource(ctx.instanceSettings, ctx.templateSrv); ds.addTracingHeaders(httpOptions, options); fn(httpOptions); }); diff --git a/public/app/plugins/datasource/influxdb/datasource.ts b/public/app/plugins/datasource/influxdb/datasource.ts index 1534e16865e..78c0359dd91 100644 --- a/public/app/plugins/datasource/influxdb/datasource.ts +++ b/public/app/plugins/datasource/influxdb/datasource.ts @@ -6,7 +6,7 @@ import InfluxQueryModel from './influx_query_model'; import ResponseParser from './response_parser'; import { InfluxQueryBuilder } from './query_builder'; import { InfluxQuery, InfluxOptions } from './types'; -import { BackendSrv } from 'app/core/services/backend_srv'; +import { getBackendSrv } from '@grafana/runtime'; import { TemplateSrv } from 'app/features/templating/template_srv'; export default class InfluxDatasource extends DataSourceApi { @@ -23,11 +23,7 @@ export default class InfluxDatasource extends DataSourceApi, - private backendSrv: BackendSrv, - private templateSrv: TemplateSrv - ) { + constructor(instanceSettings: DataSourceInstanceSettings, private templateSrv: TemplateSrv) { super(instanceSettings); this.type = 'influxdb'; this.urls = _.map(instanceSettings.url.split(','), url => { @@ -208,7 +204,7 @@ export default class InfluxDatasource extends DataSourceApi this.responseParser.parse(query)); } getTagKeys(options: any = {}) { @@ -320,28 +316,30 @@ export default class InfluxDatasource extends DataSourceApi { - return result.data; - }, - (err: any) => { - if (err.status !== 0 || err.status >= 300) { - if (err.data && err.data.error) { - throw { - message: 'InfluxDB Error: ' + err.data.error, - data: err.data, - config: err.config, - }; - } else { - throw { - message: 'Network Error: ' + err.statusText + '(' + err.status + ')', - data: err.data, - config: err.config, - }; + return getBackendSrv() + .datasourceRequest(req) + .then( + (result: any) => { + return result.data; + }, + (err: any) => { + if (err.status !== 0 || err.status >= 300) { + if (err.data && err.data.error) { + throw { + message: 'InfluxDB Error: ' + err.data.error, + data: err.data, + config: err.config, + }; + } else { + throw { + message: 'Network Error: ' + err.statusText + '(' + err.status + ')', + data: err.data, + config: err.config, + }; + } } } - } - ); + ); } getTimeFilter(options: any) { diff --git a/public/app/plugins/datasource/influxdb/specs/datasource.test.ts b/public/app/plugins/datasource/influxdb/specs/datasource.test.ts index d55ffcab56d..fb3a9a277e2 100644 --- a/public/app/plugins/datasource/influxdb/specs/datasource.test.ts +++ b/public/app/plugins/datasource/influxdb/specs/datasource.test.ts @@ -1,18 +1,26 @@ import InfluxDatasource from '../datasource'; import { TemplateSrvStub } from 'test/specs/helpers'; +import { backendSrv } from 'app/core/services/backend_srv'; // will use the version in __mocks__ + +jest.mock('@grafana/runtime', () => ({ + ...jest.requireActual('@grafana/runtime'), + getBackendSrv: () => backendSrv, +})); describe('InfluxDataSource', () => { const ctx: any = { - backendSrv: {}, //@ts-ignore templateSrv: new TemplateSrvStub(), instanceSettings: { url: 'url', name: 'influxDb', jsonData: { httpMode: 'GET' } }, }; + const datasourceRequestMock = jest.spyOn(backendSrv, 'datasourceRequest'); + beforeEach(() => { + jest.clearAllMocks(); ctx.instanceSettings.url = '/api/datasources/proxy/1'; - ctx.ds = new InfluxDatasource(ctx.instanceSettings, ctx.backendSrv, ctx.templateSrv); + ctx.ds = new InfluxDatasource(ctx.instanceSettings, ctx.templateSrv); }); describe('When issuing metricFindQuery', () => { @@ -26,7 +34,7 @@ describe('InfluxDataSource', () => { let requestQuery: any, requestMethod: any, requestData: any; beforeEach(async () => { - ctx.backendSrv.datasourceRequest = (req: any) => { + datasourceRequestMock.mockImplementation((req: any) => { requestMethod = req.method; requestQuery = req.params.q; requestData = req.data; @@ -43,7 +51,7 @@ describe('InfluxDataSource', () => { }, ], }); - }; + }); await ctx.ds.metricFindQuery(query, queryOptions).then(() => {}); }); @@ -60,60 +68,59 @@ describe('InfluxDataSource', () => { expect(requestData).toBeNull(); }); }); -}); -describe('InfluxDataSource in POST query mode', () => { - const ctx: any = { - backendSrv: {}, - //@ts-ignore - templateSrv: new TemplateSrvStub(), - instanceSettings: { url: 'url', name: 'influxDb', jsonData: { httpMode: 'POST' } }, - }; + describe('InfluxDataSource in POST query mode', () => { + const ctx: any = { + //@ts-ignore + templateSrv: new TemplateSrvStub(), + instanceSettings: { url: 'url', name: 'influxDb', jsonData: { httpMode: 'POST' } }, + }; - beforeEach(() => { - ctx.instanceSettings.url = '/api/datasources/proxy/1'; - ctx.ds = new InfluxDatasource(ctx.instanceSettings, ctx.backendSrv, ctx.templateSrv); - }); + beforeEach(() => { + ctx.instanceSettings.url = '/api/datasources/proxy/1'; + ctx.ds = new InfluxDatasource(ctx.instanceSettings, ctx.templateSrv); + }); - describe('When issuing metricFindQuery', () => { - const query = 'SELECT max(value) FROM measurement'; - const queryOptions: any = {}; - let requestMethod: any, requestQueryParameter: any, queryEncoded: any, requestQuery: any; + describe('When issuing metricFindQuery', () => { + const query = 'SELECT max(value) FROM measurement'; + const queryOptions: any = {}; + let requestMethod: any, requestQueryParameter: any, queryEncoded: any, requestQuery: any; - beforeEach(async () => { - ctx.backendSrv.datasourceRequest = (req: any) => { - requestMethod = req.method; - requestQueryParameter = req.params; - requestQuery = req.data; - return Promise.resolve({ - results: [ - { - series: [ - { - name: 'measurement', - columns: ['max'], - values: [[1]], - }, - ], - }, - ], + beforeEach(async () => { + datasourceRequestMock.mockImplementation((req: any) => { + requestMethod = req.method; + requestQueryParameter = req.params; + requestQuery = req.data; + return Promise.resolve({ + results: [ + { + series: [ + { + name: 'measurement', + columns: ['max'], + values: [[1]], + }, + ], + }, + ], + }); }); - }; - queryEncoded = await ctx.ds.serializeParams({ q: query }); - await ctx.ds.metricFindQuery(query, queryOptions).then(() => {}); - }); + queryEncoded = await ctx.ds.serializeParams({ q: query }); + await ctx.ds.metricFindQuery(query, queryOptions).then(() => {}); + }); - it('should have the query form urlencoded', () => { - expect(requestQuery).toBe(queryEncoded); - }); + it('should have the query form urlencoded', () => { + expect(requestQuery).toBe(queryEncoded); + }); - it('should use the HTTP POST method', () => { - expect(requestMethod).toBe('POST'); - }); + it('should use the HTTP POST method', () => { + expect(requestMethod).toBe('POST'); + }); - it('should not have q as a query parameter', () => { - expect(requestQueryParameter).not.toHaveProperty('q'); + it('should not have q as a query parameter', () => { + expect(requestQueryParameter).not.toHaveProperty('q'); + }); }); }); }); diff --git a/public/app/plugins/datasource/loki/datasource.test.ts b/public/app/plugins/datasource/loki/datasource.test.ts index bf5ab13a691..693db6e1a54 100644 --- a/public/app/plugins/datasource/loki/datasource.test.ts +++ b/public/app/plugins/datasource/loki/datasource.test.ts @@ -2,13 +2,20 @@ import LokiDatasource, { RangeQueryOptions } from './datasource'; import { LokiQuery, LokiResultType, LokiResponse, LokiLegacyStreamResponse } from './types'; import { getQueryOptions } from 'test/helpers/getQueryOptions'; import { AnnotationQueryRequest, DataSourceApi, DataFrame, dateTime, TimeRange } from '@grafana/data'; -import { BackendSrv } from 'app/core/services/backend_srv'; import { TemplateSrv } from 'app/features/templating/template_srv'; import { CustomVariable } from 'app/features/templating/custom_variable'; import { makeMockLokiDatasource } from './mocks'; import { ExploreMode } from 'app/types'; import { of } from 'rxjs'; import omit from 'lodash/omit'; +import { backendSrv } from 'app/core/services/backend_srv'; // will use the version in __mocks__ + +jest.mock('@grafana/runtime', () => ({ + ...jest.requireActual('@grafana/runtime'), + getBackendSrv: () => backendSrv, +})); + +const datasourceRequestMock = jest.spyOn(backendSrv, 'datasourceRequest'); describe('LokiDatasource', () => { const instanceSettings: any = { @@ -42,8 +49,10 @@ describe('LokiDatasource', () => { }, }; - const backendSrvMock = { datasourceRequest: jest.fn() }; - const backendSrv = (backendSrvMock as unknown) as BackendSrv; + beforeEach(() => { + jest.clearAllMocks(); + datasourceRequestMock.mockImplementation(() => Promise.resolve()); + }); const templateSrvMock = ({ getAdhocFilters: (): any[] => [], @@ -56,7 +65,7 @@ describe('LokiDatasource', () => { beforeEach(() => { const customData = { ...(instanceSettings.jsonData || {}), maxLines: 20 }; const customSettings = { ...instanceSettings, jsonData: customData }; - ds = new LokiDatasource(customSettings, backendSrv, templateSrvMock); + ds = new LokiDatasource(customSettings, templateSrvMock); adjustIntervalSpy = jest.spyOn(ds, 'adjustInterval'); }); @@ -95,8 +104,8 @@ describe('LokiDatasource', () => { beforeEach(() => { const customData = { ...(instanceSettings.jsonData || {}), maxLines: 20 }; const customSettings = { ...instanceSettings, jsonData: customData }; - ds = new LokiDatasource(customSettings, backendSrv, templateSrvMock); - backendSrvMock.datasourceRequest = jest.fn(() => Promise.resolve(legacyTestResp)); + ds = new LokiDatasource(customSettings, templateSrvMock); + datasourceRequestMock.mockImplementation(() => Promise.resolve(legacyTestResp)); }); test('should try latest endpoint but fall back to legacy endpoint if it cannot be reached', async () => { @@ -112,14 +121,18 @@ describe('LokiDatasource', () => { }); describe('when querying', () => { - const testLimit = makeLimitTest(instanceSettings, backendSrvMock, backendSrv, templateSrvMock, legacyTestResp); let ds: LokiDatasource; + let testLimit: any; + + beforeAll(() => { + testLimit = makeLimitTest(instanceSettings, datasourceRequestMock, templateSrvMock, legacyTestResp); + }); beforeEach(() => { const customData = { ...(instanceSettings.jsonData || {}), maxLines: 20 }; const customSettings = { ...instanceSettings, jsonData: customData }; - ds = new LokiDatasource(customSettings, backendSrv, templateSrvMock); - backendSrvMock.datasourceRequest = jest.fn(() => Promise.resolve(testResp)); + ds = new LokiDatasource(customSettings, templateSrvMock); + datasourceRequestMock.mockImplementation(() => Promise.resolve(testResp)); }); test('should run instant query and range query when in metrics mode', async () => { @@ -183,11 +196,13 @@ describe('LokiDatasource', () => { test('should return series data', async () => { const customData = { ...(instanceSettings.jsonData || {}), maxLines: 20 }; const customSettings = { ...instanceSettings, jsonData: customData }; - const ds = new LokiDatasource(customSettings, backendSrv, templateSrvMock); - backendSrvMock.datasourceRequest = jest - .fn() - .mockReturnValueOnce(Promise.resolve(legacyTestResp)) - .mockReturnValueOnce(Promise.resolve(omit(legacyTestResp, 'status'))); + const ds = new LokiDatasource(customSettings, templateSrvMock); + datasourceRequestMock.mockImplementation( + jest + .fn() + .mockReturnValueOnce(Promise.resolve(legacyTestResp)) + .mockReturnValueOnce(Promise.resolve(omit(legacyTestResp, 'status'))) + ); const options = getQueryOptions({ targets: [{ expr: '{job="grafana"} |= "foo"', refId: 'B' }], @@ -209,7 +224,7 @@ describe('LokiDatasource', () => { beforeEach(() => { const customData = { ...(instanceSettings.jsonData || {}), maxLines: 20 }; const customSettings = { ...instanceSettings, jsonData: customData }; - ds = new LokiDatasource(customSettings, backendSrv, templateSrvMock); + ds = new LokiDatasource(customSettings, templateSrvMock); variable = new CustomVariable({}, {} as any); }); @@ -256,17 +271,15 @@ describe('LokiDatasource', () => { describe('and call succeeds', () => { beforeEach(async () => { - const backendSrv = ({ - async datasourceRequest() { - return Promise.resolve({ - status: 200, - data: { - data: ['avalue'], - }, - }); - }, - } as unknown) as BackendSrv; - ds = new LokiDatasource(instanceSettings, backendSrv, {} as TemplateSrv); + datasourceRequestMock.mockImplementation(async () => { + return Promise.resolve({ + status: 200, + data: { + values: ['avalue'], + }, + }); + }); + ds = new LokiDatasource(instanceSettings, {} as TemplateSrv); result = await ds.testDatasource(); }); @@ -278,7 +291,7 @@ describe('LokiDatasource', () => { describe('and call fails with 401 error', () => { let ds: LokiDatasource; beforeEach(() => { - backendSrvMock.datasourceRequest = jest.fn(() => + datasourceRequestMock.mockImplementation(() => Promise.reject({ statusText: 'Unauthorized', status: 401, @@ -290,7 +303,7 @@ describe('LokiDatasource', () => { const customData = { ...(instanceSettings.jsonData || {}), maxLines: 20 }; const customSettings = { ...instanceSettings, jsonData: customData }; - ds = new LokiDatasource(customSettings, backendSrv, templateSrvMock); + ds = new LokiDatasource(customSettings, templateSrvMock); }); it('should return error status and a detailed error message', async () => { @@ -302,16 +315,14 @@ describe('LokiDatasource', () => { describe('and call fails with 404 error', () => { beforeEach(async () => { - const backendSrv = ({ - async datasourceRequest() { - return Promise.reject({ - statusText: 'Not found', - status: 404, - data: '404 page not found', - }); - }, - } as unknown) as BackendSrv; - ds = new LokiDatasource(instanceSettings, backendSrv, {} as TemplateSrv); + datasourceRequestMock.mockImplementation(() => + Promise.reject({ + statusText: 'Not found', + status: 404, + data: '404 page not found', + }) + ); + ds = new LokiDatasource(instanceSettings, {} as TemplateSrv); result = await ds.testDatasource(); }); @@ -323,16 +334,14 @@ describe('LokiDatasource', () => { describe('and call fails with 502 error', () => { beforeEach(async () => { - const backendSrv = ({ - async datasourceRequest() { - return Promise.reject({ - statusText: 'Bad Gateway', - status: 502, - data: '', - }); - }, - } as unknown) as BackendSrv; - ds = new LokiDatasource(instanceSettings, backendSrv, {} as TemplateSrv); + datasourceRequestMock.mockImplementation(() => + Promise.reject({ + statusText: 'Bad Gateway', + status: 502, + data: '', + }) + ); + ds = new LokiDatasource(instanceSettings, {} as TemplateSrv); result = await ds.testDatasource(); }); @@ -344,7 +353,7 @@ describe('LokiDatasource', () => { }); describe('when creating a range query', () => { - const ds = new LokiDatasource(instanceSettings, backendSrv, templateSrvMock); + const ds = new LokiDatasource(instanceSettings, templateSrvMock); const query: LokiQuery = { expr: 'foo', refId: 'bar' }; // Loki v1 API has an issue with float step parameters, can be removed when API is fixed @@ -362,31 +371,33 @@ describe('LokiDatasource', () => { describe('annotationQuery', () => { it('should transform the loki data to annotation response', async () => { - const ds = new LokiDatasource(instanceSettings, backendSrv, templateSrvMock); - backendSrvMock.datasourceRequest = jest - .fn() - .mockReturnValueOnce( - Promise.resolve({ - data: [], - status: 404, - }) - ) - .mockReturnValueOnce( - Promise.resolve({ - data: { - streams: [ - { - entries: [{ ts: '2019-02-01T10:27:37.498180581Z', line: 'hello' }], - labels: '{label="value"}', - }, - { - entries: [{ ts: '2019-02-01T12:27:37.498180581Z', line: 'hello 2' }], - labels: '{label2="value2"}', - }, - ], - }, - }) - ); + const ds = new LokiDatasource(instanceSettings, templateSrvMock); + datasourceRequestMock.mockImplementation( + jest + .fn() + .mockReturnValueOnce( + Promise.resolve({ + data: [], + status: 404, + }) + ) + .mockReturnValueOnce( + Promise.resolve({ + data: { + streams: [ + { + entries: [{ ts: '2019-02-01T10:27:37.498180581Z', line: 'hello' }], + labels: '{label="value"}', + }, + { + entries: [{ ts: '2019-02-01T12:27:37.498180581Z', line: 'hello 2' }], + labels: '{label2="value2"}', + }, + ], + }, + }) + ) + ); const query = makeAnnotationQueryRequest(); const res = await ds.annotationQuery(query); @@ -400,7 +411,7 @@ describe('LokiDatasource', () => { }); describe('metricFindQuery', () => { - const ds = new LokiDatasource(instanceSettings, backendSrv, templateSrvMock); + const ds = new LokiDatasource(instanceSettings, templateSrvMock); const mocks = makeMetadataAndVersionsMocks(); mocks.forEach((mock, index) => { @@ -456,21 +467,15 @@ type LimitTestArgs = { maxLines?: number; expectedLimit: number; }; -function makeLimitTest( - instanceSettings: any, - backendSrvMock: any, - backendSrv: any, - templateSrvMock: any, - testResp: any -) { +function makeLimitTest(instanceSettings: any, datasourceRequestMock: any, templateSrvMock: any, testResp: any) { return ({ maxDataPoints, maxLines, expectedLimit }: LimitTestArgs) => { let settings = instanceSettings; if (Number.isFinite(maxLines)) { const customData = { ...(instanceSettings.jsonData || {}), maxLines: 20 }; settings = { ...instanceSettings, jsonData: customData }; } - const ds = new LokiDatasource(settings, backendSrv, templateSrvMock); - backendSrvMock.datasourceRequest = jest.fn(() => Promise.resolve(testResp)); + const ds = new LokiDatasource(settings, templateSrvMock); + datasourceRequestMock.mockImplementation(() => Promise.resolve(testResp)); const options = getQueryOptions({ targets: [{ expr: 'foo', refId: 'B' }] }); if (Number.isFinite(maxDataPoints)) { @@ -482,8 +487,8 @@ function makeLimitTest( ds.query(options); - expect(backendSrvMock.datasourceRequest.mock.calls.length).toBe(1); - expect(backendSrvMock.datasourceRequest.mock.calls[0][0].url).toContain(`limit=${expectedLimit}`); + expect(datasourceRequestMock.mock.calls.length).toBe(1); + expect(datasourceRequestMock.mock.calls[0][0].url).toContain(`limit=${expectedLimit}`); }; } diff --git a/public/app/plugins/datasource/loki/datasource.ts b/public/app/plugins/datasource/loki/datasource.ts index 796d4d08fc3..f59a67205e8 100644 --- a/public/app/plugins/datasource/loki/datasource.ts +++ b/public/app/plugins/datasource/loki/datasource.ts @@ -6,7 +6,8 @@ import { map, filter, catchError, switchMap, mergeMap } from 'rxjs/operators'; // Services & Utils import { dateMath } from '@grafana/data'; import { addLabelToSelector, keepSelectorFilters } from 'app/plugins/datasource/prometheus/add_label_to_query'; -import { BackendSrv, DatasourceRequestOptions } from 'app/core/services/backend_srv'; +import { DatasourceRequestOptions } from 'app/core/services/backend_srv'; +import { getBackendSrv } from '@grafana/runtime'; import { TemplateSrv } from 'app/features/templating/template_srv'; import { safeStringifyValue, convertToWebSocketUrl } from 'app/core/utils/explore'; import { @@ -84,11 +85,7 @@ export class LokiDatasource extends DataSourceApi { maxLines: number; /** @ngInject */ - constructor( - private instanceSettings: DataSourceInstanceSettings, - private backendSrv: BackendSrv, - private templateSrv: TemplateSrv - ) { + constructor(private instanceSettings: DataSourceInstanceSettings, private templateSrv: TemplateSrv) { super(instanceSettings); this.languageProvider = new LanguageProvider(this); @@ -122,7 +119,7 @@ export class LokiDatasource extends DataSourceApi { url, }; - return from(this.backendSrv.datasourceRequest(req)); + return from(getBackendSrv().datasourceRequest(req)); } query(options: DataQueryRequest): Observable { diff --git a/public/app/plugins/datasource/mssql/datasource.ts b/public/app/plugins/datasource/mssql/datasource.ts index 14853bd2021..8059f6165c2 100644 --- a/public/app/plugins/datasource/mssql/datasource.ts +++ b/public/app/plugins/datasource/mssql/datasource.ts @@ -1,6 +1,6 @@ import _ from 'lodash'; import ResponseParser from './response_parser'; -import { BackendSrv } from 'app/core/services/backend_srv'; +import { getBackendSrv } from '@grafana/runtime'; import { TemplateSrv } from 'app/features/templating/template_srv'; import { TimeSrv } from 'app/features/dashboard/services/TimeSrv'; //Types @@ -13,12 +13,7 @@ export class MssqlDatasource { interval: string; /** @ngInject */ - constructor( - instanceSettings: any, - private backendSrv: BackendSrv, - private templateSrv: TemplateSrv, - private timeSrv: TimeSrv - ) { + constructor(instanceSettings: any, private templateSrv: TemplateSrv, private timeSrv: TimeSrv) { this.name = instanceSettings.name; this.id = instanceSettings.id; this.responseParser = new ResponseParser(); @@ -81,7 +76,7 @@ export class MssqlDatasource { return Promise.resolve({ data: [] }); } - return this.backendSrv + return getBackendSrv() .datasourceRequest({ url: '/api/tsdb/query', method: 'POST', @@ -106,7 +101,7 @@ export class MssqlDatasource { format: 'table', }; - return this.backendSrv + return getBackendSrv() .datasourceRequest({ url: '/api/tsdb/query', method: 'POST', @@ -139,7 +134,7 @@ export class MssqlDatasource { to: range.to.valueOf().toString(), }; - return this.backendSrv + return getBackendSrv() .datasourceRequest({ url: '/api/tsdb/query', method: 'POST', @@ -149,7 +144,7 @@ export class MssqlDatasource { } testDatasource() { - return this.backendSrv + return getBackendSrv() .datasourceRequest({ url: '/api/tsdb/query', method: 'POST', diff --git a/public/app/plugins/datasource/mssql/specs/datasource.test.ts b/public/app/plugins/datasource/mssql/specs/datasource.test.ts index df545d89dff..3413f80edf9 100644 --- a/public/app/plugins/datasource/mssql/specs/datasource.test.ts +++ b/public/app/plugins/datasource/mssql/specs/datasource.test.ts @@ -4,19 +4,26 @@ import { CustomVariable } from 'app/features/templating/custom_variable'; import { dateTime } from '@grafana/data'; import { TemplateSrv } from 'app/features/templating/template_srv'; +import { backendSrv } from 'app/core/services/backend_srv'; // will use the version in __mocks__ + +jest.mock('@grafana/runtime', () => ({ + ...jest.requireActual('@grafana/runtime'), + getBackendSrv: () => backendSrv, +})); describe('MSSQLDatasource', () => { const templateSrv: TemplateSrv = new TemplateSrv(); + const datasourceRequestMock = jest.spyOn(backendSrv, 'datasourceRequest'); const ctx: any = { - backendSrv: {}, timeSrv: new TimeSrvStub(), }; beforeEach(() => { - ctx.instanceSettings = { name: 'mssql' }; + jest.clearAllMocks(); - ctx.ds = new MssqlDatasource(ctx.instanceSettings, ctx.backendSrv, templateSrv, ctx.timeSrv); + ctx.instanceSettings = { name: 'mssql' }; + ctx.ds = new MssqlDatasource(ctx.instanceSettings, templateSrv, ctx.timeSrv); }); describe('When performing annotationQuery', () => { @@ -54,9 +61,7 @@ describe('MSSQLDatasource', () => { }; beforeEach(() => { - ctx.backendSrv.datasourceRequest = (options: any) => { - return Promise.resolve({ data: response, status: 200 }); - }; + datasourceRequestMock.mockImplementation((options: any) => Promise.resolve({ data: response, status: 200 })); return ctx.ds.annotationQuery(options).then((data: any) => { results = data; @@ -102,9 +107,7 @@ describe('MSSQLDatasource', () => { }; beforeEach(() => { - ctx.backendSrv.datasourceRequest = (options: any) => { - return Promise.resolve({ data: response, status: 200 }); - }; + datasourceRequestMock.mockImplementation((options: any) => Promise.resolve({ data: response, status: 200 })); return ctx.ds.metricFindQuery(query).then((data: any) => { results = data; @@ -143,9 +146,7 @@ describe('MSSQLDatasource', () => { }; beforeEach(() => { - ctx.backendSrv.datasourceRequest = (options: any) => { - return Promise.resolve({ data: response, status: 200 }); - }; + datasourceRequestMock.mockImplementation((options: any) => Promise.resolve({ data: response, status: 200 })); return ctx.ds.metricFindQuery(query).then((data: any) => { results = data; @@ -186,10 +187,7 @@ describe('MSSQLDatasource', () => { }; beforeEach(() => { - ctx.backendSrv.datasourceRequest = (options: any) => { - return Promise.resolve({ data: response, status: 200 }); - }; - + datasourceRequestMock.mockImplementation((options: any) => Promise.resolve({ data: response, status: 200 })); return ctx.ds.metricFindQuery(query).then((data: any) => { results = data; }); @@ -229,10 +227,10 @@ describe('MSSQLDatasource', () => { beforeEach(() => { ctx.timeSrv.setTime(time); - ctx.backendSrv.datasourceRequest = (options: any) => { + datasourceRequestMock.mockImplementation((options: any) => { results = options.data; return Promise.resolve({ data: response, status: 200 }); - }; + }); return ctx.ds.metricFindQuery(query); }); diff --git a/public/app/plugins/datasource/mysql/datasource.ts b/public/app/plugins/datasource/mysql/datasource.ts index acf589c711f..7a5133331a5 100644 --- a/public/app/plugins/datasource/mysql/datasource.ts +++ b/public/app/plugins/datasource/mysql/datasource.ts @@ -1,7 +1,7 @@ import _ from 'lodash'; import ResponseParser from './response_parser'; import MysqlQuery from 'app/plugins/datasource/mysql/mysql_query'; -import { BackendSrv } from 'app/core/services/backend_srv'; +import { getBackendSrv } from '@grafana/runtime'; import { TemplateSrv } from 'app/features/templating/template_srv'; import { TimeSrv } from 'app/features/dashboard/services/TimeSrv'; //Types @@ -16,12 +16,7 @@ export class MysqlDatasource { interval: string; /** @ngInject */ - constructor( - instanceSettings: any, - private backendSrv: BackendSrv, - private templateSrv: TemplateSrv, - private timeSrv: TimeSrv - ) { + constructor(instanceSettings: any, private templateSrv: TemplateSrv, private timeSrv: TimeSrv) { this.name = instanceSettings.name; this.id = instanceSettings.id; this.responseParser = new ResponseParser(); @@ -83,7 +78,7 @@ export class MysqlDatasource { return Promise.resolve({ data: [] }); } - return this.backendSrv + return getBackendSrv() .datasourceRequest({ url: '/api/tsdb/query', method: 'POST', @@ -110,7 +105,7 @@ export class MysqlDatasource { format: 'table', }; - return this.backendSrv + return getBackendSrv() .datasourceRequest({ url: '/api/tsdb/query', method: 'POST', @@ -156,7 +151,7 @@ export class MysqlDatasource { data['to'] = optionalOptions.range.to.valueOf().toString(); } - return this.backendSrv + return getBackendSrv() .datasourceRequest({ url: '/api/tsdb/query', method: 'POST', @@ -166,7 +161,7 @@ export class MysqlDatasource { } testDatasource() { - return this.backendSrv + return getBackendSrv() .datasourceRequest({ url: '/api/tsdb/query', method: 'POST', diff --git a/public/app/plugins/datasource/mysql/specs/datasource.test.ts b/public/app/plugins/datasource/mysql/specs/datasource.test.ts index 62fff24ac3f..06c5cd82af3 100644 --- a/public/app/plugins/datasource/mysql/specs/datasource.test.ts +++ b/public/app/plugins/datasource/mysql/specs/datasource.test.ts @@ -1,20 +1,28 @@ import { MysqlDatasource } from '../datasource'; import { CustomVariable } from 'app/features/templating/custom_variable'; import { dateTime, toUtc } 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 { TemplateSrv } from 'app/features/templating/template_srv'; +jest.mock('@grafana/runtime', () => ({ + ...jest.requireActual('@grafana/runtime'), + getBackendSrv: () => backendSrv, +})); + describe('MySQLDatasource', () => { const instanceSettings = { name: 'mysql' }; - const backendSrv = {}; const templateSrv: TemplateSrv = new TemplateSrv(); + const datasourceRequestMock = jest.spyOn(backendSrv, 'datasourceRequest'); + + beforeEach(() => { + jest.clearAllMocks(); + }); const raw = { from: toUtc('2018-04-25 10:00'), to: toUtc('2018-04-25 11:00'), }; const ctx = { - backendSrv, timeSrvMock: { timeRange: () => ({ from: raw.from, @@ -25,7 +33,7 @@ describe('MySQLDatasource', () => { } as any; beforeEach(() => { - ctx.ds = new MysqlDatasource(instanceSettings, backendSrv as BackendSrv, templateSrv, ctx.timeSrvMock); + ctx.ds = new MysqlDatasource(instanceSettings, templateSrv, ctx.timeSrvMock); }); describe('When performing annotationQuery', () => { @@ -63,9 +71,8 @@ describe('MySQLDatasource', () => { }; beforeEach(() => { - ctx.backendSrv.datasourceRequest = jest.fn(options => { - return Promise.resolve({ data: response, status: 200 }); - }); + datasourceRequestMock.mockImplementation(options => Promise.resolve({ data: response, status: 200 })); + ctx.ds.annotationQuery(options).then((data: any) => { results = data; }); @@ -110,9 +117,8 @@ describe('MySQLDatasource', () => { }; beforeEach(() => { - ctx.backendSrv.datasourceRequest = jest.fn(options => { - return Promise.resolve({ data: response, status: 200 }); - }); + datasourceRequestMock.mockImplementation(options => Promise.resolve({ data: response, status: 200 })); + ctx.ds.metricFindQuery(query).then((data: any) => { results = data; }); @@ -151,7 +157,7 @@ describe('MySQLDatasource', () => { }; beforeEach(() => { - ctx.backendSrv.datasourceRequest = jest.fn(options => { + datasourceRequestMock.mockImplementation(options => { calledWith = options; return Promise.resolve({ data: response, status: 200 }); }); @@ -161,7 +167,7 @@ describe('MySQLDatasource', () => { }); it('should return list of all column values', () => { - expect(ctx.backendSrv.datasourceRequest).toBeCalledTimes(1); + expect(datasourceRequestMock).toBeCalledTimes(1); expect(calledWith.data.queries[0].rawSql).toBe("select title from atable where title LIKE 'aTit%'"); expect(results.length).toBe(6); }); @@ -193,7 +199,7 @@ describe('MySQLDatasource', () => { }; beforeEach(() => { - ctx.backendSrv.datasourceRequest = jest.fn(options => { + datasourceRequestMock.mockImplementation(options => { calledWith = options; return Promise.resolve({ data: response, status: 200 }); }); @@ -203,7 +209,7 @@ describe('MySQLDatasource', () => { }); it('should return list of all column values', () => { - expect(ctx.backendSrv.datasourceRequest).toBeCalledTimes(1); + expect(datasourceRequestMock).toBeCalledTimes(1); expect(calledWith.data.queries[0].rawSql).toBe("select title from atable where title LIKE '%'"); expect(results.length).toBe(6); }); @@ -234,9 +240,7 @@ describe('MySQLDatasource', () => { }; beforeEach(() => { - ctx.backendSrv.datasourceRequest = jest.fn(options => { - return Promise.resolve({ data: response, status: 200 }); - }); + datasourceRequestMock.mockImplementation(() => Promise.resolve({ data: response, status: 200 })); ctx.ds.metricFindQuery(query).then((data: any) => { results = data; }); @@ -276,9 +280,7 @@ describe('MySQLDatasource', () => { }; beforeEach(() => { - ctx.backendSrv.datasourceRequest = jest.fn(options => { - return Promise.resolve({ data: response, status: 200 }); - }); + datasourceRequestMock.mockImplementation(() => Promise.resolve({ data: response, status: 200 })); ctx.ds.metricFindQuery(query).then((data: any) => { results = data; }); diff --git a/public/app/plugins/datasource/opentsdb/datasource.ts b/public/app/plugins/datasource/opentsdb/datasource.ts index 7219f775283..c5beee6b338 100644 --- a/public/app/plugins/datasource/opentsdb/datasource.ts +++ b/public/app/plugins/datasource/opentsdb/datasource.ts @@ -1,7 +1,7 @@ import angular from 'angular'; import _ from 'lodash'; import { dateMath, DataQueryRequest, DataSourceApi } 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 { OpenTsdbOptions, OpenTsdbQuery } from './types'; @@ -19,9 +19,8 @@ export default class OpenTsDatasource extends DataSourceApi ({ + ...jest.requireActual('@grafana/runtime'), + getBackendSrv: () => backendSrv, +})); describe('opentsdb', () => { + const datasourceRequestMock = jest.spyOn(backendSrv, 'datasourceRequest'); + + beforeEach(() => { + jest.clearAllMocks(); + }); + const ctx = { - backendSrv: {}, ds: {}, templateSrv: { replace: (str: string) => str, @@ -11,7 +22,7 @@ describe('opentsdb', () => { const instanceSettings = { url: '', jsonData: { tsdbVersion: 1 } }; beforeEach(() => { - ctx.ctrl = new OpenTsDatasource(instanceSettings, ctx.backendSrv, ctx.templateSrv); + ctx.ctrl = new OpenTsDatasource(instanceSettings, ctx.templateSrv); }); describe('When performing metricFindQuery', () => { @@ -19,20 +30,22 @@ describe('opentsdb', () => { let requestOptions: any; beforeEach(async () => { - ctx.backendSrv.datasourceRequest = await ((options: any) => { - requestOptions = options; - return Promise.resolve({ - data: [ - { - target: 'prod1.count', - datapoints: [ - [10, 1], - [12, 1], - ], - }, - ], - }); - }); + datasourceRequestMock.mockImplementation( + await ((options: any) => { + requestOptions = options; + return Promise.resolve({ + data: [ + { + target: 'prod1.count', + datapoints: [ + [10, 1], + [12, 1], + ], + }, + ], + }); + }) + ); }); it('metrics() should generate api suggest query', () => { diff --git a/public/app/plugins/datasource/postgres/datasource.ts b/public/app/plugins/datasource/postgres/datasource.ts index 8d2e02aa2d5..44ef009baeb 100644 --- a/public/app/plugins/datasource/postgres/datasource.ts +++ b/public/app/plugins/datasource/postgres/datasource.ts @@ -1,7 +1,7 @@ import _ from 'lodash'; import ResponseParser from './response_parser'; import PostgresQuery from 'app/plugins/datasource/postgres/postgres_query'; -import { BackendSrv } from 'app/core/services/backend_srv'; +import { getBackendSrv } from '@grafana/runtime'; import { TemplateSrv } from 'app/features/templating/template_srv'; import { TimeSrv } from 'app/features/dashboard/services/TimeSrv'; //Types @@ -19,7 +19,6 @@ export class PostgresDatasource { /** @ngInject */ constructor( instanceSettings: { name: any; id?: any; jsonData?: any }, - private backendSrv: BackendSrv, private templateSrv: TemplateSrv, private timeSrv: TimeSrv ) { @@ -85,7 +84,7 @@ export class PostgresDatasource { return Promise.resolve({ data: [] }); } - return this.backendSrv + return getBackendSrv() .datasourceRequest({ url: '/api/tsdb/query', method: 'POST', @@ -112,7 +111,7 @@ export class PostgresDatasource { format: 'table', }; - return this.backendSrv + return getBackendSrv() .datasourceRequest({ url: '/api/tsdb/query', method: 'POST', @@ -151,7 +150,7 @@ export class PostgresDatasource { to: range.to.valueOf().toString(), }; - return this.backendSrv + return getBackendSrv() .datasourceRequest({ url: '/api/tsdb/query', method: 'POST', diff --git a/public/app/plugins/datasource/postgres/specs/datasource.test.ts b/public/app/plugins/datasource/postgres/specs/datasource.test.ts index ec2504b6779..4c6f415408f 100644 --- a/public/app/plugins/datasource/postgres/specs/datasource.test.ts +++ b/public/app/plugins/datasource/postgres/specs/datasource.test.ts @@ -1,20 +1,29 @@ import { PostgresDatasource } from '../datasource'; import { CustomVariable } from 'app/features/templating/custom_variable'; import { dateTime, toUtc } 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 { TemplateSrv } from 'app/features/templating/template_srv'; +jest.mock('@grafana/runtime', () => ({ + ...jest.requireActual('@grafana/runtime'), + getBackendSrv: () => backendSrv, +})); + describe('PostgreSQLDatasource', () => { + const datasourceRequestMock = jest.spyOn(backendSrv, 'datasourceRequest'); + + beforeEach(() => { + jest.clearAllMocks(); + }); + const instanceSettings = { name: 'postgresql' }; - const backendSrv = {}; const templateSrv: TemplateSrv = new TemplateSrv(); const raw = { from: toUtc('2018-04-25 10:00'), to: toUtc('2018-04-25 11:00'), }; const ctx = { - backendSrv, timeSrvMock: { timeRange: () => ({ from: raw.from, @@ -25,7 +34,7 @@ describe('PostgreSQLDatasource', () => { } as any; beforeEach(() => { - ctx.ds = new PostgresDatasource(instanceSettings, backendSrv as BackendSrv, templateSrv, ctx.timeSrvMock); + ctx.ds = new PostgresDatasource(instanceSettings, templateSrv, ctx.timeSrvMock); }); describe('When performing annotationQuery', () => { @@ -63,9 +72,8 @@ describe('PostgreSQLDatasource', () => { }; beforeEach(() => { - ctx.backendSrv.datasourceRequest = jest.fn(options => { - return Promise.resolve({ data: response, status: 200 }); - }); + datasourceRequestMock.mockImplementation(options => Promise.resolve({ data: response, status: 200 })); + ctx.ds.annotationQuery(options).then((data: any) => { results = data; }); @@ -110,9 +118,8 @@ describe('PostgreSQLDatasource', () => { }; beforeEach(() => { - ctx.backendSrv.datasourceRequest = jest.fn(options => { - return Promise.resolve({ data: response, status: 200 }); - }); + datasourceRequestMock.mockImplementation(options => Promise.resolve({ data: response, status: 200 })); + ctx.ds.metricFindQuery(query).then((data: any) => { results = data; }); @@ -151,17 +158,18 @@ describe('PostgreSQLDatasource', () => { }; beforeEach(() => { - ctx.backendSrv.datasourceRequest = jest.fn(options => { + datasourceRequestMock.mockImplementation(options => { calledWith = options; return Promise.resolve({ data: response, status: 200 }); }); + ctx.ds.metricFindQuery(query, { searchFilter: 'aTit' }).then((data: any) => { results = data; }); }); it('should return list of all column values', () => { - expect(ctx.backendSrv.datasourceRequest).toBeCalledTimes(1); + expect(datasourceRequestMock).toBeCalledTimes(1); expect(calledWith.data.queries[0].rawSql).toBe("select title from atable where title LIKE 'aTit%'"); expect(results.length).toBe(6); }); @@ -193,17 +201,18 @@ describe('PostgreSQLDatasource', () => { }; beforeEach(() => { - ctx.backendSrv.datasourceRequest = jest.fn(options => { + datasourceRequestMock.mockImplementation(options => { calledWith = options; return Promise.resolve({ data: response, status: 200 }); }); + ctx.ds.metricFindQuery(query, {}).then((data: any) => { results = data; }); }); it('should return list of all column values', () => { - expect(ctx.backendSrv.datasourceRequest).toBeCalledTimes(1); + expect(datasourceRequestMock).toBeCalledTimes(1); expect(calledWith.data.queries[0].rawSql).toBe("select title from atable where title LIKE '%'"); expect(results.length).toBe(6); }); @@ -234,9 +243,8 @@ describe('PostgreSQLDatasource', () => { }; beforeEach(() => { - ctx.backendSrv.datasourceRequest = jest.fn(options => { - return Promise.resolve({ data: response, status: 200 }); - }); + datasourceRequestMock.mockImplementation(options => Promise.resolve({ data: response, status: 200 })); + ctx.ds.metricFindQuery(query).then((data: any) => { results = data; }); @@ -276,9 +284,8 @@ describe('PostgreSQLDatasource', () => { }; beforeEach(() => { - ctx.backendSrv.datasourceRequest = jest.fn(options => { - return Promise.resolve({ data: response, status: 200 }); - }); + datasourceRequestMock.mockImplementation(options => Promise.resolve({ data: response, status: 200 })); + ctx.ds.metricFindQuery(query).then((data: any) => { results = data; }); diff --git a/public/app/plugins/datasource/prometheus/datasource.test.ts b/public/app/plugins/datasource/prometheus/datasource.test.ts index c9c9047701f..a842035588e 100644 --- a/public/app/plugins/datasource/prometheus/datasource.test.ts +++ b/public/app/plugins/datasource/prometheus/datasource.test.ts @@ -18,17 +18,23 @@ import { PromOptions, PromQuery } from './types'; import templateSrv from 'app/features/templating/template_srv'; import { getTimeSrv, TimeSrv } from 'app/features/dashboard/services/TimeSrv'; import { CustomVariable } from 'app/features/templating/custom_variable'; -import * as Backend from 'app/core/services/backend_srv'; -import { BackendSrv } from '@grafana/runtime/src/services/backendSrv'; + +const datasourceRequestMock = jest.fn().mockResolvedValue(createDefaultPromResponse()); jest.mock('./metric_find_query'); -jest.mock('app/core/services/backend_srv'); +jest.mock('@grafana/runtime', () => ({ + getBackendSrv: () => ({ + datasourceRequest: datasourceRequestMock, + }), +})); + jest.mock('app/features/templating/template_srv', () => { return { getAdhocFilters: jest.fn(() => [] as any[]), replace: jest.fn((a: string) => a), }; }); + jest.mock('app/features/dashboard/services/TimeSrv', () => ({ __esModule: true, getTimeSrv: jest.fn().mockReturnValue({ @@ -41,13 +47,12 @@ jest.mock('app/features/dashboard/services/TimeSrv', () => ({ }), })); -const getBackendSrvMock = (Backend.getBackendSrv as any) as jest.Mock; const getAdhocFiltersMock = (templateSrv.getAdhocFilters as any) as jest.Mock; const replaceMock = (templateSrv.replace as any) as jest.Mock; const getTimeSrvMock = (getTimeSrv as any) as jest.Mock; beforeEach(() => { - getBackendSrvMock.mockClear(); + datasourceRequestMock.mockClear(); getAdhocFiltersMock.mockClear(); replaceMock.mockClear(); getTimeSrvMock.mockClear(); @@ -126,34 +131,13 @@ describe('PrometheusDatasource', () => { }); describe('Datasource metadata requests', () => { - const originalBackendMock = getBackendSrvMock(); - - afterAll(() => { - getBackendSrvMock.mockImplementation(() => originalBackendMock); - }); - it('should perform a GET request with the default config', () => { - const datasourceRequestMock = jest.fn(); - getBackendSrvMock.mockImplementation( - () => - ({ - datasourceRequest: datasourceRequestMock, - } as any) - ); - ds.metadataRequest('/foo'); expect(datasourceRequestMock.mock.calls.length).toBe(1); expect(datasourceRequestMock.mock.calls[0][0].method).toBe('GET'); }); it('should still perform a GET request with the DS HTTP method set to POST', () => { - const datasourceRequestMock = jest.fn(); - getBackendSrvMock.mockImplementation( - () => - ({ - datasourceRequest: datasourceRequestMock, - } as any) - ); const postSettings = _.cloneDeep(instanceSettings); postSettings.jsonData.httpMethod = 'POST'; const promDs = new PrometheusDatasource(postSettings); @@ -215,26 +199,19 @@ describe('PrometheusDatasource', () => { }); describe('When performing performSuggestQuery', () => { - const originalBackendMock = getBackendSrvMock(); - it('should cache response', async () => { - const datasourceRequestMock = Promise.resolve({ - status: 'success', - data: { data: ['value1', 'value2', 'value3'] }, - }); - - getBackendSrvMock.mockImplementation( - () => - ({ - datasourceRequest: () => datasourceRequestMock, - } as any) + datasourceRequestMock.mockImplementation(() => + Promise.resolve({ + status: 'success', + data: { data: ['value1', 'value2', 'value3'] }, + }) ); let results = await ds.performSuggestQuery('value', true); expect(results).toHaveLength(3); - getBackendSrvMock.mockImplementation(() => originalBackendMock); + datasourceRequestMock.mockImplementation(jest.fn()); results = await ds.performSuggestQuery('value', true); expect(results).toHaveLength(3); @@ -596,14 +573,14 @@ describe('PrometheusDatasource', () => { }, }, }; - getBackendSrvMock().datasourceRequest = jest.fn(() => Promise.resolve(response)); + datasourceRequestMock.mockImplementation(() => Promise.resolve(response)); ds.query(query as any).subscribe((data: any) => { results = data; }); }); it('should generate the correct query', () => { - const res = (getBackendSrvMock().datasourceRequest as jest.Mock).mock.calls[0][0]; + const res = datasourceRequestMock.mock.calls[0][0]; expect(res.method).toBe('GET'); expect(res.url).toBe(urlExpected); }); @@ -632,7 +609,7 @@ describe('PrometheusDatasource', () => { }; it('should generate an error', () => { - getBackendSrvMock().datasourceRequest = jest.fn(() => Promise.reject(response)); + datasourceRequestMock.mockImplementation(() => Promise.reject(response)); ds.query(query as any).subscribe((e: any) => { results = e.message; expect(results).toBe(`"${errMessage}"`); @@ -677,7 +654,7 @@ describe('PrometheusDatasource', () => { }, }; - getBackendSrvMock().datasourceRequest = jest.fn(() => Promise.resolve(response)); + datasourceRequestMock.mockImplementation(() => Promise.resolve(response)); ds.query(query as any).subscribe((data: any) => { results = data; @@ -740,14 +717,14 @@ describe('PrometheusDatasource', () => { }, }; - getBackendSrvMock().datasourceRequest = jest.fn(() => Promise.resolve(response)); + datasourceRequestMock.mockImplementation(() => Promise.resolve(response)); ds.query(query as any).subscribe((data: any) => { results = data; }); }); it('should generate the correct query', () => { - const res = (getBackendSrvMock().datasourceRequest as jest.Mock).mock.calls[0][0]; + const res = datasourceRequestMock.mock.calls[0][0]; expect(res.method).toBe('GET'); expect(res.url).toBe(urlExpected); }); @@ -796,7 +773,7 @@ describe('PrometheusDatasource', () => { describe('when time series query is cancelled', () => { it('should return empty results', async () => { - getBackendSrvMock().datasourceRequest = jest.fn(() => Promise.resolve({ cancelled: true })); + datasourceRequestMock.mockImplementation(() => Promise.resolve({ cancelled: true })); await ds.annotationQuery(options).then((data: any) => { results = data; @@ -809,7 +786,7 @@ describe('PrometheusDatasource', () => { describe('not use useValueForTime', () => { beforeEach(async () => { options.annotation.useValueForTime = false; - getBackendSrvMock().datasourceRequest = jest.fn(() => Promise.resolve(response)); + datasourceRequestMock.mockImplementation(() => Promise.resolve(response)); await ds.annotationQuery(options).then((data: any) => { results = data; @@ -828,7 +805,7 @@ describe('PrometheusDatasource', () => { describe('use useValueForTime', () => { beforeEach(async () => { options.annotation.useValueForTime = true; - getBackendSrvMock().datasourceRequest = jest.fn(() => Promise.resolve(response)); + datasourceRequestMock.mockImplementation(() => Promise.resolve(response)); await ds.annotationQuery(options).then((data: any) => { results = data; @@ -842,7 +819,7 @@ describe('PrometheusDatasource', () => { describe('step parameter', () => { beforeEach(() => { - getBackendSrvMock().datasourceRequest = jest.fn(() => Promise.resolve(response)); + datasourceRequestMock.mockImplementation(() => Promise.resolve(response)); }); it('should use default step for short range if no interval is given', () => { @@ -854,7 +831,7 @@ describe('PrometheusDatasource', () => { }, }; ds.annotationQuery(query); - const req = (getBackendSrvMock().datasourceRequest as jest.Mock).mock.calls[0][0]; + const req = datasourceRequestMock.mock.calls[0][0]; expect(req.url).toContain('step=60'); }); @@ -872,7 +849,7 @@ describe('PrometheusDatasource', () => { }, }; ds.annotationQuery(query); - const req = (getBackendSrvMock().datasourceRequest as jest.Mock).mock.calls[0][0]; + const req = datasourceRequestMock.mock.calls[0][0]; expect(req.url).toContain('step=10'); }); @@ -890,7 +867,7 @@ describe('PrometheusDatasource', () => { }, }; ds.annotationQuery(query); - const req = (getBackendSrvMock().datasourceRequest as jest.Mock).mock.calls[0][0]; + const req = datasourceRequestMock.mock.calls[0][0]; expect(req.url).toContain('step=10'); }); @@ -903,7 +880,7 @@ describe('PrometheusDatasource', () => { }, }; ds.annotationQuery(query); - const req = (getBackendSrvMock().datasourceRequest as jest.Mock).mock.calls[0][0]; + const req = datasourceRequestMock.mock.calls[0][0]; // Range in seconds: (to - from) / 1000 // Max_datapoints: 11000 // Step: range / max_datapoints @@ -937,7 +914,7 @@ describe('PrometheusDatasource', () => { }, }; - getBackendSrvMock().datasourceRequest = jest.fn(() => Promise.resolve(response)); + datasourceRequestMock.mockImplementation(() => Promise.resolve(response)); ds.query(query as any).subscribe((data: any) => { results = data; }); @@ -973,9 +950,9 @@ describe('PrometheusDatasource', () => { }; const urlExpected = 'proxied/api/v1/query_range?query=test&start=60&end=420&step=10'; - getBackendSrvMock().datasourceRequest = jest.fn(() => Promise.resolve(response)); + datasourceRequestMock.mockImplementation(() => Promise.resolve(response)); ds.query(query as any); - const res = (getBackendSrvMock().datasourceRequest as jest.Mock).mock.calls[0][0]; + const res = datasourceRequestMock.mock.calls[0][0]; expect(res.method).toBe('GET'); expect(res.url).toBe(urlExpected); }); @@ -988,9 +965,9 @@ describe('PrometheusDatasource', () => { interval: '100ms', }; const urlExpected = 'proxied/api/v1/query_range?query=test&start=60&end=420&step=1'; - getBackendSrvMock().datasourceRequest = jest.fn(() => Promise.resolve(response)); + datasourceRequestMock.mockImplementation(() => Promise.resolve(response)); ds.query(query as any); - const res = (getBackendSrvMock().datasourceRequest as jest.Mock).mock.calls[0][0]; + const res = datasourceRequestMock.mock.calls[0][0]; expect(res.method).toBe('GET'); expect(res.url).toBe(urlExpected); }); @@ -1008,9 +985,9 @@ describe('PrometheusDatasource', () => { interval: '10s', }; const urlExpected = 'proxied/api/v1/query_range?query=test&start=60&end=420&step=10'; - getBackendSrvMock().datasourceRequest = jest.fn(() => Promise.resolve(response)); + datasourceRequestMock.mockImplementation(() => Promise.resolve(response)); ds.query(query as any); - const res = (getBackendSrvMock().datasourceRequest as jest.Mock).mock.calls[0][0]; + const res = datasourceRequestMock.mock.calls[0][0]; expect(res.method).toBe('GET'); expect(res.url).toBe(urlExpected); }); @@ -1025,9 +1002,9 @@ describe('PrometheusDatasource', () => { const end = 7 * 60 * 60; const start = 60 * 60; const urlExpected = 'proxied/api/v1/query_range?query=test&start=' + start + '&end=' + end + '&step=2'; - getBackendSrvMock().datasourceRequest = jest.fn(() => Promise.resolve(response)); + datasourceRequestMock.mockImplementation(() => Promise.resolve(response)); ds.query(query as any); - const res = (getBackendSrvMock().datasourceRequest as jest.Mock).mock.calls[0][0]; + const res = datasourceRequestMock.mock.calls[0][0]; expect(res.method).toBe('GET'); expect(res.url).toBe(urlExpected); }); @@ -1047,9 +1024,9 @@ describe('PrometheusDatasource', () => { }; // times get rounded up to interval const urlExpected = 'proxied/api/v1/query_range?query=test&start=50&end=400&step=50'; - getBackendSrvMock().datasourceRequest = jest.fn(() => Promise.resolve(response)); + datasourceRequestMock.mockImplementation(() => Promise.resolve(response)); ds.query(query as any); - const res = (getBackendSrvMock().datasourceRequest as jest.Mock).mock.calls[0][0]; + const res = datasourceRequestMock.mock.calls[0][0]; expect(res.method).toBe('GET'); expect(res.url).toBe(urlExpected); }); @@ -1068,9 +1045,9 @@ describe('PrometheusDatasource', () => { interval: '5s', }; const urlExpected = 'proxied/api/v1/query_range?query=test' + '&start=60&end=420&step=15'; - getBackendSrvMock().datasourceRequest = jest.fn(() => Promise.resolve(response)); + datasourceRequestMock.mockImplementation(() => Promise.resolve(response)); ds.query(query as any); - const res = (getBackendSrvMock().datasourceRequest as jest.Mock).mock.calls[0][0]; + const res = datasourceRequestMock.mock.calls[0][0]; expect(res.method).toBe('GET'); expect(res.url).toBe(urlExpected); }); @@ -1090,9 +1067,9 @@ describe('PrometheusDatasource', () => { }; // times get aligned to interval const urlExpected = 'proxied/api/v1/query_range?query=test' + '&start=0&end=400&step=100'; - getBackendSrvMock().datasourceRequest = jest.fn(() => Promise.resolve(response)); + datasourceRequestMock.mockImplementation(() => Promise.resolve(response)); ds.query(query as any); - const res = (getBackendSrvMock().datasourceRequest as jest.Mock).mock.calls[0][0]; + const res = datasourceRequestMock.mock.calls[0][0]; expect(res.method).toBe('GET'); expect(res.url).toBe(urlExpected); }); @@ -1112,9 +1089,9 @@ describe('PrometheusDatasource', () => { const end = 7 * 24 * 60 * 60; const start = 0; const urlExpected = 'proxied/api/v1/query_range?query=test' + '&start=' + start + '&end=' + end + '&step=100'; - getBackendSrvMock().datasourceRequest = jest.fn(() => Promise.resolve(response)); + datasourceRequestMock.mockImplementation(() => Promise.resolve(response)); ds.query(query as any); - const res = (getBackendSrvMock().datasourceRequest as jest.Mock).mock.calls[0][0]; + const res = datasourceRequestMock.mock.calls[0][0]; expect(res.method).toBe('GET'); expect(res.url).toBe(urlExpected); }); @@ -1145,9 +1122,9 @@ describe('PrometheusDatasource', () => { ); const urlExpected = 'proxied/api/v1/query_range?query=test' + '&start=' + adjusted.start + '&end=' + adjusted.end + '&step=' + step; - getBackendSrvMock().datasourceRequest = jest.fn(() => Promise.resolve(response)); + datasourceRequestMock.mockImplementation(() => Promise.resolve(response)); ds.query(query as any); - const res = (getBackendSrvMock().datasourceRequest as jest.Mock).mock.calls[0][0]; + const res = datasourceRequestMock.mock.calls[0][0]; expect(res.method).toBe('GET'); expect(res.url).toBe(urlExpected); }); @@ -1187,9 +1164,9 @@ describe('PrometheusDatasource', () => { '&start=60&end=420&step=10'; templateSrv.replace = jest.fn(str => str); - getBackendSrvMock().datasourceRequest = jest.fn(() => Promise.resolve(response)); + datasourceRequestMock.mockImplementation(() => Promise.resolve(response)); ds.query(query as any); - const res = (getBackendSrvMock().datasourceRequest as jest.Mock).mock.calls[0][0]; + const res = datasourceRequestMock.mock.calls[0][0]; expect(res.method).toBe('GET'); expect(res.url).toBe(urlExpected); @@ -1226,10 +1203,10 @@ describe('PrometheusDatasource', () => { 'proxied/api/v1/query_range?query=' + encodeURIComponent('rate(test[$__interval])') + '&start=60&end=420&step=10'; - getBackendSrvMock().datasourceRequest = jest.fn(() => Promise.resolve(response)); + datasourceRequestMock.mockImplementation(() => Promise.resolve(response)); templateSrv.replace = jest.fn(str => str); ds.query(query as any); - const res = (getBackendSrvMock().datasourceRequest as jest.Mock).mock.calls[0][0]; + const res = datasourceRequestMock.mock.calls[0][0]; expect(res.method).toBe('GET'); expect(res.url).toBe(urlExpected); @@ -1267,10 +1244,10 @@ describe('PrometheusDatasource', () => { 'proxied/api/v1/query_range?query=' + encodeURIComponent('rate(test[$__interval])') + '&start=0&end=400&step=100'; - getBackendSrvMock().datasourceRequest = jest.fn(() => Promise.resolve(response)); + datasourceRequestMock.mockImplementation(() => Promise.resolve(response)); templateSrv.replace = jest.fn(str => str); ds.query(query as any); - const res = (getBackendSrvMock().datasourceRequest as jest.Mock).mock.calls[0][0]; + const res = datasourceRequestMock.mock.calls[0][0]; expect(res.method).toBe('GET'); expect(res.url).toBe(urlExpected); @@ -1315,9 +1292,9 @@ describe('PrometheusDatasource', () => { '&start=50&end=400&step=50'; templateSrv.replace = jest.fn(str => str); - getBackendSrvMock().datasourceRequest = jest.fn(() => Promise.resolve(response)); + datasourceRequestMock.mockImplementation(() => Promise.resolve(response)); ds.query(query as any); - const res = (getBackendSrvMock().datasourceRequest as jest.Mock).mock.calls[0][0]; + const res = datasourceRequestMock.mock.calls[0][0]; expect(res.method).toBe('GET'); expect(res.url).toBe(urlExpected); @@ -1356,9 +1333,9 @@ describe('PrometheusDatasource', () => { encodeURIComponent('rate(test[$__interval])') + '&start=60&end=420&step=15'; - getBackendSrvMock().datasourceRequest = jest.fn(() => Promise.resolve(response)); + datasourceRequestMock.mockImplementation(() => Promise.resolve(response)); ds.query(query as any); - const res = (getBackendSrvMock().datasourceRequest as jest.Mock).mock.calls[0][0]; + const res = datasourceRequestMock.mock.calls[0][0]; expect(res.method).toBe('GET'); expect(res.url).toBe(urlExpected); @@ -1412,10 +1389,10 @@ describe('PrometheusDatasource', () => { adjusted.end + '&step=' + step; - getBackendSrvMock().datasourceRequest = jest.fn(() => Promise.resolve(response)); + datasourceRequestMock.mockImplementation(() => Promise.resolve(response)); templateSrv.replace = jest.fn(str => str); ds.query(query as any); - const res = (getBackendSrvMock().datasourceRequest as jest.Mock).mock.calls[0][0]; + const res = datasourceRequestMock.mock.calls[0][0]; expect(res.method).toBe('GET'); expect(res.url).toBe(urlExpected); @@ -1464,9 +1441,9 @@ describe('PrometheusDatasource', () => { )}&start=0&end=3600&step=60`; templateSrv.replace = jest.fn(str => str); - getBackendSrvMock().datasourceRequest = jest.fn(() => Promise.resolve(response)); + datasourceRequestMock.mockImplementation(() => Promise.resolve(response)); ds.query(query as any); - const res = (getBackendSrvMock().datasourceRequest as jest.Mock).mock.calls[0][0]; + const res = datasourceRequestMock.mock.calls[0][0]; expect(res.url).toBe(urlExpected); // @ts-ignore @@ -1533,14 +1510,14 @@ describe('PrometheusDatasource for POST', () => { }, }, }; - getBackendSrvMock().datasourceRequest = jest.fn(() => Promise.resolve(response)); + datasourceRequestMock.mockImplementation(() => Promise.resolve(response)); ds.query(query as any).subscribe((data: any) => { results = data; }); }); it('should generate the correct query', () => { - const res = (getBackendSrvMock().datasourceRequest as jest.Mock).mock.calls[0][0]; + const res = datasourceRequestMock.mock.calls[0][0]; expect(res.method).toBe('POST'); expect(res.url).toBe(urlExpected); expect(res.data).toEqual(dataExpected); @@ -1799,3 +1776,21 @@ function createDataRequest(targets: any[], overrides?: Partial return Object.assign(defaults, overrides || {}) as DataQueryRequest; } + +function createDefaultPromResponse() { + return { + data: { + data: { + result: [ + { + metric: { + __name__: 'test_metric', + }, + values: [[1568369640, 1]], + }, + ], + resultType: 'matrix', + }, + }, + }; +} diff --git a/public/app/plugins/datasource/prometheus/datasource.ts b/public/app/plugins/datasource/prometheus/datasource.ts index c3ece16eb39..4f2eded5a83 100644 --- a/public/app/plugins/datasource/prometheus/datasource.ts +++ b/public/app/plugins/datasource/prometheus/datasource.ts @@ -25,7 +25,7 @@ import { filter, map, tap } from 'rxjs/operators'; import PrometheusMetricFindQuery from './metric_find_query'; import { ResultTransformer } from './result_transformer'; import PrometheusLanguageProvider from './language_provider'; -import { getBackendSrv } from 'app/core/services/backend_srv'; +import { getBackendSrv } from '@grafana/runtime'; import addLabelToQuery from './add_label_to_query'; import { getQueryHints } from './query_hints'; import { expandRecordingRules } from './language_utils'; diff --git a/public/app/plugins/datasource/prometheus/metric_find_query.test.ts b/public/app/plugins/datasource/prometheus/metric_find_query.test.ts index 9e53eada694..99e5cd5fd88 100644 --- a/public/app/plugins/datasource/prometheus/metric_find_query.test.ts +++ b/public/app/plugins/datasource/prometheus/metric_find_query.test.ts @@ -1,13 +1,9 @@ import { PrometheusDatasource } from './datasource'; import PrometheusMetricFindQuery from './metric_find_query'; import { toUtc, DataSourceInstanceSettings } from '@grafana/data'; +import { backendSrv } from 'app/core/services/backend_srv'; // will use the version in __mocks__ import { PromOptions } from './types'; -import templateSrv from 'app/features/templating/template_srv'; -import { getTimeSrv, TimeSrv } from 'app/features/dashboard/services/TimeSrv'; -import * as Backend from 'app/core/services/backend_srv'; -import { BackendSrv } from '@grafana/runtime/src/services/backendSrv'; - jest.mock('app/features/templating/template_srv', () => { return { getAdhocFilters: jest.fn(() => [] as any[]), @@ -15,15 +11,12 @@ jest.mock('app/features/templating/template_srv', () => { }; }); -const getAdhocFiltersMock = (templateSrv.getAdhocFilters as any) as jest.Mock; -const replaceMock = (templateSrv.replace as any) as jest.Mock; -jest.mock('app/core/services/backend_srv'); +jest.mock('@grafana/runtime', () => ({ + ...jest.requireActual('@grafana/runtime'), + getBackendSrv: () => backendSrv, +})); -const backendSrvMock = { - datasourceRequest: jest.fn(() => Promise.resolve({})), -}; -const getBackendSrvMock = (Backend.getBackendSrv as any) as jest.Mock; -getBackendSrvMock.mockImplementation(() => backendSrvMock as any); +const datasourceRequestMock = jest.spyOn(backendSrv, 'datasourceRequest'); const instanceSettings = ({ url: 'proxied', @@ -50,31 +43,18 @@ jest.mock('app/features/dashboard/services/TimeSrv', () => ({ }), })); -const getTimeSrvMock = (getTimeSrv as any) as jest.Mock; - beforeEach(() => { - getBackendSrvMock.mockClear(); - getAdhocFiltersMock.mockClear(); - replaceMock.mockClear(); - getAdhocFiltersMock.mockClear(); - getTimeSrvMock.mockClear(); + jest.clearAllMocks(); }); describe('PrometheusMetricFindQuery', () => { - let datasourceRequestMock: any; let ds: PrometheusDatasource; beforeEach(() => { ds = new PrometheusDatasource(instanceSettings); }); const setupMetricFindQuery = (data: any) => { - datasourceRequestMock = jest.fn(() => Promise.resolve({ status: 'success', data: data.response })); - getBackendSrvMock.mockImplementation( - () => - ({ - datasourceRequest: datasourceRequestMock, - } as any) - ); + datasourceRequestMock.mockImplementation(() => Promise.resolve({ status: 'success', data: data.response })); return new PrometheusMetricFindQuery(ds, data.query); }; @@ -176,7 +156,7 @@ describe('PrometheusMetricFindQuery', () => { ], }, }); - const results = await query.process(); + const results: any = await query.process(); expect(results).toHaveLength(2); expect(results[0].text).toBe('value1'); @@ -224,7 +204,7 @@ describe('PrometheusMetricFindQuery', () => { }, }, }); - const results = await query.process(); + const results: any = await query.process(); expect(results).toHaveLength(1); expect(results[0].text).toBe('metric{job="testjob"} 3846 1443454528000'); @@ -248,7 +228,7 @@ describe('PrometheusMetricFindQuery', () => { ], }, }); - const results = await query.process(); + const results: any = await query.process(); expect(results).toHaveLength(3); expect(results[0].text).toBe('up{instance="127.0.0.1:1234",job="job1"}'); diff --git a/public/app/plugins/datasource/stackdriver/datasource.ts b/public/app/plugins/datasource/stackdriver/datasource.ts index 7333dfd1d06..41d65f889b5 100644 --- a/public/app/plugins/datasource/stackdriver/datasource.ts +++ b/public/app/plugins/datasource/stackdriver/datasource.ts @@ -4,7 +4,7 @@ import _ from 'lodash'; import StackdriverMetricFindQuery from './StackdriverMetricFindQuery'; import { StackdriverQuery, MetricDescriptor, StackdriverOptions, Filter } from './types'; import { DataSourceApi, DataQueryRequest, DataSourceInstanceSettings, ScopedVars } from '@grafana/data'; -import { BackendSrv } from 'app/core/services/backend_srv'; +import { getBackendSrv } from '@grafana/runtime'; import { TemplateSrv } from 'app/features/templating/template_srv'; import { TimeSrv } from 'app/features/dashboard/services/TimeSrv'; import { CoreEvents } from 'app/types'; @@ -20,7 +20,6 @@ export default class StackdriverDatasource extends DataSourceApi, - private backendSrv: BackendSrv, public templateSrv: TemplateSrv, private timeSrv: TimeSrv ) { @@ -59,7 +58,7 @@ export default class StackdriverDatasource extends DataSourceApi 0) { - const { data } = await this.backendSrv.datasourceRequest({ + const { data } = await getBackendSrv().datasourceRequest({ url: '/api/tsdb/query', method: 'POST', data: { @@ -181,7 +180,7 @@ export default class StackdriverDatasource extends DataSourceApi { + return getBackendSrv() .datasourceRequest({ url: this.url + url, method: 'GET', diff --git a/public/app/plugins/datasource/stackdriver/specs/datasource.test.ts b/public/app/plugins/datasource/stackdriver/specs/datasource.test.ts index 6b6e08c7d6e..b4ea09eebdd 100644 --- a/public/app/plugins/datasource/stackdriver/specs/datasource.test.ts +++ b/public/app/plugins/datasource/stackdriver/specs/datasource.test.ts @@ -4,9 +4,14 @@ import { TemplateSrv } from 'app/features/templating/template_srv'; import { CustomVariable } from 'app/features/templating/all'; import { DataSourceInstanceSettings, toUtc } from '@grafana/data'; import { StackdriverOptions } from '../types'; -import { BackendSrv } from 'app/core/services/backend_srv'; +import { backendSrv } from 'app/core/services/backend_srv'; // will use the version in __mocks__ import { TimeSrv } from 'app/features/dashboard/services/TimeSrv'; +jest.mock('@grafana/runtime', () => ({ + ...jest.requireActual('@grafana/runtime'), + getBackendSrv: () => backendSrv, +})); + interface Result { status: any; message?: any; @@ -20,20 +25,23 @@ describe('StackdriverDataSource', () => { } as unknown) as DataSourceInstanceSettings; const templateSrv = new TemplateSrv(); const timeSrv = {} as TimeSrv; + const datasourceRequestMock = jest.spyOn(backendSrv, 'datasourceRequest'); + + beforeEach(() => { + jest.clearAllMocks(); + datasourceRequestMock.mockImplementation(jest.fn()); + }); describe('when performing testDataSource', () => { describe('and call to stackdriver api succeeds', () => { let ds; let result: Result; beforeEach(async () => { - const backendSrv = ({ - async datasourceRequest() { - return Promise.resolve({ status: 200 }); - }, - } as unknown) as BackendSrv; - ds = new StackdriverDataSource(instanceSettings, backendSrv, templateSrv, timeSrv); + datasourceRequestMock.mockImplementation(() => Promise.resolve({ status: 200 })); + ds = new StackdriverDataSource(instanceSettings, templateSrv, timeSrv); result = await ds.testDatasource(); }); + it('should return successfully', () => { expect(result.status).toBe('success'); }); @@ -43,12 +51,12 @@ describe('StackdriverDataSource', () => { let ds; let result: Result; beforeEach(async () => { - const backendSrv = ({ - datasourceRequest: async () => Promise.resolve({ status: 200, data: metricDescriptors }), - } as unknown) as BackendSrv; - ds = new StackdriverDataSource(instanceSettings, backendSrv, templateSrv, timeSrv); + datasourceRequestMock.mockImplementation(() => Promise.resolve({ status: 200, data: metricDescriptors })); + + ds = new StackdriverDataSource(instanceSettings, templateSrv, timeSrv); result = await ds.testDatasource(); }); + it('should return status success', () => { expect(result.status).toBe('success'); }); @@ -58,16 +66,16 @@ describe('StackdriverDataSource', () => { let ds; let result: Result; beforeEach(async () => { - const backendSrv = ({ - datasourceRequest: async () => - Promise.reject({ - statusText: 'Bad Request', - data: { - error: { code: 400, message: 'Field interval.endTime had an invalid value' }, - }, - }), - } as unknown) as BackendSrv; - ds = new StackdriverDataSource(instanceSettings, backendSrv, templateSrv, timeSrv); + datasourceRequestMock.mockImplementation(() => + Promise.reject({ + statusText: 'Bad Request', + data: { + error: { code: 400, message: 'Field interval.endTime had an invalid value' }, + }, + }) + ); + + ds = new StackdriverDataSource(instanceSettings, templateSrv, timeSrv); result = await ds.testDatasource(); }); @@ -111,10 +119,8 @@ describe('StackdriverDataSource', () => { }; beforeEach(() => { - const backendSrv = ({ - datasourceRequest: async () => Promise.resolve({ status: 200, data: response }), - } as unknown) as BackendSrv; - ds = new StackdriverDataSource(instanceSettings, backendSrv, templateSrv, timeSrv); + datasourceRequestMock.mockImplementation(() => Promise.resolve({ status: 200, data: response })); + ds = new StackdriverDataSource(instanceSettings, templateSrv, timeSrv); }); it('should return a list of datapoints', () => { @@ -130,28 +136,28 @@ describe('StackdriverDataSource', () => { let ds; let result: any; beforeEach(async () => { - const backendSrv = ({ - async datasourceRequest() { - return Promise.resolve({ - data: { - metricDescriptors: [ - { - displayName: 'test metric name 1', - type: 'compute.googleapis.com/instance/cpu/test-metric-type-1', - description: 'A description', - }, - { - type: 'logging.googleapis.com/user/logbased-metric-with-no-display-name', - }, - ], - }, - }); - }, - } as unknown) as BackendSrv; - ds = new StackdriverDataSource(instanceSettings, backendSrv, templateSrv, timeSrv); + datasourceRequestMock.mockImplementation(() => + Promise.resolve({ + data: { + metricDescriptors: [ + { + displayName: 'test metric name 1', + type: 'compute.googleapis.com/instance/cpu/test-metric-type-1', + description: 'A description', + }, + { + type: 'logging.googleapis.com/user/logbased-metric-with-no-display-name', + }, + ], + }, + }) + ); + + ds = new StackdriverDataSource(instanceSettings, templateSrv, timeSrv); // @ts-ignore result = await ds.getMetricTypes(); }); + it('should return successfully', () => { expect(result.length).toBe(2); expect(result[0].service).toBe('compute.googleapis.com'); @@ -164,14 +170,12 @@ describe('StackdriverDataSource', () => { }); }); - const noopBackendSrv = ({} as unknown) as BackendSrv; - describe('when interpolating a template variable for the filter', () => { let interpolated: any[]; describe('and is single value variable', () => { beforeEach(() => { const filterTemplateSrv = initTemplateSrv('filtervalue1'); - const ds = new StackdriverDataSource(instanceSettings, noopBackendSrv, filterTemplateSrv, timeSrv); + const ds = new StackdriverDataSource(instanceSettings, filterTemplateSrv, timeSrv); interpolated = ds.interpolateFilters(['resource.label.zone', '=~', '${test}'], {}); }); @@ -184,7 +188,7 @@ describe('StackdriverDataSource', () => { describe('and is multi value variable', () => { beforeEach(() => { const filterTemplateSrv = initTemplateSrv(['filtervalue1', 'filtervalue2'], true); - const ds = new StackdriverDataSource(instanceSettings, noopBackendSrv, filterTemplateSrv, timeSrv); + const ds = new StackdriverDataSource(instanceSettings, filterTemplateSrv, timeSrv); interpolated = ds.interpolateFilters(['resource.label.zone', '=~', '[[test]]'], {}); }); @@ -200,7 +204,7 @@ describe('StackdriverDataSource', () => { describe('and is single value variable', () => { beforeEach(() => { const groupByTemplateSrv = initTemplateSrv('groupby1'); - const ds = new StackdriverDataSource(instanceSettings, noopBackendSrv, groupByTemplateSrv, timeSrv); + const ds = new StackdriverDataSource(instanceSettings, groupByTemplateSrv, timeSrv); interpolated = ds.interpolateGroupBys(['[[test]]'], {}); }); @@ -213,7 +217,7 @@ describe('StackdriverDataSource', () => { describe('and is multi value variable', () => { beforeEach(() => { const groupByTemplateSrv = initTemplateSrv(['groupby1', 'groupby2'], true); - const ds = new StackdriverDataSource(instanceSettings, noopBackendSrv, groupByTemplateSrv, timeSrv); + const ds = new StackdriverDataSource(instanceSettings, groupByTemplateSrv, timeSrv); interpolated = ds.interpolateGroupBys(['[[test]]'], {}); }); @@ -228,7 +232,7 @@ describe('StackdriverDataSource', () => { describe('unit parsing', () => { let ds: StackdriverDataSource, res: any; beforeEach(() => { - ds = new StackdriverDataSource(instanceSettings, noopBackendSrv, templateSrv, timeSrv); + ds = new StackdriverDataSource(instanceSettings, templateSrv, timeSrv); }); describe('when theres only one target', () => { describe('and the stackdriver unit doesnt have a corresponding grafana unit', () => { diff --git a/public/app/plugins/datasource/testdata/QueryEditor.tsx b/public/app/plugins/datasource/testdata/QueryEditor.tsx index e05892baadb..709ff79fe9b 100644 --- a/public/app/plugins/datasource/testdata/QueryEditor.tsx +++ b/public/app/plugins/datasource/testdata/QueryEditor.tsx @@ -33,7 +33,7 @@ export class QueryEditor extends PureComponent { query.scenarioId = query.scenarioId || 'random_walk'; - // const scenarioList = await this.backendSrv.get('/api/tsdb/testdata/scenarios'); + // const scenarioList = await backendSrv.get('/api/tsdb/testdata/scenarios'); const scenarioList = await datasource.getScenarios(); const current: any = _.find(scenarioList, { id: query.scenarioId }); diff --git a/public/app/plugins/datasource/testdata/datasource.ts b/public/app/plugins/datasource/testdata/datasource.ts index 62e797b5491..7b7ab65f9ce 100644 --- a/public/app/plugins/datasource/testdata/datasource.ts +++ b/public/app/plugins/datasource/testdata/datasource.ts @@ -9,7 +9,7 @@ import { DataQueryError, } from '@grafana/data'; import { Scenario, TestDataQuery } from './types'; -import { getBackendSrv } from 'app/core/services/backend_srv'; +import { getBackendSrv } from '@grafana/runtime'; import { queryMetricTree } from './metricTree'; import { from, merge, Observable } from 'rxjs'; import { runStream } from './runStreams'; diff --git a/public/app/plugins/datasource/testdata/query_ctrl.ts b/public/app/plugins/datasource/testdata/query_ctrl.ts index 2a53edb16a4..99b84c30e9d 100644 --- a/public/app/plugins/datasource/testdata/query_ctrl.ts +++ b/public/app/plugins/datasource/testdata/query_ctrl.ts @@ -4,7 +4,9 @@ import { e2e } from '@grafana/e2e'; import { QueryCtrl } from 'app/plugins/sdk'; import { defaultQuery } from './runStreams'; -import { getBackendSrv } from 'app/core/services/backend_srv'; +import { getBackendSrv } from '@grafana/runtime'; +import { promiseToDigest } from 'app/core/utils/promiseToDigest'; +import { IScope } from 'angular'; export const defaultPulse: any = { timeStep: 60, @@ -29,12 +31,13 @@ export class TestDataQueryCtrl extends QueryCtrl { newPointValue: number; newPointTime: any; selectedPoint: any; + digest: (promise: Promise) => Promise; showLabels = false; selectors: typeof e2e.pages.Dashboard.Panels.DataSource.TestData.QueryTab.selectors; /** @ngInject */ - constructor($scope: any, $injector: any) { + constructor($scope: IScope, $injector: any) { super($scope, $injector); this.target.scenarioId = this.target.scenarioId || 'random_walk'; @@ -73,12 +76,14 @@ export class TestDataQueryCtrl extends QueryCtrl { } $onInit() { - return getBackendSrv() - .get('/api/tsdb/testdata/scenarios') - .then((res: any) => { - this.scenarioList = res; - this.scenario = _.find(this.scenarioList, { id: this.target.scenarioId }); - }); + return promiseToDigest(this.$scope)( + getBackendSrv() + .get('/api/tsdb/testdata/scenarios') + .then((res: any) => { + this.scenarioList = res; + this.scenario = _.find(this.scenarioList, { id: this.target.scenarioId }); + }) + ); } scenarioChanged() { diff --git a/public/app/plugins/panel/alertlist/module.ts b/public/app/plugins/panel/alertlist/module.ts index b8972b56f84..bfe59bb3c22 100644 --- a/public/app/plugins/panel/alertlist/module.ts +++ b/public/app/plugins/panel/alertlist/module.ts @@ -4,8 +4,9 @@ import { PanelCtrl } from 'app/plugins/sdk'; import { dateMath, dateTime } from '@grafana/data'; import { PanelEvents } from '@grafana/data'; -import { auto } from 'angular'; -import { BackendSrv } from '@grafana/runtime'; +import { auto, IScope } from 'angular'; +import { getBackendSrv } from '@grafana/runtime'; +import { promiseToDigest } from 'app/core/utils/promiseToDigest'; class AlertListPanel extends PanelCtrl { static templateUrl = 'module.html'; @@ -41,7 +42,7 @@ class AlertListPanel extends PanelCtrl { }; /** @ngInject */ - constructor($scope: any, $injector: auto.IInjectorService, private backendSrv: BackendSrv) { + constructor($scope: IScope, $injector: auto.IInjectorService) { super($scope, $injector); _.defaults(this.panel, this.panelDefaults); @@ -120,18 +121,22 @@ class AlertListPanel extends PanelCtrl { params.from = dateMath.parse(this.dashboard.time.from).unix() * 1000; params.to = dateMath.parse(this.dashboard.time.to).unix() * 1000; - return this.backendSrv.get(`/api/annotations`, params).then(res => { - this.alertHistory = _.map(res, al => { - al.time = this.dashboard.formatDate(al.time, 'MMM D, YYYY HH:mm:ss'); - al.stateModel = alertDef.getStateDisplayModel(al.newState); - al.info = alertDef.getAlertAnnotationInfo(al); - return al; - }); + return promiseToDigest(this.$scope)( + getBackendSrv() + .get(`/api/annotations`, params) + .then(res => { + this.alertHistory = _.map(res, al => { + al.time = this.dashboard.formatDate(al.time, 'MMM D, YYYY HH:mm:ss'); + al.stateModel = alertDef.getStateDisplayModel(al.newState); + al.info = alertDef.getAlertAnnotationInfo(al); + return al; + }); - this.noAlertsMessage = this.alertHistory.length === 0 ? 'No alerts in current time range' : ''; + this.noAlertsMessage = this.alertHistory.length === 0 ? 'No alerts in current time range' : ''; - return this.alertHistory; - }); + return this.alertHistory; + }) + ); } getCurrentAlertState() { @@ -159,23 +164,27 @@ class AlertListPanel extends PanelCtrl { params.dashboardTag = this.panel.dashboardTags; } - return this.backendSrv.get(`/api/alerts`, params).then(res => { - this.currentAlerts = this.sortResult( - _.map(res, al => { - al.stateModel = alertDef.getStateDisplayModel(al.state); - al.newStateDateAgo = dateTime(al.newStateDate) - .locale('en') - .fromNow(true); - return al; - }) - ); - if (this.currentAlerts.length > this.panel.limit) { - this.currentAlerts = this.currentAlerts.slice(0, this.panel.limit); - } - this.noAlertsMessage = this.currentAlerts.length === 0 ? 'No alerts' : ''; + return promiseToDigest(this.$scope)( + getBackendSrv() + .get(`/api/alerts`, params) + .then(res => { + this.currentAlerts = this.sortResult( + _.map(res, al => { + al.stateModel = alertDef.getStateDisplayModel(al.state); + al.newStateDateAgo = dateTime(al.newStateDate) + .locale('en') + .fromNow(true); + return al; + }) + ); + if (this.currentAlerts.length > this.panel.limit) { + this.currentAlerts = this.currentAlerts.slice(0, this.panel.limit); + } + this.noAlertsMessage = this.currentAlerts.length === 0 ? 'No alerts' : ''; - return this.currentAlerts; - }); + return this.currentAlerts; + }) + ); } onInitEditMode() { diff --git a/public/app/plugins/panel/annolist/AnnoListPanel.tsx b/public/app/plugins/panel/annolist/AnnoListPanel.tsx index 691f1e8b486..63549d18971 100644 --- a/public/app/plugins/panel/annolist/AnnoListPanel.tsx +++ b/public/app/plugins/panel/annolist/AnnoListPanel.tsx @@ -5,7 +5,7 @@ import React, { PureComponent } from 'react'; import { AnnoOptions } from './types'; import { PanelProps, dateTime, DurationUnit, AnnotationEvent, AppEvents } from '@grafana/data'; import { Tooltip } from '@grafana/ui'; -import { getBackendSrv } from 'app/core/services/backend_srv'; +import { getBackendSrv } from '@grafana/runtime'; import { AbstractList } from '@grafana/ui/src/components/List/AbstractList'; import { TagBadge } from 'app/core/components/TagFilter/TagBadge'; import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv'; diff --git a/public/app/plugins/panel/dashlist/module.ts b/public/app/plugins/panel/dashlist/module.ts index 38ac4226626..276e423f06b 100644 --- a/public/app/plugins/panel/dashlist/module.ts +++ b/public/app/plugins/panel/dashlist/module.ts @@ -1,10 +1,11 @@ import _ from 'lodash'; import { PanelCtrl } from 'app/plugins/sdk'; import impressionSrv from 'app/core/services/impression_srv'; -import { auto } from 'angular'; -import { BackendSrv } from 'app/core/services/backend_srv'; +import { auto, IScope } from 'angular'; +import { backendSrv } from 'app/core/services/backend_srv'; import { DashboardSrv } from 'app/features/dashboard/services/DashboardSrv'; import { PanelEvents } from '@grafana/data'; +import { promiseToDigest } from '../../../core/utils/promiseToDigest'; class DashListCtrl extends PanelCtrl { static templateUrl = 'module.html'; @@ -25,12 +26,7 @@ class DashListCtrl extends PanelCtrl { }; /** @ngInject */ - constructor( - $scope: any, - $injector: auto.IInjectorService, - private backendSrv: BackendSrv, - private dashboardSrv: DashboardSrv - ) { + constructor($scope: IScope, $injector: auto.IInjectorService, private dashboardSrv: DashboardSrv) { super($scope, $injector); _.defaults(this.panel, this.panelDefaults); @@ -97,9 +93,11 @@ class DashListCtrl extends PanelCtrl { type: 'dash-db', }; - return this.backendSrv.search(params).then(result => { - this.groups[2].list = result; - }); + return promiseToDigest(this.$scope)( + backendSrv.search(params).then(result => { + this.groups[2].list = result; + }) + ); } getStarred() { @@ -109,9 +107,11 @@ class DashListCtrl extends PanelCtrl { } const params = { limit: this.panel.limit, starred: 'true' }; - return this.backendSrv.search(params).then(result => { - this.groups[0].list = result; - }); + return promiseToDigest(this.$scope)( + backendSrv.search(params).then(result => { + this.groups[0].list = result; + }) + ); } starDashboard(dash: any, evt: any) { @@ -132,17 +132,19 @@ class DashListCtrl extends PanelCtrl { } const dashIds = _.take(impressionSrv.getDashboardOpened(), this.panel.limit); - return this.backendSrv.search({ dashboardIds: dashIds, limit: this.panel.limit }).then(result => { - this.groups[1].list = dashIds - .map(orderId => { - return _.find(result, dashboard => { - return dashboard.id === orderId; + return promiseToDigest(this.$scope)( + backendSrv.search({ dashboardIds: dashIds, limit: this.panel.limit }).then(result => { + this.groups[1].list = dashIds + .map(orderId => { + return _.find(result, dashboard => { + return dashboard.id === orderId; + }); + }) + .filter(el => { + return el !== undefined; }); - }) - .filter(el => { - return el !== undefined; - }); - }); + }) + ); } onFolderChange(folder: any) { diff --git a/public/app/plugins/panel/gettingstarted/GettingStarted.tsx b/public/app/plugins/panel/gettingstarted/GettingStarted.tsx index 706d3b2fe84..a5f96f56c6d 100644 --- a/public/app/plugins/panel/gettingstarted/GettingStarted.tsx +++ b/public/app/plugins/panel/gettingstarted/GettingStarted.tsx @@ -3,7 +3,7 @@ import React, { PureComponent } from 'react'; import { PanelProps } from '@grafana/data'; 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 { contextSrv } from 'app/core/core'; import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv'; @@ -65,11 +65,9 @@ export class GettingStarted extends PureComponent { icon: 'gicon gicon-dashboard', href: 'dashboard/new?gettingstarted', check: () => { - return getBackendSrv() - .search({ limit: 1 }) - .then(result => { - return result.length > 0; - }); + return backendSrv.search({ limit: 1 }).then(result => { + return result.length > 0; + }); }, }, { @@ -78,12 +76,10 @@ export class GettingStarted extends PureComponent { icon: 'gicon gicon-team', href: 'org/users?gettingstarted', check: () => { - return getBackendSrv() - .get('/api/org/users/lookup') - .then((res: any) => { - /* return res.length > 1; */ - return false; - }); + return backendSrv.get('/api/org/users/lookup').then((res: any) => { + /* return res.length > 1; */ + return false; + }); }, }, { @@ -92,11 +88,9 @@ export class GettingStarted extends PureComponent { icon: 'gicon gicon-plugins', href: 'https://grafana.com/plugins?utm_source=grafana_getting_started', check: () => { - return getBackendSrv() - .get('/api/plugins', { embedded: 0, core: 0 }) - .then((plugins: any[]) => { - return plugins.length > 0; - }); + return backendSrv.get('/api/plugins', { embedded: 0, core: 0 }).then((plugins: any[]) => { + return plugins.length > 0; + }); }, }, ]; @@ -130,7 +124,7 @@ export class GettingStarted extends PureComponent { const dashboard = getDashboardSrv().getCurrent(); const panel = dashboard.getPanelById(id); dashboard.removePanel(panel); - getBackendSrv() + backendSrv .request({ method: 'PUT', url: '/api/user/helpflags/1', diff --git a/public/app/plugins/panel/pluginlist/module.ts b/public/app/plugins/panel/pluginlist/module.ts index abf6551d858..6e8ca6d8021 100644 --- a/public/app/plugins/panel/pluginlist/module.ts +++ b/public/app/plugins/panel/pluginlist/module.ts @@ -1,10 +1,11 @@ import _ from 'lodash'; import { PanelCtrl } from '../../../features/panel/panel_ctrl'; -import { auto } from 'angular'; -import { BackendSrv } from '@grafana/runtime'; +import { auto, IScope } from 'angular'; import { PanelEvents } from '@grafana/data'; import { ContextSrv } from '../../../core/services/context_srv'; import { CoreEvents } from 'app/types'; +import { getBackendSrv } from '@grafana/runtime'; +import { promiseToDigest } from 'app/core/utils/promiseToDigest'; class PluginListCtrl extends PanelCtrl { static templateUrl = 'module.html'; @@ -18,7 +19,7 @@ class PluginListCtrl extends PanelCtrl { panelDefaults = {}; /** @ngInject */ - constructor($scope: any, $injector: auto.IInjectorService, private backendSrv: BackendSrv, contextSrv: ContextSrv) { + constructor($scope: IScope, $injector: auto.IInjectorService, contextSrv: ContextSrv) { super($scope, $injector); _.defaults(this.panel, this.panelDefaults); @@ -60,20 +61,24 @@ class PluginListCtrl extends PanelCtrl { } update() { - this.backendSrv.get('api/plugins', { embedded: 0, core: 0 }).then(plugins => { - this.pluginList = plugins; - this.viewModel[0].list = _.filter(plugins, { type: 'app' }); - this.viewModel[1].list = _.filter(plugins, { type: 'panel' }); - this.viewModel[2].list = _.filter(plugins, { type: 'datasource' }); + promiseToDigest(this.$scope)( + getBackendSrv() + .get('api/plugins', { embedded: 0, core: 0 }) + .then(plugins => { + this.pluginList = plugins; + this.viewModel[0].list = _.filter(plugins, { type: 'app' }); + this.viewModel[1].list = _.filter(plugins, { type: 'panel' }); + this.viewModel[2].list = _.filter(plugins, { type: 'datasource' }); - for (const plugin of this.pluginList) { - if (plugin.hasUpdate) { - plugin.state = 'has-update'; - } else if (!plugin.enabled) { - plugin.state = 'not-enabled'; - } - } - }); + for (const plugin of this.pluginList) { + if (plugin.hasUpdate) { + plugin.state = 'has-update'; + } else if (!plugin.enabled) { + plugin.state = 'not-enabled'; + } + } + }) + ); } } diff --git a/public/app/routes/GrafanaCtrl.ts b/public/app/routes/GrafanaCtrl.ts index 5d028d7d1e7..9d869355008 100644 --- a/public/app/routes/GrafanaCtrl.ts +++ b/public/app/routes/GrafanaCtrl.ts @@ -6,7 +6,7 @@ import Drop from 'tether-drop'; // Utils and servies import { colors } from '@grafana/ui'; -import { setBackendSrv, BackendSrv, setDataSourceSrv } from '@grafana/runtime'; +import { setBackendSrv, setDataSourceSrv } from '@grafana/runtime'; import config from 'app/core/config'; import coreModule from 'app/core/core_module'; import { profiler } from 'app/core/profiler'; @@ -30,6 +30,7 @@ import { PlaylistSrv } from 'app/features/playlist/playlist_srv'; import { DashboardSrv, setDashboardSrv } from 'app/features/dashboard/services/DashboardSrv'; import { ILocationService, ITimeoutService, IRootScopeService, IAngularEvent } from 'angular'; import { AppEvent, AppEvents } from '@grafana/data'; +import { backendSrv } from 'app/core/services/backend_srv'; export type GrafanaRootScope = IRootScopeService & AppEventEmitter & AppEventConsumer & { colors: string[] }; @@ -41,7 +42,6 @@ export class GrafanaCtrl { $rootScope: GrafanaRootScope, contextSrv: ContextSrv, bridgeSrv: BridgeSrv, - backendSrv: BackendSrv, timeSrv: TimeSrv, linkSrv: LinkSrv, datasourceSrv: DatasourceSrv, diff --git a/public/app/routes/dashboard_loaders.ts b/public/app/routes/dashboard_loaders.ts index d44ee1a6217..bbb17b721b7 100644 --- a/public/app/routes/dashboard_loaders.ts +++ b/public/app/routes/dashboard_loaders.ts @@ -2,9 +2,9 @@ import coreModule from 'app/core/core_module'; import locationUtil from 'app/core/utils/location_util'; import { UrlQueryMap } from '@grafana/runtime'; import { DashboardLoaderSrv } from 'app/features/dashboard/services/DashboardLoaderSrv'; -import { BackendSrv } from 'app/core/services/backend_srv'; import { ILocationService } from 'angular'; import { Scope, CoreEvents, AppEventEmitter } from 'app/types'; +import { backendSrv } from 'app/core/services/backend_srv'; export class LoadDashboardCtrl { /** @ngInject */ @@ -12,7 +12,6 @@ export class LoadDashboardCtrl { $scope: Scope & AppEventEmitter, $routeParams: UrlQueryMap, dashboardLoaderSrv: DashboardLoaderSrv, - backendSrv: BackendSrv, $location: ILocationService, $browser: any ) { diff --git a/public/app/types/angular.ts b/public/app/types/angular.ts index ad20a89773c..05af2828765 100644 --- a/public/app/types/angular.ts +++ b/public/app/types/angular.ts @@ -1 +1,5 @@ -export type Scope = { [key: string]: any }; +import { IScope } from 'angular'; + +export interface Scope extends IScope { + [key: string]: any; +} diff --git a/yarn.lock b/yarn.lock index 4a3d1d902e1..abe59d0fb8c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -19198,10 +19198,10 @@ rxjs-spy@^7.5.1: error-stack-parser "^2.0.1" stacktrace-gps "^3.0.2" -rxjs@6.4.0: - version "6.4.0" - resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.4.0.tgz#f3bb0fe7bda7fb69deac0c16f17b50b0b8790504" - integrity sha512-Z9Yfa11F6B9Sg/BK9MnqnQ+aQYicPLtilXBp2yUtDt2JRCE0h26d33EnfO3ZxoNxG0T92OUucP3Ct7cpfkdFfw== +rxjs@6.5.4: + version "6.5.4" + resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.5.4.tgz#e0777fe0d184cec7872df147f303572d414e211c" + integrity sha512-naMQXcgEo3csAEGvw/NydRA0fuS2nDZJiw1YUWFKU7aPPAPGZEsD4Iimit96qwCieH6y614MCLYwdkrWx7z/7Q== dependencies: tslib "^1.9.0"