mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
PanelEdit: Show when field options have override rules or data config that overrides the default (#40250)
* First pass at showing data override dots * Added test * Adding override rule dots * Added unit test * Minor changes * Update public/app/features/dashboard/components/PanelEditor/OptionsPaneItemDescriptor.tsx Co-authored-by: Hugo Häggmark <hugo.haggmark@grafana.com> * Fixed ts issues * review feedback changes * skipp broken e2e test Co-authored-by: Hugo Häggmark <hugo.haggmark@grafana.com>
This commit is contained in:
@@ -3,6 +3,8 @@ import { Field, Label } from '@grafana/ui';
|
||||
import React, { ReactNode } from 'react';
|
||||
import Highlighter from 'react-highlight-words';
|
||||
import { OptionsPaneCategoryDescriptor } from './OptionsPaneCategoryDescriptor';
|
||||
import { OptionsPaneItemOverrides } from './OptionsPaneItemOverrides';
|
||||
import { OptionPaneItemOverrideInfo } from './types';
|
||||
|
||||
export interface OptionsPaneItemProps {
|
||||
title: string;
|
||||
@@ -12,6 +14,7 @@ export interface OptionsPaneItemProps {
|
||||
render: () => React.ReactNode;
|
||||
skipField?: boolean;
|
||||
showIf?: () => boolean;
|
||||
overrides?: OptionPaneItemOverrideInfo[];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -23,15 +26,20 @@ export class OptionsPaneItemDescriptor {
|
||||
constructor(public props: OptionsPaneItemProps) {}
|
||||
|
||||
getLabel(searchQuery?: string): ReactNode {
|
||||
const { title, description } = this.props;
|
||||
const { title, description, overrides } = this.props;
|
||||
|
||||
if (!searchQuery) {
|
||||
// Do not render label for categories with only one child
|
||||
if (this.parent.props.title === title) {
|
||||
if (this.parent.props.title === title && !overrides?.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return title;
|
||||
return (
|
||||
<Label description={description}>
|
||||
{title}
|
||||
{overrides && overrides.length > 0 && <OptionsPaneItemOverrides overrides={overrides} />}
|
||||
</Label>
|
||||
);
|
||||
}
|
||||
|
||||
const categories: React.ReactNode[] = [];
|
||||
@@ -47,6 +55,7 @@ export class OptionsPaneItemDescriptor {
|
||||
return (
|
||||
<Label description={description && this.highlightWord(description, searchQuery)} category={categories}>
|
||||
{this.highlightWord(title, searchQuery)}
|
||||
{overrides && overrides.length > 0 && <OptionsPaneItemOverrides overrides={overrides} />}
|
||||
</Label>
|
||||
);
|
||||
}
|
||||
@@ -57,6 +66,13 @@ export class OptionsPaneItemDescriptor {
|
||||
);
|
||||
}
|
||||
|
||||
renderOverrides() {
|
||||
const { overrides } = this.props;
|
||||
if (!overrides || overrides.length === 0) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
render(searchQuery?: string) {
|
||||
const { title, description, render, showIf, skipField } = this.props;
|
||||
const key = `${this.parent.props.id} ${title}`;
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
import React from 'react';
|
||||
import { Tooltip, useStyles2 } from '@grafana/ui';
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { css, CSSObject } from '@emotion/css';
|
||||
import { OptionPaneItemOverrideInfo } from './types';
|
||||
|
||||
export interface Props {
|
||||
overrides: OptionPaneItemOverrideInfo[];
|
||||
}
|
||||
|
||||
export function OptionsPaneItemOverrides({ overrides }: Props) {
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
return (
|
||||
<div className={styles.wrapper}>
|
||||
{overrides.map((override, index) => (
|
||||
<Tooltip content={override.tooltip} key={index.toString()} placement="top">
|
||||
<div aria-label={override.description} className={styles[override.type]} />
|
||||
</Tooltip>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => {
|
||||
const common: CSSObject = {
|
||||
width: 8,
|
||||
height: 8,
|
||||
borderRadius: '50%',
|
||||
marginLeft: theme.spacing(1),
|
||||
position: 'relative',
|
||||
top: '-1px',
|
||||
};
|
||||
|
||||
return {
|
||||
wrapper: css({
|
||||
display: 'flex',
|
||||
}),
|
||||
rule: css({
|
||||
...common,
|
||||
backgroundColor: theme.colors.primary.main,
|
||||
}),
|
||||
data: css({
|
||||
...common,
|
||||
backgroundColor: theme.colors.warning.main,
|
||||
}),
|
||||
};
|
||||
};
|
||||
@@ -2,10 +2,12 @@ import React from 'react';
|
||||
import { fireEvent, render, screen, within } from '@testing-library/react';
|
||||
import {
|
||||
FieldConfigSource,
|
||||
FieldType,
|
||||
LoadingState,
|
||||
PanelData,
|
||||
standardEditorsRegistry,
|
||||
standardFieldConfigEditorRegistry,
|
||||
toDataFrame,
|
||||
} from '@grafana/data';
|
||||
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
@@ -15,6 +17,7 @@ import { Provider } from 'react-redux';
|
||||
import configureMockStore from 'redux-mock-store';
|
||||
import { getPanelPlugin } from 'app/features/plugins/__mocks__/pluginMocks';
|
||||
import { getStandardFieldConfigs, getStandardOptionEditors } from '@grafana/ui';
|
||||
import { dataOverrideTooltipDescription, overrideRuleTooltipDescription } from './state/getOptionOverrides';
|
||||
|
||||
standardEditorsRegistry.setInit(getStandardOptionEditors);
|
||||
standardFieldConfigEditorRegistry.setInit(getStandardFieldConfigs);
|
||||
@@ -241,4 +244,46 @@ describe('OptionsPaneOptions', () => {
|
||||
within(thresholdsSection).getByLabelText(OptionsPaneSelector.fieldLabel('Thresholds CustomThresholdOption'))
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should show data override info dot', async () => {
|
||||
const scenario = new OptionsPaneOptionsTestScenario();
|
||||
scenario.panelData.series = [
|
||||
toDataFrame({
|
||||
fields: [
|
||||
{
|
||||
name: 'Value',
|
||||
type: FieldType.number,
|
||||
values: [10, 200],
|
||||
config: {
|
||||
min: 100,
|
||||
},
|
||||
},
|
||||
],
|
||||
refId: 'A',
|
||||
}),
|
||||
];
|
||||
|
||||
scenario.render();
|
||||
|
||||
expect(screen.getByLabelText(dataOverrideTooltipDescription)).toBeInTheDocument();
|
||||
expect(screen.queryByLabelText(overrideRuleTooltipDescription)).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should show override rule info dot', async () => {
|
||||
const scenario = new OptionsPaneOptionsTestScenario();
|
||||
scenario.panel.fieldConfig.overrides = [
|
||||
{
|
||||
matcher: { id: 'byName', options: 'SeriesA' },
|
||||
properties: [
|
||||
{
|
||||
id: 'decimals',
|
||||
value: 2,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
scenario.render();
|
||||
expect(screen.getByLabelText(overrideRuleTooltipDescription)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -10,8 +10,9 @@ import {
|
||||
isNestedPanelOptions,
|
||||
NestedValueAccess,
|
||||
PanelOptionsEditorBuilder,
|
||||
} from '../../../../../../packages/grafana-data/src/utils/OptionsUIBuilders';
|
||||
} from '@grafana/data/src/utils/OptionsUIBuilders';
|
||||
import { PanelOptionsSupplier } from '@grafana/data/src/panel/PanelPlugin';
|
||||
import { getOptionOverrides } from './state/getOptionOverrides';
|
||||
|
||||
type categoryGetter = (categoryNames?: string[]) => OptionsPaneCategoryDescriptor;
|
||||
|
||||
@@ -91,6 +92,7 @@ export function getVizualizationOptions(props: OptionPaneRenderProps): OptionsPa
|
||||
new OptionsPaneItemDescriptor({
|
||||
title: fieldOption.name,
|
||||
description: fieldOption.description,
|
||||
overrides: getOptionOverrides(fieldOption, currentFieldConfig, data?.series),
|
||||
render: function renderEditor() {
|
||||
const onChange = (v: any) => {
|
||||
onFieldConfigsChange(
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
import { DataFrame, FieldConfigPropertyItem, FieldConfigSource } from '@grafana/data';
|
||||
import { get as lodashGet } from 'lodash';
|
||||
import { OptionPaneItemOverrideInfo } from '../types';
|
||||
|
||||
export const dataOverrideTooltipDescription =
|
||||
'Some data fields have this option pre-configured. Add a field override rule to override the pre-configured value.';
|
||||
export const overrideRuleTooltipDescription = 'An override rule exists for this property';
|
||||
|
||||
export function getOptionOverrides(
|
||||
fieldOption: FieldConfigPropertyItem,
|
||||
fieldConfig: FieldConfigSource,
|
||||
frames: DataFrame[] | undefined
|
||||
): OptionPaneItemOverrideInfo[] {
|
||||
const infoDots: OptionPaneItemOverrideInfo[] = [];
|
||||
|
||||
// Look for options overriden in data field config
|
||||
if (frames) {
|
||||
for (const frame of frames) {
|
||||
for (const field of frame.fields) {
|
||||
const value = lodashGet(field.config, fieldOption.path);
|
||||
if (value == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
infoDots.push({
|
||||
type: 'data',
|
||||
description: dataOverrideTooltipDescription,
|
||||
tooltip: dataOverrideTooltipDescription,
|
||||
});
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const overrideRuleFound = fieldConfig.overrides.some((rule) =>
|
||||
rule.properties.some((prop) => prop.id === fieldOption.id)
|
||||
);
|
||||
|
||||
if (overrideRuleFound) {
|
||||
infoDots.push({
|
||||
type: 'rule',
|
||||
description: overrideRuleTooltipDescription,
|
||||
tooltip: overrideRuleTooltipDescription,
|
||||
});
|
||||
}
|
||||
|
||||
return infoDots;
|
||||
}
|
||||
@@ -59,3 +59,10 @@ export interface OptionPaneRenderProps {
|
||||
onPanelOptionsChanged: (options: any) => void;
|
||||
onFieldConfigsChange: (config: FieldConfigSource) => void;
|
||||
}
|
||||
|
||||
export interface OptionPaneItemOverrideInfo {
|
||||
type: 'data' | 'rule';
|
||||
onClick?: () => void;
|
||||
tooltip: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user