diff --git a/docs/sources/features/datasources/stackdriver.md b/docs/sources/features/datasources/stackdriver.md index d19dbe4ea50..2c14d897d8e 100644 --- a/docs/sources/features/datasources/stackdriver.md +++ b/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 diff --git a/public/app/core/components/Picker/__snapshots__/PickerOption.test.tsx.snap b/public/app/core/components/Picker/__snapshots__/PickerOption.test.tsx.snap index 748fcbee4aa..b376ab24934 100644 --- a/public/app/core/components/Picker/__snapshots__/PickerOption.test.tsx.snap +++ b/public/app/core/components/Picker/__snapshots__/PickerOption.test.tsx.snap @@ -14,3 +14,4 @@ exports[`PickerOption renders correctly 1`] = ` `; + \ No newline at end of file diff --git a/public/app/features/plugins/VariableQueryComponentLoader.tsx b/public/app/features/plugins/VariableQueryComponentLoader.tsx new file mode 100644 index 00000000000..631161b4e9b --- /dev/null +++ b/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(, elem[0]); + scope.$on('$destroy', () => { + ReactDOM.unmountComponentAtNode(elem[0]); + }); + }, + }; +} + +coreModule.directive('variableQueryEditorLoader', variableQueryEditorLoader); diff --git a/public/app/features/plugins/all.ts b/public/app/features/plugins/all.ts index c9fb250266c..757a9a2f491 100644 --- a/public/app/features/plugins/all.ts +++ b/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'; diff --git a/public/app/features/templating/DefaultVariableQueryEditor.tsx b/public/app/features/templating/DefaultVariableQueryEditor.tsx new file mode 100644 index 00000000000..ea5f2acaade --- /dev/null +++ b/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 { + 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 ( +
+ Query + this.handleChange(e)} + onBlur={e => this.handleBlur(e)} + placeholder="metric name or tags query" + required + /> +
+ ); + } +} diff --git a/public/app/features/templating/editor_ctrl.ts b/public/app/features/templating/editor_ctrl.ts index cef7c9cc912..fdab5587b42 100644 --- a/public/app/features/templating/editor_ctrl.ts +++ b/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; + }); + }; } } diff --git a/public/app/features/templating/partials/editor.html b/public/app/features/templating/partials/editor.html index c4463972177..15984eba7d6 100644 --- a/public/app/features/templating/partials/editor.html +++ b/public/app/features/templating/partials/editor.html @@ -17,14 +17,16 @@
What do variables do?
-

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. +

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 - - Templating documentation - for more information. + Check out the + + Templating documentation + for more information.

