mirror of
https://github.com/grafana/grafana.git
synced 2025-09-28 12:53:47 +08:00
Cloud Monitoring: Use new annotation API (#49026)
* remove angular code * format annotation on backend * format time with time type instead of string * update annotation query tests * update get alignment data function * update annotation query editor * add annotation query editor test * update struct * add tests * remove extracted function * remove non-null assertion * remove stray commented out console.log * fix jest haste map warning * add alignment period * add AnnotationMetricQuery type
This commit is contained in:
@ -4,11 +4,19 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||||
"github.com/grafana/grafana-plugin-sdk-go/data"
|
"github.com/grafana/grafana-plugin-sdk-go/data"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type annotationEvent struct {
|
||||||
|
Title string
|
||||||
|
Time time.Time
|
||||||
|
Tags string
|
||||||
|
Text string
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Service) executeAnnotationQuery(ctx context.Context, req *backend.QueryDataRequest, dsInfo datasourceInfo) (
|
func (s *Service) executeAnnotationQuery(ctx context.Context, req *backend.QueryDataRequest, dsInfo datasourceInfo) (
|
||||||
*backend.QueryDataResponse, error) {
|
*backend.QueryDataResponse, error) {
|
||||||
resp := backend.NewQueryDataResponse()
|
resp := backend.NewQueryDataResponse()
|
||||||
@ -24,8 +32,10 @@ func (s *Service) executeAnnotationQuery(ctx context.Context, req *backend.Query
|
|||||||
}
|
}
|
||||||
|
|
||||||
mq := struct {
|
mq := struct {
|
||||||
Title string `json:"title"`
|
MetricQuery struct {
|
||||||
Text string `json:"text"`
|
Title string `json:"title"`
|
||||||
|
Text string `json:"text"`
|
||||||
|
} `json:"metricQuery"`
|
||||||
}{}
|
}{}
|
||||||
|
|
||||||
firstQuery := req.Queries[0]
|
firstQuery := req.Queries[0]
|
||||||
@ -33,32 +43,23 @@ func (s *Service) executeAnnotationQuery(ctx context.Context, req *backend.Query
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return resp, nil
|
return resp, nil
|
||||||
}
|
}
|
||||||
err = queries[0].parseToAnnotations(queryRes, dr, mq.Title, mq.Text)
|
err = queries[0].parseToAnnotations(queryRes, dr, mq.MetricQuery.Title, mq.MetricQuery.Text)
|
||||||
resp.Responses[firstQuery.RefID] = *queryRes
|
resp.Responses[firstQuery.RefID] = *queryRes
|
||||||
|
|
||||||
return resp, err
|
return resp, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (timeSeriesQuery cloudMonitoringTimeSeriesQuery) transformAnnotationToFrame(annotations []map[string]string, result *backend.DataResponse) {
|
func (timeSeriesQuery cloudMonitoringTimeSeriesQuery) transformAnnotationToFrame(annotations []*annotationEvent, result *backend.DataResponse) {
|
||||||
frames := data.Frames{}
|
frame := data.NewFrame(timeSeriesQuery.RefID,
|
||||||
|
data.NewField("time", nil, []time.Time{}),
|
||||||
|
data.NewField("title", nil, []string{}),
|
||||||
|
data.NewField("tags", nil, []string{}),
|
||||||
|
data.NewField("text", nil, []string{}),
|
||||||
|
)
|
||||||
for _, a := range annotations {
|
for _, a := range annotations {
|
||||||
frame := &data.Frame{
|
frame.AppendRow(a.Time, a.Title, a.Tags, a.Text)
|
||||||
RefID: timeSeriesQuery.getRefID(),
|
|
||||||
Fields: []*data.Field{
|
|
||||||
data.NewField("time", nil, a["time"]),
|
|
||||||
data.NewField("title", nil, a["title"]),
|
|
||||||
data.NewField("tags", nil, a["tags"]),
|
|
||||||
data.NewField("text", nil, a["text"]),
|
|
||||||
},
|
|
||||||
Meta: &data.FrameMeta{
|
|
||||||
Custom: map[string]interface{}{
|
|
||||||
"rowCount": len(a),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
frames = append(frames, frame)
|
|
||||||
}
|
}
|
||||||
result.Frames = frames
|
result.Frames = append(result.Frames, frame)
|
||||||
slog.Info("anno", "len", len(annotations))
|
slog.Info("anno", "len", len(annotations))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -20,10 +20,15 @@ func TestExecutor_parseToAnnotations(t *testing.T) {
|
|||||||
"atext {{resource.label.zone}}")
|
"atext {{resource.label.zone}}")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
require.Len(t, res.Frames, 3)
|
require.Len(t, res.Frames, 1)
|
||||||
|
assert.Equal(t, "time", res.Frames[0].Fields[0].Name)
|
||||||
assert.Equal(t, "title", res.Frames[0].Fields[1].Name)
|
assert.Equal(t, "title", res.Frames[0].Fields[1].Name)
|
||||||
assert.Equal(t, "tags", res.Frames[0].Fields[2].Name)
|
assert.Equal(t, "tags", res.Frames[0].Fields[2].Name)
|
||||||
assert.Equal(t, "text", res.Frames[0].Fields[3].Name)
|
assert.Equal(t, "text", res.Frames[0].Fields[3].Name)
|
||||||
|
assert.Equal(t, 9, res.Frames[0].Fields[0].Len())
|
||||||
|
assert.Equal(t, 9, res.Frames[0].Fields[1].Len())
|
||||||
|
assert.Equal(t, 9, res.Frames[0].Fields[2].Len())
|
||||||
|
assert.Equal(t, 9, res.Frames[0].Fields[3].Len())
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCloudMonitoringExecutor_parseToAnnotations_emptyTimeSeries(t *testing.T) {
|
func TestCloudMonitoringExecutor_parseToAnnotations_emptyTimeSeries(t *testing.T) {
|
||||||
@ -37,7 +42,15 @@ func TestCloudMonitoringExecutor_parseToAnnotations_emptyTimeSeries(t *testing.T
|
|||||||
err := query.parseToAnnotations(res, response, "atitle", "atext")
|
err := query.parseToAnnotations(res, response, "atitle", "atext")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
require.Len(t, res.Frames, 0)
|
require.Len(t, res.Frames, 1)
|
||||||
|
assert.Equal(t, "time", res.Frames[0].Fields[0].Name)
|
||||||
|
assert.Equal(t, "title", res.Frames[0].Fields[1].Name)
|
||||||
|
assert.Equal(t, "tags", res.Frames[0].Fields[2].Name)
|
||||||
|
assert.Equal(t, "text", res.Frames[0].Fields[3].Name)
|
||||||
|
assert.Equal(t, 0, res.Frames[0].Fields[0].Len())
|
||||||
|
assert.Equal(t, 0, res.Frames[0].Fields[1].Len())
|
||||||
|
assert.Equal(t, 0, res.Frames[0].Fields[2].Len())
|
||||||
|
assert.Equal(t, 0, res.Frames[0].Fields[3].Len())
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCloudMonitoringExecutor_parseToAnnotations_noPointsInSeries(t *testing.T) {
|
func TestCloudMonitoringExecutor_parseToAnnotations_noPointsInSeries(t *testing.T) {
|
||||||
@ -53,5 +66,13 @@ func TestCloudMonitoringExecutor_parseToAnnotations_noPointsInSeries(t *testing.
|
|||||||
err := query.parseToAnnotations(res, response, "atitle", "atext")
|
err := query.parseToAnnotations(res, response, "atitle", "atext")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
require.Len(t, res.Frames, 0)
|
require.Len(t, res.Frames, 1)
|
||||||
|
assert.Equal(t, "time", res.Frames[0].Fields[0].Name)
|
||||||
|
assert.Equal(t, "title", res.Frames[0].Fields[1].Name)
|
||||||
|
assert.Equal(t, "tags", res.Frames[0].Fields[2].Name)
|
||||||
|
assert.Equal(t, "text", res.Frames[0].Fields[3].Name)
|
||||||
|
assert.Equal(t, 0, res.Frames[0].Fields[0].Len())
|
||||||
|
assert.Equal(t, 0, res.Frames[0].Fields[1].Len())
|
||||||
|
assert.Equal(t, 0, res.Frames[0].Fields[2].Len())
|
||||||
|
assert.Equal(t, 0, res.Frames[0].Fields[3].Len())
|
||||||
}
|
}
|
||||||
|
@ -250,33 +250,36 @@ func (timeSeriesFilter *cloudMonitoringTimeSeriesFilter) handleNonDistributionSe
|
|||||||
|
|
||||||
func (timeSeriesFilter *cloudMonitoringTimeSeriesFilter) parseToAnnotations(dr *backend.DataResponse,
|
func (timeSeriesFilter *cloudMonitoringTimeSeriesFilter) parseToAnnotations(dr *backend.DataResponse,
|
||||||
response cloudMonitoringResponse, title, text string) error {
|
response cloudMonitoringResponse, title, text string) error {
|
||||||
frames := data.Frames{}
|
frame := data.NewFrame(timeSeriesFilter.RefID,
|
||||||
|
data.NewField("time", nil, []time.Time{}),
|
||||||
|
data.NewField("title", nil, []string{}),
|
||||||
|
data.NewField("tags", nil, []string{}),
|
||||||
|
data.NewField("text", nil, []string{}),
|
||||||
|
)
|
||||||
|
|
||||||
for _, series := range response.TimeSeries {
|
for _, series := range response.TimeSeries {
|
||||||
if len(series.Points) == 0 {
|
if len(series.Points) == 0 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
annotation := make(map[string][]string)
|
|
||||||
for i := len(series.Points) - 1; i >= 0; i-- {
|
for i := len(series.Points) - 1; i >= 0; i-- {
|
||||||
point := series.Points[i]
|
point := series.Points[i]
|
||||||
value := strconv.FormatFloat(point.Value.DoubleValue, 'f', 6, 64)
|
value := strconv.FormatFloat(point.Value.DoubleValue, 'f', 6, 64)
|
||||||
if series.ValueType == "STRING" {
|
if series.ValueType == "STRING" {
|
||||||
value = point.Value.StringValue
|
value = point.Value.StringValue
|
||||||
}
|
}
|
||||||
annotation["time"] = append(annotation["time"], point.Interval.EndTime.UTC().Format(time.RFC3339))
|
annotation := &annotationEvent{
|
||||||
annotation["title"] = append(annotation["title"], formatAnnotationText(title, value, series.Metric.Type,
|
Time: point.Interval.EndTime,
|
||||||
series.Metric.Labels, series.Resource.Labels))
|
Title: formatAnnotationText(title, value, series.Metric.Type,
|
||||||
annotation["tags"] = append(annotation["tags"], "")
|
series.Metric.Labels, series.Resource.Labels),
|
||||||
annotation["text"] = append(annotation["text"], formatAnnotationText(text, value, series.Metric.Type,
|
Tags: "",
|
||||||
series.Metric.Labels, series.Resource.Labels))
|
Text: formatAnnotationText(text, value, series.Metric.Type,
|
||||||
|
series.Metric.Labels, series.Resource.Labels),
|
||||||
|
}
|
||||||
|
frame.AppendRow(annotation.Time, annotation.Title, annotation.Tags, annotation.Text)
|
||||||
}
|
}
|
||||||
frames = append(frames, data.NewFrame(timeSeriesFilter.getRefID(),
|
|
||||||
data.NewField("time", nil, annotation["time"]),
|
|
||||||
data.NewField("title", nil, annotation["title"]),
|
|
||||||
data.NewField("tags", nil, annotation["tags"]),
|
|
||||||
data.NewField("text", nil, annotation["text"]),
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
dr.Frames = frames
|
dr.Frames = append(dr.Frames, frame)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -266,7 +266,7 @@ func (timeSeriesQuery cloudMonitoringTimeSeriesQuery) parseResponse(queryRes *ba
|
|||||||
|
|
||||||
func (timeSeriesQuery cloudMonitoringTimeSeriesQuery) parseToAnnotations(queryRes *backend.DataResponse,
|
func (timeSeriesQuery cloudMonitoringTimeSeriesQuery) parseToAnnotations(queryRes *backend.DataResponse,
|
||||||
data cloudMonitoringResponse, title, text string) error {
|
data cloudMonitoringResponse, title, text string) error {
|
||||||
annotations := make([]map[string]string, 0)
|
annotations := make([]*annotationEvent, 0)
|
||||||
|
|
||||||
for _, series := range data.TimeSeriesData {
|
for _, series := range data.TimeSeriesData {
|
||||||
metricLabels := make(map[string]string)
|
metricLabels := make(map[string]string)
|
||||||
@ -302,12 +302,12 @@ func (timeSeriesQuery cloudMonitoringTimeSeriesQuery) parseToAnnotations(queryRe
|
|||||||
if d.ValueType == "STRING" {
|
if d.ValueType == "STRING" {
|
||||||
value = point.Values[n].StringValue
|
value = point.Values[n].StringValue
|
||||||
}
|
}
|
||||||
annotation := make(map[string]string)
|
annotations = append(annotations, &annotationEvent{
|
||||||
annotation["time"] = point.TimeInterval.EndTime.UTC().Format(time.RFC3339)
|
Time: point.TimeInterval.EndTime,
|
||||||
annotation["title"] = formatAnnotationText(title, value, d.MetricKind, metricLabels, resourceLabels)
|
Title: formatAnnotationText(title, value, d.MetricKind, metricLabels, resourceLabels),
|
||||||
annotation["tags"] = ""
|
Tags: "",
|
||||||
annotation["text"] = formatAnnotationText(text, value, d.MetricKind, metricLabels, resourceLabels)
|
Text: formatAnnotationText(text, value, d.MetricKind, metricLabels, resourceLabels),
|
||||||
annotations = append(annotations, annotation)
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -13,7 +13,6 @@ import {
|
|||||||
import { react2AngularDirective } from 'app/angular/react2angular';
|
import { react2AngularDirective } from 'app/angular/react2angular';
|
||||||
import { FolderPicker } from 'app/core/components/Select/FolderPicker';
|
import { FolderPicker } from 'app/core/components/Select/FolderPicker';
|
||||||
import { TimePickerSettings } from 'app/features/dashboard/components/DashboardSettings/TimePickerSettings';
|
import { TimePickerSettings } from 'app/features/dashboard/components/DashboardSettings/TimePickerSettings';
|
||||||
import { AnnotationQueryEditor as CloudMonitoringAnnotationQueryEditor } from 'app/plugins/datasource/cloud-monitoring/components/AnnotationQueryEditor';
|
|
||||||
import { QueryEditor as CloudMonitoringQueryEditor } from 'app/plugins/datasource/cloud-monitoring/components/QueryEditor';
|
import { QueryEditor as CloudMonitoringQueryEditor } from 'app/plugins/datasource/cloud-monitoring/components/QueryEditor';
|
||||||
|
|
||||||
import EmptyListCTA from '../core/components/EmptyListCTA/EmptyListCTA';
|
import EmptyListCTA from '../core/components/EmptyListCTA/EmptyListCTA';
|
||||||
@ -117,12 +116,6 @@ export function registerAngularDirectives() {
|
|||||||
['datasource', { watchDepth: 'reference' }],
|
['datasource', { watchDepth: 'reference' }],
|
||||||
['templateSrv', { watchDepth: 'reference' }],
|
['templateSrv', { watchDepth: 'reference' }],
|
||||||
]);
|
]);
|
||||||
react2AngularDirective('cloudMonitoringAnnotationQueryEditor', CloudMonitoringAnnotationQueryEditor, [
|
|
||||||
'target',
|
|
||||||
'onQueryChange',
|
|
||||||
['datasource', { watchDepth: 'reference' }],
|
|
||||||
['templateSrv', { watchDepth: 'reference' }],
|
|
||||||
]);
|
|
||||||
react2AngularDirective('secretFormField', SecretFormField, [
|
react2AngularDirective('secretFormField', SecretFormField, [
|
||||||
'value',
|
'value',
|
||||||
'isConfigured',
|
'isConfigured',
|
||||||
|
@ -0,0 +1,13 @@
|
|||||||
|
import Datasource from '../datasource';
|
||||||
|
|
||||||
|
export const createMockDatasource = () => {
|
||||||
|
const datasource: Partial<Datasource> = {
|
||||||
|
intervalMs: 0,
|
||||||
|
getVariables: jest.fn().mockReturnValue([]),
|
||||||
|
getMetricTypes: jest.fn().mockResolvedValue([]),
|
||||||
|
getProjects: jest.fn().mockResolvedValue([]),
|
||||||
|
getDefaultProject: jest.fn().mockReturnValue('cloud-monitoring-default-project'),
|
||||||
|
};
|
||||||
|
|
||||||
|
return jest.mocked(datasource as Datasource, true);
|
||||||
|
};
|
@ -0,0 +1,21 @@
|
|||||||
|
import { CloudMonitoringQuery, EditorMode, MetricQuery, QueryType } from '../types';
|
||||||
|
|
||||||
|
export const createMockMetricQuery: () => MetricQuery = () => {
|
||||||
|
return {
|
||||||
|
editorMode: EditorMode.Visual,
|
||||||
|
metricType: '',
|
||||||
|
crossSeriesReducer: 'REDUCE_NONE',
|
||||||
|
query: '',
|
||||||
|
projectName: 'cloud-monitoring-default-project',
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const createMockQuery: () => CloudMonitoringQuery = () => {
|
||||||
|
return {
|
||||||
|
refId: 'cloudMonitoringRefId',
|
||||||
|
queryType: QueryType.METRICS,
|
||||||
|
intervalMs: 0,
|
||||||
|
type: 'timeSeriesQuery',
|
||||||
|
metricQuery: createMockMetricQuery(),
|
||||||
|
};
|
||||||
|
};
|
@ -0,0 +1,114 @@
|
|||||||
|
import { AnnotationQuery } from '@grafana/data';
|
||||||
|
|
||||||
|
import { createMockDatasource } from './__mocks__/cloudMonitoringDatasource';
|
||||||
|
import { CloudMonitoringAnnotationSupport } from './annotationSupport';
|
||||||
|
import {
|
||||||
|
AlignmentTypes,
|
||||||
|
CloudMonitoringQuery,
|
||||||
|
EditorMode,
|
||||||
|
LegacyCloudMonitoringAnnotationQuery,
|
||||||
|
MetricKind,
|
||||||
|
QueryType,
|
||||||
|
} from './types';
|
||||||
|
|
||||||
|
const query: CloudMonitoringQuery = {
|
||||||
|
refId: 'query',
|
||||||
|
queryType: QueryType.METRICS,
|
||||||
|
type: 'annotationQuery',
|
||||||
|
intervalMs: 0,
|
||||||
|
metricQuery: {
|
||||||
|
editorMode: EditorMode.Visual,
|
||||||
|
projectName: 'project-name',
|
||||||
|
metricType: '',
|
||||||
|
filters: [],
|
||||||
|
metricKind: MetricKind.GAUGE,
|
||||||
|
valueType: '',
|
||||||
|
title: '',
|
||||||
|
text: '',
|
||||||
|
query: '',
|
||||||
|
crossSeriesReducer: 'REDUCE_NONE',
|
||||||
|
perSeriesAligner: AlignmentTypes.ALIGN_NONE,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const legacyQuery: LegacyCloudMonitoringAnnotationQuery = {
|
||||||
|
projectName: 'project-name',
|
||||||
|
metricType: 'metric-type',
|
||||||
|
filters: ['filter1', 'filter2'],
|
||||||
|
metricKind: MetricKind.CUMULATIVE,
|
||||||
|
valueType: 'value-type',
|
||||||
|
refId: 'annotationQuery',
|
||||||
|
title: 'title',
|
||||||
|
text: 'text',
|
||||||
|
};
|
||||||
|
|
||||||
|
const annotationQuery: AnnotationQuery<CloudMonitoringQuery> = {
|
||||||
|
name: 'Anno',
|
||||||
|
enable: false,
|
||||||
|
iconColor: '',
|
||||||
|
target: query,
|
||||||
|
};
|
||||||
|
|
||||||
|
const legacyAnnotationQuery: AnnotationQuery<LegacyCloudMonitoringAnnotationQuery> = {
|
||||||
|
name: 'Anno',
|
||||||
|
enable: false,
|
||||||
|
iconColor: '',
|
||||||
|
target: legacyQuery,
|
||||||
|
};
|
||||||
|
|
||||||
|
const ds = createMockDatasource();
|
||||||
|
const annotationSupport = CloudMonitoringAnnotationSupport(ds);
|
||||||
|
|
||||||
|
describe('CloudMonitoringAnnotationSupport', () => {
|
||||||
|
describe('prepareAnnotation', () => {
|
||||||
|
it('returns query if it is already a Cloud Monitoring annotation query', () => {
|
||||||
|
expect(annotationSupport.prepareAnnotation?.(annotationQuery)).toBe(annotationQuery);
|
||||||
|
});
|
||||||
|
it('returns an updated query if it is a legacy Cloud Monitoring annotation query', () => {
|
||||||
|
const expectedQuery = {
|
||||||
|
datasource: undefined,
|
||||||
|
enable: false,
|
||||||
|
iconColor: '',
|
||||||
|
name: 'Anno',
|
||||||
|
target: {
|
||||||
|
intervalMs: 0,
|
||||||
|
metricQuery: {
|
||||||
|
crossSeriesReducer: 'REDUCE_NONE',
|
||||||
|
editorMode: 'visual',
|
||||||
|
filters: ['filter1', 'filter2'],
|
||||||
|
metricKind: 'CUMULATIVE',
|
||||||
|
metricType: 'metric-type',
|
||||||
|
perSeriesAligner: 'ALIGN_NONE',
|
||||||
|
projectName: 'project-name',
|
||||||
|
query: '',
|
||||||
|
text: 'text',
|
||||||
|
title: 'title',
|
||||||
|
},
|
||||||
|
queryType: 'metrics',
|
||||||
|
refId: 'annotationQuery',
|
||||||
|
type: 'annotationQuery',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
expect(annotationSupport.prepareAnnotation?.(legacyAnnotationQuery)).toEqual(expectedQuery);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('prepareQuery', () => {
|
||||||
|
it('should ensure queryType is set to "metrics"', () => {
|
||||||
|
const queryWithoutMetricsQueryType = { ...annotationQuery, queryType: 'blah' };
|
||||||
|
expect(annotationSupport.prepareQuery?.(queryWithoutMetricsQueryType)).toEqual(
|
||||||
|
expect.objectContaining({ queryType: 'metrics' })
|
||||||
|
);
|
||||||
|
});
|
||||||
|
it('should ensure type is set "annotationQuery"', () => {
|
||||||
|
const queryWithoutAnnotationQueryType = { ...annotationQuery, type: 'blah' };
|
||||||
|
expect(annotationSupport.prepareQuery?.(queryWithoutAnnotationQueryType)).toEqual(
|
||||||
|
expect.objectContaining({ type: 'annotationQuery' })
|
||||||
|
);
|
||||||
|
});
|
||||||
|
it('should return undefined if there is no query', () => {
|
||||||
|
const queryWithUndefinedTarget = { ...annotationQuery, target: undefined };
|
||||||
|
expect(annotationSupport.prepareQuery?.(queryWithUndefinedTarget)).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,78 @@
|
|||||||
|
import { AnnotationSupport, AnnotationQuery } from '@grafana/data';
|
||||||
|
|
||||||
|
import { AnnotationQueryEditor } from './components/AnnotationQueryEditor';
|
||||||
|
import CloudMonitoringDatasource from './datasource';
|
||||||
|
import {
|
||||||
|
AlignmentTypes,
|
||||||
|
CloudMonitoringQuery,
|
||||||
|
EditorMode,
|
||||||
|
LegacyCloudMonitoringAnnotationQuery,
|
||||||
|
MetricKind,
|
||||||
|
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,
|
||||||
|
// then it is a legacy query.
|
||||||
|
const isLegacyCloudMonitoringAnnotation = (
|
||||||
|
query: unknown
|
||||||
|
): query is AnnotationQuery<LegacyCloudMonitoringAnnotationQuery> =>
|
||||||
|
(query as AnnotationQuery<LegacyCloudMonitoringAnnotationQuery>).target?.title !== undefined ||
|
||||||
|
(query as AnnotationQuery<LegacyCloudMonitoringAnnotationQuery>).target?.text !== undefined;
|
||||||
|
|
||||||
|
export const CloudMonitoringAnnotationSupport: (
|
||||||
|
ds: CloudMonitoringDatasource
|
||||||
|
) => AnnotationSupport<CloudMonitoringQuery> = (ds: CloudMonitoringDatasource) => {
|
||||||
|
return {
|
||||||
|
prepareAnnotation: (
|
||||||
|
query: AnnotationQuery<LegacyCloudMonitoringAnnotationQuery> | AnnotationQuery<CloudMonitoringQuery>
|
||||||
|
): AnnotationQuery<CloudMonitoringQuery> => {
|
||||||
|
if (!isLegacyCloudMonitoringAnnotation(query)) {
|
||||||
|
return query;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { enable, name, iconColor } = query;
|
||||||
|
const { target } = query;
|
||||||
|
const result: AnnotationQuery<CloudMonitoringQuery> = {
|
||||||
|
datasource: query.datasource,
|
||||||
|
enable,
|
||||||
|
name,
|
||||||
|
iconColor,
|
||||||
|
target: {
|
||||||
|
intervalMs: ds.intervalMs,
|
||||||
|
refId: target?.refId || 'annotationQuery',
|
||||||
|
type: 'annotationQuery',
|
||||||
|
queryType: QueryType.METRICS,
|
||||||
|
metricQuery: {
|
||||||
|
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 || '',
|
||||||
|
text: target?.text || '',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
return result;
|
||||||
|
},
|
||||||
|
prepareQuery: (anno: AnnotationQuery<CloudMonitoringQuery>) => {
|
||||||
|
if (!anno.target) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...anno.target,
|
||||||
|
queryType: QueryType.METRICS,
|
||||||
|
type: 'annotationQuery',
|
||||||
|
metricQuery: {
|
||||||
|
...anno.target.metricQuery,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
QueryEditor: AnnotationQueryEditor,
|
||||||
|
};
|
||||||
|
};
|
@ -1,18 +0,0 @@
|
|||||||
import { AnnotationTarget } from './types';
|
|
||||||
|
|
||||||
export class CloudMonitoringAnnotationsQueryCtrl {
|
|
||||||
static templateUrl = 'partials/annotations.editor.html';
|
|
||||||
declare annotation: any;
|
|
||||||
|
|
||||||
/** @ngInject */
|
|
||||||
constructor($scope: any) {
|
|
||||||
this.annotation = $scope.ctrl.annotation || {};
|
|
||||||
this.annotation.target = $scope.ctrl.annotation.target || {};
|
|
||||||
|
|
||||||
this.onQueryChange = this.onQueryChange.bind(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
onQueryChange(target: AnnotationTarget) {
|
|
||||||
Object.assign(this.annotation.target, target);
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,54 @@
|
|||||||
|
import { render, screen } from '@testing-library/react';
|
||||||
|
import userEvent from '@testing-library/user-event';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { createMockDatasource } from '../__mocks__/cloudMonitoringDatasource';
|
||||||
|
import { createMockQuery } from '../__mocks__/cloudMonitoringQuery';
|
||||||
|
|
||||||
|
import { AnnotationQueryEditor } from './AnnotationQueryEditor';
|
||||||
|
|
||||||
|
describe('AnnotationQueryEditor', () => {
|
||||||
|
it('renders correctly', async () => {
|
||||||
|
const onChange = jest.fn();
|
||||||
|
const onRunQuery = jest.fn();
|
||||||
|
const datasource = createMockDatasource();
|
||||||
|
const query = createMockQuery();
|
||||||
|
render(<AnnotationQueryEditor onChange={onChange} onRunQuery={onRunQuery} query={query} datasource={datasource} />);
|
||||||
|
|
||||||
|
expect(await screen.findByLabelText('Project')).toBeInTheDocument();
|
||||||
|
expect(await screen.findByLabelText('Service')).toBeInTheDocument();
|
||||||
|
expect(await screen.findByLabelText('Metric name')).toBeInTheDocument();
|
||||||
|
expect(await screen.findByLabelText('Group by')).toBeInTheDocument();
|
||||||
|
expect(await screen.findByLabelText('Group by function')).toBeInTheDocument();
|
||||||
|
expect(await screen.findByLabelText('Alignment function')).toBeInTheDocument();
|
||||||
|
expect(await screen.findByLabelText('Alignment period')).toBeInTheDocument();
|
||||||
|
expect(await screen.findByLabelText('Alias by')).toBeInTheDocument();
|
||||||
|
expect(await screen.findByLabelText('Title')).toBeInTheDocument();
|
||||||
|
expect(await screen.findByLabelText('Text')).toBeInTheDocument();
|
||||||
|
expect(await screen.findByText('Annotation Query Format')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can set the title', async () => {
|
||||||
|
const onChange = jest.fn();
|
||||||
|
const onRunQuery = jest.fn();
|
||||||
|
const datasource = createMockDatasource();
|
||||||
|
const query = createMockQuery();
|
||||||
|
render(<AnnotationQueryEditor onChange={onChange} onRunQuery={onRunQuery} query={query} datasource={datasource} />);
|
||||||
|
|
||||||
|
const title = 'user-title';
|
||||||
|
await userEvent.type(screen.getByLabelText('Title'), title);
|
||||||
|
expect(await screen.findByDisplayValue(title)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can set the text', async () => {
|
||||||
|
const onChange = jest.fn();
|
||||||
|
const onRunQuery = jest.fn();
|
||||||
|
const datasource = createMockDatasource();
|
||||||
|
const query = createMockQuery();
|
||||||
|
render(<AnnotationQueryEditor onChange={onChange} onRunQuery={onRunQuery} query={query} datasource={datasource} />);
|
||||||
|
|
||||||
|
const text = 'user-text';
|
||||||
|
await userEvent.type(screen.getByLabelText('Text'), text);
|
||||||
|
expect(await screen.findByDisplayValue(text)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
@ -1,34 +1,29 @@
|
|||||||
import React from 'react';
|
import React, { useState } from 'react';
|
||||||
|
import { useDebounce } from 'react-use';
|
||||||
|
|
||||||
import { SelectableValue, toOption } from '@grafana/data';
|
import { QueryEditorProps, toOption } from '@grafana/data';
|
||||||
import { TemplateSrv } from '@grafana/runtime';
|
import { Input } from '@grafana/ui';
|
||||||
import { LegacyForms } from '@grafana/ui';
|
|
||||||
|
|
||||||
|
import { INPUT_WIDTH } from '../constants';
|
||||||
import CloudMonitoringDatasource from '../datasource';
|
import CloudMonitoringDatasource from '../datasource';
|
||||||
import { AnnotationTarget, EditorMode, MetricDescriptor, MetricKind } from '../types';
|
import {
|
||||||
|
EditorMode,
|
||||||
|
MetricKind,
|
||||||
|
AnnotationMetricQuery,
|
||||||
|
CloudMonitoringOptions,
|
||||||
|
CloudMonitoringQuery,
|
||||||
|
AlignmentTypes,
|
||||||
|
} from '../types';
|
||||||
|
|
||||||
import { AnnotationsHelp, LabelFilter, Metrics, Project, QueryEditorRow } from './';
|
import { MetricQueryEditor } from './MetricQueryEditor';
|
||||||
|
|
||||||
const { Input } = LegacyForms;
|
import { AnnotationsHelp, QueryEditorRow } from './';
|
||||||
|
|
||||||
export interface Props {
|
export type Props = QueryEditorProps<CloudMonitoringDatasource, CloudMonitoringQuery, CloudMonitoringOptions>;
|
||||||
refId: string;
|
|
||||||
onQueryChange: (target: AnnotationTarget) => void;
|
|
||||||
target: AnnotationTarget;
|
|
||||||
datasource: CloudMonitoringDatasource;
|
|
||||||
templateSrv: TemplateSrv;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface State extends AnnotationTarget {
|
export const defaultQuery: (datasource: CloudMonitoringDatasource) => AnnotationMetricQuery = (datasource) => ({
|
||||||
variableOptionGroup: SelectableValue<string>;
|
|
||||||
variableOptions: Array<SelectableValue<string>>;
|
|
||||||
labels: any;
|
|
||||||
[key: string]: any;
|
|
||||||
}
|
|
||||||
|
|
||||||
const DefaultTarget: State = {
|
|
||||||
editorMode: EditorMode.Visual,
|
editorMode: EditorMode.Visual,
|
||||||
projectName: '',
|
projectName: datasource.getDefaultProject(),
|
||||||
projects: [],
|
projects: [],
|
||||||
metricType: '',
|
metricType: '',
|
||||||
filters: [],
|
filters: [],
|
||||||
@ -40,112 +35,68 @@ const DefaultTarget: State = {
|
|||||||
labels: {},
|
labels: {},
|
||||||
variableOptionGroup: {},
|
variableOptionGroup: {},
|
||||||
variableOptions: [],
|
variableOptions: [],
|
||||||
};
|
query: '',
|
||||||
|
crossSeriesReducer: 'REDUCE_NONE',
|
||||||
|
perSeriesAligner: AlignmentTypes.ALIGN_NONE,
|
||||||
|
alignmentPeriod: 'grafana-auto',
|
||||||
|
});
|
||||||
|
|
||||||
export class AnnotationQueryEditor extends React.Component<Props, State> {
|
export const AnnotationQueryEditor = (props: Props) => {
|
||||||
state: State = DefaultTarget;
|
const { datasource, query, onRunQuery, data, onChange } = props;
|
||||||
|
const meta = data?.series.length ? data?.series[0].meta : {};
|
||||||
async UNSAFE_componentWillMount() {
|
const customMetaData = meta?.custom ?? {};
|
||||||
// Unfortunately, migrations like this need to go UNSAFE_componentWillMount. As soon as there's
|
const metricQuery = { ...defaultQuery(datasource), ...query.metricQuery };
|
||||||
// migration hook for this module.ts, we can do the migrations there instead.
|
const [title, setTitle] = useState(metricQuery.title || '');
|
||||||
const { target, datasource } = this.props;
|
const [text, setText] = useState(metricQuery.text || '');
|
||||||
if (!target.projectName) {
|
const variableOptionGroup = {
|
||||||
target.projectName = datasource.getDefaultProject();
|
label: 'Template Variables',
|
||||||
}
|
options: datasource.getVariables().map(toOption),
|
||||||
|
|
||||||
const variableOptionGroup = {
|
|
||||||
label: 'Template Variables',
|
|
||||||
options: datasource.getVariables().map(toOption),
|
|
||||||
};
|
|
||||||
|
|
||||||
const projects = await datasource.getProjects();
|
|
||||||
this.setState({
|
|
||||||
variableOptionGroup,
|
|
||||||
variableOptions: variableOptionGroup.options,
|
|
||||||
...target,
|
|
||||||
projects,
|
|
||||||
});
|
|
||||||
|
|
||||||
datasource
|
|
||||||
.getLabels(target.metricType, target.projectName, target.refId)
|
|
||||||
.then((labels) => this.setState({ labels }));
|
|
||||||
}
|
|
||||||
|
|
||||||
onMetricTypeChange = ({ valueType, metricKind, type, unit }: MetricDescriptor) => {
|
|
||||||
const { onQueryChange, datasource } = this.props;
|
|
||||||
this.setState(
|
|
||||||
{
|
|
||||||
metricType: type,
|
|
||||||
unit,
|
|
||||||
valueType,
|
|
||||||
metricKind,
|
|
||||||
},
|
|
||||||
() => {
|
|
||||||
onQueryChange(this.state);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
datasource.getLabels(type, this.state.refId, this.state.projectName).then((labels) => this.setState({ labels }));
|
|
||||||
};
|
};
|
||||||
|
|
||||||
onChange(prop: string, value: string | string[]) {
|
const handleQueryChange = (metricQuery: AnnotationMetricQuery) => onChange({ ...query, metricQuery });
|
||||||
this.setState({ [prop]: value }, () => {
|
const handleTitleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
this.props.onQueryChange(this.state);
|
setTitle(e.target.value);
|
||||||
});
|
};
|
||||||
}
|
const handleTextChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
setText(e.target.value);
|
||||||
|
};
|
||||||
|
|
||||||
render() {
|
useDebounce(
|
||||||
const { metricType, projectName, filters, title, text, variableOptionGroup, labels, variableOptions } = this.state;
|
() => {
|
||||||
const { datasource } = this.props;
|
onChange({ ...query, metricQuery: { ...metricQuery, title } });
|
||||||
|
},
|
||||||
|
1000,
|
||||||
|
[title, onChange]
|
||||||
|
);
|
||||||
|
useDebounce(
|
||||||
|
() => {
|
||||||
|
onChange({ ...query, metricQuery: { ...metricQuery, text } });
|
||||||
|
},
|
||||||
|
1000,
|
||||||
|
[text, onChange]
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Project
|
<MetricQueryEditor
|
||||||
refId={this.props.refId}
|
refId={query.refId}
|
||||||
templateVariableOptions={variableOptions}
|
variableOptionGroup={variableOptionGroup}
|
||||||
datasource={datasource}
|
customMetaData={customMetaData}
|
||||||
projectName={projectName || datasource.getDefaultProject()}
|
onChange={handleQueryChange}
|
||||||
onChange={(value) => this.onChange('projectName', value)}
|
onRunQuery={onRunQuery}
|
||||||
/>
|
datasource={datasource}
|
||||||
<Metrics
|
query={metricQuery}
|
||||||
refId={this.props.refId}
|
/>
|
||||||
projectName={projectName}
|
|
||||||
metricType={metricType}
|
|
||||||
templateSrv={datasource.templateSrv}
|
|
||||||
datasource={datasource}
|
|
||||||
templateVariableOptions={variableOptions}
|
|
||||||
onChange={(metric) => this.onMetricTypeChange(metric)}
|
|
||||||
>
|
|
||||||
{(metric) => (
|
|
||||||
<>
|
|
||||||
<LabelFilter
|
|
||||||
labels={labels}
|
|
||||||
filters={filters}
|
|
||||||
onChange={(value) => this.onChange('filters', value)}
|
|
||||||
variableOptionGroup={variableOptionGroup}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Metrics>
|
|
||||||
|
|
||||||
<QueryEditorRow label="Title">
|
<QueryEditorRow label="Title" htmlFor="annotation-query-title">
|
||||||
<Input
|
<Input id="annotation-query-title" value={title} width={INPUT_WIDTH} onChange={handleTitleChange} />
|
||||||
type="text"
|
</QueryEditorRow>
|
||||||
className="gf-form-input width-20"
|
|
||||||
value={title}
|
|
||||||
onChange={(e) => this.onChange('title', e.target.value)}
|
|
||||||
/>
|
|
||||||
</QueryEditorRow>
|
|
||||||
<QueryEditorRow label="Text">
|
|
||||||
<Input
|
|
||||||
type="text"
|
|
||||||
className="gf-form-input width-20"
|
|
||||||
value={text}
|
|
||||||
onChange={(e) => this.onChange('text', e.target.value)}
|
|
||||||
/>
|
|
||||||
</QueryEditorRow>
|
|
||||||
|
|
||||||
<AnnotationsHelp />
|
<QueryEditorRow label="Text" htmlFor="annotation-query-text">
|
||||||
</>
|
<Input id="annotation-query-text" value={text} width={INPUT_WIDTH} onChange={handleTextChange} />
|
||||||
);
|
</QueryEditorRow>
|
||||||
}
|
|
||||||
}
|
<AnnotationsHelp />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
@ -13,6 +13,20 @@ export const AUTH_TYPES = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
export const ALIGNMENTS = [
|
export const ALIGNMENTS = [
|
||||||
|
{
|
||||||
|
text: 'none',
|
||||||
|
value: 'ALIGN_NONE',
|
||||||
|
valueTypes: [
|
||||||
|
ValueTypes.INT64,
|
||||||
|
ValueTypes.DOUBLE,
|
||||||
|
ValueTypes.MONEY,
|
||||||
|
ValueTypes.DISTRIBUTION,
|
||||||
|
ValueTypes.STRING,
|
||||||
|
ValueTypes.VALUE_TYPE_UNSPECIFIED,
|
||||||
|
ValueTypes.BOOL,
|
||||||
|
],
|
||||||
|
metricKinds: [MetricKind.GAUGE],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
text: 'delta',
|
text: 'delta',
|
||||||
value: 'ALIGN_DELTA',
|
value: 'ALIGN_DELTA',
|
||||||
|
@ -13,6 +13,7 @@ import { DataSourceWithBackend, getBackendSrv, toDataQueryResponse } from '@graf
|
|||||||
import { getTimeSrv, TimeSrv } from 'app/features/dashboard/services/TimeSrv';
|
import { getTimeSrv, TimeSrv } from 'app/features/dashboard/services/TimeSrv';
|
||||||
import { getTemplateSrv, TemplateSrv } from 'app/features/templating/template_srv';
|
import { getTemplateSrv, TemplateSrv } from 'app/features/templating/template_srv';
|
||||||
|
|
||||||
|
import { CloudMonitoringAnnotationSupport } from './annotationSupport';
|
||||||
import {
|
import {
|
||||||
CloudMonitoringOptions,
|
CloudMonitoringOptions,
|
||||||
CloudMonitoringQuery,
|
CloudMonitoringQuery,
|
||||||
@ -41,6 +42,7 @@ export default class CloudMonitoringDatasource extends DataSourceWithBackend<
|
|||||||
this.authenticationType = instanceSettings.jsonData.authenticationType || 'jwt';
|
this.authenticationType = instanceSettings.jsonData.authenticationType || 'jwt';
|
||||||
this.variables = new CloudMonitoringVariableSupport(this);
|
this.variables = new CloudMonitoringVariableSupport(this);
|
||||||
this.intervalMs = 0;
|
this.intervalMs = 0;
|
||||||
|
this.annotations = CloudMonitoringAnnotationSupport(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
getVariables() {
|
getVariables() {
|
||||||
@ -55,73 +57,15 @@ export default class CloudMonitoringDatasource extends DataSourceWithBackend<
|
|||||||
return super.query(request);
|
return super.query(request);
|
||||||
}
|
}
|
||||||
|
|
||||||
async annotationQuery(options: any) {
|
|
||||||
await this.ensureGCEDefaultProject();
|
|
||||||
const annotation = options.annotation;
|
|
||||||
const queries = [
|
|
||||||
{
|
|
||||||
refId: 'annotationQuery',
|
|
||||||
type: 'annotationQuery',
|
|
||||||
datasource: this.getRef(),
|
|
||||||
view: 'FULL',
|
|
||||||
crossSeriesReducer: 'REDUCE_NONE',
|
|
||||||
perSeriesAligner: 'ALIGN_NONE',
|
|
||||||
metricType: this.templateSrv.replace(annotation.target.metricType, options.scopedVars || {}),
|
|
||||||
title: this.templateSrv.replace(annotation.target.title, options.scopedVars || {}),
|
|
||||||
text: this.templateSrv.replace(annotation.target.text, options.scopedVars || {}),
|
|
||||||
projectName: this.templateSrv.replace(
|
|
||||||
annotation.target.projectName ? annotation.target.projectName : this.getDefaultProject(),
|
|
||||||
options.scopedVars || {}
|
|
||||||
),
|
|
||||||
filters: this.interpolateFilters(annotation.target.filters || [], options.scopedVars),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
return lastValueFrom(
|
|
||||||
getBackendSrv()
|
|
||||||
.fetch<PostResponse>({
|
|
||||||
url: '/api/ds/query',
|
|
||||||
method: 'POST',
|
|
||||||
data: {
|
|
||||||
from: options.range.from.valueOf().toString(),
|
|
||||||
to: options.range.to.valueOf().toString(),
|
|
||||||
queries,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.pipe(
|
|
||||||
map(({ data }) => {
|
|
||||||
const dataQueryResponse = toDataQueryResponse({
|
|
||||||
data: data,
|
|
||||||
});
|
|
||||||
const df: any = [];
|
|
||||||
if (dataQueryResponse.data.length !== 0) {
|
|
||||||
for (let i = 0; i < dataQueryResponse.data.length; i++) {
|
|
||||||
for (let j = 0; j < dataQueryResponse.data[i].fields[0].values.length; j++) {
|
|
||||||
df.push({
|
|
||||||
annotation: annotation,
|
|
||||||
time: Date.parse(dataQueryResponse.data[i].fields[0].values.get(j)),
|
|
||||||
title: dataQueryResponse.data[i].fields[1].values.get(j),
|
|
||||||
tags: [],
|
|
||||||
text: dataQueryResponse.data[i].fields[3].values.get(j),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return df;
|
|
||||||
})
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
applyTemplateVariables(
|
applyTemplateVariables(
|
||||||
{ metricQuery, refId, queryType, sloQuery }: CloudMonitoringQuery,
|
{ metricQuery, refId, queryType, sloQuery, type = 'timeSeriesQuery' }: CloudMonitoringQuery,
|
||||||
scopedVars: ScopedVars
|
scopedVars: ScopedVars
|
||||||
): Record<string, any> {
|
): Record<string, any> {
|
||||||
return {
|
return {
|
||||||
datasource: this.getRef(),
|
datasource: this.getRef(),
|
||||||
refId,
|
refId,
|
||||||
intervalMs: this.intervalMs,
|
intervalMs: this.intervalMs,
|
||||||
type: 'timeSeriesQuery',
|
type,
|
||||||
queryType,
|
queryType,
|
||||||
metricQuery: {
|
metricQuery: {
|
||||||
...this.interpolateProps(metricQuery, scopedVars),
|
...this.interpolateProps(metricQuery, scopedVars),
|
||||||
|
@ -127,7 +127,7 @@ describe('functions', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should return all alignment options except two', () => {
|
it('should return all alignment options except two', () => {
|
||||||
expect(result.length).toBe(9);
|
expect(result.length).toBe(10);
|
||||||
expect(result.map((o: any) => o.value)).toEqual(
|
expect(result.map((o: any) => o.value)).toEqual(
|
||||||
expect.not.arrayContaining(['REDUCE_COUNT_TRUE', 'REDUCE_COUNT_FALSE'])
|
expect.not.arrayContaining(['REDUCE_COUNT_TRUE', 'REDUCE_COUNT_FALSE'])
|
||||||
);
|
);
|
||||||
@ -173,7 +173,7 @@ describe('functions', () => {
|
|||||||
describe('getAlignmentPickerData', () => {
|
describe('getAlignmentPickerData', () => {
|
||||||
it('should return default data', () => {
|
it('should return default data', () => {
|
||||||
const res = getAlignmentPickerData();
|
const res = getAlignmentPickerData();
|
||||||
expect(res.alignOptions).toHaveLength(9);
|
expect(res.alignOptions).toHaveLength(10);
|
||||||
expect(res.perSeriesAligner).toEqual(AlignmentTypes.ALIGN_MEAN);
|
expect(res.perSeriesAligner).toEqual(AlignmentTypes.ALIGN_MEAN);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import { DataSourcePlugin } from '@grafana/data';
|
import { DataSourcePlugin } from '@grafana/data';
|
||||||
|
|
||||||
import { CloudMonitoringAnnotationsQueryCtrl } from './annotations_query_ctrl';
|
|
||||||
import CloudMonitoringCheatSheet from './components/CloudMonitoringCheatSheet';
|
import CloudMonitoringCheatSheet from './components/CloudMonitoringCheatSheet';
|
||||||
import { ConfigEditor } from './components/ConfigEditor/ConfigEditor';
|
import { ConfigEditor } from './components/ConfigEditor/ConfigEditor';
|
||||||
import { QueryEditor } from './components/QueryEditor';
|
import { QueryEditor } from './components/QueryEditor';
|
||||||
@ -12,5 +11,4 @@ export const plugin = new DataSourcePlugin<CloudMonitoringDatasource, CloudMonit
|
|||||||
.setQueryEditorHelp(CloudMonitoringCheatSheet)
|
.setQueryEditorHelp(CloudMonitoringCheatSheet)
|
||||||
.setQueryEditor(QueryEditor)
|
.setQueryEditor(QueryEditor)
|
||||||
.setConfigEditor(ConfigEditor)
|
.setConfigEditor(ConfigEditor)
|
||||||
.setAnnotationQueryCtrl(CloudMonitoringAnnotationsQueryCtrl)
|
|
||||||
.setVariableQueryEditor(CloudMonitoringVariableQueryEditor);
|
.setVariableQueryEditor(CloudMonitoringVariableQueryEditor);
|
||||||
|
@ -1,6 +0,0 @@
|
|||||||
<cloud-monitoring-annotation-query-editor
|
|
||||||
target="ctrl.annotation.target"
|
|
||||||
on-query-change="(ctrl.onQueryChange)"
|
|
||||||
datasource="ctrl.datasource"
|
|
||||||
template-srv="ctrl.templateSrv"
|
|
||||||
></cloud-monitoring-annotation-query-editor>
|
|
@ -106,6 +106,7 @@ export enum AlignmentTypes {
|
|||||||
ALIGN_PERCENTILE_50 = 'ALIGN_PERCENTILE_50',
|
ALIGN_PERCENTILE_50 = 'ALIGN_PERCENTILE_50',
|
||||||
ALIGN_PERCENTILE_05 = 'ALIGN_PERCENTILE_05',
|
ALIGN_PERCENTILE_05 = 'ALIGN_PERCENTILE_05',
|
||||||
ALIGN_PERCENT_CHANGE = 'ALIGN_PERCENT_CHANGE',
|
ALIGN_PERCENT_CHANGE = 'ALIGN_PERCENT_CHANGE',
|
||||||
|
ALIGN_NONE = 'ALIGN_NONE',
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface BaseQuery {
|
export interface BaseQuery {
|
||||||
@ -130,6 +131,11 @@ export interface MetricQuery extends BaseQuery {
|
|||||||
graphPeriod?: 'disabled' | string;
|
graphPeriod?: 'disabled' | string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface AnnotationMetricQuery extends MetricQuery {
|
||||||
|
title?: string;
|
||||||
|
text?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface SLOQuery extends BaseQuery {
|
export interface SLOQuery extends BaseQuery {
|
||||||
selectorName: string;
|
selectorName: string;
|
||||||
serviceId: string;
|
serviceId: string;
|
||||||
@ -142,7 +148,7 @@ export interface SLOQuery extends BaseQuery {
|
|||||||
export interface CloudMonitoringQuery extends DataQuery {
|
export interface CloudMonitoringQuery extends DataQuery {
|
||||||
datasourceId?: number; // Should not be necessary anymore
|
datasourceId?: number; // Should not be necessary anymore
|
||||||
queryType: QueryType;
|
queryType: QueryType;
|
||||||
metricQuery: MetricQuery;
|
metricQuery: MetricQuery | AnnotationMetricQuery;
|
||||||
sloQuery?: SLOQuery;
|
sloQuery?: SLOQuery;
|
||||||
intervalMs: number;
|
intervalMs: number;
|
||||||
type: string;
|
type: string;
|
||||||
@ -160,7 +166,7 @@ export interface CloudMonitoringSecureJsonData {
|
|||||||
privateKey?: string;
|
privateKey?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AnnotationTarget {
|
export interface LegacyCloudMonitoringAnnotationQuery {
|
||||||
projectName: string;
|
projectName: string;
|
||||||
metricType: string;
|
metricType: string;
|
||||||
refId: string;
|
refId: string;
|
||||||
|
Reference in New Issue
Block a user