From 3dee34c009c1351846a6efda69506314fa7ffcb6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 8 Nov 2021 13:18:14 +0100 Subject: [PATCH] PanelEdit: Show when field options have override rules or data config that overrides the default (#40250) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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 * Fixed ts issues * review feedback changes * skipp broken e2e test Co-authored-by: Hugo Häggmark --- .yarn/sdks/eslint/bin/eslint.js | 8 +- .yarn/sdks/eslint/lib/api.js | 8 +- .yarn/sdks/integrations.yml | 1 + .yarn/sdks/prettier/index.js | 8 +- .yarn/sdks/stylelint/bin/stylelint.js | 8 +- .yarn/sdks/stylelint/lib/index.js | 8 +- .yarn/sdks/typescript/lib/tsc.js | 8 +- .yarn/sdks/typescript/lib/tsserver.js | 134 +++++++----------- .yarn/sdks/typescript/lib/tsserverlibrary.js | 134 +++++++----------- .yarn/sdks/typescript/lib/typescript.js | 8 +- e2e/suite1/specs/dashboard-time-zone.spec.ts | 2 +- .../PanelEditor/OptionsPaneItemDescriptor.tsx | 22 ++- .../PanelEditor/OptionsPaneItemOverrides.tsx | 48 +++++++ .../PanelEditor/OptionsPaneOptions.test.tsx | 45 ++++++ .../PanelEditor/getVizualizationOptions.tsx | 4 +- .../PanelEditor/state/getOptionOverrides.ts | 49 +++++++ .../dashboard/components/PanelEditor/types.ts | 7 + 17 files changed, 309 insertions(+), 193 deletions(-) create mode 100644 public/app/features/dashboard/components/PanelEditor/OptionsPaneItemOverrides.tsx create mode 100644 public/app/features/dashboard/components/PanelEditor/state/getOptionOverrides.ts diff --git a/.yarn/sdks/eslint/bin/eslint.js b/.yarn/sdks/eslint/bin/eslint.js index 774cf54870e..4d327a49a06 100755 --- a/.yarn/sdks/eslint/bin/eslint.js +++ b/.yarn/sdks/eslint/bin/eslint.js @@ -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); diff --git a/.yarn/sdks/eslint/lib/api.js b/.yarn/sdks/eslint/lib/api.js index 0f2216b364a..97a052442a8 100644 --- a/.yarn/sdks/eslint/lib/api.js +++ b/.yarn/sdks/eslint/lib/api.js @@ -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); diff --git a/.yarn/sdks/integrations.yml b/.yarn/sdks/integrations.yml index aa9d0d0ad81..401be99859b 100644 --- a/.yarn/sdks/integrations.yml +++ b/.yarn/sdks/integrations.yml @@ -2,4 +2,5 @@ # Manual changes might be lost! integrations: + - vim - vscode diff --git a/.yarn/sdks/prettier/index.js b/.yarn/sdks/prettier/index.js index f5040146272..f6882d80972 100755 --- a/.yarn/sdks/prettier/index.js +++ b/.yarn/sdks/prettier/index.js @@ -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); diff --git a/.yarn/sdks/stylelint/bin/stylelint.js b/.yarn/sdks/stylelint/bin/stylelint.js index ebc40d7412a..04ffaf8eefa 100755 --- a/.yarn/sdks/stylelint/bin/stylelint.js +++ b/.yarn/sdks/stylelint/bin/stylelint.js @@ -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); diff --git a/.yarn/sdks/stylelint/lib/index.js b/.yarn/sdks/stylelint/lib/index.js index 09d3a62c27f..1b0b443f818 100644 --- a/.yarn/sdks/stylelint/lib/index.js +++ b/.yarn/sdks/stylelint/lib/index.js @@ -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); diff --git a/.yarn/sdks/typescript/lib/tsc.js b/.yarn/sdks/typescript/lib/tsc.js index 51dc5720100..16042d01d4f 100644 --- a/.yarn/sdks/typescript/lib/tsc.js +++ b/.yarn/sdks/typescript/lib/tsc.js @@ -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); diff --git a/.yarn/sdks/typescript/lib/tsserver.js b/.yarn/sdks/typescript/lib/tsserver.js index d26f9c85c96..71e35cf6119 100644 --- a/.yarn/sdks/typescript/lib/tsserver.js +++ b/.yarn/sdks/typescript/lib/tsserver.js @@ -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 //zipfile://.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 //zipfile://.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; diff --git a/.yarn/sdks/typescript/lib/tsserverlibrary.js b/.yarn/sdks/typescript/lib/tsserverlibrary.js index d0516a25036..7a2d65ea220 100644 --- a/.yarn/sdks/typescript/lib/tsserverlibrary.js +++ b/.yarn/sdks/typescript/lib/tsserverlibrary.js @@ -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 //zipfile://.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 //zipfile://.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; diff --git a/.yarn/sdks/typescript/lib/typescript.js b/.yarn/sdks/typescript/lib/typescript.js index 6185deec60f..cbdbf1500fb 100644 --- a/.yarn/sdks/typescript/lib/typescript.js +++ b/.yarn/sdks/typescript/lib/typescript.js @@ -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); diff --git a/e2e/suite1/specs/dashboard-time-zone.spec.ts b/e2e/suite1/specs/dashboard-time-zone.spec.ts index bc492e1e8e8..3ea09955b55 100644 --- a/e2e/suite1/specs/dashboard-time-zone.spec.ts +++ b/e2e/suite1/specs/dashboard-time-zone.spec.ts @@ -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' }); diff --git a/public/app/features/dashboard/components/PanelEditor/OptionsPaneItemDescriptor.tsx b/public/app/features/dashboard/components/PanelEditor/OptionsPaneItemDescriptor.tsx index ba34c989657..a8f2ef53233 100644 --- a/public/app/features/dashboard/components/PanelEditor/OptionsPaneItemDescriptor.tsx +++ b/public/app/features/dashboard/components/PanelEditor/OptionsPaneItemDescriptor.tsx @@ -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 ( + + ); } const categories: React.ReactNode[] = []; @@ -47,6 +55,7 @@ export class OptionsPaneItemDescriptor { return ( ); } @@ -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}`; diff --git a/public/app/features/dashboard/components/PanelEditor/OptionsPaneItemOverrides.tsx b/public/app/features/dashboard/components/PanelEditor/OptionsPaneItemOverrides.tsx new file mode 100644 index 00000000000..7e086908788 --- /dev/null +++ b/public/app/features/dashboard/components/PanelEditor/OptionsPaneItemOverrides.tsx @@ -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 ( +
+ {overrides.map((override, index) => ( + +
+ + ))} +
+ ); +} + +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, + }), + }; +}; diff --git a/public/app/features/dashboard/components/PanelEditor/OptionsPaneOptions.test.tsx b/public/app/features/dashboard/components/PanelEditor/OptionsPaneOptions.test.tsx index 6287fcc8e64..c95fc83db16 100644 --- a/public/app/features/dashboard/components/PanelEditor/OptionsPaneOptions.test.tsx +++ b/public/app/features/dashboard/components/PanelEditor/OptionsPaneOptions.test.tsx @@ -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(); + }); }); diff --git a/public/app/features/dashboard/components/PanelEditor/getVizualizationOptions.tsx b/public/app/features/dashboard/components/PanelEditor/getVizualizationOptions.tsx index f10d3104ccf..59b638b49cf 100644 --- a/public/app/features/dashboard/components/PanelEditor/getVizualizationOptions.tsx +++ b/public/app/features/dashboard/components/PanelEditor/getVizualizationOptions.tsx @@ -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( diff --git a/public/app/features/dashboard/components/PanelEditor/state/getOptionOverrides.ts b/public/app/features/dashboard/components/PanelEditor/state/getOptionOverrides.ts new file mode 100644 index 00000000000..7160401a861 --- /dev/null +++ b/public/app/features/dashboard/components/PanelEditor/state/getOptionOverrides.ts @@ -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; +} diff --git a/public/app/features/dashboard/components/PanelEditor/types.ts b/public/app/features/dashboard/components/PanelEditor/types.ts index 7420de836dc..7ca735ded5b 100644 --- a/public/app/features/dashboard/components/PanelEditor/types.ts +++ b/public/app/features/dashboard/components/PanelEditor/types.ts @@ -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; +}