diff --git a/public/app/plugins/datasource/stackdriver/datasource.ts b/public/app/plugins/datasource/stackdriver/datasource.ts index 30499866b3f..8a5a730eb69 100644 --- a/public/app/plugins/datasource/stackdriver/datasource.ts +++ b/public/app/plugins/datasource/stackdriver/datasource.ts @@ -1,11 +1,13 @@ import { stackdriverUnitMappings } from './constants'; -/** @ngInject */ +import appEvents from 'app/core/app_events'; + export default class StackdriverDatasource { id: number; url: string; baseUrl: string; projectName: string; + /** @ngInject */ constructor(instanceSettings, private backendSrv, private templateSrv, private timeSrv) { this.baseUrl = `/stackdriver/`; this.url = instanceSettings.url; @@ -121,6 +123,49 @@ export default class StackdriverDatasource { return { data: result }; } + async annotationQuery(options) { + const annotation = options.annotation; + const queries = [ + { + refId: 'annotationQuery', + datasourceId: this.id, + metricType: this.templateSrv.replace(annotation.target.metricType, options.scopedVars || {}), + primaryAggregation: 'REDUCE_NONE', + perSeriesAligner: 'ALIGN_NONE', + title: this.templateSrv.replace(annotation.target.title, options.scopedVars || {}), + text: this.templateSrv.replace(annotation.target.text, options.scopedVars || {}), + tags: this.templateSrv.replace(annotation.target.tags, options.scopedVars || {}), + view: 'FULL', + filters: (annotation.target.filters || []).map(f => { + return this.templateSrv.replace(f, options.scopedVars || {}); + }), + type: 'annotationQuery', + }, + ]; + + const { data } = await this.backendSrv.datasourceRequest({ + url: '/api/tsdb/query', + method: 'POST', + data: { + from: options.range.from.valueOf().toString(), + to: options.range.to.valueOf().toString(), + queries, + }, + }); + + const results = data.results['annotationQuery'].tables[0].rows.map(v => { + return { + annotation: annotation, + time: Date.parse(v[0]), + title: v[1], + tags: [v[2]], + text: v[3], + }; + }); + + return results; + } + testDatasource() { const path = `v3/projects/${this.projectName}/metricDescriptors`; return this.doRequest(`${this.baseUrl}${path}`) @@ -161,12 +206,30 @@ export default class StackdriverDatasource { } async getDefaultProject() { - const projects = await this.getProjects(); - if (projects && projects.length > 0) { - const test = projects.filter(p => p.id === this.projectName)[0]; - return test; - } else { - throw new Error('No projects found'); + try { + const projects = await this.getProjects(); + if (projects && projects.length > 0) { + const test = projects.filter(p => p.id === this.projectName)[0]; + return test; + } else { + throw new Error('No projects found'); + } + } catch (error) { + let message = 'Projects cannot be fetched: '; + message += error.statusText ? error.statusText + ': ' : ''; + if (error && error.data && error.data.error && error.data.error.message) { + if (error.data.error.code === 403) { + message += ` + A list of projects could not be fetched from the Google Cloud Resource Manager API. + You might need to enable it first: + https://console.developers.google.com/apis/library/cloudresourcemanager.googleapis.com`; + } else { + message += error.data.error.code + '. ' + error.data.error.message; + } + } else { + message += 'Cannot connect to Stackdriver API'; + } + appEvents.emit('ds-request-error', message); } } diff --git a/public/app/plugins/datasource/stackdriver/partials/query.editor.html b/public/app/plugins/datasource/stackdriver/partials/query.editor.html index 48ef31c2b43..64fabeae38e 100755 --- a/public/app/plugins/datasource/stackdriver/partials/query.editor.html +++ b/public/app/plugins/datasource/stackdriver/partials/query.editor.html @@ -1,46 +1,5 @@ -
-
- Service - -
-
-
-
-
-
-
- Metric - -
-
-
-
-
-
-
- Filter -
- -
-
-
-
-
-
-
-
- Group By -
- -
-
-
-
-
-
+
@@ -100,4 +59,4 @@
{{ctrl.lastQueryError}}
- \ No newline at end of file + diff --git a/public/app/plugins/datasource/stackdriver/partials/query.filter.html b/public/app/plugins/datasource/stackdriver/partials/query.filter.html new file mode 100644 index 00000000000..e962a4b8772 --- /dev/null +++ b/public/app/plugins/datasource/stackdriver/partials/query.filter.html @@ -0,0 +1,42 @@ +
+
+ Service + +
+
+
+
+
+
+
+ Metric + +
+
+
+
+
+
+
+ Filter +
+ +
+
+
+
+
+
+
+
+ Group By +
+ +
+
+
+
+
+
diff --git a/public/app/plugins/datasource/stackdriver/query_aggregation_ctrl.ts b/public/app/plugins/datasource/stackdriver/query_aggregation_ctrl.ts index 1afb464bc12..27d2c0cd4cd 100644 --- a/public/app/plugins/datasource/stackdriver/query_aggregation_ctrl.ts +++ b/public/app/plugins/datasource/stackdriver/query_aggregation_ctrl.ts @@ -19,6 +19,7 @@ export class StackdriverAggregation { } export class StackdriverAggregationCtrl { + /** @ngInject */ constructor(private $scope) { $scope.aggOptions = options.aggOptions; this.setAggOptions(); diff --git a/public/app/plugins/datasource/stackdriver/query_ctrl.ts b/public/app/plugins/datasource/stackdriver/query_ctrl.ts index 3b97f074e3e..3dcb8df13d9 100644 --- a/public/app/plugins/datasource/stackdriver/query_ctrl.ts +++ b/public/app/plugins/datasource/stackdriver/query_ctrl.ts @@ -1,8 +1,7 @@ import _ from 'lodash'; import { QueryCtrl } from 'app/plugins/sdk'; -import appEvents from 'app/core/app_events'; -import { FilterSegments, DefaultRemoveFilterValue } from './filter_segments'; import './query_aggregation_ctrl'; +import './query_filter_ctrl'; export interface QueryMeta { alignmentPeriod: string; @@ -34,11 +33,9 @@ export class StackdriverQueryCtrl extends QueryCtrl { metricKind: any; valueType: any; }; + defaultDropdownValue = 'Select Metric'; defaultServiceValue = 'All Services'; - defaultRemoveGroupByValue = '-- remove group by --'; - loadLabelsPromise: Promise; - stackdriverConstants; defaults = { project: { @@ -62,270 +59,18 @@ export class StackdriverQueryCtrl extends QueryCtrl { valueType: '', }; - service: string; - metricType: string; - metricDescriptors: any[]; - metrics: any[]; - services: any[]; - groupBySegments: any[]; - removeSegment: any; showHelp: boolean; showLastQuery: boolean; lastQueryMeta: QueryMeta; lastQueryError?: string; - metricLabels: { [key: string]: string[] }; - resourceLabels: { [key: string]: string[] }; - filterSegments: any; /** @ngInject */ - constructor($scope, $injector, private uiSegmentSrv, private templateSrv) { + constructor($scope, $injector) { super($scope, $injector); _.defaultsDeep(this.target, this.defaults); - this.metricDescriptors = []; - this.metrics = []; - this.services = []; - this.metricType = this.defaultDropdownValue; - this.service = this.defaultServiceValue; + this.panelCtrl.events.on('data-received', this.onDataReceived.bind(this), $scope); this.panelCtrl.events.on('data-error', this.onDataError.bind(this), $scope); - this.getCurrentProject() - .then(this.loadMetricDescriptors.bind(this)) - .then(this.getLabels.bind(this)); - this.initSegments(); - } - - initSegments() { - this.groupBySegments = this.target.aggregation.groupBys.map(groupBy => { - return this.uiSegmentSrv.getSegmentForValue(groupBy); - }); - this.removeSegment = this.uiSegmentSrv.newSegment({ fake: true, value: '-- remove group by --' }); - this.ensurePlusButton(this.groupBySegments); - - this.filterSegments = new FilterSegments( - this.uiSegmentSrv, - this.target, - this.getGroupBys.bind(this, null, null, DefaultRemoveFilterValue, false), - this.getFilterValues.bind(this) - ); - this.filterSegments.buildSegmentModel(); - } - - async getCurrentProject() { - try { - this.target.project = await this.datasource.getDefaultProject(); - } catch (error) { - let message = 'Projects cannot be fetched: '; - message += error.statusText ? error.statusText + ': ' : ''; - if (error && error.data && error.data.error && error.data.error.message) { - if (error.data.error.code === 403) { - message += ` - A list of projects could not be fetched from the Google Cloud Resource Manager API. - You might need to enable it first: - https://console.developers.google.com/apis/library/cloudresourcemanager.googleapis.com`; - } else { - message += error.data.error.code + '. ' + error.data.error.message; - } - } else { - message += 'Cannot connect to Stackdriver API'; - } - appEvents.emit('ds-request-error', message); - } - } - - async loadMetricDescriptors() { - if (this.target.project.id !== 'default') { - this.metricDescriptors = await this.datasource.getMetricTypes(this.target.project.id); - this.services = this.getServicesList(); - this.metrics = this.getMetricsList(); - return this.metricDescriptors; - } else { - return []; - } - } - - getServicesList() { - const defaultValue = { value: this.defaultServiceValue, text: this.defaultServiceValue }; - const services = this.metricDescriptors.map(m => { - const [service] = m.type.split('/'); - const [serviceShortName] = service.split('.'); - return { - value: service, - text: serviceShortName, - }; - }); - - if (services.find(m => m.value === this.target.service)) { - this.service = this.target.service; - } - - return services.length > 0 ? [defaultValue, ..._.uniqBy(services, 'value')] : []; - } - - getMetricsList() { - const metrics = this.metricDescriptors.map(m => { - const [service] = m.type.split('/'); - const [serviceShortName] = service.split('.'); - return { - service, - value: m.type, - serviceShortName, - text: m.displayName, - title: m.description, - }; - }); - - let result; - if (this.target.service === this.defaultServiceValue) { - result = metrics.map(m => ({ ...m, text: `${m.service} - ${m.text}` })); - } else { - result = metrics.filter(m => m.service === this.target.service); - } - - if (result.find(m => m.value === this.target.metricType)) { - this.metricType = this.target.metricType; - } else if (result.length > 0) { - this.metricType = this.target.metricType = result[0].value; - } - return result; - } - - async getLabels() { - this.loadLabelsPromise = new Promise(async resolve => { - try { - const data = await this.datasource.getLabels(this.target.metricType, this.target.refId); - this.metricLabels = data.results[this.target.refId].meta.metricLabels; - this.resourceLabels = data.results[this.target.refId].meta.resourceLabels; - resolve(); - } catch (error) { - console.log(error.data.message); - appEvents.emit('alert-error', ['Error', 'Error loading metric labels for ' + this.target.metricType]); - resolve(); - } - }); - } - - onServiceChange() { - this.target.service = this.service; - this.metrics = this.getMetricsList(); - this.setMetricType(); - if (!this.metrics.find(m => m.value === this.target.metricType)) { - this.target.metricType = this.defaultDropdownValue; - } else { - this.refresh(); - } - } - - async onMetricTypeChange() { - this.setMetricType(); - this.refresh(); - this.getLabels(); - } - - setMetricType() { - this.target.metricType = this.metricType; - const { valueType, metricKind, unit } = this.metricDescriptors.find(m => m.type === this.target.metricType); - this.target.unit = unit; - this.target.valueType = valueType; - this.target.metricKind = metricKind; - this.$scope.$broadcast('metricTypeChanged'); - } - - async getGroupBys(segment, index, removeText?: string, removeUsed = true) { - await this.loadLabelsPromise; - - const metricLabels = Object.keys(this.metricLabels || {}) - .filter(ml => { - if (!removeUsed) { - return true; - } - return this.target.aggregation.groupBys.indexOf('metric.label.' + ml) === -1; - }) - .map(l => { - return this.uiSegmentSrv.newSegment({ - value: `metric.label.${l}`, - expandable: false, - }); - }); - - const resourceLabels = Object.keys(this.resourceLabels || {}) - .filter(ml => { - if (!removeUsed) { - return true; - } - - return this.target.aggregation.groupBys.indexOf('resource.label.' + ml) === -1; - }) - .map(l => { - return this.uiSegmentSrv.newSegment({ - value: `resource.label.${l}`, - expandable: false, - }); - }); - - const noValueOrPlusButton = !segment || segment.type === 'plus-button'; - if (noValueOrPlusButton && metricLabels.length === 0 && resourceLabels.length === 0) { - return Promise.resolve([]); - } - - this.removeSegment.value = removeText || this.defaultRemoveGroupByValue; - return Promise.resolve([...metricLabels, ...resourceLabels, this.removeSegment]); - } - - groupByChanged(segment, index) { - if (segment.value === this.removeSegment.value) { - this.groupBySegments.splice(index, 1); - } else { - segment.type = 'value'; - } - - const reducer = (memo, seg) => { - if (!seg.fake) { - memo.push(seg.value); - } - return memo; - }; - - this.target.aggregation.groupBys = this.groupBySegments.reduce(reducer, []); - this.ensurePlusButton(this.groupBySegments); - this.refresh(); - } - - async getFilters(segment, index) { - const hasNoFilterKeys = this.metricLabels && Object.keys(this.metricLabels).length === 0; - return this.filterSegments.getFilters(segment, index, hasNoFilterKeys); - } - - getFilterValues(index) { - const filterKey = this.templateSrv.replace(this.filterSegments.filterSegments[index - 2].value); - if (!filterKey || !this.metricLabels || Object.keys(this.metricLabels).length === 0) { - return []; - } - - const shortKey = filterKey.substring(filterKey.indexOf('.label.') + 7); - - if (filterKey.startsWith('metric.label.') && this.metricLabels.hasOwnProperty(shortKey)) { - return this.metricLabels[shortKey]; - } - - if (filterKey.startsWith('resource.label.') && this.resourceLabels.hasOwnProperty(shortKey)) { - return this.resourceLabels[shortKey]; - } - - return []; - } - - filterSegmentUpdated(segment, index) { - this.target.filters = this.filterSegments.filterSegmentUpdated(segment, index); - this.refresh(); - } - - ensurePlusButton(segments) { - const count = segments.length; - const lastSegment = segments[Math.max(count - 1, 0)]; - - if (!lastSegment || lastSegment.type !== 'plus-button') { - segments.push(this.uiSegmentSrv.newPlusButton()); - } } onDataReceived(dataList) { diff --git a/public/app/plugins/datasource/stackdriver/query_filter_ctrl.ts b/public/app/plugins/datasource/stackdriver/query_filter_ctrl.ts new file mode 100644 index 00000000000..d843de1074a --- /dev/null +++ b/public/app/plugins/datasource/stackdriver/query_filter_ctrl.ts @@ -0,0 +1,278 @@ +import angular from 'angular'; +import _ from 'lodash'; +import { FilterSegments, DefaultRemoveFilterValue } from './filter_segments'; +import appEvents from 'app/core/app_events'; + +export class StackdriverFilter { + constructor() { + return { + templateUrl: 'public/app/plugins/datasource/stackdriver/partials/query.filter.html', + controller: 'StackdriverFilterCtrl', + controllerAs: 'ctrl', + restrict: 'E', + scope: { + target: '=', + datasource: '=', + refresh: '&', + defaultDropdownValue: '<', + defaultServiceValue: '<', + }, + }; + } +} + +export class StackdriverFilterCtrl { + metricLabels: { [key: string]: string[] }; + resourceLabels: { [key: string]: string[] }; + + defaultRemoveGroupByValue = '-- remove group by --'; + loadLabelsPromise: Promise; + + service: string; + metricType: string; + metricDescriptors: any[]; + metrics: any[]; + services: any[]; + groupBySegments: any[]; + filterSegments: FilterSegments; + removeSegment: any; + target: any; + datasource: any; + + /** @ngInject */ + constructor(private $scope, private uiSegmentSrv, private templateSrv) { + this.datasource = $scope.datasource; + this.target = $scope.target; + this.metricType = $scope.defaultDropdownValue; + this.service = $scope.defaultServiceValue; + + this.metricDescriptors = []; + this.metrics = []; + this.services = []; + + this.getCurrentProject() + .then(this.loadMetricDescriptors.bind(this)) + .then(this.getLabels.bind(this)); + + this.initSegments(); + } + + initSegments() { + this.groupBySegments = this.target.aggregation.groupBys.map(groupBy => { + return this.uiSegmentSrv.getSegmentForValue(groupBy); + }); + this.removeSegment = this.uiSegmentSrv.newSegment({ fake: true, value: '-- remove group by --' }); + this.ensurePlusButton(this.groupBySegments); + + this.filterSegments = new FilterSegments( + this.uiSegmentSrv, + this.target, + this.getGroupBys.bind(this, null, null, DefaultRemoveFilterValue, false), + this.getFilterValues.bind(this) + ); + this.filterSegments.buildSegmentModel(); + } + + async getCurrentProject() { + this.target.project = await this.datasource.getDefaultProject(); + } + + async loadMetricDescriptors() { + if (this.target.project.id !== 'default') { + this.metricDescriptors = await this.datasource.getMetricTypes(this.target.project.id); + this.services = this.getServicesList(); + this.metrics = this.getMetricsList(); + return this.metricDescriptors; + } else { + return []; + } + } + + getServicesList() { + const defaultValue = { value: this.$scope.defaultServiceValue, text: this.$scope.defaultServiceValue }; + const services = this.metricDescriptors.map(m => { + const [service] = m.type.split('/'); + const [serviceShortName] = service.split('.'); + return { + value: service, + text: serviceShortName, + }; + }); + + if (services.find(m => m.value === this.target.service)) { + this.service = this.target.service; + } + + return services.length > 0 ? [defaultValue, ..._.uniqBy(services, 'value')] : []; + } + + getMetricsList() { + const metrics = this.metricDescriptors.map(m => { + const [service] = m.type.split('/'); + const [serviceShortName] = service.split('.'); + return { + service, + value: m.type, + serviceShortName, + text: m.displayName, + title: m.description, + }; + }); + + let result; + if (this.target.service === this.$scope.defaultServiceValue) { + result = metrics.map(m => ({ ...m, text: `${m.service} - ${m.text}` })); + } else { + result = metrics.filter(m => m.service === this.target.service); + } + + if (result.find(m => m.value === this.target.metricType)) { + this.metricType = this.target.metricType; + } else if (result.length > 0) { + this.metricType = this.target.metricType = result[0].value; + } + return result; + } + + async getLabels() { + this.loadLabelsPromise = new Promise(async resolve => { + try { + const data = await this.datasource.getLabels(this.target.metricType, this.target.refId); + this.metricLabels = data.results[this.target.refId].meta.metricLabels; + this.resourceLabels = data.results[this.target.refId].meta.resourceLabels; + resolve(); + } catch (error) { + console.log(error.data.message); + appEvents.emit('alert-error', ['Error', 'Error loading metric labels for ' + this.target.metricType]); + resolve(); + } + }); + } + + onServiceChange() { + this.target.service = this.service; + this.metrics = this.getMetricsList(); + this.setMetricType(); + if (!this.metrics.find(m => m.value === this.target.metricType)) { + this.target.metricType = this.$scope.defaultDropdownValue; + } else { + this.$scope.refresh(); + } + } + + async onMetricTypeChange() { + this.setMetricType(); + this.$scope.refresh(); + this.getLabels(); + } + + setMetricType() { + this.target.metricType = this.metricType; + const { valueType, metricKind, unit } = this.metricDescriptors.find(m => m.type === this.target.metricType); + this.target.unit = unit; + this.target.valueType = valueType; + this.target.metricKind = metricKind; + this.$scope.$broadcast('metricTypeChanged'); + } + + async getGroupBys(segment, index, removeText?: string, removeUsed = true) { + await this.loadLabelsPromise; + + const metricLabels = Object.keys(this.metricLabels || {}) + .filter(ml => { + if (!removeUsed) { + return true; + } + return this.target.aggregation.groupBys.indexOf('metric.label.' + ml) === -1; + }) + .map(l => { + return this.uiSegmentSrv.newSegment({ + value: `metric.label.${l}`, + expandable: false, + }); + }); + + const resourceLabels = Object.keys(this.resourceLabels || {}) + .filter(ml => { + if (!removeUsed) { + return true; + } + + return this.target.aggregation.groupBys.indexOf('resource.label.' + ml) === -1; + }) + .map(l => { + return this.uiSegmentSrv.newSegment({ + value: `resource.label.${l}`, + expandable: false, + }); + }); + + const noValueOrPlusButton = !segment || segment.type === 'plus-button'; + if (noValueOrPlusButton && metricLabels.length === 0 && resourceLabels.length === 0) { + return Promise.resolve([]); + } + + this.removeSegment.value = removeText || this.defaultRemoveGroupByValue; + return Promise.resolve([...metricLabels, ...resourceLabels, this.removeSegment]); + } + + groupByChanged(segment, index) { + if (segment.value === this.removeSegment.value) { + this.groupBySegments.splice(index, 1); + } else { + segment.type = 'value'; + } + + const reducer = (memo, seg) => { + if (!seg.fake) { + memo.push(seg.value); + } + return memo; + }; + + this.target.aggregation.groupBys = this.groupBySegments.reduce(reducer, []); + this.ensurePlusButton(this.groupBySegments); + this.$scope.refresh(); + } + + async getFilters(segment, index) { + const hasNoFilterKeys = this.metricLabels && Object.keys(this.metricLabels).length === 0; + return this.filterSegments.getFilters(segment, index, hasNoFilterKeys); + } + + getFilterValues(index) { + const filterKey = this.templateSrv.replace(this.filterSegments.filterSegments[index - 2].value); + if (!filterKey || !this.metricLabels || Object.keys(this.metricLabels).length === 0) { + return []; + } + + const shortKey = filterKey.substring(filterKey.indexOf('.label.') + 7); + + if (filterKey.startsWith('metric.label.') && this.metricLabels.hasOwnProperty(shortKey)) { + return this.metricLabels[shortKey]; + } + + if (filterKey.startsWith('resource.label.') && this.resourceLabels.hasOwnProperty(shortKey)) { + return this.resourceLabels[shortKey]; + } + + return []; + } + + filterSegmentUpdated(segment, index) { + this.target.filters = this.filterSegments.filterSegmentUpdated(segment, index); + this.$scope.refresh(); + } + + ensurePlusButton(segments) { + const count = segments.length; + const lastSegment = segments[Math.max(count - 1, 0)]; + + if (!lastSegment || lastSegment.type !== 'plus-button') { + segments.push(this.uiSegmentSrv.newPlusButton()); + } + } +} + +angular.module('grafana.controllers').directive('stackdriverFilter', StackdriverFilter); +angular.module('grafana.controllers').controller('StackdriverFilterCtrl', StackdriverFilterCtrl); diff --git a/public/app/plugins/datasource/stackdriver/specs/query_ctrl.test.ts b/public/app/plugins/datasource/stackdriver/specs/query_filter_ctrl.test.ts similarity index 95% rename from public/app/plugins/datasource/stackdriver/specs/query_ctrl.test.ts rename to public/app/plugins/datasource/stackdriver/specs/query_filter_ctrl.test.ts index 68fbcbdb2a8..3dc01aa62aa 100644 --- a/public/app/plugins/datasource/stackdriver/specs/query_ctrl.test.ts +++ b/public/app/plugins/datasource/stackdriver/specs/query_filter_ctrl.test.ts @@ -1,8 +1,8 @@ -import { StackdriverQueryCtrl } from '../query_ctrl'; +import { StackdriverFilterCtrl } from '../query_filter_ctrl'; import { TemplateSrvStub } from 'test/specs/helpers'; import { DefaultRemoveFilterValue, DefaultFilterValue } from '../filter_segments'; -describe('StackdriverQueryCtrl', () => { +describe('StackdriverQueryFilterCtrl', () => { let ctrl; let result; @@ -367,16 +367,16 @@ describe('StackdriverQueryCtrl', () => { }); function createCtrlWithFakes(existingFilters?: string[]) { - StackdriverQueryCtrl.prototype.panelCtrl = { - events: { on: () => {} }, - panel: { scopedVars: [], targets: [] }, - refresh: () => {}, - }; - StackdriverQueryCtrl.prototype.target = createTarget(existingFilters); - StackdriverQueryCtrl.prototype.loadMetricDescriptors = () => { + // StackdriverFilterCtrl.prototype.panelCtrl = { + // events: { on: () => {} }, + // panel: { scopedVars: [], targets: [] }, + // refresh: () => {}, + // }; + // StackdriverFilterCtrl.prototype.target = + StackdriverFilterCtrl.prototype.loadMetricDescriptors = () => { return Promise.resolve([]); }; - StackdriverQueryCtrl.prototype.getLabels = () => { + StackdriverFilterCtrl.prototype.getLabels = () => { return Promise.resolve(); }; @@ -408,7 +408,19 @@ function createCtrlWithFakes(existingFilters?: string[]) { return { type: 'condition', value: val }; }, }; - return new StackdriverQueryCtrl(null, null, fakeSegmentServer, new TemplateSrvStub()); + const scope = { + target: createTarget(existingFilters), + datasource: { + getDefaultProject: () => { + return 'project'; + }, + }, + defaultDropdownValue: 'Select Metric', + defaultServiceValue: 'All Services', + refresh: () => {}, + }; + + return new StackdriverFilterCtrl(scope, fakeSegmentServer, new TemplateSrvStub()); } function createTarget(existingFilters?: string[]) {