standardAnnotationSupport.ts 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254
  1. import { isString } from 'lodash';
  2. import { Observable, of, OperatorFunction } from 'rxjs';
  3. import { map, mergeMap } from 'rxjs/operators';
  4. import {
  5. AnnotationEvent,
  6. AnnotationEventFieldSource,
  7. AnnotationEventMappings,
  8. AnnotationQuery,
  9. AnnotationSupport,
  10. DataFrame,
  11. DataSourceApi,
  12. Field,
  13. FieldType,
  14. getFieldDisplayName,
  15. KeyValue,
  16. standardTransformers,
  17. } from '@grafana/data';
  18. export const standardAnnotationSupport: AnnotationSupport = {
  19. /**
  20. * Assume the stored value is standard model.
  21. */
  22. prepareAnnotation: (json: any) => {
  23. if (isString(json?.query)) {
  24. const { query, ...rest } = json;
  25. return {
  26. ...rest,
  27. target: {
  28. refId: 'annotation_query',
  29. query,
  30. },
  31. mappings: {},
  32. };
  33. }
  34. return json as AnnotationQuery;
  35. },
  36. /**
  37. * Default will just return target from the annotation.
  38. */
  39. prepareQuery: (anno: AnnotationQuery) => anno.target,
  40. /**
  41. * Provides default processing from dataFrame to annotation events.
  42. */
  43. processEvents: (anno: AnnotationQuery, data: DataFrame[]) => {
  44. return getAnnotationsFromData(data, anno.mappings);
  45. },
  46. };
  47. /**
  48. * Flatten all frames into a single frame with mergeTransformer.
  49. */
  50. export function singleFrameFromPanelData(): OperatorFunction<DataFrame[], DataFrame | undefined> {
  51. return (source) =>
  52. source.pipe(
  53. mergeMap((data) => {
  54. if (!data?.length) {
  55. return of(undefined);
  56. }
  57. if (data.length === 1) {
  58. return of(data[0]);
  59. }
  60. return of(data).pipe(
  61. standardTransformers.mergeTransformer.operator({}),
  62. map((d) => d[0])
  63. );
  64. })
  65. );
  66. }
  67. interface AnnotationEventFieldSetter {
  68. key: keyof AnnotationEvent;
  69. field?: Field;
  70. text?: string;
  71. regex?: RegExp;
  72. split?: string; // for tags
  73. }
  74. export interface AnnotationFieldInfo {
  75. key: keyof AnnotationEvent;
  76. split?: string;
  77. field?: (frame: DataFrame) => Field | undefined;
  78. placeholder?: string;
  79. help?: string;
  80. }
  81. // These fields get added to the standard UI
  82. export const annotationEventNames: AnnotationFieldInfo[] = [
  83. {
  84. key: 'time',
  85. field: (frame: DataFrame) => frame.fields.find((f) => f.type === FieldType.time),
  86. placeholder: 'time, or the first time field',
  87. },
  88. { key: 'timeEnd', help: 'When this field is defined, the annotation will be treated as a range' },
  89. {
  90. key: 'title',
  91. },
  92. {
  93. key: 'text',
  94. field: (frame: DataFrame) => frame.fields.find((f) => f.type === FieldType.string),
  95. placeholder: 'text, or the first text field',
  96. },
  97. { key: 'tags', split: ',', help: 'The results will be split on comma (,)' },
  98. {
  99. key: 'id',
  100. },
  101. ];
  102. // Given legacy infrastructure, alert events are passed though the same annotation
  103. // pipeline, but include fields that should not be exposed generally
  104. const alertEventAndAnnotationFields: AnnotationFieldInfo[] = [
  105. ...annotationEventNames,
  106. { key: 'userId' },
  107. { key: 'login' },
  108. { key: 'email' },
  109. { key: 'prevState' },
  110. { key: 'newState' },
  111. { key: 'data' as any },
  112. { key: 'panelId' },
  113. { key: 'alertId' },
  114. { key: 'dashboardId' },
  115. ];
  116. export function getAnnotationsFromData(
  117. data: DataFrame[],
  118. options?: AnnotationEventMappings
  119. ): Observable<AnnotationEvent[]> {
  120. return of(data).pipe(
  121. singleFrameFromPanelData(),
  122. map((frame) => {
  123. if (!frame?.length) {
  124. return [];
  125. }
  126. let hasTime = false;
  127. let hasText = false;
  128. const byName: KeyValue<Field> = {};
  129. for (const f of frame.fields) {
  130. const name = getFieldDisplayName(f, frame);
  131. byName[name.toLowerCase()] = f;
  132. }
  133. if (!options) {
  134. options = {};
  135. }
  136. const fields: AnnotationEventFieldSetter[] = [];
  137. for (const evts of alertEventAndAnnotationFields) {
  138. const opt = options[evts.key] || {}; //AnnotationEventFieldMapping
  139. if (opt.source === AnnotationEventFieldSource.Skip) {
  140. continue;
  141. }
  142. const setter: AnnotationEventFieldSetter = { key: evts.key, split: evts.split };
  143. if (opt.source === AnnotationEventFieldSource.Text) {
  144. setter.text = opt.value;
  145. } else {
  146. const lower = (opt.value || evts.key).toLowerCase();
  147. setter.field = byName[lower];
  148. if (!setter.field && evts.field) {
  149. setter.field = evts.field(frame);
  150. }
  151. }
  152. if (setter.field || setter.text) {
  153. fields.push(setter);
  154. if (setter.key === 'time') {
  155. hasTime = true;
  156. } else if (setter.key === 'text') {
  157. hasText = true;
  158. }
  159. }
  160. }
  161. if (!hasTime || !hasText) {
  162. return []; // throw an error?
  163. }
  164. // Add each value to the string
  165. const events: AnnotationEvent[] = [];
  166. for (let i = 0; i < frame.length; i++) {
  167. const anno: AnnotationEvent = {
  168. type: 'default',
  169. color: 'red',
  170. };
  171. for (const f of fields) {
  172. let v: any = undefined;
  173. if (f.text) {
  174. v = f.text; // TODO support templates!
  175. } else if (f.field) {
  176. v = f.field.values.get(i);
  177. if (v !== undefined && f.regex) {
  178. const match = f.regex.exec(v);
  179. if (match) {
  180. v = match[1] ? match[1] : match[0];
  181. }
  182. }
  183. }
  184. if (v !== null && v !== undefined) {
  185. if (f.split && typeof v === 'string') {
  186. v = v.split(',');
  187. }
  188. (anno as any)[f.key] = v;
  189. }
  190. }
  191. events.push(anno);
  192. }
  193. return events;
  194. })
  195. );
  196. }
  197. // These opt outs are here only for quicker and easier migration to react based annotations editors and because
  198. // annotation support API needs some work to support less "standard" editors like prometheus and here it is not
  199. // polluting public API.
  200. /**
  201. * Opt out of using the default mapping functionality on frontend.
  202. */
  203. export function shouldUseMappingUI(datasource: DataSourceApi): boolean {
  204. return (
  205. datasource.type !== 'prometheus' &&
  206. datasource.type !== 'grafana-opensearch-datasource' &&
  207. datasource.type !== 'grafana-splunk-datasource'
  208. );
  209. }
  210. /**
  211. * Use legacy runner. Used only as an escape hatch for easier transition to React based annotation editor.
  212. */
  213. export function shouldUseLegacyRunner(datasource: DataSourceApi): boolean {
  214. return (
  215. datasource.type === 'prometheus' ||
  216. datasource.type === 'grafana-opensearch-datasource' ||
  217. datasource.type === 'grafana-splunk-datasource'
  218. );
  219. }