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:
Kevin Yu 2022-05-19 13:52:52 -07:00 committed by GitHub
parent 26e98a6f1b
commit 0a95d493e3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 455 additions and 268 deletions

View File

@ -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))
}

View File

@ -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())
}

View File

@ -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
}

View File

@ -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),
})
}
}
}

View File

@ -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',

View File

@ -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);
};

View File

@ -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(),
};
};

View File

@ -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();
});
});
});

View File

@ -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,
};
};

View File

@ -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);
}
}

View File

@ -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();
});
});

View File

@ -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 />
</>
);
};

View File

@ -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',

View File

@ -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),

View File

@ -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);
});

View File

@ -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);

View File

@ -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>

View File

@ -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;