Influxdb Datasource: Remove angular dependencies for Influxdb influxql annotations (#52546)

* migrate influxQL annotations to react and build new annotation editor

* use es-lint ignore for any type in migration

* changes for PR comments

* handle annotation editor on load error without query

* correct label for ann editor query

* add null coalesce operator and remove comment

* fix tooltip
This commit is contained in:
Brendan O'Handley 2022-08-17 13:20:46 -04:00 committed by GitHub
parent 2fef8e6f2c
commit 085258c035
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 184 additions and 62 deletions

View File

@ -7116,13 +7116,13 @@ exports[`better eslint`] = {
[0, 0, 0, "Unexpected any. Specify a different type.", "14"],
[0, 0, 0, "Unexpected any. Specify a different type.", "15"],
[0, 0, 0, "Unexpected any. Specify a different type.", "16"],
[0, 0, 0, "Unexpected any. Specify a different type.", "17"],
[0, 0, 0, "Do not use any type assertions.", "18"],
[0, 0, 0, "Do not use any type assertions.", "17"],
[0, 0, 0, "Unexpected any. Specify a different type.", "18"],
[0, 0, 0, "Unexpected any. Specify a different type.", "19"],
[0, 0, 0, "Unexpected any. Specify a different type.", "20"],
[0, 0, 0, "Unexpected any. Specify a different type.", "21"],
[0, 0, 0, "Unexpected any. Specify a different type.", "22"],
[0, 0, 0, "Do not use any type assertions.", "23"],
[0, 0, 0, "Do not use any type assertions.", "22"],
[0, 0, 0, "Unexpected any. Specify a different type.", "23"],
[0, 0, 0, "Unexpected any. Specify a different type.", "24"],
[0, 0, 0, "Unexpected any. Specify a different type.", "25"],
[0, 0, 0, "Unexpected any. Specify a different type.", "26"],
@ -7132,8 +7132,7 @@ exports[`better eslint`] = {
[0, 0, 0, "Unexpected any. Specify a different type.", "30"],
[0, 0, 0, "Unexpected any. Specify a different type.", "31"],
[0, 0, 0, "Unexpected any. Specify a different type.", "32"],
[0, 0, 0, "Unexpected any. Specify a different type.", "33"],
[0, 0, 0, "Unexpected any. Specify a different type.", "34"]
[0, 0, 0, "Unexpected any. Specify a different type.", "33"]
],
"public/app/plugins/datasource/influxdb/influx_query_model.ts:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
@ -7179,6 +7178,9 @@ exports[`better eslint`] = {
[0, 0, 0, "Unexpected any. Specify a different type.", "17"],
[0, 0, 0, "Unexpected any. Specify a different type.", "18"]
],
"public/app/plugins/datasource/influxdb/migrations.ts:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
],
"public/app/plugins/datasource/influxdb/query_builder.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,83 @@
import React, { useState } from 'react';
import { QueryEditorProps } from '@grafana/data';
import { InlineFormLabel, Input } from '@grafana/ui';
import { InfluxQuery, InfluxOptions } from '../types';
import InfluxDatasource from './../datasource';
export const AnnotationEditor = (props: QueryEditorProps<InfluxDatasource, InfluxQuery, InfluxOptions>) => {
const { query, onChange } = props;
const [eventQuery, setEventQuery] = useState<string>(query.query ?? '');
const [textColumn, setTextColumn] = useState<string>(query.textColumn ?? '');
const [tagsColumn, setTagsColumn] = useState<string>(query.tagsColumn ?? '');
const [timeEndColumn, setTimeEndColumn] = useState<string>(query?.timeEndColumn ?? '');
const [titleColumn] = useState<string>(query?.titleColumn ?? '');
const updateValue = <K extends keyof InfluxQuery, V extends InfluxQuery[K]>(key: K, val: V) => {
onChange({
...query,
[key]: val,
fromAnnotations: true,
textEditor: true,
});
};
return (
<div className="gf-form-group">
<div className="gf-form">
<InlineFormLabel width={12}>InfluxQL Query</InlineFormLabel>
<Input
value={eventQuery}
onChange={(e) => setEventQuery(e.currentTarget.value ?? '')}
onBlur={() => updateValue('query', eventQuery)}
placeholder="select text from events where $timeFilter limit 1000"
/>
</div>
<InlineFormLabel
width={12}
tooltip={
<div>
If your influxdb query returns more than one field you need to specify the column names below. An annotation
event is composed of a title, tags, and an additional text field. Optionally you can map the timeEnd column
for region annotation usage.
</div>
}
>
Field mappings
</InlineFormLabel>
<div className="gf-form-group">
<div className="gf-form-inline">
<div className="gf-form">
<InlineFormLabel width={12}>Text</InlineFormLabel>
<Input
value={textColumn}
onChange={(e) => setTextColumn(e.currentTarget.value ?? '')}
onBlur={() => updateValue('textColumn', textColumn)}
/>
</div>
<div className="gf-form">
<InlineFormLabel width={12}>Tags</InlineFormLabel>
<Input
value={tagsColumn}
onChange={(e) => setTagsColumn(e.currentTarget.value ?? '')}
onBlur={() => updateValue('tagsColumn', tagsColumn)}
/>
</div>
<div className="gf-form">
<InlineFormLabel width={12}>TimeEnd</InlineFormLabel>
<Input
value={timeEndColumn}
onChange={(e) => setTimeEndColumn(e.currentTarget.value ?? '')}
onBlur={() => updateValue('timeEndColumn', timeEndColumn)}
/>
</div>
<div className="gf-form ng-hide">
<InlineFormLabel width={12}>Title</InlineFormLabel>
<Input defaultValue={titleColumn} />
</div>
</div>
</div>
</div>
);
};

View File

@ -1,10 +1,9 @@
import { cloneDeep, extend, groupBy, has, isString, map as _map, omit, pick, reduce } from 'lodash';
import { lastValueFrom, Observable, of, throwError } from 'rxjs';
import { lastValueFrom, merge, Observable, of, throwError } from 'rxjs';
import { catchError, map } from 'rxjs/operators';
import {
AnnotationEvent,
AnnotationQueryRequest,
ArrayVector,
DataFrame,
DataQueryError,
@ -19,6 +18,7 @@ import {
TIME_SERIES_TIME_FIELD_NAME,
TIME_SERIES_VALUE_FIELD_NAME,
TimeSeries,
toDataFrame,
} from '@grafana/data';
import {
BackendDataSourceResponse,
@ -30,10 +30,12 @@ import {
import config from 'app/core/config';
import { getTemplateSrv, TemplateSrv } from 'app/features/templating/template_srv';
import { AnnotationEditor } from './components/AnnotationEditor';
import { FluxQueryEditor } from './components/FluxQueryEditor';
import { BROWSER_MODE_DISABLED_MESSAGE } from './constants';
import InfluxQueryModel from './influx_query_model';
import InfluxSeries from './influx_series';
import { prepareAnnotation } from './migrations';
import { buildRawQuery } from './queryUtils';
import { InfluxQueryBuilder } from './query_builder';
import ResponseParser from './response_parser';
@ -156,6 +158,11 @@ export default class InfluxDatasource extends DataSourceWithBackend<InfluxQuery,
this.annotations = {
QueryEditor: FluxQueryEditor,
};
} else {
this.annotations = {
QueryEditor: AnnotationEditor,
prepareAnnotation,
};
}
}
@ -259,6 +266,26 @@ export default class InfluxDatasource extends DataSourceWithBackend<InfluxQuery,
* The unchanged pre 7.1 query implementation
*/
classicQuery(options: any): Observable<DataQueryResponse> {
// migrate annotations
if (options.targets.some((target: InfluxQuery) => target.fromAnnotations)) {
const streams: Array<Observable<DataQueryResponse>> = [];
for (const target of options.targets) {
if (target.query) {
streams.push(
new Observable((subscriber) => {
this.annotationEvents(options, target)
.then((events) => subscriber.next({ data: [toDataFrame(events)] }))
.catch((ex) => subscriber.error(new Error(ex)))
.finally(() => subscriber.complete());
})
);
}
}
return merge(...streams);
}
let timeFilter = this.getTimeFilter(options);
const scopedVars = options.scopedVars;
const targets = cloneDeep(options.targets);
@ -353,7 +380,7 @@ export default class InfluxDatasource extends DataSourceWithBackend<InfluxQuery,
);
}
async annotationQuery(options: AnnotationQueryRequest<any>): Promise<AnnotationEvent[]> {
async annotationEvents(options: DataQueryRequest, annotation: InfluxQuery): Promise<AnnotationEvent[]> {
if (this.isFlux) {
return Promise.reject({
message: 'Flux requires the standard annotation query',
@ -361,7 +388,7 @@ export default class InfluxDatasource extends DataSourceWithBackend<InfluxQuery,
}
// InfluxQL puts a query string on the annotation
if (!options.annotation.query) {
if (!annotation.query) {
return Promise.reject({
message: 'Query missing in annotation definition',
});
@ -372,7 +399,7 @@ export default class InfluxDatasource extends DataSourceWithBackend<InfluxQuery,
const target: InfluxQuery = {
refId: 'metricFindQuery',
datasource: this.getRef(),
query: this.templateSrv.replace(options.annotation.query ?? '', undefined, 'regex'),
query: this.templateSrv.replace(annotation.query, undefined, 'regex'),
rawQuery: true,
};
@ -386,19 +413,19 @@ export default class InfluxDatasource extends DataSourceWithBackend<InfluxQuery,
to: options.range.to.valueOf().toString(),
queries: [target],
},
requestId: options.annotation.name,
requestId: annotation.name,
})
.pipe(
map(
async (res: FetchResponse<BackendDataSourceResponse>) =>
await this.responseParser.transformAnnotationResponse(options, res, target)
await this.responseParser.transformAnnotationResponse(annotation, res, target)
)
)
);
}
const timeFilter = this.getTimeFilter({ rangeRaw: options.rangeRaw, timezone: options.dashboard.timezone });
let query = options.annotation.query.replace('$timeFilter', timeFilter);
const timeFilter = this.getTimeFilter({ rangeRaw: options.range.raw, timezone: options.timezone });
let query = annotation.query.replace('$timeFilter', timeFilter);
query = this.templateSrv.replace(query, undefined, 'regex');
return lastValueFrom(this._seriesQuery(query, options)).then((data: any) => {
@ -407,7 +434,7 @@ export default class InfluxDatasource extends DataSourceWithBackend<InfluxQuery,
}
return new InfluxSeries({
series: data.results[0].series,
annotation: options.annotation,
annotation: annotation,
}).getAnnotations();
});
}

