Suggestions: Proposed PanelPlugin API (#113688)

* Suggestions: Update ownership of core files and improve some types

* Suggestions: Proposed PanelPlugin API

* get rid of .useSuggestionsConfig

* update i18n

* iterate on TypeScript types, add example in radialbar

* tweak implementation, add commentary

* actually, suggestions really does not need panel defaults

* split suggestions handler into its own file and add tests for radialbar

* small comment revision

* fix test

* add ds.hasData check back for state-timeline

* restore a handful of comments that got lost in the merge shuffle

* more updated commnets

* remove pluginId from VisualizationSuggestion, whoops

* fix getAllSuggestions test

* update i18n

* might as well restore description here

* move fieldconfig back on radialbar

* call them suppliers, remove boolean return type in favor of internal util

* Update packages/grafana-data/src/panel/PanelPlugin.ts

* Update packages/grafana-data/src/panel/PanelPlugin.ts

* Update packages/grafana-data/src/panel/PanelPlugin.ts

* tweak return type for setSuggestionSupplier to be this
This commit is contained in:
Paul Marbach
2025-11-18 18:49:29 +01:00
committed by GitHub
parent a636e1dc8e
commit 92ac33736b
18 changed files with 456 additions and 206 deletions
+2
View File
@@ -715,6 +715,8 @@ export {
export {
type VisualizationSuggestion,
type VisualizationSuggestionsSupplier,
type VisualizationSuggestionsSupplierFn,
type PanelPluginVisualizationSuggestion,
VisualizationSuggestionScore,
VisualizationSuggestionsBuilder,
VisualizationSuggestionsListAppender,
+36 -5
View File
@@ -14,7 +14,7 @@ import {
PanelPluginDataSupport,
} from '../types/panel';
import { GrafanaPlugin } from '../types/plugin';
import { VisualizationSuggestionsSupplier } from '../types/suggestions';
import { VisualizationSuggestionsSupplierFn, VisualizationSuggestionsSupplier } from '../types/suggestions';
import { FieldConfigEditorBuilder, PanelOptionsEditorBuilder } from '../utils/OptionsUIBuilders';
import { deprecationWarning } from '../utils/deprecationWarning';
@@ -363,11 +363,34 @@ export class PanelPlugin<
}
/**
* Sets function that can return visualization examples and suggestions.
* @alpha
* @deprecated use VisualizationSuggestionsSupplierFn
*/
setSuggestionsSupplier(supplier: VisualizationSuggestionsSupplier) {
this.suggestionsSupplier = supplier;
setSuggestionsSupplier(supplier: VisualizationSuggestionsSupplier): this;
/**
* @alpha
* sets function that can return visualization examples and suggestions.
*/
setSuggestionsSupplier(supplier: VisualizationSuggestionsSupplierFn<TOptions, TFieldConfigOptions>): this;
setSuggestionsSupplier(
supplier: VisualizationSuggestionsSupplier | VisualizationSuggestionsSupplierFn<TOptions, TFieldConfigOptions>
): this {
this.suggestionsSupplier =
typeof supplier === 'function'
? {
getSuggestionsForData: (builder) => {
const appender = builder.getListAppender<TOptions, TFieldConfigOptions>({
pluginId: this.meta.id,
name: this.meta.name,
});
const result = supplier(builder.dataSummary);
if (Array.isArray(result)) {
appender.appendAll(result);
}
},
}
: supplier;
return this;
}
@@ -379,6 +402,14 @@ export class PanelPlugin<
return this.suggestionsSupplier;
}
/**
* @alpha
* returns whether the plugin has configured suggestions
*/
hasSuggestions(): boolean {
return this.suggestionsSupplier !== undefined;
}
hasPluginId(pluginId: string) {
return this.meta.id === pluginId;
}
+55 -19
View File
@@ -1,4 +1,3 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { defaultsDeep } from 'lodash';
import { DataTransformerConfig } from '@grafana/schema';
@@ -11,29 +10,45 @@ import { PanelData } from './panel';
/**
* @alpha
* A suggestion for a visualization given some data. This represents the shape of the panel (including options and field config)
* that will be used to show a small preview in the Grafana UI when suggesting visualizations in the Panel Editor.
*/
export interface VisualizationSuggestion<TOptions = any, TFieldConfig = any> {
export interface VisualizationSuggestion<TOptions extends unknown = {}, TFieldConfig extends {} = {}> {
/** Name of suggestion */
name: string;
name?: string;
/** Description */
description?: string;
/** Panel plugin id */
pluginId: string;
/** Panel plugin options */
options?: Partial<TOptions>;
/** Panel plugin field options */
fieldConfig?: FieldConfigSource<Partial<TFieldConfig>>;
/** Data transformations */
transformations?: DataTransformerConfig[];
/** A value between 0-100 how suitable suggestion is */
score?: VisualizationSuggestionScore;
/** Options for how to render suggestion card */
cardOptions?: {
/** Tweak for small preview */
previewModifier?: (suggestion: VisualizationSuggestion) => void;
/**
* Given that the suggestion is being rendered as a small preview, you may want to modify certain options
* specifically for the smaller preview version of the visualization. In this method, you should directly
* mutate the suggestion object which is passed in as the first argument.
*/
previewModifier?: (suggestion: VisualizationSuggestion<TOptions, TFieldConfig>) => void;
icon?: string;
imgSrc?: string;
};
/** A value between 0-100 how suitable suggestion is */
score?: VisualizationSuggestionScore;
}
/**
* @internal
* the internal interface that the PanelPlugin transforms the supplied suggestions into.
*/
export interface PanelPluginVisualizationSuggestion<TOptions extends unknown = {}, TFieldConfig extends {} = {}>
extends VisualizationSuggestion<TOptions, TFieldConfig> {
/** Name of suggestion */
name: string;
/** Panel plugin id */
pluginId: string;
}
/**
@@ -49,12 +64,13 @@ export enum VisualizationSuggestionScore {
}
/**
* @alpha
* @internal
* TODO this will move into the grafana app code once suppliers are migrated.
*/
export class VisualizationSuggestionsBuilder {
/** Summary stats for current data */
dataSummary: PanelDataSummary;
private list: VisualizationSuggestion[] = [];
private list: PanelPluginVisualizationSuggestion[] = [];
constructor(
/** Current data */
@@ -65,7 +81,9 @@ export class VisualizationSuggestionsBuilder {
this.dataSummary = getPanelDataSummary(data?.series);
}
getListAppender<TOptions, TFieldConfig>(defaults: VisualizationSuggestion<TOptions, TFieldConfig>) {
getListAppender<TOptions extends unknown, TFieldConfig extends {} = {}>(
defaults: PanelPluginVisualizationSuggestion<TOptions, TFieldConfig>
) {
return new VisualizationSuggestionsListAppender<TOptions, TFieldConfig>(this.list, defaults);
}
@@ -76,25 +94,43 @@ export class VisualizationSuggestionsBuilder {
/**
* @alpha
* TODO: this name is temporary; it will become just "VisualizationSuggestionsSupplier" when the other interface is deleted.
*
* executed while rendering suggestions each time the DataFrame changes, this method
* determines which suggestions can be shown for this PanelPlugin given the PanelDataSummary.
*
* - returns an array of VisualizationSuggestions
* - boolean return equates to "show a single suggestion card for this panel plugin with the default options" (true = show, false or void = hide)
*/
export type VisualizationSuggestionsSupplierFn<TOptions extends unknown, TFieldConfig extends {} = {}> = (
panelDataSummary: PanelDataSummary
) => Array<VisualizationSuggestion<TOptions, TFieldConfig>> | void;
/**
* @deprecated use VisualizationSuggestionsSupplierFn instead.
*/
export type VisualizationSuggestionsSupplier = {
/**
* Adds good suitable suggestions for the current data
* Adds suitable suggestions for the current data
*/
getSuggestionsForData: (builder: VisualizationSuggestionsBuilder) => void;
};
/**
* Helps with typings and defaults
* @alpha
* @internal
* TODO this will move into the grafana app code once suppliers are migrated.
*/
export class VisualizationSuggestionsListAppender<TOptions, TFieldConfig> {
export class VisualizationSuggestionsListAppender<TOptions extends unknown, TFieldConfig extends {} = {}> {
constructor(
private list: VisualizationSuggestion[],
private defaults: VisualizationSuggestion<TOptions, TFieldConfig>
private defaults: Partial<PanelPluginVisualizationSuggestion<TOptions, TFieldConfig>> = {}
) {}
append(overrides: Partial<VisualizationSuggestion<TOptions, TFieldConfig>>) {
this.list.push(defaultsDeep(overrides, this.defaults));
append(suggestion: VisualizationSuggestion<TOptions, TFieldConfig>) {
this.list.push(defaultsDeep(suggestion, this.defaults));
}
appendAll(suggestions: Array<VisualizationSuggestion<TOptions, TFieldConfig>>) {
this.list.push(...suggestions.map((o) => defaultsDeep(o, this.defaults)));
}
}
@@ -1,6 +1,6 @@
import * as React from 'react';
import { FieldConfigSource, PanelData, VisualizationSuggestion } from '@grafana/data';
import { FieldConfigSource, PanelData, PanelPluginVisualizationSuggestion } from '@grafana/data';
/**
* Describes the properties that can be passed to the PanelDataErrorView.
@@ -15,7 +15,7 @@ export interface PanelDataErrorViewProps {
needsTimeField?: boolean;
needsNumberField?: boolean;
needsStringField?: boolean;
suggestions?: VisualizationSuggestion[];
suggestions?: PanelPluginVisualizationSuggestion[];
}
/**