diff --git a/.betterer.results b/.betterer.results index 9cf91ecd0f9..8666780475f 100644 --- a/.betterer.results +++ b/.betterer.results @@ -9,8 +9,7 @@ exports[`better eslint`] = { [0, 0, 0, "Do not use any type assertions.", "0"] ], "packages/grafana-data/src/dataframe/ArrayDataFrame.ts:5381": [ - [0, 0, 0, "Unexpected any. Specify a different type.", "0"], - [0, 0, 0, "Unexpected any. Specify a different type.", "1"] + [0, 0, 0, "Unexpected any. Specify a different type.", "0"] ], "packages/grafana-data/src/dataframe/CircularDataFrame.ts:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"] diff --git a/packages/grafana-data/src/dataframe/ArrayDataFrame.test.ts b/packages/grafana-data/src/dataframe/ArrayDataFrame.test.ts index 75222ede2d0..624829ba778 100644 --- a/packages/grafana-data/src/dataframe/ArrayDataFrame.test.ts +++ b/packages/grafana-data/src/dataframe/ArrayDataFrame.test.ts @@ -101,4 +101,44 @@ describe('Array DataFrame', () => { } `); }); + + test('Handles first null value', () => { + const f = arrayToDataFrame([null, { id: 'abc' }]); + expect(f).toMatchInlineSnapshot(` + { + "fields": [ + { + "config": {}, + "name": "id", + "type": "string", + "values": [ + null, + "abc", + ], + }, + ], + "length": 2, + } + `); + }); + + test('Handles first undefined value', () => { + const f = arrayToDataFrame([undefined, { id: 'abc' }]); + expect(f).toMatchInlineSnapshot(` + { + "fields": [ + { + "config": {}, + "name": "id", + "type": "string", + "values": [ + undefined, + "abc", + ], + }, + ], + "length": 2, + } + `); + }); }); diff --git a/packages/grafana-data/src/dataframe/ArrayDataFrame.ts b/packages/grafana-data/src/dataframe/ArrayDataFrame.ts index aa06d36c02a..06092b59da9 100644 --- a/packages/grafana-data/src/dataframe/ArrayDataFrame.ts +++ b/packages/grafana-data/src/dataframe/ArrayDataFrame.ts @@ -21,11 +21,13 @@ export class ArrayDataFrame implements DataFrame { } /** - * arrayToDataFrame will convert any array into a DataFrame + * arrayToDataFrame will convert any array into a DataFrame. + * @param source - can be an array of objects or an array of simple values. + * @param names - will be used for ordering of fields. Source needs to be array of objects if names are provided. * * @public */ -export function arrayToDataFrame(source: any[], names?: string[]): DataFrame { +export function arrayToDataFrame(source: Array> | unknown[], names?: string[]): DataFrame { const df: DataFrame = { fields: [], length: source.length, @@ -34,30 +36,46 @@ export function arrayToDataFrame(source: any[], names?: string[]): DataFrame { return df; } + // If names are provided then we assume the source is an array of objects with the names as keys (field names). This + // makes ordering of the fields predictable. if (names) { + if (!isObjectArray(source)) { + throw new Error('source is not an array of objects'); + } + for (const name of names) { df.fields.push( makeFieldFromValues( name, - source.map((v) => v[name]) + source.map((v) => (v ? v[name] : v)) ) ); } return df; } - const first = source.find((v) => v != null); // first not null|undefined - if (first != null) { - if (typeof first === 'object') { - df.fields = Object.keys(first).map((name) => { - return makeFieldFromValues( - name, - source.map((v) => v[name]) - ); - }); - } else { - df.fields.push(makeFieldFromValues(TIME_SERIES_VALUE_FIELD_NAME, source)); - } + const firstDefined = source.find((v) => v); // first not null|undefined + // This means if the source is lots of null/undefined values we throw that away and return empty dataFrame. This is + // different to how we preserve null/undefined values if there is some defined rows. Not sure this inconsistency + // is by design or not. + if (firstDefined === null) { + return df; + } + + // If is an array of objects we use the keys as field names. + if (isObjectArray(source)) { + // We need to do this to please TS. We know source is array of objects and that there is some object in there but + // TS still thinks it can all be undefined|nulls. + const first = source.find((v) => v); + df.fields = Object.keys(first || {}).map((name) => { + return makeFieldFromValues( + name, + source.map((v) => (v ? v[name] : v)) + ); + }); + } else { + // Otherwise source should be an array of simple values, so we create single field data frame. + df.fields.push(makeFieldFromValues(TIME_SERIES_VALUE_FIELD_NAME, source)); } return df; } @@ -67,3 +85,8 @@ function makeFieldFromValues(name: string, values: unknown[]): Field { f.type = guessFieldTypeForField(f) ?? FieldType.other; return f; } + +function isObjectArray(arr: unknown[]): arr is Array | null | undefined> { + const first = arr.find((v) => v); // first not null|undefined + return arr.length > 0 && typeof first === 'object'; +}