diff --git a/public/app/features/dashboard/components/PanelEditor/getVizualizationOptions.tsx b/public/app/features/dashboard/components/PanelEditor/getVizualizationOptions.tsx index 5222dd8156c..f0c3ce7a1c8 100644 --- a/public/app/features/dashboard/components/PanelEditor/getVizualizationOptions.tsx +++ b/public/app/features/dashboard/components/PanelEditor/getVizualizationOptions.tsx @@ -1,9 +1,9 @@ import React from 'react'; import { StandardEditorContext, VariableSuggestionsScope } from '@grafana/data'; -import { get as lodashGet, set as lodashSet } from 'lodash'; +import { get as lodashGet } from 'lodash'; import { getDataLinksVariableSuggestions } from 'app/features/panel/panellinks/link_srv'; import { OptionPaneRenderProps } from './types'; -import { updateDefaultFieldConfigValue } from './utils'; +import { updateDefaultFieldConfigValue, setOptionImmutably } from './utils'; import { OptionsPaneItemDescriptor } from './OptionsPaneItemDescriptor'; import { OptionsPaneCategoryDescriptor } from './OptionsPaneCategoryDescriptor'; @@ -54,7 +54,7 @@ export function getVizualizationOptions(props: OptionPaneRenderProps): OptionsPa description: pluginOption.description, render: function renderEditor() { const onChange = (value: any) => { - const newOptions = lodashSet({ ...currentOptions }, pluginOption.path, value); + const newOptions = setOptionImmutably(currentOptions, pluginOption.path, value); onPanelOptionsChanged(newOptions); }; diff --git a/public/app/features/dashboard/components/PanelEditor/utils.test.ts b/public/app/features/dashboard/components/PanelEditor/utils.test.ts index 4dc7de9547b..c495c4c84e3 100644 --- a/public/app/features/dashboard/components/PanelEditor/utils.test.ts +++ b/public/app/features/dashboard/components/PanelEditor/utils.test.ts @@ -1,5 +1,5 @@ import { FieldConfig, FieldConfigSource, PanelPlugin, standardFieldConfigEditorRegistry } from '@grafana/data'; -import { supportsDataQuery, updateDefaultFieldConfigValue } from './utils'; +import { setOptionImmutably, supportsDataQuery, updateDefaultFieldConfigValue } from './utils'; describe('standardFieldConfigEditorRegistry', () => { const dummyConfig: FieldConfig = { @@ -86,3 +86,26 @@ describe('updateDefaultFieldConfigValue', () => { } ); }); + +describe('setOptionImmutably', () => { + it.each` + source | path | value | expected + ${{}} | ${'a'} | ${1} | ${{ a: 1 }} + ${{}} | ${'a.b.c'} | ${[1, 2]} | ${{ a: { b: { c: [1, 2] } } }} + ${{ a: {} }} | ${'a.b.c'} | ${[1, 2]} | ${{ a: { b: { c: [1, 2] } } }} + ${{ b: {} }} | ${'a.b.c'} | ${[1, 2]} | ${{ a: { b: { c: [1, 2] } }, b: {} }} + ${{ a: { b: { c: 3 } } }} | ${'a.b.c'} | ${[1, 2]} | ${{ a: { b: { c: [1, 2] } } }} + `('numeric-like text mapping, value:${value', ({ source, path, value, expected }) => { + expect(setOptionImmutably(source, path, value)).toEqual(expected); + }); + + it('does not mutate object under a path', () => { + const input = { a: { b: { c: { d: 1 }, e: { f: 1 } } }, aa: 1 }; + const result = setOptionImmutably(input, 'a.b.c', { d: 2 }); + expect(input.a).not.toEqual(result.a); + expect(input.aa).toEqual(result.aa); + expect(input.a.b).not.toEqual(result.a.b); + expect(input.a.b.c).not.toEqual(result.a.b.c); + expect(input.a.b.e).toEqual(result.a.b.e); + }); +}); diff --git a/public/app/features/dashboard/components/PanelEditor/utils.ts b/public/app/features/dashboard/components/PanelEditor/utils.ts index 52bd49416b4..a6e4265857f 100644 --- a/public/app/features/dashboard/components/PanelEditor/utils.ts +++ b/public/app/features/dashboard/components/PanelEditor/utils.ts @@ -1,5 +1,5 @@ import { CSSProperties } from 'react'; -import { set as lodashSet, omit } from 'lodash'; +import { omit } from 'lodash'; import { FieldConfigSource, PanelPlugin } from '@grafana/data'; import { PanelModel } from '../../state/PanelModel'; import { DisplayMode } from './types'; @@ -41,22 +41,22 @@ export const updateDefaultFieldConfigValue = ( isCustom?: boolean ) => { let defaults = { ...config.defaults }; - const remove = value === undefined || value === null || ''; + const remove = value == null || value === ''; if (isCustom) { if (defaults.custom) { if (remove) { defaults.custom = omit(defaults.custom, name); } else { - defaults.custom = lodashSet({ ...defaults.custom }, name, value); + defaults.custom = setOptionImmutably(defaults.custom, name, value); } } else if (!remove) { - defaults.custom = lodashSet({ ...defaults.custom }, name, value); + defaults.custom = setOptionImmutably(defaults.custom, name, value); } } else if (remove) { defaults = omit(defaults, name); } else { - defaults = lodashSet({ ...defaults }, name, value); + defaults = setOptionImmutably(defaults, name, value); } return { @@ -64,3 +64,21 @@ export const updateDefaultFieldConfigValue = ( defaults, }; }; + +export function setOptionImmutably(options: T, path: string | string[], value: any): T { + const splat = !Array.isArray(path) ? path.split('.') : path; + + const key = splat.shift()!; + + if (!splat.length) { + return { ...options, [key]: value }; + } + + let current = (options as Record)[key]; + + if (current == null || typeof current !== 'object') { + current = {}; + } + + return { ...options, [key]: setOptionImmutably(current, splat, value) }; +}