Commit 4aeea563 by Daniel Lee Committed by GitHub

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

Stackdriver template query editor
parents 55fefde2 7021e07a
......@@ -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>
`;
\ No newline at end of file
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';
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;
});
};
}
}
......
......@@ -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 */
......
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 =
......
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 };
}
}
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;
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();
});
});
});
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>
);
}
}
// 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>,
"",
]
`;
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 [];
......
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>
\ No newline at end of file
<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>
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', () => {
......
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;
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment