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:
Torkel Ödegaard 2021-11-25 10:52:01 +01:00 committed by GitHub
parent a897154017
commit 781067ee45
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 163 additions and 145 deletions

View File

@ -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,

View File

@ -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;
}

View File

@ -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;

View File

@ -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}

View File

@ -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]);
});
});

View File

@ -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);
});
}

View File

@ -22,8 +22,10 @@ export class BarChartSuggestionsSupplier {
},
overrides: [],
},
previewModifier: (s) => {
s.options!.barWidth = 0.8;
cardOptions: {
previewModifier: (s) => {
s.options!.barWidth = 0.8;
},
},
});
}

View File

@ -21,7 +21,6 @@ export class BarGaugeSuggestionsSupplier {
},
overrides: [],
},
previewModifier: (s) => {},
});
// This is probably not a good option for many numeric fields

View File

@ -38,7 +38,6 @@ export class CandlestickSuggestionsSupplier {
},
overrides: [],
},
previewModifier: (s) => {},
});
list.append({

View File

@ -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());
});

View File

@ -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({});
}
}

View File

@ -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;
}
},
},
});

View File

@ -14,7 +14,6 @@ export class LogsPanelSuggestionsSupplier {
},
overrides: [],
},
previewModifier: () => {},
});
const { dataSummary: ds } = builder;

View File

@ -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;
},
},
});

View File

@ -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;
}
},
},
});

View File

@ -35,7 +35,6 @@ export class StatTimelineSuggestionsSupplier {
},
overrides: [],
},
previewModifier: (s) => {},
});
list.append({ name: SuggestionName.StateTimeline });

View File

@ -43,8 +43,10 @@ export class StatusHistorySuggestionsSupplier {
},
overrides: [],
},
previewModifier: (s) => {
s.options!.colWidth = 0.7;
cardOptions: {
previewModifier: (s) => {
s.options!.colWidth = 0.7;
},
},
});

View File

@ -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({});
}
}
}

View File

@ -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);

View File

@ -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({});
}
}

View File

@ -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);
}
},
},
});

View File

@ -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',
}