Stackdriver: Support meta labels (#21373)

* Rewrite angular segments for filter and group by in react

* wip: refactoring

* Update metric find queries

* Remove old maps used to create labels - use one map for all types instead

* Use value as label (again) for filters ang groupby

* Remove old filter

* Remove not used code

* Fixes after pr feedback

* Fix broken tests and add new metadata tests

* Add index file to make imports cleaner

* Cleanup. Remove old angular filter code

* Fix broken tests

* Use type switching instead of if statements

* Use globals for regex

* Updates after pr feedback

* Make sure it's possible to filter using the same key multiple times

* Replace metric select with segment component

* Pass template vars as props

* Refactor meta labels code

* Reorder template variables

* Fix broken tests

* Reset metric value when changing service

* Fix lint issue.

* Make tests independant of element order

* Include kubernetes.io in regex

* Add instruction in help section
This commit is contained in:
Erik Sundell 2020-01-17 12:25:47 +01:00 committed by GitHub
parent 72023d90bd
commit 260239d98b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
33 changed files with 792 additions and 1597 deletions

View File

@ -30,9 +30,15 @@ import (
)
var (
slog log.Logger
legendKeyFormat *regexp.Regexp
metricNameFormat *regexp.Regexp
slog log.Logger
)
var (
matchAllCap = regexp.MustCompile("(.)([A-Z][a-z]*)")
legendKeyFormat = regexp.MustCompile(`\{\{\s*(.+?)\s*\}\}`)
metricNameFormat = regexp.MustCompile(`([\w\d_]+)\.(googleapis\.com|io)/(.+)`)
wildcardRegexRe = regexp.MustCompile(`[-\/^$+?.()|[\]{}]`)
alignmentPeriodRe = regexp.MustCompile("[0-9]+")
)
const (
@ -62,8 +68,6 @@ func NewStackdriverExecutor(dsInfo *models.DataSource) (tsdb.TsdbQueryEndpoint,
func init() {
slog = log.New("tsdb.stackdriver")
tsdb.RegisterTsdbQueryEndpoint("stackdriver", NewStackdriverExecutor)
legendKeyFormat = regexp.MustCompile(`\{\{\s*(.+?)\s*\}\}`)
metricNameFormat = regexp.MustCompile(`([\w\d_]+)\.googleapis\.com/(.+)`)
}
// Query takes in the frontend queries, parses them into the Stackdriver query format
@ -197,8 +201,7 @@ func interpolateFilterWildcards(value string) string {
value = reverse(strings.Replace(reverse(value), "*", "", 1))
value = fmt.Sprintf(`starts_with("%s")`, value)
} else if matches != 0 {
re := regexp.MustCompile(`[-\/^$+?.()|[\]{}]`)
value = string(re.ReplaceAllFunc([]byte(value), func(in []byte) []byte {
value = string(wildcardRegexRe.ReplaceAllFunc([]byte(value), func(in []byte) []byte {
return []byte(strings.Replace(string(in), string(in), `\\`+string(in), 1))
}))
value = strings.Replace(value, "*", ".*", -1)
@ -287,8 +290,7 @@ func (e *StackdriverExecutor) executeQuery(ctx context.Context, query *Stackdriv
alignmentPeriod, ok := req.URL.Query()["aggregation.alignmentPeriod"]
if ok {
re := regexp.MustCompile("[0-9]+")
seconds, err := strconv.ParseInt(re.FindString(alignmentPeriod[0]), 10, 64)
seconds, err := strconv.ParseInt(alignmentPeriodRe.FindString(alignmentPeriod[0]), 10, 64)
if err == nil {
queryResult.Meta.Set("alignmentPeriod", seconds)
}
@ -333,8 +335,6 @@ func (e *StackdriverExecutor) unmarshalResponse(res *http.Response) (Stackdriver
return StackdriverResponse{}, err
}
// slog.Info("stackdriver", "response", string(body))
if res.StatusCode/100 != 2 {
slog.Error("Request failed", "status", res.Status, "body", string(body))
return StackdriverResponse{}, fmt.Errorf(string(body))
@ -351,42 +351,66 @@ func (e *StackdriverExecutor) unmarshalResponse(res *http.Response) (Stackdriver
}
func (e *StackdriverExecutor) parseResponse(queryRes *tsdb.QueryResult, data StackdriverResponse, query *StackdriverQuery) error {
metricLabels := make(map[string][]string)
resourceLabels := make(map[string][]string)
var resourceTypes []string
for _, series := range data.TimeSeries {
if !containsLabel(resourceTypes, series.Resource.Type) {
resourceTypes = append(resourceTypes, series.Resource.Type)
}
}
labels := make(map[string]map[string]bool)
for _, series := range data.TimeSeries {
points := make([]tsdb.TimePoint, 0)
seriesLabels := make(map[string]string)
defaultMetricName := series.Metric.Type
if len(resourceTypes) > 1 {
defaultMetricName += " " + series.Resource.Type
}
labels["resource.type"] = map[string]bool{series.Resource.Type: true}
for key, value := range series.Metric.Labels {
if !containsLabel(metricLabels[key], value) {
metricLabels[key] = append(metricLabels[key], value)
if _, ok := labels["metric.label."+key]; !ok {
labels["metric.label."+key] = map[string]bool{}
}
labels["metric.label."+key][value] = true
seriesLabels["metric.label."+key] = value
if len(query.GroupBys) == 0 || containsLabel(query.GroupBys, "metric.label."+key) {
defaultMetricName += " " + value
}
}
for key, value := range series.Resource.Labels {
if !containsLabel(resourceLabels[key], value) {
resourceLabels[key] = append(resourceLabels[key], value)
if _, ok := labels["resource.label."+key]; !ok {
labels["resource.label."+key] = map[string]bool{}
}
labels["resource.label."+key][value] = true
seriesLabels["resource.label."+key] = value
if containsLabel(query.GroupBys, "resource.label."+key) {
defaultMetricName += " " + value
}
}
for labelType, labelTypeValues := range series.MetaData {
for labelKey, labelValue := range labelTypeValues {
key := toSnakeCase(fmt.Sprintf("metadata.%s.%s", labelType, labelKey))
if _, ok := labels[key]; !ok {
labels[key] = map[string]bool{}
}
switch v := labelValue.(type) {
case string:
labels[key][v] = true
seriesLabels[key] = v
case bool:
strVal := strconv.FormatBool(v)
labels[key][strVal] = true
seriesLabels[key] = strVal
case []interface{}:
for _, v := range v {
strVal := v.(string)
labels[key][strVal] = true
if len(seriesLabels[key]) > 0 {
strVal = fmt.Sprintf("%s, %s", seriesLabels[key], strVal)
}
seriesLabels[key] = strVal
}
}
}
}
// reverse the order to be ascending
if series.ValueType != "DISTRIBUTION" {
for i := len(series.Points) - 1; i >= 0; i-- {
@ -411,7 +435,7 @@ func (e *StackdriverExecutor) parseResponse(queryRes *tsdb.QueryResult, data Sta
points = append(points, tsdb.NewTimePoint(null.FloatFrom(value), float64((point.Interval.EndTime).Unix())*1000))
}
metricName := formatLegendKeys(series.Metric.Type, defaultMetricName, series.Resource.Type, series.Metric.Labels, series.Resource.Labels, make(map[string]string), query)
metricName := formatLegendKeys(series.Metric.Type, defaultMetricName, seriesLabels, nil, query)
queryRes.Series = append(queryRes.Series, &tsdb.TimeSeries{
Name: metricName,
@ -437,7 +461,7 @@ func (e *StackdriverExecutor) parseResponse(queryRes *tsdb.QueryResult, data Sta
bucketBound := calcBucketBound(point.Value.DistributionValue.BucketOptions, i)
additionalLabels := map[string]string{"bucket": bucketBound}
buckets[i] = &tsdb.TimeSeries{
Name: formatLegendKeys(series.Metric.Type, defaultMetricName, series.Resource.Type, series.Metric.Labels, series.Resource.Labels, additionalLabels, query),
Name: formatLegendKeys(series.Metric.Type, defaultMetricName, nil, additionalLabels, query),
Points: make([]tsdb.TimePoint, 0),
}
if maxKey < i {
@ -453,7 +477,7 @@ func (e *StackdriverExecutor) parseResponse(queryRes *tsdb.QueryResult, data Sta
bucketBound := calcBucketBound(point.Value.DistributionValue.BucketOptions, i)
additionalLabels := map[string]string{"bucket": bucketBound}
buckets[i] = &tsdb.TimeSeries{
Name: formatLegendKeys(series.Metric.Type, defaultMetricName, series.Resource.Type, series.Metric.Labels, series.Resource.Labels, additionalLabels, query),
Name: formatLegendKeys(series.Metric.Type, defaultMetricName, seriesLabels, additionalLabels, query),
Points: make([]tsdb.TimePoint, 0),
}
}
@ -465,14 +489,23 @@ func (e *StackdriverExecutor) parseResponse(queryRes *tsdb.QueryResult, data Sta
}
}
queryRes.Meta.Set("resourceLabels", resourceLabels)
queryRes.Meta.Set("metricLabels", metricLabels)
labelsByKey := make(map[string][]string)
for key, values := range labels {
for value := range values {
labelsByKey[key] = append(labelsByKey[key], value)
}
}
queryRes.Meta.Set("labels", labelsByKey)
queryRes.Meta.Set("groupBys", query.GroupBys)
queryRes.Meta.Set("resourceTypes", resourceTypes)
return nil
}
func toSnakeCase(str string) string {
return strings.ToLower(matchAllCap.ReplaceAllString(str, "${1}_${2}"))
}
func containsLabel(labels []string, newLabel string) bool {
for _, val := range labels {
if val == newLabel {
@ -482,7 +515,7 @@ func containsLabel(labels []string, newLabel string) bool {
return false
}
func formatLegendKeys(metricType string, defaultMetricName string, resourceType string, metricLabels map[string]string, resourceLabels map[string]string, additionalLabels map[string]string, query *StackdriverQuery) string {
func formatLegendKeys(metricType string, defaultMetricName string, labels map[string]string, additionalLabels map[string]string, query *StackdriverQuery) string {
if query.AliasBy == "" {
return defaultMetricName
}
@ -496,25 +529,13 @@ func formatLegendKeys(metricType string, defaultMetricName string, resourceType
return []byte(metricType)
}
if metaPartName == "resource.type" && resourceType != "" {
return []byte(resourceType)
}
metricPart := replaceWithMetricPart(metaPartName, metricType)
if metricPart != nil {
return metricPart
}
metaPartName = strings.Replace(metaPartName, "metric.label.", "", 1)
if val, exists := metricLabels[metaPartName]; exists {
return []byte(val)
}
metaPartName = strings.Replace(metaPartName, "resource.label.", "", 1)
if val, exists := resourceLabels[metaPartName]; exists {
if val, exists := labels[metaPartName]; exists {
return []byte(val)
}
@ -533,8 +554,8 @@ func replaceWithMetricPart(metaPartName string, metricType string) []byte {
shortMatches := metricNameFormat.FindStringSubmatch(metricType)
if metaPartName == "metric.name" {
if len(shortMatches) > 0 {
return []byte(shortMatches[2])
if len(shortMatches) > 2 {
return []byte(shortMatches[3])
}
}

View File

@ -260,22 +260,20 @@ func TestStackdriver(t *testing.T) {
})
Convey("Should add meta for labels to the response", func() {
metricLabels := res.Meta.Get("metricLabels").Interface().(map[string][]string)
So(metricLabels, ShouldNotBeNil)
So(len(metricLabels["instance_name"]), ShouldEqual, 3)
So(metricLabels["instance_name"][0], ShouldEqual, "collector-asia-east-1")
So(metricLabels["instance_name"][1], ShouldEqual, "collector-europe-west-1")
So(metricLabels["instance_name"][2], ShouldEqual, "collector-us-east-1")
labels := res.Meta.Get("labels").Interface().(map[string][]string)
So(labels, ShouldNotBeNil)
So(len(labels["metric.label.instance_name"]), ShouldEqual, 3)
So(labels["metric.label.instance_name"], ShouldContain, "collector-asia-east-1")
So(labels["metric.label.instance_name"], ShouldContain, "collector-europe-west-1")
So(labels["metric.label.instance_name"], ShouldContain, "collector-us-east-1")
resourceLabels := res.Meta.Get("resourceLabels").Interface().(map[string][]string)
So(resourceLabels, ShouldNotBeNil)
So(len(resourceLabels["zone"]), ShouldEqual, 3)
So(resourceLabels["zone"][0], ShouldEqual, "asia-east1-a")
So(resourceLabels["zone"][1], ShouldEqual, "europe-west1-b")
So(resourceLabels["zone"][2], ShouldEqual, "us-east1-b")
So(len(labels["resource.label.zone"]), ShouldEqual, 3)
So(labels["resource.label.zone"], ShouldContain, "asia-east1-a")
So(labels["resource.label.zone"], ShouldContain, "europe-west1-b")
So(labels["resource.label.zone"], ShouldContain, "us-east1-b")
So(len(resourceLabels["project_id"]), ShouldEqual, 1)
So(resourceLabels["project_id"][0], ShouldEqual, "grafana-prod")
So(len(labels["resource.label.project_id"]), ShouldEqual, 1)
So(labels["resource.label.project_id"][0], ShouldEqual, "grafana-prod")
})
})
@ -419,6 +417,72 @@ func TestStackdriver(t *testing.T) {
So(res.Series[10].Points[1][0].Float64, ShouldEqual, 56)
})
})
Convey("when data from query returns metadata system labels", func() {
data, err := loadTestFile("./test-data/5-series-response-meta-data.json")
So(err, ShouldBeNil)
So(len(data.TimeSeries), ShouldEqual, 3)
res := &tsdb.QueryResult{Meta: simplejson.New(), RefId: "A"}
query := &StackdriverQuery{AliasBy: "{{bucket}}"}
err = executor.parseResponse(res, data, query)
labels := res.Meta.Get("labels").Interface().(map[string][]string)
So(err, ShouldBeNil)
So(len(res.Series), ShouldEqual, 3)
Convey("and systemlabel contains key with array of string", func() {
So(len(labels["metadata.system_labels.test"]), ShouldEqual, 5)
So(labels["metadata.system_labels.test"], ShouldContain, "value1")
So(labels["metadata.system_labels.test"], ShouldContain, "value2")
So(labels["metadata.system_labels.test"], ShouldContain, "value3")
So(labels["metadata.system_labels.test"], ShouldContain, "value4")
So(labels["metadata.system_labels.test"], ShouldContain, "value5")
})
Convey("and systemlabel contains key with primitive strings", func() {
So(len(labels["metadata.system_labels.region"]), ShouldEqual, 2)
So(labels["metadata.system_labels.region"], ShouldContain, "us-central1")
So(labels["metadata.system_labels.region"], ShouldContain, "us-west1")
})
Convey("and userLabel contains key with primitive strings", func() {
So(len(labels["metadata.user_labels.region"]), ShouldEqual, 2)
So(labels["metadata.user_labels.region"], ShouldContain, "region1")
So(labels["metadata.user_labels.region"], ShouldContain, "region3")
So(len(labels["metadata.user_labels.name"]), ShouldEqual, 2)
So(labels["metadata.user_labels.name"], ShouldContain, "name1")
So(labels["metadata.user_labels.name"], ShouldContain, "name3")
})
})
Convey("when data from query returns metadata system labels and alias by is defined", func() {
data, err := loadTestFile("./test-data/5-series-response-meta-data.json")
So(err, ShouldBeNil)
So(len(data.TimeSeries), ShouldEqual, 3)
Convey("and systemlabel contains key with array of string", func() {
res := &tsdb.QueryResult{Meta: simplejson.New(), RefId: "A"}
query := &StackdriverQuery{AliasBy: "{{metadata.system_labels.test}}"}
err = executor.parseResponse(res, data, query)
So(err, ShouldBeNil)
So(len(res.Series), ShouldEqual, 3)
fmt.Println(res.Series[0].Name)
So(res.Series[0].Name, ShouldEqual, "value1, value2")
So(res.Series[1].Name, ShouldEqual, "value1, value2, value3")
So(res.Series[2].Name, ShouldEqual, "value1, value2, value4, value5")
})
Convey("and systemlabel contains key with array of string2", func() {
res := &tsdb.QueryResult{Meta: simplejson.New(), RefId: "A"}
query := &StackdriverQuery{AliasBy: "{{metadata.system_labels.test2}}"}
err = executor.parseResponse(res, data, query)
So(err, ShouldBeNil)
So(len(res.Series), ShouldEqual, 3)
fmt.Println(res.Series[0].Name)
So(res.Series[2].Name, ShouldEqual, "testvalue")
})
})
})
Convey("when interpolating filter wildcards", func() {

View File

@ -0,0 +1,78 @@
{
"timeSeries": [{
"metric": {
"type": "compute.googleapis.com/firewall/dropped_bytes_count"
},
"resource": {
"type": "gce_instance",
"labels": {
"instance_id": "114250375703598695",
"project_id": "raintank-dev"
}
},
"metricKind": "DELTA",
"valueType": "INT64",
"metadata": {
"systemLabels": {
"region": "us-west1",
"name": "diana-debian9",
"test": ["value1", "value2"],
"spot_instance": false
},
"userLabels": {
"name": "name1",
"region": "region1"
}
}
},
{
"metric": {
"type": "compute.googleapis.com/firewall/dropped_bytes_count"
},
"resource": {
"type": "gce_instance",
"labels": {
"instance_id": "2268399396335228490",
"project_id": "raintank-dev"
}
},
"metricKind": "DELTA",
"valueType": "INT64",
"metadata": {
"systemLabels": {
"region": "us-west1",
"name": "diana-ubuntu1910",
"test": ["value1", "value2","value3"],
"spot_instance": false
}
}
},
{
"metric": {
"type": "compute.googleapis.com/firewall/dropped_bytes_count"
},
"resource": {
"type": "gce_instance",
"labels": {
"instance_id": "2390486324245305300",
"project_id": "raintank-dev"
}
},
"metricKind": "DELTA",
"valueType": "INT64",
"metadata": {
"systemLabels": {
"name": "premium-plugin-staging",
"region": "us-central1",
"test": ["value1", "value2","value4", "value5"],
"test2": ["testvalue"],
"spot_instance": true
},
"userLabels": {
"name": "name3",
"region": "region3"
}
}
}
]
}

View File

@ -41,8 +41,9 @@ type StackdriverResponse struct {
Type string `json:"type"`
Labels map[string]string `json:"labels"`
} `json:"resource"`
MetricKind string `json:"metricKind"`
ValueType string `json:"valueType"`
MetaData map[string]map[string]interface{} `json:"metadata"`
MetricKind string `json:"metricKind"`
ValueType string `json:"valueType"`
Points []struct {
Interval struct {
StartTime time.Time `json:"startTime"`

View File

@ -90,7 +90,6 @@ export function registerAngularDirectives() {
react2AngularDirective('stackdriverAnnotationQueryEditor', StackdriverAnnotationQueryEditor, [
'target',
'onQueryChange',
'onExecuteQuery',
['datasource', { watchDepth: 'reference' }],
['templateSrv', { watchDepth: 'reference' }],
]);

View File

@ -77,16 +77,9 @@ export default class StackdriverMetricFindQuery {
return [];
}
const refId = 'handleLabelValuesQuery';
const response = await this.datasource.getLabels(selectedMetricType, refId);
const labels = await this.datasource.getLabels(selectedMetricType, refId, [labelKey]);
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];
}
const values = labels.hasOwnProperty(interpolatedKey) ? labels[interpolatedKey] : [];
return values.map(this.toFindQueryResult);
}
@ -95,8 +88,8 @@ export default class StackdriverMetricFindQuery {
return [];
}
const refId = 'handleResourceTypeQueryQueryType';
const response = await this.datasource.getLabels(selectedMetricType, refId);
return response.meta.resourceTypes ? response.meta.resourceTypes.map(this.toFindQueryResult) : [];
const labels = await this.datasource.getLabels(selectedMetricType, refId);
return labels['resource.type'].map(this.toFindQueryResult);
}
async handleAlignersQuery({ selectedMetricType }: any) {

View File

@ -6,8 +6,7 @@ export class StackdriverAnnotationsQueryCtrl {
templateSrv: TemplateSrv;
/** @ngInject */
constructor(templateSrv: TemplateSrv) {
this.templateSrv = templateSrv;
constructor() {
this.annotation.target = this.annotation.target || {};
this.onQueryChange = this.onQueryChange.bind(this);
}

View File

@ -16,6 +16,7 @@ const props: Props = {
crossSeriesReducer: '',
groupBys: [],
children: renderProps => <div />,
templateVariableOptions: [],
};
describe('Aggregations', () => {
@ -32,7 +33,7 @@ describe('Aggregations', () => {
wrapper = shallow(<Aggregations {...newProps} />);
});
it('', () => {
const options = wrapper.state().aggOptions[0].options;
const options = wrapper.state().aggOptions;
expect(options.length).toEqual(11);
expect(options.map((o: any) => o.value)).toEqual(
expect.not.arrayContaining(['REDUCE_COUNT_TRUE', 'REDUCE_COUNT_FALSE'])
@ -49,8 +50,7 @@ describe('Aggregations', () => {
wrapper = shallow(<Aggregations {...newProps} />);
});
it('', () => {
const options = wrapper.state().aggOptions[0].options;
const options = wrapper.state().aggOptions;
expect(options.length).toEqual(10);
expect(options.map((o: any) => o.value)).toEqual(expect.arrayContaining(['REDUCE_NONE']));
});

View File

@ -1,14 +1,13 @@
import React from 'react';
import _ from 'lodash';
import { MetricSelect } from 'app/core/components/Select/MetricSelect';
import { SelectableValue } from '@grafana/data';
import { Segment } from '@grafana/ui';
import { getAggregationOptionsByMetric } from '../functions';
import { TemplateSrv } from 'app/features/templating/template_srv';
import { ValueTypes, MetricKind } from '../constants';
export interface Props {
onChange: (metricDescriptor: any) => void;
templateSrv: TemplateSrv;
metricDescriptor: {
valueType: string;
metricKind: string;
@ -16,6 +15,7 @@ export interface Props {
crossSeriesReducer: string;
groupBys: string[];
children?: (renderProps: any) => JSX.Element;
templateVariableOptions: Array<SelectableValue<string>>;
}
export interface State {
@ -40,19 +40,13 @@ export class Aggregations extends React.Component<Props, State> {
setAggOptions({ metricDescriptor }: Props) {
let aggOptions: any[] = [];
if (metricDescriptor) {
aggOptions = [
{
label: 'Aggregations',
expanded: true,
options: getAggregationOptionsByMetric(
metricDescriptor.valueType as ValueTypes,
metricDescriptor.metricKind as MetricKind
).map(a => ({
...a,
label: a.text,
})),
},
];
aggOptions = getAggregationOptionsByMetric(
metricDescriptor.valueType as ValueTypes,
metricDescriptor.metricKind as MetricKind
).map(a => ({
...a,
label: a.text,
}));
}
this.setState({ aggOptions });
}
@ -65,22 +59,28 @@ export class Aggregations extends React.Component<Props, State> {
render() {
const { displayAdvancedOptions, aggOptions } = this.state;
const { templateSrv, onChange, crossSeriesReducer } = this.props;
const { templateVariableOptions, onChange, crossSeriesReducer } = this.props;
return (
<>
<div className="gf-form-inline">
<div className="gf-form">
<label className="gf-form-label query-keyword width-9">Aggregation</label>
<MetricSelect
onChange={onChange}
value={crossSeriesReducer}
variables={templateSrv.variables}
options={aggOptions}
placeholder="Select Reducer"
className="width-15"
/>
</div>
<label className="gf-form-label query-keyword width-9">Aggregation</label>
<Segment
onChange={({ value }) => onChange(value)}
value={[...aggOptions, ...templateVariableOptions].find(s => s.value === crossSeriesReducer)}
options={[
{
label: 'Template Variables',
options: templateVariableOptions,
},
{
label: 'Aggregations',
expanded: true,
options: aggOptions,
},
]}
placeholder="Select Reducer"
></Segment>
<div className="gf-form gf-form--grow">
<label className="gf-form-label gf-form-label--grow">
<a onClick={this.onToggleDisplayAdvanced}>

View File

@ -1,14 +1,16 @@
import React, { FC } from 'react';
import _ from 'lodash';
import kbn from 'app/core/utils/kbn';
import { MetricSelect } from 'app/core/components/Select/MetricSelect';
import { alignmentPeriods, alignOptions } from '../constants';
import { TemplateSrv } from 'app/features/templating/template_srv';
import kbn from 'app/core/utils/kbn';
import { SelectableValue } from '@grafana/data';
import { Segment } from '@grafana/ui';
import { alignmentPeriods, alignOptions } from '../constants';
export interface Props {
onChange: (alignmentPeriod: any) => void;
templateSrv: TemplateSrv;
templateVariableOptions: Array<SelectableValue<string>>;
alignmentPeriod: string;
perSeriesAligner: string;
usedAlignmentPeriod: string;
@ -17,36 +19,38 @@ export interface Props {
export const AlignmentPeriods: FC<Props> = ({
alignmentPeriod,
templateSrv,
templateVariableOptions,
onChange,
perSeriesAligner,
usedAlignmentPeriod,
}) => {
const alignment = alignOptions.find(ap => ap.value === templateSrv.replace(perSeriesAligner));
const formatAlignmentText = `${kbn.secondsToHms(usedAlignmentPeriod)} interval (${alignment ? alignment.text : ''})`;
const options = alignmentPeriods.map(ap => ({
...ap,
label: ap.text,
}));
return (
<>
<div className="gf-form-inline">
<div className="gf-form">
<label className="gf-form-label query-keyword width-9">Alignment Period</label>
<MetricSelect
onChange={onChange}
value={alignmentPeriod}
variables={templateSrv.variables}
options={[
{
label: 'Alignment options',
expanded: true,
options: alignmentPeriods.map(ap => ({
...ap,
label: ap.text,
})),
},
]}
placeholder="Select Alignment"
className="width-15"
/>
</div>
<label className="gf-form-label query-keyword width-9">Alignment Period</label>
<Segment
onChange={({ value }) => onChange(value)}
value={[...options, ...templateVariableOptions].find(s => s.value === alignmentPeriod)}
options={[
{
label: 'Template Variables',
options: templateVariableOptions,
},
{
label: 'Aggregations',
expanded: true,
options: options,
},
]}
placeholder="Select Alignment"
></Segment>
<div className="gf-form gf-form--grow">
{usedAlignmentPeriod && <label className="gf-form-label gf-form-label--grow">{formatAlignmentText}</label>}
</div>

View File

@ -1,30 +1,37 @@
import React, { FC } from 'react';
import { MetricSelect } from 'app/core/components/Select/MetricSelect';
import { TemplateSrv } from 'app/features/templating/template_srv';
import { SelectableValue } from '@grafana/data';
import { Segment } from '@grafana/ui';
export interface Props {
onChange: (perSeriesAligner: any) => void;
templateSrv: TemplateSrv;
templateVariableOptions: Array<SelectableValue<string>>;
alignOptions: Array<SelectableValue<string>>;
perSeriesAligner: string;
}
export const Alignments: FC<Props> = ({ perSeriesAligner, templateSrv, onChange, alignOptions }) => {
export const Alignments: FC<Props> = ({ perSeriesAligner, templateVariableOptions, onChange, alignOptions }) => {
return (
<>
<div className="gf-form-group">
<div className="gf-form-inline">
<div className="gf-form offset-width-9">
<label className="gf-form-label query-keyword width-15">Aligner</label>
<MetricSelect
onChange={onChange}
value={perSeriesAligner}
variables={templateSrv.variables}
options={alignOptions}
<Segment
onChange={({ value }) => onChange(value)}
value={[...alignOptions, ...templateVariableOptions].find(s => s.value === perSeriesAligner)}
options={[
{
label: 'Template Variables',
options: templateVariableOptions,
},
{
label: 'Alignment options',
expanded: true,
options: alignOptions,
},
]}
placeholder="Select Alignment"
className="width-15"
/>
></Segment>
</div>
</div>
</>

View File

@ -2,12 +2,12 @@ import React from 'react';
import { Input } from '@grafana/ui';
import { TemplateSrv } from 'app/features/templating/template_srv';
import { SelectableValue } from '@grafana/data';
import StackdriverDatasource from '../datasource';
import { Metrics } from './Metrics';
import { Filter } from './Filter';
import { Metrics, Filters, AnnotationsHelp } from './';
import { toOption } from '../functions';
import { AnnotationTarget, MetricDescriptor } from '../types';
import { AnnotationsHelp } from './AnnotationsHelp';
export interface Props {
onQueryChange: (target: AnnotationTarget) => void;
@ -17,6 +17,9 @@ export interface Props {
}
interface State extends AnnotationTarget {
variableOptionGroup: SelectableValue<string>;
variableOptions: Array<SelectableValue<string>>;
labels: any;
[key: string]: any;
}
@ -29,19 +32,32 @@ const DefaultTarget: State = {
refId: 'annotationQuery',
title: '',
text: '',
labels: {},
variableOptionGroup: {},
variableOptions: [],
};
export class AnnotationQueryEditor extends React.Component<Props, State> {
state: State = DefaultTarget;
componentDidMount() {
const { target, datasource } = this.props;
const variableOptionGroup = {
label: 'Template Variables',
options: datasource.variables.map(toOption),
};
this.setState({
...this.props.target,
variableOptionGroup,
variableOptions: variableOptionGroup.options,
...target,
});
datasource.getLabels(target.metricType, target.refId).then(labels => this.setState({ labels }));
}
onMetricTypeChange = ({ valueType, metricKind, type, unit }: MetricDescriptor) => {
const { onQueryChange } = this.props;
const { onQueryChange, datasource } = this.props;
this.setState(
{
metricType: type,
@ -53,6 +69,7 @@ export class AnnotationQueryEditor extends React.Component<Props, State> {
onQueryChange(this.state);
}
);
datasource.getLabels(type, this.state.refId).then(labels => this.setState({ labels }));
};
onChange(prop: string, value: string | string[]) {
@ -62,28 +79,35 @@ export class AnnotationQueryEditor extends React.Component<Props, State> {
}
render() {
const { defaultProject, metricType, filters, refId, title, text } = this.state;
const { datasource, templateSrv } = this.props;
const {
defaultProject,
metricType,
filters,
title,
text,
variableOptionGroup,
labels,
variableOptions,
} = this.state;
const { datasource } = this.props;
return (
<>
<Metrics
defaultProject={defaultProject}
metricType={metricType}
templateSrv={templateSrv}
templateSrv={datasource.templateSrv}
datasource={datasource}
onChange={this.onMetricTypeChange}
templateVariableOptions={variableOptions}
onChange={metric => this.onMetricTypeChange(metric)}
>
{metric => (
<>
<Filter
filtersChanged={value => this.onChange('filters', value)}
<Filters
labels={labels}
filters={filters}
refId={refId}
hideGroupBys={true}
templateSrv={templateSrv}
datasource={datasource}
metricType={metric ? metric.type : ''}
onChange={value => this.onChange('filters', value)}
variableOptionGroup={variableOptionGroup}
/>
</>
)}

View File

@ -1,116 +0,0 @@
import React from 'react';
import _ from 'lodash';
import appEvents from 'app/core/app_events';
import { QueryMeta } from '../types';
import { getAngularLoader, AngularComponent } from '@grafana/runtime';
import { TemplateSrv } from 'app/features/templating/template_srv';
import StackdriverDatasource from '../datasource';
import '../query_filter_ctrl';
import { AppEvents } from '@grafana/data';
export interface Props {
filtersChanged: (filters: string[]) => void;
groupBysChanged?: (groupBys: string[]) => void;
metricType: string;
templateSrv: TemplateSrv;
groupBys?: string[];
filters: string[];
datasource: StackdriverDatasource;
refId: string;
hideGroupBys: boolean;
}
interface State {
labelData: QueryMeta;
loading: Promise<any>;
}
const labelData: any = {
metricLabels: {},
resourceLabels: {},
resourceTypes: [],
};
export class Filter extends React.Component<Props, State> {
element: any;
component: AngularComponent;
async componentDidMount() {
if (!this.element) {
return;
}
const { groupBys, filters, hideGroupBys } = this.props;
const loader = getAngularLoader();
const filtersChanged = (filters: string[]) => {
this.props.filtersChanged(filters);
};
const groupBysChanged = (groupBys: string[]) => {
this.props.groupBysChanged(groupBys);
};
const scopeProps: any = {
loading: null,
labelData,
groupBys,
filters,
filtersChanged,
groupBysChanged,
hideGroupBys,
};
const loading = this.loadLabels(scopeProps);
scopeProps.loading = loading;
const template = `<stackdriver-filter
filters="filters"
group-bys="groupBys"
label-data="labelData"
loading="loading"
filters-changed="filtersChanged(filters)"
group-bys-changed="groupBysChanged(groupBys)"
hide-group-bys="hideGroupBys"/>`;
this.component = loader.load(this.element, scopeProps, template);
}
componentDidUpdate(prevProps: Props) {
if (!this.element) {
return;
}
const scope = this.component.getScope();
if (prevProps.metricType !== this.props.metricType) {
scope.loading = this.loadLabels(scope);
}
scope.filters = this.props.filters;
scope.groupBys = this.props.groupBys;
}
componentWillUnmount() {
if (this.component) {
this.component.destroy();
}
}
async loadLabels(scope: any) {
return new Promise(async resolve => {
try {
if (!this.props.metricType) {
scope.labelData = labelData;
} else {
const { meta } = await this.props.datasource.getLabels(this.props.metricType, this.props.refId);
scope.labelData = meta;
}
resolve();
} catch (error) {
appEvents.emit(AppEvents.alertError, ['Error', 'Error loading metric labels for ' + this.props.metricType]);
scope.labelData = labelData;
resolve();
}
});
}
render() {
return <div ref={element => (this.element = element)} style={{ width: '100%' }} />;
}
}

View File

@ -0,0 +1,103 @@
import React, { FunctionComponent, Fragment } from 'react';
import _ from 'lodash';
import { SelectableValue } from '@grafana/data';
import { Segment } from '@grafana/ui';
import { labelsToGroupedOptions, toOption } from '../functions';
import { Filter } from '../types';
export interface Props {
labels: { [key: string]: string[] };
filters: string[];
onChange: (filters: string[]) => void;
variableOptionGroup: SelectableValue<string>;
}
const removeText = '-- remove filter --';
const removeOption: SelectableValue<string> = { label: removeText, value: removeText, icon: 'fa fa-remove' };
const operators = ['=', '!=', '=~', '!=~'];
const filtersToStringArray = (filters: Filter[]) =>
_.flatten(filters.map(({ key, operator, value, condition }) => [key, operator, value, condition]));
const stringArrayToFilters = (filterArray: string[]) =>
_.chunk(filterArray, 4).map(([key, operator, value, condition = 'AND']) => ({
key,
operator,
value,
condition,
}));
export const Filters: FunctionComponent<Props> = ({
labels = {},
filters: filterArray,
onChange,
variableOptionGroup,
}) => {
const filters = stringArrayToFilters(filterArray);
const options = [removeOption, variableOptionGroup, ...labelsToGroupedOptions(Object.keys(labels))];
return (
<div className="gf-form-inline">
<label className="gf-form-label query-keyword width-9">Filter</label>
{filters.map(({ key, operator, value, condition }, index) => (
<Fragment key={index}>
<Segment
allowCustomValue
value={key}
options={options}
onChange={({ value: key }) => {
if (key === removeText) {
onChange(filtersToStringArray(filters.filter((_, i) => i !== index)));
} else {
onChange(
filtersToStringArray(
filters.map((f, i) => (i === index ? { key, operator, condition, value: '' } : f))
)
);
}
}}
/>
<Segment
value={operator}
className="gf-form-label query-segment-operator"
options={operators.map(toOption)}
onChange={({ value: operator }) =>
onChange(filtersToStringArray(filters.map((f, i) => (i === index ? { ...f, operator } : f))))
}
/>
<Segment
allowCustomValue
value={value}
placeholder="add filter value"
options={
labels.hasOwnProperty(key) ? [variableOptionGroup, ...labels[key].map(toOption)] : [variableOptionGroup]
}
onChange={({ value }) =>
onChange(filtersToStringArray(filters.map((f, i) => (i === index ? { ...f, value } : f))))
}
/>
{filters.length > 1 && index + 1 !== filters.length && (
<label className="gf-form-label query-keyword">{condition}</label>
)}
</Fragment>
))}
{Object.values(filters).every(({ value }) => value) && (
<Segment
allowCustomValue
Component={
<a className="gf-form-label query-part">
<i className="fa fa-plus" />
</a>
}
options={[variableOptionGroup, ...labelsToGroupedOptions(Object.keys(labels))]}
onChange={({ value: key }) =>
onChange(filtersToStringArray([...filters, { key, operator: '=', condition: 'AND', value: '' } as Filter]))
}
/>
)}
<div className="gf-form gf-form--grow">
<label className="gf-form-label gf-form-label--grow"></label>
</div>
</div>
);
};

View File

@ -0,0 +1,58 @@
import React, { FunctionComponent } from 'react';
import { SelectableValue } from '@grafana/data';
import { Segment } from '@grafana/ui';
import { labelsToGroupedOptions } from '../functions';
import { systemLabels } from '../constants';
export interface Props {
values: string[];
onChange: (values: string[]) => void;
variableOptionGroup: SelectableValue<string>;
groupBys: string[];
}
const removeText = '-- remove group by --';
const removeOption: SelectableValue<string> = { label: removeText, value: removeText };
export const GroupBys: FunctionComponent<Props> = ({ groupBys = [], values = [], onChange, variableOptionGroup }) => {
const options = [removeOption, variableOptionGroup, ...labelsToGroupedOptions([...groupBys, ...systemLabels])];
return (
<div className="gf-form-inline">
<label className="gf-form-label query-keyword width-9">Group By</label>
{values &&
values.map((value, index) => (
<Segment
allowCustomValue
key={value + index}
value={value}
options={options}
onChange={({ value }) =>
onChange(
value === removeText
? values.filter((_, i) => i !== index)
: values.map((v, i) => (i === index ? value : v))
)
}
/>
))}
{values.length !== groupBys.length && (
<Segment
Component={
<a className="gf-form-label query-part">
<i className="fa fa-plus" />
</a>
}
allowCustomValue
onChange={({ value }) => onChange([...values, value])}
options={[
variableOptionGroup,
...labelsToGroupedOptions([...groupBys.filter(groupBy => !values.includes(groupBy)), ...systemLabels]),
]}
/>
)}
<div className="gf-form gf-form--grow">
<label className="gf-form-label gf-form-label--grow"></label>
</div>
</div>
);
};

View File

@ -97,6 +97,14 @@ export class Help extends React.Component<Props, State> {
<li>
<code>{`${'{{resource.label.label_name}}'}`}</code> = Resource label metadata e.g. resource.label.zone
</li>
<li>
<code>{`${'{{metadata.system_labels.name}}'}`}</code> = Meta data system labels e.g.
metadata.system_labels.name. For this to work, the needs to be included in the group by
</li>
<li>
<code>{`${'{{metadata.user_labels.name}}'}`}</code> = Meta data user labels e.g.
metadata.user_labels.name. For this to work, the needs to be included in the group by
</li>
<li>
<code>{`${'{{bucket}}'}`}</code> = bucket boundary for distribution metrics when using a heatmap in
Grafana

View File

@ -1,16 +1,18 @@
import React from 'react';
import _ from 'lodash';
import { TemplateSrv } from 'app/features/templating/template_srv';
import { SelectableValue } from '@grafana/data';
import StackdriverDatasource from '../datasource';
import appEvents from 'app/core/app_events';
import { Segment } from '@grafana/ui';
import { MetricDescriptor } from '../types';
import { MetricSelect } from 'app/core/components/Select/MetricSelect';
import { TemplateSrv } from 'app/features/templating/template_srv';
import { CoreEvents } from 'app/types';
export interface Props {
onChange: (metricDescriptor: MetricDescriptor) => void;
templateSrv: TemplateSrv;
templateVariableOptions: Array<SelectableValue<string>>;
datasource: StackdriverDatasource;
defaultProject: string;
metricType: string;
@ -104,9 +106,9 @@ export class Metrics extends React.Component<Props, State> {
return metricsByService;
}
onServiceChange = (service: any) => {
onServiceChange = ({ value: service }: any) => {
const { metricDescriptors } = this.state;
const { templateSrv, metricType } = this.props;
const { metricType, templateSrv } = this.props;
const metrics = metricDescriptors
.filter(m => m.service === templateSrv.replace(service))
@ -120,11 +122,11 @@ export class Metrics extends React.Component<Props, State> {
this.setState({ service, metrics });
if (metrics.length > 0 && !metrics.some(m => m.value === templateSrv.replace(metricType))) {
this.onMetricTypeChange(metrics[0].value);
this.onMetricTypeChange(metrics[0]);
}
};
onMetricTypeChange = (value: any) => {
onMetricTypeChange = ({ value }: any) => {
const metricDescriptor = this.getSelectedMetricDescriptor(value);
this.setState({ metricDescriptor });
this.props.onChange({ ...metricDescriptor, type: value });
@ -139,55 +141,46 @@ export class Metrics extends React.Component<Props, State> {
return services.length > 0 ? _.uniqBy(services, s => s.value) : [];
}
getTemplateVariablesGroup() {
return {
label: 'Template Variables',
options: this.props.templateSrv.variables.map(v => ({
label: `$${v.name}`,
value: `$${v.name}`,
})),
};
}
render() {
const { services, service, metrics } = this.state;
const { metricType, templateSrv } = this.props;
const { metricType, templateVariableOptions } = this.props;
return (
<>
<div className="gf-form-inline">
<div className="gf-form">
<span className="gf-form-label width-9 query-keyword">Service</span>
<MetricSelect
onChange={this.onServiceChange}
value={service}
options={services}
placeholder="Select Services"
className="width-15"
/>
</div>
<span className="gf-form-label width-9 query-keyword">Service</span>
<Segment
onChange={this.onServiceChange}
value={[...services, ...templateVariableOptions].find(s => s.value === service)}
options={[
{
label: 'Template Variables',
options: templateVariableOptions,
},
...services,
]}
placeholder="Select Services"
></Segment>
<div className="gf-form gf-form--grow">
<div className="gf-form-label gf-form-label--grow" />
</div>
</div>
<div className="gf-form-inline">
<div className="gf-form">
<span className="gf-form-label width-9 query-keyword">Metric</span>
<MetricSelect
onChange={this.onMetricTypeChange}
value={metricType}
variables={templateSrv.variables}
options={[
{
label: 'Metrics',
expanded: true,
options: metrics,
},
]}
placeholder="Select Metric"
className="width-26"
/>
</div>
<span className="gf-form-label width-9 query-keyword">Metric</span>
<Segment
className="query-part"
onChange={this.onMetricTypeChange}
value={[...metrics, ...templateVariableOptions].find(s => s.value === metricType)}
options={[
{
label: 'Template Variables',
options: templateVariableOptions,
},
...metrics,
]}
placeholder="Select Metric"
></Segment>
<div className="gf-form gf-form--grow">
<div className="gf-form-label gf-form-label--grow" />
</div>

View File

@ -11,6 +11,8 @@ const props: Props = {
datasource: {
getDefaultProject: () => Promise.resolve('project'),
getMetricTypes: () => Promise.resolve([]),
getLabels: () => Promise.resolve([]),
variables: [],
} as any,
templateSrv: new TemplateSrv(),
};

View File

@ -3,15 +3,9 @@ import _ from 'lodash';
import { TemplateSrv } from 'app/features/templating/template_srv';
import { Metrics } from './Metrics';
import { Filter } from './Filter';
import { Aggregations } from './Aggregations';
import { Alignments } from './Alignments';
import { AlignmentPeriods } from './AlignmentPeriods';
import { AliasBy } from './AliasBy';
import { Help } from './Help';
import { Aggregations, Metrics, Filters, GroupBys, Alignments, AlignmentPeriods, AliasBy, Help } from './';
import { StackdriverQuery, MetricDescriptor } from '../types';
import { getAlignmentPickerData } from '../functions';
import { getAlignmentPickerData, toOption } from '../functions';
import StackdriverDatasource from '../datasource';
import { TimeSeries, SelectableValue } from '@grafana/data';
import { PanelEvents } from '@grafana/data';
@ -26,9 +20,12 @@ export interface Props {
}
interface State extends StackdriverQuery {
variableOptions: Array<SelectableValue<string>>;
variableOptionGroup: SelectableValue<string>;
alignOptions: Array<SelectableValue<string>>;
lastQuery: string;
lastQueryError: string;
labels: any;
[key: string]: any;
}
@ -45,26 +42,39 @@ export const DefaultTarget: State = {
perSeriesAligner: 'ALIGN_MEAN',
groupBys: [],
filters: [],
filter: [],
aliasBy: '',
alignOptions: [],
lastQuery: '',
lastQueryError: '',
usedAlignmentPeriod: '',
labels: {},
variableOptionGroup: {},
variableOptions: [],
};
export class QueryEditor extends React.Component<Props, State> {
state: State = DefaultTarget;
componentDidMount() {
const { events, target, templateSrv } = this.props;
async componentDidMount() {
const { events, target, templateSrv, datasource } = this.props;
events.on(PanelEvents.dataReceived, this.onDataReceived.bind(this));
events.on(PanelEvents.dataError, this.onDataError.bind(this));
const { perSeriesAligner, alignOptions } = getAlignmentPickerData(target, templateSrv);
const variableOptionGroup = {
label: 'Template Variables',
expanded: false,
options: datasource.variables.map(toOption),
};
this.setState({
...this.props.target,
alignOptions,
perSeriesAligner,
variableOptionGroup,
variableOptions: variableOptionGroup.options,
});
datasource.getLabels(target.metricType, target.refId, target.groupBys).then(labels => this.setState({ labels }));
}
componentWillUnmount() {
@ -102,12 +112,13 @@ export class QueryEditor extends React.Component<Props, State> {
this.setState({ lastQuery, lastQueryError });
}
onMetricTypeChange = ({ valueType, metricKind, type, unit }: MetricDescriptor) => {
const { templateSrv, onQueryChange, onExecuteQuery } = this.props;
onMetricTypeChange = async ({ valueType, metricKind, type, unit }: MetricDescriptor) => {
const { templateSrv, onQueryChange, onExecuteQuery, target } = this.props;
const { perSeriesAligner, alignOptions } = getAlignmentPickerData(
{ valueType, metricKind, perSeriesAligner: this.state.perSeriesAligner },
templateSrv
);
const labels = await this.props.datasource.getLabels(type, target.refId, target.groupBys);
this.setState(
{
alignOptions,
@ -116,6 +127,7 @@ export class QueryEditor extends React.Component<Props, State> {
unit,
valueType,
metricKind,
labels,
},
() => {
onQueryChange(this.state);
@ -124,6 +136,15 @@ export class QueryEditor extends React.Component<Props, State> {
);
};
onGroupBysChange(value: string[]) {
const { target, datasource } = this.props;
this.setState({ groupBys: value }, () => {
this.props.onQueryChange(this.state);
this.props.onExecuteQuery();
});
datasource.getLabels(target.metricType, target.refId, value).then(labels => this.setState({ labels }));
}
onPropertyChange(prop: string, value: string[]) {
this.setState({ [prop]: value }, () => {
this.props.onQueryChange(this.state);
@ -145,35 +166,39 @@ export class QueryEditor extends React.Component<Props, State> {
aliasBy,
lastQuery,
lastQueryError,
refId,
labels,
variableOptionGroup,
variableOptions,
} = this.state;
const { datasource, templateSrv } = this.props;
return (
<>
<Metrics
templateSrv={templateSrv}
defaultProject={defaultProject}
metricType={metricType}
templateSrv={templateSrv}
templateVariableOptions={variableOptions}
datasource={datasource}
onChange={this.onMetricTypeChange}
>
{metric => (
<>
<Filter
filtersChanged={value => this.onPropertyChange('filters', value)}
groupBysChanged={value => this.onPropertyChange('groupBys', value)}
<Filters
labels={labels}
filters={filters}
groupBys={groupBys}
refId={refId}
hideGroupBys={false}
templateSrv={templateSrv}
datasource={datasource}
metricType={metric ? metric.type : ''}
onChange={value => this.onPropertyChange('filters', value)}
variableOptionGroup={variableOptionGroup}
/>
<GroupBys
groupBys={Object.keys(labels)}
values={groupBys}
onChange={this.onGroupBysChange.bind(this)}
variableOptionGroup={variableOptionGroup}
/>
<Aggregations
metricDescriptor={metric}
templateSrv={templateSrv}
templateVariableOptions={variableOptions}
crossSeriesReducer={crossSeriesReducer}
groupBys={groupBys}
onChange={value => this.onPropertyChange('crossSeriesReducer', value)}
@ -182,7 +207,7 @@ export class QueryEditor extends React.Component<Props, State> {
displayAdvancedOptions && (
<Alignments
alignOptions={alignOptions}
templateSrv={templateSrv}
templateVariableOptions={variableOptions}
perSeriesAligner={perSeriesAligner}
onChange={value => this.onPropertyChange('perSeriesAligner', value)}
/>
@ -191,6 +216,7 @@ export class QueryEditor extends React.Component<Props, State> {
</Aggregations>
<AlignmentPeriods
templateSrv={templateSrv}
templateVariableOptions={variableOptions}
alignmentPeriod={alignmentPeriod}
perSeriesAligner={perSeriesAligner}
usedAlignmentPeriod={usedAlignmentPeriod}

View File

@ -7,7 +7,7 @@ interface Props {
label: string;
}
const SimpleSelect: FC<Props> = props => {
export const SimpleSelect: FC<Props> = props => {
const { label, onValueChange, value, options } = props;
return (
<div className="gf-form max-width-21">
@ -24,5 +24,3 @@ const SimpleSelect: FC<Props> = props => {
</div>
);
};
export default SimpleSelect;

View File

@ -1,6 +1,6 @@
import React, { ChangeEvent, PureComponent } from 'react';
import { VariableQueryProps } from 'app/types/plugins';
import SimpleSelect from './SimpleSelect';
import { SimpleSelect } from './';
import { getMetricTypes, getLabelKeys, extractServicesFromMetricDescriptors } from '../functions';
import { MetricFindQueryTypes, VariableQueryData } from '../types';

View File

@ -5,99 +5,20 @@ Array [
<div
className="gf-form-inline"
>
<label
className="gf-form-label query-keyword width-9"
>
Aggregation
</label>
<div
className="gf-form"
onClick={[Function]}
>
<label
className="gf-form-label query-keyword width-9"
<a
className="gf-form-label query-part query-placeholder"
>
Aggregation
</label>
<div>
<div
className="css-0 gf-form-input gf-form-input--form-dropdown width-15"
onKeyDown={[Function]}
>
<div
className="css-0 gf-form-select-box__control"
onMouseDown={[Function]}
onTouchEnd={[Function]}
>
<div
className="css-0 gf-form-select-box__value-container"
>
<div
className="css-0 gf-form-select-box__placeholder"
>
Select Reducer
</div>
<div
className="css-0"
>
<div
className="gf-form-select-box__input"
style={
Object {
"display": "inline-block",
}
}
>
<input
aria-autocomplete="list"
autoCapitalize="none"
autoComplete="off"
autoCorrect="off"
disabled={false}
id="react-select-2-input"
onBlur={[Function]}
onChange={[Function]}
onFocus={[Function]}
spellCheck="false"
style={
Object {
"background": 0,
"border": 0,
"boxSizing": "content-box",
"color": "inherit",
"fontSize": "inherit",
"opacity": 1,
"outline": 0,
"padding": 0,
"width": "1px",
}
}
tabIndex="0"
type="text"
value=""
/>
<div
style={
Object {
"height": 0,
"left": 0,
"overflow": "scroll",
"position": "absolute",
"top": 0,
"visibility": "hidden",
"whiteSpace": "pre",
}
}
>
</div>
</div>
</div>
</div>
<div
className="css-0 gf-form-select-box__indicators"
>
<span
className="gf-form-select-box__select-arrow "
/>
</div>
</div>
</div>
</div>
Select Reducer
</a>
</div>
<div
className="gf-form gf-form--grow"

View File

@ -5,99 +5,20 @@ Array [
<div
className="gf-form-inline"
>
<span
className="gf-form-label width-9 query-keyword"
>
Service
</span>
<div
className="gf-form"
onClick={[Function]}
>
<span
className="gf-form-label width-9 query-keyword"
<a
className="gf-form-label query-part query-placeholder"
>
Service
</span>
<div>
<div
className="css-0 gf-form-input gf-form-input--form-dropdown width-15"
onKeyDown={[Function]}
>
<div
className="css-0 gf-form-select-box__control"
onMouseDown={[Function]}
onTouchEnd={[Function]}
>
<div
className="css-0 gf-form-select-box__value-container"
>
<div
className="css-0 gf-form-select-box__placeholder"
>
Select Services
</div>
<div
className="css-0"
>
<div
className="gf-form-select-box__input"
style={
Object {
"display": "inline-block",
}
}
>
<input
aria-autocomplete="list"
autoCapitalize="none"
autoComplete="off"
autoCorrect="off"
disabled={false}
id="react-select-2-input"
onBlur={[Function]}
onChange={[Function]}
onFocus={[Function]}
spellCheck="false"
style={
Object {
"background": 0,
"border": 0,
"boxSizing": "content-box",
"color": "inherit",
"fontSize": "inherit",
"opacity": 1,
"outline": 0,
"padding": 0,
"width": "1px",
}
}
tabIndex="0"
type="text"
value=""
/>
<div
style={
Object {
"height": 0,
"left": 0,
"overflow": "scroll",
"position": "absolute",
"top": 0,
"visibility": "hidden",
"whiteSpace": "pre",
}
}
>
</div>
</div>
</div>
</div>
<div
className="css-0 gf-form-select-box__indicators"
>
<span
className="gf-form-select-box__select-arrow "
/>
</div>
</div>
</div>
</div>
Select Services
</a>
</div>
<div
className="gf-form gf-form--grow"
@ -110,99 +31,20 @@ Array [
<div
className="gf-form-inline"
>
<span
className="gf-form-label width-9 query-keyword"
>
Metric
</span>
<div
className="gf-form"
onClick={[Function]}
>
<span
className="gf-form-label width-9 query-keyword"
<a
className="gf-form-label query-part query-placeholder query-part"
>
Metric
</span>
<div>
<div
className="css-0 gf-form-input gf-form-input--form-dropdown width-26"
onKeyDown={[Function]}
>
<div
className="css-0 gf-form-select-box__control"
onMouseDown={[Function]}
onTouchEnd={[Function]}
>
<div
className="css-0 gf-form-select-box__value-container"
>
<div
className="css-0 gf-form-select-box__placeholder"
>
Select Metric
</div>
<div
className="css-0"
>
<div
className="gf-form-select-box__input"
style={
Object {
"display": "inline-block",
}
}
>
<input
aria-autocomplete="list"
autoCapitalize="none"
autoComplete="off"
autoCorrect="off"
disabled={false}
id="react-select-3-input"
onBlur={[Function]}
onChange={[Function]}
onFocus={[Function]}
spellCheck="false"
style={
Object {
"background": 0,
"border": 0,
"boxSizing": "content-box",
"color": "inherit",
"fontSize": "inherit",
"opacity": 1,
"outline": 0,
"padding": 0,
"width": "1px",
}
}
tabIndex="0"
type="text"
value=""
/>
<div
style={
Object {
"height": 0,
"left": 0,
"overflow": "scroll",
"position": "absolute",
"top": 0,
"visibility": "hidden",
"whiteSpace": "pre",
}
}
>
</div>
</div>
</div>
</div>
<div
className="css-0 gf-form-select-box__indicators"
>
<span
className="gf-form-select-box__select-arrow "
/>
</div>
</div>
</div>
</div>
Select Metric
</a>
</div>
<div
className="gf-form gf-form--grow"
@ -212,109 +54,67 @@ Array [
/>
</div>
</div>,
<div
style={
Object {
"width": "100%",
}
}
/>,
<div
className="gf-form-inline"
>
<label
className="gf-form-label query-keyword width-9"
>
Filter
</label>
<div
className="gf-form"
onClick={[Function]}
>
<a
className="gf-form-label query-part"
>
<i
className="fa fa-plus"
/>
</a>
</div>
<div
className="gf-form gf-form--grow"
>
<label
className="gf-form-label query-keyword width-9"
className="gf-form-label gf-form-label--grow"
/>
</div>
</div>,
<div
className="gf-form-inline"
>
<label
className="gf-form-label query-keyword width-9"
>
Group By
</label>
<div
className="gf-form gf-form--grow"
>
<label
className="gf-form-label gf-form-label--grow"
/>
</div>
</div>,
<div
className="gf-form-inline"
>
<label
className="gf-form-label query-keyword width-9"
>
Aggregation
</label>
<div
className="gf-form"
onClick={[Function]}
>
<a
className="gf-form-label query-part query-placeholder"
>
Aggregation
</label>
<div>
<div
className="css-0 gf-form-input gf-form-input--form-dropdown width-15"
onKeyDown={[Function]}
>
<div
className="css-0 gf-form-select-box__control"
onMouseDown={[Function]}
onTouchEnd={[Function]}
>
<div
className="css-0 gf-form-select-box__value-container"
>
<div
className="css-0 gf-form-select-box__placeholder"
>
Select Reducer
</div>
<div
className="css-0"
>
<div
className="gf-form-select-box__input"
style={
Object {
"display": "inline-block",
}
}
>
<input
aria-autocomplete="list"
autoCapitalize="none"
autoComplete="off"
autoCorrect="off"
disabled={false}
id="react-select-4-input"
onBlur={[Function]}
onChange={[Function]}
onFocus={[Function]}
spellCheck="false"
style={
Object {
"background": 0,
"border": 0,
"boxSizing": "content-box",
"color": "inherit",
"fontSize": "inherit",
"opacity": 1,
"outline": 0,
"padding": 0,
"width": "1px",
}
}
tabIndex="0"
type="text"
value=""
/>
<div
style={
Object {
"height": 0,
"left": 0,
"overflow": "scroll",
"position": "absolute",
"top": 0,
"visibility": "hidden",
"whiteSpace": "pre",
}
}
>
</div>
</div>
</div>
</div>
<div
className="css-0 gf-form-select-box__indicators"
>
<span
className="gf-form-select-box__select-arrow "
/>
</div>
</div>
</div>
</div>
Select Reducer
</a>
</div>
<div
className="gf-form gf-form--grow"
@ -336,103 +136,20 @@ Array [
<div
className="gf-form-inline"
>
<label
className="gf-form-label query-keyword width-9"
>
Alignment Period
</label>
<div
className="gf-form"
onClick={[Function]}
>
<label
className="gf-form-label query-keyword width-9"
<a
className="gf-form-label query-part"
>
Alignment Period
</label>
<div>
<div
className="css-0 gf-form-input gf-form-input--form-dropdown width-15"
onKeyDown={[Function]}
>
<div
className="css-0 gf-form-select-box__control"
onMouseDown={[Function]}
onTouchEnd={[Function]}
>
<div
className="css-0 gf-form-select-box__value-container gf-form-select-box__value-container--has-value"
>
<div
className="css-0 gf-form-select-box__single-value"
>
<div
className="css-38iae9-singleValue"
>
stackdriver auto
</div>
</div>
<div
className="css-0"
>
<div
className="gf-form-select-box__input"
style={
Object {
"display": "inline-block",
}
}
>
<input
aria-autocomplete="list"
autoCapitalize="none"
autoComplete="off"
autoCorrect="off"
disabled={false}
id="react-select-5-input"
onBlur={[Function]}
onChange={[Function]}
onFocus={[Function]}
spellCheck="false"
style={
Object {
"background": 0,
"border": 0,
"boxSizing": "content-box",
"color": "inherit",
"fontSize": "inherit",
"opacity": 1,
"outline": 0,
"padding": 0,
"width": "1px",
}
}
tabIndex="0"
type="text"
value=""
/>
<div
style={
Object {
"height": 0,
"left": 0,
"overflow": "scroll",
"position": "absolute",
"top": 0,
"visibility": "hidden",
"whiteSpace": "pre",
}
}
>
</div>
</div>
</div>
</div>
<div
className="css-0 gf-form-select-box__indicators"
>
<span
className="gf-form-select-box__select-arrow "
/>
</div>
</div>
</div>
</div>
stackdriver auto
</a>
</div>
<div
className="gf-form gf-form--grow"

View File

@ -0,0 +1,11 @@
export { Project } from './Project';
export { Metrics } from './Metrics';
export { Help } from './Help';
export { GroupBys } from './GroupBys';
export { Filters } from './Filters';
export { AnnotationsHelp } from './AnnotationsHelp';
export { Alignments } from './Alignments';
export { AlignmentPeriods } from './AlignmentPeriods';
export { AliasBy } from './AliasBy';
export { Aggregations } from './Aggregations';
export { SimpleSelect } from './SimpleSelect';

View File

@ -260,3 +260,16 @@ export const stackdriverUnitMappings = {
'By/s': 'Bps',
GBy: 'decgbytes',
};
export const systemLabels = [
'metadata.system_labels.cloud_account',
'metadata.system_labels.name',
'metadata.system_labels.region',
'metadata.system_labels.state',
'metadata.system_labels.instance_group',
'metadata.system_labels.node_name',
'metadata.system_labels.service_name',
'metadata.system_labels.top_level_controller_type',
'metadata.system_labels.top_level_controller_name',
'metadata.system_labels.container_image',
];

View File

@ -2,7 +2,7 @@ import { stackdriverUnitMappings } from './constants';
import appEvents from 'app/core/app_events';
import _ from 'lodash';
import StackdriverMetricFindQuery from './StackdriverMetricFindQuery';
import { StackdriverQuery, MetricDescriptor, StackdriverOptions } from './types';
import { StackdriverQuery, MetricDescriptor, StackdriverOptions, Filter } from './types';
import { DataSourceApi, DataQueryRequest, DataSourceInstanceSettings, ScopedVars } from '@grafana/data';
import { BackendSrv } from 'app/core/services/backend_srv';
import { TemplateSrv } from 'app/features/templating/template_srv';
@ -21,7 +21,7 @@ export default class StackdriverDatasource extends DataSourceApi<StackdriverQuer
constructor(
instanceSettings: DataSourceInstanceSettings<StackdriverOptions>,
private backendSrv: BackendSrv,
private templateSrv: TemplateSrv,
public templateSrv: TemplateSrv,
private timeSrv: TimeSrv
) {
super(instanceSettings);
@ -32,6 +32,10 @@ export default class StackdriverDatasource extends DataSourceApi<StackdriverQuer
this.metricTypes = [];
}
get variables() {
return this.templateSrv.variables.map(v => `$${v.name}`);
}
async getTimeSeries(options: any) {
const queries = options.targets
.filter((target: any) => {
@ -71,26 +75,40 @@ export default class StackdriverDatasource extends DataSourceApi<StackdriverQuer
}
interpolateFilters(filters: string[], scopedVars: ScopedVars) {
return (filters || []).map(f => {
const completeFilter = _.chunk(filters, 4)
.map(([key, operator, value, condition = 'AND']) => ({
key,
operator,
value,
condition,
}))
.reduce((res, filter) => (filter.value ? [...res, filter] : res), []);
const filterArray = _.flatten(
completeFilter.map(({ key, operator, value, condition }: Filter) => [key, operator, value, condition])
);
return (filterArray || []).map(f => {
return this.templateSrv.replace(f, scopedVars || {}, 'regex');
});
}
async getLabels(metricType: string, refId: string) {
async getLabels(metricType: string, refId: string, groupBys?: string[]) {
const response = await this.getTimeSeries({
targets: [
{
refId: refId,
datasourceId: this.id,
metricType: this.templateSrv.replace(metricType),
groupBys: this.interpolateGroupBys(groupBys || [], {}),
crossSeriesReducer: 'REDUCE_NONE',
view: 'HEADERS',
},
],
range: this.timeSrv.timeRange(),
});
return response.results[refId];
const result = response.results[refId];
return result && result.meta ? result.meta.labels : {};
}
interpolateGroupBys(groupBys: string[], scopedVars: {}): string[] {
@ -158,9 +176,7 @@ export default class StackdriverDatasource extends DataSourceApi<StackdriverQuer
text: this.templateSrv.replace(annotation.target.text, options.scopedVars || {}),
tags: this.templateSrv.replace(annotation.target.tags, options.scopedVars || {}),
view: 'FULL',
filters: (annotation.target.filters || []).map((f: any) => {
return this.templateSrv.replace(f, options.scopedVars || {});
}),
filters: this.interpolateFilters(annotation.target.filters || [], options.scopedVars),
type: 'annotationQuery',
},
];

View File

@ -1,123 +0,0 @@
import { Segment } from './types';
export const DefaultRemoveFilterValue = '-- remove filter --';
export const DefaultFilterValue = 'select value';
export class FilterSegments {
filterSegments: any[];
removeSegment: any;
constructor(
private uiSegmentSrv: any,
private filters: string[],
private getFilterKeysFunc: (arg0: any, arg1: string) => any,
private getFilterValuesFunc: (arg0: any) => any
) {}
buildSegmentModel() {
this.removeSegment = this.uiSegmentSrv.newSegment({ fake: true, value: DefaultRemoveFilterValue });
this.filterSegments = [];
this.filters.forEach((f, index) => {
switch (index % 4) {
case 0:
this.filterSegments.push(this.uiSegmentSrv.newKey(f));
break;
case 1:
this.filterSegments.push(this.uiSegmentSrv.newOperator(f));
break;
case 2:
this.filterSegments.push(this.uiSegmentSrv.newKeyValue(f));
break;
case 3:
this.filterSegments.push(this.uiSegmentSrv.newCondition(f));
break;
}
});
this.ensurePlusButton(this.filterSegments);
}
async getFilters(segment: { type: any; value?: any }, index: number, hasNoFilterKeys: boolean) {
if (segment.type === 'condition') {
return [this.uiSegmentSrv.newSegment('AND')];
}
if (segment.type === 'operator') {
return this.uiSegmentSrv.newOperators(['=', '!=', '=~', '!=~']);
}
if (segment.type === 'key' || segment.type === 'plus-button') {
if (hasNoFilterKeys && segment.value && segment.value !== DefaultRemoveFilterValue) {
this.removeSegment.value = DefaultRemoveFilterValue;
return Promise.resolve([this.removeSegment]);
} else {
return this.getFilterKeysFunc(segment, DefaultRemoveFilterValue);
}
}
if (segment.type === 'value') {
const filterValues = this.getFilterValuesFunc(index);
if (filterValues.length > 0) {
return this.getValuesForFilterKey(filterValues);
}
}
return [];
}
getValuesForFilterKey(labels: any[]) {
const filterValues = labels.map(l => {
return this.uiSegmentSrv.newSegment({
value: `${l}`,
expandable: false,
});
});
return filterValues;
}
addNewFilterSegments(segment: Segment, index: number) {
if (index > 2) {
this.filterSegments.splice(index, 0, this.uiSegmentSrv.newCondition('AND'));
}
segment.type = 'key';
this.filterSegments.push(this.uiSegmentSrv.newOperator('='));
this.filterSegments.push(this.uiSegmentSrv.newFake(DefaultFilterValue, 'value', 'query-segment-value'));
}
removeFilterSegment(index: number) {
this.filterSegments.splice(index, 3);
// remove trailing condition
if (index > 2 && this.filterSegments[index - 1].type === 'condition') {
this.filterSegments.splice(index - 1, 1);
}
// remove condition if it is first segment
if (index === 0 && this.filterSegments.length > 0 && this.filterSegments[0].type === 'condition') {
this.filterSegments.splice(0, 1);
}
}
ensurePlusButton(segments: Segment[]) {
const count = segments.length;
const lastSegment = segments[Math.max(count - 1, 0)];
if (!lastSegment || lastSegment.type !== 'plus-button') {
segments.push(this.uiSegmentSrv.newPlusButton());
}
}
filterSegmentUpdated(segment: Segment, index: number) {
if (segment.type === 'plus-button') {
this.addNewFilterSegments(segment, index);
} else if (segment.type === 'key' && segment.value === DefaultRemoveFilterValue) {
this.removeFilterSegment(index);
this.ensurePlusButton(this.filterSegments);
} else if (segment.type === 'value' && segment.value !== DefaultFilterValue) {
this.ensurePlusButton(this.filterSegments);
}
return this.filterSegments.filter(s => s.type !== 'plus-button').map(seg => seg.value);
}
}

View File

@ -1,9 +1,10 @@
import uniqBy from 'lodash/uniqBy';
import { alignOptions, aggOptions, ValueTypes, MetricKind } from './constants';
import _ from 'lodash';
import { alignOptions, aggOptions, ValueTypes, MetricKind, systemLabels } from './constants';
import { SelectableValue } from '@grafana/data';
import { TemplateSrv } from 'app/features/templating/template_srv';
import { StackdriverQuery } from './types';
export const extractServicesFromMetricDescriptors = (metricDescriptors: any) => uniqBy(metricDescriptors, 'service');
export const extractServicesFromMetricDescriptors = (metricDescriptors: any) => _.uniqBy(metricDescriptors, 'service');
export const getMetricTypesByService = (metricDescriptors: any, service: any) =>
metricDescriptors.filter((m: any) => m.service === service);
@ -44,33 +45,40 @@ export const getAggregationOptionsByMetric = (valueType: ValueTypes, metricKind:
export const getLabelKeys = async (datasource: any, selectedMetricType: any) => {
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;
const labels = await datasource.getLabels(selectedMetricType, refId);
return [...Object.keys(labels), ...systemLabels];
};
export const getAlignmentPickerData = (
{ valueType, metricKind, perSeriesAligner }: Partial<StackdriverQuery>,
templateSrv: TemplateSrv
) => {
const options = getAlignmentOptionsByMetric(valueType, metricKind).map(option => ({
const alignOptions = getAlignmentOptionsByMetric(valueType, metricKind).map(option => ({
...option,
label: option.text,
}));
const alignOptions = [
{
label: 'Alignment options',
expanded: true,
options,
},
];
if (!options.some(o => o.value === templateSrv.replace(perSeriesAligner))) {
perSeriesAligner = options.length > 0 ? options[0].value : '';
if (!alignOptions.some(o => o.value === templateSrv.replace(perSeriesAligner))) {
perSeriesAligner = alignOptions.length > 0 ? alignOptions[0].value : '';
}
return { alignOptions, perSeriesAligner };
};
export const labelsToGroupedOptions = (groupBys: string[]) => {
const groups = groupBys.reduce((acc: any, curr: string) => {
const arr = curr.split('.').map(_.startCase);
const group = (arr.length === 2 ? arr : _.initial(arr)).join(' ');
const option = {
value: curr,
label: curr,
};
if (acc[group]) {
acc[group] = [...acc[group], option];
} else {
acc[group] = [option];
}
return acc;
}, {});
return Object.entries(groups).map(([label, options]) => ({ label, options, expanded: true }), []);
};
export const toOption = (value: string) => ({ label: value, value } as SelectableValue<string>);

View File

@ -1,26 +0,0 @@
<div class="gf-form-inline">
<div class="gf-form">
<span class="gf-form-label query-keyword width-9">Filter</span>
<div class="gf-form" ng-repeat="segment in ctrl.filterSegments.filterSegments">
<metric-segment
segment="segment"
get-options="ctrl.getFilters(segment, $index)"
on-change="ctrl.filterSegmentUpdated(segment, $index)"
></metric-segment>
</div>
</div>
<div class="gf-form gf-form--grow"><div class="gf-form-label gf-form-label--grow"></div></div>
</div>
<div class="gf-form-inline" ng-hide="ctrl.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>
</div>
</div>
<div class="gf-form gf-form--grow"><div class="gf-form-label gf-form-label--grow"></div></div>
</div>

View File

@ -1,196 +0,0 @@
import coreModule from 'app/core/core_module';
import _ from 'lodash';
import { FilterSegments, DefaultFilterValue } from './filter_segments';
import { TemplateSrv } from 'app/features/templating/template_srv';
import { Segment } from './types';
export class StackdriverFilterCtrl {
defaultRemoveGroupByValue = '-- remove group by --';
resourceTypeValue = 'resource.type';
groupBySegments: any[];
filterSegments: FilterSegments;
removeSegment: any;
filters: string[];
groupBys: string[];
hideGroupBys: boolean;
labelData: any;
loading: Promise<any>;
filtersChanged: (filters: any) => void;
groupBysChanged: (groupBys: any) => void;
/** @ngInject */
constructor(private $scope: any, private uiSegmentSrv: any, private templateSrv: TemplateSrv) {
this.$scope.ctrl = this;
this.initSegments(this.hideGroupBys);
}
initSegments(hideGroupBys: boolean) {
if (!hideGroupBys) {
this.groupBySegments = this.groupBys.map(groupBy => {
return this.uiSegmentSrv.getSegmentForValue(groupBy);
});
this.ensurePlusButton(this.groupBySegments);
}
this.removeSegment = this.uiSegmentSrv.newSegment({ fake: true, value: '-- remove group by --' });
this.filterSegments = new FilterSegments(
this.uiSegmentSrv,
this.filters,
this.getFilterKeys.bind(this),
this.getFilterValues.bind(this)
);
this.filterSegments.buildSegmentModel();
}
async createLabelKeyElements() {
await this.loading;
let elements = Object.keys(this.labelData.metricLabels || {}).map(l => {
return this.uiSegmentSrv.newSegment({
value: `metric.label.${l}`,
expandable: false,
});
});
elements = [
...elements,
...Object.keys(this.labelData.resourceLabels || {}).map(l => {
return this.uiSegmentSrv.newSegment({
value: `resource.label.${l}`,
expandable: false,
});
}),
];
if (this.labelData.resourceTypes && this.labelData.resourceTypes.length > 0) {
elements = [
...elements,
this.uiSegmentSrv.newSegment({
value: this.resourceTypeValue,
expandable: false,
}),
];
}
return elements;
}
async getFilterKeys(segment: { type: string }, removeText: string) {
let elements = await this.createLabelKeyElements();
if (this.filters.indexOf(this.resourceTypeValue) !== -1) {
elements = elements.filter(e => e.value !== this.resourceTypeValue);
}
const noValueOrPlusButton = !segment || segment.type === 'plus-button';
if (noValueOrPlusButton && elements.length === 0) {
return [];
}
return segment.type === 'plus-button'
? elements
: [
...elements,
this.uiSegmentSrv.newSegment({ fake: true, value: removeText || this.defaultRemoveGroupByValue }),
];
}
async getGroupBys(segment: { type: any }) {
let elements = await this.createLabelKeyElements();
elements = elements.filter(e => this.groupBys.indexOf(e.value) === -1);
const noValueOrPlusButton = !segment || segment.type === 'plus-button';
if (noValueOrPlusButton && elements.length === 0) {
return [];
}
this.removeSegment.value = this.defaultRemoveGroupByValue;
return segment.type === 'plus-button' ? elements : [...elements, this.removeSegment];
}
groupByChanged(segment: any, index?: number) {
if (segment.value === this.removeSegment.value) {
this.groupBySegments.splice(index, 1);
} else {
segment.type = 'value';
}
const reducer = (memo: any[], seg: { fake: any; value: any }) => {
if (!seg.fake) {
memo.push(seg.value);
}
return memo;
};
const groupBys = this.groupBySegments.reduce(reducer, []);
this.groupBysChanged({ groupBys });
this.ensurePlusButton(this.groupBySegments);
}
async getFilters(segment: { type: string }, index: number) {
await this.loading;
const hasNoFilterKeys = this.labelData.metricLabels && Object.keys(this.labelData.metricLabels).length === 0;
return this.filterSegments.getFilters(segment, index, hasNoFilterKeys);
}
getFilterValues(index: number) {
const filterKey = this.templateSrv.replace(this.filterSegments.filterSegments[index - 2].value);
if (!filterKey || !this.labelData.metricLabels || Object.keys(this.labelData.metricLabels).length === 0) {
return [];
}
const shortKey = filterKey.substring(filterKey.indexOf('.label.') + 7);
if (filterKey.startsWith('metric.label.') && this.labelData.metricLabels.hasOwnProperty(shortKey)) {
return this.labelData.metricLabels[shortKey];
}
if (filterKey.startsWith('resource.label.') && this.labelData.resourceLabels.hasOwnProperty(shortKey)) {
return this.labelData.resourceLabels[shortKey];
}
if (filterKey === this.resourceTypeValue) {
return this.labelData.resourceTypes;
}
return [];
}
filterSegmentUpdated(segment: { value: string; type: string }, index: number) {
const filters = this.filterSegments.filterSegmentUpdated(segment, index);
if (!filters.some(f => f === DefaultFilterValue)) {
this.filtersChanged({ filters });
}
}
ensurePlusButton(segments: Segment[]) {
const count = segments.length;
const lastSegment = segments[Math.max(count - 1, 0)];
if (!lastSegment || lastSegment.type !== 'plus-button') {
segments.push(this.uiSegmentSrv.newPlusButton());
}
}
}
/** @ngInject */
function stackdriverFilter() {
return {
templateUrl: 'public/app/plugins/datasource/stackdriver/partials/query.filter.html',
controller: StackdriverFilterCtrl,
controllerAs: 'ctrl',
bindToController: true,
restrict: 'E',
scope: {
labelData: '<',
loading: '<',
groupBys: '<',
filters: '<',
filtersChanged: '&',
groupBysChanged: '&',
hideGroupBys: '<',
},
};
}
coreModule.directive('stackdriverFilter', stackdriverFilter);

View File

@ -176,7 +176,7 @@ describe('StackdriverDataSource', () => {
});
it('should replace the variable with the value', () => {
expect(interpolated.length).toBe(3);
expect(interpolated.length).toBe(4);
expect(interpolated[2]).toBe('filtervalue1');
});
});

View File

@ -1,415 +0,0 @@
import { StackdriverFilterCtrl } from '../query_filter_ctrl';
import { TemplateSrvStub } from 'test/specs/helpers';
import { DefaultRemoveFilterValue, DefaultFilterValue } from '../filter_segments';
describe('StackdriverQueryFilterCtrl', () => {
let ctrl: StackdriverFilterCtrl;
let result: any;
let groupByChangedMock: any;
describe('when initializing query editor', () => {
beforeEach(() => {
const existingFilters = ['key1', '=', 'val1', 'AND', 'key2', '=', 'val2'];
ctrl = createCtrlWithFakes(existingFilters);
});
it('should initialize filter segments using the target filter values', () => {
expect(ctrl.filterSegments.filterSegments.length).toBe(8);
expect(ctrl.filterSegments.filterSegments[0].type).toBe('key');
expect(ctrl.filterSegments.filterSegments[1].type).toBe('operator');
expect(ctrl.filterSegments.filterSegments[2].type).toBe('value');
expect(ctrl.filterSegments.filterSegments[3].type).toBe('condition');
expect(ctrl.filterSegments.filterSegments[4].type).toBe('key');
expect(ctrl.filterSegments.filterSegments[5].type).toBe('operator');
expect(ctrl.filterSegments.filterSegments[6].type).toBe('value');
expect(ctrl.filterSegments.filterSegments[7].type).toBe('plus-button');
});
});
describe('group bys', () => {
beforeEach(() => {
ctrl = createCtrlWithFakes();
});
describe('when labels are fetched', () => {
beforeEach(async () => {
ctrl.labelData.metricLabels = { 'metric-key-1': ['metric-value-1'] };
ctrl.labelData.resourceLabels = { 'resource-key-1': ['resource-value-1'] };
result = await ctrl.getGroupBys({ type: '' });
});
it('should populate group bys segments', () => {
expect(result.length).toBe(3);
expect(result[0].value).toBe('metric.label.metric-key-1');
expect(result[1].value).toBe('resource.label.resource-key-1');
expect(result[2].value).toBe('-- remove group by --');
});
});
describe('when a group by label is selected', () => {
beforeEach(async () => {
ctrl.labelData.metricLabels = {
'metric-key-1': ['metric-value-1'],
'metric-key-2': ['metric-value-2'],
};
ctrl.labelData.resourceLabels = {
'resource-key-1': ['resource-value-1'],
'resource-key-2': ['resource-value-2'],
};
ctrl.groupBys = ['metric.label.metric-key-1', 'resource.label.resource-key-1'];
result = await ctrl.getGroupBys({ type: '' });
});
it('should not be used to populate group bys segments', () => {
expect(result.length).toBe(3);
expect(result[0].value).toBe('metric.label.metric-key-2');
expect(result[1].value).toBe('resource.label.resource-key-2');
expect(result[2].value).toBe('-- remove group by --');
});
});
describe('when a group by is selected', () => {
beforeEach(() => {
groupByChangedMock = jest.fn();
ctrl.groupBysChanged = groupByChangedMock;
const removeSegment = { fake: true, value: '-- remove group by --' };
const segment = { value: 'groupby1' };
ctrl.groupBySegments = [segment, removeSegment];
ctrl.groupByChanged(segment);
});
it('should be added to group bys list', () => {
expect(groupByChangedMock).toHaveBeenCalledWith({ groupBys: ['groupby1'] });
});
});
describe('when a selected group by is removed', () => {
beforeEach(() => {
groupByChangedMock = jest.fn();
ctrl.groupBysChanged = groupByChangedMock;
const removeSegment = { fake: true, value: '-- remove group by --' };
const segment = { value: 'groupby1' };
ctrl.groupBySegments = [segment, removeSegment];
ctrl.groupByChanged(removeSegment);
});
it('should be added to group bys list', () => {
expect(groupByChangedMock).toHaveBeenCalledWith({ groupBys: [] });
});
});
});
describe('filters', () => {
beforeEach(() => {
ctrl = createCtrlWithFakes();
});
describe('when values for a condition filter part are fetched', () => {
beforeEach(async () => {
const segment = { type: 'condition' };
result = await ctrl.getFilters(segment, 0);
});
it('should populate condition segments', () => {
expect(result.length).toBe(1);
expect(result[0].value).toBe('AND');
});
});
describe('when values for a operator filter part are fetched', () => {
beforeEach(async () => {
const segment = { type: 'operator' };
result = await ctrl.getFilters(segment, 0);
});
it('should populate group bys segments', () => {
expect(result.length).toBe(4);
expect(result[0].value).toBe('=');
expect(result[1].value).toBe('!=');
expect(result[2].value).toBe('=~');
expect(result[3].value).toBe('!=~');
});
});
describe('when values for a key filter part are fetched', () => {
beforeEach(async () => {
ctrl.labelData.metricLabels = {
'metric-key-1': ['metric-value-1'],
'metric-key-2': ['metric-value-2'],
};
ctrl.labelData.resourceLabels = {
'resource-key-1': ['resource-value-1'],
'resource-key-2': ['resource-value-2'],
};
const segment = { type: 'key' };
result = await ctrl.getFilters(segment, 0);
});
it('should populate filter key segments', () => {
expect(result.length).toBe(5);
expect(result[0].value).toBe('metric.label.metric-key-1');
expect(result[1].value).toBe('metric.label.metric-key-2');
expect(result[2].value).toBe('resource.label.resource-key-1');
expect(result[3].value).toBe('resource.label.resource-key-2');
expect(result[4].value).toBe('-- remove filter --');
});
});
describe('when values for a value filter part are fetched', () => {
beforeEach(async () => {
ctrl.labelData.metricLabels = {
'metric-key-1': ['metric-value-1'],
'metric-key-2': ['metric-value-2'],
};
ctrl.labelData.resourceLabels = {
'resource-key-1': ['resource-value-1'],
'resource-key-2': ['resource-value-2'],
};
ctrl.filterSegments.filterSegments = [
{ type: 'key', value: 'metric.label.metric-key-1' },
{ type: 'operator', value: '=' },
];
const segment = { type: 'value' };
result = await ctrl.getFilters(segment, 2);
});
it('should populate filter value segments', () => {
expect(result.length).toBe(1);
expect(result[0].value).toBe('metric-value-1');
});
});
describe('when a filter is created by clicking on plus button', () => {
describe('and there are no other filters', () => {
beforeEach(() => {
const segment = { value: 'filterkey1', type: 'plus-button' };
ctrl.filterSegments.filterSegments = [segment];
ctrl.filterSegmentUpdated(segment, 0);
});
it('should transform the plus button segment to a key segment', () => {
expect(ctrl.filterSegments.filterSegments[0].type).toBe('key');
});
it('should add an operator, value segment and plus button segment', () => {
expect(ctrl.filterSegments.filterSegments.length).toBe(3);
expect(ctrl.filterSegments.filterSegments[1].type).toBe('operator');
expect(ctrl.filterSegments.filterSegments[2].type).toBe('value');
});
});
});
describe('when has one existing filter', () => {
describe('and user clicks on key segment', () => {
beforeEach(() => {
const existingKeySegment = { value: 'filterkey1', type: 'key' };
const existingOperatorSegment = { value: '=', type: 'operator' };
const existingValueSegment = { value: 'filtervalue', type: 'value' };
const plusSegment = { value: '', type: 'plus-button' };
ctrl.filterSegments.filterSegments = [
existingKeySegment,
existingOperatorSegment,
existingValueSegment,
plusSegment,
];
ctrl.filterSegmentUpdated(existingKeySegment, 0);
});
it('should not add any new segments', () => {
expect(ctrl.filterSegments.filterSegments.length).toBe(4);
expect(ctrl.filterSegments.filterSegments[0].type).toBe('key');
expect(ctrl.filterSegments.filterSegments[1].type).toBe('operator');
expect(ctrl.filterSegments.filterSegments[2].type).toBe('value');
});
});
describe('and user clicks on value segment and value not equal to fake value', () => {
beforeEach(() => {
const existingKeySegment = { value: 'filterkey1', type: 'key' };
const existingOperatorSegment = { value: '=', type: 'operator' };
const existingValueSegment = { value: 'filtervalue', type: 'value' };
ctrl.filterSegments.filterSegments = [existingKeySegment, existingOperatorSegment, existingValueSegment];
ctrl.filterSegmentUpdated(existingValueSegment, 2);
});
it('should ensure that plus segment exists', () => {
expect(ctrl.filterSegments.filterSegments.length).toBe(4);
expect(ctrl.filterSegments.filterSegments[0].type).toBe('key');
expect(ctrl.filterSegments.filterSegments[1].type).toBe('operator');
expect(ctrl.filterSegments.filterSegments[2].type).toBe('value');
expect(ctrl.filterSegments.filterSegments[3].type).toBe('plus-button');
});
});
describe('and user clicks on value segment and value is equal to fake value', () => {
beforeEach(() => {
const existingKeySegment = { value: 'filterkey1', type: 'key' };
const existingOperatorSegment = { value: '=', type: 'operator' };
const existingValueSegment = { value: DefaultFilterValue, type: 'value' };
ctrl.filterSegments.filterSegments = [existingKeySegment, existingOperatorSegment, existingValueSegment];
ctrl.filterSegmentUpdated(existingValueSegment, 2);
});
it('should not add plus segment', () => {
expect(ctrl.filterSegments.filterSegments.length).toBe(3);
expect(ctrl.filterSegments.filterSegments[0].type).toBe('key');
expect(ctrl.filterSegments.filterSegments[1].type).toBe('operator');
expect(ctrl.filterSegments.filterSegments[2].type).toBe('value');
});
});
describe('and user removes key segment', () => {
beforeEach(() => {
const existingKeySegment = { value: DefaultRemoveFilterValue, type: 'key' };
const existingOperatorSegment = { value: '=', type: 'operator' };
const existingValueSegment = { value: 'filtervalue', type: 'value' };
const plusSegment = { value: '', type: 'plus-button' };
ctrl.filterSegments.filterSegments = [
existingKeySegment,
existingOperatorSegment,
existingValueSegment,
plusSegment,
];
ctrl.filterSegmentUpdated(existingKeySegment, 0);
});
it('should remove filter segments', () => {
expect(ctrl.filterSegments.filterSegments.length).toBe(1);
expect(ctrl.filterSegments.filterSegments[0].type).toBe('plus-button');
});
});
describe('and user removes key segment and there is a previous filter', () => {
beforeEach(() => {
const existingKeySegment1 = { value: DefaultRemoveFilterValue, type: 'key' };
const existingKeySegment2 = { value: DefaultRemoveFilterValue, type: 'key' };
const existingOperatorSegment = { value: '=', type: 'operator' };
const existingValueSegment = { value: 'filtervalue', type: 'value' };
const conditionSegment = { value: 'AND', type: 'condition' };
const plusSegment = { value: '', type: 'plus-button' };
ctrl.filterSegments.filterSegments = [
existingKeySegment1,
existingOperatorSegment,
existingValueSegment,
conditionSegment,
existingKeySegment2,
Object.assign({}, existingOperatorSegment),
Object.assign({}, existingValueSegment),
plusSegment,
];
ctrl.filterSegmentUpdated(existingKeySegment2, 4);
});
it('should remove filter segments and the condition segment', () => {
expect(ctrl.filterSegments.filterSegments.length).toBe(4);
expect(ctrl.filterSegments.filterSegments[0].type).toBe('key');
expect(ctrl.filterSegments.filterSegments[1].type).toBe('operator');
expect(ctrl.filterSegments.filterSegments[2].type).toBe('value');
expect(ctrl.filterSegments.filterSegments[3].type).toBe('plus-button');
});
});
describe('and user removes key segment and there is a filter after it', () => {
beforeEach(() => {
const existingKeySegment1 = { value: DefaultRemoveFilterValue, type: 'key' };
const existingKeySegment2 = { value: DefaultRemoveFilterValue, type: 'key' };
const existingOperatorSegment = { value: '=', type: 'operator' };
const existingValueSegment = { value: 'filtervalue', type: 'value' };
const conditionSegment = { value: 'AND', type: 'condition' };
const plusSegment = { value: '', type: 'plus-button' };
ctrl.filterSegments.filterSegments = [
existingKeySegment1,
existingOperatorSegment,
existingValueSegment,
conditionSegment,
existingKeySegment2,
Object.assign({}, existingOperatorSegment),
Object.assign({}, existingValueSegment),
plusSegment,
];
ctrl.filterSegmentUpdated(existingKeySegment1, 0);
});
it('should remove filter segments and the condition segment', () => {
expect(ctrl.filterSegments.filterSegments.length).toBe(4);
expect(ctrl.filterSegments.filterSegments[0].type).toBe('key');
expect(ctrl.filterSegments.filterSegments[1].type).toBe('operator');
expect(ctrl.filterSegments.filterSegments[2].type).toBe('value');
expect(ctrl.filterSegments.filterSegments[3].type).toBe('plus-button');
});
});
describe('and user clicks on plus button', () => {
beforeEach(() => {
const existingKeySegment = { value: 'filterkey1', type: 'key' };
const existingOperatorSegment = { value: '=', type: 'operator' };
const existingValueSegment = { value: 'filtervalue', type: 'value' };
const plusSegment = { value: 'filterkey2', type: 'plus-button' };
ctrl.filterSegments.filterSegments = [
existingKeySegment,
existingOperatorSegment,
existingValueSegment,
plusSegment,
];
ctrl.filterSegmentUpdated(plusSegment, 3);
});
it('should condition segment and new filter segments', () => {
expect(ctrl.filterSegments.filterSegments.length).toBe(7);
expect(ctrl.filterSegments.filterSegments[0].type).toBe('key');
expect(ctrl.filterSegments.filterSegments[1].type).toBe('operator');
expect(ctrl.filterSegments.filterSegments[2].type).toBe('value');
expect(ctrl.filterSegments.filterSegments[3].type).toBe('condition');
expect(ctrl.filterSegments.filterSegments[4].type).toBe('key');
expect(ctrl.filterSegments.filterSegments[5].type).toBe('operator');
expect(ctrl.filterSegments.filterSegments[6].type).toBe('value');
});
});
});
});
});
function createCtrlWithFakes(existingFilters?: string[]) {
const fakeSegmentServer = {
newKey: (val: any) => {
return { value: val, type: 'key' };
},
newKeyValue: (val: any) => {
return { value: val, type: 'value' };
},
newSegment: (obj: any) => {
return { value: obj.value ? obj.value : obj };
},
newOperators: (ops: any) => {
return ops.map((o: any) => {
return { type: 'operator', value: o };
});
},
newFake: (value: any, type: any, cssClass: any) => {
return { value, type, cssClass };
},
newOperator: (op: any) => {
return { value: op, type: 'operator' };
},
newPlusButton: () => {
return { type: 'plus-button' };
},
newCondition: (val: any) => {
return { type: 'condition', value: val };
},
};
const scope: any = {
hideGroupBys: false,
groupBys: [],
filters: existingFilters || [],
labelData: {
metricLabels: {},
resourceLabels: {},
resourceTypes: [],
},
filtersChanged: () => {},
groupBysChanged: () => {},
datasource: {
getDefaultProject: () => {
return 'project';
},
},
refresh: () => {},
};
Object.assign(StackdriverFilterCtrl.prototype, scope);
// @ts-ignore
return new StackdriverFilterCtrl(scope, fakeSegmentServer, new TemplateSrvStub());
}

View File

@ -80,3 +80,10 @@ export interface Segment {
type: string;
value: string;
}
export interface Filter {
key: string;
operator: string;
value: string;
condition: string;
}