mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Merge pull request #14007 from grafana/stackdriver-template-query-editor
Stackdriver template query editor
This commit is contained in:
commit
4aeea56342
@ -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
|
||||
|
||||
|
@ -14,3 +14,4 @@ exports[`PickerOption renders correctly 1`] = `
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
36
public/app/features/plugins/VariableQueryComponentLoader.tsx
Normal file
36
public/app/features/plugins/VariableQueryComponentLoader.tsx
Normal file
@ -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);
|
@ -4,3 +4,4 @@ import './import_list/import_list';
|
||||
import './ds_edit_ctrl';
|
||||
import './datasource_srv';
|
||||
import './plugin_component';
|
||||
import './VariableQueryComponentLoader';
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
@ -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;
|
||||
});
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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.
|
||||
<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.
|
||||
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>
|
@ -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 */
|
||||
|
@ -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 =
|
||||
|
@ -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 };
|
||||
}
|
||||
}
|
@ -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;
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
@ -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>,
|
||||
"",
|
||||
]
|
||||
`;
|
@ -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
public/app/plugins/datasource/stackdriver/functions.ts
Normal file
48
public/app/plugins/datasource/stackdriver/functions.ts
Normal file
@ -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,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,
|
||||
};
|
||||
|
@ -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>
|
||||
|
||||
|
@ -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>
|
@ -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>
|
||||
|
@ -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) {
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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
public/app/plugins/datasource/stackdriver/types.ts
Normal file
21
public/app/plugins/datasource/stackdriver/types.ts
Normal file
@ -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 }>;
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user