View File

@ -0,0 +1,31 @@
type LegacyAnnotation = {
query?: string;
queryType?: string;
fromAnnotations?: boolean;
tagsColumn?: string;
textColumn?: string;
timeEndColumn?: string;
titleColumn?: string;
name?: string;
};
// this becomes the target in the migrated annotations
const migrateLegacyAnnotation = (json: LegacyAnnotation) => {
return {
query: json.query ?? '',
queryType: 'tags',
fromAnnotations: true,
tagsColumn: json.tagsColumn ?? '',
textColumn: json.textColumn ?? '',
timeEndColumn: json.timeEndColumn ?? '',
titleColumn: json.titleColumn ?? '',
name: json.name ?? '',
};
};
// eslint-ignore-next-line
export const prepareAnnotation = (json: any) => {
json.target = json.target ?? migrateLegacyAnnotation(json);
return json;
};

View File

@ -6,13 +6,8 @@ import { QueryEditor } from './components/QueryEditor';
import VariableQueryEditor from './components/VariableQueryEditor';
import InfluxDatasource from './datasource';
class InfluxAnnotationsQueryCtrl {
static templateUrl = 'partials/annotations.editor.html';
}
export const plugin = new DataSourcePlugin(InfluxDatasource)
.setConfigEditor(ConfigEditor)
.setQueryEditor(QueryEditor)
.setAnnotationQueryCtrl(InfluxAnnotationsQueryCtrl)
.setVariableQueryEditor(VariableQueryEditor)
.setQueryEditorHelp(InfluxStartPage);

