feat: query_range migration from v3/v4 -> v5 (#8192)

* feat: query_range migration from v3/v4 -> v5

* feat: cleanup files

* feat: cleanup code

* feat: metric payload improvements

* feat: metric payload improvements

* feat: data retention and qb v2 for dashboard cleanup

* feat: corrected datasource change daata updatation in qb v2

* feat: fix value panel plotting with new query v5

* feat: alert migration

* feat: fixed aggregation css

* feat: explorer pages migration

* feat: trace and logs explorer fixes
This commit is contained in:
SagarRajput-7 2025-06-17 17:57:50 +05:30 committed by Yunus M
parent 996080aaf8
commit aa544f52f3
34 changed files with 1658 additions and 75 deletions

View File

@ -3,6 +3,7 @@ const apiV1 = '/api/v1/';
export const apiV2 = '/api/v2/';
export const apiV3 = '/api/v3/';
export const apiV4 = '/api/v4/';
export const apiV5 = '/api/v5/';
export const gatewayApiV1 = '/api/gateway/v1/';
export const gatewayApiV2 = '/api/gateway/v2/';
export const apiAlertManager = '/api/alertmanager/';

View File

@ -19,6 +19,7 @@ import apiV1, {
apiV2,
apiV3,
apiV4,
apiV5,
gatewayApiV1,
gatewayApiV2,
} from './apiV1';
@ -171,6 +172,18 @@ ApiV4Instance.interceptors.response.use(
ApiV4Instance.interceptors.request.use(interceptorsRequestResponse);
//
// axios V5
export const ApiV5Instance = axios.create({
baseURL: `${ENVIRONMENT.baseURL}${apiV5}`,
});
ApiV5Instance.interceptors.response.use(
interceptorsResponse,
interceptorRejected,
);
ApiV5Instance.interceptors.request.use(interceptorsRequestResponse);
//
// axios Base
export const ApiBaseInstance = axios.create({
baseURL: `${ENVIRONMENT.baseURL}${apiV1}`,

View File

@ -0,0 +1,168 @@
// V5 Query Range Constants
import { ENTITY_VERSION_V5 } from 'constants/app';
import {
FunctionName,
RequestType,
SignalType,
Step,
} from 'types/api/v5/queryRange';
// ===================== Schema and Version Constants =====================
export const SCHEMA_VERSION_V5 = ENTITY_VERSION_V5;
export const API_VERSION_V5 = 'v5';
// ===================== Default Values =====================
export const DEFAULT_STEP_INTERVAL: Step = '60s';
export const DEFAULT_LIMIT = 100;
export const DEFAULT_OFFSET = 0;
// ===================== Request Type Constants =====================
export const REQUEST_TYPES: Record<string, RequestType> = {
SCALAR: 'scalar',
TIME_SERIES: 'time_series',
RAW: 'raw',
DISTRIBUTION: 'distribution',
} as const;
// ===================== Signal Type Constants =====================
export const SIGNAL_TYPES: Record<string, SignalType> = {
TRACES: 'traces',
LOGS: 'logs',
METRICS: 'metrics',
} as const;
// ===================== Common Aggregation Expressions =====================
export const TRACE_AGGREGATIONS = {
COUNT: 'count()',
COUNT_DISTINCT_TRACE_ID: 'count_distinct(traceID)',
AVG_DURATION: 'avg(duration_nano)',
P50_DURATION: 'p50(duration_nano)',
P95_DURATION: 'p95(duration_nano)',
P99_DURATION: 'p99(duration_nano)',
MAX_DURATION: 'max(duration_nano)',
MIN_DURATION: 'min(duration_nano)',
SUM_DURATION: 'sum(duration_nano)',
} as const;
export const LOG_AGGREGATIONS = {
COUNT: 'count()',
COUNT_DISTINCT_HOST: 'count_distinct(host.name)',
COUNT_DISTINCT_SERVICE: 'count_distinct(service.name)',
COUNT_DISTINCT_CONTAINER: 'count_distinct(container.name)',
} as const;
// ===================== Common Filter Expressions =====================
export const COMMON_FILTERS = {
// Trace filters
SERVER_SPANS: "kind_string = 'Server'",
CLIENT_SPANS: "kind_string = 'Client'",
INTERNAL_SPANS: "kind_string = 'Internal'",
ERROR_SPANS: 'http.status_code >= 400',
SUCCESS_SPANS: 'http.status_code < 400',
// Common service filters
EXCLUDE_HEALTH_CHECKS: "http.route != '/health' AND http.route != '/ping'",
HTTP_REQUESTS: "http.method != ''",
// Log filters
ERROR_LOGS: "severity_text = 'ERROR'",
WARN_LOGS: "severity_text = 'WARN'",
INFO_LOGS: "severity_text = 'INFO'",
DEBUG_LOGS: "severity_text = 'DEBUG'",
} as const;
// ===================== Common Group By Fields =====================
export const COMMON_GROUP_BY_FIELDS = {
SERVICE_NAME: {
name: 'service.name',
fieldDataType: 'string' as const,
fieldContext: 'resource' as const,
},
HTTP_METHOD: {
name: 'http.method',
fieldDataType: 'string' as const,
fieldContext: 'attribute' as const,
},
HTTP_ROUTE: {
name: 'http.route',
fieldDataType: 'string' as const,
fieldContext: 'attribute' as const,
},
HTTP_STATUS_CODE: {
name: 'http.status_code',
fieldDataType: 'int64' as const,
fieldContext: 'attribute' as const,
},
HOST_NAME: {
name: 'host.name',
fieldDataType: 'string' as const,
fieldContext: 'resource' as const,
},
CONTAINER_NAME: {
name: 'container.name',
fieldDataType: 'string' as const,
fieldContext: 'resource' as const,
},
} as const;
// ===================== Function Names =====================
export const FUNCTION_NAMES: Record<string, FunctionName> = {
CUT_OFF_MIN: 'cutOffMin',
CUT_OFF_MAX: 'cutOffMax',
CLAMP_MIN: 'clampMin',
CLAMP_MAX: 'clampMax',
ABSOLUTE: 'absolute',
RUNNING_DIFF: 'runningDiff',
LOG2: 'log2',
LOG10: 'log10',
CUM_SUM: 'cumSum',
EWMA3: 'ewma3',
EWMA5: 'ewma5',
EWMA7: 'ewma7',
MEDIAN3: 'median3',
MEDIAN5: 'median5',
MEDIAN7: 'median7',
TIME_SHIFT: 'timeShift',
ANOMALY: 'anomaly',
} as const;
// ===================== Common Step Intervals =====================
export const STEP_INTERVALS = {
FIFTEEN_SECONDS: '15s',
THIRTY_SECONDS: '30s',
ONE_MINUTE: '60s',
FIVE_MINUTES: '300s',
TEN_MINUTES: '600s',
FIFTEEN_MINUTES: '900s',
THIRTY_MINUTES: '1800s',
ONE_HOUR: '3600s',
TWO_HOURS: '7200s',
SIX_HOURS: '21600s',
TWELVE_HOURS: '43200s',
ONE_DAY: '86400s',
} as const;
// ===================== Time Range Presets =====================
export const TIME_RANGE_PRESETS = {
LAST_5_MINUTES: 5 * 60 * 1000,
LAST_15_MINUTES: 15 * 60 * 1000,
LAST_30_MINUTES: 30 * 60 * 1000,
LAST_HOUR: 60 * 60 * 1000,
LAST_3_HOURS: 3 * 60 * 60 * 1000,
LAST_6_HOURS: 6 * 60 * 60 * 1000,
LAST_12_HOURS: 12 * 60 * 60 * 1000,
LAST_24_HOURS: 24 * 60 * 60 * 1000,
LAST_3_DAYS: 3 * 24 * 60 * 60 * 1000,
LAST_7_DAYS: 7 * 24 * 60 * 60 * 1000,
} as const;

View File

@ -0,0 +1,358 @@
import { isEmpty } from 'lodash-es';
import { SuccessResponse } from 'types/api';
import { MetricRangePayloadV3 } from 'types/api/metrics/getQueryRange';
import {
DistributionData,
MetricRangePayloadV5,
RawData,
ScalarData,
TimeSeriesData,
} from 'types/api/v5/queryRange';
import { QueryDataV3 } from 'types/api/widgets/getQuery';
/**
* Converts V5 TimeSeriesData to legacy format
*/
function convertTimeSeriesData(
timeSeriesData: TimeSeriesData,
legendMap: Record<string, string>,
): QueryDataV3 {
// Convert V5 time series format to legacy QueryDataV3 format
return {
queryName: timeSeriesData.queryName,
legend: legendMap[timeSeriesData.queryName] || timeSeriesData.queryName,
series: timeSeriesData.aggregations.flatMap((aggregation) =>
aggregation.series.map((series) => ({
labels: series.labels
? Object.fromEntries(
series.labels.map((label) => [label.key.name, label.value]),
)
: {},
labelsArray: series.labels
? series.labels.map((label) => ({ [label.key.name]: label.value }))
: [],
values: series.values.map((value) => ({
timestamp: value.timestamp,
value: String(value.value),
})),
})),
),
list: null,
};
}
/**
* Helper function to collect columns from scalar data
*/
function collectColumnsFromScalarData(
scalarData: ScalarData[],
): { name: string; queryName: string; isValueColumn: boolean }[] {
const columnMap = new Map<
string,
{ name: string; queryName: string; isValueColumn: boolean }
>();
scalarData.forEach((scalar) => {
scalar.columns.forEach((col) => {
if (col.columnType === 'group') {
// For group columns, use the column name as-is
const key = `${col.name}_group`;
if (!columnMap.has(key)) {
columnMap.set(key, {
name: col.name,
queryName: '', // Group columns don't have query names
isValueColumn: false,
});
}
} else if (col.columnType === 'aggregation') {
// For aggregation columns, use the query name as the column name
const key = `${col.queryName}_aggregation`;
if (!columnMap.has(key)) {
columnMap.set(key, {
name: col.queryName, // Use query name as column name (A, B, etc.)
queryName: col.queryName,
isValueColumn: true,
});
}
}
});
});
return Array.from(columnMap.values()).sort((a, b) => {
if (a.isValueColumn !== b.isValueColumn) {
return a.isValueColumn ? 1 : -1;
}
return a.name.localeCompare(b.name);
});
}
/**
* Helper function to process scalar data rows with unified table structure
*/
function processScalarDataRows(
scalarData: ScalarData[],
): { data: Record<string, any> }[] {
// First, identify all group columns and all value columns
const allGroupColumns = new Set<string>();
const allValueColumns = new Set<string>();
scalarData.forEach((scalar) => {
scalar.columns.forEach((col) => {
if (col.columnType === 'group') {
allGroupColumns.add(col.name);
} else if (col.columnType === 'aggregation') {
// Use query name for value columns to match expected format
allValueColumns.add(col.queryName);
}
});
});
// Create a unified row structure
const unifiedRows = new Map<string, Record<string, any>>();
// Process each scalar result
scalarData.forEach((scalar) => {
scalar.data.forEach((dataRow) => {
const groupColumns = scalar.columns.filter(
(col) => col.columnType === 'group',
);
// Create row key based on group columns
let rowKey: string;
const groupValues: Record<string, any> = {};
if (groupColumns.length > 0) {
const keyParts: string[] = [];
groupColumns.forEach((col, index) => {
const value = dataRow[index];
keyParts.push(String(value));
groupValues[col.name] = value;
});
rowKey = keyParts.join('|');
} else {
// For scalar values without grouping, create a default row
rowKey = 'default_row';
// Set all group columns to 'n/a' for this row
Array.from(allGroupColumns).forEach((groupCol) => {
groupValues[groupCol] = 'n/a';
});
}
// Get or create the unified row
if (!unifiedRows.has(rowKey)) {
const newRow: Record<string, any> = { ...groupValues };
// Initialize all value columns to 'n/a'
Array.from(allValueColumns).forEach((valueCol) => {
newRow[valueCol] = 'n/a';
});
unifiedRows.set(rowKey, newRow);
}
const row = unifiedRows.get(rowKey)!;
// Fill in the aggregation values using query name as column name
scalar.columns.forEach((col, colIndex) => {
if (col.columnType === 'aggregation') {
row[col.queryName] = dataRow[colIndex];
}
});
});
});
return Array.from(unifiedRows.values()).map((rowData) => ({
data: rowData,
}));
}
/**
* Converts V5 ScalarData array to legacy format with table structure
*/
function convertScalarDataArrayToTable(
scalarDataArray: ScalarData[],
legendMap: Record<string, string>,
): QueryDataV3 {
// If no scalar data, return empty structure
if (!scalarDataArray || scalarDataArray.length === 0) {
return {
queryName: '',
legend: '',
series: null,
list: null,
table: {
columns: [],
rows: [],
},
};
}
// Collect columns and process rows
const columns = collectColumnsFromScalarData(scalarDataArray);
const rows = processScalarDataRows(scalarDataArray);
// Get the primary query name
const primaryQuery = scalarDataArray.find((s) =>
s.columns.some((c) => c.columnType === 'aggregation'),
);
const queryName =
primaryQuery?.columns.find((c) => c.columnType === 'aggregation')
?.queryName ||
scalarDataArray[0]?.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<string, string>,
): 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<string, string>,
): 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<string, string>,
): 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 a single table
const combinedTable = convertScalarDataArrayToTable(scalarData, legendMap);
return {
resultType: 'scalar',
result: [combinedTable],
};
}
case 'raw': {
const rawData = v5Data.data.results as RawData[];
return {
resultType: 'raw',
result: rawData.map((raw) => convertRawData(raw, 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
*/
export function convertV5ResponseToLegacy(
v5Response: SuccessResponse<MetricRangePayloadV5>,
legendMap: Record<string, string>,
// formatForWeb?: boolean,
): SuccessResponse<MetricRangePayloadV3> {
const { payload } = v5Response;
const v5Data = payload?.data;
// todo - sagar
// If formatForWeb is true, return as-is (like existing logic)
// Exception: scalar data should always be converted to table format
// if (formatForWeb && v5Data?.type !== 'scalar') {
// return v5Response as any;
// }
// Convert based on V5 response type
const convertedData = convertV5DataByType(v5Data, legendMap);
// Create legacy-compatible response structure
const legacyResponse: SuccessResponse<MetricRangePayloadV3> = {
...v5Response,
payload: {
data: convertedData,
},
};
// 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 = 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) {
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;
}

View File

@ -0,0 +1,51 @@
import { ApiV5Instance } from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import { ENTITY_VERSION_V5 } from 'constants/app';
import { ErrorResponse, SuccessResponse } from 'types/api';
import {
MetricRangePayloadV5,
QueryRangePayloadV5,
} from 'types/api/v5/queryRange';
export const getQueryRangeV5 = async (
props: QueryRangePayloadV5,
version: string,
signal: AbortSignal,
headers?: Record<string, string>,
): Promise<SuccessResponse<MetricRangePayloadV5> | ErrorResponse> => {
try {
if (version && version === ENTITY_VERSION_V5) {
const response = await ApiV5Instance.post('/query_range', props, {
signal,
headers,
});
return {
statusCode: 200,
error: null,
message: response.data.status,
payload: response.data,
params: props,
};
}
// Default V5 behavior
const response = await ApiV5Instance.post('/query_range', props, {
signal,
headers,
});
return {
statusCode: 200,
error: null,
message: response.data.status,
payload: response.data,
params: props,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);
}
};
export default getQueryRangeV5;

View File

@ -0,0 +1,384 @@
/* eslint-disable sonarjs/cognitive-complexity */
import { PANEL_TYPES } from 'constants/queryBuilder';
import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults';
import getStartEndRangeTime from 'lib/getStartEndRangeTime';
import { mapQueryDataToApi } from 'lib/newQueryBuilder/queryBuilderMappers/mapQueryDataToApi';
import { isEmpty } from 'lodash-es';
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
import {
IBuilderQuery,
QueryFunctionProps,
} from 'types/api/queryBuilder/queryBuilderData';
import {
BaseBuilderQuery,
FieldContext,
FieldDataType,
FunctionName,
GroupByKey,
LogAggregation,
MetricAggregation,
OrderBy,
QueryEnvelope,
QueryFunction,
QueryRangePayloadV5,
QueryType,
RequestType,
TelemetryFieldKey,
TraceAggregation,
} from 'types/api/v5/queryRange';
import { EQueryType } from 'types/common/dashboard';
import { DataSource } from 'types/common/queryBuilder';
type PrepareQueryRangePayloadV5Result = {
queryPayload: QueryRangePayloadV5;
legendMap: Record<string, string>;
};
/**
* Maps panel types to V5 request types
*/
function mapPanelTypeToRequestType(panelType: PANEL_TYPES): RequestType {
switch (panelType) {
case PANEL_TYPES.TIME_SERIES:
case PANEL_TYPES.BAR:
return 'time_series';
case PANEL_TYPES.TABLE:
case PANEL_TYPES.PIE:
case PANEL_TYPES.VALUE:
case PANEL_TYPES.TRACE:
return 'scalar';
case PANEL_TYPES.LIST:
return 'raw';
case PANEL_TYPES.HISTOGRAM:
return 'distribution';
default:
return '';
}
}
/**
* Gets signal type from data source
*/
function getSignalType(dataSource: string): 'traces' | 'logs' | 'metrics' {
if (dataSource === 'traces') return 'traces';
if (dataSource === 'logs') return 'logs';
return 'metrics';
}
/**
* Creates base spec for builder queries
*/
function createBaseSpec(
queryData: IBuilderQuery,
requestType: RequestType,
): BaseBuilderQuery {
return {
stepInterval: queryData.stepInterval,
disabled: queryData.disabled,
filter: queryData?.filter?.expression ? queryData.filter : undefined,
groupBy:
queryData.groupBy?.length > 0
? queryData.groupBy.map(
(item: any): GroupByKey => ({
name: item.key,
fieldDataType: item?.dataType,
fieldContext: item?.type,
description: item?.description,
unit: item?.unit,
signal: item?.signal,
materialized: item?.materialized,
}),
)
: undefined,
limit: isEmpty(queryData.limit)
? queryData?.pageSize ?? undefined
: queryData.limit ?? undefined,
offset: requestType === 'raw' ? queryData.offset : undefined,
order:
queryData.orderBy.length > 0
? queryData.orderBy.map(
(order: any): OrderBy => ({
key: {
name: order.columnName,
},
direction: order.order,
}),
)
: undefined,
// legend: isEmpty(queryData.legend) ? undefined : queryData.legend,
having: isEmpty(queryData.havingExpression)
? undefined
: queryData?.havingExpression,
functions: isEmpty(queryData.functions)
? undefined
: queryData.functions.map(
(func: QueryFunctionProps): QueryFunction => ({
name: func.name as FunctionName,
args: func.args.map((arg) => ({
// name: arg.name,
value: arg,
})),
}),
),
selectFields: isEmpty(queryData.selectColumns)
? undefined
: queryData.selectColumns?.map(
(column: BaseAutocompleteData): TelemetryFieldKey => ({
name: column.key,
fieldDataType: column?.dataType as FieldDataType,
fieldContext: column?.type as FieldContext,
}),
),
};
}
// Utility to parse aggregation expressions with optional alias
export function parseAggregations(
expression: string,
): { expression: string; alias?: string }[] {
const result: { expression: string; alias?: string }[] = [];
const regex = /([a-zA-Z0-9_]+\([^)]*\))(?:\s*as\s+([a-zA-Z0-9_]+))?/g;
let match = regex.exec(expression);
while (match !== null) {
const expr = match[1];
const alias = match[2];
if (alias) {
result.push({ expression: expr, alias });
} else {
result.push({ expression: expr });
}
match = regex.exec(expression);
}
return result;
}
function createAggregation(
queryData: any,
): TraceAggregation[] | LogAggregation[] | MetricAggregation[] {
if (queryData.dataSource === DataSource.METRICS) {
return [
{
metricName: queryData?.aggregateAttribute?.key,
temporality: queryData?.aggregateAttribute?.temporality,
timeAggregation: queryData?.timeAggregation,
spaceAggregation: queryData?.spaceAggregation,
},
];
}
if (queryData.aggregations?.length > 0) {
return isEmpty(parseAggregations(queryData.aggregations?.[0].expression))
? [{ expression: 'count()' }]
: parseAggregations(queryData.aggregations?.[0].expression);
}
return [{ expression: 'count()' }];
}
/**
* Converts query builder data to V5 builder queries
*/
function convertBuilderQueriesToV5(
builderQueries: Record<string, any>, // eslint-disable-line @typescript-eslint/no-explicit-any
requestType: RequestType,
): QueryEnvelope[] {
return Object.entries(builderQueries).map(
([queryName, queryData]): QueryEnvelope => {
const signal = getSignalType(queryData.dataSource);
const baseSpec = createBaseSpec(queryData, requestType);
let spec: QueryEnvelope['spec'];
const aggregations = createAggregation(queryData);
switch (signal) {
case 'traces':
spec = {
name: queryName,
signal: 'traces' as const,
...baseSpec,
aggregations: aggregations as TraceAggregation[],
limit: baseSpec?.limit ?? (requestType === 'raw' ? 10 : undefined),
};
break;
case 'logs':
spec = {
name: queryName,
signal: 'logs' as const,
...baseSpec,
aggregations: aggregations as LogAggregation[],
limit: baseSpec?.limit ?? (requestType === 'raw' ? 10 : undefined),
};
break;
case 'metrics':
default:
spec = {
name: queryName,
signal: 'metrics' as const,
...baseSpec,
aggregations: aggregations as MetricAggregation[],
// reduceTo: queryData.reduceTo,
limit: baseSpec?.limit ?? (requestType === 'raw' ? 10 : undefined),
};
break;
}
return {
type: 'builder_query' as QueryType,
spec,
};
},
);
}
/**
* Converts PromQL queries to V5 format
*/
function convertPromQueriesToV5(
promQueries: Record<string, any>, // eslint-disable-line @typescript-eslint/no-explicit-any
): QueryEnvelope[] {
return Object.entries(promQueries).map(
([queryName, queryData]): QueryEnvelope => ({
type: 'promql' as QueryType,
spec: {
name: queryName,
query: queryData.query,
disabled: queryData.disabled || false,
step: queryData.stepInterval,
stats: false, // PromQL specific field
},
}),
);
}
/**
* Converts ClickHouse queries to V5 format
*/
function convertClickHouseQueriesToV5(
chQueries: Record<string, any>, // eslint-disable-line @typescript-eslint/no-explicit-any
): QueryEnvelope[] {
return Object.entries(chQueries).map(
([queryName, queryData]): QueryEnvelope => ({
type: 'clickhouse_sql' as QueryType,
spec: {
name: queryName,
query: queryData.query,
disabled: queryData.disabled || false,
// ClickHouse doesn't have step or stats like PromQL
},
}),
);
}
/**
* Converts query formulas to V5 format
*/
function convertFormulasToV5(
formulas: Record<string, any>, // eslint-disable-line @typescript-eslint/no-explicit-any
): QueryEnvelope[] {
return Object.entries(formulas).map(
([queryName, formulaData]): QueryEnvelope => ({
type: 'builder_formula' as QueryType,
spec: {
name: queryName,
expression: formulaData.expression || '',
functions: formulaData.functions,
},
}),
);
}
/**
* Helper function to reduce query arrays to objects
*/
function reduceQueriesToObject(
queryArray: any[], // eslint-disable-line @typescript-eslint/no-explicit-any
): { queries: Record<string, any>; legends: Record<string, string> } {
// eslint-disable-line @typescript-eslint/no-explicit-any
const legends: Record<string, string> = {};
const queries = queryArray.reduce((acc, queryItem) => {
if (!queryItem.query) return acc;
acc[queryItem.name] = queryItem;
legends[queryItem.name] = queryItem.legend;
return acc;
}, {} as Record<string, any>); // eslint-disable-line @typescript-eslint/no-explicit-any
return { queries, legends };
}
/**
* Prepares V5 query range payload from GetQueryResultsProps
*/
export const prepareQueryRangePayloadV5 = ({
query,
globalSelectedInterval,
graphType,
selectedTime,
tableParams,
variables = {},
start: startTime,
end: endTime,
}: GetQueryResultsProps): PrepareQueryRangePayloadV5Result => {
let legendMap: Record<string, string> = {};
const requestType = mapPanelTypeToRequestType(graphType);
let queries: QueryEnvelope[] = [];
switch (query.queryType) {
case EQueryType.QUERY_BUILDER: {
const { queryData: data, queryFormulas } = query.builder;
const currentQueryData = mapQueryDataToApi(data, 'queryName', tableParams);
const currentFormulas = mapQueryDataToApi(queryFormulas, 'queryName');
// Combine legend maps
legendMap = {
...currentQueryData.newLegendMap,
...currentFormulas.newLegendMap,
};
// Convert builder queries
const builderQueries = convertBuilderQueriesToV5(
currentQueryData.data,
requestType,
);
// Convert formulas as separate query type
const formulaQueries = convertFormulasToV5(currentFormulas.data);
// Combine both types
queries = [...builderQueries, ...formulaQueries];
break;
}
case EQueryType.PROM: {
const promQueries = reduceQueriesToObject(query[query.queryType]);
queries = convertPromQueriesToV5(promQueries.queries);
legendMap = promQueries.legends;
break;
}
case EQueryType.CLICKHOUSE: {
const chQueries = reduceQueriesToObject(query[query.queryType]);
queries = convertClickHouseQueriesToV5(chQueries.queries);
legendMap = chQueries.legends;
break;
}
default:
break;
}
// Calculate time range
const { start, end } = getStartEndRangeTime({
type: selectedTime,
interval: globalSelectedInterval,
});
// Create V5 payload
const queryPayload: QueryRangePayloadV5 = {
schemaVersion: 'v1',
start: startTime ? startTime * 1e3 : parseInt(start, 10) * 1e3,
end: endTime ? endTime * 1e3 : parseInt(end, 10) * 1e3,
requestType,
compositeQuery: {
queries,
},
variables,
};
return { legendMap, queryPayload };
};

View File

@ -0,0 +1,8 @@
// V5 API exports
export * from './queryRange/constants';
export { convertV5ResponseToLegacy } from './queryRange/convertV5Response';
export { getQueryRangeV5 } from './queryRange/getQueryRange';
export { prepareQueryRangePayloadV5 } from './queryRange/prepareQueryRangePayloadV5';
// Export types from proper location
export * from 'types/api/v5/queryRange';

View File

@ -15,6 +15,7 @@ import { Button } from 'antd';
import { useQueryBuilderV2Context } from 'components/QueryBuilderV2/QueryBuilderV2Context';
import { X } from 'lucide-react';
import { useEffect, useMemo, useRef, useState } from 'react';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
const havingOperators = [
{
@ -67,16 +68,30 @@ const conjunctions = [
{ label: 'OR', value: 'OR' },
];
function HavingFilter({ onClose }: { onClose: () => void }): JSX.Element {
function HavingFilter({
onClose,
onChange,
queryData,
}: {
onClose: () => void;
onChange: (value: string) => void;
queryData: IBuilderQuery;
}): JSX.Element {
const { aggregationOptions } = useQueryBuilderV2Context();
const [input, setInput] = useState('');
const [input, setInput] = useState(
queryData?.havingExpression?.expression || '',
);
const [isFocused, setIsFocused] = useState(false);
const editorRef = useRef<EditorView | null>(null);
const [options, setOptions] = useState<{ label: string; value: string }[]>([]);
// Effect to handle focus state and trigger suggestions
const handleChange = (value: string): void => {
setInput(value);
onChange(value);
};
useEffect(() => {
if (isFocused && editorRef.current && options.length > 0) {
startCompletion(editorRef.current);
@ -237,9 +252,10 @@ function HavingFilter({ onClose }: { onClose: () => void }): JSX.Element {
<div className="having-filter-select-container">
<CodeMirror
value={input}
onChange={handleChange}
theme={copilot}
onChange={setInput}
className="having-filter-select-editor"
width="100%"
extensions={[
havingAutocomplete,
javascript({ jsx: false, typescript: false }),

View File

@ -7,6 +7,7 @@ import { GroupByFilter } from 'container/QueryBuilder/filters/GroupByFilter/Grou
import { OrderByFilter } from 'container/QueryBuilder/filters/OrderByFilter/OrderByFilter';
import { ReduceToFilter } from 'container/QueryBuilder/filters/ReduceToFilter/ReduceToFilter';
import { useQueryOperations } from 'hooks/queryBuilder/useQueryBuilderOperations';
import { isEmpty } from 'lodash-es';
import { BarChart2, ScrollText, X } from 'lucide-react';
import { useCallback, useEffect, useState } from 'react';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
@ -98,22 +99,38 @@ function QueryAddOns({
return;
}
let filteredAddOns: AddOn[];
if (panelType === PANEL_TYPES.VALUE) {
// Filter out all add-ons except legend format
setAddOns((prevAddOns) =>
prevAddOns.filter((addOn) => addOn.key === ADD_ONS_KEYS.LEGEND_FORMAT),
filteredAddOns = ADD_ONS.filter(
(addOn) => addOn.key === ADD_ONS_KEYS.LEGEND_FORMAT,
);
} else {
setAddOns(Object.values(ADD_ONS));
filteredAddOns = Object.values(ADD_ONS);
// Filter out group_by for metrics data source
if (query.dataSource === DataSource.METRICS) {
filteredAddOns = filteredAddOns.filter(
(addOn) => addOn.key !== ADD_ONS_KEYS.GROUP_BY,
);
}
}
// add reduce to if showReduceTo is true
if (showReduceTo) {
setAddOns((prevAddOns) => [...prevAddOns, REDUCE_TO]);
filteredAddOns = [...filteredAddOns, REDUCE_TO];
}
setAddOns(filteredAddOns);
// Filter selectedViews to only include add-ons present in filteredAddOns
setSelectedViews((prevSelectedViews) =>
prevSelectedViews.filter((view) =>
filteredAddOns.some((addOn) => addOn.key === view.key),
),
);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [panelType, isListViewPanel]);
}, [panelType, isListViewPanel, query.dataSource]);
const handleOptionClick = (e: RadioChangeEvent): void => {
if (selectedViews.find((view) => view.key === e.target.value.key)) {
@ -167,6 +184,15 @@ function QueryAddOns({
[handleChangeQueryData],
);
const handleChangeHaving = useCallback(
(value: string) => {
handleChangeQueryData('havingExpression', {
expression: value,
});
},
[handleChangeQueryData],
);
return (
<div className="query-add-ons">
{selectedViews.length > 0 && (
@ -204,6 +230,8 @@ function QueryAddOns({
selectedViews.filter((view) => view.key !== 'having'),
);
}}
onChange={handleChangeHaving}
queryData={query}
/>
</div>
</div>
@ -214,6 +242,7 @@ function QueryAddOns({
<InputWithLabel
label="Limit"
onChange={handleChangeLimit}
initialValue={query?.limit ?? undefined}
placeholder="Enter limit"
onClose={(): void => {
setSelectedViews(selectedViews.filter((view) => view.key !== 'limit'));
@ -233,11 +262,13 @@ function QueryAddOns({
isListViewPanel={isListViewPanel}
/>
</div>
<Button
className="close-btn periscope-btn ghost"
icon={<X size={16} />}
onClick={(): void => handleRemoveView('order_by')}
/>
{!isListViewPanel && (
<Button
className="close-btn periscope-btn ghost"
icon={<X size={16} />}
onClick={(): void => handleRemoveView('order_by')}
/>
)}
</div>
</div>
)}
@ -265,6 +296,7 @@ function QueryAddOns({
label="Legend format"
placeholder="Write legend format"
onChange={handleChangeQueryLegend}
initialValue={isEmpty(query?.legend) ? undefined : query?.legend}
onClose={(): void => {
setSelectedViews(
selectedViews.filter((view) => view.key !== 'legend_format'),

View File

@ -52,6 +52,7 @@
flex-direction: row;
align-items: center;
flex: 1;
min-width: 400px;
.query-aggregation-select-editor {
border-radius: 2px;

View File

@ -3,6 +3,7 @@ import './QueryAggregation.styles.scss';
import InputWithLabel from 'components/InputWithLabel/InputWithLabel';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { useMemo } from 'react';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import { DataSource } from 'types/common/queryBuilder';
import QueryAggregationSelect from './QueryAggregationSelect';
@ -11,10 +12,14 @@ function QueryAggregationOptions({
dataSource,
panelType,
onAggregationIntervalChange,
onChange,
queryData,
}: {
dataSource: DataSource;
panelType?: string;
onAggregationIntervalChange: (value: number) => void;
onChange?: (value: string) => void;
queryData: IBuilderQuery;
}): JSX.Element {
const showAggregationInterval = useMemo(() => {
// eslint-disable-next-line sonarjs/prefer-single-boolean-return
@ -36,14 +41,14 @@ function QueryAggregationOptions({
return (
<div className="query-aggregation-container">
<div className="aggregation-container">
<QueryAggregationSelect />
<QueryAggregationSelect onChange={onChange} queryData={queryData} />
{showAggregationInterval && (
<div className="query-aggregation-interval">
<div className="query-aggregation-interval-label">every</div>
<div className="query-aggregation-interval-input-container">
<InputWithLabel
initialValue="60"
initialValue={queryData.stepInterval ? queryData.stepInterval : '60'}
className="query-aggregation-interval-input"
label="Seconds"
placeholder="60"
@ -62,6 +67,7 @@ function QueryAggregationOptions({
QueryAggregationOptions.defaultProps = {
panelType: null,
onChange: undefined,
};
export default QueryAggregationOptions;

View File

@ -28,10 +28,10 @@ import CodeMirror, {
import { getAggregateAttribute } from 'api/queryBuilder/getAggregateAttribute';
import { QueryBuilderKeys } from 'constants/queryBuilder';
import { tracesAggregateOperatorOptions } from 'constants/queryBuilderOperators';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useQuery } from 'react-query';
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import { TracesAggregatorOperator } from 'types/common/queryBuilder';
import { useQueryBuilderV2Context } from '../../QueryBuilderV2Context';
@ -113,11 +113,18 @@ function getFunctionContextAtCursor(
}
// eslint-disable-next-line react/no-this-in-sfc
function QueryAggregationSelect(): JSX.Element {
const { currentQuery } = useQueryBuilder();
function QueryAggregationSelect({
onChange,
queryData,
}: {
onChange?: (value: string) => void;
queryData: IBuilderQuery;
}): JSX.Element {
const { setAggregationOptions } = useQueryBuilderV2Context();
const queryData = currentQuery.builder.queryData[0];
const [input, setInput] = useState('');
const [input, setInput] = useState(
queryData?.aggregations?.map((i: any) => i.expression).join(' ') || '',
);
const [cursorPos, setCursorPos] = useState(0);
const [functionArgPairs, setFunctionArgPairs] = useState<
{ func: string; arg: string }[]
@ -416,7 +423,10 @@ function QueryAggregationSelect(): JSX.Element {
<div className="query-aggregation-select-container">
<CodeMirror
value={input}
onChange={setInput}
onChange={(value): void => {
setInput(value);
onChange?.(value);
}}
className="query-aggregation-select-editor"
theme={copilot}
extensions={[
@ -458,4 +468,8 @@ function QueryAggregationSelect(): JSX.Element {
);
}
QueryAggregationSelect.defaultProps = {
onChange: undefined,
};
export default QueryAggregationSelect;

View File

@ -28,6 +28,7 @@ import {
IQueryContext,
IValidationResult,
} from 'types/antlrQueryTypes';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import { QueryKeySuggestionsProps } from 'types/api/querySuggestions/types';
import { queryOperatorSuggestions, validateQuery } from 'utils/antlrQueryUtils';
import { getQueryContextAtCursor } from 'utils/queryContextUtils';
@ -61,9 +62,14 @@ const disallowMultipleSpaces: Extension = EditorView.inputHandler.of(
},
);
// eslint-disable-next-line sonarjs/cognitive-complexity
function QuerySearch(): JSX.Element {
const [query, setQuery] = useState<string>('');
function QuerySearch({
onChange,
queryData,
}: {
onChange: (value: string) => void;
queryData: IBuilderQuery;
}): JSX.Element {
const [query, setQuery] = useState<string>(queryData.filter?.expression || '');
const [valueSuggestions, setValueSuggestions] = useState<any[]>([
{ label: 'error', type: 'value' },
{ label: 'frontend', type: 'value' },
@ -343,6 +349,7 @@ function QuerySearch(): JSX.Element {
const handleChange = (value: string): void => {
setQuery(value);
handleQueryChange(value);
onChange(value);
};
const handleQueryValidation = (newQuery: string): void => {
@ -366,6 +373,13 @@ function QuerySearch(): JSX.Element {
}
};
useEffect(() => {
if (query) {
handleQueryValidation(query);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const handleExampleClick = (exampleQuery: string): void => {
// If there's an existing query, append the example with AND
const newQuery = query ? `${query} AND ${exampleQuery}` : exampleQuery;

View File

@ -10,6 +10,7 @@ import { useQueryOperations } from 'hooks/queryBuilder/useQueryBuilderOperations
import { Copy, Ellipsis, Trash } from 'lucide-react';
import { memo, useCallback, useMemo, useState } from 'react';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import { HandleChangeQueryDataV5 } from 'types/common/operations.types';
import { DataSource } from 'types/common/queryBuilder';
import MetricsAggregateSection from './MerticsAggregateSection/MetricsAggregateSection';
@ -80,6 +81,26 @@ export const QueryV2 = memo(function QueryV2({
[handleChangeQueryData],
);
const handleSearchChange = useCallback(
(value: string) => {
(handleChangeQueryData as HandleChangeQueryDataV5)('filter', {
expression: value,
});
},
[handleChangeQueryData],
);
const handleChangeAggregation = useCallback(
(value: string) => {
(handleChangeQueryData as HandleChangeQueryDataV5)('aggregations', [
{
expression: value,
},
]);
},
[handleChangeQueryData],
);
return (
<div
className={cx('query-v2', { 'where-clause-view': showOnlyWhereClause })}
@ -154,7 +175,7 @@ export const QueryV2 = memo(function QueryV2({
<div className="qb-search-filter-container">
<div className="query-search-container">
<QuerySearch />
<QuerySearch onChange={handleSearchChange} queryData={query} />
</div>
{showSpanScopeSelector && (
@ -166,13 +187,17 @@ export const QueryV2 = memo(function QueryV2({
</div>
</div>
{!showOnlyWhereClause && !isListViewPanel && (
<QueryAggregation
dataSource={dataSource}
panelType={panelType || undefined}
onAggregationIntervalChange={handleChangeAggregateEvery}
/>
)}
{!showOnlyWhereClause &&
!isListViewPanel &&
dataSource !== DataSource.METRICS && (
<QueryAggregation
dataSource={dataSource}
panelType={panelType || undefined}
onAggregationIntervalChange={handleChangeAggregateEvery}
onChange={handleChangeAggregation}
queryData={query}
/>
)}
{!showOnlyWhereClause && dataSource === DataSource.METRICS && (
<MetricsAggregateSection

View File

@ -15,3 +15,4 @@ export const DASHBOARD_TIME_IN_DURATION = 'refreshInterval';
export const DEFAULT_ENTITY_VERSION = 'v3';
export const ENTITY_VERSION_V4 = 'v4';
export const ENTITY_VERSION_V5 = 'v5';

View File

@ -2,7 +2,7 @@ import './ChartPreview.styles.scss';
import { InfoCircleOutlined } from '@ant-design/icons';
import Spinner from 'components/Spinner';
import { DEFAULT_ENTITY_VERSION } from 'constants/app';
import { ENTITY_VERSION_V5 } from 'constants/app';
import { FeatureKeys } from 'constants/features';
import { QueryParams } from 'constants/query';
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
@ -144,7 +144,8 @@ function ChartPreview({
},
originalGraphType: graphType,
},
alertDef?.version || DEFAULT_ENTITY_VERSION,
// alertDef?.version || DEFAULT_ENTITY_VERSION,
ENTITY_VERSION_V5,
{
queryKey: [
'chartPreview',

View File

@ -10,7 +10,7 @@ import cx from 'classnames';
import { ToggleGraphProps } from 'components/Graph/types';
import Spinner from 'components/Spinner';
import TimePreference from 'components/TimePreferenceDropDown';
import { DEFAULT_ENTITY_VERSION } from 'constants/app';
import { ENTITY_VERSION_V5 } from 'constants/app';
import { QueryParams } from 'constants/query';
import { PANEL_TYPES } from 'constants/queryBuilder';
import {
@ -122,7 +122,8 @@ function FullView({
const response = useGetQueryRange(
requestData,
selectedDashboard?.data?.version || version || DEFAULT_ENTITY_VERSION,
// selectedDashboard?.data?.version || version || DEFAULT_ENTITY_VERSION,
ENTITY_VERSION_V5,
{
queryKey: [widget?.query, widget?.panelTypes, requestData, version],
enabled: !isDependedDataLoaded,

View File

@ -1,5 +1,5 @@
import logEvent from 'api/common/logEvent';
import { DEFAULT_ENTITY_VERSION } from 'constants/app';
import { ENTITY_VERSION_V5 } from 'constants/app';
import { QueryParams } from 'constants/query';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { CustomTimeType } from 'container/TopNav/DateTimeSelectionV2/config';
@ -209,7 +209,8 @@ function GridCardGraph({
end: customTimeRange?.endTime || end,
originalGraphType: widget?.panelTypes,
},
version || DEFAULT_ENTITY_VERSION,
ENTITY_VERSION_V5,
// version || DEFAULT_ENTITY_VERSION,
{
queryKey: [
maxTime,

View File

@ -6,7 +6,7 @@ import { getQueryStats, WsDataEvent } from 'api/common/getQueryStats';
import logEvent from 'api/common/logEvent';
import { getYAxisFormattedValue } from 'components/Graph/yAxisConfig';
import LogsFormatOptionsMenu from 'components/LogsFormatOptionsMenu/LogsFormatOptionsMenu';
import { ENTITY_VERSION_V4 } from 'constants/app';
import { ENTITY_VERSION_V5 } from 'constants/app';
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
import { LOCALSTORAGE } from 'constants/localStorage';
import { AVAILABLE_EXPORT_PANEL_TYPES } from 'constants/panelTypes';
@ -248,7 +248,8 @@ function LogsExplorerViewsContainer({
} = useGetExplorerQueryRange(
listChartQuery,
PANEL_TYPES.TIME_SERIES,
ENTITY_VERSION_V4,
// ENTITY_VERSION_V4,
ENTITY_VERSION_V5,
{
enabled: !!listChartQuery && panelType === PANEL_TYPES.LIST,
},
@ -268,7 +269,8 @@ function LogsExplorerViewsContainer({
} = useGetExplorerQueryRange(
requestData,
panelType,
ENTITY_VERSION_V4,
// ENTITY_VERSION_V4,
ENTITY_VERSION_V5,
{
keepPreviousData: true,
enabled: !isLimit && !!requestData,

View File

@ -1,5 +1,5 @@
import classNames from 'classnames';
import { ENTITY_VERSION_V4 } from 'constants/app';
import { ENTITY_VERSION_V5 } from 'constants/app';
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import { BuilderUnitsFilter } from 'container/QueryBuilder/filters/BuilderUnitsFilter/BuilderUnits';
@ -63,7 +63,7 @@ function TimeSeries({ showOneChartPerQuery }: TimeSeriesProps): JSX.Element {
queryKey: [
REACT_QUERY_KEY.GET_QUERY_RANGE,
payload,
ENTITY_VERSION_V4,
ENTITY_VERSION_V5,
globalSelectedTime,
maxTime,
minTime,
@ -80,7 +80,8 @@ function TimeSeries({ showOneChartPerQuery }: TimeSeriesProps): JSX.Element {
dataSource: DataSource.METRICS,
},
},
ENTITY_VERSION_V4,
// ENTITY_VERSION_V4,
ENTITY_VERSION_V5,
),
enabled: !!payload,
})),

View File

@ -43,6 +43,7 @@ function WidgetGraphContainer({
if (
selectedGraph !== PANEL_TYPES.LIST &&
selectedGraph !== PANEL_TYPES.VALUE &&
queryResponse.data?.payload.data?.result?.length === 0
) {
return (
@ -52,7 +53,7 @@ function WidgetGraphContainer({
);
}
if (
selectedGraph === PANEL_TYPES.LIST &&
(selectedGraph === PANEL_TYPES.LIST || selectedGraph === PANEL_TYPES.VALUE) &&
queryResponse.data?.payload?.data?.newResult?.data?.result?.length === 0
) {
return (

View File

@ -1,11 +1,10 @@
import './LeftContainer.styles.scss';
import { DEFAULT_ENTITY_VERSION } from 'constants/app';
import { ENTITY_VERSION_V5 } from 'constants/app';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useDashboard } from 'providers/Dashboard/Dashboard';
import { memo, useEffect } from 'react';
import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
@ -30,7 +29,7 @@ function LeftContainer({
setQueryResponse,
}: WidgetGraphProps): JSX.Element {
const { stagedQuery } = useQueryBuilder();
const { selectedDashboard } = useDashboard();
// const { selectedDashboard } = useDashboard();
const { selectedTime: globalSelectedInterval } = useSelector<
AppState,
@ -38,7 +37,8 @@ function LeftContainer({
>((state) => state.globalTime);
const queryResponse = useGetQueryRange(
requestData,
selectedDashboard?.data?.version || DEFAULT_ENTITY_VERSION,
// selectedDashboard?.data?.version || DEFAULT_ENTITY_VERSION,
ENTITY_VERSION_V5,
{
enabled: !!stagedQuery,
retry: false,

View File

@ -9,9 +9,17 @@ function ValuePanelWrapper({
}: PanelWrapperProps): JSX.Element {
const { yAxisUnit, thresholds } = widget;
const data = getUPlotChartData(queryResponse?.data?.payload);
const dataNew = Object.values(
queryResponse?.data?.payload?.data?.newResult?.data?.result[0]?.table
?.rows?.[0]?.data || {},
);
// this is for handling both query_range v3 and v5 responses
const gridValueData = data?.[0].length > 0 ? data : [[0], dataNew];
return (
<GridValueComponent
data={data}
data={gridValueData}
yAxisUnit={yAxisUnit}
thresholds={thresholds}
/>

View File

@ -5,13 +5,13 @@ export function removePrefix(str: string, type: string): string {
const resourcePrefix = `${MetricsType.Resource}_`;
const scopePrefix = `${MetricsType.Scope}_`;
if (str.startsWith(tagPrefix)) {
if (str?.startsWith(tagPrefix)) {
return str.slice(tagPrefix.length);
}
if (str.startsWith(resourcePrefix)) {
if (str?.startsWith(resourcePrefix)) {
return str.slice(resourcePrefix.length);
}
if (str.startsWith(scopePrefix) && type === MetricsType.Scope) {
if (str?.startsWith(scopePrefix) && type === MetricsType.Scope) {
return str.slice(scopePrefix.length);
}

View File

@ -1,6 +1,6 @@
import './TimeSeriesView.styles.scss';
import { ENTITY_VERSION_V4 } from 'constants/app';
import { ENTITY_VERSION_V5 } from 'constants/app';
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
@ -54,7 +54,8 @@ function TimeSeriesViewContainer({
dataSource,
},
},
ENTITY_VERSION_V4,
// ENTITY_VERSION_V4,
ENTITY_VERSION_V5,
{
queryKey: [
REACT_QUERY_KEY.GET_QUERY_RANGE,

View File

@ -3,7 +3,7 @@ import './ListView.styles.scss';
import { Select } from 'antd';
import logEvent from 'api/common/logEvent';
import { ResizeTable } from 'components/ResizeTable';
import { ENTITY_VERSION_V4 } from 'constants/app';
import { ENTITY_VERSION_V5 } from 'constants/app';
import { LOCALSTORAGE } from 'constants/localStorage';
import { QueryParams } from 'constants/query';
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
@ -110,7 +110,8 @@ function ListView({ isFilterApplied }: ListViewProps): JSX.Element {
selectColumns: options?.selectColumns,
},
},
ENTITY_VERSION_V4,
// ENTITY_VERSION_V4,
ENTITY_VERSION_V5,
{
queryKey,
enabled:

View File

@ -1,5 +1,5 @@
import { Space } from 'antd';
import { ENTITY_VERSION_V4 } from 'constants/app';
import { ENTITY_VERSION_V5 } from 'constants/app';
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import { QueryTable } from 'container/QueryTable';
@ -28,7 +28,8 @@ function TableView(): JSX.Element {
dataSource: 'traces',
},
},
ENTITY_VERSION_V4,
// ENTITY_VERSION_V4,
ENTITY_VERSION_V5,
{
queryKey: [
REACT_QUERY_KEY.GET_QUERY_RANGE,

View File

@ -1,7 +1,7 @@
import { Select, Typography } from 'antd';
import logEvent from 'api/common/logEvent';
import { ResizeTable } from 'components/ResizeTable';
import { ENTITY_VERSION_V4 } from 'constants/app';
import { ENTITY_VERSION_V5 } from 'constants/app';
import { QueryParams } from 'constants/query';
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
@ -68,7 +68,8 @@ function TracesView({ isFilterApplied }: TracesViewProps): JSX.Element {
pagination: paginationQueryData,
},
},
ENTITY_VERSION_V4,
// ENTITY_VERSION_V4,
ENTITY_VERSION_V5,
{
queryKey: [
REACT_QUERY_KEY.GET_QUERY_RANGE,

View File

@ -34,6 +34,7 @@ import {
import {
HandleChangeFormulaData,
HandleChangeQueryData,
HandleChangeQueryDataV5,
UseQueryOperations,
} from 'types/common/operations.types';
import { DataSource, MetricAggregateOperator } from 'types/common/queryBuilder';
@ -292,9 +293,11 @@ export const useQueryOperations: UseQueryOperations = ({
index,
]);
const handleChangeQueryData: HandleChangeQueryData = useCallback(
(key, value) => {
const newQuery: IBuilderQuery = {
const handleChangeQueryData:
| HandleChangeQueryData
| HandleChangeQueryDataV5 = useCallback(
(key: string, value: any) => {
const newQuery = {
...query,
[key]:
key === LEGEND && typeof value === 'string'

View File

@ -3,6 +3,12 @@
// @ts-nocheck
import { getMetricsQueryRange } from 'api/metrics/getQueryRange';
import {
convertV5ResponseToLegacy,
getQueryRangeV5,
prepareQueryRangePayloadV5,
} from 'api/v5/v5';
import { ENTITY_VERSION_V5 } from 'constants/app';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { timePreferenceType } from 'container/NewWidget/RightContainer/timeItems';
import { Time } from 'container/TopNav/DateTimeSelection/config';
@ -26,13 +32,42 @@ export async function GetMetricQueryRange(
headers?: Record<string, string>,
isInfraMonitoring?: boolean,
): Promise<SuccessResponse<MetricRangePayloadProps>> {
const { legendMap, queryPayload } = prepareQueryRangePayload(props);
const response = await getMetricsQueryRange(
queryPayload,
version || 'v3',
signal,
headers,
);
let legendMap: Record<string, string>;
let response: SuccessResponse<MetricRangePayloadProps>;
if (version === ENTITY_VERSION_V5) {
const v5Result = prepareQueryRangePayloadV5(props);
legendMap = v5Result.legendMap;
const v5Response = await getQueryRangeV5(
v5Result.queryPayload,
version,
signal,
headers,
);
// Convert V5 response to legacy format for components
response = convertV5ResponseToLegacy(v5Response, legendMap);
} else {
const legacyResult = prepareQueryRangePayload(props);
legendMap = legacyResult.legendMap;
response = await getMetricsQueryRange(
legacyResult.queryPayload,
version || 'v3',
signal,
headers,
);
}
// todo: Sagar
if (response.statusCode >= 400 && version === ENTITY_VERSION_V5) {
let error = `API responded with ${response.statusCode} - ${response.error?.message} status: ${response?.status}`;
if (response.body && !isEmpty(response.body)) {
error = `${error}, errors: ${response.body}`;
}
throw new Error(error);
}
if (response.statusCode >= 400) {
let error = `API responded with ${response.statusCode} - ${response.error} status: ${response.message}`;

View File

@ -6,6 +6,13 @@ import {
ReduceOperators,
} from 'types/common/queryBuilder';
import {
Filter,
Having as HavingV5,
LogAggregation,
MetricAggregation,
TraceAggregation,
} from '../v5/queryRange';
import { BaseAutocompleteData } from './queryAutocompleteResponse';
// Type for Formula
@ -59,15 +66,18 @@ export type IBuilderQuery = {
dataSource: DataSource;
aggregateOperator: string;
aggregateAttribute: BaseAutocompleteData;
aggregations?: TraceAggregation[] | LogAggregation[] | MetricAggregation[];
timeAggregation: string;
spaceAggregation?: string;
temporality?: string;
functions: QueryFunctionProps[];
filter?: Filter;
filters: TagFilter;
groupBy: BaseAutocompleteData[];
expression: string;
disabled: boolean;
having: Having[];
havingExpression?: HavingV5;
limit: number | null;
stepInterval: number;
orderBy: OrderByPayload[];

View File

@ -0,0 +1,397 @@
// ===================== Base Types =====================
export type Step = string | number; // Duration string (e.g., "30s") or seconds as number
export type RequestType =
| 'scalar'
| 'time_series'
| 'raw'
| 'distribution'
| '';
export type QueryType =
| 'builder_query'
| 'builder_formula'
| 'builder_sub_query'
| 'builder_join'
| 'clickhouse_sql'
| 'promql';
export type OrderDirection = 'asc' | 'desc';
export type JoinType = 'inner' | 'left' | 'right' | 'full' | 'cross';
export type SignalType = 'traces' | 'logs' | 'metrics';
export type DataType = 'string' | 'number' | 'boolean' | 'array';
export type FieldType =
| 'resource'
| 'attribute'
| 'instrumentation_library'
| 'span';
export type FieldContext =
| 'metric'
| 'log'
| 'span'
| 'trace'
| 'resource'
| 'scope'
| 'attribute'
| 'event'
| '';
export type FieldDataType =
| 'string'
| 'bool'
| 'float64'
| 'int64'
| 'number'
| '[]string'
| '[]float64'
| '[]bool'
| '[]int64'
| '[]number'
| '';
export type FunctionName =
| 'cutOffMin'
| 'cutOffMax'
| 'clampMin'
| 'clampMax'
| 'absolute'
| 'runningDiff'
| 'log2'
| 'log10'
| 'cumSum'
| 'ewma3'
| 'ewma5'
| 'ewma7'
| 'median3'
| 'median5'
| 'median7'
| 'timeShift'
| 'anomaly';
export type Temporality = 'cumulative' | 'delta' | '';
export type MetricType =
| 'gauge'
| 'sum'
| 'histogram'
| 'summary'
| 'exponential_histogram'
| '';
export type TimeAggregation =
| 'latest'
| 'sum'
| 'avg'
| 'min'
| 'max'
| 'count'
| 'count_distinct'
| 'rate'
| 'increase'
| '';
export type SpaceAggregation =
| 'sum'
| 'avg'
| 'min'
| 'max'
| 'count'
| 'p50'
| 'p75'
| 'p90'
| 'p95'
| 'p99'
| '';
export type ColumnType = 'group' | 'aggregation';
// ===================== Core Interface Types =====================
export interface TelemetryFieldKey {
name: string;
description?: string;
unit?: string;
signal?: SignalType;
fieldContext?: FieldContext;
fieldDataType?: FieldDataType;
materialized?: boolean;
}
export interface Filter {
expression: string;
}
export interface Having {
expression: string;
}
export type GroupByKey = TelemetryFieldKey;
export interface OrderBy {
key: TelemetryFieldKey;
direction: OrderDirection;
}
export interface LimitBy {
keys: string[];
value: string;
}
export interface QueryRef {
name: string;
}
export interface FunctionArg {
name?: string;
value: string | number;
}
export interface QueryFunction {
name: FunctionName;
args?: FunctionArg[];
}
// ===================== Aggregation Types =====================
export interface TraceAggregation {
expression: string;
alias?: string;
}
export interface LogAggregation {
expression: string;
alias?: string;
}
export interface MetricAggregation {
metricName: string;
temporality: Temporality;
timeAggregation: TimeAggregation;
spaceAggregation: SpaceAggregation;
}
export interface SecondaryAggregation {
stepInterval?: Step;
expression: string;
alias?: string;
groupBy?: GroupByKey[];
order?: OrderBy[];
limit?: number;
limitBy?: LimitBy;
}
// ===================== Query Types =====================
export interface BaseBuilderQuery {
name?: string;
stepInterval?: Step;
disabled?: boolean;
filter?: Filter;
groupBy?: GroupByKey[];
order?: OrderBy[];
selectFields?: TelemetryFieldKey[];
limit?: number;
limitBy?: LimitBy;
offset?: number;
cursor?: string;
having?: Having;
secondaryAggregations?: SecondaryAggregation[];
functions?: QueryFunction[];
legend?: string;
}
export interface TraceBuilderQuery extends BaseBuilderQuery {
signal: 'traces';
aggregations?: TraceAggregation[];
}
export interface LogBuilderQuery extends BaseBuilderQuery {
signal: 'logs';
aggregations?: LogAggregation[];
}
export interface MetricBuilderQuery extends BaseBuilderQuery {
signal: 'metrics';
aggregations?: MetricAggregation[];
}
export type BuilderQuery =
| TraceBuilderQuery
| LogBuilderQuery
| MetricBuilderQuery;
export interface QueryBuilderFormula {
name: string;
expression: string;
functions?: QueryFunction[];
}
export interface QueryBuilderJoin {
name: string;
disabled?: boolean;
left: QueryRef;
right: QueryRef;
type: JoinType;
on: string;
aggregations?: any[]; // eslint-disable-line @typescript-eslint/no-explicit-any
selectFields?: TelemetryFieldKey[];
filter?: Filter;
groupBy?: GroupByKey[];
having?: Having;
order?: OrderBy[];
limit?: number;
secondaryAggregations?: SecondaryAggregation[];
functions?: QueryFunction[];
}
export interface PromQuery {
name: string;
query: string;
disabled?: boolean;
stats?: boolean;
step?: Step;
}
export interface ClickHouseQuery {
name: string;
query: string;
disabled?: boolean;
}
// ===================== Query Envelope =====================
export interface QueryEnvelope {
type: QueryType;
spec:
| BuilderQuery // Will be same for both builder_query and builder_sub_query
| QueryBuilderFormula
| QueryBuilderJoin
| PromQuery
| ClickHouseQuery;
}
export interface CompositeQuery {
queries: QueryEnvelope[];
}
// ===================== Request Types =====================
export interface QueryRangeRequestV5 {
schemaVersion: string;
start: number; // epoch milliseconds
end: number; // epoch milliseconds
requestType: RequestType;
compositeQuery: CompositeQuery;
variables?: Record<string, any>; // eslint-disable-line @typescript-eslint/no-explicit-any
}
// ===================== Response Types =====================
export interface ExecStats {
rowsScanned: number;
bytesScanned: number;
durationMs: number;
}
export interface Label {
key: TelemetryFieldKey;
value: any; // eslint-disable-line @typescript-eslint/no-explicit-any
}
export interface Bucket {
step: number;
}
export interface TimeSeriesValue {
timestamp: number; // Unix timestamp in milliseconds
value: number;
values?: number[]; // For heatmap type charts
bucket?: Bucket;
}
export interface TimeSeries {
labels?: Label[];
values: TimeSeriesValue[];
}
export interface AggregationBucket {
index: number;
alias: string;
meta: Record<string, any>; // eslint-disable-line @typescript-eslint/no-explicit-any
series: TimeSeries[];
}
export interface TimeSeriesData {
queryName: string;
aggregations: AggregationBucket[];
}
export interface ColumnDescriptor extends TelemetryFieldKey {
queryName: string;
aggregationIndex: number;
columnType: ColumnType;
}
export interface ScalarData {
columns: ColumnDescriptor[];
data: any[][]; // eslint-disable-line @typescript-eslint/no-explicit-any
}
export interface RawRow {
timestamp: string; // ISO date-time
data: Record<string, any>; // eslint-disable-line @typescript-eslint/no-explicit-any
}
export interface RawData {
queryName: string;
nextCursor?: string;
rows: RawRow[];
}
export interface DistributionData {
// Structure to be defined based on requirements
[key: string]: any; // eslint-disable-line @typescript-eslint/no-explicit-any
}
// Response data structures with results array
export interface TimeSeriesResponseData {
results: TimeSeriesData[];
}
export interface ScalarResponseData {
results: ScalarData[];
}
export interface RawResponseData {
results: RawData[];
}
export interface DistributionResponseData {
results: DistributionData[];
}
export type QueryRangeDataV5 =
| TimeSeriesResponseData
| ScalarResponseData
| RawResponseData
| DistributionResponseData;
export interface QueryRangeResponseV5 {
type: RequestType;
data: QueryRangeDataV5;
meta: ExecStats;
}
// ===================== Payload Types for API Functions =====================
export type QueryRangePayloadV5 = QueryRangeRequestV5;
export interface MetricRangePayloadV5 {
data: QueryRangeResponseV5;
}

View File

@ -53,6 +53,18 @@ export interface QueryDataV3 {
predictedSeries?: SeriesItem[] | null;
anomalyScores?: SeriesItem[] | null;
isAnomaly?: boolean;
table?: {
rows: {
data: {
[key: string]: any;
};
}[];
columns: {
name: string;
queryName: string;
isValueColumn: boolean;
}[];
};
}
export interface Props {

View File

@ -6,6 +6,12 @@ import {
IBuilderQuery,
QueryFunctionProps,
} from 'types/api/queryBuilder/queryBuilderData';
import {
BaseBuilderQuery,
LogBuilderQuery,
MetricBuilderQuery,
TraceBuilderQuery,
} from 'types/api/v5/queryRange';
import { DataSource } from 'types/common/queryBuilder';
import { SelectOption } from './select';
@ -17,14 +23,23 @@ type UseQueryOperationsParams = Pick<QueryProps, 'index' | 'query'> &
entityVersion: string;
};
export type HandleChangeQueryData = <
Key extends keyof IBuilderQuery,
Value extends IBuilderQuery[Key]
// Generic type that can work with both legacy and V5 query types
export type HandleChangeQueryData<T = IBuilderQuery> = <
Key extends keyof T,
Value extends T[Key]
>(
key: Key,
value: Value,
) => void;
// Legacy version for backward compatibility
export type HandleChangeQueryDataLegacy = HandleChangeQueryData<IBuilderQuery>;
// V5 version for new API
export type HandleChangeQueryDataV5 = HandleChangeQueryData<
BaseBuilderQuery & (TraceBuilderQuery | LogBuilderQuery | MetricBuilderQuery)
>;
export type HandleChangeFormulaData = <
Key extends keyof IBuilderFormula,
Value extends IBuilderFormula[Key]