Compare commits

...

17 Commits

Author SHA1 Message Date
SagarRajput-7
e0d6411e83 fix: test and lints cases failed after rebase with main 2025-07-23 17:08:42 +05:30
Abhi kumar
ac8df7dd90 fix: added fix for logs orderby filter refetching (#8585)
* fix: added fix for logs orderby filter refetching

* chore: added cleanup for debouncetimer on unmount

* chore: added cleanup for debouncetimer on unmount
2025-07-23 16:54:44 +05:30
SagarRajput-7
d7dcd37e8f feat: usability, style and feat fixes of qb misc (#8571)
* feat: usability, style and feat fixes of qb misc

* feat: added function support for metrics

* feat: updated where clause placeholder

* feat: added tooltip wiht learn more, for add query and forumla buttons
2025-07-23 16:54:44 +05:30
SagarRajput-7
9da578f054 feat: fix and update the labels, unit, threshold for multi-aggregation (#8557)
* feat: fix and update the labels, unit, threshold for multi-aggregation

* feat: fix the uplot label / legend

* feat: fixed tooltip to be in sync with labels

* feat: fixed table labels, unit and threshold

* feat: disabled errorModal on dashboard detail page

* feat: fixed having expression payload in query_range and widget

* feat: added helper comments for the alias extraction regex
2025-07-23 16:54:44 +05:30
Abhi kumar
4074a89721 fix: replace old editor with the new query builder in log context view (#8559)
* chore: added changes for filtering log context resource keys

* chore: added method to convert expression to filters

* fix: added changes for replacing log context editor

* chore: typo fix

* chore: moved regex to constants
2025-07-23 16:54:42 +05:30
Abhi kumar
f7f5c6ca53 fix: added fix for log context view not working (#8544) 2025-07-23 16:53:13 +05:30
Abhi kumar
ccbd4e292e fix: reset query in quick filters (#8528)
* fix: added fix for reset all

* chore: added fix for empty query

* fix: fixed value suggestion issues

* chore: minor pr review changes

* chore: pr review fixes

* chore: cleanup for debouncedFetchValueSuggestions
2025-07-23 16:53:13 +05:30
SagarRajput-7
4afce1bc8f feat: added dashboard widget migration (#8524)
* feat: added dashboard widget migration

* feat: handle dashboard widget config without filters obj

* feat: fixed reduceTo not working

* feat: added fillgaps in the payload
2025-07-23 16:53:07 +05:30
SagarRajput-7
19657dcaa3 feat: query builder v2 enhancement and styling upgrades (#8522)
* feat: added order by and limit in formula for qb v2

* feat: added smart retries for query_range v5 API

* feat: added lightMode styles for new QB v2

* feat: added order by with keys support in list view of explorer pages
2025-07-23 16:50:03 +05:30
Abhi kumar
395bd07756 fix: query search showing redundant error icon (#8525)
* fix: added fix for queryvalidation when query being updated from outside

* chore: removed unused code
2025-07-23 16:50:03 +05:30
Abhi kumar
3cf9790beb fix: preserve query context inside parentheses + removed normalizespaces (#8521)
* fix: fixed context for query in parenthesis

* fix: enhance context detection after closing parenthesis in query

* fix: remove unused function call in getCurrentQueryPair

* fix: added fix for context breaking with spaces

* chore: renamed a function

* chore: pr review fixes
2025-07-23 16:50:03 +05:30
Abhi kumar
8b3e763f11 Fix: quick filter with the new query builder (#8520)
* feat: enhance filter conversion with existing query support and array operator handling

* chore: added fixes for quick filtes to work with the new query builder

* fix: improve filter expression formatting and prevent duplicates in existing query conversion

* fix: optimize filter processing by removing redundant set and improving query pair checks

* fix: fix for all selection not working in checkbox quick filter

* chore: added relavant comments

* chore: segregated removequery in a util and fixed clear all

* fix: added fix for spanscopeselector

* chore: fix minor spelling mistake
2025-07-23 16:50:03 +05:30
Yunus M
5b3ce916ea feat: query builder v2 components - [PR-02] (#8468)
* feat: query builder v2 components

* feat: query builder view and related component updates [PR-03] (#8469)

* feat: query builder view and related component updates, minor ui fixes and updates

* feat: explorer options ui updates

* feat: qb - metrics pod related code changes [PR-04] (#8470)

* feat: qb - metrics pod related code changes

* feat: qb - events pod related code changes [PR-05] (#8471)

* feat: qb - events pod related code changes

* feat: qb - dashboard related code changes (#8472)
2025-07-23 16:50:03 +05:30
Yunus M
bfc7a45aa7 chore: handle types in widget utils 2025-07-23 16:50:03 +05:30
Yunus M
5cc4d9d653 feat: uri encoded query parameters, remove unwanted file 2025-07-23 16:50:03 +05:30
Yunus M
a1914d3f1e chore: prettier ignore the parser files as they are auto generated 2025-07-23 16:50:03 +05:30
Yunus M
abb849a3a4 feat: new query builder - base setup, parser, api and util files 2025-07-23 16:50:03 +05:30
277 changed files with 18087 additions and 1674 deletions

View File

@@ -1,4 +1,5 @@
module.exports = {
ignorePatterns: ['src/parser/*.ts'],
env: {
browser: true,
es2021: true,

View File

@@ -8,3 +8,6 @@ public/
# Ignore all JSON files:
**/*.json
# Ignore all files in parser folder:
src/parser/**

View File

@@ -28,6 +28,8 @@
"dependencies": {
"@ant-design/colors": "6.0.0",
"@ant-design/icons": "4.8.0",
"@codemirror/autocomplete": "6.18.6",
"@codemirror/lang-javascript": "6.2.3",
"@dnd-kit/core": "6.1.0",
"@dnd-kit/modifiers": "7.0.0",
"@dnd-kit/sortable": "8.0.0",
@@ -43,6 +45,8 @@
"@signozhq/design-tokens": "1.1.4",
"@tanstack/react-table": "8.20.6",
"@tanstack/react-virtual": "3.11.2",
"@uiw/codemirror-theme-copilot": "4.23.11",
"@uiw/react-codemirror": "4.23.10",
"@uiw/react-md-editor": "3.23.5",
"@visx/group": "3.3.0",
"@visx/hierarchy": "3.12.0",
@@ -53,6 +57,7 @@
"antd": "5.11.0",
"antd-table-saveas-excel": "2.2.1",
"axios": "1.8.2",
"antlr4": "4.13.2",
"babel-eslint": "^10.1.0",
"babel-jest": "^29.6.4",
"babel-loader": "9.1.3",

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,28 @@
import axios from 'api';
import { AxiosResponse } from 'axios';
import {
QueryKeyRequestProps,
QueryKeySuggestionsResponseProps,
} from 'types/api/querySuggestions/types';
export const getKeySuggestions = (
props: QueryKeyRequestProps,
): Promise<AxiosResponse<QueryKeySuggestionsResponseProps>> => {
const {
signal = '',
searchText = '',
metricName = '',
fieldContext = '',
fieldDataType = '',
} = props;
const encodedSignal = encodeURIComponent(signal);
const encodedSearchText = encodeURIComponent(searchText);
const encodedMetricName = encodeURIComponent(metricName);
const encodedFieldContext = encodeURIComponent(fieldContext);
const encodedFieldDataType = encodeURIComponent(fieldDataType);
return axios.get(
`/fields/keys?signal=${encodedSignal}&searchText=${encodedSearchText}&metricName=${encodedMetricName}&fieldContext=${encodedFieldContext}&fieldDataType=${encodedFieldDataType}`,
);
};

View File

@@ -0,0 +1,20 @@
import axios from 'api';
import { AxiosResponse } from 'axios';
import {
QueryKeyValueRequestProps,
QueryKeyValueSuggestionsResponseProps,
} from 'types/api/querySuggestions/types';
export const getValueSuggestions = (
props: QueryKeyValueRequestProps,
): Promise<AxiosResponse<QueryKeyValueSuggestionsResponseProps>> => {
const { signal, key, searchText } = props;
const encodedSignal = encodeURIComponent(signal);
const encodedKey = encodeURIComponent(key);
const encodedSearchText = encodeURIComponent(searchText);
return axios.get(
`/fields/values?signal=${encodedSignal}&name=${encodedKey}&searchText=${encodedSearchText}`,
);
};

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,387 @@
import { cloneDeep, isEmpty } from 'lodash-es';
import { SuccessResponse } 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<string, string>,
aggregationPerQuery: Record<string, any>,
): 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, any>,
): 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<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) => {
const { index, alias, series } = aggregation;
if (!series || !series.length) {
return [];
}
return 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),
})),
metaData: {
alias,
index,
queryName: timeSeriesData.queryName,
},
}));
}),
list: null,
};
}
/**
* Converts V5 ScalarData array to legacy format with table structure
*/
function convertScalarDataArrayToTable(
scalarDataArray: ScalarData[],
legendMap: Record<string, string>,
aggregationPerQuery: Record<string, any>,
): 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<string, any> = {};
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<string, string>,
aggregationPerQuery: Record<string, any>,
): 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<string, any> = {};
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<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>,
aggregationPerQuery: Record<string, any>,
): 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 '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<MetricRangePayloadV5>,
legendMap: Record<string, string>,
formatForWeb?: boolean,
): SuccessResponse<MetricRangePayloadV3> {
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<string, any>) || {};
// 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,
},
},
};
}
// Convert based on V5 response type
const convertedData = convertV5DataByType(
v5Data,
legendMap,
aggregationPerQuery,
);
// 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 = 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;
}

View File

@@ -0,0 +1,45 @@
import { ApiV5Instance } from 'api';
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
import { AxiosError } from 'axios';
import { ENTITY_VERSION_V5 } from 'constants/app';
import { ErrorV2Resp, SuccessResponseV2 } 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<SuccessResponseV2<MetricRangePayloadV5>> => {
try {
if (version && version === ENTITY_VERSION_V5) {
const response = await ApiV5Instance.post('/query_range', props, {
signal,
headers,
});
return {
httpStatusCode: response.status,
data: response.data,
};
}
// Default V5 behavior
const response = await ApiV5Instance.post('/query_range', props, {
signal,
headers,
});
return {
httpStatusCode: response.status,
data: response.data.data,
};
} catch (error) {
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
}
};
export default getQueryRangeV5;

View File

@@ -0,0 +1,434 @@
/* 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,
Having,
LogAggregation,
MetricAggregation,
OrderBy,
QueryEnvelope,
QueryFunction,
QueryRangePayloadV5,
QueryType,
RequestType,
TelemetryFieldKey,
TraceAggregation,
VariableItem,
} 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,
panelType?: PANEL_TYPES,
): BaseBuilderQuery {
const nonEmptySelectColumns = (queryData.selectColumns as (
| BaseAutocompleteData
| TelemetryFieldKey
)[])?.filter((c) => ('key' in c ? c?.key : c?.name));
return {
stepInterval: queryData?.stepInterval || undefined,
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:
panelType === PANEL_TYPES.TABLE || panelType === PANEL_TYPES.LIST
? 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.having) ? undefined : (queryData?.having as Having),
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(nonEmptySelectColumns)
? undefined
: nonEmptySelectColumns?.map(
(column: any): TelemetryFieldKey => ({
name: column.name ?? column.key,
fieldDataType:
column?.fieldDataType ?? (column?.dataType as FieldDataType),
fieldContext: column?.fieldContext ?? (column?.type as FieldContext),
signal: column?.signal ?? undefined,
}),
),
};
}
// Utility to parse aggregation expressions with optional alias
export function parseAggregations(
expression: string,
): { expression: string; alias?: string }[] {
const result: { expression: string; alias?: string }[] = [];
// Matches function calls like "count()" or "sum(field)" with optional alias like "as 'alias'"
// Handles quoted ('alias'), dash-separated (field-name), and unquoted values after "as" keyword
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];
let alias = match[2];
if (alias) {
// Remove quotes if present
alias = alias.replace(/^['"]|['"]$/g, '');
result.push({ expression: expr, alias });
} else {
result.push({ expression: expr });
}
match = regex.exec(expression);
}
return result;
}
export function createAggregation(
queryData: any,
panelType?: PANEL_TYPES,
): TraceAggregation[] | LogAggregation[] | MetricAggregation[] {
if (!queryData) {
return [];
}
const haveReduceTo =
queryData.dataSource === DataSource.METRICS &&
panelType &&
(panelType === PANEL_TYPES.TABLE ||
panelType === PANEL_TYPES.PIE ||
panelType === PANEL_TYPES.VALUE);
if (queryData.dataSource === DataSource.METRICS) {
return [
{
metricName:
queryData?.aggregations?.[0]?.metricName ||
queryData?.aggregateAttribute?.key,
temporality:
queryData?.aggregations?.[0]?.temporality ||
queryData?.aggregateAttribute?.temporality,
timeAggregation:
queryData?.aggregations?.[0]?.timeAggregation ||
queryData?.timeAggregation,
spaceAggregation:
queryData?.aggregations?.[0]?.spaceAggregation ||
queryData?.spaceAggregation,
reduceTo: haveReduceTo
? queryData?.aggregations?.[0]?.reduceTo || queryData?.reduceTo
: undefined,
},
];
}
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,
panelType?: PANEL_TYPES,
): QueryEnvelope[] {
return Object.entries(builderQueries).map(
([queryName, queryData]): QueryEnvelope => {
const signal = getSignalType(queryData.dataSource);
const baseSpec = createBaseSpec(queryData, requestType, panelType);
let spec: QueryEnvelope['spec'];
const aggregations = createAggregation(queryData, panelType);
switch (signal) {
case 'traces':
spec = {
name: queryName,
signal: 'traces' as const,
...baseSpec,
aggregations: aggregations as TraceAggregation[],
};
break;
case 'logs':
spec = {
name: queryName,
signal: 'logs' as const,
...baseSpec,
aggregations: aggregations as LogAggregation[],
};
break;
case 'metrics':
default:
spec = {
name: queryName,
signal: 'metrics' as const,
...baseSpec,
aggregations: aggregations as MetricAggregation[],
// reduceTo: queryData.reduceTo,
};
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
},
}),
);
}
/**
* 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,
formatForWeb,
originalGraphType,
fillGaps,
}: 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,
graphType,
);
// Convert formulas as separate query type
const formulaQueries = Object.entries(currentFormulas.data).map(
([queryName, formulaData]): QueryEnvelope => ({
type: 'builder_formula' as const,
spec: {
name: queryName,
expression: formulaData.expression || '',
disabled: formulaData.disabled,
limit: formulaData.limit ?? undefined,
order: formulaData.orderBy?.map(
// eslint-disable-next-line sonarjs/no-identical-functions
(order: any): OrderBy => ({
key: {
name: order.columnName,
},
direction: order.order,
}),
),
},
}),
);
// 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,
},
formatOptions: {
formatTableResultForUI:
!!formatForWeb ||
(originalGraphType
? originalGraphType === PANEL_TYPES.TABLE
: graphType === PANEL_TYPES.TABLE),
fillGaps: fillGaps || false,
},
variables: Object.entries(variables).reduce((acc, [key, value]) => {
acc[key] = { value };
return acc;
}, {} as Record<string, VariableItem>),
};
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

@@ -64,7 +64,8 @@ export function applyCeleryFilterOnWidgetData(
...queryItem,
filters: {
...queryItem.filters,
items: [...queryItem.filters.items, ...filters],
items: [...(queryItem.filters?.items || []), ...filters],
op: queryItem.filters?.op || 'AND',
},
}
: queryItem,

View File

@@ -41,7 +41,8 @@ export function useNavigateToExplorer(): (
aggregateOperator: MetricAggregateOperator.NOOP,
filters: {
...item.filters,
items: selectedFilters,
items: [...(item.filters?.items || []), ...selectedFilters],
op: item.filters?.op || 'AND',
},
groupBy: [],
disabled: false,

View File

@@ -18,7 +18,7 @@ function ErrorContent({ error }: ErrorContentProps): JSX.Element {
errors: errorMessages,
code: errorCode,
message: errorMessage,
} = error.error.error;
} = error?.error?.error || {};
return (
<section className="error-content">
{/* Summary Header */}

View File

@@ -43,13 +43,13 @@ export const omitIdFromQuery = (query: Query | null): any => ({
builder: {
...query?.builder,
queryData: query?.builder.queryData.map((queryData) => {
const { id, ...rest } = queryData.aggregateAttribute;
const { id, ...rest } = queryData.aggregateAttribute || {};
const newAggregateAttribute = rest;
const newGroupByAttributes = queryData.groupBy.map((groupByAttribute) => {
const { id, ...rest } = groupByAttribute;
return rest;
});
const newItems = queryData.filters.items.map((item) => {
const newItems = queryData.filters?.items?.map((item) => {
const { id, ...newItem } = item;
if (item.key) {
const { id, ...rest } = item.key;

View File

@@ -74,16 +74,16 @@ function HostMetricTraces({
...currentQuery.builder.queryData[0].aggregateAttribute,
},
filters: {
items: tracesFilters.items.filter(
(item) => item.key?.key !== 'host.name',
),
items:
tracesFilters?.items?.filter((item) => item.key?.key !== 'host.name') ||
[],
op: 'AND',
},
},
],
},
}),
[currentQuery, tracesFilters.items],
[currentQuery, tracesFilters?.items],
);
const query = updatedCurrentQuery?.builder?.queryData[0] || null;
@@ -140,7 +140,8 @@ function HostMetricTraces({
const isDataEmpty =
!isLoading && !isFetching && !isError && traces.length === 0;
const hasAdditionalFilters = tracesFilters.items.length > 1;
const hasAdditionalFilters =
tracesFilters?.items && tracesFilters?.items?.length > 1;
const totalCount =
data?.payload?.data?.newResult?.data?.result?.[0]?.list?.length || 0;
@@ -158,7 +159,7 @@ function HostMetricTraces({
<div className="filter-section">
{query && (
<QueryBuilderSearch
query={query}
query={query as IBuilderQuery}
onChange={(value): void =>
handleChangeTracesFilters(value, VIEWS.TRACES)
}

View File

@@ -216,15 +216,17 @@ function HostMetricsDetails({
const handleChangeLogFilters = useCallback(
(value: IBuilderQuery['filters'], view: VIEWS) => {
setLogFilters((prevFilters) => {
const hostNameFilter = prevFilters.items.find(
const hostNameFilter = prevFilters?.items?.find(
(item) => item.key?.key === 'host.name',
);
const paginationFilter = value.items.find((item) => item.key?.key === 'id');
const newFilters = value.items.filter(
const paginationFilter = value?.items?.find(
(item) => item.key?.key === 'id',
);
const newFilters = value?.items?.filter(
(item) => item.key?.key !== 'id' && item.key?.key !== 'host.name',
);
if (newFilters.length > 0) {
if (newFilters && newFilters?.length > 0) {
logEvent(InfraMonitoringEvents.FilterApplied, {
entity: InfraMonitoringEvents.HostEntity,
view: InfraMonitoringEvents.LogsView,
@@ -236,7 +238,7 @@ function HostMetricsDetails({
op: 'AND',
items: [
hostNameFilter,
...newFilters,
...(newFilters || []),
...(paginationFilter ? [paginationFilter] : []),
].filter((item): item is TagFilterItem => item !== undefined),
};
@@ -258,11 +260,11 @@ function HostMetricsDetails({
const handleChangeTracesFilters = useCallback(
(value: IBuilderQuery['filters'], view: VIEWS) => {
setTracesFilters((prevFilters) => {
const hostNameFilter = prevFilters.items.find(
const hostNameFilter = prevFilters?.items?.find(
(item) => item.key?.key === 'host.name',
);
if (value.items.length > 0) {
if (value?.items && value?.items?.length > 0) {
logEvent(InfraMonitoringEvents.FilterApplied, {
entity: InfraMonitoringEvents.HostEntity,
view: InfraMonitoringEvents.TracesView,
@@ -274,7 +276,7 @@ function HostMetricsDetails({
op: 'AND',
items: [
hostNameFilter,
...value.items.filter((item) => item.key?.key !== 'host.name'),
...(value?.items?.filter((item) => item.key?.key !== 'host.name') || []),
].filter((item): item is TagFilterItem => item !== undefined),
};
@@ -311,7 +313,7 @@ function HostMetricsDetails({
if (selectedView === VIEW_TYPES.LOGS) {
const filtersWithoutPagination = {
...logFilters,
items: logFilters.items.filter((item) => item.key?.key !== 'id'),
items: logFilters?.items?.filter((item) => item.key?.key !== 'id') || [],
};
const compositeQuery = {

View File

@@ -52,14 +52,16 @@ function HostMetricLogsDetailedView({
...currentQuery.builder.queryData[0].aggregateAttribute,
},
filters: {
items: logFilters.items.filter((item) => item.key?.key !== 'host.name'),
items:
logFilters?.items?.filter((item) => item.key?.key !== 'host.name') ||
[],
op: 'AND',
},
},
],
},
}),
[currentQuery, logFilters.items],
[currentQuery, logFilters?.items],
);
const query = updatedCurrentQuery?.builder?.queryData[0] || null;
@@ -70,7 +72,7 @@ function HostMetricLogsDetailedView({
<div className="filter-section">
{query && (
<QueryBuilderSearch
query={query}
query={query as IBuilderQuery}
onChange={(value): void => handleChangeLogFilters(value, VIEWS.LOGS)}
disableNavigationShortcuts
/>

View File

@@ -13,6 +13,7 @@ import {
CustomTimeType,
Time,
} from 'container/TopNav/DateTimeSelectionV2/config';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { useResizeObserver } from 'hooks/useDimensions';
import { useMultiIntersectionObserver } from 'hooks/useMultiIntersectionObserver';
@@ -86,6 +87,7 @@ function Metrics({
const isDarkMode = useIsDarkMode();
const graphRef = useRef<HTMLDivElement>(null);
const dimensions = useResizeObserver(graphRef);
const { currentQuery } = useQueryBuilder();
const chartData = useMemo(
() => queries.map(({ data }) => getUPlotChartData(data?.payload)),
@@ -144,9 +146,17 @@ function Metrics({
minTimeScale: graphTimeIntervals[idx].start,
maxTimeScale: graphTimeIntervals[idx].end,
onDragSelect: (start, end) => onDragSelect(start, end, idx),
query: currentQuery,
}),
),
[queries, isDarkMode, dimensions, graphTimeIntervals, onDragSelect],
[
queries,
isDarkMode,
dimensions,
graphTimeIntervals,
onDragSelect,
currentQuery,
],
);
const renderCardContent = (

View File

@@ -0,0 +1,101 @@
.input-with-label {
display: flex;
flex-direction: row;
border-radius: 2px 0px 0px 2px;
.label {
color: var(--bg-vanilla-400);
font-size: 12px;
font-style: normal;
font-weight: 500;
line-height: 18px; /* 128.571% */
letter-spacing: 0.56px;
max-width: 150px;
min-width: 60px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
padding: 0px 8px;
border-radius: 2px 0px 0px 2px;
border: 1px solid var(--bg-slate-400);
background: var(--bg-ink-300);
display: flex;
justify-content: flex-start;
align-items: center;
font-weight: var(--font-weight-light);
}
.input {
flex: 1;
min-width: 150px;
font-family: 'Space Mono', monospace !important;
border-radius: 2px 0px 0px 2px;
border: 1px solid var(--bg-slate-400);
background: var(--bg-ink-300);
border-right: none;
border-left: none;
border-top-right-radius: 0px;
border-bottom-right-radius: 0px;
border-top-left-radius: 0px;
border-bottom-left-radius: 0px;
}
.close-btn {
border-radius: 0px 2px 2px 0px;
border: 1px solid var(--bg-slate-400);
background: var(--bg-ink-300);
height: 38px;
width: 38px;
}
&.labelAfter {
.input {
border-radius: 0px 2px 2px 0px;
border: 1px solid var(--bg-slate-400);
background: var(--bg-ink-300);
border-top-right-radius: 0px;
border-bottom-right-radius: 0px;
}
.label {
border-left: none;
border-top-left-radius: 0px;
border-bottom-left-radius: 0px;
}
}
}
.lightMode {
.input-with-label {
.label {
color: var(--bg-ink-500) !important;
border: 1px solid var(--bg-vanilla-300) !important;
background: var(--bg-vanilla-100) !important;
}
.input {
border: 1px solid var(--bg-vanilla-300) !important;
background: var(--bg-vanilla-100) !important;
}
.close-btn {
border: 1px solid var(--bg-vanilla-300) !important;
background: var(--bg-vanilla-100) !important;
}
&.labelAfter {
.input {
border: 1px solid var(--bg-vanilla-300) !important;
background: var(--bg-vanilla-100) !important;
}
}
}
}

View File

@@ -0,0 +1,74 @@
import './InputWithLabel.styles.scss';
import { Button, Input, Typography } from 'antd';
import cx from 'classnames';
import { X } from 'lucide-react';
import { useState } from 'react';
function InputWithLabel({
label,
initialValue,
placeholder,
type,
onClose,
labelAfter,
onChange,
className,
closeIcon,
}: {
label: string;
initialValue?: string | number;
placeholder: string;
type?: string;
onClose?: () => void;
labelAfter?: boolean;
onChange: (value: string) => void;
className?: string;
closeIcon?: React.ReactNode;
}): JSX.Element {
const [inputValue, setInputValue] = useState<string>(
initialValue ? initialValue.toString() : '',
);
const handleChange = (e: React.ChangeEvent<HTMLInputElement>): void => {
setInputValue(e.target.value);
onChange?.(e.target.value);
};
return (
<div
className={cx('input-with-label', className, {
labelAfter,
})}
>
{!labelAfter && <Typography.Text className="label">{label}</Typography.Text>}
<Input
className="input"
placeholder={placeholder}
type={type}
value={inputValue}
onChange={handleChange}
name={label.toLowerCase()}
/>
{labelAfter && <Typography.Text className="label">{label}</Typography.Text>}
{onClose && (
<Button
className="periscope-btn ghost close-btn"
icon={closeIcon || <X size={16} />}
onClick={onClose}
/>
)}
</div>
);
}
InputWithLabel.defaultProps = {
type: 'text',
onClose: undefined,
labelAfter: false,
initialValue: undefined,
className: undefined,
closeIcon: undefined,
};
export default InputWithLabel;

View File

@@ -20,3 +20,7 @@ export type LogDetailProps = {
} & Pick<AddToQueryHOCProps, 'onAddToQuery'> &
Partial<Pick<ActionItemProps, 'onClickActionItem'>> &
Pick<DrawerProps, 'onClose'>;
export type LogDetailInnerProps = LogDetailProps & {
log: NonNullable<LogDetailProps['log']>;
};

View File

@@ -62,6 +62,10 @@
box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.1);
}
&-query-container {
margin-bottom: 16px;
}
.log-detail-drawer__log {
width: 100%;
display: flex;

View File

@@ -7,6 +7,8 @@ import { Button, Divider, Drawer, Radio, Tooltip, Typography } from 'antd';
import { RadioChangeEvent } from 'antd/lib';
import cx from 'classnames';
import { LogType } from 'components/Logs/LogStateIndicator/LogStateIndicator';
import QuerySearch from 'components/QueryBuilderV2/QueryV2/QuerySearch/QuerySearch';
import { convertExpressionToFilters } from 'components/QueryBuilderV2/utils';
import { LOCALSTORAGE } from 'constants/localStorage';
import { QueryParams } from 'constants/query';
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
@@ -21,6 +23,7 @@ import {
removeEscapeCharacters,
unescapeString,
} from 'container/LogDetailedView/utils';
import useInitialQuery from 'container/LogsExplorerContext/useInitialQuery';
import { useOptionsMenu } from 'container/OptionsMenu';
import dompurify from 'dompurify';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
@@ -28,13 +31,13 @@ import { useIsDarkMode } from 'hooks/useDarkMode';
import { useNotifications } from 'hooks/useNotifications';
import { useSafeNavigate } from 'hooks/useSafeNavigate';
import createQueryParams from 'lib/createQueryParams';
import { cloneDeep } from 'lodash-es';
import {
BarChart2,
Braces,
Compass,
Copy,
Filter,
HardHat,
Table,
TextSelect,
X,
@@ -49,12 +52,11 @@ import { GlobalReducer } from 'types/reducer/globalTime';
import { FORBID_DOM_PURIFY_TAGS } from 'utils/app';
import { RESOURCE_KEYS, VIEW_TYPES, VIEWS } from './constants';
import { LogDetailProps } from './LogDetail.interfaces';
import QueryBuilderSearchWrapper from './QueryBuilderSearchWrapper';
import { LogDetailInnerProps, LogDetailProps } from './LogDetail.interfaces';
const convert = new Convert();
function LogDetail({
function LogDetailInner({
log,
onClose,
onAddToQuery,
@@ -63,13 +65,16 @@ function LogDetail({
selectedTab,
isListViewPanel = false,
listViewPanelSelectedFields,
}: LogDetailProps): JSX.Element {
}: LogDetailInnerProps): JSX.Element {
const initialContextQuery = useInitialQuery(log);
const [contextQuery, setContextQuery] = useState<Query | undefined>(
initialContextQuery,
);
const [, copyToClipboard] = useCopyToClipboard();
const [selectedView, setSelectedView] = useState<VIEWS>(selectedTab);
const [isFilterVisibile, setIsFilterVisible] = useState<boolean>(false);
const [isFilterVisible, setIsFilterVisible] = useState<boolean>(false);
const [contextQuery, setContextQuery] = useState<Query | undefined>();
const [filters, setFilters] = useState<TagFilter | null>(null);
const [isEdit, setIsEdit] = useState<boolean>(false);
const { stagedQuery, updateAllQueriesOperators } = useQueryBuilder();
@@ -104,7 +109,7 @@ function LogDetail({
};
const handleFilterVisible = (): void => {
setIsFilterVisible(!isFilterVisibile);
setIsFilterVisible(!isFilterVisible);
setIsEdit(!isEdit);
};
@@ -151,6 +156,44 @@ function LogDetail({
safeNavigate(`${ROUTES.LOGS_EXPLORER}?${createQueryParams(queryParams)}`);
};
const handleRunQuery = (expression: string): void => {
let updatedContextQuery = cloneDeep(contextQuery);
if (!updatedContextQuery || !updatedContextQuery.builder) {
return;
}
const newFilters: TagFilter = {
items: expression ? convertExpressionToFilters(expression) : [],
op: 'AND',
};
updatedContextQuery = {
...updatedContextQuery,
builder: {
...updatedContextQuery?.builder,
queryData: updatedContextQuery?.builder.queryData.map((queryData) => ({
...queryData,
filter: {
...queryData.filter,
expression,
},
filters: {
...queryData.filters,
...newFilters,
op: queryData.filters?.op ?? 'AND',
},
})),
},
};
setContextQuery(updatedContextQuery);
if (newFilters) {
setFilters(newFilters);
}
};
// Only show when opened from infra monitoring page
const showOpenInExplorerBtn = useMemo(
() => location.pathname?.includes('/infrastructure-monitoring'),
@@ -158,11 +201,6 @@ function LogDetail({
[],
);
if (!log) {
// eslint-disable-next-line react/jsx-no-useless-fragment
return <></>;
}
const logType = log?.attributes_string?.log_level || LogType.INFO;
return (
@@ -278,18 +316,16 @@ function LogDetail({
/>
)}
</div>
<QueryBuilderSearchWrapper
isEdit={isEdit}
log={log}
filters={filters}
setContextQuery={setContextQuery}
setFilters={setFilters}
contextQuery={contextQuery}
suffixIcon={
<HardHat size={12} style={{ paddingRight: Spacing.PADDING_2 }} />
}
/>
{isFilterVisible && contextQuery?.builder.queryData[0] && (
<div className="log-detail-drawer-query-container">
<QuerySearch
onChange={(): void => {}}
dataSource={DataSource.LOGS}
queryData={contextQuery?.builder.queryData[0]}
onRun={handleRunQuery}
/>
</div>
)}
{selectedView === VIEW_TYPES.OVERVIEW && (
<Overview
@@ -325,4 +361,15 @@ function LogDetail({
);
}
function LogDetail(props: LogDetailProps): JSX.Element {
const { log } = props;
if (!log) {
// eslint-disable-next-line react/jsx-no-useless-fragment
return <></>;
}
// eslint-disable-next-line react/jsx-props-no-spreading
return <LogDetailInner {...(props as LogDetailInnerProps)} />;
}
export default LogDetail;

View File

@@ -410,18 +410,18 @@ export default function LogsFormatOptionsMenu({
)}
<div className="column-format">
{addColumn?.value?.map(({ key, id }) => (
<div className="column-name" key={id}>
{addColumn?.value?.map(({ name }) => (
<div className="column-name" key={name}>
<div className="name">
<Tooltip placement="left" title={key}>
{key}
<Tooltip placement="left" title={name}>
{name}
</Tooltip>
</div>
{addColumn?.value?.length > 1 && (
<X
className="delete-btn"
size={14}
onClick={(): void => addColumn.onRemove(id as string)}
onClick={(): void => addColumn.onRemove(name)}
/>
)}
</div>

View File

@@ -0,0 +1,7 @@
.order-by-loading-container {
padding: 4px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}

View File

@@ -0,0 +1,115 @@
import './ListViewOrderBy.styles.scss';
import { Select, Spin } from 'antd';
import { getKeySuggestions } from 'api/querySuggestions/getKeySuggestions';
import { useEffect, useRef, useState } from 'react';
import { useQuery } from 'react-query';
import { QueryKeyDataSuggestionsProps } from 'types/api/querySuggestions/types';
import { DataSource } from 'types/common/queryBuilder';
interface ListViewOrderByProps {
value: string;
onChange: (value: string) => void;
dataSource: DataSource;
}
// Loader component for the dropdown when loading or no results
function Loader({ isLoading }: { isLoading: boolean }): JSX.Element {
return (
<div className="order-by-loading-container">
{isLoading ? <Spin size="default" /> : 'No results found'}
</div>
);
}
function ListViewOrderBy({
value,
onChange,
dataSource,
}: ListViewOrderByProps): JSX.Element {
const [searchInput, setSearchInput] = useState('');
const [debouncedInput, setDebouncedInput] = useState('');
const [selectOptions, setSelectOptions] = useState<
{ label: string; value: string }[]
>([]);
const debounceTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
// Fetch key suggestions based on debounced input
const { data, isLoading } = useQuery({
queryKey: ['orderByKeySuggestions', dataSource, debouncedInput],
queryFn: async () => {
const response = await getKeySuggestions({
signal: dataSource,
searchText: debouncedInput,
});
return response.data;
},
});
useEffect(
() => (): void => {
if (debounceTimer.current) {
clearTimeout(debounceTimer.current);
}
},
[],
);
// Update options when API data changes
useEffect(() => {
const rawKeys: QueryKeyDataSuggestionsProps[] = data?.data.keys
? Object.values(data.data.keys).flat()
: [];
const keyNames = rawKeys.map((key) => key.name);
const uniqueKeys = [
...new Set(searchInput ? keyNames : ['timestamp', ...keyNames]),
];
const updatedOptions = uniqueKeys.flatMap((key) => [
{ label: `${key} (desc)`, value: `${key}:desc` },
{ label: `${key} (asc)`, value: `${key}:asc` },
]);
setSelectOptions(updatedOptions);
}, [data, searchInput]);
// Handle search input with debounce
const handleSearch = (input: string): void => {
setSearchInput(input);
// Filter current options for instant client-side match
const filteredOptions = selectOptions.filter((option) =>
option.value.toLowerCase().includes(input.trim().toLowerCase()),
);
// If no match found or input is empty, trigger debounced fetch
if (filteredOptions.length === 0 || input === '') {
if (debounceTimer.current) {
clearTimeout(debounceTimer.current);
}
debounceTimer.current = setTimeout(() => {
setDebouncedInput(input);
}, 100);
}
};
return (
<Select
showSearch
value={value}
onChange={onChange}
onSearch={handleSearch}
notFoundContent={<Loader isLoading={isLoading} />}
placeholder="Select an attribute"
style={{ width: 200 }}
options={selectOptions}
filterOption={(input, option): boolean =>
(option?.value ?? '').toLowerCase().includes(input.trim().toLowerCase())
}
/>
);
}
export default ListViewOrderBy;

View File

@@ -0,0 +1,553 @@
.query-builder-v2 {
display: flex;
flex-direction: row;
gap: 4px;
width: 100%;
border-bottom: 1px solid var(--bg-slate-400);
border-top: 1px solid var(--bg-slate-400);
font-family: Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto,
'Helvetica Neue', sans-serif;
border-right: none;
border-left: none;
.qb-content-container {
display: flex;
flex-direction: column;
width: calc(100% - 44px);
flex: 1;
position: relative;
}
.qb-content-section {
display: flex;
flex-direction: column;
gap: 8px;
padding: 8px;
flex: 1;
.qb-header-container {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
margin-left: 32px;
.query-actions-container {
display: flex;
flex-direction: row;
gap: 8px;
justify-content: space-between;
align-items: center;
width: 100%;
}
}
.qb-elements-container {
display: flex;
flex-direction: column;
gap: 8px;
margin-left: 108px;
.code-mirror-where-clause,
.query-aggregation-container,
.query-add-ons,
.metrics-aggregation-section-content {
position: relative;
&::before {
content: '';
position: absolute;
left: -10px;
top: 12px;
width: 6px;
height: 6px;
border-left: 6px dotted #1d212d;
}
/* Horizontal line pointing from vertical to the item */
&::after {
content: '';
position: absolute;
left: -28px;
top: 15px;
width: 24px;
height: 1px;
background: repeating-linear-gradient(
to right,
#1d212d,
#1d212d 4px,
transparent 4px,
transparent 8px
);
}
}
}
}
.where-clause-view {
.qb-content-section {
.qb-elements-container {
margin-left: 0px;
.code-mirror-where-clause,
.query-aggregation-container,
.query-add-ons,
.metrics-aggregation-section-content {
&::before {
display: none;
}
&::after {
display: none;
}
}
}
}
}
.query-names-section {
display: flex;
flex-direction: column;
gap: 8px;
width: 44px;
padding: 8px;
border-left: 1px solid var(--bg-slate-400);
.query-name {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
gap: 10px;
padding: 4px;
border-radius: 0px 2px 2px 0px;
border-radius: 2px;
border: 1px solid rgba(242, 71, 105, 0.2);
background: rgba(242, 71, 105, 0.1);
color: var(--Sakura-400, #f56c87);
font-family: 'Space Mono';
font-size: 12px;
font-style: normal;
font-weight: 400;
line-height: 16px; /* 128.571% */
text-transform: uppercase;
}
.formula-name {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
gap: 10px;
padding: 4px;
border-radius: 0px 2px 2px 0px;
border-radius: 2px;
border: 1px solid rgba(173, 127, 88, 0.2);
background: rgba(173, 127, 88, 0.1);
color: var(--Sienna-500, #ad7f58);
font-family: 'Space Mono';
font-size: 12px;
font-style: normal;
font-weight: 400;
line-height: 16px; /* 128.571% */
text-transform: uppercase;
}
}
.qb-formulas-container {
display: flex;
flex-direction: column;
gap: 8px;
margin-left: 32px;
padding-bottom: 16px;
padding-left: 8px;
.qb-formula {
.ant-row {
row-gap: 8px !important;
}
.qb-entity-options {
margin-left: 8px;
padding-right: 8px;
}
.formula-container {
margin-left: 82px;
padding: 4px 0px;
.ant-col {
&::before {
content: '';
position: absolute;
left: -10px;
top: 12px;
width: 6px;
height: 6px;
border-left: 6px dotted #1d212d;
}
/* Horizontal line pointing from vertical to the item */
&::after {
content: '';
position: absolute;
left: -28px;
top: 15px;
width: 24px;
height: 1px;
background: repeating-linear-gradient(
to right,
#1d212d,
#1d212d 4px,
transparent 4px,
transparent 8px
);
}
}
.formula-expression {
border-bottom-left-radius: 0px !important;
border-bottom-right-radius: 0px !important;
font-family: 'Space Mono';
font-size: 12px;
font-style: normal;
font-weight: 400;
line-height: 16px; /* 128.571% */
resize: none;
}
.formula-legend {
border-top-left-radius: 0px !important;
border-top-right-radius: 0px !important;
.ant-input-group-addon {
border-top-left-radius: 0px !important;
border-top-right-radius: 0px !important;
}
.ant-input {
border-top-left-radius: 0px !important;
border-top-right-radius: 0px !important;
}
}
}
}
}
.qb-footer {
padding: 0 8px 16px 8px;
.qb-footer-container {
display: flex;
flex-direction: row;
gap: 8px;
margin-left: 32px;
.qb-add-new-query {
display: flex;
flex-direction: row;
gap: 8px;
&::before {
content: '';
height: calc(100% - 82px);
content: '';
position: absolute;
left: 56px;
top: 31px;
bottom: 0;
width: 1px;
background: repeating-linear-gradient(
to bottom,
#1d212d,
#1d212d 4px,
transparent 4px,
transparent 8px
);
}
}
}
}
.qb-entity-options {
display: flex;
flex-direction: row;
gap: 8px;
.options {
.query-name {
border-radius: 0px 2px 2px 0px !important;
border: 1px solid rgba(242, 71, 105, 0.2) !important;
background: rgba(242, 71, 105, 0.1) !important;
color: var(--Sakura-400, #f56c87) !important;
font-family: 'Space Mono';
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 18px; /* 128.571% */
text-transform: uppercase;
&::before {
content: '';
height: 120px;
content: '';
position: absolute;
left: 0;
top: 31px;
bottom: 0;
width: 1px;
background: repeating-linear-gradient(
to bottom,
#1d212d,
#1d212d 4px,
transparent 4px,
transparent 8px
);
left: 15px;
}
}
.formula-name {
border-radius: 0px 2px 2px 0px;
border: 1px solid rgba(173, 127, 88, 0.2);
background: rgba(173, 127, 88, 0.1);
font-family: 'Space Mono';
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 18px; /* 128.571% */
text-transform: uppercase;
&::before {
content: '';
height: 65px;
content: '';
position: absolute;
left: 0;
top: 31px;
bottom: 0;
width: 1px;
background: repeating-linear-gradient(
to bottom,
#1d212d,
#1d212d 4px,
transparent 4px,
transparent 8px
);
left: 15px;
}
}
}
.query-data-source {
margin-left: 8px;
.ant-select-selector {
min-width: 120px;
border-radius: 2px;
border: 1px solid var(--Slate-400, #1d212d);
background: var(--Ink-300, #16181d);
box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.1);
}
}
}
.qb-search-container {
.metrics-select-container {
margin-bottom: 12px;
}
}
.qb-search-filter-container {
display: flex;
flex-direction: row;
align-items: flex-start;
gap: 8px;
flex-wrap: wrap;
.query-search-container {
flex: 1;
}
.traces-search-filter-container {
display: flex;
flex-direction: row;
align-items: center;
gap: 8px;
width: 180px;
}
.ant-select {
height: auto;
}
.ant-select-selector {
border-radius: 2px;
border: 1px solid var(--Slate-400, #1d212d) !important;
background: var(--Ink-300, #16181d) !important;
height: 34px !important;
box-sizing: border-box !important;
}
.ant-select-arrow {
color: var(--bg-vanilla-400) !important;
}
}
.query-actions-dropdown {
cursor: pointer;
}
}
.lightMode {
.query-builder-v2 {
border-bottom: 1px solid var(--bg-vanilla-300);
border-top: 1px solid var(--bg-vanilla-300);
.qb-content-section {
.qb-elements-container {
.code-mirror-where-clause,
.query-aggregation-container,
.query-add-ons,
.metrics-aggregation-section-content {
&::before {
border-left: 6px dotted var(--bg-vanilla-300);
}
/* Horizontal line pointing from vertical to the item */
&::after {
background: repeating-linear-gradient(
to right,
var(--bg-vanilla-300),
var(--bg-vanilla-300) 4px,
transparent 4px,
transparent 8px
);
}
}
}
}
.query-names-section {
border-left: 1px solid var(--bg-vanilla-300);
}
.qb-formulas-container {
.qb-formula {
.formula-container {
.ant-col {
&::before {
border-left: 6px dotted var(--bg-vanilla-300);
}
/* Horizontal line pointing from vertical to the item */
&::after {
background: repeating-linear-gradient(
to right,
var(--bg-vanilla-300),
var(--bg-vanilla-300) 4px,
transparent 4px,
transparent 8px
);
}
}
}
}
}
.qb-footer {
.qb-footer-container {
.qb-add-new-query {
&::before {
background: repeating-linear-gradient(
to bottom,
var(--bg-vanilla-300),
var(--bg-vanilla-300) 4px,
transparent 4px,
transparent 8px
);
}
}
}
}
.qb-entity-options {
.options {
.query-name {
&::before {
background: repeating-linear-gradient(
to bottom,
var(--bg-vanilla-300),
var(--bg-vanilla-300) 4px,
transparent 4px,
transparent 8px
);
}
}
.formula-name {
&::before {
background: repeating-linear-gradient(
to bottom,
var(--bg-vanilla-300),
var(--bg-vanilla-300) 4px,
transparent 4px,
transparent 8px
);
left: 15px;
}
}
}
.query-data-source {
.ant-select-selector {
border: 1px solid var(--bg-vanilla-300) !important;
background: var(--bg-vanilla-100) !important;
box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.1) !important;
}
}
}
.qb-search-filter-container {
.ant-select-selector {
border: 1px solid var(--bg-vanilla-300) !important;
background: var(--bg-vanilla-100) !important;
box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.1) !important;
}
.ant-select-arrow {
color: var(--bg-vanilla-400) !important;
}
}
}
}

View File

@@ -0,0 +1,186 @@
import './QueryBuilderV2.styles.scss';
import { OPERATORS, PANEL_TYPES } from 'constants/queryBuilder';
import { Formula } from 'container/QueryBuilder/components/Formula';
import { QueryBuilderProps } from 'container/QueryBuilder/QueryBuilder.interfaces';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { memo, useEffect, useMemo, useRef } from 'react';
import { DataSource } from 'types/common/queryBuilder';
import { QueryBuilderV2Provider } from './QueryBuilderV2Context';
import QueryFooter from './QueryV2/QueryFooter/QueryFooter';
import { QueryV2 } from './QueryV2/QueryV2';
export const QueryBuilderV2 = memo(function QueryBuilderV2({
config,
panelType: newPanelType,
filterConfigs = {},
queryComponents,
isListViewPanel = false,
showOnlyWhereClause = false,
version,
}: QueryBuilderProps): JSX.Element {
const {
currentQuery,
addNewBuilderQuery,
addNewFormula,
handleSetConfig,
panelType,
initialDataSource,
} = useQueryBuilder();
const containerRef = useRef(null);
const currentDataSource = useMemo(
() =>
(config && config.queryVariant === 'static' && config.initialDataSource) ||
null,
[config],
);
useEffect(() => {
if (currentDataSource !== initialDataSource || newPanelType !== panelType) {
if (newPanelType === PANEL_TYPES.BAR) {
handleSetConfig(PANEL_TYPES.BAR, DataSource.METRICS);
return;
}
handleSetConfig(newPanelType, currentDataSource);
}
}, [
handleSetConfig,
panelType,
initialDataSource,
currentDataSource,
newPanelType,
]);
const listViewLogFilterConfigs: QueryBuilderProps['filterConfigs'] = useMemo(() => {
const config: QueryBuilderProps['filterConfigs'] = {
stepInterval: { isHidden: true, isDisabled: true },
having: { isHidden: true, isDisabled: true },
filters: {
customKey: 'body',
customOp: OPERATORS.CONTAINS,
},
};
return config;
}, []);
const listViewTracesFilterConfigs: QueryBuilderProps['filterConfigs'] = useMemo(() => {
const config: QueryBuilderProps['filterConfigs'] = {
stepInterval: { isHidden: true, isDisabled: true },
having: { isHidden: true, isDisabled: true },
limit: { isHidden: true, isDisabled: true },
filters: {
customKey: 'body',
customOp: OPERATORS.CONTAINS,
},
};
return config;
}, []);
const queryFilterConfigs = useMemo(() => {
if (isListViewPanel) {
return currentQuery.builder.queryData[0].dataSource === DataSource.TRACES
? listViewTracesFilterConfigs
: listViewLogFilterConfigs;
}
return filterConfigs;
}, [
isListViewPanel,
filterConfigs,
currentQuery.builder.queryData,
listViewLogFilterConfigs,
listViewTracesFilterConfigs,
]);
return (
<QueryBuilderV2Provider>
<div className="query-builder-v2">
<div className="qb-content-container">
{isListViewPanel && (
<QueryV2
ref={containerRef}
key={currentQuery.builder.queryData[0].queryName}
index={0}
query={currentQuery.builder.queryData[0]}
filterConfigs={queryFilterConfigs}
queryComponents={queryComponents}
version={version}
isAvailableToDisable={false}
queryVariant={config?.queryVariant || 'dropdown'}
showOnlyWhereClause={showOnlyWhereClause}
isListViewPanel={isListViewPanel}
/>
)}
{!isListViewPanel &&
currentQuery.builder.queryData.map((query, index) => (
<QueryV2
ref={containerRef}
key={query.queryName}
index={index}
query={query}
filterConfigs={queryFilterConfigs}
queryComponents={queryComponents}
version={version}
isAvailableToDisable={false}
queryVariant={config?.queryVariant || 'dropdown'}
showOnlyWhereClause={showOnlyWhereClause}
isListViewPanel={isListViewPanel}
/>
))}
{!showOnlyWhereClause && currentQuery.builder.queryFormulas.length > 0 && (
<div className="qb-formulas-container">
{currentQuery.builder.queryFormulas.map((formula, index) => {
const query =
currentQuery.builder.queryData[index] ||
currentQuery.builder.queryData[0];
return (
<div key={formula.queryName} className="qb-formula">
<Formula
filterConfigs={filterConfigs}
query={query}
formula={formula}
index={index}
isAdditionalFilterEnable={false}
isQBV2
/>
</div>
);
})}
</div>
)}
{!showOnlyWhereClause && !isListViewPanel && (
<QueryFooter
addNewBuilderQuery={addNewBuilderQuery}
addNewFormula={addNewFormula}
/>
)}
</div>
{!showOnlyWhereClause && !isListViewPanel && (
<div className="query-names-section">
{currentQuery.builder.queryData.map((query) => (
<div key={query.queryName} className="query-name">
{query.queryName}
</div>
))}
{currentQuery.builder.queryFormulas.map((formula) => (
<div key={formula.queryName} className="formula-name">
{formula.queryName}
</div>
))}
</div>
)}
</div>
</QueryBuilderV2Provider>
);
});

View File

@@ -0,0 +1,62 @@
import { createContext, ReactNode, useContext, useMemo, useState } from 'react';
// Types for the context state
export type AggregationOption = { func: string; arg: string };
interface QueryBuilderV2ContextType {
searchText: string;
setSearchText: (text: string) => void;
aggregationOptions: AggregationOption[];
setAggregationOptions: (options: AggregationOption[]) => void;
aggregationInterval: string;
setAggregationInterval: (interval: string) => void;
queryAddValues: any; // Replace 'any' with a more specific type if available
setQueryAddValues: (values: any) => void;
}
const QueryBuilderV2Context = createContext<
QueryBuilderV2ContextType | undefined
>(undefined);
export function QueryBuilderV2Provider({
children,
}: {
children: ReactNode;
}): JSX.Element {
const [searchText, setSearchText] = useState('');
const [aggregationOptions, setAggregationOptions] = useState<
AggregationOption[]
>([]);
const [aggregationInterval, setAggregationInterval] = useState('');
const [queryAddValues, setQueryAddValues] = useState<any>(null); // Replace 'any' if you have a type
return (
<QueryBuilderV2Context.Provider
value={useMemo(
() => ({
searchText,
setSearchText,
aggregationOptions,
setAggregationOptions,
aggregationInterval,
setAggregationInterval,
queryAddValues,
setQueryAddValues,
}),
[searchText, aggregationOptions, aggregationInterval, queryAddValues],
)}
>
{children}
</QueryBuilderV2Context.Provider>
);
}
export const useQueryBuilderV2Context = (): QueryBuilderV2ContextType => {
const context = useContext(QueryBuilderV2Context);
if (!context) {
throw new Error(
'useQueryBuilderV2Context must be used within a QueryBuilderV2Provider',
);
}
return context;
};

View File

@@ -0,0 +1,175 @@
.metrics-aggregate-section {
display: flex;
flex-direction: column;
gap: 16px;
margin: 4px 0;
.metrics-time-aggregation-section {
display: flex;
flex-direction: column;
gap: 12px;
}
.non-histogram-container {
display: flex;
flex-direction: column;
gap: 16px;
}
&:not(.is-histogram) {
.metrics-time-aggregation-section,
.metrics-space-aggregation-section {
display: flex;
flex-direction: row;
align-items: center;
.metrics-aggregation-section-content {
flex-wrap: nowrap;
}
}
}
.metrics-space-aggregation-section {
display: flex;
flex-direction: column;
gap: 4px;
.metrics-space-aggregation-section-title {
display: flex;
align-items: center;
gap: 6px;
color: var(--Slate-50, #62687c);
font-family: 'Geist Mono';
font-size: 12px;
font-style: normal;
font-weight: 500;
line-height: 18px; /* 150% */
letter-spacing: 0.48px;
}
}
.metrics-aggregation-section-content {
display: flex;
flex-direction: row;
flex-wrap: wrap;
gap: 8px;
.group-by-filter-container {
min-width: 340px !important;
}
.metrics-aggregation-section-content-item {
display: flex;
align-items: center;
gap: 10px;
.metrics-aggregation-section-content-item-label {
color: var(--Vanilla-400, #c0c1c3);
font-family: 'Geist Mono';
font-size: 13px;
font-style: normal;
font-weight: 400;
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
&.main-label {
color: var(--Slate-50, #62687c);
font-family: 'Geist Mono';
font-size: 12px;
font-style: normal;
font-weight: 500;
line-height: 18px; /* 150% */
letter-spacing: 0.48px;
display: flex;
align-items: center;
gap: 6px;
}
}
.metrics-aggregation-section-content-item-value {
min-width: 140px;
.ant-select {
width: 100%;
}
.ant-select-selector {
border-radius: 2px;
border: 1.005px solid var(--Slate-400, #1d212d);
background: var(--Ink-300, #16181d);
}
.input-with-label {
.label {
min-width: 80px;
}
.input {
flex: initial;
width: 100px !important;
}
}
}
}
}
&.is-histogram {
.group-by-filter-container {
width: 420px;
}
.histogram-every-input {
.input {
flex: initial;
width: 100px !important;
}
.label {
min-width: 80px;
}
}
}
}
.metrics-operators-select {
border-radius: 2px;
border: 1.005px solid var(--Slate-400, #1d212d);
background: var(--Ink-300, #16181d);
color: var(--Vanilla-400, #c0c1c3);
font-family: 'Geist Mono';
font-size: 13px;
font-style: normal;
font-weight: 400;
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
}
.lightMode {
.metrics-aggregate-section {
.metrics-aggregation-section-content {
.metrics-aggregation-section-content-item {
.metrics-aggregation-section-content-item-label {
color: var(--text-ink-200);
&.main-label {
color: var(--text-slate-100);
}
}
.metrics-aggregation-section-content-item-value {
.ant-select-selector {
border: 1px solid var(--bg-vanilla-300) !important;
background: var(--bg-vanilla-100) !important;
}
}
}
}
}
.metrics-operators-select {
border: 1px solid var(--bg-vanilla-300) !important;
background: var(--bg-vanilla-100) !important;
color: var(--text-ink-100);
}
}

View File

@@ -0,0 +1,233 @@
import './MetricsAggregateSection.styles.scss';
import { Tooltip } from 'antd';
import cx from 'classnames';
import InputWithLabel from 'components/InputWithLabel/InputWithLabel';
import { ATTRIBUTE_TYPES, PANEL_TYPES } from 'constants/queryBuilder';
import SpaceAggregationOptions from 'container/QueryBuilder/components/SpaceAggregationOptions/SpaceAggregationOptions';
import { GroupByFilter, OperatorsSelect } from 'container/QueryBuilder/filters';
import { useQueryOperations } from 'hooks/queryBuilder/useQueryBuilderOperations';
import { Info } from 'lucide-react';
import { memo, useCallback, useEffect, useMemo } from 'react';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import { MetricAggregation } from 'types/api/v5/queryRange';
import { useQueryBuilderV2Context } from '../../QueryBuilderV2Context';
const MetricsAggregateSection = memo(function MetricsAggregateSection({
query,
index,
version,
panelType,
}: {
query: IBuilderQuery;
index: number;
version: string;
panelType: PANEL_TYPES | null;
}): JSX.Element {
const { setAggregationOptions } = useQueryBuilderV2Context();
const {
operators,
spaceAggregationOptions,
handleChangeQueryData,
handleChangeOperator,
handleSpaceAggregationChange,
} = useQueryOperations({
index,
query,
entityVersion: version,
});
// this function is only relevant for metrics and now operators are part of aggregations
const queryAggregation = useMemo(
() => query.aggregations?.[0] as MetricAggregation,
[query.aggregations],
);
const isHistogram = useMemo(
() => query.aggregateAttribute?.type === ATTRIBUTE_TYPES.HISTOGRAM,
[query.aggregateAttribute?.type],
);
useEffect(() => {
setAggregationOptions([
{
func: queryAggregation.spaceAggregation || 'count',
arg: queryAggregation.metricName || '',
},
]);
}, [
queryAggregation.spaceAggregation,
queryAggregation.metricName,
setAggregationOptions,
query,
]);
const handleChangeGroupByKeys = useCallback(
(value: IBuilderQuery['groupBy']) => {
handleChangeQueryData('groupBy', value);
},
[handleChangeQueryData],
);
const handleChangeAggregateEvery = useCallback(
(value: string) => {
handleChangeQueryData('stepInterval', Number(value));
},
[handleChangeQueryData],
);
const showAggregationInterval = useMemo(() => {
// eslint-disable-next-line sonarjs/prefer-single-boolean-return
if (panelType === PANEL_TYPES.VALUE) {
return false;
}
return true;
}, [panelType]);
const disableOperatorSelector =
!queryAggregation.metricName || queryAggregation.metricName === '';
return (
<div
className={cx('metrics-aggregate-section', {
'is-histogram': isHistogram,
})}
>
{!isHistogram && (
<div className="non-histogram-container">
<div className="metrics-time-aggregation-section">
<div className="metrics-aggregation-section-content">
<div className="metrics-aggregation-section-content-item">
<div className="metrics-aggregation-section-content-item-label main-label">
AGGREGATE BY TIME{' '}
<Tooltip title="AGGREGATE BY TIME">
<Info size={12} />
</Tooltip>
</div>
<div className="metrics-aggregation-section-content-item-value">
<OperatorsSelect
value={queryAggregation.timeAggregation || ''}
onChange={handleChangeOperator}
operators={operators}
className="metrics-operators-select"
/>
</div>
</div>
{showAggregationInterval && (
<div className="metrics-aggregation-section-content-item">
<div className="metrics-aggregation-section-content-item-label">
every
</div>
<div className="metrics-aggregation-section-content-item-value">
<InputWithLabel
onChange={handleChangeAggregateEvery}
label="Seconds"
placeholder="Auto"
labelAfter
initialValue={query?.stepInterval ?? undefined}
/>
</div>
</div>
)}
</div>
</div>
<div className="metrics-space-aggregation-section">
<div className="metrics-aggregation-section-content">
<div className="metrics-aggregation-section-content-item">
<div className="metrics-aggregation-section-content-item-label main-label">
AGGREGATE LABELS
<Tooltip title="AGGREGATE LABELS">
<Info size={12} />
</Tooltip>
</div>
<div className="metrics-aggregation-section-content-item-value">
<SpaceAggregationOptions
panelType={panelType}
key={`${panelType}${queryAggregation.spaceAggregation}${queryAggregation.timeAggregation}`}
aggregatorAttributeType={
query?.aggregateAttribute?.type as ATTRIBUTE_TYPES
}
selectedValue={queryAggregation.spaceAggregation || ''}
disabled={disableOperatorSelector}
onSelect={handleSpaceAggregationChange}
operators={spaceAggregationOptions}
qbVersion="v3"
/>
</div>
</div>
<div className="metrics-aggregation-section-content-item">
<div className="metrics-aggregation-section-content-item-label">by</div>
<div className="metrics-aggregation-section-content-item-value group-by-filter-container">
<GroupByFilter
disabled={!queryAggregation.metricName}
query={query}
onChange={handleChangeGroupByKeys}
/>
</div>
</div>
</div>
</div>
</div>
)}
{isHistogram && (
<div className="metrics-space-aggregation-section">
<div className="metrics-aggregation-section-content">
<div className="metrics-aggregation-section-content-item">
<div className="metrics-aggregation-section-content-item-value">
<SpaceAggregationOptions
panelType={panelType}
key={`${panelType}${queryAggregation.spaceAggregation}${queryAggregation.timeAggregation}`}
aggregatorAttributeType={
query?.aggregateAttribute?.type as ATTRIBUTE_TYPES
}
selectedValue={queryAggregation.spaceAggregation || ''}
disabled={disableOperatorSelector}
onSelect={handleSpaceAggregationChange}
operators={spaceAggregationOptions}
qbVersion="v3"
/>
</div>
</div>
<div className="metrics-aggregation-section-content-item">
<div className="metrics-aggregation-section-content-item-label">by</div>
<div className="metrics-aggregation-section-content-item-value group-by-filter-container">
<GroupByFilter
disabled={!queryAggregation.metricName}
query={query}
onChange={handleChangeGroupByKeys}
/>
</div>
</div>
<div className="metrics-aggregation-section-content-item">
<div className="metrics-aggregation-section-content-item-label">
every
</div>
<div className="metrics-aggregation-section-content-item-value">
<InputWithLabel
onChange={handleChangeAggregateEvery}
label="Seconds"
placeholder="Auto"
labelAfter
initialValue={query?.stepInterval ?? undefined}
className="histogram-every-input"
/>
</div>
</div>
</div>
</div>
)}
</div>
);
});
export default MetricsAggregateSection;

View File

@@ -0,0 +1,74 @@
.metrics-select-container {
margin-bottom: 8px;
.ant-select-selector {
width: 100%;
border-radius: 2px;
border: 1px solid #1d212d !important;
background: #16181d;
color: #fff;
font-family: 'Geist Mono';
font-size: 13px;
font-style: normal;
font-weight: 400;
line-height: 20px; /* 142.857% */
min-height: 36px;
}
.ant-select-dropdown {
border-radius: 4px;
border: 1px solid var(--bg-slate-400);
background: linear-gradient(
139deg,
rgba(18, 19, 23, 0.8) 0%,
rgba(18, 19, 23, 0.9) 98.68%
);
box-shadow: 4px 10px 16px 2px rgba(0, 0, 0, 0.2);
backdrop-filter: blur(20px);
.ant-select-item {
color: #fff;
font-family: 'Geist Mono';
font-size: 13px;
font-style: normal;
font-weight: 400;
line-height: 20px; /* 142.857% */
&:hover {
background: rgba(171, 189, 255, 0.04) !important;
}
}
}
}
.lightMode {
.metrics-select-container {
.ant-select-selector {
border: 1px solid var(--bg-slate-300) !important;
background: var(--bg-vanilla-100);
color: var(--text-ink-100);
}
.ant-select-dropdown {
background: var(--bg-vanilla-100);
box-shadow: 0 3px 6px -4px rgba(0, 0, 0, 0.12),
0 6px 16px 0 rgba(0, 0, 0, 0.08), 0 9px 28px 8px rgba(0, 0, 0, 0.05);
backdrop-filter: none;
.ant-select-item {
color: var(--text-ink-100);
&:hover,
&.ant-select-item-option-active {
background: var(--bg-vanilla-300) !important;
}
&.ant-select-item-option-selected {
background: var(--bg-vanilla-300) !important;
border: 1px solid var(--bg-slate-400);
font-weight: 600;
}
}
}
}
}

View File

@@ -0,0 +1,28 @@
import './MetricsSelect.styles.scss';
import { AggregatorFilter } from 'container/QueryBuilder/filters';
import { useQueryOperations } from 'hooks/queryBuilder/useQueryBuilderOperations';
import { memo } from 'react';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
export const MetricsSelect = memo(function MetricsSelect({
query,
index,
version,
}: {
query: IBuilderQuery;
index: number;
version: string;
}): JSX.Element {
const { handleChangeAggregatorAttribute } = useQueryOperations({
index,
query,
entityVersion: version,
});
return (
<div className="metrics-select-container">
<AggregatorFilter onChange={handleChangeAggregatorAttribute} query={query} />
</div>
);
});

View File

@@ -0,0 +1,375 @@
/* eslint-disable import/no-extraneous-dependencies */
/* eslint-disable sonarjs/cognitive-complexity */
import {
autocompletion,
closeCompletion,
Completion,
CompletionContext,
completionKeymap,
CompletionResult,
startCompletion,
} from '@codemirror/autocomplete';
import { javascript } from '@codemirror/lang-javascript';
import { copilot } from '@uiw/codemirror-theme-copilot';
import CodeMirror, { EditorView, keymap } from '@uiw/react-codemirror';
import { Button } from 'antd';
import { Having } from 'api/v5/v5';
import { useQueryBuilderV2Context } from 'components/QueryBuilderV2/QueryBuilderV2Context';
import { ChevronUp } from 'lucide-react';
import { useEffect, useMemo, useRef, useState } from 'react';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
const havingOperators = [
{
label: '=',
value: '=',
},
{
label: '!=',
value: '!=',
},
{
label: '>',
value: '>',
},
{
label: '<',
value: '<',
},
{
label: '>=',
value: '>=',
},
{
label: '<=',
value: '<=',
},
{
label: 'IN',
value: 'IN',
},
{
label: 'NOT_IN',
value: 'NOT_IN',
},
];
const conjunctions = [
{ label: 'AND', value: 'AND ' },
{ label: 'OR', value: 'OR ' },
];
// Custom extension to stop events from propagating to global shortcuts
const stopEventsExtension = EditorView.domEventHandlers({
keydown: (event) => {
// Stop all keyboard events from propagating to global shortcuts
event.stopPropagation();
event.stopImmediatePropagation();
return false; // Important for CM to know you handled it
},
input: (event) => {
event.stopPropagation();
return false;
},
focus: (event) => {
// Ensure focus events don't interfere with global shortcuts
event.stopPropagation();
return false;
},
blur: (event) => {
// Ensure blur events don't interfere with global shortcuts
event.stopPropagation();
return false;
},
});
function HavingFilter({
onClose,
onChange,
queryData,
}: {
onClose: () => void;
onChange: (value: string) => void;
queryData: IBuilderQuery;
}): JSX.Element {
const { aggregationOptions } = useQueryBuilderV2Context();
const having = queryData?.having as Having;
const [input, setInput] = useState(having?.expression || '');
useEffect(() => {
setInput(having?.expression || '');
}, [having?.expression]);
const [isFocused, setIsFocused] = useState(false);
const editorRef = useRef<EditorView | null>(null);
const [options, setOptions] = useState<{ label: string; value: string }[]>([]);
const handleChange = (value: string): void => {
setInput(value);
onChange(value);
};
useEffect(() => {
if (isFocused && editorRef.current && options.length > 0) {
startCompletion(editorRef.current);
}
}, [isFocused, options]);
// Update options when aggregation options change
useEffect(() => {
const newOptions = [];
for (let i = 0; i < aggregationOptions.length; i++) {
const opt = aggregationOptions[i];
for (let j = 0; j < havingOperators.length; j++) {
const operator = havingOperators[j];
newOptions.push({
label: `${opt.func}(${opt.arg}) ${operator.label}`,
value: `${opt.func}(${opt.arg}) ${operator.label} `,
apply: (
view: EditorView,
completion: { label: string; value: string },
from: number,
to: number,
): void => {
view.dispatch({
changes: { from, to, insert: completion.value },
selection: { anchor: from + completion.value.length },
});
// Trigger value suggestions immediately after operator
setTimeout(() => {
startCompletion(view);
}, 0);
},
});
}
}
setOptions(newOptions);
}, [aggregationOptions]);
// Helper to check if a string is a number
const isNumber = (token: string): boolean => /^-?\d+(\.\d+)?$/.test(token);
// Helper to check if we're after an operator
const isAfterOperator = (tokens: string[]): boolean => {
if (tokens.length === 0) return false;
const lastToken = tokens[tokens.length - 1];
// Check if the last token is exactly an operator or ends with an operator and space
return havingOperators.some((op) => {
const opWithSpace = `${op.value} `;
return lastToken === op.value || lastToken.endsWith(opWithSpace);
});
};
// Helper function for applying completion with space
const applyCompletionWithSpace = (
view: EditorView,
completion: Completion,
from: number,
to: number,
): void => {
const insertValue =
typeof completion.apply === 'string' ? completion.apply : completion.label;
const newText = `${insertValue} `;
const newPos = from + newText.length;
view.dispatch({
changes: { from, to, insert: newText },
selection: { anchor: newPos, head: newPos },
effects: EditorView.scrollIntoView(newPos),
});
};
const havingAutocomplete = useMemo(() => {
// Helper functions for applying completions
const forceCompletion = (view: EditorView): void => {
setTimeout(() => {
if (view) {
startCompletion(view);
}
}, 0);
};
const applyValueCompletion = (
view: EditorView,
completion: Completion,
from: number,
to: number,
): void => {
applyCompletionWithSpace(view, completion, from, to);
forceCompletion(view);
};
const applyOperatorCompletion = (
view: EditorView,
completion: Completion,
from: number,
to: number,
): void => {
const insertValue =
typeof completion.apply === 'string' ? completion.apply : completion.label;
const insertWithSpace = `${insertValue} `;
view.dispatch({
changes: { from, to, insert: insertWithSpace },
selection: { anchor: from + insertWithSpace.length },
});
forceCompletion(view);
};
return autocompletion({
override: [
(context: CompletionContext): CompletionResult | null => {
const text = context.state.sliceDoc(0, context.pos);
const trimmedText = text.trim();
const tokens = trimmedText.split(/\s+/).filter(Boolean);
// Handle empty state when no aggregation options are available
if (options.length === 0) {
return {
from: context.pos,
options: [
{
label:
'No aggregation functions available. Please add aggregation functions first.',
type: 'text',
apply: (): boolean => true,
},
],
};
}
// Close dropdown after operator to allow custom value entry
if (isAfterOperator(tokens)) {
return null;
}
// Hide suggestions while typing a value after an operator
if (
!text.endsWith(' ') &&
tokens.length >= 2 &&
havingOperators.some((op) => op.value === tokens[tokens.length - 2])
) {
return null;
}
// Suggest key/operator pairs and ( for grouping
if (
tokens.length === 0 ||
conjunctions.some((c) => tokens[tokens.length - 1] === c.value.trim()) ||
tokens[tokens.length - 1] === '('
) {
return {
from: context.pos,
options: options.map((opt) => ({
...opt,
apply: applyOperatorCompletion,
})),
};
}
// Show suggestions when typing
if (tokens.length > 0) {
const lastToken = tokens[tokens.length - 1];
const filteredOptions = options.filter((opt) =>
opt.label.toLowerCase().includes(lastToken.toLowerCase()),
);
if (filteredOptions.length > 0) {
return {
from: context.pos - lastToken.length,
options: filteredOptions.map((opt) => ({
...opt,
apply: applyOperatorCompletion,
})),
};
}
}
// Suggest conjunctions after a value and a space
if (
tokens.length > 0 &&
(isNumber(tokens[tokens.length - 1]) ||
tokens[tokens.length - 1] === ')') &&
text.endsWith(' ')
) {
return {
from: context.pos,
options: conjunctions.map((conj) => ({
...conj,
apply: applyValueCompletion,
})),
};
}
// Show all options if no other condition matches
return {
from: context.pos,
options: options.map((opt) => ({
...opt,
apply: applyOperatorCompletion,
})),
};
},
],
defaultKeymap: true,
closeOnBlur: true,
maxRenderedOptions: 200,
activateOnTyping: true,
});
}, [options]);
return (
<div className="having-filter-container">
<div className="having-filter-select-container">
<CodeMirror
value={input}
onChange={handleChange}
theme={copilot}
className="having-filter-select-editor"
width="100%"
extensions={[
havingAutocomplete,
javascript({ jsx: false, typescript: false }),
stopEventsExtension,
EditorView.lineWrapping,
keymap.of([
...completionKeymap,
{
key: 'Escape',
run: closeCompletion,
},
]),
]}
placeholder="Type Having query like count() > 10 ..."
basicSetup={{
lineNumbers: false,
autocompletion: true,
completionKeymap: true,
}}
onCreateEditor={(view: EditorView): void => {
editorRef.current = view;
}}
onFocus={(): void => {
setIsFocused(true);
if (editorRef.current) {
startCompletion(editorRef.current);
}
}}
onBlur={(): void => {
setIsFocused(false);
if (editorRef.current) {
closeCompletion(editorRef.current);
}
}}
/>
<Button
className="close-btn periscope-btn ghost"
icon={<ChevronUp size={16} />}
onClick={onClose}
/>
</div>
</div>
);
}
export default HavingFilter;

View File

@@ -0,0 +1,377 @@
.add-ons-list {
display: flex;
justify-content: space-between;
align-items: center;
.add-ons-tabs {
display: flex;
flex-wrap: wrap;
.add-on-tab-title {
display: flex;
gap: var(--margin-2);
align-items: center;
justify-content: center;
font-size: var(--font-size-xs);
font-style: normal;
font-weight: var(--font-weight-normal);
color: var(--Vanilla-400, #c0c1c3);
}
.tab {
border: 1px solid var(--bg-slate-400);
border-left: none;
min-width: 120px;
height: 36px;
line-height: 36px;
&:first-child {
border-left: 1px solid var(--bg-slate-400);
}
}
.tab::before {
background: var(--bg-slate-400);
}
.selected-view {
color: var(--text-robin-500);
border: 1px solid var(--bg-slate-400);
display: none;
}
.selected-view::before {
background: var(--bg-slate-400);
}
}
.compass-button {
width: 30px;
height: 30px;
border-radius: 2px;
border: 1px solid var(--bg-slate-400);
background: var(--bg-ink-300);
box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.1);
}
}
.having-filter-container {
width: 100%;
display: flex;
flex-direction: row;
align-items: center;
.having-filter-select-container {
width: 100%;
display: flex;
flex-direction: row;
align-items: center;
.having-filter-select-editor {
border-radius: 2px;
flex: 1;
width: calc(100% - 40px);
.cm-content {
padding: 0;
}
.cm-editor {
border-radius: 2px;
background-color: transparent !important;
position: relative !important;
&:focus-within {
border-color: var(--bg-robin-500);
}
&.cm-focused {
outline: none !important;
}
.cm-content {
border-radius: 2px;
border: 1px solid var(--Slate-400, #1d212d);
border-top-right-radius: 0px;
border-bottom-right-radius: 0px;
padding: 0px !important;
background-color: #121317 !important;
&:focus-within {
border-color: var(--bg-ink-200);
}
}
.cm-tooltip-autocomplete {
background: var(--bg-ink-300) !important;
color: var(--bg-ink-500) !important;
border-radius: 2px !important;
font-size: 12px !important;
font-weight: 500 !important;
margin-top: -2px !important;
width: 100% !important;
position: absolute !important;
top: 38px !important;
left: 0px !important;
border-radius: 4px;
border: 1px solid var(--bg-slate-200, #1d212d);
border-top: none !important;
border-top-left-radius: 0px !important;
border-top-right-radius: 0px !important;
background: linear-gradient(
139deg,
rgba(18, 19, 23, 0.8) 0%,
rgba(18, 19, 23, 0.9) 98.68%
) !important;
backdrop-filter: blur(20px);
box-sizing: border-box;
font-family: 'Space Mono', monospace !important;
ul {
width: 100% !important;
max-width: 100% !important;
font-family: 'Space Mono', monospace !important;
min-height: 200px !important;
&::-webkit-scrollbar {
width: 0.3rem;
}
&::-webkit-scrollbar-corner {
background: transparent;
}
&::-webkit-scrollbar-thumb {
background: rgb(136, 136, 136);
border-radius: 0.625rem;
}
&::-webkit-scrollbar-track {
background: transparent;
}
li {
width: 100% !important;
max-width: 100% !important;
line-height: 36px !important;
height: 36px !important;
padding: 4px 8px !important;
display: flex !important;
align-items: center !important;
gap: 8px !important;
box-sizing: border-box;
overflow: hidden;
font-family: 'Space Mono', monospace !important;
color: var(--bg-vanilla-100) !important;
.cm-completionIcon {
display: none !important;
}
&[aria-selected='true'] {
// background-color: rgba(78, 116, 248, 0.7) !important;
background: rgba(171, 189, 255, 0.04) !important;
}
}
}
}
.cm-gutters {
display: none !important;
}
.cm-scroller {
scrollbar-width: none;
&::-webkit-scrollbar {
display: none;
}
&::-webkit-scrollbar-thumb {
display: none;
}
&::-webkit-scrollbar-track {
display: none;
}
&::-webkit-scrollbar-corner {
display: none;
}
}
.cm-line {
line-height: 36px !important;
font-family: 'Space Mono', monospace !important;
background-color: #121317 !important;
::-moz-selection {
background: var(--bg-ink-100) !important;
opacity: 0.5 !important;
}
::selection {
background: var(--bg-ink-100) !important;
opacity: 0.5 !important;
}
.cm-function {
color: var(--bg-robin-500) !important;
}
.chip-decorator {
background: rgba(36, 40, 52, 1) !important;
color: var(--bg-vanilla-100) !important;
border-radius: 4px;
padding: 2px 4px;
margin-right: 4px;
}
}
.cm-selectionBackground {
background: var(--bg-ink-100) !important;
opacity: 0.5 !important;
}
}
}
.close-btn {
border-radius: 0px 2px 2px 0px;
border: 1px solid var(--bg-slate-400);
background: var(--bg-ink-300);
height: 38px;
width: 38px;
border-left: transparent;
border-top-left-radius: 0px;
border-bottom-left-radius: 0px;
}
}
}
.selected-add-ons-content {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(420px, 1fr));
gap: 8px;
padding-bottom: 8px;
position: relative;
.add-on-content {
display: flex;
flex-direction: column;
gap: 8px;
max-width: 100%;
min-width: 100%;
min-width: 420px;
box-sizing: border-box;
position: relative;
}
}
.lightMode {
.add-ons-list {
.add-ons-tabs {
.add-on-tab-title {
color: var(--bg-ink-500) !important;
}
.tab {
border: 1px solid var(--bg-vanilla-300) !important;
background: var(--bg-vanilla-100) !important;
&:first-child {
border-left: 1px solid var(--bg-vanilla-300) !important;
}
}
.tab::before {
background: var(--bg-vanilla-300) !important;
}
.selected-view {
color: var(--bg-robin-500) !important;
border: 1px solid var(--bg-vanilla-300) !important;
}
.selected-view::before {
background: var(--bg-vanilla-300) !important;
}
}
.compass-button {
border: 1px solid var(--bg-vanilla-300) !important;
background: var(--bg-vanilla-100) !important;
}
}
.having-filter-container {
.having-filter-select-container {
.having-filter-select-editor {
.cm-editor {
&:focus-within {
border-color: var(--bg-vanilla-300) !important;
}
.cm-content {
border: 1px solid var(--bg-vanilla-300) !important;
background: var(--bg-vanilla-100) !important;
&:focus-within {
border-color: var(--bg-vanilla-300) !important;
}
}
.cm-tooltip-autocomplete {
background: var(--bg-vanilla-100) !important;
border: 1px solid var(--bg-vanilla-300) !important;
color: var(--bg-ink-500) !important;
ul {
li {
&:hover {
background: var(--bg-vanilla-300) !important;
}
&[aria-selected='true'] {
color: var(--bg-ink-500) !important;
background: var(--bg-vanilla-300) !important;
}
}
}
}
.cm-line {
background-color: var(--bg-vanilla-100) !important;
::-moz-selection {
background: var(--bg-vanilla-100) !important;
}
::selection {
background: var(--bg-ink-100) !important;
}
.chip-decorator {
background: var(--bg-robin-100) !important;
color: var(--bg-ink-400) !important;
}
}
.cm-selectionBackground {
background: var(--bg-vanilla-100) !important;
}
}
}
.close-btn {
border: 1px solid var(--bg-vanilla-300) !important;
background: var(--bg-vanilla-100) !important;
}
}
}
}

View File

@@ -0,0 +1,354 @@
import './QueryAddOns.styles.scss';
import { Button, Radio, RadioChangeEvent } from 'antd';
import InputWithLabel from 'components/InputWithLabel/InputWithLabel';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { GroupByFilter } from 'container/QueryBuilder/filters/GroupByFilter/GroupByFilter';
import { OrderByFilter } from 'container/QueryBuilder/filters/OrderByFilter/OrderByFilter';
import { ReduceToFilter } from 'container/QueryBuilder/filters/ReduceToFilter/ReduceToFilter';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useQueryOperations } from 'hooks/queryBuilder/useQueryBuilderOperations';
import { isEmpty } from 'lodash-es';
import { BarChart2, ChevronUp, ScrollText } from 'lucide-react';
import { useCallback, useEffect, useState } from 'react';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import { MetricAggregation } from 'types/api/v5/queryRange';
import { DataSource, ReduceOperators } from 'types/common/queryBuilder';
import HavingFilter from './HavingFilter/HavingFilter';
interface AddOn {
icon: React.ReactNode;
label: string;
key: string;
}
const ADD_ONS_KEYS = {
GROUP_BY: 'group_by',
HAVING: 'having',
ORDER_BY: 'order_by',
LIMIT: 'limit',
LEGEND_FORMAT: 'legend_format',
};
const ADD_ONS = [
{
icon: <BarChart2 size={14} />,
label: 'Group By',
key: 'group_by',
},
{
icon: <ScrollText size={14} />,
label: 'Having',
key: 'having',
},
{
icon: <ScrollText size={14} />,
label: 'Order By',
key: 'order_by',
},
{
icon: <ScrollText size={14} />,
label: 'Limit',
key: 'limit',
},
{
icon: <ScrollText size={14} />,
label: 'Legend format',
key: 'legend_format',
},
];
const REDUCE_TO = {
icon: <ScrollText size={14} />,
label: 'Reduce to',
key: 'reduce_to',
};
function QueryAddOns({
query,
version,
isListViewPanel,
showReduceTo,
panelType,
index,
}: {
query: IBuilderQuery;
version: string;
isListViewPanel: boolean;
showReduceTo: boolean;
panelType: PANEL_TYPES | null;
index: number;
}): JSX.Element {
const [addOns, setAddOns] = useState<AddOn[]>(ADD_ONS);
const [selectedViews, setSelectedViews] = useState<AddOn[]>([]);
const { handleChangeQueryData } = useQueryOperations({
index,
query,
entityVersion: '',
});
const { handleSetQueryData } = useQueryBuilder();
useEffect(() => {
if (isListViewPanel) {
setAddOns([]);
setSelectedViews([
ADD_ONS.find((addOn) => addOn.key === ADD_ONS_KEYS.ORDER_BY) as AddOn,
]);
return;
}
let filteredAddOns: AddOn[];
if (panelType === PANEL_TYPES.VALUE) {
// Filter out all add-ons except legend format
filteredAddOns = ADD_ONS.filter(
(addOn) => addOn.key === ADD_ONS_KEYS.LEGEND_FORMAT,
);
} else {
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) {
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, query.dataSource]);
const handleOptionClick = (e: RadioChangeEvent): void => {
if (selectedViews.find((view) => view.key === e.target.value.key)) {
setSelectedViews(
selectedViews.filter((view) => view.key !== e.target.value.key),
);
} else {
setSelectedViews([...selectedViews, e.target.value]);
}
};
const handleChangeGroupByKeys = useCallback(
(value: IBuilderQuery['groupBy']) => {
handleChangeQueryData('groupBy', value);
},
[handleChangeQueryData],
);
const handleChangeOrderByKeys = useCallback(
(value: IBuilderQuery['orderBy']) => {
handleChangeQueryData('orderBy', value);
},
[handleChangeQueryData],
);
const handleChangeReduceToV5 = useCallback(
(value: ReduceOperators) => {
handleSetQueryData(index, {
...query,
aggregations: [
{
...(query.aggregations?.[0] as MetricAggregation),
reduceTo: value,
},
],
});
},
[handleSetQueryData, index, query],
);
const handleRemoveView = useCallback(
(key: string): void => {
setSelectedViews(selectedViews.filter((view) => view.key !== key));
},
[selectedViews],
);
const handleChangeQueryLegend = useCallback(
(value: string) => {
handleChangeQueryData('legend', value);
},
[handleChangeQueryData],
);
const handleChangeLimit = useCallback(
(value: string) => {
handleChangeQueryData('limit', Number(value) || null);
},
[handleChangeQueryData],
);
const handleChangeHaving = useCallback(
(value: string) => {
handleChangeQueryData('having', {
expression: value,
});
},
[handleChangeQueryData],
);
return (
<div className="query-add-ons">
{selectedViews.length > 0 && (
<div className="selected-add-ons-content">
{selectedViews.find((view) => view.key === 'group_by') && (
<div className="add-on-content">
<div className="periscope-input-with-label">
<div className="label">Group By</div>
<div className="input">
<GroupByFilter
disabled={
query.dataSource === DataSource.METRICS &&
!(query.aggregations?.[0] as MetricAggregation)?.metricName
}
query={query}
onChange={handleChangeGroupByKeys}
/>
</div>
<Button
className="close-btn periscope-btn ghost"
icon={<ChevronUp size={16} />}
onClick={(): void => handleRemoveView('group_by')}
/>
</div>
</div>
)}
{selectedViews.find((view) => view.key === 'having') && (
<div className="add-on-content">
<div className="periscope-input-with-label">
<div className="label">Having</div>
<div className="input">
<HavingFilter
onClose={(): void => {
setSelectedViews(
selectedViews.filter((view) => view.key !== 'having'),
);
}}
onChange={handleChangeHaving}
queryData={query}
/>
</div>
</div>
</div>
)}
{selectedViews.find((view) => view.key === 'limit') && (
<div className="add-on-content">
<InputWithLabel
label="Limit"
onChange={handleChangeLimit}
initialValue={query?.limit ?? undefined}
placeholder="Enter limit"
onClose={(): void => {
setSelectedViews(selectedViews.filter((view) => view.key !== 'limit'));
}}
closeIcon={<ChevronUp size={16} />}
/>
</div>
)}
{selectedViews.find((view) => view.key === 'order_by') && (
<div className="add-on-content">
<div className="periscope-input-with-label">
<div className="label">Order By</div>
<div className="input">
<OrderByFilter
entityVersion={version}
query={query}
onChange={handleChangeOrderByKeys}
isListViewPanel={isListViewPanel}
isNewQueryV2
/>
</div>
{!isListViewPanel && (
<Button
className="close-btn periscope-btn ghost"
icon={<ChevronUp size={16} />}
onClick={(): void => handleRemoveView('order_by')}
/>
)}
</div>
</div>
)}
{selectedViews.find((view) => view.key === 'reduce_to') && showReduceTo && (
<div className="add-on-content">
<div className="periscope-input-with-label">
<div className="label">Reduce to</div>
<div className="input">
<ReduceToFilter query={query} onChange={handleChangeReduceToV5} />
</div>
<Button
className="close-btn periscope-btn ghost"
icon={<ChevronUp size={16} />}
onClick={(): void => handleRemoveView('reduce_to')}
/>
</div>
</div>
)}
{selectedViews.find((view) => view.key === 'legend_format') && (
<div className="add-on-content">
<InputWithLabel
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'),
);
}}
closeIcon={<ChevronUp size={16} />}
/>
</div>
)}
</div>
)}
<div className="add-ons-list">
<Radio.Group
className="add-ons-tabs"
onChange={handleOptionClick}
value={selectedViews}
>
{addOns.map((addOn) => (
<Radio.Button
key={addOn.label}
className={
selectedViews.find((view) => view.key === addOn.key)
? 'selected-view tab'
: 'tab'
}
value={addOn}
>
<div className="add-on-tab-title">
{addOn.icon}
{addOn.label}
</div>
</Radio.Button>
))}
</Radio.Group>
</div>
</div>
);
}
export default QueryAddOns;

View File

@@ -0,0 +1,336 @@
.query-aggregation-container {
display: block;
.aggregation-container {
display: flex;
flex-direction: row;
gap: 8px;
align-items: flex-start;
flex-wrap: wrap;
.query-aggregation-select-container {
display: flex;
flex-direction: row;
align-items: center;
flex: 1;
min-width: 400px;
position: relative;
.query-aggregation-select-editor {
border-radius: 2px;
flex: 1;
min-width: 0;
&.error {
.cm-editor {
.cm-content {
border-color: var(--bg-cherry-500) !important;
}
}
}
.cm-content {
padding: 0;
}
.cm-editor {
border-radius: 2px;
background-color: transparent !important;
position: relative !important;
&.cm-focused {
outline: none !important;
}
&:focus-within {
border-color: var(--bg-robin-500);
}
.cm-content {
border-radius: 2px;
border: 1px solid var(--Slate-400, #1d212d);
border-top-right-radius: 0px;
border-bottom-right-radius: 0px;
padding: 0px !important;
background-color: #121317 !important;
&:focus-within {
border-color: var(--bg-ink-200);
}
}
.cm-tooltip-autocomplete {
background: var(--bg-ink-300) !important;
border-radius: 2px !important;
font-size: 12px !important;
font-weight: 500 !important;
margin-top: 8px !important;
min-width: 400px !important;
position: absolute !important;
left: 0px !important;
width: 100% !important;
border-radius: 4px;
border: 1px solid var(--bg-slate-200, #1d212d);
border-top: none !important;
border-top-left-radius: 0px !important;
border-top-right-radius: 0px !important;
background: linear-gradient(
139deg,
rgba(18, 19, 23, 0.8) 0%,
rgba(18, 19, 23, 0.9) 98.68%
) !important;
backdrop-filter: blur(20px);
box-sizing: border-box;
font-family: 'Space Mono', monospace !important;
ul {
width: 100% !important;
max-width: 100% !important;
font-family: 'Space Mono', monospace !important;
min-height: 200px !important;
&::-webkit-scrollbar {
width: 0.3rem;
}
&::-webkit-scrollbar-corner {
background: transparent;
}
&::-webkit-scrollbar-thumb {
background: rgb(136, 136, 136);
border-radius: 0.625rem;
}
&::-webkit-scrollbar-track {
background: transparent;
}
li {
width: 100% !important;
max-width: 100% !important;
line-height: 36px !important;
height: 36px !important;
padding: 4px 8px !important;
display: flex !important;
align-items: center !important;
gap: 8px !important;
box-sizing: border-box;
overflow: hidden;
font-family: 'Space Mono', monospace !important;
.cm-completionIcon {
display: none !important;
}
&[aria-selected='true'] {
// background-color: rgba(78, 116, 248, 0.7) !important;
background: rgba(171, 189, 255, 0.04) !important;
}
}
}
}
.cm-gutters {
display: none !important;
}
.cm-line {
line-height: 36px !important;
font-family: 'Space Mono', monospace !important;
background-color: #121317 !important;
::-moz-selection {
background: var(--bg-ink-100) !important;
opacity: 0.5 !important;
}
::selection {
background: var(--bg-ink-100) !important;
opacity: 0.5 !important;
}
.cm-function {
color: var(--bg-robin-500) !important;
}
.chip-decorator {
background: rgba(36, 40, 52, 1) !important;
color: var(--bg-vanilla-100) !important;
border-radius: 4px;
padding: 2px 4px;
margin-right: 4px;
}
}
.cm-selectionBackground {
background: var(--bg-ink-100) !important;
opacity: 0.5 !important;
}
}
}
.query-aggregation-error-container {
position: absolute;
right: 8px;
top: 50%;
transform: translateY(-50%);
z-index: 1;
.query-aggregation-error-content {
padding: 8px;
max-width: 300px;
.query-aggregation-error-message {
color: var(--bg-cherry-500);
font-size: 12px;
line-height: 16px;
}
}
.query-aggregation-error-btn {
padding: 4px;
height: auto;
min-width: auto;
}
}
.close-btn {
border-radius: 0px 2px 2px 0px;
border: 1px solid var(--bg-slate-400);
background: var(--bg-ink-300);
height: 38px;
width: 38px;
border-left: transparent;
border-top-left-radius: 0px;
border-bottom-left-radius: 0px;
}
}
.query-aggregation-options-input {
width: 100%;
height: 36px;
line-height: 36px;
border-radius: 2px;
border: 1px solid var(--bg-slate-400);
box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.1);
font-family: 'Space Mono', monospace !important;
&::placeholder {
color: var(--bg-vanilla-100);
opacity: 0.5;
}
}
.query-aggregation-interval {
display: flex;
flex-direction: row;
align-items: center;
gap: 8px;
max-width: 360px;
.query-aggregation-interval-input-container {
.query-aggregation-interval-input {
input {
max-width: 120px;
}
}
}
}
}
}
.lightMode {
.query-aggregation-container {
.aggregation-container {
.query-aggregation-options-input {
border-color: var(--bg-vanilla-300) !important;
&::placeholder {
color: var(--bg-ink-400) !important;
opacity: 0.5 !important;
}
}
.query-aggregation-select-container {
.query-aggregation-select-editor {
.cm-editor {
.cm-content {
border: 1px solid var(--bg-vanilla-300) !important;
background: var(--bg-vanilla-100) !important;
box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.1) !important;
&:focus-within {
border-color: var(--bg-vanilla-300) !important;
}
}
.cm-tooltip-autocomplete {
background: var(--bg-vanilla-100) !important;
border: 1px solid var(--bg-vanilla-300) !important;
color: var(--bg-ink-500) !important;
ul {
li {
&:hover {
background-color: var(--bg-vanilla-300) !important;
color: var(--bg-ink-500) !important;
font-weight: 600;
}
&[aria-selected='true'] {
background: var(--bg-vanilla-300) !important;
color: var(--bg-ink-500) !important;
font-weight: 600;
}
}
}
}
.cm-line {
background-color: var(--bg-vanilla-100) !important;
::-moz-selection {
background: var(--bg-vanilla-100) !important;
opacity: 0.5 !important;
}
::selection {
background: var(--bg-vanilla-100) !important;
opacity: 0.5 !important;
}
.cm-function {
color: var(--bg-robin-500) !important;
}
.chip-decorator {
background: var(--bg-robin-500) !important;
color: var(--bg-ink-400) !important;
}
}
// .cm-selectionBackground {
// background: var(--bg-vanilla-100) !important;
// opacity: 0.5 !important;
// }
}
}
.close-btn {
border-color: var(--bg-vanilla-300) !important;
background: var(--bg-vanilla-100) !important;
}
}
}
}
}
.query-aggregation-error-popover {
.ant-popover-inner {
background-color: var(--bg-slate-500);
border: 1px solid var(--bg-slate-400);
border-radius: 4px;
box-shadow: 0px 4px 12px rgba(0, 0, 0, 0.1);
}
}

View File

@@ -0,0 +1,82 @@
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';
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
if (panelType === PANEL_TYPES.VALUE) {
return false;
}
if (dataSource === DataSource.TRACES || dataSource === DataSource.LOGS) {
return !(panelType === PANEL_TYPES.TABLE || panelType === PANEL_TYPES.PIE);
}
return true;
}, [dataSource, panelType]);
const handleAggregationIntervalChange = (value: string): void => {
onAggregationIntervalChange(Number(value));
};
return (
<div className="query-aggregation-container">
<div className="aggregation-container">
<QueryAggregationSelect
onChange={onChange}
queryData={queryData}
maxAggregations={
panelType === PANEL_TYPES.VALUE || panelType === PANEL_TYPES.PIE
? 1
: undefined
}
/>
{showAggregationInterval && (
<div className="query-aggregation-interval">
<div className="query-aggregation-interval-label">every</div>
<div className="query-aggregation-interval-input-container">
<InputWithLabel
initialValue={
queryData?.stepInterval ? queryData?.stepInterval : undefined
}
className="query-aggregation-interval-input"
label="Seconds"
placeholder="Auto"
type="number"
onChange={handleAggregationIntervalChange}
labelAfter
/>
</div>
</div>
)}
</div>
</div>
);
}
QueryAggregationOptions.defaultProps = {
panelType: null,
onChange: undefined,
};
export default QueryAggregationOptions;

View File

@@ -0,0 +1,668 @@
/* eslint-disable import/no-extraneous-dependencies */
/* eslint-disable no-cond-assign */
/* eslint-disable no-restricted-syntax */
/* eslint-disable class-methods-use-this */
/* eslint-disable react/no-this-in-sfc */
/* eslint-disable sonarjs/cognitive-complexity */
import './QueryAggregation.styles.scss';
import {
autocompletion,
closeCompletion,
Completion,
CompletionContext,
completionKeymap,
CompletionResult,
startCompletion,
} from '@codemirror/autocomplete';
import { javascript } from '@codemirror/lang-javascript';
import { EditorState, RangeSetBuilder, Transaction } from '@codemirror/state';
import { Color } from '@signozhq/design-tokens';
import { copilot } from '@uiw/codemirror-theme-copilot';
import CodeMirror, {
Decoration,
EditorView,
keymap,
ViewPlugin,
ViewUpdate,
} from '@uiw/react-codemirror';
import { Button, Popover } from 'antd';
import { getKeySuggestions } from 'api/querySuggestions/getKeySuggestions';
import { QUERY_BUILDER_KEY_TYPES } from 'constants/antlrQueryConstants';
import { QueryBuilderKeys } from 'constants/queryBuilder';
import { tracesAggregateOperatorOptions } from 'constants/queryBuilderOperators';
import { TriangleAlert } from 'lucide-react';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useQuery } from 'react-query';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import { TracesAggregatorOperator } from 'types/common/queryBuilder';
import { useQueryBuilderV2Context } from '../../QueryBuilderV2Context';
const chipDecoration = Decoration.mark({
class: 'chip-decorator',
});
const operatorArgMeta: Record<
string,
{ acceptsArgs: boolean; multiple: boolean }
> = {
[TracesAggregatorOperator.NOOP]: { acceptsArgs: false, multiple: false },
[TracesAggregatorOperator.COUNT]: { acceptsArgs: false, multiple: false },
[TracesAggregatorOperator.COUNT_DISTINCT]: {
acceptsArgs: true,
multiple: true,
},
[TracesAggregatorOperator.SUM]: { acceptsArgs: true, multiple: false },
[TracesAggregatorOperator.AVG]: { acceptsArgs: true, multiple: false },
[TracesAggregatorOperator.MAX]: { acceptsArgs: true, multiple: false },
[TracesAggregatorOperator.MIN]: { acceptsArgs: true, multiple: false },
[TracesAggregatorOperator.P05]: { acceptsArgs: true, multiple: false },
[TracesAggregatorOperator.P10]: { acceptsArgs: true, multiple: false },
[TracesAggregatorOperator.P20]: { acceptsArgs: true, multiple: false },
[TracesAggregatorOperator.P25]: { acceptsArgs: true, multiple: false },
[TracesAggregatorOperator.P50]: { acceptsArgs: true, multiple: false },
[TracesAggregatorOperator.P75]: { acceptsArgs: true, multiple: false },
[TracesAggregatorOperator.P90]: { acceptsArgs: true, multiple: false },
[TracesAggregatorOperator.P95]: { acceptsArgs: true, multiple: false },
[TracesAggregatorOperator.P99]: { acceptsArgs: true, multiple: false },
[TracesAggregatorOperator.RATE]: { acceptsArgs: true, multiple: false },
[TracesAggregatorOperator.RATE_SUM]: { acceptsArgs: true, multiple: false },
[TracesAggregatorOperator.RATE_AVG]: { acceptsArgs: true, multiple: false },
[TracesAggregatorOperator.RATE_MIN]: { acceptsArgs: true, multiple: false },
[TracesAggregatorOperator.RATE_MAX]: { acceptsArgs: true, multiple: false },
};
function getFunctionContextAtCursor(
text: string,
cursorPos: number,
): string | null {
// Find the nearest function name to the left of the nearest unmatched '('
let openParenIndex = -1;
let funcName: string | null = null;
let parenStack = 0;
for (let i = cursorPos - 1; i >= 0; i--) {
if (text[i] === ')') parenStack++;
else if (text[i] === '(') {
if (parenStack === 0) {
openParenIndex = i;
const before = text.slice(0, i);
const match = before.match(/(\w+)\s*$/);
if (match) funcName = match[1].toLowerCase();
break;
}
parenStack--;
}
}
if (openParenIndex === -1 || !funcName) return null;
// Scan forwards to find the matching closing parenthesis
let closeParenIndex = -1;
let depth = 1;
for (let j = openParenIndex + 1; j < text.length; j++) {
if (text[j] === '(') depth++;
else if (text[j] === ')') depth--;
if (depth === 0) {
closeParenIndex = j;
break;
}
}
if (
cursorPos > openParenIndex &&
(closeParenIndex === -1 || cursorPos <= closeParenIndex)
) {
return funcName;
}
return null;
}
// Custom extension to stop events from propagating to global shortcuts
const stopEventsExtension = EditorView.domEventHandlers({
keydown: (event) => {
// Stop all keyboard events from propagating to global shortcuts
event.stopPropagation();
event.stopImmediatePropagation();
return false; // Important for CM to know you handled it
},
input: (event) => {
event.stopPropagation();
return false;
},
focus: (event) => {
// Ensure focus events don't interfere with global shortcuts
event.stopPropagation();
return false;
},
blur: (event) => {
// Ensure blur events don't interfere with global shortcuts
event.stopPropagation();
return false;
},
});
// eslint-disable-next-line react/no-this-in-sfc
function QueryAggregationSelect({
onChange,
queryData,
maxAggregations,
}: {
onChange?: (value: string) => void;
queryData: IBuilderQuery;
maxAggregations?: number;
}): JSX.Element {
const { setAggregationOptions } = useQueryBuilderV2Context();
const [input, setInput] = useState(
queryData?.aggregations?.map((i: any) => i.expression).join(' ') || '',
);
useEffect(() => {
setInput(
queryData?.aggregations?.map((i: any) => i.expression).join(' ') || '',
);
}, [queryData?.aggregations]);
const [cursorPos, setCursorPos] = useState(0);
const [functionArgPairs, setFunctionArgPairs] = useState<
{ func: string; arg: string }[]
>([]);
const [validationError, setValidationError] = useState<string | null>(null);
const editorRef = useRef<EditorView | null>(null);
const [isFocused, setIsFocused] = useState(false);
// Get valid function names (lowercase)
const validFunctions = useMemo(
() => tracesAggregateOperatorOptions.map((op) => op.value.toLowerCase()),
[],
);
// Helper function to safely start completion
const safeStartCompletion = useCallback((): void => {
requestAnimationFrame(() => {
if (editorRef.current) {
startCompletion(editorRef.current);
}
});
}, []);
// Update cursor position on every editor update
const handleUpdate = (update: { view: EditorView }): void => {
const pos = update.view.state.selection.main.from;
setCursorPos(pos);
};
// Effect to handle focus state and trigger suggestions
useEffect(() => {
if (isFocused) {
safeStartCompletion();
}
}, [isFocused, safeStartCompletion]);
// Extract all valid function-argument pairs from the input
useEffect(() => {
const pairs: { func: string; arg: string }[] = [];
const regex = /([a-zA-Z_][\w]*)\s*\(([^)]*)\)/g;
let match;
while ((match = regex.exec(input)) !== null) {
const func = match[1].toLowerCase();
const args = match[2]
.split(',')
.map((arg) => arg.trim())
.filter((arg) => arg.length > 0);
if (args.length === 0) {
// For functions with no arguments, add a pair with empty string as arg
pairs.push({ func, arg: '' });
} else {
args.forEach((arg) => {
pairs.push({ func, arg });
});
}
}
// Validation logic
const validateAggregations = (): string | null => {
// Check maxAggregations limit
if (maxAggregations !== undefined && pairs.length > maxAggregations) {
return `Maximum ${maxAggregations} aggregation${
maxAggregations === 1 ? '' : 's'
} allowed`;
}
// Check for invalid functions
const invalidFuncs = pairs.filter(
(pair) => !validFunctions.includes(pair.func),
);
if (invalidFuncs.length > 0) {
const funcs = invalidFuncs.map((f) => f.func).join(', ');
return `Invalid function${invalidFuncs.length === 1 ? '' : 's'}: ${funcs}`;
}
// Check for incomplete function calls
if (/([a-zA-Z_][\w]*)\s*\([^)]*$/g.test(input)) {
return 'Incomplete function call - missing closing parenthesis';
}
// Check for empty function calls that require arguments
const emptyFuncs = (input.match(/([a-zA-Z_][\w]*)\s*\(\s*\)/g) || [])
.map((call) => call.match(/([a-zA-Z_][\w]*)/)?.[1])
.filter((func): func is string => Boolean(func))
.filter((func) => operatorArgMeta[func.toLowerCase()]?.acceptsArgs);
if (emptyFuncs.length > 0) {
const isPlural = emptyFuncs.length > 1;
return `Function${isPlural ? 's' : ''} ${emptyFuncs.join(', ')} require${
isPlural ? '' : 's'
} arguments`;
}
return null;
};
setValidationError(validateAggregations());
setFunctionArgPairs(pairs);
setAggregationOptions(pairs);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [input, maxAggregations, validFunctions]);
// Transaction filter to limit aggregations
const transactionFilterExtension = useMemo(() => {
if (maxAggregations === undefined) return [];
return EditorState.transactionFilter.of((tr: Transaction) => {
if (!tr.docChanged) return tr;
const regex = /([a-zA-Z_][\w]*)\s*\(([^)]*)\)/g;
const oldMatches = [
...tr.startState.doc.toString().matchAll(regex),
].filter((match) => validFunctions.includes(match[1].toLowerCase()));
const newMatches = [
...tr.newDoc.toString().matchAll(regex),
].filter((match) => validFunctions.includes(match[1].toLowerCase()));
if (
newMatches.length > oldMatches.length &&
newMatches.length > maxAggregations
) {
return []; // Cancel transaction
}
return tr;
});
}, [maxAggregations, validFunctions]);
// Find function context for fetching suggestions
const functionContextForFetch = getFunctionContextAtCursor(input, cursorPos);
const { data: aggregateAttributeData, isLoading: isLoadingFields } = useQuery(
[
QueryBuilderKeys.GET_AGGREGATE_ATTRIBUTE,
functionContextForFetch,
queryData.dataSource,
],
() => {
const operatorsWithoutDataType: (string | undefined)[] = [
TracesAggregatorOperator.COUNT,
TracesAggregatorOperator.COUNT_DISTINCT,
TracesAggregatorOperator.RATE,
];
const fieldDataType =
functionContextForFetch &&
operatorsWithoutDataType.includes(functionContextForFetch)
? undefined
: QUERY_BUILDER_KEY_TYPES.NUMBER;
return getKeySuggestions({
signal: queryData.dataSource,
searchText: '',
fieldDataType: fieldDataType as QUERY_BUILDER_KEY_TYPES,
});
},
{
enabled:
!!functionContextForFetch &&
!!operatorArgMeta[functionContextForFetch]?.acceptsArgs,
},
);
// Memoized chipPlugin that highlights valid function calls like count(), max(arg), min(arg)
const chipPlugin = useMemo(
() =>
ViewPlugin.fromClass(
class {
decorations: import('@codemirror/view').DecorationSet;
constructor(view: EditorView) {
this.decorations = this.buildDecorations(view);
}
update(update: ViewUpdate): void {
if (update.docChanged || update.viewportChanged) {
this.decorations = this.buildDecorations(update.view);
}
}
buildDecorations(
view: EditorView,
): import('@codemirror/view').DecorationSet {
const builder = new RangeSetBuilder<Decoration>();
for (const { from, to } of view.visibleRanges) {
const text = view.state.doc.sliceString(from, to);
const regex = /\b([a-zA-Z_][\w]*)\s*\(([^)]*)\)/g;
let match;
while ((match = regex.exec(text)) !== null) {
const func = match[1].toLowerCase();
if (validFunctions.includes(func)) {
const start = from + match.index;
const end = start + match[0].length;
builder.add(start, end, chipDecoration);
}
}
}
return builder.finish();
}
},
{
decorations: (v: any): import('@codemirror/view').DecorationSet =>
v.decorations,
},
),
[validFunctions],
) as any;
const operatorCompletions: Completion[] = tracesAggregateOperatorOptions.map(
(op) => ({
label: op.value,
type: 'function',
info: op.label,
apply: (
view: EditorView,
completion: Completion,
from: number,
to: number,
): void => {
const acceptsArgs = operatorArgMeta[op.value]?.acceptsArgs;
let insertText: string;
let cursorPos: number;
if (!acceptsArgs) {
insertText = `${op.value}() `;
cursorPos = from + insertText.length; // Use insertText.length instead of hardcoded values
} else {
insertText = `${op.value}(`;
cursorPos = from + insertText.length; // Use insertText.length instead of hardcoded values
}
view.dispatch({
changes: { from, to, insert: insertText },
selection: { anchor: cursorPos },
});
// Trigger suggestions after a small delay
setTimeout(() => {
safeStartCompletion();
}, 50);
},
}),
);
// Memoize field suggestions from API (no filtering here)
const fieldSuggestions = useMemo(
() =>
Object.keys(aggregateAttributeData?.data.data.keys || {}).flatMap((key) => {
const attributeKeys = aggregateAttributeData?.data.data.keys[key];
if (!attributeKeys) return [];
return attributeKeys.map((attributeKey) => ({
label: attributeKey.name,
type: 'variable',
info: attributeKey.fieldDataType,
apply: (
view: EditorView,
completion: Completion,
from: number,
to: number,
): void => {
const text = view.state.sliceDoc(0, from);
const funcName = getFunctionContextAtCursor(text, from);
const multiple = funcName ? operatorArgMeta[funcName]?.multiple : false;
// Insert the selected key followed by either a comma or closing parenthesis
const insertText = multiple
? `${completion.label},`
: `${completion.label}) `;
const cursorPos = from + insertText.length; // Use insertText.length instead of hardcoded values
view.dispatch({
changes: { from, to, insert: insertText },
selection: { anchor: cursorPos },
});
// Trigger next suggestions after a small delay
setTimeout(() => {
safeStartCompletion();
}, 50);
},
}));
}) || [],
[aggregateAttributeData, safeStartCompletion],
);
const aggregatorAutocomplete = useMemo(
() =>
autocompletion({
override: [
(context: CompletionContext): CompletionResult | null => {
const text = context.state.sliceDoc(0, context.state.doc.length);
const cursorPos = context.pos;
const funcName = getFunctionContextAtCursor(text, cursorPos);
// Check if over limit and not editing existing
if (maxAggregations !== undefined) {
const regex = /([a-zA-Z_][\w]*)\s*\(([^)]*)\)/g;
const matches = [...text.matchAll(regex)].filter((match) =>
validFunctions.includes(match[1].toLowerCase()),
);
if (matches.length >= maxAggregations) {
const isEditing = matches.some((match) => {
const start = match.index ?? 0;
return cursorPos >= start && cursorPos <= start + match[0].length;
});
if (!isEditing) return null;
}
}
// Do not show suggestions if inside count()
if (
funcName === TracesAggregatorOperator.COUNT &&
cursorPos > 0 &&
text[cursorPos - 1] !== ')'
) {
return null;
}
// If inside a function that accepts args, show field suggestions
if (funcName && operatorArgMeta[funcName]?.acceptsArgs) {
if (isLoadingFields) {
return {
from: cursorPos,
options: [
{
label: 'Loading suggestions...',
type: 'text',
apply: (): void => {},
},
],
};
}
const doc = context.state.sliceDoc(0, cursorPos);
const lastOpenParen = doc.lastIndexOf('(');
const lastComma = doc.lastIndexOf(',', cursorPos - 1);
const startOfArg =
lastComma > lastOpenParen ? lastComma + 1 : lastOpenParen + 1;
const inputText = doc.slice(startOfArg, cursorPos).trim();
// Parse arguments already present in the function call (before the cursor)
const usedArgs = new Set<string>();
if (lastOpenParen !== -1) {
const argsString = doc.slice(lastOpenParen + 1, cursorPos);
argsString.split(',').forEach((arg) => {
const trimmed = arg.trim();
if (trimmed) usedArgs.add(trimmed);
});
}
// Exclude arguments already paired with this function elsewhere in the input
const globalUsedArgs = new Set(
functionArgPairs
.filter((pair) => pair.func === funcName)
.map((pair) => pair.arg),
);
const availableSuggestions = fieldSuggestions.filter(
(suggestion) =>
!usedArgs.has(suggestion.label) &&
!globalUsedArgs.has(suggestion.label),
);
const filteredSuggestions =
inputText === ''
? availableSuggestions
: availableSuggestions.filter((suggestion) =>
suggestion.label.toLowerCase().includes(inputText.toLowerCase()),
);
return {
from: startOfArg,
options: filteredSuggestions,
};
}
// Show operator suggestions if no function context or not accepting args
if (!funcName || !operatorArgMeta[funcName]?.acceptsArgs) {
// Check if 'count(' is present in the current input (case-insensitive)
const hasCount = text.toLowerCase().includes('count(');
const availableOperators = hasCount
? operatorCompletions.filter((op) => op.label.toLowerCase() !== 'count')
: operatorCompletions;
// Get the word before cursor if any
const word = context.matchBefore(/[\w\d_]+/);
// Show suggestions if:
// 1. There's a word match
// 2. The input is empty (cursor at start)
// 3. The user explicitly triggered completion
if (word || cursorPos === 0 || context.explicit) {
return {
from: word ? word.from : cursorPos,
options: availableOperators,
};
}
}
return null;
},
],
defaultKeymap: true,
closeOnBlur: true,
maxRenderedOptions: 50,
activateOnTyping: true,
}),
[
operatorCompletions,
isLoadingFields,
fieldSuggestions,
functionArgPairs,
maxAggregations,
validFunctions,
],
);
return (
<div className="query-aggregation-select-container">
<CodeMirror
value={input}
onChange={(value): void => {
setInput(value);
onChange?.(value);
}}
className={`query-aggregation-select-editor ${
validationError ? 'error' : ''
}`}
theme={copilot}
extensions={[
chipPlugin,
aggregatorAutocomplete,
transactionFilterExtension,
javascript({ jsx: false, typescript: false }),
EditorView.lineWrapping,
stopEventsExtension,
keymap.of([
...completionKeymap,
{
key: 'Escape',
run: closeCompletion,
},
]),
]}
placeholder={
maxAggregations !== undefined
? `Type aggregator functions (max ${maxAggregations}) like sum(), count_distinct(...), etc.`
: 'Type aggregator functions like sum(), count_distinct(...), etc.'
}
basicSetup={{
lineNumbers: false,
autocompletion: true,
completionKeymap: true,
}}
onUpdate={handleUpdate}
onCreateEditor={(view: EditorView): void => {
editorRef.current = view;
}}
onFocus={(): void => {
setIsFocused(true);
safeStartCompletion();
}}
onBlur={(): void => {
setIsFocused(false);
if (editorRef.current) {
closeCompletion(editorRef.current);
}
}}
/>
{validationError && (
<div className="query-aggregation-error-container">
<Popover
placement="bottomRight"
showArrow={false}
content={
<div className="query-aggregation-error-content">
<div className="query-aggregation-error-message">{validationError}</div>
</div>
}
overlayClassName="query-aggregation-error-popover"
>
<Button
type="text"
icon={<TriangleAlert size={14} color={Color.BG_CHERRY_500} />}
className="periscope-btn ghost query-aggregation-error-btn"
/>
</Popover>
</div>
)}
</div>
);
}
QueryAggregationSelect.defaultProps = {
onChange: undefined,
maxAggregations: undefined,
};
export default QueryAggregationSelect;

View File

@@ -0,0 +1,69 @@
import { Button, Tooltip, Typography } from 'antd';
import { Plus, Sigma } from 'lucide-react';
export default function QueryFooter({
addNewBuilderQuery,
addNewFormula,
}: {
addNewBuilderQuery: () => void;
addNewFormula: () => void;
}): JSX.Element {
return (
<div className="qb-footer">
<div className="qb-footer-container">
<div className="qb-add-new-query">
<Tooltip
title={
<div style={{ textAlign: 'center' }}>
Add New Query
<Typography.Link
href="https://signoz.io/docs/userguide/query-builder/?utm_source=product&utm_medium=query-builder#multiple-queries-and-functions"
target="_blank"
style={{ textDecoration: 'underline' }}
>
{' '}
<br />
Learn more
</Typography.Link>
</div>
}
>
<Button
className="add-new-query-button periscope-btn secondary"
type="text"
icon={<Plus size={16} />}
onClick={addNewBuilderQuery}
/>
</Tooltip>
</div>
<div className="qb-add-formula">
<Tooltip
title={
<div style={{ textAlign: 'center' }}>
Add New Formula
<Typography.Link
href="https://signoz.io/docs/userguide/query-builder/?utm_source=product&utm_medium=query-builder#multiple-queries-and-functions"
target="_blank"
style={{ textDecoration: 'underline' }}
>
{' '}
<br />
Learn more
</Typography.Link>
</div>
}
>
<Button
className="add-formula-button periscope-btn secondary"
icon={<Sigma size={16} />}
onClick={addNewFormula}
>
Add Formula
</Button>
</Tooltip>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,715 @@
.code-mirror-where-clause {
width: 100%;
display: flex;
flex-direction: column;
gap: 8px;
font-family: Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto,
'Helvetica Neue', sans-serif;
.query-where-clause-editor-container {
display: flex;
flex-direction: row;
.query-where-clause-editor {
flex: 1;
min-width: 400px;
}
.query-status-container {
width: 32px;
background-color: #121317 !important;
display: flex;
justify-content: center;
align-items: center;
border: 1px solid var(--bg-slate-200);
border-radius: 2px;
border-top-left-radius: 0px !important;
border-bottom-left-radius: 0px !important;
border-left: none !important;
&.hasErrors {
border-color: var(--bg-cherry-500);
}
}
}
.query-where-clause-editor {
&.hasErrors {
.cm-editor {
.cm-content {
border-color: var(--bg-cherry-500);
border-top-right-radius: 0px !important;
border-bottom-right-radius: 0px !important;
}
}
}
}
.cm-editor {
border-radius: 2px;
overflow: hidden;
background-color: transparent !important;
&:focus-within {
border-color: var(--bg-robin-500);
}
.cm-content {
border-radius: 2px;
border: 1px solid var(--Slate-400, #1d212d);
padding: 0px !important;
background-color: #121317 !important;
&:focus-within {
border-color: var(--bg-ink-200);
}
}
&.cm-focused {
outline: 1px solid var(--bg-slate-200);
}
.cm-tooltip-autocomplete {
background: var(--bg-ink-300) !important;
border-radius: 2px !important;
font-size: 12px !important;
font-weight: 500 !important;
margin-top: -2px !important;
min-width: 400px !important;
position: relative !important;
top: 0px !important;
left: 0px !important;
border-radius: 4px;
border: 0px;
background: linear-gradient(
139deg,
rgba(18, 19, 23, 0.8) 0%,
rgba(18, 19, 23, 0.9) 98.68%
) !important;
backdrop-filter: blur(20px);
box-sizing: border-box;
font-family: 'Space Mono', monospace !important;
ul {
width: 100% !important;
max-width: 100% !important;
font-family: 'Space Mono', monospace !important;
min-height: 200px !important;
&::-webkit-scrollbar {
width: 0.3rem;
}
&::-webkit-scrollbar-corner {
background: transparent;
}
&::-webkit-scrollbar-thumb {
background: rgb(136, 136, 136);
border-radius: 0.625rem;
}
&::-webkit-scrollbar-track {
background: transparent;
}
li {
width: 100% !important;
max-width: 100% !important;
line-height: 36px !important;
height: 36px !important;
padding: 4px 8px !important;
display: flex !important;
align-items: center !important;
gap: 8px !important;
box-sizing: border-box;
overflow: hidden;
font-family: 'Space Mono', monospace !important;
&:hover {
background: var(--bg-ink-100) !important;
}
.cm-completionIcon {
display: none !important;
}
&[aria-selected='true'] {
// background-color: rgba(78, 116, 248, 0.7) !important;
background: rgba(171, 189, 255, 0.04) !important;
}
}
}
}
.cm-gutters {
display: none !important;
}
.cm-line {
line-height: 34px !important;
font-family: 'Space Mono', monospace !important;
background-color: #121317 !important;
::-moz-selection {
background: var(--bg-ink-100) !important;
opacity: 0.5 !important;
}
::selection {
background: var(--bg-ink-100) !important;
opacity: 0.5 !important;
}
}
.cm-selectionBackground {
background: var(--bg-ink-100) !important;
opacity: 0.5 !important;
}
}
.cursor-position {
font-size: 12px;
color: var(--bg-ink-200);
padding: 6px;
background-color: var(--bg-vanilla-200);
border-radius: 4px;
display: inline-flex;
align-items: center;
margin-bottom: 8px;
margin-top: 8px;
}
.query-validation {
display: flex;
flex-direction: column;
gap: 8px;
margin-bottom: 8px;
margin-top: 16px;
.valid,
.invalid {
display: inline-flex;
align-items: center;
padding: 4px 8px;
border-radius: 4px;
font-weight: 500;
font-size: 12px;
}
.valid {
background-color: rgba(39, 174, 96, 0.1);
color: #27ae60;
}
.invalid {
background-color: rgba(235, 87, 87, 0.1);
color: #eb5757;
}
.query-validation-status {
display: flex;
align-items: center;
gap: 8px;
}
.query-validation-errors {
display: flex;
flex-direction: column;
gap: 8px;
.query-validation-error {
display: flex;
flex-direction: row;
gap: 16px;
font-size: 12px;
font-family: 'Space Mono', monospace !important;
color: var(--bg-cherry-500);
}
}
}
.query-context {
padding: 12px;
background-color: var(--bg-ink-400);
border-radius: 4px;
border-left: 3px solid var(--bg-robin-500);
color: var(--bg-ink-300) !important;
.ant-card-head {
color: var(--bg-vanilla-300) !important;
}
.context-details {
display: flex;
flex-wrap: wrap;
gap: 12px;
p {
margin: 0;
font-size: 13px;
strong {
color: var(--bg-vanilla-300);
margin-right: 4px;
}
}
}
}
.code-mirror-card {
.ant-card-body {
padding: 8px;
}
}
.query-text-preview-title {
font-size: 13px;
color: var(--bg-vanilla-100);
background-color: var(--bg-robin-500);
padding: 2px 6px;
border-radius: 2px;
margin-right: 4px;
}
.query-text-preview {
font-family: 'Space Mono', monospace;
font-size: 13px;
color: var(--bg-vanilla-200);
padding: 2px 6px;
font-style: italic;
}
.query-examples-card {
background-color: var(--bg-ink-400);
border: 1px solid var(--bg-slate-200);
.ant-card-body {
padding: 0;
}
.query-examples {
.ant-collapse-header {
padding: 8px 16px !important;
color: var(--bg-vanilla-300) !important;
font-weight: 500;
}
.ant-collapse-content {
background-color: transparent !important;
}
.query-examples-list {
display: flex;
flex-direction: row;
gap: 8px;
flex-wrap: wrap;
}
.query-example-tag {
display: flex;
flex-direction: column;
gap: 4px;
padding: 8px 12px;
background-color: var(--bg-ink-400);
border: 1px solid var(--bg-slate-200);
border-radius: 4px;
cursor: pointer;
transition: all 0.2s ease;
outline: none;
&:hover {
background-color: var(--bg-ink-300);
border-color: var(--bg-robin-500);
}
&:focus-visible {
outline: 2px solid var(--bg-robin-500);
outline-offset: 2px;
}
.query-example-content {
display: flex;
align-items: center;
gap: 8px;
}
.query-example-label {
font-weight: 500;
color: var(--bg-vanilla-300);
font-size: 13px;
}
.query-example-query {
font-family: 'Space Mono', monospace;
font-size: 12px;
color: var(--bg-vanilla-200);
background-color: var(--bg-ink-300);
padding: 2px 6px;
border-radius: 2px;
}
.query-example-description {
font-size: 12px;
color: var(--bg-vanilla-200);
opacity: 0.8;
}
}
.query-example-content {
display: inline-flex;
cursor: pointer;
}
}
}
// Context indicator styles
.context-indicator {
display: flex;
align-items: center;
flex-wrap: wrap;
padding: 8px 12px;
margin-bottom: 8px;
border-radius: 4px;
font-size: 13px;
background-color: #f5f5f5;
border-left: 4px solid #1890ff;
display: none;
.triplet-info {
margin-left: 16px;
display: inline-flex;
align-items: center;
gap: 4px;
}
.query-pair-info {
display: inline-flex;
align-items: center;
gap: 4px;
border-left: 1px solid rgba(0, 0, 0, 0.1);
padding-left: 8px;
background-color: rgba(0, 0, 0, 0.03);
padding: 4px 8px;
border-radius: 4px;
}
// Color variations based on context
&.context-indicator-key {
border-left-color: #1890ff; // blue
background-color: rgba(24, 144, 255, 0.1);
}
&.context-indicator-operator {
border-left-color: #722ed1; // purple
background-color: rgba(114, 46, 209, 0.1);
}
&.context-indicator-value {
border-left-color: #52c41a; // green
background-color: rgba(82, 196, 26, 0.1);
}
&.context-indicator-conjunction {
border-left-color: #fa8c16; // orange
background-color: rgba(250, 140, 22, 0.1);
}
&.context-indicator-function {
border-left-color: #13c2c2; // cyan
background-color: rgba(19, 194, 194, 0.1);
}
&.context-indicator-parenthesis {
border-left-color: #eb2f96; // magenta
background-color: rgba(235, 47, 150, 0.1);
}
}
}
.query-status-popover {
.ant-popover-arrow {
display: none !important;
}
.ant-popover-content {
background: linear-gradient(
139deg,
rgba(18, 19, 23, 0.8) 0%,
rgba(18, 19, 23, 0.9) 98.68%
);
box-shadow: 4px 10px 16px 2px rgba(0, 0, 0, 0.2);
backdrop-filter: blur(20px);
margin-top: -6px !important;
}
}
// /* Dark mode support */
// :global(.darkMode) {
// .code-mirror-where-clause {
// .cm-editor {
// border-color: var(--bg-slate-500);
// background-color: var(--bg-ink-400);
// }
// .cursor-position {
// background-color: var(--bg-ink-400);
// color: var(--bg-vanilla-100);
// }
// .query-context {
// background-color: var(--bg-ink-400);
// color: var(--bg-vanilla-100);
// h3 {
// color: var(--bg-vanilla-100);
// }
// .context-details {
// p {
// strong {
// color: var(--bg-vanilla-200);
// }
// }
// }
// }
// .query-examples-card {
// background-color: var(--bg-ink-400);
// border-color: var(--bg-slate-500);
// .ant-collapse-header {
// color: var(--bg-vanilla-100) !important;
// }
// .query-example-tag {
// background-color: var(--bg-ink-400);
// border-color: var(--bg-slate-500);
// &:hover {
// background-color: var(--bg-ink-300);
// border-color: var(--bg-robin-500);
// }
// .query-example-label {
// color: var(--bg-vanilla-100);
// }
// .query-example-query {
// color: var(--bg-vanilla-100);
// background-color: var(--bg-ink-300);
// }
// .query-example-description {
// color: var(--bg-vanilla-100);
// }
// }
// }
// .context-indicator {
// background-color: var(--bg-ink-300);
// color: var(--bg-vanilla-100);
// .query-pair-info {
// border-left: 1px solid rgba(255, 255, 255, 0.1);
// background-color: rgba(255, 255, 255, 0.05);
// }
// }
// }
// }
.lightMode {
.code-mirror-where-clause {
.query-where-clause-editor-container {
.query-status-container {
background-color: var(--bg-vanilla-100) !important;
border: 1px solid var(--bg-vanilla-300);
&.hasErrors {
border-color: var(--bg-cherry-500);
}
}
}
.query-where-clause-editor {
&.hasErrors {
.cm-editor {
.cm-content {
border-color: var(--bg-cherry-500);
border-top-right-radius: 0px !important;
border-bottom-right-radius: 0px !important;
}
}
}
}
.cm-editor {
&:focus-within {
border-color: var(--bg-robin-500);
}
&.cm-focused {
outline: 1px solid var(--bg-vanilla-300);
}
.cm-content {
border-radius: 2px;
border: 1px solid var(--bg-vanilla-300);
padding: 0px !important;
background-color: var(--bg-vanilla-100) !important;
&:focus-within {
border-color: var(--bg-vanilla-200);
}
}
.cm-tooltip-autocomplete {
background: var(--bg-vanilla-100) !important;
border: 0px;
backdrop-filter: blur(20px);
ul {
li {
background-color: var(--bg-vanilla-100) !important;
color: var(--bg-ink-300) !important;
&:hover,
&[aria-selected='true'] {
background-color: var(--bg-vanilla-300) !important;
font-weight: 600;
}
}
}
}
.cm-line {
background-color: var(--bg-vanilla-100) !important;
::-moz-selection {
background: var(--bg-vanilla-100) !important;
}
::selection {
background: var(--bg-vanilla-100) !important;
}
}
.cm-selectionBackground {
background: var(--bg-vanilla-100) !important;
}
}
.cursor-position {
color: var(--bg-vanilla-200);
background-color: var(--bg-vanilla-100);
}
.query-context {
background-color: var(--bg-vanilla-100);
border-left: 3px solid var(--bg-vanilla-300);
color: var(--bg-vanilla-300) !important;
.ant-card-head {
color: var(--bg-ink-300) !important;
}
.context-details {
p {
strong {
color: var(--bg-ink-300);
}
}
}
}
.query-examples-card {
background-color: var(--bg-vanilla-100);
border: 1px solid var(--bg-vanilla-300);
.query-examples {
.ant-collapse-header {
color: var(--bg-ink-300) !important;
}
.query-example-tag {
background-color: var(--bg-vanilla-100);
border: 1px solid var(--bg-vanilla-300);
&:hover {
background-color: var(--bg-vanilla-200);
border-color: var(--bg-vanilla-300);
}
.query-example-label {
color: var(--bg-ink-300);
}
.query-example-query {
color: var(--bg-ink-300);
background-color: var(--bg-vanilla-100);
}
.query-example-description {
color: var(--bg-ink-300);
}
}
}
}
.context-indicator {
background-color: var(--bg-vanilla-100);
border-left: 4px solid var(--bg-vanilla-300);
display: none;
.query-pair-info {
border-left: 1px solid rgba(255, 255, 255, 0.1);
background-color: rgba(255, 255, 255, 0.03);
}
// Color variations based on context
&.context-indicator-key {
border-left-color: #1890ff; // blue
background-color: rgba(24, 144, 255, 0.1);
}
&.context-indicator-operator {
border-left-color: #722ed1; // purple
background-color: rgba(114, 46, 209, 0.1);
}
&.context-indicator-value {
border-left-color: #52c41a; // green
background-color: rgba(82, 196, 26, 0.1);
}
&.context-indicator-conjunction {
border-left-color: #fa8c16; // orange
background-color: rgba(250, 140, 22, 0.1);
}
&.context-indicator-function {
border-left-color: #13c2c2; // cyan
background-color: rgba(19, 194, 194, 0.1);
}
&.context-indicator-parenthesis {
border-left-color: #eb2f96; // magenta
background-color: rgba(235, 47, 150, 0.1);
}
}
}
.query-status-popover {
.ant-popover-content {
background: var(--bg-vanilla-100);
box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.1);
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,79 @@
export const queryExamples = [
{
label: 'Basic Query',
query: "status = 'error'",
description: 'Find all errors',
},
{
label: 'Multiple Conditions',
query: "status = 'error' AND service = 'frontend'",
description: 'Find errors from frontend service',
},
{
label: 'IN Operator',
query: "status IN ['error', 'warning']",
description: 'Find items with specific statuses',
},
{
label: 'Function Usage',
query: "HAS(service, 'frontend')",
description: 'Use HAS function',
},
{
label: 'Numeric Comparison',
query: 'duration > 1000',
description: 'Find items with duration greater than 1000ms',
},
{
label: 'Range Query',
query: 'duration BETWEEN 100 AND 1000',
description: 'Find items with duration between 100ms and 1000ms',
},
{
label: 'Pattern Matching',
query: "service LIKE 'front%'",
description: 'Find services starting with "front"',
},
{
label: 'Complex Conditions',
query: "(status = 'error' OR status = 'warning') AND service = 'frontend'",
description: 'Find errors or warnings from frontend service',
},
{
label: 'Multiple Functions',
query: "HAS(service, 'frontend') AND HAS(status, 'error')",
description: 'Use multiple HAS functions',
},
{
label: 'NOT Operator',
query: "NOT status = 'success'",
description: 'Find items that are not successful',
},
{
label: 'Array Contains',
query: "tags CONTAINS 'production'",
description: 'Find items with production tag',
},
{
label: 'Regex Pattern',
query: "service REGEXP '^prod-.*'",
description: 'Find services matching regex pattern',
},
{
label: 'Null Check',
query: 'error IS NULL',
description: 'Find items without errors',
},
{
label: 'Multiple Attributes',
query:
"service = 'frontend' AND environment = 'production' AND status = 'error'",
description: 'Find production frontend errors',
},
{
label: 'Nested Conditions',
query:
"(service = 'frontend' OR service = 'backend') AND (status = 'error' OR status = 'warning')",
description: 'Find errors or warnings from frontend or backend',
},
];

View File

@@ -0,0 +1,239 @@
import { Dropdown } from 'antd';
import cx from 'classnames';
import { ENTITY_VERSION_V4, ENTITY_VERSION_V5 } from 'constants/app';
import { PANEL_TYPES } from 'constants/queryBuilder';
import QBEntityOptions from 'container/QueryBuilder/components/QBEntityOptions/QBEntityOptions';
import { QueryProps } from 'container/QueryBuilder/components/Query/Query.interfaces';
import SpanScopeSelector from 'container/QueryBuilder/filters/QueryBuilderSearchV2/SpanScopeSelector';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
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';
import { MetricsSelect } from './MetricsSelect/MetricsSelect';
import QueryAddOns from './QueryAddOns/QueryAddOns';
import QueryAggregation from './QueryAggregation/QueryAggregation';
import QuerySearch from './QuerySearch/QuerySearch';
export const QueryV2 = memo(function QueryV2({
ref,
index,
queryVariant,
query,
filterConfigs,
isListViewPanel = false,
version,
showOnlyWhereClause = false,
}: QueryProps & { ref: React.RefObject<HTMLDivElement> }): JSX.Element {
const { cloneQuery, panelType } = useQueryBuilder();
const showFunctions = query?.functions?.length > 0;
const { dataSource } = query;
const [isCollapsed, setIsCollapsed] = useState(false);
const {
handleChangeQueryData,
handleDeleteQuery,
handleQueryFunctionsUpdates,
handleChangeDataSource,
} = useQueryOperations({
index,
query,
filterConfigs,
isListViewPanel,
entityVersion: version,
});
const handleToggleDisableQuery = useCallback(() => {
handleChangeQueryData('disabled', !query.disabled);
}, [handleChangeQueryData, query]);
const handleToggleCollapsQuery = (): void => {
setIsCollapsed(!isCollapsed);
};
const handleCloneEntity = (): void => {
cloneQuery('query', query);
};
const showReduceTo = useMemo(
() =>
dataSource === DataSource.METRICS &&
(panelType === PANEL_TYPES.TABLE ||
panelType === PANEL_TYPES.PIE ||
panelType === PANEL_TYPES.VALUE),
[dataSource, panelType],
);
const showSpanScopeSelector = useMemo(() => dataSource === DataSource.TRACES, [
dataSource,
]);
const handleChangeAggregateEvery = useCallback(
(value: IBuilderQuery['stepInterval']) => {
handleChangeQueryData('stepInterval', value);
},
[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 })}
ref={ref}
>
<div className="qb-content-section">
{!showOnlyWhereClause && (
<div className="qb-header-container">
<div className="query-actions-container">
<div className="query-actions-left-container">
<QBEntityOptions
isMetricsDataSource={dataSource === DataSource.METRICS}
showFunctions={
(version && version === ENTITY_VERSION_V4) ||
query.dataSource === DataSource.LOGS ||
query.dataSource === DataSource.METRICS ||
showFunctions ||
false
}
isCollapsed={isCollapsed}
entityType="query"
entityData={query}
onToggleVisibility={handleToggleDisableQuery}
onDelete={handleDeleteQuery}
onCloneQuery={cloneQuery}
onCollapseEntity={handleToggleCollapsQuery}
query={query}
onQueryFunctionsUpdates={handleQueryFunctionsUpdates}
showDeleteButton={false}
showCloneOption={false}
isListViewPanel={isListViewPanel}
index={index}
queryVariant={queryVariant}
onChangeDataSource={handleChangeDataSource}
/>
</div>
{!isListViewPanel && (
<Dropdown
className="query-actions-dropdown"
menu={{
items: [
{
label: 'Clone',
key: 'clone-query',
icon: <Copy size={14} />,
onClick: handleCloneEntity,
},
{
label: 'Delete',
key: 'delete-query',
icon: <Trash size={14} />,
onClick: handleDeleteQuery,
},
],
}}
placement="bottomRight"
>
<Ellipsis size={16} />
</Dropdown>
)}
</div>
</div>
)}
{!isCollapsed && (
<div className="qb-elements-container">
<div className="qb-search-container">
{dataSource === DataSource.METRICS && (
<div className="metrics-select-container">
<MetricsSelect
query={query}
index={index}
version={ENTITY_VERSION_V5}
/>
</div>
)}
<div className="qb-search-filter-container">
<div className="query-search-container">
<QuerySearch
key={`query-search-${query.queryName}-${query.dataSource}`}
onChange={handleSearchChange}
queryData={query}
dataSource={dataSource}
/>
</div>
{showSpanScopeSelector && (
<div className="traces-search-filter-container">
<div className="traces-search-filter-in">in</div>
<SpanScopeSelector query={query} />
</div>
)}
</div>
</div>
{!showOnlyWhereClause &&
!isListViewPanel &&
dataSource !== DataSource.METRICS && (
<QueryAggregation
dataSource={dataSource}
key={`query-search-${query.queryName}-${query.dataSource}`}
panelType={panelType || undefined}
onAggregationIntervalChange={handleChangeAggregateEvery}
onChange={handleChangeAggregation}
queryData={query}
/>
)}
{!showOnlyWhereClause && dataSource === DataSource.METRICS && (
<MetricsAggregateSection
panelType={panelType}
query={query}
index={index}
key={`metrics-aggregate-section-${query.queryName}-${query.dataSource}`}
version="v4"
/>
)}
{!showOnlyWhereClause && (
<QueryAddOns
index={index}
query={query}
version="v3"
isListViewPanel={isListViewPanel}
showReduceTo={showReduceTo}
panelType={panelType}
/>
)}
</div>
)}
</div>
</div>
);
});

View File

@@ -0,0 +1,670 @@
/* eslint-disable sonarjs/cognitive-complexity */
import { createAggregation } from 'api/v5/queryRange/prepareQueryRangePayloadV5';
import { OPERATORS } from 'constants/antlrQueryConstants';
import { getOperatorValue } from 'container/QueryBuilder/filters/QueryBuilderSearch/utils';
import { cloneDeep } from 'lodash-es';
import { IQueryPair } from 'types/antlrQueryTypes';
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
import {
Having,
IBuilderQuery,
Query,
TagFilter,
TagFilterItem,
} from 'types/api/queryBuilder/queryBuilderData';
import {
LogAggregation,
MetricAggregation,
TraceAggregation,
} from 'types/api/v5/queryRange';
import { EQueryType } from 'types/common/dashboard';
import { DataSource } from 'types/common/queryBuilder';
import { extractQueryPairs } from 'utils/queryContextUtils';
import { unquote } from 'utils/stringUtils';
import { v4 as uuid } from 'uuid';
/**
* Check if an operator requires array values (like IN, NOT IN)
* @param operator - The operator to check
* @returns True if the operator requires array values
*/
const isArrayOperator = (operator: string): boolean => {
const arrayOperators = ['in', 'not in', 'IN', 'NOT IN'];
return arrayOperators.includes(operator);
};
/**
* Format a value for the expression string
* @param value - The value to format
* @param operator - The operator being used (to determine if array is needed)
* @returns Formatted value string
*/
const formatValueForExpression = (
value: string[] | string | number | boolean,
operator?: string,
): string => {
// For IN operators, ensure value is always an array
if (isArrayOperator(operator || '')) {
const arrayValue = Array.isArray(value) ? value : [value];
return `[${arrayValue
.map((v) =>
typeof v === 'string' ? `'${v.replace(/'/g, "\\'")}'` : String(v),
)
.join(', ')}]`;
}
if (Array.isArray(value)) {
// Handle array values (e.g., for IN operations)
return `[${value
.map((v) =>
typeof v === 'string' ? `'${v.replace(/'/g, "\\'")}'` : String(v),
)
.join(', ')}]`;
}
if (typeof value === 'string') {
// Add single quotes around all string values and escape internal single quotes
return `'${value.replace(/'/g, "\\'")}'`;
}
return String(value);
};
export const convertFiltersToExpression = (
filters: TagFilter,
): { expression: string } => {
if (!filters?.items || filters.items.length === 0) {
return { expression: '' };
}
const expressions = filters.items
.map((filter) => {
const { key, op, value } = filter;
// Skip if key is not defined
if (!key?.key) {
return '';
}
const formattedValue = formatValueForExpression(value, op);
return `${key.key} ${op} ${formattedValue}`;
})
.filter((expression) => expression !== ''); // Remove empty expressions
return {
expression: expressions.join(' AND '),
};
};
const formatValuesForFilter = (value: string | string[]): string | string[] => {
if (Array.isArray(value)) {
return value.map((v) => (typeof v === 'string' ? unquote(v) : String(v)));
}
if (typeof value === 'string') {
return unquote(value);
}
return String(value);
};
export const convertExpressionToFilters = (
expression: string,
): TagFilterItem[] => {
if (!expression) return [];
const queryPairs = extractQueryPairs(expression);
const filters: TagFilterItem[] = [];
queryPairs.forEach((pair) => {
const operator = pair.hasNegation
? getOperatorValue(`NOT_${pair.operator}`.toUpperCase())
: getOperatorValue(pair.operator.toUpperCase());
filters.push({
id: uuid(),
op: operator,
key: {
id: pair.key,
key: pair.key,
type: '',
},
value: pair.isMultiValue
? formatValuesForFilter(pair.valueList as string[]) ?? []
: formatValuesForFilter(pair.value as string) ?? '',
});
});
return filters;
};
export const convertFiltersToExpressionWithExistingQuery = (
filters: TagFilter,
existingQuery: string | undefined,
): { filters: TagFilter; filter: { expression: string } } => {
if (!existingQuery) {
// If no existing query, return filters with a newly generated expression
return {
filters,
filter: convertFiltersToExpression(filters),
};
}
// Extract query pairs from the existing query
const queryPairs = extractQueryPairs(existingQuery.trim());
let queryPairsMap: Map<string, IQueryPair> = new Map();
const updatedFilters = cloneDeep(filters); // Clone filters to avoid direct mutation
const nonExistingFilters: TagFilterItem[] = [];
let modifiedQuery = existingQuery; // We'll modify this query as we proceed
const visitedPairs: Set<string> = new Set(); // Set to track visited query pairs
// Map extracted query pairs to key-specific pair information for faster access
if (queryPairs.length > 0) {
queryPairsMap = new Map(
queryPairs.map((pair) => {
const key = pair.hasNegation
? `${pair.key}-not ${pair.operator}`.trim().toLowerCase()
: `${pair.key}-${pair.operator}`.trim().toLowerCase();
return [key, pair];
}),
);
}
filters?.items?.forEach((filter) => {
const { key, op, value } = filter;
// Skip invalid filters with no key
if (!key) return;
let shouldAddToNonExisting = true; // Flag to decide if the filter should be added to non-existing filters
const sanitizedOperator = op.trim().toUpperCase();
// Check if the operator is IN or NOT IN
if (
[OPERATORS.IN, `${OPERATORS.NOT} ${OPERATORS.IN}`].includes(
sanitizedOperator,
)
) {
const existingPair = queryPairsMap.get(
`${key.key}-${op}`.trim().toLowerCase(),
);
const formattedValue = formatValueForExpression(value, op);
// If a matching query pair exists, modify the query
if (
existingPair &&
existingPair.position?.valueStart &&
existingPair.position?.valueEnd
) {
visitedPairs.add(`${key.key}-${op}`.trim().toLowerCase());
modifiedQuery =
modifiedQuery.slice(0, existingPair.position.valueStart) +
formattedValue +
modifiedQuery.slice(existingPair.position.valueEnd + 1);
return;
}
// Handle the different cases for IN operator
switch (sanitizedOperator) {
case OPERATORS.IN:
// If there's a NOT IN or equal operator, merge the filter
if (
queryPairsMap.has(
`${key.key}-${OPERATORS.NOT} ${op}`.trim().toLowerCase(),
)
) {
const notInPair = queryPairsMap.get(
`${key.key}-${OPERATORS.NOT} ${op}`.trim().toLowerCase(),
);
visitedPairs.add(
`${key.key}-${OPERATORS.NOT} ${op}`.trim().toLowerCase(),
);
if (notInPair?.position?.valueEnd) {
modifiedQuery = `${modifiedQuery.slice(
0,
notInPair.position.negationStart,
)}${OPERATORS.IN} ${formattedValue} ${modifiedQuery.slice(
notInPair.position.valueEnd + 1,
)}`;
}
shouldAddToNonExisting = false; // Don't add this to non-existing filters
} else if (
queryPairsMap.has(`${key.key}-${OPERATORS['=']}`.trim().toLowerCase())
) {
const equalsPair = queryPairsMap.get(
`${key.key}-${OPERATORS['=']}`.trim().toLowerCase(),
);
visitedPairs.add(`${key.key}-${OPERATORS['=']}`.trim().toLowerCase());
if (equalsPair?.position?.valueEnd) {
modifiedQuery = `${modifiedQuery.slice(
0,
equalsPair.position.operatorStart,
)}${OPERATORS.IN} ${formattedValue} ${modifiedQuery.slice(
equalsPair.position.valueEnd + 1,
)}`;
}
shouldAddToNonExisting = false; // Don't add this to non-existing filters
} else if (
queryPairsMap.has(`${key.key}-${OPERATORS['!=']}`.trim().toLowerCase())
) {
const notEqualsPair = queryPairsMap.get(
`${key.key}-${OPERATORS['!=']}`.trim().toLowerCase(),
);
visitedPairs.add(`${key.key}-${OPERATORS['!=']}`.trim().toLowerCase());
if (notEqualsPair?.position?.valueEnd) {
modifiedQuery = `${modifiedQuery.slice(
0,
notEqualsPair.position.operatorStart,
)}${OPERATORS.IN} ${formattedValue} ${modifiedQuery.slice(
notEqualsPair.position.valueEnd + 1,
)}`;
}
shouldAddToNonExisting = false; // Don't add this to non-existing filters
}
break;
case `${OPERATORS.NOT} ${OPERATORS.IN}`:
if (
queryPairsMap.has(`${key.key}-${OPERATORS['!=']}`.trim().toLowerCase())
) {
const notEqualsPair = queryPairsMap.get(
`${key.key}-${OPERATORS['!=']}`.trim().toLowerCase(),
);
visitedPairs.add(`${key.key}-${OPERATORS['!=']}`.trim().toLowerCase());
if (notEqualsPair?.position?.valueEnd) {
modifiedQuery = `${modifiedQuery.slice(
0,
notEqualsPair.position.operatorStart,
)}${OPERATORS.NOT} ${
OPERATORS.IN
} ${formattedValue} ${modifiedQuery.slice(
notEqualsPair.position.valueEnd + 1,
)}`;
}
shouldAddToNonExisting = false; // Don't add this to non-existing filters
}
break; // No operation needed for NOT IN case
default:
break;
}
}
if (
queryPairsMap.has(`${filter.key?.key}-${filter.op}`.trim().toLowerCase())
) {
visitedPairs.add(`${filter.key?.key}-${filter.op}`.trim().toLowerCase());
}
// Add filters that don't have an existing pair to non-existing filters
if (
shouldAddToNonExisting &&
!queryPairsMap.has(`${filter.key?.key}-${filter.op}`.trim().toLowerCase())
) {
nonExistingFilters.push(filter);
}
});
// Create new filters from non-visited query pairs
const newFilterItems: TagFilterItem[] = [];
queryPairsMap.forEach((pair, key) => {
if (!visitedPairs.has(key)) {
const operator = pair.hasNegation
? getOperatorValue(`NOT_${pair.operator}`.toUpperCase())
: getOperatorValue(pair.operator.toUpperCase());
newFilterItems.push({
id: uuid(),
op: operator,
key: {
id: pair.key,
key: pair.key,
type: '',
},
value: pair.isMultiValue
? formatValuesForFilter(pair.valueList as string[]) ?? ''
: formatValuesForFilter(pair.value as string) ?? '',
});
}
});
// Merge new filter items with existing ones
if (newFilterItems.length > 0 && updatedFilters?.items) {
updatedFilters.items = [...updatedFilters.items, ...newFilterItems];
}
// If no non-existing filters, return the modified query directly
if (nonExistingFilters.length === 0) {
return {
filters: updatedFilters,
filter: { expression: modifiedQuery },
};
}
// Convert non-existing filters to an expression and append to the modified query
const nonExistingFilterExpression = convertFiltersToExpression({
items: nonExistingFilters,
op: filters.op || 'AND',
});
if (nonExistingFilterExpression.expression) {
return {
filters: updatedFilters,
filter: {
expression: `${modifiedQuery.trim()} ${
nonExistingFilterExpression.expression
}`,
},
};
}
// Return the final result with the modified query
return {
filters: updatedFilters,
filter: { expression: modifiedQuery || '' },
};
};
/**
* Removes specified key-value pairs from a logical query expression string.
*
* This function parses the given query expression and removes any query pairs
* whose keys match those in the `keysToRemove` array. It also removes any trailing
* logical conjunctions (e.g., `AND`, `OR`) and whitespace that follow the matched pairs,
* ensuring that the resulting expression remains valid and clean.
*
* @param expression - The full query string.
* @param keysToRemove - An array of keys (case-insensitive) that should be removed from the expression.
* @returns A new expression string with the specified keys and their associated clauses removed.
*/
export const removeKeysFromExpression = (
expression: string,
keysToRemove: string[],
): string => {
if (!keysToRemove || keysToRemove.length === 0) {
return expression;
}
let updatedExpression = expression;
if (updatedExpression) {
keysToRemove.forEach((key) => {
// Extract key-value query pairs from the expression
const existingQueryPairs = extractQueryPairs(updatedExpression);
let queryPairsMap: Map<string, IQueryPair>;
if (existingQueryPairs.length > 0) {
// Build a map for quick lookup of query pairs by their lowercase trimmed keys
queryPairsMap = new Map(
existingQueryPairs.map((pair) => {
const key = pair.key.trim().toLowerCase();
return [key, pair];
}),
);
// Lookup the current query pair using the attribute key (case-insensitive)
const currentQueryPair = queryPairsMap.get(`${key}`.trim().toLowerCase());
if (currentQueryPair && currentQueryPair.isComplete) {
// Determine the start index of the query pair (fallback order: key → operator → value)
const queryPairStart =
currentQueryPair.position.keyStart ??
currentQueryPair.position.operatorStart ??
currentQueryPair.position.valueStart;
// Determine the end index of the query pair (fallback order: value → operator → key)
let queryPairEnd =
currentQueryPair.position.valueEnd ??
currentQueryPair.position.operatorEnd ??
currentQueryPair.position.keyEnd;
// Get the part of the expression that comes after the current query pair
const expressionAfterPair = `${updatedExpression.slice(queryPairEnd + 1)}`;
// Match optional spaces and an optional conjunction (AND/OR), case-insensitive
const conjunctionOrSpacesRegex = /^(\s*((AND|OR)\s+)?)/i;
const match = expressionAfterPair.match(conjunctionOrSpacesRegex);
if (match && match.length > 0) {
// If match is found, extend the queryPairEnd to include the matched part
queryPairEnd += match[0].length;
}
// Remove the full query pair (including any conjunction/whitespace) from the expression
updatedExpression = `${updatedExpression.slice(
0,
queryPairStart,
)}${updatedExpression.slice(queryPairEnd + 1)}`.trim();
}
}
});
}
return updatedExpression;
};
/**
* Convert old having format to new having format
* @param having - Array of old having objects with columnName, op, and value
* @returns New having format with expression string
*/
export const convertHavingToExpression = (
having: Having[],
): { expression: string } => {
if (!having || having.length === 0) {
return { expression: '' };
}
const expressions = having
.map((havingItem) => {
const { columnName, op, value } = havingItem;
// Skip if columnName is not defined
if (!columnName) {
return '';
}
// Format value based on its type
let formattedValue: string;
if (Array.isArray(value)) {
// For array values, format as [val1, val2, ...]
formattedValue = `[${value.join(', ')}]`;
} else {
// For single values, just convert to string
formattedValue = String(value);
}
return `${columnName} ${op} ${formattedValue}`;
})
.filter((expression) => expression !== ''); // Remove empty expressions
return {
expression: expressions.join(' AND '),
};
};
/**
* Convert old aggregation format to new aggregation format
* @param aggregateOperator - The aggregate operator (e.g., 'sum', 'count', 'avg')
* @param aggregateAttribute - The attribute to aggregate
* @param dataSource - The data source type
* @param timeAggregation - Time aggregation for metrics (optional)
* @param spaceAggregation - Space aggregation for metrics (optional)
* @param alias - Optional alias for the aggregation
* @returns New aggregation format based on data source
*
*/
export const convertAggregationToExpression = (
aggregateOperator: string,
aggregateAttribute: BaseAutocompleteData,
dataSource: DataSource,
timeAggregation?: string,
spaceAggregation?: string,
alias?: string,
): (TraceAggregation | LogAggregation | MetricAggregation)[] | undefined => {
// Skip if no operator or attribute key
if (!aggregateOperator) {
return undefined;
}
// Replace noop with count as default
const normalizedOperator =
aggregateOperator === 'noop' ? 'count' : aggregateOperator;
const normalizedTimeAggregation =
timeAggregation === 'noop' ? 'count' : timeAggregation;
const normalizedSpaceAggregation =
spaceAggregation === 'noop' ? 'count' : spaceAggregation;
// For metrics, use the MetricAggregation format
if (dataSource === DataSource.METRICS) {
return [
{
metricName: aggregateAttribute.key,
timeAggregation: (normalizedTimeAggregation || normalizedOperator) as any,
spaceAggregation: (normalizedSpaceAggregation || normalizedOperator) as any,
} as MetricAggregation,
];
}
// For traces and logs, use expression format
const expression = `${normalizedOperator}(${aggregateAttribute.key})`;
if (dataSource === DataSource.TRACES) {
return [
{
expression,
...(alias && { alias }),
} as TraceAggregation,
];
}
// For logs
return [
{
expression,
...(alias && { alias }),
} as LogAggregation,
];
};
export const getQueryTitles = (currentQuery: Query): string[] => {
if (currentQuery.queryType === EQueryType.QUERY_BUILDER) {
const queryTitles: string[] = [];
// Handle builder queries with multiple aggregations
currentQuery.builder.queryData.forEach((q) => {
const aggregationCount = q.aggregations?.length || 1;
if (aggregationCount > 1) {
// If multiple aggregations, create titles like A.0, A.1, A.2
for (let i = 0; i < aggregationCount; i++) {
queryTitles.push(`${q.queryName}.${i}`);
}
} else {
// Single aggregation, just use query name
queryTitles.push(q.queryName);
}
});
// Handle formulas (they don't have aggregations, so just use query name)
const formulas = currentQuery.builder.queryFormulas.map((q) => q.queryName);
return [...queryTitles, ...formulas];
}
if (currentQuery.queryType === EQueryType.CLICKHOUSE) {
return currentQuery.clickhouse_sql.map((q) => q.name);
}
return currentQuery.promql.map((q) => q.name);
};
function getColId(
queryName: string,
aggregation: { alias?: string; expression?: string },
): string {
return `${queryName}.${aggregation.expression}`;
}
// function to give you label value for query name taking multiaggregation into account
export function getQueryLabelWithAggregation(
queryData: IBuilderQuery[],
): { label: string; value: string }[] {
const labels: { label: string; value: string }[] = [];
const aggregationPerQuery =
queryData.reduce((acc, query) => {
if (query.queryName && query.aggregations?.length) {
acc[query.queryName] = createAggregation(query).map((a: any) => ({
alias: a.alias,
expression: a.expression,
}));
}
return acc;
}, {} as Record<string, any>) || {};
Object.entries(aggregationPerQuery).forEach(([queryName, aggregations]) => {
const isMultipleAggregations = aggregations.length > 1;
aggregations.forEach((agg: any, index: number) => {
const columnId = getColId(queryName, agg);
// For display purposes, show the aggregation index for multiple aggregations
const displayLabel = isMultipleAggregations
? `${queryName}.${index}`
: queryName;
labels.push({
label: displayLabel,
value: columnId, // This matches the ID format used in table columns
});
});
});
return labels;
}
export const adjustQueryForV5 = (currentQuery: Query): Query => {
if (currentQuery.queryType === EQueryType.QUERY_BUILDER) {
const newQueryData = currentQuery.builder.queryData.map((query) => {
const aggregations = query.aggregations?.map((aggregation) => {
if (query.dataSource === DataSource.METRICS) {
const metricAggregation = aggregation as MetricAggregation;
return {
...aggregation,
metricName:
metricAggregation.metricName || query.aggregateAttribute?.key || '',
timeAggregation:
metricAggregation.timeAggregation || query.timeAggregation || '',
spaceAggregation:
metricAggregation.spaceAggregation || query.spaceAggregation || '',
reduceTo: metricAggregation.reduceTo || query.reduceTo || 'avg',
};
}
return aggregation;
});
const {
aggregateAttribute,
aggregateOperator,
timeAggregation,
spaceAggregation,
reduceTo,
filters,
...retainedQuery
} = query;
const newAggregations =
query.dataSource === DataSource.METRICS
? (aggregations as MetricAggregation[])
: (aggregations as (TraceAggregation | LogAggregation)[]);
return {
...retainedQuery,
aggregations: newAggregations,
};
});
return {
...currentQuery,
builder: {
...currentQuery.builder,
queryData: newQueryData,
},
};
}
return currentQuery;
};

View File

@@ -6,14 +6,13 @@ import './Checkbox.styles.scss';
import { Button, Checkbox, Input, Skeleton, Typography } from 'antd';
import cx from 'classnames';
import { removeKeysFromExpression } from 'components/QueryBuilderV2/utils';
import {
IQuickFiltersConfig,
QuickFiltersSource,
} from 'components/QuickFilters/types';
import {
DATA_TYPE_VS_ATTRIBUTE_VALUES_KEY,
OPERATORS,
} from 'constants/queryBuilder';
import { OPERATORS } from 'constants/antlrQueryConstants';
import { DATA_TYPE_VS_ATTRIBUTE_VALUES_KEY } from 'constants/queryBuilder';
import { DEBOUNCE_DELAY } from 'constants/queryBuilderFilterConfig';
import { getOperatorValue } from 'container/QueryBuilder/filters/QueryBuilderSearch/utils';
import { useGetAggregateValues } from 'hooks/queryBuilder/useGetAggregateValues';
@@ -30,7 +29,7 @@ import { v4 as uuid } from 'uuid';
import LogsQuickFilterEmptyState from './LogsQuickFilterEmptyState';
const SELECTED_OPERATORS = [OPERATORS['='], 'in'];
const NON_SELECTED_OPERATORS = [OPERATORS['!='], 'nin'];
const NON_SELECTED_OPERATORS = [OPERATORS['!='], 'not in'];
const SOURCES_WITH_EMPTY_STATE_ENABLED = [QuickFiltersSource.LOGS_EXPLORER];
@@ -168,14 +167,20 @@ export default function CheckboxFilter(props: ICheckboxProps): JSX.Element {
...currentQuery.builder,
queryData: currentQuery.builder.queryData.map((item, idx) => ({
...item,
filter: {
expression: removeKeysFromExpression(item.filter?.expression ?? '', [
filter.attributeKey.key,
]),
},
filters: {
...item.filters,
items:
idx === lastUsedQuery
? item.filters.items.filter(
? item.filters?.items?.filter(
(fil) => !isEqual(fil.key?.key, filter.attributeKey.key),
)
: [...item.filters.items],
) || []
: [...(item.filters?.items || [])],
op: item.filters?.op || 'AND',
},
})),
},
@@ -213,6 +218,14 @@ export default function CheckboxFilter(props: ICheckboxProps): JSX.Element {
query.filters.items = query.filters.items.filter(
(q) => !isEqual(q.key?.key, filter.attributeKey.key),
);
if (query.filter?.expression) {
query.filter.expression = removeKeysFromExpression(
query.filter.expression,
[filter.attributeKey.key],
);
}
if (isOnlyOrAll === 'Only') {
const newFilterItem: TagFilterItem = {
id: uuid(),
@@ -293,7 +306,7 @@ export default function CheckboxFilter(props: ICheckboxProps): JSX.Element {
}
}
break;
case 'nin':
case 'not in':
// if the current running operator is NIN then when unchecking the value it gets
// added to the clause like key NIN [value1 , currentUnselectedValue]
if (!checked) {
@@ -372,7 +385,7 @@ export default function CheckboxFilter(props: ICheckboxProps): JSX.Element {
if (!checked) {
const newFilter = {
...currentFilter,
op: getOperatorValue(OPERATORS.NIN),
op: getOperatorValue('NOT_IN'),
value: [currentFilter.value as string, value],
};
query.filters.items = query.filters.items.map((item) => {
@@ -395,7 +408,7 @@ export default function CheckboxFilter(props: ICheckboxProps): JSX.Element {
// case - when there is no filter for the current key that means all are selected right now.
const newFilterItem: TagFilterItem = {
id: uuid(),
op: getOperatorValue(OPERATORS.NIN),
op: getOperatorValue('NOT_IN'),
key: filter.attributeKey,
value,
};

View File

@@ -178,10 +178,12 @@ function Duration({
...data,
filters: {
...data.filters,
items: data.filters?.items?.map((item) => ({
...item,
id: '',
})),
items:
data.filters?.items?.map((item) => ({
...item,
id: '',
})) || [],
op: data.filters?.op || 'AND',
},
}));
return clonedQuery;
@@ -199,11 +201,12 @@ function Duration({
...item.filters,
items: props?.resetAll
? []
: (unionTagFilterItems(item.filters?.items, preparePostData())
: (unionTagFilterItems(item.filters?.items || [], preparePostData())
.map((item) =>
item.key?.key === props?.clearByType ? undefined : item,
)
.filter((i) => i) as TagFilterItem[]),
op: item.filters?.op || 'AND',
},
})),
},

View File

@@ -90,9 +90,14 @@ export default function QuickFilters(props: IQuickFiltersProps): JSX.Element {
...currentQuery.builder,
queryData: currentQuery.builder.queryData.map((item, idx) => ({
...item,
filter: {
...item.filter,
expression: '',
},
filters: {
...item.filters,
items: idx === lastUsedQuery ? [] : [...item.filters.items],
items: idx === lastUsedQuery ? [] : [...(item.filters?.items || [])],
op: item.filters?.op || 'AND',
},
})),
},

View File

@@ -0,0 +1,78 @@
export const OPERATORS = {
IN: 'IN',
LIKE: 'LIKE',
ILIKE: 'ILIKE',
REGEXP: 'REGEXP',
EXISTS: 'EXISTS',
CONTAINS: 'CONTAINS',
BETWEEN: 'BETWEEN',
NOT: 'NOT',
'=': '=',
'!=': '!=',
'>=': '>=',
'>': '>',
'<=': '<=',
'<': '<',
};
export const NON_VALUE_OPERATORS = [OPERATORS.EXISTS];
// eslint-disable-next-line @typescript-eslint/naming-convention
export enum QUERY_BUILDER_KEY_TYPES {
STRING = 'string',
NUMBER = 'number',
BOOLEAN = 'boolean',
}
export const QUERY_BUILDER_OPERATORS_BY_KEY_TYPE = {
[QUERY_BUILDER_KEY_TYPES.STRING]: [
OPERATORS['='],
OPERATORS['!='],
OPERATORS.IN,
OPERATORS.LIKE,
OPERATORS.ILIKE,
OPERATORS.CONTAINS,
OPERATORS.EXISTS,
OPERATORS.REGEXP,
OPERATORS.NOT,
],
[QUERY_BUILDER_KEY_TYPES.NUMBER]: [
OPERATORS['='],
OPERATORS['!='],
OPERATORS['>='],
OPERATORS['>'],
OPERATORS['<='],
OPERATORS['<'],
OPERATORS.IN,
OPERATORS.EXISTS,
OPERATORS.BETWEEN,
OPERATORS.NOT,
],
[QUERY_BUILDER_KEY_TYPES.BOOLEAN]: [
OPERATORS['='],
OPERATORS['!='],
OPERATORS.EXISTS,
OPERATORS.NOT,
],
};
export const negationQueryOperatorSuggestions = [
{ label: OPERATORS.LIKE, type: 'operator', info: 'Like' },
{ label: OPERATORS.ILIKE, type: 'operator', info: 'Case insensitive like' },
{ label: OPERATORS.EXISTS, type: 'operator', info: 'Exists' },
{ label: OPERATORS.BETWEEN, type: 'operator', info: 'Between' },
{ label: OPERATORS.IN, type: 'operator', info: 'In' },
{ label: OPERATORS.REGEXP, type: 'operator', info: 'Regular expression' },
{ label: OPERATORS.CONTAINS, type: 'operator', info: 'Contains' },
];
export const queryOperatorSuggestions = [
{ label: OPERATORS['='], type: 'operator', info: 'Equal to' },
{ label: OPERATORS['!='], type: 'operator', info: 'Not equal to' },
{ label: OPERATORS['>'], type: 'operator', info: 'Greater than' },
{ label: OPERATORS['<'], type: 'operator', info: 'Less than' },
{ label: OPERATORS['>='], type: 'operator', info: 'Greater than or equal to' },
{ label: OPERATORS['<='], type: 'operator', info: 'Less than or equal to' },
{ label: OPERATORS.NOT, type: 'operator', info: 'Not' },
...negationQueryOperatorSuggestions,
];

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

@@ -47,4 +47,5 @@ export enum QueryParams {
destination = 'destination',
kindString = 'kindString',
tab = 'tab',
selectedExplorerView = 'selectedExplorerView',
}

View File

@@ -169,6 +169,16 @@ export const initialQueryBuilderFormValues: IBuilderQuery = {
aggregateAttribute: initialAutocompleteData,
timeAggregation: MetricAggregateOperator.RATE,
spaceAggregation: MetricAggregateOperator.SUM,
filter: { expression: '' },
aggregations: [
{
metricName: '',
temporality: '',
timeAggregation: MetricAggregateOperator.COUNT,
spaceAggregation: MetricAggregateOperator.SUM,
reduceTo: 'avg',
},
],
functions: [],
filters: { items: [], op: 'AND' },
expression: createNewBuilderItemName({
@@ -176,7 +186,7 @@ export const initialQueryBuilderFormValues: IBuilderQuery = {
sourceNames: alphabet,
}),
disabled: false,
stepInterval: 60,
stepInterval: undefined,
having: [],
limit: null,
orderBy: [],
@@ -188,12 +198,14 @@ export const initialQueryBuilderFormValues: IBuilderQuery = {
const initialQueryBuilderFormLogsValues: IBuilderQuery = {
...initialQueryBuilderFormValues,
aggregateOperator: LogsAggregatorOperator.COUNT,
aggregations: [{ expression: 'count() ' }],
dataSource: DataSource.LOGS,
};
const initialQueryBuilderFormTracesValues: IBuilderQuery = {
...initialQueryBuilderFormValues,
aggregateOperator: TracesAggregatorOperator.COUNT,
aggregations: [{ expression: 'count() ' }],
dataSource: DataSource.TRACES,
};

View File

@@ -6,10 +6,6 @@ import {
import { SelectOption } from 'types/common/select';
export const metricAggregateOperatorOptions: SelectOption<string, string>[] = [
{
value: MetricAggregateOperator.NOOP,
label: 'NOOP',
},
{
value: MetricAggregateOperator.COUNT,
label: 'Count',
@@ -130,10 +126,6 @@ export const metricAggregateOperatorOptions: SelectOption<string, string>[] = [
];
export const tracesAggregateOperatorOptions: SelectOption<string, string>[] = [
{
value: TracesAggregatorOperator.NOOP,
label: 'NOOP',
},
{
value: TracesAggregatorOperator.COUNT,
label: 'Count',
@@ -217,10 +209,6 @@ export const tracesAggregateOperatorOptions: SelectOption<string, string>[] = [
];
export const logsAggregateOperatorOptions: SelectOption<string, string>[] = [
{
value: LogsAggregatorOperator.NOOP,
label: 'NOOP',
},
{
value: LogsAggregatorOperator.COUNT,
label: 'Count',

View File

@@ -29,7 +29,7 @@ export const CompositeQueryOperatorsConfig: Array<{
traceValue: 'In',
},
{
label: 'nin',
label: 'not in',
metricValue: '!~',
traceValue: 'NotIn',
},
@@ -49,7 +49,7 @@ export const CompositeQueryOperatorsConfig: Array<{
traceValue: 'Exists',
},
{
label: 'nexists',
label: 'not exists',
metricValue: '!~',
traceValue: 'NotExists',
},
@@ -59,7 +59,7 @@ export const CompositeQueryOperatorsConfig: Array<{
traceValue: 'Contains',
},
{
label: 'ncontains',
label: 'not contains',
metricValue: '!~',
traceValue: 'NotContains',
},

View File

@@ -134,7 +134,7 @@ function AllErrors(): JSX.Element {
exceptionType: getUpdatedExceptionType,
serviceName: getUpdatedServiceName,
tags: convertCompositeQueryToTraceSelectedTags(
compositeData?.builder.queryData?.[0]?.filters.items,
compositeData?.builder.queryData?.[0]?.filters?.items || [],
),
}),
enabled: !loading,
@@ -155,7 +155,7 @@ function AllErrors(): JSX.Element {
exceptionType: getUpdatedExceptionType,
serviceName: getUpdatedServiceName,
tags: convertCompositeQueryToTraceSelectedTags(
compositeData?.builder.queryData?.[0]?.filters.items,
compositeData?.builder.queryData?.[0]?.filters?.items || [],
),
}),
enabled: !loading,
@@ -461,10 +461,11 @@ function AllErrors(): JSX.Element {
logEvent('Exception: List page visited', {
numberOfExceptions: errorCountResponse?.data?.payload,
selectedEnvironments,
resourceAttributeUsed: !!compositeData?.builder.queryData?.[0]?.filters
.items?.length,
resourceAttributeUsed: !!(
compositeData?.builder.queryData?.[0]?.filters?.items?.length || 0
),
tags: convertCompositeQueryToTraceSelectedTags(
compositeData?.builder.queryData?.[0]?.filters.items,
compositeData?.builder.queryData?.[0]?.filters?.items || [],
),
});
}

View File

@@ -101,14 +101,14 @@ describe('API Monitoring Utils', () => {
// Check that each query includes the domainName filter
result.query.builder.queryData.forEach((query) => {
const serverNameFilter = query.filters.items.find(
const serverNameFilter = query.filters?.items?.find(
(item) => item.key && item.key.key === SPAN_ATTRIBUTES.SERVER_NAME,
);
expect(serverNameFilter).toBeDefined();
expect(serverNameFilter?.value).toBe(domainName);
// Check that the custom filters were included
const testFilter = query.filters.items.find(
const testFilter = query.filters?.items?.find(
(item) => item.id === 'test-filter',
);
expect(testFilter).toBeDefined();
@@ -210,13 +210,13 @@ describe('API Monitoring Utils', () => {
// Assert
expect(result).toBeDefined();
expect(result.op).toBe('AND');
expect(result?.op).toBe('AND');
// The implementation includes all keys from rowData, not just those in groupBy
expect(result.items.length).toBeGreaterThanOrEqual(3);
expect(result?.items?.length).toBeGreaterThanOrEqual(3);
// Verify each filter matches the corresponding groupBy
expect(
result.items.some(
result?.items?.some(
(item) =>
item.key &&
item.key.key === 'http.method' &&
@@ -226,7 +226,7 @@ describe('API Monitoring Utils', () => {
).toBe(true);
expect(
result.items.some(
result?.items?.some(
(item) =>
item.key &&
item.key.key === 'http.status_code' &&
@@ -236,7 +236,7 @@ describe('API Monitoring Utils', () => {
).toBe(true);
expect(
result.items.some(
result?.items?.some(
(item) =>
item.key &&
item.key.key === 'service.name' &&
@@ -272,10 +272,10 @@ describe('API Monitoring Utils', () => {
// Assert
expect(result).toBeDefined();
// The implementation includes all keys from rowData, not just those in groupBy
expect(result.items.length).toBeGreaterThanOrEqual(1);
expect(result?.items?.length).toBeGreaterThanOrEqual(1);
// Should include the known field with the proper dataType from groupBy
const knownField = result.items.find(
const knownField = result?.items?.find(
(item) => item.key && item.key.key === 'http.method',
);
expect(knownField).toBeDefined();
@@ -285,7 +285,7 @@ describe('API Monitoring Utils', () => {
}
// Should include the unknown field
const unknownField = result.items.find(
const unknownField = result?.items?.find(
(item) => item.key && item.key.key === 'unknown.field',
);
expect(unknownField).toBeDefined();
@@ -304,8 +304,8 @@ describe('API Monitoring Utils', () => {
// Assert
expect(result).toBeDefined();
expect(result.op).toBe('AND');
expect(result.items).toHaveLength(0);
expect(result?.op).toBe('AND');
expect(result?.items).toHaveLength(0);
});
});
@@ -386,11 +386,11 @@ describe('API Monitoring Utils', () => {
// Assert
expect(result).toBeDefined();
expect(result.op).toBe('AND');
expect(result.items.length).toBeGreaterThanOrEqual(3);
expect(result?.op).toBe('AND');
expect(result?.items?.length).toBeGreaterThanOrEqual(3);
// Check domain filter
const domainFilter = result.items.find(
const domainFilter = result?.items?.find(
(item) =>
item.key &&
item.key.key === SPAN_ATTRIBUTES.SERVER_NAME &&
@@ -399,7 +399,7 @@ describe('API Monitoring Utils', () => {
expect(domainFilter).toBeDefined();
// Check endpoint filter
const endpointFilter = result.items.find(
const endpointFilter = result?.items?.find(
(item) =>
item.key &&
item.key.key === SPAN_ATTRIBUTES.URL_PATH &&
@@ -408,7 +408,7 @@ describe('API Monitoring Utils', () => {
expect(endpointFilter).toBeDefined();
// Check status code filter
const statusFilter = result.items.find(
const statusFilter = result?.items?.find(
(item) =>
item.key &&
item.key.key === SPAN_ATTRIBUTES.RESPONSE_STATUS_CODE &&
@@ -469,7 +469,7 @@ describe('API Monitoring Utils', () => {
expect(queryData.filters).toBeDefined();
// Check for domain filter
const domainFilter = queryData.filters.items.find(
const domainFilter = queryData.filters?.items?.find(
// eslint-disable-next-line sonarjs/no-identical-functions
(item) =>
item.key &&
@@ -479,7 +479,7 @@ describe('API Monitoring Utils', () => {
expect(domainFilter).toBeDefined();
// Check that custom filters were included
const testFilter = queryData.filters.items.find(
const testFilter = queryData.filters?.items?.find(
(item) => item.id === 'test-filter',
);
expect(testFilter).toBeDefined();
@@ -583,7 +583,7 @@ describe('API Monitoring Utils', () => {
} = query;
queryData.forEach((qd) => {
if (qd.filters && qd.filters.items) {
const serverNameFilter = qd.filters.items.find(
const serverNameFilter = qd.filters?.items?.find(
(item) => item.key && item.key.key === SPAN_ATTRIBUTES.SERVER_NAME,
);
expect(serverNameFilter).toBeDefined();
@@ -595,7 +595,7 @@ describe('API Monitoring Utils', () => {
}
// Should include our custom filter
const customFilter = qd.filters.items.find(
const customFilter = qd.filters?.items?.find(
(item) => item.id === 'test-filter',
);
expect(customFilter).toBeDefined();
@@ -631,7 +631,7 @@ describe('API Monitoring Utils', () => {
const queryData = result.query.builder.queryData[0];
// Should have domain filter
const domainFilter = queryData.filters.items.find(
const domainFilter = queryData.filters?.items?.find(
(item) => item.key && item.key.key === SPAN_ATTRIBUTES.SERVER_NAME,
);
expect(domainFilter).toBeDefined();
@@ -698,7 +698,7 @@ describe('API Monitoring Utils', () => {
const queryData = result.query.builder.queryData[0];
// Should have domain filter
const domainFilter = queryData.filters.items.find(
const domainFilter = queryData.filters?.items?.find(
(item) => item.key && item.key.key === SPAN_ATTRIBUTES.SERVER_NAME,
);
expect(domainFilter).toBeDefined();
@@ -1375,7 +1375,7 @@ describe('API Monitoring Utils', () => {
const queryData = result.query.builder.queryData[0];
// Should have domain filter
const domainFilter = queryData.filters.items.find(
const domainFilter = queryData.filters?.items?.find(
(item) => item.key && item.key.key === SPAN_ATTRIBUTES.SERVER_NAME,
);
expect(domainFilter).toBeDefined();
@@ -1384,7 +1384,7 @@ describe('API Monitoring Utils', () => {
}
// Should have endpoint filter if provided
const endpointFilter = queryData.filters.items.find(
const endpointFilter = queryData.filters?.items?.find(
(item) => item.key && item.key.key === SPAN_ATTRIBUTES.URL_PATH,
);
expect(endpointFilter).toBeDefined();
@@ -1422,7 +1422,7 @@ describe('API Monitoring Utils', () => {
const queryData = result.query.builder.queryData[0];
// Should include our custom filter
const includedFilter = queryData.filters.items.find(
const includedFilter = queryData.filters?.items?.find(
(item) => item.id === 'custom-filter',
);
expect(includedFilter).toBeDefined();

View File

@@ -64,7 +64,7 @@ function AllEndPoints({
return params.allEndpointsLocalFilters;
}
// Initialize filters based on the initial endPointName prop
const initialItems = [...initialFilters.items];
const initialItems = [...(initialFilters?.items || [])];
return { op: 'AND', items: initialItems };
});
@@ -204,8 +204,8 @@ function AllEndPoints({
setSelectedEndPointName(props[SPAN_ATTRIBUTES.URL_PATH] as string);
setSelectedView(VIEWS.ENDPOINT_STATS);
const initialItems = [
...filters.items,
...getGroupByFiltersFromGroupByValues(props, groupBy).items,
...(filters?.items || []),
...(getGroupByFiltersFromGroupByValues(props, groupBy)?.items || []),
];
setInitialFiltersEndPointStats({
items: initialItems,

View File

@@ -68,8 +68,8 @@ function EndPointDetails({
const [filters, setFilters] = useState<IBuilderQuery['filters']>(() => {
// Initialize filters based on the initial endPointName prop
const initialItems = params.endPointDetailsLocalFilters
? [...params.endPointDetailsLocalFilters.items]
: [...initialFilters.items];
? [...(params.endPointDetailsLocalFilters?.items || [])]
: [...(initialFilters?.items || [])];
if (endPointName) {
initialItems.push({
id: '92b8a1c1',
@@ -84,7 +84,7 @@ function EndPointDetails({
// Effect to synchronize local filters when the endPointName prop changes (e.g., from dropdown)
useEffect(() => {
setFilters((currentFilters) => {
const existingHttpUrlFilter = currentFilters.items.find(
const existingHttpUrlFilter = currentFilters?.items?.find(
(item) => item.key?.key === httpUrlKey.key,
);
const existingHttpUrlValue = (existingHttpUrlFilter?.value as string) || '';
@@ -95,10 +95,10 @@ function EndPointDetails({
}
// Rebuild filters: Keep non-http.url filters and add/update http.url filter based on prop
const otherFilters = currentFilters.items.filter(
const otherFilters = currentFilters?.items?.filter(
(item) => item.key?.key !== httpUrlKey.key,
);
const newItems = [...otherFilters];
const newItems = [...(otherFilters || [])];
if (endPointName) {
newItems.push({
id: '92b8a1c1',
@@ -115,7 +115,8 @@ function EndPointDetails({
useEffect(() => {
const filtersWithoutHttpUrl = {
op: 'AND',
items: filters.items.filter((item) => item.key?.key !== httpUrlKey.key),
items:
filters?.items?.filter((item) => item.key?.key !== httpUrlKey.key) || [],
};
setParams({ endPointDetailsLocalFilters: filtersWithoutHttpUrl });
}, [filters, setParams]);
@@ -128,12 +129,14 @@ function EndPointDetails({
// Filter out http.url filter before saving to params
const filteredNewFilters = {
op: 'AND',
items: newFilters.items.filter((item) => item.key?.key !== httpUrlKey.key),
items:
newFilters?.items?.filter((item) => item.key?.key !== httpUrlKey.key) ||
[],
};
setParams({ endPointDetailsLocalFilters: filteredNewFilters });
// 2. Derive the endpoint name from the *new* filters state
const httpUrlFilter = newFilters.items.find(
const httpUrlFilter = newFilters?.items?.find(
(item) => item.key?.key === httpUrlKey.key,
);
const derivedEndPointName = (httpUrlFilter?.value as string) || '';
@@ -168,7 +171,7 @@ function EndPointDetails({
const query = updatedCurrentQuery?.builder?.queryData[0] || null;
const isServicesFilterApplied = useMemo(
() => filters.items.some((item) => item.key?.key === 'service.name'),
() => filters?.items?.some((item) => item.key?.key === 'service.name'),
[filters],
);
@@ -182,7 +185,7 @@ function EndPointDetails({
queryKey: [
END_POINT_DETAILS_QUERY_KEYS_ARRAY[index],
payload,
filters.items, // Include filters.items in queryKey for better caching
filters?.items, // Include filters.items in queryKey for better caching
ENTITY_VERSION_V4,
],
queryFn: (): Promise<SuccessResponse<MetricRangePayloadProps>> =>

View File

@@ -66,9 +66,9 @@ function TopErrors({
op: '=',
value: endPointName,
},
...initialFilters.items,
...(initialFilters?.items || []),
]
: [...initialFilters.items],
: [...(initialFilters?.items || [])],
op: 'AND',
},
showStatusCodeErrors,
@@ -236,7 +236,7 @@ function TopErrors({
record.statusCode,
);
navigateToExplorer({
filters: [...filters.items],
filters: [...(filters?.items || [])],
dataSource: DataSource.TRACES,
startTime: minTime,
endTime: maxTime,

View File

@@ -63,7 +63,7 @@ function ExpandedRow({
(queryData) => ({
...queryData,
filters: {
items: [...(queryData.filters?.items || []), ...filters.items],
items: [...(queryData.filters?.items || []), ...(filters?.items || [])],
op: 'AND',
},
}),

View File

@@ -14,6 +14,7 @@ import {
import { handleGraphClick } from 'container/GridCardLayout/GridCard/utils';
import { useGraphClickToShowButton } from 'container/GridCardLayout/useGraphClickToShowButton';
import useNavigateToExplorerPages from 'container/GridCardLayout/useNavigateToExplorerPages';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { useResizeObserver } from 'hooks/useDimensions';
import { useNotifications } from 'hooks/useNotifications';
@@ -112,6 +113,7 @@ function StatusCodeBarCharts({
});
const navigateToExplorer = useNavigateToExplorer();
const { currentQuery } = useQueryBuilder();
const navigateToExplorerPages = useNavigateToExplorerPages();
const { notifications } = useNotifications();
@@ -136,8 +138,8 @@ function StatusCodeBarCharts({
const widget = useMemo<Widgets>(
() =>
getStatusCodeBarChartWidgetData(domainName, endPointName, {
items: [...filters.items],
op: filters.op,
items: [...(filters?.items || [])],
op: filters?.op || 'AND',
}),
[domainName, endPointName, filters],
);
@@ -204,6 +206,7 @@ function StatusCodeBarCharts({
customSeries: getCustomSeries,
onDragSelect,
colorMapping,
query: currentQuery,
}),
[
minTime,
@@ -217,6 +220,7 @@ function StatusCodeBarCharts({
getCustomSeries,
onDragSelect,
colorMapping,
currentQuery,
],
);

View File

@@ -21,6 +21,7 @@ import { useQuery } from 'react-query';
import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import { DataSource } from 'types/common/queryBuilder';
import { GlobalReducer } from 'types/reducer/globalTime';
@@ -54,19 +55,22 @@ function DomainList(): JSX.Element {
// initialise tab with default query.
useShareBuilderUrl({
...initialQueriesMap.traces,
builder: {
...initialQueriesMap.traces.builder,
queryData: [
{
...initialQueriesMap.traces.builder.queryData[0],
dataSource: DataSource.TRACES,
aggregateOperator: 'noop',
aggregateAttribute: {
...initialQueriesMap.traces.builder.queryData[0].aggregateAttribute,
defaultValue: {
...initialQueriesMap.traces,
builder: {
...initialQueriesMap.traces.builder,
queryData: [
{
...initialQueriesMap.traces.builder.queryData[0],
dataSource: DataSource.TRACES,
aggregateOperator: 'noop',
aggregateAttribute: {
...(initialQueriesMap.traces.builder.queryData[0]
.aggregateAttribute as BaseAutocompleteData),
},
},
},
],
],
},
},
});
@@ -101,7 +105,7 @@ function DomainList(): JSX.Element {
op: '=',
value: 'Client',
},
...(compositeData?.builder?.queryData[0]?.filters.items || []),
...(compositeData?.builder?.queryData[0]?.filters?.items || []),
],
},
};

View File

@@ -374,7 +374,7 @@ export const getDomainMetricsQueryPayload = (
op: '=',
value: domainName,
},
...filters.items,
...(filters?.items || []),
],
op: 'AND',
},
@@ -416,7 +416,7 @@ export const getDomainMetricsQueryPayload = (
op: '=',
value: domainName,
},
...filters.items,
...(filters?.items || []),
],
op: 'AND',
},
@@ -470,7 +470,7 @@ export const getDomainMetricsQueryPayload = (
op: '=',
value: 'true',
},
...filters.items,
...(filters?.items || []),
],
op: 'AND',
},
@@ -512,7 +512,7 @@ export const getDomainMetricsQueryPayload = (
op: '=',
value: domainName,
},
...filters.items,
...(filters?.items || []),
],
op: 'AND',
},
@@ -993,7 +993,7 @@ export const getTopErrorsQueryPayload = (
op: '=',
value: true,
},
...filters.items,
...(filters?.items || []),
],
},
expression: 'A',
@@ -1586,7 +1586,7 @@ export const getEndPointDetailsQueryPayload = (
op: '=',
value: 'Client',
},
...filters.items,
...(filters?.items || []),
],
op: 'AND',
},
@@ -1640,7 +1640,7 @@ export const getEndPointDetailsQueryPayload = (
op: '=',
value: 'Client',
},
...filters.items,
...(filters?.items || []),
],
op: 'AND',
},
@@ -1706,7 +1706,7 @@ export const getEndPointDetailsQueryPayload = (
op: '=',
value: 'Client',
},
...filters.items,
...(filters?.items || []),
],
op: 'AND',
},
@@ -1760,7 +1760,7 @@ export const getEndPointDetailsQueryPayload = (
op: '=',
value: 'Client',
},
...filters.items,
...(filters?.items || []),
],
op: 'AND',
},
@@ -1814,7 +1814,7 @@ export const getEndPointDetailsQueryPayload = (
op: '=',
value: 'Client',
},
...filters.items,
...(filters?.items || []),
],
op: 'AND',
},
@@ -1909,7 +1909,7 @@ export const getEndPointDetailsQueryPayload = (
op: '=',
value: 'Client',
},
...filters.items,
...(filters?.items || []),
],
op: 'AND',
},
@@ -1971,7 +1971,7 @@ export const getEndPointDetailsQueryPayload = (
op: '=',
value: 'Client',
},
...filters.items,
...(filters?.items || []),
],
op: 'AND',
},
@@ -2036,7 +2036,7 @@ export const getEndPointDetailsQueryPayload = (
op: '=',
value: 'Client',
},
...filters.items,
...(filters?.items || []),
],
op: 'AND',
},
@@ -2132,7 +2132,7 @@ export const getEndPointDetailsQueryPayload = (
op: '=',
value: 'Client',
},
...filters.items,
...(filters?.items || []),
],
op: 'AND',
},
@@ -2226,7 +2226,7 @@ export const getEndPointDetailsQueryPayload = (
op: '=',
value: 'Client',
},
...filters.items,
...(filters?.items || []),
],
op: 'AND',
},
@@ -2289,7 +2289,7 @@ export const getEndPointDetailsQueryPayload = (
op: '=',
value: 'Client',
},
...filters.items,
...(filters?.items || []),
],
op: 'AND',
},
@@ -2352,7 +2352,7 @@ export const getEndPointDetailsQueryPayload = (
op: '=',
value: 'Client',
},
...filters.items,
...(filters?.items || []),
],
op: 'AND',
},
@@ -2427,7 +2427,7 @@ export const getEndPointDetailsQueryPayload = (
op: '=',
value: 'Client',
},
...filters.items,
...(filters?.items || []),
],
op: 'AND',
},
@@ -2531,7 +2531,7 @@ export const getEndPointDetailsQueryPayload = (
op: '=',
value: 'Client',
},
...filters.items,
...(filters?.items || []),
],
op: 'AND',
},
@@ -2627,7 +2627,7 @@ export const getEndPointDetailsQueryPayload = (
op: '=',
value: 'Client',
},
...filters.items,
...(filters?.items || []),
],
op: 'AND',
},
@@ -3318,7 +3318,7 @@ export const getStatusCodeBarChartWidgetData = (
},
]
: []),
...filters.items,
...(filters?.items || []),
],
op: 'AND',
},
@@ -3462,7 +3462,7 @@ export const getAllEndpointsWidgetData = (
op: '=',
value: 'Client',
},
...filters.items,
...(filters?.items || []),
],
op: 'AND',
},
@@ -3518,7 +3518,7 @@ export const getAllEndpointsWidgetData = (
op: '=',
value: 'Client',
},
...filters.items,
...(filters?.items || []),
],
op: 'AND',
},
@@ -3574,7 +3574,7 @@ export const getAllEndpointsWidgetData = (
op: '=',
value: 'Client',
},
...filters.items,
...(filters?.items || []),
],
op: 'AND',
},
@@ -3642,7 +3642,7 @@ export const getAllEndpointsWidgetData = (
op: '=',
value: 'Client',
},
...filters.items,
...(filters?.items || []),
],
op: 'AND',
},
@@ -3832,7 +3832,7 @@ export const getRateOverTimeWidgetData = (
op: '=',
value: domainName,
},
...filters.items,
...(filters?.items || []),
],
op: 'AND',
},
@@ -3897,7 +3897,7 @@ export const getLatencyOverTimeWidgetData = (
op: '=',
value: domainName,
},
...filters.items,
...(filters?.items || []),
],
op: 'AND',
},

View File

@@ -69,7 +69,7 @@ function Download({ data, isLoading, fileName }: DownloadProps): JSX.Element {
}
>
<Button
className="periscope-btn"
className="periscope-btn ghost"
loading={isLoading}
icon={<FileDown size={14} />}
/>

View File

@@ -257,6 +257,10 @@
}
.lightMode {
.explorer-options-container {
background: var(--bg-vanilla-100);
}
.explorer-options {
background: transparent;
box-shadow: none;

View File

@@ -15,6 +15,7 @@ import {
Typography,
} from 'antd';
import logEvent from 'api/common/logEvent';
import { TelemetryFieldKey } from 'api/v5/v5';
import axios from 'axios';
import cx from 'classnames';
import { getViewDetailsUsingViewKey } from 'components/ExplorerCard/utils';
@@ -67,7 +68,6 @@ import {
} from 'react';
import { useHistory } from 'react-router-dom';
import { Dashboard } from 'types/api/dashboard/getAll';
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
import { ViewProps } from 'types/api/saveViews/types';
import { DataSource, StringOperators } from 'types/common/queryBuilder';
@@ -270,7 +270,7 @@ function ExplorerOptions({
const getUpdatedExtraData = (
extraData: string | undefined,
newSelectedColumns: BaseAutocompleteData[],
newSelectedColumns: TelemetryFieldKey[],
formattingOptions?: FormattingOptions,
): string => {
let updatedExtraData;
@@ -354,7 +354,7 @@ function ExplorerOptions({
const { handleExplorerTabChange } = useHandleExplorerTabChange();
type ExtraData = {
selectColumns?: BaseAutocompleteData[];
selectColumns?: TelemetryFieldKey[];
version?: number;
};
@@ -589,13 +589,6 @@ function ExplorerOptions({
[isDarkMode],
);
const hideToolbar = (): void => {
setExplorerToolBarVisibility(false, sourcepage);
if (setIsExplorerOptionHidden) {
setIsExplorerOptionHidden(true);
}
};
const isEditDeleteSupported = allowedRoles.includes(user.role as string);
const [
@@ -782,6 +775,13 @@ function ExplorerOptions({
);
}, [disabled, isOneChartPerQuery, onAddToDashboard, splitedQueries]);
const hideToolbar = (): void => {
setExplorerToolBarVisibility(false, sourcepage);
if (setIsExplorerOptionHidden) {
setIsExplorerOptionHidden(true);
}
};
return (
<div className="explorer-options-container">
{

View File

@@ -20,9 +20,9 @@ function ExplorerOrderBy({ query, onChange }: OrderByFilterProps): JSX.Element {
const { data, isFetching } = useGetAggregateKeys(
{
aggregateAttribute: query.aggregateAttribute.key,
aggregateAttribute: query.aggregateAttribute?.key || '',
dataSource: query.dataSource,
aggregateOperator: query.aggregateOperator,
aggregateOperator: query.aggregateOperator || '',
searchText: debouncedSearchText,
},
{

View File

@@ -1,6 +1,6 @@
import { Button, Typography } from 'antd';
import createDashboard from 'api/v1/dashboards/create';
import { ENTITY_VERSION_V4 } from 'constants/app';
import { ENTITY_VERSION_V5 } from 'constants/app';
import { useGetAllDashboard } from 'hooks/dashboard/useGetAllDashboard';
import { useErrorModal } from 'providers/ErrorModalProvider';
import { useCallback, useMemo, useState } from 'react';
@@ -75,7 +75,7 @@ function ExportPanelContainer({
ns: 'dashboard',
}),
uploadedGrafana: false,
version: ENTITY_VERSION_V4,
version: ENTITY_VERSION_V5,
});
} catch (error) {
showErrorModal(error as APIError);

View File

@@ -2,12 +2,13 @@ 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';
import AnomalyAlertEvaluationView from 'container/AnomalyAlertEvaluationView';
import GridPanelSwitch from 'container/GridPanelSwitch';
import { populateMultipleResults } from 'container/NewWidget/LeftContainer/WidgetGraph/util';
import { getFormatNameByOptionId } from 'container/NewWidget/RightContainer/alertFomatCategories';
import { timePreferenceType } from 'container/NewWidget/RightContainer/timeItems';
import { Time } from 'container/TopNav/DateTimeSelection/config';
@@ -16,6 +17,7 @@ import {
Time as TimeV2,
} from 'container/TopNav/DateTimeSelectionV2/config';
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { useResizeObserver } from 'hooks/useDimensions';
import useUrlQuery from 'hooks/useUrlQuery';
@@ -78,6 +80,7 @@ function ChartPreview({
const threshold = alertDef?.condition.target || 0;
const [minTimeScale, setMinTimeScale] = useState<number>();
const [maxTimeScale, setMaxTimeScale] = useState<number>();
const { currentQuery } = useQueryBuilder();
const { minTime, maxTime, selectedTime: globalSelectedInterval } = useSelector<
AppState,
@@ -144,7 +147,8 @@ function ChartPreview({
},
originalGraphType: graphType,
},
alertDef?.version || DEFAULT_ENTITY_VERSION,
// alertDef?.version || DEFAULT_ENTITY_VERSION,
ENTITY_VERSION_V5,
{
queryKey: [
'chartPreview',
@@ -154,7 +158,6 @@ function ChartPreview({
maxTime,
alertDef?.ruleType,
],
retry: false,
enabled: canQuery,
},
);
@@ -175,6 +178,12 @@ function ChartPreview({
queryResponse.data.payload.data.result = sortedSeriesData;
}
if (queryResponse.data && graphType === PANEL_TYPES.PIE) {
const transformedData = populateMultipleResults(queryResponse?.data);
// eslint-disable-next-line no-param-reassign
queryResponse.data = transformedData;
}
const containerDimensions = useResizeObserver(graphRef);
const isDarkMode = useIsDarkMode();
@@ -246,6 +255,8 @@ function ChartPreview({
tzDate: (timestamp: number) =>
uPlot.tzDate(new Date(timestamp * 1e3), timezone.value),
timezone: timezone.value,
currentQuery,
query: query || currentQuery,
}),
[
yAxisUnit,
@@ -261,6 +272,8 @@ function ChartPreview({
alertDef?.condition.targetUnit,
graphType,
timezone.value,
currentQuery,
query,
],
);

View File

@@ -1,4 +1,6 @@
.alert-tabs {
padding: 0px 8px;
.ant-tabs-tab {
border: none !important;
margin-left: 0px !important;
@@ -48,6 +50,30 @@
}
}
.alert-query-section-container {
.ant-card-body {
padding: 0px;
.alert-tabs {
padding: 0px;
.ant-tabs {
.ant-tabs-nav {
padding: 8px 0;
.ant-tabs-nav-wrap {
padding: 0px;
}
}
.ant-tabs-extra-content {
padding: 0px;
}
}
}
}
}
.lightMode {
.alert-tabs {
.ant-tabs-nav-list {

View File

@@ -4,11 +4,11 @@ import { Color } from '@signozhq/design-tokens';
import { Button, Tabs, Tooltip } from 'antd';
import logEvent from 'api/common/logEvent';
import PromQLIcon from 'assets/Dashboard/PromQl';
import { QueryBuilderV2 } from 'components/QueryBuilderV2/QueryBuilderV2';
import { ALERTS_DATA_SOURCE_MAP } from 'constants/alerts';
import { ENTITY_VERSION_V4 } from 'constants/app';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { QBShortcuts } from 'constants/shortcuts/QBShortcuts';
import { QueryBuilder } from 'container/QueryBuilder';
import { useKeyboardHotkeys } from 'hooks/hotkeys/useKeyboardHotkeys';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { isEmpty } from 'lodash-es';
@@ -48,7 +48,7 @@ function QuerySection({
const isDarkMode = useIsDarkMode();
const renderMetricUI = (): JSX.Element => (
<QueryBuilder
<QueryBuilderV2
panelType={panelType}
config={{
queryVariant: 'static',
@@ -144,7 +144,7 @@ function QuerySection({
<div className="alert-tabs">
<Tabs
type="card"
style={{ width: '100%' }}
style={{ width: '100%', padding: '0px 8px' }}
defaultActiveKey={currentTab}
activeKey={currentTab}
onChange={handleQueryCategoryChange}
@@ -178,7 +178,7 @@ function QuerySection({
<div className="alert-tabs">
<Tabs
type="card"
style={{ width: '100%' }}
style={{ width: '100%', padding: '0px 8px' }}
defaultActiveKey={currentTab}
activeKey={currentTab}
onChange={handleQueryCategoryChange}
@@ -218,7 +218,7 @@ function QuerySection({
return (
<>
<StepHeading> {t('alert_form_step2', { step: step2Label })}</StepHeading>
<FormContainer>
<FormContainer className="alert-query-section-container">
<div>{renderTabs(alertType)}</div>
{renderQuerySection(currentTab)}
</FormContainer>

View File

@@ -162,7 +162,7 @@ function FormAlertRules({
const sq = useMemo(() => mapQueryDataFromApi(initQuery), [initQuery]);
useShareBuilderUrl(sq);
useShareBuilderUrl({ defaultValue: sq });
const handleDetectionMethodChange = (value: string): void => {
setAlertDef((def) => ({
@@ -832,7 +832,7 @@ function FormAlertRules({
queryCategory={currentQuery.queryType}
setQueryCategory={onQueryCategoryChange}
alertType={alertType || AlertTypes.METRICS_BASED_ALERT}
runQuery={(): void => handleRunQuery(true)}
runQuery={(): void => handleRunQuery(true, true)}
alertDef={alertDef}
panelType={panelType || PANEL_TYPES.TIME_SERIES}
key={currentQuery.queryType}

View File

@@ -10,9 +10,10 @@ 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 { populateMultipleResults } from 'container/NewWidget/LeftContainer/WidgetGraph/util';
import {
timeItems,
timePreferance,
@@ -122,7 +123,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,
@@ -178,6 +180,12 @@ function FullView({
response.data.payload.data.result = sortedSeriesData;
}
if (response.data && widget.panelTypes === PANEL_TYPES.PIE) {
const transformedData = populateMultipleResults(response?.data);
// eslint-disable-next-line no-param-reassign
response.data = transformedData;
}
useEffect(() => {
graphsVisibilityStates?.forEach((e, i) => {
fullViewChartRef?.current?.toggleGraph(i, e);

View File

@@ -2,6 +2,7 @@ import logEvent from 'api/common/logEvent';
import { DEFAULT_ENTITY_VERSION } from 'constants/app';
import { QueryParams } from 'constants/query';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { populateMultipleResults } from 'container/NewWidget/LeftContainer/WidgetGraph/util';
import { CustomTimeType } from 'container/TopNav/DateTimeSelectionV2/config';
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
import { useIntersectionObserver } from 'hooks/useIntersectionObserver';
@@ -136,6 +137,7 @@ function GridCardGraph({
formatForWeb: widget.panelTypes === PANEL_TYPES.TABLE,
start: customTimeRange?.startTime || start,
end: customTimeRange?.endTime || end,
originalGraphType: widget.panelTypes,
};
}
updatedQuery.builder.queryData[0].pageSize = 10;
@@ -259,6 +261,7 @@ function GridCardGraph({
getGraphData?.(data?.payload?.data);
setDashboardQueryRangeCalled(true);
},
showErrorModal: false,
},
);
@@ -271,6 +274,12 @@ function GridCardGraph({
queryResponse.data.payload.data.result = sortedSeriesData;
}
if (queryResponse.data && widget.panelTypes === PANEL_TYPES.PIE) {
const transformedData = populateMultipleResults(queryResponse?.data);
// eslint-disable-next-line no-param-reassign
queryResponse.data = transformedData;
}
const menuList =
widget.panelTypes === PANEL_TYPES.TABLE ||
widget.panelTypes === PANEL_TYPES.LIST ||

View File

@@ -6,6 +6,7 @@ import { Button, Form, Input, Modal, Typography } from 'antd';
import { useForm } from 'antd/es/form/Form';
import logEvent from 'api/common/logEvent';
import cx from 'classnames';
import { ENTITY_VERSION_V5 } from 'constants/app';
import { QueryParams } from 'constants/query';
import { PANEL_GROUP_TYPES, PANEL_TYPES } from 'constants/queryBuilder';
import { themeColors } from 'constants/theme';
@@ -579,7 +580,8 @@ function GraphLayout(props: GraphLayoutProps): JSX.Element {
widget={(currentWidget as Widgets) || ({ id, query: {} } as Widgets)}
headerMenuList={widgetActions}
variables={variables}
version={selectedDashboard?.data?.version}
// version={selectedDashboard?.data?.version}
version={ENTITY_VERSION_V5}
onDragSelect={onDragSelect}
dataAvailable={checkIfDataExists}
/>

View File

@@ -46,6 +46,7 @@ function useUpdatedQuery(): UseUpdatedQueryResult {
selectedTime: widgetConfig.timePreferance,
globalSelectedInterval,
variables: getDashboardVariables(selectedDashboard?.data?.variables),
originalGraphType: widgetConfig.panelTypes,
});
// Execute query and process results

View File

@@ -101,7 +101,7 @@ export function updateStepInterval(
// if user haven't enter anything manually, that is we have default value of 60 then do the interval adjustment for bar otherwise apply the user's value
const getSteps = (queryData: IBuilderQuery): number =>
queryData.stepInterval === 60
queryData?.stepInterval === 60
? stepIntervalPoints || 60
: queryData?.stepInterval || 60;

View File

@@ -4,16 +4,19 @@ export const tableDataMultipleQueriesSuccessResponse = {
name: 'service_name',
queryName: '',
isValueColumn: false,
id: 'service_name',
},
{
name: 'A',
queryName: 'A',
isValueColumn: true,
id: 'A',
},
{
name: 'B',
queryName: 'B',
isValueColumn: true,
id: 'B',
},
],
rows: [

View File

@@ -3,6 +3,7 @@ import './GridTableComponent.styles.scss';
import { ExclamationCircleFilled } from '@ant-design/icons';
import { Space, Tooltip } from 'antd';
import { ColumnType } from 'antd/es/table';
import { getYAxisFormattedValue } from 'components/Graph/yAxisConfig';
import { Events } from 'constants/events';
import { QueryTable } from 'container/QueryTable';
@@ -84,11 +85,12 @@ function GridTableComponent({
const newValue = { ...val };
Object.keys(val).forEach((k) => {
if (columnUnits[k]) {
// the check below takes care of not adding units for rows that have n/a values
newValue[k] =
val[k] !== 'n/a'
? getYAxisFormattedValue(String(val[k]), columnUnits[k])
: val[k];
// the check below takes care of not adding units for rows that have n/a or null values
if (val[k] !== 'n/a' && val[k] !== null) {
newValue[k] = getYAxisFormattedValue(String(val[k]), columnUnits[k]);
} else if (val[k] === null) {
newValue[k] = 'n/a';
}
newValue[`${k}_without_unit`] = val[k];
}
});
@@ -113,25 +115,27 @@ function GridTableComponent({
}
}, [createDataInCorrectFormat, dataSource, tableProcessedDataRef]);
// eslint-disable-next-line sonarjs/cognitive-complexity
const newColumnData = columns.map((e) => ({
...e,
render: (text: string, ...rest: any): ReactNode => {
let textForThreshold = text;
if (columnUnits && columnUnits?.[e.title as string]) {
textForThreshold = rest[0][`${e.title}_without_unit`];
const dataIndex = (e as ColumnType<RowData>)?.dataIndex || e.title;
if (columnUnits && columnUnits?.[dataIndex as string]) {
textForThreshold = rest[0][`${dataIndex}_without_unit`];
}
const isNumber = !Number.isNaN(Number(textForThreshold));
if (thresholds && isNumber) {
const { hasMultipleMatches, threshold } = findMatchingThreshold(
thresholds,
e.title as string,
dataIndex as string,
Number(textForThreshold),
columnUnits?.[e.title as string],
columnUnits?.[dataIndex as string],
);
const idx = thresholds.findIndex(
(t) => t.thresholdTableOptions === e.title,
(t) => t.thresholdTableOptions === dataIndex,
);
if (threshold && idx !== -1) {
return (

View File

@@ -98,7 +98,12 @@ export function findMatchingThreshold(
}
export interface TableData {
columns: { name: string; queryName: string; isValueColumn: boolean }[];
columns: {
name: string;
queryName: string;
isValueColumn: boolean;
id: string;
}[];
rows: { data: any }[];
}
@@ -183,10 +188,15 @@ export function createColumnsAndDataSource(
? getQueryLegend(currentQuery, item.queryName)
: undefined;
const isNewAggregation =
currentQuery?.builder?.queryData?.find(
(query) => query.queryName === item.queryName,
)?.aggregations?.length || 0;
const column: ColumnType<RowData> = {
dataIndex: item.name,
dataIndex: item.id || item.name,
// if no legend present then rely on the column name value
title: !isEmpty(legend) ? legend : item.name,
title: !isNewAggregation && !isEmpty(legend) ? legend : item.name,
width: QUERY_TABLE_CONFIG.width,
render: renderColumnCell && renderColumnCell[item.name],
sorter: (a: RowData, b: RowData): number => sortFunction(a, b, item),

View File

@@ -153,7 +153,7 @@ function HostsList(): JSX.Element {
const handleFiltersChange = useCallback(
(value: IBuilderQuery['filters']): void => {
const isNewFilterAdded = value.items.length !== filters.items.length;
const isNewFilterAdded = value?.items?.length !== filters?.items?.length;
setFilters(value);
handleChangeQueryData('filters', value);
setSearchParams({
@@ -163,7 +163,7 @@ function HostsList(): JSX.Element {
if (isNewFilterAdded) {
setCurrentPage(1);
if (value.items.length > 0) {
if (value?.items && value?.items?.length > 0) {
logEvent(InfraMonitoringEvents.FilterApplied, {
entity: InfraMonitoringEvents.HostEntity,
page: InfraMonitoringEvents.ListPage,
@@ -251,7 +251,7 @@ function HostsList(): JSX.Element {
isError={isError}
tableData={data}
hostMetricsData={hostMetricsData}
filters={filters}
filters={filters || { items: [], op: 'AND' }}
currentPage={currentPage}
setCurrentPage={setCurrentPage}
onHostClick={handleHostClick}

View File

@@ -50,7 +50,7 @@ function HostsListControls({
<div className="hosts-list-controls">
<div className="hosts-list-controls-left">
<QueryBuilderSearch
query={query}
query={query as IBuilderQuery}
onChange={handleChangeTagFilters}
isInfraMonitoring
disableNavigationShortcuts

View File

@@ -263,16 +263,18 @@ function ClusterDetails({
const handleChangeLogFilters = useCallback(
(value: IBuilderQuery['filters'], view: VIEWS) => {
setLogsAndTracesFilters((prevFilters) => {
const primaryFilters = prevFilters.items.filter((item) =>
const primaryFilters = prevFilters?.items?.filter((item) =>
[QUERY_KEYS.K8S_CLUSTER_NAME].includes(item.key?.key ?? ''),
);
const paginationFilter = value.items.find((item) => item.key?.key === 'id');
const newFilters = value.items.filter(
const paginationFilter = value?.items?.find(
(item) => item.key?.key === 'id',
);
const newFilters = value?.items?.filter(
(item) =>
item.key?.key !== 'id' && item.key?.key !== QUERY_KEYS.K8S_CLUSTER_NAME,
);
if (newFilters.length > 0) {
if (newFilters && newFilters?.length > 0) {
logEvent(InfraMonitoringEvents.FilterApplied, {
entity: InfraMonitoringEvents.K8sEntity,
page: InfraMonitoringEvents.DetailedPage,
@@ -285,8 +287,8 @@ function ClusterDetails({
op: 'AND',
items: filterDuplicateFilters(
[
...primaryFilters,
...newFilters,
...(primaryFilters || []),
...(newFilters || []),
...(paginationFilter ? [paginationFilter] : []),
].filter((item): item is TagFilterItem => item !== undefined),
),
@@ -310,11 +312,11 @@ function ClusterDetails({
const handleChangeTracesFilters = useCallback(
(value: IBuilderQuery['filters'], view: VIEWS) => {
setLogsAndTracesFilters((prevFilters) => {
const primaryFilters = prevFilters.items.filter((item) =>
const primaryFilters = prevFilters?.items?.filter((item) =>
[QUERY_KEYS.K8S_CLUSTER_NAME].includes(item.key?.key ?? ''),
);
if (value.items.length > 0) {
if (value?.items && value?.items?.length > 0) {
logEvent(InfraMonitoringEvents.FilterApplied, {
entity: InfraMonitoringEvents.K8sEntity,
page: InfraMonitoringEvents.DetailedPage,
@@ -327,10 +329,10 @@ function ClusterDetails({
op: 'AND',
items: filterDuplicateFilters(
[
...primaryFilters,
...value.items.filter(
...(primaryFilters || []),
...(value?.items?.filter(
(item) => item.key?.key !== QUERY_KEYS.K8S_CLUSTER_NAME,
),
) || []),
].filter((item): item is TagFilterItem => item !== undefined),
),
};
@@ -353,14 +355,14 @@ function ClusterDetails({
const handleChangeEventsFilters = useCallback(
(value: IBuilderQuery['filters'], view: VIEWS) => {
setEventsFilters((prevFilters) => {
const clusterKindFilter = prevFilters.items.find(
const clusterKindFilter = prevFilters?.items?.find(
(item) => item.key?.key === QUERY_KEYS.K8S_OBJECT_KIND,
);
const clusterNameFilter = prevFilters.items.find(
const clusterNameFilter = prevFilters?.items?.find(
(item) => item.key?.key === QUERY_KEYS.K8S_OBJECT_NAME,
);
if (value.items.length > 0) {
if (value?.items && value?.items?.length > 0) {
logEvent(InfraMonitoringEvents.FilterApplied, {
entity: InfraMonitoringEvents.K8sEntity,
page: InfraMonitoringEvents.DetailedPage,
@@ -375,11 +377,11 @@ function ClusterDetails({
[
clusterKindFilter,
clusterNameFilter,
...value.items.filter(
...(value?.items?.filter(
(item) =>
item.key?.key !== QUERY_KEYS.K8S_OBJECT_KIND &&
item.key?.key !== QUERY_KEYS.K8S_OBJECT_NAME,
),
) || []),
].filter((item): item is TagFilterItem => item !== undefined),
),
};
@@ -418,7 +420,9 @@ function ClusterDetails({
if (selectedView === VIEW_TYPES.LOGS) {
const filtersWithoutPagination = {
...logsAndTracesFilters,
items: logsAndTracesFilters.items.filter((item) => item.key?.key !== 'id'),
items:
logsAndTracesFilters?.items?.filter((item) => item.key?.key !== 'id') ||
[],
};
const compositeQuery = {

View File

@@ -404,7 +404,7 @@ function K8sClustersList({
setFiltersInitialised(true);
}
if (value.items.length > 0) {
if (value?.items && value?.items?.length > 0) {
logEvent(InfraMonitoringEvents.FilterApplied, {
entity: InfraMonitoringEvents.K8sEntity,
category: InfraMonitoringEvents.Cluster,

View File

@@ -279,19 +279,21 @@ function DaemonSetDetails({
const handleChangeLogFilters = useCallback(
(value: IBuilderQuery['filters'], view: VIEWS) => {
setLogAndTracesFilters((prevFilters) => {
const primaryFilters = prevFilters.items.filter((item) =>
const primaryFilters = prevFilters?.items?.filter((item) =>
[QUERY_KEYS.K8S_DAEMON_SET_NAME, QUERY_KEYS.K8S_NAMESPACE_NAME].includes(
item.key?.key ?? '',
),
);
const paginationFilter = value.items.find((item) => item.key?.key === 'id');
const newFilters = value.items.filter(
const paginationFilter = value?.items?.find(
(item) => item.key?.key === 'id',
);
const newFilters = value?.items?.filter(
(item) =>
item.key?.key !== 'id' &&
item.key?.key !== QUERY_KEYS.K8S_DAEMON_SET_NAME,
);
if (newFilters.length > 0) {
if (newFilters && newFilters?.length > 0) {
logEvent(InfraMonitoringEvents.FilterApplied, {
entity: InfraMonitoringEvents.K8sEntity,
page: InfraMonitoringEvents.DetailedPage,
@@ -303,8 +305,8 @@ function DaemonSetDetails({
const updatedFilters = {
op: 'AND',
items: [
...primaryFilters,
...newFilters,
...(primaryFilters || []),
...(newFilters || []),
...(paginationFilter ? [paginationFilter] : []),
].filter((item): item is TagFilterItem => item !== undefined),
};
@@ -326,13 +328,13 @@ function DaemonSetDetails({
const handleChangeTracesFilters = useCallback(
(value: IBuilderQuery['filters'], view: VIEWS) => {
setLogAndTracesFilters((prevFilters) => {
const primaryFilters = prevFilters.items.filter((item) =>
const primaryFilters = prevFilters?.items?.filter((item) =>
[QUERY_KEYS.K8S_DAEMON_SET_NAME, QUERY_KEYS.K8S_NAMESPACE_NAME].includes(
item.key?.key ?? '',
),
);
if (value.items.length > 0) {
if (value?.items && value?.items?.length > 0) {
logEvent(InfraMonitoringEvents.FilterApplied, {
entity: InfraMonitoringEvents.K8sEntity,
page: InfraMonitoringEvents.DetailedPage,
@@ -344,10 +346,10 @@ function DaemonSetDetails({
const updatedFilters = {
op: 'AND',
items: [
...primaryFilters,
...value.items.filter(
...(primaryFilters || []),
...(value?.items?.filter(
(item) => item.key?.key !== QUERY_KEYS.K8S_DAEMON_SET_NAME,
),
) || []),
].filter((item): item is TagFilterItem => item !== undefined),
};
@@ -369,14 +371,14 @@ function DaemonSetDetails({
const handleChangeEventsFilters = useCallback(
(value: IBuilderQuery['filters'], view: VIEWS) => {
setEventsFilters((prevFilters) => {
const daemonSetKindFilter = prevFilters.items.find(
const daemonSetKindFilter = prevFilters?.items?.find(
(item) => item.key?.key === QUERY_KEYS.K8S_OBJECT_KIND,
);
const daemonSetNameFilter = prevFilters.items.find(
const daemonSetNameFilter = prevFilters?.items?.find(
(item) => item.key?.key === QUERY_KEYS.K8S_OBJECT_NAME,
);
if (value.items.length > 0) {
if (value?.items && value?.items?.length > 0) {
logEvent(InfraMonitoringEvents.FilterApplied, {
entity: InfraMonitoringEvents.K8sEntity,
page: InfraMonitoringEvents.DetailedPage,
@@ -390,11 +392,11 @@ function DaemonSetDetails({
items: [
daemonSetKindFilter,
daemonSetNameFilter,
...value.items.filter(
...(value?.items?.filter(
(item) =>
item.key?.key !== QUERY_KEYS.K8S_OBJECT_KIND &&
item.key?.key !== QUERY_KEYS.K8S_OBJECT_NAME,
),
) || []),
].filter((item): item is TagFilterItem => item !== undefined),
};
@@ -432,7 +434,7 @@ function DaemonSetDetails({
if (selectedView === VIEW_TYPES.LOGS) {
const filtersWithoutPagination = {
...logAndTracesFilters,
items: logAndTracesFilters.items.filter((item) => item.key?.key !== 'id'),
items: logAndTracesFilters?.items?.filter((item) => item.key?.key !== 'id'),
};
const compositeQuery = {

View File

@@ -408,7 +408,7 @@ function K8sDaemonSetsList({
setFiltersInitialised(true);
}
if (value.items.length > 0) {
if (value?.items && value?.items?.length > 0) {
logEvent(InfraMonitoringEvents.FilterApplied, {
entity: InfraMonitoringEvents.K8sEntity,
page: InfraMonitoringEvents.ListPage,

View File

@@ -283,19 +283,21 @@ function DeploymentDetails({
const handleChangeLogFilters = useCallback(
(value: IBuilderQuery['filters'], view: VIEWS) => {
setLogAndTracesFilters((prevFilters) => {
const primaryFilters = prevFilters.items.filter((item) =>
const primaryFilters = prevFilters?.items?.filter((item) =>
[QUERY_KEYS.K8S_DEPLOYMENT_NAME, QUERY_KEYS.K8S_NAMESPACE_NAME].includes(
item.key?.key ?? '',
),
);
const paginationFilter = value.items.find((item) => item.key?.key === 'id');
const newFilters = value.items.filter(
const paginationFilter = value?.items?.find(
(item) => item.key?.key === 'id',
);
const newFilters = value?.items?.filter(
(item) =>
item.key?.key !== 'id' &&
item.key?.key !== QUERY_KEYS.K8S_DEPLOYMENT_NAME,
);
if (value.items.length > 0) {
if (value?.items && value?.items?.length > 0) {
logEvent(InfraMonitoringEvents.FilterApplied, {
entity: InfraMonitoringEvents.K8sEntity,
page: InfraMonitoringEvents.DetailedPage,
@@ -308,8 +310,8 @@ function DeploymentDetails({
op: 'AND',
items: filterDuplicateFilters(
[
...primaryFilters,
...newFilters,
...(primaryFilters || []),
...(newFilters || []),
...(paginationFilter ? [paginationFilter] : []),
].filter((item): item is TagFilterItem => item !== undefined),
),
@@ -333,13 +335,13 @@ function DeploymentDetails({
const handleChangeTracesFilters = useCallback(
(value: IBuilderQuery['filters'], view: VIEWS) => {
setLogAndTracesFilters((prevFilters) => {
const primaryFilters = prevFilters.items.filter((item) =>
const primaryFilters = prevFilters?.items?.filter((item) =>
[QUERY_KEYS.K8S_DEPLOYMENT_NAME, QUERY_KEYS.K8S_NAMESPACE_NAME].includes(
item.key?.key ?? '',
),
);
if (value.items.length > 0) {
if (value?.items && value?.items?.length > 0) {
logEvent(InfraMonitoringEvents.FilterApplied, {
entity: InfraMonitoringEvents.K8sEntity,
page: InfraMonitoringEvents.DetailedPage,
@@ -352,10 +354,10 @@ function DeploymentDetails({
op: 'AND',
items: filterDuplicateFilters(
[
...primaryFilters,
...value.items.filter(
...(primaryFilters || []),
...(value?.items?.filter(
(item) => item.key?.key !== QUERY_KEYS.K8S_DEPLOYMENT_NAME,
),
) || []),
].filter((item): item is TagFilterItem => item !== undefined),
),
};
@@ -378,14 +380,14 @@ function DeploymentDetails({
const handleChangeEventsFilters = useCallback(
(value: IBuilderQuery['filters'], view: VIEWS) => {
setEventsFilters((prevFilters) => {
const deploymentKindFilter = prevFilters.items.find(
const deploymentKindFilter = prevFilters?.items?.find(
(item) => item.key?.key === QUERY_KEYS.K8S_OBJECT_KIND,
);
const deploymentNameFilter = prevFilters.items.find(
const deploymentNameFilter = prevFilters?.items?.find(
(item) => item.key?.key === QUERY_KEYS.K8S_OBJECT_NAME,
);
if (value.items.length > 0) {
if (value?.items && value?.items?.length > 0) {
logEvent(InfraMonitoringEvents.FilterApplied, {
entity: InfraMonitoringEvents.K8sEntity,
page: InfraMonitoringEvents.DetailedPage,
@@ -400,11 +402,11 @@ function DeploymentDetails({
[
deploymentKindFilter,
deploymentNameFilter,
...value.items.filter(
...(value?.items?.filter(
(item) =>
item.key?.key !== QUERY_KEYS.K8S_OBJECT_KIND &&
item.key?.key !== QUERY_KEYS.K8S_OBJECT_NAME,
),
) || []),
].filter((item): item is TagFilterItem => item !== undefined),
),
};
@@ -443,7 +445,7 @@ function DeploymentDetails({
if (selectedView === VIEW_TYPES.LOGS) {
const filtersWithoutPagination = {
...logAndTracesFilters,
items: logAndTracesFilters.items.filter((item) => item.key?.key !== 'id'),
items: logAndTracesFilters?.items?.filter((item) => item.key?.key !== 'id'),
};
const compositeQuery = {

View File

@@ -411,7 +411,7 @@ function K8sDeploymentsList({
setFiltersInitialised(true);
}
if (value.items.length > 0) {
if (value?.items && value?.items?.length > 0) {
logEvent(InfraMonitoringEvents.FilterApplied, {
entity: InfraMonitoringEvents.K8sEntity,
page: InfraMonitoringEvents.ListPage,

View File

@@ -108,7 +108,7 @@ export default function Events({
...currentQuery.builder.queryData[0].aggregateAttribute,
},
filters: {
items: filters.items.filter(
items: filters?.items?.filter(
(item) =>
item.key?.key !== QUERY_KEYS.K8S_OBJECT_KIND &&
item.key?.key !== QUERY_KEYS.K8S_OBJECT_NAME,
@@ -251,7 +251,7 @@ export default function Events({
<div className="filter-section">
{query && (
<QueryBuilderSearch
query={query}
query={query as IBuilderQuery}
onChange={(value): void => handleChangeEventFilters(value, VIEWS.EVENTS)}
disableNavigationShortcuts
/>

View File

@@ -60,14 +60,14 @@ function EntityLogsDetailedView({
...currentQuery.builder.queryData[0].aggregateAttribute,
},
filters: {
items: filterOutPrimaryFilters(logFilters.items, queryKeyFilters),
items: filterOutPrimaryFilters(logFilters?.items || [], queryKeyFilters),
op: 'AND',
},
},
],
},
}),
[currentQuery, logFilters.items, queryKeyFilters],
[currentQuery, logFilters?.items, queryKeyFilters],
);
const query = updatedCurrentQuery?.builder?.queryData[0] || null;
@@ -78,7 +78,7 @@ function EntityLogsDetailedView({
<div className="filter-section">
{query && (
<QueryBuilderSearch
query={query}
query={query as IBuilderQuery}
onChange={(value): void => handleChangeLogFilters(value, VIEWS.LOGS)}
disableNavigationShortcuts
/>

View File

@@ -15,6 +15,7 @@ import {
CustomTimeType,
Time,
} from 'container/TopNav/DateTimeSelectionV2/config';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { useResizeObserver } from 'hooks/useDimensions';
import {
@@ -113,6 +114,7 @@ function EntityMetrics<T>({
const isDarkMode = useIsDarkMode();
const graphRef = useRef<HTMLDivElement>(null);
const dimensions = useResizeObserver(graphRef);
const { currentQuery } = useQueryBuilder();
const chartData = useMemo(
() =>
@@ -181,6 +183,7 @@ function EntityMetrics<T>({
minTimeScale: graphTimeIntervals[idx].start,
maxTimeScale: graphTimeIntervals[idx].end,
onDragSelect: (start, end) => onDragSelect(start, end, idx),
query: currentQuery,
});
}),
[
@@ -190,6 +193,7 @@ function EntityMetrics<T>({
entityWidgetInfo,
graphTimeIntervals,
onDragSelect,
currentQuery,
],
);

View File

@@ -84,14 +84,17 @@ function EntityTraces({
...currentQuery.builder.queryData[0].aggregateAttribute,
},
filters: {
items: filterOutPrimaryFilters(tracesFilters.items, queryKeyFilters),
items: filterOutPrimaryFilters(
tracesFilters?.items || [],
queryKeyFilters,
),
op: 'AND',
},
},
],
},
}),
[currentQuery, queryKeyFilters, tracesFilters.items],
[currentQuery, queryKeyFilters, tracesFilters?.items],
);
const query = updatedCurrentQuery?.builder?.queryData[0] || null;
@@ -148,7 +151,8 @@ function EntityTraces({
const isDataEmpty =
!isLoading && !isFetching && !isError && traces.length === 0;
const hasAdditionalFilters = tracesFilters.items.length > 1;
const hasAdditionalFilters =
tracesFilters?.items && tracesFilters?.items?.length > 1;
const totalCount =
data?.payload?.data?.newResult?.data?.result?.[0]?.list?.length || 0;
@@ -167,7 +171,7 @@ function EntityTraces({
<div className="filter-section">
{query && (
<QueryBuilderSearch
query={query}
query={query as IBuilderQuery}
onChange={(value): void =>
handleChangeTracesFilters(value, VIEWS.TRACES)
}

View File

@@ -272,18 +272,20 @@ function JobDetails({
const handleChangeLogFilters = useCallback(
(value: IBuilderQuery['filters'], view: VIEWS) => {
setLogAndTracesFilters((prevFilters) => {
const primaryFilters = prevFilters.items.filter((item) =>
const primaryFilters = prevFilters?.items?.filter((item) =>
[QUERY_KEYS.K8S_JOB_NAME, QUERY_KEYS.K8S_NAMESPACE_NAME].includes(
item.key?.key ?? '',
),
);
const paginationFilter = value.items.find((item) => item.key?.key === 'id');
const newFilters = value.items.filter(
const paginationFilter = value?.items?.find(
(item) => item.key?.key === 'id',
);
const newFilters = value?.items?.filter(
(item) =>
item.key?.key !== 'id' && item.key?.key !== QUERY_KEYS.K8S_JOB_NAME,
);
if (newFilters.length > 0) {
if (newFilters && newFilters?.length > 0) {
logEvent(InfraMonitoringEvents.FilterApplied, {
entity: InfraMonitoringEvents.K8sEntity,
page: InfraMonitoringEvents.DetailedPage,
@@ -295,8 +297,8 @@ function JobDetails({
const updatedFilters = {
op: 'AND',
items: [
...primaryFilters,
...newFilters,
...(primaryFilters || []),
...(newFilters || []),
...(paginationFilter ? [paginationFilter] : []),
].filter((item): item is TagFilterItem => item !== undefined),
};
@@ -319,13 +321,13 @@ function JobDetails({
const handleChangeTracesFilters = useCallback(
(value: IBuilderQuery['filters'], view: VIEWS) => {
setLogAndTracesFilters((prevFilters) => {
const primaryFilters = prevFilters.items.filter((item) =>
const primaryFilters = prevFilters?.items?.filter((item) =>
[QUERY_KEYS.K8S_JOB_NAME, QUERY_KEYS.K8S_NAMESPACE_NAME].includes(
item.key?.key ?? '',
),
);
if (value.items.length > 0) {
if (value?.items && value?.items?.length > 0) {
logEvent(InfraMonitoringEvents.FilterApplied, {
entity: InfraMonitoringEvents.K8sEntity,
page: InfraMonitoringEvents.DetailedPage,
@@ -337,10 +339,10 @@ function JobDetails({
const updatedFilters = {
op: 'AND',
items: [
...primaryFilters,
...value.items.filter(
...(primaryFilters || []),
...(value?.items?.filter(
(item) => item.key?.key !== QUERY_KEYS.K8S_JOB_NAME,
),
) || []),
].filter((item): item is TagFilterItem => item !== undefined),
};
@@ -362,14 +364,14 @@ function JobDetails({
const handleChangeEventsFilters = useCallback(
(value: IBuilderQuery['filters'], view: VIEWS) => {
setEventsFilters((prevFilters) => {
const jobKindFilter = prevFilters.items.find(
const jobKindFilter = prevFilters?.items?.find(
(item) => item.key?.key === QUERY_KEYS.K8S_OBJECT_KIND,
);
const jobNameFilter = prevFilters.items.find(
const jobNameFilter = prevFilters?.items?.find(
(item) => item.key?.key === QUERY_KEYS.K8S_OBJECT_NAME,
);
if (value.items.length > 0) {
if (value?.items && value?.items?.length > 0) {
logEvent(InfraMonitoringEvents.FilterApplied, {
entity: InfraMonitoringEvents.K8sEntity,
page: InfraMonitoringEvents.DetailedPage,
@@ -383,11 +385,11 @@ function JobDetails({
items: [
jobKindFilter,
jobNameFilter,
...value.items.filter(
...(value?.items?.filter(
(item) =>
item.key?.key !== QUERY_KEYS.K8S_OBJECT_KIND &&
item.key?.key !== QUERY_KEYS.K8S_OBJECT_NAME,
),
) || []),
].filter((item): item is TagFilterItem => item !== undefined),
};
@@ -425,7 +427,8 @@ function JobDetails({
if (selectedView === VIEW_TYPES.LOGS) {
const filtersWithoutPagination = {
...logAndTracesFilters,
items: logAndTracesFilters.items.filter((item) => item.key?.key !== 'id'),
items:
logAndTracesFilters?.items?.filter((item) => item.key?.key !== 'id') || [],
};
const compositeQuery = {

View File

@@ -392,7 +392,7 @@ function K8sJobsList({
setFiltersInitialised(true);
}
if (value.items.length > 0) {
if (value?.items && value?.items?.length > 0) {
logEvent(InfraMonitoringEvents.FilterApplied, {
entity: InfraMonitoringEvents.K8sEntity,
page: InfraMonitoringEvents.ListPage,

View File

@@ -111,7 +111,7 @@ function K8sHeader({
<div className="k8s-qb-search-container">
<QueryBuilderSearch
query={query}
query={query as IBuilderQuery}
onChange={handleChangeTagFilters}
isInfraMonitoring
disableNavigationShortcuts

View File

@@ -407,7 +407,7 @@ function K8sNamespacesList({
setFiltersInitialised(true);
}
if (value.items.length > 0) {
if (value?.items && value?.items?.length > 0) {
logEvent(InfraMonitoringEvents.FilterApplied, {
entity: InfraMonitoringEvents.K8sEntity,
page: InfraMonitoringEvents.ListPage,

View File

@@ -263,18 +263,20 @@ function NamespaceDetails({
const handleChangeLogFilters = useCallback(
(value: IBuilderQuery['filters'], view: VIEWS) => {
setLogAndTracesFilters((prevFilters) => {
const primaryFilters = prevFilters.items.filter((item) =>
const primaryFilters = prevFilters?.items?.filter((item) =>
[QUERY_KEYS.K8S_NAMESPACE_NAME, QUERY_KEYS.K8S_CLUSTER_NAME].includes(
item.key?.key ?? '',
),
);
const paginationFilter = value.items.find((item) => item.key?.key === 'id');
const newFilters = value.items.filter(
const paginationFilter = value?.items?.find(
(item) => item.key?.key === 'id',
);
const newFilters = value?.items?.filter(
(item) =>
item.key?.key !== 'id' && item.key?.key !== QUERY_KEYS.K8S_NAMESPACE_NAME,
);
if (newFilters.length > 0) {
if (newFilters && newFilters?.length > 0) {
logEvent(InfraMonitoringEvents.FilterApplied, {
entity: InfraMonitoringEvents.K8sEntity,
page: InfraMonitoringEvents.DetailedPage,
@@ -286,8 +288,8 @@ function NamespaceDetails({
const updatedFilters = {
op: 'AND',
items: [
...primaryFilters,
...newFilters,
...(primaryFilters || []),
...(newFilters || []),
...(paginationFilter ? [paginationFilter] : []),
].filter((item): item is TagFilterItem => item !== undefined),
};
@@ -311,13 +313,13 @@ function NamespaceDetails({
const handleChangeTracesFilters = useCallback(
(value: IBuilderQuery['filters'], view: VIEWS) => {
setLogAndTracesFilters((prevFilters) => {
const primaryFilters = prevFilters.items.filter((item) =>
const primaryFilters = prevFilters?.items?.filter((item) =>
[QUERY_KEYS.K8S_NAMESPACE_NAME, QUERY_KEYS.K8S_CLUSTER_NAME].includes(
item.key?.key ?? '',
),
);
if (value.items.length > 0) {
if (value?.items && value?.items?.length > 0) {
logEvent(InfraMonitoringEvents.FilterApplied, {
entity: InfraMonitoringEvents.K8sEntity,
page: InfraMonitoringEvents.DetailedPage,
@@ -329,10 +331,10 @@ function NamespaceDetails({
const updatedFilters = {
op: 'AND',
items: [
...primaryFilters,
...value.items.filter(
...(primaryFilters || []),
...(value?.items?.filter(
(item) => item.key?.key !== QUERY_KEYS.K8S_NAMESPACE_NAME,
),
) || []),
].filter((item): item is TagFilterItem => item !== undefined),
};
@@ -354,14 +356,14 @@ function NamespaceDetails({
const handleChangeEventsFilters = useCallback(
(value: IBuilderQuery['filters'], view: VIEWS) => {
setEventsFilters((prevFilters) => {
const namespaceKindFilter = prevFilters.items.find(
const namespaceKindFilter = prevFilters?.items?.find(
(item) => item.key?.key === QUERY_KEYS.K8S_OBJECT_KIND,
);
const namespaceNameFilter = prevFilters.items.find(
const namespaceNameFilter = prevFilters?.items?.find(
(item) => item.key?.key === QUERY_KEYS.K8S_OBJECT_NAME,
);
if (value.items.length > 0) {
if (value?.items && value?.items?.length > 0) {
logEvent(InfraMonitoringEvents.FilterApplied, {
entity: InfraMonitoringEvents.K8sEntity,
page: InfraMonitoringEvents.DetailedPage,
@@ -375,11 +377,11 @@ function NamespaceDetails({
items: [
namespaceKindFilter,
namespaceNameFilter,
...value.items.filter(
...(value?.items?.filter(
(item) =>
item.key?.key !== QUERY_KEYS.K8S_OBJECT_KIND &&
item.key?.key !== QUERY_KEYS.K8S_OBJECT_NAME,
),
) || []),
].filter((item): item is TagFilterItem => item !== undefined),
};
@@ -417,7 +419,8 @@ function NamespaceDetails({
if (selectedView === VIEW_TYPES.LOGS) {
const filtersWithoutPagination = {
...logAndTracesFilters,
items: logAndTracesFilters.items.filter((item) => item.key?.key !== 'id'),
items:
logAndTracesFilters?.items?.filter((item) => item.key?.key !== 'id') || [],
};
const compositeQuery = {

View File

@@ -397,7 +397,7 @@ function K8sNodesList({
setFiltersInitialised(true);
}
if (value.items.length > 0) {
if (value?.items && value?.items?.length > 0) {
logEvent(InfraMonitoringEvents.FilterApplied, {
entity: InfraMonitoringEvents.K8sEntity,
page: InfraMonitoringEvents.ListPage,

Some files were not shown because too many files have changed in this diff Show More