From 82f0a58490d9bc4589b7050d1221143f2892a123 Mon Sep 17 00:00:00 2001 From: Haris Rozajac <58232930+harisrozajac@users.noreply.github.com> Date: Mon, 12 May 2025 08:06:02 -0600 Subject: [PATCH] Dashboard: Improve diffing by handling the Infinity value (#105037) * convert infinity values to 0 * convert infinity in V1 transformer * update test --- public/app/core/utils/object.test.ts | 16 ++++++++++++++ public/app/core/utils/object.ts | 22 ++++++++++++++----- .../transformSceneToSaveModel.ts | 2 +- .../transformSceneToSaveModelSchemaV2.ts | 2 +- 4 files changed, 34 insertions(+), 8 deletions(-) diff --git a/public/app/core/utils/object.test.ts b/public/app/core/utils/object.test.ts index c84f7d71d9e..b5d3875ab45 100644 --- a/public/app/core/utils/object.test.ts +++ b/public/app/core/utils/object.test.ts @@ -19,6 +19,22 @@ describe('objects', () => { world: { deeper: 10, arr: [null, 1, 'hello'], + value: -Infinity, + }, + simple: 'A', + }); + expect(value.hello).toBeNull(); + expect(value.world.foo).toBeNull(); + expect(value.bar).toBeUndefined(); + }); + + it('returns a clean copy with Infinity converted to 0', () => { + const copy = sortedDeepCloneWithoutNulls(value, true); + expect(copy).toMatchObject({ + world: { + deeper: 10, + arr: [null, 1, 'hello'], + value: 0, }, simple: 'A', }); diff --git a/public/app/core/utils/object.ts b/public/app/core/utils/object.ts index a51d593b635..7ace78598c4 100644 --- a/public/app/core/utils/object.ts +++ b/public/app/core/utils/object.ts @@ -1,19 +1,29 @@ import { isArray, isPlainObject } from 'lodash'; -/** @returns a deep clone of the object, but with any null value removed */ -export function sortedDeepCloneWithoutNulls(value: T): T { +/** + * @returns A deep clone of the object, but with any null value removed. + * @param value - The object to be cloned and cleaned. + * @param convertInfinity - If true, -Infinity or Infinity is converted to 0. + * This is because Infinity is not a valid JSON value, and sometimes we want to convert it to 0 instead of default null. + */ +export function sortedDeepCloneWithoutNulls(value: T, convertInfinity?: boolean): T { if (isArray(value)) { - return value.map(sortedDeepCloneWithoutNulls) as unknown as T; + return value.map((item) => sortedDeepCloneWithoutNulls(item, convertInfinity)) as unknown as T; } if (isPlainObject(value)) { return Object.keys(value as { [key: string]: any }) .sort() .reduce((acc: any, key) => { const v = (value as any)[key]; - // Remove null values and also -Infinity which is not a valid JSON value - if (v != null && v !== -Infinity) { - acc[key] = sortedDeepCloneWithoutNulls(v); + // Remove null values + if (v != null) { + acc[key] = sortedDeepCloneWithoutNulls(v, convertInfinity); } + + if (convertInfinity && (v === Infinity || v === -Infinity)) { + acc[key] = 0; + } + return acc; }, {}); } diff --git a/public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.ts b/public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.ts index b9f280b4a42..68633912597 100644 --- a/public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.ts +++ b/public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.ts @@ -146,7 +146,7 @@ export function transformSceneToSaveModel(scene: DashboardScene, isSnapshot = fa scopeMeta: state.scopeMeta, }; - return sortedDeepCloneWithoutNulls(dashboard); + return sortedDeepCloneWithoutNulls(dashboard, true); } export function gridItemToPanel(gridItem: DashboardGridItem, isSnapshot = false): Panel { diff --git a/public/app/features/dashboard-scene/serialization/transformSceneToSaveModelSchemaV2.ts b/public/app/features/dashboard-scene/serialization/transformSceneToSaveModelSchemaV2.ts index 426c1f5c7bc..f8b332bd2e9 100644 --- a/public/app/features/dashboard-scene/serialization/transformSceneToSaveModelSchemaV2.ts +++ b/public/app/features/dashboard-scene/serialization/transformSceneToSaveModelSchemaV2.ts @@ -117,7 +117,7 @@ export function transformSceneToSaveModelSchemaV2(scene: DashboardScene, isSnaps try { // validateDashboardSchemaV2 will throw an error if the dashboard is not valid if (validateDashboardSchemaV2(dashboardSchemaV2)) { - return sortedDeepCloneWithoutNulls(dashboardSchemaV2); + return sortedDeepCloneWithoutNulls(dashboardSchemaV2, true); } // should never reach this point, validation should throw an error throw new Error('Error we could transform the dashboard to schema v2: ' + dashboardSchemaV2);