import { cloneDeep, isEmpty } from 'lodash-es'; import { SuccessResponse, Warning } from 'types/api'; import { MetricRangePayloadV3 } from 'types/api/metrics/getQueryRange'; import { DistributionData, MetricRangePayloadV5, QueryRangeRequestV5, RawData, ScalarData, TimeSeriesData, } from 'types/api/v5/queryRange'; import { QueryDataV3 } from 'types/api/widgets/getQuery'; function getColName( col: ScalarData['columns'][number], legendMap: Record, aggregationPerQuery: Record, ): string { if (col.columnType === 'group') { return col.name; } const aggregation = aggregationPerQuery?.[col.queryName]?.[col.aggregationIndex]; const legend = legendMap[col.queryName]; const alias = aggregation?.alias; const expression = aggregation?.expression || ''; const aggregationsCount = aggregationPerQuery[col.queryName]?.length || 0; const isSingleAggregation = aggregationsCount === 1; // Single aggregation: Priority is alias > legend > expression if (isSingleAggregation) { return alias || legend || expression; } // Multiple aggregations: Each follows single rules BUT never shows legend // Priority: alias > expression (legend is ignored for multiple aggregations) return alias || expression; } function getColId( col: ScalarData['columns'][number], aggregationPerQuery: Record, ): string { if (col.columnType === 'group') { return col.name; } const aggregation = aggregationPerQuery?.[col.queryName]?.[col.aggregationIndex]; const expression = aggregation?.expression || ''; return `${col.queryName}.${expression}`; } /** * Converts V5 TimeSeriesData to legacy format */ function convertTimeSeriesData( timeSeriesData: TimeSeriesData, legendMap: Record, ): QueryDataV3 { // Convert V5 time series format to legacy QueryDataV3 format // Helper function to process series data const processSeriesData = ( aggregations: any[], seriesKey: | 'series' | 'predictedSeries' | 'upperBoundSeries' | 'lowerBoundSeries' | 'anomalyScores', ): any[] => aggregations?.flatMap((aggregation) => { const { index, alias } = aggregation; const seriesData = aggregation[seriesKey]; if (!seriesData || !seriesData.length) { return []; } return seriesData.map((series: any) => ({ labels: series.labels ? Object.fromEntries( series.labels.map((label: any) => [label.key.name, label.value]), ) : {}, labelsArray: series.labels ? series.labels.map((label: any) => ({ [label.key.name]: label.value })) : [], values: series.values.map((value: any) => ({ timestamp: value.timestamp, value: String(value.value), })), metaData: { alias, index, queryName: timeSeriesData.queryName, }, })); }); return { queryName: timeSeriesData.queryName, legend: legendMap[timeSeriesData.queryName] || timeSeriesData.queryName, series: processSeriesData(timeSeriesData?.aggregations, 'series'), predictedSeries: processSeriesData( timeSeriesData?.aggregations, 'predictedSeries', ), upperBoundSeries: processSeriesData( timeSeriesData?.aggregations, 'upperBoundSeries', ), lowerBoundSeries: processSeriesData( timeSeriesData?.aggregations, 'lowerBoundSeries', ), anomalyScores: processSeriesData( timeSeriesData?.aggregations, 'anomalyScores', ), list: null, }; } /** * Converts V5 ScalarData array to legacy format with table structure */ function convertScalarDataArrayToTable( scalarDataArray: ScalarData[], legendMap: Record, aggregationPerQuery: Record, ): QueryDataV3[] { // If no scalar data, return empty structure if (!scalarDataArray || scalarDataArray.length === 0) { return []; } // Process each scalar data separately to maintain query separation return scalarDataArray?.map((scalarData) => { // Get query name from the first column const queryName = scalarData?.columns?.[0]?.queryName || ''; if ((scalarData as any)?.aggregations?.length > 0) { return { ...convertTimeSeriesData(scalarData as any, legendMap), table: { columns: [], rows: [], }, list: null, }; } // Collect columns for this specific query const columns = scalarData?.columns?.map((col) => ({ name: getColName(col, legendMap, aggregationPerQuery), queryName: col.queryName, isValueColumn: col.columnType === 'aggregation', id: getColId(col, aggregationPerQuery), })); // Process rows for this specific query const rows = scalarData?.data?.map((dataRow) => { const rowData: Record = {}; scalarData?.columns?.forEach((col, colIndex) => { const columnName = getColName(col, legendMap, aggregationPerQuery); const columnId = getColId(col, aggregationPerQuery); rowData[columnId || columnName] = dataRow[colIndex]; }); return { data: rowData }; }); return { queryName, legend: legendMap[queryName] || '', series: null, list: null, table: { columns, rows, }, }; }); } function convertScalarWithFormatForWeb( scalarDataArray: ScalarData[], legendMap: Record, aggregationPerQuery: Record, ): QueryDataV3[] { if (!scalarDataArray || scalarDataArray.length === 0) { return []; } return scalarDataArray.map((scalarData) => { const columns = scalarData.columns?.map((col) => { const colName = getColName(col, legendMap, aggregationPerQuery); return { name: colName, queryName: col.queryName, isValueColumn: col.columnType === 'aggregation', id: getColId(col, aggregationPerQuery), }; }) || []; const rows = scalarData.data?.map((dataRow) => { const rowData: Record = {}; columns?.forEach((col, colIndex) => { rowData[col.id || col.name] = dataRow[colIndex]; }); return { data: rowData }; }) || []; const queryName = scalarData.columns?.[0]?.queryName || ''; return { queryName, legend: legendMap[queryName] || queryName, series: null, list: null, table: { columns, rows, }, }; }); } /** * Converts V5 RawData to legacy format */ function convertRawData( rawData: RawData, legendMap: Record, ): QueryDataV3 { // Convert V5 raw format to legacy QueryDataV3 format return { queryName: rawData.queryName, legend: legendMap[rawData.queryName] || rawData.queryName, series: null, list: rawData.rows?.map((row) => ({ timestamp: row.timestamp, data: { // Map raw data to ILog structure - spread row.data first to include all properties ...row.data, date: row.timestamp, } as any, })), }; } /** * Converts V5 DistributionData to legacy format */ function convertDistributionData( distributionData: DistributionData, legendMap: Record, ): any { // eslint-disable-line @typescript-eslint/no-explicit-any // Convert V5 distribution format to legacy histogram format return { ...distributionData, legendMap, }; } /** * Helper function to convert V5 data based on type */ function convertV5DataByType( v5Data: any, legendMap: Record, aggregationPerQuery: Record, ): MetricRangePayloadV3['data'] { switch (v5Data?.type) { case 'time_series': { const timeSeriesData = v5Data.data.results as TimeSeriesData[]; return { resultType: 'time_series', result: timeSeriesData.map((timeSeries) => convertTimeSeriesData(timeSeries, legendMap), ), }; } case 'scalar': { const scalarData = v5Data.data.results as ScalarData[]; // For scalar data, combine all results into separate table entries const combinedTables = convertScalarDataArrayToTable( scalarData, legendMap, aggregationPerQuery, ); return { resultType: 'scalar', result: combinedTables, }; } case 'raw': { const rawData = v5Data.data.results as RawData[]; return { resultType: 'raw', result: rawData.map((raw) => convertRawData(raw, legendMap)), }; } case 'trace': { const traceData = v5Data.data.results as RawData[]; return { resultType: 'trace', result: traceData.map((trace) => convertRawData(trace, legendMap)), }; } case 'distribution': { const distributionData = v5Data.data.results as DistributionData[]; return { resultType: 'distribution', result: distributionData.map((distribution) => convertDistributionData(distribution, legendMap), ), }; } default: return { resultType: '', result: [], }; } } /** * Converts V5 API response to legacy format expected by frontend components */ // eslint-disable-next-line sonarjs/cognitive-complexity export function convertV5ResponseToLegacy( v5Response: SuccessResponse, legendMap: Record, formatForWeb?: boolean, ): SuccessResponse & { warning?: Warning } { const { payload, params } = v5Response; const v5Data = payload?.data; const aggregationPerQuery = (params as QueryRangeRequestV5)?.compositeQuery?.queries ?.filter((query) => query.type === 'builder_query') .reduce((acc, query) => { if ( query.type === 'builder_query' && 'aggregations' in query.spec && query.spec.name ) { acc[query.spec.name] = query.spec.aggregations; } return acc; }, {} as Record) || {}; // If formatForWeb is true, return as-is (like existing logic) if (formatForWeb && v5Data?.type === 'scalar') { const scalarData = v5Data.data.results as ScalarData[]; const webTables = convertScalarWithFormatForWeb( scalarData, legendMap, aggregationPerQuery, ); return { ...v5Response, payload: { data: { resultType: 'scalar', result: webTables, }, warning: v5Data?.warning || undefined, }, warning: v5Data?.warning || undefined, }; } // Convert based on V5 response type const convertedData = convertV5DataByType( v5Data, legendMap, aggregationPerQuery, ); // Create legacy-compatible response structure const legacyResponse: SuccessResponse = { ...v5Response, payload: { data: convertedData, warning: v5Response.payload?.data?.warning || undefined, }, }; // Apply legend mapping (similar to existing logic) if (legacyResponse.payload?.data?.result) { legacyResponse.payload.data.result = legacyResponse.payload.data.result.map( (queryData: any) => { // eslint-disable-line @typescript-eslint/no-explicit-any const newQueryData = cloneDeep(queryData); newQueryData.legend = legendMap[queryData.queryName]; // If metric names is an empty object if (isEmpty(queryData.metric)) { // If metrics list is empty && the user haven't defined a legend then add the legend equal to the name of the query. if (newQueryData.legend === undefined || newQueryData.legend === null) { newQueryData.legend = queryData.queryName; } // If name of the query and the legend if inserted is same then add the same to the metrics object. if (queryData.queryName === newQueryData.legend) { newQueryData.metric = newQueryData.metric || {}; newQueryData.metric[queryData.queryName] = queryData.queryName; } } return newQueryData; }, ); } return legacyResponse; }