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
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 309 additions and 193 deletions

View File

@ -1,10 +1,10 @@
#!/usr/bin/env node
const { existsSync } = require(`fs`);
const { createRequire, createRequireFromPath } = require(`module`);
const { resolve } = require(`path`);
const {existsSync} = require(`fs`);
const {createRequire, createRequireFromPath} = require(`module`);
const {resolve} = require(`path`);
const relPnpApiPath = '../../../../.pnp.cjs';
const relPnpApiPath = "../../../../.pnp.cjs";
const absPnpApiPath = resolve(__dirname, relPnpApiPath);
const absRequire = (createRequire || createRequireFromPath)(absPnpApiPath);

View File

@ -1,10 +1,10 @@
#!/usr/bin/env node
const { existsSync } = require(`fs`);
const { createRequire, createRequireFromPath } = require(`module`);
const { resolve } = require(`path`);
const {existsSync} = require(`fs`);
const {createRequire, createRequireFromPath} = require(`module`);
const {resolve} = require(`path`);
const relPnpApiPath = '../../../../.pnp.cjs';
const relPnpApiPath = "../../../../.pnp.cjs";
const absPnpApiPath = resolve(__dirname, relPnpApiPath);
const absRequire = (createRequire || createRequireFromPath)(absPnpApiPath);

View File

@ -2,4 +2,5 @@
# Manual changes might be lost!
integrations:
- vim
- vscode

View File

@ -1,10 +1,10 @@
#!/usr/bin/env node
const { existsSync } = require(`fs`);
const { createRequire, createRequireFromPath } = require(`module`);
const { resolve } = require(`path`);
const {existsSync} = require(`fs`);
const {createRequire, createRequireFromPath} = require(`module`);
const {resolve} = require(`path`);
const relPnpApiPath = '../../../.pnp.cjs';
const relPnpApiPath = "../../../.pnp.cjs";
const absPnpApiPath = resolve(__dirname, relPnpApiPath);
const absRequire = (createRequire || createRequireFromPath)(absPnpApiPath);

View File

@ -1,10 +1,10 @@
#!/usr/bin/env node
const { existsSync } = require(`fs`);
const { createRequire, createRequireFromPath } = require(`module`);
const { resolve } = require(`path`);
const {existsSync} = require(`fs`);
const {createRequire, createRequireFromPath} = require(`module`);
const {resolve} = require(`path`);
const relPnpApiPath = '../../../../.pnp.cjs';
const relPnpApiPath = "../../../../.pnp.cjs";
const absPnpApiPath = resolve(__dirname, relPnpApiPath);
const absRequire = (createRequire || createRequireFromPath)(absPnpApiPath);

View File

@ -1,10 +1,10 @@
#!/usr/bin/env node
const { existsSync } = require(`fs`);
const { createRequire, createRequireFromPath } = require(`module`);
const { resolve } = require(`path`);
const {existsSync} = require(`fs`);
const {createRequire, createRequireFromPath} = require(`module`);
const {resolve} = require(`path`);
const relPnpApiPath = '../../../../.pnp.cjs';
const relPnpApiPath = "../../../../.pnp.cjs";
const absPnpApiPath = resolve(__dirname, relPnpApiPath);
const absRequire = (createRequire || createRequireFromPath)(absPnpApiPath);

View File

@ -1,10 +1,10 @@
#!/usr/bin/env node
const { existsSync } = require(`fs`);
const { createRequire, createRequireFromPath } = require(`module`);
const { resolve } = require(`path`);
const {existsSync} = require(`fs`);
const {createRequire, createRequireFromPath} = require(`module`);
const {resolve} = require(`path`);
const relPnpApiPath = '../../../../.pnp.cjs';
const relPnpApiPath = "../../../../.pnp.cjs";
const absPnpApiPath = resolve(__dirname, relPnpApiPath);
const absRequire = (createRequire || createRequireFromPath)(absPnpApiPath);

View File

