Explorar o código

Merge pull request #14007 from grafana/stackdriver-template-query-editor

Stackdriver template query editor
Daniel Lee %!s(int64=7) %!d(string=hai) anos
pai
achega
4aeea56342
Modificáronse 26 ficheiros con 823 adicións e 131 borrados
  1. 14 4
      docs/sources/features/datasources/stackdriver.md
  2. 1 0
      public/app/core/components/Picker/__snapshots__/PickerOption.test.tsx.snap
  3. 36 0
      public/app/features/plugins/VariableQueryComponentLoader.tsx
  4. 1 0
      public/app/features/plugins/all.ts
  5. 34 0
      public/app/features/templating/DefaultVariableQueryEditor.tsx
  6. 17 0
      public/app/features/templating/editor_ctrl.ts
  7. 63 51
      public/app/features/templating/partials/editor.html
  8. 2 0
      public/app/features/templating/query_variable.ts
  9. 2 0
      public/app/features/templating/variable.ts
  10. 129 0
      public/app/plugins/datasource/stackdriver/StackdriverMetricFindQuery.ts
  11. 28 0
      public/app/plugins/datasource/stackdriver/components/SimpleSelect.tsx
  12. 47 0
      public/app/plugins/datasource/stackdriver/components/VariableQueryEditor.test.tsx
  13. 196 0
      public/app/plugins/datasource/stackdriver/components/VariableQueryEditor.tsx
  14. 67 0
      public/app/plugins/datasource/stackdriver/components/__snapshots__/VariableQueryEditor.test.tsx.snap
  15. 22 14
      public/app/plugins/datasource/stackdriver/datasource.ts
  16. 48 0
      public/app/plugins/datasource/stackdriver/functions.ts
  17. 2 0
      public/app/plugins/datasource/stackdriver/module.ts
  18. 6 6
      public/app/plugins/datasource/stackdriver/partials/query.aggregation.html
  19. 2 2
      public/app/plugins/datasource/stackdriver/partials/query.editor.html
  20. 32 17
      public/app/plugins/datasource/stackdriver/partials/query.filter.html
  21. 12 19
      public/app/plugins/datasource/stackdriver/query_aggregation_ctrl.ts
  22. 0 1
      public/app/plugins/datasource/stackdriver/query_ctrl.ts
  23. 8 6
      public/app/plugins/datasource/stackdriver/query_filter_ctrl.ts
  24. 25 11
      public/app/plugins/datasource/stackdriver/specs/query_aggregation_ctrl.test.ts
  25. 21 0
      public/app/plugins/datasource/stackdriver/types.ts
  26. 8 0
      public/app/types/plugins.ts

+ 14 - 4
docs/sources/features/datasources/stackdriver.md

@@ -158,9 +158,9 @@ Example Result: `compute.googleapis.com/instance/cpu/usage_time - server1-prod`
 
 It is also possible to resolve the name of the Monitored Resource Type. 
 
-| Alias Pattern Format     | Description                                     | Example Result   |
-| ------------------------ | ------------------------------------------------| ---------------- |
-| `{{resource.type}}`      | returns the name of the monitored resource type | `gce_instance`     |
+| Alias Pattern Format | Description                                     | Example Result |
+| -------------------- | ----------------------------------------------- | -------------- |
+| `{{resource.type}}`  | returns the name of the monitored resource type | `gce_instance` |
 
 Example Alias By: `{{resource.type}} - {{metric.type}}`
 
@@ -177,7 +177,17 @@ types of template variables.
 
 ### Query Variable
 
-Writing variable queries is not supported yet.
+Variable of the type *Query* allows you to query Stackdriver for various types of data. The Stackdriver data source plugin provides the following `Query Types`.
+
+| Name                | Description                                                                                       |
+| ------------------- | ------------------------------------------------------------------------------------------------- |
+| *Metric Types*      | Returns a list of metric type names that are available for the specified service.                 |
+| *Labels Keys*       | Returns a list of keys for `metric label` and `resource label` in the specified metric.           |
+| *Labels Values*     | Returns a list of values for the label in the specified metric.                                   |
+| *Resource Types*    | Returns a list of resource types for the the specified metric.                                    |
+| *Aggregations*      | Returns a list of aggregations (cross series reducers) for the the specified metric.              |
+| *Aligners*          | Returns a list of aligners (per series aligners) for the the specified metric.                    |
+| *Alignment periods* | Returns a list of all alignment periods that are available in Stackdriver query editor in Grafana |
 
 ### Using variables in queries
 

+ 1 - 0
public/app/core/components/Picker/__snapshots__/PickerOption.test.tsx.snap

@@ -14,3 +14,4 @@ exports[`PickerOption renders correctly 1`] = `
   </div>
 </div>
 `;
+  

+ 36 - 0
public/app/features/plugins/VariableQueryComponentLoader.tsx

@@ -0,0 +1,36 @@
+import coreModule from 'app/core/core_module';
+import { importPluginModule } from './plugin_loader';
+import React from 'react';
+import ReactDOM from 'react-dom';
+import DefaultVariableQueryEditor from '../templating/DefaultVariableQueryEditor';
+
+async function loadComponent(module) {
+  const component = await importPluginModule(module);
+  if (component && component.VariableQueryEditor) {
+    return component.VariableQueryEditor;
+  } else {
+    return DefaultVariableQueryEditor;
+  }
+}
+
+/** @ngInject */
+function variableQueryEditorLoader(templateSrv) {
+  return {
+    restrict: 'E',
+    link: async (scope, elem) => {
+      const Component = await loadComponent(scope.currentDatasource.meta.module);
+      const props = {
+        datasource: scope.currentDatasource,
+        query: scope.current.query,
+        onChange: scope.onQueryChange,
+        templateSrv,
+      };
+      ReactDOM.render(<Component {...props} />, elem[0]);
+      scope.$on('$destroy', () => {
+        ReactDOM.unmountComponentAtNode(elem[0]);
+      });
+    },
+  };
+}
+
+coreModule.directive('variableQueryEditorLoader', variableQueryEditorLoader);

+ 1 - 0
public/app/features/plugins/all.ts

@@ -4,3 +4,4 @@ import './import_list/import_list';
 import './ds_edit_ctrl';
 import './datasource_srv';
 import './plugin_component';
+import './VariableQueryComponentLoader';

+ 34 - 0
public/app/features/templating/DefaultVariableQueryEditor.tsx

@@ -0,0 +1,34 @@
+import React, { PureComponent } from 'react';
+import { VariableQueryProps } from 'app/types/plugins';
+
+export default class DefaultVariableQueryEditor extends PureComponent<VariableQueryProps, any> {
+  constructor(props) {
+    super(props);
+    this.state = { value: props.query };
+  }
+
+  handleChange(event) {
+    this.setState({ value: event.target.value });
+  }
+
+  handleBlur(event) {
+    this.props.onChange(event.target.value, event.target.value);
+  }
+
+  render() {
+    return (
+      <div className="gf-form">
+        <span className="gf-form-label width-10">Query</span>
+        <input
+          type="text"
+          className="gf-form-input"
+          value={this.state.value}
+          onChange={e => this.handleChange(e)}
+          onBlur={e => this.handleBlur(e)}
+          placeholder="metric name or tags query"
+          required
+        />
+      </div>
+    );
+  }
+}

