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:
Torkel Ödegaard
2021-11-08 13:18:14 +01:00
committed by GitHub
parent e9d90231e0
commit 3dee34c009
17 changed files with 309 additions and 193 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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