@ -1,30 +1,28 @@
#!/usr/bin/env node
const { existsSync } = require(`fs`);
const { createRequire, createRequireFromPath } = require(`module`);
const { resolve } = require(`path`);
const {existsSync} = require(`fs`);
const {createRequire, createRequireFromPath} = require(`module`);
const {resolve} = require(`path`);
const relPnpApiPath = '../../../../.pnp.cjs';
const relPnpApiPath = "../../../../.pnp.cjs";
const absPnpApiPath = resolve(__dirname, relPnpApiPath);
const absRequire = (createRequire || createRequireFromPath)(absPnpApiPath);
const moduleWrapper = (tsserver) => {
const moduleWrapper = tsserver => {
if (!process.versions.pnp) {
return tsserver;
}
const { isAbsolute } = require(`path`);
const {isAbsolute} = require(`path`);
const pnpApi = require(`pnpapi`);
const isVirtual = (str) => str.match(/\/(\$\$virtual|__virtual__)\//);
const normalize = (str) => str.replace(/\\/g, `/`).replace(/^\/?/, `/`);
const isVirtual = str => str.match(/\/(\$\$virtual|__virtual__)\//);
const normalize = str => str.replace(/\\/g, `/`).replace(/^\/?/, `/`);
const dependencyTreeRoots = new Set(
pnpApi.getDependencyTreeRoots().map((locator) => {
return `${locator.name}@${locator.reference}`;
})
);
const dependencyTreeRoots = new Set(pnpApi.getDependencyTreeRoots().map(locator => {
return `${locator.name}@${locator.reference}`;
}));
// VSCode sends the zip paths to TS using the "zip://" prefix, that TS
// doesn't understand. This layer makes sure to remove the protocol
@ -66,43 +64,33 @@ const moduleWrapper = (tsserver) => {
// Before | ^zip:/c:/foo/bar.zip/package.json
// After | ^/zip//c:/foo/bar.zip/package.json
//
case `vscode <1.61`:
{
str = `^zip:${str}`;
}
break;
case `vscode <1.61`: {
str = `^zip:${str}`;
} break;
case `vscode`:
{
str = `^/zip/${str}`;
}
break;
case `vscode`: {
str = `^/zip/${str}`;
} break;
// To make "go to definition" work,
// We have to resolve the actual file system path from virtual path
// and convert scheme to supported by [vim-rzip](https://github.com/lbrayner/vim-rzip)
case `coc-nvim`:
{
str = normalize(resolved).replace(/\.zip\//, `.zip::`);
str = resolve(`zipfile:${str}`);
}
break;
case `coc-nvim`: {
str = normalize(resolved).replace(/\.zip\//, `.zip::`);
str = resolve(`zipfile:${str}`);
} break;
// Support neovim native LSP and [typescript-language-server](https://github.com/theia-ide/typescript-language-server)
// We have to resolve the actual file system path from virtual path,
// everything else is up to neovim
case `neovim`:
{
str = normalize(resolved).replace(/\.zip\//, `.zip::`);
str = `zipfile:${str}`;
}
break;
case `neovim`: {
str = normalize(resolved).replace(/\.zip\//, `.zip::`);
str = `zipfile:${str}`;
} break;
default:
{
str = `zip:${str}`;
}
break;
default: {
str = `zip:${str}`;
} break;
}
}
}
@ -113,24 +101,22 @@ const moduleWrapper = (tsserver) => {
function fromEditorPath(str) {
switch (hostInfo) {
case `coc-nvim`:
case `neovim`:
{
str = str.replace(/\.zip::/, `.zip/`);
// The path for coc-nvim is in format of /<pwd>/zipfile:/<pwd>/.yarn/...
// So in order to convert it back, we use .* to match all the thing
// before `zipfile:`
return process.platform === `win32` ? str.replace(/^.*zipfile:\//, ``) : str.replace(/^.*zipfile:/, ``);
}
break;
case `neovim`: {
str = str.replace(/\.zip::/, `.zip/`);
// The path for coc-nvim is in format of /<pwd>/zipfile:/<pwd>/.yarn/...
// So in order to convert it back, we use .* to match all the thing
// before `zipfile:`
return process.platform === `win32`
? str.replace(/^.*zipfile:\//, ``)
: str.replace(/^.*zipfile:/, ``);
} break;
case `vscode`:
default:
{
return process.platform === `win32`
? str.replace(/^\^?(zip:|\/zip)\/+/, ``)
: str.replace(/^\^?(zip:|\/zip)\/+/, `/`);
}
break;
default: {
return process.platform === `win32`
? str.replace(/^\^?(zip:|\/zip)\/+/, ``)
: str.replace(/^\^?(zip:|\/zip)\/+/, `/`);
} break;
}
}
@ -142,8 +128,8 @@ const moduleWrapper = (tsserver) => {
// TypeScript already does local loads and if this code is running the user trusts the workspace
// https://github.com/microsoft/vscode/issues/45856
const ConfiguredProject = tsserver.server.ConfiguredProject;
const { enablePluginsWithOptions: originalEnablePluginsWithOptions } = ConfiguredProject.prototype;
ConfiguredProject.prototype.enablePluginsWithOptions = function () {
const {enablePluginsWithOptions: originalEnablePluginsWithOptions} = ConfiguredProject.prototype;
ConfiguredProject.prototype.enablePluginsWithOptions = function() {
this.projectService.allowLocalPluginLoads = true;
return originalEnablePluginsWithOptions.apply(this, arguments);
};
@ -153,12 +139,12 @@ const moduleWrapper = (tsserver) => {
// like an absolute path of ours and normalize it.
const Session = tsserver.server.Session;
const { onMessage: originalOnMessage, send: originalSend } = Session.prototype;
const {onMessage: originalOnMessage, send: originalSend} = Session.prototype;
let hostInfo = `unknown`;
Object.assign(Session.prototype, {
onMessage(/** @type {string} */ message) {
const parsedMessage = JSON.parse(message);
const parsedMessage = JSON.parse(message)
if (
parsedMessage != null &&
@ -167,33 +153,21 @@ const moduleWrapper = (tsserver) => {
typeof parsedMessage.arguments.hostInfo === `string`
) {
hostInfo = parsedMessage.arguments.hostInfo;
if (
hostInfo === `vscode` &&
process.env.VSCODE_IPC_HOOK &&
process.env.VSCODE_IPC_HOOK.match(/Code\/1\.([1-5][0-9]|60)\./)
) {
if (hostInfo === `vscode` && process.env.VSCODE_IPC_HOOK && process.env.VSCODE_IPC_HOOK.match(/Code\/1\.([1-5][0-9]|60)\./)) {
hostInfo += ` <1.61`;
}
}
return originalOnMessage.call(
this,
JSON.stringify(parsedMessage, (key, value) => {
return typeof value === `string` ? fromEditorPath(value) : value;
})
);
return originalOnMessage.call(this, JSON.stringify(parsedMessage, (key, value) => {
return typeof value === `string` ? fromEditorPath(value) : value;
}));
},
send(/** @type {any} */ msg) {
return originalSend.call(
this,
JSON.parse(
JSON.stringify(msg, (key, value) => {
return typeof value === `string` ? toEditorPath(value) : value;
})
)
);
},
return originalSend.call(this, JSON.parse(JSON.stringify(msg, (key, value) => {
return typeof value === `string` ? toEditorPath(value) : value;
})));
}
});
return tsserver;

View File

@ -1,30 +1,28 @@
#!/usr/bin/env node
const { existsSync } = require(`fs`);
const { createRequire, createRequireFromPath } = require(`module`);
const { resolve } = require(`path`);
const {existsSync} = require(`fs`);
const {createRequire, createRequireFromPath} = require(`module`);
const {resolve} = require(`path`);
const relPnpApiPath = '../../../../.pnp.cjs';
const relPnpApiPath = "../../../../.pnp.cjs";
const absPnpApiPath = resolve(__dirname, relPnpApiPath);
const absRequire = (createRequire || createRequireFromPath)(absPnpApiPath);
const moduleWrapper = (tsserver) => {
const moduleWrapper = tsserver => {
if (!process.versions.pnp) {
return tsserver;
}
const { isAbsolute } = require(`path`);
const {isAbsolute} = require(`path`);
const pnpApi = require(`pnpapi`);
const isVirtual = (str) => str.match(/\/(\$\$virtual|__virtual__)\//);
const normalize = (str) => str.replace(/\\/g, `/`).replace(/^\/?/, `/`);
const isVirtual = str => str.match(/\/(\$\$virtual|__virtual__)\//);
const normalize = str => str.replace(/\\/g, `/`).replace(/^\/?/, `/`);
const dependencyTreeRoots = new Set(
pnpApi.getDependencyTreeRoots().map((locator) => {
return `${locator.name}@${locator.reference}`;
})
);
const dependencyTreeRoots = new Set(pnpApi.getDependencyTreeRoots().map(locator => {
return `${locator.name}@${locator.reference}`;
}));
// VSCode sends the zip paths to TS using the "zip://" prefix, that TS
// doesn't understand. This layer makes sure to remove the protocol
@ -66,43 +64,33 @@ const moduleWrapper = (tsserver) => {
// Before | ^zip:/c:/foo/bar.zip/package.json
// After | ^/zip//c:/foo/bar.zip/package.json
//
case `vscode <1.61`:
{
str = `^zip:${str}`;
}
break;
case `vscode <1.61`: {
str = `^zip:${str}`;
} break;
case `vscode`:
{
str = `^/zip/${str}`;
}
break;
case `vscode`: {
str = `^/zip/${str}`;
} break;
// To make "go to definition" work,
// We have to resolve the actual file system path from virtual path
// and convert scheme to supported by [vim-rzip](https://github.com/lbrayner/vim-rzip)
case `coc-nvim`:
{
str = normalize(resolved).replace(/\.zip\//, `.zip::`);
str = resolve(`zipfile:${str}`);
}
break;
case `coc-nvim`: {
str = normalize(resolved).replace(/\.zip\//, `.zip::`);
str = resolve(`zipfile:${str}`);
} break;
// Support neovim native LSP and [typescript-language-server](https://github.com/theia-ide/typescript-language-server)
// We have to resolve the actual file system path from virtual path,
// everything else is up to neovim
case `neovim`:
{
str = normalize(resolved).replace(/\.zip\//, `.zip::`);
str = `zipfile:${str}`;
}
break;
case `neovim`: {
str = normalize(resolved).replace(/\.zip\//, `.zip::`);
str = `zipfile:${str}`;
} break;
default:
{
str = `zip:${str}`;
}
break;
default: {
str = `zip:${str}`;
} break;
}
}
}
@ -113,24 +101,22 @@ const moduleWrapper = (tsserver) => {
function fromEditorPath(str) {
switch (hostInfo) {
case `coc-nvim`:
case `neovim`:
{
str = str.replace(/\.zip::/, `.zip/`);
// The path for coc-nvim is in format of /<pwd>/zipfile:/<pwd>/.yarn/...
// So in order to convert it back, we use .* to match all the thing
// before `zipfile:`
return process.platform === `win32` ? str.replace(/^.*zipfile:\//, ``) : str.replace(/^.*zipfile:/, ``);
}
break;
case `neovim`: {
str = str.replace(/\.zip::/, `.zip/`);
// The path for coc-nvim is in format of /<pwd>/zipfile:/<pwd>/.yarn/...
// So in order to convert it back, we use .* to match all the thing
// before `zipfile:`
return process.platform === `win32`
? str.replace(/^.*zipfile:\//, ``)
: str.replace(/^.*zipfile:/, ``);
} break;
case `vscode`:
default:
{
return process.platform === `win32`
? str.replace(/^\^?(zip:|\/zip)\/+/, ``)
: str.replace(/^\^?(zip:|\/zip)\/+/, `/`);
}
break;
default: {
return process.platform === `win32`
? str.replace(/^\^?(zip:|\/zip)\/+/, ``)
: str.replace(/^\^?(zip:|\/zip)\/+/, `/`);
} break;
}
}
@ -142,8 +128,8 @@ const moduleWrapper = (tsserver) => {
// TypeScript already does local loads and if this code is running the user trusts the workspace
// https://github.com/microsoft/vscode/issues/45856
const ConfiguredProject = tsserver.server.ConfiguredProject;
const { enablePluginsWithOptions: originalEnablePluginsWithOptions } = ConfiguredProject.prototype;
ConfiguredProject.prototype.enablePluginsWithOptions = function () {
const {enablePluginsWithOptions: originalEnablePluginsWithOptions} = ConfiguredProject.prototype;
ConfiguredProject.prototype.enablePluginsWithOptions = function() {
this.projectService.allowLocalPluginLoads = true;
return originalEnablePluginsWithOptions.apply(this, arguments);
};
@ -153,12 +139,12 @@ const moduleWrapper = (tsserver) => {
// like an absolute path of ours and normalize it.
const Session = tsserver.server.Session;
const { onMessage: originalOnMessage, send: originalSend } = Session.prototype;
const {onMessage: originalOnMessage, send: originalSend} = Session.prototype;
let hostInfo = `unknown`;
Object.assign(Session.prototype, {
onMessage(/** @type {string} */ message) {
const parsedMessage = JSON.parse(message);
const parsedMessage = JSON.parse(message)
if (
parsedMessage != null &&
@ -167,33 +153,21 @@ const moduleWrapper = (tsserver) => {
typeof parsedMessage.arguments.hostInfo === `string`
) {
hostInfo = parsedMessage.arguments.hostInfo;
if (
hostInfo === `vscode` &&
process.env.VSCODE_IPC_HOOK &&
process.env.VSCODE_IPC_HOOK.match(/Code\/1\.([1-5][0-9]|60)\./)
) {
if (hostInfo === `vscode` && process.env.VSCODE_IPC_HOOK && process.env.VSCODE_IPC_HOOK.match(/Code\/1\.([1-5][0-9]|60)\./)) {
hostInfo += ` <1.61`;
}
}
return originalOnMessage.call(
this,
JSON.stringify(parsedMessage, (key, value) => {
return typeof value === `string` ? fromEditorPath(value) : value;
})
);
return originalOnMessage.call(this, JSON.stringify(parsedMessage, (key, value) => {
return typeof value === `string` ? fromEditorPath(value) : value;
}));
},
send(/** @type {any} */ msg) {
return originalSend.call(
this,
JSON.parse(
JSON.stringify(msg, (key, value) => {
return typeof value === `string` ? toEditorPath(value) : value;
})
)
);
},
return originalSend.call(this, JSON.parse(JSON.stringify(msg, (key, value) => {
return typeof value === `string` ? toEditorPath(value) : value;
})));
}
});
return tsserver;

View File

@ -1,10 +1,10 @@
#!/usr/bin/env node
const { existsSync } = require(`fs`);
const { createRequire, createRequireFromPath } = require(`module`);
const { resolve } = require(`path`);
const {existsSync} = require(`fs`);
const {createRequire, createRequireFromPath} = require(`module`);
const {resolve} = require(`path`);
const relPnpApiPath = '../../../../.pnp.cjs';
const relPnpApiPath = "../../../../.pnp.cjs";
const absPnpApiPath = resolve(__dirname, relPnpApiPath);
const absRequire = (createRequire || createRequireFromPath)(absPnpApiPath);

View File

@ -15,7 +15,7 @@ e2e.scenario({
itName: 'Tests dashboard time zone scenarios',
addScenarioDataSource: false,
addScenarioDashBoard: false,
skipScenario: false,
skipScenario: true,
scenario: () => {
e2e.flows.openDashboard({ uid: '5SdHCasdf' });

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