diff --git a/frontend/src/container/GridTableComponent/__tests__/response.ts b/frontend/src/container/GridTableComponent/__tests__/response.ts index c19a25917494..bf18d4909572 100644 --- a/frontend/src/container/GridTableComponent/__tests__/response.ts +++ b/frontend/src/container/GridTableComponent/__tests__/response.ts @@ -1,3 +1,4 @@ +/* eslint-disable sonarjs/no-duplicate-string */ export const tableDataMultipleQueriesSuccessResponse = { columns: [ { @@ -210,3 +211,278 @@ export const expectedOutputWithLegends = { }, ], }; + +// QB v5 Aggregations Mock Data +export const tableDataQBv5MultiAggregations = { + columns: [ + { + name: 'service.name', + queryName: 'A', + isValueColumn: false, + id: 'service.name', + }, + { + name: 'host.name', + queryName: 'A', + isValueColumn: false, + id: 'host.name', + }, + { + name: 'count()', + queryName: 'A', + isValueColumn: true, + id: 'A.count()', + }, + { + name: 'count_distinct(app.ads.count)', + queryName: 'A', + isValueColumn: true, + id: 'A.count_distinct(app.ads.count)', + }, + { + name: 'count()', + queryName: 'B', + isValueColumn: true, + id: 'B.count()', + }, + { + name: 'count_distinct(app.ads.count)', + queryName: 'B', + isValueColumn: true, + id: 'B.count_distinct(app.ads.count)', + }, + { + name: 'count()', + queryName: 'C', + isValueColumn: true, + id: 'C.count()', + }, + { + name: 'count_distinct(app.ads.count)', + queryName: 'C', + isValueColumn: true, + id: 'C.count_distinct(app.ads.count)', + }, + ], + rows: [ + { + data: { + 'service.name': 'frontend-proxy', + 'host.name': 'test-host.name', + 'A.count()': 144679, + 'A.count_distinct(app.ads.count)': 0, + 'B.count()': 144679, + 'B.count_distinct(app.ads.count)': 0, + 'C.count()': 144679, + 'C.count_distinct(app.ads.count)': 0, + }, + }, + { + data: { + 'service.name': 'frontend', + 'host.name': 'test-host.name', + 'A.count()': 142311, + 'A.count_distinct(app.ads.count)': 0, + 'B.count()': 142311, + 'B.count_distinct(app.ads.count)': 0, + 'C.count()': 142311, + 'C.count_distinct(app.ads.count)': 0, + }, + }, + ], +}; + +export const widgetQueryQBv5MultiAggregations = { + clickhouse_sql: [ + { + name: 'A', + legend: 'p99', + disabled: false, + query: '', + }, + { + name: 'B', + legend: '', + disabled: false, + query: '', + }, + { + name: 'C', + legend: 'max', + disabled: false, + query: '', + }, + ], + promql: [ + { + name: 'A', + query: '', + legend: 'p99', + disabled: false, + }, + { + name: 'B', + query: '', + legend: '', + disabled: false, + }, + { + name: 'C', + query: '', + legend: 'max', + disabled: false, + }, + ], + builder: { + queryData: [ + { + dataSource: 'metrics', + queryName: 'A', + aggregateOperator: 'count', + aggregateAttribute: { + dataType: 'float64', + id: 'signoz_latency--float64--ExponentialHistogram--true', + key: 'signoz_latency', + type: 'ExponentialHistogram', + }, + timeAggregation: '', + spaceAggregation: 'p90', + functions: [], + filters: { + items: [], + op: 'AND', + }, + expression: 'A', + disabled: false, + stepInterval: 60, + having: [], + limit: null, + orderBy: [], + groupBy: [ + { + dataType: 'string', + key: 'service.name', + type: 'tag', + id: 'service.name--string--tag--false', + }, + { + dataType: 'string', + key: 'host.name', + type: 'tag', + id: 'host.name--string--tag--false', + }, + ], + legend: 'p99', + reduceTo: 'avg', + }, + { + dataSource: 'metrics', + queryName: 'B', + aggregateOperator: 'rate', + aggregateAttribute: { + dataType: 'float64', + id: 'system_disk_operations--float64--Sum--true', + key: 'system_disk_operations', + type: 'Sum', + }, + timeAggregation: 'rate', + spaceAggregation: 'sum', + functions: [], + filters: { + items: [], + op: 'AND', + }, + expression: 'B', + disabled: false, + stepInterval: 60, + having: [], + limit: null, + orderBy: [], + groupBy: [ + { + dataType: 'string', + key: 'service.name', + type: 'tag', + id: 'service.name--string--tag--false', + }, + { + dataType: 'string', + key: 'host.name', + type: 'tag', + id: 'host.name--string--tag--false', + }, + ], + legend: '', + reduceTo: 'avg', + }, + { + dataSource: 'metrics', + queryName: 'C', + aggregateOperator: 'count', + aggregateAttribute: { + dataType: 'float64', + id: 'signoz_latency--float64--ExponentialHistogram--true', + key: 'signoz_latency', + type: 'ExponentialHistogram', + }, + timeAggregation: '', + spaceAggregation: 'p90', + functions: [], + filters: { + items: [], + op: 'AND', + }, + expression: 'C', + disabled: false, + stepInterval: 60, + having: [], + limit: null, + orderBy: [], + groupBy: [ + { + dataType: 'string', + key: 'service.name', + type: 'tag', + id: 'service.name--string--tag--false', + }, + { + dataType: 'string', + key: 'host.name', + type: 'tag', + id: 'host.name--string--tag--false', + }, + ], + legend: 'max', + reduceTo: 'avg', + }, + ], + queryFormulas: [], + }, + id: 'qb-v5-multi-aggregations-test', + queryType: 'builder', +}; + +export const expectedOutputQBv5MultiAggregations = { + dataSource: [ + { + 'service.name': 'frontend-proxy', + 'host.name': 'test-host.name', + 'A.count()': 144679, + 'A.count_distinct(app.ads.count)': 0, + 'B.count()': 144679, + 'B.count_distinct(app.ads.count)': 0, + 'C.count()': 144679, + 'C.count_distinct(app.ads.count)': 0, + }, + { + 'service.name': 'frontend', + 'host.name': 'test-host.name', + 'A.count()': 142311, + 'A.count_distinct(app.ads.count)': 0, + 'B.count()': 142311, + 'B.count_distinct(app.ads.count)': 0, + 'C.count()': 142311, + 'C.count_distinct(app.ads.count)': 0, + }, + ], +}; diff --git a/frontend/src/container/GridTableComponent/__tests__/utils.test.tsx b/frontend/src/container/GridTableComponent/__tests__/utils.test.tsx index e8f7a631bd82..ed3ba194d181 100644 --- a/frontend/src/container/GridTableComponent/__tests__/utils.test.tsx +++ b/frontend/src/container/GridTableComponent/__tests__/utils.test.tsx @@ -6,8 +6,11 @@ import { sortFunction, } from '../utils'; import { + expectedOutputQBv5MultiAggregations, expectedOutputWithLegends, tableDataMultipleQueriesSuccessResponse, + tableDataQBv5MultiAggregations, + widgetQueryQBv5MultiAggregations, widgetQueryWithLegend, } from './response'; @@ -67,6 +70,7 @@ describe('Table Panel utils', () => { isValueColumn: true, name: 'A', queryName: 'A', + id: 'A', }; // A has value and value is considered bigger than n/a hence 1 expect(sortFunction(rowA, rowB, item)).toBe(1); @@ -128,3 +132,96 @@ describe('Table Panel utils', () => { expect(sortFunction(rowA, rowB, item)).toBe(0); }); }); + +describe('Table Panel utils with QB v5 aggregations', () => { + it('createColumnsAndDataSource function - QB v5 multi-aggregations', () => { + const data = tableDataQBv5MultiAggregations; + const query = widgetQueryQBv5MultiAggregations as Query; + + const { columns, dataSource } = createColumnsAndDataSource(data, query); + + // Verify column structure for multi-aggregations + expect(columns).toHaveLength(8); + expect(columns[0].title).toBe('service.name'); + expect(columns[1].title).toBe('host.name'); + // All columns with queryName 'A' get the legend 'p99' + expect(columns[2].title).toBe('p99'); // A.count() uses legend from query A + expect(columns[3].title).toBe('p99'); // A.count_distinct() uses legend from query A + expect(columns[4].title).toBe('count()'); // B.count() uses column name (no legend) + expect(columns[5].title).toBe('count_distinct(app.ads.count)'); // B.count_distinct() uses column name + expect(columns[6].title).toBe('max'); // C.count() uses legend from query C + expect(columns[7].title).toBe('max'); // C.count_distinct() uses legend from query C + + // Verify dataIndex mapping + expect((columns[0] as any).dataIndex).toBe('service.name'); + expect((columns[2] as any).dataIndex).toBe('A.count()'); + expect((columns[3] as any).dataIndex).toBe('A.count_distinct(app.ads.count)'); + + // Verify dataSource structure + expect(dataSource).toStrictEqual( + expectedOutputQBv5MultiAggregations.dataSource, + ); + }); + + it('getQueryLegend function - QB v5 multi-query support', () => { + const query = widgetQueryQBv5MultiAggregations as Query; + + expect(getQueryLegend(query, 'A')).toBe('p99'); + expect(getQueryLegend(query, 'B')).toBeUndefined(); + expect(getQueryLegend(query, 'C')).toBe('max'); + expect(getQueryLegend(query, 'D')).toBeUndefined(); + }); + + it('sorter function - QB v5 multi-aggregation columns', () => { + const item = { + isValueColumn: true, + name: 'count()', + queryName: 'A', + id: 'A.count()', + }; + + // Test numeric sorting + expect( + sortFunction( + { 'A.count()': 100, key: '1', timestamp: 1000 }, + { 'A.count()': 200, key: '2', timestamp: 1000 }, + item, + ), + ).toBe(-100); + + // Test n/a handling + expect( + sortFunction( + { 'A.count()': 'n/a', key: '1', timestamp: 1000 }, + { 'A.count()': 100, key: '2', timestamp: 1000 }, + item, + ), + ).toBe(-1); + + expect( + sortFunction( + { 'A.count()': 100, key: '1', timestamp: 1000 }, + { 'A.count()': 'n/a', key: '2', timestamp: 1000 }, + item, + ), + ).toBe(1); + + // Test string sorting + expect( + sortFunction( + { 'A.count()': 'read', key: '1', timestamp: 1000 }, + { 'A.count()': 'write', key: '2', timestamp: 1000 }, + item, + ), + ).toBe(-1); + + // Test equal values + expect( + sortFunction( + { 'A.count()': 'n/a', key: '1', timestamp: 1000 }, + { 'A.count()': 'n/a', key: '2', timestamp: 1000 }, + item, + ), + ).toBe(0); + }); +}); diff --git a/frontend/src/container/GridTableComponent/utils.ts b/frontend/src/container/GridTableComponent/utils.ts index 5d0c65c83352..c2b735098d16 100644 --- a/frontend/src/container/GridTableComponent/utils.ts +++ b/frontend/src/container/GridTableComponent/utils.ts @@ -150,11 +150,14 @@ export function sortFunction( name: string; queryName: string; isValueColumn: boolean; + id: string; }, ): number { + const colId = item.id; + const colName = item.name; // assumption :- number values is bigger than 'n/a' - const valueA = Number(a[`${item.name}_without_unit`] ?? a[item.name]); - const valueB = Number(b[`${item.name}_without_unit`] ?? b[item.name]); + const valueA = Number(a[`${colId}_without_unit`] ?? a[colId] ?? a[colName]); + const valueB = Number(b[`${colId}_without_unit`] ?? b[colId] ?? b[colName]); // if both the values are numbers then return the difference here if (!isNaN(valueA) && !isNaN(valueB)) { @@ -172,10 +175,11 @@ export function sortFunction( } // if both of them are strings do the localecompare - return ((a[item.name] as string) || '').localeCompare( - (b[item.name] as string) || '', + return ((a[colId] as string) || (a[colName] as string) || '').localeCompare( + (b[colId] as string) || (b[colName] as string) || '', ); } + export function createColumnsAndDataSource( data: TableData, currentQuery: Query, @@ -198,7 +202,7 @@ export function createColumnsAndDataSource( // if no legend present then rely on the column name value title: !isNewAggregation && !isEmpty(legend) ? legend : item.name, width: QUERY_TABLE_CONFIG.width, - render: renderColumnCell && renderColumnCell[item.name], + render: renderColumnCell && renderColumnCell[item.id], sorter: (a: RowData, b: RowData): number => sortFunction(a, b, item), };