Bläddra i källkod

stackdriver: resolve merge conflict

Erik Sundell 7 år sedan
förälder
incheckning
26d9e9243a

+ 70 - 7
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);
     }
   }
 

+ 2 - 43
public/app/plugins/datasource/stackdriver/partials/query.editor.html

@@ -1,46 +1,5 @@
 <query-editor-row query-ctrl="ctrl" has-text-edit-mode="false">
-  <div class="gf-form-inline">
-    <div class="gf-form">
-      <span class="gf-form-label width-9">Service</span>
-      <gf-form-dropdown model="ctrl.service" get-options="ctrl.services" class="min-width-20" disabled type="text"
-        allow-custom="true" lookup-text="true" css-class="min-width-12" on-change="ctrl.onServiceChange(ctrl.service)"></gf-form-dropdown>
-    </div>
-    <div class="gf-form gf-form--grow">
-      <div class="gf-form-label gf-form-label--grow"></div>
-    </div>
-  </div>
-  <div class="gf-form-inline">
-    <div class="gf-form">
-      <span class="gf-form-label width-9">Metric</span>
-      <gf-form-dropdown model="ctrl.metricType" get-options="ctrl.metrics" class="min-width-20" disabled type="text"
-        allow-custom="true" lookup-text="true" css-class="min-width-12" on-change="ctrl.onMetricTypeChange()"></gf-form-dropdown>
-    </div>
-    <div class="gf-form gf-form--grow">
-      <div class="gf-form-label gf-form-label--grow"></div>
-    </div>
-  </div>
-  <div class="gf-form-inline">
-    <div class="gf-form">
-      <span class="gf-form-label query-keyword width-9">Filter</span>
-      <div class="gf-form" ng-repeat="segment in ctrl.filterSegments.filterSegments">
-        <metric-segment segment="segment" get-options="ctrl.getFilters(segment, $index)" on-change="ctrl.filterSegmentUpdated(segment, $index)"></metric-segment>
-      </div>
-    </div>
-    <div class="gf-form gf-form--grow">
-      <div class="gf-form-label gf-form-label--grow"></div>
-    </div>
-  </div>
-  <div class="gf-form-inline">
-    <div class="gf-form">
-      <span class="gf-form-label query-keyword width-9">Group By</span>
-      <div class="gf-form" ng-repeat="segment in ctrl.groupBySegments">
-        <metric-segment segment="segment" get-options="ctrl.getGroupBys(segment, $index)" on-change="ctrl.groupByChanged(segment, $index)"></metric-segment>
-      </div>
-    </div>
-    <div class="gf-form gf-form--grow">
-      <div class="gf-form-label gf-form-label--grow"></div>
-    </div>
-  </div>
+  <stackdriver-filter  target="ctrl.target" refresh="ctrl.refresh()" datasource="ctrl.datasource" default-dropdown-value="ctrl.defaultDropdownValue" default-service-value="ctrl.defaultServiceValue"></stackdriver-filter>
   <stackdriver-aggregation target="ctrl.target" alignment-period="ctrl.lastQueryMeta.alignmentPeriod" refresh="ctrl.refresh()"></stackdriver-aggregation>
   <div class="gf-form-inline">
     <div class="gf-form">
@@ -100,4 +59,4 @@
   <div class="gf-form" ng-show="ctrl.lastQueryError">
     <pre class="gf-form-pre alert alert-error">{{ctrl.lastQueryError}}</pre>
   </div>
-</query-editor-row>
+</query-editor-row>

+ 42 - 0
public/app/plugins/datasource/stackdriver/partials/query.filter.html

@@ -0,0 +1,42 @@
+<div class="gf-form-inline">
+  <div class="gf-form">
+    <span class="gf-form-label width-9">Service</span>
+    <gf-form-dropdown model="ctrl.service" get-options="ctrl.services" class="min-width-20" disabled type="text"
+      allow-custom="true" lookup-text="true" css-class="min-width-12" on-change="ctrl.onServiceChange(ctrl.service)"></gf-form-dropdown>
+  </div>
+  <div class="gf-form gf-form--grow">
+    <div class="gf-form-label gf-form-label--grow"></div>
+  </div>
+</div>
+<div class="gf-form-inline">
+  <div class="gf-form">
+    <span class="gf-form-label width-9">Metric</span>
+    <gf-form-dropdown model="ctrl.metricType" get-options="ctrl.metrics" class="min-width-20" disabled type="text"
+      allow-custom="true" lookup-text="true" css-class="min-width-12" on-change="ctrl.onMetricTypeChange()"></gf-form-dropdown>
+  </div>
+  <div class="gf-form gf-form--grow">
+    <div class="gf-form-label gf-form-label--grow"></div>
+  </div>
+</div>
+<div class="gf-form-inline">
+  <div class="gf-form">
+    <span class="gf-form-label query-keyword width-9">Filter</span>
+    <div class="gf-form" ng-repeat="segment in ctrl.filterSegments.filterSegments">
+      <metric-segment segment="segment" get-options="ctrl.getFilters(segment, $index)" on-change="ctrl.filterSegmentUpdated(segment, $index)"></metric-segment>
+    </div>
+  </div>
+  <div class="gf-form gf-form--grow">
+    <div class="gf-form-label gf-form-label--grow"></div>
+  </div>
+</div>
+<div class="gf-form-inline">
+  <div class="gf-form">
+    <span class="gf-form-label query-keyword width-9">Group By</span>
+    <div class="gf-form" ng-repeat="segment in ctrl.groupBySegments">
+      <metric-segment segment="segment" get-options="ctrl.getGroupBys(segment, $index)" on-change="ctrl.groupByChanged(segment, $index)"></metric-segment>
+    </div>
+  </div>
+  <div class="gf-form gf-form--grow">
+    <div class="gf-form-label gf-form-label--grow"></div>
+  </div>
+</div>

+ 4 - 259
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<any>;
-  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) {

+ 278 - 0
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<any>;
+
+  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);

+ 23 - 11
public/app/plugins/datasource/stackdriver/specs/query_ctrl.test.ts → 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[]) {