mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
GoogleCloudMonitoring: Adapt frontend to the new API format (#60173)
* GoogleCloudMonitoring: Migrate queries to the new format * Refactor Aligment and AligmentFunction components (#60235) * Adapt CloudMonitoringDatasource and CloudMonitoringAnnotationSupport (#60177) * Fix: avoid migration for new queries (#60375) * Move preprocessor handling to the backend (#60383) * Other fixes and new function (#60411) * Adapt components to the new API (#60451) * Split metrics query type in time series list and query (#60475) * Clean up metricQuery references (#60478) * More bug fixes (#60525)
This commit is contained in:
parent
f6f140c412
commit
4d693863c0
@ -5212,17 +5212,9 @@ exports[`better eslint`] = {
|
|||||||
"public/app/plugins/datasource/cloud-monitoring/components/MQLQueryEditor.tsx:5381": [
|
"public/app/plugins/datasource/cloud-monitoring/components/MQLQueryEditor.tsx:5381": [
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
|
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
|
||||||
],
|
],
|
||||||
"public/app/plugins/datasource/cloud-monitoring/components/MetricQueryEditor.tsx:5381": [
|
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
|
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "1"]
|
|
||||||
],
|
|
||||||
"public/app/plugins/datasource/cloud-monitoring/components/Metrics.tsx:5381": [
|
"public/app/plugins/datasource/cloud-monitoring/components/Metrics.tsx:5381": [
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
|
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
|
||||||
],
|
],
|
||||||
"public/app/plugins/datasource/cloud-monitoring/components/QueryEditor.tsx:5381": [
|
|
||||||
[0, 0, 0, "Do not use any type assertions.", "0"],
|
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "1"]
|
|
||||||
],
|
|
||||||
"public/app/plugins/datasource/cloud-monitoring/components/VariableQueryEditor.test.tsx:5381": [
|
"public/app/plugins/datasource/cloud-monitoring/components/VariableQueryEditor.test.tsx:5381": [
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
|
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "1"]
|
[0, 0, 0, "Unexpected any. Specify a different type.", "1"]
|
||||||
@ -5241,14 +5233,13 @@ exports[`better eslint`] = {
|
|||||||
],
|
],
|
||||||
"public/app/plugins/datasource/cloud-monitoring/datasource.ts:5381": [
|
"public/app/plugins/datasource/cloud-monitoring/datasource.ts:5381": [
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
|
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
|
||||||
[0, 0, 0, "Do not use any type assertions.", "1"],
|
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "2"],
|
[0, 0, 0, "Do not use any type assertions.", "2"],
|
||||||
[0, 0, 0, "Do not use any type assertions.", "3"],
|
[0, 0, 0, "Do not use any type assertions.", "3"],
|
||||||
[0, 0, 0, "Do not use any type assertions.", "4"],
|
[0, 0, 0, "Unexpected any. Specify a different type.", "4"],
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "5"],
|
[0, 0, 0, "Unexpected any. Specify a different type.", "5"],
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "6"],
|
[0, 0, 0, "Do not use any type assertions.", "6"],
|
||||||
[0, 0, 0, "Do not use any type assertions.", "7"],
|
[0, 0, 0, "Do not use any type assertions.", "7"]
|
||||||
[0, 0, 0, "Do not use any type assertions.", "8"]
|
|
||||||
],
|
],
|
||||||
"public/app/plugins/datasource/cloud-monitoring/functions.test.ts:5381": [
|
"public/app/plugins/datasource/cloud-monitoring/functions.test.ts:5381": [
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
|
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
|
||||||
|
@ -56,7 +56,8 @@ const (
|
|||||||
gceAuthentication = "gce"
|
gceAuthentication = "gce"
|
||||||
jwtAuthentication = "jwt"
|
jwtAuthentication = "jwt"
|
||||||
annotationQueryType = "annotation"
|
annotationQueryType = "annotation"
|
||||||
metricQueryType = "metrics"
|
timeSeriesListQueryType = "timeSeriesList"
|
||||||
|
timeSeriesQueryQueryType = "timeSeriesQuery"
|
||||||
sloQueryType = "slo"
|
sloQueryType = "slo"
|
||||||
crossSeriesReducerDefault = "REDUCE_NONE"
|
crossSeriesReducerDefault = "REDUCE_NONE"
|
||||||
perSeriesAlignerDefault = "ALIGN_MEAN"
|
perSeriesAlignerDefault = "ALIGN_MEAN"
|
||||||
@ -216,32 +217,6 @@ func migrateMetricTypeFilter(metricTypeFilter string, prevFilters interface{}) [
|
|||||||
return metricTypeFilterArray
|
return metricTypeFilterArray
|
||||||
}
|
}
|
||||||
|
|
||||||
func migratePreprocessor(tsl *timeSeriesList, preprocessor string) {
|
|
||||||
// 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
|
|
||||||
t := toPreprocessorType(preprocessor)
|
|
||||||
if t != PreprocessorTypeNone {
|
|
||||||
// Move aggregation to secondaryAggregation
|
|
||||||
tsl.SecondaryAlignmentPeriod = tsl.AlignmentPeriod
|
|
||||||
tsl.SecondaryCrossSeriesReducer = tsl.CrossSeriesReducer
|
|
||||||
tsl.SecondaryPerSeriesAligner = tsl.PerSeriesAligner
|
|
||||||
tsl.SecondaryGroupBys = tsl.GroupBys
|
|
||||||
|
|
||||||
// Set a default cross series reducer if grouped
|
|
||||||
if len(tsl.GroupBys) == 0 {
|
|
||||||
tsl.CrossSeriesReducer = crossSeriesReducerDefault
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set aligner based on preprocessor type
|
|
||||||
aligner := "ALIGN_RATE"
|
|
||||||
if t == PreprocessorTypeDelta {
|
|
||||||
aligner = "ALIGN_DELTA"
|
|
||||||
}
|
|
||||||
tsl.PerSeriesAligner = aligner
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func migrateRequest(req *backend.QueryDataRequest) error {
|
func migrateRequest(req *backend.QueryDataRequest) error {
|
||||||
for i, q := range req.Queries {
|
for i, q := range req.Queries {
|
||||||
var rawQuery map[string]interface{}
|
var rawQuery map[string]interface{}
|
||||||
@ -250,14 +225,17 @@ func migrateRequest(req *backend.QueryDataRequest) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if rawQuery["metricQuery"] == nil {
|
if rawQuery["metricQuery"] == nil &&
|
||||||
|
rawQuery["timeSeriesQuery"] == nil &&
|
||||||
|
rawQuery["timeSeriesList"] == nil &&
|
||||||
|
rawQuery["sloQuery"] == nil {
|
||||||
// migrate legacy query
|
// migrate legacy query
|
||||||
var mq timeSeriesList
|
var mq timeSeriesList
|
||||||
err = json.Unmarshal(q.JSON, &mq)
|
err = json.Unmarshal(q.JSON, &mq)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
q.QueryType = metricQueryType
|
q.QueryType = timeSeriesListQueryType
|
||||||
gq := grafanaQuery{
|
gq := grafanaQuery{
|
||||||
TimeSeriesList: &mq,
|
TimeSeriesList: &mq,
|
||||||
}
|
}
|
||||||
@ -268,9 +246,6 @@ func migrateRequest(req *backend.QueryDataRequest) error {
|
|||||||
// metricType should be a filter
|
// metricType should be a filter
|
||||||
gq.TimeSeriesList.Filters = migrateMetricTypeFilter(rawQuery["metricType"].(string), rawQuery["filters"])
|
gq.TimeSeriesList.Filters = migrateMetricTypeFilter(rawQuery["metricType"].(string), rawQuery["filters"])
|
||||||
}
|
}
|
||||||
if rawQuery["preprocessor"] != nil {
|
|
||||||
migratePreprocessor(gq.TimeSeriesList, rawQuery["preprocessor"].(string))
|
|
||||||
}
|
|
||||||
|
|
||||||
b, err := json.Marshal(gq)
|
b, err := json.Marshal(gq)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -288,7 +263,7 @@ func migrateRequest(req *backend.QueryDataRequest) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Metric query was divided between timeSeriesList and timeSeriesQuery API calls
|
// Metric query was divided between timeSeriesList and timeSeriesQuery API calls
|
||||||
if rawQuery["metricQuery"] != nil {
|
if rawQuery["metricQuery"] != nil && q.QueryType == "metrics" {
|
||||||
metricQuery := rawQuery["metricQuery"].(map[string]interface{})
|
metricQuery := rawQuery["metricQuery"].(map[string]interface{})
|
||||||
|
|
||||||
if metricQuery["editorMode"] != nil && toString(metricQuery["editorMode"]) == "mql" {
|
if metricQuery["editorMode"] != nil && toString(metricQuery["editorMode"]) == "mql" {
|
||||||
@ -297,6 +272,7 @@ func migrateRequest(req *backend.QueryDataRequest) error {
|
|||||||
Query: toString(metricQuery["query"]),
|
Query: toString(metricQuery["query"]),
|
||||||
GraphPeriod: toString(metricQuery["graphPeriod"]),
|
GraphPeriod: toString(metricQuery["graphPeriod"]),
|
||||||
}
|
}
|
||||||
|
q.QueryType = timeSeriesQueryQueryType
|
||||||
} else {
|
} else {
|
||||||
tslb, err := json.Marshal(metricQuery)
|
tslb, err := json.Marshal(metricQuery)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -311,11 +287,10 @@ func migrateRequest(req *backend.QueryDataRequest) error {
|
|||||||
// metricType should be a filter
|
// metricType should be a filter
|
||||||
tsl.Filters = migrateMetricTypeFilter(metricQuery["metricType"].(string), metricQuery["filters"])
|
tsl.Filters = migrateMetricTypeFilter(metricQuery["metricType"].(string), metricQuery["filters"])
|
||||||
}
|
}
|
||||||
if rawQuery["preprocessor"] != nil {
|
|
||||||
migratePreprocessor(tsl, rawQuery["preprocessor"].(string))
|
|
||||||
}
|
|
||||||
rawQuery["timeSeriesList"] = tsl
|
rawQuery["timeSeriesList"] = tsl
|
||||||
|
q.QueryType = timeSeriesListQueryType
|
||||||
}
|
}
|
||||||
|
// AliasBy is now a top level property
|
||||||
if metricQuery["aliasBy"] != nil {
|
if metricQuery["aliasBy"] != nil {
|
||||||
rawQuery["aliasBy"] = metricQuery["aliasBy"]
|
rawQuery["aliasBy"] = metricQuery["aliasBy"]
|
||||||
}
|
}
|
||||||
@ -323,12 +298,22 @@ func migrateRequest(req *backend.QueryDataRequest) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if q.QueryType == "" {
|
|
||||||
q.QueryType = metricQueryType
|
|
||||||
}
|
|
||||||
q.JSON = b
|
q.JSON = b
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if rawQuery["sloQuery"] != nil && q.QueryType == sloQueryType {
|
||||||
|
sloQuery := rawQuery["sloQuery"].(map[string]interface{})
|
||||||
|
// AliasBy is now a top level property
|
||||||
|
if sloQuery["aliasBy"] != nil {
|
||||||
|
rawQuery["aliasBy"] = sloQuery["aliasBy"]
|
||||||
|
b, err := json.Marshal(rawQuery)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
q.JSON = b
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
req.Queries[i] = q
|
req.Queries[i] = q
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -408,29 +393,25 @@ func (s *Service) buildQueryExecutors(logger log.Logger, req *backend.QueryDataR
|
|||||||
|
|
||||||
var queryInterface cloudMonitoringQueryExecutor
|
var queryInterface cloudMonitoringQueryExecutor
|
||||||
switch query.QueryType {
|
switch query.QueryType {
|
||||||
case metricQueryType, annotationQueryType:
|
case timeSeriesListQueryType, annotationQueryType:
|
||||||
if q.TimeSeriesQuery != nil {
|
cmtsf := &cloudMonitoringTimeSeriesList{
|
||||||
queryInterface = &cloudMonitoringTimeSeriesQuery{
|
refID: query.RefID,
|
||||||
refID: query.RefID,
|
logger: logger,
|
||||||
aliasBy: q.AliasBy,
|
aliasBy: q.AliasBy,
|
||||||
parameters: q.TimeSeriesQuery,
|
}
|
||||||
IntervalMS: query.Interval.Milliseconds(),
|
if q.TimeSeriesList.View == "" {
|
||||||
timeRange: req.Queries[0].TimeRange,
|
q.TimeSeriesList.View = "FULL"
|
||||||
}
|
}
|
||||||
} else if q.TimeSeriesList != nil {
|
cmtsf.parameters = q.TimeSeriesList
|
||||||
cmtsf := &cloudMonitoringTimeSeriesList{
|
cmtsf.setParams(startTime, endTime, durationSeconds, query.Interval.Milliseconds())
|
||||||
refID: query.RefID,
|
queryInterface = cmtsf
|
||||||
logger: logger,
|
case timeSeriesQueryQueryType:
|
||||||
aliasBy: q.AliasBy,
|
queryInterface = &cloudMonitoringTimeSeriesQuery{
|
||||||
}
|
refID: query.RefID,
|
||||||
if q.TimeSeriesList.View == "" {
|
aliasBy: q.AliasBy,
|
||||||
q.TimeSeriesList.View = "FULL"
|
parameters: q.TimeSeriesQuery,
|
||||||
}
|
IntervalMS: query.Interval.Milliseconds(),
|
||||||
cmtsf.parameters = q.TimeSeriesList
|
timeRange: req.Queries[0].TimeRange,
|
||||||
cmtsf.setParams(startTime, endTime, durationSeconds, query.Interval.Milliseconds())
|
|
||||||
queryInterface = cmtsf
|
|
||||||
} else {
|
|
||||||
return nil, fmt.Errorf("missing query info")
|
|
||||||
}
|
}
|
||||||
case sloQueryType:
|
case sloQueryType:
|
||||||
cmslo := &cloudMonitoringSLO{
|
cmslo := &cloudMonitoringSLO{
|
||||||
|
@ -664,7 +664,7 @@ func TestCloudMonitoring(t *testing.T) {
|
|||||||
"projectName": "test-proj",
|
"projectName": "test-proj",
|
||||||
"alignmentPeriod": "stackdriver-auto",
|
"alignmentPeriod": "stackdriver-auto",
|
||||||
"perSeriesAligner": "ALIGN_NEXT_OLDER",
|
"perSeriesAligner": "ALIGN_NEXT_OLDER",
|
||||||
"aliasBy": "",
|
"aliasBy": "test-alias",
|
||||||
"selectorName": "select_slo_health",
|
"selectorName": "select_slo_health",
|
||||||
"serviceId": "test-service",
|
"serviceId": "test-service",
|
||||||
"sloId": "test-slo"
|
"sloId": "test-slo"
|
||||||
@ -683,7 +683,7 @@ func TestCloudMonitoring(t *testing.T) {
|
|||||||
assert.Equal(t, "2018-03-15T13:00:00Z", queries[0].params["interval.startTime"][0])
|
assert.Equal(t, "2018-03-15T13:00:00Z", queries[0].params["interval.startTime"][0])
|
||||||
assert.Equal(t, "2018-03-15T13:34:00Z", queries[0].params["interval.endTime"][0])
|
assert.Equal(t, "2018-03-15T13:34:00Z", queries[0].params["interval.endTime"][0])
|
||||||
assert.Equal(t, `+60s`, queries[0].params["aggregation.alignmentPeriod"][0])
|
assert.Equal(t, `+60s`, queries[0].params["aggregation.alignmentPeriod"][0])
|
||||||
assert.Equal(t, "", queries[0].aliasBy)
|
assert.Equal(t, "test-alias", queries[0].aliasBy)
|
||||||
assert.Equal(t, "ALIGN_MEAN", queries[0].params["aggregation.perSeriesAligner"][0])
|
assert.Equal(t, "ALIGN_MEAN", queries[0].params["aggregation.perSeriesAligner"][0])
|
||||||
assert.Equal(t, `aggregation.alignmentPeriod=%2B60s&aggregation.perSeriesAligner=ALIGN_MEAN&filter=select_slo_health%28%22projects%2Ftest-proj%2Fservices%2Ftest-service%2FserviceLevelObjectives%2Ftest-slo%22%29&interval.endTime=2018-03-15T13%3A34%3A00Z&interval.startTime=2018-03-15T13%3A00%3A00Z`, queries[0].params.Encode())
|
assert.Equal(t, `aggregation.alignmentPeriod=%2B60s&aggregation.perSeriesAligner=ALIGN_MEAN&filter=select_slo_health%28%22projects%2Ftest-proj%2Fservices%2Ftest-service%2FserviceLevelObjectives%2Ftest-slo%22%29&interval.endTime=2018-03-15T13%3A34%3A00Z&interval.startTime=2018-03-15T13%3A00%3A00Z`, queries[0].params.Encode())
|
||||||
assert.Equal(t, 5, len(queries[0].params))
|
assert.Equal(t, 5, len(queries[0].params))
|
||||||
@ -1103,7 +1103,7 @@ func baseTimeSeriesList() *backend.QueryDataRequest {
|
|||||||
From: fromStart,
|
From: fromStart,
|
||||||
To: fromStart.Add(34 * time.Minute),
|
To: fromStart.Add(34 * time.Minute),
|
||||||
},
|
},
|
||||||
QueryType: "metrics",
|
QueryType: timeSeriesListQueryType,
|
||||||
JSON: json.RawMessage(`{
|
JSON: json.RawMessage(`{
|
||||||
"timeSeriesList": {
|
"timeSeriesList": {
|
||||||
"filters": ["metric.type=\"a/metric/type\""],
|
"filters": ["metric.type=\"a/metric/type\""],
|
||||||
@ -1127,7 +1127,7 @@ func baseTimeSeriesQuery() *backend.QueryDataRequest {
|
|||||||
From: fromStart,
|
From: fromStart,
|
||||||
To: fromStart.Add(34 * time.Minute),
|
To: fromStart.Add(34 * time.Minute),
|
||||||
},
|
},
|
||||||
QueryType: "metrics",
|
QueryType: timeSeriesQueryQueryType,
|
||||||
JSON: json.RawMessage(`{
|
JSON: json.RawMessage(`{
|
||||||
"queryType": "metrics",
|
"queryType": "metrics",
|
||||||
"timeSeriesQuery": {
|
"timeSeriesQuery": {
|
||||||
|
@ -147,6 +147,32 @@ func (timeSeriesFilter *cloudMonitoringTimeSeriesList) getFilter() string {
|
|||||||
return strings.Trim(filterString, " ")
|
return strings.Trim(filterString, " ")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (timeSeriesFilter *cloudMonitoringTimeSeriesList) setPreprocessor() {
|
||||||
|
// 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
|
||||||
|
t := toPreprocessorType(timeSeriesFilter.parameters.Preprocessor)
|
||||||
|
if t != PreprocessorTypeNone {
|
||||||
|
// Move aggregation to secondaryAggregation
|
||||||
|
timeSeriesFilter.parameters.SecondaryAlignmentPeriod = timeSeriesFilter.parameters.AlignmentPeriod
|
||||||
|
timeSeriesFilter.parameters.SecondaryCrossSeriesReducer = timeSeriesFilter.parameters.CrossSeriesReducer
|
||||||
|
timeSeriesFilter.parameters.SecondaryPerSeriesAligner = timeSeriesFilter.parameters.PerSeriesAligner
|
||||||
|
timeSeriesFilter.parameters.SecondaryGroupBys = timeSeriesFilter.parameters.GroupBys
|
||||||
|
|
||||||
|
// Set a default cross series reducer if grouped
|
||||||
|
if len(timeSeriesFilter.parameters.GroupBys) == 0 {
|
||||||
|
timeSeriesFilter.parameters.CrossSeriesReducer = crossSeriesReducerDefault
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set aligner based on preprocessor type
|
||||||
|
aligner := "ALIGN_RATE"
|
||||||
|
if t == PreprocessorTypeDelta {
|
||||||
|
aligner = "ALIGN_DELTA"
|
||||||
|
}
|
||||||
|
timeSeriesFilter.parameters.PerSeriesAligner = aligner
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (timeSeriesFilter *cloudMonitoringTimeSeriesList) setParams(startTime time.Time, endTime time.Time, durationSeconds int, intervalMs int64) {
|
func (timeSeriesFilter *cloudMonitoringTimeSeriesList) setParams(startTime time.Time, endTime time.Time, durationSeconds int, intervalMs int64) {
|
||||||
params := url.Values{}
|
params := url.Values{}
|
||||||
query := timeSeriesFilter.parameters
|
query := timeSeriesFilter.parameters
|
||||||
@ -165,6 +191,8 @@ func (timeSeriesFilter *cloudMonitoringTimeSeriesList) setParams(startTime time.
|
|||||||
query.PerSeriesAligner = perSeriesAlignerDefault
|
query.PerSeriesAligner = perSeriesAlignerDefault
|
||||||
}
|
}
|
||||||
|
|
||||||
|
timeSeriesFilter.setPreprocessor()
|
||||||
|
|
||||||
alignmentPeriod := calculateAlignmentPeriod(query.AlignmentPeriod, intervalMs, durationSeconds)
|
alignmentPeriod := calculateAlignmentPeriod(query.AlignmentPeriod, intervalMs, durationSeconds)
|
||||||
params.Add("aggregation.alignmentPeriod", alignmentPeriod)
|
params.Add("aggregation.alignmentPeriod", alignmentPeriod)
|
||||||
if query.CrossSeriesReducer != "" {
|
if query.CrossSeriesReducer != "" {
|
||||||
|
@ -18,6 +18,42 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func TestTimeSeriesFilter(t *testing.T) {
|
func TestTimeSeriesFilter(t *testing.T) {
|
||||||
|
t.Run("parses params", func(t *testing.T) {
|
||||||
|
query := &cloudMonitoringTimeSeriesList{parameters: &timeSeriesList{}}
|
||||||
|
query.setParams(time.Time{}, time.Time{}, 0, 0)
|
||||||
|
|
||||||
|
assert.Equal(t, "0001-01-01T00:00:00Z", query.params.Get("interval.startTime"))
|
||||||
|
assert.Equal(t, "0001-01-01T00:00:00Z", query.params.Get("interval.endTime"))
|
||||||
|
assert.Equal(t, "", query.params.Get("filter"))
|
||||||
|
assert.Equal(t, "", query.params.Get("view"))
|
||||||
|
assert.Equal(t, "+60s", query.params.Get("aggregation.alignmentPeriod"))
|
||||||
|
assert.Equal(t, "REDUCE_NONE", query.params.Get("aggregation.crossSeriesReducer"))
|
||||||
|
assert.Equal(t, "ALIGN_MEAN", query.params.Get("aggregation.perSeriesAligner"))
|
||||||
|
assert.Equal(t, "", query.params.Get("aggregation.groupByFields"))
|
||||||
|
assert.Equal(t, "", query.params.Get("secondaryAggregation.alignmentPeriod"))
|
||||||
|
assert.Equal(t, "", query.params.Get("secondaryAggregation.crossSeriesReducer"))
|
||||||
|
assert.Equal(t, "", query.params.Get("secondaryAggregation.perSeriesAligner"))
|
||||||
|
assert.Equal(t, "", query.params.Get("secondaryAggregation.groupByFields"))
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("parses params with preprocessor", func(t *testing.T) {
|
||||||
|
query := &cloudMonitoringTimeSeriesList{parameters: &timeSeriesList{Preprocessor: "rate"}}
|
||||||
|
query.setParams(time.Time{}, time.Time{}, 0, 0)
|
||||||
|
|
||||||
|
assert.Equal(t, "0001-01-01T00:00:00Z", query.params.Get("interval.startTime"))
|
||||||
|
assert.Equal(t, "0001-01-01T00:00:00Z", query.params.Get("interval.endTime"))
|
||||||
|
assert.Equal(t, "", query.params.Get("filter"))
|
||||||
|
assert.Equal(t, "", query.params.Get("view"))
|
||||||
|
assert.Equal(t, "+60s", query.params.Get("aggregation.alignmentPeriod"))
|
||||||
|
assert.Equal(t, "REDUCE_NONE", query.params.Get("aggregation.crossSeriesReducer"))
|
||||||
|
assert.Equal(t, "ALIGN_RATE", query.params.Get("aggregation.perSeriesAligner"))
|
||||||
|
assert.Equal(t, "", query.params.Get("aggregation.groupByFields"))
|
||||||
|
assert.Equal(t, "", query.params.Get("secondaryAggregation.alignmentPeriod"))
|
||||||
|
assert.Equal(t, "REDUCE_NONE", query.params.Get("secondaryAggregation.crossSeriesReducer"))
|
||||||
|
assert.Equal(t, "ALIGN_MEAN", query.params.Get("secondaryAggregation.perSeriesAligner"))
|
||||||
|
assert.Equal(t, "", query.params.Get("secondaryAggregation.groupByFields"))
|
||||||
|
})
|
||||||
|
|
||||||
t.Run("when data from query aggregated to one time series", func(t *testing.T) {
|
t.Run("when data from query aggregated to one time series", func(t *testing.T) {
|
||||||
data, err := loadTestFile("./test-data/1-series-response-agg-one-metric.json")
|
data, err := loadTestFile("./test-data/1-series-response-agg-one-metric.json")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
@ -50,6 +50,10 @@ type (
|
|||||||
SecondaryCrossSeriesReducer string `json:"secondaryCrossSeriesReducer"`
|
SecondaryCrossSeriesReducer string `json:"secondaryCrossSeriesReducer"`
|
||||||
SecondaryPerSeriesAligner string `json:"secondaryPerSeriesAligner"`
|
SecondaryPerSeriesAligner string `json:"secondaryPerSeriesAligner"`
|
||||||
SecondaryGroupBys []string `json:"secondaryGroupBys"`
|
SecondaryGroupBys []string `json:"secondaryGroupBys"`
|
||||||
|
// Preprocessor is not part of the GCM API but added for simplicity
|
||||||
|
// It will overwrite AligmentPeriod, CrossSeriesReducer, PerSeriesAligner, GroupBys
|
||||||
|
// and its secondary counterparts
|
||||||
|
Preprocessor string `json:"preprocessor"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// sloQuery is an internal convention but the API is the same as timeSeriesList
|
// sloQuery is an internal convention but the API is the same as timeSeriesList
|
||||||
|
@ -14,6 +14,7 @@ export const createMockDatasource = (overrides?: Partial<Datasource>) => {
|
|||||||
getDefaultProject: jest.fn().mockReturnValue('cloud-monitoring-default-project'),
|
getDefaultProject: jest.fn().mockReturnValue('cloud-monitoring-default-project'),
|
||||||
templateSrv,
|
templateSrv,
|
||||||
getSLOServices: jest.fn().mockResolvedValue([]),
|
getSLOServices: jest.fn().mockResolvedValue([]),
|
||||||
|
migrateQuery: jest.fn().mockImplementation((query) => query),
|
||||||
...overrides,
|
...overrides,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1,25 +1,9 @@
|
|||||||
import { AlignmentTypes, CloudMonitoringQuery, EditorMode, MetricQuery, QueryType, SLOQuery } from '../types';
|
import { AlignmentTypes, CloudMonitoringQuery, QueryType, SLOQuery, TimeSeriesList, TimeSeriesQuery } from '../types';
|
||||||
|
|
||||||
type Subset<K> = {
|
type Subset<K> = {
|
||||||
[attr in keyof K]?: K[attr] extends object ? Subset<K[attr]> : K[attr];
|
[attr in keyof K]?: K[attr] extends object ? Subset<K[attr]> : K[attr];
|
||||||
};
|
};
|
||||||
|
|
||||||
export const createMockMetricQuery: (overrides?: Partial<MetricQuery>) => MetricQuery = (
|
|
||||||
overrides?: Partial<MetricQuery>
|
|
||||||
) => {
|
|
||||||
return {
|
|
||||||
editorMode: EditorMode.Visual,
|
|
||||||
metricType: '',
|
|
||||||
crossSeriesReducer: 'REDUCE_NONE',
|
|
||||||
query: '',
|
|
||||||
projectName: 'cloud-monitoring-default-project',
|
|
||||||
filters: [],
|
|
||||||
groupBys: [],
|
|
||||||
view: 'FULL',
|
|
||||||
...overrides,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export const createMockSLOQuery: (overrides?: Partial<SLOQuery>) => SLOQuery = (overrides) => {
|
export const createMockSLOQuery: (overrides?: Partial<SLOQuery>) => SLOQuery = (overrides) => {
|
||||||
return {
|
return {
|
||||||
projectName: 'projectName',
|
projectName: 'projectName',
|
||||||
@ -36,6 +20,29 @@ export const createMockSLOQuery: (overrides?: Partial<SLOQuery>) => SLOQuery = (
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const createMockTimeSeriesList: (overrides?: Partial<TimeSeriesList>) => TimeSeriesList = (
|
||||||
|
overrides?: Partial<TimeSeriesList>
|
||||||
|
) => {
|
||||||
|
return {
|
||||||
|
crossSeriesReducer: 'REDUCE_NONE',
|
||||||
|
projectName: 'cloud-monitoring-default-project',
|
||||||
|
filters: [],
|
||||||
|
groupBys: [],
|
||||||
|
view: 'FULL',
|
||||||
|
...overrides,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const createMockTimeSeriesQuery: (overrides?: Partial<TimeSeriesQuery>) => TimeSeriesQuery = (
|
||||||
|
overrides?: Partial<TimeSeriesQuery>
|
||||||
|
) => {
|
||||||
|
return {
|
||||||
|
query: '',
|
||||||
|
projectName: 'cloud-monitoring-default-project',
|
||||||
|
...overrides,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export const createMockQuery: (overrides?: Subset<CloudMonitoringQuery>) => CloudMonitoringQuery = (overrides) => {
|
export const createMockQuery: (overrides?: Subset<CloudMonitoringQuery>) => CloudMonitoringQuery = (overrides) => {
|
||||||
return {
|
return {
|
||||||
datasource: {
|
datasource: {
|
||||||
@ -43,12 +50,12 @@ export const createMockQuery: (overrides?: Subset<CloudMonitoringQuery>) => Clou
|
|||||||
uid: 'abc',
|
uid: 'abc',
|
||||||
},
|
},
|
||||||
refId: 'cloudMonitoringRefId',
|
refId: 'cloudMonitoringRefId',
|
||||||
queryType: QueryType.METRICS,
|
queryType: QueryType.TIME_SERIES_LIST,
|
||||||
intervalMs: 0,
|
intervalMs: 0,
|
||||||
type: 'timeSeriesQuery',
|
|
||||||
hide: false,
|
hide: false,
|
||||||
...overrides,
|
...overrides,
|
||||||
metricQuery: createMockMetricQuery(overrides?.metricQuery),
|
|
||||||
sloQuery: createMockSLOQuery(overrides?.sloQuery),
|
sloQuery: createMockSLOQuery(overrides?.sloQuery),
|
||||||
|
timeSeriesList: createMockTimeSeriesList(overrides?.timeSeriesList),
|
||||||
|
timeSeriesQuery: createMockTimeSeriesQuery(overrides?.timeSeriesQuery),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -5,7 +5,6 @@ import { CloudMonitoringAnnotationSupport } from './annotationSupport';
|
|||||||
import {
|
import {
|
||||||
AlignmentTypes,
|
AlignmentTypes,
|
||||||
CloudMonitoringQuery,
|
CloudMonitoringQuery,
|
||||||
EditorMode,
|
|
||||||
LegacyCloudMonitoringAnnotationQuery,
|
LegacyCloudMonitoringAnnotationQuery,
|
||||||
MetricKind,
|
MetricKind,
|
||||||
QueryType,
|
QueryType,
|
||||||
@ -15,16 +14,11 @@ const query: CloudMonitoringQuery = {
|
|||||||
refId: 'query',
|
refId: 'query',
|
||||||
queryType: QueryType.ANNOTATION,
|
queryType: QueryType.ANNOTATION,
|
||||||
intervalMs: 0,
|
intervalMs: 0,
|
||||||
metricQuery: {
|
timeSeriesList: {
|
||||||
editorMode: EditorMode.Visual,
|
|
||||||
projectName: 'project-name',
|
projectName: 'project-name',
|
||||||
metricType: '',
|
|
||||||
filters: [],
|
filters: [],
|
||||||
metricKind: MetricKind.GAUGE,
|
|
||||||
valueType: '',
|
|
||||||
title: '',
|
title: '',
|
||||||
text: '',
|
text: '',
|
||||||
query: '',
|
|
||||||
crossSeriesReducer: 'REDUCE_NONE',
|
crossSeriesReducer: 'REDUCE_NONE',
|
||||||
perSeriesAligner: AlignmentTypes.ALIGN_NONE,
|
perSeriesAligner: AlignmentTypes.ALIGN_NONE,
|
||||||
},
|
},
|
||||||
@ -71,15 +65,11 @@ describe('CloudMonitoringAnnotationSupport', () => {
|
|||||||
name: 'Anno',
|
name: 'Anno',
|
||||||
target: {
|
target: {
|
||||||
intervalMs: 0,
|
intervalMs: 0,
|
||||||
metricQuery: {
|
timeSeriesList: {
|
||||||
crossSeriesReducer: 'REDUCE_NONE',
|
crossSeriesReducer: 'REDUCE_NONE',
|
||||||
editorMode: 'visual',
|
|
||||||
filters: ['filter1', 'filter2'],
|
filters: ['filter1', 'filter2'],
|
||||||
metricKind: 'CUMULATIVE',
|
|
||||||
metricType: 'metric-type',
|
|
||||||
perSeriesAligner: 'ALIGN_NONE',
|
perSeriesAligner: 'ALIGN_NONE',
|
||||||
projectName: 'project-name',
|
projectName: 'project-name',
|
||||||
query: '',
|
|
||||||
text: 'text',
|
text: 'text',
|
||||||
title: 'title',
|
title: 'title',
|
||||||
},
|
},
|
||||||
@ -92,10 +82,10 @@ describe('CloudMonitoringAnnotationSupport', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('prepareQuery', () => {
|
describe('prepareQuery', () => {
|
||||||
it('should ensure queryType is set to "metrics"', () => {
|
it('should ensure queryType is set to "annotation"', () => {
|
||||||
const queryWithoutMetricsQueryType = { ...annotationQuery, queryType: 'blah' };
|
const queryWithoutMetricsQueryType = { ...annotationQuery, queryType: 'blah' };
|
||||||
expect(annotationSupport.prepareQuery?.(queryWithoutMetricsQueryType)).toEqual(
|
expect(annotationSupport.prepareQuery?.(queryWithoutMetricsQueryType)).toEqual(
|
||||||
expect.objectContaining({ queryType: 'metrics' })
|
expect.objectContaining({ queryType: QueryType.ANNOTATION })
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
it('should ensure type is set "annotationQuery"', () => {
|
it('should ensure type is set "annotationQuery"', () => {
|
||||||
|
@ -2,14 +2,7 @@ import { AnnotationSupport, AnnotationQuery } from '@grafana/data';
|
|||||||
|
|
||||||
import { AnnotationQueryEditor } from './components/AnnotationQueryEditor';
|
import { AnnotationQueryEditor } from './components/AnnotationQueryEditor';
|
||||||
import CloudMonitoringDatasource from './datasource';
|
import CloudMonitoringDatasource from './datasource';
|
||||||
import {
|
import { AlignmentTypes, CloudMonitoringQuery, LegacyCloudMonitoringAnnotationQuery, QueryType } from './types';
|
||||||
AlignmentTypes,
|
|
||||||
CloudMonitoringQuery,
|
|
||||||
EditorMode,
|
|
||||||
LegacyCloudMonitoringAnnotationQuery,
|
|
||||||
MetricKind,
|
|
||||||
QueryType,
|
|
||||||
} from './types';
|
|
||||||
|
|
||||||
// The legacy query format sets the title and text values to empty strings by default.
|
// The legacy query format sets the title and text values to empty strings by default.
|
||||||
// If the title or text is not undefined at the top-level of the annotation target,
|
// If the title or text is not undefined at the top-level of the annotation target,
|
||||||
@ -42,13 +35,9 @@ export const CloudMonitoringAnnotationSupport: (
|
|||||||
intervalMs: ds.intervalMs,
|
intervalMs: ds.intervalMs,
|
||||||
refId: target?.refId || 'annotationQuery',
|
refId: target?.refId || 'annotationQuery',
|
||||||
queryType: QueryType.ANNOTATION,
|
queryType: QueryType.ANNOTATION,
|
||||||
metricQuery: {
|
timeSeriesList: {
|
||||||
projectName: target?.projectName || ds.getDefaultProject(),
|
projectName: target?.projectName || ds.getDefaultProject(),
|
||||||
editorMode: EditorMode.Visual,
|
|
||||||
metricType: target?.metricType || '',
|
|
||||||
filters: target?.filters || [],
|
filters: target?.filters || [],
|
||||||
metricKind: target?.metricKind || MetricKind.GAUGE,
|
|
||||||
query: '',
|
|
||||||
crossSeriesReducer: 'REDUCE_NONE',
|
crossSeriesReducer: 'REDUCE_NONE',
|
||||||
perSeriesAligner: AlignmentTypes.ALIGN_NONE,
|
perSeriesAligner: AlignmentTypes.ALIGN_NONE,
|
||||||
title: target?.title || '',
|
title: target?.title || '',
|
||||||
@ -65,11 +54,8 @@ export const CloudMonitoringAnnotationSupport: (
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
...anno.target,
|
...anno.target,
|
||||||
queryType: QueryType.METRICS,
|
queryType: QueryType.ANNOTATION,
|
||||||
type: 'annotationQuery',
|
type: 'annotationQuery',
|
||||||
metricQuery: {
|
|
||||||
...anno.target.metricQuery,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
QueryEditor: AnnotationQueryEditor,
|
QueryEditor: AnnotationQueryEditor,
|
||||||
|
@ -6,7 +6,8 @@ import { openMenu } from 'react-select-event';
|
|||||||
import { TemplateSrvMock } from 'app/features/templating/template_srv.mock';
|
import { TemplateSrvMock } from 'app/features/templating/template_srv.mock';
|
||||||
|
|
||||||
import { createMockDatasource } from '../__mocks__/cloudMonitoringDatasource';
|
import { createMockDatasource } from '../__mocks__/cloudMonitoringDatasource';
|
||||||
import { createMockMetricQuery } from '../__mocks__/cloudMonitoringQuery';
|
import { createMockMetricDescriptor } from '../__mocks__/cloudMonitoringMetricDescriptor';
|
||||||
|
import { createMockTimeSeriesList } from '../__mocks__/cloudMonitoringQuery';
|
||||||
import { MetricKind, ValueTypes } from '../types';
|
import { MetricKind, ValueTypes } from '../types';
|
||||||
|
|
||||||
import { Alignment } from './Alignment';
|
import { Alignment } from './Alignment';
|
||||||
@ -19,7 +20,7 @@ jest.mock('@grafana/runtime', () => ({
|
|||||||
describe('Alignment', () => {
|
describe('Alignment', () => {
|
||||||
it('renders alignment fields', () => {
|
it('renders alignment fields', () => {
|
||||||
const datasource = createMockDatasource();
|
const datasource = createMockDatasource();
|
||||||
const query = createMockMetricQuery();
|
const query = createMockTimeSeriesList();
|
||||||
const onChange = jest.fn();
|
const onChange = jest.fn();
|
||||||
|
|
||||||
render(
|
render(
|
||||||
@ -39,7 +40,7 @@ describe('Alignment', () => {
|
|||||||
|
|
||||||
it('can set the alignment function', async () => {
|
it('can set the alignment function', async () => {
|
||||||
const datasource = createMockDatasource();
|
const datasource = createMockDatasource();
|
||||||
const query = createMockMetricQuery({ metricKind: MetricKind.GAUGE, valueType: ValueTypes.INT64 });
|
const query = createMockTimeSeriesList();
|
||||||
const onChange = jest.fn();
|
const onChange = jest.fn();
|
||||||
|
|
||||||
render(
|
render(
|
||||||
@ -50,6 +51,7 @@ describe('Alignment', () => {
|
|||||||
query={query}
|
query={query}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
templateVariableOptions={[]}
|
templateVariableOptions={[]}
|
||||||
|
metricDescriptor={createMockMetricDescriptor({ metricKind: MetricKind.GAUGE, valueType: ValueTypes.INT64 })}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -61,7 +63,7 @@ describe('Alignment', () => {
|
|||||||
|
|
||||||
it('can set the alignment period', async () => {
|
it('can set the alignment period', async () => {
|
||||||
const datasource = createMockDatasource();
|
const datasource = createMockDatasource();
|
||||||
const query = createMockMetricQuery();
|
const query = createMockTimeSeriesList();
|
||||||
const onChange = jest.fn();
|
const onChange = jest.fn();
|
||||||
|
|
||||||
render(
|
render(
|
||||||
|
@ -6,18 +6,20 @@ import { EditorField, EditorFieldGroup } from '@grafana/experimental';
|
|||||||
import { ALIGNMENT_PERIODS } from '../constants';
|
import { ALIGNMENT_PERIODS } from '../constants';
|
||||||
import CloudMonitoringDatasource from '../datasource';
|
import CloudMonitoringDatasource from '../datasource';
|
||||||
import { alignmentPeriodLabel } from '../functions';
|
import { alignmentPeriodLabel } from '../functions';
|
||||||
import { CustomMetaData, MetricQuery, SLOQuery } from '../types';
|
import { CustomMetaData, MetricDescriptor, PreprocessorType, TimeSeriesList } from '../types';
|
||||||
|
|
||||||
import { AlignmentFunction } from './AlignmentFunction';
|
import { AlignmentFunction } from './AlignmentFunction';
|
||||||
import { PeriodSelect } from './PeriodSelect';
|
import { PeriodSelect } from './PeriodSelect';
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
refId: string;
|
refId: string;
|
||||||
onChange: (query: MetricQuery | SLOQuery) => void;
|
onChange: (query: TimeSeriesList) => void;
|
||||||
query: MetricQuery;
|
query: TimeSeriesList;
|
||||||
templateVariableOptions: Array<SelectableValue<string>>;
|
templateVariableOptions: Array<SelectableValue<string>>;
|
||||||
customMetaData: CustomMetaData;
|
customMetaData: CustomMetaData;
|
||||||
datasource: CloudMonitoringDatasource;
|
datasource: CloudMonitoringDatasource;
|
||||||
|
metricDescriptor?: MetricDescriptor;
|
||||||
|
preprocessor?: PreprocessorType;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Alignment: FC<Props> = ({
|
export const Alignment: FC<Props> = ({
|
||||||
@ -27,6 +29,8 @@ export const Alignment: FC<Props> = ({
|
|||||||
query,
|
query,
|
||||||
customMetaData,
|
customMetaData,
|
||||||
datasource,
|
datasource,
|
||||||
|
metricDescriptor,
|
||||||
|
preprocessor,
|
||||||
}) => {
|
}) => {
|
||||||
const alignmentLabel = useMemo(() => alignmentPeriodLabel(customMetaData, datasource), [customMetaData, datasource]);
|
const alignmentLabel = useMemo(() => alignmentPeriodLabel(customMetaData, datasource), [customMetaData, datasource]);
|
||||||
return (
|
return (
|
||||||
@ -39,7 +43,9 @@ export const Alignment: FC<Props> = ({
|
|||||||
inputId={`${refId}-alignment-function`}
|
inputId={`${refId}-alignment-function`}
|
||||||
templateVariableOptions={templateVariableOptions}
|
templateVariableOptions={templateVariableOptions}
|
||||||
query={query}
|
query={query}
|
||||||
onChange={onChange}
|
onChange={(q) => onChange({ ...query, ...q })}
|
||||||
|
metricDescriptor={metricDescriptor}
|
||||||
|
preprocessor={preprocessor}
|
||||||
/>
|
/>
|
||||||
</EditorField>
|
</EditorField>
|
||||||
<EditorField label="Alignment period" tooltip={alignmentLabel}>
|
<EditorField label="Alignment period" tooltip={alignmentLabel}>
|
||||||
|
@ -4,17 +4,28 @@ import { SelectableValue } from '@grafana/data';
|
|||||||
import { Select } from '@grafana/ui';
|
import { Select } from '@grafana/ui';
|
||||||
|
|
||||||
import { getAlignmentPickerData } from '../functions';
|
import { getAlignmentPickerData } from '../functions';
|
||||||
import { MetricQuery } from '../types';
|
import { MetricDescriptor, PreprocessorType, SLOQuery, TimeSeriesList } from '../types';
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
inputId: string;
|
inputId: string;
|
||||||
onChange: (query: MetricQuery) => void;
|
onChange: (query: TimeSeriesList | SLOQuery) => void;
|
||||||
query: MetricQuery;
|
query: TimeSeriesList | SLOQuery;
|
||||||
templateVariableOptions: Array<SelectableValue<string>>;
|
templateVariableOptions: Array<SelectableValue<string>>;
|
||||||
|
metricDescriptor?: MetricDescriptor;
|
||||||
|
preprocessor?: PreprocessorType;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const AlignmentFunction: FC<Props> = ({ inputId, query, templateVariableOptions, onChange }) => {
|
export const AlignmentFunction: FC<Props> = ({
|
||||||
const { valueType, metricKind, perSeriesAligner: psa, preprocessor } = query;
|
inputId,
|
||||||
|
query,
|
||||||
|
templateVariableOptions,
|
||||||
|
onChange,
|
||||||
|
metricDescriptor,
|
||||||
|
preprocessor,
|
||||||
|
}) => {
|
||||||
|
const { perSeriesAligner: psa } = query;
|
||||||
|
let { valueType, metricKind } = metricDescriptor || {};
|
||||||
|
|
||||||
const { perSeriesAligner, alignOptions } = useMemo(
|
const { perSeriesAligner, alignOptions } = useMemo(
|
||||||
() => getAlignmentPickerData(valueType, metricKind, psa, preprocessor),
|
() => getAlignmentPickerData(valueType, metricKind, psa, preprocessor),
|
||||||
[valueType, metricKind, psa, preprocessor]
|
[valueType, metricKind, psa, preprocessor]
|
||||||
|
@ -7,6 +7,13 @@ import { createMockQuery } from '../__mocks__/cloudMonitoringQuery';
|
|||||||
|
|
||||||
import { AnnotationQueryEditor } from './AnnotationQueryEditor';
|
import { AnnotationQueryEditor } from './AnnotationQueryEditor';
|
||||||
|
|
||||||
|
jest.mock('@grafana/runtime', () => ({
|
||||||
|
...(jest.requireActual('@grafana/runtime') as unknown as object),
|
||||||
|
getTemplateSrv: () => ({
|
||||||
|
replace: (val: string) => val,
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
describe('AnnotationQueryEditor', () => {
|
describe('AnnotationQueryEditor', () => {
|
||||||
it('renders correctly', async () => {
|
it('renders correctly', async () => {
|
||||||
const onChange = jest.fn();
|
const onChange = jest.fn();
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { useDebounce } from 'react-use';
|
import { useDebounce } from 'react-use';
|
||||||
|
|
||||||
import { QueryEditorProps, toOption } from '@grafana/data';
|
import { QueryEditorProps, toOption } from '@grafana/data';
|
||||||
@ -6,54 +6,32 @@ import { EditorField, EditorRows } from '@grafana/experimental';
|
|||||||
import { Input } from '@grafana/ui';
|
import { Input } from '@grafana/ui';
|
||||||
|
|
||||||
import CloudMonitoringDatasource from '../datasource';
|
import CloudMonitoringDatasource from '../datasource';
|
||||||
import {
|
import { AnnotationQuery, CloudMonitoringOptions, CloudMonitoringQuery, QueryType } from '../types';
|
||||||
EditorMode,
|
|
||||||
MetricKind,
|
|
||||||
AnnotationMetricQuery,
|
|
||||||
CloudMonitoringOptions,
|
|
||||||
CloudMonitoringQuery,
|
|
||||||
AlignmentTypes,
|
|
||||||
} from '../types';
|
|
||||||
|
|
||||||
import { MetricQueryEditor } from './MetricQueryEditor';
|
import { MetricQueryEditor, defaultTimeSeriesList } from './MetricQueryEditor';
|
||||||
|
|
||||||
import { AnnotationsHelp } from './';
|
import { AnnotationsHelp } from './';
|
||||||
|
|
||||||
export type Props = QueryEditorProps<CloudMonitoringDatasource, CloudMonitoringQuery, CloudMonitoringOptions>;
|
export type Props = QueryEditorProps<CloudMonitoringDatasource, CloudMonitoringQuery, CloudMonitoringOptions>;
|
||||||
|
|
||||||
export const defaultQuery: (datasource: CloudMonitoringDatasource) => AnnotationMetricQuery = (datasource) => ({
|
export const defaultQuery: (datasource: CloudMonitoringDatasource) => AnnotationQuery = (datasource) => ({
|
||||||
editorMode: EditorMode.Visual,
|
...defaultTimeSeriesList(datasource),
|
||||||
projectName: datasource.getDefaultProject(),
|
|
||||||
projects: [],
|
|
||||||
metricType: '',
|
|
||||||
filters: [],
|
|
||||||
metricKind: MetricKind.GAUGE,
|
|
||||||
valueType: '',
|
|
||||||
refId: 'annotationQuery',
|
|
||||||
title: '',
|
title: '',
|
||||||
text: '',
|
text: '',
|
||||||
labels: {},
|
|
||||||
variableOptionGroup: {},
|
|
||||||
variableOptions: [],
|
|
||||||
query: '',
|
|
||||||
crossSeriesReducer: 'REDUCE_NONE',
|
|
||||||
perSeriesAligner: AlignmentTypes.ALIGN_NONE,
|
|
||||||
alignmentPeriod: 'grafana-auto',
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export const AnnotationQueryEditor = (props: Props) => {
|
export const AnnotationQueryEditor = (props: Props) => {
|
||||||
const { datasource, query, onRunQuery, data, onChange } = props;
|
const { datasource, query, onRunQuery, data, onChange } = props;
|
||||||
const meta = data?.series.length ? data?.series[0].meta : {};
|
const meta = data?.series.length ? data?.series[0].meta : {};
|
||||||
const customMetaData = meta?.custom ?? {};
|
const customMetaData = meta?.custom ?? {};
|
||||||
const metricQuery = { ...defaultQuery(datasource), ...query.metricQuery };
|
const timeSeriesList = { ...defaultQuery(datasource), ...query.timeSeriesList };
|
||||||
const [title, setTitle] = useState(metricQuery.title || '');
|
const [title, setTitle] = useState(timeSeriesList.title || '');
|
||||||
const [text, setText] = useState(metricQuery.text || '');
|
const [text, setText] = useState(timeSeriesList.text || '');
|
||||||
const variableOptionGroup = {
|
const variableOptionGroup = {
|
||||||
label: 'Template Variables',
|
label: 'Template Variables',
|
||||||
options: datasource.getVariables().map(toOption),
|
options: datasource.getVariables().map(toOption),
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleQueryChange = (metricQuery: AnnotationMetricQuery) => onChange({ ...query, metricQuery });
|
|
||||||
const handleTitleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
const handleTitleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
setTitle(e.target.value);
|
setTitle(e.target.value);
|
||||||
};
|
};
|
||||||
@ -63,19 +41,26 @@ export const AnnotationQueryEditor = (props: Props) => {
|
|||||||
|
|
||||||
useDebounce(
|
useDebounce(
|
||||||
() => {
|
() => {
|
||||||
onChange({ ...query, metricQuery: { ...metricQuery, title } });
|
onChange({ ...query, timeSeriesList: { ...timeSeriesList, title } });
|
||||||
},
|
},
|
||||||
1000,
|
1000,
|
||||||
[title, onChange]
|
[title, onChange]
|
||||||
);
|
);
|
||||||
useDebounce(
|
useDebounce(
|
||||||
() => {
|
() => {
|
||||||
onChange({ ...query, metricQuery: { ...metricQuery, text } });
|
onChange({ ...query, timeSeriesList: { ...timeSeriesList, text } });
|
||||||
},
|
},
|
||||||
1000,
|
1000,
|
||||||
[text, onChange]
|
[text, onChange]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Use a known query type
|
||||||
|
useEffect(() => {
|
||||||
|
if (!Object.values(QueryType).includes(query.queryType)) {
|
||||||
|
onChange({ ...query, queryType: QueryType.TIME_SERIES_LIST });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<EditorRows>
|
<EditorRows>
|
||||||
<>
|
<>
|
||||||
@ -83,10 +68,10 @@ export const AnnotationQueryEditor = (props: Props) => {
|
|||||||
refId={query.refId}
|
refId={query.refId}
|
||||||
variableOptionGroup={variableOptionGroup}
|
variableOptionGroup={variableOptionGroup}
|
||||||
customMetaData={customMetaData}
|
customMetaData={customMetaData}
|
||||||
onChange={handleQueryChange}
|
onChange={onChange}
|
||||||
onRunQuery={onRunQuery}
|
onRunQuery={onRunQuery}
|
||||||
datasource={datasource}
|
datasource={datasource}
|
||||||
query={metricQuery}
|
query={query}
|
||||||
/>
|
/>
|
||||||
<EditorField label="Title" htmlFor="annotation-query-title">
|
<EditorField label="Title" htmlFor="annotation-query-title">
|
||||||
<Input id="annotation-query-title" value={title} onChange={handleTitleChange} />
|
<Input id="annotation-query-title" value={title} onChange={handleTitleChange} />
|
||||||
|
@ -2,7 +2,7 @@ import { render, screen } from '@testing-library/react';
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { openMenu, select } from 'react-select-event';
|
import { openMenu, select } from 'react-select-event';
|
||||||
|
|
||||||
import { createMockMetricQuery } from '../__mocks__/cloudMonitoringQuery';
|
import { createMockTimeSeriesList } from '../__mocks__/cloudMonitoringQuery';
|
||||||
|
|
||||||
import { GroupBy, Props } from './GroupBy';
|
import { GroupBy, Props } from './GroupBy';
|
||||||
|
|
||||||
@ -15,7 +15,7 @@ const props: Props = {
|
|||||||
} as any,
|
} as any,
|
||||||
variableOptionGroup: { options: [] },
|
variableOptionGroup: { options: [] },
|
||||||
labels: [],
|
labels: [],
|
||||||
query: createMockMetricQuery(),
|
query: createMockTimeSeriesList(),
|
||||||
};
|
};
|
||||||
|
|
||||||
describe('GroupBy', () => {
|
describe('GroupBy', () => {
|
||||||
|
@ -6,7 +6,7 @@ import { MultiSelect } from '@grafana/ui';
|
|||||||
|
|
||||||
import { SYSTEM_LABELS } from '../constants';
|
import { SYSTEM_LABELS } from '../constants';
|
||||||
import { labelsToGroupedOptions } from '../functions';
|
import { labelsToGroupedOptions } from '../functions';
|
||||||
import { MetricDescriptor, MetricQuery } from '../types';
|
import { MetricDescriptor, TimeSeriesList } from '../types';
|
||||||
|
|
||||||
import { Aggregation } from './Aggregation';
|
import { Aggregation } from './Aggregation';
|
||||||
|
|
||||||
@ -15,8 +15,8 @@ export interface Props {
|
|||||||
variableOptionGroup: SelectableValue<string>;
|
variableOptionGroup: SelectableValue<string>;
|
||||||
labels: string[];
|
labels: string[];
|
||||||
metricDescriptor?: MetricDescriptor;
|
metricDescriptor?: MetricDescriptor;
|
||||||
onChange: (query: MetricQuery) => void;
|
onChange: (query: TimeSeriesList) => void;
|
||||||
query: MetricQuery;
|
query: TimeSeriesList;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const GroupBy: FunctionComponent<Props> = ({
|
export const GroupBy: FunctionComponent<Props> = ({
|
||||||
|
@ -26,6 +26,13 @@ describe('LabelFilter', () => {
|
|||||||
expect(screen.getByText('value_1')).toBeInTheDocument();
|
expect(screen.getByText('value_1')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should render skip "protected" filters', () => {
|
||||||
|
const filters = ['metric.type', '=', 'value_1'];
|
||||||
|
render(<LabelFilter labels={{}} filters={filters} onChange={() => {}} variableOptionGroup={[]} />);
|
||||||
|
expect(screen.queryByText('metric.type')).not.toBeInTheDocument();
|
||||||
|
expect(screen.queryByText('value_1')).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
it('can add filters', async () => {
|
it('can add filters', async () => {
|
||||||
const onChange = jest.fn();
|
const onChange = jest.fn();
|
||||||
render(<LabelFilter labels={{}} filters={[]} onChange={onChange} variableOptionGroup={[]} />);
|
render(<LabelFilter labels={{}} filters={[]} onChange={onChange} variableOptionGroup={[]} />);
|
||||||
|
@ -28,13 +28,20 @@ const filtersToStringArray = (filters: Filter[]) =>
|
|||||||
|
|
||||||
const operators = ['=', '!=', '=~', '!=~'].map(toOption);
|
const operators = ['=', '!=', '=~', '!=~'].map(toOption);
|
||||||
|
|
||||||
|
// These keys are not editable as labels but they have its own selector.
|
||||||
|
// For example the 'metric.type' is set with the metric name selector.
|
||||||
|
const protectedFilterKeys = ['metric.type'];
|
||||||
|
|
||||||
export const LabelFilter: FunctionComponent<Props> = ({
|
export const LabelFilter: FunctionComponent<Props> = ({
|
||||||
labels = {},
|
labels = {},
|
||||||
filters: filterArray,
|
filters: filterArray,
|
||||||
onChange: _onChange,
|
onChange: _onChange,
|
||||||
variableOptionGroup,
|
variableOptionGroup,
|
||||||
}) => {
|
}) => {
|
||||||
const filters: Filter[] = useMemo(() => stringArrayToFilters(filterArray), [filterArray]);
|
const rawFilters: Filter[] = stringArrayToFilters(filterArray);
|
||||||
|
const filters = rawFilters.filter(({ key }) => !protectedFilterKeys.includes(key));
|
||||||
|
const protectedFilters = rawFilters.filter(({ key }) => protectedFilterKeys.includes(key));
|
||||||
|
|
||||||
const options = useMemo(
|
const options = useMemo(
|
||||||
() => [variableOptionGroup, ...labelsToGroupedOptions(Object.keys(labels))],
|
() => [variableOptionGroup, ...labelsToGroupedOptions(Object.keys(labels))],
|
||||||
[labels, variableOptionGroup]
|
[labels, variableOptionGroup]
|
||||||
@ -64,7 +71,7 @@ export const LabelFilter: FunctionComponent<Props> = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const onChange = (items: Array<Partial<Filter>>) => {
|
const onChange = (items: Array<Partial<Filter>>) => {
|
||||||
const filters = items.map(({ key, operator, value, condition }) => ({
|
const filters = items.concat(protectedFilters).map(({ key, operator, value, condition }) => ({
|
||||||
key: key || '',
|
key: key || '',
|
||||||
operator: operator || DEFAULT_OPERATOR,
|
operator: operator || DEFAULT_OPERATOR,
|
||||||
value: value || '',
|
value: value || '',
|
||||||
|
@ -0,0 +1,58 @@
|
|||||||
|
import { render, screen } from '@testing-library/react';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { createMockDatasource } from '../__mocks__/cloudMonitoringDatasource';
|
||||||
|
import { createMockQuery } from '../__mocks__/cloudMonitoringQuery';
|
||||||
|
import { QueryType } from '../types';
|
||||||
|
|
||||||
|
import { MetricQueryEditor } from './MetricQueryEditor';
|
||||||
|
|
||||||
|
jest.mock('@grafana/runtime', () => ({
|
||||||
|
...(jest.requireActual('@grafana/runtime') as unknown as object),
|
||||||
|
getTemplateSrv: () => ({
|
||||||
|
replace: (val: string) => val,
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const defaultProps = {
|
||||||
|
refId: 'A',
|
||||||
|
customMetaData: {},
|
||||||
|
variableOptionGroup: { options: [] },
|
||||||
|
onChange: jest.fn(),
|
||||||
|
onRunQuery: jest.fn(),
|
||||||
|
query: createMockQuery(),
|
||||||
|
datasource: createMockDatasource(),
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('MetricQueryEditor', () => {
|
||||||
|
it('renders a default time series list query', async () => {
|
||||||
|
const onChange = jest.fn();
|
||||||
|
const query = createMockQuery();
|
||||||
|
// Force to populate with default values
|
||||||
|
delete query.timeSeriesList;
|
||||||
|
|
||||||
|
render(<MetricQueryEditor {...defaultProps} onChange={onChange} query={query} />);
|
||||||
|
expect(onChange).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders a default time series query', async () => {
|
||||||
|
const onChange = jest.fn();
|
||||||
|
const query = createMockQuery();
|
||||||
|
// Force to populate with default values
|
||||||
|
delete query.timeSeriesQuery;
|
||||||
|
query.queryType = QueryType.TIME_SERIES_QUERY;
|
||||||
|
|
||||||
|
render(<MetricQueryEditor {...defaultProps} onChange={onChange} query={query} />);
|
||||||
|
expect(onChange).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders an annotation query', async () => {
|
||||||
|
const onChange = jest.fn();
|
||||||
|
const query = createMockQuery();
|
||||||
|
query.queryType = QueryType.ANNOTATION;
|
||||||
|
|
||||||
|
render(<MetricQueryEditor {...defaultProps} onChange={onChange} query={query} />);
|
||||||
|
const l = await screen.findByLabelText('Project');
|
||||||
|
expect(l).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
@ -1,20 +1,16 @@
|
|||||||
import React, { useCallback, useEffect, useState } from 'react';
|
import React, { useCallback, useEffect } from 'react';
|
||||||
|
|
||||||
import { SelectableValue } from '@grafana/data';
|
import { SelectableValue } from '@grafana/data';
|
||||||
import { EditorRows } from '@grafana/experimental';
|
import { EditorRows } from '@grafana/experimental';
|
||||||
|
|
||||||
import CloudMonitoringDatasource from '../datasource';
|
import CloudMonitoringDatasource from '../datasource';
|
||||||
import { getAlignmentPickerData } from '../functions';
|
|
||||||
import {
|
import {
|
||||||
AlignmentTypes,
|
AlignmentTypes,
|
||||||
|
CloudMonitoringQuery,
|
||||||
CustomMetaData,
|
CustomMetaData,
|
||||||
EditorMode,
|
QueryType,
|
||||||
MetricDescriptor,
|
TimeSeriesList,
|
||||||
MetricKind,
|
TimeSeriesQuery,
|
||||||
MetricQuery,
|
|
||||||
PreprocessorType,
|
|
||||||
SLOQuery,
|
|
||||||
ValueTypes,
|
|
||||||
} from '../types';
|
} from '../types';
|
||||||
|
|
||||||
import { GraphPeriod } from './GraphPeriod';
|
import { GraphPeriod } from './GraphPeriod';
|
||||||
@ -25,35 +21,24 @@ export interface Props {
|
|||||||
refId: string;
|
refId: string;
|
||||||
customMetaData: CustomMetaData;
|
customMetaData: CustomMetaData;
|
||||||
variableOptionGroup: SelectableValue<string>;
|
variableOptionGroup: SelectableValue<string>;
|
||||||
onChange: (query: MetricQuery) => void;
|
onChange: (query: CloudMonitoringQuery) => void;
|
||||||
onRunQuery: () => void;
|
onRunQuery: () => void;
|
||||||
query: MetricQuery;
|
query: CloudMonitoringQuery;
|
||||||
datasource: CloudMonitoringDatasource;
|
datasource: CloudMonitoringDatasource;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface State {
|
export const defaultTimeSeriesList: (dataSource: CloudMonitoringDatasource) => TimeSeriesList = (dataSource) => ({
|
||||||
labels: any;
|
|
||||||
[key: string]: any;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const defaultState: State = {
|
|
||||||
labels: {},
|
|
||||||
};
|
|
||||||
|
|
||||||
export const defaultQuery: (dataSource: CloudMonitoringDatasource) => MetricQuery = (dataSource) => ({
|
|
||||||
editorMode: EditorMode.Visual,
|
|
||||||
projectName: dataSource.getDefaultProject(),
|
projectName: dataSource.getDefaultProject(),
|
||||||
metricType: '',
|
|
||||||
metricKind: MetricKind.GAUGE,
|
|
||||||
valueType: '',
|
|
||||||
crossSeriesReducer: 'REDUCE_NONE',
|
crossSeriesReducer: 'REDUCE_NONE',
|
||||||
alignmentPeriod: 'cloud-monitoring-auto',
|
alignmentPeriod: 'cloud-monitoring-auto',
|
||||||
perSeriesAligner: AlignmentTypes.ALIGN_MEAN,
|
perSeriesAligner: AlignmentTypes.ALIGN_MEAN,
|
||||||
groupBys: [],
|
groupBys: [],
|
||||||
filters: [],
|
filters: [],
|
||||||
aliasBy: '',
|
});
|
||||||
|
|
||||||
|
export const defaultTimeSeriesQuery: (dataSource: CloudMonitoringDatasource) => TimeSeriesQuery = (dataSource) => ({
|
||||||
|
projectName: dataSource.getDefaultProject(),
|
||||||
query: '',
|
query: '',
|
||||||
preprocessor: PreprocessorType.None,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
function Editor({
|
function Editor({
|
||||||
@ -65,69 +50,63 @@ function Editor({
|
|||||||
customMetaData,
|
customMetaData,
|
||||||
variableOptionGroup,
|
variableOptionGroup,
|
||||||
}: React.PropsWithChildren<Props>) {
|
}: React.PropsWithChildren<Props>) {
|
||||||
const [state, setState] = useState<State>(defaultState);
|
const onChangeTimeSeriesList = useCallback(
|
||||||
const { projectName, metricType, groupBys, editorMode, crossSeriesReducer } = query;
|
(timeSeriesList: TimeSeriesList) => {
|
||||||
|
onQueryChange({ ...query, timeSeriesList });
|
||||||
useEffect(() => {
|
|
||||||
if (projectName && metricType) {
|
|
||||||
datasource
|
|
||||||
.getLabels(metricType, refId, projectName)
|
|
||||||
.then((labels) => setState((prevState) => ({ ...prevState, labels })));
|
|
||||||
}
|
|
||||||
}, [datasource, groupBys, metricType, projectName, refId, crossSeriesReducer]);
|
|
||||||
|
|
||||||
const onChange = useCallback(
|
|
||||||
(metricQuery: MetricQuery | SLOQuery) => {
|
|
||||||
onQueryChange({ ...query, ...metricQuery });
|
|
||||||
onRunQuery();
|
onRunQuery();
|
||||||
},
|
},
|
||||||
[onQueryChange, onRunQuery, query]
|
[onQueryChange, onRunQuery, query]
|
||||||
);
|
);
|
||||||
|
|
||||||
const onMetricTypeChange = useCallback(
|
const onChangeTimeSeriesQuery = useCallback(
|
||||||
({ valueType, metricKind, type }: MetricDescriptor) => {
|
(timeSeriesQuery: TimeSeriesQuery) => {
|
||||||
const preprocessor =
|
onQueryChange({ ...query, timeSeriesQuery });
|
||||||
metricKind === MetricKind.GAUGE || valueType === ValueTypes.DISTRIBUTION
|
onRunQuery();
|
||||||
? PreprocessorType.None
|
|
||||||
: PreprocessorType.Rate;
|
|
||||||
const { perSeriesAligner } = getAlignmentPickerData(valueType, metricKind, state.perSeriesAligner, preprocessor);
|
|
||||||
onChange({
|
|
||||||
...query,
|
|
||||||
perSeriesAligner,
|
|
||||||
metricType: type,
|
|
||||||
valueType,
|
|
||||||
metricKind,
|
|
||||||
preprocessor,
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
[onChange, query, state]
|
[onQueryChange, onRunQuery, query]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (query.queryType === QueryType.TIME_SERIES_LIST && !query.timeSeriesList) {
|
||||||
|
onChangeTimeSeriesList(defaultTimeSeriesList(datasource));
|
||||||
|
}
|
||||||
|
if (query.queryType === QueryType.TIME_SERIES_QUERY && !query.timeSeriesQuery) {
|
||||||
|
onChangeTimeSeriesQuery(defaultTimeSeriesQuery(datasource));
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
onChangeTimeSeriesList,
|
||||||
|
onChangeTimeSeriesQuery,
|
||||||
|
query.queryType,
|
||||||
|
query.timeSeriesList,
|
||||||
|
query.timeSeriesQuery,
|
||||||
|
datasource,
|
||||||
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<EditorRows>
|
<EditorRows>
|
||||||
{editorMode === EditorMode.Visual && (
|
{[QueryType.TIME_SERIES_LIST, QueryType.ANNOTATION].includes(query.queryType) && query.timeSeriesList && (
|
||||||
<VisualMetricQueryEditor
|
<VisualMetricQueryEditor
|
||||||
refId={refId}
|
refId={refId}
|
||||||
labels={state.labels}
|
|
||||||
variableOptionGroup={variableOptionGroup}
|
variableOptionGroup={variableOptionGroup}
|
||||||
customMetaData={customMetaData}
|
customMetaData={customMetaData}
|
||||||
onMetricTypeChange={onMetricTypeChange}
|
onChange={onChangeTimeSeriesList}
|
||||||
onChange={onChange}
|
|
||||||
datasource={datasource}
|
datasource={datasource}
|
||||||
query={query}
|
query={query.timeSeriesList}
|
||||||
|
aliasBy={query.aliasBy}
|
||||||
|
onChangeAliasBy={(aliasBy: string) => onQueryChange({ ...query, aliasBy })}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{editorMode === EditorMode.MQL && (
|
{query.queryType === QueryType.TIME_SERIES_QUERY && query.timeSeriesQuery && (
|
||||||
<>
|
<>
|
||||||
<MQLQueryEditor
|
<MQLQueryEditor
|
||||||
onChange={(q: string) => onQueryChange({ ...query, query: q })}
|
onChange={(q: string) => onChangeTimeSeriesQuery({ ...query.timeSeriesQuery!, query: q })}
|
||||||
onRunQuery={onRunQuery}
|
onRunQuery={onRunQuery}
|
||||||
query={query.query}
|
query={query.timeSeriesQuery.query}
|
||||||
></MQLQueryEditor>
|
></MQLQueryEditor>
|
||||||
<GraphPeriod
|
<GraphPeriod
|
||||||
onChange={(graphPeriod: string) => onQueryChange({ ...query, graphPeriod })}
|
onChange={(graphPeriod: string) => onChangeTimeSeriesQuery({ ...query.timeSeriesQuery!, graphPeriod })}
|
||||||
graphPeriod={query.graphPeriod}
|
graphPeriod={query.timeSeriesQuery.graphPeriod}
|
||||||
refId={refId}
|
refId={refId}
|
||||||
variableOptionGroup={variableOptionGroup}
|
variableOptionGroup={variableOptionGroup}
|
||||||
/>
|
/>
|
||||||
|
@ -4,14 +4,14 @@ import { openMenu, select } from 'react-select-event';
|
|||||||
|
|
||||||
import { createMockDatasource } from '../__mocks__/cloudMonitoringDatasource';
|
import { createMockDatasource } from '../__mocks__/cloudMonitoringDatasource';
|
||||||
import { createMockMetricDescriptor } from '../__mocks__/cloudMonitoringMetricDescriptor';
|
import { createMockMetricDescriptor } from '../__mocks__/cloudMonitoringMetricDescriptor';
|
||||||
import { createMockMetricQuery } from '../__mocks__/cloudMonitoringQuery';
|
import { createMockTimeSeriesList } from '../__mocks__/cloudMonitoringQuery';
|
||||||
|
|
||||||
import { Metrics } from './Metrics';
|
import { Metrics } from './Metrics';
|
||||||
|
|
||||||
describe('Metrics', () => {
|
describe('Metrics', () => {
|
||||||
it('renders metrics fields', async () => {
|
it('renders metrics fields', async () => {
|
||||||
const onChange = jest.fn();
|
const onChange = jest.fn();
|
||||||
const query = createMockMetricQuery();
|
const query = createMockTimeSeriesList();
|
||||||
const datasource = createMockDatasource();
|
const datasource = createMockDatasource();
|
||||||
|
|
||||||
render(
|
render(
|
||||||
@ -35,7 +35,7 @@ describe('Metrics', () => {
|
|||||||
|
|
||||||
it('can select a service', async () => {
|
it('can select a service', async () => {
|
||||||
const onChange = jest.fn();
|
const onChange = jest.fn();
|
||||||
const query = createMockMetricQuery();
|
const query = createMockTimeSeriesList();
|
||||||
const datasource = createMockDatasource({
|
const datasource = createMockDatasource({
|
||||||
getMetricTypes: jest.fn().mockResolvedValue([createMockMetricDescriptor()]),
|
getMetricTypes: jest.fn().mockResolvedValue([createMockMetricDescriptor()]),
|
||||||
});
|
});
|
||||||
@ -63,7 +63,7 @@ describe('Metrics', () => {
|
|||||||
|
|
||||||
it('can select a metric name', async () => {
|
it('can select a metric name', async () => {
|
||||||
const onChange = jest.fn();
|
const onChange = jest.fn();
|
||||||
const query = createMockMetricQuery();
|
const query = createMockTimeSeriesList();
|
||||||
const datasource = createMockDatasource({
|
const datasource = createMockDatasource({
|
||||||
getMetricTypes: jest.fn().mockResolvedValue([createMockMetricDescriptor()]),
|
getMetricTypes: jest.fn().mockResolvedValue([createMockMetricDescriptor()]),
|
||||||
});
|
});
|
||||||
@ -91,7 +91,7 @@ describe('Metrics', () => {
|
|||||||
|
|
||||||
it('should render available metric options according to the selected service', async () => {
|
it('should render available metric options according to the selected service', async () => {
|
||||||
const onChange = jest.fn();
|
const onChange = jest.fn();
|
||||||
const query = createMockMetricQuery();
|
const query = createMockTimeSeriesList();
|
||||||
const datasource = createMockDatasource({
|
const datasource = createMockDatasource({
|
||||||
getMetricTypes: jest.fn().mockResolvedValue([
|
getMetricTypes: jest.fn().mockResolvedValue([
|
||||||
createMockMetricDescriptor({
|
createMockMetricDescriptor({
|
||||||
@ -180,7 +180,7 @@ describe('Metrics', () => {
|
|||||||
}),
|
}),
|
||||||
]),
|
]),
|
||||||
});
|
});
|
||||||
const query = createMockMetricQuery();
|
const query = createMockTimeSeriesList();
|
||||||
|
|
||||||
render(
|
render(
|
||||||
<Metrics
|
<Metrics
|
||||||
|
@ -7,7 +7,7 @@ import { EditorField, EditorFieldGroup, EditorRow } from '@grafana/experimental'
|
|||||||
import { getSelectStyles, Select, useStyles2, useTheme2 } from '@grafana/ui';
|
import { getSelectStyles, Select, useStyles2, useTheme2 } from '@grafana/ui';
|
||||||
|
|
||||||
import CloudMonitoringDatasource from '../datasource';
|
import CloudMonitoringDatasource from '../datasource';
|
||||||
import { MetricDescriptor, MetricQuery } from '../types';
|
import { MetricDescriptor, TimeSeriesList } from '../types';
|
||||||
|
|
||||||
import { Project } from './Project';
|
import { Project } from './Project';
|
||||||
|
|
||||||
@ -18,9 +18,9 @@ export interface Props {
|
|||||||
datasource: CloudMonitoringDatasource;
|
datasource: CloudMonitoringDatasource;
|
||||||
projectName: string;
|
projectName: string;
|
||||||
metricType: string;
|
metricType: string;
|
||||||
query: MetricQuery;
|
query: TimeSeriesList;
|
||||||
children: (metricDescriptor?: MetricDescriptor) => JSX.Element;
|
children: (metricDescriptor?: MetricDescriptor) => JSX.Element;
|
||||||
onProjectChange: (query: MetricQuery) => void;
|
onProjectChange: (query: TimeSeriesList) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Metrics(props: Props) {
|
export function Metrics(props: Props) {
|
||||||
|
@ -5,7 +5,7 @@ import React from 'react';
|
|||||||
import { TemplateSrvMock } from 'app/features/templating/template_srv.mock';
|
import { TemplateSrvMock } from 'app/features/templating/template_srv.mock';
|
||||||
|
|
||||||
import { createMockMetricDescriptor } from '../__mocks__/cloudMonitoringMetricDescriptor';
|
import { createMockMetricDescriptor } from '../__mocks__/cloudMonitoringMetricDescriptor';
|
||||||
import { createMockMetricQuery } from '../__mocks__/cloudMonitoringQuery';
|
import { createMockTimeSeriesList } from '../__mocks__/cloudMonitoringQuery';
|
||||||
import { MetricKind, ValueTypes } from '../types';
|
import { MetricKind, ValueTypes } from '../types';
|
||||||
|
|
||||||
import { Preprocessor } from './Preprocessor';
|
import { Preprocessor } from './Preprocessor';
|
||||||
@ -17,7 +17,7 @@ jest.mock('@grafana/runtime', () => ({
|
|||||||
|
|
||||||
describe('Preprocessor', () => {
|
describe('Preprocessor', () => {
|
||||||
it('only provides "None" as an option if no metric descriptor is provided', () => {
|
it('only provides "None" as an option if no metric descriptor is provided', () => {
|
||||||
const query = createMockMetricQuery();
|
const query = createMockTimeSeriesList();
|
||||||
const onChange = jest.fn();
|
const onChange = jest.fn();
|
||||||
|
|
||||||
render(<Preprocessor onChange={onChange} query={query} />);
|
render(<Preprocessor onChange={onChange} query={query} />);
|
||||||
@ -28,7 +28,7 @@ describe('Preprocessor', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('only provides "None" as an option if metric kind is "Gauge"', () => {
|
it('only provides "None" as an option if metric kind is "Gauge"', () => {
|
||||||
const query = createMockMetricQuery();
|
const query = createMockTimeSeriesList();
|
||||||
const onChange = jest.fn();
|
const onChange = jest.fn();
|
||||||
const metricDescriptor = createMockMetricDescriptor({ metricKind: MetricKind.GAUGE });
|
const metricDescriptor = createMockMetricDescriptor({ metricKind: MetricKind.GAUGE });
|
||||||
|
|
||||||
@ -40,7 +40,7 @@ describe('Preprocessor', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('only provides "None" as an option if value type is "Distribution"', () => {
|
it('only provides "None" as an option if value type is "Distribution"', () => {
|
||||||
const query = createMockMetricQuery();
|
const query = createMockTimeSeriesList();
|
||||||
const onChange = jest.fn();
|
const onChange = jest.fn();
|
||||||
const metricDescriptor = createMockMetricDescriptor({ valueType: ValueTypes.DISTRIBUTION });
|
const metricDescriptor = createMockMetricDescriptor({ valueType: ValueTypes.DISTRIBUTION });
|
||||||
|
|
||||||
@ -52,7 +52,7 @@ describe('Preprocessor', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('provides "None" and "Rate" as options if metric kind is not "Delta" or "Cumulative" and value type is not "Distribution"', () => {
|
it('provides "None" and "Rate" as options if metric kind is not "Delta" or "Cumulative" and value type is not "Distribution"', () => {
|
||||||
const query = createMockMetricQuery();
|
const query = createMockTimeSeriesList();
|
||||||
const onChange = jest.fn();
|
const onChange = jest.fn();
|
||||||
const metricDescriptor = createMockMetricDescriptor({ metricKind: MetricKind.DELTA });
|
const metricDescriptor = createMockMetricDescriptor({ metricKind: MetricKind.DELTA });
|
||||||
|
|
||||||
@ -64,7 +64,7 @@ describe('Preprocessor', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('provides all options if metric kind is "Cumulative" and value type is not "Distribution"', () => {
|
it('provides all options if metric kind is "Cumulative" and value type is not "Distribution"', () => {
|
||||||
const query = createMockMetricQuery();
|
const query = createMockTimeSeriesList();
|
||||||
const onChange = jest.fn();
|
const onChange = jest.fn();
|
||||||
const metricDescriptor = createMockMetricDescriptor({ metricKind: MetricKind.CUMULATIVE });
|
const metricDescriptor = createMockMetricDescriptor({ metricKind: MetricKind.CUMULATIVE });
|
||||||
|
|
||||||
@ -76,7 +76,7 @@ describe('Preprocessor', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('provides all options if metric kind is "Cumulative" and value type is not "Distribution"', async () => {
|
it('provides all options if metric kind is "Cumulative" and value type is not "Distribution"', async () => {
|
||||||
const query = createMockMetricQuery();
|
const query = createMockTimeSeriesList();
|
||||||
const onChange = jest.fn();
|
const onChange = jest.fn();
|
||||||
const metricDescriptor = createMockMetricDescriptor({ metricKind: MetricKind.CUMULATIVE });
|
const metricDescriptor = createMockMetricDescriptor({ metricKind: MetricKind.CUMULATIVE });
|
||||||
|
|
||||||
|
@ -5,18 +5,19 @@ import { EditorField } from '@grafana/experimental';
|
|||||||
import { RadioButtonGroup } from '@grafana/ui';
|
import { RadioButtonGroup } from '@grafana/ui';
|
||||||
|
|
||||||
import { getAlignmentPickerData } from '../functions';
|
import { getAlignmentPickerData } from '../functions';
|
||||||
import { MetricDescriptor, MetricKind, MetricQuery, PreprocessorType, ValueTypes } from '../types';
|
import { MetricDescriptor, MetricKind, PreprocessorType, TimeSeriesList, ValueTypes } from '../types';
|
||||||
|
|
||||||
const NONE_OPTION = { label: 'None', value: PreprocessorType.None };
|
const NONE_OPTION = { label: 'None', value: PreprocessorType.None };
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
metricDescriptor?: MetricDescriptor;
|
metricDescriptor?: MetricDescriptor;
|
||||||
onChange: (query: MetricQuery) => void;
|
onChange: (query: TimeSeriesList) => void;
|
||||||
query: MetricQuery;
|
query: TimeSeriesList;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Preprocessor: FunctionComponent<Props> = ({ query, metricDescriptor, onChange }) => {
|
export const Preprocessor: FunctionComponent<Props> = ({ query, metricDescriptor, onChange }) => {
|
||||||
const options = useOptions(metricDescriptor);
|
const options = useOptions(metricDescriptor);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<EditorField
|
<EditorField
|
||||||
label="Pre-processing"
|
label="Pre-processing"
|
||||||
@ -24,7 +25,8 @@ export const Preprocessor: FunctionComponent<Props> = ({ query, metricDescriptor
|
|||||||
>
|
>
|
||||||
<RadioButtonGroup
|
<RadioButtonGroup
|
||||||
onChange={(value: PreprocessorType) => {
|
onChange={(value: PreprocessorType) => {
|
||||||
const { valueType, metricKind, perSeriesAligner: psa } = query;
|
const { perSeriesAligner: psa } = query;
|
||||||
|
const { valueType, metricKind } = metricDescriptor ?? {};
|
||||||
const { perSeriesAligner } = getAlignmentPickerData(valueType, metricKind, psa, value);
|
const { perSeriesAligner } = getAlignmentPickerData(valueType, metricKind, psa, value);
|
||||||
onChange({ ...query, preprocessor: value, perSeriesAligner });
|
onChange({ ...query, preprocessor: value, perSeriesAligner });
|
||||||
}}
|
}}
|
||||||
|
@ -0,0 +1,46 @@
|
|||||||
|
import { render, waitFor } from '@testing-library/react';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { createMockDatasource } from '../__mocks__/cloudMonitoringDatasource';
|
||||||
|
import { createMockQuery } from '../__mocks__/cloudMonitoringQuery';
|
||||||
|
import { QueryType } from '../types';
|
||||||
|
|
||||||
|
import { QueryEditor } from './QueryEditor';
|
||||||
|
|
||||||
|
jest.mock('@grafana/runtime', () => ({
|
||||||
|
...(jest.requireActual('@grafana/runtime') as unknown as object),
|
||||||
|
getTemplateSrv: () => ({
|
||||||
|
replace: (val: string) => val,
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const defaultProps = {
|
||||||
|
refId: 'A',
|
||||||
|
customMetaData: {},
|
||||||
|
variableOptionGroup: { options: [] },
|
||||||
|
onChange: jest.fn(),
|
||||||
|
onRunQuery: jest.fn(),
|
||||||
|
query: createMockQuery(),
|
||||||
|
datasource: createMockDatasource(),
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('QueryEditor', () => {
|
||||||
|
it('should migrate the given query', async () => {
|
||||||
|
const datasource = createMockDatasource();
|
||||||
|
datasource.migrateQuery = jest.fn().mockReturnValue(defaultProps.query);
|
||||||
|
|
||||||
|
render(<QueryEditor {...defaultProps} datasource={datasource} />);
|
||||||
|
await waitFor(() => expect(datasource.migrateQuery).toHaveBeenCalledTimes(1));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set a known query type', async () => {
|
||||||
|
const query = createMockQuery();
|
||||||
|
query.queryType = 'other' as QueryType;
|
||||||
|
const onChange = jest.fn();
|
||||||
|
|
||||||
|
render(<QueryEditor {...defaultProps} query={query} onChange={onChange} />);
|
||||||
|
await waitFor(() =>
|
||||||
|
expect(onChange).toHaveBeenCalledWith(expect.objectContaining({ queryType: QueryType.TIME_SERIES_LIST }))
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
@ -1,12 +1,11 @@
|
|||||||
import React, { PureComponent } from 'react';
|
import React, { useEffect, useMemo, useState } from 'react';
|
||||||
|
|
||||||
import { QueryEditorProps, toOption } from '@grafana/data';
|
import { QueryEditorProps, toOption } from '@grafana/data';
|
||||||
import { EditorRows } from '@grafana/experimental';
|
import { EditorRows } from '@grafana/experimental';
|
||||||
|
|
||||||
import CloudMonitoringDatasource from '../datasource';
|
import CloudMonitoringDatasource from '../datasource';
|
||||||
import { CloudMonitoringQuery, MetricQuery, QueryType, SLOQuery, CloudMonitoringOptions } from '../types';
|
import { CloudMonitoringQuery, QueryType, SLOQuery, CloudMonitoringOptions } from '../types';
|
||||||
|
|
||||||
import { defaultQuery } from './MetricQueryEditor';
|
|
||||||
import { QueryHeader } from './QueryHeader';
|
import { QueryHeader } from './QueryHeader';
|
||||||
import { defaultQuery as defaultSLOQuery } from './SLOQueryEditor';
|
import { defaultQuery as defaultSLOQuery } from './SLOQueryEditor';
|
||||||
|
|
||||||
@ -14,80 +13,68 @@ import { MetricQueryEditor, SLOQueryEditor } from './';
|
|||||||
|
|
||||||
export type Props = QueryEditorProps<CloudMonitoringDatasource, CloudMonitoringQuery, CloudMonitoringOptions>;
|
export type Props = QueryEditorProps<CloudMonitoringDatasource, CloudMonitoringQuery, CloudMonitoringOptions>;
|
||||||
|
|
||||||
export class QueryEditor extends PureComponent<Props> {
|
export const QueryEditor = (props: Props) => {
|
||||||
async UNSAFE_componentWillMount() {
|
const { datasource, query: oldQ, onRunQuery, onChange } = props;
|
||||||
const { datasource, query } = this.props;
|
// Migrate query if needed
|
||||||
|
const [migrated, setMigrated] = useState(false);
|
||||||
// Unfortunately, migrations like this need to go UNSAFE_componentWillMount. As soon as there's
|
const query = useMemo(() => {
|
||||||
// migration hook for this module.ts, we can do the migrations there instead.
|
if (!migrated) {
|
||||||
if (!this.props.query.hasOwnProperty('metricQuery')) {
|
setMigrated(true);
|
||||||
const { hide, refId, datasource, key, queryType, maxLines, metric, ...metricQuery } = this.props.query as any;
|
return datasource.migrateQuery(oldQ);
|
||||||
this.props.query.metricQuery = metricQuery;
|
|
||||||
}
|
}
|
||||||
|
return oldQ;
|
||||||
|
}, [oldQ, datasource, migrated]);
|
||||||
|
|
||||||
if (![QueryType.METRICS, QueryType.SLO].includes(this.props.query.queryType)) {
|
const sloQuery = { ...defaultSLOQuery(datasource), ...query.sloQuery };
|
||||||
this.props.query.queryType = QueryType.METRICS;
|
const onSLOQueryChange = (q: SLOQuery) => {
|
||||||
|
onChange({ ...query, sloQuery: q });
|
||||||
|
onRunQuery();
|
||||||
|
};
|
||||||
|
|
||||||
|
const meta = props.data?.series.length ? props.data?.series[0].meta : {};
|
||||||
|
const customMetaData = meta?.custom ?? {};
|
||||||
|
const variableOptionGroup = {
|
||||||
|
label: 'Template Variables',
|
||||||
|
expanded: false,
|
||||||
|
options: datasource.getVariables().map(toOption),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Use a known query type
|
||||||
|
useEffect(() => {
|
||||||
|
if (!Object.values(QueryType).includes(query.queryType)) {
|
||||||
|
onChange({ ...query, queryType: QueryType.TIME_SERIES_LIST });
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
const queryType = query.queryType;
|
||||||
|
|
||||||
await datasource.ensureGCEDefaultProject();
|
return (
|
||||||
if (!query.metricQuery.projectName) {
|
<EditorRows>
|
||||||
this.props.query.metricQuery.projectName = datasource.getDefaultProject();
|
<QueryHeader query={query} onChange={onChange} onRunQuery={onRunQuery} />
|
||||||
}
|
{queryType !== QueryType.SLO && (
|
||||||
}
|
<MetricQueryEditor
|
||||||
|
refId={query.refId}
|
||||||
onQueryChange(prop: string, value: MetricQuery | SLOQuery) {
|
variableOptionGroup={variableOptionGroup}
|
||||||
this.props.onChange({ ...this.props.query, [prop]: value });
|
customMetaData={customMetaData}
|
||||||
this.props.onRunQuery();
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { datasource, query, onRunQuery, onChange } = this.props;
|
|
||||||
const metricQuery = { ...defaultQuery(datasource), ...query.metricQuery };
|
|
||||||
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 customMetaData = meta?.custom ?? {};
|
|
||||||
const variableOptionGroup = {
|
|
||||||
label: 'Template Variables',
|
|
||||||
expanded: false,
|
|
||||||
options: datasource.getVariables().map(toOption),
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<EditorRows>
|
|
||||||
<QueryHeader
|
|
||||||
query={query}
|
|
||||||
metricQuery={metricQuery}
|
|
||||||
sloQuery={sloQuery}
|
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
onRunQuery={onRunQuery}
|
onRunQuery={onRunQuery}
|
||||||
|
datasource={datasource}
|
||||||
|
query={query}
|
||||||
/>
|
/>
|
||||||
{queryType === QueryType.METRICS && (
|
)}
|
||||||
<MetricQueryEditor
|
|
||||||
refId={query.refId}
|
|
||||||
variableOptionGroup={variableOptionGroup}
|
|
||||||
customMetaData={customMetaData}
|
|
||||||
onChange={(metricQuery: MetricQuery) => {
|
|
||||||
this.props.onChange({ ...this.props.query, metricQuery });
|
|
||||||
}}
|
|
||||||
onRunQuery={onRunQuery}
|
|
||||||
datasource={datasource}
|
|
||||||
query={metricQuery}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{queryType === QueryType.SLO && (
|
{queryType === QueryType.SLO && (
|
||||||
<SLOQueryEditor
|
<SLOQueryEditor
|
||||||
refId={query.refId}
|
refId={query.refId}
|
||||||
variableOptionGroup={variableOptionGroup}
|
variableOptionGroup={variableOptionGroup}
|
||||||
customMetaData={customMetaData}
|
customMetaData={customMetaData}
|
||||||
onChange={(query: SLOQuery) => this.onQueryChange('sloQuery', query)}
|
onChange={onSLOQueryChange}
|
||||||
onRunQuery={onRunQuery}
|
onRunQuery={onRunQuery}
|
||||||
datasource={datasource}
|
datasource={datasource}
|
||||||
query={sloQuery}
|
query={sloQuery}
|
||||||
/>
|
aliasBy={query.aliasBy}
|
||||||
)}
|
onChangeAliasBy={(aliasBy: string) => onChange({ ...query, aliasBy })}
|
||||||
</EditorRows>
|
/>
|
||||||
);
|
)}
|
||||||
}
|
</EditorRows>
|
||||||
}
|
);
|
||||||
|
};
|
||||||
|
@ -1,74 +1,19 @@
|
|||||||
import { render, screen } from '@testing-library/react';
|
import { render, screen } from '@testing-library/react';
|
||||||
import userEvent from '@testing-library/user-event';
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { openMenu, select } from 'react-select-event';
|
import { openMenu, select } from 'react-select-event';
|
||||||
|
|
||||||
import { createMockQuery, createMockSLOQuery } from '../__mocks__/cloudMonitoringQuery';
|
import { createMockQuery } from '../__mocks__/cloudMonitoringQuery';
|
||||||
import { EditorMode, QueryType } from '../types';
|
import { QueryType } from '../types';
|
||||||
|
|
||||||
import { QueryHeader } from './QueryHeader';
|
import { QueryHeader } from './QueryHeader';
|
||||||
|
|
||||||
describe('QueryHeader', () => {
|
describe('QueryHeader', () => {
|
||||||
it('renders an editor mode radio group if query type is a metric query', () => {
|
it('can change query types to SLO', async () => {
|
||||||
const query = createMockQuery();
|
const query = createMockQuery();
|
||||||
const { metricQuery } = query;
|
|
||||||
const sloQuery = createMockSLOQuery();
|
|
||||||
const onChange = jest.fn();
|
const onChange = jest.fn();
|
||||||
const onRunQuery = jest.fn();
|
const onRunQuery = jest.fn();
|
||||||
|
|
||||||
render(
|
render(<QueryHeader query={query} onChange={onChange} onRunQuery={onRunQuery} />);
|
||||||
<QueryHeader
|
|
||||||
query={query}
|
|
||||||
metricQuery={metricQuery}
|
|
||||||
sloQuery={sloQuery}
|
|
||||||
onChange={onChange}
|
|
||||||
onRunQuery={onRunQuery}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(screen.getByLabelText(/Query type/)).toBeInTheDocument();
|
|
||||||
expect(screen.getByLabelText('Builder')).toBeInTheDocument();
|
|
||||||
expect(screen.getByLabelText('MQL')).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('does not render an editor mode radio group if query type is a SLO query', () => {
|
|
||||||
const query = createMockQuery({ queryType: QueryType.SLO });
|
|
||||||
const { metricQuery } = query;
|
|
||||||
const sloQuery = createMockSLOQuery();
|
|
||||||
const onChange = jest.fn();
|
|
||||||
const onRunQuery = jest.fn();
|
|
||||||
|
|
||||||
render(
|
|
||||||
<QueryHeader
|
|
||||||
query={query}
|
|
||||||
metricQuery={metricQuery}
|
|
||||||
sloQuery={sloQuery}
|
|
||||||
onChange={onChange}
|
|
||||||
onRunQuery={onRunQuery}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(screen.getByLabelText(/Query type/)).toBeInTheDocument();
|
|
||||||
expect(screen.queryByLabelText('Builder')).not.toBeInTheDocument();
|
|
||||||
expect(screen.queryByLabelText('MQL')).not.toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('can change query types', async () => {
|
|
||||||
const query = createMockQuery();
|
|
||||||
const { metricQuery } = query;
|
|
||||||
const sloQuery = createMockSLOQuery();
|
|
||||||
const onChange = jest.fn();
|
|
||||||
const onRunQuery = jest.fn();
|
|
||||||
|
|
||||||
render(
|
|
||||||
<QueryHeader
|
|
||||||
query={query}
|
|
||||||
metricQuery={metricQuery}
|
|
||||||
sloQuery={sloQuery}
|
|
||||||
onChange={onChange}
|
|
||||||
onRunQuery={onRunQuery}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
const queryType = screen.getByLabelText(/Query type/);
|
const queryType = screen.getByLabelText(/Query type/);
|
||||||
await openMenu(queryType);
|
await openMenu(queryType);
|
||||||
@ -76,32 +21,16 @@ describe('QueryHeader', () => {
|
|||||||
expect(onChange).toBeCalledWith(expect.objectContaining({ queryType: QueryType.SLO }));
|
expect(onChange).toBeCalledWith(expect.objectContaining({ queryType: QueryType.SLO }));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('can change editor modes when query is a metric query type', async () => {
|
it('can change query types to MQL', async () => {
|
||||||
const query = createMockQuery();
|
const query = createMockQuery();
|
||||||
const { metricQuery } = query;
|
|
||||||
const sloQuery = createMockSLOQuery();
|
|
||||||
const onChange = jest.fn();
|
const onChange = jest.fn();
|
||||||
const onRunQuery = jest.fn();
|
const onRunQuery = jest.fn();
|
||||||
|
|
||||||
render(
|
render(<QueryHeader query={query} onChange={onChange} onRunQuery={onRunQuery} />);
|
||||||
<QueryHeader
|
|
||||||
query={query}
|
|
||||||
metricQuery={metricQuery}
|
|
||||||
sloQuery={sloQuery}
|
|
||||||
onChange={onChange}
|
|
||||||
onRunQuery={onRunQuery}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
const builder = screen.getByLabelText('Builder');
|
const queryType = screen.getByLabelText(/Query type/);
|
||||||
const MQL = screen.getByLabelText('MQL');
|
await openMenu(queryType);
|
||||||
expect(builder).toBeChecked();
|
await select(screen.getByLabelText('Select options menu'), 'MQL');
|
||||||
expect(MQL).not.toBeChecked();
|
expect(onChange).toBeCalledWith(expect.objectContaining({ queryType: QueryType.TIME_SERIES_QUERY }));
|
||||||
|
|
||||||
await userEvent.click(MQL);
|
|
||||||
|
|
||||||
expect(onChange).toBeCalledWith(
|
|
||||||
expect.objectContaining({ metricQuery: expect.objectContaining({ editorMode: EditorMode.MQL }) })
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -1,28 +1,19 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import { EditorHeader, FlexItem, InlineSelect } from '@grafana/experimental';
|
import { EditorHeader, FlexItem, InlineSelect } from '@grafana/experimental';
|
||||||
import { RadioButtonGroup } from '@grafana/ui';
|
|
||||||
|
|
||||||
import { QUERY_TYPES } from '../constants';
|
import { QUERY_TYPES } from '../constants';
|
||||||
import { EditorMode, CloudMonitoringQuery, QueryType, SLOQuery, MetricQuery } from '../types';
|
import { CloudMonitoringQuery } from '../types';
|
||||||
|
|
||||||
export interface QueryEditorHeaderProps {
|
export interface QueryEditorHeaderProps {
|
||||||
query: CloudMonitoringQuery;
|
query: CloudMonitoringQuery;
|
||||||
metricQuery: MetricQuery;
|
|
||||||
sloQuery: SLOQuery;
|
|
||||||
onChange: (value: CloudMonitoringQuery) => void;
|
onChange: (value: CloudMonitoringQuery) => void;
|
||||||
onRunQuery: () => void;
|
onRunQuery: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const EDITOR_MODES = [
|
|
||||||
{ label: 'Builder', value: EditorMode.Visual },
|
|
||||||
{ label: 'MQL', value: EditorMode.MQL },
|
|
||||||
];
|
|
||||||
|
|
||||||
export const QueryHeader = (props: QueryEditorHeaderProps) => {
|
export const QueryHeader = (props: QueryEditorHeaderProps) => {
|
||||||
const { query, metricQuery, sloQuery, onChange, onRunQuery } = props;
|
const { query, onChange, onRunQuery } = props;
|
||||||
const { queryType } = query;
|
const { queryType } = query;
|
||||||
const { editorMode } = metricQuery;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<EditorHeader>
|
<EditorHeader>
|
||||||
@ -31,27 +22,11 @@ export const QueryHeader = (props: QueryEditorHeaderProps) => {
|
|||||||
options={QUERY_TYPES}
|
options={QUERY_TYPES}
|
||||||
value={queryType}
|
value={queryType}
|
||||||
onChange={({ value }) => {
|
onChange={({ value }) => {
|
||||||
onChange({ ...query, sloQuery, queryType: value! });
|
onChange({ ...query, queryType: value! });
|
||||||
onRunQuery();
|
onRunQuery();
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<FlexItem grow={1} />
|
<FlexItem grow={1} />
|
||||||
{queryType !== QueryType.SLO && (
|
|
||||||
<RadioButtonGroup
|
|
||||||
size="sm"
|
|
||||||
options={EDITOR_MODES}
|
|
||||||
value={editorMode || EditorMode.Visual}
|
|
||||||
onChange={(value) => {
|
|
||||||
onChange({
|
|
||||||
...query,
|
|
||||||
metricQuery: {
|
|
||||||
...metricQuery,
|
|
||||||
editorMode: value,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</EditorHeader>
|
</EditorHeader>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -24,6 +24,8 @@ export interface Props {
|
|||||||
onRunQuery: () => void;
|
onRunQuery: () => void;
|
||||||
query: SLOQuery;
|
query: SLOQuery;
|
||||||
datasource: CloudMonitoringDatasource;
|
datasource: CloudMonitoringDatasource;
|
||||||
|
aliasBy?: string;
|
||||||
|
onChangeAliasBy: (aliasBy: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const defaultQuery: (dataSource: CloudMonitoringDatasource) => SLOQuery = (dataSource) => ({
|
export const defaultQuery: (dataSource: CloudMonitoringDatasource) => SLOQuery = (dataSource) => ({
|
||||||
@ -46,6 +48,8 @@ export function SLOQueryEditor({
|
|||||||
onChange,
|
onChange,
|
||||||
variableOptionGroup,
|
variableOptionGroup,
|
||||||
customMetaData,
|
customMetaData,
|
||||||
|
aliasBy,
|
||||||
|
onChangeAliasBy,
|
||||||
}: React.PropsWithChildren<Props>) {
|
}: React.PropsWithChildren<Props>) {
|
||||||
const alignmentLabel = useMemo(() => alignmentPeriodLabel(customMetaData, datasource), [customMetaData, datasource]);
|
const alignmentLabel = useMemo(() => alignmentPeriodLabel(customMetaData, datasource), [customMetaData, datasource]);
|
||||||
return (
|
return (
|
||||||
@ -100,7 +104,7 @@ export function SLOQueryEditor({
|
|||||||
</EditorField>
|
</EditorField>
|
||||||
</EditorFieldGroup>
|
</EditorFieldGroup>
|
||||||
|
|
||||||
<AliasBy refId={refId} value={query.aliasBy} onChange={(aliasBy) => onChange({ ...query, aliasBy })} />
|
<AliasBy refId={refId} value={aliasBy} onChange={onChangeAliasBy} />
|
||||||
</EditorRow>
|
</EditorRow>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@ -1,10 +1,11 @@
|
|||||||
import React from 'react';
|
import React, { useCallback, useEffect, useState } from 'react';
|
||||||
|
|
||||||
import { SelectableValue } from '@grafana/data';
|
import { SelectableValue } from '@grafana/data';
|
||||||
import { EditorRow } from '@grafana/experimental';
|
import { EditorRow } from '@grafana/experimental';
|
||||||
|
|
||||||
import CloudMonitoringDatasource from '../datasource';
|
import CloudMonitoringDatasource from '../datasource';
|
||||||
import { CustomMetaData, MetricDescriptor, MetricQuery, SLOQuery } from '../types';
|
import { getAlignmentPickerData, getMetricType, setMetricType } from '../functions';
|
||||||
|
import { CustomMetaData, MetricDescriptor, MetricKind, PreprocessorType, TimeSeriesList, ValueTypes } from '../types';
|
||||||
|
|
||||||
import { AliasBy } from './AliasBy';
|
import { AliasBy } from './AliasBy';
|
||||||
import { Alignment } from './Alignment';
|
import { Alignment } from './Alignment';
|
||||||
@ -17,28 +18,59 @@ export interface Props {
|
|||||||
refId: string;
|
refId: string;
|
||||||
customMetaData: CustomMetaData;
|
customMetaData: CustomMetaData;
|
||||||
variableOptionGroup: SelectableValue<string>;
|
variableOptionGroup: SelectableValue<string>;
|
||||||
onMetricTypeChange: (query: MetricDescriptor) => void;
|
onChange: (query: TimeSeriesList) => void;
|
||||||
onChange: (query: MetricQuery | SLOQuery) => void;
|
query: TimeSeriesList;
|
||||||
query: MetricQuery;
|
|
||||||
datasource: CloudMonitoringDatasource;
|
datasource: CloudMonitoringDatasource;
|
||||||
labels: any;
|
aliasBy?: string;
|
||||||
|
onChangeAliasBy: (aliasBy: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
function Editor({
|
function Editor({
|
||||||
refId,
|
refId,
|
||||||
query,
|
query,
|
||||||
labels,
|
|
||||||
datasource,
|
datasource,
|
||||||
onChange,
|
onChange,
|
||||||
onMetricTypeChange,
|
|
||||||
customMetaData,
|
customMetaData,
|
||||||
variableOptionGroup,
|
variableOptionGroup,
|
||||||
|
aliasBy,
|
||||||
|
onChangeAliasBy,
|
||||||
}: React.PropsWithChildren<Props>) {
|
}: React.PropsWithChildren<Props>) {
|
||||||
|
const [labels, setLabels] = useState<{ [k: string]: any }>({});
|
||||||
|
const { projectName, groupBys, crossSeriesReducer } = query;
|
||||||
|
const metricType = getMetricType(query);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (projectName && metricType) {
|
||||||
|
datasource.getLabels(metricType, refId, projectName).then((labels) => setLabels(labels));
|
||||||
|
}
|
||||||
|
}, [datasource, groupBys, metricType, projectName, refId, crossSeriesReducer]);
|
||||||
|
|
||||||
|
const onMetricTypeChange = useCallback(
|
||||||
|
({ valueType, metricKind, type }: MetricDescriptor) => {
|
||||||
|
const preprocessor =
|
||||||
|
metricKind === MetricKind.GAUGE || valueType === ValueTypes.DISTRIBUTION
|
||||||
|
? PreprocessorType.None
|
||||||
|
: PreprocessorType.Rate;
|
||||||
|
const { perSeriesAligner } = getAlignmentPickerData(valueType, metricKind, query.perSeriesAligner, preprocessor);
|
||||||
|
onChange({
|
||||||
|
...setMetricType(
|
||||||
|
{
|
||||||
|
...query,
|
||||||
|
perSeriesAligner,
|
||||||
|
},
|
||||||
|
type
|
||||||
|
),
|
||||||
|
preprocessor,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[onChange, query]
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Metrics
|
<Metrics
|
||||||
refId={refId}
|
refId={refId}
|
||||||
projectName={query.projectName}
|
projectName={query.projectName}
|
||||||
metricType={query.metricType}
|
metricType={metricType}
|
||||||
templateVariableOptions={variableOptionGroup.options}
|
templateVariableOptions={variableOptionGroup.options}
|
||||||
datasource={datasource}
|
datasource={datasource}
|
||||||
onChange={onMetricTypeChange}
|
onChange={onMetricTypeChange}
|
||||||
@ -70,14 +102,10 @@ function Editor({
|
|||||||
query={query}
|
query={query}
|
||||||
customMetaData={customMetaData}
|
customMetaData={customMetaData}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
|
metricDescriptor={metric}
|
||||||
|
preprocessor={query.preprocessor}
|
||||||
/>
|
/>
|
||||||
<AliasBy
|
<AliasBy refId={refId} value={aliasBy} onChange={onChangeAliasBy} />
|
||||||
refId={refId}
|
|
||||||
value={query.aliasBy}
|
|
||||||
onChange={(aliasBy) => {
|
|
||||||
onChange({ ...query, aliasBy });
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</EditorRow>
|
</EditorRow>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
@ -316,6 +316,7 @@ export const SELECTORS = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
export const QUERY_TYPES = [
|
export const QUERY_TYPES = [
|
||||||
{ label: 'Metrics', value: QueryType.METRICS },
|
{ label: 'Builder', value: QueryType.TIME_SERIES_LIST },
|
||||||
|
{ label: 'MQL', value: QueryType.TIME_SERIES_QUERY },
|
||||||
{ label: 'Service Level Objectives (SLO)', value: QueryType.SLO },
|
{ label: 'Service Level Objectives (SLO)', value: QueryType.SLO },
|
||||||
];
|
];
|
||||||
|
@ -1,8 +1,12 @@
|
|||||||
|
import { get } from 'lodash';
|
||||||
|
import { lastValueFrom, of } from 'rxjs';
|
||||||
|
|
||||||
import { TemplateSrv } from 'app/features/templating/template_srv';
|
import { TemplateSrv } from 'app/features/templating/template_srv';
|
||||||
|
|
||||||
import { createMockInstanceSetttings } from './__mocks__/cloudMonitoringInstanceSettings';
|
import { createMockInstanceSetttings } from './__mocks__/cloudMonitoringInstanceSettings';
|
||||||
import { createMockQuery } from './__mocks__/cloudMonitoringQuery';
|
import { createMockQuery } from './__mocks__/cloudMonitoringQuery';
|
||||||
import Datasource from './datasource';
|
import Datasource from './datasource';
|
||||||
|
import { CloudMonitoringQuery, MetricKind, PreprocessorType, QueryType } from './types';
|
||||||
|
|
||||||
describe('Cloud Monitoring Datasource', () => {
|
describe('Cloud Monitoring Datasource', () => {
|
||||||
describe('interpolateVariablesInQueries', () => {
|
describe('interpolateVariablesInQueries', () => {
|
||||||
@ -14,15 +18,289 @@ describe('Cloud Monitoring Datasource', () => {
|
|||||||
expect(templateVariablesApplied[0]).toEqual(query);
|
expect(templateVariablesApplied[0]).toEqual(query);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should correctly apply template variables', () => {
|
it('should correctly apply template variables for metricQuery (deprecated)', () => {
|
||||||
const templateSrv = new TemplateSrv();
|
const templateSrv = new TemplateSrv();
|
||||||
templateSrv.replace = jest.fn().mockReturnValue('project-variable');
|
templateSrv.replace = jest.fn().mockReturnValue('project-variable');
|
||||||
const mockInstanceSettings = createMockInstanceSetttings();
|
const mockInstanceSettings = createMockInstanceSetttings();
|
||||||
const ds = new Datasource(mockInstanceSettings, templateSrv);
|
const ds = new Datasource(mockInstanceSettings, templateSrv);
|
||||||
const query = createMockQuery({ metricQuery: { projectName: '$testVar' } });
|
const query = createMockQuery({ timeSeriesList: { projectName: '$testVar', crossSeriesReducer: '' } });
|
||||||
const templatedQuery = ds.interpolateVariablesInQueries([query], {});
|
const templatedQuery = ds.interpolateVariablesInQueries([query], {});
|
||||||
expect(templatedQuery[0]).toHaveProperty('datasource');
|
expect(templatedQuery[0]).toHaveProperty('datasource');
|
||||||
expect(templatedQuery[0].metricQuery.projectName).toEqual('project-variable');
|
expect(templatedQuery[0].timeSeriesList?.projectName).toEqual('project-variable');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should correctly apply template variables for timeSeriesList', () => {
|
||||||
|
const templateSrv = new TemplateSrv();
|
||||||
|
templateSrv.replace = jest.fn().mockReturnValue('project-variable');
|
||||||
|
const mockInstanceSettings = createMockInstanceSetttings();
|
||||||
|
const ds = new Datasource(mockInstanceSettings, templateSrv);
|
||||||
|
const query = createMockQuery({ timeSeriesList: { projectName: '$testVar', crossSeriesReducer: '' } });
|
||||||
|
const templatedQuery = ds.interpolateVariablesInQueries([query], {});
|
||||||
|
expect(templatedQuery[0]).toHaveProperty('datasource');
|
||||||
|
expect(templatedQuery[0].timeSeriesList?.projectName).toEqual('project-variable');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should correctly apply template variables for timeSeriesQuery', () => {
|
||||||
|
const templateSrv = new TemplateSrv();
|
||||||
|
templateSrv.replace = jest.fn().mockReturnValue('project-variable');
|
||||||
|
const mockInstanceSettings = createMockInstanceSetttings();
|
||||||
|
const ds = new Datasource(mockInstanceSettings, templateSrv);
|
||||||
|
const query = createMockQuery({ timeSeriesQuery: { projectName: '$testVar', query: '' } });
|
||||||
|
const templatedQuery = ds.interpolateVariablesInQueries([query], {});
|
||||||
|
expect(templatedQuery[0]).toHaveProperty('datasource');
|
||||||
|
expect(templatedQuery[0].timeSeriesList?.projectName).toEqual('project-variable');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('migrateQuery', () => {
|
||||||
|
describe('should migrate the query to the new format', () => {
|
||||||
|
[
|
||||||
|
{
|
||||||
|
description: 'a list query with a metric type and no filters',
|
||||||
|
input: {
|
||||||
|
refId: 'A',
|
||||||
|
queryType: 'metrics',
|
||||||
|
intervalMs: 1000,
|
||||||
|
metricQuery: {
|
||||||
|
metricType: 'cloudsql_database',
|
||||||
|
projectName: 'project',
|
||||||
|
filters: [],
|
||||||
|
groupBys: [],
|
||||||
|
aliasBy: '',
|
||||||
|
alignmentPeriod: 'cloud-monitoring-auto',
|
||||||
|
crossSeriesReducer: 'REDUCE_NONE',
|
||||||
|
perSeriesAligner: 'ALIGN_MEAN',
|
||||||
|
metricKind: MetricKind.DELTA,
|
||||||
|
valueType: 'DOUBLE',
|
||||||
|
query: '',
|
||||||
|
editorMode: 'visual',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: {
|
||||||
|
timeSeriesList: {
|
||||||
|
alignmentPeriod: 'cloud-monitoring-auto',
|
||||||
|
crossSeriesReducer: 'REDUCE_NONE',
|
||||||
|
filters: ['metric.type', '=', 'cloudsql_database'],
|
||||||
|
groupBys: [],
|
||||||
|
perSeriesAligner: 'ALIGN_MEAN',
|
||||||
|
projectName: 'project',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: 'a list query with filters',
|
||||||
|
input: {
|
||||||
|
refId: 'A',
|
||||||
|
queryType: 'metrics',
|
||||||
|
intervalMs: 1000,
|
||||||
|
metricQuery: {
|
||||||
|
metricType: 'cloudsql_database',
|
||||||
|
projectName: 'project',
|
||||||
|
filters: ['foo', '=', 'bar'],
|
||||||
|
groupBys: [],
|
||||||
|
aliasBy: '',
|
||||||
|
alignmentPeriod: 'cloud-monitoring-auto',
|
||||||
|
crossSeriesReducer: 'REDUCE_NONE',
|
||||||
|
perSeriesAligner: 'ALIGN_MEAN',
|
||||||
|
metricKind: MetricKind.DELTA,
|
||||||
|
valueType: 'DOUBLE',
|
||||||
|
query: '',
|
||||||
|
editorMode: 'visual',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: {
|
||||||
|
timeSeriesList: {
|
||||||
|
alignmentPeriod: 'cloud-monitoring-auto',
|
||||||
|
crossSeriesReducer: 'REDUCE_NONE',
|
||||||
|
filters: ['foo', '=', 'bar', 'AND', 'metric.type', '=', 'cloudsql_database'],
|
||||||
|
groupBys: [],
|
||||||
|
perSeriesAligner: 'ALIGN_MEAN',
|
||||||
|
projectName: 'project',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: 'a list query with preprocessor',
|
||||||
|
input: {
|
||||||
|
refId: 'A',
|
||||||
|
queryType: 'metrics',
|
||||||
|
intervalMs: 1000,
|
||||||
|
metricQuery: {
|
||||||
|
metricType: 'cloudsql_database',
|
||||||
|
projectName: 'project',
|
||||||
|
filters: ['foo', '=', 'bar'],
|
||||||
|
groupBys: [],
|
||||||
|
aliasBy: '',
|
||||||
|
alignmentPeriod: 'cloud-monitoring-auto',
|
||||||
|
crossSeriesReducer: 'REDUCE_NONE',
|
||||||
|
perSeriesAligner: 'ALIGN_MEAN',
|
||||||
|
metricKind: MetricKind.DELTA,
|
||||||
|
valueType: 'DOUBLE',
|
||||||
|
query: '',
|
||||||
|
editorMode: 'visual',
|
||||||
|
preprocessor: PreprocessorType.Delta,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: {
|
||||||
|
timeSeriesList: {
|
||||||
|
alignmentPeriod: 'cloud-monitoring-auto',
|
||||||
|
crossSeriesReducer: 'REDUCE_NONE',
|
||||||
|
filters: ['foo', '=', 'bar', 'AND', 'metric.type', '=', 'cloudsql_database'],
|
||||||
|
groupBys: [],
|
||||||
|
projectName: 'project',
|
||||||
|
perSeriesAligner: 'ALIGN_MEAN',
|
||||||
|
preprocessor: PreprocessorType.Delta,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: 'a mql query',
|
||||||
|
input: {
|
||||||
|
refId: 'A',
|
||||||
|
queryType: 'metrics',
|
||||||
|
intervalMs: 1000,
|
||||||
|
metricQuery: {
|
||||||
|
metricType: 'cloudsql_database',
|
||||||
|
projectName: 'project',
|
||||||
|
filters: ['foo', '=', 'bar'],
|
||||||
|
groupBys: [],
|
||||||
|
aliasBy: '',
|
||||||
|
alignmentPeriod: 'cloud-monitoring-auto',
|
||||||
|
crossSeriesReducer: 'REDUCE_NONE',
|
||||||
|
perSeriesAligner: 'ALIGN_MEAN',
|
||||||
|
metricKind: MetricKind.DELTA,
|
||||||
|
valueType: 'DOUBLE',
|
||||||
|
query: 'test query',
|
||||||
|
editorMode: 'mql',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: {
|
||||||
|
timeSeriesQuery: {
|
||||||
|
projectName: 'project',
|
||||||
|
query: 'test query',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: 'a SLO query with alias',
|
||||||
|
input: {
|
||||||
|
refId: 'A',
|
||||||
|
queryType: QueryType.SLO,
|
||||||
|
intervalMs: 1000,
|
||||||
|
sloQuery: {
|
||||||
|
aliasBy: 'alias',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: {
|
||||||
|
aliasBy: 'alias',
|
||||||
|
sloQuery: {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
].forEach((t) =>
|
||||||
|
it(t.description, () => {
|
||||||
|
const mockInstanceSettings = createMockInstanceSetttings();
|
||||||
|
const ds = new Datasource(mockInstanceSettings);
|
||||||
|
const oldQuery = { ...t.input } as CloudMonitoringQuery;
|
||||||
|
const newQuery = ds.migrateQuery(oldQuery);
|
||||||
|
expect(get(newQuery, 'metricQuery')).toBeUndefined();
|
||||||
|
expect(newQuery).toMatchObject(t.expected);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('filterQuery', () => {
|
||||||
|
[
|
||||||
|
{
|
||||||
|
description: 'should filter out queries with no metric type',
|
||||||
|
input: {},
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: 'should include an SLO query',
|
||||||
|
input: {
|
||||||
|
queryType: QueryType.SLO,
|
||||||
|
sloQuery: {
|
||||||
|
selectorName: 'selector',
|
||||||
|
serviceId: 'service',
|
||||||
|
sloId: 'slo',
|
||||||
|
projectName: 'project',
|
||||||
|
lookbackPeriod: '30d',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: 'should include a time series query',
|
||||||
|
input: {
|
||||||
|
queryType: QueryType.TIME_SERIES_QUERY,
|
||||||
|
timeSeriesQuery: {
|
||||||
|
projectName: 'project',
|
||||||
|
query: 'test query',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: 'should include a time series list query',
|
||||||
|
input: {
|
||||||
|
queryType: QueryType.TIME_SERIES_LIST,
|
||||||
|
timeSeriesList: {
|
||||||
|
projectName: 'project',
|
||||||
|
filters: ['metric.type', '=', 'cloudsql_database'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: 'should include an annotation query',
|
||||||
|
input: {
|
||||||
|
queryType: QueryType.ANNOTATION,
|
||||||
|
timeSeriesList: {
|
||||||
|
projectName: 'project',
|
||||||
|
filters: ['metric.type', '=', 'cloudsql_database'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
].forEach((t) =>
|
||||||
|
it(t.description, () => {
|
||||||
|
const mockInstanceSettings = createMockInstanceSetttings();
|
||||||
|
const ds = new Datasource(mockInstanceSettings);
|
||||||
|
const query = { ...t.input } as CloudMonitoringQuery;
|
||||||
|
const result = ds.filterQuery(query);
|
||||||
|
expect(result).toBe(t.expected);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getLabels', () => {
|
||||||
|
it('should get labels', async () => {
|
||||||
|
const mockInstanceSettings = createMockInstanceSetttings();
|
||||||
|
const ds = new Datasource(mockInstanceSettings);
|
||||||
|
ds.backendSrv = {
|
||||||
|
...ds.backendSrv,
|
||||||
|
fetch: jest.fn().mockReturnValue(lastValueFrom(of({ results: [] }))),
|
||||||
|
};
|
||||||
|
await ds.getLabels('gce_instance', 'A', 'my-project');
|
||||||
|
expect(ds.backendSrv.fetch).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
data: expect.objectContaining({
|
||||||
|
queries: expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
queryType: 'timeSeriesList',
|
||||||
|
timeSeriesList: {
|
||||||
|
crossSeriesReducer: 'REDUCE_NONE',
|
||||||
|
filters: ['metric.type', '=', 'gce_instance'],
|
||||||
|
groupBys: [],
|
||||||
|
projectName: 'my-project',
|
||||||
|
view: 'HEADERS',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { chunk, flatten, isString, isArray } from 'lodash';
|
import { chunk, flatten, isString, isArray, has, get, omit } from 'lodash';
|
||||||
import { from, lastValueFrom, Observable, of } from 'rxjs';
|
import { from, lastValueFrom, Observable, of } from 'rxjs';
|
||||||
import { map, mergeMap } from 'rxjs/operators';
|
import { map, mergeMap } from 'rxjs/operators';
|
||||||
|
|
||||||
@ -9,21 +9,22 @@ import {
|
|||||||
ScopedVars,
|
ScopedVars,
|
||||||
SelectableValue,
|
SelectableValue,
|
||||||
} from '@grafana/data';
|
} from '@grafana/data';
|
||||||
import { DataSourceWithBackend, getBackendSrv, toDataQueryResponse } from '@grafana/runtime';
|
import { DataSourceWithBackend, getBackendSrv, toDataQueryResponse, BackendSrv } from '@grafana/runtime';
|
||||||
import { getTimeSrv, TimeSrv } from 'app/features/dashboard/services/TimeSrv';
|
import { getTimeSrv, TimeSrv } from 'app/features/dashboard/services/TimeSrv';
|
||||||
import { getTemplateSrv, TemplateSrv } from 'app/features/templating/template_srv';
|
import { getTemplateSrv, TemplateSrv } from 'app/features/templating/template_srv';
|
||||||
|
|
||||||
import { CloudMonitoringAnnotationSupport } from './annotationSupport';
|
import { CloudMonitoringAnnotationSupport } from './annotationSupport';
|
||||||
import { SLO_BURN_RATE_SELECTOR_NAME } from './constants';
|
import { SLO_BURN_RATE_SELECTOR_NAME } from './constants';
|
||||||
|
import { getMetricType, setMetricType } from './functions';
|
||||||
import {
|
import {
|
||||||
CloudMonitoringOptions,
|
CloudMonitoringOptions,
|
||||||
CloudMonitoringQuery,
|
CloudMonitoringQuery,
|
||||||
EditorMode,
|
|
||||||
Filter,
|
Filter,
|
||||||
MetricDescriptor,
|
MetricDescriptor,
|
||||||
QueryType,
|
QueryType,
|
||||||
PostResponse,
|
PostResponse,
|
||||||
Aggregation,
|
Aggregation,
|
||||||
|
MetricQuery,
|
||||||
} from './types';
|
} from './types';
|
||||||
import { CloudMonitoringVariableSupport } from './variables';
|
import { CloudMonitoringVariableSupport } from './variables';
|
||||||
|
|
||||||
@ -33,6 +34,7 @@ export default class CloudMonitoringDatasource extends DataSourceWithBackend<
|
|||||||
> {
|
> {
|
||||||
authenticationType: string;
|
authenticationType: string;
|
||||||
intervalMs: number;
|
intervalMs: number;
|
||||||
|
backendSrv: BackendSrv;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private instanceSettings: DataSourceInstanceSettings<CloudMonitoringOptions>,
|
private instanceSettings: DataSourceInstanceSettings<CloudMonitoringOptions>,
|
||||||
@ -44,6 +46,7 @@ export default class CloudMonitoringDatasource extends DataSourceWithBackend<
|
|||||||
this.variables = new CloudMonitoringVariableSupport(this);
|
this.variables = new CloudMonitoringVariableSupport(this);
|
||||||
this.intervalMs = 0;
|
this.intervalMs = 0;
|
||||||
this.annotations = CloudMonitoringAnnotationSupport(this);
|
this.annotations = CloudMonitoringAnnotationSupport(this);
|
||||||
|
this.backendSrv = getBackendSrv();
|
||||||
}
|
}
|
||||||
|
|
||||||
getVariables() {
|
getVariables() {
|
||||||
@ -59,21 +62,28 @@ export default class CloudMonitoringDatasource extends DataSourceWithBackend<
|
|||||||
}
|
}
|
||||||
|
|
||||||
applyTemplateVariables(target: CloudMonitoringQuery, scopedVars: ScopedVars): Record<string, any> {
|
applyTemplateVariables(target: CloudMonitoringQuery, scopedVars: ScopedVars): Record<string, any> {
|
||||||
const { metricQuery, sloQuery } = target;
|
const { timeSeriesList, timeSeriesQuery, sloQuery } = target;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...target,
|
...target,
|
||||||
datasource: this.getRef(),
|
datasource: this.getRef(),
|
||||||
intervalMs: this.intervalMs,
|
intervalMs: this.intervalMs,
|
||||||
metricQuery: {
|
timeSeriesList: timeSeriesList && {
|
||||||
...this.interpolateProps(metricQuery, scopedVars),
|
...this.interpolateProps(timeSeriesList, scopedVars),
|
||||||
projectName: this.templateSrv.replace(
|
projectName: this.templateSrv.replace(
|
||||||
metricQuery.projectName ? metricQuery.projectName : this.getDefaultProject(),
|
timeSeriesList.projectName ? timeSeriesList.projectName : this.getDefaultProject(),
|
||||||
|
scopedVars
|
||||||
|
),
|
||||||
|
filters: this.interpolateFilters(timeSeriesList.filters || [], scopedVars),
|
||||||
|
groupBys: this.interpolateGroupBys(timeSeriesList.groupBys || [], scopedVars),
|
||||||
|
view: timeSeriesList.view || 'FULL',
|
||||||
|
},
|
||||||
|
timeSeriesQuery: timeSeriesQuery && {
|
||||||
|
...this.interpolateProps(timeSeriesQuery, scopedVars),
|
||||||
|
projectName: this.templateSrv.replace(
|
||||||
|
timeSeriesQuery.projectName ? timeSeriesQuery.projectName : this.getDefaultProject(),
|
||||||
scopedVars
|
scopedVars
|
||||||
),
|
),
|
||||||
filters: this.interpolateFilters(metricQuery.filters || [], scopedVars),
|
|
||||||
groupBys: this.interpolateGroupBys(metricQuery.groupBys || [], scopedVars),
|
|
||||||
view: metricQuery.view || 'FULL',
|
|
||||||
editorMode: metricQuery.editorMode,
|
|
||||||
},
|
},
|
||||||
sloQuery: sloQuery && this.interpolateProps(sloQuery, scopedVars),
|
sloQuery: sloQuery && this.interpolateProps(sloQuery, scopedVars),
|
||||||
};
|
};
|
||||||
@ -85,18 +95,20 @@ export default class CloudMonitoringDatasource extends DataSourceWithBackend<
|
|||||||
{
|
{
|
||||||
refId,
|
refId,
|
||||||
datasource: this.getRef(),
|
datasource: this.getRef(),
|
||||||
queryType: QueryType.METRICS,
|
queryType: QueryType.TIME_SERIES_LIST,
|
||||||
metricQuery: {
|
timeSeriesList: setMetricType(
|
||||||
projectName: this.templateSrv.replace(projectName),
|
{
|
||||||
metricType: this.templateSrv.replace(metricType),
|
projectName: this.templateSrv.replace(projectName),
|
||||||
groupBys: this.interpolateGroupBys(aggregation?.groupBys || [], {}),
|
groupBys: this.interpolateGroupBys(aggregation?.groupBys || [], {}),
|
||||||
crossSeriesReducer: aggregation?.crossSeriesReducer ?? 'REDUCE_NONE',
|
crossSeriesReducer: aggregation?.crossSeriesReducer ?? 'REDUCE_NONE',
|
||||||
view: 'HEADERS',
|
view: 'HEADERS',
|
||||||
},
|
},
|
||||||
|
metricType
|
||||||
|
),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
range: this.timeSrv.timeRange(),
|
range: this.timeSrv.timeRange(),
|
||||||
} as DataQueryRequest<CloudMonitoringQuery>;
|
};
|
||||||
|
|
||||||
const queries = options.targets;
|
const queries = options.targets;
|
||||||
|
|
||||||
@ -107,7 +119,7 @@ export default class CloudMonitoringDatasource extends DataSourceWithBackend<
|
|||||||
return lastValueFrom(
|
return lastValueFrom(
|
||||||
from(this.ensureGCEDefaultProject()).pipe(
|
from(this.ensureGCEDefaultProject()).pipe(
|
||||||
mergeMap(() => {
|
mergeMap(() => {
|
||||||
return getBackendSrv().fetch<PostResponse>({
|
return this.backendSrv.fetch<PostResponse>({
|
||||||
url: '/api/ds/query',
|
url: '/api/ds/query',
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: this.getRequestHeaders(),
|
headers: this.getRequestHeaders(),
|
||||||
@ -193,20 +205,73 @@ export default class CloudMonitoringDatasource extends DataSourceWithBackend<
|
|||||||
return this.getResource(`projects`);
|
return this.getResource(`projects`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
migrateMetricTypeFilter(metricType: string, filters?: string[]) {
|
||||||
|
const metricTypeFilterArray = ['metric.type', '=', metricType];
|
||||||
|
if (filters?.length) {
|
||||||
|
return filters.concat('AND', metricTypeFilterArray);
|
||||||
|
}
|
||||||
|
return metricTypeFilterArray;
|
||||||
|
}
|
||||||
|
|
||||||
|
// This is a manual port of the migration code in cloudmonitoring.go
|
||||||
|
// DO NOT UPDATE THIS CODE WITHOUT UPDATING THE BACKEND CODE
|
||||||
migrateQuery(query: CloudMonitoringQuery): CloudMonitoringQuery {
|
migrateQuery(query: CloudMonitoringQuery): CloudMonitoringQuery {
|
||||||
if (!query.hasOwnProperty('metricQuery')) {
|
if (
|
||||||
|
!query.hasOwnProperty('metricQuery') &&
|
||||||
|
!query.hasOwnProperty('sloQuery') &&
|
||||||
|
!query.hasOwnProperty('timeSeriesQuery') &&
|
||||||
|
!query.hasOwnProperty('timeSeriesList')
|
||||||
|
) {
|
||||||
const { hide, refId, datasource, key, queryType, maxLines, metric, intervalMs, type, ...rest } = query as any;
|
const { hide, refId, datasource, key, queryType, maxLines, metric, intervalMs, type, ...rest } = query as any;
|
||||||
return {
|
return {
|
||||||
refId,
|
refId,
|
||||||
intervalMs,
|
intervalMs,
|
||||||
hide,
|
hide,
|
||||||
queryType: type === 'annotationQuery' ? QueryType.ANNOTATION : QueryType.METRICS,
|
queryType: type === 'annotationQuery' ? QueryType.ANNOTATION : QueryType.TIME_SERIES_LIST,
|
||||||
metricQuery: {
|
timeSeriesList: {
|
||||||
...rest,
|
...rest,
|
||||||
view: rest.view || 'FULL',
|
view: rest.view || 'FULL',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (has(query, 'metricQuery') && ['metrics', QueryType.ANNOTATION].includes(query.queryType)) {
|
||||||
|
const metricQuery: MetricQuery = get(query, 'metricQuery')!;
|
||||||
|
if (metricQuery.editorMode === 'mql') {
|
||||||
|
query.timeSeriesQuery = {
|
||||||
|
projectName: metricQuery.projectName,
|
||||||
|
query: metricQuery.query,
|
||||||
|
graphPeriod: metricQuery.graphPeriod,
|
||||||
|
};
|
||||||
|
query.queryType = QueryType.TIME_SERIES_QUERY;
|
||||||
|
} else {
|
||||||
|
query.timeSeriesList = {
|
||||||
|
projectName: metricQuery.projectName,
|
||||||
|
crossSeriesReducer: metricQuery.crossSeriesReducer,
|
||||||
|
alignmentPeriod: metricQuery.alignmentPeriod,
|
||||||
|
perSeriesAligner: metricQuery.perSeriesAligner,
|
||||||
|
groupBys: metricQuery.groupBys,
|
||||||
|
filters: metricQuery.filters,
|
||||||
|
view: metricQuery.view,
|
||||||
|
preprocessor: metricQuery.preprocessor,
|
||||||
|
};
|
||||||
|
query.queryType = QueryType.TIME_SERIES_LIST;
|
||||||
|
if (metricQuery.metricType) {
|
||||||
|
query.timeSeriesList.filters = this.migrateMetricTypeFilter(
|
||||||
|
metricQuery.metricType,
|
||||||
|
query.timeSeriesList.filters
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
query.aliasBy = metricQuery.aliasBy;
|
||||||
|
query = omit(query, 'metricQuery');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (query.queryType === QueryType.SLO && has(query, 'sloQuery.aliasBy')) {
|
||||||
|
query.aliasBy = get(query, 'sloQuery.aliasBy');
|
||||||
|
query = omit(query, 'sloQuery.aliasBy');
|
||||||
|
}
|
||||||
|
|
||||||
return query;
|
return query;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -224,7 +289,10 @@ export default class CloudMonitoringDatasource extends DataSourceWithBackend<
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (query.queryType && query.queryType === QueryType.SLO && query.sloQuery) {
|
if (query.queryType === QueryType.SLO) {
|
||||||
|
if (!query.sloQuery) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
const { selectorName, serviceId, sloId, projectName, lookbackPeriod } = query.sloQuery;
|
const { selectorName, serviceId, sloId, projectName, lookbackPeriod } = query.sloQuery;
|
||||||
return (
|
return (
|
||||||
!!selectorName &&
|
!!selectorName &&
|
||||||
@ -235,13 +303,15 @@ export default class CloudMonitoringDatasource extends DataSourceWithBackend<
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (query.queryType && query.queryType === QueryType.METRICS && query.metricQuery.editorMode === EditorMode.MQL) {
|
if (query.queryType === QueryType.TIME_SERIES_QUERY) {
|
||||||
return !!query.metricQuery.projectName && !!query.metricQuery.query;
|
return !!query.timeSeriesQuery && !!query.timeSeriesQuery.projectName && !!query.timeSeriesQuery.query;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { metricType } = query.metricQuery;
|
if ([QueryType.TIME_SERIES_LIST, QueryType.ANNOTATION].includes(query.queryType)) {
|
||||||
|
return !!query.timeSeriesList && !!query.timeSeriesList.projectName && !!getMetricType(query.timeSeriesList);
|
||||||
|
}
|
||||||
|
|
||||||
return !!metricType;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
interpolateVariablesInQueries(queries: CloudMonitoringQuery[], scopedVars: ScopedVars): CloudMonitoringQuery[] {
|
interpolateVariablesInQueries(queries: CloudMonitoringQuery[], scopedVars: ScopedVars): CloudMonitoringQuery[] {
|
||||||
|
@ -10,9 +10,11 @@ import {
|
|||||||
labelsToGroupedOptions,
|
labelsToGroupedOptions,
|
||||||
stringArrayToFilters,
|
stringArrayToFilters,
|
||||||
alignmentPeriodLabel,
|
alignmentPeriodLabel,
|
||||||
|
getMetricType,
|
||||||
|
setMetricType,
|
||||||
} from './functions';
|
} from './functions';
|
||||||
import { newMockDatasource } from './specs/testData';
|
import { newMockDatasource } from './specs/testData';
|
||||||
import { AlignmentTypes, MetricDescriptor, MetricKind, ValueTypes } from './types';
|
import { AlignmentTypes, MetricDescriptor, MetricKind, TimeSeriesList, ValueTypes } from './types';
|
||||||
|
|
||||||
jest.mock('@grafana/runtime', () => ({
|
jest.mock('@grafana/runtime', () => ({
|
||||||
...(jest.requireActual('@grafana/runtime') as unknown as object),
|
...(jest.requireActual('@grafana/runtime') as unknown as object),
|
||||||
@ -236,4 +238,23 @@ describe('functions', () => {
|
|||||||
expect(label).toBe('10s interval (delta)');
|
expect(label).toBe('10s interval (delta)');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('getMetricType', () => {
|
||||||
|
it('returns metric type', () => {
|
||||||
|
const metricType = getMetricType({ filters: ['metric.type', '=', 'test'] } as TimeSeriesList);
|
||||||
|
expect(metricType).toBe('test');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('setMetricType', () => {
|
||||||
|
it('sets a metric type if the filter did not exist', () => {
|
||||||
|
const metricType = setMetricType({} as TimeSeriesList, 'test');
|
||||||
|
expect(metricType.filters).toEqual(['metric.type', '=', 'test']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets a metric type if the filter exists', () => {
|
||||||
|
const metricType = setMetricType({ filters: ['metric.type', '=', 'test'] } as TimeSeriesList, 'other');
|
||||||
|
expect(metricType.filters).toEqual(['metric.type', '=', 'other']);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -5,7 +5,15 @@ import { getTemplateSrv, TemplateSrv } from '@grafana/runtime';
|
|||||||
|
|
||||||
import { AGGREGATIONS, ALIGNMENTS, SYSTEM_LABELS } from './constants';
|
import { AGGREGATIONS, ALIGNMENTS, SYSTEM_LABELS } from './constants';
|
||||||
import CloudMonitoringDatasource from './datasource';
|
import CloudMonitoringDatasource from './datasource';
|
||||||
import { AlignmentTypes, CustomMetaData, MetricDescriptor, MetricKind, PreprocessorType, ValueTypes } from './types';
|
import {
|
||||||
|
AlignmentTypes,
|
||||||
|
CustomMetaData,
|
||||||
|
MetricDescriptor,
|
||||||
|
MetricKind,
|
||||||
|
PreprocessorType,
|
||||||
|
TimeSeriesList,
|
||||||
|
ValueTypes,
|
||||||
|
} from './types';
|
||||||
|
|
||||||
export const extractServicesFromMetricDescriptors = (metricDescriptors: MetricDescriptor[]) =>
|
export const extractServicesFromMetricDescriptors = (metricDescriptors: MetricDescriptor[]) =>
|
||||||
uniqBy(metricDescriptors, 'service');
|
uniqBy(metricDescriptors, 'service');
|
||||||
@ -35,8 +43,8 @@ export const getMetricTypes = (
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const getAlignmentOptionsByMetric = (
|
export const getAlignmentOptionsByMetric = (
|
||||||
metricValueType: string,
|
metricValueType?: string,
|
||||||
metricKind: string,
|
metricKind?: string,
|
||||||
preprocessor?: PreprocessorType
|
preprocessor?: PreprocessorType
|
||||||
) => {
|
) => {
|
||||||
if (preprocessor && preprocessor === PreprocessorType.Rate) {
|
if (preprocessor && preprocessor === PreprocessorType.Rate) {
|
||||||
@ -78,7 +86,7 @@ export const getAlignmentPickerData = (
|
|||||||
preprocessor?: PreprocessorType
|
preprocessor?: PreprocessorType
|
||||||
) => {
|
) => {
|
||||||
const templateSrv: TemplateSrv = getTemplateSrv();
|
const templateSrv: TemplateSrv = getTemplateSrv();
|
||||||
const alignOptions = getAlignmentOptionsByMetric(valueType!, metricKind!, preprocessor!).map((option) => ({
|
const alignOptions = getAlignmentOptionsByMetric(valueType, metricKind, preprocessor).map((option) => ({
|
||||||
...option,
|
...option,
|
||||||
label: option.text,
|
label: option.text,
|
||||||
}));
|
}));
|
||||||
@ -125,3 +133,25 @@ export const alignmentPeriodLabel = (customMetaData: CustomMetaData, datasource:
|
|||||||
const hms = rangeUtil.secondsToHms(seconds);
|
const hms = rangeUtil.secondsToHms(seconds);
|
||||||
return `${hms} interval (${alignment?.text ?? ''})`;
|
return `${hms} interval (${alignment?.text ?? ''})`;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getMetricType = (query?: TimeSeriesList) => {
|
||||||
|
const metricTypeKey = query?.filters?.findIndex((f) => f === 'metric.type')!;
|
||||||
|
// filters are in the format [key, operator, value] so we need to add 2 to get the value
|
||||||
|
const metricType = query?.filters?.[metricTypeKey + 2];
|
||||||
|
return metricType || '';
|
||||||
|
};
|
||||||
|
|
||||||
|
export const setMetricType = (query: TimeSeriesList, metricType: string) => {
|
||||||
|
if (!query.filters) {
|
||||||
|
query.filters = ['metric.type', '=', metricType];
|
||||||
|
return query;
|
||||||
|
}
|
||||||
|
const metricTypeKey = query?.filters?.findIndex((f) => f === 'metric.type')!;
|
||||||
|
if (metricTypeKey === -1) {
|
||||||
|
query.filters.push('metric.type', '=', metricType);
|
||||||
|
} else {
|
||||||
|
// filters are in the format [key, operator, value] so we need to add 2 to get the value
|
||||||
|
query.filters![metricTypeKey + 2] = metricType;
|
||||||
|
}
|
||||||
|
return query;
|
||||||
|
};
|
||||||
|
@ -94,10 +94,10 @@ describe('CloudMonitoringDataSource', () => {
|
|||||||
const { ds } = getTestcontext();
|
const { ds } = getTestcontext();
|
||||||
await ds.getLabels('cpu', 'a', 'default-proj');
|
await ds.getLabels('cpu', 'a', 'default-proj');
|
||||||
|
|
||||||
await expect(fetchMock.mock.calls[0][0].data.queries[0].metricQuery).toMatchObject({
|
await expect(fetchMock.mock.calls[0][0].data.queries[0].timeSeriesList).toMatchObject({
|
||||||
crossSeriesReducer: 'REDUCE_NONE',
|
crossSeriesReducer: 'REDUCE_NONE',
|
||||||
groupBys: [],
|
groupBys: [],
|
||||||
metricType: 'cpu',
|
filters: ['metric.type', '=', 'cpu'],
|
||||||
projectName: 'default-proj',
|
projectName: 'default-proj',
|
||||||
view: 'HEADERS',
|
view: 'HEADERS',
|
||||||
});
|
});
|
||||||
@ -112,10 +112,10 @@ describe('CloudMonitoringDataSource', () => {
|
|||||||
groupBys: ['metadata.system_label.name'],
|
groupBys: ['metadata.system_label.name'],
|
||||||
});
|
});
|
||||||
|
|
||||||
await expect(fetchMock.mock.calls[0][0].data.queries[0].metricQuery).toMatchObject({
|
await expect(fetchMock.mock.calls[0][0].data.queries[0].timeSeriesList).toMatchObject({
|
||||||
crossSeriesReducer: 'REDUCE_MEAN',
|
crossSeriesReducer: 'REDUCE_MEAN',
|
||||||
groupBys: ['metadata.system_label.name'],
|
groupBys: ['metadata.system_label.name'],
|
||||||
metricType: 'sql',
|
filters: ['metric.type', '=', 'sql'],
|
||||||
projectName: 'default-proj',
|
projectName: 'default-proj',
|
||||||
view: 'HEADERS',
|
view: 'HEADERS',
|
||||||
});
|
});
|
||||||
|
@ -55,16 +55,12 @@ export interface Aggregation {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export enum QueryType {
|
export enum QueryType {
|
||||||
METRICS = 'metrics',
|
TIME_SERIES_LIST = 'timeSeriesList',
|
||||||
|
TIME_SERIES_QUERY = 'timeSeriesQuery',
|
||||||
SLO = 'slo',
|
SLO = 'slo',
|
||||||
ANNOTATION = 'annotation',
|
ANNOTATION = 'annotation',
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum EditorMode {
|
|
||||||
Visual = 'visual',
|
|
||||||
MQL = 'mql',
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum PreprocessorType {
|
export enum PreprocessorType {
|
||||||
None = 'none',
|
None = 'none',
|
||||||
Rate = 'rate',
|
Rate = 'rate',
|
||||||
@ -110,15 +106,14 @@ export enum AlignmentTypes {
|
|||||||
ALIGN_NONE = 'ALIGN_NONE',
|
ALIGN_NONE = 'ALIGN_NONE',
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface BaseQuery {
|
// deprecated: use TimeSeriesList instead
|
||||||
|
// left here for migration purposes
|
||||||
|
export interface MetricQuery {
|
||||||
projectName: string;
|
projectName: string;
|
||||||
perSeriesAligner?: string;
|
perSeriesAligner?: string;
|
||||||
alignmentPeriod?: string;
|
alignmentPeriod?: string;
|
||||||
aliasBy?: string;
|
aliasBy?: string;
|
||||||
}
|
editorMode: string;
|
||||||
|
|
||||||
export interface MetricQuery extends BaseQuery {
|
|
||||||
editorMode: EditorMode;
|
|
||||||
metricType: string;
|
metricType: string;
|
||||||
crossSeriesReducer: string;
|
crossSeriesReducer: string;
|
||||||
groupBys?: string[];
|
groupBys?: string[];
|
||||||
@ -132,12 +127,39 @@ export interface MetricQuery extends BaseQuery {
|
|||||||
graphPeriod?: 'disabled' | string;
|
graphPeriod?: 'disabled' | string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AnnotationMetricQuery extends MetricQuery {
|
export interface TimeSeriesList {
|
||||||
|
projectName: string;
|
||||||
|
crossSeriesReducer: string;
|
||||||
|
alignmentPeriod?: string;
|
||||||
|
perSeriesAligner?: string;
|
||||||
|
groupBys?: string[];
|
||||||
|
filters?: string[];
|
||||||
|
view?: string;
|
||||||
|
secondaryCrossSeriesReducer?: string;
|
||||||
|
secondaryAlignmentPeriod?: string;
|
||||||
|
secondaryPerSeriesAligner?: string;
|
||||||
|
secondaryGroupBys?: string[];
|
||||||
|
// preprocessor is not part of the API, but is used to store the preprocessor
|
||||||
|
// and not affect the UI for the rest of parameters
|
||||||
|
preprocessor?: PreprocessorType;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TimeSeriesQuery {
|
||||||
|
projectName: string;
|
||||||
|
query: string;
|
||||||
|
// To disable the graphPeriod, it should explictly be set to 'disabled'
|
||||||
|
graphPeriod?: 'disabled' | string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AnnotationQuery extends TimeSeriesList {
|
||||||
title?: string;
|
title?: string;
|
||||||
text?: string;
|
text?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SLOQuery extends BaseQuery {
|
export interface SLOQuery {
|
||||||
|
projectName: string;
|
||||||
|
perSeriesAligner?: string;
|
||||||
|
alignmentPeriod?: string;
|
||||||
selectorName: string;
|
selectorName: string;
|
||||||
serviceId: string;
|
serviceId: string;
|
||||||
serviceName: string;
|
serviceName: string;
|
||||||
@ -148,9 +170,11 @@ export interface SLOQuery extends BaseQuery {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface CloudMonitoringQuery extends DataQuery {
|
export interface CloudMonitoringQuery extends DataQuery {
|
||||||
|
aliasBy?: string;
|
||||||
datasourceId?: number; // Should not be necessary anymore
|
datasourceId?: number; // Should not be necessary anymore
|
||||||
queryType: QueryType;
|
queryType: QueryType;
|
||||||
metricQuery: MetricQuery | AnnotationMetricQuery;
|
timeSeriesList?: TimeSeriesList | AnnotationQuery;
|
||||||
|
timeSeriesQuery?: TimeSeriesQuery;
|
||||||
sloQuery?: SLOQuery;
|
sloQuery?: SLOQuery;
|
||||||
intervalMs: number;
|
intervalMs: number;
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user