mirror of
https://github.com/grafana/grafana.git
synced 2025-01-16 19:52:33 -06:00
CloudMonitoring: Add support for preprocessing (#33011)
* add support for handling preprocessors in the backend * add preprocessor tests * use uppercase for constants * add super label component * remove error message from query editor since its not working (probably cause onDataError doesnt work anymore) * use cheat sheet instead of help * add return type annotation for projects * add support for preprocessing. replace segment comp with select. change components names and refactoring * cleanup * more pr feedback * fix annotations editor * rename aggregation component * fix broken test * remove unnecessary cast * fix strict errors * fix more strict errors * remove not used prop * update docs * use same inline label for annotation editor * fix react prop warning * disable preprocessing for distribution types * using new default values for reducer * auto select 'rate' if metric kind is not gauge * fix create label format * pr feedback * more pr feedback * update images
This commit is contained in:
parent
e3188458d5
commit
5042dc3b52
@ -81,7 +81,7 @@ The Google Cloud Monitoring query editor allows you to build two types of querie
|
||||
|
||||
### Metric Queries
|
||||
|
||||
{{< docs-imagebox img="/img/docs/v70/metric-query-builder.png" max-width= "400px" class="docs-image--right" >}}
|
||||
{{< docs-imagebox img="/img/docs/google-cloud-monitoring/metric-query-builder-8-0.png" max-width= "400px" class="docs-image--right" >}}
|
||||
|
||||
The metric query editor allows you to select metrics, group/aggregate by labels and by time, and use filters to specify which time series you want in the results.
|
||||
|
||||
@ -97,7 +97,7 @@ Google Cloud Monitoring supports different kinds of metrics like `GAUGE`, `DELTA
|
||||
|
||||
#### Filter
|
||||
|
||||
To add a filter, click the plus icon and choose a field to filter by and enter a filter value e.g. `instance_name = grafana-1`. You can remove the filter by clicking on the filter name and select `--remove filter--`.
|
||||
To add a filter, click the plus icon and choose a field to filter by and enter a filter value e.g. `instance_name = grafana-1`. You can remove the filter by clicking on the trash icon.
|
||||
|
||||
##### Simple wildcards
|
||||
|
||||
@ -107,13 +107,41 @@ When the operator is set to `=` or `!=` it is possible to add wildcards to the f
|
||||
|
||||
When the operator is set to `=~` or `!=~` it is possible to add regular expressions to the filter value field. E.g `us-central[1-3]-[af]` would match all values that starts with "us-central", is followed by a number in the range of 1 to 3, a dash and then either an "a" or an "f". Leading and trailing slashes are not needed when creating regular expressions.
|
||||
|
||||
#### Aggregation
|
||||
#### Pre-processing
|
||||
|
||||
The aggregation field lets you combine time series based on common statistics. For more information about aggregation, refer to [aggregation options](https://cloud.google.com/monitoring/charts/metrics-selector#aggregation-options).
|
||||
Preprocessing options are displayed in the UI when the selected metric has a metric kind of `delta` or `cumulative`. If the selected metric has a metric kind of `gauge`, no pre-processing option will be displayed.
|
||||
|
||||
The `Aligner` field allows you to align multiple time series after the same group by time interval. For more information about aligner, refer to [alignment metric selector](https://cloud.google.com/monitoring/charts/metrics-selector#alignment).
|
||||
If you select 'Rate', data points are aligned and converted to a rate per time series. If you select 'Delta', data points are aligned by their delta (difference) per time series.
|
||||
|
||||
##### Alignment Period/Group by Time
|
||||
#### Grouping
|
||||
|
||||
You can reduce the amount of data returned for a metric by combining different time series. To combine multiple time series, specify a grouping and a function.
|
||||
|
||||
##### Group by
|
||||
|
||||
Group by resource or metric labels to reduce the number of time series and to aggregate the results by a group. For example, group by `instance_name` to view an aggregated metric for a Compute instance.
|
||||
|
||||
###### Metadata labels
|
||||
|
||||
Resource metadata labels contain information that can uniquely identify a resource in Google Cloud. Metadata labels are only returned in the time series response if they're part of the **Group By** segment in the time series request.
|
||||
|
||||
There's no API for retrieving metadata labels. As a result, you cannot populate the group by list with the metadata labels that are available for the selected service and metric. However, the **Group By** field list comes with a pre-defined set of common system labels.
|
||||
|
||||
User labels cannot be predefined, but you can enter them manually in the **Group By** field. If a metadata label, user label, or system label is included in the **Group By** segment, then you can create filters based on it and expand its value on the **Alias** field.
|
||||
|
||||
##### Group by function
|
||||
|
||||
Select a grouping function to combine the time series in the group into a single time series.
|
||||
|
||||
#### Alignment
|
||||
|
||||
The process of alignment consists of collecting all data points received in a fixed length of time, applying a function to combine those data points, and assigning a timestamp to the result.
|
||||
|
||||
##### Alignment function
|
||||
|
||||
During alignment, all data points are received in a fixed interval. Within each interval (determined by the alignment period) and for each time series, the data is aggregated into a single point. The value of that point is determined by the type of alignment function used. For more information on alignment functions, refer to [alignment metric selector](https://cloud.google.com/monitoring/charts/metrics-selector#alignment).
|
||||
|
||||
##### Alignment period
|
||||
|
||||
The `Alignment Period` groups a metric by time if an aggregation is chosen. The default is to use the GCP Google Cloud Monitoring default groupings (which allows you to compare graphs in Grafana with graphs in the Google Cloud Monitoring UI).
|
||||
The option is called `cloud monitoring auto` and the defaults are:
|
||||
@ -124,23 +152,13 @@ The option is called `cloud monitoring auto` and the defaults are:
|
||||
|
||||
The other automatic option is `grafana auto`. This will automatically set the group by time depending on the time range chosen and the width of the time series panel. For more information about grafana auto, refer to the [interval variable](http://docs.grafana.org/variables/templates-and-variables/#the-interval-variable).
|
||||
|
||||
It is also possible to choose fixed time intervals to group by, like `1h` or `1d`.
|
||||
|
||||
#### Group By
|
||||
|
||||
Group by resource or metric labels to reduce the number of time series and to aggregate the results by a group by. E.g. Group by instance_name to see an aggregated metric for a Compute instance.
|
||||
|
||||
##### Metadata labels
|
||||
|
||||
Resource metadata labels contain information to uniquely identify a resource in Google Cloud. Metadata labels are only returned in the time series response if they're part of the **Group By** segment in the time series request. There's no API for retrieving metadata labels, so it's not possible to populate the group by dropdown with the metadata labels that are available for the selected service and metric. However, the **Group By** field dropdown comes with a pre-defined list of common system labels.
|
||||
|
||||
User labels cannot be pre-defined, but it's possible to enter them manually in the **Group By** field. If a metadata label, user label or system label is included in the **Group By** segment, then you can create filters based on it and expand its value on the **Alias** field.
|
||||
You can also choose fixed time intervals to group by, like `1h` or `1d`.
|
||||
|
||||
#### Alias patterns
|
||||
|
||||
The Alias By field allows you to control the format of the legend keys. The default is to show the metric name and labels. This can be long and hard to read. Using the following patterns in the alias field, you can format the legend key the way you want it.
|
||||
|
||||
#### Metric Type Patterns
|
||||
#### Metric type patterns
|
||||
|
||||
| Alias Pattern | Description | Example Result |
|
||||
| -------------------- | ---------------------------- | ------------------------------------------------- |
|
||||
@ -148,7 +166,7 @@ The Alias By field allows you to control the format of the legend keys. The defa
|
||||
| `{{metric.name}}` | returns the metric name part | `instance/cpu/utilization` |
|
||||
| `{{metric.service}}` | returns the service part | `compute` |
|
||||
|
||||
#### Label Patterns
|
||||
#### Label patterns
|
||||
|
||||
In the Group By dropdown, you can see a list of metric and resource labels for a metric. These can be included in the legend key using alias patterns.
|
||||
|
||||
@ -190,7 +208,7 @@ Grafana issues one query to the Cloud Monitoring API per query editor row, and e
|
||||
|
||||
> **Note:** Available in Grafana v7.0 and later versions.
|
||||
|
||||
{{< docs-imagebox img="/img/docs/v70/slo-query-builder.png" max-width= "400px" class="docs-image--right" >}}
|
||||
{{< docs-imagebox img="/img/docs/google-cloud-monitoring/slo-query-builder-8-0.png" max-width= "400px" class="docs-image--right" >}}
|
||||
|
||||
The SLO query builder in the Google Cloud Monitoring data source allows you to display SLO data in time series format. To get an understanding of the basic concepts in service monitoring, please refer to Google Cloud Monitoring's [official docs](https://cloud.google.com/monitoring/service-monitoring).
|
||||
|
||||
@ -212,7 +230,7 @@ The friendly names for the time series selectors are shown in Grafana. Here is t
|
||||
| SLO Compliance | select_slo_compliance |
|
||||
| SLO Error Budget Remaining | select_slo_budget_fraction |
|
||||
|
||||
#### Alias Patterns for SLO queries
|
||||
#### Alias patterns for SLO queries
|
||||
|
||||
The Alias By field allows you to control the format of the legend keys for SLO queries too.
|
||||
|
||||
@ -223,7 +241,7 @@ The Alias By field allows you to control the format of the legend keys for SLO q
|
||||
| `{{slo}}` | returns the SLO | `latency-slo` |
|
||||
| `{{selector}}` | returns the selector | `select_slo_health` |
|
||||
|
||||
#### Alignment Period/Group by Time for SLO queries
|
||||
#### Alignment period/group by time for SLO queries
|
||||
|
||||
SLO queries use the same [alignment period functionality as metric queries]({{< relref "#metric-queries" >}}).
|
||||
|
||||
@ -285,7 +303,7 @@ Why two ways? The first syntax is easier to read and write but does not allow yo
|
||||
|
||||
## Annotations
|
||||
|
||||
{{< docs-imagebox img="/img/docs/v71/cloudmonitoring_annotations_query_editor.png" max-width= "400px" class="docs-image--right" >}}
|
||||
{{< docs-imagebox img="/img/docs/google-cloud-monitoring/annotations-8-0.png" max-width= "400px" class="docs-image--right" >}}
|
||||
|
||||
[Annotations]({{< relref "../../dashboards/annotations.md" >}}) allow you to overlay rich event information on top of graphs. You add annotation
|
||||
queries via the Dashboard menu / Annotations view. Annotation rendering is expensive so it is important to limit the number of rows returned. There is no support for showing Google Cloud Monitoring annotations and events yet but it works well with [custom metrics](https://cloud.google.com/monitoring/custom-metrics/) in Google Cloud Monitoring.
|
||||
|
@ -57,11 +57,13 @@ var (
|
||||
)
|
||||
|
||||
const (
|
||||
gceAuthentication string = "gce"
|
||||
jwtAuthentication string = "jwt"
|
||||
metricQueryType string = "metrics"
|
||||
sloQueryType string = "slo"
|
||||
mqlEditorMode string = "mql"
|
||||
gceAuthentication string = "gce"
|
||||
jwtAuthentication string = "jwt"
|
||||
metricQueryType string = "metrics"
|
||||
sloQueryType string = "slo"
|
||||
mqlEditorMode string = "mql"
|
||||
crossSeriesReducerDefault string = "REDUCE_NONE"
|
||||
perSeriesAlignerDefault string = "ALIGN_MEAN"
|
||||
)
|
||||
|
||||
func init() {
|
||||
@ -210,6 +212,7 @@ func (e *Executor) buildQueryExecutors(tsdbQuery plugins.DataQuery) ([]cloudMoni
|
||||
if err := json.Unmarshal(model, &q); err != nil {
|
||||
return nil, fmt.Errorf("could not unmarshal CloudMonitoringQuery json: %w", err)
|
||||
}
|
||||
q.MetricQuery.PreprocessorType = toPreprocessorType(q.MetricQuery.Preprocessor)
|
||||
var target string
|
||||
params := url.Values{}
|
||||
params.Add("interval.startTime", startTime.UTC().Format(time.RFC3339))
|
||||
@ -345,16 +348,44 @@ func buildSLOFilterExpression(q sloQuery) string {
|
||||
|
||||
func setMetricAggParams(params *url.Values, query *metricQuery, durationSeconds int, intervalMs int64) {
|
||||
if query.CrossSeriesReducer == "" {
|
||||
query.CrossSeriesReducer = "REDUCE_NONE"
|
||||
query.CrossSeriesReducer = crossSeriesReducerDefault
|
||||
}
|
||||
|
||||
if query.PerSeriesAligner == "" {
|
||||
query.PerSeriesAligner = "ALIGN_MEAN"
|
||||
query.PerSeriesAligner = perSeriesAlignerDefault
|
||||
}
|
||||
|
||||
params.Add("aggregation.crossSeriesReducer", query.CrossSeriesReducer)
|
||||
params.Add("aggregation.perSeriesAligner", query.PerSeriesAligner)
|
||||
params.Add("aggregation.alignmentPeriod", calculateAlignmentPeriod(query.AlignmentPeriod, intervalMs, durationSeconds))
|
||||
alignmentPeriod := calculateAlignmentPeriod(query.AlignmentPeriod, intervalMs, durationSeconds)
|
||||
|
||||
// In case a preprocessor is defined, the preprocessor becomes the primary aggregation
|
||||
// and the aggregation that is specified in the UI becomes the secondary aggregation
|
||||
// Rules are specified in this issue: https://github.com/grafana/grafana/issues/30866
|
||||
if query.PreprocessorType != PreprocessorTypeNone {
|
||||
params.Add("secondaryAggregation.alignmentPeriod", alignmentPeriod)
|
||||
params.Add("secondaryAggregation.crossSeriesReducer", query.CrossSeriesReducer)
|
||||
params.Add("secondaryAggregation.perSeriesAligner", query.PerSeriesAligner)
|
||||
|
||||
primaryCrossSeriesReducer := crossSeriesReducerDefault
|
||||
if len(query.GroupBys) > 0 {
|
||||
primaryCrossSeriesReducer = query.CrossSeriesReducer
|
||||
}
|
||||
params.Add("aggregation.crossSeriesReducer", primaryCrossSeriesReducer)
|
||||
|
||||
aligner := "ALIGN_RATE"
|
||||
if query.PreprocessorType == PreprocessorTypeDelta {
|
||||
aligner = "ALIGN_DELTA"
|
||||
}
|
||||
params.Add("aggregation.perSeriesAligner", aligner)
|
||||
|
||||
for _, groupBy := range query.GroupBys {
|
||||
params.Add("secondaryAggregation.groupByFields", groupBy)
|
||||
}
|
||||
} else {
|
||||
params.Add("aggregation.crossSeriesReducer", query.CrossSeriesReducer)
|
||||
params.Add("aggregation.perSeriesAligner", query.PerSeriesAligner)
|
||||
}
|
||||
|
||||
params.Add("aggregation.alignmentPeriod", alignmentPeriod)
|
||||
|
||||
for _, groupBy := range query.GroupBys {
|
||||
params.Add("aggregation.groupByFields", groupBy)
|
||||
|
@ -1043,6 +1043,169 @@ func TestCloudMonitoring(t *testing.T) {
|
||||
assert.Contains(t, value, `zone=monitoring.regex.full_match("us-central1-a~")`)
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("and query preprocessor is not defined", func(t *testing.T) {
|
||||
tsdbQuery := getBaseQuery()
|
||||
tsdbQuery.Queries[0].Model = simplejson.NewFromAny(map[string]interface{}{
|
||||
"metricType": "a/metric/type",
|
||||
"crossSeriesReducer": "REDUCE_MIN",
|
||||
"perSeriesAligner": "REDUCE_SUM",
|
||||
"alignmentPeriod": "+60s",
|
||||
"groupBys": []string{"labelname"},
|
||||
"view": "FULL",
|
||||
})
|
||||
|
||||
qes, err := executor.buildQueryExecutors(tsdbQuery)
|
||||
require.NoError(t, err)
|
||||
queries := getCloudMonitoringQueriesFromInterface(t, qes)
|
||||
|
||||
assert.Equal(t, 1, len(queries))
|
||||
assert.Equal(t, "REDUCE_MIN", queries[0].Params["aggregation.crossSeriesReducer"][0])
|
||||
assert.Equal(t, "REDUCE_SUM", queries[0].Params["aggregation.perSeriesAligner"][0])
|
||||
assert.Equal(t, "+60s", queries[0].Params["aggregation.alignmentPeriod"][0])
|
||||
assert.Equal(t, "labelname", queries[0].Params["aggregation.groupByFields"][0])
|
||||
|
||||
assert.NotContains(t, queries[0].Params, "secondaryAggregation.crossSeriesReducer")
|
||||
assert.NotContains(t, "REDUCE_SUM", queries[0].Params, "secondaryAggregation.perSeriesAligner")
|
||||
assert.NotContains(t, "+60s", queries[0].Params, "secondaryAggregation.alignmentPeriod")
|
||||
assert.NotContains(t, "labelname", queries[0].Params, "secondaryAggregation.groupByFields")
|
||||
})
|
||||
|
||||
t.Run("and query preprocessor is set to none", func(t *testing.T) {
|
||||
tsdbQuery := getBaseQuery()
|
||||
tsdbQuery.Queries[0].Model = simplejson.NewFromAny(map[string]interface{}{
|
||||
"metricType": "a/metric/type",
|
||||
"crossSeriesReducer": "REDUCE_MIN",
|
||||
"perSeriesAligner": "REDUCE_SUM",
|
||||
"alignmentPeriod": "+60s",
|
||||
"groupBys": []string{"labelname"},
|
||||
"view": "FULL",
|
||||
"preprocessor": "none",
|
||||
})
|
||||
|
||||
qes, err := executor.buildQueryExecutors(tsdbQuery)
|
||||
require.NoError(t, err)
|
||||
queries := getCloudMonitoringQueriesFromInterface(t, qes)
|
||||
|
||||
assert.Equal(t, 1, len(queries))
|
||||
assert.Equal(t, "REDUCE_MIN", queries[0].Params["aggregation.crossSeriesReducer"][0])
|
||||
assert.Equal(t, "REDUCE_SUM", queries[0].Params["aggregation.perSeriesAligner"][0])
|
||||
assert.Equal(t, "+60s", queries[0].Params["aggregation.alignmentPeriod"][0])
|
||||
assert.Equal(t, "labelname", queries[0].Params["aggregation.groupByFields"][0])
|
||||
|
||||
assert.NotContains(t, queries[0].Params, "secondaryAggregation.crossSeriesReducer")
|
||||
assert.NotContains(t, "REDUCE_SUM", queries[0].Params, "secondaryAggregation.perSeriesAligner")
|
||||
assert.NotContains(t, "+60s", queries[0].Params, "secondaryAggregation.alignmentPeriod")
|
||||
assert.NotContains(t, "labelname", queries[0].Params, "secondaryAggregation.groupByFields")
|
||||
})
|
||||
|
||||
t.Run("and query preprocessor is set to rate and there's no group bys", func(t *testing.T) {
|
||||
tsdbQuery := getBaseQuery()
|
||||
tsdbQuery.Queries[0].Model = simplejson.NewFromAny(map[string]interface{}{
|
||||
"metricType": "a/metric/type",
|
||||
"crossSeriesReducer": "REDUCE_SUM",
|
||||
"perSeriesAligner": "REDUCE_MIN",
|
||||
"alignmentPeriod": "+60s",
|
||||
"groupBys": []string{},
|
||||
"view": "FULL",
|
||||
"preprocessor": "rate",
|
||||
})
|
||||
|
||||
qes, err := executor.buildQueryExecutors(tsdbQuery)
|
||||
require.NoError(t, err)
|
||||
queries := getCloudMonitoringQueriesFromInterface(t, qes)
|
||||
|
||||
assert.Equal(t, 1, len(queries))
|
||||
assert.Equal(t, "REDUCE_NONE", queries[0].Params["aggregation.crossSeriesReducer"][0])
|
||||
assert.Equal(t, "ALIGN_RATE", queries[0].Params["aggregation.perSeriesAligner"][0])
|
||||
assert.Equal(t, "+60s", queries[0].Params["aggregation.alignmentPeriod"][0])
|
||||
|
||||
assert.Equal(t, "REDUCE_SUM", queries[0].Params["secondaryAggregation.crossSeriesReducer"][0])
|
||||
assert.Equal(t, "REDUCE_MIN", queries[0].Params["secondaryAggregation.perSeriesAligner"][0])
|
||||
assert.Equal(t, "+60s", queries[0].Params["secondaryAggregation.alignmentPeriod"][0])
|
||||
})
|
||||
|
||||
t.Run("and query preprocessor is set to rate and group bys exist", func(t *testing.T) {
|
||||
tsdbQuery := getBaseQuery()
|
||||
tsdbQuery.Queries[0].Model = simplejson.NewFromAny(map[string]interface{}{
|
||||
"metricType": "a/metric/type",
|
||||
"crossSeriesReducer": "REDUCE_SUM",
|
||||
"perSeriesAligner": "REDUCE_MIN",
|
||||
"alignmentPeriod": "+60s",
|
||||
"groupBys": []string{"labelname"},
|
||||
"view": "FULL",
|
||||
"preprocessor": "rate",
|
||||
})
|
||||
|
||||
qes, err := executor.buildQueryExecutors(tsdbQuery)
|
||||
require.NoError(t, err)
|
||||
queries := getCloudMonitoringQueriesFromInterface(t, qes)
|
||||
|
||||
assert.Equal(t, 1, len(queries))
|
||||
assert.Equal(t, "REDUCE_SUM", queries[0].Params["aggregation.crossSeriesReducer"][0])
|
||||
assert.Equal(t, "ALIGN_RATE", queries[0].Params["aggregation.perSeriesAligner"][0])
|
||||
assert.Equal(t, "+60s", queries[0].Params["aggregation.alignmentPeriod"][0])
|
||||
assert.Equal(t, "labelname", queries[0].Params["aggregation.groupByFields"][0])
|
||||
|
||||
assert.Equal(t, "REDUCE_SUM", queries[0].Params["secondaryAggregation.crossSeriesReducer"][0])
|
||||
assert.Equal(t, "REDUCE_MIN", queries[0].Params["secondaryAggregation.perSeriesAligner"][0])
|
||||
assert.Equal(t, "+60s", queries[0].Params["secondaryAggregation.alignmentPeriod"][0])
|
||||
assert.Equal(t, "labelname", queries[0].Params["secondaryAggregation.groupByFields"][0])
|
||||
})
|
||||
|
||||
t.Run("and query preprocessor is set to delta and there's no group bys", func(t *testing.T) {
|
||||
tsdbQuery := getBaseQuery()
|
||||
tsdbQuery.Queries[0].Model = simplejson.NewFromAny(map[string]interface{}{
|
||||
"metricType": "a/metric/type",
|
||||
"crossSeriesReducer": "REDUCE_MIN",
|
||||
"perSeriesAligner": "REDUCE_SUM",
|
||||
"alignmentPeriod": "+60s",
|
||||
"groupBys": []string{},
|
||||
"view": "FULL",
|
||||
"preprocessor": "delta",
|
||||
})
|
||||
|
||||
qes, err := executor.buildQueryExecutors(tsdbQuery)
|
||||
require.NoError(t, err)
|
||||
queries := getCloudMonitoringQueriesFromInterface(t, qes)
|
||||
|
||||
assert.Equal(t, 1, len(queries))
|
||||
assert.Equal(t, "REDUCE_NONE", queries[0].Params["aggregation.crossSeriesReducer"][0])
|
||||
assert.Equal(t, "ALIGN_DELTA", queries[0].Params["aggregation.perSeriesAligner"][0])
|
||||
assert.Equal(t, "+60s", queries[0].Params["aggregation.alignmentPeriod"][0])
|
||||
|
||||
assert.Equal(t, "REDUCE_MIN", queries[0].Params["secondaryAggregation.crossSeriesReducer"][0])
|
||||
assert.Equal(t, "REDUCE_SUM", queries[0].Params["secondaryAggregation.perSeriesAligner"][0])
|
||||
assert.Equal(t, "+60s", queries[0].Params["secondaryAggregation.alignmentPeriod"][0])
|
||||
})
|
||||
|
||||
t.Run("and query preprocessor is set to delta and group bys exist", func(t *testing.T) {
|
||||
tsdbQuery := getBaseQuery()
|
||||
tsdbQuery.Queries[0].Model = simplejson.NewFromAny(map[string]interface{}{
|
||||
"metricType": "a/metric/type",
|
||||
"crossSeriesReducer": "REDUCE_MIN",
|
||||
"perSeriesAligner": "REDUCE_SUM",
|
||||
"alignmentPeriod": "+60s",
|
||||
"groupBys": []string{"labelname"},
|
||||
"view": "FULL",
|
||||
"preprocessor": "delta",
|
||||
})
|
||||
|
||||
qes, err := executor.buildQueryExecutors(tsdbQuery)
|
||||
require.NoError(t, err)
|
||||
queries := getCloudMonitoringQueriesFromInterface(t, qes)
|
||||
|
||||
assert.Equal(t, 1, len(queries))
|
||||
assert.Equal(t, "REDUCE_MIN", queries[0].Params["aggregation.crossSeriesReducer"][0])
|
||||
assert.Equal(t, "ALIGN_DELTA", queries[0].Params["aggregation.perSeriesAligner"][0])
|
||||
assert.Equal(t, "+60s", queries[0].Params["aggregation.alignmentPeriod"][0])
|
||||
assert.Equal(t, "labelname", queries[0].Params["aggregation.groupByFields"][0])
|
||||
|
||||
assert.Equal(t, "REDUCE_MIN", queries[0].Params["secondaryAggregation.crossSeriesReducer"][0])
|
||||
assert.Equal(t, "REDUCE_SUM", queries[0].Params["secondaryAggregation.perSeriesAligner"][0])
|
||||
assert.Equal(t, "+60s", queries[0].Params["secondaryAggregation.alignmentPeriod"][0])
|
||||
assert.Equal(t, "labelname", queries[0].Params["secondaryAggregation.groupByFields"][0])
|
||||
})
|
||||
}
|
||||
|
||||
func loadTestFile(path string) (cloudMonitoringResponse, error) {
|
||||
|
22
pkg/tsdb/cloudmonitoring/preprocessor.go
Normal file
22
pkg/tsdb/cloudmonitoring/preprocessor.go
Normal file
@ -0,0 +1,22 @@
|
||||
package cloudmonitoring
|
||||
|
||||
type preprocessorType int
|
||||
|
||||
const (
|
||||
PreprocessorTypeNone preprocessorType = iota
|
||||
PreprocessorTypeRate
|
||||
PreprocessorTypeDelta
|
||||
)
|
||||
|
||||
func toPreprocessorType(preprocessorTypeString string) preprocessorType {
|
||||
switch preprocessorTypeString {
|
||||
case "none":
|
||||
return PreprocessorTypeNone
|
||||
case "rate":
|
||||
return PreprocessorTypeRate
|
||||
case "delta":
|
||||
return PreprocessorTypeDelta
|
||||
default:
|
||||
return PreprocessorTypeNone
|
||||
}
|
||||
}
|
@ -95,6 +95,10 @@ func (timeSeriesFilter *cloudMonitoringTimeSeriesFilter) parseResponse(queryRes
|
||||
frame.RefID = timeSeriesFilter.RefID
|
||||
frame.Meta = &data.FrameMeta{
|
||||
ExecutedQueryString: executedQueryString,
|
||||
Custom: map[string]interface{}{
|
||||
"alignmentPeriod": timeSeriesFilter.Params.Get("aggregation.alignmentPeriod"),
|
||||
"perSeriesAligner": timeSeriesFilter.Params.Get("aggregation.perSeriesAligner"),
|
||||
},
|
||||
}
|
||||
|
||||
for key, value := range series.Metric.Labels {
|
||||
|
@ -56,6 +56,8 @@ type (
|
||||
View string
|
||||
EditorMode string
|
||||
Query string
|
||||
Preprocessor string
|
||||
PreprocessorType preprocessorType
|
||||
}
|
||||
|
||||
sloQuery struct {
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { isString } from 'lodash';
|
||||
import { alignmentPeriods, MetricKind, selectors, ValueTypes } from './constants';
|
||||
import { ALIGNMENT_PERIODS, SELECTORS } from './constants';
|
||||
import CloudMonitoringDatasource from './datasource';
|
||||
import { CloudMonitoringVariableQuery, MetricFindQueryTypes } from './types';
|
||||
import { CloudMonitoringVariableQuery, MetricDescriptor, MetricFindQueryTypes, MetricKind, ValueTypes } from './types';
|
||||
import { SelectableValue } from '@grafana/data';
|
||||
import {
|
||||
extractServicesFromMetricDescriptors,
|
||||
@ -65,7 +65,7 @@ export default class CloudMonitoringMetricFindQuery {
|
||||
|
||||
async handleServiceQuery({ projectName }: CloudMonitoringVariableQuery) {
|
||||
const metricDescriptors = await this.datasource.getMetricTypes(projectName);
|
||||
const services: any[] = extractServicesFromMetricDescriptors(metricDescriptors);
|
||||
const services: MetricDescriptor[] = extractServicesFromMetricDescriptors(metricDescriptors);
|
||||
return services.map((s) => ({
|
||||
text: s.serviceShortName,
|
||||
value: s.service,
|
||||
@ -79,7 +79,7 @@ export default class CloudMonitoringMetricFindQuery {
|
||||
}
|
||||
const metricDescriptors = await this.datasource.getMetricTypes(projectName);
|
||||
return getMetricTypesByService(metricDescriptors, this.datasource.templateSrv.replace(selectedService)).map(
|
||||
(s: any) => ({
|
||||
(s) => ({
|
||||
text: s.displayName,
|
||||
value: s.type,
|
||||
expandable: true,
|
||||
@ -121,7 +121,7 @@ export default class CloudMonitoringMetricFindQuery {
|
||||
}
|
||||
const metricDescriptors = await this.datasource.getMetricTypes(projectName);
|
||||
const descriptor = metricDescriptors.find(
|
||||
(m: any) => m.type === this.datasource.templateSrv.replace(selectedMetricType)
|
||||
(m) => m.type === this.datasource.templateSrv.replace(selectedMetricType)
|
||||
);
|
||||
|
||||
if (!descriptor) {
|
||||
@ -138,7 +138,7 @@ export default class CloudMonitoringMetricFindQuery {
|
||||
|
||||
const metricDescriptors = await this.datasource.getMetricTypes(projectName);
|
||||
const descriptor = metricDescriptors.find(
|
||||
(m: any) => m.type === this.datasource.templateSrv.replace(selectedMetricType)
|
||||
(m) => m.type === this.datasource.templateSrv.replace(selectedMetricType)
|
||||
);
|
||||
|
||||
if (!descriptor) {
|
||||
@ -161,11 +161,11 @@ export default class CloudMonitoringMetricFindQuery {
|
||||
}
|
||||
|
||||
async handleSelectorQuery() {
|
||||
return selectors.map(this.toFindQueryResult);
|
||||
return SELECTORS.map(this.toFindQueryResult);
|
||||
}
|
||||
|
||||
handleAlignmentPeriodQuery() {
|
||||
return alignmentPeriods.map(this.toFindQueryResult);
|
||||
return ALIGNMENT_PERIODS.map(this.toFindQueryResult);
|
||||
}
|
||||
|
||||
toFindQueryResult(x: any) {
|
||||
|
@ -2,10 +2,13 @@ import { AnnotationTarget } from './types';
|
||||
|
||||
export class CloudMonitoringAnnotationsQueryCtrl {
|
||||
static templateUrl = 'partials/annotations.editor.html';
|
||||
annotation: any;
|
||||
declare annotation: any;
|
||||
|
||||
/** @ngInject */
|
||||
constructor($scope: any) {
|
||||
this.annotation = $scope.ctrl.annotation || {};
|
||||
this.annotation.target = $scope.ctrl.annotation.target || {};
|
||||
|
||||
constructor() {
|
||||
this.annotation.target = this.annotation.target || {};
|
||||
this.onQueryChange = this.onQueryChange.bind(this);
|
||||
}
|
||||
|
||||
|
@ -1,9 +1,9 @@
|
||||
import React from 'react';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { Segment } from '@grafana/ui';
|
||||
import { Aggregations, Props } from './Aggregations';
|
||||
import { ValueTypes, MetricKind } from '../constants';
|
||||
import { Select } from '@grafana/ui';
|
||||
import { Aggregation, Props } from './Aggregation';
|
||||
import { ValueTypes, MetricKind } from '../types';
|
||||
import { TemplateSrvStub } from 'test/specs/helpers';
|
||||
|
||||
const props: Props = {
|
||||
@ -16,16 +16,13 @@ const props: Props = {
|
||||
} as any,
|
||||
crossSeriesReducer: '',
|
||||
groupBys: [],
|
||||
children(renderProps) {
|
||||
return <div />;
|
||||
},
|
||||
templateVariableOptions: [],
|
||||
};
|
||||
|
||||
describe('Aggregations', () => {
|
||||
describe('Aggregation', () => {
|
||||
it('renders correctly', () => {
|
||||
render(<Aggregations {...props} />);
|
||||
expect(screen.getByTestId('aggregations')).toBeInTheDocument();
|
||||
render(<Aggregation {...props} />);
|
||||
expect(screen.getByTestId('cloud-monitoring-aggregation')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
describe('options', () => {
|
||||
@ -39,8 +36,8 @@ describe('Aggregations', () => {
|
||||
};
|
||||
|
||||
it('should not have the reduce values', () => {
|
||||
const wrapper = shallow(<Aggregations {...nextProps} />);
|
||||
const { options } = wrapper.find(Segment).props() as any;
|
||||
const wrapper = shallow(<Aggregation {...nextProps} />);
|
||||
const { options } = wrapper.find(Select).props() as any;
|
||||
const [, aggGroup] = options;
|
||||
|
||||
expect(aggGroup.options.length).toEqual(11);
|
||||
@ -60,8 +57,8 @@ describe('Aggregations', () => {
|
||||
};
|
||||
|
||||
it('should have the reduce values', () => {
|
||||
const wrapper = shallow(<Aggregations {...nextProps} />);
|
||||
const { options } = wrapper.find(Segment).props() as any;
|
||||
const wrapper = shallow(<Aggregation {...nextProps} />);
|
||||
const { options } = wrapper.find(Select).props() as any;
|
||||
const [, aggGroup] = options;
|
||||
|
||||
expect(aggGroup.options.length).toEqual(11);
|
@ -0,0 +1,65 @@
|
||||
import React, { FC, useMemo } from 'react';
|
||||
|
||||
import { SelectableValue } from '@grafana/data';
|
||||
import { Select } from '@grafana/ui';
|
||||
import { QueryEditorField } from '.';
|
||||
import { getAggregationOptionsByMetric } from '../functions';
|
||||
import { MetricDescriptor, ValueTypes, MetricKind } from '../types';
|
||||
|
||||
export interface Props {
|
||||
onChange: (metricDescriptor: string) => void;
|
||||
metricDescriptor?: MetricDescriptor;
|
||||
crossSeriesReducer: string;
|
||||
groupBys: string[];
|
||||
templateVariableOptions: Array<SelectableValue<string>>;
|
||||
}
|
||||
|
||||
export const Aggregation: FC<Props> = (props) => {
|
||||
const aggOptions = useAggregationOptionsByMetric(props);
|
||||
const selected = useSelectedFromOptions(aggOptions, props);
|
||||
|
||||
return (
|
||||
<QueryEditorField labelWidth={18} label="Group by function" data-testid="cloud-monitoring-aggregation">
|
||||
<Select
|
||||
width={16}
|
||||
onChange={({ value }) => props.onChange(value!)}
|
||||
value={selected}
|
||||
options={[
|
||||
{
|
||||
label: 'Template Variables',
|
||||
options: props.templateVariableOptions,
|
||||
},
|
||||
{
|
||||
label: 'Aggregations',
|
||||
expanded: true,
|
||||
options: aggOptions,
|
||||
},
|
||||
]}
|
||||
placeholder="Select Reducer"
|
||||
/>
|
||||
</QueryEditorField>
|
||||
);
|
||||
};
|
||||
|
||||
const useAggregationOptionsByMetric = ({ metricDescriptor }: Props): Array<SelectableValue<string>> => {
|
||||
const valueType = metricDescriptor?.valueType;
|
||||
const metricKind = metricDescriptor?.metricKind;
|
||||
|
||||
return useMemo(() => {
|
||||
if (!valueType || !metricKind) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return getAggregationOptionsByMetric(valueType as ValueTypes, metricKind as MetricKind).map((a) => ({
|
||||
...a,
|
||||
label: a.text,
|
||||
}));
|
||||
}, [valueType, metricKind]);
|
||||
};
|
||||
|
||||
const useSelectedFromOptions = (aggOptions: Array<SelectableValue<string>>, props: Props) => {
|
||||
return useMemo(() => {
|
||||
const allOptions = [...aggOptions, ...props.templateVariableOptions];
|
||||
return allOptions.find((s) => s.value === props.crossSeriesReducer);
|
||||
}, [aggOptions, props.crossSeriesReducer, props.templateVariableOptions]);
|
||||
};
|
@ -1,79 +0,0 @@
|
||||
import React, { FC, useState, useMemo } from 'react';
|
||||
|
||||
import { SelectableValue } from '@grafana/data';
|
||||
import { Segment, Icon } from '@grafana/ui';
|
||||
import { getAggregationOptionsByMetric } from '../functions';
|
||||
import { ValueTypes, MetricKind } from '../constants';
|
||||
import { MetricDescriptor } from '../types';
|
||||
|
||||
export interface Props {
|
||||
onChange: (metricDescriptor: string) => void;
|
||||
metricDescriptor?: MetricDescriptor;
|
||||
crossSeriesReducer: string;
|
||||
groupBys: string[];
|
||||
children: (displayAdvancedOptions: boolean) => React.ReactNode;
|
||||
templateVariableOptions: Array<SelectableValue<string>>;
|
||||
}
|
||||
|
||||
export const Aggregations: FC<Props> = (props) => {
|
||||
const [displayAdvancedOptions, setDisplayAdvancedOptions] = useState(false);
|
||||
const aggOptions = useAggregationOptionsByMetric(props);
|
||||
const selected = useSelectedFromOptions(aggOptions, props);
|
||||
|
||||
return (
|
||||
<div data-testid="aggregations">
|
||||
<div className="gf-form-inline">
|
||||
<label className="gf-form-label query-keyword width-9">Aggregation</label>
|
||||
<Segment
|
||||
onChange={({ value }) => props.onChange(value!)}
|
||||
value={selected}
|
||||
options={[
|
||||
{
|
||||
label: 'Template Variables',
|
||||
options: props.templateVariableOptions,
|
||||
},
|
||||
{
|
||||
label: 'Aggregations',
|
||||
expanded: true,
|
||||
options: aggOptions,
|
||||
},
|
||||
]}
|
||||
placeholder="Select Reducer"
|
||||
/>
|
||||
<div className="gf-form gf-form--grow">
|
||||
<label className="gf-form-label gf-form-label--grow">
|
||||
<a onClick={() => setDisplayAdvancedOptions(!displayAdvancedOptions)}>
|
||||
<>
|
||||
<Icon name={displayAdvancedOptions ? 'angle-down' : 'angle-right'} /> Advanced Options
|
||||
</>
|
||||
</a>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
{props.children(displayAdvancedOptions)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const useAggregationOptionsByMetric = ({ metricDescriptor }: Props): Array<SelectableValue<string>> => {
|
||||
const valueType = metricDescriptor?.valueType;
|
||||
const metricKind = metricDescriptor?.metricKind;
|
||||
|
||||
return useMemo(() => {
|
||||
if (!valueType || !metricKind) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return getAggregationOptionsByMetric(valueType as ValueTypes, metricKind as MetricKind).map((a) => ({
|
||||
...a,
|
||||
label: a.text,
|
||||
}));
|
||||
}, [valueType, metricKind]);
|
||||
};
|
||||
|
||||
const useSelectedFromOptions = (aggOptions: Array<SelectableValue<string>>, props: Props) => {
|
||||
return useMemo(() => {
|
||||
const allOptions = [...aggOptions, ...props.templateVariableOptions];
|
||||
return allOptions.find((s) => s.value === props.crossSeriesReducer);
|
||||
}, [aggOptions, props.crossSeriesReducer, props.templateVariableOptions]);
|
||||
};
|
@ -1,6 +1,8 @@
|
||||
import React, { FunctionComponent, useState } from 'react';
|
||||
import { debounce } from 'lodash';
|
||||
import { QueryInlineField } from '.';
|
||||
import { Input } from '@grafana/ui';
|
||||
import { QueryEditorRow } from '.';
|
||||
import { INPUT_WIDTH } from '../constants';
|
||||
|
||||
export interface Props {
|
||||
onChange: (alias: any) => void;
|
||||
@ -18,8 +20,8 @@ export const AliasBy: FunctionComponent<Props> = ({ value = '', onChange }) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<QueryInlineField label="Alias By">
|
||||
<input type="text" className="gf-form-input width-26" value={alias} onChange={onChange} />
|
||||
</QueryInlineField>
|
||||
<QueryEditorRow label="Alias by">
|
||||
<Input width={INPUT_WIDTH} value={alias} onChange={onChange} />
|
||||
</QueryEditorRow>
|
||||
);
|
||||
};
|
||||
|
@ -0,0 +1,34 @@
|
||||
import React, { FC } from 'react';
|
||||
import { SelectableValue } from '@grafana/data';
|
||||
import { SELECT_WIDTH } from '../constants';
|
||||
import { CustomMetaData, MetricQuery } from '../types';
|
||||
import { AlignmentFunction, AlignmentPeriod, AlignmentPeriodLabel, QueryEditorField, QueryEditorRow } from '.';
|
||||
import CloudMonitoringDatasource from '../datasource';
|
||||
|
||||
export interface Props {
|
||||
onChange: (query: MetricQuery) => void;
|
||||
query: MetricQuery;
|
||||
templateVariableOptions: Array<SelectableValue<string>>;
|
||||
customMetaData: CustomMetaData;
|
||||
datasource: CloudMonitoringDatasource;
|
||||
}
|
||||
|
||||
export const Alignment: FC<Props> = ({ templateVariableOptions, onChange, query, customMetaData, datasource }) => {
|
||||
return (
|
||||
<QueryEditorRow
|
||||
label="Alignment function"
|
||||
tooltip="The process of alignment consists of collecting all data points received in a fixed length of time, applying a function to combine those data points, and assigning a timestamp to the result."
|
||||
fillComponent={<AlignmentPeriodLabel datasource={datasource} customMetaData={customMetaData} />}
|
||||
>
|
||||
<AlignmentFunction templateVariableOptions={templateVariableOptions} query={query} onChange={onChange} />
|
||||
<QueryEditorField label="Alignment period">
|
||||
<AlignmentPeriod
|
||||
selectWidth={SELECT_WIDTH}
|
||||
templateVariableOptions={templateVariableOptions}
|
||||
query={query}
|
||||
onChange={onChange}
|
||||
/>
|
||||
</QueryEditorField>
|
||||
</QueryEditorRow>
|
||||
);
|
||||
};
|
@ -0,0 +1,40 @@
|
||||
import React, { FC, useMemo } from 'react';
|
||||
import { SelectableValue } from '@grafana/data';
|
||||
import { Select } from '@grafana/ui';
|
||||
import { MetricQuery } from '../types';
|
||||
import { getAlignmentPickerData } from '../functions';
|
||||
import { SELECT_WIDTH } from '../constants';
|
||||
|
||||
export interface Props {
|
||||
onChange: (query: MetricQuery) => void;
|
||||
query: MetricQuery;
|
||||
templateVariableOptions: Array<SelectableValue<string>>;
|
||||
}
|
||||
|
||||
export const AlignmentFunction: FC<Props> = ({ query, templateVariableOptions, onChange }) => {
|
||||
const { valueType, metricKind, perSeriesAligner: psa, preprocessor } = query;
|
||||
const { perSeriesAligner, alignOptions } = useMemo(
|
||||
() => getAlignmentPickerData(valueType, metricKind, psa, preprocessor),
|
||||
[valueType, metricKind, psa, preprocessor]
|
||||
);
|
||||
|
||||
return (
|
||||
<Select
|
||||
width={SELECT_WIDTH}
|
||||
onChange={({ value }) => onChange({ ...query, perSeriesAligner: value! })}
|
||||
value={[...alignOptions, ...templateVariableOptions].find((s) => s.value === perSeriesAligner)}
|
||||
options={[
|
||||
{
|
||||
label: 'Template Variables',
|
||||
options: templateVariableOptions,
|
||||
},
|
||||
{
|
||||
label: 'Alignment options',
|
||||
expanded: true,
|
||||
options: alignOptions,
|
||||
},
|
||||
]}
|
||||
placeholder="Select Alignment"
|
||||
></Select>
|
||||
);
|
||||
};
|
@ -0,0 +1,44 @@
|
||||
import React, { FC, useMemo } from 'react';
|
||||
import { SelectableValue } from '@grafana/data';
|
||||
import { Select } from '@grafana/ui';
|
||||
import { ALIGNMENT_PERIODS } from '../constants';
|
||||
import { BaseQuery } from '../types';
|
||||
|
||||
export interface Props {
|
||||
onChange: (query: BaseQuery) => void;
|
||||
query: BaseQuery;
|
||||
templateVariableOptions: Array<SelectableValue<string>>;
|
||||
selectWidth?: number;
|
||||
}
|
||||
|
||||
export const AlignmentPeriod: FC<Props> = ({ templateVariableOptions, onChange, query, selectWidth }) => {
|
||||
const options = useMemo(
|
||||
() =>
|
||||
ALIGNMENT_PERIODS.map((ap) => ({
|
||||
...ap,
|
||||
label: ap.text,
|
||||
})),
|
||||
[]
|
||||
);
|
||||
const visibleOptions = useMemo(() => options.filter((ap) => !ap.hidden), [options]);
|
||||
|
||||
return (
|
||||
<Select
|
||||
width={selectWidth}
|
||||
onChange={({ value }) => onChange({ ...query, alignmentPeriod: value! })}
|
||||
value={[...options, ...templateVariableOptions].find((s) => s.value === query.alignmentPeriod)}
|
||||
options={[
|
||||
{
|
||||
label: 'Template Variables',
|
||||
options: templateVariableOptions,
|
||||
},
|
||||
{
|
||||
label: 'Aggregations',
|
||||
expanded: true,
|
||||
options: visibleOptions,
|
||||
},
|
||||
]}
|
||||
placeholder="Select Alignment"
|
||||
></Select>
|
||||
);
|
||||
};
|
@ -0,0 +1,26 @@
|
||||
import React, { FC, useMemo } from 'react';
|
||||
import { rangeUtil } from '@grafana/data';
|
||||
import { ALIGNMENTS } from '../constants';
|
||||
import CloudMonitoringDatasource from '../datasource';
|
||||
import { CustomMetaData } from '../types';
|
||||
|
||||
export interface Props {
|
||||
customMetaData: CustomMetaData;
|
||||
datasource: CloudMonitoringDatasource;
|
||||
}
|
||||
|
||||
export const AlignmentPeriodLabel: FC<Props> = ({ customMetaData, datasource }) => {
|
||||
const { perSeriesAligner, alignmentPeriod } = customMetaData;
|
||||
const formatAlignmentText = useMemo(() => {
|
||||
if (!alignmentPeriod || !perSeriesAligner) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const alignment = ALIGNMENTS.find((ap) => ap.value === datasource.templateSrv.replace(perSeriesAligner));
|
||||
const seconds = parseInt(alignmentPeriod ?? ''.replace(/[^0-9]/g, ''), 10);
|
||||
const hms = rangeUtil.secondsToHms(seconds);
|
||||
return `${hms} interval (${alignment?.text ?? ''})`;
|
||||
}, [datasource, perSeriesAligner, alignmentPeriod]);
|
||||
|
||||
return <label>{formatAlignmentText}</label>;
|
||||
};
|
@ -1,61 +0,0 @@
|
||||
import React, { FC } from 'react';
|
||||
|
||||
import { TemplateSrv } from '@grafana/runtime';
|
||||
import { SelectableValue, rangeUtil } from '@grafana/data';
|
||||
import { Segment } from '@grafana/ui';
|
||||
import { alignmentPeriods, alignOptions } from '../constants';
|
||||
|
||||
export interface Props {
|
||||
onChange: (alignmentPeriod: string) => void;
|
||||
templateSrv: TemplateSrv;
|
||||
templateVariableOptions: Array<SelectableValue<string>>;
|
||||
alignmentPeriod: string;
|
||||
perSeriesAligner: string;
|
||||
usedAlignmentPeriod?: number;
|
||||
}
|
||||
|
||||
export const AlignmentPeriods: FC<Props> = ({
|
||||
alignmentPeriod,
|
||||
templateSrv,
|
||||
templateVariableOptions,
|
||||
onChange,
|
||||
perSeriesAligner,
|
||||
usedAlignmentPeriod,
|
||||
}) => {
|
||||
const alignment = alignOptions.find((ap) => ap.value === templateSrv.replace(perSeriesAligner));
|
||||
const formatAlignmentText = usedAlignmentPeriod
|
||||
? `${rangeUtil.secondsToHms(usedAlignmentPeriod)} interval (${alignment ? alignment.text : ''})`
|
||||
: '';
|
||||
const options = alignmentPeriods.map((ap) => ({
|
||||
...ap,
|
||||
label: ap.text,
|
||||
}));
|
||||
const visibleOptions = options.filter((ap) => !ap.hidden);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="gf-form-inline">
|
||||
<label className="gf-form-label query-keyword width-9">Alignment Period</label>
|
||||
<Segment
|
||||
onChange={({ value }) => onChange(value!)}
|
||||
value={[...options, ...templateVariableOptions].find((s) => s.value === alignmentPeriod)}
|
||||
options={[
|
||||
{
|
||||
label: 'Template Variables',
|
||||
options: templateVariableOptions,
|
||||
},
|
||||
{
|
||||
label: 'Aggregations',
|
||||
expanded: true,
|
||||
options: visibleOptions,
|
||||
},
|
||||
]}
|
||||
placeholder="Select Alignment"
|
||||
></Segment>
|
||||
<div className="gf-form gf-form--grow">
|
||||
{usedAlignmentPeriod && <label className="gf-form-label gf-form-label--grow">{formatAlignmentText}</label>}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
@ -1,39 +0,0 @@
|
||||
import React, { FC } from 'react';
|
||||
|
||||
import { SelectableValue } from '@grafana/data';
|
||||
import { Segment } from '@grafana/ui';
|
||||
|
||||
export interface Props {
|
||||
onChange: (perSeriesAligner: string) => void;
|
||||
templateVariableOptions: Array<SelectableValue<string>>;
|
||||
alignOptions: Array<SelectableValue<string>>;
|
||||
perSeriesAligner: string;
|
||||
}
|
||||
|
||||
export const Alignments: FC<Props> = ({ perSeriesAligner, templateVariableOptions, onChange, alignOptions }) => {
|
||||
return (
|
||||
<>
|
||||
<div className="gf-form-inline">
|
||||
<div className="gf-form offset-width-9">
|
||||
<label className="gf-form-label query-keyword width-15">Aligner</label>
|
||||
<Segment
|
||||
onChange={({ value }) => onChange(value!)}
|
||||
value={[...alignOptions, ...templateVariableOptions].find((s) => s.value === perSeriesAligner)}
|
||||
options={[
|
||||
{
|
||||
label: 'Template Variables',
|
||||
options: templateVariableOptions,
|
||||
},
|
||||
{
|
||||
label: 'Alignment options',
|
||||
expanded: true,
|
||||
options: alignOptions,
|
||||
},
|
||||
]}
|
||||
placeholder="Select Alignment"
|
||||
></Segment>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
@ -4,9 +4,9 @@ import { TemplateSrv } from '@grafana/runtime';
|
||||
import { SelectableValue } from '@grafana/data';
|
||||
|
||||
import CloudMonitoringDatasource from '../datasource';
|
||||
import { AnnotationsHelp, LabelFilter, Metrics, Project } from './';
|
||||
import { AnnotationsHelp, LabelFilter, Metrics, Project, QueryEditorRow } from './';
|
||||
import { toOption } from '../functions';
|
||||
import { AnnotationTarget, EditorMode, MetricDescriptor } from '../types';
|
||||
import { AnnotationTarget, EditorMode, MetricDescriptor, MetricKind } from '../types';
|
||||
|
||||
const { Input } = LegacyForms;
|
||||
|
||||
@ -30,7 +30,7 @@ const DefaultTarget: State = {
|
||||
projects: [],
|
||||
metricType: '',
|
||||
filters: [],
|
||||
metricKind: '',
|
||||
metricKind: MetricKind.GAUGE,
|
||||
valueType: '',
|
||||
refId: 'annotationQuery',
|
||||
title: '',
|
||||
@ -122,29 +122,23 @@ export class AnnotationQueryEditor extends React.Component<Props, State> {
|
||||
</>
|
||||
)}
|
||||
</Metrics>
|
||||
<div className="gf-form gf-form-inline">
|
||||
<div className="gf-form">
|
||||
<span className="gf-form-label query-keyword width-9">Title</span>
|
||||
<Input
|
||||
type="text"
|
||||
className="gf-form-input width-20"
|
||||
value={title}
|
||||
onChange={(e) => this.onChange('title', e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="gf-form">
|
||||
<span className="gf-form-label query-keyword width-9">Text</span>
|
||||
<Input
|
||||
type="text"
|
||||
className="gf-form-input width-20"
|
||||
value={text}
|
||||
onChange={(e) => this.onChange('text', e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="gf-form gf-form--grow">
|
||||
<div className="gf-form-label gf-form-label--grow" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<QueryEditorRow label="Title">
|
||||
<Input
|
||||
type="text"
|
||||
className="gf-form-input width-20"
|
||||
value={title}
|
||||
onChange={(e) => this.onChange('title', e.target.value)}
|
||||
/>
|
||||
</QueryEditorRow>
|
||||
<QueryEditorRow label="Text">
|
||||
<Input
|
||||
type="text"
|
||||
className="gf-form-input width-20"
|
||||
value={text}
|
||||
onChange={(e) => this.onChange('text', e.target.value)}
|
||||
/>
|
||||
</QueryEditorRow>
|
||||
|
||||
<AnnotationsHelp />
|
||||
</>
|
||||
|
@ -0,0 +1,72 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import { QueryEditorHelpProps } from '@grafana/data';
|
||||
import { css } from '@emotion/css';
|
||||
|
||||
export default class CloudMonitoringCheatSheet extends PureComponent<QueryEditorHelpProps, { userExamples: string[] }> {
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<h2>Cloud Monitoring alias patterns</h2>
|
||||
<div>
|
||||
<p>
|
||||
Format the legend keys any way you want by using alias patterns. Format the legend keys any way you want by
|
||||
using alias patterns.
|
||||
</p>
|
||||
Example:
|
||||
<code>{`${'{{metric.name}} - {{metric.label.instance_name}}'}`}</code>
|
||||
<br />
|
||||
Result: <code>cpu/usage_time - server1-europe-west-1</code>
|
||||
<br />
|
||||
<br />
|
||||
<label>Patterns</label>
|
||||
<br />
|
||||
<ul
|
||||
className={css`
|
||||
list-style: none;
|
||||
`}
|
||||
>
|
||||
<li>
|
||||
<code>{`${'{{metric.type}}'}`}</code> = metric type e.g. compute.googleapis.com/instance/cpu/usage_time
|
||||
</li>
|
||||
<li>
|
||||
<code>{`${'{{metric.name}}'}`}</code> = name part of metric e.g. instance/cpu/usage_time
|
||||
</li>
|
||||
<li>
|
||||
<code>{`${'{{metric.service}}'}`}</code> = service part of metric e.g. compute
|
||||
</li>
|
||||
<li>
|
||||
<code>{`${'{{metric.label.label_name}}'}`}</code> = Metric label metadata e.g. metric.label.instance_name
|
||||
</li>
|
||||
<li>
|
||||
<code>{`${'{{resource.label.label_name}}'}`}</code> = Resource label metadata e.g. resource.label.zone
|
||||
</li>
|
||||
<li>
|
||||
<code>{`${'{{metadata.system_labels.name}}'}`}</code> = Meta data system labels e.g.
|
||||
metadata.system_labels.name. For this to work, the needs to be included in the group by
|
||||
</li>
|
||||
<li>
|
||||
<code>{`${'{{metadata.user_labels.name}}'}`}</code> = Meta data user labels e.g.
|
||||
metadata.user_labels.name. For this to work, the needs to be included in the group by
|
||||
</li>
|
||||
<li>
|
||||
<code>{`${'{{bucket}}'}`}</code> = bucket boundary for distribution metrics when using a heatmap in
|
||||
Grafana
|
||||
</li>
|
||||
<li>
|
||||
<code>{`${'{{project}}'}`}</code> = The project name that was specified in the query editor
|
||||
</li>
|
||||
<li>
|
||||
<code>{`${'{{service}}'}`}</code> = The service id that was specified in the SLO query editor
|
||||
</li>
|
||||
<li>
|
||||
<code>{`${'{{slo}}'}`}</code> = The SLO id that was specified in the SLO query editor
|
||||
</li>
|
||||
<li>
|
||||
<code>{`${'{{selector}}'}`}</code> = The Selector function that was specified in the SLO query editor
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
@ -1,32 +1,8 @@
|
||||
import React, { InputHTMLAttributes, FunctionComponent } from 'react';
|
||||
import { InlineFormLabel, Select, InlineField } from '@grafana/ui';
|
||||
import React, { FC } from 'react';
|
||||
import { SelectableValue } from '@grafana/data';
|
||||
|
||||
export interface Props extends InputHTMLAttributes<HTMLInputElement> {
|
||||
label: string;
|
||||
tooltip?: string;
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
export const QueryField: FunctionComponent<Partial<Props>> = ({ label, tooltip, children }) => (
|
||||
<>
|
||||
<InlineFormLabel width={9} className="query-keyword" tooltip={tooltip}>
|
||||
{label}
|
||||
</InlineFormLabel>
|
||||
{children}
|
||||
</>
|
||||
);
|
||||
|
||||
export const QueryInlineField: FunctionComponent<Props> = ({ ...props }) => {
|
||||
return (
|
||||
<div className={'gf-form-inline'}>
|
||||
<QueryField {...props} />
|
||||
<div className="gf-form gf-form--grow">
|
||||
<div className="gf-form-label gf-form-label--grow" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
import { HorizontalGroup, InlineLabel, PopoverContent, Select, InlineField } from '@grafana/ui';
|
||||
import { css } from '@emotion/css';
|
||||
import { INNER_LABEL_WIDTH, LABEL_WIDTH } from '../constants';
|
||||
|
||||
interface VariableQueryFieldProps {
|
||||
onChange: (value: string) => void;
|
||||
@ -36,7 +12,7 @@ interface VariableQueryFieldProps {
|
||||
allowCustomValue?: boolean;
|
||||
}
|
||||
|
||||
export const VariableQueryField: FunctionComponent<VariableQueryFieldProps> = ({
|
||||
export const VariableQueryField: FC<VariableQueryFieldProps> = ({
|
||||
label,
|
||||
onChange,
|
||||
value,
|
||||
@ -55,3 +31,58 @@ export const VariableQueryField: FunctionComponent<VariableQueryFieldProps> = ({
|
||||
</InlineField>
|
||||
);
|
||||
};
|
||||
|
||||
export interface Props {
|
||||
children: React.ReactNode;
|
||||
tooltip?: PopoverContent;
|
||||
label?: React.ReactNode;
|
||||
className?: string;
|
||||
noFillEnd?: boolean;
|
||||
labelWidth?: number;
|
||||
fillComponent?: React.ReactNode;
|
||||
}
|
||||
|
||||
export const QueryEditorRow: FC<Props> = ({
|
||||
children,
|
||||
label,
|
||||
tooltip,
|
||||
fillComponent,
|
||||
noFillEnd = false,
|
||||
labelWidth = LABEL_WIDTH,
|
||||
...rest
|
||||
}) => {
|
||||
return (
|
||||
<div className="gf-form" {...rest}>
|
||||
{label && (
|
||||
<InlineLabel width={labelWidth} tooltip={tooltip}>
|
||||
{label}
|
||||
</InlineLabel>
|
||||
)}
|
||||
<div
|
||||
className={css`
|
||||
margin-right: 4px;
|
||||
`}
|
||||
>
|
||||
<HorizontalGroup spacing="xs" width="auto">
|
||||
{children}
|
||||
</HorizontalGroup>
|
||||
</div>
|
||||
<div className={'gf-form--grow'}>
|
||||
{noFillEnd || <div className={'gf-form-label gf-form-label--grow'}>{fillComponent}</div>}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const QueryEditorField: FC<Props> = ({ children, label, tooltip, labelWidth = INNER_LABEL_WIDTH, ...rest }) => {
|
||||
return (
|
||||
<>
|
||||
{label && (
|
||||
<InlineLabel width={labelWidth} tooltip={tooltip} {...rest}>
|
||||
{label}
|
||||
</InlineLabel>
|
||||
)}
|
||||
{children}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -0,0 +1,52 @@
|
||||
import React, { FunctionComponent, useMemo } from 'react';
|
||||
import { SelectableValue } from '@grafana/data';
|
||||
import { MultiSelect } from '@grafana/ui';
|
||||
import { labelsToGroupedOptions } from '../functions';
|
||||
import { SYSTEM_LABELS, INPUT_WIDTH } from '../constants';
|
||||
import { MetricDescriptor, MetricQuery } from '../types';
|
||||
import { Aggregation, QueryEditorRow } from '.';
|
||||
|
||||
export interface Props {
|
||||
variableOptionGroup: SelectableValue<string>;
|
||||
labels: string[];
|
||||
metricDescriptor?: MetricDescriptor;
|
||||
onChange: (query: MetricQuery) => void;
|
||||
query: MetricQuery;
|
||||
}
|
||||
|
||||
export const GroupBy: FunctionComponent<Props> = ({
|
||||
labels: groupBys = [],
|
||||
query,
|
||||
onChange,
|
||||
variableOptionGroup,
|
||||
metricDescriptor,
|
||||
}) => {
|
||||
const options = useMemo(() => [variableOptionGroup, ...labelsToGroupedOptions([...groupBys, ...SYSTEM_LABELS])], [
|
||||
groupBys,
|
||||
variableOptionGroup,
|
||||
]);
|
||||
|
||||
return (
|
||||
<QueryEditorRow
|
||||
label="Group by"
|
||||
tooltip="You can reduce the amount of data returned for a metric by combining different time series. To combine multiple time series, you can specify a grouping and a function. Grouping is done on the basis of labels. The grouping function is used to combine the time series in the group into a single time series."
|
||||
>
|
||||
<MultiSelect
|
||||
width={INPUT_WIDTH}
|
||||
placeholder="Choose label"
|
||||
options={options}
|
||||
value={query.groupBys ?? []}
|
||||
onChange={(options) => {
|
||||
onChange({ ...query, groupBys: options.map((o) => o.value!) });
|
||||
}}
|
||||
></MultiSelect>
|
||||
<Aggregation
|
||||
metricDescriptor={metricDescriptor}
|
||||
templateVariableOptions={variableOptionGroup.options}
|
||||
crossSeriesReducer={query.crossSeriesReducer}
|
||||
groupBys={query.groupBys ?? []}
|
||||
onChange={(crossSeriesReducer) => onChange({ ...query, crossSeriesReducer })}
|
||||
></Aggregation>
|
||||
</QueryEditorRow>
|
||||
);
|
||||
};
|
@ -1,58 +0,0 @@
|
||||
import React, { FunctionComponent } from 'react';
|
||||
import { SelectableValue } from '@grafana/data';
|
||||
import { Segment, Icon } 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">
|
||||
<Icon name="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>
|
||||
);
|
||||
};
|
@ -1,139 +0,0 @@
|
||||
import React from 'react';
|
||||
import { MetricDescriptor } from '../types';
|
||||
import { Icon } from '@grafana/ui';
|
||||
|
||||
export interface Props {
|
||||
rawQuery: string;
|
||||
lastQueryError?: string;
|
||||
metricDescriptor?: MetricDescriptor;
|
||||
}
|
||||
|
||||
interface State {
|
||||
displayHelp: boolean;
|
||||
displaRawQuery: boolean;
|
||||
}
|
||||
|
||||
export class Help extends React.Component<Props, State> {
|
||||
state: State = {
|
||||
displayHelp: false,
|
||||
displaRawQuery: false,
|
||||
};
|
||||
|
||||
onHelpClicked = () => {
|
||||
this.setState({ displayHelp: !this.state.displayHelp });
|
||||
};
|
||||
|
||||
onRawQueryClicked = () => {
|
||||
this.setState({ displaRawQuery: !this.state.displaRawQuery });
|
||||
};
|
||||
|
||||
shouldComponentUpdate(nextProps: Props) {
|
||||
return nextProps.metricDescriptor !== null;
|
||||
}
|
||||
|
||||
render() {
|
||||
const { displayHelp, displaRawQuery } = this.state;
|
||||
const { rawQuery, lastQueryError } = this.props;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="gf-form-inline">
|
||||
<div className="gf-form" onClick={this.onHelpClicked}>
|
||||
<label className="gf-form-label query-keyword pointer">
|
||||
Show Help <Icon name={displayHelp ? 'angle-down' : 'angle-right'} />
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{rawQuery && (
|
||||
<div className="gf-form" onClick={this.onRawQueryClicked}>
|
||||
<label className="gf-form-label query-keyword">
|
||||
Raw query
|
||||
<Icon
|
||||
name={displaRawQuery ? 'angle-down' : 'angle-right'}
|
||||
ng-show="ctrl.showHelp"
|
||||
style={{ marginTop: '3px' }}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="gf-form gf-form--grow">
|
||||
<div className="gf-form-label gf-form-label--grow" />
|
||||
</div>
|
||||
</div>
|
||||
{rawQuery && displaRawQuery && (
|
||||
<div className="gf-form">
|
||||
<pre className="gf-form-pre">{rawQuery}</pre>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{displayHelp && (
|
||||
<div className="gf-form grafana-info-box alert-info">
|
||||
<div>
|
||||
<h5>Alias Patterns</h5>Format the legend keys any way you want by using alias patterns. Format the legend
|
||||
keys any way you want by using alias patterns.
|
||||
<br /> <br />
|
||||
Example:
|
||||
<code>{`${'{{metric.name}} - {{metric.label.instance_name}}'}`}</code>
|
||||
<br />
|
||||
Result: <code>cpu/usage_time - server1-europe-west-1</code>
|
||||
<br />
|
||||
<br />
|
||||
<strong>Patterns</strong>
|
||||
<br />
|
||||
<ul>
|
||||
<li>
|
||||
<code>{`${'{{metric.type}}'}`}</code> = metric type e.g.
|
||||
compute.googleapis.com/instance/cpu/usage_time
|
||||
</li>
|
||||
<li>
|
||||
<code>{`${'{{metric.name}}'}`}</code> = name part of metric e.g. instance/cpu/usage_time
|
||||
</li>
|
||||
<li>
|
||||
<code>{`${'{{metric.service}}'}`}</code> = service part of metric e.g. compute
|
||||
</li>
|
||||
<li>
|
||||
<code>{`${'{{metric.label.label_name}}'}`}</code> = Metric label metadata e.g.
|
||||
metric.label.instance_name
|
||||
</li>
|
||||
<li>
|
||||
<code>{`${'{{resource.label.label_name}}'}`}</code> = Resource label metadata e.g. resource.label.zone
|
||||
</li>
|
||||
<li>
|
||||
<code>{`${'{{metadata.system_labels.name}}'}`}</code> = Meta data system labels e.g.
|
||||
metadata.system_labels.name. For this to work, the needs to be included in the group by
|
||||
</li>
|
||||
<li>
|
||||
<code>{`${'{{metadata.user_labels.name}}'}`}</code> = Meta data user labels e.g.
|
||||
metadata.user_labels.name. For this to work, the needs to be included in the group by
|
||||
</li>
|
||||
<li>
|
||||
<code>{`${'{{bucket}}'}`}</code> = bucket boundary for distribution metrics when using a heatmap in
|
||||
Grafana
|
||||
</li>
|
||||
<li>
|
||||
<code>{`${'{{project}}'}`}</code> = The project name that was specified in the query editor
|
||||
</li>
|
||||
<li>
|
||||
<code>{`${'{{service}}'}`}</code> = The service id that was specified in the SLO query editor
|
||||
</li>
|
||||
<li>
|
||||
<code>{`${'{{slo}}'}`}</code> = The SLO id that was specified in the SLO query editor
|
||||
</li>
|
||||
<li>
|
||||
<code>{`${'{{selector}}'}`}</code> = The Selector function that was specified in the SLO query editor
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{lastQueryError && (
|
||||
<div className="gf-form">
|
||||
<pre className="gf-form-pre alert alert-error">{lastQueryError}</pre>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
@ -1,8 +1,13 @@
|
||||
import React, { FunctionComponent, Fragment } from 'react';
|
||||
import React, { FunctionComponent, useCallback, useMemo } from 'react';
|
||||
import { flatten } from 'lodash';
|
||||
|
||||
import { SelectableValue } from '@grafana/data';
|
||||
import { Segment, Icon } from '@grafana/ui';
|
||||
import { labelsToGroupedOptions, filtersToStringArray, stringArrayToFilters, toOption } from '../functions';
|
||||
import { CustomControlProps } from '@grafana/ui/src/components/Select/types';
|
||||
import { Button, HorizontalGroup, Select, VerticalGroup } from '@grafana/ui';
|
||||
import { labelsToGroupedOptions, stringArrayToFilters, toOption } from '../functions';
|
||||
import { Filter } from '../types';
|
||||
import { SELECT_WIDTH } from '../constants';
|
||||
import { QueryEditorRow } from '.';
|
||||
|
||||
export interface Props {
|
||||
labels: { [key: string]: string[] };
|
||||
@ -11,82 +16,114 @@ export interface Props {
|
||||
variableOptionGroup: SelectableValue<string>;
|
||||
}
|
||||
|
||||
const removeText = '-- remove filter --';
|
||||
const removeOption: SelectableValue<string> = { label: removeText, value: removeText, icon: 'times' };
|
||||
const operators = ['=', '!=', '=~', '!=~'];
|
||||
|
||||
const FilterButton = React.forwardRef<HTMLButtonElement, CustomControlProps<string>>(
|
||||
({ value, isOpen, invalid, ...rest }, ref) => {
|
||||
return <Button ref={ref} {...rest} variant="secondary" icon="plus"></Button>;
|
||||
}
|
||||
);
|
||||
FilterButton.displayName = 'FilterButton';
|
||||
|
||||
const OperatorButton = React.forwardRef<HTMLButtonElement, CustomControlProps<string>>(({ value, ...rest }, ref) => {
|
||||
return (
|
||||
<Button ref={ref} {...rest} variant="secondary">
|
||||
<span className="query-segment-operator">{value?.label}</span>
|
||||
</Button>
|
||||
);
|
||||
});
|
||||
OperatorButton.displayName = 'OperatorButton';
|
||||
|
||||
export const LabelFilter: FunctionComponent<Props> = ({
|
||||
labels = {},
|
||||
filters: filterArray,
|
||||
onChange,
|
||||
variableOptionGroup,
|
||||
}) => {
|
||||
const filters = stringArrayToFilters(filterArray);
|
||||
const filters = useMemo(() => stringArrayToFilters(filterArray), [filterArray]);
|
||||
const options = useMemo(() => [variableOptionGroup, ...labelsToGroupedOptions(Object.keys(labels))], [
|
||||
labels,
|
||||
variableOptionGroup,
|
||||
]);
|
||||
|
||||
const options = [removeOption, variableOptionGroup, ...labelsToGroupedOptions(Object.keys(labels))];
|
||||
const filtersToStringArray = useCallback((filters: Filter[]) => {
|
||||
const strArr = flatten(filters.map(({ key, operator, value, condition }) => [key, operator, value, condition!]));
|
||||
return strArr.slice(0, strArr.length - 1);
|
||||
}, []);
|
||||
|
||||
const AddFilter = () => {
|
||||
return (
|
||||
<Select
|
||||
allowCustomValue
|
||||
options={[variableOptionGroup, ...labelsToGroupedOptions(Object.keys(labels))]}
|
||||
onChange={({ value: key = '' }) =>
|
||||
onChange(filtersToStringArray([...filters, { key, operator: '=', condition: 'AND', value: '' }]))
|
||||
}
|
||||
menuPlacement="bottom"
|
||||
renderControl={FilterButton}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
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 {
|
||||
<QueryEditorRow
|
||||
label="Filter"
|
||||
tooltip={
|
||||
'To reduce the amount of data charted, apply a filter. A filter has three components: a label, a comparison, and a value. The comparison can be an equality, inequality, or regular expression.'
|
||||
}
|
||||
noFillEnd={filters.length > 1}
|
||||
>
|
||||
<VerticalGroup spacing="xs" width="auto">
|
||||
{filters.map(({ key, operator, value, condition }, index) => (
|
||||
<HorizontalGroup key={index} spacing="xs" width="auto">
|
||||
<Select
|
||||
width={SELECT_WIDTH}
|
||||
allowCustomValue
|
||||
formatCreateLabel={(v) => `Use label key: ${v}`}
|
||||
value={key}
|
||||
options={options}
|
||||
onChange={({ value: key = '' }) => {
|
||||
onChange(
|
||||
filtersToStringArray(
|
||||
filters.map((f, i) => (i === index ? { key, operator, condition, value: '' } : f))
|
||||
)
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<Select
|
||||
value={operator}
|
||||
options={operators.map(toOption)}
|
||||
onChange={({ value: operator = '=' }) =>
|
||||
onChange(filtersToStringArray(filters.map((f, i) => (i === index ? { ...f, operator } : 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">
|
||||
<Icon name="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>
|
||||
menuPlacement="bottom"
|
||||
renderControl={OperatorButton}
|
||||
/>
|
||||
<Select
|
||||
width={SELECT_WIDTH}
|
||||
formatCreateLabel={(v) => `Use label value: ${v}`}
|
||||
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))))
|
||||
}
|
||||
/>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="md"
|
||||
icon="trash-alt"
|
||||
aria-label="Remove"
|
||||
onClick={() => onChange(filtersToStringArray(filters.filter((_, i) => i !== index)))}
|
||||
></Button>
|
||||
{index + 1 === filters.length && Object.values(filters).every(({ value }) => value) && <AddFilter />}
|
||||
</HorizontalGroup>
|
||||
))}
|
||||
{!filters.length && <AddFilter />}
|
||||
</VerticalGroup>
|
||||
</QueryEditorRow>
|
||||
);
|
||||
};
|
||||
|
@ -1,14 +1,23 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import { SelectableValue } from '@grafana/data';
|
||||
import { Project, VisualMetricQueryEditor, AliasBy } from '.';
|
||||
import { MetricQuery, MetricDescriptor, EditorMode } from '../types';
|
||||
import {
|
||||
MetricQuery,
|
||||
MetricDescriptor,
|
||||
EditorMode,
|
||||
MetricKind,
|
||||
PreprocessorType,
|
||||
AlignmentTypes,
|
||||
CustomMetaData,
|
||||
ValueTypes,
|
||||
} from '../types';
|
||||
import { getAlignmentPickerData } from '../functions';
|
||||
import CloudMonitoringDatasource from '../datasource';
|
||||
import { SelectableValue } from '@grafana/data';
|
||||
import { MQLQueryEditor } from './MQLQueryEditor';
|
||||
|
||||
export interface Props {
|
||||
refId: string;
|
||||
usedAlignmentPeriod?: number;
|
||||
customMetaData: CustomMetaData;
|
||||
variableOptionGroup: SelectableValue<string>;
|
||||
onChange: (query: MetricQuery) => void;
|
||||
onRunQuery: () => void;
|
||||
@ -29,16 +38,16 @@ export const defaultQuery: (dataSource: CloudMonitoringDatasource) => MetricQuer
|
||||
editorMode: EditorMode.Visual,
|
||||
projectName: dataSource.getDefaultProject(),
|
||||
metricType: '',
|
||||
metricKind: '',
|
||||
metricKind: MetricKind.GAUGE,
|
||||
valueType: '',
|
||||
unit: '',
|
||||
crossSeriesReducer: 'REDUCE_MEAN',
|
||||
alignmentPeriod: 'cloud-monitoring-auto',
|
||||
perSeriesAligner: 'ALIGN_MEAN',
|
||||
perSeriesAligner: AlignmentTypes.ALIGN_MEAN,
|
||||
groupBys: [],
|
||||
filters: [],
|
||||
aliasBy: '',
|
||||
query: '',
|
||||
preprocessor: PreprocessorType.None,
|
||||
});
|
||||
|
||||
function Editor({
|
||||
@ -47,7 +56,7 @@ function Editor({
|
||||
datasource,
|
||||
onChange: onQueryChange,
|
||||
onRunQuery,
|
||||
usedAlignmentPeriod,
|
||||
customMetaData,
|
||||
variableOptionGroup,
|
||||
}: React.PropsWithChildren<Props>) {
|
||||
const [state, setState] = useState<State>(defaultState);
|
||||
@ -61,22 +70,32 @@ function Editor({
|
||||
}
|
||||
}, [datasource, groupBys, metricType, projectName, refId]);
|
||||
|
||||
const onChange = (metricQuery: MetricQuery) => {
|
||||
onQueryChange({ ...query, ...metricQuery });
|
||||
onRunQuery();
|
||||
};
|
||||
const onChange = useCallback(
|
||||
(metricQuery: MetricQuery) => {
|
||||
onQueryChange({ ...query, ...metricQuery });
|
||||
onRunQuery();
|
||||
},
|
||||
[onQueryChange, onRunQuery, query]
|
||||
);
|
||||
|
||||
const onMetricTypeChange = async ({ valueType, metricKind, type, unit }: MetricDescriptor) => {
|
||||
const { perSeriesAligner, alignOptions } = getAlignmentPickerData(
|
||||
{ valueType, metricKind, perSeriesAligner: state.perSeriesAligner },
|
||||
datasource.templateSrv
|
||||
);
|
||||
setState({
|
||||
...state,
|
||||
alignOptions,
|
||||
});
|
||||
onChange({ ...query, perSeriesAligner, metricType: type, unit, valueType, metricKind });
|
||||
};
|
||||
const onMetricTypeChange = useCallback(
|
||||
({ valueType, metricKind, type }: MetricDescriptor) => {
|
||||
const preprocessor =
|
||||
metricKind === MetricKind.GAUGE || valueType === ValueTypes.DISTRIBUTION
|
||||
? PreprocessorType.None
|
||||
: PreprocessorType.Rate;
|
||||
const { perSeriesAligner } = getAlignmentPickerData(valueType, metricKind, state.perSeriesAligner, preprocessor);
|
||||
onChange({
|
||||
...query,
|
||||
perSeriesAligner,
|
||||
metricType: type,
|
||||
valueType,
|
||||
metricKind,
|
||||
preprocessor,
|
||||
});
|
||||
},
|
||||
[onChange, query, state]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
@ -93,7 +112,7 @@ function Editor({
|
||||
<VisualMetricQueryEditor
|
||||
labels={state.labels}
|
||||
variableOptionGroup={variableOptionGroup}
|
||||
usedAlignmentPeriod={usedAlignmentPeriod}
|
||||
customMetaData={customMetaData}
|
||||
onMetricTypeChange={onMetricTypeChange}
|
||||
onChange={onChange}
|
||||
datasource={datasource}
|
||||
|
@ -1,10 +1,12 @@
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { startCase, uniqBy } from 'lodash';
|
||||
|
||||
import { Select } from '@grafana/ui';
|
||||
import { TemplateSrv } from '@grafana/runtime';
|
||||
import { SelectableValue } from '@grafana/data';
|
||||
import { QueryEditorRow, QueryEditorField } from '.';
|
||||
import CloudMonitoringDatasource from '../datasource';
|
||||
import { Segment } from '@grafana/ui';
|
||||
import { INNER_LABEL_WIDTH, LABEL_WIDTH, SELECT_WIDTH } from '../constants';
|
||||
import { MetricDescriptor } from '../types';
|
||||
|
||||
export interface Props {
|
||||
@ -118,44 +120,39 @@ export function Metrics(props: Props) {
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="gf-form-inline">
|
||||
<span className="gf-form-label width-9 query-keyword">Service</span>
|
||||
<Segment
|
||||
onChange={onServiceChange}
|
||||
value={[...services, ...templateVariableOptions].find((s) => s.value === service)}
|
||||
options={[
|
||||
{
|
||||
label: 'Template Variables',
|
||||
options: templateVariableOptions,
|
||||
},
|
||||
...services,
|
||||
]}
|
||||
placeholder="Select Services"
|
||||
></Segment>
|
||||
<div className="gf-form gf-form--grow">
|
||||
<div className="gf-form-label gf-form-label--grow" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="gf-form-inline">
|
||||
<span className="gf-form-label width-9 query-keyword">Metric</span>
|
||||
<QueryEditorRow>
|
||||
<QueryEditorField labelWidth={LABEL_WIDTH} label="Service">
|
||||
<Select
|
||||
width={SELECT_WIDTH}
|
||||
onChange={onServiceChange}
|
||||
value={[...services, ...templateVariableOptions].find((s) => s.value === service)}
|
||||
options={[
|
||||
{
|
||||
label: 'Template Variables',
|
||||
options: templateVariableOptions,
|
||||
},
|
||||
...services,
|
||||
]}
|
||||
placeholder="Select Services"
|
||||
></Select>
|
||||
</QueryEditorField>
|
||||
<QueryEditorField label="Metric name" labelWidth={INNER_LABEL_WIDTH}>
|
||||
<Select
|
||||
width={SELECT_WIDTH}
|
||||
onChange={onMetricTypeChange}
|
||||
value={[...metrics, ...templateVariableOptions].find((s) => s.value === metricType)}
|
||||
options={[
|
||||
{
|
||||
label: 'Template Variables',
|
||||
options: templateVariableOptions,
|
||||
},
|
||||
...metrics,
|
||||
]}
|
||||
placeholder="Select Metric"
|
||||
></Select>
|
||||
</QueryEditorField>
|
||||
</QueryEditorRow>
|
||||
|
||||
<Segment
|
||||
className="query-part"
|
||||
onChange={onMetricTypeChange}
|
||||
value={[...metrics, ...templateVariableOptions].find((s) => s.value === metricType)}
|
||||
options={[
|
||||
{
|
||||
label: 'Template Variables',
|
||||
options: templateVariableOptions,
|
||||
},
|
||||
...metrics,
|
||||
]}
|
||||
placeholder="Select Metric"
|
||||
></Segment>
|
||||
<div className="gf-form gf-form--grow">
|
||||
<div className="gf-form-label gf-form-label--grow" />
|
||||
</div>
|
||||
</div>
|
||||
{children(state.metricDescriptor)}
|
||||
</>
|
||||
);
|
||||
|
@ -0,0 +1,65 @@
|
||||
import React, { FunctionComponent, useMemo } from 'react';
|
||||
import { SelectableValue } from '@grafana/data';
|
||||
import { RadioButtonGroup } from '@grafana/ui';
|
||||
import { MetricDescriptor, MetricKind, MetricQuery, PreprocessorType, ValueTypes } from '../types';
|
||||
import { getAlignmentPickerData } from '../functions';
|
||||
import { QueryEditorRow } from '.';
|
||||
|
||||
const NONE_OPTION = { label: 'None', value: PreprocessorType.None };
|
||||
|
||||
export interface Props {
|
||||
metricDescriptor?: MetricDescriptor;
|
||||
onChange: (query: MetricQuery) => void;
|
||||
query: MetricQuery;
|
||||
}
|
||||
|
||||
export const Preprocessor: FunctionComponent<Props> = ({ query, metricDescriptor, onChange }) => {
|
||||
const options = useOptions(metricDescriptor);
|
||||
return (
|
||||
<QueryEditorRow
|
||||
label="Pre-processing"
|
||||
tooltip="Preprocessing options are displayed when the selected metric has a metric kind of delta or cumulative. The specific options available are determined by the metic's value type. If you select 'Rate', data points are aligned and converted to a rate per time series. If you select 'Delta', data points are aligned by their delta (difference) per time series"
|
||||
>
|
||||
<RadioButtonGroup
|
||||
onChange={(value: PreprocessorType) => {
|
||||
const { valueType, metricKind, perSeriesAligner: psa } = query;
|
||||
const { perSeriesAligner } = getAlignmentPickerData(valueType, metricKind, psa, value);
|
||||
onChange({ ...query, preprocessor: value, perSeriesAligner });
|
||||
}}
|
||||
value={query.preprocessor ?? PreprocessorType.None}
|
||||
options={options}
|
||||
></RadioButtonGroup>
|
||||
</QueryEditorRow>
|
||||
);
|
||||
};
|
||||
|
||||
const useOptions = (metricDescriptor?: MetricDescriptor): Array<SelectableValue<string>> => {
|
||||
const metricKind = metricDescriptor?.metricKind;
|
||||
const valueType = metricDescriptor?.valueType;
|
||||
|
||||
return useMemo(() => {
|
||||
if (!metricKind || metricKind === MetricKind.GAUGE || valueType === ValueTypes.DISTRIBUTION) {
|
||||
return [NONE_OPTION];
|
||||
}
|
||||
|
||||
const options = [
|
||||
NONE_OPTION,
|
||||
{
|
||||
label: 'Rate',
|
||||
value: PreprocessorType.Rate,
|
||||
description: 'Data points are aligned and converted to a rate per time series',
|
||||
},
|
||||
];
|
||||
|
||||
return metricKind === MetricKind.CUMULATIVE
|
||||
? [
|
||||
...options,
|
||||
{
|
||||
label: 'Delta',
|
||||
value: PreprocessorType.Delta,
|
||||
description: 'Data points are aligned by their delta (difference) per time series',
|
||||
},
|
||||
]
|
||||
: options;
|
||||
}, [metricKind, valueType]);
|
||||
};
|
@ -1,7 +1,9 @@
|
||||
import React from 'react';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { SelectableValue } from '@grafana/data';
|
||||
import { SegmentAsync } from '@grafana/ui';
|
||||
import { Select } from '@grafana/ui';
|
||||
import CloudMonitoringDatasource from '../datasource';
|
||||
import { SELECT_WIDTH } from '../constants';
|
||||
import { QueryEditorRow } from '.';
|
||||
|
||||
export interface Props {
|
||||
datasource: CloudMonitoringDatasource;
|
||||
@ -11,27 +13,30 @@ export interface Props {
|
||||
}
|
||||
|
||||
export function Project({ projectName, datasource, onChange, templateVariableOptions }: Props) {
|
||||
const [projects, setProjects] = useState<Array<SelectableValue<string>>>([]);
|
||||
useEffect(() => {
|
||||
datasource.getProjects().then((projects) =>
|
||||
setProjects([
|
||||
{
|
||||
label: 'Template Variables',
|
||||
options: templateVariableOptions,
|
||||
},
|
||||
...projects,
|
||||
])
|
||||
);
|
||||
}, [datasource, templateVariableOptions]);
|
||||
|
||||
return (
|
||||
<div className="gf-form-inline">
|
||||
<span className="gf-form-label width-9 query-keyword">Project</span>
|
||||
<SegmentAsync
|
||||
<QueryEditorRow label="Project">
|
||||
<Select
|
||||
width={SELECT_WIDTH}
|
||||
allowCustomValue
|
||||
formatCreateLabel={(v) => `Use project: ${v}`}
|
||||
onChange={({ value }) => onChange(value!)}
|
||||
loadOptions={() =>
|
||||
datasource.getProjects().then((projects) => [
|
||||
{
|
||||
label: 'Template Variables',
|
||||
options: templateVariableOptions,
|
||||
},
|
||||
...projects,
|
||||
])
|
||||
}
|
||||
value={projectName}
|
||||
options={projects}
|
||||
value={{ value: projectName, label: projectName }}
|
||||
placeholder="Select Project"
|
||||
/>
|
||||
<div className="gf-form gf-form--grow">
|
||||
<div className="gf-form-label gf-form-label--grow" />
|
||||
</div>
|
||||
</div>
|
||||
</QueryEditorRow>
|
||||
);
|
||||
}
|
||||
|
@ -1,24 +1,18 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import appEvents from 'app/core/app_events';
|
||||
import { CoreEvents } from 'app/types';
|
||||
import { ExploreQueryFieldProps, SelectableValue } from '@grafana/data';
|
||||
import { Segment } from '@grafana/ui';
|
||||
import { Help, MetricQueryEditor, SLOQueryEditor } from './';
|
||||
import { CloudMonitoringQuery, MetricQuery, QueryType, SLOQuery, queryTypes, EditorMode } from '../types';
|
||||
import { css } from '@emotion/css';
|
||||
import { ExploreQueryFieldProps } from '@grafana/data';
|
||||
import { Button, Select } from '@grafana/ui';
|
||||
import { MetricQueryEditor, SLOQueryEditor, QueryEditorRow } from './';
|
||||
import { CloudMonitoringQuery, MetricQuery, QueryType, SLOQuery, EditorMode } from '../types';
|
||||
import { SELECT_WIDTH, QUERY_TYPES } from '../constants';
|
||||
import { defaultQuery } from './MetricQueryEditor';
|
||||
import { defaultQuery as defaultSLOQuery } from './SLOQueryEditor';
|
||||
import { formatCloudMonitoringError, toOption } from '../functions';
|
||||
import { defaultQuery as defaultSLOQuery } from './SLO/SLOQueryEditor';
|
||||
import { toOption } from '../functions';
|
||||
import CloudMonitoringDatasource from '../datasource';
|
||||
|
||||
export type Props = ExploreQueryFieldProps<CloudMonitoringDatasource, CloudMonitoringQuery>;
|
||||
|
||||
interface State {
|
||||
lastQueryError: string;
|
||||
}
|
||||
|
||||
export class QueryEditor extends PureComponent<Props, State> {
|
||||
state: State = { lastQueryError: '' };
|
||||
|
||||
export class QueryEditor extends PureComponent<Props> {
|
||||
async UNSAFE_componentWillMount() {
|
||||
const { datasource, query } = this.props;
|
||||
|
||||
@ -39,24 +33,6 @@ export class QueryEditor extends PureComponent<Props, State> {
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
appEvents.on(CoreEvents.dsRequestError, this.onDataError.bind(this));
|
||||
appEvents.on(CoreEvents.dsRequestResponse, this.onDataResponse.bind(this));
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
appEvents.off(CoreEvents.dsRequestResponse, this.onDataResponse.bind(this));
|
||||
appEvents.on(CoreEvents.dsRequestError, this.onDataError.bind(this));
|
||||
}
|
||||
|
||||
onDataResponse() {
|
||||
this.setState({ lastQueryError: '' });
|
||||
}
|
||||
|
||||
onDataError(error: any) {
|
||||
this.setState({ lastQueryError: formatCloudMonitoringError(error) });
|
||||
}
|
||||
|
||||
onQueryChange(prop: string, value: any) {
|
||||
this.props.onChange({ ...this.props.query, [prop]: value });
|
||||
this.props.onRunQuery();
|
||||
@ -68,7 +44,7 @@ export class QueryEditor extends PureComponent<Props, State> {
|
||||
const sloQuery = { ...defaultSLOQuery(datasource), ...query.sloQuery };
|
||||
const queryType = query.queryType || QueryType.METRICS;
|
||||
const meta = this.props.data?.series.length ? this.props.data?.series[0].meta : {};
|
||||
const usedAlignmentPeriod = meta?.alignmentPeriod;
|
||||
const customMetaData = meta?.custom ?? {};
|
||||
const variableOptionGroup = {
|
||||
label: 'Template Variables',
|
||||
expanded: false,
|
||||
@ -77,48 +53,44 @@ export class QueryEditor extends PureComponent<Props, State> {
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="gf-form-inline">
|
||||
<label className="gf-form-label query-keyword width-9">Query Type</label>
|
||||
<Segment
|
||||
value={[...queryTypes, ...variableOptionGroup.options].find((qt) => qt.value === queryType)}
|
||||
options={[
|
||||
...queryTypes,
|
||||
{
|
||||
label: 'Template Variables',
|
||||
options: variableOptionGroup.options,
|
||||
},
|
||||
]}
|
||||
onChange={({ value }: SelectableValue<QueryType>) => {
|
||||
<QueryEditorRow
|
||||
label="Query type"
|
||||
fillComponent={
|
||||
query.queryType !== QueryType.SLO && (
|
||||
<Button
|
||||
variant="secondary"
|
||||
className={css`
|
||||
margin-left: auto;
|
||||
`}
|
||||
icon="edit"
|
||||
onClick={() =>
|
||||
this.onQueryChange('metricQuery', {
|
||||
...metricQuery,
|
||||
editorMode: metricQuery.editorMode === EditorMode.MQL ? EditorMode.Visual : EditorMode.MQL,
|
||||
})
|
||||
}
|
||||
>
|
||||
{metricQuery.editorMode === EditorMode.MQL ? 'Switch to builder' : 'Edit MQL'}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
>
|
||||
<Select
|
||||
width={SELECT_WIDTH}
|
||||
value={queryType}
|
||||
options={QUERY_TYPES}
|
||||
onChange={({ value }) => {
|
||||
onChange({ ...query, sloQuery, queryType: value! });
|
||||
onRunQuery();
|
||||
}}
|
||||
/>
|
||||
|
||||
{query.queryType !== QueryType.SLO && (
|
||||
<button
|
||||
className="gf-form-label "
|
||||
onClick={() =>
|
||||
this.onQueryChange('metricQuery', {
|
||||
...metricQuery,
|
||||
editorMode: metricQuery.editorMode === EditorMode.MQL ? EditorMode.Visual : EditorMode.MQL,
|
||||
})
|
||||
}
|
||||
>
|
||||
<span className="query-keyword">{'<>'}</span>
|
||||
{metricQuery.editorMode === EditorMode.MQL ? 'Switch to builder' : 'Edit MQL'}
|
||||
</button>
|
||||
)}
|
||||
|
||||
<div className="gf-form gf-form--grow">
|
||||
<label className="gf-form-label gf-form-label--grow"></label>
|
||||
</div>
|
||||
</div>
|
||||
</QueryEditorRow>
|
||||
|
||||
{queryType === QueryType.METRICS && (
|
||||
<MetricQueryEditor
|
||||
refId={query.refId}
|
||||
variableOptionGroup={variableOptionGroup}
|
||||
usedAlignmentPeriod={usedAlignmentPeriod}
|
||||
customMetaData={customMetaData}
|
||||
onChange={(metricQuery: MetricQuery) => {
|
||||
this.props.onChange({ ...this.props.query, metricQuery });
|
||||
}}
|
||||
@ -131,18 +103,13 @@ export class QueryEditor extends PureComponent<Props, State> {
|
||||
{queryType === QueryType.SLO && (
|
||||
<SLOQueryEditor
|
||||
variableOptionGroup={variableOptionGroup}
|
||||
usedAlignmentPeriod={usedAlignmentPeriod}
|
||||
customMetaData={customMetaData}
|
||||
onChange={(query: SLOQuery) => this.onQueryChange('sloQuery', query)}
|
||||
onRunQuery={onRunQuery}
|
||||
datasource={datasource}
|
||||
query={sloQuery}
|
||||
></SLOQueryEditor>
|
||||
)}
|
||||
|
||||
<Help
|
||||
rawQuery={decodeURIComponent(meta?.executedQueryString ?? '')}
|
||||
lastQueryError={this.state.lastQueryError}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@ -1,7 +1,8 @@
|
||||
import React, { FunctionComponent } from 'react';
|
||||
import { SelectableValue } from '@grafana/data';
|
||||
import { Segment } from '@grafana/ui';
|
||||
import { QueryType, queryTypes } from '../types';
|
||||
import { QueryType } from '../types';
|
||||
import { QUERY_TYPES } from '../constants';
|
||||
|
||||
export interface Props {
|
||||
value: QueryType;
|
||||
@ -14,9 +15,9 @@ export const QueryTypeSelector: FunctionComponent<Props> = ({ onChange, value, t
|
||||
<div className="gf-form-inline">
|
||||
<label className="gf-form-label query-keyword width-9">Query Type</label>
|
||||
<Segment
|
||||
value={[...queryTypes, ...templateVariableOptions].find((qt) => qt.value === value)}
|
||||
value={[...QUERY_TYPES, ...templateVariableOptions].find((qt) => qt.value === value)}
|
||||
options={[
|
||||
...queryTypes,
|
||||
...QUERY_TYPES,
|
||||
{
|
||||
label: 'Template Variables',
|
||||
options: templateVariableOptions,
|
||||
|
@ -0,0 +1,52 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Select } from '@grafana/ui';
|
||||
import { SelectableValue } from '@grafana/data';
|
||||
import { QueryEditorRow } from '..';
|
||||
import CloudMonitoringDatasource from '../../datasource';
|
||||
import { SLOQuery } from '../../types';
|
||||
import { SELECT_WIDTH } from '../../constants';
|
||||
|
||||
export interface Props {
|
||||
onChange: (query: SLOQuery) => void;
|
||||
query: SLOQuery;
|
||||
templateVariableOptions: Array<SelectableValue<string>>;
|
||||
datasource: CloudMonitoringDatasource;
|
||||
}
|
||||
|
||||
export const SLO: React.FC<Props> = ({ query, templateVariableOptions, onChange, datasource }) => {
|
||||
const [slos, setSLOs] = useState<Array<SelectableValue<string>>>([]);
|
||||
const { projectName, serviceId } = query;
|
||||
|
||||
useEffect(() => {
|
||||
if (!projectName || !serviceId) {
|
||||
return;
|
||||
}
|
||||
|
||||
datasource.getServiceLevelObjectives(projectName, serviceId).then((sloIds: Array<SelectableValue<string>>) => {
|
||||
setSLOs([
|
||||
{
|
||||
label: 'Template Variables',
|
||||
options: templateVariableOptions,
|
||||
},
|
||||
...sloIds,
|
||||
]);
|
||||
});
|
||||
}, [datasource, projectName, serviceId, templateVariableOptions]);
|
||||
|
||||
return (
|
||||
<QueryEditorRow label="SLO">
|
||||
<Select
|
||||
width={SELECT_WIDTH}
|
||||
allowCustomValue
|
||||
value={query?.sloId && { value: query?.sloId, label: query?.sloName || query?.sloId }}
|
||||
placeholder="Select SLO"
|
||||
options={slos}
|
||||
onChange={async ({ value: sloId = '', label: sloName = '' }) => {
|
||||
const slos = await datasource.getServiceLevelObjectives(projectName, serviceId);
|
||||
const slo = slos.find(({ value }) => value === datasource.templateSrv.replace(sloId));
|
||||
onChange({ ...query, sloId, sloName, goal: slo?.goal });
|
||||
}}
|
||||
/>
|
||||
</QueryEditorRow>
|
||||
);
|
||||
};
|
@ -0,0 +1,80 @@
|
||||
import React from 'react';
|
||||
import { SelectableValue } from '@grafana/data';
|
||||
import { Project, AliasBy, AlignmentPeriod, AlignmentPeriodLabel, QueryEditorRow } from '..';
|
||||
import { AlignmentTypes, CustomMetaData, SLOQuery } from '../../types';
|
||||
import CloudMonitoringDatasource from '../../datasource';
|
||||
import { Selector, Service, SLO } from '.';
|
||||
import { SELECT_WIDTH } from '../../constants';
|
||||
|
||||
export interface Props {
|
||||
customMetaData: CustomMetaData;
|
||||
variableOptionGroup: SelectableValue<string>;
|
||||
onChange: (query: SLOQuery) => void;
|
||||
onRunQuery: () => void;
|
||||
query: SLOQuery;
|
||||
datasource: CloudMonitoringDatasource;
|
||||
}
|
||||
|
||||
export const defaultQuery: (dataSource: CloudMonitoringDatasource) => SLOQuery = (dataSource) => ({
|
||||
projectName: dataSource.getDefaultProject(),
|
||||
alignmentPeriod: 'cloud-monitoring-auto',
|
||||
perSeriesAligner: AlignmentTypes.ALIGN_MEAN,
|
||||
aliasBy: '',
|
||||
selectorName: 'select_slo_health',
|
||||
serviceId: '',
|
||||
serviceName: '',
|
||||
sloId: '',
|
||||
sloName: '',
|
||||
});
|
||||
|
||||
export function SLOQueryEditor({
|
||||
query,
|
||||
datasource,
|
||||
onChange,
|
||||
variableOptionGroup,
|
||||
customMetaData,
|
||||
}: React.PropsWithChildren<Props>) {
|
||||
return (
|
||||
<>
|
||||
<Project
|
||||
templateVariableOptions={variableOptionGroup.options}
|
||||
projectName={query.projectName}
|
||||
datasource={datasource}
|
||||
onChange={(projectName) => onChange({ ...query, projectName })}
|
||||
/>
|
||||
<Service
|
||||
datasource={datasource}
|
||||
templateVariableOptions={variableOptionGroup.options}
|
||||
query={query}
|
||||
onChange={onChange}
|
||||
></Service>
|
||||
<SLO
|
||||
datasource={datasource}
|
||||
templateVariableOptions={variableOptionGroup.options}
|
||||
query={query}
|
||||
onChange={onChange}
|
||||
></SLO>
|
||||
<Selector
|
||||
datasource={datasource}
|
||||
templateVariableOptions={variableOptionGroup.options}
|
||||
query={query}
|
||||
onChange={onChange}
|
||||
></Selector>
|
||||
|
||||
<QueryEditorRow label="Alignment period">
|
||||
<AlignmentPeriod
|
||||
templateVariableOptions={variableOptionGroup.options}
|
||||
query={{
|
||||
...query,
|
||||
perSeriesAligner: query.selectorName === 'select_slo_health' ? 'ALIGN_MEAN' : 'ALIGN_NEXT_OLDER',
|
||||
}}
|
||||
onChange={onChange}
|
||||
selectWidth={SELECT_WIDTH}
|
||||
/>
|
||||
<AlignmentPeriodLabel datasource={datasource} customMetaData={customMetaData} />
|
||||
</QueryEditorRow>
|
||||
|
||||
<AliasBy value={query.aliasBy} onChange={(aliasBy) => onChange({ ...query, aliasBy })} />
|
||||
</>
|
||||
);
|
||||
}
|
@ -0,0 +1,34 @@
|
||||
import React from 'react';
|
||||
import { Select } from '@grafana/ui';
|
||||
import { SelectableValue } from '@grafana/data';
|
||||
import { QueryEditorRow } from '..';
|
||||
import CloudMonitoringDatasource from '../../datasource';
|
||||
import { SLOQuery } from '../../types';
|
||||
import { SELECT_WIDTH, SELECTORS } from '../../constants';
|
||||
|
||||
export interface Props {
|
||||
onChange: (query: SLOQuery) => void;
|
||||
query: SLOQuery;
|
||||
templateVariableOptions: Array<SelectableValue<string>>;
|
||||
datasource: CloudMonitoringDatasource;
|
||||
}
|
||||
|
||||
export const Selector: React.FC<Props> = ({ query, templateVariableOptions, onChange, datasource }) => {
|
||||
return (
|
||||
<QueryEditorRow label="Selector">
|
||||
<Select
|
||||
width={SELECT_WIDTH}
|
||||
allowCustomValue
|
||||
value={[...SELECTORS, ...templateVariableOptions].find((s) => s.value === query?.selectorName ?? '')}
|
||||
options={[
|
||||
{
|
||||
label: 'Template Variables',
|
||||
options: templateVariableOptions,
|
||||
},
|
||||
...SELECTORS,
|
||||
]}
|
||||
onChange={({ value: selectorName }) => onChange({ ...query, selectorName: selectorName ?? '' })}
|
||||
/>
|
||||
</QueryEditorRow>
|
||||
);
|
||||
};
|
@ -0,0 +1,50 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Select } from '@grafana/ui';
|
||||
import { SelectableValue } from '@grafana/data';
|
||||
import { QueryEditorRow } from '..';
|
||||
import CloudMonitoringDatasource from '../../datasource';
|
||||
import { SLOQuery } from '../../types';
|
||||
import { SELECT_WIDTH } from '../../constants';
|
||||
|
||||
export interface Props {
|
||||
onChange: (query: SLOQuery) => void;
|
||||
query: SLOQuery;
|
||||
templateVariableOptions: Array<SelectableValue<string>>;
|
||||
datasource: CloudMonitoringDatasource;
|
||||
}
|
||||
|
||||
export const Service: React.FC<Props> = ({ query, templateVariableOptions, onChange, datasource }) => {
|
||||
const [services, setServices] = useState<Array<SelectableValue<string>>>([]);
|
||||
const { projectName } = query;
|
||||
|
||||
useEffect(() => {
|
||||
if (!projectName) {
|
||||
return;
|
||||
}
|
||||
|
||||
datasource.getSLOServices(projectName).then((services: Array<SelectableValue<string>>) => {
|
||||
setServices([
|
||||
{
|
||||
label: 'Template Variables',
|
||||
options: templateVariableOptions,
|
||||
},
|
||||
...services,
|
||||
]);
|
||||
});
|
||||
}, [datasource, projectName, templateVariableOptions]);
|
||||
|
||||
return (
|
||||
<QueryEditorRow label="Service">
|
||||
<Select
|
||||
width={SELECT_WIDTH}
|
||||
allowCustomValue
|
||||
value={query?.serviceId && { value: query?.serviceId, label: query?.serviceName || query?.serviceId }}
|
||||
placeholder="Select service"
|
||||
options={services}
|
||||
onChange={({ value: serviceId = '', label: serviceName = '' }) =>
|
||||
onChange({ ...query, serviceId, serviceName, sloId: '' })
|
||||
}
|
||||
/>
|
||||
</QueryEditorRow>
|
||||
);
|
||||
};
|
@ -0,0 +1,3 @@
|
||||
export { Service } from './Service';
|
||||
export { SLO } from './SLO';
|
||||
export { Selector } from './Selector';
|
@ -1,112 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Segment, SegmentAsync } from '@grafana/ui';
|
||||
import { SelectableValue } from '@grafana/data';
|
||||
import { selectors } from '../constants';
|
||||
import { Project, AlignmentPeriods, AliasBy, QueryInlineField } from '.';
|
||||
import { SLOQuery } from '../types';
|
||||
import CloudMonitoringDatasource from '../datasource';
|
||||
|
||||
export interface Props {
|
||||
usedAlignmentPeriod?: number;
|
||||
variableOptionGroup: SelectableValue<string>;
|
||||
onChange: (query: SLOQuery) => void;
|
||||
onRunQuery: () => void;
|
||||
query: SLOQuery;
|
||||
datasource: CloudMonitoringDatasource;
|
||||
}
|
||||
|
||||
export const defaultQuery: (dataSource: CloudMonitoringDatasource) => SLOQuery = (dataSource) => ({
|
||||
projectName: dataSource.getDefaultProject(),
|
||||
alignmentPeriod: 'cloud-monitoring-auto',
|
||||
aliasBy: '',
|
||||
selectorName: 'select_slo_health',
|
||||
serviceId: '',
|
||||
serviceName: '',
|
||||
sloId: '',
|
||||
sloName: '',
|
||||
});
|
||||
|
||||
export function SLOQueryEditor({
|
||||
query,
|
||||
datasource,
|
||||
onChange,
|
||||
variableOptionGroup,
|
||||
usedAlignmentPeriod,
|
||||
}: React.PropsWithChildren<Props>) {
|
||||
return (
|
||||
<>
|
||||
<Project
|
||||
templateVariableOptions={variableOptionGroup.options}
|
||||
projectName={query.projectName}
|
||||
datasource={datasource}
|
||||
onChange={(projectName) => onChange({ ...query, projectName })}
|
||||
/>
|
||||
<QueryInlineField label="Service">
|
||||
<SegmentAsync
|
||||
allowCustomValue
|
||||
value={{ value: query?.serviceId, label: query?.serviceName || query?.serviceId }}
|
||||
placeholder="Select service"
|
||||
loadOptions={() =>
|
||||
datasource.getSLOServices(query.projectName).then((services) => [
|
||||
{
|
||||
label: 'Template Variables',
|
||||
options: variableOptionGroup.options,
|
||||
},
|
||||
...services,
|
||||
])
|
||||
}
|
||||
onChange={({ value: serviceId = '', label: serviceName = '' }) =>
|
||||
onChange({ ...query, serviceId, serviceName, sloId: '' })
|
||||
}
|
||||
/>
|
||||
</QueryInlineField>
|
||||
|
||||
<QueryInlineField label="SLO">
|
||||
<SegmentAsync
|
||||
allowCustomValue
|
||||
value={{ value: query?.sloId, label: query?.sloName || query?.sloId }}
|
||||
placeholder="Select SLO"
|
||||
loadOptions={() =>
|
||||
datasource.getServiceLevelObjectives(query.projectName, query.serviceId).then((sloIds) => [
|
||||
{
|
||||
label: 'Template Variables',
|
||||
options: variableOptionGroup.options,
|
||||
},
|
||||
...sloIds,
|
||||
])
|
||||
}
|
||||
onChange={async ({ value: sloId = '', label: sloName = '' }) => {
|
||||
const slos = await datasource.getServiceLevelObjectives(query.projectName, query.serviceId);
|
||||
const slo = slos.find(({ value }) => value === datasource.templateSrv.replace(sloId));
|
||||
onChange({ ...query, sloId, sloName, goal: slo?.goal });
|
||||
}}
|
||||
/>
|
||||
</QueryInlineField>
|
||||
|
||||
<QueryInlineField label="Selector">
|
||||
<Segment
|
||||
allowCustomValue
|
||||
value={[...selectors, ...variableOptionGroup.options].find((s) => s.value === query?.selectorName ?? '')}
|
||||
options={[
|
||||
{
|
||||
label: 'Template Variables',
|
||||
options: variableOptionGroup.options,
|
||||
},
|
||||
...selectors,
|
||||
]}
|
||||
onChange={({ value: selectorName }) => onChange({ ...query, selectorName })}
|
||||
/>
|
||||
</QueryInlineField>
|
||||
|
||||
<AlignmentPeriods
|
||||
templateSrv={datasource.templateSrv}
|
||||
templateVariableOptions={variableOptionGroup.options}
|
||||
alignmentPeriod={query.alignmentPeriod || ''}
|
||||
perSeriesAligner={query.selectorName === 'select_slo_health' ? 'ALIGN_MEAN' : 'ALIGN_NEXT_OLDER'}
|
||||
usedAlignmentPeriod={usedAlignmentPeriod}
|
||||
onChange={(alignmentPeriod) => onChange({ ...query, alignmentPeriod })}
|
||||
/>
|
||||
<AliasBy value={query.aliasBy} onChange={(aliasBy) => onChange({ ...query, aliasBy })} />
|
||||
</>
|
||||
);
|
||||
}
|
@ -1,12 +1,11 @@
|
||||
import React from 'react';
|
||||
import { Aggregations, Metrics, LabelFilter, GroupBys, Alignments, AlignmentPeriods } from '.';
|
||||
import { MetricQuery, MetricDescriptor } from '../types';
|
||||
import { getAlignmentPickerData } from '../functions';
|
||||
import CloudMonitoringDatasource from '../datasource';
|
||||
import { SelectableValue } from '@grafana/data';
|
||||
import { Metrics, LabelFilter, GroupBy, Preprocessor, Alignment } from '.';
|
||||
import { MetricQuery, MetricDescriptor, CustomMetaData } from '../types';
|
||||
import CloudMonitoringDatasource from '../datasource';
|
||||
|
||||
export interface Props {
|
||||
usedAlignmentPeriod?: number;
|
||||
customMetaData: CustomMetaData;
|
||||
variableOptionGroup: SelectableValue<string>;
|
||||
onMetricTypeChange: (query: MetricDescriptor) => void;
|
||||
onChange: (query: MetricQuery) => void;
|
||||
@ -21,11 +20,9 @@ function Editor({
|
||||
datasource,
|
||||
onChange,
|
||||
onMetricTypeChange,
|
||||
usedAlignmentPeriod,
|
||||
customMetaData,
|
||||
variableOptionGroup,
|
||||
}: React.PropsWithChildren<Props>) {
|
||||
const { perSeriesAligner, alignOptions } = getAlignmentPickerData(query, datasource.templateSrv);
|
||||
|
||||
return (
|
||||
<Metrics
|
||||
templateSrv={datasource.templateSrv}
|
||||
@ -40,40 +37,23 @@ function Editor({
|
||||
<LabelFilter
|
||||
labels={labels}
|
||||
filters={query.filters!}
|
||||
onChange={(filters) => onChange({ ...query, filters })}
|
||||
onChange={(filters: string[]) => onChange({ ...query, filters })}
|
||||
variableOptionGroup={variableOptionGroup}
|
||||
/>
|
||||
<GroupBys
|
||||
groupBys={Object.keys(labels)}
|
||||
values={query.groupBys!}
|
||||
onChange={(groupBys) => onChange({ ...query, groupBys })}
|
||||
<Preprocessor metricDescriptor={metric} query={query} onChange={onChange} />
|
||||
<GroupBy
|
||||
labels={Object.keys(labels)}
|
||||
query={query}
|
||||
onChange={onChange}
|
||||
variableOptionGroup={variableOptionGroup}
|
||||
/>
|
||||
<Aggregations
|
||||
metricDescriptor={metric}
|
||||
/>
|
||||
<Alignment
|
||||
datasource={datasource}
|
||||
templateVariableOptions={variableOptionGroup.options}
|
||||
crossSeriesReducer={query.crossSeriesReducer}
|
||||
groupBys={query.groupBys!}
|
||||
onChange={(crossSeriesReducer) => onChange({ ...query, crossSeriesReducer })}
|
||||
>
|
||||
{(displayAdvancedOptions) =>
|
||||
displayAdvancedOptions && (
|
||||
<Alignments
|
||||
alignOptions={alignOptions}
|
||||
templateVariableOptions={variableOptionGroup.options}
|
||||
perSeriesAligner={perSeriesAligner || ''}
|
||||
onChange={(perSeriesAligner) => onChange({ ...query, perSeriesAligner })}
|
||||
/>
|
||||
)
|
||||
}
|
||||
</Aggregations>
|
||||
<AlignmentPeriods
|
||||
templateSrv={datasource.templateSrv}
|
||||
templateVariableOptions={variableOptionGroup.options}
|
||||
alignmentPeriod={query.alignmentPeriod || ''}
|
||||
perSeriesAligner={query.perSeriesAligner || ''}
|
||||
usedAlignmentPeriod={usedAlignmentPeriod}
|
||||
onChange={(alignmentPeriod) => onChange({ ...query, alignmentPeriod })}
|
||||
query={query}
|
||||
customMetaData={customMetaData}
|
||||
onChange={onChange}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
@ -1,16 +1,18 @@
|
||||
export { Project } from './Project';
|
||||
export { Metrics } from './Metrics';
|
||||
export { Help } from './Help';
|
||||
export { GroupBys } from './GroupBys';
|
||||
export { GroupBy } from './GroupBy';
|
||||
export { Alignment } from './Alignment';
|
||||
export { LabelFilter } from './LabelFilter';
|
||||
export { AnnotationsHelp } from './AnnotationsHelp';
|
||||
export { Alignments } from './Alignments';
|
||||
export { AlignmentPeriods } from './AlignmentPeriods';
|
||||
export { AlignmentFunction } from './AlignmentFunction';
|
||||
export { AlignmentPeriod } from './AlignmentPeriod';
|
||||
export { AlignmentPeriodLabel } from './AlignmentPeriodLabel';
|
||||
export { AliasBy } from './AliasBy';
|
||||
export { Aggregations } from './Aggregations';
|
||||
export { Aggregation } from './Aggregation';
|
||||
export { MetricQueryEditor } from './MetricQueryEditor';
|
||||
export { SLOQueryEditor } from './SLOQueryEditor';
|
||||
export { SLOQueryEditor } from './SLO/SLOQueryEditor';
|
||||
export { MQLQueryEditor } from './MQLQueryEditor';
|
||||
export { QueryTypeSelector } from './QueryType';
|
||||
export { QueryInlineField, QueryField, VariableQueryField } from './Fields';
|
||||
export { VariableQueryField, QueryEditorRow, QueryEditorField } from './Fields';
|
||||
export { VisualMetricQueryEditor } from './VisualMetricQueryEditor';
|
||||
export { Preprocessor } from './Preprocessor';
|
||||
|
@ -1,21 +1,16 @@
|
||||
export enum MetricKind {
|
||||
METRIC_KIND_UNSPECIFIED = 'METRIC_KIND_UNSPECIFIED',
|
||||
GAUGE = 'GAUGE',
|
||||
DELTA = 'DELTA',
|
||||
CUMULATIVE = 'CUMULATIVE',
|
||||
}
|
||||
import { AuthType, MetricKind, QueryType, ValueTypes } from './types';
|
||||
|
||||
export enum ValueTypes {
|
||||
VALUE_TYPE_UNSPECIFIED = 'VALUE_TYPE_UNSPECIFIED',
|
||||
BOOL = 'BOOL',
|
||||
INT64 = 'INT64',
|
||||
DOUBLE = 'DOUBLE',
|
||||
STRING = 'STRING',
|
||||
DISTRIBUTION = 'DISTRIBUTION',
|
||||
MONEY = 'MONEY',
|
||||
}
|
||||
// not super excited about using uneven numbers, but this makes it align perfectly with rows that has two fields
|
||||
export const INPUT_WIDTH = 71;
|
||||
export const LABEL_WIDTH = 19;
|
||||
export const INNER_LABEL_WIDTH = 14;
|
||||
export const SELECT_WIDTH = 28;
|
||||
export const AUTH_TYPES = [
|
||||
{ value: 'Google JWT File', key: AuthType.JWT },
|
||||
{ value: 'GCE Default Service Account', key: AuthType.GCE },
|
||||
];
|
||||
|
||||
export const alignOptions = [
|
||||
export const ALIGNMENTS = [
|
||||
{
|
||||
text: 'delta',
|
||||
value: 'ALIGN_DELTA',
|
||||
@ -134,7 +129,7 @@ export const alignOptions = [
|
||||
},
|
||||
];
|
||||
|
||||
export const aggOptions = [
|
||||
export const AGGREGATIONS = [
|
||||
{
|
||||
text: 'none',
|
||||
value: 'REDUCE_NONE',
|
||||
@ -229,7 +224,7 @@ export const aggOptions = [
|
||||
},
|
||||
];
|
||||
|
||||
export const alignmentPeriods = [
|
||||
export const ALIGNMENT_PERIODS = [
|
||||
{ text: 'grafana auto', value: 'grafana-auto' },
|
||||
{ text: 'stackdriver auto', value: 'stackdriver-auto', hidden: true },
|
||||
{ text: 'cloud monitoring auto', value: 'cloud-monitoring-auto' },
|
||||
@ -246,7 +241,7 @@ export const alignmentPeriods = [
|
||||
{ text: '1w', value: '+604800s' },
|
||||
];
|
||||
|
||||
export const systemLabels = [
|
||||
export const SYSTEM_LABELS = [
|
||||
'metadata.system_labels.cloud_account',
|
||||
'metadata.system_labels.name',
|
||||
'metadata.system_labels.region',
|
||||
@ -259,8 +254,13 @@ export const systemLabels = [
|
||||
'metadata.system_labels.container_image',
|
||||
];
|
||||
|
||||
export const selectors = [
|
||||
export const SELECTORS = [
|
||||
{ label: 'SLI Value', value: 'select_slo_health' },
|
||||
{ label: 'SLO Compliance', value: 'select_slo_compliance' },
|
||||
{ label: 'SLO Error Budget Remaining', value: 'select_slo_budget_fraction' },
|
||||
];
|
||||
|
||||
export const QUERY_TYPES = [
|
||||
{ label: 'Metrics', value: QueryType.METRICS },
|
||||
{ label: 'Service Level Objectives (SLO)', value: QueryType.SLO },
|
||||
];
|
||||
|
@ -34,6 +34,7 @@ export default class CloudMonitoringDatasource extends DataSourceWithBackend<
|
||||
this.authenticationType = instanceSettings.jsonData.authenticationType || 'jwt';
|
||||
this.api = new API(`${instanceSettings.url!}/cloudmonitoring/v3/projects/`);
|
||||
this.variables = new CloudMonitoringVariableSupport(this);
|
||||
this.intervalMs = 0;
|
||||
}
|
||||
|
||||
getVariables() {
|
||||
@ -250,7 +251,7 @@ export default class CloudMonitoringDatasource extends DataSourceWithBackend<
|
||||
}
|
||||
|
||||
return this.api.get(`${this.templateSrv.replace(projectName)}/metricDescriptors`, {
|
||||
responseMap: (m: any) => {
|
||||
responseMap: (m: MetricDescriptor) => {
|
||||
const [service] = m.type.split('/');
|
||||
const [serviceShortName] = service.split('.');
|
||||
m.service = service;
|
||||
@ -285,7 +286,7 @@ export default class CloudMonitoringDatasource extends DataSourceWithBackend<
|
||||
});
|
||||
}
|
||||
|
||||
getProjects() {
|
||||
getProjects(): Promise<Array<SelectableValue<string>>> {
|
||||
return this.api.get(`projects`, {
|
||||
responseMap: ({ projectId, name }: { projectId: string; name: string }) => ({
|
||||
value: projectId,
|
||||
@ -346,7 +347,7 @@ export default class CloudMonitoringDatasource extends DataSourceWithBackend<
|
||||
}
|
||||
|
||||
interpolateFilters(filters: string[], scopedVars: ScopedVars) {
|
||||
const completeFilter = chunk(filters, 4)
|
||||
const completeFilter: Filter[] = chunk(filters, 4)
|
||||
.map(([key, operator, value, condition]) => ({
|
||||
key,
|
||||
operator,
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { getAlignmentOptionsByMetric } from './functions';
|
||||
import { ValueTypes, MetricKind } from './constants';
|
||||
import { ValueTypes, MetricKind } from './types';
|
||||
|
||||
describe('functions', () => {
|
||||
let result: any;
|
||||
|
@ -1,9 +1,11 @@
|
||||
import { chunk, flatten, initial, startCase, uniqBy } from 'lodash';
|
||||
import { alignOptions, aggOptions, ValueTypes, MetricKind, systemLabels } from './constants';
|
||||
import { ALIGNMENTS, AGGREGATIONS, SYSTEM_LABELS } from './constants';
|
||||
import { SelectableValue } from '@grafana/data';
|
||||
import CloudMonitoringDatasource from './datasource';
|
||||
import { TemplateSrv } from '@grafana/runtime';
|
||||
import { MetricDescriptor, Filter, MetricQuery } from './types';
|
||||
import { TemplateSrv, getTemplateSrv } from '@grafana/runtime';
|
||||
import { MetricDescriptor, ValueTypes, MetricKind, AlignmentTypes, PreprocessorType, Filter } from './types';
|
||||
|
||||
const templateSrv: TemplateSrv = getTemplateSrv();
|
||||
|
||||
export const extractServicesFromMetricDescriptors = (metricDescriptors: MetricDescriptor[]) =>
|
||||
uniqBy(metricDescriptors, 'service');
|
||||
@ -17,7 +19,7 @@ export const getMetricTypes = (
|
||||
interpolatedMetricType: string,
|
||||
selectedService: string
|
||||
) => {
|
||||
const metricTypes = getMetricTypesByService(metricDescriptors, selectedService).map((m: any) => ({
|
||||
const metricTypes = getMetricTypesByService(metricDescriptors, selectedService).map((m) => ({
|
||||
value: m.type,
|
||||
name: m.displayName,
|
||||
}));
|
||||
@ -32,10 +34,18 @@ export const getMetricTypes = (
|
||||
};
|
||||
};
|
||||
|
||||
export const getAlignmentOptionsByMetric = (metricValueType: string, metricKind: string) => {
|
||||
export const getAlignmentOptionsByMetric = (
|
||||
metricValueType: string,
|
||||
metricKind: string,
|
||||
preprocessor?: PreprocessorType
|
||||
) => {
|
||||
if (preprocessor && preprocessor === PreprocessorType.Rate) {
|
||||
metricKind = MetricKind.GAUGE;
|
||||
}
|
||||
|
||||
return !metricValueType
|
||||
? []
|
||||
: alignOptions.filter((i) => {
|
||||
: ALIGNMENTS.filter((i) => {
|
||||
return (
|
||||
i.valueTypes.indexOf(metricValueType as ValueTypes) !== -1 &&
|
||||
i.metricKinds.indexOf(metricKind as MetricKind) !== -1
|
||||
@ -46,7 +56,7 @@ export const getAlignmentOptionsByMetric = (metricValueType: string, metricKind:
|
||||
export const getAggregationOptionsByMetric = (valueType: ValueTypes, metricKind: MetricKind) => {
|
||||
return !metricKind
|
||||
? []
|
||||
: aggOptions.filter((i) => {
|
||||
: AGGREGATIONS.filter((i) => {
|
||||
return i.valueTypes.indexOf(valueType) !== -1 && i.metricKinds.indexOf(metricKind) !== -1;
|
||||
});
|
||||
};
|
||||
@ -58,19 +68,21 @@ export const getLabelKeys = async (
|
||||
) => {
|
||||
const refId = 'handleLabelKeysQuery';
|
||||
const labels = await datasource.getLabels(selectedMetricType, refId, projectName);
|
||||
return [...Object.keys(labels), ...systemLabels];
|
||||
return [...Object.keys(labels), ...SYSTEM_LABELS];
|
||||
};
|
||||
|
||||
export const getAlignmentPickerData = (
|
||||
{ valueType, metricKind, perSeriesAligner }: Partial<MetricQuery>,
|
||||
templateSrv: TemplateSrv
|
||||
valueType: string | undefined = ValueTypes.DOUBLE,
|
||||
metricKind: string | undefined = MetricKind.GAUGE,
|
||||
perSeriesAligner: string | undefined = AlignmentTypes.ALIGN_MEAN,
|
||||
preprocessor?: PreprocessorType
|
||||
) => {
|
||||
const alignOptions = getAlignmentOptionsByMetric(valueType!, metricKind!).map((option) => ({
|
||||
const alignOptions = getAlignmentOptionsByMetric(valueType!, metricKind!, preprocessor!).map((option) => ({
|
||||
...option,
|
||||
label: option.text,
|
||||
}));
|
||||
if (!alignOptions.some((o: { value: string }) => o.value === templateSrv.replace(perSeriesAligner!))) {
|
||||
perSeriesAligner = alignOptions.length > 0 ? alignOptions[0].value : '';
|
||||
if (!alignOptions.some((o: { value: string }) => o.value === templateSrv.replace(perSeriesAligner))) {
|
||||
perSeriesAligner = alignOptions.length > 0 ? alignOptions[0].value : AlignmentTypes.ALIGN_MEAN;
|
||||
}
|
||||
return { alignOptions, perSeriesAligner };
|
||||
};
|
||||
|
@ -3,11 +3,13 @@ import CloudMonitoringDatasource from './datasource';
|
||||
import { QueryEditor } from './components/QueryEditor';
|
||||
import { ConfigEditor } from './components/ConfigEditor/ConfigEditor';
|
||||
|
||||
import CloudMonitoringCheatSheet from './components/CloudMonitoringCheatSheet';
|
||||
import { CloudMonitoringAnnotationsQueryCtrl } from './annotations_query_ctrl';
|
||||
import { CloudMonitoringVariableQueryEditor } from './components/VariableQueryEditor';
|
||||
import { CloudMonitoringQuery } from './types';
|
||||
|
||||
export const plugin = new DataSourcePlugin<CloudMonitoringDatasource, CloudMonitoringQuery>(CloudMonitoringDatasource)
|
||||
.setQueryEditorHelp(CloudMonitoringCheatSheet)
|
||||
.setQueryEditor(QueryEditor)
|
||||
.setConfigEditor(ConfigEditor)
|
||||
.setAnnotationQueryCtrl(CloudMonitoringAnnotationsQueryCtrl)
|
||||
|
@ -63,33 +63,71 @@ export enum EditorMode {
|
||||
MQL = 'mql',
|
||||
}
|
||||
|
||||
export const queryTypes = [
|
||||
{ label: 'Metrics', value: QueryType.METRICS },
|
||||
{ label: 'Service Level Objectives (SLO)', value: QueryType.SLO },
|
||||
];
|
||||
export enum PreprocessorType {
|
||||
None = 'none',
|
||||
Rate = 'rate',
|
||||
Delta = 'delta',
|
||||
}
|
||||
|
||||
export interface MetricQuery {
|
||||
editorMode: EditorMode;
|
||||
export enum MetricKind {
|
||||
METRIC_KIND_UNSPECIFIED = 'METRIC_KIND_UNSPECIFIED',
|
||||
GAUGE = 'GAUGE',
|
||||
DELTA = 'DELTA',
|
||||
CUMULATIVE = 'CUMULATIVE',
|
||||
}
|
||||
|
||||
export enum ValueTypes {
|
||||
VALUE_TYPE_UNSPECIFIED = 'VALUE_TYPE_UNSPECIFIED',
|
||||
BOOL = 'BOOL',
|
||||
INT64 = 'INT64',
|
||||
DOUBLE = 'DOUBLE',
|
||||
STRING = 'STRING',
|
||||
DISTRIBUTION = 'DISTRIBUTION',
|
||||
MONEY = 'MONEY',
|
||||
}
|
||||
|
||||
export enum AlignmentTypes {
|
||||
ALIGN_DELTA = 'ALIGN_DELTA',
|
||||
ALIGN_RATE = 'ALIGN_RATE',
|
||||
ALIGN_INTERPOLATE = 'ALIGN_INTERPOLATE',
|
||||
ALIGN_NEXT_OLDER = 'ALIGN_NEXT_OLDER',
|
||||
ALIGN_MIN = 'ALIGN_MIN',
|
||||
ALIGN_MAX = 'ALIGN_MAX',
|
||||
ALIGN_MEAN = 'ALIGN_MEAN',
|
||||
ALIGN_COUNT = 'ALIGN_COUNT',
|
||||
ALIGN_SUM = 'ALIGN_SUM',
|
||||
ALIGN_STDDEV = 'ALIGN_STDDEV',
|
||||
ALIGN_COUNT_TRUE = 'ALIGN_COUNT_TRUE',
|
||||
ALIGN_COUNT_FALSE = 'ALIGN_COUNT_FALSE',
|
||||
ALIGN_FRACTION_TRUE = 'ALIGN_FRACTION_TRUE',
|
||||
ALIGN_PERCENTILE_99 = 'ALIGN_PERCENTILE_99',
|
||||
ALIGN_PERCENTILE_95 = 'ALIGN_PERCENTILE_95',
|
||||
ALIGN_PERCENTILE_50 = 'ALIGN_PERCENTILE_50',
|
||||
ALIGN_PERCENTILE_05 = 'ALIGN_PERCENTILE_05',
|
||||
ALIGN_PERCENT_CHANGE = 'ALIGN_PERCENT_CHANGE',
|
||||
}
|
||||
|
||||
export interface BaseQuery {
|
||||
projectName: string;
|
||||
unit?: string;
|
||||
perSeriesAligner?: string;
|
||||
alignmentPeriod?: string;
|
||||
aliasBy?: string;
|
||||
}
|
||||
|
||||
export interface MetricQuery extends BaseQuery {
|
||||
editorMode: EditorMode;
|
||||
metricType: string;
|
||||
crossSeriesReducer: string;
|
||||
alignmentPeriod?: string;
|
||||
perSeriesAligner?: string;
|
||||
groupBys?: string[];
|
||||
filters?: string[];
|
||||
aliasBy?: string;
|
||||
metricKind?: string;
|
||||
metricKind?: MetricKind;
|
||||
valueType?: string;
|
||||
view?: string;
|
||||
query: string;
|
||||
preprocessor?: PreprocessorType;
|
||||
}
|
||||
|
||||
export interface SLOQuery {
|
||||
projectName: string;
|
||||
alignmentPeriod?: string;
|
||||
perSeriesAligner?: string;
|
||||
aliasBy?: string;
|
||||
export interface SLOQuery extends BaseQuery {
|
||||
selectorName: string;
|
||||
serviceId: string;
|
||||
serviceName: string;
|
||||
@ -124,7 +162,7 @@ export interface AnnotationTarget {
|
||||
metricType: string;
|
||||
refId: string;
|
||||
filters: string[];
|
||||
metricKind: string;
|
||||
metricKind: MetricKind;
|
||||
valueType: string;
|
||||
title: string;
|
||||
text: string;
|
||||
@ -141,7 +179,7 @@ export interface QueryMeta {
|
||||
|
||||
export interface MetricDescriptor {
|
||||
valueType: string;
|
||||
metricKind: string;
|
||||
metricKind: MetricKind;
|
||||
type: string;
|
||||
unit: string;
|
||||
service: string;
|
||||
@ -161,3 +199,8 @@ export interface Filter {
|
||||
value: string;
|
||||
condition?: string;
|
||||
}
|
||||
|
||||
export interface CustomMetaData {
|
||||
perSeriesAligner?: string;
|
||||
alignmentPeriod?: string;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user