From 0e419e9af1e3f658c3b2ccf4aadafc699297f08b Mon Sep 17 00:00:00 2001 From: Ryan McKinley Date: Tue, 9 Nov 2021 07:55:42 -0800 Subject: [PATCH] Dashboard schema: strip null values from save model (#41385) --- public/app/core/utils/object.test.ts | 28 +++++++++++++++++++ public/app/core/utils/object.ts | 20 +++++++++++++ public/app/core/utils/sort_by_keys.ts | 17 ----------- .../dashboard/state/DashboardModel.test.ts | 4 ++- .../dashboard/state/DashboardModel.ts | 4 +-- 5 files changed, 53 insertions(+), 20 deletions(-) create mode 100644 public/app/core/utils/object.test.ts create mode 100644 public/app/core/utils/object.ts delete mode 100644 public/app/core/utils/sort_by_keys.ts diff --git a/public/app/core/utils/object.test.ts b/public/app/core/utils/object.test.ts new file mode 100644 index 00000000000..8e27feccc6b --- /dev/null +++ b/public/app/core/utils/object.test.ts @@ -0,0 +1,28 @@ +import { sortedDeepCloneWithoutNulls } from './object'; + +describe('objects', () => { + const value = { + hello: null, + world: { + deeper: 10, + foo: null, + arr: [null, 1, 'hello'], + }, + bar: undefined, + simple: 'A', + }; + + it('returns a clean copy', () => { + const copy = sortedDeepCloneWithoutNulls(value); + expect(copy).toMatchObject({ + world: { + deeper: 10, + arr: [null, 1, 'hello'], + }, + simple: 'A', + }); + expect(value.hello).toBeNull(); + expect(value.world.foo).toBeNull(); + expect(value.bar).toBeUndefined(); + }); +}); diff --git a/public/app/core/utils/object.ts b/public/app/core/utils/object.ts new file mode 100644 index 00000000000..bec0eaea6f8 --- /dev/null +++ b/public/app/core/utils/object.ts @@ -0,0 +1,20 @@ +import { isArray, isPlainObject } from 'lodash'; + +/** @returns a deep clone of the object, but with any null value removed */ +export function sortedDeepCloneWithoutNulls(value: T): T { + if (isArray(value)) { + return (value.map(sortedDeepCloneWithoutNulls) as unknown) as T; + } + if (isPlainObject(value)) { + return Object.keys(value) + .sort() + .reduce((acc: any, key) => { + const v = (value as any)[key]; + if (v != null) { + acc[key] = sortedDeepCloneWithoutNulls(v); + } + return acc; + }, {}); + } + return value; +} diff --git a/public/app/core/utils/sort_by_keys.ts b/public/app/core/utils/sort_by_keys.ts deleted file mode 100644 index 1169e450c7a..00000000000 --- a/public/app/core/utils/sort_by_keys.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { isArray, isPlainObject, keys } from 'lodash'; - -export default function sortByKeys(input: any): any { - if (isArray(input)) { - return input.map(sortByKeys); - } - - if (isPlainObject(input)) { - const sortedObject: any = {}; - for (const key of keys(input).sort()) { - sortedObject[key] = sortByKeys(input[key]); - } - return sortedObject; - } - - return input; -} diff --git a/public/app/features/dashboard/state/DashboardModel.test.ts b/public/app/features/dashboard/state/DashboardModel.test.ts index 7460e7ff29d..e321af4c294 100644 --- a/public/app/features/dashboard/state/DashboardModel.test.ts +++ b/public/app/features/dashboard/state/DashboardModel.test.ts @@ -56,11 +56,13 @@ describe('DashboardModel', () => { describe('getSaveModelClone', () => { it('should sort keys', () => { const model = new DashboardModel({}); + model.autoUpdate = null; + const saveModel = model.getSaveModelClone(); const keys = _keys(saveModel); expect(keys[0]).toBe('annotations'); - expect(keys[1]).toBe('autoUpdate'); + expect(keys[1]).toBe('editable'); }); it('should remove add panel panels', () => { diff --git a/public/app/features/dashboard/state/DashboardModel.ts b/public/app/features/dashboard/state/DashboardModel.ts index 164e17b6010..28bc2692997 100644 --- a/public/app/features/dashboard/state/DashboardModel.ts +++ b/public/app/features/dashboard/state/DashboardModel.ts @@ -18,7 +18,6 @@ import { DEFAULT_ANNOTATION_COLOR } from '@grafana/ui'; import { GRID_CELL_HEIGHT, GRID_CELL_VMARGIN, GRID_COLUMN_COUNT, REPEAT_DIR_VERTICAL } from 'app/core/constants'; // Utils & Services import { contextSrv } from 'app/core/services/context_srv'; -import sortByKeys from 'app/core/utils/sort_by_keys'; // Types import { GridPos, PanelModel } from './PanelModel'; import { DashboardMigrator } from './DashboardMigrator'; @@ -47,6 +46,7 @@ import { getTimeSrv } from '../services/TimeSrv'; import { mergePanels, PanelMergeInfo } from '../utils/panelMerge'; import { isOnTheSameGridRow } from './utils'; import { RefreshEvent, TimeRangeUpdatedEvent } from '@grafana/runtime'; +import { sortedDeepCloneWithoutNulls } from 'app/core/utils/object'; import { Subscription } from 'rxjs'; import { appEvents } from '../../../core/core'; import { @@ -264,7 +264,7 @@ export class DashboardModel { copy.panels = this.getPanelSaveModels(); // sort by keys - copy = sortByKeys(copy); + copy = sortedDeepCloneWithoutNulls(copy); copy.getVariables = () => { return copy.templating.list; };