Graphite Plugin: Remove angular dependencies for graphite annotations (#52261)

* fix merge conflict

* fix betterer

* handle new creating annotations

* add h5 'or' tag to annotation editor

* fix annotation regression looking for tags before target

* remove angular annotation partial

* change ann tags type to string[] and use TagsInput to create ann

* remove GraphiteEventsType, return annotations targets setting 'textEditor': true

* fix yarn typecheck errors

* add dateTime for yarn fix to tests

* fix incorrect merge conflict resolution

* fix betterer

* making changes for PR approval resolutions

* fix prettier issue

* fix prettier
This commit is contained in:
Brendan O'Handley 2022-07-27 16:27:42 -04:00 committed by GitHub
parent 6ec9a7682d
commit 6c3efb0c88
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 198 additions and 73 deletions

View File

@ -7215,6 +7215,9 @@ exports[`better eslint`] = {
[0, 0, 0, "Unexpected any. Specify a different type.", "5"],
[0, 0, 0, "Unexpected any. Specify a different type.", "6"]
],
"public/app/plugins/datasource/graphite/migrations.ts:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
],
"public/app/plugins/datasource/graphite/parser.ts:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],

View File

@ -0,0 +1,56 @@
import React, { useState } from 'react';
import { QueryEditorProps } from '@grafana/data';
import { InlineFormLabel, Input, TagsInput } from '@grafana/ui';
import { GraphiteDatasource } from '../datasource';
import { GraphiteQuery, GraphiteOptions } from '../types';
export const AnnotationEditor = (props: QueryEditorProps<GraphiteDatasource, GraphiteQuery, GraphiteOptions>) => {
const { query, onChange } = props;
const [target, setTarget] = useState<string>(query.target ?? '');
const [tags, setTags] = useState<string[]>(query.tags ?? []);
const updateValue = <K extends keyof GraphiteQuery, V extends GraphiteQuery[K]>(key: K, val: V) => {
if (key === 'tags') {
onChange({
...query,
[key]: val,
fromAnnotations: true,
queryType: key,
});
} else {
onChange({
...query,
[key]: val,
fromAnnotations: true,
textEditor: true,
});
}
};
const onTagsChange = (tagsInput: string[]) => {
setTags(tagsInput);
updateValue('tags', tagsInput);
};
return (
<div className="gf-form-group">
<div className="gf-form">
<InlineFormLabel width={12}>Graphite Query</InlineFormLabel>
<Input
value={target}
onChange={(e) => setTarget(e.currentTarget.value || '')}
onBlur={() => updateValue('target', target)}
placeholder="Example: statsd.application.counters.*.count"
/>
</div>
<h5 className="section-heading">Or</h5>
<div className="gf-form">
<InlineFormLabel width={12}>Graphite events tags</InlineFormLabel>
<TagsInput id="tags-input" tags={tags} onChange={onTagsChange} placeholder="Example: event_tag" />
</div>
</div>
);
};

View File

