mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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:
parent
72023d90bd
commit
260239d98b
@ -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])
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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() {
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
@ -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"`
|
||||
|
@ -90,7 +90,6 @@ export function registerAngularDirectives() {
|
||||
react2AngularDirective('stackdriverAnnotationQueryEditor', StackdriverAnnotationQueryEditor, [
|
||||
'target',
|
||||
'onQueryChange',
|
||||
'onExecuteQuery',
|
||||
['datasource', { watchDepth: 'reference' }],
|
||||
['templateSrv', { watchDepth: 'reference' }],
|
||||
]);
|
||||
|
@ -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) {
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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']));
|
||||
});
|
||||
|
@ -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}>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
</>
|
||||
|
@ -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}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
@ -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%' }} />;
|
||||
}
|
||||
}
|
103
public/app/plugins/datasource/stackdriver/components/Filters.tsx
Normal file
103
public/app/plugins/datasource/stackdriver/components/Filters.tsx
Normal 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>
|
||||
);
|
||||
};
|
@ -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>
|
||||
);
|
||||
};
|
@ -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
|
||||
|
@ -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>
|
||||
|
@ -11,6 +11,8 @@ const props: Props = {
|
||||
datasource: {
|
||||
getDefaultProject: () => Promise.resolve('project'),
|
||||
getMetricTypes: () => Promise.resolve([]),
|
||||
getLabels: () => Promise.resolve([]),
|
||||
variables: [],
|
||||
} as any,
|
||||
templateSrv: new TemplateSrv(),
|
||||
};
|
||||
|
@ -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}
|
||||
|
@ -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;
|
||||
|
@ -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';
|
||||
|
||||
|
@ -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"
|
||||
|
@ -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"
|
||||
|
@ -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';
|
@ -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',
|
||||
];
|
||||
|
@ -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',
|
||||
},
|
||||
];
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
@ -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>);
|
||||
|
@ -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>
|
@ -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);
|
@ -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');
|
||||
});
|
||||
});
|
||||
|
@ -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());
|
||||
}
|
@ -80,3 +80,10 @@ export interface Segment {
|
||||
type: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
export interface Filter {
|
||||
key: string;
|
||||
operator: string;
|
||||
value: string;
|
||||
condition: string;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user