diff --git a/packages/grafana-data/src/field/standardFieldConfigEditorRegistry.ts b/packages/grafana-data/src/field/standardFieldConfigEditorRegistry.ts index 83b7f5050e3..584c3779d23 100644 --- a/packages/grafana-data/src/field/standardFieldConfigEditorRegistry.ts +++ b/packages/grafana-data/src/field/standardFieldConfigEditorRegistry.ts @@ -16,6 +16,7 @@ export interface StandardEditorContext { options?: TOptions; instanceState?: TState; isOverride?: boolean; + annotations?: DataFrame[]; } export interface StandardEditorProps { diff --git a/packages/grafana-data/src/types/options.ts b/packages/grafana-data/src/types/options.ts index d23ceb35c5e..84f45884a36 100644 --- a/packages/grafana-data/src/types/options.ts +++ b/packages/grafana-data/src/types/options.ts @@ -51,5 +51,5 @@ export interface OptionEditorConfig { /** * 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; } diff --git a/packages/grafana-ui/src/options/builder/tooltip.tsx b/packages/grafana-ui/src/options/builder/tooltip.tsx index 3679ac18df8..7a9b1f59e1a 100644 --- a/packages/grafana-ui/src/options/builder/tooltip.tsx +++ b/packages/grafana-ui/src/options/builder/tooltip.tsx @@ -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( 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'; + }) + ); + }, }); } diff --git a/public/app/features/dashboard/components/PanelEditor/getVisualizationOptions.test.ts b/public/app/features/dashboard/components/PanelEditor/getVisualizationOptions.test.ts index 01f12b81f1c..6a64a1d02ea 100644 --- a/public/app/features/dashboard/components/PanelEditor/getVisualizationOptions.test.ts +++ b/public/app/features/dashboard/components/PanelEditor/getVisualizationOptions.test.ts @@ -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(() => { + 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(() => { + 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(() => { + 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'); + }); }); }); diff --git a/public/app/features/dashboard/components/PanelEditor/getVisualizationOptions.tsx b/public/app/features/dashboard/components/PanelEditor/getVisualizationOptions.tsx index be19274ca5d..b62d0493c2e 100644 --- a/public/app/features/dashboard/components/PanelEditor/getVisualizationOptions.tsx +++ b/public/app/features/dashboard/components/PanelEditor/getVisualizationOptions.tsx @@ -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; }