Various Panels: Add ability to toggle legend with keyboard shortcut (#52241)

This commit is contained in:
Alyssa Bull
2022-07-27 13:39:55 -06:00
committed by GitHub
parent d06ea6ea0e
commit 6ec9a7682d
30 changed files with 192 additions and 72 deletions

View File

@ -133,7 +133,7 @@ GraphThresholdsStyleConfig: {
LegendPlacement: "bottom" | "right" @cuetsy(kind="type")
// TODO docs
LegendDisplayMode: "list" | "table" | "hidden" @cuetsy(kind="enum")
LegendDisplayMode: "list" | "table" @cuetsy(kind="enum")
// TODO docs
TableSortByFieldState: {
@ -235,6 +235,7 @@ GraphFieldConfig: {
VizLegendOptions: {
displayMode: LegendDisplayMode
placement: LegendPlacement
showLegend: bool
asTable?: bool
isVisible?: bool
sortBy?: string

View File

@ -168,7 +168,6 @@ export interface GraphThresholdsStyleConfig {
export type LegendPlacement = ('bottom' | 'right');
export enum LegendDisplayMode {
Hidden = 'hidden',
List = 'list',
Table = 'table',
}
@ -289,6 +288,7 @@ export interface VizLegendOptions {
displayMode: LegendDisplayMode;
isVisible?: boolean;
placement: LegendPlacement;
showLegend: boolean;
sortBy?: string;
sortDesc?: boolean;
width?: number;

View File

@ -119,13 +119,7 @@ export const WithLegend: Story<StoryProps> = ({ rightAxisSeries, displayMode, le
return (
<GraphWithLegend
legendDisplayMode={
displayMode === 'hidden'
? LegendDisplayMode.Hidden
: displayMode === 'table'
? LegendDisplayMode.Table
: LegendDisplayMode.List
}
legendDisplayMode={displayMode === 'table' ? LegendDisplayMode.Table : LegendDisplayMode.List}
{...args}
{...props}
/>

View File

@ -15,6 +15,7 @@ import { Graph, GraphProps } from './Graph';
export interface GraphWithLegendProps extends GraphProps {
legendDisplayMode: LegendDisplayMode;
legendVisibility: boolean;
placement: LegendPlacement;
hideEmpty?: boolean;
hideZero?: boolean;
@ -58,6 +59,7 @@ export const GraphWithLegend: React.FunctionComponent<GraphWithLegendProps> = (p
sortLegendBy,
sortLegendDesc,
legendDisplayMode,
legendVisibility,
placement,
onSeriesToggle,
onToggleSort,
@ -106,7 +108,7 @@ export const GraphWithLegend: React.FunctionComponent<GraphWithLegendProps> = (p
</Graph>
</div>
{legendDisplayMode !== LegendDisplayMode.Hidden && (
{legendVisibility && (
<div className={legendContainer}>
<CustomScrollbar hideHorizontalTrack>
<VizLegend

View File

@ -1,7 +1,6 @@
import React from 'react';
import { DataFrame, TimeRange } from '@grafana/data';
import { LegendDisplayMode } from '@grafana/schema';
import { PropDiffFn } from '../../../../../packages/grafana-ui/src/components/GraphNG/GraphNG';
import { withTheme2 } from '../../themes/ThemeContext';
@ -41,7 +40,8 @@ export class UnthemedTimeSeries extends React.Component<TimeSeriesProps> {
renderLegend = (config: UPlotConfigBuilder) => {
const { legend, frames } = this.props;
if (!config || (legend && legend.displayMode === LegendDisplayMode.Hidden)) {
//hides and shows the legend ON the uPlot graph
if (!config || (legend && !legend.showLegend)) {
return null;
}

View File

@ -9,9 +9,16 @@ export function addLegendOptions<T extends OptionsWithLegend>(
includeLegendCalcs = true
) {
builder
.addBooleanSwitch({
path: 'legend.showLegend',
name: 'Visibility',
category: ['Legend'],
description: '',
defaultValue: true,
})
.addRadio({
path: 'legend.displayMode',
name: 'Legend mode',
name: 'Mode',
category: ['Legend'],
description: '',
defaultValue: LegendDisplayMode.List,
@ -19,13 +26,13 @@ export function addLegendOptions<T extends OptionsWithLegend>(
options: [
{ value: LegendDisplayMode.List, label: 'List' },
{ value: LegendDisplayMode.Table, label: 'Table' },
{ value: LegendDisplayMode.Hidden, label: 'Hidden' },
],
},
showIf: (c) => c.legend.showLegend,
})
.addRadio({
path: 'legend.placement',
name: 'Legend placement',
name: 'Placement',
category: ['Legend'],
description: '',
defaultValue: 'bottom',
@ -35,7 +42,7 @@ export function addLegendOptions<T extends OptionsWithLegend>(
{ value: 'right', label: 'Right' },
],
},
showIf: (c) => c.legend.displayMode !== LegendDisplayMode.Hidden,
showIf: (c) => c.legend.showLegend,
})
.addNumberInput({
path: 'legend.width',
@ -44,14 +51,14 @@ export function addLegendOptions<T extends OptionsWithLegend>(
settings: {
placeholder: 'Auto',
},
showIf: (c) => c.legend.displayMode !== LegendDisplayMode.Hidden && c.legend.placement === 'right',
showIf: (c) => c.legend.showLegend && c.legend.placement === 'right',
});
if (includeLegendCalcs) {
builder.addCustomEditor<StatsPickerConfigSettings, string[]>({
id: 'legend.calcs',
path: 'legend.calcs',
name: 'Legend values',
name: 'Values',
category: ['Legend'],
description: 'Select values or calculations to show in legend',
editor: standardEditorsRegistry.get('stats-picker').editor as any,
@ -59,7 +66,7 @@ export function addLegendOptions<T extends OptionsWithLegend>(
settings: {
allowMultiple: true,
},
showIf: (currentConfig) => currentConfig.legend.displayMode !== LegendDisplayMode.Hidden,
showIf: (currentConfig) => currentConfig.legend.showLegend !== false,
});
}
}

View File

@ -239,6 +239,16 @@ export class KeybindingSrv {
locationService.partial({ viewPanel: isViewing ? null : panelId });
});
//toggle legend
this.bindWithPanelId('p l', (panelId) => {
const panel = dashboard.getPanelById(panelId)!;
const newOptions = { ...panel.options };
newOptions.legend.showLegend ? (newOptions.legend.showLegend = false) : (newOptions.legend.showLegend = true);
panel.updateOptions(newOptions);
});
this.bindWithPanelId('i', (panelId) => {
locationService.partial({ inspect: panelId });
});
@ -293,14 +303,6 @@ export class KeybindingSrv {
});
// toggle panel legend
this.bindWithPanelId('p l', (panelId) => {
const panelInfo = dashboard.getPanelInfoById(panelId)!;
if (panelInfo.panel.legend) {
panelInfo.panel.legend.show = !panelInfo.panel.legend.show;
panelInfo.panel.render();
}
});
// toggle all panel legends
this.bind('d l', () => {

View File

@ -193,7 +193,7 @@ describe('DashboardModel', () => {
});
it('dashboard schema version should be set to latest', () => {
expect(model.schemaVersion).toBe(36);
expect(model.schemaVersion).toBe(37);
});
it('graph thresholds should be migrated', () => {
@ -2026,6 +2026,72 @@ describe('DashboardModel', () => {
});
});
describe('when generating the legend for a panel', () => {
let model: DashboardModel;
beforeEach(() => {
model = new DashboardModel({
panels: [
{
id: 0,
options: {
legend: {
displayMode: 'hidden',
placement: 'bottom',
},
tooltipOptions: {
mode: 'single',
},
},
},
{
id: 1,
options: {
legend: {
displayMode: 'list',
placement: 'right',
},
tooltipOptions: {
mode: 'single',
},
},
},
{
id: 2,
options: {
legend: {
displayMode: 'table',
placement: 'bottom',
},
tooltipOptions: {
mode: 'single',
},
},
},
],
schemaVersion: 30,
});
});
it('should update displayMode = hidden to showLegend = false and displayMode = list', () => {
expect(model.panels[0].options.legend).toEqual({ displayMode: 'list', showLegend: false, placement: 'bottom' });
});
it('should keep displayMode = list and update to showLegend = true', () => {
expect(model.panels[1].options.legend).toEqual({ displayMode: 'list', showLegend: true, placement: 'right' });
});
it('should keep displayMode = table and update to showLegend = true', () => {
expect(model.panels[2].options.legend).toEqual({ displayMode: 'table', showLegend: true, placement: 'bottom' });
});
it('should preserve the placement', () => {
expect(model.panels[0].options.legend.placement).toEqual('bottom');
expect(model.panels[1].options.legend.placement).toEqual('right');
expect(model.panels[2].options.legend.placement).toEqual('bottom');
});
});
function createRow(options: any, panelDescriptions: any[]) {
const PANEL_HEIGHT_STEP = GRID_CELL_HEIGHT + GRID_CELL_VMARGIN;
const { collapse, showTitle, title, repeat, repeatIteration } = options;

View File

@ -76,7 +76,7 @@ export class DashboardMigrator {
let i, j, k, n;
const oldVersion = this.dashboard.schemaVersion;
const panelUpgrades: PanelSchemeUpgradeHandler[] = [];
this.dashboard.schemaVersion = 36;
this.dashboard.schemaVersion = 37;
if (oldVersion === this.dashboard.schemaVersion) {
return;
@ -777,6 +777,18 @@ export class DashboardMigrator {
}
}
if (oldVersion < 37) {
panelUpgrades.push((panel: PanelModel) => {
if (panel.options?.legend && panel.options.legend.displayMode === 'hidden') {
panel.options.legend.displayMode = 'list';
panel.options.legend.showLegend = false;
} else if (panel.options?.legend) {
panel.options.legend = { ...panel.options?.legend, showLegend: true };
}
return panel;
});
}
if (panelUpgrades.length === 0) {
return;
}

View File

@ -45,6 +45,12 @@ describe('getPanelMenu', () => {
"shortcut": "x",
"text": "Explore",
},
Object {
"iconClassName": "exchange-alt",
"onClick": [Function],
"shortcut": "p l",
"text": "Show legend",
},
Object {
"iconClassName": "info-circle",
"onClick": [Function],
@ -129,6 +135,12 @@ describe('getPanelMenu', () => {
"shortcut": "x",
"text": "Explore",
},
Object {
"iconClassName": "exchange-alt",
"onClick": [Function],
"shortcut": "p l",
"text": "Show legend",
},
Object {
"iconClassName": "info-circle",
"onClick": [Function],

View File

@ -12,6 +12,7 @@ import {
duplicatePanel,
removePanel,
sharePanel,
toggleLegend,
unlinkLibraryPanel,
} from 'app/features/dashboard/utils/panel';
import { isPanelModelLibraryPanel } from 'app/features/library-panels/guard';
@ -46,6 +47,11 @@ export function getPanelMenu(
sharePanel(dashboard, panel);
};
const onToggleLegend = (event: React.MouseEvent<any>) => {
event.preventDefault();
toggleLegend(panel);
};
const onAddLibraryPanel = (event: React.MouseEvent<any>) => {
event.preventDefault();
addLibraryPanel(dashboard, panel);
@ -129,11 +135,18 @@ export function getPanelMenu(
menu.push({
text: 'Explore',
iconClassName: 'compass',
shortcut: 'x',
onClick: onNavigateToExplore,
shortcut: 'x',
});
}
menu.push({
text: panel.options.legend?.showLegend ? 'Hide legend' : 'Show legend',
iconClassName: 'exchange-alt',
onClick: onToggleLegend,
shortcut: 'p l',
});
const inspectMenu: PanelMenuItem[] = [];
// Only show these inspect actions for data plugins

View File

@ -102,10 +102,11 @@ export const refreshPanel = (panel: PanelModel) => {
};
export const toggleLegend = (panel: PanelModel) => {
console.warn('Toggle legend is not implemented yet');
// We need to set panel.legend defaults first
// panel.legend.show = !panel.legend.show;
refreshPanel(panel);
const newOptions = { ...panel.options };
newOptions.legend.showLegend === true
? (newOptions.legend.showLegend = false)
: (newOptions.legend.showLegend = true);
panel.updateOptions(newOptions);
};
export interface TimeOverrideResult {

View File

@ -173,7 +173,12 @@ export function ExploreGraph({
options={
{
tooltip: { mode: tooltipDisplayMode, sort: SortOrder.None },
legend: { displayMode: LegendDisplayMode.List, placement: 'bottom', calcs: [] },
legend: {
displayMode: LegendDisplayMode.List,
showLegend: true,
placement: 'bottom',
calcs: [],
},
} as TimeSeriesOptions
}
/>

View File

@ -12,7 +12,6 @@ import {
VizOrientation,
} from '@grafana/data';
import { PanelDataErrorView } from '@grafana/runtime';
import { LegendDisplayMode } from '@grafana/schema';
import {
GraphNG,
GraphNGProps,
@ -183,7 +182,7 @@ export const BarChartPanel: React.FunctionComponent<Props> = ({
const renderLegend = (config: UPlotConfigBuilder) => {
const { legend } = options;
if (!config || legend.displayMode === LegendDisplayMode.Hidden) {
if (!config || legend.showLegend === false) {
return null;
}

View File

@ -1,5 +1,5 @@
import { VisualizationSuggestionsBuilder, VizOrientation } from '@grafana/data';
import { LegendDisplayMode, StackingMode, VisibilityMode } from '@grafana/schema';
import { StackingMode, VisibilityMode } from '@grafana/schema';
import { SuggestionName } from 'app/types/suggestions';
import { BarChartFieldConfig, PanelOptions } from './models.gen';
@ -12,7 +12,7 @@ export class BarChartSuggestionsSupplier {
options: {
showValue: VisibilityMode.Never,
legend: {
displayMode: LegendDisplayMode.Hidden,
showLegend: true,
placement: 'right',
} as any,
},

View File

@ -94,6 +94,7 @@ describe('BarChart utils', () => {
showValue: VisibilityMode.Always,
legend: {
displayMode: LegendDisplayMode.List,
showLegend: true,
placement: 'bottom',
calcs: [],
},

View File

@ -66,6 +66,7 @@ export const defaultPanelOptions: CandlestickOptions = {
fields: {},
legend: {
displayMode: LegendDisplayMode.List,
showLegend: true,
placement: 'bottom',
calcs: [],
},

View File

@ -12,14 +12,7 @@ import {
histogramBucketSizes,
histogramFrameBucketMaxFieldName,
} from '@grafana/data/src/transformations/transformers/histogram';
import {
VizLegendOptions,
LegendDisplayMode,
ScaleDistribution,
AxisPlacement,
ScaleDirection,
ScaleOrientation,
} from '@grafana/schema';
import { VizLegendOptions, ScaleDistribution, AxisPlacement, ScaleDirection, ScaleOrientation } from '@grafana/schema';
import {
Themeable2,
UPlotConfigBuilder,
@ -273,7 +266,7 @@ export class Histogram extends React.Component<HistogramProps, State> {
renderLegend(config: UPlotConfigBuilder) {
const { legend } = this.props;
if (!config || legend.displayMode === LegendDisplayMode.Hidden) {
if (!config || legend.showLegend === false) {
return null;
}

View File

@ -25,6 +25,7 @@ export const defaultPanelOptions: PanelOptions = {
bucketOffset: 0,
legend: {
displayMode: LegendDisplayMode.List,
showLegend: true,
placement: 'bottom',
calcs: [],
},

View File

@ -171,6 +171,7 @@ const setup = (propsOverrides?: {}) => {
displayLabels: [],
legend: {
displayMode: LegendDisplayMode.List,
showLegend: true,
placement: 'right',
calcs: [],
values: [PieChartLegendValues.Percent],

View File

@ -27,6 +27,7 @@ import { filterDisplayItems, sumDisplayItemsReducer } from './utils';
const defaultLegendOptions: PieChartLegendOptions = {
displayMode: LegendDisplayMode.List,
showLegend: true,
placement: 'right',
calcs: [],
values: [PieChartLegendValues.Percent],
@ -77,7 +78,7 @@ export function PieChartPanel(props: Props) {
function getLegend(props: Props, displayValues: FieldDisplay[]) {
const legendOptions = props.options.legend ?? defaultLegendOptions;
if (legendOptions.displayMode === LegendDisplayMode.Hidden) {
if (legendOptions.showLegend === false) {
return undefined;
}
const total = displayValues.filter(filterDisplayItems).reduce(sumDisplayItemsReducer, 0);

View File

@ -1,5 +1,4 @@
import { FieldColorModeId, FieldConfigProperty, FieldMatcherID, PanelModel } from '@grafana/data';
import { LegendDisplayMode } from '@grafana/schema';
import { PieChartPanelChangedHandler } from './migrations';
import { PieChartLabels } from './types';
@ -71,6 +70,6 @@ describe('PieChart -> PieChartV2 migrations', () => {
},
};
const options = PieChartPanelChangedHandler(panel, 'grafana-piechart-panel', oldPieChartOptions);
expect(options).toMatchObject({ legend: { displayMode: LegendDisplayMode.Hidden } });
expect(options).toMatchObject({ legend: { showLegend: false } });
});
});

View File

@ -45,7 +45,13 @@ export const PieChartPanelChangedHandler = (
},
};
options.legend = { placement: 'right', values: [], displayMode: LegendDisplayMode.Table, calcs: [] };
options.legend = {
placement: 'right',
values: [],
displayMode: LegendDisplayMode.Table,
showLegend: true,
calcs: [],
};
if (angular.valueName) {
options.reduceOptions = { calcs: [] };
@ -88,7 +94,7 @@ export const PieChartPanelChangedHandler = (
if (angular.legend) {
if (!angular.legend.show) {
options.legend.displayMode = LegendDisplayMode.Hidden;
options.legend.showLegend = false;
}
if (angular.legend.values) {
options.legend.values.push(PieChartLegendValues.Value);
@ -98,13 +104,13 @@ export const PieChartPanelChangedHandler = (
}
if (!angular.legend.percentage && !angular.legend.values) {
// If you deselect both value and percentage in the old pie chart plugin, the legend is hidden.
options.legend.displayMode = LegendDisplayMode.Hidden;
options.legend.showLegend = false;
}
}
// Set up labels when the old piechart is using 'on graph', for the legend option.
if (angular.legendType === 'On graph') {
options.legend.displayMode = LegendDisplayMode.Hidden;
options.legend.showLegend = false;
options.displayLabels = [PieChartLabels.Name];
if (angular.legend.values) {
options.displayLabels.push(PieChartLabels.Value);

View File

@ -1,5 +1,4 @@
import { FieldColorModeId, FieldConfigProperty, PanelPlugin } from '@grafana/data';
import { LegendDisplayMode } from '@grafana/schema';
import { commonOptionsBuilder } from '@grafana/ui';
import { addStandardDataReduceOptions } from '../stat/common';
@ -70,7 +69,7 @@ export const plugin = new PanelPlugin<PieChartOptions>(PieChartPanel)
{ value: PieChartLegendValues.Value, label: 'Value' },
],
},
showIf: (c) => c.legend.displayMode !== LegendDisplayMode.Hidden,
showIf: (c) => c.legend.showLegend !== false,
});
})
.setSuggestionsSupplier(new PieChartSuggestionsSupplier());

View File

@ -1,5 +1,4 @@
import { VisualizationSuggestionsBuilder } from '@grafana/data';
import { LegendDisplayMode } from '@grafana/schema';
import { SuggestionName } from 'app/types/suggestions';
import { PieChartLabels, PieChartOptions, PieChartType } from './types';
@ -23,7 +22,7 @@ export class PieChartSuggestionsSupplier {
cardOptions: {
previewModifier: (s) => {
// Hide labels in preview
s.options!.legend.displayMode = LegendDisplayMode.Hidden;
s.options!.legend.showLegend = false;
},
},
});

View File

@ -1,7 +1,7 @@
import React from 'react';
import { DataFrame, FALLBACK_COLOR, FieldType, TimeRange } from '@grafana/data';
import { LegendDisplayMode, VisibilityMode } from '@grafana/schema';
import { VisibilityMode } from '@grafana/schema';
import {
PanelContext,
PanelContextRoot,
@ -73,7 +73,7 @@ export class TimelineChart extends React.Component<TimelineProps> {
renderLegend = (config: UPlotConfigBuilder) => {
const { legend, legendItems } = this.props;
if (!config || !legendItems || !legend || legend.displayMode === LegendDisplayMode.Hidden) {
if (!config || !legendItems || !legend || legend.showLegend === false) {
return null;
}

View File

@ -497,7 +497,7 @@ export function prepareTimelineLegendItems(
options: VizLegendOptions,
theme: GrafanaTheme2
): VizLegendItem[] | undefined {
if (!frames || options.displayMode === 'hidden') {
if (!frames || options.showLegend === false) {
return undefined;
}

View File

@ -44,6 +44,7 @@ Object {
],
"displayMode": "table",
"placement": "bottom",
"showLegend": true,
},
"tooltip": Object {
"mode": "multi",
@ -72,6 +73,7 @@ Object {
],
"displayMode": "list",
"placement": "bottom",
"showLegend": true,
},
"tooltip": Object {
"mode": "single",
@ -98,6 +100,7 @@ Object {
"calcs": Array [],
"displayMode": "list",
"placement": "bottom",
"showLegend": true,
},
"tooltip": Object {
"mode": "single",
@ -169,6 +172,7 @@ Object {
],
"displayMode": "table",
"placement": "bottom",
"showLegend": true,
},
"tooltip": Object {
"mode": "multi",
@ -240,6 +244,7 @@ Object {
],
"displayMode": "table",
"placement": "bottom",
"showLegend": true,
},
"tooltip": Object {
"mode": "multi",
@ -268,6 +273,7 @@ Object {
"calcs": Array [],
"displayMode": "list",
"placement": "bottom",
"showLegend": true,
},
"tooltip": Object {
"mode": "single",
@ -355,6 +361,7 @@ Object {
],
"displayMode": "table",
"placement": "bottom",
"showLegend": true,
},
"tooltip": Object {
"mode": "multi",
@ -412,6 +419,7 @@ Object {
],
"displayMode": "table",
"placement": "bottom",
"showLegend": true,
},
"tooltip": Object {
"mode": "multi",
@ -453,6 +461,7 @@ Object {
],
"displayMode": "table",
"placement": "bottom",
"showLegend": true,
},
"tooltip": Object {
"mode": "multi",
@ -502,6 +511,7 @@ Object {
"calcs": Array [],
"displayMode": "list",
"placement": "bottom",
"showLegend": true,
},
"tooltip": Object {
"mode": "multi",
@ -638,6 +648,7 @@ Object {
"calcs": Array [],
"displayMode": "list",
"placement": "bottom",
"showLegend": true,
},
"tooltip": Object {
"mode": "multi",

View File

@ -319,6 +319,7 @@ export function flotToGraphOptions(angular: any): { fieldConfig: FieldConfigSour
const options: TimeSeriesOptions = {
legend: {
displayMode: LegendDisplayMode.List,
showLegend: true,
placement: 'bottom',
calcs: [],
},
@ -334,7 +335,7 @@ export function flotToGraphOptions(angular: any): { fieldConfig: FieldConfigSour
if (legendConfig.show) {
options.legend.displayMode = legendConfig.alignAsTable ? LegendDisplayMode.Table : LegendDisplayMode.List;
} else {
options.legend.displayMode = LegendDisplayMode.Hidden;
options.legend.showLegend = false;
}
if (legendConfig.rightSide) {

View File

@ -1,12 +1,5 @@
import { FieldColorModeId, VisualizationSuggestionsBuilder } from '@grafana/data';
import {
GraphDrawStyle,
GraphFieldConfig,
GraphGradientMode,
LegendDisplayMode,
LineInterpolation,
StackingMode,
} from '@grafana/schema';
import { GraphDrawStyle, GraphFieldConfig, GraphGradientMode, LineInterpolation, StackingMode } from '@grafana/schema';
import { SuggestionName } from 'app/types/suggestions';
import { TimeSeriesOptions } from './types';
@ -33,7 +26,7 @@ export class TimeSeriesSuggestionsSupplier {
},
cardOptions: {
previewModifier: (s) => {
s.options!.legend.displayMode = LegendDisplayMode.Hidden;
s.options!.legend.showLegend = false;
if (s.fieldConfig?.defaults.custom?.drawStyle !== GraphDrawStyle.Bars) {
s.fieldConfig!.defaults.custom!.lineWidth = Math.max(s.fieldConfig!.defaults.custom!.lineWidth ?? 1, 2);