mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06: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:
parent
26e98a6f1b
commit
0a95d493e3
@ -4,11 +4,19 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||
"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) (
|
||||
*backend.QueryDataResponse, error) {
|
||||
resp := backend.NewQueryDataResponse()
|
||||
@ -24,8 +32,10 @@ func (s *Service) executeAnnotationQuery(ctx context.Context, req *backend.Query
|
||||
}
|
||||
|
||||
mq := struct {
|
||||
Title string `json:"title"`
|
||||
Text string `json:"text"`
|
||||
MetricQuery struct {
|
||||
Title string `json:"title"`
|
||||
Text string `json:"text"`
|
||||
} `json:"metricQuery"`
|
||||
}{}
|
||||
|
||||
firstQuery := req.Queries[0]
|
||||
@ -33,32 +43,23 @@ func (s *Service) executeAnnotationQuery(ctx context.Context, req *backend.Query
|
||||
if err != 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
|
||||
|
||||
return resp, err
|
||||
}
|
||||
|
||||
func (timeSeriesQuery cloudMonitoringTimeSeriesQuery) transformAnnotationToFrame(annotations []map[string]string, result *backend.DataResponse) {
|
||||
frames := data.Frames{}
|
||||
func (timeSeriesQuery cloudMonitoringTimeSeriesQuery) transformAnnotationToFrame(annotations []*annotationEvent, result *backend.DataResponse) {
|
||||
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 {
|
||||
frame := &data.Frame{
|
||||
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)
|
||||
frame.AppendRow(a.Time, a.Title, a.Tags, a.Text)
|
||||
}
|
||||
result.Frames = frames
|
||||
result.Frames = append(result.Frames, frame)
|
||||
slog.Info("anno", "len", len(annotations))
|
||||
}
|
||||
|
||||
|
@ -20,10 +20,15 @@ func TestExecutor_parseToAnnotations(t *testing.T) {
|
||||
"atext {{resource.label.zone}}")
|
||||
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, "tags", res.Frames[0].Fields[2].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) {
|
||||
@ -37,7 +42,15 @@ func TestCloudMonitoringExecutor_parseToAnnotations_emptyTimeSeries(t *testing.T
|
||||
err := query.parseToAnnotations(res, response, "atitle", "atext")
|
||||
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) {
|
||||
@ -53,5 +66,13 @@ func TestCloudMonitoringExecutor_parseToAnnotations_noPointsInSeries(t *testing.
|
||||
err := query.parseToAnnotations(res, response, "atitle", "atext")
|
||||
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,
|
||||
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 {
|
||||
if len(series.Points) == 0 {
|
||||
continue
|
||||
}
|
||||
annotation := make(map[string][]string)
|
||||
|
||||
for i := len(series.Points) - 1; i >= 0; i-- {
|
||||
point := series.Points[i]
|
||||
value := strconv.FormatFloat(point.Value.DoubleValue, 'f', 6, 64)
|
||||
if series.ValueType == "STRING" {
|
||||
value = point.Value.StringValue
|
||||
}
|
||||
annotation["time"] = append(annotation["time"], point.Interval.EndTime.UTC().Format(time.RFC3339))
|
||||
annotation["title"] = append(annotation["title"], formatAnnotationText(title, value, series.Metric.Type,
|
||||
series.Metric.Labels, series.Resource.Labels))
|
||||
annotation["tags"] = append(annotation["tags"], "")
|
||||
annotation["text"] = append(annotation["text"], formatAnnotationText(text, value, series.Metric.Type,
|
||||
series.Metric.Labels, series.Resource.Labels))
|
||||
annotation := &annotationEvent{
|
||||
Time: point.Interval.EndTime,
|
||||
Title: formatAnnotationText(title, value, series.Metric.Type,
|
||||
series.Metric.Labels, series.Resource.Labels),
|
||||
Tags: "",
|
||||
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
|
||||
}
|
||||
|
@ -266,7 +266,7 @@ func (timeSeriesQuery cloudMonitoringTimeSeriesQuery) parseResponse(queryRes *ba
|
||||
|
||||
func (timeSeriesQuery cloudMonitoringTimeSeriesQuery) parseToAnnotations(queryRes *backend.DataResponse,
|
||||
data cloudMonitoringResponse, title, text string) error {
|
||||
annotations := make([]map[string]string, 0)
|
||||
annotations := make([]*annotationEvent, 0)
|
||||
|
||||
for _, series := range data.TimeSeriesData {
|
||||
metricLabels := make(map[string]string)
|
||||
@ -302,12 +302,12 @@ func (timeSeriesQuery cloudMonitoringTimeSeriesQuery) parseToAnnotations(queryRe
|
||||
if d.ValueType == "STRING" {
|
||||
value = point.Values[n].StringValue
|
||||
}
|
||||
annotation := make(map[string]string)
|
||||
annotation["time"] = point.TimeInterval.EndTime.UTC().Format(time.RFC3339)
|
||||
annotation["title"] = formatAnnotationText(title, value, d.MetricKind, metricLabels, resourceLabels)
|
||||
annotation["tags"] = ""
|
||||
annotation["text"] = formatAnnotationText(text, value, d.MetricKind, metricLabels, resourceLabels)
|
||||
annotations = append(annotations, annotation)
|
||||
annotations = append(annotations, &annotationEvent{
|
||||
Time: point.TimeInterval.EndTime,
|
||||
Title: formatAnnotationText(title, value, d.MetricKind, metricLabels, resourceLabels),
|
||||
Tags: "",
|
||||
Text: formatAnnotationText(text, value, d.MetricKind, metricLabels, resourceLabels),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -13,7 +13,6 @@ import {
|
||||
import { react2AngularDirective } from 'app/angular/react2angular';
|
||||
import { FolderPicker } from 'app/core/components/Select/FolderPicker';
|
||||
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 EmptyListCTA from '../core/components/EmptyListCTA/EmptyListCTA';
|
||||
@ -117,12 +116,6 @@ export function registerAngularDirectives() {
|
||||
['datasource', { watchDepth: 'reference' }],
|
||||
['templateSrv', { watchDepth: 'reference' }],
|
||||
]);
|
||||
react2AngularDirective('cloudMonitoringAnnotationQueryEditor', CloudMonitoringAnnotationQueryEditor, [
|
||||
'target',
|
||||
'onQueryChange',
|
||||
['datasource', { watchDepth: 'reference' }],
|
||||
['templateSrv', { watchDepth: 'reference' }],
|
||||
]);
|
||||
react2AngularDirective('secretFormField', SecretFormField, [
|
||||
'value',
|
||||
'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 { TemplateSrv } from '@grafana/runtime';
|
||||
import { LegacyForms } from '@grafana/ui';
|
||||
import { QueryEditorProps, toOption } from '@grafana/data';
|
||||
import { Input } from '@grafana/ui';
|
||||
|
||||
import { INPUT_WIDTH } from '../constants';
|
||||
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 {
|
||||
refId: string;
|
||||
onQueryChange: (target: AnnotationTarget) => void;
|
||||
target: AnnotationTarget;
|
||||
datasource: CloudMonitoringDatasource;
|
||||
templateSrv: TemplateSrv;
|
||||
}
|
||||
export type Props = QueryEditorProps<CloudMonitoringDatasource, CloudMonitoringQuery, CloudMonitoringOptions>;
|
||||
|
||||
interface State extends AnnotationTarget {
|
||||
variableOptionGroup: SelectableValue<string>;
|
||||
variableOptions: Array<SelectableValue<string>>;
|
||||
labels: any;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
const DefaultTarget: State = {
|
||||
export const defaultQuery: (datasource: CloudMonitoringDatasource) => AnnotationMetricQuery = (datasource) => ({
|
||||
editorMode: EditorMode.Visual,
|
||||
projectName: '',
|
||||
projectName: datasource.getDefaultProject(),
|
||||
projects: [],
|
||||
metricType: '',
|
||||
filters: [],
|
||||
@ -40,112 +35,68 @@ const DefaultTarget: State = {
|
||||
labels: {},
|
||||
variableOptionGroup: {},
|
||||
variableOptions: [],
|
||||
};
|
||||
query: '',
|
||||
crossSeriesReducer: 'REDUCE_NONE',
|
||||
perSeriesAligner: AlignmentTypes.ALIGN_NONE,
|
||||
alignmentPeriod: 'grafana-auto',
|
||||
});
|
||||
|
||||
export class AnnotationQueryEditor extends React.Component<Props, State> {
|
||||
state: State = DefaultTarget;
|
||||
|
||||
async UNSAFE_componentWillMount() {
|
||||
// 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.
|
||||
const { target, datasource } = this.props;
|
||||
if (!target.projectName) {
|
||||
target.projectName = datasource.getDefaultProject();
|
||||
}
|
||||
|
||||
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 }));
|
||||
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 variableOptionGroup = {
|
||||
label: 'Template Variables',
|
||||
options: datasource.getVariables().map(toOption),
|
||||
};
|
||||
|
||||
onChange(prop: string, value: string | string[]) {
|
||||
this.setState({ [prop]: value }, () => {
|
||||
this.props.onQueryChange(this.state);
|
||||
});
|
||||
}
|
||||
const handleQueryChange = (metricQuery: AnnotationMetricQuery) => onChange({ ...query, metricQuery });
|
||||
const handleTitleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setTitle(e.target.value);
|
||||
};
|
||||
const handleTextChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setText(e.target.value);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { metricType, projectName, filters, title, text, variableOptionGroup, labels, variableOptions } = this.state;
|
||||
const { datasource } = this.props;
|
||||
useDebounce(
|
||||
() => {
|
||||
onChange({ ...query, metricQuery: { ...metricQuery, title } });
|
||||
},
|
||||
1000,
|
||||
[title, onChange]
|
||||
);
|
||||
useDebounce(
|
||||
() => {
|
||||
onChange({ ...query, metricQuery: { ...metricQuery, text } });
|
||||
},
|
||||
1000,
|
||||
[text, onChange]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Project
|
||||
refId={this.props.refId}
|
||||
templateVariableOptions={variableOptions}
|
||||
datasource={datasource}
|
||||
projectName={projectName || datasource.getDefaultProject()}
|
||||
onChange={(value) => this.onChange('projectName', value)}
|
||||
/>
|
||||
<Metrics
|
||||
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>
|
||||
return (
|
||||
<>
|
||||
<MetricQueryEditor
|
||||
refId={query.refId}
|
||||
variableOptionGroup={variableOptionGroup}
|
||||
customMetaData={customMetaData}
|
||||
onChange={handleQueryChange}
|
||||
onRunQuery={onRunQuery}
|
||||
datasource={datasource}
|
||||
query={metricQuery}
|
||||
/>
|
||||
|
||||
<QueryEditorRow label="Title">
|
||||
<Input
|
||||
type="text"
|
||||
className="gf-form-input width-20"
|
||||
value={title}
|
||||
onChange={(e) => this.onChange('title', e.target.value)}
|
||||
/>
|
||||
</QueryEditorRow>
|
||||
<QueryEditorRow label="Text">
|
||||
<Input
|
||||
type="text"
|
||||
className="gf-form-input width-20"
|
||||
value={text}
|
||||
onChange={(e) => this.onChange('text', e.target.value)}
|
||||
/>
|
||||
</QueryEditorRow>
|
||||
<QueryEditorRow label="Title" htmlFor="annotation-query-title">
|
||||
<Input id="annotation-query-title" value={title} width={INPUT_WIDTH} onChange={handleTitleChange} />
|
||||
</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 = [
|
||||
{
|
||||
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',
|
||||
value: 'ALIGN_DELTA',
|
||||
|
@ -13,6 +13,7 @@ import { DataSourceWithBackend, getBackendSrv, toDataQueryResponse } from '@graf
|
||||
import { getTimeSrv, TimeSrv } from 'app/features/dashboard/services/TimeSrv';
|
||||
import { getTemplateSrv, TemplateSrv } from 'app/features/templating/template_srv';
|
||||
|
||||
import { CloudMonitoringAnnotationSupport } from './annotationSupport';
|
||||
import {
|
||||
CloudMonitoringOptions,
|
||||
CloudMonitoringQuery,
|
||||
@ -41,6 +42,7 @@ export default class CloudMonitoringDatasource extends DataSourceWithBackend<
|
||||
this.authenticationType = instanceSettings.jsonData.authenticationType || 'jwt';
|
||||
this.variables = new CloudMonitoringVariableSupport(this);
|
||||
this.intervalMs = 0;
|
||||
this.annotations = CloudMonitoringAnnotationSupport(this);
|
||||
}
|
||||
|
||||
getVariables() {
|
||||
@ -55,73 +57,15 @@ export default class CloudMonitoringDatasource extends DataSourceWithBackend<
|
||||
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(
|
||||
{ metricQuery, refId, queryType, sloQuery }: CloudMonitoringQuery,
|
||||
{ metricQuery, refId, queryType, sloQuery, type = 'timeSeriesQuery' }: CloudMonitoringQuery,
|
||||
scopedVars: ScopedVars
|
||||
): Record<string, any> {
|
||||
return {
|
||||
datasource: this.getRef(),
|
||||
refId,
|
||||
intervalMs: this.intervalMs,
|
||||
type: 'timeSeriesQuery',
|
||||
type,
|
||||
queryType,
|
||||
metricQuery: {
|
||||
...this.interpolateProps(metricQuery, scopedVars),
|
||||
|
@ -127,7 +127,7 @@ describe('functions', () => {
|
||||
});
|
||||
|
||||
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.not.arrayContaining(['REDUCE_COUNT_TRUE', 'REDUCE_COUNT_FALSE'])
|
||||
);
|
||||
@ -173,7 +173,7 @@ describe('functions', () => {
|
||||
describe('getAlignmentPickerData', () => {
|
||||
it('should return default data', () => {
|
||||
const res = getAlignmentPickerData();
|
||||
expect(res.alignOptions).toHaveLength(9);
|
||||
expect(res.alignOptions).toHaveLength(10);
|
||||
expect(res.perSeriesAligner).toEqual(AlignmentTypes.ALIGN_MEAN);
|
||||
});
|
||||
|
||||
|
@ -1,6 +1,5 @@
|
||||
import { DataSourcePlugin } from '@grafana/data';
|
||||
|
||||
import { CloudMonitoringAnnotationsQueryCtrl } from './annotations_query_ctrl';
|
||||
import CloudMonitoringCheatSheet from './components/CloudMonitoringCheatSheet';
|
||||
import { ConfigEditor } from './components/ConfigEditor/ConfigEditor';
|
||||
import { QueryEditor } from './components/QueryEditor';
|
||||
@ -12,5 +11,4 @@ export const plugin = new DataSourcePlugin<CloudMonitoringDatasource, CloudMonit
|
||||
.setQueryEditorHelp(CloudMonitoringCheatSheet)
|
||||
.setQueryEditor(QueryEditor)
|
||||
.setConfigEditor(ConfigEditor)
|
||||
.setAnnotationQueryCtrl(CloudMonitoringAnnotationsQueryCtrl)
|
||||
.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_05 = 'ALIGN_PERCENTILE_05',
|
||||
ALIGN_PERCENT_CHANGE = 'ALIGN_PERCENT_CHANGE',
|
||||
ALIGN_NONE = 'ALIGN_NONE',
|
||||
}
|
||||
|
||||
export interface BaseQuery {
|
||||
@ -130,6 +131,11 @@ export interface MetricQuery extends BaseQuery {
|
||||
graphPeriod?: 'disabled' | string;
|
||||
}
|
||||
|
||||
export interface AnnotationMetricQuery extends MetricQuery {
|
||||
title?: string;
|
||||
text?: string;
|
||||
}
|
||||
|
||||
export interface SLOQuery extends BaseQuery {
|
||||
selectorName: string;
|
||||
serviceId: string;
|
||||
@ -142,7 +148,7 @@ export interface SLOQuery extends BaseQuery {
|
||||
export interface CloudMonitoringQuery extends DataQuery {
|
||||
datasourceId?: number; // Should not be necessary anymore
|
||||
queryType: QueryType;
|
||||
metricQuery: MetricQuery;
|
||||
metricQuery: MetricQuery | AnnotationMetricQuery;
|
||||
sloQuery?: SLOQuery;
|
||||
intervalMs: number;
|
||||
type: string;
|
||||
@ -160,7 +166,7 @@ export interface CloudMonitoringSecureJsonData {
|
||||
privateKey?: string;
|
||||
}
|
||||
|
||||
export interface AnnotationTarget {
|
||||
export interface LegacyCloudMonitoringAnnotationQuery {
|
||||
projectName: string;
|
||||
metricType: string;
|
||||
refId: string;
|
||||
|
Loading…
Reference in New Issue
Block a user