PanelData: Adds annotations and QueryResultMeta dataTopic (#27757)

* topics round two

* more props

* remove Data from enum

* PanelData: Simplify ideas a bit to only focus on the addition of annotations in panel data

* Test data scenario

* Old graph panel showing annotations from PanelData

* Fixed comment

* Fixed issues trying to remove use of event.source

* Added unit test

Co-authored-by: Ryan McKinley <ryantxu@gmail.com>
pull/27775/head
Torkel Ödegaard 5 years ago committed by GitHub
parent f06dcfc9ee
commit adc1b965f3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 3
      packages/grafana-data/src/types/annotations.ts
  2. 12
      packages/grafana-data/src/types/data.ts
  3. 7
      packages/grafana-data/src/types/datasource.ts
  4. 37
      packages/grafana-data/src/types/panel.ts
  5. 8
      pkg/tsdb/testdatasource/scenarios.go
  6. 2
      public/app/features/annotations/annotations_srv.ts
  7. 8
      public/app/features/annotations/event_manager.ts
  8. 17
      public/app/features/annotations/standardAnnotationSupport.test.ts
  9. 35
      public/app/features/annotations/standardAnnotationSupport.ts
  10. 20
      public/app/features/dashboard/state/runRequest.test.ts
  11. 44
      public/app/features/dashboard/state/runRequest.ts
  12. 3
      public/app/features/panel/metrics_panel_ctrl.ts
  13. 48
      public/app/plugins/datasource/testdata/datasource.ts
  14. 6
      public/app/plugins/panel/graph/module.ts
  15. 1
      public/app/plugins/panel/graph/specs/graph_ctrl.test.ts

@ -34,6 +34,7 @@ export interface AnnotationEvent {
text?: string; text?: string;
type?: string; type?: string;
tags?: string[]; tags?: string[];
color?: string;
// Currently used to merge annotations from alerts and dashboard // Currently used to merge annotations from alerts and dashboard
source?: any; // source.type === 'dashboard' source?: any; // source.type === 'dashboard'
@ -49,7 +50,7 @@ export enum AnnotationEventFieldSource {
} }
export interface AnnotationEventFieldMapping { export interface AnnotationEventFieldMapping {
source?: AnnotationEventFieldSource; // defautls to 'field' source?: AnnotationEventFieldSource; // defaults to 'field'
value?: string; value?: string;
regex?: string; regex?: string;
} }

@ -2,7 +2,7 @@ import { FieldConfig } from './dataFrame';
import { DataTransformerConfig } from './transformations'; import { DataTransformerConfig } from './transformations';
import { ApplyFieldOverrideOptions } from './fieldOverrides'; import { ApplyFieldOverrideOptions } from './fieldOverrides';
export type KeyValue<T = any> = { [s: string]: T }; export type KeyValue<T = any> = Record<string, T>;
/** /**
* Represent panel data loading state. * Represent panel data loading state.
@ -15,6 +15,10 @@ export enum LoadingState {
Error = 'Error', Error = 'Error',
} }
export enum DataTopic {
Annotations = 'annotations',
}
export type PreferredVisualisationType = 'graph' | 'table' | 'logs' | 'trace'; export type PreferredVisualisationType = 'graph' | 'table' | 'logs' | 'trace';
export interface QueryResultMeta { export interface QueryResultMeta {
@ -33,6 +37,12 @@ export interface QueryResultMeta {
/** Currently used to show results in Explore only in preferred visualisation option */ /** Currently used to show results in Explore only in preferred visualisation option */
preferredVisualisationType?: PreferredVisualisationType; preferredVisualisationType?: PreferredVisualisationType;
/**
* Optionally identify which topic the frame should be assigned to.
* A value specified in the response will override what the request asked for.
*/
dataTopic?: DataTopic;
/** /**
* This is the raw query sent to the underlying system. All macros and templating * This is the raw query sent to the underlying system. All macros and templating
* as been applied. When metadata contains this value, it will be shown in the query inspector * as been applied. When metadata contains this value, it will be shown in the query inspector

@ -4,7 +4,7 @@ import { GrafanaPlugin, PluginMeta } from './plugin';
import { PanelData } from './panel'; import { PanelData } from './panel';
import { LogRowModel } from './logs'; import { LogRowModel } from './logs';
import { AnnotationEvent, AnnotationSupport } from './annotations'; import { AnnotationEvent, AnnotationSupport } from './annotations';
import { KeyValue, LoadingState, TableData, TimeSeries } from './data'; import { KeyValue, LoadingState, TableData, TimeSeries, DataTopic } from './data';
import { DataFrame, DataFrameDTO } from './dataFrame'; import { DataFrame, DataFrameDTO } from './dataFrame';
import { RawTimeRange, TimeRange } from './time'; import { RawTimeRange, TimeRange } from './time';
import { ScopedVars } from './ScopedVars'; import { ScopedVars } from './ScopedVars';
@ -412,6 +412,11 @@ export interface DataQuery {
*/ */
queryType?: string; queryType?: string;
/**
* The data topic resuls should be attached to
*/
dataTopic?: DataTopic;
/** /**
* For mixed data sources the selected datasource is on the query level. * For mixed data sources the selected datasource is on the query level.
* For non mixed scenarios this is undefined. * For non mixed scenarios this is undefined.

@ -27,6 +27,9 @@ export interface PanelData {
/** Contains data frames with field overrides applied */ /** Contains data frames with field overrides applied */
series: DataFrame[]; series: DataFrame[];
/** A list of annotation items */
annotations?: DataFrame[];
/** Request contains the queries and properties sent to the datasource */ /** Request contains the queries and properties sent to the datasource */
request?: DataQueryRequest; request?: DataQueryRequest;
@ -43,34 +46,48 @@ export interface PanelData {
export interface PanelProps<T = any> { export interface PanelProps<T = any> {
/** ID of the panel within the current dashboard */ /** ID of the panel within the current dashboard */
id: number; id: number;
/** Result set of panel queries */ /** Result set of panel queries */
data: PanelData; data: PanelData;
/** Time range of the current dashboard */ /** Time range of the current dashboard */
timeRange: TimeRange; timeRange: TimeRange;
/** Time zone of the current dashboard */ /** Time zone of the current dashboard */
timeZone: TimeZone; timeZone: TimeZone;
/** Panel options */ /** Panel options */
options: T; options: T;
/** Panel options change handler */
onOptionsChange: (options: T) => void;
/** Field options configuration */
fieldConfig: FieldConfigSource;
/** Field config change handler */
onFieldConfigChange: (config: FieldConfigSource) => void;
/** Indicates whether or not panel should be rendered transparent */ /** Indicates whether or not panel should be rendered transparent */
transparent: boolean; transparent: boolean;
/** Current width of the panel */ /** Current width of the panel */
width: number; width: number;
/** Current height of the panel */ /** Current height of the panel */
height: number; height: number;
/** Template variables interpolation function */
replaceVariables: InterpolateFunction; /** Field options configuration */
/** Time range change handler */ fieldConfig: FieldConfigSource;
onChangeTimeRange: (timeRange: AbsoluteTimeRange) => void;
/** @internal */ /** @internal */
renderCounter: number; renderCounter: number;
/** Panel title */ /** Panel title */
title: string; title: string;
/** Panel options change handler */
onOptionsChange: (options: T) => void;
/** Field config change handler */
onFieldConfigChange: (config: FieldConfigSource) => void;
/** Template variables interpolation function */
replaceVariables: InterpolateFunction;
/** Time range change handler */
onChangeTimeRange: (timeRange: AbsoluteTimeRange) => void;
} }
export interface PanelEditorProps<T = any> { export interface PanelEditorProps<T = any> {

@ -277,6 +277,14 @@ func init() {
}, },
}) })
registerScenario(&Scenario{
Id: "annotations",
Name: "Annotations",
Handler: func(query *tsdb.Query, context *tsdb.TsdbQuery) *tsdb.QueryResult {
return tsdb.NewQueryResult()
},
})
registerScenario(&Scenario{ registerScenario(&Scenario{
Id: "table_static", Id: "table_static",
Name: "Table Static", Name: "Table Static",

@ -203,6 +203,8 @@ export class AnnotationsSrv {
for (const item of results) { for (const item of results) {
item.source = annotation; item.source = annotation;
item.color = annotation.iconColor;
item.type = annotation.name;
item.isRegion = item.timeEnd && item.time !== item.timeEnd; item.isRegion = item.timeEnd && item.time !== item.timeEnd;
} }

@ -115,16 +115,16 @@ export class EventManager {
// add properties used by jquery flot events // add properties used by jquery flot events
item.min = item.time; item.min = item.time;
item.max = item.time; item.max = item.time;
item.eventType = item.source.name; item.eventType = item.type;
if (item.newState) { if (item.newState) {
item.eventType = '$__' + item.newState; item.eventType = '$__' + item.newState;
continue; continue;
} }
if (!types[item.source.name]) { if (!types[item.type]) {
types[item.source.name] = { types[item.type] = {
color: item.source.iconColor, color: item.color,
position: 'BOTTOM', position: 'BOTTOM',
markerSize: 5, markerSize: 5,
}; };

@ -15,33 +15,43 @@ describe('DataFrame to annotations', () => {
expect(events).toMatchInlineSnapshot(` expect(events).toMatchInlineSnapshot(`
Array [ Array [
Object { Object {
"color": "red",
"tags": Array [ "tags": Array [
"aaa", "aaa",
"bbb", "bbb",
], ],
"text": "t1", "text": "t1",
"time": 1, "time": 1,
"type": "default",
}, },
Object { Object {
"color": "red",
"tags": Array [ "tags": Array [
"bbb", "bbb",
"ccc", "ccc",
], ],
"text": "t2", "text": "t2",
"time": 2, "time": 2,
"type": "default",
}, },
Object { Object {
"color": "red",
"tags": Array [ "tags": Array [
"zyz", "zyz",
], ],
"text": "t3", "text": "t3",
"time": 3, "time": 3,
"type": "default",
}, },
Object { Object {
"color": "red",
"time": 4, "time": 4,
"type": "default",
}, },
Object { Object {
"color": "red",
"time": 5, "time": 5,
"type": "default",
}, },
] ]
`); `);
@ -63,25 +73,32 @@ describe('DataFrame to annotations', () => {
timeEnd: { value: 'time1' }, timeEnd: { value: 'time1' },
title: { value: 'aaaaa' }, title: { value: 'aaaaa' },
}); });
expect(events).toMatchInlineSnapshot(` expect(events).toMatchInlineSnapshot(`
Array [ Array [
Object { Object {
"color": "red",
"text": "b1", "text": "b1",
"time": 100, "time": 100,
"timeEnd": 111, "timeEnd": 111,
"title": "a1", "title": "a1",
"type": "default",
}, },
Object { Object {
"color": "red",
"text": "b2", "text": "b2",
"time": 200, "time": 200,
"timeEnd": 222, "timeEnd": 222,
"title": "a2", "title": "a2",
"type": "default",
}, },
Object { Object {
"color": "red",
"text": "b3", "text": "b3",
"time": 300, "time": 300,
"timeEnd": 333, "timeEnd": 333,
"title": "a3", "title": "a3",
"type": "default",
}, },
] ]
`); `);

@ -2,7 +2,7 @@ import {
DataFrame, DataFrame,
AnnotationQuery, AnnotationQuery,
AnnotationSupport, AnnotationSupport,
transformDataFrame, standardTransformers,
FieldType, FieldType,
Field, Field,
KeyValue, KeyValue,
@ -54,19 +54,12 @@ export function singleFrameFromPanelData(data: DataFrame[]): DataFrame | undefin
if (!data?.length) { if (!data?.length) {
return undefined; return undefined;
} }
if (data.length === 1) { if (data.length === 1) {
return data[0]; return data[0];
} }
return transformDataFrame( return standardTransformers.mergeTransformer.transformer({})(data)[0];
[
{
id: 'seriesToColumns',
options: { byField: 'Time' },
},
],
data
)[0];
} }
interface AnnotationEventFieldSetter { interface AnnotationEventFieldSetter {
@ -109,6 +102,7 @@ export const annotationEventNames: AnnotationFieldInfo[] = [
export function getAnnotationsFromData(data: DataFrame[], options?: AnnotationEventMappings): AnnotationEvent[] { export function getAnnotationsFromData(data: DataFrame[], options?: AnnotationEventMappings): AnnotationEvent[] {
const frame = singleFrameFromPanelData(data); const frame = singleFrameFromPanelData(data);
if (!frame?.length) { if (!frame?.length) {
return []; return [];
} }
@ -116,6 +110,7 @@ export function getAnnotationsFromData(data: DataFrame[], options?: AnnotationEv
let hasTime = false; let hasTime = false;
let hasText = false; let hasText = false;
const byName: KeyValue<Field> = {}; const byName: KeyValue<Field> = {};
for (const f of frame.fields) { for (const f of frame.fields) {
const name = getFieldDisplayName(f, frame); const name = getFieldDisplayName(f, frame);
byName[name.toLowerCase()] = f; byName[name.toLowerCase()] = f;
@ -126,11 +121,14 @@ export function getAnnotationsFromData(data: DataFrame[], options?: AnnotationEv
} }
const fields: AnnotationEventFieldSetter[] = []; const fields: AnnotationEventFieldSetter[] = [];
for (const evts of annotationEventNames) { for (const evts of annotationEventNames) {
const opt = options[evts.key] || {}; //AnnotationEventFieldMapping const opt = options[evts.key] || {}; //AnnotationEventFieldMapping
if (opt.source === AnnotationEventFieldSource.Skip) { if (opt.source === AnnotationEventFieldSource.Skip) {
continue; continue;
} }
const setter: AnnotationEventFieldSetter = { key: evts.key, split: evts.split }; const setter: AnnotationEventFieldSetter = { key: evts.key, split: evts.split };
if (opt.source === AnnotationEventFieldSource.Text) { if (opt.source === AnnotationEventFieldSource.Text) {
@ -138,6 +136,7 @@ export function getAnnotationsFromData(data: DataFrame[], options?: AnnotationEv
} else { } else {
const lower = (opt.value || evts.key).toLowerCase(); const lower = (opt.value || evts.key).toLowerCase();
setter.field = byName[lower]; setter.field = byName[lower];
if (!setter.field && evts.field) { if (!setter.field && evts.field) {
setter.field = evts.field(frame); setter.field = evts.field(frame);
} }
@ -159,10 +158,16 @@ export function getAnnotationsFromData(data: DataFrame[], options?: AnnotationEv
// Add each value to the string // Add each value to the string
const events: AnnotationEvent[] = []; const events: AnnotationEvent[] = [];
for (let i = 0; i < frame.length; i++) { for (let i = 0; i < frame.length; i++) {
const anno: AnnotationEvent = {}; const anno: AnnotationEvent = {
type: 'default',
color: 'red',
};
for (const f of fields) { for (const f of fields) {
let v: any = undefined; let v: any = undefined;
if (f.text) { if (f.text) {
v = f.text; // TODO support templates! v = f.text; // TODO support templates!
} else if (f.field) { } else if (f.field) {
@ -175,14 +180,16 @@ export function getAnnotationsFromData(data: DataFrame[], options?: AnnotationEv
} }
} }
if (!(v === null || v === undefined)) { if (v !== null && v !== undefined) {
if (v && f.split) { if (f.split && typeof v === 'string') {
v = (v as string).split(','); v = v.split(',');
} }
(anno as any)[f.key] = v; (anno as any)[f.key] = v;
} }
} }
events.push(anno); events.push(anno);
} }
return events; return events;
} }

@ -6,6 +6,7 @@ import {
dateTime, dateTime,
LoadingState, LoadingState,
PanelData, PanelData,
DataTopic,
} from '@grafana/data'; } from '@grafana/data';
import { Observable, Subscriber, Subscription } from 'rxjs'; import { Observable, Subscriber, Subscription } from 'rxjs';
import { runRequest } from './runRequest'; import { runRequest } from './runRequest';
@ -251,6 +252,25 @@ describe('runRequest', () => {
expectThatRangeHasNotMutated(ctx); expectThatRangeHasNotMutated(ctx);
}); });
}); });
runRequestScenario('With annotations dataTopic', ctx => {
ctx.setup(() => {
ctx.start();
ctx.emitPacket({
data: [{ name: 'DataA-1' } as DataFrame],
key: 'A',
});
ctx.emitPacket({
data: [{ name: 'DataA-2', meta: { dataTopic: DataTopic.Annotations } } as DataFrame],
key: 'B',
});
});
it('should seperate annotations results', () => {
expect(ctx.results[1].annotations?.length).toBe(1);
expect(ctx.results[1].series.length).toBe(1);
});
});
}); });
const expectThatRangeHasNotMutated = (ctx: ScenarioCtx) => { const expectThatRangeHasNotMutated = (ctx: ScenarioCtx) => {

@ -1,6 +1,6 @@
// Libraries // Libraries
import { Observable, of, timer, merge, from } from 'rxjs'; import { Observable, of, timer, merge, from } from 'rxjs';
import { flatten, map as lodashMap, isArray, isString } from 'lodash'; import { map as isArray, isString } from 'lodash';
import { map, catchError, takeUntil, mapTo, share, finalize, tap } from 'rxjs/operators'; import { map, catchError, takeUntil, mapTo, share, finalize, tap } from 'rxjs/operators';
// Utils & Services // Utils & Services
import { backendSrv } from 'app/core/services/backend_srv'; import { backendSrv } from 'app/core/services/backend_srv';
@ -16,6 +16,7 @@ import {
dateMath, dateMath,
toDataFrame, toDataFrame,
DataFrame, DataFrame,
DataTopic,
guessFieldTypes, guessFieldTypes,
} from '@grafana/data'; } from '@grafana/data';
import { toDataQueryError } from '@grafana/runtime'; import { toDataQueryError } from '@grafana/runtime';
@ -54,19 +55,33 @@ export function processResponsePacket(packet: DataQueryResponse, state: RunningQ
} }
: range; : range;
const combinedData = flatten( const series: DataQueryResponseData[] = [];
lodashMap(packets, (packet: DataQueryResponse) => { const annotations: DataQueryResponseData[] = [];
for (const key in packets) {
const packet = packets[key];
if (packet.error) { if (packet.error) {
loadingState = LoadingState.Error; loadingState = LoadingState.Error;
error = packet.error; error = packet.error;
} }
return packet.data;
}) if (packet.data && packet.data.length) {
); for (const dataItem of packet.data) {
if (dataItem.meta?.dataTopic === DataTopic.Annotations) {
annotations.push(dataItem);
continue;
}
series.push(dataItem);
}
}
}
const panelData = { const panelData = {
state: loadingState, state: loadingState,
series: combinedData, series,
annotations,
error, error,
request, request,
timeRange, timeRange,
@ -77,11 +92,10 @@ export function processResponsePacket(packet: DataQueryResponse, state: RunningQ
/** /**
* This function handles the excecution of requests & and processes the single or multiple response packets into * This function handles the excecution of requests & and processes the single or multiple response packets into
* a combined PanelData response. * a combined PanelData response. It will
* It will * Merge multiple responses into a single DataFrame array based on the packet key
* * Merge multiple responses into a single DataFrame array based on the packet key * Will emit a loading state if no response after 50ms
* * Will emit a loading state if no response after 50ms * Cancel any still running network requests on unsubscribe (using request.requestId)
* * Cancel any still running network requests on unsubscribe (using request.requestId)
*/ */
export function runRequest(datasource: DataSourceApi, request: DataQueryRequest): Observable<PanelData> { export function runRequest(datasource: DataSourceApi, request: DataQueryRequest): Observable<PanelData> {
let state: RunningQueryState = { let state: RunningQueryState = {
@ -162,7 +176,7 @@ export function callQueryMethod(datasource: DataSourceApi, request: DataQueryReq
* This is also used by PanelChrome for snapshot support * This is also used by PanelChrome for snapshot support
*/ */
export function getProcessedDataFrames(results?: DataQueryResponseData[]): DataFrame[] { export function getProcessedDataFrames(results?: DataQueryResponseData[]): DataFrame[] {
if (!isArray(results)) { if (!results || !isArray(results)) {
return []; return [];
} }
@ -185,7 +199,7 @@ export function getProcessedDataFrames(results?: DataQueryResponseData[]): DataF
} }
export function preProcessPanelData(data: PanelData, lastResult?: PanelData): PanelData { export function preProcessPanelData(data: PanelData, lastResult?: PanelData): PanelData {
const { series } = data; const { series, annotations } = data;
// for loading states with no data, use last result // for loading states with no data, use last result
if (data.state === LoadingState.Loading && series.length === 0) { if (data.state === LoadingState.Loading && series.length === 0) {
@ -203,11 +217,13 @@ export function preProcessPanelData(data: PanelData, lastResult?: PanelData): Pa
// Make sure the data frames are properly formatted // Make sure the data frames are properly formatted
const STARTTIME = performance.now(); const STARTTIME = performance.now();
const processedDataFrames = getProcessedDataFrames(series); const processedDataFrames = getProcessedDataFrames(series);
const annotationsProcessed = getProcessedDataFrames(annotations);
const STOPTIME = performance.now(); const STOPTIME = performance.now();
return { return {
...data, ...data,
series: processedDataFrames, series: processedDataFrames,
annotations: annotationsProcessed,
timings: { dataProcessingTime: STOPTIME - STARTTIME }, timings: { dataProcessingTime: STOPTIME - STARTTIME },
}; };
} }

@ -35,6 +35,7 @@ class MetricsPanelCtrl extends PanelCtrl {
dataList: LegacyResponseData[]; dataList: LegacyResponseData[];
querySubscription?: Unsubscribable | null; querySubscription?: Unsubscribable | null;
useDataFrames = false; useDataFrames = false;
panelData?: PanelData;
constructor($scope: any, $injector: any) { constructor($scope: any, $injector: any) {
super($scope, $injector); super($scope, $injector);
@ -130,6 +131,8 @@ class MetricsPanelCtrl extends PanelCtrl {
// Updates the response with information from the stream // Updates the response with information from the stream
panelDataObserver = { panelDataObserver = {
next: (data: PanelData) => { next: (data: PanelData) => {
this.panelData = data;
if (data.state === LoadingState.Error) { if (data.state === LoadingState.Error) {
this.loading = false; this.loading = false;
this.processDataError(data.error); this.processDataError(data.error);

@ -14,6 +14,9 @@ import {
MetricFindValue, MetricFindValue,
TableData, TableData,
TimeSeries, TimeSeries,
TimeRange,
DataTopic,
AnnotationEvent,
} from '@grafana/data'; } from '@grafana/data';
import { Scenario, TestDataQuery } from './types'; import { Scenario, TestDataQuery } from './types';
import { getBackendSrv, toDataQueryError } from '@grafana/runtime'; import { getBackendSrv, toDataQueryError } from '@grafana/runtime';
@ -40,13 +43,21 @@ export class TestDataDataSource extends DataSourceApi<TestDataQuery> {
if (target.hide) { if (target.hide) {
continue; continue;
} }
if (target.scenarioId === 'streaming_client') {
switch (target.scenarioId) {
case 'streaming_client':
streams.push(runStream(target, options)); streams.push(runStream(target, options));
} else if (target.scenarioId === 'grafana_api') { break;
case 'grafana_api':
streams.push(runGrafanaAPI(target, options)); streams.push(runGrafanaAPI(target, options));
} else if (target.scenarioId === 'arrow') { break;
case 'arrow':
streams.push(runArrowFile(target, options)); streams.push(runArrowFile(target, options));
} else { break;
case 'annotations':
streams.push(this.annotationDataTopicTest(target, options));
break;
default:
queries.push({ queries.push({
...target, ...target,
intervalMs: options.intervalMs, intervalMs: options.intervalMs,
@ -109,23 +120,36 @@ export class TestDataDataSource extends DataSourceApi<TestDataQuery> {
return { data, error }; return { data, error };
} }
annotationQuery(options: any) { annotationDataTopicTest(target: TestDataQuery, req: DataQueryRequest<TestDataQuery>): Observable<DataQueryResponse> {
let timeWalker = options.range.from.valueOf(); return new Observable<DataQueryResponse>(observer => {
const to = options.range.to.valueOf(); const events = this.buildFakeAnnotationEvents(req.range, 10);
const dataFrame = new ArrayDataFrame(events);
dataFrame.meta = { dataTopic: DataTopic.Annotations };
observer.next({ key: target.refId, data: [dataFrame] });
});
}
buildFakeAnnotationEvents(range: TimeRange, count: number): AnnotationEvent[] {
let timeWalker = range.from.valueOf();
const to = range.to.valueOf();
const events = []; const events = [];
const eventCount = 10; const step = (to - timeWalker) / count;
const step = (to - timeWalker) / eventCount;
for (let i = 0; i < eventCount; i++) { for (let i = 0; i < count; i++) {
events.push({ events.push({
annotation: options.annotation,
time: timeWalker, time: timeWalker,
text: 'This is the text, <a href="https://grafana.com">Grafana.com</a>', text: 'This is the text, <a href="https://grafana.com">Grafana.com</a>',
tags: ['text', 'server'], tags: ['text', 'server'],
}); });
timeWalker += step; timeWalker += step;
} }
return Promise.resolve(events);
return events;
}
annotationQuery(options: any) {
return Promise.resolve(this.buildFakeAnnotationEvents(options.range, 10));
} }
getQueryDisplayText(query: TestDataQuery) { getQueryDisplayText(query: TestDataQuery) {

@ -25,8 +25,8 @@ import { getLocationSrv } from '@grafana/runtime';
import { getDataTimeRange } from './utils'; import { getDataTimeRange } from './utils';
import { changePanelPlugin } from 'app/features/dashboard/state/actions'; import { changePanelPlugin } from 'app/features/dashboard/state/actions';
import { dispatch } from 'app/store/store'; import { dispatch } from 'app/store/store';
import { ThresholdMapper } from 'app/features/alerting/state/ThresholdMapper'; import { ThresholdMapper } from 'app/features/alerting/state/ThresholdMapper';
import { getAnnotationsFromData } from 'app/features/annotations/standardAnnotationSupport';
export class GraphCtrl extends MetricsPanelCtrl { export class GraphCtrl extends MetricsPanelCtrl {
static template = template; static template = template;
@ -235,6 +235,10 @@ export class GraphCtrl extends MetricsPanelCtrl {
(this.seriesList as any).alertState = this.alertState.state; (this.seriesList as any).alertState = this.alertState.state;
} }
if (this.panelData!.annotations?.length) {
this.annotations = getAnnotationsFromData(this.panelData!.annotations!);
}
this.render(this.seriesList); this.render(this.seriesList);
}, },
() => { () => {

@ -39,6 +39,7 @@ describe('GraphCtrl', () => {
ctx.ctrl.events = { ctx.ctrl.events = {
emit: () => {}, emit: () => {},
}; };
ctx.ctrl.panelData = {};
ctx.ctrl.annotationsSrv = { ctx.ctrl.annotationsSrv = {
getAnnotations: () => Promise.resolve({}), getAnnotations: () => Promise.resolve({}),
}; };

Loading…
Cancel
Save