123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254 |
- import { isString } from 'lodash';
- import { Observable, of, OperatorFunction } from 'rxjs';
- import { map, mergeMap } from 'rxjs/operators';
- import {
- AnnotationEvent,
- AnnotationEventFieldSource,
- AnnotationEventMappings,
- AnnotationQuery,
- AnnotationSupport,
- DataFrame,
- DataSourceApi,
- Field,
- FieldType,
- getFieldDisplayName,
- KeyValue,
- standardTransformers,
- } from '@grafana/data';
- 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 as AnnotationQuery;
- },
- /**
- * 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]);
- }
- return of(data).pipe(
- standardTransformers.mergeTransformer.operator({}),
- 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',
- },
- ];
- // Given legacy infrastructure, alert events are passed though the same annotation
- // pipeline, but include fields that should not be exposed generally
- const alertEventAndAnnotationFields: AnnotationFieldInfo[] = [
- ...annotationEventNames,
- { key: 'userId' },
- { key: 'login' },
- { key: 'email' },
- { key: 'prevState' },
- { key: 'newState' },
- { key: 'data' as any },
- { key: 'panelId' },
- { key: 'alertId' },
- { key: 'dashboardId' },
- ];
- 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) {
- return []; // throw an error?
- }
- // 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: any = undefined;
- if (f.text) {
- v = f.text; // TODO support templates!
- } else if (f.field) {
- v = f.field.values.get(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 as any)[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.
- /**
- * Opt out of using the default mapping functionality on frontend.
- */
- export function shouldUseMappingUI(datasource: DataSourceApi): boolean {
- return (
- datasource.type !== 'prometheus' &&
- datasource.type !== 'grafana-opensearch-datasource' &&
- datasource.type !== 'grafana-splunk-datasource'
- );
- }
- /**
- * Use legacy runner. Used only as an escape hatch for easier transition to React based annotation editor.
- */
- export function shouldUseLegacyRunner(datasource: DataSourceApi): boolean {
- return (
- datasource.type === 'prometheus' ||
- datasource.type === 'grafana-opensearch-datasource' ||
- datasource.type === 'grafana-splunk-datasource'
- );
- }
|