diff --git a/.betterer.results b/.betterer.results index bc05669f6c5..8be8218b1b7 100644 --- a/.betterer.results +++ b/.betterer.results @@ -5212,17 +5212,9 @@ exports[`better eslint`] = { "public/app/plugins/datasource/cloud-monitoring/components/MQLQueryEditor.tsx:5381": [ [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": [ [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": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"], [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": [ [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.", "2"], + [0, 0, 0, "Unexpected any. Specify a different type.", "1"], + [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.", "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.", "6"], - [0, 0, 0, "Do not use any type assertions.", "7"], - [0, 0, 0, "Do not use any type assertions.", "8"] + [0, 0, 0, "Do not use any type assertions.", "6"], + [0, 0, 0, "Do not use any type assertions.", "7"] ], "public/app/plugins/datasource/cloud-monitoring/functions.test.ts:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"], diff --git a/pkg/tsdb/cloudmonitoring/cloudmonitoring.go b/pkg/tsdb/cloudmonitoring/cloudmonitoring.go index c48edd5d52e..386c661aeff 100644 --- a/pkg/tsdb/cloudmonitoring/cloudmonitoring.go +++ b/pkg/tsdb/cloudmonitoring/cloudmonitoring.go @@ -56,7 +56,8 @@ const ( gceAuthentication = "gce" jwtAuthentication = "jwt" annotationQueryType = "annotation" - metricQueryType = "metrics" + timeSeriesListQueryType = "timeSeriesList" + timeSeriesQueryQueryType = "timeSeriesQuery" sloQueryType = "slo" crossSeriesReducerDefault = "REDUCE_NONE" perSeriesAlignerDefault = "ALIGN_MEAN" @@ -216,32 +217,6 @@ func migrateMetricTypeFilter(metricTypeFilter string, prevFilters interface{}) [ 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 { for i, q := range req.Queries { var rawQuery map[string]interface{} @@ -250,14 +225,17 @@ func migrateRequest(req *backend.QueryDataRequest) error { return err } - if rawQuery["metricQuery"] == nil { + if rawQuery["metricQuery"] == nil && + rawQuery["timeSeriesQuery"] == nil && + rawQuery["timeSeriesList"] == nil && + rawQuery["sloQuery"] == nil { // migrate legacy query var mq timeSeriesList err = json.Unmarshal(q.JSON, &mq) if err != nil { return err } - q.QueryType = metricQueryType + q.QueryType = timeSeriesListQueryType gq := grafanaQuery{ TimeSeriesList: &mq, } @@ -268,9 +246,6 @@ func migrateRequest(req *backend.QueryDataRequest) error { // metricType should be a filter gq.TimeSeriesList.Filters = migrateMetricTypeFilter(rawQuery["metricType"].(string), rawQuery["filters"]) } - if rawQuery["preprocessor"] != nil { - migratePreprocessor(gq.TimeSeriesList, rawQuery["preprocessor"].(string)) - } b, err := json.Marshal(gq) if err != nil { @@ -288,7 +263,7 @@ func migrateRequest(req *backend.QueryDataRequest) error { } // 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{}) if metricQuery["editorMode"] != nil && toString(metricQuery["editorMode"]) == "mql" { @@ -297,6 +272,7 @@ func migrateRequest(req *backend.QueryDataRequest) error { Query: toString(metricQuery["query"]), GraphPeriod: toString(metricQuery["graphPeriod"]), } + q.QueryType = timeSeriesQueryQueryType } else { tslb, err := json.Marshal(metricQuery) if err != nil { @@ -311,11 +287,10 @@ func migrateRequest(req *backend.QueryDataRequest) error { // metricType should be a filter tsl.Filters = migrateMetricTypeFilter(metricQuery["metricType"].(string), metricQuery["filters"]) } - if rawQuery["preprocessor"] != nil { - migratePreprocessor(tsl, rawQuery["preprocessor"].(string)) - } rawQuery["timeSeriesList"] = tsl + q.QueryType = timeSeriesListQueryType } + // AliasBy is now a top level property if metricQuery["aliasBy"] != nil { rawQuery["aliasBy"] = metricQuery["aliasBy"] } @@ -323,12 +298,22 @@ func migrateRequest(req *backend.QueryDataRequest) error { if err != nil { return err } - if q.QueryType == "" { - q.QueryType = metricQueryType - } 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 } @@ -408,29 +393,25 @@ func (s *Service) buildQueryExecutors(logger log.Logger, req *backend.QueryDataR var queryInterface cloudMonitoringQueryExecutor switch query.QueryType { - case metricQueryType, annotationQueryType: - if q.TimeSeriesQuery != nil { - queryInterface = &cloudMonitoringTimeSeriesQuery{ - refID: query.RefID, - aliasBy: q.AliasBy, - parameters: q.TimeSeriesQuery, - IntervalMS: query.Interval.Milliseconds(), - timeRange: req.Queries[0].TimeRange, - } - } else if q.TimeSeriesList != nil { - cmtsf := &cloudMonitoringTimeSeriesList{ - refID: query.RefID, - logger: logger, - aliasBy: q.AliasBy, - } - if q.TimeSeriesList.View == "" { - q.TimeSeriesList.View = "FULL" - } - cmtsf.parameters = q.TimeSeriesList - cmtsf.setParams(startTime, endTime, durationSeconds, query.Interval.Milliseconds()) - queryInterface = cmtsf - } else { - return nil, fmt.Errorf("missing query info") + case timeSeriesListQueryType, annotationQueryType: + cmtsf := &cloudMonitoringTimeSeriesList{ + refID: query.RefID, + logger: logger, + aliasBy: q.AliasBy, + } + if q.TimeSeriesList.View == "" { + q.TimeSeriesList.View = "FULL" + } + cmtsf.parameters = q.TimeSeriesList + cmtsf.setParams(startTime, endTime, durationSeconds, query.Interval.Milliseconds()) + queryInterface = cmtsf + case timeSeriesQueryQueryType: + queryInterface = &cloudMonitoringTimeSeriesQuery{ + refID: query.RefID, + aliasBy: q.AliasBy, + parameters: q.TimeSeriesQuery, + IntervalMS: query.Interval.Milliseconds(), + timeRange: req.Queries[0].TimeRange, } case sloQueryType: cmslo := &cloudMonitoringSLO{ diff --git a/pkg/tsdb/cloudmonitoring/cloudmonitoring_test.go b/pkg/tsdb/cloudmonitoring/cloudmonitoring_test.go index 272a64b007c..5209b122edc 100644 --- a/pkg/tsdb/cloudmonitoring/cloudmonitoring_test.go +++ b/pkg/tsdb/cloudmonitoring/cloudmonitoring_test.go @@ -664,7 +664,7 @@ func TestCloudMonitoring(t *testing.T) { "projectName": "test-proj", "alignmentPeriod": "stackdriver-auto", "perSeriesAligner": "ALIGN_NEXT_OLDER", - "aliasBy": "", + "aliasBy": "test-alias", "selectorName": "select_slo_health", "serviceId": "test-service", "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:34:00Z", queries[0].params["interval.endTime"][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, `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)) @@ -1103,7 +1103,7 @@ func baseTimeSeriesList() *backend.QueryDataRequest { From: fromStart, To: fromStart.Add(34 * time.Minute), }, - QueryType: "metrics", + QueryType: timeSeriesListQueryType, JSON: json.RawMessage(`{ "timeSeriesList": { "filters": ["metric.type=\"a/metric/type\""], @@ -1127,7 +1127,7 @@ func baseTimeSeriesQuery() *backend.QueryDataRequest { From: fromStart, To: fromStart.Add(34 * time.Minute), }, - QueryType: "metrics", + QueryType: timeSeriesQueryQueryType, JSON: json.RawMessage(`{ "queryType": "metrics", "timeSeriesQuery": { diff --git a/pkg/tsdb/cloudmonitoring/time_series_filter.go b/pkg/tsdb/cloudmonitoring/time_series_filter.go index d394156ec11..f0fe4b32696 100644 --- a/pkg/tsdb/cloudmonitoring/time_series_filter.go +++ b/pkg/tsdb/cloudmonitoring/time_series_filter.go @@ -147,6 +147,32 @@ func (timeSeriesFilter *cloudMonitoringTimeSeriesList) getFilter() string { 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) { params := url.Values{} query := timeSeriesFilter.parameters @@ -165,6 +191,8 @@ func (timeSeriesFilter *cloudMonitoringTimeSeriesList) setParams(startTime time. query.PerSeriesAligner = perSeriesAlignerDefault } + timeSeriesFilter.setPreprocessor() + alignmentPeriod := calculateAlignmentPeriod(query.AlignmentPeriod, intervalMs, durationSeconds) params.Add("aggregation.alignmentPeriod", alignmentPeriod) if query.CrossSeriesReducer != "" { diff --git a/pkg/tsdb/cloudmonitoring/time_series_filter_test.go b/pkg/tsdb/cloudmonitoring/time_series_filter_test.go index 74fa9f476e6..003edb0ffad 100644 --- a/pkg/tsdb/cloudmonitoring/time_series_filter_test.go +++ b/pkg/tsdb/cloudmonitoring/time_series_filter_test.go @@ -18,6 +18,42 @@ import ( ) 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) { data, err := loadTestFile("./test-data/1-series-response-agg-one-metric.json") require.NoError(t, err) diff --git a/pkg/tsdb/cloudmonitoring/types.go b/pkg/tsdb/cloudmonitoring/types.go index d42613788a9..118b2244682 100644 --- a/pkg/tsdb/cloudmonitoring/types.go +++ b/pkg/tsdb/cloudmonitoring/types.go @@ -50,6 +50,10 @@ type ( SecondaryCrossSeriesReducer string `json:"secondaryCrossSeriesReducer"` SecondaryPerSeriesAligner string `json:"secondaryPerSeriesAligner"` 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 diff --git a/public/app/plugins/datasource/cloud-monitoring/__mocks__/cloudMonitoringDatasource.ts b/public/app/plugins/datasource/cloud-monitoring/__mocks__/cloudMonitoringDatasource.ts index 390310cbac6..5ce6325ac66 100644 --- a/public/app/plugins/datasource/cloud-monitoring/__mocks__/cloudMonitoringDatasource.ts +++ b/public/app/plugins/datasource/cloud-monitoring/__mocks__/cloudMonitoringDatasource.ts @@ -14,6 +14,7 @@ export const createMockDatasource = (overrides?: Partial) => { getDefaultProject: jest.fn().mockReturnValue('cloud-monitoring-default-project'), templateSrv, getSLOServices: jest.fn().mockResolvedValue([]), + migrateQuery: jest.fn().mockImplementation((query) => query), ...overrides, }; diff --git a/public/app/plugins/datasource/cloud-monitoring/__mocks__/cloudMonitoringQuery.ts b/public/app/plugins/datasource/cloud-monitoring/__mocks__/cloudMonitoringQuery.ts index 191b64495aa..365b0febc1c 100644 --- a/public/app/plugins/datasource/cloud-monitoring/__mocks__/cloudMonitoringQuery.ts +++ b/public/app/plugins/datasource/cloud-monitoring/__mocks__/cloudMonitoringQuery.ts @@ -1,25 +1,9 @@ -import { AlignmentTypes, CloudMonitoringQuery, EditorMode, MetricQuery, QueryType, SLOQuery } from '../types'; +import { AlignmentTypes, CloudMonitoringQuery, QueryType, SLOQuery, TimeSeriesList, TimeSeriesQuery } from '../types'; type Subset = { [attr in keyof K]?: K[attr] extends object ? Subset : K[attr]; }; -export const createMockMetricQuery: (overrides?: Partial) => MetricQuery = ( - overrides?: Partial -) => { - 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 = (overrides) => { return { projectName: 'projectName', @@ -36,6 +20,29 @@ export const createMockSLOQuery: (overrides?: Partial) => SLOQuery = ( }; }; +export const createMockTimeSeriesList: (overrides?: Partial) => TimeSeriesList = ( + overrides?: Partial +) => { + return { + crossSeriesReducer: 'REDUCE_NONE', + projectName: 'cloud-monitoring-default-project', + filters: [], + groupBys: [], + view: 'FULL', + ...overrides, + }; +}; + +export const createMockTimeSeriesQuery: (overrides?: Partial) => TimeSeriesQuery = ( + overrides?: Partial +) => { + return { + query: '', + projectName: 'cloud-monitoring-default-project', + ...overrides, + }; +}; + export const createMockQuery: (overrides?: Subset) => CloudMonitoringQuery = (overrides) => { return { datasource: { @@ -43,12 +50,12 @@ export const createMockQuery: (overrides?: Subset) => Clou uid: 'abc', }, refId: 'cloudMonitoringRefId', - queryType: QueryType.METRICS, + queryType: QueryType.TIME_SERIES_LIST, intervalMs: 0, - type: 'timeSeriesQuery', hide: false, ...overrides, - metricQuery: createMockMetricQuery(overrides?.metricQuery), sloQuery: createMockSLOQuery(overrides?.sloQuery), + timeSeriesList: createMockTimeSeriesList(overrides?.timeSeriesList), + timeSeriesQuery: createMockTimeSeriesQuery(overrides?.timeSeriesQuery), }; }; diff --git a/public/app/plugins/datasource/cloud-monitoring/annotationSupport.test.ts b/public/app/plugins/datasource/cloud-monitoring/annotationSupport.test.ts index f4489bb5bbd..790f1c4ab3d 100644 --- a/public/app/plugins/datasource/cloud-monitoring/annotationSupport.test.ts +++ b/public/app/plugins/datasource/cloud-monitoring/annotationSupport.test.ts @@ -5,7 +5,6 @@ import { CloudMonitoringAnnotationSupport } from './annotationSupport'; import { AlignmentTypes, CloudMonitoringQuery, - EditorMode, LegacyCloudMonitoringAnnotationQuery, MetricKind, QueryType, @@ -15,16 +14,11 @@ const query: CloudMonitoringQuery = { refId: 'query', queryType: QueryType.ANNOTATION, intervalMs: 0, - metricQuery: { - editorMode: EditorMode.Visual, + timeSeriesList: { projectName: 'project-name', - metricType: '', filters: [], - metricKind: MetricKind.GAUGE, - valueType: '', title: '', text: '', - query: '', crossSeriesReducer: 'REDUCE_NONE', perSeriesAligner: AlignmentTypes.ALIGN_NONE, }, @@ -71,15 +65,11 @@ describe('CloudMonitoringAnnotationSupport', () => { name: 'Anno', target: { intervalMs: 0, - metricQuery: { + timeSeriesList: { crossSeriesReducer: 'REDUCE_NONE', - editorMode: 'visual', filters: ['filter1', 'filter2'], - metricKind: 'CUMULATIVE', - metricType: 'metric-type', perSeriesAligner: 'ALIGN_NONE', projectName: 'project-name', - query: '', text: 'text', title: 'title', }, @@ -92,10 +82,10 @@ describe('CloudMonitoringAnnotationSupport', () => { }); describe('prepareQuery', () => { - it('should ensure queryType is set to "metrics"', () => { + it('should ensure queryType is set to "annotation"', () => { const queryWithoutMetricsQueryType = { ...annotationQuery, queryType: 'blah' }; expect(annotationSupport.prepareQuery?.(queryWithoutMetricsQueryType)).toEqual( - expect.objectContaining({ queryType: 'metrics' }) + expect.objectContaining({ queryType: QueryType.ANNOTATION }) ); }); it('should ensure type is set "annotationQuery"', () => { diff --git a/public/app/plugins/datasource/cloud-monitoring/annotationSupport.ts b/public/app/plugins/datasource/cloud-monitoring/annotationSupport.ts index ba237c16193..2969d9a2912 100644 --- a/public/app/plugins/datasource/cloud-monitoring/annotationSupport.ts +++ b/public/app/plugins/datasource/cloud-monitoring/annotationSupport.ts @@ -2,14 +2,7 @@ import { AnnotationSupport, AnnotationQuery } from '@grafana/data'; import { AnnotationQueryEditor } from './components/AnnotationQueryEditor'; import CloudMonitoringDatasource from './datasource'; -import { - AlignmentTypes, - CloudMonitoringQuery, - EditorMode, - LegacyCloudMonitoringAnnotationQuery, - MetricKind, - QueryType, -} from './types'; +import { AlignmentTypes, CloudMonitoringQuery, LegacyCloudMonitoringAnnotationQuery, QueryType } from './types'; // 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, @@ -42,13 +35,9 @@ export const CloudMonitoringAnnotationSupport: ( intervalMs: ds.intervalMs, refId: target?.refId || 'annotationQuery', queryType: QueryType.ANNOTATION, - metricQuery: { + timeSeriesList: { projectName: target?.projectName || ds.getDefaultProject(), - editorMode: EditorMode.Visual, - metricType: target?.metricType || '', filters: target?.filters || [], - metricKind: target?.metricKind || MetricKind.GAUGE, - query: '', crossSeriesReducer: 'REDUCE_NONE', perSeriesAligner: AlignmentTypes.ALIGN_NONE, title: target?.title || '', @@ -65,11 +54,8 @@ export const CloudMonitoringAnnotationSupport: ( return { ...anno.target, - queryType: QueryType.METRICS, + queryType: QueryType.ANNOTATION, type: 'annotationQuery', - metricQuery: { - ...anno.target.metricQuery, - }, }; }, QueryEditor: AnnotationQueryEditor, diff --git a/public/app/plugins/datasource/cloud-monitoring/components/Alignment.test.tsx b/public/app/plugins/datasource/cloud-monitoring/components/Alignment.test.tsx index ea7ddbe435d..01e9dcd24b1 100644 --- a/public/app/plugins/datasource/cloud-monitoring/components/Alignment.test.tsx +++ b/public/app/plugins/datasource/cloud-monitoring/components/Alignment.test.tsx @@ -6,7 +6,8 @@ import { openMenu } from 'react-select-event'; import { TemplateSrvMock } from 'app/features/templating/template_srv.mock'; 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 { Alignment } from './Alignment'; @@ -19,7 +20,7 @@ jest.mock('@grafana/runtime', () => ({ describe('Alignment', () => { it('renders alignment fields', () => { const datasource = createMockDatasource(); - const query = createMockMetricQuery(); + const query = createMockTimeSeriesList(); const onChange = jest.fn(); render( @@ -39,7 +40,7 @@ describe('Alignment', () => { it('can set the alignment function', async () => { const datasource = createMockDatasource(); - const query = createMockMetricQuery({ metricKind: MetricKind.GAUGE, valueType: ValueTypes.INT64 }); + const query = createMockTimeSeriesList(); const onChange = jest.fn(); render( @@ -50,6 +51,7 @@ describe('Alignment', () => { query={query} onChange={onChange} templateVariableOptions={[]} + metricDescriptor={createMockMetricDescriptor({ metricKind: MetricKind.GAUGE, valueType: ValueTypes.INT64 })} /> ); @@ -61,7 +63,7 @@ describe('Alignment', () => { it('can set the alignment period', async () => { const datasource = createMockDatasource(); - const query = createMockMetricQuery(); + const query = createMockTimeSeriesList(); const onChange = jest.fn(); render( diff --git a/public/app/plugins/datasource/cloud-monitoring/components/Alignment.tsx b/public/app/plugins/datasource/cloud-monitoring/components/Alignment.tsx index 37694a33773..4c19378d2dc 100644 --- a/public/app/plugins/datasource/cloud-monitoring/components/Alignment.tsx +++ b/public/app/plugins/datasource/cloud-monitoring/components/Alignment.tsx @@ -6,18 +6,20 @@ import { EditorField, EditorFieldGroup } from '@grafana/experimental'; import { ALIGNMENT_PERIODS } from '../constants'; import CloudMonitoringDatasource from '../datasource'; import { alignmentPeriodLabel } from '../functions'; -import { CustomMetaData, MetricQuery, SLOQuery } from '../types'; +import { CustomMetaData, MetricDescriptor, PreprocessorType, TimeSeriesList } from '../types'; import { AlignmentFunction } from './AlignmentFunction'; import { PeriodSelect } from './PeriodSelect'; export interface Props { refId: string; - onChange: (query: MetricQuery | SLOQuery) => void; - query: MetricQuery; + onChange: (query: TimeSeriesList) => void; + query: TimeSeriesList; templateVariableOptions: Array>; customMetaData: CustomMetaData; datasource: CloudMonitoringDatasource; + metricDescriptor?: MetricDescriptor; + preprocessor?: PreprocessorType; } export const Alignment: FC = ({ @@ -27,6 +29,8 @@ export const Alignment: FC = ({ query, customMetaData, datasource, + metricDescriptor, + preprocessor, }) => { const alignmentLabel = useMemo(() => alignmentPeriodLabel(customMetaData, datasource), [customMetaData, datasource]); return ( @@ -39,7 +43,9 @@ export const Alignment: FC = ({ inputId={`${refId}-alignment-function`} templateVariableOptions={templateVariableOptions} query={query} - onChange={onChange} + onChange={(q) => onChange({ ...query, ...q })} + metricDescriptor={metricDescriptor} + preprocessor={preprocessor} /> diff --git a/public/app/plugins/datasource/cloud-monitoring/components/AlignmentFunction.tsx b/public/app/plugins/datasource/cloud-monitoring/components/AlignmentFunction.tsx index eb0a56f2d9d..9478e4bc910 100644 --- a/public/app/plugins/datasource/cloud-monitoring/components/AlignmentFunction.tsx +++ b/public/app/plugins/datasource/cloud-monitoring/components/AlignmentFunction.tsx @@ -4,17 +4,28 @@ import { SelectableValue } from '@grafana/data'; import { Select } from '@grafana/ui'; import { getAlignmentPickerData } from '../functions'; -import { MetricQuery } from '../types'; +import { MetricDescriptor, PreprocessorType, SLOQuery, TimeSeriesList } from '../types'; export interface Props { inputId: string; - onChange: (query: MetricQuery) => void; - query: MetricQuery; + onChange: (query: TimeSeriesList | SLOQuery) => void; + query: TimeSeriesList | SLOQuery; templateVariableOptions: Array>; + metricDescriptor?: MetricDescriptor; + preprocessor?: PreprocessorType; } -export const AlignmentFunction: FC = ({ inputId, query, templateVariableOptions, onChange }) => { - const { valueType, metricKind, perSeriesAligner: psa, preprocessor } = query; +export const AlignmentFunction: FC = ({ + inputId, + query, + templateVariableOptions, + onChange, + metricDescriptor, + preprocessor, +}) => { + const { perSeriesAligner: psa } = query; + let { valueType, metricKind } = metricDescriptor || {}; + const { perSeriesAligner, alignOptions } = useMemo( () => getAlignmentPickerData(valueType, metricKind, psa, preprocessor), [valueType, metricKind, psa, preprocessor] diff --git a/public/app/plugins/datasource/cloud-monitoring/components/AnnotationQueryEditor.test.tsx b/public/app/plugins/datasource/cloud-monitoring/components/AnnotationQueryEditor.test.tsx index 5ac47371e7b..37decde5b95 100644 --- a/public/app/plugins/datasource/cloud-monitoring/components/AnnotationQueryEditor.test.tsx +++ b/public/app/plugins/datasource/cloud-monitoring/components/AnnotationQueryEditor.test.tsx @@ -7,6 +7,13 @@ import { createMockQuery } from '../__mocks__/cloudMonitoringQuery'; import { AnnotationQueryEditor } from './AnnotationQueryEditor'; +jest.mock('@grafana/runtime', () => ({ + ...(jest.requireActual('@grafana/runtime') as unknown as object), + getTemplateSrv: () => ({ + replace: (val: string) => val, + }), +})); + describe('AnnotationQueryEditor', () => { it('renders correctly', async () => { const onChange = jest.fn(); diff --git a/public/app/plugins/datasource/cloud-monitoring/components/AnnotationQueryEditor.tsx b/public/app/plugins/datasource/cloud-monitoring/components/AnnotationQueryEditor.tsx index c868129ebda..ec655c10529 100644 --- a/public/app/plugins/datasource/cloud-monitoring/components/AnnotationQueryEditor.tsx +++ b/public/app/plugins/datasource/cloud-monitoring/components/AnnotationQueryEditor.tsx @@ -1,4 +1,4 @@ -import React, { useState } from 'react'; +import React, { useEffect, useState } from 'react'; import { useDebounce } from 'react-use'; import { QueryEditorProps, toOption } from '@grafana/data'; @@ -6,54 +6,32 @@ import { EditorField, EditorRows } from '@grafana/experimental'; import { Input } from '@grafana/ui'; import CloudMonitoringDatasource from '../datasource'; -import { - EditorMode, - MetricKind, - AnnotationMetricQuery, - CloudMonitoringOptions, - CloudMonitoringQuery, - AlignmentTypes, -} from '../types'; +import { AnnotationQuery, CloudMonitoringOptions, CloudMonitoringQuery, QueryType } from '../types'; -import { MetricQueryEditor } from './MetricQueryEditor'; +import { MetricQueryEditor, defaultTimeSeriesList } from './MetricQueryEditor'; import { AnnotationsHelp } from './'; export type Props = QueryEditorProps; -export const defaultQuery: (datasource: CloudMonitoringDatasource) => AnnotationMetricQuery = (datasource) => ({ - editorMode: EditorMode.Visual, - projectName: datasource.getDefaultProject(), - projects: [], - metricType: '', - filters: [], - metricKind: MetricKind.GAUGE, - valueType: '', - refId: 'annotationQuery', +export const defaultQuery: (datasource: CloudMonitoringDatasource) => AnnotationQuery = (datasource) => ({ + ...defaultTimeSeriesList(datasource), title: '', text: '', - labels: {}, - variableOptionGroup: {}, - variableOptions: [], - query: '', - crossSeriesReducer: 'REDUCE_NONE', - perSeriesAligner: AlignmentTypes.ALIGN_NONE, - alignmentPeriod: 'grafana-auto', }); export const AnnotationQueryEditor = (props: Props) => { const { datasource, query, onRunQuery, data, onChange } = props; const meta = data?.series.length ? data?.series[0].meta : {}; const customMetaData = meta?.custom ?? {}; - const metricQuery = { ...defaultQuery(datasource), ...query.metricQuery }; - const [title, setTitle] = useState(metricQuery.title || ''); - const [text, setText] = useState(metricQuery.text || ''); + const timeSeriesList = { ...defaultQuery(datasource), ...query.timeSeriesList }; + const [title, setTitle] = useState(timeSeriesList.title || ''); + const [text, setText] = useState(timeSeriesList.text || ''); const variableOptionGroup = { label: 'Template Variables', options: datasource.getVariables().map(toOption), }; - const handleQueryChange = (metricQuery: AnnotationMetricQuery) => onChange({ ...query, metricQuery }); const handleTitleChange = (e: React.ChangeEvent) => { setTitle(e.target.value); }; @@ -63,19 +41,26 @@ export const AnnotationQueryEditor = (props: Props) => { useDebounce( () => { - onChange({ ...query, metricQuery: { ...metricQuery, title } }); + onChange({ ...query, timeSeriesList: { ...timeSeriesList, title } }); }, 1000, [title, onChange] ); useDebounce( () => { - onChange({ ...query, metricQuery: { ...metricQuery, text } }); + onChange({ ...query, timeSeriesList: { ...timeSeriesList, text } }); }, 1000, [text, onChange] ); + // Use a known query type + useEffect(() => { + if (!Object.values(QueryType).includes(query.queryType)) { + onChange({ ...query, queryType: QueryType.TIME_SERIES_LIST }); + } + }); + return ( <> @@ -83,10 +68,10 @@ export const AnnotationQueryEditor = (props: Props) => { refId={query.refId} variableOptionGroup={variableOptionGroup} customMetaData={customMetaData} - onChange={handleQueryChange} + onChange={onChange} onRunQuery={onRunQuery} datasource={datasource} - query={metricQuery} + query={query} /> diff --git a/public/app/plugins/datasource/cloud-monitoring/components/GroupBy.test.tsx b/public/app/plugins/datasource/cloud-monitoring/components/GroupBy.test.tsx index 781b7fe32a1..5e8ebfeccaa 100644 --- a/public/app/plugins/datasource/cloud-monitoring/components/GroupBy.test.tsx +++ b/public/app/plugins/datasource/cloud-monitoring/components/GroupBy.test.tsx @@ -2,7 +2,7 @@ import { render, screen } from '@testing-library/react'; import React from 'react'; import { openMenu, select } from 'react-select-event'; -import { createMockMetricQuery } from '../__mocks__/cloudMonitoringQuery'; +import { createMockTimeSeriesList } from '../__mocks__/cloudMonitoringQuery'; import { GroupBy, Props } from './GroupBy'; @@ -15,7 +15,7 @@ const props: Props = { } as any, variableOptionGroup: { options: [] }, labels: [], - query: createMockMetricQuery(), + query: createMockTimeSeriesList(), }; describe('GroupBy', () => { diff --git a/public/app/plugins/datasource/cloud-monitoring/components/GroupBy.tsx b/public/app/plugins/datasource/cloud-monitoring/components/GroupBy.tsx index fd30735b017..b01b8bb4255 100644 --- a/public/app/plugins/datasource/cloud-monitoring/components/GroupBy.tsx +++ b/public/app/plugins/datasource/cloud-monitoring/components/GroupBy.tsx @@ -6,7 +6,7 @@ import { MultiSelect } from '@grafana/ui'; import { SYSTEM_LABELS } from '../constants'; import { labelsToGroupedOptions } from '../functions'; -import { MetricDescriptor, MetricQuery } from '../types'; +import { MetricDescriptor, TimeSeriesList } from '../types'; import { Aggregation } from './Aggregation'; @@ -15,8 +15,8 @@ export interface Props { variableOptionGroup: SelectableValue; labels: string[]; metricDescriptor?: MetricDescriptor; - onChange: (query: MetricQuery) => void; - query: MetricQuery; + onChange: (query: TimeSeriesList) => void; + query: TimeSeriesList; } export const GroupBy: FunctionComponent = ({ diff --git a/public/app/plugins/datasource/cloud-monitoring/components/LabelFilter.test.tsx b/public/app/plugins/datasource/cloud-monitoring/components/LabelFilter.test.tsx index afa143e7404..786f8fc6c5e 100644 --- a/public/app/plugins/datasource/cloud-monitoring/components/LabelFilter.test.tsx +++ b/public/app/plugins/datasource/cloud-monitoring/components/LabelFilter.test.tsx @@ -26,6 +26,13 @@ describe('LabelFilter', () => { expect(screen.getByText('value_1')).toBeInTheDocument(); }); + it('should render skip "protected" filters', () => { + const filters = ['metric.type', '=', 'value_1']; + render( {}} variableOptionGroup={[]} />); + expect(screen.queryByText('metric.type')).not.toBeInTheDocument(); + expect(screen.queryByText('value_1')).not.toBeInTheDocument(); + }); + it('can add filters', async () => { const onChange = jest.fn(); render(); diff --git a/public/app/plugins/datasource/cloud-monitoring/components/LabelFilter.tsx b/public/app/plugins/datasource/cloud-monitoring/components/LabelFilter.tsx index 7298534bbaf..c2fb2664f65 100644 --- a/public/app/plugins/datasource/cloud-monitoring/components/LabelFilter.tsx +++ b/public/app/plugins/datasource/cloud-monitoring/components/LabelFilter.tsx @@ -28,13 +28,20 @@ const filtersToStringArray = (filters: Filter[]) => 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 = ({ labels = {}, filters: filterArray, onChange: _onChange, 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( () => [variableOptionGroup, ...labelsToGroupedOptions(Object.keys(labels))], [labels, variableOptionGroup] @@ -64,7 +71,7 @@ export const LabelFilter: FunctionComponent = ({ }; const onChange = (items: Array>) => { - const filters = items.map(({ key, operator, value, condition }) => ({ + const filters = items.concat(protectedFilters).map(({ key, operator, value, condition }) => ({ key: key || '', operator: operator || DEFAULT_OPERATOR, value: value || '', diff --git a/public/app/plugins/datasource/cloud-monitoring/components/MetricQueryEditor.test.tsx b/public/app/plugins/datasource/cloud-monitoring/components/MetricQueryEditor.test.tsx new file mode 100644 index 00000000000..82ca0af7ee1 --- /dev/null +++ b/public/app/plugins/datasource/cloud-monitoring/components/MetricQueryEditor.test.tsx @@ -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(); + 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(); + expect(onChange).toHaveBeenCalled(); + }); + + it('renders an annotation query', async () => { + const onChange = jest.fn(); + const query = createMockQuery(); + query.queryType = QueryType.ANNOTATION; + + render(); + const l = await screen.findByLabelText('Project'); + expect(l).toBeInTheDocument(); + }); +}); diff --git a/public/app/plugins/datasource/cloud-monitoring/components/MetricQueryEditor.tsx b/public/app/plugins/datasource/cloud-monitoring/components/MetricQueryEditor.tsx index c02f07dd439..6da65007a55 100644 --- a/public/app/plugins/datasource/cloud-monitoring/components/MetricQueryEditor.tsx +++ b/public/app/plugins/datasource/cloud-monitoring/components/MetricQueryEditor.tsx @@ -1,20 +1,16 @@ -import React, { useCallback, useEffect, useState } from 'react'; +import React, { useCallback, useEffect } from 'react'; import { SelectableValue } from '@grafana/data'; import { EditorRows } from '@grafana/experimental'; import CloudMonitoringDatasource from '../datasource'; -import { getAlignmentPickerData } from '../functions'; import { AlignmentTypes, + CloudMonitoringQuery, CustomMetaData, - EditorMode, - MetricDescriptor, - MetricKind, - MetricQuery, - PreprocessorType, - SLOQuery, - ValueTypes, + QueryType, + TimeSeriesList, + TimeSeriesQuery, } from '../types'; import { GraphPeriod } from './GraphPeriod'; @@ -25,35 +21,24 @@ export interface Props { refId: string; customMetaData: CustomMetaData; variableOptionGroup: SelectableValue; - onChange: (query: MetricQuery) => void; + onChange: (query: CloudMonitoringQuery) => void; onRunQuery: () => void; - query: MetricQuery; + query: CloudMonitoringQuery; datasource: CloudMonitoringDatasource; } -interface State { - labels: any; - [key: string]: any; -} - -export const defaultState: State = { - labels: {}, -}; - -export const defaultQuery: (dataSource: CloudMonitoringDatasource) => MetricQuery = (dataSource) => ({ - editorMode: EditorMode.Visual, +export const defaultTimeSeriesList: (dataSource: CloudMonitoringDatasource) => TimeSeriesList = (dataSource) => ({ projectName: dataSource.getDefaultProject(), - metricType: '', - metricKind: MetricKind.GAUGE, - valueType: '', crossSeriesReducer: 'REDUCE_NONE', alignmentPeriod: 'cloud-monitoring-auto', perSeriesAligner: AlignmentTypes.ALIGN_MEAN, groupBys: [], filters: [], - aliasBy: '', +}); + +export const defaultTimeSeriesQuery: (dataSource: CloudMonitoringDatasource) => TimeSeriesQuery = (dataSource) => ({ + projectName: dataSource.getDefaultProject(), query: '', - preprocessor: PreprocessorType.None, }); function Editor({ @@ -65,69 +50,63 @@ function Editor({ customMetaData, variableOptionGroup, }: React.PropsWithChildren) { - const [state, setState] = useState(defaultState); - const { projectName, metricType, groupBys, editorMode, crossSeriesReducer } = query; - - 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 }); + const onChangeTimeSeriesList = useCallback( + (timeSeriesList: TimeSeriesList) => { + onQueryChange({ ...query, timeSeriesList }); onRunQuery(); }, [onQueryChange, onRunQuery, query] ); - const onMetricTypeChange = useCallback( - ({ valueType, metricKind, type }: MetricDescriptor) => { - const preprocessor = - metricKind === MetricKind.GAUGE || valueType === ValueTypes.DISTRIBUTION - ? PreprocessorType.None - : PreprocessorType.Rate; - const { perSeriesAligner } = getAlignmentPickerData(valueType, metricKind, state.perSeriesAligner, preprocessor); - onChange({ - ...query, - perSeriesAligner, - metricType: type, - valueType, - metricKind, - preprocessor, - }); + const onChangeTimeSeriesQuery = useCallback( + (timeSeriesQuery: TimeSeriesQuery) => { + onQueryChange({ ...query, timeSeriesQuery }); + onRunQuery(); }, - [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 ( - {editorMode === EditorMode.Visual && ( + {[QueryType.TIME_SERIES_LIST, QueryType.ANNOTATION].includes(query.queryType) && query.timeSeriesList && ( onQueryChange({ ...query, aliasBy })} /> )} - {editorMode === EditorMode.MQL && ( + {query.queryType === QueryType.TIME_SERIES_QUERY && query.timeSeriesQuery && ( <> onQueryChange({ ...query, query: q })} + onChange={(q: string) => onChangeTimeSeriesQuery({ ...query.timeSeriesQuery!, query: q })} onRunQuery={onRunQuery} - query={query.query} + query={query.timeSeriesQuery.query} > onQueryChange({ ...query, graphPeriod })} - graphPeriod={query.graphPeriod} + onChange={(graphPeriod: string) => onChangeTimeSeriesQuery({ ...query.timeSeriesQuery!, graphPeriod })} + graphPeriod={query.timeSeriesQuery.graphPeriod} refId={refId} variableOptionGroup={variableOptionGroup} /> diff --git a/public/app/plugins/datasource/cloud-monitoring/components/Metrics.test.tsx b/public/app/plugins/datasource/cloud-monitoring/components/Metrics.test.tsx index 53a2f18077b..74973ba7abe 100644 --- a/public/app/plugins/datasource/cloud-monitoring/components/Metrics.test.tsx +++ b/public/app/plugins/datasource/cloud-monitoring/components/Metrics.test.tsx @@ -4,14 +4,14 @@ import { openMenu, select } from 'react-select-event'; import { createMockDatasource } from '../__mocks__/cloudMonitoringDatasource'; import { createMockMetricDescriptor } from '../__mocks__/cloudMonitoringMetricDescriptor'; -import { createMockMetricQuery } from '../__mocks__/cloudMonitoringQuery'; +import { createMockTimeSeriesList } from '../__mocks__/cloudMonitoringQuery'; import { Metrics } from './Metrics'; describe('Metrics', () => { it('renders metrics fields', async () => { const onChange = jest.fn(); - const query = createMockMetricQuery(); + const query = createMockTimeSeriesList(); const datasource = createMockDatasource(); render( @@ -35,7 +35,7 @@ describe('Metrics', () => { it('can select a service', async () => { const onChange = jest.fn(); - const query = createMockMetricQuery(); + const query = createMockTimeSeriesList(); const datasource = createMockDatasource({ getMetricTypes: jest.fn().mockResolvedValue([createMockMetricDescriptor()]), }); @@ -63,7 +63,7 @@ describe('Metrics', () => { it('can select a metric name', async () => { const onChange = jest.fn(); - const query = createMockMetricQuery(); + const query = createMockTimeSeriesList(); const datasource = createMockDatasource({ getMetricTypes: jest.fn().mockResolvedValue([createMockMetricDescriptor()]), }); @@ -91,7 +91,7 @@ describe('Metrics', () => { it('should render available metric options according to the selected service', async () => { const onChange = jest.fn(); - const query = createMockMetricQuery(); + const query = createMockTimeSeriesList(); const datasource = createMockDatasource({ getMetricTypes: jest.fn().mockResolvedValue([ createMockMetricDescriptor({ @@ -180,7 +180,7 @@ describe('Metrics', () => { }), ]), }); - const query = createMockMetricQuery(); + const query = createMockTimeSeriesList(); render( JSX.Element; - onProjectChange: (query: MetricQuery) => void; + onProjectChange: (query: TimeSeriesList) => void; } export function Metrics(props: Props) { diff --git a/public/app/plugins/datasource/cloud-monitoring/components/Preprocessor.test.tsx b/public/app/plugins/datasource/cloud-monitoring/components/Preprocessor.test.tsx index f2c7c216717..a1d419ae543 100644 --- a/public/app/plugins/datasource/cloud-monitoring/components/Preprocessor.test.tsx +++ b/public/app/plugins/datasource/cloud-monitoring/components/Preprocessor.test.tsx @@ -5,7 +5,7 @@ import React from 'react'; import { TemplateSrvMock } from 'app/features/templating/template_srv.mock'; import { createMockMetricDescriptor } from '../__mocks__/cloudMonitoringMetricDescriptor'; -import { createMockMetricQuery } from '../__mocks__/cloudMonitoringQuery'; +import { createMockTimeSeriesList } from '../__mocks__/cloudMonitoringQuery'; import { MetricKind, ValueTypes } from '../types'; import { Preprocessor } from './Preprocessor'; @@ -17,7 +17,7 @@ jest.mock('@grafana/runtime', () => ({ describe('Preprocessor', () => { it('only provides "None" as an option if no metric descriptor is provided', () => { - const query = createMockMetricQuery(); + const query = createMockTimeSeriesList(); const onChange = jest.fn(); render(); @@ -28,7 +28,7 @@ describe('Preprocessor', () => { }); it('only provides "None" as an option if metric kind is "Gauge"', () => { - const query = createMockMetricQuery(); + const query = createMockTimeSeriesList(); const onChange = jest.fn(); const metricDescriptor = createMockMetricDescriptor({ metricKind: MetricKind.GAUGE }); @@ -40,7 +40,7 @@ describe('Preprocessor', () => { }); it('only provides "None" as an option if value type is "Distribution"', () => { - const query = createMockMetricQuery(); + const query = createMockTimeSeriesList(); const onChange = jest.fn(); 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"', () => { - const query = createMockMetricQuery(); + const query = createMockTimeSeriesList(); const onChange = jest.fn(); 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"', () => { - const query = createMockMetricQuery(); + const query = createMockTimeSeriesList(); const onChange = jest.fn(); 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 () => { - const query = createMockMetricQuery(); + const query = createMockTimeSeriesList(); const onChange = jest.fn(); const metricDescriptor = createMockMetricDescriptor({ metricKind: MetricKind.CUMULATIVE }); diff --git a/public/app/plugins/datasource/cloud-monitoring/components/Preprocessor.tsx b/public/app/plugins/datasource/cloud-monitoring/components/Preprocessor.tsx index 526ca98658f..d38d5dca93c 100644 --- a/public/app/plugins/datasource/cloud-monitoring/components/Preprocessor.tsx +++ b/public/app/plugins/datasource/cloud-monitoring/components/Preprocessor.tsx @@ -5,18 +5,19 @@ import { EditorField } from '@grafana/experimental'; import { RadioButtonGroup } from '@grafana/ui'; 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 }; export interface Props { metricDescriptor?: MetricDescriptor; - onChange: (query: MetricQuery) => void; - query: MetricQuery; + onChange: (query: TimeSeriesList) => void; + query: TimeSeriesList; } export const Preprocessor: FunctionComponent = ({ query, metricDescriptor, onChange }) => { const options = useOptions(metricDescriptor); + return ( = ({ query, metricDescriptor > { - const { valueType, metricKind, perSeriesAligner: psa } = query; + const { perSeriesAligner: psa } = query; + const { valueType, metricKind } = metricDescriptor ?? {}; const { perSeriesAligner } = getAlignmentPickerData(valueType, metricKind, psa, value); onChange({ ...query, preprocessor: value, perSeriesAligner }); }} diff --git a/public/app/plugins/datasource/cloud-monitoring/components/QueryEditor.test.tsx b/public/app/plugins/datasource/cloud-monitoring/components/QueryEditor.test.tsx new file mode 100644 index 00000000000..0cd79aac4e6 --- /dev/null +++ b/public/app/plugins/datasource/cloud-monitoring/components/QueryEditor.test.tsx @@ -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(); + 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(); + await waitFor(() => + expect(onChange).toHaveBeenCalledWith(expect.objectContaining({ queryType: QueryType.TIME_SERIES_LIST })) + ); + }); +}); diff --git a/public/app/plugins/datasource/cloud-monitoring/components/QueryEditor.tsx b/public/app/plugins/datasource/cloud-monitoring/components/QueryEditor.tsx index 3df829e58d7..817ff119bd6 100644 --- a/public/app/plugins/datasource/cloud-monitoring/components/QueryEditor.tsx +++ b/public/app/plugins/datasource/cloud-monitoring/components/QueryEditor.tsx @@ -1,12 +1,11 @@ -import React, { PureComponent } from 'react'; +import React, { useEffect, useMemo, useState } from 'react'; import { QueryEditorProps, toOption } from '@grafana/data'; import { EditorRows } from '@grafana/experimental'; 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 { defaultQuery as defaultSLOQuery } from './SLOQueryEditor'; @@ -14,80 +13,68 @@ import { MetricQueryEditor, SLOQueryEditor } from './'; export type Props = QueryEditorProps; -export class QueryEditor extends PureComponent { - async UNSAFE_componentWillMount() { - const { datasource, query } = this.props; - - // Unfortunately, migrations like this need to go UNSAFE_componentWillMount. As soon as there's - // migration hook for this module.ts, we can do the migrations there instead. - if (!this.props.query.hasOwnProperty('metricQuery')) { - const { hide, refId, datasource, key, queryType, maxLines, metric, ...metricQuery } = this.props.query as any; - this.props.query.metricQuery = metricQuery; +export const QueryEditor = (props: Props) => { + const { datasource, query: oldQ, onRunQuery, onChange } = props; + // Migrate query if needed + const [migrated, setMigrated] = useState(false); + const query = useMemo(() => { + if (!migrated) { + setMigrated(true); + return datasource.migrateQuery(oldQ); } + return oldQ; + }, [oldQ, datasource, migrated]); - if (![QueryType.METRICS, QueryType.SLO].includes(this.props.query.queryType)) { - this.props.query.queryType = QueryType.METRICS; + const sloQuery = { ...defaultSLOQuery(datasource), ...query.sloQuery }; + 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(); - if (!query.metricQuery.projectName) { - this.props.query.metricQuery.projectName = datasource.getDefaultProject(); - } - } - - onQueryChange(prop: string, value: MetricQuery | SLOQuery) { - this.props.onChange({ ...this.props.query, [prop]: value }); - 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 ( - - + + {queryType !== QueryType.SLO && ( + - {queryType === QueryType.METRICS && ( - { - this.props.onChange({ ...this.props.query, metricQuery }); - }} - onRunQuery={onRunQuery} - datasource={datasource} - query={metricQuery} - /> - )} + )} - {queryType === QueryType.SLO && ( - this.onQueryChange('sloQuery', query)} - onRunQuery={onRunQuery} - datasource={datasource} - query={sloQuery} - /> - )} - - ); - } -} + {queryType === QueryType.SLO && ( + onChange({ ...query, aliasBy })} + /> + )} + + ); +}; diff --git a/public/app/plugins/datasource/cloud-monitoring/components/QueryHeader.test.tsx b/public/app/plugins/datasource/cloud-monitoring/components/QueryHeader.test.tsx index 478e67138ee..8241884b7ac 100644 --- a/public/app/plugins/datasource/cloud-monitoring/components/QueryHeader.test.tsx +++ b/public/app/plugins/datasource/cloud-monitoring/components/QueryHeader.test.tsx @@ -1,74 +1,19 @@ import { render, screen } from '@testing-library/react'; -import userEvent from '@testing-library/user-event'; import React from 'react'; import { openMenu, select } from 'react-select-event'; -import { createMockQuery, createMockSLOQuery } from '../__mocks__/cloudMonitoringQuery'; -import { EditorMode, QueryType } from '../types'; +import { createMockQuery } from '../__mocks__/cloudMonitoringQuery'; +import { QueryType } from '../types'; import { QueryHeader } from './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 { metricQuery } = query; - const sloQuery = createMockSLOQuery(); const onChange = jest.fn(); const onRunQuery = jest.fn(); - render( - - ); - - 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( - - ); - - 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( - - ); + render(); const queryType = screen.getByLabelText(/Query type/); await openMenu(queryType); @@ -76,32 +21,16 @@ describe('QueryHeader', () => { 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 { metricQuery } = query; - const sloQuery = createMockSLOQuery(); const onChange = jest.fn(); const onRunQuery = jest.fn(); - render( - - ); + render(); - const builder = screen.getByLabelText('Builder'); - const MQL = screen.getByLabelText('MQL'); - expect(builder).toBeChecked(); - expect(MQL).not.toBeChecked(); - - await userEvent.click(MQL); - - expect(onChange).toBeCalledWith( - expect.objectContaining({ metricQuery: expect.objectContaining({ editorMode: EditorMode.MQL }) }) - ); + const queryType = screen.getByLabelText(/Query type/); + await openMenu(queryType); + await select(screen.getByLabelText('Select options menu'), 'MQL'); + expect(onChange).toBeCalledWith(expect.objectContaining({ queryType: QueryType.TIME_SERIES_QUERY })); }); }); diff --git a/public/app/plugins/datasource/cloud-monitoring/components/QueryHeader.tsx b/public/app/plugins/datasource/cloud-monitoring/components/QueryHeader.tsx index 86eea6a3e35..7d16fcd1aca 100644 --- a/public/app/plugins/datasource/cloud-monitoring/components/QueryHeader.tsx +++ b/public/app/plugins/datasource/cloud-monitoring/components/QueryHeader.tsx @@ -1,28 +1,19 @@ import React from 'react'; import { EditorHeader, FlexItem, InlineSelect } from '@grafana/experimental'; -import { RadioButtonGroup } from '@grafana/ui'; import { QUERY_TYPES } from '../constants'; -import { EditorMode, CloudMonitoringQuery, QueryType, SLOQuery, MetricQuery } from '../types'; +import { CloudMonitoringQuery } from '../types'; export interface QueryEditorHeaderProps { query: CloudMonitoringQuery; - metricQuery: MetricQuery; - sloQuery: SLOQuery; onChange: (value: CloudMonitoringQuery) => void; onRunQuery: () => void; } -const EDITOR_MODES = [ - { label: 'Builder', value: EditorMode.Visual }, - { label: 'MQL', value: EditorMode.MQL }, -]; - export const QueryHeader = (props: QueryEditorHeaderProps) => { - const { query, metricQuery, sloQuery, onChange, onRunQuery } = props; + const { query, onChange, onRunQuery } = props; const { queryType } = query; - const { editorMode } = metricQuery; return ( @@ -31,27 +22,11 @@ export const QueryHeader = (props: QueryEditorHeaderProps) => { options={QUERY_TYPES} value={queryType} onChange={({ value }) => { - onChange({ ...query, sloQuery, queryType: value! }); + onChange({ ...query, queryType: value! }); onRunQuery(); }} /> - {queryType !== QueryType.SLO && ( - { - onChange({ - ...query, - metricQuery: { - ...metricQuery, - editorMode: value, - }, - }); - }} - /> - )} ); }; diff --git a/public/app/plugins/datasource/cloud-monitoring/components/SLOQueryEditor.tsx b/public/app/plugins/datasource/cloud-monitoring/components/SLOQueryEditor.tsx index bb863416287..0ee2681d789 100644 --- a/public/app/plugins/datasource/cloud-monitoring/components/SLOQueryEditor.tsx +++ b/public/app/plugins/datasource/cloud-monitoring/components/SLOQueryEditor.tsx @@ -24,6 +24,8 @@ export interface Props { onRunQuery: () => void; query: SLOQuery; datasource: CloudMonitoringDatasource; + aliasBy?: string; + onChangeAliasBy: (aliasBy: string) => void; } export const defaultQuery: (dataSource: CloudMonitoringDatasource) => SLOQuery = (dataSource) => ({ @@ -46,6 +48,8 @@ export function SLOQueryEditor({ onChange, variableOptionGroup, customMetaData, + aliasBy, + onChangeAliasBy, }: React.PropsWithChildren) { const alignmentLabel = useMemo(() => alignmentPeriodLabel(customMetaData, datasource), [customMetaData, datasource]); return ( @@ -100,7 +104,7 @@ export function SLOQueryEditor({ - onChange({ ...query, aliasBy })} /> + ); diff --git a/public/app/plugins/datasource/cloud-monitoring/components/VisualMetricQueryEditor.tsx b/public/app/plugins/datasource/cloud-monitoring/components/VisualMetricQueryEditor.tsx index 318b51a37a4..ebaae3bf0aa 100644 --- a/public/app/plugins/datasource/cloud-monitoring/components/VisualMetricQueryEditor.tsx +++ b/public/app/plugins/datasource/cloud-monitoring/components/VisualMetricQueryEditor.tsx @@ -1,10 +1,11 @@ -import React from 'react'; +import React, { useCallback, useEffect, useState } from 'react'; import { SelectableValue } from '@grafana/data'; import { EditorRow } from '@grafana/experimental'; 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 { Alignment } from './Alignment'; @@ -17,28 +18,59 @@ export interface Props { refId: string; customMetaData: CustomMetaData; variableOptionGroup: SelectableValue; - onMetricTypeChange: (query: MetricDescriptor) => void; - onChange: (query: MetricQuery | SLOQuery) => void; - query: MetricQuery; + onChange: (query: TimeSeriesList) => void; + query: TimeSeriesList; datasource: CloudMonitoringDatasource; - labels: any; + aliasBy?: string; + onChangeAliasBy: (aliasBy: string) => void; } function Editor({ refId, query, - labels, datasource, onChange, - onMetricTypeChange, customMetaData, variableOptionGroup, + aliasBy, + onChangeAliasBy, }: React.PropsWithChildren) { + 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 ( - { - onChange({ ...query, aliasBy }); - }} - /> + )} diff --git a/public/app/plugins/datasource/cloud-monitoring/constants.ts b/public/app/plugins/datasource/cloud-monitoring/constants.ts index 09c215b76e6..e916247cb8e 100644 --- a/public/app/plugins/datasource/cloud-monitoring/constants.ts +++ b/public/app/plugins/datasource/cloud-monitoring/constants.ts @@ -316,6 +316,7 @@ export const SELECTORS = [ ]; 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 }, ]; diff --git a/public/app/plugins/datasource/cloud-monitoring/datasource.test.ts b/public/app/plugins/datasource/cloud-monitoring/datasource.test.ts index 792544145cf..86b7446117d 100644 --- a/public/app/plugins/datasource/cloud-monitoring/datasource.test.ts +++ b/public/app/plugins/datasource/cloud-monitoring/datasource.test.ts @@ -1,8 +1,12 @@ +import { get } from 'lodash'; +import { lastValueFrom, of } from 'rxjs'; + import { TemplateSrv } from 'app/features/templating/template_srv'; import { createMockInstanceSetttings } from './__mocks__/cloudMonitoringInstanceSettings'; import { createMockQuery } from './__mocks__/cloudMonitoringQuery'; import Datasource from './datasource'; +import { CloudMonitoringQuery, MetricKind, PreprocessorType, QueryType } from './types'; describe('Cloud Monitoring Datasource', () => { describe('interpolateVariablesInQueries', () => { @@ -14,15 +18,289 @@ describe('Cloud Monitoring Datasource', () => { expect(templateVariablesApplied[0]).toEqual(query); }); - it('should correctly apply template variables', () => { + it('should correctly apply template variables for metricQuery (deprecated)', () => { const templateSrv = new TemplateSrv(); templateSrv.replace = jest.fn().mockReturnValue('project-variable'); const mockInstanceSettings = createMockInstanceSetttings(); const ds = new Datasource(mockInstanceSettings, templateSrv); - const query = createMockQuery({ metricQuery: { projectName: '$testVar' } }); + const query = createMockQuery({ timeSeriesList: { projectName: '$testVar', crossSeriesReducer: '' } }); const templatedQuery = ds.interpolateVariablesInQueries([query], {}); 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', + }, + }), + ]), + }), + }) + ); }); }); }); diff --git a/public/app/plugins/datasource/cloud-monitoring/datasource.ts b/public/app/plugins/datasource/cloud-monitoring/datasource.ts index 1ae6d23f7e4..2211c09942a 100644 --- a/public/app/plugins/datasource/cloud-monitoring/datasource.ts +++ b/public/app/plugins/datasource/cloud-monitoring/datasource.ts @@ -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 { map, mergeMap } from 'rxjs/operators'; @@ -9,21 +9,22 @@ import { ScopedVars, SelectableValue, } 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 { getTemplateSrv, TemplateSrv } from 'app/features/templating/template_srv'; import { CloudMonitoringAnnotationSupport } from './annotationSupport'; import { SLO_BURN_RATE_SELECTOR_NAME } from './constants'; +import { getMetricType, setMetricType } from './functions'; import { CloudMonitoringOptions, CloudMonitoringQuery, - EditorMode, Filter, MetricDescriptor, QueryType, PostResponse, Aggregation, + MetricQuery, } from './types'; import { CloudMonitoringVariableSupport } from './variables'; @@ -33,6 +34,7 @@ export default class CloudMonitoringDatasource extends DataSourceWithBackend< > { authenticationType: string; intervalMs: number; + backendSrv: BackendSrv; constructor( private instanceSettings: DataSourceInstanceSettings, @@ -44,6 +46,7 @@ export default class CloudMonitoringDatasource extends DataSourceWithBackend< this.variables = new CloudMonitoringVariableSupport(this); this.intervalMs = 0; this.annotations = CloudMonitoringAnnotationSupport(this); + this.backendSrv = getBackendSrv(); } getVariables() { @@ -59,21 +62,28 @@ export default class CloudMonitoringDatasource extends DataSourceWithBackend< } applyTemplateVariables(target: CloudMonitoringQuery, scopedVars: ScopedVars): Record { - const { metricQuery, sloQuery } = target; + const { timeSeriesList, timeSeriesQuery, sloQuery } = target; + return { ...target, datasource: this.getRef(), intervalMs: this.intervalMs, - metricQuery: { - ...this.interpolateProps(metricQuery, scopedVars), + timeSeriesList: timeSeriesList && { + ...this.interpolateProps(timeSeriesList, scopedVars), 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 ), - 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), }; @@ -85,18 +95,20 @@ export default class CloudMonitoringDatasource extends DataSourceWithBackend< { refId, datasource: this.getRef(), - queryType: QueryType.METRICS, - metricQuery: { - projectName: this.templateSrv.replace(projectName), - metricType: this.templateSrv.replace(metricType), - groupBys: this.interpolateGroupBys(aggregation?.groupBys || [], {}), - crossSeriesReducer: aggregation?.crossSeriesReducer ?? 'REDUCE_NONE', - view: 'HEADERS', - }, + queryType: QueryType.TIME_SERIES_LIST, + timeSeriesList: setMetricType( + { + projectName: this.templateSrv.replace(projectName), + groupBys: this.interpolateGroupBys(aggregation?.groupBys || [], {}), + crossSeriesReducer: aggregation?.crossSeriesReducer ?? 'REDUCE_NONE', + view: 'HEADERS', + }, + metricType + ), }, ], range: this.timeSrv.timeRange(), - } as DataQueryRequest; + }; const queries = options.targets; @@ -107,7 +119,7 @@ export default class CloudMonitoringDatasource extends DataSourceWithBackend< return lastValueFrom( from(this.ensureGCEDefaultProject()).pipe( mergeMap(() => { - return getBackendSrv().fetch({ + return this.backendSrv.fetch({ url: '/api/ds/query', method: 'POST', headers: this.getRequestHeaders(), @@ -193,20 +205,73 @@ export default class CloudMonitoringDatasource extends DataSourceWithBackend< 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 { - 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; return { refId, intervalMs, hide, - queryType: type === 'annotationQuery' ? QueryType.ANNOTATION : QueryType.METRICS, - metricQuery: { + queryType: type === 'annotationQuery' ? QueryType.ANNOTATION : QueryType.TIME_SERIES_LIST, + timeSeriesList: { ...rest, 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; } @@ -224,7 +289,10 @@ export default class CloudMonitoringDatasource extends DataSourceWithBackend< 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; return ( !!selectorName && @@ -235,13 +303,15 @@ export default class CloudMonitoringDatasource extends DataSourceWithBackend< ); } - if (query.queryType && query.queryType === QueryType.METRICS && query.metricQuery.editorMode === EditorMode.MQL) { - return !!query.metricQuery.projectName && !!query.metricQuery.query; + if (query.queryType === QueryType.TIME_SERIES_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[] { diff --git a/public/app/plugins/datasource/cloud-monitoring/functions.test.ts b/public/app/plugins/datasource/cloud-monitoring/functions.test.ts index 3d932ed7ef5..2256f311a04 100644 --- a/public/app/plugins/datasource/cloud-monitoring/functions.test.ts +++ b/public/app/plugins/datasource/cloud-monitoring/functions.test.ts @@ -10,9 +10,11 @@ import { labelsToGroupedOptions, stringArrayToFilters, alignmentPeriodLabel, + getMetricType, + setMetricType, } from './functions'; 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.requireActual('@grafana/runtime') as unknown as object), @@ -236,4 +238,23 @@ describe('functions', () => { 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']); + }); + }); }); diff --git a/public/app/plugins/datasource/cloud-monitoring/functions.ts b/public/app/plugins/datasource/cloud-monitoring/functions.ts index 598053ebf87..bcca4c601d1 100644 --- a/public/app/plugins/datasource/cloud-monitoring/functions.ts +++ b/public/app/plugins/datasource/cloud-monitoring/functions.ts @@ -5,7 +5,15 @@ import { getTemplateSrv, TemplateSrv } from '@grafana/runtime'; import { AGGREGATIONS, ALIGNMENTS, SYSTEM_LABELS } from './constants'; 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[]) => uniqBy(metricDescriptors, 'service'); @@ -35,8 +43,8 @@ export const getMetricTypes = ( }; export const getAlignmentOptionsByMetric = ( - metricValueType: string, - metricKind: string, + metricValueType?: string, + metricKind?: string, preprocessor?: PreprocessorType ) => { if (preprocessor && preprocessor === PreprocessorType.Rate) { @@ -78,7 +86,7 @@ export const getAlignmentPickerData = ( preprocessor?: PreprocessorType ) => { const templateSrv: TemplateSrv = getTemplateSrv(); - const alignOptions = getAlignmentOptionsByMetric(valueType!, metricKind!, preprocessor!).map((option) => ({ + const alignOptions = getAlignmentOptionsByMetric(valueType, metricKind, preprocessor).map((option) => ({ ...option, label: option.text, })); @@ -125,3 +133,25 @@ export const alignmentPeriodLabel = (customMetaData: CustomMetaData, datasource: const hms = rangeUtil.secondsToHms(seconds); 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; +}; diff --git a/public/app/plugins/datasource/cloud-monitoring/specs/datasource.test.ts b/public/app/plugins/datasource/cloud-monitoring/specs/datasource.test.ts index 9cfc69d10e0..b68d79a39b4 100644 --- a/public/app/plugins/datasource/cloud-monitoring/specs/datasource.test.ts +++ b/public/app/plugins/datasource/cloud-monitoring/specs/datasource.test.ts @@ -94,10 +94,10 @@ describe('CloudMonitoringDataSource', () => { const { ds } = getTestcontext(); 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', groupBys: [], - metricType: 'cpu', + filters: ['metric.type', '=', 'cpu'], projectName: 'default-proj', view: 'HEADERS', }); @@ -112,10 +112,10 @@ describe('CloudMonitoringDataSource', () => { 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', groupBys: ['metadata.system_label.name'], - metricType: 'sql', + filters: ['metric.type', '=', 'sql'], projectName: 'default-proj', view: 'HEADERS', }); diff --git a/public/app/plugins/datasource/cloud-monitoring/types.ts b/public/app/plugins/datasource/cloud-monitoring/types.ts index f8b1d1b01d9..d20e951f4f2 100644 --- a/public/app/plugins/datasource/cloud-monitoring/types.ts +++ b/public/app/plugins/datasource/cloud-monitoring/types.ts @@ -55,16 +55,12 @@ export interface Aggregation { } export enum QueryType { - METRICS = 'metrics', + TIME_SERIES_LIST = 'timeSeriesList', + TIME_SERIES_QUERY = 'timeSeriesQuery', SLO = 'slo', ANNOTATION = 'annotation', } -export enum EditorMode { - Visual = 'visual', - MQL = 'mql', -} - export enum PreprocessorType { None = 'none', Rate = 'rate', @@ -110,15 +106,14 @@ export enum AlignmentTypes { ALIGN_NONE = 'ALIGN_NONE', } -export interface BaseQuery { +// deprecated: use TimeSeriesList instead +// left here for migration purposes +export interface MetricQuery { projectName: string; perSeriesAligner?: string; alignmentPeriod?: string; aliasBy?: string; -} - -export interface MetricQuery extends BaseQuery { - editorMode: EditorMode; + editorMode: string; metricType: string; crossSeriesReducer: string; groupBys?: string[]; @@ -132,12 +127,39 @@ export interface MetricQuery extends BaseQuery { 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; text?: string; } -export interface SLOQuery extends BaseQuery { +export interface SLOQuery { + projectName: string; + perSeriesAligner?: string; + alignmentPeriod?: string; selectorName: string; serviceId: string; serviceName: string; @@ -148,9 +170,11 @@ export interface SLOQuery extends BaseQuery { } export interface CloudMonitoringQuery extends DataQuery { + aliasBy?: string; datasourceId?: number; // Should not be necessary anymore queryType: QueryType; - metricQuery: MetricQuery | AnnotationMetricQuery; + timeSeriesList?: TimeSeriesList | AnnotationQuery; + timeSeriesQuery?: TimeSeriesQuery; sloQuery?: SLOQuery; intervalMs: number; }