@ -2,7 +2,7 @@ import { isArray } from 'lodash';
import { of } from 'rxjs';
import { createFetchResponse } from 'test/helpers/createFetchResponse';
import { AbstractLabelMatcher, AbstractLabelOperator, dateTime, getFrameDisplayName } from '@grafana/data';
import { AbstractLabelMatcher, AbstractLabelOperator, getFrameDisplayName, dateTime } from '@grafana/data';
import { backendSrv } from 'app/core/services/backend_srv'; // will use the version in __mocks__
import { TemplateSrv } from 'app/features/templating/template_srv';
@ -196,13 +196,21 @@ describe('graphiteDatasource', () => {
});
const options = {
annotation: {
tags: 'tag1',
},
targets: [
{
fromAnnotations: true,
tags: ['tag1'],
queryType: 'tags',
},
],
range: {
from: dateTime(1432288354),
to: dateTime(1432288401),
raw: { from: 'now-24h', to: 'now' },
from: '2022-06-06T07:03:03.109Z',
to: '2022-06-07T07:03:03.109Z',
raw: {
from: '2022-06-06T07:03:03.109Z',
to: '2022-06-07T07:03:03.109Z',
},
},
};
@ -221,7 +229,7 @@ describe('graphiteDatasource', () => {
fetchMock.mockImplementation((options: any) => {
return of(createFetchResponse(response));
});
await ctx.ds.annotationQuery(options).then((data: any) => {
await ctx.ds.annotationEvents(options.range, options.targets[0]).then((data: any) => {
results = data;
});
});
@ -250,7 +258,7 @@ describe('graphiteDatasource', () => {
return of(createFetchResponse(response));
});
await ctx.ds.annotationQuery(options).then((data: any) => {
await ctx.ds.annotationEvents(options.range, options.targets[0]).then((data: any) => {
results = data;
});
});
@ -267,7 +275,7 @@ describe('graphiteDatasource', () => {
fetchMock.mockImplementation((options: any) => {
return of(createFetchResponse('zzzzzzz'));
});
await ctx.ds.annotationQuery(options).then((data: any) => {
await ctx.ds.annotationEvents(options.range, options.targets[0]).then((data: any) => {
results = data;
});
expect(results).toEqual([]);

View File

@ -1,5 +1,5 @@
import { each, indexOf, isArray, isString, map as _map } from 'lodash';
import { lastValueFrom, Observable, of, OperatorFunction, pipe, throwError } from 'rxjs';
import { lastValueFrom, merge, Observable, of, OperatorFunction, pipe, throwError } from 'rxjs';
import { catchError, map } from 'rxjs/operators';
import {
@ -26,15 +26,18 @@ import { getRollupNotice, getRuntimeConsolidationNotice } from 'app/plugins/data
import { getSearchFilterScopedVar } from '../../../features/variables/utils';
import { AnnotationEditor } from './components/AnnotationsEditor';
import { convertToGraphiteQueryObject } from './components/helpers';
import gfunc, { FuncDefs, FuncInstance } from './gfunc';
import GraphiteQueryModel from './graphite_query';
import { prepareAnnotation } from './migrations';
// Types
import {
GraphiteLokiMapping,
GraphiteMetricLokiMatcher,
GraphiteOptions,
GraphiteQuery,
GraphiteQueryRequest,
GraphiteQueryImportConfiguration,
GraphiteQueryType,
GraphiteType,
@ -97,6 +100,10 @@ export class GraphiteDatasource
this.funcDefs = null;
this.funcDefsPromise = null;
this._seriesRefLetters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
this.annotations = {
QueryEditor: AnnotationEditor,
prepareAnnotation,
};
}
getQueryOptionsInfo() {
@ -182,40 +189,62 @@ export class GraphiteDatasource
}
query(options: DataQueryRequest<GraphiteQuery>): Observable<DataQueryResponse> {
const graphOptions = {
from: this.translateTime(options.range.from, false, options.timezone),
until: this.translateTime(options.range.to, true, options.timezone),
targets: options.targets,
format: (options as any).format ?? 'json',
cacheTimeout: options.cacheTimeout || this.cacheTimeout,
maxDataPoints: options.maxDataPoints,
};
const streams: Array<Observable<DataQueryResponse>> = [];
const params = this.buildGraphiteParams(graphOptions, options.scopedVars);
if (params.length === 0) {
for (const target of options.targets) {
// hiding target is handled in buildGraphiteParams
if (target.fromAnnotations) {
streams.push(
new Observable((subscriber) => {
this.annotationEvents(options.range, target)
.then((events) => subscriber.next({ data: [toDataFrame(events)] }))
.catch((ex) => subscriber.error(new Error(ex)))
.finally(() => subscriber.complete());
})
);
} else {
// handle the queries here
const graphOptions = {
from: this.translateTime(options.range.from, false, options.timezone),
until: this.translateTime(options.range.to, true, options.timezone),
targets: options.targets,
format: (options as GraphiteQueryRequest).format,
cacheTimeout: options.cacheTimeout || this.cacheTimeout,
maxDataPoints: options.maxDataPoints,
};
const params = this.buildGraphiteParams(graphOptions, options.scopedVars);
if (params.length === 0) {
return of({ data: [] });
}
if (this.isMetricTank) {
params.push('meta=true');
}
const httpOptions: any = {
method: 'POST',
url: '/render',
data: params.join('&'),
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
};
this.addTracingHeaders(httpOptions, options);
if (options.panelId) {
httpOptions.requestId = this.name + '.panelId.' + options.panelId;
}
streams.push(this.doGraphiteRequest(httpOptions).pipe(map(this.convertResponseToDataFrames)));
}
}
if (streams.length === 0) {
return of({ data: [] });
}
if (this.isMetricTank) {
params.push('meta=true');
}
const httpOptions: any = {
method: 'POST',
url: '/render',
data: params.join('&'),
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
};
this.addTracingHeaders(httpOptions, options);
if (options.panelId) {
httpOptions.requestId = this.name + '.panelId.' + options.panelId;
}
return this.doGraphiteRequest(httpOptions).pipe(map(this.convertResponseToDataFrames));
return merge(...streams);
}
addTracingHeaders(httpOptions: { headers: any }, options: { dashboardId?: number; panelId?: number }) {
@ -330,13 +359,13 @@ export class GraphiteDatasource
return expandedQueries;
}
annotationQuery(options: any) {
// Graphite metric as annotation
if (options.annotation.target) {
const target = this.templateSrv.replace(options.annotation.target, {}, 'glob');
annotationEvents(range: any, target: any) {
if (target.target) {
// Graphite query as target as annotation
const targetAnnotation = this.templateSrv.replace(target.target, {}, 'glob');
const graphiteQuery = {
range: options.range,
targets: [{ target: target }],
range: range,
targets: [{ target: targetAnnotation }],
format: 'json',
maxDataPoints: 100,
} as unknown as DataQueryRequest<GraphiteQuery>;
@ -358,7 +387,7 @@ export class GraphiteDatasource
}
list.push({
annotation: options.annotation,
annotation: target,
time,
title: target.name,
});
@ -370,9 +399,9 @@ export class GraphiteDatasource
)
);
} else {
// Graphite event as annotation
const tags = this.templateSrv.replace(options.annotation.tags);
return this.events({ range: options.range, tags: tags }).then((results: any) => {
// Graphite event/tag as annotation
const tags = this.templateSrv.replace(target.tags?.join(' '));
return this.events({ range: range, tags: tags }).then((results: any) => {
const list = [];
if (!isArray(results.data)) {
console.error(`Unable to get annotations from ${results.url}.`);
@ -387,7 +416,7 @@ export class GraphiteDatasource
}
list.push({
annotation: options.annotation,
annotation: target,
time: e.when * 1000,
title: e.what,
tags: tags,

View File

@ -0,0 +1,39 @@
type LegacyAnnotation = {
target?: string;
tags?: string;
};
// this becomes the target in the migrated annotations
const migrateLegacyAnnotation = (json: LegacyAnnotation) => {
// return the target annotation
if (typeof json.target === 'string' && json.target) {
return {
fromAnnotations: true,
target: json.target,
textEditor: true,
};
}
// return the tags annotation
return {
queryType: 'tags',
tags: (json.tags || '').split(' '),
fromAnnotations: true,
};
};
// eslint-ignore-next-line
export const prepareAnnotation = (json: any) => {
// annotation attributes are either 'tags' or 'target'(a graphite query string)
// because the new annotations will also have a target attribute, {}
// we need to handle the ambiguous 'target' when migrating legacy annotations
// so, to migrate legacy annotations
// we check that target is a string
// or
// there is a tags attribute with no target
const resultingTarget = json.target && typeof json.target !== 'string' ? json.target : migrateLegacyAnnotation(json);
json.target = resultingTarget;
return json;
};

View File

@ -6,13 +6,8 @@ import { MetricTankMetaInspector } from './components/MetricTankMetaInspector';
import { ConfigEditor } from './configuration/ConfigEditor';
import { GraphiteDatasource } from './datasource';
class AnnotationsQueryCtrl {
static templateUrl = 'partials/annotations.editor.html';
}
export const plugin = new DataSourcePlugin(GraphiteDatasource)
.setQueryEditor(GraphiteQueryEditor)
.setConfigEditor(ConfigEditor)
.setVariableQueryEditor(GraphiteVariableEditor)
.setMetadataInspector(MetricTankMetaInspector)
.setAnnotationQueryCtrl(AnnotationsQueryCtrl);
.setMetadataInspector(MetricTankMetaInspector);

View File

@ -1,13 +0,0 @@
<div class="gf-form-group">
<div class="gf-form">
<span class="gf-form-label width-12">Graphite query</span>
<input type="text" class="gf-form-input" ng-model='ctrl.annotation.target' placeholder="Example: statsd.application.counters.*.count"></input>
</div>
<h5 class="section-heading">Or</h5>
<div class="gf-form">
<span class="gf-form-label width-12">Graphite events tags</span>
<input type="text" class="gf-form-input" ng-model='ctrl.annotation.tags' placeholder="Example: event_tag_name"></input>
</div>
</div>

View File

@ -1,4 +1,4 @@
import { DataQuery, DataSourceJsonData, TimeRange } from '@grafana/data';
import { DataQuery, DataQueryRequest, DataSourceJsonData, TimeRange } from '@grafana/data';
import { TemplateSrv } from '../../../features/templating/template_srv';
@ -11,7 +11,11 @@ export enum GraphiteQueryType {
}
export interface GraphiteQuery extends DataQuery {
queryType?: string;
textEditor?: boolean;
target?: string;
tags?: string[];
fromAnnotations?: boolean;
}
export interface GraphiteOptions extends DataSourceJsonData {
@ -93,3 +97,7 @@ export type GraphiteQueryEditorDependencies = {
// schedule onChange/onRunQuery after the reducer actions finishes
refresh: () => void;
};
export interface GraphiteQueryRequest extends DataQueryRequest {
format: string;
}