Merge pull request #10050 from davkal/davkal/multi-query-table
Add support to render values of multiple queries in the same table
This commit is contained in:
@@ -118,7 +118,7 @@ export class PrometheusDatasource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (activeTargets[index].format === "table") {
|
if (activeTargets[index].format === "table") {
|
||||||
result.push(self.transformMetricDataToTable(response.data.data.result));
|
result.push(self.transformMetricDataToTable(response.data.data.result, responseList.length, index));
|
||||||
} else {
|
} else {
|
||||||
for (let metricData of response.data.data.result) {
|
for (let metricData of response.data.data.result) {
|
||||||
if (response.data.data.resultType === 'matrix') {
|
if (response.data.data.resultType === 'matrix') {
|
||||||
@@ -301,7 +301,7 @@ export class PrometheusDatasource {
|
|||||||
return { target: metricLabel, datapoints: dps };
|
return { target: metricLabel, datapoints: dps };
|
||||||
}
|
}
|
||||||
|
|
||||||
transformMetricDataToTable(md) {
|
transformMetricDataToTable(md, resultCount: number, resultIndex: number) {
|
||||||
var table = new TableModel();
|
var table = new TableModel();
|
||||||
var i, j;
|
var i, j;
|
||||||
var metricLabels = {};
|
var metricLabels = {};
|
||||||
@@ -326,7 +326,8 @@ export class PrometheusDatasource {
|
|||||||
metricLabels[label] = labelIndex + 1;
|
metricLabels[label] = labelIndex + 1;
|
||||||
table.columns.push({text: label});
|
table.columns.push({text: label});
|
||||||
});
|
});
|
||||||
table.columns.push({text: 'Value'});
|
let valueText = resultCount > 1 ? `Value #${String.fromCharCode(65 + resultIndex)}` : 'Value';
|
||||||
|
table.columns.push({text: valueText});
|
||||||
|
|
||||||
// Populate rows, set value to empty string when label not present.
|
// Populate rows, set value to empty string when label not present.
|
||||||
_.each(md, function(series) {
|
_.each(md, function(series) {
|
||||||
|
|||||||
@@ -94,7 +94,232 @@ describe('when transforming time series table', () => {
|
|||||||
expect(table.columns[2].text).toBe('Min');
|
expect(table.columns[2].text).toBe('Min');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('table data sets', () => {
|
||||||
|
describe('Table', () => {
|
||||||
|
const transform = 'table';
|
||||||
|
var panel = {
|
||||||
|
transform,
|
||||||
|
};
|
||||||
|
var time = new Date().getTime();
|
||||||
|
|
||||||
|
var nonTableData = [
|
||||||
|
{
|
||||||
|
type: 'foo',
|
||||||
|
columns: [
|
||||||
|
{ text: 'Time' },
|
||||||
|
{ text: 'Label Key 1' },
|
||||||
|
{ text: 'Value' },
|
||||||
|
],
|
||||||
|
rows: [
|
||||||
|
[time, 'Label Value 1', 42],
|
||||||
|
],
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
var singleQueryData = [
|
||||||
|
{
|
||||||
|
type: 'table',
|
||||||
|
columns: [
|
||||||
|
{ text: 'Time' },
|
||||||
|
{ text: 'Label Key 1' },
|
||||||
|
{ text: 'Value' },
|
||||||
|
],
|
||||||
|
rows: [
|
||||||
|
[time, 'Label Value 1', 42],
|
||||||
|
],
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
var multipleQueriesDataSameLabels = [
|
||||||
|
{
|
||||||
|
type: 'table',
|
||||||
|
columns: [
|
||||||
|
{ text: 'Time' },
|
||||||
|
{ text: 'Label Key 1' },
|
||||||
|
{ text: 'Label Key 2' },
|
||||||
|
{ text: 'Value #A' },
|
||||||
|
],
|
||||||
|
rows: [
|
||||||
|
[time, 'Label Value 1', 'Label Value 2', 42],
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'table',
|
||||||
|
columns: [
|
||||||
|
{ text: 'Time' },
|
||||||
|
{ text: 'Label Key 1' },
|
||||||
|
{ text: 'Label Key 2' },
|
||||||
|
{ text: 'Value #B' },
|
||||||
|
],
|
||||||
|
rows: [
|
||||||
|
[time, 'Label Value 1', 'Label Value 2', 13],
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'table',
|
||||||
|
columns: [
|
||||||
|
{ text: 'Time' },
|
||||||
|
{ text: 'Label Key 1' },
|
||||||
|
{ text: 'Label Key 2' },
|
||||||
|
{ text: 'Value #C' },
|
||||||
|
],
|
||||||
|
rows: [
|
||||||
|
[time, 'Label Value 1', 'Label Value 2', 4],
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'table',
|
||||||
|
columns: [
|
||||||
|
{ text: 'Time' },
|
||||||
|
{ text: 'Label Key 1' },
|
||||||
|
{ text: 'Label Key 2' },
|
||||||
|
{ text: 'Value #C' },
|
||||||
|
],
|
||||||
|
rows: [
|
||||||
|
[time, 'Label Value 1', 'Label Value 2', 7],
|
||||||
|
],
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
var multipleQueriesDataDifferentLabels = [
|
||||||
|
{
|
||||||
|
type: 'table',
|
||||||
|
columns: [
|
||||||
|
{ text: 'Time' },
|
||||||
|
{ text: 'Label Key 1' },
|
||||||
|
{ text: 'Value #A' },
|
||||||
|
],
|
||||||
|
rows: [
|
||||||
|
[time, 'Label Value 1', 42],
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'table',
|
||||||
|
columns: [
|
||||||
|
{ text: 'Time' },
|
||||||
|
{ text: 'Label Key 2' },
|
||||||
|
{ text: 'Value #B' },
|
||||||
|
],
|
||||||
|
rows: [
|
||||||
|
[time, 'Label Value 2', 13],
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'table',
|
||||||
|
columns: [
|
||||||
|
{ text: 'Time' },
|
||||||
|
{ text: 'Label Key 1' },
|
||||||
|
{ text: 'Value #C' },
|
||||||
|
],
|
||||||
|
rows: [
|
||||||
|
[time, 'Label Value 3', 7],
|
||||||
|
],
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
describe('getColumns', function() {
|
||||||
|
it('should return data columns given a single query', function() {
|
||||||
|
var columns = transformers[transform].getColumns(singleQueryData);
|
||||||
|
expect(columns[0].text).toBe('Time');
|
||||||
|
expect(columns[1].text).toBe('Label Key 1');
|
||||||
|
expect(columns[2].text).toBe('Value');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return the union of data columns given a multiple queries', function() {
|
||||||
|
var columns = transformers[transform].getColumns(multipleQueriesDataSameLabels);
|
||||||
|
expect(columns[0].text).toBe('Time');
|
||||||
|
expect(columns[1].text).toBe('Label Key 1');
|
||||||
|
expect(columns[2].text).toBe('Label Key 2');
|
||||||
|
expect(columns[3].text).toBe('Value #A');
|
||||||
|
expect(columns[4].text).toBe('Value #B');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return the union of data columns given a multiple queries with different labels', function() {
|
||||||
|
var columns = transformers[transform].getColumns(multipleQueriesDataDifferentLabels);
|
||||||
|
expect(columns[0].text).toBe('Time');
|
||||||
|
expect(columns[1].text).toBe('Label Key 1');
|
||||||
|
expect(columns[2].text).toBe('Value #A');
|
||||||
|
expect(columns[3].text).toBe('Label Key 2');
|
||||||
|
expect(columns[4].text).toBe('Value #B');
|
||||||
|
expect(columns[5].text).toBe('Value #C');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('transform', function() {
|
||||||
|
it ('should throw an error with non-table data', () => {
|
||||||
|
expect(() => transformDataToTable(nonTableData, panel)).toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
it ('should return 3 columns for single queries', () => {
|
||||||
|
table = transformDataToTable(singleQueryData, panel);
|
||||||
|
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 the union of columns for multiple queries', () => {
|
||||||
|
table = transformDataToTable(multipleQueriesDataSameLabels, panel);
|
||||||
|
expect(table.columns.length).toBe(6);
|
||||||
|
expect(table.columns[0].text).toBe('Time');
|
||||||
|
expect(table.columns[1].text).toBe('Label Key 1');
|
||||||
|
expect(table.columns[2].text).toBe('Label Key 2');
|
||||||
|
expect(table.columns[3].text).toBe('Value #A');
|
||||||
|
expect(table.columns[4].text).toBe('Value #B');
|
||||||
|
expect(table.columns[5].text).toBe('Value #C');
|
||||||
|
});
|
||||||
|
|
||||||
|
it ('should return 1 row for a single query', () => {
|
||||||
|
table = transformDataToTable(singleQueryData, panel);
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
|
||||||
|
it ('should return 2 rows for a mulitple queries with same label values plus one extra row', () => {
|
||||||
|
table = transformDataToTable(multipleQueriesDataSameLabels, panel);
|
||||||
|
expect(table.rows.length).toBe(2);
|
||||||
|
expect(table.rows[0][0]).toBe(time);
|
||||||
|
expect(table.rows[0][1]).toBe('Label Value 1');
|
||||||
|
expect(table.rows[0][2]).toBe('Label Value 2');
|
||||||
|
expect(table.rows[0][3]).toBe(42);
|
||||||
|
expect(table.rows[0][4]).toBe(13);
|
||||||
|
expect(table.rows[0][5]).toBe(4);
|
||||||
|
expect(table.rows[1][0]).toBe(time);
|
||||||
|
expect(table.rows[1][1]).toBe('Label Value 1');
|
||||||
|
expect(table.rows[1][2]).toBe('Label Value 2');
|
||||||
|
expect(table.rows[1][3]).toBeUndefined();
|
||||||
|
expect(table.rows[1][4]).toBeUndefined();
|
||||||
|
expect(table.rows[1][5]).toBe(7);
|
||||||
|
});
|
||||||
|
|
||||||
|
it ('should return 2 rows for mulitple queries with different label values', () => {
|
||||||
|
table = transformDataToTable(multipleQueriesDataDifferentLabels, panel);
|
||||||
|
expect(table.rows.length).toBe(2);
|
||||||
|
expect(table.columns.length).toBe(6);
|
||||||
|
|
||||||
|
expect(table.rows[0][0]).toBe(time);
|
||||||
|
expect(table.rows[0][1]).toBe('Label Value 1');
|
||||||
|
expect(table.rows[0][2]).toBe(42);
|
||||||
|
expect(table.rows[0][3]).toBe('Label Value 2');
|
||||||
|
expect(table.rows[0][4]).toBe(13);
|
||||||
|
expect(table.rows[0][5]).toBeUndefined();
|
||||||
|
|
||||||
|
expect(table.rows[1][0]).toBe(time);
|
||||||
|
expect(table.rows[1][1]).toBe('Label Value 3');
|
||||||
|
expect(table.rows[1][2]).toBeUndefined();
|
||||||
|
expect(table.rows[1][3]).toBeUndefined();
|
||||||
|
expect(table.rows[1][4]).toBeUndefined();
|
||||||
|
expect(table.rows[1][5]).toBe(7);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('doc data sets', () => {
|
||||||
describe('JSON Data', () => {
|
describe('JSON Data', () => {
|
||||||
var panel = {
|
var panel = {
|
||||||
transform: 'json',
|
transform: 'json',
|
||||||
@@ -148,7 +373,9 @@ describe('when transforming time series table', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('annotation data', () => {
|
||||||
describe('Annnotations', () => {
|
describe('Annnotations', () => {
|
||||||
var panel = {transform: 'annotations'};
|
var panel = {transform: 'annotations'};
|
||||||
var rawData = {
|
var rawData = {
|
||||||
|
|||||||
@@ -135,19 +135,134 @@ transformers['table'] = {
|
|||||||
if (!data || data.length === 0) {
|
if (!data || data.length === 0) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
return data[0].columns;
|
|
||||||
|
// Single query returns data columns as is
|
||||||
|
if (data.length === 1) {
|
||||||
|
return [...data[0].columns];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Track column indexes: name -> index
|
||||||
|
const columnNames = {};
|
||||||
|
|
||||||
|
// Union of all columns
|
||||||
|
const columns = data.reduce((acc, series) => {
|
||||||
|
series.columns.forEach(col => {
|
||||||
|
const { text } = col;
|
||||||
|
if (columnNames[text] === undefined) {
|
||||||
|
columnNames[text] = acc.length;
|
||||||
|
acc.push(col);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return acc;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return columns;
|
||||||
},
|
},
|
||||||
transform: function(data, panel, model) {
|
transform: function(data, panel, model) {
|
||||||
if (!data || data.length === 0) {
|
if (!data || data.length === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data[0].type !== 'table') {
|
const noTableIndex = _.findIndex(data, d => d.type !== 'table');
|
||||||
throw {message: 'Query result is not in table format, try using another transform.'};
|
if (noTableIndex > -1) {
|
||||||
|
throw {message: `Result of query #${String.fromCharCode(65 + noTableIndex)} is not in table format, try using another transform.`};
|
||||||
}
|
}
|
||||||
|
|
||||||
model.columns = data[0].columns;
|
// Single query returns data columns and rows as is
|
||||||
model.rows = data[0].rows;
|
if (data.length === 1) {
|
||||||
|
model.columns = [...data[0].columns];
|
||||||
|
model.rows = [...data[0].rows];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Track column indexes of union: name -> index
|
||||||
|
const columnNames = {};
|
||||||
|
|
||||||
|
// Union of all non-value columns
|
||||||
|
const columnsUnion = data.reduce((acc, series) => {
|
||||||
|
series.columns.forEach(col => {
|
||||||
|
const { text } = col;
|
||||||
|
if (columnNames[text] === undefined) {
|
||||||
|
columnNames[text] = acc.length;
|
||||||
|
acc.push(col);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return acc;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Map old column index to union index per series, e.g.,
|
||||||
|
// given columnNames {A: 0, B: 1} and
|
||||||
|
// data [{columns: [{ text: 'A' }]}, {columns: [{ text: 'B' }]}] => [[0], [1]]
|
||||||
|
const columnIndexMapper = data.map(series =>
|
||||||
|
series.columns.map(col => columnNames[col.text])
|
||||||
|
);
|
||||||
|
|
||||||
|
// Flatten rows of all series and adjust new column indexes
|
||||||
|
const flattenedRows = data.reduce((acc, series, seriesIndex) => {
|
||||||
|
const mapper = columnIndexMapper[seriesIndex];
|
||||||
|
series.rows.forEach(row => {
|
||||||
|
const alteredRow = [];
|
||||||
|
// Shifting entries according to index mapper
|
||||||
|
mapper.forEach((to, from) => {
|
||||||
|
alteredRow[to] = row[from];
|
||||||
|
});
|
||||||
|
acc.push(alteredRow);
|
||||||
|
});
|
||||||
|
return acc;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Returns true if both rows have matching non-empty fields as well as matching
|
||||||
|
// indexes where one field is empty and the other is not
|
||||||
|
function areRowsMatching(columns, row, otherRow) {
|
||||||
|
let foundFieldToMatch = false;
|
||||||
|
for (let columnIndex = 0; columnIndex < columns.length; columnIndex++) {
|
||||||
|
if (row[columnIndex] !== undefined && otherRow[columnIndex] !== undefined) {
|
||||||
|
if (row[columnIndex] !== otherRow[columnIndex]) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} else if (row[columnIndex] === undefined || otherRow[columnIndex] === undefined) {
|
||||||
|
foundFieldToMatch = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return foundFieldToMatch;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Merge rows that have same values for columns
|
||||||
|
const mergedRows = {};
|
||||||
|
const compactedRows = flattenedRows.reduce((acc, row, rowIndex) => {
|
||||||
|
if (!mergedRows[rowIndex]) {
|
||||||
|
// Look from current row onwards
|
||||||
|
let offset = rowIndex + 1;
|
||||||
|
// More than one row can be merged into current row
|
||||||
|
while (offset < flattenedRows.length) {
|
||||||
|
// Find next row that could be merged
|
||||||
|
const match = _.findIndex(flattenedRows,
|
||||||
|
otherRow => areRowsMatching(columnsUnion, row, otherRow),
|
||||||
|
offset);
|
||||||
|
if (match > -1) {
|
||||||
|
const matchedRow = flattenedRows[match];
|
||||||
|
// Merge values from match into current row if there is a gap in the current row
|
||||||
|
for (let columnIndex = 0; columnIndex < columnsUnion.length; columnIndex++) {
|
||||||
|
if (row[columnIndex] === undefined && matchedRow[columnIndex] !== undefined) {
|
||||||
|
row[columnIndex] = matchedRow[columnIndex];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Dont visit this row again
|
||||||
|
mergedRows[match] = matchedRow;
|
||||||
|
// Keep looking for more rows to merge
|
||||||
|
offset = match + 1;
|
||||||
|
} else {
|
||||||
|
// No match found, stop looking
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
acc.push(row);
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
model.columns = columnsUnion;
|
||||||
|
model.rows = compactedRows;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user