View File

@ -1,28 +0,0 @@
<div class="gf-form-group">
<div class="gf-form">
<input type="text" class="gf-form-input" ng-model='ctrl.annotation.query' placeholder="select text from events where $timeFilter limit 1000"></input>
</div>
</div>
<h5 class="section-heading">Field mappings <tip>If your influxdb query returns more than one field you need to specify the column names below. An annotation event is composed of a title, tags, and an additional text field. Optionally you can map the timeEnd column for region annotation usage.</tip></h5>
<div class="gf-form-group">
<div class="gf-form-inline">
<div class="gf-form">
<span class="gf-form-label width-4">Text</span>
<input type="text" class="gf-form-input max-width-10" ng-model='ctrl.annotation.textColumn' placeholder=""></input>
</div>
<div class="gf-form">
<span class="gf-form-label width-4">Tags</span>
<input type="text" class="gf-form-input max-width-10" ng-model='ctrl.annotation.tagsColumn' placeholder=""></input>
</div>
<div class="gf-form">
<span class="gf-form-label width-4">TimeEnd</span>
<input type="text" class="gf-form-input max-width-10" ng-model='ctrl.annotation.timeEndColumn' placeholder=""></input>
</div>
<div class="gf-form" ng-show="ctrl.annotation.titleColumn">
<span class="gf-form-label width-4">Title <em class="muted">(deprecated)</em></span>
<input type="text" class="gf-form-input max-width-10" ng-model='ctrl.annotation.titleColumn' placeholder=""></input>
</div>
</div>
</div>

