grafana/public/app/features/annotations/standardAnnotationSupport.ts
Torkel Ödegaard 3ee26df41e
PublicDashboards: Variables refactor (#73476)
Co-authored-by: Juan Cabanas <juan.cabanas@grafana.com>
Co-authored-by: Ezequiel Victorero <ezequiel.victorero@grafana.com>
Co-authored-by: Ryan McKinley <ryantxu@gmail.com>
2023-08-25 13:56:02 -05:00

280 lines
6.9 KiB
TypeScript

import { isString } from 'lodash';
import { Observable, of, OperatorFunction } from 'rxjs';
import { map, mergeMap } from 'rxjs/operators';
import {
AnnotationEvent,
AnnotationEventFieldSource,
AnnotationEventMappings,
AnnotationQuery,
AnnotationSupport,
DataFrame,
DataSourceApi,
DataTransformContext,
Field,
FieldType,
getFieldDisplayName,
KeyValue,
standardTransformers,
} from '@grafana/data';
import { config } from 'app/core/config';
export const standardAnnotationSupport: AnnotationSupport = {
/**
* Assume the stored value is standard model.
*/
prepareAnnotation: (json: any) => {
if (isString(json?.query)) {
const { query, ...rest } = json;
return {
...rest,
target: {
refId: 'annotation_query',
query,
},
mappings: {},
};
}
return json;
},
/**
* Default will just return target from the annotation.
*/
prepareQuery: (anno: AnnotationQuery) => anno.target,
/**
* Provides default processing from dataFrame to annotation events.
*/
processEvents: (anno: AnnotationQuery, data: DataFrame[]) => {
return getAnnotationsFromData(data, anno.mappings);
},
};
/**
* Flatten all frames into a single frame with mergeTransformer.
*/
export function singleFrameFromPanelData(): OperatorFunction<DataFrame[], DataFrame | undefined> {
return (source) =>
source.pipe(
mergeMap((data) => {
if (!data?.length) {
return of(undefined);
}
if (data.length === 1) {
return of(data[0]);
}
const ctx: DataTransformContext = {
interpolate: (v: string) => v,
};
return of(data).pipe(
standardTransformers.mergeTransformer.operator({}, ctx),
map((d) => d[0])
);
})
);
}
interface AnnotationEventFieldSetter {
key: keyof AnnotationEvent;
field?: Field;
text?: string;
regex?: RegExp;
split?: string; // for tags
}
export interface AnnotationFieldInfo {
key: keyof AnnotationEvent;
split?: string;
field?: (frame: DataFrame) => Field | undefined;
placeholder?: string;
help?: string;
}
// These fields get added to the standard UI
export const annotationEventNames: AnnotationFieldInfo[] = [
{
key: 'time',
field: (frame: DataFrame) => frame.fields.find((f) => f.type === FieldType.time),
placeholder: 'time, or the first time field',
},
{ key: 'timeEnd', help: 'When this field is defined, the annotation will be treated as a range' },
{
key: 'title',
},
{
key: 'text',
field: (frame: DataFrame) => frame.fields.find((f) => f.type === FieldType.string),
placeholder: 'text, or the first text field',
},
{ key: 'tags', split: ',', help: 'The results will be split on comma (,)' },
{
key: 'id',
},
];
export const publicDashboardEventNames: AnnotationFieldInfo[] = [
{
key: 'color',
},
{
key: 'isRegion',
},
{
key: 'source',
},
];
// Given legacy infrastructure, alert events are passed though the same annotation
// pipeline, but include fields that should not be exposed generally
const alertEventAndAnnotationFields: AnnotationFieldInfo[] = [
...(config.publicDashboardAccessToken ? publicDashboardEventNames : []),
...annotationEventNames,
{ key: 'userId' },
{ key: 'login' },
{ key: 'email' },
{ key: 'prevState' },
{ key: 'newState' },
{ key: 'data' as any },
{ key: 'panelId' },
{ key: 'alertId' },
{ key: 'dashboardId' },
{ key: 'dashboardUID' },
];
export function getAnnotationsFromData(
data: DataFrame[],
options?: AnnotationEventMappings
): Observable<AnnotationEvent[]> {
return of(data).pipe(
singleFrameFromPanelData(),
map((frame) => {
if (!frame?.length) {
return [];
}
let hasTime = false;
let hasText = false;
const byName: KeyValue<Field> = {};
for (const f of frame.fields) {
const name = getFieldDisplayName(f, frame);
byName[name.toLowerCase()] = f;
}
if (!options) {
options = {};
}
const fields: AnnotationEventFieldSetter[] = [];
for (const evts of alertEventAndAnnotationFields) {
const opt = options[evts.key] || {}; //AnnotationEventFieldMapping
if (opt.source === AnnotationEventFieldSource.Skip) {
continue;
}
const setter: AnnotationEventFieldSetter = { key: evts.key, split: evts.split };
if (opt.source === AnnotationEventFieldSource.Text) {
setter.text = opt.value;
} else {
const lower = (opt.value || evts.key).toLowerCase();
setter.field = byName[lower];
if (!setter.field && evts.field) {
setter.field = evts.field(frame);
}
}
if (setter.field || setter.text) {
fields.push(setter);
if (setter.key === 'time') {
hasTime = true;
} else if (setter.key === 'text') {
hasText = true;
}
}
}
if (!hasTime || !hasText) {
console.error('Cannot process annotation fields. No time or text present.');
return [];
}
// Add each value to the string
const events: AnnotationEvent[] = [];
for (let i = 0; i < frame.length; i++) {
const anno: AnnotationEvent = {
type: 'default',
color: 'red',
};
for (const f of fields) {
let v = undefined;
if (f.text) {
v = f.text; // TODO support templates!
} else if (f.field) {
v = f.field.values[i];
if (v !== undefined && f.regex) {
const match = f.regex.exec(v);
if (match) {
v = match[1] ? match[1] : match[0];
}
}
}
if (v !== null && v !== undefined) {
if (f.split && typeof v === 'string') {
v = v.split(',');
}
anno[f.key] = v;
}
}
events.push(anno);
}
return events;
})
);
}
// These opt outs are here only for quicker and easier migration to react based annotations editors and because
// annotation support API needs some work to support less "standard" editors like prometheus and here it is not
// polluting public API.
const legacyRunner = [
'prometheus',
'loki',
'elasticsearch',
'grafana-opensearch-datasource', // external
];
/**
* Opt out of using the default mapping functionality on frontend.
*/
export function shouldUseMappingUI(datasource: DataSourceApi): boolean {
const { type } = datasource;
return !(
type === 'datasource' || // ODD behavior for "-- Grafana --" datasource
legacyRunner.includes(type)
);
}
/**
* Use legacy runner. Used only as an escape hatch for easier transition to React based annotation editor.
*/
export function shouldUseLegacyRunner(datasource: DataSourceApi): boolean {
const { type } = datasource;
return legacyRunner.includes(type);
}