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:
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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));
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user