VizSuggestions: Error handling (#115428)
* error handling * retry fetching suggestions * add translation * useAsyncRetry * hasError test * update error handling * clean up the text panel stuff for the current version * cleanup for loop * some more tests for some failure cases * fix lint issue --------- Co-authored-by: Paul Marbach <paul.marbach@grafana.com>
This commit is contained in:
co-authored by
Paul Marbach
parent
2fbe2f77e3
commit
3522efdf32
@@ -120,7 +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 suggestions = await getAllSuggestions(data);
|
||||
const { suggestions } = await getAllSuggestions(data);
|
||||
|
||||
if (suggestions.length > 0) {
|
||||
const defaultFirstSuggestion = suggestions[0];
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { css } from '@emotion/css';
|
||||
import { Fragment, useState, useEffect, useCallback, useMemo } from 'react';
|
||||
import { useAsync, useMeasure } from 'react-use';
|
||||
import { useAsyncRetry, useMeasure } from 'react-use';
|
||||
|
||||
import {
|
||||
GrafanaTheme2,
|
||||
@@ -28,19 +28,23 @@ export interface Props {
|
||||
panel?: PanelModel;
|
||||
}
|
||||
|
||||
const useSuggestions = (data: PanelData | undefined) => {
|
||||
const [hasFetched, setHasFetched] = useState(false);
|
||||
const { value, loading, error, retry } = useAsyncRetry(async () => {
|
||||
await new Promise((resolve) => setTimeout(resolve, hasFetched ? 75 : 0));
|
||||
setHasFetched(true);
|
||||
return await getAllSuggestions(data);
|
||||
}, [hasFetched, data]);
|
||||
return { value, loading, error, retry };
|
||||
};
|
||||
|
||||
export function VisualizationSuggestions({ onChange, data, panel }: Props) {
|
||||
const styles = useStyles2(getStyles);
|
||||
const {
|
||||
value: suggestions,
|
||||
loading,
|
||||
error,
|
||||
} = useAsync(async () => {
|
||||
if (!hasData(data)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return await getAllSuggestions(data);
|
||||
}, [data]);
|
||||
const { value: result, loading, error, retry } = useSuggestions(data);
|
||||
|
||||
const suggestions = result?.suggestions;
|
||||
const hasLoadingErrors = result?.hasErrors ?? false;
|
||||
const [suggestionHash, setSuggestionHash] = useState<string | null>(null);
|
||||
const [firstCardRef, { width }] = useMeasure<HTMLDivElement>();
|
||||
const [firstCardHash, setFirstCardHash] = useState<string | null>(null);
|
||||
@@ -131,80 +135,97 @@ export function VisualizationSuggestions({ onChange, data, panel }: Props) {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.grid}>
|
||||
{isNewVizSuggestionsEnabled
|
||||
? suggestionsByVizType.map(([vizType, vizTypeSuggestions], groupIndex) => (
|
||||
<Fragment key={vizType?.id || `unknown-viz-type-${groupIndex}`}>
|
||||
<div className={styles.vizTypeHeader}>
|
||||
<Text variant="body" weight="medium">
|
||||
{vizType?.info && <img className={styles.vizTypeLogo} src={vizType.info.logos.small} alt="" />}
|
||||
{vizType?.name || t('panel.visualization-suggestions.unknown-viz-type', 'Unknown visualization type')}
|
||||
</Text>
|
||||
</div>
|
||||
{vizTypeSuggestions?.map((suggestion, index) => {
|
||||
const isCardSelected = suggestionHash === suggestion.hash;
|
||||
return (
|
||||
<div
|
||||
key={suggestion.hash}
|
||||
className={styles.cardContainer}
|
||||
tabIndex={0}
|
||||
role="button"
|
||||
aria-pressed={isCardSelected}
|
||||
onKeyDown={(ev) => {
|
||||
if (ev.key === 'Enter' || ev.key === ' ') {
|
||||
ev.preventDefault();
|
||||
applySuggestion(suggestion, isNewVizSuggestionsEnabled && !isCardSelected);
|
||||
}
|
||||
}}
|
||||
ref={index === 0 ? firstCardRef : undefined}
|
||||
>
|
||||
{isCardSelected && (
|
||||
<Button
|
||||
// rather than allow direct focus, we handle ketboard events in the card.
|
||||
tabIndex={-1}
|
||||
variant="primary"
|
||||
size={'md'}
|
||||
className={styles.applySuggestionButton}
|
||||
aria-label={t(
|
||||
'panel.visualization-suggestions.apply-suggestion-aria-label',
|
||||
'Apply {{suggestionName}} visualization',
|
||||
{ suggestionName: suggestion.name }
|
||||
)}
|
||||
onClick={() =>
|
||||
onChange({
|
||||
pluginId: suggestion.pluginId,
|
||||
withModKey: false,
|
||||
})
|
||||
<>
|
||||
{hasLoadingErrors && (
|
||||
<Alert severity="warning" title={''}>
|
||||
<div className={styles.alertContent}>
|
||||
<Trans i18nKey="panel.visualization-suggestions.error-loading-some-suggestions.message">
|
||||
Some suggestions could not be loaded
|
||||
</Trans>
|
||||
<Button variant="secondary" size="sm" onClick={retry}>
|
||||
<Trans i18nKey="panel.visualization-suggestions.error-loading-suggestions.try-again-button">
|
||||
Try again
|
||||
</Trans>
|
||||
</Button>
|
||||
</div>
|
||||
</Alert>
|
||||
)}
|
||||
<div className={styles.grid}>
|
||||
{isNewVizSuggestionsEnabled
|
||||
? suggestionsByVizType.map(([vizType, vizTypeSuggestions], groupIndex) => (
|
||||
<Fragment key={vizType?.id || `unknown-viz-type-${groupIndex}`}>
|
||||
<div className={styles.vizTypeHeader}>
|
||||
<Text variant="body" weight="medium">
|
||||
{vizType?.info && <img className={styles.vizTypeLogo} src={vizType.info.logos.small} alt="" />}
|
||||
{vizType?.name ||
|
||||
t('panel.visualization-suggestions.unknown-viz-type', 'Unknown visualization type')}
|
||||
</Text>
|
||||
</div>
|
||||
{vizTypeSuggestions?.map((suggestion, index) => {
|
||||
const isCardSelected = suggestionHash === suggestion.hash;
|
||||
return (
|
||||
<div
|
||||
key={suggestion.hash}
|
||||
className={styles.cardContainer}
|
||||
tabIndex={0}
|
||||
role="button"
|
||||
aria-pressed={isCardSelected}
|
||||
onKeyDown={(ev) => {
|
||||
if (ev.key === 'Enter' || ev.key === ' ') {
|
||||
ev.preventDefault();
|
||||
applySuggestion(suggestion, isNewVizSuggestionsEnabled && !isCardSelected);
|
||||
}
|
||||
>
|
||||
{t('panel.visualization-suggestions.use-this-suggestion', 'Use this suggestion')}
|
||||
</Button>
|
||||
)}
|
||||
<VisualizationSuggestionCard
|
||||
data={data}
|
||||
suggestion={suggestion}
|
||||
width={width}
|
||||
isSelected={isCardSelected}
|
||||
onClick={() => applySuggestion(suggestion, true)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</Fragment>
|
||||
))
|
||||
: suggestions?.map((suggestion, index) => (
|
||||
<div key={suggestion.hash} className={styles.cardContainer} ref={index === 0 ? firstCardRef : undefined}>
|
||||
<VisualizationSuggestionCard
|
||||
key={index}
|
||||
data={data}
|
||||
suggestion={suggestion}
|
||||
width={width}
|
||||
tabIndex={index}
|
||||
onClick={() => applySuggestion(suggestion)}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
}}
|
||||
ref={index === 0 ? firstCardRef : undefined}
|
||||
>
|
||||
{isCardSelected && (
|
||||
<Button
|
||||
// rather than allow direct focus, we handle ketboard events in the card.
|
||||
tabIndex={-1}
|
||||
variant="primary"
|
||||
size={'md'}
|
||||
className={styles.applySuggestionButton}
|
||||
aria-label={t(
|
||||
'panel.visualization-suggestions.apply-suggestion-aria-label',
|
||||
'Apply {{suggestionName}} visualization',
|
||||
{ suggestionName: suggestion.name }
|
||||
)}
|
||||
onClick={() =>
|
||||
onChange({
|
||||
pluginId: suggestion.pluginId,
|
||||
withModKey: false,
|
||||
})
|
||||
}
|
||||
>
|
||||
{t('panel.visualization-suggestions.use-this-suggestion', 'Use this suggestion')}
|
||||
</Button>
|
||||
)}
|
||||
<VisualizationSuggestionCard
|
||||
data={data}
|
||||
suggestion={suggestion}
|
||||
width={width}
|
||||
isSelected={isCardSelected}
|
||||
onClick={() => applySuggestion(suggestion, true)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</Fragment>
|
||||
))
|
||||
: suggestions?.map((suggestion, index) => (
|
||||
<div key={suggestion.hash} className={styles.cardContainer} ref={index === 0 ? firstCardRef : undefined}>
|
||||
<VisualizationSuggestionCard
|
||||
key={index}
|
||||
data={data}
|
||||
suggestion={suggestion}
|
||||
width={width}
|
||||
tabIndex={index}
|
||||
onClick={() => applySuggestion(suggestion)}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -217,6 +238,11 @@ const getStyles = (theme: GrafanaTheme2) => {
|
||||
width: '100%',
|
||||
marginTop: theme.spacing(6),
|
||||
}),
|
||||
alertContent: css({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
}),
|
||||
filterRow: css({
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
|
||||
@@ -16,4 +16,5 @@ export const panelsToCheckFirst = [
|
||||
'heatmap',
|
||||
'histogram',
|
||||
'geomap',
|
||||
'text',
|
||||
];
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import {
|
||||
AppEvents,
|
||||
DataFrame,
|
||||
FieldType,
|
||||
getDefaultTimeRange,
|
||||
@@ -18,10 +19,20 @@ import {
|
||||
StackingMode,
|
||||
VizOrientation,
|
||||
} from '@grafana/schema';
|
||||
import { appEvents } from 'app/core/app_events';
|
||||
import { config } from 'app/core/config';
|
||||
import { clearPanelPluginCache } from 'app/features/plugins/importPanelPlugin';
|
||||
import { pluginImporter } from 'app/features/plugins/importer/pluginImporter';
|
||||
|
||||
import { panelsToCheckFirst } from './consts';
|
||||
import { getAllSuggestions, sortSuggestions } from './getAllSuggestions';
|
||||
import { getAllSuggestions, loadPlugins, sortSuggestions } from './getAllSuggestions';
|
||||
|
||||
jest.mock('app/core/app_events', () => ({
|
||||
appEvents: {
|
||||
subscribe: jest.fn(() => ({ unsubscribe: jest.fn() })),
|
||||
publish: jest.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
config.featureToggles.externalVizSuggestions = true;
|
||||
|
||||
@@ -52,28 +63,6 @@ for (const pluginId of panelsToCheckFirst) {
|
||||
};
|
||||
}
|
||||
|
||||
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: {
|
||||
version: '1.0.0',
|
||||
updated: '2025-01-01',
|
||||
links: [],
|
||||
screenshots: [],
|
||||
author: {
|
||||
name: 'Grafana Labs',
|
||||
},
|
||||
description: 'Text panel',
|
||||
logos: { small: 'small/logo', large: 'large/logo' },
|
||||
},
|
||||
};
|
||||
|
||||
jest.mock('../state/util', () => {
|
||||
const originalModule = jest.requireActual('../state/util');
|
||||
return {
|
||||
@@ -103,7 +92,8 @@ class ScenarioContext {
|
||||
timeRange: getDefaultTimeRange(),
|
||||
};
|
||||
|
||||
this.suggestions = await getAllSuggestions(panelData);
|
||||
const result = await getAllSuggestions(panelData);
|
||||
this.suggestions = result.suggestions;
|
||||
}
|
||||
|
||||
names() {
|
||||
@@ -554,6 +544,81 @@ describe('sortSuggestions', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('Visualization suggestions error handling', () => {
|
||||
it('returns result with hasErrors flag', async () => {
|
||||
const result = await getAllSuggestions({
|
||||
series: [
|
||||
toDataFrame({
|
||||
fields: [
|
||||
{ name: 'Time', type: FieldType.time, values: [1, 2] },
|
||||
{ name: 'Max', type: FieldType.number, values: [1, 10] },
|
||||
],
|
||||
}),
|
||||
],
|
||||
state: LoadingState.Done,
|
||||
timeRange: getDefaultTimeRange(),
|
||||
});
|
||||
|
||||
expect(result).toHaveProperty('suggestions');
|
||||
expect(result).toHaveProperty('hasErrors');
|
||||
expect(result.hasErrors).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
// this needs to happen before any
|
||||
describe('loadPlugins', () => {
|
||||
beforeEach(() => {
|
||||
clearPanelPluginCache();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
if (jest.isMockFunction(pluginImporter.importPanel)) {
|
||||
jest.mocked(pluginImporter.importPanel).mockRestore();
|
||||
}
|
||||
});
|
||||
|
||||
it('should swallow errors when failing to load core plugins', async () => {
|
||||
jest.spyOn(console, 'error').mockImplementation();
|
||||
|
||||
const _importPanel = pluginImporter.importPanel;
|
||||
jest.spyOn(pluginImporter, 'importPanel').mockImplementation(async (meta) => {
|
||||
if (meta.id === 'timeseries') {
|
||||
throw new Error('Failed to load core panel plugin');
|
||||
}
|
||||
return await _importPanel(meta);
|
||||
});
|
||||
|
||||
const panelIds = ['timeseries', 'table'];
|
||||
const { plugins, hasErrors } = await loadPlugins(panelIds);
|
||||
|
||||
expect(plugins).toEqual([expect.objectContaining({ meta: expect.objectContaining({ id: 'table' }) })]);
|
||||
expect(hasErrors).toBe(true);
|
||||
expect(appEvents.publish).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should swallow errors when failing to load external plugins', async () => {
|
||||
jest.spyOn(console, 'error').mockImplementation();
|
||||
|
||||
const panelIds = ['non-existent-panel'];
|
||||
const { plugins, hasErrors } = await loadPlugins(panelIds);
|
||||
|
||||
expect(plugins).toEqual([]);
|
||||
expect(hasErrors).toBe(false);
|
||||
expect(appEvents.publish).toHaveBeenCalledWith({
|
||||
type: AppEvents.alertError.name,
|
||||
payload: [expect.stringContaining('Failed to load panel plugin: non-existent-panel.')],
|
||||
});
|
||||
});
|
||||
|
||||
it('should load panel plugins with suggestions', async () => {
|
||||
const panelIds = ['timeseries', 'table'];
|
||||
const { plugins, hasErrors } = await loadPlugins(panelIds);
|
||||
|
||||
expect(plugins.map((p) => p.meta.id)).toEqual(expect.arrayContaining(['timeseries', 'table']));
|
||||
expect(hasErrors).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
function repeatFrame(count: number, frame: DataFrame): DataFrame[] {
|
||||
const frames: DataFrame[] = [];
|
||||
for (let i = 0; i < count; i++) {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import {
|
||||
AppEvents,
|
||||
getPanelDataSummary,
|
||||
PanelData,
|
||||
PanelDataSummary,
|
||||
@@ -7,41 +8,67 @@ import {
|
||||
PreferredVisualisationType,
|
||||
VisualizationSuggestionScore,
|
||||
} from '@grafana/data';
|
||||
import { t } from '@grafana/i18n';
|
||||
import { config } from '@grafana/runtime';
|
||||
import { appEvents } from 'app/core/app_events';
|
||||
import { importPanelPlugin, isBuiltInPlugin } from 'app/features/plugins/importPanelPlugin';
|
||||
|
||||
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
|
||||
*/
|
||||
async function getPanelsWithSuggestions(): Promise<PanelPlugin[]> {
|
||||
// list of plugins to load is determined by the feature flag
|
||||
const pluginIds: string[] = config.featureToggles.externalVizSuggestions
|
||||
interface PluginLoadResult {
|
||||
plugins: PanelPlugin[];
|
||||
hasErrors: boolean;
|
||||
}
|
||||
|
||||
function getPanelPluginIds(): string[] {
|
||||
return config.featureToggles.externalVizSuggestions
|
||||
? getAllPanelPluginMeta()
|
||||
.filter((panel) => panel.suggestions)
|
||||
.map((m) => m.id)
|
||||
: panelsToCheckFirst;
|
||||
}
|
||||
|
||||
/**
|
||||
* gather and cache the plugins which provide visualization suggestions so they can be invoked to build suggestions
|
||||
*/
|
||||
export async function loadPlugins(pluginIds: string[]): Promise<PluginLoadResult> {
|
||||
// import the plugins in parallel using Promise.allSettled
|
||||
const plugins: PanelPlugin[] = [];
|
||||
const settledPromises = await Promise.allSettled(pluginIds.map((id) => importPanelPlugin(id)));
|
||||
let hasErrors = false;
|
||||
const settledPromises = await Promise.allSettled(
|
||||
pluginIds.map(async (pluginId) => {
|
||||
return await importPanelPlugin(pluginId);
|
||||
})
|
||||
);
|
||||
|
||||
for (let i = 0; i < settledPromises.length; i++) {
|
||||
const settled = settledPromises[i];
|
||||
|
||||
if (settled.status === 'fulfilled') {
|
||||
plugins.push(settled.value);
|
||||
} else {
|
||||
const pluginId = pluginIds[i];
|
||||
console.error(`Failed to load ${pluginId} for visualization suggestions:`, settled.reason);
|
||||
|
||||
if (isBuiltInPlugin(pluginId)) {
|
||||
hasErrors = true;
|
||||
} else {
|
||||
appEvents.publish({
|
||||
type: AppEvents.alertError.name,
|
||||
payload: [
|
||||
t(
|
||||
'panel.visualization-suggestions.error-loading-suggestions.plugin-failed',
|
||||
'Failed to load panel plugin: {{ pluginId }}.',
|
||||
{ pluginId }
|
||||
),
|
||||
],
|
||||
});
|
||||
}
|
||||
}
|
||||
// TODO: do we want to somehow log if there were errors loading some of the plugins?
|
||||
}
|
||||
|
||||
if (plugins.length === 0) {
|
||||
throw new Error('No panel plugins with visualization suggestions found');
|
||||
}
|
||||
|
||||
return plugins;
|
||||
return { plugins, hasErrors };
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -89,41 +116,37 @@ export function sortSuggestions(suggestions: PanelPluginVisualizationSuggestion[
|
||||
});
|
||||
}
|
||||
|
||||
export interface SuggestionsResult {
|
||||
suggestions: PanelPluginVisualizationSuggestion[];
|
||||
hasErrors: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
* @returns {SuggestionsResult} sorted list of suggestions and error status
|
||||
*/
|
||||
export async function getAllSuggestions(data?: PanelData): Promise<PanelPluginVisualizationSuggestion[]> {
|
||||
export async function getAllSuggestions(data?: PanelData): Promise<SuggestionsResult> {
|
||||
const dataSummary = getPanelDataSummary(data?.series);
|
||||
const list: PanelPluginVisualizationSuggestion[] = [];
|
||||
|
||||
for (const plugin of await getPanelsWithSuggestions()) {
|
||||
const suggestions = plugin.getSuggestions(dataSummary);
|
||||
if (suggestions) {
|
||||
list.push(...suggestions);
|
||||
}
|
||||
}
|
||||
const pluginIds: string[] = getPanelPluginIds();
|
||||
const { plugins, hasErrors: pluginLoadErrors } = await loadPlugins(pluginIds);
|
||||
|
||||
if (dataSummary.fieldCount === 0) {
|
||||
for (const plugin of Object.values(config.panels)) {
|
||||
if (!plugin.skipDataQuery || plugin.hideFromList) {
|
||||
continue;
|
||||
let pluginSuggestionsError = false;
|
||||
for (const plugin of plugins) {
|
||||
try {
|
||||
const suggestions = plugin.getSuggestions(dataSummary);
|
||||
if (suggestions) {
|
||||
list.push(...suggestions);
|
||||
}
|
||||
|
||||
list.push({
|
||||
name: plugin.name,
|
||||
pluginId: plugin.id,
|
||||
description: plugin.info.description,
|
||||
hash: 'plugin-empty-' + plugin.id,
|
||||
cardOptions: {
|
||||
imgSrc: plugin.info.logos.small,
|
||||
},
|
||||
});
|
||||
} catch (e) {
|
||||
console.warn(`error when loading suggestions from plugin "${plugin.meta.id}"`, e);
|
||||
pluginSuggestionsError = true;
|
||||
}
|
||||
}
|
||||
|
||||
sortSuggestions(list, dataSummary);
|
||||
|
||||
return list;
|
||||
return { suggestions: list, hasErrors: pluginLoadErrors || pluginSuggestionsError };
|
||||
}
|
||||
|
||||
@@ -62,3 +62,9 @@ export function syncGetPanelPlugin(id: string): PanelPlugin | undefined {
|
||||
function getPanelPlugin(meta: PanelPluginMeta): Promise<PanelPlugin> {
|
||||
return pluginImporter.importPanel(meta);
|
||||
}
|
||||
|
||||
export function clearPanelPluginCache(): void {
|
||||
for (const key of Object.keys(promiseCache)) {
|
||||
delete promiseCache[key];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { PanelDataSummary, VisualizationSuggestionScore, VisualizationSuggestionsSupplier } from '@grafana/data';
|
||||
import { config } from 'app/core/config';
|
||||
import icnTablePanelSvg from 'app/plugins/panel/table/img/icn-table-panel.svg';
|
||||
|
||||
import { Options, FieldConfig } from './panelcfg.gen';
|
||||
@@ -29,7 +30,7 @@ export const tableSuggestionsSupplier: VisualizationSuggestionsSupplier<Options,
|
||||
},
|
||||
// If there is no data, suggest table anyway, but use icon instead of real preview
|
||||
// TODO: delete this in once "new" suggestions are fully rolled out
|
||||
imgSrc: dataSummary.fieldCount === 0 ? icnTablePanelSvg : undefined,
|
||||
imgSrc: dataSummary.fieldCount === 0 && !config.featureToggles.newVizSuggestions ? icnTablePanelSvg : undefined,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { PanelPlugin } from '@grafana/data';
|
||||
import { t } from '@grafana/i18n';
|
||||
import { config } from 'app/core/config';
|
||||
|
||||
import { TextPanel } from './TextPanel';
|
||||
import { TextPanelEditor } from './TextPanelEditor';
|
||||
import icnTextPanelSvg from './img/icn-text-panel.svg';
|
||||
import { CodeLanguage, defaultCodeOptions, defaultOptions, Options, TextMode } from './panelcfg.gen';
|
||||
import { textPanelMigrationHandler } from './textPanelMigrationHandler';
|
||||
|
||||
@@ -59,4 +61,9 @@ export const plugin = new PanelPlugin<Options>(TextPanel)
|
||||
defaultValue: defaultOptions.content,
|
||||
});
|
||||
})
|
||||
.setMigrationHandler(textPanelMigrationHandler);
|
||||
.setMigrationHandler(textPanelMigrationHandler)
|
||||
.setSuggestionsSupplier((ds) =>
|
||||
ds.fieldCount === 0 && !config.featureToggles.newVizSuggestions
|
||||
? [{ cardOptions: { imgSrc: icnTextPanelSvg } }]
|
||||
: []
|
||||
);
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"type": "panel",
|
||||
"name": "Text",
|
||||
"id": "text",
|
||||
|
||||
"suggestions": true,
|
||||
"skipDataQuery": true,
|
||||
|
||||
"info": {
|
||||
|
||||
@@ -11216,9 +11216,14 @@
|
||||
},
|
||||
"visualization-suggestions": {
|
||||
"apply-suggestion-aria-label": "Apply {{suggestionName}} visualization",
|
||||
"error-loading-some-suggestions": {
|
||||
"message": "Some suggestions could not be loaded"
|
||||
},
|
||||
"error-loading-suggestions": {
|
||||
"message": "An error occurred when loading visualization suggestions.",
|
||||
"title": "Error"
|
||||
"plugin-failed": "Failed to load panel plugin: {{ pluginId }}.",
|
||||
"title": "Error",
|
||||
"try-again-button": "Try again"
|
||||
},
|
||||
"unknown-viz-type": "Unknown visualization type",
|
||||
"use-this-suggestion": "Use this suggestion"
|
||||
|
||||
Reference in New Issue
Block a user