mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
GoogleCloudMonitoring: Adapt frontend to the new API format (#60173)
* GoogleCloudMonitoring: Migrate queries to the new format * Refactor Aligment and AligmentFunction components (#60235) * Adapt CloudMonitoringDatasource and CloudMonitoringAnnotationSupport (#60177) * Fix: avoid migration for new queries (#60375) * Move preprocessor handling to the backend (#60383) * Other fixes and new function (#60411) * Adapt components to the new API (#60451) * Split metrics query type in time series list and query (#60475) * Clean up metricQuery references (#60478) * More bug fixes (#60525)
This commit is contained in:
parent
f6f140c412
commit
4d693863c0
@ -5212,17 +5212,9 @@ exports[`better eslint`] = {
|
||||
"public/app/plugins/datasource/cloud-monitoring/components/MQLQueryEditor.tsx:5381": [
|
||||
[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"],
|
||||
|
@ -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{
|
||||
|
@ -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": {
|
||||
|
@ -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 != "" {
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -14,6 +14,7 @@ export const createMockDatasource = (overrides?: Partial<Datasource>) => {
|
||||
getDefaultProject: jest.fn().mockReturnValue('cloud-monitoring-default-project'),
|
||||
templateSrv,
|
||||
getSLOServices: jest.fn().mockResolvedValue([]),
|
||||
migrateQuery: jest.fn().mockImplementation((query) => query),
|
||||
...overrides,
|
||||
};
|
||||
|
||||
|
@ -1,25 +1,9 @@
|
||||
import { AlignmentTypes, CloudMonitoringQuery, EditorMode, MetricQuery, QueryType, SLOQuery } from '../types';
|
||||
import { AlignmentTypes, CloudMonitoringQuery, QueryType, SLOQuery, TimeSeriesList, TimeSeriesQuery } from '../types';
|
||||
|
||||
type Subset<K> = {
|
||||
[attr in keyof K]?: K[attr] extends object ? Subset<K[attr]> : K[attr];
|
||||
};
|
||||
|
||||
export const createMockMetricQuery: (overrides?: Partial<MetricQuery>) => MetricQuery = (
|
||||
overrides?: Partial<MetricQuery>
|
||||
) => {
|
||||
return {
|
||||
editorMode: EditorMode.Visual,
|
||||
metricType: '',
|
||||
crossSeriesReducer: 'REDUCE_NONE',
|
||||
query: '',
|
||||
projectName: 'cloud-monitoring-default-project',
|
||||
filters: [],
|
||||
groupBys: [],
|
||||
view: 'FULL',
|
||||
...overrides,
|
||||
};
|
||||
};
|
||||
|
||||
export const createMockSLOQuery: (overrides?: Partial<SLOQuery>) => SLOQuery = (overrides) => {
|
||||
return {
|
||||
projectName: 'projectName',
|
||||
@ -36,6 +20,29 @@ export const createMockSLOQuery: (overrides?: Partial<SLOQuery>) => SLOQuery = (
|
||||
};
|
||||
};
|
||||
|
||||
export const createMockTimeSeriesList: (overrides?: Partial<TimeSeriesList>) => TimeSeriesList = (
|
||||
overrides?: Partial<TimeSeriesList>
|
||||
) => {
|
||||
return {
|
||||
crossSeriesReducer: 'REDUCE_NONE',
|
||||
projectName: 'cloud-monitoring-default-project',
|
||||
filters: [],
|
||||
groupBys: [],
|
||||
view: 'FULL',
|
||||
...overrides,
|
||||
};
|
||||
};
|
||||
|
||||
export const createMockTimeSeriesQuery: (overrides?: Partial<TimeSeriesQuery>) => TimeSeriesQuery = (
|
||||
overrides?: Partial<TimeSeriesQuery>
|
||||
) => {
|
||||
return {
|
||||
query: '',
|
||||
projectName: 'cloud-monitoring-default-project',
|
||||
...overrides,
|
||||
};
|
||||
};
|
||||
|
||||
export const createMockQuery: (overrides?: Subset<CloudMonitoringQuery>) => CloudMonitoringQuery = (overrides) => {
|
||||
return {
|
||||
datasource: {
|
||||
@ -43,12 +50,12 @@ export const createMockQuery: (overrides?: Subset<CloudMonitoringQuery>) => 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),
|
||||
};
|
||||
};
|
||||
|
@ -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"', () => {
|
||||
|
@ -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,
|
||||
|
@ -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(
|
||||
|
@ -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<SelectableValue<string>>;
|
||||
customMetaData: CustomMetaData;
|
||||
datasource: CloudMonitoringDatasource;
|
||||
metricDescriptor?: MetricDescriptor;
|
||||
preprocessor?: PreprocessorType;
|
||||
}
|
||||
|
||||
export const Alignment: FC<Props> = ({
|
||||
@ -27,6 +29,8 @@ export const Alignment: FC<Props> = ({
|
||||
query,
|
||||
customMetaData,
|
||||
datasource,
|
||||
metricDescriptor,
|
||||
preprocessor,
|
||||
}) => {
|
||||
const alignmentLabel = useMemo(() => alignmentPeriodLabel(customMetaData, datasource), [customMetaData, datasource]);
|
||||
return (
|
||||
@ -39,7 +43,9 @@ export const Alignment: FC<Props> = ({
|
||||
inputId={`${refId}-alignment-function`}
|
||||
templateVariableOptions={templateVariableOptions}
|
||||
query={query}
|
||||
onChange={onChange}
|
||||
onChange={(q) => onChange({ ...query, ...q })}
|
||||
metricDescriptor={metricDescriptor}
|
||||
preprocessor={preprocessor}
|
||||
/>
|
||||
</EditorField>
|
||||
<EditorField label="Alignment period" tooltip={alignmentLabel}>
|
||||
|
@ -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<SelectableValue<string>>;
|
||||
metricDescriptor?: MetricDescriptor;
|
||||
preprocessor?: PreprocessorType;
|
||||
}
|
||||
|
||||
export const AlignmentFunction: FC<Props> = ({ inputId, query, templateVariableOptions, onChange }) => {
|
||||
const { valueType, metricKind, perSeriesAligner: psa, preprocessor } = query;
|
||||
export const AlignmentFunction: FC<Props> = ({
|
||||
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]
|
||||
|
@ -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();
|
||||
|
@ -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<CloudMonitoringDatasource, CloudMonitoringQuery, CloudMonitoringOptions>;
|
||||
|
||||
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<HTMLInputElement>) => {
|
||||
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 (
|
||||
<EditorRows>
|
||||
<>
|
||||
@ -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}
|
||||
/>
|
||||
<EditorField label="Title" htmlFor="annotation-query-title">
|
||||
<Input id="annotation-query-title" value={title} onChange={handleTitleChange} />
|
||||
|
@ -2,7 +2,7 @@ import { render, screen } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import { 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', () => {
|
||||
|
@ -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<string>;
|
||||
labels: string[];
|
||||
metricDescriptor?: MetricDescriptor;
|
||||
onChange: (query: MetricQuery) => void;
|
||||
query: MetricQuery;
|
||||
onChange: (query: TimeSeriesList) => void;
|
||||
query: TimeSeriesList;
|
||||
}
|
||||
|
||||
export const GroupBy: FunctionComponent<Props> = ({
|
||||
|
@ -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(<LabelFilter labels={{}} filters={filters} onChange={() => {}} variableOptionGroup={[]} />);
|
||||
expect(screen.queryByText('metric.type')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('value_1')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('can add filters', async () => {
|
||||
const onChange = jest.fn();
|
||||
render(<LabelFilter labels={{}} filters={[]} onChange={onChange} variableOptionGroup={[]} />);
|
||||
|
@ -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<Props> = ({
|
||||
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<Props> = ({
|
||||
};
|
||||
|
||||
const onChange = (items: Array<Partial<Filter>>) => {
|
||||
const filters = items.map(({ key, operator, value, condition }) => ({
|
||||
const filters = items.concat(protectedFilters).map(({ key, operator, value, condition }) => ({
|
||||
key: key || '',
|
||||
operator: operator || DEFAULT_OPERATOR,
|
||||
value: value || '',
|
||||
|
@ -0,0 +1,58 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
|
||||
import { createMockDatasource } from '../__mocks__/cloudMonitoringDatasource';
|
||||
import { createMockQuery } from '../__mocks__/cloudMonitoringQuery';
|
||||
import { QueryType } from '../types';
|
||||
|
||||
import { MetricQueryEditor } from './MetricQueryEditor';
|
||||
|
||||
jest.mock('@grafana/runtime', () => ({
|
||||
...(jest.requireActual('@grafana/runtime') as unknown as object),
|
||||
getTemplateSrv: () => ({
|
||||
replace: (val: string) => val,
|
||||
}),
|
||||
}));
|
||||
|
||||
const defaultProps = {
|
||||
refId: 'A',
|
||||
customMetaData: {},
|
||||
variableOptionGroup: { options: [] },
|
||||
onChange: jest.fn(),
|
||||
onRunQuery: jest.fn(),
|
||||
query: createMockQuery(),
|
||||
datasource: createMockDatasource(),
|
||||
};
|
||||
|
||||
describe('MetricQueryEditor', () => {
|
||||
it('renders a default time series list query', async () => {
|
||||
const onChange = jest.fn();
|
||||
const query = createMockQuery();
|
||||
// Force to populate with default values
|
||||
delete query.timeSeriesList;
|
||||
|
||||
render(<MetricQueryEditor {...defaultProps} onChange={onChange} query={query} />);
|
||||
expect(onChange).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('renders a default time series query', async () => {
|
||||
const onChange = jest.fn();
|
||||
const query = createMockQuery();
|
||||
// Force to populate with default values
|
||||
delete query.timeSeriesQuery;
|
||||
query.queryType = QueryType.TIME_SERIES_QUERY;
|
||||
|
||||
render(<MetricQueryEditor {...defaultProps} onChange={onChange} query={query} />);
|
||||
expect(onChange).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('renders an annotation query', async () => {
|
||||
const onChange = jest.fn();
|
||||
const query = createMockQuery();
|
||||
query.queryType = QueryType.ANNOTATION;
|
||||
|
||||
render(<MetricQueryEditor {...defaultProps} onChange={onChange} query={query} />);
|
||||
const l = await screen.findByLabelText('Project');
|
||||
expect(l).toBeInTheDocument();
|
||||
});
|
||||
});
|
@ -1,20 +1,16 @@
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import React, { useCallback, useEffect } from 'react';
|
||||
|
||||
import { SelectableValue } from '@grafana/data';
|
||||
import { 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<string>;
|
||||
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<Props>) {
|
||||
const [state, setState] = useState<State>(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 (
|
||||
<EditorRows>
|
||||
{editorMode === EditorMode.Visual && (
|
||||
{[QueryType.TIME_SERIES_LIST, QueryType.ANNOTATION].includes(query.queryType) && query.timeSeriesList && (
|
||||
<VisualMetricQueryEditor
|
||||
refId={refId}
|
||||
labels={state.labels}
|
||||
variableOptionGroup={variableOptionGroup}
|
||||
customMetaData={customMetaData}
|
||||
onMetricTypeChange={onMetricTypeChange}
|
||||
onChange={onChange}
|
||||
onChange={onChangeTimeSeriesList}
|
||||
datasource={datasource}
|
||||
query={query}
|
||||
query={query.timeSeriesList}
|
||||
aliasBy={query.aliasBy}
|
||||
onChangeAliasBy={(aliasBy: string) => onQueryChange({ ...query, aliasBy })}
|
||||
/>
|
||||
)}
|
||||
|
||||
{editorMode === EditorMode.MQL && (
|
||||
{query.queryType === QueryType.TIME_SERIES_QUERY && query.timeSeriesQuery && (
|
||||
<>
|
||||
<MQLQueryEditor
|
||||
onChange={(q: string) => onQueryChange({ ...query, query: q })}
|
||||
onChange={(q: string) => onChangeTimeSeriesQuery({ ...query.timeSeriesQuery!, query: q })}
|
||||
onRunQuery={onRunQuery}
|
||||
query={query.query}
|
||||
query={query.timeSeriesQuery.query}
|
||||
></MQLQueryEditor>
|
||||
<GraphPeriod
|
||||
onChange={(graphPeriod: string) => onQueryChange({ ...query, graphPeriod })}
|
||||
graphPeriod={query.graphPeriod}
|
||||
onChange={(graphPeriod: string) => onChangeTimeSeriesQuery({ ...query.timeSeriesQuery!, graphPeriod })}
|
||||
graphPeriod={query.timeSeriesQuery.graphPeriod}
|
||||
refId={refId}
|
||||
variableOptionGroup={variableOptionGroup}
|
||||
/>
|
||||
|
@ -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(
|
||||
<Metrics
|
||||
|
@ -7,7 +7,7 @@ import { EditorField, EditorFieldGroup, EditorRow } from '@grafana/experimental'
|
||||
import { getSelectStyles, Select, useStyles2, useTheme2 } from '@grafana/ui';
|
||||
|
||||
import CloudMonitoringDatasource from '../datasource';
|
||||
import { MetricDescriptor, MetricQuery } from '../types';
|
||||
import { MetricDescriptor, TimeSeriesList } from '../types';
|
||||
|
||||
import { Project } from './Project';
|
||||
|
||||
@ -18,9 +18,9 @@ export interface Props {
|
||||
datasource: CloudMonitoringDatasource;
|
||||
projectName: string;
|
||||
metricType: string;
|
||||
query: MetricQuery;
|
||||
query: TimeSeriesList;
|
||||
children: (metricDescriptor?: MetricDescriptor) => JSX.Element;
|
||||
onProjectChange: (query: MetricQuery) => void;
|
||||
onProjectChange: (query: TimeSeriesList) => void;
|
||||
}
|
||||
|
||||
export function Metrics(props: Props) {
|
||||
|
@ -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(<Preprocessor onChange={onChange} query={query} />);
|
||||
@ -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 });
|
||||
|
||||
|
@ -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<Props> = ({ query, metricDescriptor, onChange }) => {
|
||||
const options = useOptions(metricDescriptor);
|
||||
|
||||
return (
|
||||
<EditorField
|
||||
label="Pre-processing"
|
||||
@ -24,7 +25,8 @@ export const Preprocessor: FunctionComponent<Props> = ({ query, metricDescriptor
|
||||
>
|
||||
<RadioButtonGroup
|
||||
onChange={(value: PreprocessorType) => {
|
||||
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 });
|
||||
}}
|
||||
|
@ -0,0 +1,46 @@
|
||||
import { render, waitFor } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
|
||||
import { createMockDatasource } from '../__mocks__/cloudMonitoringDatasource';
|
||||
import { createMockQuery } from '../__mocks__/cloudMonitoringQuery';
|
||||
import { QueryType } from '../types';
|
||||
|
||||
import { QueryEditor } from './QueryEditor';
|
||||
|
||||
jest.mock('@grafana/runtime', () => ({
|
||||
...(jest.requireActual('@grafana/runtime') as unknown as object),
|
||||
getTemplateSrv: () => ({
|
||||
replace: (val: string) => val,
|
||||
}),
|
||||
}));
|
||||
|
||||
const defaultProps = {
|
||||
refId: 'A',
|
||||
customMetaData: {},
|
||||
variableOptionGroup: { options: [] },
|
||||
onChange: jest.fn(),
|
||||
onRunQuery: jest.fn(),
|
||||
query: createMockQuery(),
|
||||
datasource: createMockDatasource(),
|
||||
};
|
||||
|
||||
describe('QueryEditor', () => {
|
||||
it('should migrate the given query', async () => {
|
||||
const datasource = createMockDatasource();
|
||||
datasource.migrateQuery = jest.fn().mockReturnValue(defaultProps.query);
|
||||
|
||||
render(<QueryEditor {...defaultProps} datasource={datasource} />);
|
||||
await waitFor(() => expect(datasource.migrateQuery).toHaveBeenCalledTimes(1));
|
||||
});
|
||||
|
||||
it('should set a known query type', async () => {
|
||||
const query = createMockQuery();
|
||||
query.queryType = 'other' as QueryType;
|
||||
const onChange = jest.fn();
|
||||
|
||||
render(<QueryEditor {...defaultProps} query={query} onChange={onChange} />);
|
||||
await waitFor(() =>
|
||||
expect(onChange).toHaveBeenCalledWith(expect.objectContaining({ queryType: QueryType.TIME_SERIES_LIST }))
|
||||
);
|
||||
});
|
||||
});
|
@ -1,12 +1,11 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import { QueryEditorProps, toOption } from '@grafana/data';
|
||||
import { 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<CloudMonitoringDatasource, CloudMonitoringQuery, CloudMonitoringOptions>;
|
||||
|
||||
export class QueryEditor extends PureComponent<Props> {
|
||||
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 (
|
||||
<EditorRows>
|
||||
<QueryHeader
|
||||
query={query}
|
||||
metricQuery={metricQuery}
|
||||
sloQuery={sloQuery}
|
||||
return (
|
||||
<EditorRows>
|
||||
<QueryHeader query={query} onChange={onChange} onRunQuery={onRunQuery} />
|
||||
{queryType !== QueryType.SLO && (
|
||||
<MetricQueryEditor
|
||||
refId={query.refId}
|
||||
variableOptionGroup={variableOptionGroup}
|
||||
customMetaData={customMetaData}
|
||||
onChange={onChange}
|
||||
onRunQuery={onRunQuery}
|
||||
datasource={datasource}
|
||||
query={query}
|
||||
/>
|
||||
{queryType === QueryType.METRICS && (
|
||||
<MetricQueryEditor
|
||||
refId={query.refId}
|
||||
variableOptionGroup={variableOptionGroup}
|
||||
customMetaData={customMetaData}
|
||||
onChange={(metricQuery: MetricQuery) => {
|
||||
this.props.onChange({ ...this.props.query, metricQuery });
|
||||
}}
|
||||
onRunQuery={onRunQuery}
|
||||
datasource={datasource}
|
||||
query={metricQuery}
|
||||
/>
|
||||
)}
|
||||
)}
|
||||
|
||||
{queryType === QueryType.SLO && (
|
||||
<SLOQueryEditor
|
||||
refId={query.refId}
|
||||
variableOptionGroup={variableOptionGroup}
|
||||
customMetaData={customMetaData}
|
||||
onChange={(query: SLOQuery) => this.onQueryChange('sloQuery', query)}
|
||||
onRunQuery={onRunQuery}
|
||||
datasource={datasource}
|
||||
query={sloQuery}
|
||||
/>
|
||||
)}
|
||||
</EditorRows>
|
||||
);
|
||||
}
|
||||
}
|
||||
{queryType === QueryType.SLO && (
|
||||
<SLOQueryEditor
|
||||
refId={query.refId}
|
||||
variableOptionGroup={variableOptionGroup}
|
||||
customMetaData={customMetaData}
|
||||
onChange={onSLOQueryChange}
|
||||
onRunQuery={onRunQuery}
|
||||
datasource={datasource}
|
||||
query={sloQuery}
|
||||
aliasBy={query.aliasBy}
|
||||
onChangeAliasBy={(aliasBy: string) => onChange({ ...query, aliasBy })}
|
||||
/>
|
||||
)}
|
||||
</EditorRows>
|
||||
);
|
||||
};
|
||||
|
@ -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(
|
||||
<QueryHeader
|
||||
query={query}
|
||||
metricQuery={metricQuery}
|
||||
sloQuery={sloQuery}
|
||||
onChange={onChange}
|
||||
onRunQuery={onRunQuery}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByLabelText(/Query type/)).toBeInTheDocument();
|
||||
expect(screen.getByLabelText('Builder')).toBeInTheDocument();
|
||||
expect(screen.getByLabelText('MQL')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('does not render an editor mode radio group if query type is a SLO query', () => {
|
||||
const query = createMockQuery({ queryType: QueryType.SLO });
|
||||
const { metricQuery } = query;
|
||||
const sloQuery = createMockSLOQuery();
|
||||
const onChange = jest.fn();
|
||||
const onRunQuery = jest.fn();
|
||||
|
||||
render(
|
||||
<QueryHeader
|
||||
query={query}
|
||||
metricQuery={metricQuery}
|
||||
sloQuery={sloQuery}
|
||||
onChange={onChange}
|
||||
onRunQuery={onRunQuery}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByLabelText(/Query type/)).toBeInTheDocument();
|
||||
expect(screen.queryByLabelText('Builder')).not.toBeInTheDocument();
|
||||
expect(screen.queryByLabelText('MQL')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('can change query types', async () => {
|
||||
const query = createMockQuery();
|
||||
const { metricQuery } = query;
|
||||
const sloQuery = createMockSLOQuery();
|
||||
const onChange = jest.fn();
|
||||
const onRunQuery = jest.fn();
|
||||
|
||||
render(
|
||||
<QueryHeader
|
||||
query={query}
|
||||
metricQuery={metricQuery}
|
||||
sloQuery={sloQuery}
|
||||
onChange={onChange}
|
||||
onRunQuery={onRunQuery}
|
||||
/>
|
||||
);
|
||||
render(<QueryHeader query={query} onChange={onChange} onRunQuery={onRunQuery} />);
|
||||
|
||||
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(
|
||||
<QueryHeader
|
||||
query={query}
|
||||
metricQuery={metricQuery}
|
||||
sloQuery={sloQuery}
|
||||
onChange={onChange}
|
||||
onRunQuery={onRunQuery}
|
||||
/>
|
||||
);
|
||||
render(<QueryHeader query={query} onChange={onChange} onRunQuery={onRunQuery} />);
|
||||
|
||||
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 }));
|
||||
});
|
||||
});
|
||||
|
@ -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 (
|
||||
<EditorHeader>
|
||||
@ -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();
|
||||
}}
|
||||
/>
|
||||
<FlexItem grow={1} />
|
||||
{queryType !== QueryType.SLO && (
|
||||
<RadioButtonGroup
|
||||
size="sm"
|
||||
options={EDITOR_MODES}
|
||||
value={editorMode || EditorMode.Visual}
|
||||
onChange={(value) => {
|
||||
onChange({
|
||||
...query,
|
||||
metricQuery: {
|
||||
...metricQuery,
|
||||
editorMode: value,
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</EditorHeader>
|
||||
);
|
||||
};
|
||||
|
@ -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<Props>) {
|
||||
const alignmentLabel = useMemo(() => alignmentPeriodLabel(customMetaData, datasource), [customMetaData, datasource]);
|
||||
return (
|
||||
@ -100,7 +104,7 @@ export function SLOQueryEditor({
|
||||
</EditorField>
|
||||
</EditorFieldGroup>
|
||||
|
||||
<AliasBy refId={refId} value={query.aliasBy} onChange={(aliasBy) => onChange({ ...query, aliasBy })} />
|
||||
<AliasBy refId={refId} value={aliasBy} onChange={onChangeAliasBy} />
|
||||
</EditorRow>
|
||||
</>
|
||||
);
|
||||
|
@ -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<string>;
|
||||
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<Props>) {
|
||||
const [labels, setLabels] = useState<{ [k: string]: any }>({});
|
||||
const { projectName, groupBys, crossSeriesReducer } = query;
|
||||
const metricType = getMetricType(query);
|
||||
|
||||
useEffect(() => {
|
||||
if (projectName && metricType) {
|
||||
datasource.getLabels(metricType, refId, projectName).then((labels) => setLabels(labels));
|
||||
}
|
||||
}, [datasource, groupBys, metricType, projectName, refId, crossSeriesReducer]);
|
||||
|
||||
const onMetricTypeChange = useCallback(
|
||||
({ valueType, metricKind, type }: MetricDescriptor) => {
|
||||
const preprocessor =
|
||||
metricKind === MetricKind.GAUGE || valueType === ValueTypes.DISTRIBUTION
|
||||
? PreprocessorType.None
|
||||
: PreprocessorType.Rate;
|
||||
const { perSeriesAligner } = getAlignmentPickerData(valueType, metricKind, query.perSeriesAligner, preprocessor);
|
||||
onChange({
|
||||
...setMetricType(
|
||||
{
|
||||
...query,
|
||||
perSeriesAligner,
|
||||
},
|
||||
type
|
||||
),
|
||||
preprocessor,
|
||||
});
|
||||
},
|
||||
[onChange, query]
|
||||
);
|
||||
|
||||
return (
|
||||
<Metrics
|
||||
refId={refId}
|
||||
projectName={query.projectName}
|
||||
metricType={query.metricType}
|
||||
metricType={metricType}
|
||||
templateVariableOptions={variableOptionGroup.options}
|
||||
datasource={datasource}
|
||||
onChange={onMetricTypeChange}
|
||||
@ -70,14 +102,10 @@ function Editor({
|
||||
query={query}
|
||||
customMetaData={customMetaData}
|
||||
onChange={onChange}
|
||||
metricDescriptor={metric}
|
||||
preprocessor={query.preprocessor}
|
||||
/>
|
||||
<AliasBy
|
||||
refId={refId}
|
||||
value={query.aliasBy}
|
||||
onChange={(aliasBy) => {
|
||||
onChange({ ...query, aliasBy });
|
||||
}}
|
||||
/>
|
||||
<AliasBy refId={refId} value={aliasBy} onChange={onChangeAliasBy} />
|
||||
</EditorRow>
|
||||
</>
|
||||
)}
|
||||
|
@ -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 },
|
||||
];
|
||||
|
@ -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',
|
||||
},
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -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<CloudMonitoringOptions>,
|
||||
@ -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<string, any> {
|
||||
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<CloudMonitoringQuery>;
|
||||
};
|
||||
|
||||
const queries = options.targets;
|
||||
|
||||
@ -107,7 +119,7 @@ export default class CloudMonitoringDatasource extends DataSourceWithBackend<
|
||||
return lastValueFrom(
|
||||
from(this.ensureGCEDefaultProject()).pipe(
|
||||
mergeMap(() => {
|
||||
return getBackendSrv().fetch<PostResponse>({
|
||||
return this.backendSrv.fetch<PostResponse>({
|
||||
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[] {
|
||||
|
@ -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']);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -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;
|
||||
};
|
||||
|
@ -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',
|
||||
});
|
||||
|
@ -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;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user