From 88b59ae3c72eb37ffbe3067eda2927fd94794ed8 Mon Sep 17 00:00:00 2001 From: Ryan McKinley Date: Sat, 14 Nov 2020 18:22:42 -0800 Subject: [PATCH] DataFrames: add utility function to check if structure has changed (#29006) * add common flag for knowing if the structure changes * remove property * fix test * fix test * update comment * fix jsdoc comments --- .../src/dataframe/frameComparisons.test.ts | 95 +++++++++++++++++++ .../src/dataframe/frameComparisons.ts | 67 +++++++++++++ packages/grafana-data/src/dataframe/index.ts | 1 + packages/grafana-runtime/src/services/live.ts | 1 - 4 files changed, 163 insertions(+), 1 deletion(-) create mode 100644 packages/grafana-data/src/dataframe/frameComparisons.test.ts create mode 100644 packages/grafana-data/src/dataframe/frameComparisons.ts diff --git a/packages/grafana-data/src/dataframe/frameComparisons.test.ts b/packages/grafana-data/src/dataframe/frameComparisons.test.ts new file mode 100644 index 00000000000..de320fca43c --- /dev/null +++ b/packages/grafana-data/src/dataframe/frameComparisons.test.ts @@ -0,0 +1,95 @@ +import { FieldType } from '../types/dataFrame'; +import { compareDataFrameStructures, compareArrayValues } from './frameComparisons'; +import { toDataFrame } from './processDataFrame'; + +describe('test comparisons', () => { + const frameA = toDataFrame({ + fields: [ + { name: 'time', type: FieldType.time, values: [100, 200, 300] }, + { name: 'name', type: FieldType.string, values: ['a', 'b', 'c'] }, + { name: 'value', type: FieldType.number, values: [1, 2, 3] }, + ], + }); + const frameB = toDataFrame({ + fields: [ + { name: 'time', type: FieldType.time, values: [100, 200, 300] }, + { + name: 'value', + type: FieldType.number, + values: [1, 2, 3], + config: { + decimals: 4, + }, + }, + ], + }); + const field0 = frameB.fields[0]; + const field1 = frameB.fields[1]; + + it('should support null/undefined without crash', () => { + expect(compareDataFrameStructures(frameA, frameA)).toBeTruthy(); + expect(compareDataFrameStructures(frameA, { ...frameA })).toBeTruthy(); + expect(compareDataFrameStructures(frameA, frameB)).toBeFalsy(); + expect(compareDataFrameStructures(frameA, null as any)).toBeFalsy(); + expect(compareDataFrameStructures(undefined as any, frameA)).toBeFalsy(); + + expect(compareArrayValues([frameA], [frameA], compareDataFrameStructures)).toBeTruthy(); + expect(compareArrayValues([frameA], null as any, compareDataFrameStructures)).toBeFalsy(); + expect(compareArrayValues(null as any, [frameA], compareDataFrameStructures)).toBeFalsy(); + }); + + it('name change and field copy is not a structure change', () => { + expect(compareDataFrameStructures(frameB, { ...frameB, name: 'AA' })).toBeTruthy(); + expect(compareDataFrameStructures(frameB, { ...frameB, fields: [field0, field1] })).toBeTruthy(); + }); + + it('changing type should change the config', () => { + expect( + compareDataFrameStructures(frameB, { + ...frameB, + fields: [ + field0, + { + ...field1, + type: FieldType.trace, // Change the type + }, + ], + }) + ).toBeFalsy(); + }); + + it('full copy of config will not change structure', () => { + expect( + compareDataFrameStructures(frameB, { + ...frameB, + fields: [ + field0, + { + ...field1, + config: { + ...field1.config, // no change + }, + }, + ], + }) + ).toBeTruthy(); // no change + }); + + it('adding an additional config field', () => { + expect( + compareDataFrameStructures(frameB, { + ...frameB, + fields: [ + field0, + { + ...field1, + config: { + ...field1.config, + unit: 'rpm', + }, + }, + ], + }) + ).toBeFalsy(); + }); +}); diff --git a/packages/grafana-data/src/dataframe/frameComparisons.ts b/packages/grafana-data/src/dataframe/frameComparisons.ts new file mode 100644 index 00000000000..fb5fabda563 --- /dev/null +++ b/packages/grafana-data/src/dataframe/frameComparisons.ts @@ -0,0 +1,67 @@ +import { DataFrame } from '../types/dataFrame'; + +/** + * Returns true if both frames have the same list of fields and configs. + * Field may have diferent names, labels and values but share the same structure + * + * To compare multiple frames use: + * ``` + * areArraysEqual(a, b, framesHaveSameStructure); + * ``` + * NOTE: this does a shallow check on the FieldConfig properties, when using the query + * editor, this should be sufficient, however if applicaitons are mutating properties + * deep in the FieldConfig this will not recognize a change + * + * @beta + */ +export function compareDataFrameStructures(a: DataFrame, b: DataFrame): boolean { + if (a === b) { + return true; + } + if (a?.fields?.length !== b?.fields?.length) { + return false; + } + for (let i = 0; i < a.fields.length; i++) { + const fA = a.fields[i]; + const fB = b.fields[i]; + if (fA.type !== fB.type) { + return false; + } + const cfgA = fA.config as any; + const cfgB = fB.config as any; + + const keys = Object.keys(cfgA); + if (keys.length !== Object.keys(cfgB).length) { + return false; + } + for (const key of keys) { + if (!cfgB.hasOwnProperty(key)) { + return false; + } + if (cfgA[key] !== cfgB[key]) { + return false; + } + } + } + return true; +} + +/** + * Check if all values in two arrays match the compare funciton + * + * @beta + */ +export function compareArrayValues(a: T[], b: T[], cmp: (a: T, b: T) => boolean) { + if (a === b) { + return true; + } + if (a?.length !== b?.length) { + return false; + } + for (let i = 0; i < a.length; i++) { + if (!cmp(a[i], b[i])) { + return false; + } + } + return true; +} diff --git a/packages/grafana-data/src/dataframe/index.ts b/packages/grafana-data/src/dataframe/index.ts index dbdd20c5cd9..05064d4367d 100644 --- a/packages/grafana-data/src/dataframe/index.ts +++ b/packages/grafana-data/src/dataframe/index.ts @@ -6,3 +6,4 @@ export * from './processDataFrame'; export * from './dimensions'; export * from './ArrowDataFrame'; export * from './ArrayDataFrame'; +export * from './frameComparisons'; diff --git a/packages/grafana-runtime/src/services/live.ts b/packages/grafana-runtime/src/services/live.ts index db01d7ab0ed..76ec6b6ff8e 100644 --- a/packages/grafana-runtime/src/services/live.ts +++ b/packages/grafana-runtime/src/services/live.ts @@ -43,6 +43,5 @@ export const setGrafanaLiveSrv = (instance: GrafanaLiveSrv) => { * server side events and streams * * @alpha -- experimental - * @public */ export const getGrafanaLiveSrv = (): GrafanaLiveSrv => singletonInstance;