View File

@ -88,7 +88,7 @@ export default class ResponseParser {
return table;
}
async transformAnnotationResponse(options: any, data: any, target: InfluxQuery): Promise<AnnotationEvent[]> {
async transformAnnotationResponse(annotation: any, data: any, target: InfluxQuery): Promise<AnnotationEvent[]> {
const rsp = toDataQueryResponse(data, [target] as DataQuery[]);
if (rsp) {
@ -105,19 +105,19 @@ export default class ResponseParser {
timeCol = index;
return;
}
if (column.text === options.annotation.titleColumn) {
if (column.text === annotation.titleColumn) {
titleCol = index;
return;
}
if (colContainsTag(column.text, options.annotation.tagsColumn)) {
if (colContainsTag(column.text, annotation.tagsColumn)) {
tagsCol.push(index);
return;
}
if (column.text.includes(options.annotation.textColumn)) {
if (column.text.includes(annotation.textColumn)) {
textCol = index;
return;
}
if (column.text === options.annotation.timeEndColumn) {
if (column.text === annotation.timeEndColumn) {
timeEndCol = index;
return;
}
@ -129,7 +129,7 @@ export default class ResponseParser {
each(table.rows, (value) => {
const data = {
annotation: options.annotation,
annotation: annotation,
time: +new Date(value[timeCol]),
title: value[titleCol],
timeEnd: value[timeEndCol],

View File

@ -306,13 +306,16 @@ describe('influxdb response parser', () => {
const fetchMock = jest.spyOn(backendSrv, 'fetch');
const annotation = {
fromAnnotations: true,
name: 'Anno',
query: 'select * from logs where time >= now() - 15m and time <= now()',
textColumn: 'textColumn',
tagsColumn: 'host,path',
};
const queryOptions: any = {
annotation: {
name: 'Anno',
query: 'select * from logs where time >= now() - 15m and time <= now()',
textColumn: 'textColumn',
tagsColumn: 'host,path',
},
targets: [annotation],
range: {
from: '2018-01-01T00:00:00Z',
to: '2018-01-02T00:00:00Z',
@ -424,7 +427,7 @@ describe('influxdb response parser', () => {
ctx.ds = new InfluxDatasource(ctx.instanceSettings, templateSrv);
ctx.ds.access = 'proxy';
config.featureToggles.influxdbBackendMigration = true;
response = await ctx.ds.annotationQuery(queryOptions);
response = await ctx.ds.annotationEvents(queryOptions, annotation);
});
it('should return annotation list', () => {

View File

@ -61,4 +61,13 @@ export interface InfluxQuery extends DataQuery {
rawQuery?: boolean;
query?: string;
alias?: string;
// for migrated InfluxQL annotations
queryType?: string;
fromAnnotations?: boolean;
tagsColumn?: string;
textColumn?: string;
timeEndColumn?: string;
titleColumn?: string;
name?: string;
textEditor?: boolean;
}