+ 17 - 0
public/app/features/templating/editor_ctrl.ts

@@ -72,6 +72,7 @@ export class VariableEditorCtrl {
 
       if (
         $scope.current.type === 'query' &&
+        _.isString($scope.current.query) &&
         $scope.current.query.match(new RegExp('\\$' + $scope.current.name + '(/| |$)'))
       ) {
         appEvents.emit('alert-warning', [
@@ -106,11 +107,20 @@ export class VariableEditorCtrl {
       });
     };
 
+    $scope.onQueryChange = (query, definition) => {
+      $scope.current.query = query;
+      $scope.current.definition = definition;
+      $scope.runQuery();
+    };
+
     $scope.edit = variable => {
       $scope.current = variable;
       $scope.currentIsNew = false;
       $scope.mode = 'edit';
       $scope.validate();
+      datasourceSrv.get($scope.current.datasource).then(ds => {
+        $scope.currentDatasource = ds;
+      });
     };
 
     $scope.duplicate = variable => {
@@ -171,6 +181,13 @@ export class VariableEditorCtrl {
     $scope.showMoreOptions = () => {
       $scope.optionsLimit += 20;
     };
+
+    $scope.datasourceChanged = async () => {
+      datasourceSrv.get($scope.current.datasource).then(ds => {
+        $scope.current.query = '';
+        $scope.currentDatasource = ds;
+      });
+    };
   }
 }
 

+ 63 - 51
public/app/features/templating/partials/editor.html

@@ -17,14 +17,16 @@
 				</a>
 				<div class="grafana-info-box">
 					<h5>What do variables do?</h5>
-					<p>Variables enable more interactive and dynamic dashboards. Instead of hard-coding things like server or sensor names
-					in your metric queries you can use variables in their place. Variables are shown as dropdown select boxes at the top of
-					the dashboard. These dropdowns make it easy to change the data being displayed in your dashboard.
-
-					Check out the
-					<a class="external-link" href="http://docs.grafana.org/reference/templating/" target="_blank">
-						Templating documentation
-					</a> for more information.
+					<p>Variables enable more interactive and dynamic dashboards. Instead of hard-coding things like server or sensor
+						names
+						in your metric queries you can use variables in their place. Variables are shown as dropdown select boxes at the
+						top of
+						the dashboard. These dropdowns make it easy to change the data being displayed in your dashboard.
+
+						Check out the
+						<a class="external-link" href="http://docs.grafana.org/reference/templating/" target="_blank">
+							Templating documentation
+						</a> for more information.
 				</div>
 			</div>
 		</div>
@@ -32,7 +34,7 @@
 		<div ng-if="variables.length">
 			<div class="page-action-bar">
 				<div class="page-action-bar__spacer"></div>
-				<a type="button" class="btn btn-success" ng-click="setMode('new');"><i class="fa fa-plus" ></i> New</a>
+				<a type="button" class="btn btn-success" ng-click="setMode('new');"><i class="fa fa-plus"></i> New</a>
 			</div>
 
 			<table class="filter-table filter-table--hover">
@@ -51,7 +53,7 @@
 							</span>
 						</td>
 						<td style="max-width: 200px;" ng-click="edit(variable)" class="pointer max-width">
-							{{variable.query}}
+							{{variable.definition ? variable.definition : variable.query}}
 						</td>
 						<td style="width: 1%"><i ng-click="_.move(variables,$index,$index-1)" ng-hide="$first" class="pointer fa fa-arrow-up"></i></td>
 						<td style="width: 1%"><i ng-click="_.move(variables,$index,$index+1)" ng-hide="$last" class="pointer fa fa-arrow-down"></i></td>
@@ -77,7 +79,8 @@
 			<div class="gf-form-inline">
 				<div class="gf-form max-width-19">
 					<span class="gf-form-label width-6">Name</span>
-					<input type="text" class="gf-form-input" name="name" placeholder="name" ng-model='current.name' required ng-pattern="namePattern"></input>
+					<input type="text" class="gf-form-input" name="name" placeholder="name" ng-model='current.name' required
+					 ng-pattern="namePattern"></input>
 				</div>
 				<div class="gf-form max-width-19">
 					<span class="gf-form-label width-6">
@@ -87,13 +90,15 @@
 						</info-popover>
 					</span>
 					<div class="gf-form-select-wrapper max-width-17">
-						<select class="gf-form-input" ng-model="current.type" ng-options="k as v.name for (k, v) in variableTypes" ng-change="typeChanged()"></select>
+						<select class="gf-form-input" ng-model="current.type" ng-options="k as v.name for (k, v) in variableTypes"
+						 ng-change="typeChanged()"></select>
 					</div>
 				</div>
 			</div>
 
 			<div class="gf-form" ng-show="ctrl.form.name.$error.pattern">
-				<span class="gf-form-label gf-form-label--error">Template names cannot begin with '__', that's reserved for Grafana's global variables</span>
+				<span class="gf-form-label gf-form-label--error">Template names cannot begin with '__', that's reserved for
+					Grafana's global variables</span>
 			</div>
 
 			<div class="gf-form-inline">
@@ -115,7 +120,8 @@
 
 			<div class="gf-form">
 				<span class="gf-form-label width-9">Values</span>
-				<input type="text" class="gf-form-input" ng-model='current.query' placeholder="1m,10m,1h,6h,1d,7d" ng-model-onblur ng-change="runQuery()" required></input>
+				<input type="text" class="gf-form-input" ng-model='current.query' placeholder="1m,10m,1h,6h,1d,7d" ng-model-onblur
+				 ng-change="runQuery()" required></input>
 			</div>
 
 			<div class="gf-form-inline">
@@ -127,14 +133,16 @@
 						Step count <tip>How many times should the current time range be divided to calculate the value</tip>
 					</span>
 					<div class="gf-form-select-wrapper max-width-10" ng-show="current.auto">
-						<select class="gf-form-input" ng-model="current.auto_count" ng-options="f for f in [1,2,3,4,5,10,20,30,40,50,100,200,300,400,500]" ng-change="runQuery()"></select>
+						<select class="gf-form-input" ng-model="current.auto_count" ng-options="f for f in [1,2,3,4,5,10,20,30,40,50,100,200,300,400,500]"
+						 ng-change="runQuery()"></select>
 					</div>
 				</div>
 				<div class="gf-form">
 					<span class="gf-form-label" ng-show="current.auto">
 						Min interval <tip>The calculated value will not go below this threshold</tip>
 					</span>
-					<input type="text" class="gf-form-input max-width-10" ng-show="current.auto" ng-model="current.auto_min" ng-change="runQuery()" placeholder="10s"></input>
+					<input type="text" class="gf-form-input max-width-10" ng-show="current.auto" ng-model="current.auto_min" ng-change="runQuery()"
+					 placeholder="10s"></input>
 				</div>
 			</div>
 		</div>
@@ -143,7 +151,8 @@
 			<h5 class="section-heading">Custom Options</h5>
 			<div class="gf-form">
 				<span class="gf-form-label width-14">Values separated by comma</span>
-				<input type="text" class="gf-form-input" ng-model='current.query' ng-blur="runQuery()" placeholder="1, 10, 20, myvalue" required></input>
+				<input type="text" class="gf-form-input" ng-model='current.query' ng-blur="runQuery()" placeholder="1, 10, 20, myvalue"
+				 required></input>
 			</div>
 		</div>
 
@@ -168,15 +177,17 @@
 
 			<div class="gf-form-inline">
 				<div class="gf-form max-width-21">
-					<span class="gf-form-label width-7">Data source</span>
+					<span class="gf-form-label width-10">Data source</span>
 					<div class="gf-form-select-wrapper max-width-14">
-						<select class="gf-form-input" ng-model="current.datasource" ng-options="f.value as f.name for f in datasources" required>
+						<select class="gf-form-input" ng-model="current.datasource" ng-options="f.value as f.name for f in datasources"
+						 ng-change="datasourceChanged()" required>
 							<option value="" ng-if="false"></option>
 						</select>
 					</div>
 				</div>
+
 				<div class="gf-form max-width-22">
-					<span class="gf-form-label width-7">
+					<span class="gf-form-label width-10">
 						Refresh
 						<info-popover mode="right-normal">
 							When to update the values of this variable.
@@ -187,28 +198,32 @@
 					</div>
 				</div>
 			</div>
+
+			<rebuild-on-change property="currentDatasource">
+				<variable-query-editor-loader>
+				</variable-query-editor-loader>
+			</rebuild-on-change>
+
 			<div class="gf-form">
-				<span class="gf-form-label width-7">Query</span>
-				<input type="text" class="gf-form-input" ng-model='current.query' placeholder="metric name or tags query" ng-model-onblur ng-change="runQuery()" required></input>
-			</div>
-			<div class="gf-form">
-				<span class="gf-form-label width-7">
+				<span class="gf-form-label width-10">
 					Regex
 					<info-popover mode="right-normal">
 						Optional, if you want to extract part of a series name or metric node segment.
 					</info-popover>
 				</span>
-				<input type="text" class="gf-form-input" ng-model='current.regex' placeholder="/.*-(.*)-.*/" ng-model-onblur ng-change="runQuery()"></input>
+				<input type="text" class="gf-form-input" ng-model='current.regex' placeholder="/.*-(.*)-.*/" ng-model-onblur
+				 ng-change="runQuery()"></input>
 			</div>
 			<div class="gf-form max-width-21">
-				<span class="gf-form-label width-7">
+				<span class="gf-form-label width-10">
 					Sort
 					<info-popover mode="right-normal">
 						How to sort the values of this variable.
 					</info-popover>
 				</span>
 				<div class="gf-form-select-wrapper max-width-14">
-					<select class="gf-form-input" ng-model="current.sort" ng-options="f.value as f.text for f in sortOptions" ng-change="runQuery()"></select>
+					<select class="gf-form-input" ng-model="current.sort" ng-options="f.value as f.text for f in sortOptions"
+					 ng-change="runQuery()"></select>
 				</div>
 			</div>
 		</div>
@@ -219,7 +234,8 @@
 			<div class="gf-form">
 				<label class="gf-form-label width-12">Type</label>
 				<div class="gf-form-select-wrapper max-width-18">
-					<select class="gf-form-input" ng-model="current.query" ng-options="f.value as f.text for f in datasourceTypes" ng-change="runQuery()"></select>
+					<select class="gf-form-input" ng-model="current.query" ng-options="f.value as f.text for f in datasourceTypes"
+					 ng-change="runQuery()"></select>
 				</div>
 			</div>
 
@@ -234,7 +250,8 @@
 
 					</info-popover>
 				</label>
-				<input type="text" class="gf-form-input max-width-18" ng-model='current.regex' placeholder="/.*-(.*)-.*/" ng-model-onblur ng-change="runQuery()"></input>
+				<input type="text" class="gf-form-input max-width-18" ng-model='current.regex' placeholder="/.*-(.*)-.*/"
+				 ng-model-onblur ng-change="runQuery()"></input>
 			</div>
 		</div>
 
@@ -243,7 +260,8 @@
 			<div class="gf-form max-width-21">
 				<span class="gf-form-label width-8">Data source</span>
 				<div class="gf-form-select-wrapper max-width-14">
-					<select class="gf-form-input" ng-model="current.datasource" ng-options="f.value as f.name for f in datasources" required ng-change="validate()">
+					<select class="gf-form-input" ng-model="current.datasource" ng-options="f.value as f.name for f in datasources"
+					 required ng-change="validate()">
 						<option value="" ng-if="false"></option>
 					</select>
 				</div>
@@ -253,18 +271,11 @@
 		<div class="section gf-form-group" ng-show="variableTypes[current.type].supportsMulti">
 			<h5 class="section-heading">Selection Options</h5>
 			<div class="section">
-				<gf-form-switch class="gf-form"
-										label="Multi-value"
-					label-class="width-10"
-		 tooltip="Enables multiple values to be selected at the same time"
-	 checked="current.multi"
-	on-change="runQuery()">
+				<gf-form-switch class="gf-form" label="Multi-value" label-class="width-10" tooltip="Enables multiple values to be selected at the same time"
+				 checked="current.multi" on-change="runQuery()">
 				</gf-form-switch>
-				<gf-form-switch class="gf-form"
-										label="Include All option"
-					label-class="width-10"
-		 checked="current.includeAll"
-	 on-change="runQuery()">
+				<gf-form-switch class="gf-form" label="Include All option" label-class="width-10" checked="current.includeAll"
+				 on-change="runQuery()">
 				</gf-form-switch>
 			</div>
 			<div class="gf-form" ng-if="current.includeAll">
@@ -279,11 +290,13 @@
 			</gf-form-switch>
 			<div class="gf-form last" ng-if="current.useTags">
 				<span class="gf-form-label width-10">Tags query</span>
-				<input type="text" class="gf-form-input" ng-model='current.tagsQuery' placeholder="metric name or tags query" ng-model-onblur></input>
+				<input type="text" class="gf-form-input" ng-model='current.tagsQuery' placeholder="metric name or tags query"
+				 ng-model-onblur></input>
 			</div>
 			<div class="gf-form" ng-if="current.useTags">
 				<li class="gf-form-label width-10">Tag values query</li>
-				<input type="text" class="gf-form-input" ng-model='current.tagValuesQuery' placeholder="apps.$tag.*" ng-model-onblur></input>
+				<input type="text" class="gf-form-input" ng-model='current.tagValuesQuery' placeholder="apps.$tag.*"
+				 ng-model-onblur></input>
 			</div>
 		</div>
 
@@ -291,11 +304,11 @@
 			<h5>Preview of values</h5>
 			<div class="gf-form-inline">
 				<div class="gf-form" ng-repeat="option in current.options | limitTo: optionsLimit">
-          <span class="gf-form-label">{{option.text}}</span>
-        </div>
-        <div class="gf-form" ng-if= "current.options.length > optionsLimit">
-          <a class="gf-form-label btn-secondary" ng-click="showMoreOptions()">Show more</a>
-        </div>
+					<span class="gf-form-label">{{option.text}}</span>
+				</div>
+				<div class="gf-form" ng-if="current.options.length > optionsLimit">
+					<a class="gf-form-label btn-secondary" ng-click="showMoreOptions()">Show more</a>
+				</div>
 			</div>
 		</div>
 
@@ -309,5 +322,4 @@
 		</div>
 
 	</form>
-</div>
-
+</div>

+ 2 - 0
public/app/features/templating/query_variable.ts

@@ -23,6 +23,7 @@ export class QueryVariable implements Variable {
   tagValuesQuery: string;
   tags: any[];
   skipUrlSync: boolean;
+  definition: string;
 
   defaults = {
     type: 'query',
@@ -44,6 +45,7 @@ export class QueryVariable implements Variable {
     tagsQuery: '',
     tagValuesQuery: '',
     skipUrlSync: false,
+    definition: '',
   };
 
   /** @ngInject */

+ 2 - 0
public/app/features/templating/variable.ts

@@ -1,3 +1,4 @@
+import _ from 'lodash';
 import { assignModelProperties } from 'app/core/utils/model_utils';
 
 /*
@@ -28,6 +29,7 @@ export { assignModelProperties };
 
 export function containsVariable(...args: any[]) {
   const variableName = args[args.length - 1];
+  args[0] = _.isString(args[0]) ? args[0] : Object['values'](args[0]).join(' ');
   const variableString = args.slice(0, -1).join(' ');
   const matches = variableString.match(variableRegex);
   const isMatchingVariable =

+ 129 - 0
public/app/plugins/datasource/stackdriver/StackdriverMetricFindQuery.ts

@@ -0,0 +1,129 @@
+import isString from 'lodash/isString';
+import { alignmentPeriods } from './constants';
+import { MetricFindQueryTypes } from './types';
+import {
+  getMetricTypesByService,
+  getAlignmentOptionsByMetric,
+  getAggregationOptionsByMetric,
+  extractServicesFromMetricDescriptors,
+  getLabelKeys,
+} from './functions';
+
+export default class StackdriverMetricFindQuery {
+  constructor(private datasource) {}
+
+  async execute(query: any) {
+    try {
+      switch (query.selectedQueryType) {
+        case MetricFindQueryTypes.Services:
+          return this.handleServiceQuery();
+        case MetricFindQueryTypes.MetricTypes:
+          return this.handleMetricTypesQuery(query);
+        case MetricFindQueryTypes.LabelKeys:
+          return this.handleLabelKeysQuery(query);
+        case MetricFindQueryTypes.LabelValues:
+          return this.handleLabelValuesQuery(query);
+        case MetricFindQueryTypes.ResourceTypes:
+          return this.handleResourceTypeQuery(query);
+        case MetricFindQueryTypes.Aligners:
+          return this.handleAlignersQuery(query);
+        case MetricFindQueryTypes.AlignmentPeriods:
+          return this.handleAlignmentPeriodQuery();
+        case MetricFindQueryTypes.Aggregations:
+          return this.handleAggregationQuery(query);
+        default:
+          return [];
+      }
+    } catch (error) {
+      console.error(`Could not run StackdriverMetricFindQuery ${query}`, error);
+      return [];
+    }
+  }
+
+  async handleServiceQuery() {
+    const metricDescriptors = await this.datasource.getMetricTypes(this.datasource.projectName);
+    const services = extractServicesFromMetricDescriptors(metricDescriptors);
+    return services.map(s => ({
+      text: s.serviceShortName,
+      value: s.service,
+      expandable: true,
+    }));
+  }
+
+  async handleMetricTypesQuery({ selectedService }) {
+    if (!selectedService) {
+      return [];
+    }
+    const metricDescriptors = await this.datasource.getMetricTypes(this.datasource.projectName);
+    return getMetricTypesByService(metricDescriptors, this.datasource.templateSrv.replace(selectedService)).map(s => ({
+      text: s.displayName,
+      value: s.type,
+      expandable: true,
+    }));
+  }
+
+  async handleLabelKeysQuery({ selectedMetricType }) {
+    if (!selectedMetricType) {
+      return [];
+    }
+    const labelKeys = await getLabelKeys(this.datasource, selectedMetricType);
+    return labelKeys.map(this.toFindQueryResult);
+  }
+
+  async handleLabelValuesQuery({ selectedMetricType, labelKey }) {
+    if (!selectedMetricType) {
+      return [];
+    }
+    const refId = 'handleLabelValuesQuery';
+    const response = await this.datasource.getLabels(selectedMetricType, refId);
+    const interpolatedKey = this.datasource.templateSrv.replace(labelKey);
+    const [name] = interpolatedKey.split('.').reverse();
+    let values = [];
+    if (response.meta && response.meta.metricLabels && response.meta.metricLabels.hasOwnProperty(name)) {
+      values = response.meta.metricLabels[name];
+    } else if (response.meta && response.meta.resourceLabels && response.meta.resourceLabels.hasOwnProperty(name)) {
+      values = response.meta.resourceLabels[name];
+    }
+
+    return values.map(this.toFindQueryResult);
+  }
+
+  async handleResourceTypeQuery({ selectedMetricType }) {
+    if (!selectedMetricType) {
+      return [];
+    }
+    const refId = 'handleResourceTypeQueryQueryType';
+    const response = await this.datasource.getLabels(selectedMetricType, refId);
+    return response.meta.resourceTypes ? response.meta.resourceTypes.map(this.toFindQueryResult) : [];
+  }
+
+  async handleAlignersQuery({ selectedMetricType }) {
+    if (!selectedMetricType) {
+      return [];
+    }
+    const metricDescriptors = await this.datasource.getMetricTypes(this.datasource.projectName);
+    const { valueType, metricKind } = metricDescriptors.find(
+      m => m.type === this.datasource.templateSrv.replace(selectedMetricType)
+    );
+    return getAlignmentOptionsByMetric(valueType, metricKind).map(this.toFindQueryResult);
+  }
+
+  async handleAggregationQuery({ selectedMetricType }) {
+    if (!selectedMetricType) {
+      return [];
+    }
+    const metricDescriptors = await this.datasource.getMetricTypes(this.datasource.projectName);
+    const { valueType, metricKind } = metricDescriptors.find(
+      m => m.type === this.datasource.templateSrv.replace(selectedMetricType)
+    );
+    return getAggregationOptionsByMetric(valueType, metricKind).map(this.toFindQueryResult);
+  }
+
+  handleAlignmentPeriodQuery() {
+    return alignmentPeriods.map(this.toFindQueryResult);
+  }
+
+  toFindQueryResult(x) {
+    return isString(x) ? { text: x, expandable: true } : { ...x, expandable: true };
+  }
+}

+ 28 - 0
public/app/plugins/datasource/stackdriver/components/SimpleSelect.tsx

@@ -0,0 +1,28 @@
+import React, { SFC } from 'react';
+
+interface Props {
+  onValueChange: (e) => void;
+  options: any[];
+  value: string;
+  label: string;
+}
+
+const SimpleSelect: SFC<Props> = props => {
+  const { label, onValueChange, value, options } = props;
+  return (
+    <div className="gf-form max-width-21">
+      <span className="gf-form-label width-10 query-keyword">{label}</span>
+      <div className="gf-form-select-wrapper max-width-12">
+        <select className="gf-form-input" required onChange={onValueChange} value={value}>
+          {options.map(({ value, name }, i) => (
+            <option key={i} value={value}>
+              {name}
+            </option>
+          ))}
+        </select>
+      </div>
+    </div>
+  );
+};
+
+export default SimpleSelect;

+ 47 - 0
public/app/plugins/datasource/stackdriver/components/VariableQueryEditor.test.tsx

@@ -0,0 +1,47 @@
+import React from 'react';
+import renderer from 'react-test-renderer';
+import { StackdriverVariableQueryEditor } from './VariableQueryEditor';
+import { VariableQueryProps } from 'app/types/plugins';
+import { MetricFindQueryTypes } from '../types';
+
+jest.mock('../functions', () => ({
+  getMetricTypes: () => ({ metricTypes: [], selectedMetricType: '' }),
+  extractServicesFromMetricDescriptors: () => [],
+}));
+
+const props: VariableQueryProps = {
+  onChange: (query, definition) => {},
+  query: {},
+  datasource: {
+    getMetricTypes: async p => [],
+  },
+  templateSrv: { replace: s => s, variables: [] },
+};
+
+describe('VariableQueryEditor', () => {
+  it('renders correctly', () => {
+    const tree = renderer.create(<StackdriverVariableQueryEditor {...props} />).toJSON();
+    expect(tree).toMatchSnapshot();
+  });
+
+  describe('and a new variable is created', () => {
+    it('should trigger a query using the first query type in the array', done => {
+      props.onChange = (query, definition) => {
+        expect(definition).toBe('Stackdriver - Services');
+        done();
+      };
+      renderer.create(<StackdriverVariableQueryEditor {...props} />).toJSON();
+    });
+  });
+
+  describe('and an existing variable is edited', () => {
+    it('should trigger new query using the saved query type', done => {
+      props.query = { selectedQueryType: MetricFindQueryTypes.LabelKeys };
+      props.onChange = (query, definition) => {
+        expect(definition).toBe('Stackdriver - Label Keys');
+        done();
+      };
+      renderer.create(<StackdriverVariableQueryEditor {...props} />).toJSON();
+    });
+  });
+});

+ 196 - 0
public/app/plugins/datasource/stackdriver/components/VariableQueryEditor.tsx

@@ -0,0 +1,196 @@
+import React, { PureComponent } from 'react';
+import { VariableQueryProps } from 'app/types/plugins';
+import SimpleSelect from './SimpleSelect';
+import { getMetricTypes, getLabelKeys, extractServicesFromMetricDescriptors } from '../functions';
+import { MetricFindQueryTypes, VariableQueryData } from '../types';
+
+export class StackdriverVariableQueryEditor extends PureComponent<VariableQueryProps, VariableQueryData> {
+  queryTypes: Array<{ value: string; name: string }> = [
+    { value: MetricFindQueryTypes.Services, name: 'Services' },
+    { value: MetricFindQueryTypes.MetricTypes, name: 'Metric Types' },
+    { value: MetricFindQueryTypes.LabelKeys, name: 'Label Keys' },
+    { value: MetricFindQueryTypes.LabelValues, name: 'Label Values' },
+    { value: MetricFindQueryTypes.ResourceTypes, name: 'Resource Types' },
+    { value: MetricFindQueryTypes.Aggregations, name: 'Aggregations' },
+    { value: MetricFindQueryTypes.Aligners, name: 'Aligners' },
+    { value: MetricFindQueryTypes.AlignmentPeriods, name: 'Alignment Periods' },
+  ];
+
+  defaults: VariableQueryData = {
+    selectedQueryType: this.queryTypes[0].value,
+    metricDescriptors: [],
+    selectedService: '',
+    selectedMetricType: '',
+    labels: [],
+    labelKey: '',
+    metricTypes: [],
+    services: [],
+  };
+
+  constructor(props: VariableQueryProps) {
+    super(props);
+    this.state = Object.assign(this.defaults, this.props.query);
+  }
+
+  async componentDidMount() {
+    const metricDescriptors = await this.props.datasource.getMetricTypes(this.props.datasource.projectName);
+    const services = extractServicesFromMetricDescriptors(metricDescriptors).map(m => ({
+      value: m.service,
+      name: m.serviceShortName,
+    }));
+
+    let selectedService = '';
+    if (services.some(s => s.value === this.props.templateSrv.replace(this.state.selectedService))) {
+      selectedService = this.state.selectedService;
+    } else if (services && services.length > 0) {
+      selectedService = services[0].value;
+    }
+
+    const { metricTypes, selectedMetricType } = getMetricTypes(
+      metricDescriptors,
+      this.state.selectedMetricType,
+      this.props.templateSrv.replace(this.state.selectedMetricType),
+      this.props.templateSrv.replace(selectedService)
+    );
+    const state: any = {
+      services,
+      selectedService,
+      metricTypes,
+      selectedMetricType,
+      metricDescriptors,
+      ...await this.getLabels(selectedMetricType),
+    };
+    this.setState(state);
+  }
+
+  async handleQueryTypeChange(event) {
+    const state: any = {
+      selectedQueryType: event.target.value,
+      ...await this.getLabels(this.state.selectedMetricType, event.target.value),
+    };
+    this.setState(state);
+  }
+
+  async onServiceChange(event) {
+    const { metricTypes, selectedMetricType } = getMetricTypes(
+      this.state.metricDescriptors,
+      this.state.selectedMetricType,
+      this.props.templateSrv.replace(this.state.selectedMetricType),
+      this.props.templateSrv.replace(event.target.value)
+    );
+    const state: any = {
+      selectedService: event.target.value,
+      metricTypes,
+      selectedMetricType,
+      ...await this.getLabels(selectedMetricType),
+    };
+    this.setState(state);
+  }
+
+  async onMetricTypeChange(event) {
+    const state: any = { selectedMetricType: event.target.value, ...await this.getLabels(event.target.value) };
+    this.setState(state);
+  }
+
+  onLabelKeyChange(event) {
+    this.setState({ labelKey: event.target.value });
+  }
+
+  componentDidUpdate() {
+    const { metricDescriptors, labels, metricTypes, services, ...queryModel } = this.state;
+    const query = this.queryTypes.find(q => q.value === this.state.selectedQueryType);
+    this.props.onChange(queryModel, `Stackdriver - ${query.name}`);
+  }
+
+  async getLabels(selectedMetricType, selectedQueryType = this.state.selectedQueryType) {
+    let result = { labels: this.state.labels, labelKey: this.state.labelKey };
+    if (selectedMetricType && selectedQueryType === MetricFindQueryTypes.LabelValues) {
+      const labels = await getLabelKeys(this.props.datasource, selectedMetricType);
+      const labelKey = labels.some(l => l === this.props.templateSrv.replace(this.state.labelKey))
+        ? this.state.labelKey
+        : labels[0];
+      result = { labels, labelKey };
+    }
+    return result;
+  }
+
+  insertTemplateVariables(options) {
+    const templateVariables = this.props.templateSrv.variables.map(v => ({ name: `$${v.name}`, value: `$${v.name}` }));
+    return [...templateVariables, ...options];
+  }
+
+  renderQueryTypeSwitch(queryType) {
+    switch (queryType) {
+      case MetricFindQueryTypes.MetricTypes:
+        return (
+          <SimpleSelect
+            value={this.state.selectedService}
+            options={this.insertTemplateVariables(this.state.services)}
+            onValueChange={e => this.onServiceChange(e)}
+            label="Service"
+          />
+        );
+      case MetricFindQueryTypes.LabelKeys:
+      case MetricFindQueryTypes.LabelValues:
+      case MetricFindQueryTypes.ResourceTypes:
+        return (
+          <React.Fragment>
+            <SimpleSelect
+              value={this.state.selectedService}
+              options={this.insertTemplateVariables(this.state.services)}
+              onValueChange={e => this.onServiceChange(e)}
+              label="Service"
+            />
+            <SimpleSelect
+              value={this.state.selectedMetricType}
+              options={this.insertTemplateVariables(this.state.metricTypes)}
+              onValueChange={e => this.onMetricTypeChange(e)}
+              label="Metric Type"
+            />
+            {queryType === MetricFindQueryTypes.LabelValues && (
+              <SimpleSelect
+                value={this.state.labelKey}
+                options={this.insertTemplateVariables(this.state.labels.map(l => ({ value: l, name: l })))}
+                onValueChange={e => this.onLabelKeyChange(e)}
+                label="Label Key"
+              />
+            )}
+          </React.Fragment>
+        );
+      case MetricFindQueryTypes.Aligners:
+      case MetricFindQueryTypes.Aggregations:
+        return (
+          <React.Fragment>
+            <SimpleSelect
+              value={this.state.selectedService}
+              options={this.insertTemplateVariables(this.state.services)}
+              onValueChange={e => this.onServiceChange(e)}
+              label="Service"
+            />
+            <SimpleSelect
+              value={this.state.selectedMetricType}
+              options={this.insertTemplateVariables(this.state.metricTypes)}
+              onValueChange={e => this.onMetricTypeChange(e)}
+              label="Metric Type"
+            />
+          </React.Fragment>
+        );
+      default:
+        return '';
+    }
+  }
+
+  render() {
+    return (
+      <React.Fragment>
+        <SimpleSelect
+          value={this.state.selectedQueryType}
+          options={this.queryTypes}
+          onValueChange={e => this.handleQueryTypeChange(e)}
+          label="Query Type"
+        />
+        {this.renderQueryTypeSwitch(this.state.selectedQueryType)}
+      </React.Fragment>
+    );
+  }
+}

+ 67 - 0
public/app/plugins/datasource/stackdriver/components/__snapshots__/VariableQueryEditor.test.tsx.snap

@@ -0,0 +1,67 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`VariableQueryEditor renders correctly 1`] = `
+Array [
+  <div
+    className="gf-form max-width-21"
+  >
+    <span
+      className="gf-form-label width-10 query-keyword"
+    >
+      Query Type
+    </span>
+    <div
+      className="gf-form-select-wrapper max-width-12"
+    >
+      <select
+        className="gf-form-input"
+        onChange={[Function]}
+        required={true}
+        value="services"
+      >
+        <option
+          value="services"
+        >
+          Services
+        </option>
+        <option
+          value="metricTypes"
+        >
+          Metric Types
+        </option>
+        <option
+          value="labelKeys"
+        >
+          Label Keys
+        </option>
+        <option
+          value="labelValues"
+        >
+          Label Values
+        </option>
+        <option
+          value="resourceTypes"
+        >
+          Resource Types
+        </option>
+        <option
+          value="aggregations"
+        >
+          Aggregations
+        </option>
+        <option
+          value="aligners"
+        >
+          Aligners
+        </option>
+        <option
+          value="alignmentPeriods"
+        >
+          Alignment Periods
+        </option>
+      </select>
+    </div>
+  </div>,
+  "",
+]
+`;

+ 22 - 14
public/app/plugins/datasource/stackdriver/datasource.ts

@@ -1,6 +1,7 @@
 import { stackdriverUnitMappings } from './constants';
 import appEvents from 'app/core/app_events';
 import _ from 'lodash';
+import StackdriverMetricFindQuery from './StackdriverMetricFindQuery';
 
 export default class StackdriverDatasource {
   id: number;
@@ -9,6 +10,7 @@ export default class StackdriverDatasource {
   projectName: string;
   authenticationType: string;
   queryPromise: Promise<any>;
+  metricTypes: any[];
 
   /** @ngInject */
   constructor(instanceSettings, private backendSrv, private templateSrv, private timeSrv) {
@@ -18,6 +20,7 @@ export default class StackdriverDatasource {
     this.id = instanceSettings.id;
     this.projectName = instanceSettings.jsonData.defaultProject || '';
     this.authenticationType = instanceSettings.jsonData.authenticationType || 'jwt';
+    this.metricTypes = [];
   }
 
   async getTimeSeries(options) {
@@ -67,7 +70,7 @@ export default class StackdriverDatasource {
   }
 
   async getLabels(metricType, refId) {
-    return await this.getTimeSeries({
+    const response = await this.getTimeSeries({
       targets: [
         {
           refId: refId,
@@ -81,6 +84,8 @@ export default class StackdriverDatasource {
       ],
       range: this.timeSrv.timeRange(),
     });
+
+    return response.results[refId];
   }
 
   interpolateGroupBys(groupBys: string[], scopedVars): string[] {
@@ -177,8 +182,9 @@ export default class StackdriverDatasource {
     return results;
   }
 
-  metricFindQuery(query) {
-    throw new Error('Template variables support is not yet imlemented');
+  async metricFindQuery(query) {
+    const stackdriverMetricFindQuery = new StackdriverMetricFindQuery(this);
+    return stackdriverMetricFindQuery.execute(query);
   }
 
   async testDatasource() {
@@ -258,19 +264,21 @@ export default class StackdriverDatasource {
 
   async getMetricTypes(projectName: string) {
     try {
-      const metricsApiPath = `v3/projects/${projectName}/metricDescriptors`;
-      const { data } = await this.doRequest(`${this.baseUrl}${metricsApiPath}`);
+      if (this.metricTypes.length === 0) {
+        const metricsApiPath = `v3/projects/${projectName}/metricDescriptors`;
+        const { data } = await this.doRequest(`${this.baseUrl}${metricsApiPath}`);
 
-      const metrics = data.metricDescriptors.map(m => {
-        const [service] = m.type.split('/');
-        const [serviceShortName] = service.split('.');
-        m.service = service;
-        m.serviceShortName = serviceShortName;
-        m.displayName = m.displayName || m.type;
-        return m;
-      });
+        this.metricTypes = data.metricDescriptors.map(m => {
+          const [service] = m.type.split('/');
+          const [serviceShortName] = service.split('.');
+          m.service = service;
+          m.serviceShortName = serviceShortName;
+          m.displayName = m.displayName || m.type;
+          return m;
+        });
+      }
 
-      return metrics;
+      return this.metricTypes;
     } catch (error) {
       appEvents.emit('ds-request-error', this.formatStackdriverError(error));
       return [];

+ 48 - 0
public/app/plugins/datasource/stackdriver/functions.ts

@@ -0,0 +1,48 @@
+import uniqBy from 'lodash/uniqBy';
+import { alignOptions, aggOptions } from './constants';
+
+export const extractServicesFromMetricDescriptors = metricDescriptors => uniqBy(metricDescriptors, 'service');
+
+export const getMetricTypesByService = (metricDescriptors, service) =>
+  metricDescriptors.filter(m => m.service === service);
+
+export const getMetricTypes = (metricDescriptors, metricType, interpolatedMetricType, selectedService) => {
+  const metricTypes = getMetricTypesByService(metricDescriptors, selectedService).map(m => ({
+    value: m.type,
+    name: m.displayName,
+  }));
+  const metricTypeExistInArray = metricTypes.some(m => m.value === interpolatedMetricType);
+  const selectedMetricType = metricTypeExistInArray ? metricType : metricTypes[0].value;
+  return {
+    metricTypes,
+    selectedMetricType,
+  };
+};
+
+export const getAlignmentOptionsByMetric = (metricValueType, metricKind) => {
+  return !metricValueType
+    ? []
+    : alignOptions.filter(i => {
+        return i.valueTypes.indexOf(metricValueType) !== -1 && i.metricKinds.indexOf(metricKind) !== -1;
+      });
+};
+
+export const getAggregationOptionsByMetric = (valueType, metricKind) => {
+  return !metricKind
+    ? []
+    : aggOptions.filter(i => {
+        return i.valueTypes.indexOf(valueType) !== -1 && i.metricKinds.indexOf(metricKind) !== -1;
+      });
+};
+
+export const getLabelKeys = async (datasource, selectedMetricType) => {
+  const refId = 'handleLabelKeysQuery';
+  const response = await datasource.getLabels(selectedMetricType, refId);
+  const labelKeys = response.meta
+    ? [
+        ...Object.keys(response.meta.resourceLabels).map(l => `resource.label.${l}`),
+        ...Object.keys(response.meta.metricLabels).map(l => `metric.label.${l}`),
+      ]
+    : [];
+  return labelKeys;
+};

+ 2 - 0
public/app/plugins/datasource/stackdriver/module.ts

@@ -2,10 +2,12 @@ import StackdriverDatasource from './datasource';
 import { StackdriverQueryCtrl } from './query_ctrl';
 import { StackdriverConfigCtrl } from './config_ctrl';
 import { StackdriverAnnotationsQueryCtrl } from './annotations_query_ctrl';
+import { StackdriverVariableQueryEditor } from './components/VariableQueryEditor';
 
 export {
   StackdriverDatasource as Datasource,
   StackdriverQueryCtrl as QueryCtrl,
   StackdriverConfigCtrl as ConfigCtrl,
   StackdriverAnnotationsQueryCtrl as AnnotationsQueryCtrl,
+  StackdriverVariableQueryEditor as VariableQueryEditor,
 };

+ 6 - 6
public/app/plugins/datasource/stackdriver/partials/query.aggregation.html

@@ -2,8 +2,8 @@
   <div class="gf-form">
     <label class="gf-form-label query-keyword width-9">Aggregation</label>
     <div class="gf-form-select-wrapper gf-form-select-wrapper--caret-indent">
-      <select class="gf-form-input width-12" ng-model="ctrl.target.aggregation.crossSeriesReducer" ng-options="f.value as f.text for f in ctrl.aggOptions"
-        ng-change="refresh()"></select>
+      <gf-form-dropdown model="ctrl.target.aggregation.crossSeriesReducer" get-options="ctrl.aggOptions" class="gf-form width-12"
+        disabled type="text" allow-custom="true" lookup-text="true" css-class="min-width-12" on-change="refresh()"></gf-form-dropdown>
     </div>
   </div>
   <div class="gf-form gf-form--grow">
@@ -20,8 +20,8 @@
   <div class="gf-form offset-width-9">
     <label class="gf-form-label query-keyword width-12">Aligner</label>
     <div class="gf-form-select-wrapper gf-form-select-wrapper--caret-indent">
-      <select class="gf-form-input width-14" ng-model="ctrl.target.aggregation.perSeriesAligner" ng-options="f.value as f.text for f in ctrl.alignOptions"
-        ng-change="refresh()"></select>
+      <gf-form-dropdown model="ctrl.target.aggregation.perSeriesAligner" get-options="ctrl.alignOptions" class="gf-form width-12"
+        disabled type="text" allow-custom="true" lookup-text="true" css-class="min-width-12" on-change="refresh()"></gf-form-dropdown>
     </div>
 
     <div class="gf-form gf-form--grow">
@@ -33,8 +33,8 @@
   <div class="gf-form">
     <label class="gf-form-label query-keyword width-9">Alignment Period</label>
     <div class="gf-form-select-wrapper gf-form-select-wrapper--caret-indent">
-      <select class="gf-form-input width-12" ng-model="ctrl.target.aggregation.alignmentPeriod" ng-options="f.value as f.text for f in ctrl.alignmentPeriods"
-        ng-change="refresh()"></select>
+      <gf-form-dropdown model="ctrl.target.aggregation.alignmentPeriod" get-options="ctrl.alignmentPeriods" class="gf-form width-12"
+        disabled type="text" allow-custom="true" lookup-text="true" css-class="min-width-12" on-change="refresh()"></gf-form-dropdown>
     </div>
   </div>
 

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

@@ -14,7 +14,7 @@
   </div>
   <div class="gf-form-inline">
     <div class="gf-form">
-      <span class="gf-form-label width-9">Project</span>
+      <span class="gf-form-label width-9 query-keyword">Project</span>
       <input class="gf-form-input" disabled type="text" ng-model='ctrl.target.defaultProject' css-class="min-width-12" />
     </div>
     <div class="gf-form">
@@ -70,4 +70,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>

+ 32 - 17
public/app/plugins/datasource/stackdriver/partials/query.filter.html

@@ -1,37 +1,52 @@
 <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>
+    <span class="gf-form-label width-9 query-keyword">Service</span>
+    <select
+      class="gf-form-input width-12"
+      ng-model="ctrl.service"
+      ng-options="f.value as f.text for f in ctrl.services"
+      ng-change="ctrl.onServiceChange(ctrl.service)"
+    ></select>
   </div>
   <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>
+    <span class="gf-form-label width-9 query-keyword">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>
+      <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 class="gf-form gf-form--grow"><div class="gf-form-label gf-form-label--grow"></div></div>
 </div>
 <div class="gf-form-inline" ng-hide="ctrl.$scope.hideGroupBys">
   <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)" on-change="ctrl.groupByChanged(segment, $index)"></metric-segment>
+      <metric-segment
+        segment="segment"
+        get-options="ctrl.getGroupBys(segment)"
+        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 class="gf-form gf-form--grow"><div class="gf-form-label gf-form-label--grow"></div></div>
 </div>

+ 12 - 19
public/app/plugins/datasource/stackdriver/query_aggregation_ctrl.ts

@@ -1,6 +1,7 @@
 import coreModule from 'app/core/core_module';
 import _ from 'lodash';
 import * as options from './constants';
+import { getAlignmentOptionsByMetric, getAggregationOptionsByMetric } from './functions';
 import kbn from 'app/core/utils/kbn';
 
 export class StackdriverAggregation {
@@ -25,7 +26,7 @@ export class StackdriverAggregationCtrl {
   target: any;
 
   /** @ngInject */
-  constructor(private $scope) {
+  constructor(private $scope, private templateSrv) {
     this.$scope.ctrl = this;
     this.target = $scope.target;
     this.alignmentPeriods = options.alignmentPeriods;
@@ -41,28 +42,16 @@ export class StackdriverAggregationCtrl {
   }
 
   setAlignOptions() {
-    this.alignOptions = !this.target.valueType
-      ? []
-      : options.alignOptions.filter(i => {
-          return (
-            i.valueTypes.indexOf(this.target.valueType) !== -1 && i.metricKinds.indexOf(this.target.metricKind) !== -1
-          );
-        });
-    if (!this.alignOptions.find(o => o.value === this.target.aggregation.perSeriesAligner)) {
+    this.alignOptions = getAlignmentOptionsByMetric(this.target.valueType, this.target.metricKind);
+    if (!this.alignOptions.find(o => o.value === this.templateSrv.replace(this.target.aggregation.perSeriesAligner))) {
       this.target.aggregation.perSeriesAligner = this.alignOptions.length > 0 ? this.alignOptions[0].value : '';
     }
   }
 
   setAggOptions() {
-    this.aggOptions = !this.target.metricKind
-      ? []
-      : options.aggOptions.filter(i => {
-          return (
-            i.valueTypes.indexOf(this.target.valueType) !== -1 && i.metricKinds.indexOf(this.target.metricKind) !== -1
-          );
-        });
+    this.aggOptions = getAggregationOptionsByMetric(this.target.valueType, this.target.metricKind);
 
-    if (!this.aggOptions.find(o => o.value === this.target.aggregation.crossSeriesReducer)) {
+    if (!this.aggOptions.find(o => o.value === this.templateSrv.replace(this.target.aggregation.crossSeriesReducer))) {
       this.deselectAggregationOption('REDUCE_NONE');
     }
 
@@ -73,8 +62,12 @@ export class StackdriverAggregationCtrl {
   }
 
   formatAlignmentText() {
-    const selectedAlignment = this.alignOptions.find(ap => ap.value === this.target.aggregation.perSeriesAligner);
-    return `${kbn.secondsToHms(this.$scope.alignmentPeriod)} interval (${selectedAlignment.text})`;
+    const selectedAlignment = this.alignOptions.find(
+      ap => ap.value === this.templateSrv.replace(this.target.aggregation.perSeriesAligner)
+    );
+    return `${kbn.secondsToHms(this.$scope.alignmentPeriod)} interval (${
+      selectedAlignment ? selectedAlignment.text : ''
+    })`;
   }
 
   deselectAggregationOption(notValidOptionValue: string) {

+ 0 - 1
public/app/plugins/datasource/stackdriver/query_ctrl.ts

@@ -62,7 +62,6 @@ export class StackdriverQueryCtrl extends QueryCtrl {
   constructor($scope, $injector) {
     super($scope, $injector);
     _.defaultsDeep(this.target, this.defaults);
-
     this.panelCtrl.events.on('data-received', this.onDataReceived.bind(this), $scope);
     this.panelCtrl.events.on('data-error', this.onDataError.bind(this), $scope);
   }

+ 8 - 6
public/app/plugins/datasource/stackdriver/query_filter_ctrl.ts

@@ -139,7 +139,7 @@ export class StackdriverFilterCtrl {
       result = metrics.filter(m => m.service === this.target.service);
     }
 
-    if (result.find(m => m.value === this.target.metricType)) {
+    if (result.find(m => m.value === this.templateSrv.replace(this.target.metricType))) {
       this.metricType = this.target.metricType;
     } else if (result.length > 0) {
       this.metricType = this.target.metricType = result[0].value;
@@ -150,10 +150,10 @@ export class StackdriverFilterCtrl {
   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;
-        this.resourceTypes = data.results[this.target.refId].meta.resourceTypes;
+        const { meta } = await this.datasource.getLabels(this.target.metricType, this.target.refId);
+        this.metricLabels = meta.metricLabels;
+        this.resourceLabels = meta.resourceLabels;
+        this.resourceTypes = meta.resourceTypes;
         resolve();
       } catch (error) {
         if (error.data && error.data.message) {
@@ -187,7 +187,9 @@ export class StackdriverFilterCtrl {
 
   setMetricType() {
     this.target.metricType = this.metricType;
-    const { valueType, metricKind, unit } = this.metricDescriptors.find(m => m.type === this.target.metricType);
+    const { valueType, metricKind, unit } = this.metricDescriptors.find(
+      m => m.type === this.templateSrv.replace(this.metricType)
+    );
     this.target.unit = unit;
     this.target.valueType = valueType;
     this.target.metricKind = metricKind;

+ 25 - 11
public/app/plugins/datasource/stackdriver/specs/query_aggregation_ctrl.test.ts

@@ -6,10 +6,19 @@ describe('StackdriverAggregationCtrl', () => {
     describe('when new query result is returned from the server', () => {
       describe('and result is double and gauge and no group by is used', () => {
         beforeEach(async () => {
-          ctrl = new StackdriverAggregationCtrl({
-            $on: () => {},
-            target: { valueType: 'DOUBLE', metricKind: 'GAUGE', aggregation: { crossSeriesReducer: '', groupBys: [] } },
-          });
+          ctrl = new StackdriverAggregationCtrl(
+            {
+              $on: () => {},
+              target: {
+                valueType: 'DOUBLE',
+                metricKind: 'GAUGE',
+                aggregation: { crossSeriesReducer: '', groupBys: [] },
+              },
+            },
+            {
+              replace: s => s,
+            }
+          );
         });
 
         it('should populate all aggregate options except two', () => {
@@ -31,14 +40,19 @@ describe('StackdriverAggregationCtrl', () => {
 
       describe('and result is double and gauge and a group by is used', () => {
         beforeEach(async () => {
-          ctrl = new StackdriverAggregationCtrl({
-            $on: () => {},
-            target: {
-              valueType: 'DOUBLE',
-              metricKind: 'GAUGE',
-              aggregation: { crossSeriesReducer: 'REDUCE_NONE', groupBys: ['resource.label.projectid'] },
+          ctrl = new StackdriverAggregationCtrl(
+            {
+              $on: () => {},
+              target: {
+                valueType: 'DOUBLE',
+                metricKind: 'GAUGE',
+                aggregation: { crossSeriesReducer: 'REDUCE_NONE', groupBys: ['resource.label.projectid'] },
+              },
             },
-          });
+            {
+              replace: s => s,
+            }
+          );
         });
 
         it('should populate all aggregate options except three', () => {

+ 21 - 0
public/app/plugins/datasource/stackdriver/types.ts

@@ -0,0 +1,21 @@
+export enum MetricFindQueryTypes {
+  Services = 'services',
+  MetricTypes = 'metricTypes',
+  LabelKeys = 'labelKeys',
+  LabelValues = 'labelValues',
+  ResourceTypes = 'resourceTypes',
+  Aggregations = 'aggregations',
+  Aligners = 'aligners',
+  AlignmentPeriods = 'alignmentPeriods',
+}
+
+export interface VariableQueryData {
+  selectedQueryType: string;
+  metricDescriptors: any[];
+  selectedService: string;
+  selectedMetricType: string;
+  labels: string[];
+  labelKey: string;
+  metricTypes: Array<{ value: string; name: string }>;
+  services: Array<{ value: string; name: string }>;
+}

+ 8 - 0
public/app/types/plugins.ts

@@ -6,6 +6,7 @@ export interface PluginExports {
   QueryCtrl?: any;
   ConfigCtrl?: any;
   AnnotationsQueryCtrl?: any;
+  VariableQueryEditor?: any;
   ExploreQueryField?: any;
   ExploreStartPage?: any;
 
@@ -98,3 +99,10 @@ export interface PluginsState {
   hasFetched: boolean;
   dashboards: PluginDashboard[];
 }
+
+export interface VariableQueryProps {
+  query: any;
+  onChange: (query: any, definition: string) => void;
+  datasource: any;
+  templateSrv: any;
+}