mirror of
https://github.com/grafana/grafana.git
synced 2024-11-30 20:54:22 -06:00
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:
parent
2fef8e6f2c
commit
085258c035
@ -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"],
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
@ -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();
|
||||
});
|
||||
}
|
||||
|
31
public/app/plugins/datasource/influxdb/migrations.ts
Normal file
31
public/app/plugins/datasource/influxdb/migrations.ts
Normal 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;
|
||||
};
|
@ -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);
|
||||
|
@ -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>
|
@ -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],
|
||||
|
@ -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', () => {
|
||||
|
@ -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;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user