Panel Edit: Add Annotations data (#101149)
* Add annotations data to panel editor * Add tests for getVisualizationOptions2 * Add annotation check to test
This commit is contained in:
@@ -16,6 +16,7 @@ export interface StandardEditorContext<TOptions, TState = any> {
|
||||
options?: TOptions;
|
||||
instanceState?: TState;
|
||||
isOverride?: boolean;
|
||||
annotations?: DataFrame[];
|
||||
}
|
||||
|
||||
export interface StandardEditorProps<TValue = any, TSettings = any, TOptions = any, TState = any> {
|
||||
|
||||
@@ -51,5 +51,5 @@ export interface OptionEditorConfig<TOptions, TSettings = any, TValue = any> {
|
||||
/**
|
||||
* Function that enables configuration of when option editor should be shown based on current panel option properties.
|
||||
*/
|
||||
showIf?: (currentOptions: TOptions, data?: DataFrame[]) => boolean | undefined;
|
||||
showIf?: (currentOptions: TOptions, data?: DataFrame[], annotations?: DataFrame[]) => boolean | undefined;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { PanelOptionsEditorBuilder } from '@grafana/data';
|
||||
import { DataFrame, PanelOptionsEditorBuilder } from '@grafana/data';
|
||||
import { OptionsWithTooltip, TooltipDisplayMode, SortOrder } from '@grafana/schema';
|
||||
|
||||
/** @internal */
|
||||
@@ -94,6 +94,13 @@ export function addTooltipOptions<T extends OptionsWithTooltip>(
|
||||
settings: {
|
||||
integer: true,
|
||||
},
|
||||
showIf: (options: T) => options.tooltip?.mode === TooltipDisplayMode.Multi,
|
||||
showIf: (options: T, data: DataFrame[] | undefined, annotations: DataFrame[] | undefined) => {
|
||||
return (
|
||||
options.tooltip?.mode === TooltipDisplayMode.Multi ||
|
||||
annotations?.some((df) => {
|
||||
return df.meta?.custom?.resultType === 'exemplar';
|
||||
})
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
+385
-146
@@ -1,162 +1,401 @@
|
||||
import { EventBusSrv, FieldType, getDefaultTimeRange, LoadingState, toDataFrame } from '@grafana/data';
|
||||
import {
|
||||
EventBusSrv,
|
||||
FieldConfigOptionsRegistry,
|
||||
FieldConfigPropertyItem,
|
||||
FieldType,
|
||||
getDefaultTimeRange,
|
||||
LoadingState,
|
||||
PanelPlugin,
|
||||
Registry,
|
||||
toDataFrame,
|
||||
} from '@grafana/data';
|
||||
import { VizPanel } from '@grafana/scenes';
|
||||
|
||||
import { getStandardEditorContext } from './getVisualizationOptions';
|
||||
import { getStandardEditorContext, getVisualizationOptions2 } from './getVisualizationOptions';
|
||||
|
||||
describe('getStandardEditorContext', () => {
|
||||
it('defaults the series data to an empty array', () => {
|
||||
const editorContext = getStandardEditorContext({
|
||||
data: undefined,
|
||||
replaceVariables: jest.fn(),
|
||||
options: {},
|
||||
eventBus: new EventBusSrv(),
|
||||
instanceState: {},
|
||||
describe('getVisualizationOptions', () => {
|
||||
describe('getStandardEditorContext', () => {
|
||||
it('defaults the series data to an empty array', () => {
|
||||
const editorContext = getStandardEditorContext({
|
||||
data: undefined,
|
||||
replaceVariables: jest.fn(),
|
||||
options: {},
|
||||
eventBus: new EventBusSrv(),
|
||||
instanceState: {},
|
||||
});
|
||||
|
||||
expect(editorContext.data).toEqual([]);
|
||||
});
|
||||
|
||||
expect(editorContext.data).toEqual([]);
|
||||
});
|
||||
it('returns suggestions for empty data', () => {
|
||||
const editorContext = getStandardEditorContext({
|
||||
data: undefined,
|
||||
replaceVariables: jest.fn(),
|
||||
options: {},
|
||||
eventBus: new EventBusSrv(),
|
||||
instanceState: {},
|
||||
});
|
||||
|
||||
it('returns suggestions for empty data', () => {
|
||||
const editorContext = getStandardEditorContext({
|
||||
data: undefined,
|
||||
replaceVariables: jest.fn(),
|
||||
options: {},
|
||||
eventBus: new EventBusSrv(),
|
||||
instanceState: {},
|
||||
expect(editorContext.getSuggestions).toBeDefined();
|
||||
expect(editorContext.getSuggestions?.()).toEqual([
|
||||
{
|
||||
documentation: 'Name of the series',
|
||||
label: 'Name',
|
||||
origin: 'series',
|
||||
value: '__series.name',
|
||||
},
|
||||
{
|
||||
documentation: 'Field name of the clicked datapoint (in ms epoch)',
|
||||
label: 'Name',
|
||||
origin: 'field',
|
||||
value: '__field.name',
|
||||
},
|
||||
{
|
||||
documentation: 'Adds current variables',
|
||||
label: 'All variables',
|
||||
origin: 'template',
|
||||
value: '__all_variables',
|
||||
},
|
||||
{
|
||||
documentation: 'Adds current time range',
|
||||
label: 'Time range',
|
||||
origin: 'built-in',
|
||||
value: '__url_time_range',
|
||||
},
|
||||
{
|
||||
documentation: "Adds current time range's from value",
|
||||
label: 'Time range: from',
|
||||
origin: 'built-in',
|
||||
value: '__from',
|
||||
},
|
||||
{
|
||||
documentation: "Adds current time range's to value",
|
||||
label: 'Time range: to',
|
||||
origin: 'built-in',
|
||||
value: '__to',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
expect(editorContext.getSuggestions).toBeDefined();
|
||||
expect(editorContext.getSuggestions?.()).toEqual([
|
||||
{
|
||||
documentation: 'Name of the series',
|
||||
label: 'Name',
|
||||
origin: 'series',
|
||||
value: '__series.name',
|
||||
},
|
||||
{
|
||||
documentation: 'Field name of the clicked datapoint (in ms epoch)',
|
||||
label: 'Name',
|
||||
origin: 'field',
|
||||
value: '__field.name',
|
||||
},
|
||||
{
|
||||
documentation: 'Adds current variables',
|
||||
label: 'All variables',
|
||||
origin: 'template',
|
||||
value: '__all_variables',
|
||||
},
|
||||
{
|
||||
documentation: 'Adds current time range',
|
||||
label: 'Time range',
|
||||
origin: 'built-in',
|
||||
value: '__url_time_range',
|
||||
},
|
||||
{
|
||||
documentation: "Adds current time range's from value",
|
||||
label: 'Time range: from',
|
||||
origin: 'built-in',
|
||||
value: '__from',
|
||||
},
|
||||
{
|
||||
documentation: "Adds current time range's to value",
|
||||
label: 'Time range: to',
|
||||
origin: 'built-in',
|
||||
value: '__to',
|
||||
},
|
||||
]);
|
||||
it('returns suggestions for non-empty data', () => {
|
||||
const series = [
|
||||
toDataFrame({
|
||||
fields: [
|
||||
{ name: 'time', type: FieldType.time },
|
||||
{ name: 'score', type: FieldType.number },
|
||||
],
|
||||
}),
|
||||
];
|
||||
|
||||
const panelData = {
|
||||
series,
|
||||
timeRange: getDefaultTimeRange(),
|
||||
state: LoadingState.Done,
|
||||
};
|
||||
|
||||
const editorContext = getStandardEditorContext({
|
||||
data: panelData,
|
||||
replaceVariables: jest.fn(),
|
||||
options: {},
|
||||
eventBus: new EventBusSrv(),
|
||||
instanceState: {},
|
||||
});
|
||||
|
||||
expect(editorContext.getSuggestions).toBeDefined();
|
||||
expect(editorContext.getSuggestions?.()).toEqual([
|
||||
{
|
||||
documentation: 'Name of the series',
|
||||
label: 'Name',
|
||||
origin: 'series',
|
||||
value: '__series.name',
|
||||
},
|
||||
{
|
||||
documentation: 'Field name of the clicked datapoint (in ms epoch)',
|
||||
label: 'Name',
|
||||
origin: 'field',
|
||||
value: '__field.name',
|
||||
},
|
||||
{
|
||||
documentation: 'Formatted value for time on the same row',
|
||||
label: 'time',
|
||||
origin: 'fields',
|
||||
value: '__data.fields.time',
|
||||
},
|
||||
{
|
||||
documentation: 'Formatted value for score on the same row',
|
||||
label: 'score',
|
||||
origin: 'fields',
|
||||
value: '__data.fields.score',
|
||||
},
|
||||
{
|
||||
documentation: 'Enter the field order',
|
||||
label: 'Select by index',
|
||||
origin: 'fields',
|
||||
value: '__data.fields[0]',
|
||||
},
|
||||
{
|
||||
documentation: 'the numeric field value',
|
||||
label: 'Show numeric value',
|
||||
origin: 'fields',
|
||||
value: '__data.fields.score.numeric',
|
||||
},
|
||||
{
|
||||
documentation: 'the text value',
|
||||
label: 'Show text value',
|
||||
origin: 'fields',
|
||||
value: '__data.fields.score.text',
|
||||
},
|
||||
{
|
||||
documentation: 'Adds current variables',
|
||||
label: 'All variables',
|
||||
origin: 'template',
|
||||
value: '__all_variables',
|
||||
},
|
||||
{
|
||||
documentation: 'Adds current time range',
|
||||
label: 'Time range',
|
||||
origin: 'built-in',
|
||||
value: '__url_time_range',
|
||||
},
|
||||
{
|
||||
documentation: "Adds current time range's from value",
|
||||
label: 'Time range: from',
|
||||
origin: 'built-in',
|
||||
value: '__from',
|
||||
},
|
||||
{
|
||||
documentation: "Adds current time range's to value",
|
||||
label: 'Time range: to',
|
||||
origin: 'built-in',
|
||||
value: '__to',
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
it('returns suggestions for non-empty data', () => {
|
||||
const series = [
|
||||
toDataFrame({
|
||||
fields: [
|
||||
{ name: 'time', type: FieldType.time },
|
||||
{ name: 'score', type: FieldType.number },
|
||||
],
|
||||
}),
|
||||
];
|
||||
describe('getVisualizationOptions2', () => {
|
||||
it('should create an options list with the right number of categories and items', () => {
|
||||
const vizPanel = new VizPanel({
|
||||
title: 'Panel A',
|
||||
pluginId: 'timeseries',
|
||||
key: 'panel-12',
|
||||
});
|
||||
|
||||
const panelData = {
|
||||
series,
|
||||
timeRange: getDefaultTimeRange(),
|
||||
state: LoadingState.Done,
|
||||
const property1: FieldConfigPropertyItem = {
|
||||
id: 'custom.property1', // Match field properties
|
||||
path: 'property1', // Match field properties
|
||||
isCustom: true,
|
||||
process: (value) => value,
|
||||
shouldApply: () => true,
|
||||
override: jest.fn(),
|
||||
editor: jest.fn(),
|
||||
name: 'Property 1',
|
||||
};
|
||||
|
||||
const property2: FieldConfigPropertyItem = {
|
||||
id: 'custom.property2', // Match field properties
|
||||
path: 'property2', // Match field properties
|
||||
isCustom: true,
|
||||
process: (value) => value,
|
||||
shouldApply: () => true,
|
||||
override: jest.fn(),
|
||||
editor: jest.fn(),
|
||||
name: 'Property 2',
|
||||
};
|
||||
|
||||
const property3: FieldConfigPropertyItem = {
|
||||
id: 'custom.property3.nested', // Match field properties
|
||||
path: 'property3.nested', // Match field properties
|
||||
isCustom: true,
|
||||
process: (value) => value,
|
||||
shouldApply: () => true,
|
||||
override: jest.fn(),
|
||||
editor: jest.fn(),
|
||||
name: 'Property 3',
|
||||
};
|
||||
|
||||
const customFieldRegistry: FieldConfigOptionsRegistry = new Registry<FieldConfigPropertyItem>(() => {
|
||||
return [property1, property2, property3];
|
||||
});
|
||||
|
||||
const plugin = {
|
||||
meta: { skipDataQuery: false },
|
||||
getPanelOptionsSupplier: jest.fn,
|
||||
fieldConfigRegistry: customFieldRegistry,
|
||||
} as unknown as PanelPlugin;
|
||||
|
||||
const vizOptions = getVisualizationOptions2({
|
||||
panel: vizPanel,
|
||||
eventBus: new EventBusSrv(),
|
||||
plugin: plugin,
|
||||
instanceState: {},
|
||||
});
|
||||
|
||||
expect(vizOptions.length).toEqual(1);
|
||||
expect(vizOptions[0].items.length).toEqual(3);
|
||||
});
|
||||
|
||||
it('should not show items when the showIf evaluates to false', () => {
|
||||
const vizPanel = new VizPanel({
|
||||
title: 'Panel A',
|
||||
pluginId: 'timeseries',
|
||||
key: 'panel-12',
|
||||
});
|
||||
|
||||
const property1: FieldConfigPropertyItem = {
|
||||
id: 'custom.property1', // Match field properties
|
||||
path: 'property1', // Match field properties
|
||||
isCustom: true,
|
||||
process: (value) => value,
|
||||
shouldApply: () => true,
|
||||
override: jest.fn(),
|
||||
editor: jest.fn(),
|
||||
name: 'Property 1',
|
||||
showIf: () => false,
|
||||
};
|
||||
|
||||
const property2: FieldConfigPropertyItem = {
|
||||
id: 'custom.property2', // Match field properties
|
||||
path: 'property2', // Match field properties
|
||||
isCustom: true,
|
||||
process: (value) => value,
|
||||
shouldApply: () => true,
|
||||
override: jest.fn(),
|
||||
editor: jest.fn(),
|
||||
name: 'Property 2',
|
||||
};
|
||||
|
||||
const property3: FieldConfigPropertyItem = {
|
||||
id: 'custom.property3.nested', // Match field properties
|
||||
path: 'property3.nested', // Match field properties
|
||||
isCustom: true,
|
||||
process: (value) => value,
|
||||
shouldApply: () => true,
|
||||
override: jest.fn(),
|
||||
editor: jest.fn(),
|
||||
name: 'Property 3',
|
||||
};
|
||||
|
||||
const customFieldRegistry: FieldConfigOptionsRegistry = new Registry<FieldConfigPropertyItem>(() => {
|
||||
return [property1, property2, property3];
|
||||
});
|
||||
|
||||
const plugin = {
|
||||
meta: { skipDataQuery: false },
|
||||
getPanelOptionsSupplier: jest.fn,
|
||||
fieldConfigRegistry: customFieldRegistry,
|
||||
} as unknown as PanelPlugin;
|
||||
|
||||
const vizOptions = getVisualizationOptions2({
|
||||
panel: vizPanel,
|
||||
eventBus: new EventBusSrv(),
|
||||
plugin: plugin,
|
||||
instanceState: {},
|
||||
});
|
||||
|
||||
expect(vizOptions.length).toEqual(1);
|
||||
expect(vizOptions[0].items.length).toEqual(2);
|
||||
});
|
||||
|
||||
const fieldConfig = {
|
||||
defaults: {
|
||||
displayName: 'default',
|
||||
custom: {
|
||||
displayName: 'custom',
|
||||
},
|
||||
},
|
||||
overrides: [],
|
||||
};
|
||||
|
||||
const editorContext = getStandardEditorContext({
|
||||
data: panelData,
|
||||
replaceVariables: jest.fn(),
|
||||
options: {},
|
||||
eventBus: new EventBusSrv(),
|
||||
instanceState: {},
|
||||
const vizPanel = new VizPanel({
|
||||
title: 'Panel A',
|
||||
pluginId: 'timeseries',
|
||||
key: 'panel-12',
|
||||
fieldConfig: fieldConfig,
|
||||
});
|
||||
|
||||
expect(editorContext.getSuggestions).toBeDefined();
|
||||
expect(editorContext.getSuggestions?.()).toEqual([
|
||||
{
|
||||
documentation: 'Name of the series',
|
||||
label: 'Name',
|
||||
origin: 'series',
|
||||
value: '__series.name',
|
||||
},
|
||||
{
|
||||
documentation: 'Field name of the clicked datapoint (in ms epoch)',
|
||||
label: 'Name',
|
||||
origin: 'field',
|
||||
value: '__field.name',
|
||||
},
|
||||
{
|
||||
documentation: 'Formatted value for time on the same row',
|
||||
label: 'time',
|
||||
origin: 'fields',
|
||||
value: '__data.fields.time',
|
||||
},
|
||||
{
|
||||
documentation: 'Formatted value for score on the same row',
|
||||
label: 'score',
|
||||
origin: 'fields',
|
||||
value: '__data.fields.score',
|
||||
},
|
||||
{
|
||||
documentation: 'Enter the field order',
|
||||
label: 'Select by index',
|
||||
origin: 'fields',
|
||||
value: '__data.fields[0]',
|
||||
},
|
||||
{
|
||||
documentation: 'the numeric field value',
|
||||
label: 'Show numeric value',
|
||||
origin: 'fields',
|
||||
value: '__data.fields.score.numeric',
|
||||
},
|
||||
{
|
||||
documentation: 'the text value',
|
||||
label: 'Show text value',
|
||||
origin: 'fields',
|
||||
value: '__data.fields.score.text',
|
||||
},
|
||||
{
|
||||
documentation: 'Adds current variables',
|
||||
label: 'All variables',
|
||||
origin: 'template',
|
||||
value: '__all_variables',
|
||||
},
|
||||
{
|
||||
documentation: 'Adds current time range',
|
||||
label: 'Time range',
|
||||
origin: 'built-in',
|
||||
value: '__url_time_range',
|
||||
},
|
||||
{
|
||||
documentation: "Adds current time range's from value",
|
||||
label: 'Time range: from',
|
||||
origin: 'built-in',
|
||||
value: '__from',
|
||||
},
|
||||
{
|
||||
documentation: "Adds current time range's to value",
|
||||
label: 'Time range: to',
|
||||
origin: 'built-in',
|
||||
value: '__to',
|
||||
},
|
||||
]);
|
||||
const getOnePropVizPlugin = (isCustom: boolean, showIfSpy: jest.Mock) => {
|
||||
const property1: FieldConfigPropertyItem = {
|
||||
id: 'custom.property1', // Match field properties
|
||||
path: 'property1', // Match field properties
|
||||
isCustom: isCustom,
|
||||
process: (value) => value,
|
||||
shouldApply: () => true,
|
||||
override: jest.fn(),
|
||||
editor: jest.fn(),
|
||||
name: 'Property 1',
|
||||
showIf: showIfSpy,
|
||||
};
|
||||
|
||||
const customFieldRegistry: FieldConfigOptionsRegistry = new Registry<FieldConfigPropertyItem>(() => {
|
||||
return [property1];
|
||||
});
|
||||
|
||||
return {
|
||||
meta: { skipDataQuery: false },
|
||||
getPanelOptionsSupplier: jest.fn,
|
||||
fieldConfigRegistry: customFieldRegistry,
|
||||
} as unknown as PanelPlugin;
|
||||
};
|
||||
|
||||
it('showIf should get custom fieldConfig if isCustom is true', () => {
|
||||
const showIfSpy = jest.fn().mockReturnValue(true);
|
||||
|
||||
const plugin = getOnePropVizPlugin(true, showIfSpy);
|
||||
|
||||
const vizOptions = getVisualizationOptions2({
|
||||
panel: vizPanel,
|
||||
eventBus: new EventBusSrv(),
|
||||
plugin: plugin,
|
||||
instanceState: {},
|
||||
data: {
|
||||
state: LoadingState.Done,
|
||||
series: [],
|
||||
timeRange: getDefaultTimeRange(),
|
||||
annotations: [
|
||||
{
|
||||
fields: [{ name: 'test', type: FieldType.string, config: { displayName: 'annotation' }, values: [1] }],
|
||||
length: 1,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
expect(vizOptions.length).toEqual(1);
|
||||
expect(vizOptions[0].items.length).toEqual(1);
|
||||
expect(showIfSpy.mock.calls.length).toEqual(1);
|
||||
expect(showIfSpy.mock.calls[0][0].displayName).toBe('custom');
|
||||
expect(showIfSpy.mock.calls[0][2][0].fields[0].config.displayName).toBe('annotation');
|
||||
});
|
||||
|
||||
it('showIf should get normal fieldConfig if isCustom is false', () => {
|
||||
const showIfSpy = jest.fn().mockReturnValue(true);
|
||||
|
||||
const plugin = getOnePropVizPlugin(false, showIfSpy);
|
||||
|
||||
const vizOptions = getVisualizationOptions2({
|
||||
panel: vizPanel,
|
||||
eventBus: new EventBusSrv(),
|
||||
plugin: plugin,
|
||||
instanceState: {},
|
||||
data: {
|
||||
state: LoadingState.Done,
|
||||
series: [],
|
||||
timeRange: getDefaultTimeRange(),
|
||||
annotations: [
|
||||
{
|
||||
fields: [{ name: 'test', type: FieldType.string, config: { displayName: 'annotation' }, values: [1] }],
|
||||
length: 1,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
expect(vizOptions.length).toEqual(1);
|
||||
expect(vizOptions[0].items.length).toEqual(1);
|
||||
expect(showIfSpy.mock.calls.length).toEqual(1);
|
||||
expect(showIfSpy.mock.calls[0][0].displayName).toBe('default');
|
||||
expect(showIfSpy.mock.calls[0][2][0].fields[0].config.displayName).toBe('annotation');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -52,6 +52,7 @@ export function getStandardEditorContext({
|
||||
eventBus,
|
||||
getSuggestions: (scope?: VariableSuggestionsScope) => getDataLinksVariableSuggestions(dataSeries, scope),
|
||||
instanceState,
|
||||
annotations: data?.annotations,
|
||||
};
|
||||
|
||||
return context;
|
||||
@@ -102,11 +103,14 @@ export function getVisualizationOptions(props: OptionPaneRenderProps): OptionsPa
|
||||
*/
|
||||
for (const fieldOption of plugin.fieldConfigRegistry.list()) {
|
||||
if (fieldOption.isCustom) {
|
||||
if (fieldOption.showIf && !fieldOption.showIf(currentFieldConfig.defaults.custom, data?.series)) {
|
||||
if (
|
||||
fieldOption.showIf &&
|
||||
!fieldOption.showIf(currentFieldConfig.defaults.custom, data?.series, data?.annotations)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
if (fieldOption.showIf && !fieldOption.showIf(currentFieldConfig.defaults, data?.series)) {
|
||||
if (fieldOption.showIf && !fieldOption.showIf(currentFieldConfig.defaults, data?.series, data?.annotations)) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
@@ -240,8 +244,8 @@ export function getVisualizationOptions2(props: OptionPaneRenderProps2): Options
|
||||
const hideOption =
|
||||
fieldOption.showIf &&
|
||||
(fieldOption.isCustom
|
||||
? !fieldOption.showIf(currentFieldConfig.defaults.custom, data?.series)
|
||||
: !fieldOption.showIf(currentFieldConfig.defaults, data?.series));
|
||||
? !fieldOption.showIf(currentFieldConfig.defaults.custom, data?.series, data?.annotations)
|
||||
: !fieldOption.showIf(currentFieldConfig.defaults, data?.series, data?.annotations));
|
||||
if (fieldOption.hideFromDefaults || hideOption) {
|
||||
continue;
|
||||
}
|
||||
@@ -298,7 +302,7 @@ export function fillOptionsPaneItems(
|
||||
supplier(builder, context);
|
||||
|
||||
for (const pluginOption of builder.getItems()) {
|
||||
if (pluginOption.showIf && !pluginOption.showIf(context.options, context.data)) {
|
||||
if (pluginOption.showIf && !pluginOption.showIf(context.options, context.data, context.annotations)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user