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:
@@ -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
|
||||
|
||||
+1
-1
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.",
|
||||
|
||||
@@ -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));
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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"`
|
||||
|
||||
@@ -105,6 +105,7 @@ type JSONData struct {
|
||||
|
||||
// Panel settings
|
||||
SkipDataQuery bool `json:"skipDataQuery"`
|
||||
Suggestions bool `json:"suggestions,omitempty"`
|
||||
|
||||
// App settings
|
||||
AutoEnabled bool `json:"autoEnabled"`
|
||||
|
||||
@@ -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;",
|
||||
|
||||
Generated
+1
@@ -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
|
||||
|
||||
|
+14
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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"],
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"type": "panel",
|
||||
"name": "Stat",
|
||||
"id": "stat",
|
||||
|
||||
"suggestions": true,
|
||||
"info": {
|
||||
"description": "Big stat values & sparklines",
|
||||
"author": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"type": "panel",
|
||||
"name": "Traces",
|
||||
"id": "traces",
|
||||
|
||||
"suggestions": true,
|
||||
"info": {
|
||||
"author": {
|
||||
"name": "Grafana Labs",
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"id": "trend",
|
||||
|
||||
"state": "beta",
|
||||
|
||||
"suggestions": true,
|
||||
"info": {
|
||||
"description": "Like timeseries, but when x != time",
|
||||
"author": {
|
||||
|
||||
Reference in New Issue
Block a user