@@ -32,7 +34,7 @@
@@ -51,7 +53,7 @@ @@ -77,7 +79,8 @@
Name - +
@@ -87,13 +90,15 @@
- +
- Template names cannot begin with '__', that's reserved for Grafana's global variables + Template names cannot begin with '__', that's reserved for + Grafana's global variables
@@ -115,7 +120,8 @@
Values - +
@@ -127,14 +133,16 @@ Step count How many times should the current time range be divided to calculate the value
- +
Min interval The calculated value will not go below this threshold - +
@@ -143,7 +151,8 @@
Custom Options
Values separated by comma - +
@@ -168,15 +177,17 @@
- Data source + Data source
-
+
- + Refresh When to update the values of this variable. @@ -187,28 +198,32 @@
+ + + + + +
- Query - -
-
- + Regex Optional, if you want to extract part of a series name or metric node segment. - +
- + Sort How to sort the values of this variable.
- +
@@ -219,7 +234,8 @@
- +
@@ -234,7 +250,8 @@ - + @@ -243,7 +260,8 @@
Data source
-
@@ -253,18 +271,11 @@
Selection Options
- + - +
@@ -279,11 +290,13 @@
Tags query - +
  • Tag values query
  • - +
    @@ -291,11 +304,11 @@
    Preview of values
    - {{option.text}} -
    -
    - Show more -
    + {{option.text}} +
    +
    + Show more +
    @@ -309,5 +322,4 @@ - - + \ No newline at end of file diff --git a/public/app/features/templating/query_variable.ts b/public/app/features/templating/query_variable.ts index d3f39023cfb..0aec1d8f412 100644 --- a/public/app/features/templating/query_variable.ts +++ b/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 */ diff --git a/public/app/features/templating/variable.ts b/public/app/features/templating/variable.ts index 1994e86eff0..930d2c49228 100644 --- a/public/app/features/templating/variable.ts +++ b/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 = diff --git a/public/app/plugins/datasource/stackdriver/StackdriverMetricFindQuery.ts b/public/app/plugins/datasource/stackdriver/StackdriverMetricFindQuery.ts new file mode 100644 index 00000000000..f8fc2e796ce --- /dev/null +++ b/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 }; + } +} diff --git a/public/app/plugins/datasource/stackdriver/components/SimpleSelect.tsx b/public/app/plugins/datasource/stackdriver/components/SimpleSelect.tsx new file mode 100644 index 00000000000..3a4a0707a2c --- /dev/null +++ b/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 => { + const { label, onValueChange, value, options } = props; + return ( +
    + {label} +
    + +
    +
    + ); +}; + +export default SimpleSelect; diff --git a/public/app/plugins/datasource/stackdriver/components/VariableQueryEditor.test.tsx b/public/app/plugins/datasource/stackdriver/components/VariableQueryEditor.test.tsx new file mode 100644 index 00000000000..0f31d25ee4e --- /dev/null +++ b/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().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().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().toJSON(); + }); + }); +}); diff --git a/public/app/plugins/datasource/stackdriver/components/VariableQueryEditor.tsx b/public/app/plugins/datasource/stackdriver/components/VariableQueryEditor.tsx new file mode 100644 index 00000000000..1f349ccab59 --- /dev/null +++ b/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 { + 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 ( + this.onServiceChange(e)} + label="Service" + /> + ); + case MetricFindQueryTypes.LabelKeys: + case MetricFindQueryTypes.LabelValues: + case MetricFindQueryTypes.ResourceTypes: + return ( + + this.onServiceChange(e)} + label="Service" + /> + this.onMetricTypeChange(e)} + label="Metric Type" + /> + {queryType === MetricFindQueryTypes.LabelValues && ( + ({ value: l, name: l })))} + onValueChange={e => this.onLabelKeyChange(e)} + label="Label Key" + /> + )} + + ); + case MetricFindQueryTypes.Aligners: + case MetricFindQueryTypes.Aggregations: + return ( + + this.onServiceChange(e)} + label="Service" + /> + this.onMetricTypeChange(e)} + label="Metric Type" + /> + + ); + default: + return ''; + } + } + + render() { + return ( + + this.handleQueryTypeChange(e)} + label="Query Type" + /> + {this.renderQueryTypeSwitch(this.state.selectedQueryType)} + + ); + } +} diff --git a/public/app/plugins/datasource/stackdriver/components/__snapshots__/VariableQueryEditor.test.tsx.snap b/public/app/plugins/datasource/stackdriver/components/__snapshots__/VariableQueryEditor.test.tsx.snap new file mode 100644 index 00000000000..e4ad23bc8e7 --- /dev/null +++ b/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 [ +
    + + Query Type + +
    + +
    +
    , + "", +] +`; diff --git a/public/app/plugins/datasource/stackdriver/datasource.ts b/public/app/plugins/datasource/stackdriver/datasource.ts index 034333cbb86..d1545655652 100644 --- a/public/app/plugins/datasource/stackdriver/datasource.ts +++ b/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; + 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 []; diff --git a/public/app/plugins/datasource/stackdriver/functions.ts b/public/app/plugins/datasource/stackdriver/functions.ts new file mode 100644 index 00000000000..e39a7d42508 --- /dev/null +++ b/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; +}; diff --git a/public/app/plugins/datasource/stackdriver/module.ts b/public/app/plugins/datasource/stackdriver/module.ts index 183c5c9ff88..1b81d29af73 100644 --- a/public/app/plugins/datasource/stackdriver/module.ts +++ b/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, }; diff --git a/public/app/plugins/datasource/stackdriver/partials/query.aggregation.html b/public/app/plugins/datasource/stackdriver/partials/query.aggregation.html index 379b9a36dc3..5f16d3fc61d 100755 --- a/public/app/plugins/datasource/stackdriver/partials/query.aggregation.html +++ b/public/app/plugins/datasource/stackdriver/partials/query.aggregation.html @@ -2,8 +2,8 @@
    - +
    @@ -20,8 +20,8 @@
    - +
    @@ -33,8 +33,8 @@
    - +
    diff --git a/public/app/plugins/datasource/stackdriver/partials/query.editor.html b/public/app/plugins/datasource/stackdriver/partials/query.editor.html index 98c8fcc83e8..5c7bc8935b1 100755 --- a/public/app/plugins/datasource/stackdriver/partials/query.editor.html +++ b/public/app/plugins/datasource/stackdriver/partials/query.editor.html @@ -14,7 +14,7 @@
    - Project + Project
    @@ -70,4 +70,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 index 5043161c492..b96b0720e33 100644 --- a/public/app/plugins/datasource/stackdriver/partials/query.filter.html +++ b/public/app/plugins/datasource/stackdriver/partials/query.filter.html @@ -1,37 +1,52 @@
    - Service - + Service +
    - Metric - -
    -
    -
    + 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 6cd6c805463..628cc494242 100644 --- a/public/app/plugins/datasource/stackdriver/query_aggregation_ctrl.ts +++ b/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) { diff --git a/public/app/plugins/datasource/stackdriver/query_ctrl.ts b/public/app/plugins/datasource/stackdriver/query_ctrl.ts index 3a1961eb14e..c2607964456 100644 --- a/public/app/plugins/datasource/stackdriver/query_ctrl.ts +++ b/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); } diff --git a/public/app/plugins/datasource/stackdriver/query_filter_ctrl.ts b/public/app/plugins/datasource/stackdriver/query_filter_ctrl.ts index 4c383e5d09e..0f5dce559fd 100644 --- a/public/app/plugins/datasource/stackdriver/query_filter_ctrl.ts +++ b/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; diff --git a/public/app/plugins/datasource/stackdriver/specs/query_aggregation_ctrl.test.ts b/public/app/plugins/datasource/stackdriver/specs/query_aggregation_ctrl.test.ts index ac9ea2ac6bc..81011f5dfe0 100644 --- a/public/app/plugins/datasource/stackdriver/specs/query_aggregation_ctrl.test.ts +++ b/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', () => { diff --git a/public/app/plugins/datasource/stackdriver/types.ts b/public/app/plugins/datasource/stackdriver/types.ts new file mode 100644 index 00000000000..df4c2886522 --- /dev/null +++ b/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 }>; +} diff --git a/public/app/types/plugins.ts b/public/app/types/plugins.ts index 817777669d8..9e3264be70c 100644 --- a/public/app/types/plugins.ts +++ b/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; +}
    - {{variable.query}} + {{variable.definition ? variable.definition : variable.query}}