Suggestions: Deprecate the old API and put external suggestions behind a flag (#114127)

* Suggestions: Deprecate previous API, enable external plugin suggestions behind flag

* fix types for deprecated builder

* restore some support for cloud-onboarding

* add support for cloud-onboarding usage, add test to ensure it keeps working

* refactor to not hardcode on 'core:'

* remove unused import
This commit is contained in:
Paul Marbach
2025-12-01 23:22:22 +00:00
committed by GitHub
parent b332a108f3
commit e36ea78771
55 changed files with 539 additions and 209 deletions
+1
View File
@@ -53,6 +53,7 @@ pluginMetaV0Alpha1: {
skipDataQuery?: bool
state?: "alpha" | "beta"
streaming?: bool
suggestions?: bool
tracing?: bool
iam?: #IAM
// +listType=atomic
@@ -40,6 +40,7 @@ type PluginMetaJSONData struct {
SkipDataQuery *bool `json:"skipDataQuery,omitempty"`
State *PluginMetaJSONDataState `json:"state,omitempty"`
Streaming *bool `json:"streaming,omitempty"`
Suggestions *bool `json:"suggestions,omitempty"`
Tracing *bool `json:"tracing,omitempty"`
Iam *PluginMetaIAM `json:"iam,omitempty"`
// +listType=atomic
File diff suppressed because one or more lines are too long
@@ -341,6 +341,10 @@
"type": "boolean",
"description": "Initialize plugin on startup. By default, the plugin initializes on first use, but when preload is set to true the plugin loads when the Grafana web app loads the first time. Only applicable to app plugins. When setting to `true`, implement [frontend code splitting](https://grafana.com/developers/plugin-tools/get-started/best-practices#app-plugins) to minimise performance implications."
},
"suggestions": {
"type": "boolean",
"description": "For panel plugins. If set to true, the plugin's suggestions supplier will be invoked and any suggestions returned will be included in the Suggestions pane in the Panel Editor."
},
"queryOptions": {
"type": "object",
"description": "For data source plugins. There is a query options section in the plugin's query editor and these options can be turned on if needed.",
+1 -3
View File
@@ -715,11 +715,9 @@ export {
export {
type VisualizationSuggestion,
type VisualizationSuggestionsSupplier,
type VisualizationSuggestionsSupplierFn,
type PanelPluginVisualizationSuggestion,
type VisualizationSuggestionsBuilder,
VisualizationSuggestionScore,
VisualizationSuggestionsBuilder,
VisualizationSuggestionsListAppender,
} from './types/suggestions';
export {
type MatcherConfig,
@@ -1,14 +1,18 @@
import { createDataFrame } from '../dataframe/processDataFrame';
import { identityOverrideProcessor } from '../field/overrides/processors';
import {
StandardEditorsRegistryItem,
standardEditorsRegistry,
standardFieldConfigEditorRegistry,
} from '../field/standardFieldConfigEditorRegistry';
import { FieldType } from '../types/dataFrame';
import { FieldConfigProperty, FieldConfigPropertyItem } from '../types/fieldOverrides';
import { PanelMigrationModel } from '../types/panel';
import { VisualizationSuggestionsBuilder, VisualizationSuggestionScore } from '../types/suggestions';
import { PanelOptionsEditorBuilder } from '../utils/OptionsUIBuilders';
import { PanelPlugin } from './PanelPlugin';
import { getPanelDataSummary } from './suggestions/getPanelDataSummary';
describe('PanelPlugin', () => {
describe('declarative options', () => {
@@ -483,4 +487,107 @@ describe('PanelPlugin', () => {
});
});
});
describe('suggestions', () => {
it('should register a suggestions supplier', () => {
const panel = new PanelPlugin(() => <div>Panel</div>);
panel.meta = panel.meta || {};
panel.meta.id = 'test-panel';
panel.meta.name = 'Test Panel';
panel.setSuggestionsSupplier((ds) => {
if (!ds.hasFieldType(FieldType.number)) {
return;
}
return [
{
name: 'Number Panel',
score: VisualizationSuggestionScore.Good,
},
];
});
const suggestions = panel.getSuggestions(
getPanelDataSummary([createDataFrame({ fields: [{ type: FieldType.number, name: 'Value' }] })])
);
expect(suggestions).toHaveLength(1);
expect(suggestions![0].pluginId).toBe(panel.meta.id);
expect(suggestions![0].name).toBe('Number Panel');
expect(
panel.getSuggestions(
getPanelDataSummary([createDataFrame({ fields: [{ type: FieldType.string, name: 'Value' }] })])
)
).toBeUndefined();
});
it('should not throw for the old syntax, but also should not register suggestions', () => {
jest.spyOn(console, 'warn').mockImplementation();
class DeprecatedSuggestionsSupplier {
getSuggestionsForData(builder: VisualizationSuggestionsBuilder): void {
const appender = builder.getListAppender({
name: 'Deprecated Suggestion',
pluginId: 'deprecated-plugin',
options: {},
});
if (builder.dataSummary.hasNumberField) {
appender.append({});
}
}
}
const panel = new PanelPlugin(() => <div>Panel</div>);
expect(() => {
panel.setSuggestionsSupplier(new DeprecatedSuggestionsSupplier());
}).not.toThrow();
expect(console.warn).toHaveBeenCalled();
expect(
panel.getSuggestions(
getPanelDataSummary([
createDataFrame({
fields: [{ type: FieldType.number, name: 'Value', values: [1, 2, 3, 4, 5] }],
}),
])
)
).toBeUndefined();
});
it('should support the deprecated pattern of getSuggestionsSupplier with builder', () => {
jest.spyOn(console, 'warn').mockImplementation();
const panel = new PanelPlugin(() => <div>Panel</div>).setSuggestionsSupplier((ds) => {
if (!ds.hasFieldType(FieldType.number)) {
return;
}
return [
{
name: 'Number Panel',
score: VisualizationSuggestionScore.Good,
},
];
});
const oldSupplier = panel.getSuggestionsSupplier();
const builder1 = new VisualizationSuggestionsBuilder([
createDataFrame({ fields: [{ type: FieldType.number, name: 'Value' }] }),
]);
oldSupplier.getSuggestionsForData(builder1);
const suggestions1 = builder1.getList();
expect(suggestions1).toHaveLength(1);
expect(suggestions1![0].pluginId).toBe(panel.meta.id);
expect(suggestions1![0].name).toBe('Number Panel');
const builder2 = new VisualizationSuggestionsBuilder([
createDataFrame({ fields: [{ type: FieldType.string, name: 'Value' }] }),
]);
oldSupplier.getSuggestionsForData(builder2);
const suggestions2 = builder2.getList();
expect(suggestions2).toHaveLength(0);
});
});
});
+72 -36
View File
@@ -1,4 +1,4 @@
import { set } from 'lodash';
import { defaultsDeep, set } from 'lodash';
import { ComponentClass, ComponentType } from 'react';
import { FieldConfigOptionsRegistry } from '../field/FieldConfigOptionsRegistry';
@@ -14,11 +14,19 @@ import {
PanelPluginDataSupport,
} from '../types/panel';
import { GrafanaPlugin } from '../types/plugin';
import { VisualizationSuggestionsSupplierFn, VisualizationSuggestionsSupplier } from '../types/suggestions';
import {
getSuggestionHash,
PanelPluginVisualizationSuggestion,
VisualizationSuggestion,
VisualizationSuggestionsSupplierDeprecated,
VisualizationSuggestionsSupplier,
VisualizationSuggestionsBuilder,
} from '../types/suggestions';
import { FieldConfigEditorBuilder, PanelOptionsEditorBuilder } from '../utils/OptionsUIBuilders';
import { deprecationWarning } from '../utils/deprecationWarning';
import { createFieldConfigRegistry } from './registryFactories';
import { PanelDataSummary } from './suggestions/getPanelDataSummary';
/** @beta */
export type StandardOptionConfig = {
@@ -109,7 +117,7 @@ export class PanelPlugin<
};
private optionsSupplier?: PanelOptionsSupplier<TOptions>;
private suggestionsSupplier?: VisualizationSuggestionsSupplier;
private suggestionsSupplier?: VisualizationSuggestionsSupplier<TOptions, TFieldConfigOptions>;
panel: ComponentType<PanelProps<TOptions>> | null;
editor?: ComponentClass<PanelEditorProps<TOptions>>;
@@ -363,56 +371,84 @@ export class PanelPlugin<
}
/**
* @deprecated use VisualizationSuggestionsSupplierFn
* @deprecated use VisualizationSuggestionsSupplier
*/
setSuggestionsSupplier(supplier: VisualizationSuggestionsSupplier): this;
setSuggestionsSupplier(supplier: VisualizationSuggestionsSupplierDeprecated): this;
/**
* @alpha
* sets function that can return visualization examples and suggestions.
*/
setSuggestionsSupplier(supplier: VisualizationSuggestionsSupplierFn<TOptions, TFieldConfigOptions>): this;
setSuggestionsSupplier(supplier: VisualizationSuggestionsSupplier<TOptions, TFieldConfigOptions>): this;
setSuggestionsSupplier(
supplier: VisualizationSuggestionsSupplier | VisualizationSuggestionsSupplierFn<TOptions, TFieldConfigOptions>
supplier:
| VisualizationSuggestionsSupplier<TOptions, TFieldConfigOptions>
| VisualizationSuggestionsSupplierDeprecated
): this {
this.suggestionsSupplier =
typeof supplier === 'function'
? {
getSuggestionsForData: (builder) => {
const appender = builder.getListAppender<TOptions, TFieldConfigOptions>({
pluginId: this.meta.id,
name: this.meta.name,
options: {},
fieldConfig: {
defaults: {},
overrides: [],
},
});
const result = supplier(builder.dataSummary);
if (Array.isArray(result)) {
appender.appendAll(result);
}
},
}
: supplier;
if (typeof supplier !== 'function') {
deprecationWarning(
'PanelPlugin',
'plugin.setSuggestionsSupplier(new Supplier())',
'plugin.setSuggestionsSupplier(dataSummary => [...])'
);
return this;
}
this.suggestionsSupplier = supplier;
return this;
}
/**
* Returns the suggestions supplier
* @alpha
* get suggestions based on the PanelDataSummary
*/
getSuggestionsSupplier(): VisualizationSuggestionsSupplier | undefined {
return this.suggestionsSupplier;
getSuggestions(
panelDataSummary: PanelDataSummary
): Array<PanelPluginVisualizationSuggestion<TOptions, TFieldConfigOptions>> | void {
const withDefaults = (
suggestion: VisualizationSuggestion<TOptions, TFieldConfigOptions>
): Omit<PanelPluginVisualizationSuggestion<TOptions, TFieldConfigOptions>, 'hash'> =>
defaultsDeep(suggestion, {
pluginId: this.meta.id,
name: this.meta.name,
options: {},
fieldConfig: {
defaults: {},
overrides: [],
},
} satisfies Omit<PanelPluginVisualizationSuggestion<TOptions, TFieldConfigOptions>, 'hash'>);
return this.suggestionsSupplier?.(panelDataSummary)?.map(
(s): PanelPluginVisualizationSuggestion<TOptions, TFieldConfigOptions> => {
const suggestionWithDefaults = withDefaults(s);
return Object.assign(suggestionWithDefaults, { hash: getSuggestionHash(suggestionWithDefaults) });
}
);
}
/**
* @alpha
* returns whether the plugin has configured suggestions
* @deprecated use getSuggestions
* we have to keep this method intact to support cloud-onboarding plugin.
*/
hasSuggestions(): boolean {
return this.suggestionsSupplier !== undefined;
getSuggestionsSupplier() {
const withDefaults = (
suggestion: VisualizationSuggestion<TOptions, TFieldConfigOptions>
): Omit<PanelPluginVisualizationSuggestion<TOptions, TFieldConfigOptions>, 'hash'> =>
defaultsDeep(suggestion, {
pluginId: this.meta.id,
name: this.meta.name,
options: {},
fieldConfig: {
defaults: {},
overrides: [],
},
} satisfies Omit<PanelPluginVisualizationSuggestion<TOptions, TFieldConfigOptions>, 'hash'>);
return {
getSuggestionsForData: (builder: VisualizationSuggestionsBuilder) => {
deprecationWarning('PanelPlugin', 'getSuggestionsSupplier()', 'getSuggestions(panelDataSummary)');
this.suggestionsSupplier?.(builder.dataSummary)?.forEach((s) => {
builder.getListAppender(withDefaults(s)).append(s);
});
},
};
}
hasPluginId(pluginId: string) {
+5
View File
@@ -1143,6 +1143,11 @@ export interface FeatureToggles {
*/
newVizSuggestions?: boolean;
/**
* Enable all plugins to supply visualization suggestions (including 3rd party plugins)
* @default false
*/
externalVizSuggestions?: boolean;
/**
* Restrict PanelChrome contents with overflow: hidden;
* @default true
*/
+2
View File
@@ -20,6 +20,8 @@ export type InterpolateFunction = (value: string, scopedVars?: ScopedVars, forma
export interface PanelPluginMeta extends PluginMeta {
/** Indicates that panel does not issue queries */
skipDataQuery?: boolean;
/** Indicates that the panel implements suggestions */
suggestions?: boolean;
/** Indicates that panel should not be available in visualisation picker */
hideFromList?: boolean;
/** Sort order */
+33 -55
View File
@@ -2,11 +2,10 @@ import { defaultsDeep } from 'lodash';
import { DataTransformerConfig } from '@grafana/schema';
import { PanelDataSummary, getPanelDataSummary } from '../panel/suggestions/getPanelDataSummary';
import { getPanelDataSummary, PanelDataSummary } from '../panel/suggestions/getPanelDataSummary';
import { PanelModel } from './dashboard';
import { DataFrame } from './dataFrame';
import { FieldConfigSource } from './fieldOverrides';
import { PanelData } from './panel';
/**
* @internal
@@ -108,35 +107,6 @@ export enum VisualizationSuggestionScore {
OK = 50,
}
/**
* @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: PanelPluginVisualizationSuggestion[] = [];
constructor(
/** Current data */
public data?: PanelData,
/** Current panel & options */
public panel?: PanelModel
) {
this.dataSummary = getPanelDataSummary(data?.series);
}
getListAppender<TOptions extends unknown, TFieldConfig extends {} = {}>(
defaults: Omit<PanelPluginVisualizationSuggestion<TOptions, TFieldConfig>, 'hash'>
) {
return new VisualizationSuggestionsListAppender<TOptions, TFieldConfig>(this.list, defaults);
}
getList() {
return this.list;
}
}
/**
* @alpha
* TODO: this name is temporary; it will become just "VisualizationSuggestionsSupplier" when the other interface is deleted.
@@ -147,40 +117,48 @@ export class VisualizationSuggestionsBuilder {
* - 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 {} = {}> = (
export type VisualizationSuggestionsSupplier<TOptions extends unknown, TFieldConfig extends {} = {}> = (
panelDataSummary: PanelDataSummary
) => Array<VisualizationSuggestion<TOptions, TFieldConfig>> | void;
/**
* @deprecated use VisualizationSuggestionsSupplierFn instead.
* DEPRECATED - the below exports need to remain in the code base to help make the transition for the Polystat plugin, which implements
* suggestions using the old API. These should be removed for Grafana 13.
*/
export type VisualizationSuggestionsSupplier = {
/**
* Adds suitable suggestions for the current data
*/
/**
* @deprecated use VisualizationSuggestionsSupplier
*/
export interface VisualizationSuggestionsSupplierDeprecated {
getSuggestionsForData: (builder: VisualizationSuggestionsBuilder) => void;
};
}
/**
* @internal
* TODO this will move into the grafana app code once suppliers are migrated.
* @deprecated use VisualizationSuggestionsSupplier
*/
export class VisualizationSuggestionsListAppender<TOptions extends unknown, TFieldConfig extends {} = {}> {
constructor(
private list: VisualizationSuggestion[],
private defaults: Partial<PanelPluginVisualizationSuggestion<TOptions, TFieldConfig>> = {}
) {}
export class VisualizationSuggestionsBuilder {
public dataSummary: PanelDataSummary;
public list: PanelPluginVisualizationSuggestion[] = [];
append(suggestion: VisualizationSuggestion<TOptions, TFieldConfig>) {
this.appendAll([suggestion]);
constructor(dataFrames: DataFrame[]) {
this.dataSummary = getPanelDataSummary(dataFrames);
}
appendAll(suggestions: Array<VisualizationSuggestion<TOptions, TFieldConfig>>) {
this.list.push(
...suggestions.map((s): PanelPluginVisualizationSuggestion<TOptions, TFieldConfig> => {
const suggestionWithDefaults = defaultsDeep(s, this.defaults);
return Object.assign(suggestionWithDefaults, { hash: getSuggestionHash(suggestionWithDefaults) });
})
);
getList(): PanelPluginVisualizationSuggestion[] {
return this.list;
}
getListAppender(suggestionDefaults: Omit<PanelPluginVisualizationSuggestion, 'hash'>) {
const withDefaults = (suggestion: VisualizationSuggestion): PanelPluginVisualizationSuggestion => {
const s = defaultsDeep({}, suggestion, suggestionDefaults);
return {
...s,
hash: getSuggestionHash(s),
};
};
return {
append: (suggestion: VisualizationSuggestion) => {
this.list.push(withDefaults(suggestion));
},
};
}
}
+1
View File
@@ -164,6 +164,7 @@ func (hs *HTTPServer) getFrontendSettings(c *contextmodel.ReqContext) (*dtos.Fro
ModuleHash: hs.pluginAssets.ModuleHash(c.Req.Context(), panel),
BaseURL: panel.BaseURL,
SkipDataQuery: panel.SkipDataQuery,
Suggestions: panel.Suggestions,
HideFromList: panel.HideFromList,
ReleaseState: string(panel.State),
Signature: string(panel.Signature),
+1
View File
@@ -319,6 +319,7 @@ type PanelDTO struct {
HideFromList bool `json:"hideFromList"`
Sort int `json:"sort"`
SkipDataQuery bool `json:"skipDataQuery"`
Suggestions bool `json:"suggestions,omitempty"`
ReleaseState string `json:"state"`
BaseURL string `json:"baseUrl"`
Signature string `json:"signature"`
+1
View File
@@ -105,6 +105,7 @@ type JSONData struct {
// Panel settings
SkipDataQuery bool `json:"skipDataQuery"`
Suggestions bool `json:"suggestions,omitempty"`
// App settings
AutoEnabled bool `json:"autoEnabled"`
+8
View File
@@ -1884,6 +1884,14 @@ var (
Owner: grafanaDatavizSquad,
Expression: "false",
},
{
Name: "externalVizSuggestions",
Description: "Enable all plugins to supply visualization suggestions (including 3rd party plugins)",
Stage: FeatureStageExperimental,
FrontendOnly: true,
Owner: grafanaDatavizSquad,
Expression: "false",
},
{
Name: "preventPanelChromeOverflow",
Description: "Restrict PanelChrome contents with overflow: hidden;",
+1
View File
@@ -256,6 +256,7 @@ cdnPluginsUrls,experimental,@grafana/plugins-platform-backend,false,false,false
pluginInstallAPISync,experimental,@grafana/plugins-platform-backend,false,false,false
newGauge,experimental,@grafana/dataviz-squad,false,false,true
newVizSuggestions,preview,@grafana/dataviz-squad,false,false,true
externalVizSuggestions,experimental,@grafana/dataviz-squad,false,false,true
preventPanelChromeOverflow,preview,@grafana/grafana-frontend-platform,false,false,true
jaegerEnableGrpcEndpoint,experimental,@grafana/oss-big-tent,false,false,false
pluginStoreServiceLoading,experimental,@grafana/plugins-platform-backend,false,false,false
1 Name Stage Owner requiresDevMode RequiresRestart FrontendOnly
256 pluginInstallAPISync experimental @grafana/plugins-platform-backend false false false
257 newGauge experimental @grafana/dataviz-squad false false true
258 newVizSuggestions preview @grafana/dataviz-squad false false true
259 externalVizSuggestions experimental @grafana/dataviz-squad false false true
260 preventPanelChromeOverflow preview @grafana/grafana-frontend-platform false false true
261 jaegerEnableGrpcEndpoint experimental @grafana/oss-big-tent false false false
262 pluginStoreServiceLoading experimental @grafana/plugins-platform-backend false false false
+14
View File
@@ -1383,6 +1383,20 @@
"codeowner": "@grafana/identity-access-team"
}
},
{
"metadata": {
"name": "externalVizSuggestions",
"resourceVersion": "1763498528748",
"creationTimestamp": "2025-11-18T20:42:08Z"
},
"spec": {
"description": "Enable all plugins to supply visualization suggestions (including 3rd party plugins)",
"stage": "experimental",
"codeowner": "@grafana/dataviz-squad",
"frontend": true,
"expression": "false"
}
},
{
"metadata": {
"name": "extraThemes",
@@ -31,7 +31,6 @@ import { UNCONFIGURED_PANEL_PLUGIN_ID } from '../scene/UnconfiguredPanel';
import { DashboardGridItem } from '../scene/layout-default/DashboardGridItem';
import { DashboardLayoutItem, isDashboardLayoutItem } from '../scene/types/DashboardLayoutItem';
import { vizPanelToPanel } from '../serialization/transformSceneToSaveModel';
import { PanelModelCompatibilityWrapper } from '../utils/PanelModelCompatibilityWrapper';
import {
activateSceneObjectAndParentTree,
getDashboardSceneFor,
@@ -121,8 +120,7 @@ export class PanelEditor extends SceneObjectBase<PanelEditorState> {
dataObject.subscribeToState(async () => {
const { data } = dataObject.state;
if (hasData(data) && panel.state.pluginId === UNCONFIGURED_PANEL_PLUGIN_ID) {
const panelModel = new PanelModelCompatibilityWrapper(panel);
const suggestions = await getAllSuggestions(data, panelModel);
const suggestions = await getAllSuggestions(data);
if (suggestions.length > 0) {
const defaultFirstSuggestion = suggestions[0];
@@ -25,7 +25,7 @@ const MIN_COLUMN_SIZE = 260;
export function VisualizationSuggestions({ onChange, data, panel }: Props) {
const styles = useStyles2(getStyles);
const { value: suggestions } = useAsync(() => getAllSuggestions(data, panel), [data, panel]);
const { value: suggestions } = useAsync(async () => await getAllSuggestions(data), [data]);
const [suggestionHash, setSuggestionHash] = useState<string | null>(null);
const [firstCardRef, { width }] = useMeasure<HTMLDivElement>();
const [firstCardHash, setFirstCardHash] = useState<string | null>(null);
@@ -0,0 +1,19 @@
export const panelsToCheckFirst = [
'timeseries',
'barchart',
'gauge',
'stat',
'piechart',
'bargauge',
'table',
'state-timeline',
'status-history',
'logs',
'candlestick',
'flamegraph',
'traces',
'nodeGraph',
'heatmap',
'histogram',
'geomap',
];
@@ -2,11 +2,13 @@ import {
DataFrame,
FieldType,
getDefaultTimeRange,
getPanelDataSummary,
LoadingState,
PanelData,
PanelPluginMeta,
PanelPluginVisualizationSuggestion,
PluginType,
toDataFrame,
VisualizationSuggestionScore,
} from '@grafana/data';
import {
BarGaugeDisplayMode,
@@ -18,26 +20,69 @@ import {
} from '@grafana/schema';
import { config } from 'app/core/config';
import { getAllSuggestions, panelsToCheckFirst } from './getAllSuggestions';
import { panelsToCheckFirst } from './consts';
import { getAllSuggestions, sortSuggestions } from './getAllSuggestions';
config.featureToggles.externalVizSuggestions = true;
let idx = 0;
for (const pluginId of panelsToCheckFirst) {
if (pluginId === 'geomap') {
continue;
}
config.panels[pluginId] = {
module: `core:plugin/${pluginId}`,
id: pluginId,
} as PanelPluginMeta;
module: `core:plugin/${pluginId}`,
sort: idx++,
name: pluginId,
type: PluginType.panel,
baseUrl: 'public/app/plugins/panel',
suggestions: true,
info: {
version: '1.0.0',
updated: '2025-01-01',
links: [],
screenshots: [],
author: {
name: 'Grafana Labs',
},
description: pluginId,
logos: { small: 'small/logo', large: 'large/logo' },
},
};
}
const SCALAR_PLUGINS = ['gauge', 'stat', 'bargauge', 'piechart', 'radialbar'];
config.panels['text'] = {
config.panels.text = {
id: 'text',
module: 'core:plugin/text',
sort: idx++,
name: 'Text',
type: PluginType.panel,
baseUrl: 'public/app/plugins/panel',
skipDataQuery: true,
suggestions: false,
info: {
description: 'pretty decent plugin',
version: '1.0.0',
updated: '2025-01-01',
links: [],
screenshots: [],
author: {
name: 'Grafana Labs',
},
description: 'Text panel',
logos: { small: 'small/logo', large: 'large/logo' },
},
} as PanelPluginMeta;
};
jest.mock('../state/util', () => {
const originalModule = jest.requireActual('../state/util');
return {
...originalModule,
getAllPanelPluginMeta: jest.fn().mockImplementation(() => [...Object.values(config.panels)]),
};
});
const SCALAR_PLUGINS = ['gauge', 'stat', 'bargauge', 'piechart', 'radialbar'];
class ScenarioContext {
data: DataFrame[] = [];
@@ -289,10 +334,8 @@ scenario('Single frame with string and number field', (ctx) => {
pluginId: 'stat',
options: expect.objectContaining({ colorMode: BigValueColorMode.Background }),
}),
expect.objectContaining({
pluginId: 'bargauge',
options: expect.objectContaining({ displayMode: BarGaugeDisplayMode.Basic }),
}),
expect.objectContaining({
pluginId: 'bargauge',
@@ -447,6 +490,70 @@ scenario('Given a preferredVisualisationType', (ctx) => {
});
});
describe('sortSuggestions', () => {
it('should sort suggestions correctly by score', () => {
const suggestions = [
{ pluginId: 'timeseries', name: 'Time series', hash: 'b', score: VisualizationSuggestionScore.OK },
{ pluginId: 'table', name: 'Table', hash: 'a', score: VisualizationSuggestionScore.OK },
{ pluginId: 'stat', name: 'Stat', hash: 'c', score: VisualizationSuggestionScore.Good },
] satisfies PanelPluginVisualizationSuggestion[];
const dataSummary = getPanelDataSummary([
toDataFrame({
fields: [
{ name: 'Time', type: FieldType.time, values: [1, 2, 3, 4, 5] },
{ name: 'ServerA', type: FieldType.number, values: [1, 10, 50, 2, 5] },
{ name: 'ServerB', type: FieldType.number, values: [1, 10, 50, 2, 5] },
],
}),
]);
sortSuggestions(suggestions, dataSummary);
expect(suggestions[0].pluginId).toBe('stat');
expect(suggestions[1].pluginId).toBe('timeseries');
expect(suggestions[2].pluginId).toBe('table');
});
it('should sort suggestions based on core module', () => {
const suggestions = [
{
pluginId: 'fake-external-panel',
name: 'Time series',
hash: 'b',
score: VisualizationSuggestionScore.Good,
},
{
pluginId: 'fake-external-panel',
name: 'Time series',
hash: 'd',
score: VisualizationSuggestionScore.Best,
},
{ pluginId: 'timeseries', name: 'Table', hash: 'a', score: VisualizationSuggestionScore.OK },
{ pluginId: 'stat', name: 'Stat', hash: 'c', score: VisualizationSuggestionScore.Good },
] satisfies PanelPluginVisualizationSuggestion[];
const dataSummary = getPanelDataSummary([
toDataFrame({
fields: [
{ name: 'Time', type: FieldType.time, values: [1, 2, 3, 4, 5] },
{ name: 'ServerA', type: FieldType.number, values: [1, 10, 50, 2, 5] },
{ name: 'ServerB', type: FieldType.number, values: [1, 10, 50, 2, 5] },
],
}),
]);
sortSuggestions(suggestions, dataSummary);
expect(suggestions[0].pluginId).toBe('stat');
expect(suggestions[1].pluginId).toBe('timeseries');
expect(suggestions[2].pluginId).toBe('fake-external-panel');
expect(suggestions[2].hash).toBe('d');
expect(suggestions[3].pluginId).toBe('fake-external-panel');
expect(suggestions[3].hash).toBe('b');
});
});
function repeatFrame(count: number, frame: DataFrame): DataFrame[] {
const frames: DataFrame[] = [];
for (let i = 0; i < count; i++) {
@@ -1,33 +1,52 @@
import {
getPanelDataSummary,
PanelData,
PanelDataSummary,
PanelPlugin,
PanelPluginVisualizationSuggestion,
VisualizationSuggestionsBuilder,
PanelModel,
VisualizationSuggestionScore,
PreferredVisualisationType,
VisualizationSuggestionScore,
} from '@grafana/data';
import { config } from '@grafana/runtime';
import { importPanelPlugin } from 'app/features/plugins/importPanelPlugin';
import { importPanelPlugin, isBuiltInPlugin } from 'app/features/plugins/importPanelPlugin';
export const panelsToCheckFirst = [
'timeseries',
'barchart',
'gauge',
'stat',
'piechart',
'bargauge',
'table',
'state-timeline',
'status-history',
'logs',
'candlestick',
'flamegraph',
'traces',
'nodeGraph',
'heatmap',
'histogram',
'geomap',
];
import { getAllPanelPluginMeta } from '../state/util';
import { panelsToCheckFirst } from './consts';
/**
* gather and cache the plugins which provide visualization suggestions so they can be invoked to build suggestions
*/
let _pluginCache: PanelPlugin[] | null = null;
async function getPanelsWithSuggestions(): Promise<PanelPlugin[]> {
if (!_pluginCache) {
_pluginCache = [];
// list of plugins to load is determined by the feature flag
const pluginIds: string[] = config.featureToggles.externalVizSuggestions
? getAllPanelPluginMeta()
.filter((panel) => panel.suggestions)
.map((m) => m.id)
: panelsToCheckFirst;
// import the plugins in parallel using Promise.allSettled
const settledPromises = await Promise.allSettled(pluginIds.map((id) => importPanelPlugin(id)));
for (let i = 0; i < settledPromises.length; i++) {
const settled = settledPromises[i];
if (settled.status === 'fulfilled') {
_pluginCache.push(settled.value);
}
// TODO: do we want to somehow log if there were errors loading some of the plugins?
}
}
if (_pluginCache.length === 0) {
throw new Error('No panel plugins with visualization suggestions found');
}
return _pluginCache;
}
/**
* some of the PreferredVisualisationTypes do not match the panel plugin ids, so we have to map them. d'oh.
@@ -44,24 +63,54 @@ const mapPreferredVisualisationTypeToPlugin = (type: string): PreferredVisualisa
return PLUGIN_ID_TO_PREFERRED_VIZ_TYPE[type];
};
export async function getAllSuggestions(
data?: PanelData,
panel?: PanelModel
): Promise<PanelPluginVisualizationSuggestion[]> {
const builder = new VisualizationSuggestionsBuilder(data, panel);
/**
* given a list of suggestions, sort them in place based on score and preferred visualisation type
*/
export function sortSuggestions(suggestions: PanelPluginVisualizationSuggestion[], dataSummary: PanelDataSummary) {
suggestions.sort((a, b) => {
// if one of these suggestions is from a built-in panel and the other isn't, prioritize the core panel.
const isPluginABuiltIn = isBuiltInPlugin(a.pluginId);
const isPluginBBuiltIn = isBuiltInPlugin(b.pluginId);
if (isPluginABuiltIn && !isPluginBBuiltIn) {
return -1;
}
if (isPluginBBuiltIn && !isPluginABuiltIn) {
return 1;
}
for (const pluginId of panelsToCheckFirst) {
const plugin = await importPanelPlugin(pluginId);
const supplier = plugin.getSuggestionsSupplier();
// if a preferred visualisation type matches the data, prioritize it
const mappedA = mapPreferredVisualisationTypeToPlugin(a.pluginId);
if (mappedA && dataSummary.hasPreferredVisualisationType(mappedA)) {
return -1;
}
const mappedB = mapPreferredVisualisationTypeToPlugin(a.pluginId);
if (mappedB && dataSummary.hasPreferredVisualisationType(mappedB)) {
return 1;
}
if (supplier) {
supplier.getSuggestionsForData(builder);
// compare scores directly if there are no other factors
return (b.score ?? VisualizationSuggestionScore.OK) - (a.score ?? VisualizationSuggestionScore.OK);
});
}
/**
* given PanelData, return a sorted list of Suggestions from all plugins which support it.
* @param {PanelData} data queried and transformed data for the panel
* @returns {PanelPluginVisualizationSuggestion[]} sorted list of suggestions
*/
export async function getAllSuggestions(data?: PanelData): Promise<PanelPluginVisualizationSuggestion[]> {
const dataSummary = getPanelDataSummary(data?.series);
const list: PanelPluginVisualizationSuggestion[] = [];
const plugins = await getPanelsWithSuggestions();
for (const plugin of plugins) {
const suggestions = plugin.getSuggestions(dataSummary);
if (suggestions) {
list.push(...suggestions);
}
}
const list = builder.getList();
if (builder.dataSummary.fieldCount === 0) {
if (dataSummary.fieldCount === 0) {
for (const plugin of Object.values(config.panels)) {
if (!plugin.skipDataQuery || plugin.hideFromList) {
continue;
@@ -79,15 +128,7 @@ export async function getAllSuggestions(
}
}
return list.sort((a, b) => {
const mappedA = mapPreferredVisualisationTypeToPlugin(a.pluginId);
if (mappedA && builder.dataSummary.hasPreferredVisualisationType(mappedA)) {
return -1;
}
const mappedB = mapPreferredVisualisationTypeToPlugin(a.pluginId);
if (mappedB && builder.dataSummary.hasPreferredVisualisationType(mappedB)) {
return 1;
}
return (b.score ?? VisualizationSuggestionScore.OK) - (a.score ?? VisualizationSuggestionScore.OK);
});
sortSuggestions(list, dataSummary);
return list;
}
@@ -115,4 +115,8 @@ const builtInPlugins: Record<string, System.Module | (() => Promise<System.Modul
'core:plugin/radialbar': radialBar,
};
export function isBuiltinPluginPath(path: string): path is keyof typeof builtInPlugins {
return Boolean(builtInPlugins[path]);
}
export default builtInPlugins;
@@ -1,6 +1,7 @@
import { PanelPlugin, PanelPluginMeta } from '@grafana/data';
import config from 'app/core/config';
import builtInPlugins, { isBuiltinPluginPath } from './built_in_plugins';
import { pluginImporter } from './importer/pluginImporter';
const promiseCache: Record<string, Promise<PanelPlugin>> = {};
@@ -25,6 +26,14 @@ export function importPanelPlugin(id: string): Promise<PanelPlugin> {
return promiseCache[id];
}
export function isBuiltInPlugin(id?: string): id is keyof typeof builtInPlugins {
if (!id) {
return false;
}
const meta = getPanelPluginMeta(id);
return Boolean(meta != null && isBuiltinPluginPath(meta.module));
}
export function hasPanelPlugin(id: string): boolean {
return !!getPanelPluginMeta(id);
}
@@ -2,7 +2,7 @@ import { DEFAULT_LANGUAGE } from '@grafana/i18n';
import { getResolvedLanguage } from '@grafana/i18n/internal';
import { config } from '@grafana/runtime';
import builtInPlugins from '../built_in_plugins';
import builtInPlugins, { isBuiltinPluginPath } from '../built_in_plugins';
import { registerPluginInfoInCache } from '../loader/pluginInfoCache';
import { SystemJS } from '../loader/systemjs';
import { resolveModulePath } from '../loader/utils';
@@ -35,8 +35,8 @@ export async function importPluginModule({
});
}
const builtIn = builtInPlugins[path];
if (builtIn) {
if (isBuiltinPluginPath(path)) {
const builtIn = builtInPlugins[path];
// for handling dynamic imports
if (typeof builtIn === 'function') {
return await builtIn();
@@ -2,7 +2,7 @@
"type": "panel",
"name": "Bar chart",
"id": "barchart",
"suggestions": true,
"info": {
"description": "Categorical charts with group support",
"author": {
@@ -1,6 +1,6 @@
import { defaultsDeep } from 'lodash';
import { FieldType, VisualizationSuggestion, VisualizationSuggestionsSupplierFn, VizOrientation } from '@grafana/data';
import { FieldType, VisualizationSuggestion, VisualizationSuggestionsSupplier, VizOrientation } from '@grafana/data';
import { t } from '@grafana/i18n';
import { LegendDisplayMode, StackingMode, VisibilityMode } from '@grafana/schema';
@@ -32,7 +32,7 @@ const withDefaults = (suggestion: VisualizationSuggestion<Options, FieldConfig>)
},
} satisfies VisualizationSuggestion<Options, FieldConfig>);
export const barchartSuggestionsSupplier: VisualizationSuggestionsSupplierFn<Options, FieldConfig> = (dataSummary) => {
export const barchartSuggestionsSupplier: VisualizationSuggestionsSupplier<Options, FieldConfig> = (dataSummary) => {
if (dataSummary.frameCount !== 1) {
return;
}
@@ -2,7 +2,7 @@
"type": "panel",
"name": "Bar gauge",
"id": "bargauge",
"suggestions": true,
"info": {
"description": "Horizontal and vertical gauges",
"author": {
@@ -4,7 +4,7 @@ import {
FieldColorModeId,
FieldType,
VisualizationSuggestion,
VisualizationSuggestionsSupplierFn,
VisualizationSuggestionsSupplier,
VizOrientation,
} from '@grafana/data';
import { t } from '@grafana/i18n';
@@ -31,7 +31,7 @@ const withDefaults = (suggestion: VisualizationSuggestion<Options>): Visualizati
const BAR_LIMIT = 30;
export const barGaugeSugggestionsSupplier: VisualizationSuggestionsSupplierFn<Options> = (dataSummary) => {
export const barGaugeSugggestionsSupplier: VisualizationSuggestionsSupplier<Options> = (dataSummary) => {
if (!dataSummary.hasData || !dataSummary.hasFieldType(FieldType.number)) {
return;
}
@@ -2,7 +2,7 @@
"type": "panel",
"name": "Candlestick",
"id": "candlestick",
"suggestions": true,
"info": {
"description": "Graphical representation of price movements of a security, derivative, or currency.",
"keywords": ["financial", "price", "currency", "k-line"],
@@ -1,10 +1,10 @@
import { FieldType, VisualizationSuggestionScore, VisualizationSuggestionsSupplierFn } from '@grafana/data';
import { FieldType, VisualizationSuggestionScore, VisualizationSuggestionsSupplier } from '@grafana/data';
import { config } from '@grafana/runtime';
import { prepareCandlestickFields } from './fields';
import { defaultOptions, Options } from './types';
export const candlestickSuggestionSupplier: VisualizationSuggestionsSupplierFn<Options> = (dataSummary) => {
export const candlestickSuggestionSupplier: VisualizationSuggestionsSupplier<Options> = (dataSummary) => {
if (
!dataSummary.rawFrames ||
!dataSummary.hasData ||
@@ -2,7 +2,7 @@
"type": "panel",
"name": "Flame Graph",
"id": "flamegraph",
"suggestions": true,
"info": {
"author": {
"name": "Grafana Labs",
+1 -1
View File
@@ -2,7 +2,7 @@
"type": "panel",
"name": "Gauge",
"id": "gauge",
"suggestions": true,
"info": {
"description": "Standard gauge visualization",
"author": {
@@ -1,6 +1,6 @@
import { defaultsDeep } from 'lodash';
import { ThresholdsMode, FieldType, VisualizationSuggestion, VisualizationSuggestionsSupplierFn } from '@grafana/data';
import { ThresholdsMode, FieldType, VisualizationSuggestion, VisualizationSuggestionsSupplier } from '@grafana/data';
import { t } from '@grafana/i18n';
import { defaultNumericVizOptions } from 'app/features/panel/suggestions/utils';
@@ -33,7 +33,7 @@ const withDefaults = (suggestion: VisualizationSuggestion<Options>): Visualizati
const GAUGE_LIMIT = 10;
export const gaugeSuggestionsSupplier: VisualizationSuggestionsSupplierFn<Options> = (dataSummary) => {
export const gaugeSuggestionsSupplier: VisualizationSuggestionsSupplier<Options> = (dataSummary) => {
if (!dataSummary.hasData || !dataSummary.hasFieldType(FieldType.number)) {
return;
}
+1 -1
View File
@@ -2,7 +2,7 @@
"type": "panel",
"name": "Geomap",
"id": "geomap",
"suggestions": true,
"info": {
"description": "Geomap panel",
"author": {
@@ -1,12 +1,10 @@
import { VisualizationSuggestionScore, VisualizationSuggestionsSupplierFn } from '@grafana/data';
import { VisualizationSuggestionScore, VisualizationSuggestionsSupplier } from '@grafana/data';
import { GraphFieldConfig } from '@grafana/ui';
import { getGeometryField, getDefaultLocationMatchers } from 'app/features/geo/utils/location';
import { Options } from './panelcfg.gen';
export const geomapSuggestionsSupplier: VisualizationSuggestionsSupplierFn<Options, GraphFieldConfig> = (
dataSummary
) => {
export const geomapSuggestionsSupplier: VisualizationSuggestionsSupplier<Options, GraphFieldConfig> = (dataSummary) => {
if (!dataSummary.hasData || !dataSummary.rawFrames) {
return;
}
+1 -1
View File
@@ -2,7 +2,7 @@
"type": "panel",
"name": "Heatmap",
"id": "heatmap",
"suggestions": true,
"info": {
"description": "Like a histogram over time",
"author": {
@@ -3,7 +3,7 @@ import {
FieldType,
PanelDataSummary,
VisualizationSuggestionScore,
VisualizationSuggestionsSupplierFn,
VisualizationSuggestionsSupplier,
} from '@grafana/data';
import { config } from '@grafana/runtime';
import { GraphFieldConfig } from '@grafana/schema';
@@ -43,7 +43,7 @@ function determineScore(dataSummary: PanelDataSummary): VisualizationSuggestionS
return VisualizationSuggestionScore.OK;
}
export const heatmapSuggestionsSupplier: VisualizationSuggestionsSupplierFn<Options, GraphFieldConfig> = (
export const heatmapSuggestionsSupplier: VisualizationSuggestionsSupplier<Options, GraphFieldConfig> = (
dataSummary: PanelDataSummary
) => {
if (
@@ -2,7 +2,7 @@
"type": "panel",
"name": "Histogram",
"id": "histogram",
"suggestions": true,
"info": {
"description": "Distribution of values presented as a bar chart.",
"keywords": ["distribution", "bar chart", "frequency", "proportional"],
+1 -1
View File
@@ -2,7 +2,7 @@
"type": "panel",
"name": "Logs",
"id": "logs",
"suggestions": true,
"info": {
"author": {
"name": "Grafana Labs",
@@ -2,7 +2,7 @@
"type": "panel",
"name": "Node Graph",
"id": "nodeGraph",
"suggestions": true,
"info": {
"author": {
"name": "Grafana Labs",
@@ -1,4 +1,4 @@
import { DataFrame, FieldType, VisualizationSuggestionScore, VisualizationSuggestionsSupplierFn } from '@grafana/data';
import { DataFrame, FieldType, VisualizationSuggestionScore, VisualizationSuggestionsSupplier } from '@grafana/data';
import { Options } from './panelcfg.gen';
@@ -44,7 +44,7 @@ function frameHasCorrectFields(frames: DataFrame[]): boolean {
return hasNodesFrame && hasEdgesFrame;
}
export const nodeGraphSuggestionsSupplier: VisualizationSuggestionsSupplierFn<Options> = (dataSummary) => {
export const nodeGraphSuggestionsSupplier: VisualizationSuggestionsSupplier<Options> = (dataSummary) => {
if (!dataSummary.rawFrames) {
return;
}
@@ -2,7 +2,7 @@
"type": "panel",
"name": "Pie chart",
"id": "piechart",
"suggestions": true,
"info": {
"description": "The new core pie chart visualization",
"author": {
@@ -4,7 +4,7 @@ import {
FieldType,
VisualizationSuggestion,
VisualizationSuggestionScore,
VisualizationSuggestionsSupplierFn,
VisualizationSuggestionsSupplier,
} from '@grafana/data';
import { t } from '@grafana/i18n';
import { LegendDisplayMode } from '@grafana/schema';
@@ -29,7 +29,7 @@ const withDefaults = (suggestion: VisualizationSuggestion<Options>): Visualizati
const SLICE_MAX = 30;
const SLICE_MIN = 2;
export const piechartSuggestionsSupplier: VisualizationSuggestionsSupplierFn<Options> = (dataSummary) => {
export const piechartSuggestionsSupplier: VisualizationSuggestionsSupplier<Options> = (dataSummary) => {
if (!dataSummary.hasFieldType(FieldType.number)) {
return;
}
@@ -3,6 +3,7 @@
"name": "New Gauge",
"id": "radialbar",
"state": "alpha",
"suggestions": false,
"info": {
"description": "Standard gauge visualization",
"author": {
@@ -1,11 +1,6 @@
import { defaultsDeep } from 'lodash';
import {
FieldColorModeId,
FieldType,
VisualizationSuggestion,
VisualizationSuggestionsSupplierFn,
} from '@grafana/data';
import { FieldColorModeId, FieldType, VisualizationSuggestion, VisualizationSuggestionsSupplier } from '@grafana/data';
import { t } from '@grafana/i18n';
import { GraphFieldConfig } from '@grafana/ui';
import { defaultNumericVizOptions } from 'app/features/panel/suggestions/utils';
@@ -40,7 +35,7 @@ const withDefaults = (
const MAX_GAUGES = 10;
export const radialBarSuggestionsSupplier: VisualizationSuggestionsSupplierFn<Options, GraphFieldConfig> = (
export const radialBarSuggestionsSupplier: VisualizationSuggestionsSupplier<Options, GraphFieldConfig> = (
dataSummary
) => {
if (!dataSummary.hasData || !dataSummary.hasFieldType(FieldType.number)) {
+1 -1
View File
@@ -2,7 +2,7 @@
"type": "panel",
"name": "Stat",
"id": "stat",
"suggestions": true,
"info": {
"description": "Big stat values & sparklines",
"author": {
+2 -2
View File
@@ -1,6 +1,6 @@
import { defaultsDeep } from 'lodash';
import { FieldType, VisualizationSuggestion, VisualizationSuggestionsSupplierFn } from '@grafana/data';
import { FieldType, VisualizationSuggestion, VisualizationSuggestionsSupplier } from '@grafana/data';
import { t } from '@grafana/i18n';
import { BigValueColorMode, BigValueGraphMode } from '@grafana/schema';
@@ -24,7 +24,7 @@ const withDefaults = (s: VisualizationSuggestion<Options>): VisualizationSuggest
},
} satisfies VisualizationSuggestion<Options>);
export const statSuggestionsSupplier: VisualizationSuggestionsSupplierFn<Options> = (ds) => {
export const statSuggestionsSupplier: VisualizationSuggestionsSupplier<Options> = (ds) => {
if (!ds.hasData) {
return;
}
@@ -2,7 +2,7 @@
"type": "panel",
"name": "State timeline",
"id": "state-timeline",
"suggestions": true,
"info": {
"description": "State changes and durations",
"author": {
@@ -2,7 +2,7 @@
"type": "panel",
"name": "Status history",
"id": "status-history",
"suggestions": true,
"info": {
"description": "Periodic status history",
"author": {
+1 -1
View File
@@ -2,7 +2,7 @@
"type": "panel",
"name": "Table",
"id": "table",
"suggestions": true,
"info": {
"description": "Supports many column styles",
"author": {
@@ -1,4 +1,4 @@
import { PanelDataSummary, VisualizationSuggestionScore, VisualizationSuggestionsSupplierFn } from '@grafana/data';
import { PanelDataSummary, VisualizationSuggestionScore, VisualizationSuggestionsSupplier } from '@grafana/data';
import icnTablePanelSvg from 'app/plugins/panel/table/img/icn-table-panel.svg';
import { Options, FieldConfig } from './panelcfg.gen';
@@ -16,7 +16,7 @@ function getTableSuggestionScore(dataSummary: PanelDataSummary): VisualizationSu
return VisualizationSuggestionScore.OK;
}
export const tableSuggestionsSupplier: VisualizationSuggestionsSupplierFn<Options, FieldConfig> = (dataSummary) => [
export const tableSuggestionsSupplier: VisualizationSuggestionsSupplier<Options, FieldConfig> = (dataSummary) => [
{
score: getTableSuggestionScore(dataSummary),
cardOptions: {
@@ -2,7 +2,7 @@
"type": "panel",
"name": "Time series",
"id": "timeseries",
"suggestions": true,
"info": {
"description": "Time based line, area and bar charts",
"author": {
@@ -7,7 +7,7 @@ import {
PanelPluginVisualizationSuggestion,
VisualizationSuggestion,
VisualizationSuggestionScore,
VisualizationSuggestionsSupplierFn,
VisualizationSuggestionsSupplier,
} from '@grafana/data';
import { t } from '@grafana/i18n';
import {
@@ -83,7 +83,7 @@ const barChart = (name: string, stacking?: StackingMode) => ({
// TODO: all "gradient color scheme" suggestions have been removed. they will be re-added as part of the "styles" feature.
export const timeseriesSuggestionsSupplier: VisualizationSuggestionsSupplierFn<Options, GraphFieldConfig> = (
export const timeseriesSuggestionsSupplier: VisualizationSuggestionsSupplier<Options, GraphFieldConfig> = (
dataSummary
) => {
if (
+1 -1
View File
@@ -2,7 +2,7 @@
"type": "panel",
"name": "Traces",
"id": "traces",
"suggestions": true,
"info": {
"author": {
"name": "Grafana Labs",
+1 -1
View File
@@ -4,7 +4,7 @@
"id": "trend",
"state": "beta",
"suggestions": true,
"info": {
"description": "Like timeseries, but when x != time",
"author": {