mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
TablePanel: Prevents crash when data contains mixed data formats (#20202)
* Fix: Prevents crash when table receives mixed data formats Fixes #20075 * Tests: Adds tests for data format reducers * Refactor: Updates after PR comments * Refactor: Missed a couple of places where filtering was needed
This commit is contained in:
parent
df6d8851d0
commit
89c553cfe6
@ -133,6 +133,18 @@ describe('mergeTables', () => {
|
|||||||
}),
|
}),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const multipleTablesButWithMixedDataFormat = [
|
||||||
|
new TableModel({
|
||||||
|
type: 'table',
|
||||||
|
columns: [{ text: 'Time' }, { text: 'Label Key 1' }, { text: 'Value' }],
|
||||||
|
rows: [[time, 'Label Value 1', 42]],
|
||||||
|
}),
|
||||||
|
({
|
||||||
|
target: 'series1',
|
||||||
|
datapoints: [[12.12, time], [14.44, time + 1]],
|
||||||
|
} as any) as TableModel,
|
||||||
|
];
|
||||||
|
|
||||||
it('should return the single table as is', () => {
|
it('should return the single table as is', () => {
|
||||||
const table = mergeTablesIntoModel(new TableModel(), singleTable);
|
const table = mergeTablesIntoModel(new TableModel(), singleTable);
|
||||||
expect(table.columns.length).toBe(3);
|
expect(table.columns.length).toBe(3);
|
||||||
@ -196,4 +208,24 @@ describe('mergeTables', () => {
|
|||||||
expect(table.rows[1][4]).toBeUndefined();
|
expect(table.rows[1][4]).toBeUndefined();
|
||||||
expect(table.rows[1][5]).toBe(7);
|
expect(table.rows[1][5]).toBe(7);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should not crash if tables array contain non table data', () => {
|
||||||
|
expect(() => mergeTablesIntoModel(new TableModel(), ...multipleTablesButWithMixedDataFormat)).not.toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should should return the single table as is if tables array also contains non table data', () => {
|
||||||
|
const table = mergeTablesIntoModel(new TableModel(), ...multipleTablesButWithMixedDataFormat);
|
||||||
|
expect(table.columns.length).toBe(3);
|
||||||
|
expect(table.columns[0].text).toBe('Time');
|
||||||
|
expect(table.columns[1].text).toBe('Label Key 1');
|
||||||
|
expect(table.columns[2].text).toBe('Value');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return 1 row for a single table if tables array also contains non table data', () => {
|
||||||
|
const table = mergeTablesIntoModel(new TableModel(), ...multipleTablesButWithMixedDataFormat);
|
||||||
|
expect(table.rows.length).toBe(1);
|
||||||
|
expect(table.rows[0][0]).toBe(time);
|
||||||
|
expect(table.rows[0][1]).toBe('Label Value 1');
|
||||||
|
expect(table.rows[0][2]).toBe(42);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -99,11 +99,14 @@ export function mergeTablesIntoModel(dst?: TableModel, ...tables: TableModel[]):
|
|||||||
return model;
|
return model;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Filter out any tables that are not of TableData format
|
||||||
|
const tableDataTables = tables.filter(table => !!table.columns);
|
||||||
|
|
||||||
// Track column indexes of union: name -> index
|
// Track column indexes of union: name -> index
|
||||||
const columnNames: { [key: string]: any } = {};
|
const columnNames: { [key: string]: any } = {};
|
||||||
|
|
||||||
// Union of all non-value columns
|
// Union of all non-value columns
|
||||||
const columnsUnion = tables.slice().reduce(
|
const columnsUnion = tableDataTables.slice().reduce(
|
||||||
(acc, series) => {
|
(acc, series) => {
|
||||||
series.columns.forEach(col => {
|
series.columns.forEach(col => {
|
||||||
const { text } = col;
|
const { text } = col;
|
||||||
@ -120,10 +123,10 @@ export function mergeTablesIntoModel(dst?: TableModel, ...tables: TableModel[]):
|
|||||||
// Map old column index to union index per series, e.g.,
|
// Map old column index to union index per series, e.g.,
|
||||||
// given columnNames {A: 0, B: 1} and
|
// given columnNames {A: 0, B: 1} and
|
||||||
// data [{columns: [{ text: 'A' }]}, {columns: [{ text: 'B' }]}] => [[0], [1]]
|
// data [{columns: [{ text: 'A' }]}, {columns: [{ text: 'B' }]}] => [[0], [1]]
|
||||||
const columnIndexMapper = tables.map(series => series.columns.map(col => columnNames[col.text]));
|
const columnIndexMapper = tableDataTables.map(series => series.columns.map(col => columnNames[col.text]));
|
||||||
|
|
||||||
// Flatten rows of all series and adjust new column indexes
|
// Flatten rows of all series and adjust new column indexes
|
||||||
const flattenedRows = tables.reduce(
|
const flattenedRows = tableDataTables.reduce(
|
||||||
(acc, series, seriesIndex) => {
|
(acc, series, seriesIndex) => {
|
||||||
const mapper = columnIndexMapper[seriesIndex];
|
const mapper = columnIndexMapper[seriesIndex];
|
||||||
series.rows.forEach(row => {
|
series.rows.forEach(row => {
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { transformers, transformDataToTable } from '../transformers';
|
import { tableDataFormatFilterer, timeSeriesFormatFilterer, transformDataToTable, transformers } from '../transformers';
|
||||||
|
|
||||||
describe('when transforming time series table', () => {
|
describe('when transforming time series table', () => {
|
||||||
let table: any;
|
let table: any;
|
||||||
@ -300,4 +300,224 @@ describe('when transforming time series table', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('given time series and table data', () => {
|
||||||
|
const time = new Date().getTime();
|
||||||
|
const data = [
|
||||||
|
{
|
||||||
|
target: 'series1',
|
||||||
|
datapoints: [[12.12, time], [14.44, time + 1]],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
type: 'time',
|
||||||
|
text: 'Time',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'mean',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
type: 'table',
|
||||||
|
rows: [[time, 13.13], [time + 1, 26.26]],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
describe('timeseries_to_rows', () => {
|
||||||
|
const panel = {
|
||||||
|
transform: 'timeseries_to_rows',
|
||||||
|
sort: { col: 0, desc: true },
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
table = transformDataToTable(data, panel);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return 2 rows', () => {
|
||||||
|
expect(table.rows.length).toBe(2);
|
||||||
|
expect(table.rows[0][1]).toBe('series1');
|
||||||
|
expect(table.rows[1][1]).toBe('series1');
|
||||||
|
expect(table.rows[0][2]).toBe(12.12);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return 3 columns', () => {
|
||||||
|
expect(table.columns.length).toBe(3);
|
||||||
|
expect(table.columns[0].text).toBe('Time');
|
||||||
|
expect(table.columns[1].text).toBe('Metric');
|
||||||
|
expect(table.columns[2].text).toBe('Value');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('timeseries_to_columns', () => {
|
||||||
|
const panel = {
|
||||||
|
transform: 'timeseries_to_columns',
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
table = transformDataToTable(data, panel);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return 2 columns', () => {
|
||||||
|
expect(table.columns.length).toBe(2);
|
||||||
|
expect(table.columns[0].text).toBe('Time');
|
||||||
|
expect(table.columns[1].text).toBe('series1');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return 2 rows', () => {
|
||||||
|
expect(table.rows.length).toBe(2);
|
||||||
|
expect(table.rows[0][1]).toBe(12.12);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be undefined when no value for timestamp', () => {
|
||||||
|
expect(table.rows[1][2]).toBe(undefined);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('timeseries_aggregations', () => {
|
||||||
|
const panel = {
|
||||||
|
transform: 'timeseries_aggregations',
|
||||||
|
sort: { col: 0, desc: true },
|
||||||
|
columns: [{ text: 'Max', value: 'max' }, { text: 'Min', value: 'min' }],
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
table = transformDataToTable(data, panel);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return 1 row', () => {
|
||||||
|
expect(table.rows.length).toBe(1);
|
||||||
|
expect(table.rows[0][0]).toBe('series1');
|
||||||
|
expect(table.rows[0][1]).toBe(14.44);
|
||||||
|
expect(table.rows[0][2]).toBe(12.12);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return 2 columns', () => {
|
||||||
|
expect(table.columns.length).toBe(3);
|
||||||
|
expect(table.columns[0].text).toBe('Metric');
|
||||||
|
expect(table.columns[1].text).toBe('Max');
|
||||||
|
expect(table.columns[2].text).toBe('Min');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('timeSeriesFormatFilterer', () => {
|
||||||
|
describe('when called with an object that contains datapoints property', () => {
|
||||||
|
it('then it should return same object in array', () => {
|
||||||
|
const data: any = { datapoints: [] };
|
||||||
|
|
||||||
|
const result = timeSeriesFormatFilterer(data);
|
||||||
|
|
||||||
|
expect(result).toEqual([data]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when called with an object that does not contain datapoints property', () => {
|
||||||
|
it('then it should return empty array', () => {
|
||||||
|
const data: any = { prop: [] };
|
||||||
|
|
||||||
|
const result = timeSeriesFormatFilterer(data);
|
||||||
|
|
||||||
|
expect(result).toEqual([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when called with an array of series with both timeseries and table data', () => {
|
||||||
|
it('then it should return an array with timeseries', () => {
|
||||||
|
const time = new Date().getTime();
|
||||||
|
const data: any[] = [
|
||||||
|
{
|
||||||
|
target: 'series1',
|
||||||
|
datapoints: [[12.12, time], [14.44, time + 1]],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
type: 'time',
|
||||||
|
text: 'Time',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'mean',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
type: 'table',
|
||||||
|
rows: [[time, 13.13], [time + 1, 26.26]],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const result = timeSeriesFormatFilterer(data);
|
||||||
|
|
||||||
|
expect(result).toEqual([
|
||||||
|
{
|
||||||
|
target: 'series1',
|
||||||
|
datapoints: [[12.12, time], [14.44, time + 1]],
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('tableDataFormatFilterer', () => {
|
||||||
|
describe('when called with an object that contains columns property', () => {
|
||||||
|
it('then it should return same object in array', () => {
|
||||||
|
const data: any = { columns: [] };
|
||||||
|
|
||||||
|
const result = tableDataFormatFilterer(data);
|
||||||
|
|
||||||
|
expect(result).toEqual([data]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when called with an object that does not contain columns property', () => {
|
||||||
|
it('then it should return empty array', () => {
|
||||||
|
const data: any = { prop: [] };
|
||||||
|
|
||||||
|
const result = tableDataFormatFilterer(data);
|
||||||
|
|
||||||
|
expect(result).toEqual([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when called with an array of series with both timeseries and table data', () => {
|
||||||
|
it('then it should return an array with table data', () => {
|
||||||
|
const time = new Date().getTime();
|
||||||
|
const data: any[] = [
|
||||||
|
{
|
||||||
|
target: 'series1',
|
||||||
|
datapoints: [[12.12, time], [14.44, time + 1]],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
type: 'time',
|
||||||
|
text: 'Time',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'mean',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
type: 'table',
|
||||||
|
rows: [[time, 13.13], [time + 1, 26.26]],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const result = tableDataFormatFilterer(data);
|
||||||
|
|
||||||
|
expect(result).toEqual([
|
||||||
|
{
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
type: 'time',
|
||||||
|
text: 'Time',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'mean',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
type: 'table',
|
||||||
|
rows: [[time, 13.13], [time + 1, 26.26]],
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -6,6 +6,33 @@ import { TableTransform } from './types';
|
|||||||
import { Column, TableData } from '@grafana/data';
|
import { Column, TableData } from '@grafana/data';
|
||||||
|
|
||||||
const transformers: { [key: string]: TableTransform } = {};
|
const transformers: { [key: string]: TableTransform } = {};
|
||||||
|
export const timeSeriesFormatFilterer = (data: any): any[] => {
|
||||||
|
if (!Array.isArray(data)) {
|
||||||
|
return data.datapoints ? [data] : [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return data.reduce((acc, series) => {
|
||||||
|
if (!series.datapoints) {
|
||||||
|
return acc;
|
||||||
|
}
|
||||||
|
|
||||||
|
return acc.concat(series);
|
||||||
|
}, []);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const tableDataFormatFilterer = (data: any): any[] => {
|
||||||
|
if (!Array.isArray(data)) {
|
||||||
|
return data.columns ? [data] : [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return data.reduce((acc, series) => {
|
||||||
|
if (!series.columns) {
|
||||||
|
return acc;
|
||||||
|
}
|
||||||
|
|
||||||
|
return acc.concat(series);
|
||||||
|
}, []);
|
||||||
|
};
|
||||||
|
|
||||||
transformers['timeseries_to_rows'] = {
|
transformers['timeseries_to_rows'] = {
|
||||||
description: 'Time series to rows',
|
description: 'Time series to rows',
|
||||||
@ -14,9 +41,10 @@ transformers['timeseries_to_rows'] = {
|
|||||||
},
|
},
|
||||||
transform: (data, panel, model) => {
|
transform: (data, panel, model) => {
|
||||||
model.columns = [{ text: 'Time', type: 'date' }, { text: 'Metric' }, { text: 'Value' }];
|
model.columns = [{ text: 'Time', type: 'date' }, { text: 'Metric' }, { text: 'Value' }];
|
||||||
|
const filteredData = timeSeriesFormatFilterer(data);
|
||||||
|
|
||||||
for (let i = 0; i < data.length; i++) {
|
for (let i = 0; i < filteredData.length; i++) {
|
||||||
const series = data[i];
|
const series = filteredData[i];
|
||||||
for (let y = 0; y < series.datapoints.length; y++) {
|
for (let y = 0; y < series.datapoints.length; y++) {
|
||||||
const dp = series.datapoints[y];
|
const dp = series.datapoints[y];
|
||||||
model.rows.push([dp[1], series.target, dp[0]]);
|
model.rows.push([dp[1], series.target, dp[0]]);
|
||||||
@ -35,9 +63,10 @@ transformers['timeseries_to_columns'] = {
|
|||||||
|
|
||||||
// group by time
|
// group by time
|
||||||
const points: any = {};
|
const points: any = {};
|
||||||
|
const filteredData = timeSeriesFormatFilterer(data);
|
||||||
|
|
||||||
for (let i = 0; i < data.length; i++) {
|
for (let i = 0; i < filteredData.length; i++) {
|
||||||
const series = data[i];
|
const series = filteredData[i];
|
||||||
model.columns.push({ text: series.target });
|
model.columns.push({ text: series.target });
|
||||||
|
|
||||||
for (let y = 0; y < series.datapoints.length; y++) {
|
for (let y = 0; y < series.datapoints.length; y++) {
|
||||||
@ -57,7 +86,7 @@ transformers['timeseries_to_columns'] = {
|
|||||||
const point = points[time];
|
const point = points[time];
|
||||||
const values = [point.time];
|
const values = [point.time];
|
||||||
|
|
||||||
for (let i = 0; i < data.length; i++) {
|
for (let i = 0; i < filteredData.length; i++) {
|
||||||
const value = point[i];
|
const value = point[i];
|
||||||
values.push(value);
|
values.push(value);
|
||||||
}
|
}
|
||||||
@ -87,10 +116,12 @@ transformers['timeseries_aggregations'] = {
|
|||||||
model.columns.push({ text: panel.columns[i].text });
|
model.columns.push({ text: panel.columns[i].text });
|
||||||
}
|
}
|
||||||
|
|
||||||
for (i = 0; i < data.length; i++) {
|
const filteredData = timeSeriesFormatFilterer(data);
|
||||||
|
|
||||||
|
for (i = 0; i < filteredData.length; i++) {
|
||||||
const series = new TimeSeries({
|
const series = new TimeSeries({
|
||||||
datapoints: data[i].datapoints,
|
datapoints: filteredData[i].datapoints,
|
||||||
alias: data[i].target,
|
alias: filteredData[i].target,
|
||||||
});
|
});
|
||||||
|
|
||||||
series.getFlotPairs('connected');
|
series.getFlotPairs('connected');
|
||||||
@ -139,11 +170,13 @@ transformers['table'] = {
|
|||||||
return [...data[0].columns];
|
return [...data[0].columns];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const filteredData = tableDataFormatFilterer(data);
|
||||||
|
|
||||||
// Track column indexes: name -> index
|
// Track column indexes: name -> index
|
||||||
const columnNames: any = {};
|
const columnNames: any = {};
|
||||||
|
|
||||||
// Union of all columns
|
// Union of all columns
|
||||||
const columns = data.reduce((acc: Column[], series: TableData) => {
|
const columns = filteredData.reduce((acc: Column[], series: TableData) => {
|
||||||
series.columns.forEach(col => {
|
series.columns.forEach(col => {
|
||||||
const { text } = col;
|
const { text } = col;
|
||||||
if (columnNames[text] === undefined) {
|
if (columnNames[text] === undefined) {
|
||||||
@ -160,7 +193,8 @@ transformers['table'] = {
|
|||||||
if (!data || data.length === 0) {
|
if (!data || data.length === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const noTableIndex = _.findIndex(data, d => 'columns' in d && 'rows' in d);
|
const filteredData = tableDataFormatFilterer(data);
|
||||||
|
const noTableIndex = _.findIndex(filteredData, d => 'columns' in d && 'rows' in d);
|
||||||
if (noTableIndex < 0) {
|
if (noTableIndex < 0) {
|
||||||
throw {
|
throw {
|
||||||
message: `Result of query #${String.fromCharCode(
|
message: `Result of query #${String.fromCharCode(
|
||||||
@ -169,7 +203,7 @@ transformers['table'] = {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
mergeTablesIntoModel(model, ...data);
|
mergeTablesIntoModel(model, ...filteredData);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user