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
@ -31,8 +31,14 @@ import (
|
|||||||
|
|
||||||
var (
|
var (
|
||||||
slog log.Logger
|
slog log.Logger
|
||||||
legendKeyFormat *regexp.Regexp
|
)
|
||||||
metricNameFormat *regexp.Regexp
|
|
||||||
|
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 (
|
const (
|
||||||
@ -62,8 +68,6 @@ func NewStackdriverExecutor(dsInfo *models.DataSource) (tsdb.TsdbQueryEndpoint,
|
|||||||
func init() {
|
func init() {
|
||||||
slog = log.New("tsdb.stackdriver")
|
slog = log.New("tsdb.stackdriver")
|
||||||
tsdb.RegisterTsdbQueryEndpoint("stackdriver", NewStackdriverExecutor)
|
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
|
// 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 = reverse(strings.Replace(reverse(value), "*", "", 1))
|
||||||
value = fmt.Sprintf(`starts_with("%s")`, value)
|
value = fmt.Sprintf(`starts_with("%s")`, value)
|
||||||
} else if matches != 0 {
|
} else if matches != 0 {
|
||||||
re := regexp.MustCompile(`[-\/^$+?.()|[\]{}]`)
|
value = string(wildcardRegexRe.ReplaceAllFunc([]byte(value), func(in []byte) []byte {
|
||||||
value = string(re.ReplaceAllFunc([]byte(value), func(in []byte) []byte {
|
|
||||||
return []byte(strings.Replace(string(in), string(in), `\\`+string(in), 1))
|
return []byte(strings.Replace(string(in), string(in), `\\`+string(in), 1))
|
||||||
}))
|
}))
|
||||||
value = strings.Replace(value, "*", ".*", -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"]
|
alignmentPeriod, ok := req.URL.Query()["aggregation.alignmentPeriod"]
|
||||||
|
|
||||||
if ok {
|
if ok {
|
||||||
re := regexp.MustCompile("[0-9]+")
|
seconds, err := strconv.ParseInt(alignmentPeriodRe.FindString(alignmentPeriod[0]), 10, 64)
|
||||||
seconds, err := strconv.ParseInt(re.FindString(alignmentPeriod[0]), 10, 64)
|
|
||||||
if err == nil {
|
if err == nil {
|
||||||
queryResult.Meta.Set("alignmentPeriod", seconds)
|
queryResult.Meta.Set("alignmentPeriod", seconds)
|
||||||
}
|
}
|
||||||
@ -333,8 +335,6 @@ func (e *StackdriverExecutor) unmarshalResponse(res *http.Response) (Stackdriver
|
|||||||
return StackdriverResponse{}, err
|
return StackdriverResponse{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// slog.Info("stackdriver", "response", string(body))
|
|
||||||
|
|
||||||
if res.StatusCode/100 != 2 {
|
if res.StatusCode/100 != 2 {
|
||||||
slog.Error("Request failed", "status", res.Status, "body", string(body))
|
slog.Error("Request failed", "status", res.Status, "body", string(body))
|
||||||
return StackdriverResponse{}, fmt.Errorf(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 {
|
func (e *StackdriverExecutor) parseResponse(queryRes *tsdb.QueryResult, data StackdriverResponse, query *StackdriverQuery) error {
|
||||||
metricLabels := make(map[string][]string)
|
labels := make(map[string]map[string]bool)
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, series := range data.TimeSeries {
|
for _, series := range data.TimeSeries {
|
||||||
points := make([]tsdb.TimePoint, 0)
|
points := make([]tsdb.TimePoint, 0)
|
||||||
|
seriesLabels := make(map[string]string)
|
||||||
defaultMetricName := series.Metric.Type
|
defaultMetricName := series.Metric.Type
|
||||||
if len(resourceTypes) > 1 {
|
labels["resource.type"] = map[string]bool{series.Resource.Type: true}
|
||||||
defaultMetricName += " " + series.Resource.Type
|
|
||||||
}
|
|
||||||
|
|
||||||
for key, value := range series.Metric.Labels {
|
for key, value := range series.Metric.Labels {
|
||||||
if !containsLabel(metricLabels[key], value) {
|
if _, ok := labels["metric.label."+key]; !ok {
|
||||||
metricLabels[key] = append(metricLabels[key], value)
|
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) {
|
if len(query.GroupBys) == 0 || containsLabel(query.GroupBys, "metric.label."+key) {
|
||||||
defaultMetricName += " " + value
|
defaultMetricName += " " + value
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for key, value := range series.Resource.Labels {
|
for key, value := range series.Resource.Labels {
|
||||||
if !containsLabel(resourceLabels[key], value) {
|
if _, ok := labels["resource.label."+key]; !ok {
|
||||||
resourceLabels[key] = append(resourceLabels[key], value)
|
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) {
|
if containsLabel(query.GroupBys, "resource.label."+key) {
|
||||||
defaultMetricName += " " + value
|
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
|
// reverse the order to be ascending
|
||||||
if series.ValueType != "DISTRIBUTION" {
|
if series.ValueType != "DISTRIBUTION" {
|
||||||
for i := len(series.Points) - 1; i >= 0; i-- {
|
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))
|
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{
|
queryRes.Series = append(queryRes.Series, &tsdb.TimeSeries{
|
||||||
Name: metricName,
|
Name: metricName,
|
||||||
@ -437,7 +461,7 @@ func (e *StackdriverExecutor) parseResponse(queryRes *tsdb.QueryResult, data Sta
|
|||||||
bucketBound := calcBucketBound(point.Value.DistributionValue.BucketOptions, i)
|
bucketBound := calcBucketBound(point.Value.DistributionValue.BucketOptions, i)
|
||||||
additionalLabels := map[string]string{"bucket": bucketBound}
|
additionalLabels := map[string]string{"bucket": bucketBound}
|
||||||
buckets[i] = &tsdb.TimeSeries{
|
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),
|
Points: make([]tsdb.TimePoint, 0),
|
||||||
}
|
}
|
||||||
if maxKey < i {
|
if maxKey < i {
|
||||||
@ -453,7 +477,7 @@ func (e *StackdriverExecutor) parseResponse(queryRes *tsdb.QueryResult, data Sta
|
|||||||
bucketBound := calcBucketBound(point.Value.DistributionValue.BucketOptions, i)
|
bucketBound := calcBucketBound(point.Value.DistributionValue.BucketOptions, i)
|
||||||
additionalLabels := map[string]string{"bucket": bucketBound}
|
additionalLabels := map[string]string{"bucket": bucketBound}
|
||||||
buckets[i] = &tsdb.TimeSeries{
|
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),
|
Points: make([]tsdb.TimePoint, 0),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -465,14 +489,23 @@ func (e *StackdriverExecutor) parseResponse(queryRes *tsdb.QueryResult, data Sta
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
queryRes.Meta.Set("resourceLabels", resourceLabels)
|
labelsByKey := make(map[string][]string)
|
||||||
queryRes.Meta.Set("metricLabels", metricLabels)
|
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("groupBys", query.GroupBys)
|
||||||
queryRes.Meta.Set("resourceTypes", resourceTypes)
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func toSnakeCase(str string) string {
|
||||||
|
return strings.ToLower(matchAllCap.ReplaceAllString(str, "${1}_${2}"))
|
||||||
|
}
|
||||||
|
|
||||||
func containsLabel(labels []string, newLabel string) bool {
|
func containsLabel(labels []string, newLabel string) bool {
|
||||||
for _, val := range labels {
|
for _, val := range labels {
|
||||||
if val == newLabel {
|
if val == newLabel {
|
||||||
@ -482,7 +515,7 @@ func containsLabel(labels []string, newLabel string) bool {
|
|||||||
return false
|
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 == "" {
|
if query.AliasBy == "" {
|
||||||
return defaultMetricName
|
return defaultMetricName
|
||||||
}
|
}
|
||||||
@ -496,25 +529,13 @@ func formatLegendKeys(metricType string, defaultMetricName string, resourceType
|
|||||||
return []byte(metricType)
|
return []byte(metricType)
|
||||||
}
|
}
|
||||||
|
|
||||||
if metaPartName == "resource.type" && resourceType != "" {
|
|
||||||
return []byte(resourceType)
|
|
||||||
}
|
|
||||||
|
|
||||||
metricPart := replaceWithMetricPart(metaPartName, metricType)
|
metricPart := replaceWithMetricPart(metaPartName, metricType)
|
||||||
|
|
||||||
if metricPart != nil {
|
if metricPart != nil {
|
||||||
return metricPart
|
return metricPart
|
||||||
}
|
}
|
||||||
|
|
||||||
metaPartName = strings.Replace(metaPartName, "metric.label.", "", 1)
|
if val, exists := labels[metaPartName]; exists {
|
||||||
|
|
||||||
if val, exists := metricLabels[metaPartName]; exists {
|
|
||||||
return []byte(val)
|
|
||||||
}
|
|
||||||
|
|
||||||
metaPartName = strings.Replace(metaPartName, "resource.label.", "", 1)
|
|
||||||
|
|
||||||
if val, exists := resourceLabels[metaPartName]; exists {
|
|
||||||
return []byte(val)
|
return []byte(val)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -533,8 +554,8 @@ func replaceWithMetricPart(metaPartName string, metricType string) []byte {
|
|||||||
shortMatches := metricNameFormat.FindStringSubmatch(metricType)
|
shortMatches := metricNameFormat.FindStringSubmatch(metricType)
|
||||||
|
|
||||||
if metaPartName == "metric.name" {
|
if metaPartName == "metric.name" {
|
||||||
if len(shortMatches) > 0 {
|
if len(shortMatches) > 2 {
|
||||||
return []byte(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() {
|
Convey("Should add meta for labels to the response", func() {
|
||||||
metricLabels := res.Meta.Get("metricLabels").Interface().(map[string][]string)
|
labels := res.Meta.Get("labels").Interface().(map[string][]string)
|
||||||
So(metricLabels, ShouldNotBeNil)
|
So(labels, ShouldNotBeNil)
|
||||||
So(len(metricLabels["instance_name"]), ShouldEqual, 3)
|
So(len(labels["metric.label.instance_name"]), ShouldEqual, 3)
|
||||||
So(metricLabels["instance_name"][0], ShouldEqual, "collector-asia-east-1")
|
So(labels["metric.label.instance_name"], ShouldContain, "collector-asia-east-1")
|
||||||
So(metricLabels["instance_name"][1], ShouldEqual, "collector-europe-west-1")
|
So(labels["metric.label.instance_name"], ShouldContain, "collector-europe-west-1")
|
||||||
So(metricLabels["instance_name"][2], ShouldEqual, "collector-us-east-1")
|
So(labels["metric.label.instance_name"], ShouldContain, "collector-us-east-1")
|
||||||
|
|
||||||
resourceLabels := res.Meta.Get("resourceLabels").Interface().(map[string][]string)
|
So(len(labels["resource.label.zone"]), ShouldEqual, 3)
|
||||||
So(resourceLabels, ShouldNotBeNil)
|
So(labels["resource.label.zone"], ShouldContain, "asia-east1-a")
|
||||||
So(len(resourceLabels["zone"]), ShouldEqual, 3)
|
So(labels["resource.label.zone"], ShouldContain, "europe-west1-b")
|
||||||
So(resourceLabels["zone"][0], ShouldEqual, "asia-east1-a")
|
So(labels["resource.label.zone"], ShouldContain, "us-east1-b")
|
||||||
So(resourceLabels["zone"][1], ShouldEqual, "europe-west1-b")
|
|
||||||
So(resourceLabels["zone"][2], ShouldEqual, "us-east1-b")
|
|
||||||
|
|
||||||
So(len(resourceLabels["project_id"]), ShouldEqual, 1)
|
So(len(labels["resource.label.project_id"]), ShouldEqual, 1)
|
||||||
So(resourceLabels["project_id"][0], ShouldEqual, "grafana-prod")
|
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)
|
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() {
|
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,6 +41,7 @@ type StackdriverResponse struct {
|
|||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
Labels map[string]string `json:"labels"`
|
Labels map[string]string `json:"labels"`
|
||||||
} `json:"resource"`
|
} `json:"resource"`
|
||||||
|
MetaData map[string]map[string]interface{} `json:"metadata"`
|
||||||
MetricKind string `json:"metricKind"`
|
MetricKind string `json:"metricKind"`
|
||||||
ValueType string `json:"valueType"`
|
ValueType string `json:"valueType"`
|
||||||
Points []struct {
|
Points []struct {
|
||||||
|
@ -90,7 +90,6 @@ export function registerAngularDirectives() {
|
|||||||
react2AngularDirective('stackdriverAnnotationQueryEditor', StackdriverAnnotationQueryEditor, [
|
react2AngularDirective('stackdriverAnnotationQueryEditor', StackdriverAnnotationQueryEditor, [
|
||||||
'target',
|
'target',
|
||||||
'onQueryChange',
|
'onQueryChange',
|
||||||
'onExecuteQuery',
|
|
||||||
['datasource', { watchDepth: 'reference' }],
|
['datasource', { watchDepth: 'reference' }],
|
||||||
['templateSrv', { watchDepth: 'reference' }],
|
['templateSrv', { watchDepth: 'reference' }],
|
||||||
]);
|
]);
|
||||||
|
@ -77,16 +77,9 @@ export default class StackdriverMetricFindQuery {
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
const refId = 'handleLabelValuesQuery';
|
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 interpolatedKey = this.datasource.templateSrv.replace(labelKey);
|
||||||
const [name] = interpolatedKey.split('.').reverse();
|
const values = labels.hasOwnProperty(interpolatedKey) ? labels[interpolatedKey] : [];
|
||||||
let values = [];
|
|
||||||
if (response.meta && response.meta.metricLabels && response.meta.metricLabels.hasOwnProperty(name)) {
|
|
||||||
values = response.meta.metricLabels[name];
|
|
||||||
} else if (response.meta && response.meta.resourceLabels && response.meta.resourceLabels.hasOwnProperty(name)) {
|
|
||||||
values = response.meta.resourceLabels[name];
|
|
||||||
}
|
|
||||||
|
|
||||||
return values.map(this.toFindQueryResult);
|
return values.map(this.toFindQueryResult);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -95,8 +88,8 @@ export default class StackdriverMetricFindQuery {
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
const refId = 'handleResourceTypeQueryQueryType';
|
const refId = 'handleResourceTypeQueryQueryType';
|
||||||
const response = await this.datasource.getLabels(selectedMetricType, refId);
|
const labels = await this.datasource.getLabels(selectedMetricType, refId);
|
||||||
return response.meta.resourceTypes ? response.meta.resourceTypes.map(this.toFindQueryResult) : [];
|
return labels['resource.type'].map(this.toFindQueryResult);
|
||||||
}
|
}
|
||||||
|
|
||||||
async handleAlignersQuery({ selectedMetricType }: any) {
|
async handleAlignersQuery({ selectedMetricType }: any) {
|
||||||
|
@ -6,8 +6,7 @@ export class StackdriverAnnotationsQueryCtrl {
|
|||||||
templateSrv: TemplateSrv;
|
templateSrv: TemplateSrv;
|
||||||
|
|
||||||
/** @ngInject */
|
/** @ngInject */
|
||||||
constructor(templateSrv: TemplateSrv) {
|
constructor() {
|
||||||
this.templateSrv = templateSrv;
|
|
||||||
this.annotation.target = this.annotation.target || {};
|
this.annotation.target = this.annotation.target || {};
|
||||||
this.onQueryChange = this.onQueryChange.bind(this);
|
this.onQueryChange = this.onQueryChange.bind(this);
|
||||||
}
|
}
|
||||||
|
@ -16,6 +16,7 @@ const props: Props = {
|
|||||||
crossSeriesReducer: '',
|
crossSeriesReducer: '',
|
||||||
groupBys: [],
|
groupBys: [],
|
||||||
children: renderProps => <div />,
|
children: renderProps => <div />,
|
||||||
|
templateVariableOptions: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
describe('Aggregations', () => {
|
describe('Aggregations', () => {
|
||||||
@ -32,7 +33,7 @@ describe('Aggregations', () => {
|
|||||||
wrapper = shallow(<Aggregations {...newProps} />);
|
wrapper = shallow(<Aggregations {...newProps} />);
|
||||||
});
|
});
|
||||||
it('', () => {
|
it('', () => {
|
||||||
const options = wrapper.state().aggOptions[0].options;
|
const options = wrapper.state().aggOptions;
|
||||||
expect(options.length).toEqual(11);
|
expect(options.length).toEqual(11);
|
||||||
expect(options.map((o: any) => o.value)).toEqual(
|
expect(options.map((o: any) => o.value)).toEqual(
|
||||||
expect.not.arrayContaining(['REDUCE_COUNT_TRUE', 'REDUCE_COUNT_FALSE'])
|
expect.not.arrayContaining(['REDUCE_COUNT_TRUE', 'REDUCE_COUNT_FALSE'])
|
||||||
@ -49,8 +50,7 @@ describe('Aggregations', () => {
|
|||||||
wrapper = shallow(<Aggregations {...newProps} />);
|
wrapper = shallow(<Aggregations {...newProps} />);
|
||||||
});
|
});
|
||||||
it('', () => {
|
it('', () => {
|
||||||
const options = wrapper.state().aggOptions[0].options;
|
const options = wrapper.state().aggOptions;
|
||||||
|
|
||||||
expect(options.length).toEqual(10);
|
expect(options.length).toEqual(10);
|
||||||
expect(options.map((o: any) => o.value)).toEqual(expect.arrayContaining(['REDUCE_NONE']));
|
expect(options.map((o: any) => o.value)).toEqual(expect.arrayContaining(['REDUCE_NONE']));
|
||||||
});
|
});
|
||||||
|
@ -1,14 +1,13 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import _ from 'lodash';
|
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 { getAggregationOptionsByMetric } from '../functions';
|
||||||
import { TemplateSrv } from 'app/features/templating/template_srv';
|
|
||||||
import { ValueTypes, MetricKind } from '../constants';
|
import { ValueTypes, MetricKind } from '../constants';
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
onChange: (metricDescriptor: any) => void;
|
onChange: (metricDescriptor: any) => void;
|
||||||
templateSrv: TemplateSrv;
|
|
||||||
metricDescriptor: {
|
metricDescriptor: {
|
||||||
valueType: string;
|
valueType: string;
|
||||||
metricKind: string;
|
metricKind: string;
|
||||||
@ -16,6 +15,7 @@ export interface Props {
|
|||||||
crossSeriesReducer: string;
|
crossSeriesReducer: string;
|
||||||
groupBys: string[];
|
groupBys: string[];
|
||||||
children?: (renderProps: any) => JSX.Element;
|
children?: (renderProps: any) => JSX.Element;
|
||||||
|
templateVariableOptions: Array<SelectableValue<string>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface State {
|
export interface State {
|
||||||
@ -40,19 +40,13 @@ export class Aggregations extends React.Component<Props, State> {
|
|||||||
setAggOptions({ metricDescriptor }: Props) {
|
setAggOptions({ metricDescriptor }: Props) {
|
||||||
let aggOptions: any[] = [];
|
let aggOptions: any[] = [];
|
||||||
if (metricDescriptor) {
|
if (metricDescriptor) {
|
||||||
aggOptions = [
|
aggOptions = getAggregationOptionsByMetric(
|
||||||
{
|
|
||||||
label: 'Aggregations',
|
|
||||||
expanded: true,
|
|
||||||
options: getAggregationOptionsByMetric(
|
|
||||||
metricDescriptor.valueType as ValueTypes,
|
metricDescriptor.valueType as ValueTypes,
|
||||||
metricDescriptor.metricKind as MetricKind
|
metricDescriptor.metricKind as MetricKind
|
||||||
).map(a => ({
|
).map(a => ({
|
||||||
...a,
|
...a,
|
||||||
label: a.text,
|
label: a.text,
|
||||||
})),
|
}));
|
||||||
},
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
this.setState({ aggOptions });
|
this.setState({ aggOptions });
|
||||||
}
|
}
|
||||||
@ -65,22 +59,28 @@ export class Aggregations extends React.Component<Props, State> {
|
|||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { displayAdvancedOptions, aggOptions } = this.state;
|
const { displayAdvancedOptions, aggOptions } = this.state;
|
||||||
const { templateSrv, onChange, crossSeriesReducer } = this.props;
|
const { templateVariableOptions, onChange, crossSeriesReducer } = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="gf-form-inline">
|
<div className="gf-form-inline">
|
||||||
<div className="gf-form">
|
|
||||||
<label className="gf-form-label query-keyword width-9">Aggregation</label>
|
<label className="gf-form-label query-keyword width-9">Aggregation</label>
|
||||||
<MetricSelect
|
<Segment
|
||||||
onChange={onChange}
|
onChange={({ value }) => onChange(value)}
|
||||||
value={crossSeriesReducer}
|
value={[...aggOptions, ...templateVariableOptions].find(s => s.value === crossSeriesReducer)}
|
||||||
variables={templateSrv.variables}
|
options={[
|
||||||
options={aggOptions}
|
{
|
||||||
|
label: 'Template Variables',
|
||||||
|
options: templateVariableOptions,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Aggregations',
|
||||||
|
expanded: true,
|
||||||
|
options: aggOptions,
|
||||||
|
},
|
||||||
|
]}
|
||||||
placeholder="Select Reducer"
|
placeholder="Select Reducer"
|
||||||
className="width-15"
|
></Segment>
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="gf-form gf-form--grow">
|
<div className="gf-form gf-form--grow">
|
||||||
<label className="gf-form-label gf-form-label--grow">
|
<label className="gf-form-label gf-form-label--grow">
|
||||||
<a onClick={this.onToggleDisplayAdvanced}>
|
<a onClick={this.onToggleDisplayAdvanced}>
|
||||||
|
@ -1,14 +1,16 @@
|
|||||||
import React, { FC } from 'react';
|
import React, { FC } from 'react';
|
||||||
import _ from 'lodash';
|
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 { 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 {
|
export interface Props {
|
||||||
onChange: (alignmentPeriod: any) => void;
|
onChange: (alignmentPeriod: any) => void;
|
||||||
templateSrv: TemplateSrv;
|
templateSrv: TemplateSrv;
|
||||||
|
templateVariableOptions: Array<SelectableValue<string>>;
|
||||||
alignmentPeriod: string;
|
alignmentPeriod: string;
|
||||||
perSeriesAligner: string;
|
perSeriesAligner: string;
|
||||||
usedAlignmentPeriod: string;
|
usedAlignmentPeriod: string;
|
||||||
@ -17,36 +19,38 @@ export interface Props {
|
|||||||
export const AlignmentPeriods: FC<Props> = ({
|
export const AlignmentPeriods: FC<Props> = ({
|
||||||
alignmentPeriod,
|
alignmentPeriod,
|
||||||
templateSrv,
|
templateSrv,
|
||||||
|
templateVariableOptions,
|
||||||
onChange,
|
onChange,
|
||||||
perSeriesAligner,
|
perSeriesAligner,
|
||||||
usedAlignmentPeriod,
|
usedAlignmentPeriod,
|
||||||
}) => {
|
}) => {
|
||||||
const alignment = alignOptions.find(ap => ap.value === templateSrv.replace(perSeriesAligner));
|
const alignment = alignOptions.find(ap => ap.value === templateSrv.replace(perSeriesAligner));
|
||||||
const formatAlignmentText = `${kbn.secondsToHms(usedAlignmentPeriod)} interval (${alignment ? alignment.text : ''})`;
|
const formatAlignmentText = `${kbn.secondsToHms(usedAlignmentPeriod)} interval (${alignment ? alignment.text : ''})`;
|
||||||
|
const options = alignmentPeriods.map(ap => ({
|
||||||
|
...ap,
|
||||||
|
label: ap.text,
|
||||||
|
}));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="gf-form-inline">
|
<div className="gf-form-inline">
|
||||||
<div className="gf-form">
|
|
||||||
<label className="gf-form-label query-keyword width-9">Alignment Period</label>
|
<label className="gf-form-label query-keyword width-9">Alignment Period</label>
|
||||||
<MetricSelect
|
<Segment
|
||||||
onChange={onChange}
|
onChange={({ value }) => onChange(value)}
|
||||||
value={alignmentPeriod}
|
value={[...options, ...templateVariableOptions].find(s => s.value === alignmentPeriod)}
|
||||||
variables={templateSrv.variables}
|
|
||||||
options={[
|
options={[
|
||||||
{
|
{
|
||||||
label: 'Alignment options',
|
label: 'Template Variables',
|
||||||
|
options: templateVariableOptions,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Aggregations',
|
||||||
expanded: true,
|
expanded: true,
|
||||||
options: alignmentPeriods.map(ap => ({
|
options: options,
|
||||||
...ap,
|
|
||||||
label: ap.text,
|
|
||||||
})),
|
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
placeholder="Select Alignment"
|
placeholder="Select Alignment"
|
||||||
className="width-15"
|
></Segment>
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="gf-form gf-form--grow">
|
<div className="gf-form gf-form--grow">
|
||||||
{usedAlignmentPeriod && <label className="gf-form-label gf-form-label--grow">{formatAlignmentText}</label>}
|
{usedAlignmentPeriod && <label className="gf-form-label gf-form-label--grow">{formatAlignmentText}</label>}
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,30 +1,37 @@
|
|||||||
import React, { FC } from 'react';
|
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 { SelectableValue } from '@grafana/data';
|
||||||
|
import { Segment } from '@grafana/ui';
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
onChange: (perSeriesAligner: any) => void;
|
onChange: (perSeriesAligner: any) => void;
|
||||||
templateSrv: TemplateSrv;
|
templateVariableOptions: Array<SelectableValue<string>>;
|
||||||
alignOptions: Array<SelectableValue<string>>;
|
alignOptions: Array<SelectableValue<string>>;
|
||||||
perSeriesAligner: string;
|
perSeriesAligner: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Alignments: FC<Props> = ({ perSeriesAligner, templateSrv, onChange, alignOptions }) => {
|
export const Alignments: FC<Props> = ({ perSeriesAligner, templateVariableOptions, onChange, alignOptions }) => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="gf-form-group">
|
<div className="gf-form-inline">
|
||||||
<div className="gf-form offset-width-9">
|
<div className="gf-form offset-width-9">
|
||||||
<label className="gf-form-label query-keyword width-15">Aligner</label>
|
<label className="gf-form-label query-keyword width-15">Aligner</label>
|
||||||
<MetricSelect
|
<Segment
|
||||||
onChange={onChange}
|
onChange={({ value }) => onChange(value)}
|
||||||
value={perSeriesAligner}
|
value={[...alignOptions, ...templateVariableOptions].find(s => s.value === perSeriesAligner)}
|
||||||
variables={templateSrv.variables}
|
options={[
|
||||||
options={alignOptions}
|
{
|
||||||
|
label: 'Template Variables',
|
||||||
|
options: templateVariableOptions,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Alignment options',
|
||||||
|
expanded: true,
|
||||||
|
options: alignOptions,
|
||||||
|
},
|
||||||
|
]}
|
||||||
placeholder="Select Alignment"
|
placeholder="Select Alignment"
|
||||||
className="width-15"
|
></Segment>
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
@ -2,12 +2,12 @@ import React from 'react';
|
|||||||
import { Input } from '@grafana/ui';
|
import { Input } from '@grafana/ui';
|
||||||
|
|
||||||
import { TemplateSrv } from 'app/features/templating/template_srv';
|
import { TemplateSrv } from 'app/features/templating/template_srv';
|
||||||
|
import { SelectableValue } from '@grafana/data';
|
||||||
|
|
||||||
import StackdriverDatasource from '../datasource';
|
import StackdriverDatasource from '../datasource';
|
||||||
import { Metrics } from './Metrics';
|
import { Metrics, Filters, AnnotationsHelp } from './';
|
||||||
import { Filter } from './Filter';
|
import { toOption } from '../functions';
|
||||||
import { AnnotationTarget, MetricDescriptor } from '../types';
|
import { AnnotationTarget, MetricDescriptor } from '../types';
|
||||||
import { AnnotationsHelp } from './AnnotationsHelp';
|
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
onQueryChange: (target: AnnotationTarget) => void;
|
onQueryChange: (target: AnnotationTarget) => void;
|
||||||
@ -17,6 +17,9 @@ export interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface State extends AnnotationTarget {
|
interface State extends AnnotationTarget {
|
||||||
|
variableOptionGroup: SelectableValue<string>;
|
||||||
|
variableOptions: Array<SelectableValue<string>>;
|
||||||
|
labels: any;
|
||||||
[key: string]: any;
|
[key: string]: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -29,19 +32,32 @@ const DefaultTarget: State = {
|
|||||||
refId: 'annotationQuery',
|
refId: 'annotationQuery',
|
||||||
title: '',
|
title: '',
|
||||||
text: '',
|
text: '',
|
||||||
|
labels: {},
|
||||||
|
variableOptionGroup: {},
|
||||||
|
variableOptions: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
export class AnnotationQueryEditor extends React.Component<Props, State> {
|
export class AnnotationQueryEditor extends React.Component<Props, State> {
|
||||||
state: State = DefaultTarget;
|
state: State = DefaultTarget;
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
|
const { target, datasource } = this.props;
|
||||||
|
const variableOptionGroup = {
|
||||||
|
label: 'Template Variables',
|
||||||
|
options: datasource.variables.map(toOption),
|
||||||
|
};
|
||||||
|
|
||||||
this.setState({
|
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) => {
|
onMetricTypeChange = ({ valueType, metricKind, type, unit }: MetricDescriptor) => {
|
||||||
const { onQueryChange } = this.props;
|
const { onQueryChange, datasource } = this.props;
|
||||||
this.setState(
|
this.setState(
|
||||||
{
|
{
|
||||||
metricType: type,
|
metricType: type,
|
||||||
@ -53,6 +69,7 @@ export class AnnotationQueryEditor extends React.Component<Props, State> {
|
|||||||
onQueryChange(this.state);
|
onQueryChange(this.state);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
datasource.getLabels(type, this.state.refId).then(labels => this.setState({ labels }));
|
||||||
};
|
};
|
||||||
|
|
||||||
onChange(prop: string, value: string | string[]) {
|
onChange(prop: string, value: string | string[]) {
|
||||||
@ -62,28 +79,35 @@ export class AnnotationQueryEditor extends React.Component<Props, State> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { defaultProject, metricType, filters, refId, title, text } = this.state;
|
const {
|
||||||
const { datasource, templateSrv } = this.props;
|
defaultProject,
|
||||||
|
metricType,
|
||||||
|
filters,
|
||||||
|
title,
|
||||||
|
text,
|
||||||
|
variableOptionGroup,
|
||||||
|
labels,
|
||||||
|
variableOptions,
|
||||||
|
} = this.state;
|
||||||
|
const { datasource } = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Metrics
|
<Metrics
|
||||||
defaultProject={defaultProject}
|
defaultProject={defaultProject}
|
||||||
metricType={metricType}
|
metricType={metricType}
|
||||||
templateSrv={templateSrv}
|
templateSrv={datasource.templateSrv}
|
||||||
datasource={datasource}
|
datasource={datasource}
|
||||||
onChange={this.onMetricTypeChange}
|
templateVariableOptions={variableOptions}
|
||||||
|
onChange={metric => this.onMetricTypeChange(metric)}
|
||||||
>
|
>
|
||||||
{metric => (
|
{metric => (
|
||||||
<>
|
<>
|
||||||
<Filter
|
<Filters
|
||||||
filtersChanged={value => this.onChange('filters', value)}
|
labels={labels}
|
||||||
filters={filters}
|
filters={filters}
|
||||||
refId={refId}
|
onChange={value => this.onChange('filters', value)}
|
||||||
hideGroupBys={true}
|
variableOptionGroup={variableOptionGroup}
|
||||||
templateSrv={templateSrv}
|
|
||||||
datasource={datasource}
|
|
||||||
metricType={metric ? metric.type : ''}
|
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
@ -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>
|
<li>
|
||||||
<code>{`${'{{resource.label.label_name}}'}`}</code> = Resource label metadata e.g. resource.label.zone
|
<code>{`${'{{resource.label.label_name}}'}`}</code> = Resource label metadata e.g. resource.label.zone
|
||||||
</li>
|
</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>
|
<li>
|
||||||
<code>{`${'{{bucket}}'}`}</code> = bucket boundary for distribution metrics when using a heatmap in
|
<code>{`${'{{bucket}}'}`}</code> = bucket boundary for distribution metrics when using a heatmap in
|
||||||
Grafana
|
Grafana
|
||||||
|
@ -1,16 +1,18 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
|
|
||||||
|
import { TemplateSrv } from 'app/features/templating/template_srv';
|
||||||
|
import { SelectableValue } from '@grafana/data';
|
||||||
import StackdriverDatasource from '../datasource';
|
import StackdriverDatasource from '../datasource';
|
||||||
import appEvents from 'app/core/app_events';
|
import appEvents from 'app/core/app_events';
|
||||||
|
import { Segment } from '@grafana/ui';
|
||||||
import { MetricDescriptor } from '../types';
|
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';
|
import { CoreEvents } from 'app/types';
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
onChange: (metricDescriptor: MetricDescriptor) => void;
|
onChange: (metricDescriptor: MetricDescriptor) => void;
|
||||||
templateSrv: TemplateSrv;
|
templateSrv: TemplateSrv;
|
||||||
|
templateVariableOptions: Array<SelectableValue<string>>;
|
||||||
datasource: StackdriverDatasource;
|
datasource: StackdriverDatasource;
|
||||||
defaultProject: string;
|
defaultProject: string;
|
||||||
metricType: string;
|
metricType: string;
|
||||||
@ -104,9 +106,9 @@ export class Metrics extends React.Component<Props, State> {
|
|||||||
return metricsByService;
|
return metricsByService;
|
||||||
}
|
}
|
||||||
|
|
||||||
onServiceChange = (service: any) => {
|
onServiceChange = ({ value: service }: any) => {
|
||||||
const { metricDescriptors } = this.state;
|
const { metricDescriptors } = this.state;
|
||||||
const { templateSrv, metricType } = this.props;
|
const { metricType, templateSrv } = this.props;
|
||||||
|
|
||||||
const metrics = metricDescriptors
|
const metrics = metricDescriptors
|
||||||
.filter(m => m.service === templateSrv.replace(service))
|
.filter(m => m.service === templateSrv.replace(service))
|
||||||
@ -120,11 +122,11 @@ export class Metrics extends React.Component<Props, State> {
|
|||||||
this.setState({ service, metrics });
|
this.setState({ service, metrics });
|
||||||
|
|
||||||
if (metrics.length > 0 && !metrics.some(m => m.value === templateSrv.replace(metricType))) {
|
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);
|
const metricDescriptor = this.getSelectedMetricDescriptor(value);
|
||||||
this.setState({ metricDescriptor });
|
this.setState({ metricDescriptor });
|
||||||
this.props.onChange({ ...metricDescriptor, type: value });
|
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) : [];
|
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() {
|
render() {
|
||||||
const { services, service, metrics } = this.state;
|
const { services, service, metrics } = this.state;
|
||||||
const { metricType, templateSrv } = this.props;
|
const { metricType, templateVariableOptions } = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="gf-form-inline">
|
<div className="gf-form-inline">
|
||||||
<div className="gf-form">
|
|
||||||
<span className="gf-form-label width-9 query-keyword">Service</span>
|
<span className="gf-form-label width-9 query-keyword">Service</span>
|
||||||
<MetricSelect
|
<Segment
|
||||||
onChange={this.onServiceChange}
|
onChange={this.onServiceChange}
|
||||||
value={service}
|
value={[...services, ...templateVariableOptions].find(s => s.value === service)}
|
||||||
options={services}
|
options={[
|
||||||
|
{
|
||||||
|
label: 'Template Variables',
|
||||||
|
options: templateVariableOptions,
|
||||||
|
},
|
||||||
|
...services,
|
||||||
|
]}
|
||||||
placeholder="Select Services"
|
placeholder="Select Services"
|
||||||
className="width-15"
|
></Segment>
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="gf-form gf-form--grow">
|
<div className="gf-form gf-form--grow">
|
||||||
<div className="gf-form-label gf-form-label--grow" />
|
<div className="gf-form-label gf-form-label--grow" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="gf-form-inline">
|
<div className="gf-form-inline">
|
||||||
<div className="gf-form">
|
|
||||||
<span className="gf-form-label width-9 query-keyword">Metric</span>
|
<span className="gf-form-label width-9 query-keyword">Metric</span>
|
||||||
<MetricSelect
|
|
||||||
|
<Segment
|
||||||
|
className="query-part"
|
||||||
onChange={this.onMetricTypeChange}
|
onChange={this.onMetricTypeChange}
|
||||||
value={metricType}
|
value={[...metrics, ...templateVariableOptions].find(s => s.value === metricType)}
|
||||||
variables={templateSrv.variables}
|
|
||||||
options={[
|
options={[
|
||||||
{
|
{
|
||||||
label: 'Metrics',
|
label: 'Template Variables',
|
||||||
expanded: true,
|
options: templateVariableOptions,
|
||||||
options: metrics,
|
|
||||||
},
|
},
|
||||||
|
...metrics,
|
||||||
]}
|
]}
|
||||||
placeholder="Select Metric"
|
placeholder="Select Metric"
|
||||||
className="width-26"
|
></Segment>
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="gf-form gf-form--grow">
|
<div className="gf-form gf-form--grow">
|
||||||
<div className="gf-form-label gf-form-label--grow" />
|
<div className="gf-form-label gf-form-label--grow" />
|
||||||
</div>
|
</div>
|
||||||
|
@ -11,6 +11,8 @@ const props: Props = {
|
|||||||
datasource: {
|
datasource: {
|
||||||
getDefaultProject: () => Promise.resolve('project'),
|
getDefaultProject: () => Promise.resolve('project'),
|
||||||
getMetricTypes: () => Promise.resolve([]),
|
getMetricTypes: () => Promise.resolve([]),
|
||||||
|
getLabels: () => Promise.resolve([]),
|
||||||
|
variables: [],
|
||||||
} as any,
|
} as any,
|
||||||
templateSrv: new TemplateSrv(),
|
templateSrv: new TemplateSrv(),
|
||||||
};
|
};
|
||||||
|
@ -3,15 +3,9 @@ import _ from 'lodash';
|
|||||||
|
|
||||||
import { TemplateSrv } from 'app/features/templating/template_srv';
|
import { TemplateSrv } from 'app/features/templating/template_srv';
|
||||||
|
|
||||||
import { Metrics } from './Metrics';
|
import { Aggregations, Metrics, Filters, GroupBys, Alignments, AlignmentPeriods, AliasBy, Help } from './';
|
||||||
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 { StackdriverQuery, MetricDescriptor } from '../types';
|
import { StackdriverQuery, MetricDescriptor } from '../types';
|
||||||
import { getAlignmentPickerData } from '../functions';
|
import { getAlignmentPickerData, toOption } from '../functions';
|
||||||
import StackdriverDatasource from '../datasource';
|
import StackdriverDatasource from '../datasource';
|
||||||
import { TimeSeries, SelectableValue } from '@grafana/data';
|
import { TimeSeries, SelectableValue } from '@grafana/data';
|
||||||
import { PanelEvents } from '@grafana/data';
|
import { PanelEvents } from '@grafana/data';
|
||||||
@ -26,9 +20,12 @@ export interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface State extends StackdriverQuery {
|
interface State extends StackdriverQuery {
|
||||||
|
variableOptions: Array<SelectableValue<string>>;
|
||||||
|
variableOptionGroup: SelectableValue<string>;
|
||||||
alignOptions: Array<SelectableValue<string>>;
|
alignOptions: Array<SelectableValue<string>>;
|
||||||
lastQuery: string;
|
lastQuery: string;
|
||||||
lastQueryError: string;
|
lastQueryError: string;
|
||||||
|
labels: any;
|
||||||
[key: string]: any;
|
[key: string]: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -45,26 +42,39 @@ export const DefaultTarget: State = {
|
|||||||
perSeriesAligner: 'ALIGN_MEAN',
|
perSeriesAligner: 'ALIGN_MEAN',
|
||||||
groupBys: [],
|
groupBys: [],
|
||||||
filters: [],
|
filters: [],
|
||||||
|
filter: [],
|
||||||
aliasBy: '',
|
aliasBy: '',
|
||||||
alignOptions: [],
|
alignOptions: [],
|
||||||
lastQuery: '',
|
lastQuery: '',
|
||||||
lastQueryError: '',
|
lastQueryError: '',
|
||||||
usedAlignmentPeriod: '',
|
usedAlignmentPeriod: '',
|
||||||
|
labels: {},
|
||||||
|
variableOptionGroup: {},
|
||||||
|
variableOptions: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
export class QueryEditor extends React.Component<Props, State> {
|
export class QueryEditor extends React.Component<Props, State> {
|
||||||
state: State = DefaultTarget;
|
state: State = DefaultTarget;
|
||||||
|
|
||||||
componentDidMount() {
|
async componentDidMount() {
|
||||||
const { events, target, templateSrv } = this.props;
|
const { events, target, templateSrv, datasource } = this.props;
|
||||||
events.on(PanelEvents.dataReceived, this.onDataReceived.bind(this));
|
events.on(PanelEvents.dataReceived, this.onDataReceived.bind(this));
|
||||||
events.on(PanelEvents.dataError, this.onDataError.bind(this));
|
events.on(PanelEvents.dataError, this.onDataError.bind(this));
|
||||||
const { perSeriesAligner, alignOptions } = getAlignmentPickerData(target, templateSrv);
|
const { perSeriesAligner, alignOptions } = getAlignmentPickerData(target, templateSrv);
|
||||||
|
const variableOptionGroup = {
|
||||||
|
label: 'Template Variables',
|
||||||
|
expanded: false,
|
||||||
|
options: datasource.variables.map(toOption),
|
||||||
|
};
|
||||||
this.setState({
|
this.setState({
|
||||||
...this.props.target,
|
...this.props.target,
|
||||||
alignOptions,
|
alignOptions,
|
||||||
perSeriesAligner,
|
perSeriesAligner,
|
||||||
|
variableOptionGroup,
|
||||||
|
variableOptions: variableOptionGroup.options,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
datasource.getLabels(target.metricType, target.refId, target.groupBys).then(labels => this.setState({ labels }));
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
@ -102,12 +112,13 @@ export class QueryEditor extends React.Component<Props, State> {
|
|||||||
this.setState({ lastQuery, lastQueryError });
|
this.setState({ lastQuery, lastQueryError });
|
||||||
}
|
}
|
||||||
|
|
||||||
onMetricTypeChange = ({ valueType, metricKind, type, unit }: MetricDescriptor) => {
|
onMetricTypeChange = async ({ valueType, metricKind, type, unit }: MetricDescriptor) => {
|
||||||
const { templateSrv, onQueryChange, onExecuteQuery } = this.props;
|
const { templateSrv, onQueryChange, onExecuteQuery, target } = this.props;
|
||||||
const { perSeriesAligner, alignOptions } = getAlignmentPickerData(
|
const { perSeriesAligner, alignOptions } = getAlignmentPickerData(
|
||||||
{ valueType, metricKind, perSeriesAligner: this.state.perSeriesAligner },
|
{ valueType, metricKind, perSeriesAligner: this.state.perSeriesAligner },
|
||||||
templateSrv
|
templateSrv
|
||||||
);
|
);
|
||||||
|
const labels = await this.props.datasource.getLabels(type, target.refId, target.groupBys);
|
||||||
this.setState(
|
this.setState(
|
||||||
{
|
{
|
||||||
alignOptions,
|
alignOptions,
|
||||||
@ -116,6 +127,7 @@ export class QueryEditor extends React.Component<Props, State> {
|
|||||||
unit,
|
unit,
|
||||||
valueType,
|
valueType,
|
||||||
metricKind,
|
metricKind,
|
||||||
|
labels,
|
||||||
},
|
},
|
||||||
() => {
|
() => {
|
||||||
onQueryChange(this.state);
|
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[]) {
|
onPropertyChange(prop: string, value: string[]) {
|
||||||
this.setState({ [prop]: value }, () => {
|
this.setState({ [prop]: value }, () => {
|
||||||
this.props.onQueryChange(this.state);
|
this.props.onQueryChange(this.state);
|
||||||
@ -145,35 +166,39 @@ export class QueryEditor extends React.Component<Props, State> {
|
|||||||
aliasBy,
|
aliasBy,
|
||||||
lastQuery,
|
lastQuery,
|
||||||
lastQueryError,
|
lastQueryError,
|
||||||
refId,
|
labels,
|
||||||
|
variableOptionGroup,
|
||||||
|
variableOptions,
|
||||||
} = this.state;
|
} = this.state;
|
||||||
const { datasource, templateSrv } = this.props;
|
const { datasource, templateSrv } = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Metrics
|
<Metrics
|
||||||
|
templateSrv={templateSrv}
|
||||||
defaultProject={defaultProject}
|
defaultProject={defaultProject}
|
||||||
metricType={metricType}
|
metricType={metricType}
|
||||||
templateSrv={templateSrv}
|
templateVariableOptions={variableOptions}
|
||||||
datasource={datasource}
|
datasource={datasource}
|
||||||
onChange={this.onMetricTypeChange}
|
onChange={this.onMetricTypeChange}
|
||||||
>
|
>
|
||||||
{metric => (
|
{metric => (
|
||||||
<>
|
<>
|
||||||
<Filter
|
<Filters
|
||||||
filtersChanged={value => this.onPropertyChange('filters', value)}
|
labels={labels}
|
||||||
groupBysChanged={value => this.onPropertyChange('groupBys', value)}
|
|
||||||
filters={filters}
|
filters={filters}
|
||||||
groupBys={groupBys}
|
onChange={value => this.onPropertyChange('filters', value)}
|
||||||
refId={refId}
|
variableOptionGroup={variableOptionGroup}
|
||||||
hideGroupBys={false}
|
/>
|
||||||
templateSrv={templateSrv}
|
<GroupBys
|
||||||
datasource={datasource}
|
groupBys={Object.keys(labels)}
|
||||||
metricType={metric ? metric.type : ''}
|
values={groupBys}
|
||||||
|
onChange={this.onGroupBysChange.bind(this)}
|
||||||
|
variableOptionGroup={variableOptionGroup}
|
||||||
/>
|
/>
|
||||||
<Aggregations
|
<Aggregations
|
||||||
metricDescriptor={metric}
|
metricDescriptor={metric}
|
||||||
templateSrv={templateSrv}
|
templateVariableOptions={variableOptions}
|
||||||
crossSeriesReducer={crossSeriesReducer}
|
crossSeriesReducer={crossSeriesReducer}
|
||||||
groupBys={groupBys}
|
groupBys={groupBys}
|
||||||
onChange={value => this.onPropertyChange('crossSeriesReducer', value)}
|
onChange={value => this.onPropertyChange('crossSeriesReducer', value)}
|
||||||
@ -182,7 +207,7 @@ export class QueryEditor extends React.Component<Props, State> {
|
|||||||
displayAdvancedOptions && (
|
displayAdvancedOptions && (
|
||||||
<Alignments
|
<Alignments
|
||||||
alignOptions={alignOptions}
|
alignOptions={alignOptions}
|
||||||
templateSrv={templateSrv}
|
templateVariableOptions={variableOptions}
|
||||||
perSeriesAligner={perSeriesAligner}
|
perSeriesAligner={perSeriesAligner}
|
||||||
onChange={value => this.onPropertyChange('perSeriesAligner', value)}
|
onChange={value => this.onPropertyChange('perSeriesAligner', value)}
|
||||||
/>
|
/>
|
||||||
@ -191,6 +216,7 @@ export class QueryEditor extends React.Component<Props, State> {
|
|||||||
</Aggregations>
|
</Aggregations>
|
||||||
<AlignmentPeriods
|
<AlignmentPeriods
|
||||||
templateSrv={templateSrv}
|
templateSrv={templateSrv}
|
||||||
|
templateVariableOptions={variableOptions}
|
||||||
alignmentPeriod={alignmentPeriod}
|
alignmentPeriod={alignmentPeriod}
|
||||||
perSeriesAligner={perSeriesAligner}
|
perSeriesAligner={perSeriesAligner}
|
||||||
usedAlignmentPeriod={usedAlignmentPeriod}
|
usedAlignmentPeriod={usedAlignmentPeriod}
|
||||||
|
@ -7,7 +7,7 @@ interface Props {
|
|||||||
label: string;
|
label: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const SimpleSelect: FC<Props> = props => {
|
export const SimpleSelect: FC<Props> = props => {
|
||||||
const { label, onValueChange, value, options } = props;
|
const { label, onValueChange, value, options } = props;
|
||||||
return (
|
return (
|
||||||
<div className="gf-form max-width-21">
|
<div className="gf-form max-width-21">
|
||||||
@ -24,5 +24,3 @@ const SimpleSelect: FC<Props> = props => {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default SimpleSelect;
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import React, { ChangeEvent, PureComponent } from 'react';
|
import React, { ChangeEvent, PureComponent } from 'react';
|
||||||
import { VariableQueryProps } from 'app/types/plugins';
|
import { VariableQueryProps } from 'app/types/plugins';
|
||||||
import SimpleSelect from './SimpleSelect';
|
import { SimpleSelect } from './';
|
||||||
import { getMetricTypes, getLabelKeys, extractServicesFromMetricDescriptors } from '../functions';
|
import { getMetricTypes, getLabelKeys, extractServicesFromMetricDescriptors } from '../functions';
|
||||||
import { MetricFindQueryTypes, VariableQueryData } from '../types';
|
import { MetricFindQueryTypes, VariableQueryData } from '../types';
|
||||||
|
|
||||||
|
@ -4,100 +4,21 @@ exports[`Aggregations renders correctly 1`] = `
|
|||||||
Array [
|
Array [
|
||||||
<div
|
<div
|
||||||
className="gf-form-inline"
|
className="gf-form-inline"
|
||||||
>
|
|
||||||
<div
|
|
||||||
className="gf-form"
|
|
||||||
>
|
>
|
||||||
<label
|
<label
|
||||||
className="gf-form-label query-keyword width-9"
|
className="gf-form-label query-keyword width-9"
|
||||||
>
|
>
|
||||||
Aggregation
|
Aggregation
|
||||||
</label>
|
</label>
|
||||||
<div>
|
|
||||||
<div
|
<div
|
||||||
className="css-0 gf-form-input gf-form-input--form-dropdown width-15"
|
className="gf-form"
|
||||||
onKeyDown={[Function]}
|
onClick={[Function]}
|
||||||
>
|
>
|
||||||
<div
|
<a
|
||||||
className="css-0 gf-form-select-box__control"
|
className="gf-form-label query-part query-placeholder"
|
||||||
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
|
Select Reducer
|
||||||
</div>
|
</a>
|
||||||
<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>
|
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className="gf-form gf-form--grow"
|
className="gf-form gf-form--grow"
|
||||||
|
@ -4,100 +4,21 @@ exports[`QueryEditor renders correctly 1`] = `
|
|||||||
Array [
|
Array [
|
||||||
<div
|
<div
|
||||||
className="gf-form-inline"
|
className="gf-form-inline"
|
||||||
>
|
|
||||||
<div
|
|
||||||
className="gf-form"
|
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
className="gf-form-label width-9 query-keyword"
|
className="gf-form-label width-9 query-keyword"
|
||||||
>
|
>
|
||||||
Service
|
Service
|
||||||
</span>
|
</span>
|
||||||
<div>
|
|
||||||
<div
|
<div
|
||||||
className="css-0 gf-form-input gf-form-input--form-dropdown width-15"
|
className="gf-form"
|
||||||
onKeyDown={[Function]}
|
onClick={[Function]}
|
||||||
>
|
>
|
||||||
<div
|
<a
|
||||||
className="css-0 gf-form-select-box__control"
|
className="gf-form-label query-part query-placeholder"
|
||||||
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
|
Select Services
|
||||||
</div>
|
</a>
|
||||||
<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>
|
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className="gf-form gf-form--grow"
|
className="gf-form gf-form--grow"
|
||||||
@ -109,100 +30,21 @@ Array [
|
|||||||
</div>,
|
</div>,
|
||||||
<div
|
<div
|
||||||
className="gf-form-inline"
|
className="gf-form-inline"
|
||||||
>
|
|
||||||
<div
|
|
||||||
className="gf-form"
|
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
className="gf-form-label width-9 query-keyword"
|
className="gf-form-label width-9 query-keyword"
|
||||||
>
|
>
|
||||||
Metric
|
Metric
|
||||||
</span>
|
</span>
|
||||||
<div>
|
|
||||||
<div
|
<div
|
||||||
className="css-0 gf-form-input gf-form-input--form-dropdown width-26"
|
className="gf-form"
|
||||||
onKeyDown={[Function]}
|
onClick={[Function]}
|
||||||
>
|
>
|
||||||
<div
|
<a
|
||||||
className="css-0 gf-form-select-box__control"
|
className="gf-form-label query-part query-placeholder query-part"
|
||||||
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
|
Select Metric
|
||||||
</div>
|
</a>
|
||||||
<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>
|
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className="gf-form gf-form--grow"
|
className="gf-form gf-form--grow"
|
||||||
@ -213,108 +55,66 @@ Array [
|
|||||||
</div>
|
</div>
|
||||||
</div>,
|
</div>,
|
||||||
<div
|
<div
|
||||||
style={
|
className="gf-form-inline"
|
||||||
Object {
|
>
|
||||||
"width": "100%",
|
<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 gf-form-label--grow"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>,
|
||||||
<div
|
<div
|
||||||
className="gf-form-inline"
|
className="gf-form-inline"
|
||||||
>
|
>
|
||||||
|
<label
|
||||||
|
className="gf-form-label query-keyword width-9"
|
||||||
|
>
|
||||||
|
Group By
|
||||||
|
</label>
|
||||||
<div
|
<div
|
||||||
className="gf-form"
|
className="gf-form gf-form--grow"
|
||||||
|
>
|
||||||
|
<label
|
||||||
|
className="gf-form-label gf-form-label--grow"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>,
|
||||||
|
<div
|
||||||
|
className="gf-form-inline"
|
||||||
>
|
>
|
||||||
<label
|
<label
|
||||||
className="gf-form-label query-keyword width-9"
|
className="gf-form-label query-keyword width-9"
|
||||||
>
|
>
|
||||||
Aggregation
|
Aggregation
|
||||||
</label>
|
</label>
|
||||||
<div>
|
|
||||||
<div
|
<div
|
||||||
className="css-0 gf-form-input gf-form-input--form-dropdown width-15"
|
className="gf-form"
|
||||||
onKeyDown={[Function]}
|
onClick={[Function]}
|
||||||
>
|
>
|
||||||
<div
|
<a
|
||||||
className="css-0 gf-form-select-box__control"
|
className="gf-form-label query-part query-placeholder"
|
||||||
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
|
Select Reducer
|
||||||
</div>
|
</a>
|
||||||
<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>
|
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className="gf-form gf-form--grow"
|
className="gf-form gf-form--grow"
|
||||||
@ -335,104 +135,21 @@ Array [
|
|||||||
</div>,
|
</div>,
|
||||||
<div
|
<div
|
||||||
className="gf-form-inline"
|
className="gf-form-inline"
|
||||||
>
|
|
||||||
<div
|
|
||||||
className="gf-form"
|
|
||||||
>
|
>
|
||||||
<label
|
<label
|
||||||
className="gf-form-label query-keyword width-9"
|
className="gf-form-label query-keyword width-9"
|
||||||
>
|
>
|
||||||
Alignment Period
|
Alignment Period
|
||||||
</label>
|
</label>
|
||||||
<div>
|
|
||||||
<div
|
<div
|
||||||
className="css-0 gf-form-input gf-form-input--form-dropdown width-15"
|
className="gf-form"
|
||||||
onKeyDown={[Function]}
|
onClick={[Function]}
|
||||||
>
|
>
|
||||||
<div
|
<a
|
||||||
className="css-0 gf-form-select-box__control"
|
className="gf-form-label query-part"
|
||||||
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
|
stackdriver auto
|
||||||
</div>
|
</a>
|
||||||
</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>
|
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className="gf-form gf-form--grow"
|
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',
|
'By/s': 'Bps',
|
||||||
GBy: 'decgbytes',
|
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 appEvents from 'app/core/app_events';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import StackdriverMetricFindQuery from './StackdriverMetricFindQuery';
|
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 { DataSourceApi, DataQueryRequest, DataSourceInstanceSettings, ScopedVars } from '@grafana/data';
|
||||||
import { BackendSrv } from 'app/core/services/backend_srv';
|
import { BackendSrv } from 'app/core/services/backend_srv';
|
||||||
import { TemplateSrv } from 'app/features/templating/template_srv';
|
import { TemplateSrv } from 'app/features/templating/template_srv';
|
||||||
@ -21,7 +21,7 @@ export default class StackdriverDatasource extends DataSourceApi<StackdriverQuer
|
|||||||
constructor(
|
constructor(
|
||||||
instanceSettings: DataSourceInstanceSettings<StackdriverOptions>,
|
instanceSettings: DataSourceInstanceSettings<StackdriverOptions>,
|
||||||
private backendSrv: BackendSrv,
|
private backendSrv: BackendSrv,
|
||||||
private templateSrv: TemplateSrv,
|
public templateSrv: TemplateSrv,
|
||||||
private timeSrv: TimeSrv
|
private timeSrv: TimeSrv
|
||||||
) {
|
) {
|
||||||
super(instanceSettings);
|
super(instanceSettings);
|
||||||
@ -32,6 +32,10 @@ export default class StackdriverDatasource extends DataSourceApi<StackdriverQuer
|
|||||||
this.metricTypes = [];
|
this.metricTypes = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get variables() {
|
||||||
|
return this.templateSrv.variables.map(v => `$${v.name}`);
|
||||||
|
}
|
||||||
|
|
||||||
async getTimeSeries(options: any) {
|
async getTimeSeries(options: any) {
|
||||||
const queries = options.targets
|
const queries = options.targets
|
||||||
.filter((target: any) => {
|
.filter((target: any) => {
|
||||||
@ -71,26 +75,40 @@ export default class StackdriverDatasource extends DataSourceApi<StackdriverQuer
|
|||||||
}
|
}
|
||||||
|
|
||||||
interpolateFilters(filters: string[], scopedVars: ScopedVars) {
|
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');
|
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({
|
const response = await this.getTimeSeries({
|
||||||
targets: [
|
targets: [
|
||||||
{
|
{
|
||||||
refId: refId,
|
refId: refId,
|
||||||
datasourceId: this.id,
|
datasourceId: this.id,
|
||||||
metricType: this.templateSrv.replace(metricType),
|
metricType: this.templateSrv.replace(metricType),
|
||||||
|
groupBys: this.interpolateGroupBys(groupBys || [], {}),
|
||||||
crossSeriesReducer: 'REDUCE_NONE',
|
crossSeriesReducer: 'REDUCE_NONE',
|
||||||
view: 'HEADERS',
|
view: 'HEADERS',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
range: this.timeSrv.timeRange(),
|
range: this.timeSrv.timeRange(),
|
||||||
});
|
});
|
||||||
|
const result = response.results[refId];
|
||||||
return response.results[refId];
|
return result && result.meta ? result.meta.labels : {};
|
||||||
}
|
}
|
||||||
|
|
||||||
interpolateGroupBys(groupBys: string[], scopedVars: {}): string[] {
|
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 || {}),
|
text: this.templateSrv.replace(annotation.target.text, options.scopedVars || {}),
|
||||||
tags: this.templateSrv.replace(annotation.target.tags, options.scopedVars || {}),
|
tags: this.templateSrv.replace(annotation.target.tags, options.scopedVars || {}),
|
||||||
view: 'FULL',
|
view: 'FULL',
|
||||||
filters: (annotation.target.filters || []).map((f: any) => {
|
filters: this.interpolateFilters(annotation.target.filters || [], options.scopedVars),
|
||||||
return this.templateSrv.replace(f, options.scopedVars || {});
|
|
||||||
}),
|
|
||||||
type: 'annotationQuery',
|
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 _ from 'lodash';
|
||||||
import { alignOptions, aggOptions, ValueTypes, MetricKind } from './constants';
|
import { alignOptions, aggOptions, ValueTypes, MetricKind, systemLabels } from './constants';
|
||||||
|
import { SelectableValue } from '@grafana/data';
|
||||||
import { TemplateSrv } from 'app/features/templating/template_srv';
|
import { TemplateSrv } from 'app/features/templating/template_srv';
|
||||||
import { StackdriverQuery } from './types';
|
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) =>
|
export const getMetricTypesByService = (metricDescriptors: any, service: any) =>
|
||||||
metricDescriptors.filter((m: any) => m.service === service);
|
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) => {
|
export const getLabelKeys = async (datasource: any, selectedMetricType: any) => {
|
||||||
const refId = 'handleLabelKeysQuery';
|
const refId = 'handleLabelKeysQuery';
|
||||||
const response = await datasource.getLabels(selectedMetricType, refId);
|
const labels = await datasource.getLabels(selectedMetricType, refId);
|
||||||
const labelKeys = response.meta
|
return [...Object.keys(labels), ...systemLabels];
|
||||||
? [
|
|
||||||
...Object.keys(response.meta.resourceLabels).map(l => `resource.label.${l}`),
|
|
||||||
...Object.keys(response.meta.metricLabels).map(l => `metric.label.${l}`),
|
|
||||||
]
|
|
||||||
: [];
|
|
||||||
return labelKeys;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getAlignmentPickerData = (
|
export const getAlignmentPickerData = (
|
||||||
{ valueType, metricKind, perSeriesAligner }: Partial<StackdriverQuery>,
|
{ valueType, metricKind, perSeriesAligner }: Partial<StackdriverQuery>,
|
||||||
templateSrv: TemplateSrv
|
templateSrv: TemplateSrv
|
||||||
) => {
|
) => {
|
||||||
const options = getAlignmentOptionsByMetric(valueType, metricKind).map(option => ({
|
const alignOptions = getAlignmentOptionsByMetric(valueType, metricKind).map(option => ({
|
||||||
...option,
|
...option,
|
||||||
label: option.text,
|
label: option.text,
|
||||||
}));
|
}));
|
||||||
const alignOptions = [
|
if (!alignOptions.some(o => o.value === templateSrv.replace(perSeriesAligner))) {
|
||||||
{
|
perSeriesAligner = alignOptions.length > 0 ? alignOptions[0].value : '';
|
||||||
label: 'Alignment options',
|
|
||||||
expanded: true,
|
|
||||||
options,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
if (!options.some(o => o.value === templateSrv.replace(perSeriesAligner))) {
|
|
||||||
perSeriesAligner = options.length > 0 ? options[0].value : '';
|
|
||||||
}
|
}
|
||||||
return { alignOptions, perSeriesAligner };
|
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', () => {
|
it('should replace the variable with the value', () => {
|
||||||
expect(interpolated.length).toBe(3);
|
expect(interpolated.length).toBe(4);
|
||||||
expect(interpolated[2]).toBe('filtervalue1');
|
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;
|
type: string;
|
||||||
value: string;
|
value: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface Filter {
|
||||||
|
key: string;
|
||||||
|
operator: string;
|
||||||
|
value: string;
|
||||||
|
condition: string;
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user