mirror of
https://github.com/grafana/grafana.git
synced 2025-02-10 07:35:45 -06:00
VisualizationSuggestions: Support image & text instead of real previews. Adds suggestions for all non data panels when there are no data (#42074)
* Make suggestion cards support img & text mode instead of only preview * Generic solution for non data panels * minor review tweaks
This commit is contained in:
parent
a897154017
commit
781067ee45
@ -201,12 +201,20 @@ export interface VisualizationSuggestion<TOptions = any, TFieldConfig = any> {
|
||||
fieldConfig?: FieldConfigSource<Partial<TFieldConfig>>;
|
||||
/** Data transformations */
|
||||
transformations?: DataTransformerConfig[];
|
||||
/** Tweak for small preview */
|
||||
previewModifier?: (suggestion: VisualizationSuggestion) => void;
|
||||
/** Options for how to render suggestion card */
|
||||
cardOptions?: {
|
||||
/** Tweak for small preview */
|
||||
previewModifier?: (suggestion: VisualizationSuggestion) => void;
|
||||
icon?: string;
|
||||
imgSrc?: string;
|
||||
};
|
||||
/** A value between 0-100 how suitable suggestion is */
|
||||
score?: VisualizationSuggestionScore;
|
||||
}
|
||||
|
||||
/**
|
||||
* @alpha
|
||||
*/
|
||||
export enum VisualizationSuggestionScore {
|
||||
/** We are pretty sure this is the best possible option */
|
||||
Best = 100,
|
||||
|
@ -57,21 +57,6 @@ export const VisualizationSelectPane: FC<Props> = ({ panel, data }) => {
|
||||
dispatch(toggleVizPicker(false));
|
||||
};
|
||||
|
||||
// const onKeyPress = useCallback(
|
||||
// (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
// if (e.key === 'Enter') {
|
||||
// const query = e.currentTarget.value;
|
||||
// const plugins = getAllPanelPluginMeta();
|
||||
// const match = filterPluginList(plugins, query, plugin.meta);
|
||||
|
||||
// if (match && match.length) {
|
||||
// onPluginTypeChange(match[0], false);
|
||||
// }
|
||||
// }
|
||||
// },
|
||||
// [onPluginTypeChange, plugin.meta]
|
||||
// );
|
||||
|
||||
if (!plugin) {
|
||||
return null;
|
||||
}
|
||||
|
@ -1,11 +1,11 @@
|
||||
import React, { CSSProperties } from 'react';
|
||||
import { GrafanaTheme2, PanelData, VisualizationSuggestion } from '@grafana/data';
|
||||
import { PanelRenderer } from '../PanelRenderer';
|
||||
import { css } from '@emotion/css';
|
||||
import { css, cx } from '@emotion/css';
|
||||
import { Tooltip, useStyles2 } from '@grafana/ui';
|
||||
import { VizTypeChangeDetails } from './types';
|
||||
import { cloneDeep } from 'lodash';
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
import { cloneDeep } from 'lodash';
|
||||
|
||||
export interface Props {
|
||||
data: PanelData;
|
||||
@ -15,50 +15,59 @@ export interface Props {
|
||||
onChange: (details: VizTypeChangeDetails) => void;
|
||||
}
|
||||
|
||||
export function VisualizationPreview({ data, suggestion, onChange, width, showTitle }: Props) {
|
||||
export function VisualizationSuggestionCard({ data, suggestion, onChange, width, showTitle }: Props) {
|
||||
const styles = useStyles2(getStyles);
|
||||
const { innerStyles, outerStyles, renderWidth, renderHeight } = getPreviewDimensionsAndStyles(width);
|
||||
const cardOptions = suggestion.cardOptions ?? {};
|
||||
|
||||
const onClick = () => {
|
||||
onChange({
|
||||
pluginId: suggestion.pluginId,
|
||||
options: suggestion.options,
|
||||
fieldConfig: suggestion.fieldConfig,
|
||||
});
|
||||
const commonButtonProps = {
|
||||
'aria-label': suggestion.name,
|
||||
className: styles.vizBox,
|
||||
'data-testid': selectors.components.VisualizationPreview.card(suggestion.name),
|
||||
style: outerStyles,
|
||||
onClick: () => {
|
||||
onChange({
|
||||
pluginId: suggestion.pluginId,
|
||||
options: suggestion.options,
|
||||
fieldConfig: suggestion.fieldConfig,
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
if (cardOptions.imgSrc) {
|
||||
return (
|
||||
<Tooltip content={suggestion.description ?? suggestion.name}>
|
||||
<button {...commonButtonProps} className={cx(styles.vizBox, styles.imgBox)}>
|
||||
<div className={styles.name}>{suggestion.name}</div>
|
||||
<img className={styles.img} src={cardOptions.imgSrc} alt={suggestion.name} />
|
||||
</button>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
let preview = suggestion;
|
||||
if (suggestion.previewModifier) {
|
||||
if (suggestion.cardOptions?.previewModifier) {
|
||||
preview = cloneDeep(suggestion);
|
||||
suggestion.previewModifier(preview);
|
||||
suggestion.cardOptions.previewModifier(preview);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
{showTitle && <div className={styles.name}>{suggestion.name}</div>}
|
||||
<button
|
||||
aria-label={suggestion.name}
|
||||
className={styles.vizBox}
|
||||
data-testid={selectors.components.VisualizationPreview.card(suggestion.name)}
|
||||
style={outerStyles}
|
||||
onClick={onClick}
|
||||
>
|
||||
<Tooltip content={suggestion.name}>
|
||||
<div style={innerStyles} className={styles.renderContainer}>
|
||||
<PanelRenderer
|
||||
title=""
|
||||
data={data}
|
||||
pluginId={suggestion.pluginId}
|
||||
width={renderWidth}
|
||||
height={renderHeight}
|
||||
options={preview.options}
|
||||
fieldConfig={preview.fieldConfig}
|
||||
/>
|
||||
<div className={styles.hoverPane} />
|
||||
</div>
|
||||
</Tooltip>
|
||||
</button>
|
||||
</div>
|
||||
<button {...commonButtonProps}>
|
||||
<Tooltip content={suggestion.name}>
|
||||
<div style={innerStyles} className={styles.renderContainer}>
|
||||
<PanelRenderer
|
||||
title=""
|
||||
data={data}
|
||||
pluginId={suggestion.pluginId}
|
||||
width={renderWidth}
|
||||
height={renderHeight}
|
||||
options={preview.options}
|
||||
fieldConfig={preview.fieldConfig}
|
||||
/>
|
||||
<div className={styles.hoverPane} />
|
||||
</div>
|
||||
</Tooltip>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
@ -77,8 +86,7 @@ const getStyles = (theme: GrafanaTheme2) => {
|
||||
background: none;
|
||||
border-radius: ${theme.shape.borderRadius(1)};
|
||||
cursor: pointer;
|
||||
text-align: left;
|
||||
border: 1px solid ${theme.colors.border.strong};
|
||||
border: 1px solid ${theme.colors.border.medium};
|
||||
|
||||
transition: ${theme.transitions.create(['background'], {
|
||||
duration: theme.transitions.duration.short,
|
||||
@ -88,7 +96,23 @@ const getStyles = (theme: GrafanaTheme2) => {
|
||||
background: ${theme.colors.background.secondary};
|
||||
}
|
||||
`,
|
||||
imgBox: css`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
|
||||
justify-self: center;
|
||||
color: ${theme.colors.text.primary};
|
||||
width: 100%;
|
||||
display: flex;
|
||||
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
`,
|
||||
name: css`
|
||||
padding-bottom: ${theme.spacing(0.5)};
|
||||
margin-top: ${theme.spacing(-1)};
|
||||
font-size: ${theme.typography.bodySmall.fontSize};
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
@ -96,6 +120,10 @@ const getStyles = (theme: GrafanaTheme2) => {
|
||||
font-weight: ${theme.typography.fontWeightMedium};
|
||||
text-overflow: ellipsis;
|
||||
`,
|
||||
img: css`
|
||||
max-width: ${theme.spacing(8)};
|
||||
max-height: ${theme.spacing(8)};
|
||||
`,
|
||||
renderContainer: css`
|
||||
position: absolute;
|
||||
transform-origin: left top;
|
@ -3,7 +3,7 @@ import { useStyles2 } from '@grafana/ui';
|
||||
import { GrafanaTheme2, PanelData, PanelPluginMeta, PanelModel, VisualizationSuggestion } from '@grafana/data';
|
||||
import { css } from '@emotion/css';
|
||||
import { VizTypeChangeDetails } from './types';
|
||||
import { VisualizationPreview } from './VisualizationPreview';
|
||||
import { VisualizationSuggestionCard } from './VisualizationSuggestionCard';
|
||||
import { getAllSuggestions } from '../../state/getAllSuggestions';
|
||||
import { useAsync, useLocalStorage } from 'react-use';
|
||||
import AutoSizer from 'react-virtualized-auto-sizer';
|
||||
@ -44,7 +44,7 @@ export function VisualizationSuggestions({ onChange, data, panel, searchQuery }:
|
||||
</div>
|
||||
<div className={styles.grid} style={{ gridTemplateColumns: `repeat(auto-fill, ${previewWidth - 1}px)` }}>
|
||||
{filteredSuggestions.map((suggestion, index) => (
|
||||
<VisualizationPreview
|
||||
<VisualizationSuggestionCard
|
||||
key={index}
|
||||
data={data!}
|
||||
suggestion={suggestion}
|
||||
|
@ -4,6 +4,7 @@ import {
|
||||
getDefaultTimeRange,
|
||||
LoadingState,
|
||||
PanelData,
|
||||
PanelPluginMeta,
|
||||
toDataFrame,
|
||||
VisualizationSuggestion,
|
||||
} from '@grafana/data';
|
||||
@ -20,6 +21,16 @@ for (const pluginId of panelsToCheckFirst) {
|
||||
} as any;
|
||||
}
|
||||
|
||||
config.panels['text'] = {
|
||||
id: 'text',
|
||||
name: 'Text',
|
||||
skipDataQuery: true,
|
||||
info: {
|
||||
description: 'pretty decent plugin',
|
||||
logos: { small: 'small/logo', large: 'large/logo' },
|
||||
},
|
||||
} as PanelPluginMeta;
|
||||
|
||||
class ScenarioContext {
|
||||
data: DataFrame[] = [];
|
||||
suggestions: VisualizationSuggestion[] = [];
|
||||
@ -58,7 +69,7 @@ scenario('No series', (ctx) => {
|
||||
ctx.setData([]);
|
||||
|
||||
it('should return correct suggestions', () => {
|
||||
expect(ctx.names()).toEqual([SuggestionName.Table, SuggestionName.TextPanel, SuggestionName.DashboardList]);
|
||||
expect(ctx.names()).toEqual([SuggestionName.Table, SuggestionName.TextPanel]);
|
||||
});
|
||||
});
|
||||
|
||||
@ -73,7 +84,7 @@ scenario('No rows', (ctx) => {
|
||||
]);
|
||||
|
||||
it('should return correct suggestions', () => {
|
||||
expect(ctx.names()).toEqual([SuggestionName.Table, SuggestionName.TextPanel, SuggestionName.DashboardList]);
|
||||
expect(ctx.names()).toEqual([SuggestionName.Table]);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -5,6 +5,7 @@ import {
|
||||
PanelModel,
|
||||
VisualizationSuggestionScore,
|
||||
} from '@grafana/data';
|
||||
import { config } from '@grafana/runtime';
|
||||
import { importPanelPlugin } from 'app/features/plugins/importPanelPlugin';
|
||||
|
||||
export const panelsToCheckFirst = [
|
||||
@ -17,8 +18,6 @@ export const panelsToCheckFirst = [
|
||||
'table',
|
||||
'state-timeline',
|
||||
'status-history',
|
||||
'text',
|
||||
'dashlist',
|
||||
'logs',
|
||||
'candlestick',
|
||||
];
|
||||
@ -35,7 +34,26 @@ export async function getAllSuggestions(data?: PanelData, panel?: PanelModel): P
|
||||
}
|
||||
}
|
||||
|
||||
return builder.getList().sort((a, b) => {
|
||||
const list = builder.getList();
|
||||
|
||||
if (builder.dataSummary.fieldCount === 0) {
|
||||
for (const plugin of Object.values(config.panels)) {
|
||||
if (!plugin.skipDataQuery || plugin.hideFromList) {
|
||||
continue;
|
||||
}
|
||||
|
||||
list.push({
|
||||
name: plugin.name,
|
||||
pluginId: plugin.id,
|
||||
description: plugin.info.description,
|
||||
cardOptions: {
|
||||
imgSrc: plugin.info.logos.small,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return list.sort((a, b) => {
|
||||
return (b.score ?? VisualizationSuggestionScore.OK) - (a.score ?? VisualizationSuggestionScore.OK);
|
||||
});
|
||||
}
|
||||
|
@ -22,8 +22,10 @@ export class BarChartSuggestionsSupplier {
|
||||
},
|
||||
overrides: [],
|
||||
},
|
||||
previewModifier: (s) => {
|
||||
s.options!.barWidth = 0.8;
|
||||
cardOptions: {
|
||||
previewModifier: (s) => {
|
||||
s.options!.barWidth = 0.8;
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
@ -21,7 +21,6 @@ export class BarGaugeSuggestionsSupplier {
|
||||
},
|
||||
overrides: [],
|
||||
},
|
||||
previewModifier: (s) => {},
|
||||
});
|
||||
|
||||
// This is probably not a good option for many numeric fields
|
||||
|
@ -38,7 +38,6 @@ export class CandlestickSuggestionsSupplier {
|
||||
},
|
||||
overrides: [],
|
||||
},
|
||||
previewModifier: (s) => {},
|
||||
});
|
||||
|
||||
list.append({
|
||||
|
@ -8,7 +8,6 @@ import {
|
||||
GENERAL_FOLDER,
|
||||
ReadonlyFolderPicker,
|
||||
} from '../../../core/components/Select/ReadonlyFolderPicker/ReadonlyFolderPicker';
|
||||
import { DashListSuggestionsSupplier } from './suggestions';
|
||||
|
||||
export const plugin = new PanelPlugin<DashListOptions>(DashList)
|
||||
.setPanelOptions((builder) => {
|
||||
@ -88,5 +87,4 @@ export const plugin = new PanelPlugin<DashListOptions>(DashList)
|
||||
}
|
||||
|
||||
return newOptions;
|
||||
})
|
||||
.setSuggestionsSupplier(new DashListSuggestionsSupplier());
|
||||
});
|
||||
|
@ -1,20 +0,0 @@
|
||||
import { VisualizationSuggestionsBuilder } from '@grafana/data';
|
||||
import { PanelOptions } from './models.gen';
|
||||
|
||||
export class DashListSuggestionsSupplier {
|
||||
getSuggestionsForData(builder: VisualizationSuggestionsBuilder) {
|
||||
const { dataSummary } = builder;
|
||||
|
||||
if (dataSummary.hasData) {
|
||||
return;
|
||||
}
|
||||
|
||||
const list = builder.getListAppender<PanelOptions, {}>({
|
||||
name: 'Dashboard list',
|
||||
pluginId: 'dashlist',
|
||||
options: {},
|
||||
});
|
||||
|
||||
list.append({});
|
||||
}
|
||||
}
|
@ -33,10 +33,12 @@ export class GaugeSuggestionsSupplier {
|
||||
},
|
||||
overrides: [],
|
||||
},
|
||||
previewModifier: (s) => {
|
||||
if (s.options!.reduceOptions.values) {
|
||||
s.options!.reduceOptions.limit = 2;
|
||||
}
|
||||
cardOptions: {
|
||||
previewModifier: (s) => {
|
||||
if (s.options!.reduceOptions.values) {
|
||||
s.options!.reduceOptions.limit = 2;
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -14,7 +14,6 @@ export class LogsPanelSuggestionsSupplier {
|
||||
},
|
||||
overrides: [],
|
||||
},
|
||||
previewModifier: () => {},
|
||||
});
|
||||
|
||||
const { dataSummary: ds } = builder;
|
||||
|
@ -19,9 +19,11 @@ export class PieChartSuggestionsSupplier {
|
||||
values: [],
|
||||
} as any,
|
||||
},
|
||||
previewModifier: (s) => {
|
||||
// Hide labels in preview
|
||||
s.options!.legend.displayMode = LegendDisplayMode.Hidden;
|
||||
cardOptions: {
|
||||
previewModifier: (s) => {
|
||||
// Hide labels in preview
|
||||
s.options!.legend.displayMode = LegendDisplayMode.Hidden;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -22,10 +22,12 @@ export class StatSuggestionsSupplier {
|
||||
},
|
||||
overrides: [],
|
||||
},
|
||||
previewModifier: (s) => {
|
||||
if (s.options!.reduceOptions.values) {
|
||||
s.options!.reduceOptions.limit = 1;
|
||||
}
|
||||
cardOptions: {
|
||||
previewModifier: (s) => {
|
||||
if (s.options!.reduceOptions.values) {
|
||||
s.options!.reduceOptions.limit = 1;
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -35,7 +35,6 @@ export class StatTimelineSuggestionsSupplier {
|
||||
},
|
||||
overrides: [],
|
||||
},
|
||||
previewModifier: (s) => {},
|
||||
});
|
||||
|
||||
list.append({ name: SuggestionName.StateTimeline });
|
||||
|
@ -43,8 +43,10 @@ export class StatusHistorySuggestionsSupplier {
|
||||
},
|
||||
overrides: [],
|
||||
},
|
||||
previewModifier: (s) => {
|
||||
s.options!.colWidth = 0.7;
|
||||
cardOptions: {
|
||||
previewModifier: (s) => {
|
||||
s.options!.colWidth = 0.7;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -6,7 +6,7 @@ import { PanelOptions } from './models.gen';
|
||||
export class TableSuggestionsSupplier {
|
||||
getSuggestionsForData(builder: VisualizationSuggestionsBuilder) {
|
||||
const list = builder.getListAppender<PanelOptions, TableFieldOptions>({
|
||||
name: '',
|
||||
name: SuggestionName.Table,
|
||||
pluginId: 'table',
|
||||
options: {},
|
||||
fieldConfig: {
|
||||
@ -15,9 +15,22 @@ export class TableSuggestionsSupplier {
|
||||
},
|
||||
overrides: [],
|
||||
},
|
||||
previewModifier: (s) => {},
|
||||
cardOptions: {
|
||||
previewModifier: (s) => {
|
||||
s.fieldConfig!.defaults.custom!.minWidth = 50;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
list.append({ name: SuggestionName.Table });
|
||||
// If there are not data suggest table anyway but use icon instead of real preview
|
||||
if (builder.dataSummary.fieldCount === 0) {
|
||||
list.append({
|
||||
cardOptions: {
|
||||
imgSrc: 'public/app/plugins/panel/table/img/icn-table-panel.svg',
|
||||
},
|
||||
});
|
||||
} else {
|
||||
list.append({});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -4,7 +4,6 @@ import { TextPanel } from './TextPanel';
|
||||
import { textPanelMigrationHandler } from './textPanelMigrationHandler';
|
||||
import { TextPanelEditor } from './TextPanelEditor';
|
||||
import { defaultPanelOptions, PanelOptions, TextMode } from './models.gen';
|
||||
import { TextPanelSuggestionSupplier } from './suggestions';
|
||||
|
||||
export const plugin = new PanelPlugin<PanelOptions>(TextPanel)
|
||||
.setPanelOptions((builder) => {
|
||||
@ -30,5 +29,4 @@ export const plugin = new PanelPlugin<PanelOptions>(TextPanel)
|
||||
defaultValue: defaultPanelOptions.content,
|
||||
});
|
||||
})
|
||||
.setMigrationHandler(textPanelMigrationHandler)
|
||||
.setSuggestionsSupplier(new TextPanelSuggestionSupplier());
|
||||
.setMigrationHandler(textPanelMigrationHandler);
|
||||
|
@ -1,29 +0,0 @@
|
||||
import { VisualizationSuggestionsBuilder } from '@grafana/data';
|
||||
import { PanelOptions } from './models.gen';
|
||||
|
||||
export class TextPanelSuggestionSupplier {
|
||||
getSuggestionsForData(builder: VisualizationSuggestionsBuilder) {
|
||||
const { dataSummary } = builder;
|
||||
|
||||
if (dataSummary.hasData) {
|
||||
return;
|
||||
}
|
||||
|
||||
const list = builder.getListAppender<PanelOptions, {}>({
|
||||
name: 'Text panel',
|
||||
pluginId: 'text',
|
||||
options: {
|
||||
content: `
|
||||
# Title
|
||||
|
||||
For markdown syntax help: [commonmark.org/help](https://commonmark.org/help/)
|
||||
|
||||
* First item
|
||||
* Second item
|
||||
* Third item`,
|
||||
},
|
||||
});
|
||||
|
||||
list.append({});
|
||||
}
|
||||
}
|
@ -30,12 +30,14 @@ export class TimeSeriesSuggestionsSupplier {
|
||||
},
|
||||
overrides: [],
|
||||
},
|
||||
previewModifier: (s) => {
|
||||
s.options!.legend.displayMode = LegendDisplayMode.Hidden;
|
||||
cardOptions: {
|
||||
previewModifier: (s) => {
|
||||
s.options!.legend.displayMode = LegendDisplayMode.Hidden;
|
||||
|
||||
if (s.fieldConfig?.defaults.custom?.drawStyle !== GraphDrawStyle.Bars) {
|
||||
s.fieldConfig!.defaults.custom!.lineWidth = Math.max(s.fieldConfig!.defaults.custom!.lineWidth ?? 1, 2);
|
||||
}
|
||||
if (s.fieldConfig?.defaults.custom?.drawStyle !== GraphDrawStyle.Bars) {
|
||||
s.fieldConfig!.defaults.custom!.lineWidth = Math.max(s.fieldConfig!.defaults.custom!.lineWidth ?? 1, 2);
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -24,7 +24,7 @@ export enum SuggestionName {
|
||||
Table = 'Table',
|
||||
StateTimeline = 'State timeline',
|
||||
StatusHistory = 'Status history',
|
||||
TextPanel = 'Text panel',
|
||||
TextPanel = 'Text',
|
||||
DashboardList = 'Dashboard list',
|
||||
Logs = 'Logs',
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user