mirror of
https://github.com/SigNoz/signoz.git
synced 2025-12-29 16:14:42 +00:00
feat: new query builder (#8466)
This commit is contained in:
parent
5c1f070d8f
commit
bb6c366031
@ -1,4 +1,5 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
|
ignorePatterns: ['src/parser/*.ts'],
|
||||||
env: {
|
env: {
|
||||||
browser: true,
|
browser: true,
|
||||||
es2021: true,
|
es2021: true,
|
||||||
|
|||||||
@ -8,3 +8,6 @@ public/
|
|||||||
|
|
||||||
# Ignore all JSON files:
|
# Ignore all JSON files:
|
||||||
**/*.json
|
**/*.json
|
||||||
|
|
||||||
|
# Ignore all files in parser folder:
|
||||||
|
src/parser/**
|
||||||
@ -25,7 +25,7 @@ const config: Config.InitialOptions = {
|
|||||||
'^.+\\.(js|jsx)$': 'babel-jest',
|
'^.+\\.(js|jsx)$': 'babel-jest',
|
||||||
},
|
},
|
||||||
transformIgnorePatterns: [
|
transformIgnorePatterns: [
|
||||||
'node_modules/(?!(lodash-es|react-dnd|core-dnd|@react-dnd|dnd-core|react-dnd-html5-backend|axios|@signozhq/design-tokens|d3-interpolate|d3-color|api)/)',
|
'node_modules/(?!(lodash-es|react-dnd|core-dnd|@react-dnd|dnd-core|react-dnd-html5-backend|axios|@signozhq/design-tokens|d3-interpolate|d3-color|api|@codemirror|@lezer|@marijn)/)',
|
||||||
],
|
],
|
||||||
setupFilesAfterEnv: ['<rootDir>jest.setup.ts'],
|
setupFilesAfterEnv: ['<rootDir>jest.setup.ts'],
|
||||||
testPathIgnorePatterns: ['/node_modules/', '/public/'],
|
testPathIgnorePatterns: ['/node_modules/', '/public/'],
|
||||||
|
|||||||
@ -28,6 +28,8 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ant-design/colors": "6.0.0",
|
"@ant-design/colors": "6.0.0",
|
||||||
"@ant-design/icons": "4.8.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/core": "6.1.0",
|
||||||
"@dnd-kit/modifiers": "7.0.0",
|
"@dnd-kit/modifiers": "7.0.0",
|
||||||
"@dnd-kit/sortable": "8.0.0",
|
"@dnd-kit/sortable": "8.0.0",
|
||||||
@ -44,6 +46,9 @@
|
|||||||
"@signozhq/design-tokens": "1.1.4",
|
"@signozhq/design-tokens": "1.1.4",
|
||||||
"@tanstack/react-table": "8.20.6",
|
"@tanstack/react-table": "8.20.6",
|
||||||
"@tanstack/react-virtual": "3.11.2",
|
"@tanstack/react-virtual": "3.11.2",
|
||||||
|
"@uiw/codemirror-theme-github": "4.24.1",
|
||||||
|
"@uiw/codemirror-theme-copilot": "4.23.11",
|
||||||
|
"@uiw/react-codemirror": "4.23.10",
|
||||||
"@uiw/react-md-editor": "3.23.5",
|
"@uiw/react-md-editor": "3.23.5",
|
||||||
"@visx/group": "3.3.0",
|
"@visx/group": "3.3.0",
|
||||||
"@visx/hierarchy": "3.12.0",
|
"@visx/hierarchy": "3.12.0",
|
||||||
@ -53,6 +58,7 @@
|
|||||||
"ansi-to-html": "0.7.2",
|
"ansi-to-html": "0.7.2",
|
||||||
"antd": "5.11.0",
|
"antd": "5.11.0",
|
||||||
"antd-table-saveas-excel": "2.2.1",
|
"antd-table-saveas-excel": "2.2.1",
|
||||||
|
"antlr4": "4.13.2",
|
||||||
"axios": "1.8.2",
|
"axios": "1.8.2",
|
||||||
"babel-eslint": "^10.1.0",
|
"babel-eslint": "^10.1.0",
|
||||||
"babel-jest": "^29.6.4",
|
"babel-jest": "^29.6.4",
|
||||||
|
|||||||
@ -3,6 +3,7 @@ const apiV1 = '/api/v1/';
|
|||||||
export const apiV2 = '/api/v2/';
|
export const apiV2 = '/api/v2/';
|
||||||
export const apiV3 = '/api/v3/';
|
export const apiV3 = '/api/v3/';
|
||||||
export const apiV4 = '/api/v4/';
|
export const apiV4 = '/api/v4/';
|
||||||
|
export const apiV5 = '/api/v5/';
|
||||||
export const gatewayApiV1 = '/api/gateway/v1/';
|
export const gatewayApiV1 = '/api/gateway/v1/';
|
||||||
export const gatewayApiV2 = '/api/gateway/v2/';
|
export const gatewayApiV2 = '/api/gateway/v2/';
|
||||||
export const apiAlertManager = '/api/alertmanager/';
|
export const apiAlertManager = '/api/alertmanager/';
|
||||||
|
|||||||
@ -19,6 +19,7 @@ import apiV1, {
|
|||||||
apiV2,
|
apiV2,
|
||||||
apiV3,
|
apiV3,
|
||||||
apiV4,
|
apiV4,
|
||||||
|
apiV5,
|
||||||
gatewayApiV1,
|
gatewayApiV1,
|
||||||
gatewayApiV2,
|
gatewayApiV2,
|
||||||
} from './apiV1';
|
} from './apiV1';
|
||||||
@ -171,6 +172,18 @@ ApiV4Instance.interceptors.response.use(
|
|||||||
ApiV4Instance.interceptors.request.use(interceptorsRequestResponse);
|
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
|
// axios Base
|
||||||
export const ApiBaseInstance = axios.create({
|
export const ApiBaseInstance = axios.create({
|
||||||
baseURL: `${ENVIRONMENT.baseURL}${apiV1}`,
|
baseURL: `${ENVIRONMENT.baseURL}${apiV1}`,
|
||||||
|
|||||||
28
frontend/src/api/querySuggestions/getKeySuggestions.ts
Normal file
28
frontend/src/api/querySuggestions/getKeySuggestions.ts
Normal 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}`,
|
||||||
|
);
|
||||||
|
};
|
||||||
20
frontend/src/api/querySuggestions/getValueSuggestion.ts
Normal file
20
frontend/src/api/querySuggestions/getValueSuggestion.ts
Normal 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}`,
|
||||||
|
);
|
||||||
|
};
|
||||||
168
frontend/src/api/v5/queryRange/constants.ts
Normal file
168
frontend/src/api/v5/queryRange/constants.ts
Normal 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;
|
||||||
423
frontend/src/api/v5/queryRange/convertV5Response.ts
Normal file
423
frontend/src/api/v5/queryRange/convertV5Response.ts
Normal file
@ -0,0 +1,423 @@
|
|||||||
|
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
|
||||||
|
|
||||||
|
// Helper function to process series data
|
||||||
|
const processSeriesData = (
|
||||||
|
aggregations: any[],
|
||||||
|
seriesKey:
|
||||||
|
| 'series'
|
||||||
|
| 'predictedSeries'
|
||||||
|
| 'upperBoundSeries'
|
||||||
|
| 'lowerBoundSeries'
|
||||||
|
| 'anomalyScores',
|
||||||
|
): any[] =>
|
||||||
|
aggregations?.flatMap((aggregation) => {
|
||||||
|
const { index, alias } = aggregation;
|
||||||
|
const seriesData = aggregation[seriesKey];
|
||||||
|
|
||||||
|
if (!seriesData || !seriesData.length) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return seriesData.map((series: any) => ({
|
||||||
|
labels: series.labels
|
||||||
|
? Object.fromEntries(
|
||||||
|
series.labels.map((label: any) => [label.key.name, label.value]),
|
||||||
|
)
|
||||||
|
: {},
|
||||||
|
labelsArray: series.labels
|
||||||
|
? series.labels.map((label: any) => ({ [label.key.name]: label.value }))
|
||||||
|
: [],
|
||||||
|
values: series.values.map((value: any) => ({
|
||||||
|
timestamp: value.timestamp,
|
||||||
|
value: String(value.value),
|
||||||
|
})),
|
||||||
|
metaData: {
|
||||||
|
alias,
|
||||||
|
index,
|
||||||
|
queryName: timeSeriesData.queryName,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
queryName: timeSeriesData.queryName,
|
||||||
|
legend: legendMap[timeSeriesData.queryName] || timeSeriesData.queryName,
|
||||||
|
series: processSeriesData(timeSeriesData?.aggregations, 'series'),
|
||||||
|
predictedSeries: processSeriesData(
|
||||||
|
timeSeriesData?.aggregations,
|
||||||
|
'predictedSeries',
|
||||||
|
),
|
||||||
|
upperBoundSeries: processSeriesData(
|
||||||
|
timeSeriesData?.aggregations,
|
||||||
|
'upperBoundSeries',
|
||||||
|
),
|
||||||
|
lowerBoundSeries: processSeriesData(
|
||||||
|
timeSeriesData?.aggregations,
|
||||||
|
'lowerBoundSeries',
|
||||||
|
),
|
||||||
|
anomalyScores: processSeriesData(
|
||||||
|
timeSeriesData?.aggregations,
|
||||||
|
'anomalyScores',
|
||||||
|
),
|
||||||
|
list: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts V5 ScalarData array to legacy format with table structure
|
||||||
|
*/
|
||||||
|
function convertScalarDataArrayToTable(
|
||||||
|
scalarDataArray: ScalarData[],
|
||||||
|
legendMap: Record<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 'trace': {
|
||||||
|
const traceData = v5Data.data.results as RawData[];
|
||||||
|
return {
|
||||||
|
resultType: 'trace',
|
||||||
|
result: traceData.map((trace) => convertRawData(trace, legendMap)),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
case 'distribution': {
|
||||||
|
const distributionData = v5Data.data.results as DistributionData[];
|
||||||
|
return {
|
||||||
|
resultType: 'distribution',
|
||||||
|
result: distributionData.map((distribution) =>
|
||||||
|
convertDistributionData(distribution, legendMap),
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return {
|
||||||
|
resultType: '',
|
||||||
|
result: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts V5 API response to legacy format expected by frontend components
|
||||||
|
*/
|
||||||
|
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||||
|
export function convertV5ResponseToLegacy(
|
||||||
|
v5Response: SuccessResponse<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;
|
||||||
|
}
|
||||||
45
frontend/src/api/v5/queryRange/getQueryRange.ts
Normal file
45
frontend/src/api/v5/queryRange/getQueryRange.ts
Normal 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;
|
||||||
447
frontend/src/api/v5/queryRange/prepareQueryRangePayloadV5.ts
Normal file
447
frontend/src/api/v5/queryRange/prepareQueryRangePayloadV5.ts
Normal file
@ -0,0 +1,447 @@
|
|||||||
|
/* 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
|
||||||
|
*/
|
||||||
|
export 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:
|
||||||
|
return 'scalar';
|
||||||
|
case PANEL_TYPES.TRACE:
|
||||||
|
return 'trace';
|
||||||
|
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' || requestType === 'trace'
|
||||||
|
? 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: isEmpty(func.namedArgs)
|
||||||
|
? func.args.map((arg) => ({
|
||||||
|
value: arg,
|
||||||
|
}))
|
||||||
|
: Object.entries(func.namedArgs).map(([name, value]) => ({
|
||||||
|
name,
|
||||||
|
value,
|
||||||
|
})),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
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
|
||||||
|
*/
|
||||||
|
export 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'];
|
||||||
|
|
||||||
|
// Skip aggregation for raw request type
|
||||||
|
const aggregations =
|
||||||
|
requestType === 'raw' ? undefined : 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
|
||||||
|
*/
|
||||||
|
export 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,
|
||||||
|
legend: isEmpty(queryData.legend) ? undefined : queryData.legend,
|
||||||
|
stats: false, // PromQL specific field
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts ClickHouse queries to V5 format
|
||||||
|
*/
|
||||||
|
export 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,
|
||||||
|
legend: isEmpty(queryData.legend) ? undefined : queryData.legend,
|
||||||
|
// 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,
|
||||||
|
legend: isEmpty(formulaData.legend) ? undefined : formulaData.legend,
|
||||||
|
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 };
|
||||||
|
};
|
||||||
8
frontend/src/api/v5/v5.ts
Normal file
8
frontend/src/api/v5/v5.ts
Normal 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';
|
||||||
@ -64,7 +64,8 @@ export function applyCeleryFilterOnWidgetData(
|
|||||||
...queryItem,
|
...queryItem,
|
||||||
filters: {
|
filters: {
|
||||||
...queryItem.filters,
|
...queryItem.filters,
|
||||||
items: [...queryItem.filters.items, ...filters],
|
items: [...(queryItem.filters?.items || []), ...filters],
|
||||||
|
op: queryItem.filters?.op || 'AND',
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
: queryItem,
|
: queryItem,
|
||||||
|
|||||||
@ -41,7 +41,8 @@ export function useNavigateToExplorer(): (
|
|||||||
aggregateOperator: MetricAggregateOperator.NOOP,
|
aggregateOperator: MetricAggregateOperator.NOOP,
|
||||||
filters: {
|
filters: {
|
||||||
...item.filters,
|
...item.filters,
|
||||||
items: selectedFilters,
|
items: [...(item.filters?.items || []), ...selectedFilters],
|
||||||
|
op: item.filters?.op || 'AND',
|
||||||
},
|
},
|
||||||
groupBy: [],
|
groupBy: [],
|
||||||
disabled: false,
|
disabled: false,
|
||||||
|
|||||||
@ -18,7 +18,7 @@ function ErrorContent({ error }: ErrorContentProps): JSX.Element {
|
|||||||
errors: errorMessages,
|
errors: errorMessages,
|
||||||
code: errorCode,
|
code: errorCode,
|
||||||
message: errorMessage,
|
message: errorMessage,
|
||||||
} = error.error.error;
|
} = error?.error?.error || {};
|
||||||
return (
|
return (
|
||||||
<section className="error-content">
|
<section className="error-content">
|
||||||
{/* Summary Header */}
|
{/* Summary Header */}
|
||||||
|
|||||||
@ -43,13 +43,13 @@ export const omitIdFromQuery = (query: Query | null): any => ({
|
|||||||
builder: {
|
builder: {
|
||||||
...query?.builder,
|
...query?.builder,
|
||||||
queryData: query?.builder.queryData.map((queryData) => {
|
queryData: query?.builder.queryData.map((queryData) => {
|
||||||
const { id, ...rest } = queryData.aggregateAttribute;
|
const { id, ...rest } = queryData.aggregateAttribute || {};
|
||||||
const newAggregateAttribute = rest;
|
const newAggregateAttribute = rest;
|
||||||
const newGroupByAttributes = queryData.groupBy.map((groupByAttribute) => {
|
const newGroupByAttributes = queryData.groupBy.map((groupByAttribute) => {
|
||||||
const { id, ...rest } = groupByAttribute;
|
const { id, ...rest } = groupByAttribute;
|
||||||
return rest;
|
return rest;
|
||||||
});
|
});
|
||||||
const newItems = queryData.filters.items.map((item) => {
|
const newItems = queryData.filters?.items?.map((item) => {
|
||||||
const { id, ...newItem } = item;
|
const { id, ...newItem } = item;
|
||||||
if (item.key) {
|
if (item.key) {
|
||||||
const { id, ...rest } = item.key;
|
const { id, ...rest } = item.key;
|
||||||
|
|||||||
@ -74,16 +74,16 @@ function HostMetricTraces({
|
|||||||
...currentQuery.builder.queryData[0].aggregateAttribute,
|
...currentQuery.builder.queryData[0].aggregateAttribute,
|
||||||
},
|
},
|
||||||
filters: {
|
filters: {
|
||||||
items: tracesFilters.items.filter(
|
items:
|
||||||
(item) => item.key?.key !== 'host.name',
|
tracesFilters?.items?.filter((item) => item.key?.key !== 'host.name') ||
|
||||||
),
|
[],
|
||||||
op: 'AND',
|
op: 'AND',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
[currentQuery, tracesFilters.items],
|
[currentQuery, tracesFilters?.items],
|
||||||
);
|
);
|
||||||
|
|
||||||
const query = updatedCurrentQuery?.builder?.queryData[0] || null;
|
const query = updatedCurrentQuery?.builder?.queryData[0] || null;
|
||||||
@ -140,7 +140,8 @@ function HostMetricTraces({
|
|||||||
|
|
||||||
const isDataEmpty =
|
const isDataEmpty =
|
||||||
!isLoading && !isFetching && !isError && traces.length === 0;
|
!isLoading && !isFetching && !isError && traces.length === 0;
|
||||||
const hasAdditionalFilters = tracesFilters.items.length > 1;
|
const hasAdditionalFilters =
|
||||||
|
tracesFilters?.items && tracesFilters?.items?.length > 1;
|
||||||
|
|
||||||
const totalCount =
|
const totalCount =
|
||||||
data?.payload?.data?.newResult?.data?.result?.[0]?.list?.length || 0;
|
data?.payload?.data?.newResult?.data?.result?.[0]?.list?.length || 0;
|
||||||
@ -158,7 +159,7 @@ function HostMetricTraces({
|
|||||||
<div className="filter-section">
|
<div className="filter-section">
|
||||||
{query && (
|
{query && (
|
||||||
<QueryBuilderSearch
|
<QueryBuilderSearch
|
||||||
query={query}
|
query={query as IBuilderQuery}
|
||||||
onChange={(value): void =>
|
onChange={(value): void =>
|
||||||
handleChangeTracesFilters(value, VIEWS.TRACES)
|
handleChangeTracesFilters(value, VIEWS.TRACES)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -216,15 +216,17 @@ function HostMetricsDetails({
|
|||||||
const handleChangeLogFilters = useCallback(
|
const handleChangeLogFilters = useCallback(
|
||||||
(value: IBuilderQuery['filters'], view: VIEWS) => {
|
(value: IBuilderQuery['filters'], view: VIEWS) => {
|
||||||
setLogFilters((prevFilters) => {
|
setLogFilters((prevFilters) => {
|
||||||
const hostNameFilter = prevFilters.items.find(
|
const hostNameFilter = prevFilters?.items?.find(
|
||||||
(item) => item.key?.key === 'host.name',
|
(item) => item.key?.key === 'host.name',
|
||||||
);
|
);
|
||||||
const paginationFilter = value.items.find((item) => item.key?.key === 'id');
|
const paginationFilter = value?.items?.find(
|
||||||
const newFilters = value.items.filter(
|
(item) => item.key?.key === 'id',
|
||||||
|
);
|
||||||
|
const newFilters = value?.items?.filter(
|
||||||
(item) => item.key?.key !== 'id' && item.key?.key !== 'host.name',
|
(item) => item.key?.key !== 'id' && item.key?.key !== 'host.name',
|
||||||
);
|
);
|
||||||
|
|
||||||
if (newFilters.length > 0) {
|
if (newFilters && newFilters?.length > 0) {
|
||||||
logEvent(InfraMonitoringEvents.FilterApplied, {
|
logEvent(InfraMonitoringEvents.FilterApplied, {
|
||||||
entity: InfraMonitoringEvents.HostEntity,
|
entity: InfraMonitoringEvents.HostEntity,
|
||||||
view: InfraMonitoringEvents.LogsView,
|
view: InfraMonitoringEvents.LogsView,
|
||||||
@ -236,7 +238,7 @@ function HostMetricsDetails({
|
|||||||
op: 'AND',
|
op: 'AND',
|
||||||
items: [
|
items: [
|
||||||
hostNameFilter,
|
hostNameFilter,
|
||||||
...newFilters,
|
...(newFilters || []),
|
||||||
...(paginationFilter ? [paginationFilter] : []),
|
...(paginationFilter ? [paginationFilter] : []),
|
||||||
].filter((item): item is TagFilterItem => item !== undefined),
|
].filter((item): item is TagFilterItem => item !== undefined),
|
||||||
};
|
};
|
||||||
@ -258,11 +260,11 @@ function HostMetricsDetails({
|
|||||||
const handleChangeTracesFilters = useCallback(
|
const handleChangeTracesFilters = useCallback(
|
||||||
(value: IBuilderQuery['filters'], view: VIEWS) => {
|
(value: IBuilderQuery['filters'], view: VIEWS) => {
|
||||||
setTracesFilters((prevFilters) => {
|
setTracesFilters((prevFilters) => {
|
||||||
const hostNameFilter = prevFilters.items.find(
|
const hostNameFilter = prevFilters?.items?.find(
|
||||||
(item) => item.key?.key === 'host.name',
|
(item) => item.key?.key === 'host.name',
|
||||||
);
|
);
|
||||||
|
|
||||||
if (value.items.length > 0) {
|
if (value?.items && value?.items?.length > 0) {
|
||||||
logEvent(InfraMonitoringEvents.FilterApplied, {
|
logEvent(InfraMonitoringEvents.FilterApplied, {
|
||||||
entity: InfraMonitoringEvents.HostEntity,
|
entity: InfraMonitoringEvents.HostEntity,
|
||||||
view: InfraMonitoringEvents.TracesView,
|
view: InfraMonitoringEvents.TracesView,
|
||||||
@ -274,7 +276,7 @@ function HostMetricsDetails({
|
|||||||
op: 'AND',
|
op: 'AND',
|
||||||
items: [
|
items: [
|
||||||
hostNameFilter,
|
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),
|
].filter((item): item is TagFilterItem => item !== undefined),
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -311,7 +313,7 @@ function HostMetricsDetails({
|
|||||||
if (selectedView === VIEW_TYPES.LOGS) {
|
if (selectedView === VIEW_TYPES.LOGS) {
|
||||||
const filtersWithoutPagination = {
|
const filtersWithoutPagination = {
|
||||||
...logFilters,
|
...logFilters,
|
||||||
items: logFilters.items.filter((item) => item.key?.key !== 'id'),
|
items: logFilters?.items?.filter((item) => item.key?.key !== 'id') || [],
|
||||||
};
|
};
|
||||||
|
|
||||||
const compositeQuery = {
|
const compositeQuery = {
|
||||||
|
|||||||
@ -52,14 +52,16 @@ function HostMetricLogsDetailedView({
|
|||||||
...currentQuery.builder.queryData[0].aggregateAttribute,
|
...currentQuery.builder.queryData[0].aggregateAttribute,
|
||||||
},
|
},
|
||||||
filters: {
|
filters: {
|
||||||
items: logFilters.items.filter((item) => item.key?.key !== 'host.name'),
|
items:
|
||||||
|
logFilters?.items?.filter((item) => item.key?.key !== 'host.name') ||
|
||||||
|
[],
|
||||||
op: 'AND',
|
op: 'AND',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
[currentQuery, logFilters.items],
|
[currentQuery, logFilters?.items],
|
||||||
);
|
);
|
||||||
|
|
||||||
const query = updatedCurrentQuery?.builder?.queryData[0] || null;
|
const query = updatedCurrentQuery?.builder?.queryData[0] || null;
|
||||||
@ -70,7 +72,7 @@ function HostMetricLogsDetailedView({
|
|||||||
<div className="filter-section">
|
<div className="filter-section">
|
||||||
{query && (
|
{query && (
|
||||||
<QueryBuilderSearch
|
<QueryBuilderSearch
|
||||||
query={query}
|
query={query as IBuilderQuery}
|
||||||
onChange={(value): void => handleChangeLogFilters(value, VIEWS.LOGS)}
|
onChange={(value): void => handleChangeLogFilters(value, VIEWS.LOGS)}
|
||||||
disableNavigationShortcuts
|
disableNavigationShortcuts
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -1,168 +0,0 @@
|
|||||||
import { ENVIRONMENT } from 'constants/env';
|
|
||||||
import {
|
|
||||||
verifyFiltersAndOrderBy,
|
|
||||||
verifyPayload,
|
|
||||||
} from 'container/LogsExplorerViews/tests/LogsExplorerPagination.test';
|
|
||||||
import { logsPaginationQueryRangeSuccessResponse } from 'mocks-server/__mockdata__/logs_query_range';
|
|
||||||
import { server } from 'mocks-server/server';
|
|
||||||
import { rest } from 'msw';
|
|
||||||
import { VirtuosoMockContext } from 'react-virtuoso';
|
|
||||||
import {
|
|
||||||
act,
|
|
||||||
fireEvent,
|
|
||||||
render,
|
|
||||||
RenderResult,
|
|
||||||
waitFor,
|
|
||||||
} from 'tests/test-utils';
|
|
||||||
import { QueryRangePayload } from 'types/api/metrics/getQueryRange';
|
|
||||||
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
|
||||||
|
|
||||||
import HostMetricsLogs from '../HostMetricsLogs';
|
|
||||||
|
|
||||||
jest.mock('uplot', () => {
|
|
||||||
const paths = {
|
|
||||||
spline: jest.fn(),
|
|
||||||
bars: jest.fn(),
|
|
||||||
};
|
|
||||||
const uplotMock = jest.fn(() => ({
|
|
||||||
paths,
|
|
||||||
}));
|
|
||||||
return {
|
|
||||||
paths,
|
|
||||||
default: uplotMock,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
jest.mock(
|
|
||||||
'components/OverlayScrollbar/OverlayScrollbar',
|
|
||||||
() =>
|
|
||||||
function MockOverlayScrollbar({
|
|
||||||
children,
|
|
||||||
}: {
|
|
||||||
children: React.ReactNode;
|
|
||||||
}): JSX.Element {
|
|
||||||
return <div>{children}</div>;
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
describe.skip('HostMetricsLogs', () => {
|
|
||||||
let capturedQueryRangePayloads: QueryRangePayload[] = [];
|
|
||||||
const itemHeight = 100;
|
|
||||||
beforeEach(() => {
|
|
||||||
server.use(
|
|
||||||
rest.post(
|
|
||||||
`${ENVIRONMENT.baseURL}/api/v3/query_range`,
|
|
||||||
async (req, res, ctx) => {
|
|
||||||
capturedQueryRangePayloads.push(await req.json());
|
|
||||||
|
|
||||||
const lastPayload =
|
|
||||||
capturedQueryRangePayloads[capturedQueryRangePayloads.length - 1];
|
|
||||||
|
|
||||||
const queryData = lastPayload?.compositeQuery.builderQueries
|
|
||||||
?.A as IBuilderQuery;
|
|
||||||
|
|
||||||
const offset = queryData?.offset ?? 0;
|
|
||||||
|
|
||||||
return res(
|
|
||||||
ctx.status(200),
|
|
||||||
ctx.json(logsPaginationQueryRangeSuccessResponse({ offset })),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
capturedQueryRangePayloads = [];
|
|
||||||
});
|
|
||||||
it('should check if host logs pagination flows work properly', async () => {
|
|
||||||
let renderResult: RenderResult;
|
|
||||||
let scrollableElement: HTMLElement;
|
|
||||||
|
|
||||||
await act(async () => {
|
|
||||||
renderResult = render(
|
|
||||||
<VirtuosoMockContext.Provider value={{ viewportHeight: 500, itemHeight }}>
|
|
||||||
<HostMetricsLogs
|
|
||||||
timeRange={{ startTime: 0, endTime: 0 }}
|
|
||||||
filters={{ items: [], op: 'AND' }}
|
|
||||||
/>
|
|
||||||
</VirtuosoMockContext.Provider>,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(capturedQueryRangePayloads.length).toBe(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
await waitFor(async () => {
|
|
||||||
// Find the Virtuoso scroller element by its data-test-id
|
|
||||||
scrollableElement = renderResult.container.querySelector(
|
|
||||||
'[data-test-id="virtuoso-scroller"]',
|
|
||||||
) as HTMLElement;
|
|
||||||
|
|
||||||
// Ensure the element exists
|
|
||||||
expect(scrollableElement).not.toBeNull();
|
|
||||||
|
|
||||||
if (scrollableElement) {
|
|
||||||
// Set the scrollTop property to simulate scrolling to the calculated end position
|
|
||||||
scrollableElement.scrollTop = 99 * itemHeight;
|
|
||||||
|
|
||||||
act(() => {
|
|
||||||
fireEvent.scroll(scrollableElement);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(capturedQueryRangePayloads.length).toBe(2);
|
|
||||||
});
|
|
||||||
|
|
||||||
const firstPayload = capturedQueryRangePayloads[0];
|
|
||||||
verifyPayload({
|
|
||||||
payload: firstPayload,
|
|
||||||
expectedOffset: 0,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Store the time range from the first payload, which should be consistent in subsequent requests
|
|
||||||
const initialTimeRange = {
|
|
||||||
start: firstPayload.start,
|
|
||||||
end: firstPayload.end,
|
|
||||||
};
|
|
||||||
|
|
||||||
const secondPayload = capturedQueryRangePayloads[1];
|
|
||||||
const secondQueryData = verifyPayload({
|
|
||||||
payload: secondPayload,
|
|
||||||
expectedOffset: 100,
|
|
||||||
initialTimeRange,
|
|
||||||
});
|
|
||||||
verifyFiltersAndOrderBy(secondQueryData);
|
|
||||||
|
|
||||||
await waitFor(async () => {
|
|
||||||
// Find the Virtuoso scroller element by its data-test-id
|
|
||||||
scrollableElement = renderResult.container.querySelector(
|
|
||||||
'[data-test-id="virtuoso-scroller"]',
|
|
||||||
) as HTMLElement;
|
|
||||||
|
|
||||||
// Ensure the element exists
|
|
||||||
expect(scrollableElement).not.toBeNull();
|
|
||||||
|
|
||||||
if (scrollableElement) {
|
|
||||||
// Set the scrollTop property to simulate scrolling to the calculated end position
|
|
||||||
scrollableElement.scrollTop = 199 * itemHeight;
|
|
||||||
|
|
||||||
act(() => {
|
|
||||||
fireEvent.scroll(scrollableElement);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(capturedQueryRangePayloads.length).toBeGreaterThanOrEqual(3);
|
|
||||||
});
|
|
||||||
|
|
||||||
const thirdPayload = capturedQueryRangePayloads[2];
|
|
||||||
const thirdQueryData = verifyPayload({
|
|
||||||
payload: thirdPayload,
|
|
||||||
expectedOffset: 200,
|
|
||||||
initialTimeRange,
|
|
||||||
});
|
|
||||||
verifyFiltersAndOrderBy(thirdQueryData);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@ -13,6 +13,7 @@ import {
|
|||||||
CustomTimeType,
|
CustomTimeType,
|
||||||
Time,
|
Time,
|
||||||
} from 'container/TopNav/DateTimeSelectionV2/config';
|
} from 'container/TopNav/DateTimeSelectionV2/config';
|
||||||
|
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||||
import { useResizeObserver } from 'hooks/useDimensions';
|
import { useResizeObserver } from 'hooks/useDimensions';
|
||||||
import { useMultiIntersectionObserver } from 'hooks/useMultiIntersectionObserver';
|
import { useMultiIntersectionObserver } from 'hooks/useMultiIntersectionObserver';
|
||||||
@ -86,6 +87,7 @@ function Metrics({
|
|||||||
const isDarkMode = useIsDarkMode();
|
const isDarkMode = useIsDarkMode();
|
||||||
const graphRef = useRef<HTMLDivElement>(null);
|
const graphRef = useRef<HTMLDivElement>(null);
|
||||||
const dimensions = useResizeObserver(graphRef);
|
const dimensions = useResizeObserver(graphRef);
|
||||||
|
const { currentQuery } = useQueryBuilder();
|
||||||
|
|
||||||
const chartData = useMemo(
|
const chartData = useMemo(
|
||||||
() => queries.map(({ data }) => getUPlotChartData(data?.payload)),
|
() => queries.map(({ data }) => getUPlotChartData(data?.payload)),
|
||||||
@ -144,9 +146,17 @@ function Metrics({
|
|||||||
minTimeScale: graphTimeIntervals[idx].start,
|
minTimeScale: graphTimeIntervals[idx].start,
|
||||||
maxTimeScale: graphTimeIntervals[idx].end,
|
maxTimeScale: graphTimeIntervals[idx].end,
|
||||||
onDragSelect: (start, end) => onDragSelect(start, end, idx),
|
onDragSelect: (start, end) => onDragSelect(start, end, idx),
|
||||||
|
query: currentQuery,
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
[queries, isDarkMode, dimensions, graphTimeIntervals, onDragSelect],
|
[
|
||||||
|
queries,
|
||||||
|
isDarkMode,
|
||||||
|
dimensions,
|
||||||
|
graphTimeIntervals,
|
||||||
|
onDragSelect,
|
||||||
|
currentQuery,
|
||||||
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
const renderCardContent = (
|
const renderCardContent = (
|
||||||
|
|||||||
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
74
frontend/src/components/InputWithLabel/InputWithLabel.tsx
Normal file
74
frontend/src/components/InputWithLabel/InputWithLabel.tsx
Normal 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;
|
||||||
@ -20,3 +20,7 @@ export type LogDetailProps = {
|
|||||||
} & Pick<AddToQueryHOCProps, 'onAddToQuery'> &
|
} & Pick<AddToQueryHOCProps, 'onAddToQuery'> &
|
||||||
Partial<Pick<ActionItemProps, 'onClickActionItem'>> &
|
Partial<Pick<ActionItemProps, 'onClickActionItem'>> &
|
||||||
Pick<DrawerProps, 'onClose'>;
|
Pick<DrawerProps, 'onClose'>;
|
||||||
|
|
||||||
|
export type LogDetailInnerProps = LogDetailProps & {
|
||||||
|
log: NonNullable<LogDetailProps['log']>;
|
||||||
|
};
|
||||||
|
|||||||
@ -62,6 +62,10 @@
|
|||||||
box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.1);
|
box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&-query-container {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
.log-detail-drawer__log {
|
.log-detail-drawer__log {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@ -6,6 +6,8 @@ import { Button, Divider, Drawer, Radio, Tooltip, Typography } from 'antd';
|
|||||||
import { RadioChangeEvent } from 'antd/lib';
|
import { RadioChangeEvent } from 'antd/lib';
|
||||||
import cx from 'classnames';
|
import cx from 'classnames';
|
||||||
import { LogType } from 'components/Logs/LogStateIndicator/LogStateIndicator';
|
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 { LOCALSTORAGE } from 'constants/localStorage';
|
||||||
import { QueryParams } from 'constants/query';
|
import { QueryParams } from 'constants/query';
|
||||||
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
|
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
|
||||||
@ -19,19 +21,20 @@ import {
|
|||||||
getSanitizedLogBody,
|
getSanitizedLogBody,
|
||||||
removeEscapeCharacters,
|
removeEscapeCharacters,
|
||||||
} from 'container/LogDetailedView/utils';
|
} from 'container/LogDetailedView/utils';
|
||||||
|
import useInitialQuery from 'container/LogsExplorerContext/useInitialQuery';
|
||||||
import { useOptionsMenu } from 'container/OptionsMenu';
|
import { useOptionsMenu } from 'container/OptionsMenu';
|
||||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||||
import { useNotifications } from 'hooks/useNotifications';
|
import { useNotifications } from 'hooks/useNotifications';
|
||||||
import { useSafeNavigate } from 'hooks/useSafeNavigate';
|
import { useSafeNavigate } from 'hooks/useSafeNavigate';
|
||||||
import createQueryParams from 'lib/createQueryParams';
|
import createQueryParams from 'lib/createQueryParams';
|
||||||
|
import { cloneDeep } from 'lodash-es';
|
||||||
import {
|
import {
|
||||||
BarChart2,
|
BarChart2,
|
||||||
Braces,
|
Braces,
|
||||||
Compass,
|
Compass,
|
||||||
Copy,
|
Copy,
|
||||||
Filter,
|
Filter,
|
||||||
HardHat,
|
|
||||||
Table,
|
Table,
|
||||||
TextSelect,
|
TextSelect,
|
||||||
X,
|
X,
|
||||||
@ -45,10 +48,9 @@ import { DataSource, StringOperators } from 'types/common/queryBuilder';
|
|||||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||||
|
|
||||||
import { RESOURCE_KEYS, VIEW_TYPES, VIEWS } from './constants';
|
import { RESOURCE_KEYS, VIEW_TYPES, VIEWS } from './constants';
|
||||||
import { LogDetailProps } from './LogDetail.interfaces';
|
import { LogDetailInnerProps, LogDetailProps } from './LogDetail.interfaces';
|
||||||
import QueryBuilderSearchWrapper from './QueryBuilderSearchWrapper';
|
|
||||||
|
|
||||||
function LogDetail({
|
function LogDetailInner({
|
||||||
log,
|
log,
|
||||||
onClose,
|
onClose,
|
||||||
onAddToQuery,
|
onAddToQuery,
|
||||||
@ -57,13 +59,16 @@ function LogDetail({
|
|||||||
selectedTab,
|
selectedTab,
|
||||||
isListViewPanel = false,
|
isListViewPanel = false,
|
||||||
listViewPanelSelectedFields,
|
listViewPanelSelectedFields,
|
||||||
}: LogDetailProps): JSX.Element {
|
}: LogDetailInnerProps): JSX.Element {
|
||||||
|
const initialContextQuery = useInitialQuery(log);
|
||||||
|
const [contextQuery, setContextQuery] = useState<Query | undefined>(
|
||||||
|
initialContextQuery,
|
||||||
|
);
|
||||||
const [, copyToClipboard] = useCopyToClipboard();
|
const [, copyToClipboard] = useCopyToClipboard();
|
||||||
const [selectedView, setSelectedView] = useState<VIEWS>(selectedTab);
|
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 [filters, setFilters] = useState<TagFilter | null>(null);
|
||||||
const [isEdit, setIsEdit] = useState<boolean>(false);
|
const [isEdit, setIsEdit] = useState<boolean>(false);
|
||||||
const { stagedQuery, updateAllQueriesOperators } = useQueryBuilder();
|
const { stagedQuery, updateAllQueriesOperators } = useQueryBuilder();
|
||||||
@ -98,7 +103,7 @@ function LogDetail({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleFilterVisible = (): void => {
|
const handleFilterVisible = (): void => {
|
||||||
setIsFilterVisible(!isFilterVisibile);
|
setIsFilterVisible(!isFilterVisible);
|
||||||
setIsEdit(!isEdit);
|
setIsEdit(!isEdit);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -141,6 +146,44 @@ function LogDetail({
|
|||||||
safeNavigate(`${ROUTES.LOGS_EXPLORER}?${createQueryParams(queryParams)}`);
|
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
|
// Only show when opened from infra monitoring page
|
||||||
const showOpenInExplorerBtn = useMemo(
|
const showOpenInExplorerBtn = useMemo(
|
||||||
() => location.pathname?.includes('/infrastructure-monitoring'),
|
() => location.pathname?.includes('/infrastructure-monitoring'),
|
||||||
@ -148,11 +191,6 @@ function LogDetail({
|
|||||||
[],
|
[],
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!log) {
|
|
||||||
// eslint-disable-next-line react/jsx-no-useless-fragment
|
|
||||||
return <></>;
|
|
||||||
}
|
|
||||||
|
|
||||||
const logType = log?.attributes_string?.log_level || LogType.INFO;
|
const logType = log?.attributes_string?.log_level || LogType.INFO;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -268,18 +306,16 @@ function LogDetail({
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
{isFilterVisible && contextQuery?.builder.queryData[0] && (
|
||||||
<QueryBuilderSearchWrapper
|
<div className="log-detail-drawer-query-container">
|
||||||
isEdit={isEdit}
|
<QuerySearch
|
||||||
log={log}
|
onChange={(): void => {}}
|
||||||
filters={filters}
|
dataSource={DataSource.LOGS}
|
||||||
setContextQuery={setContextQuery}
|
queryData={contextQuery?.builder.queryData[0]}
|
||||||
setFilters={setFilters}
|
onRun={handleRunQuery}
|
||||||
contextQuery={contextQuery}
|
|
||||||
suffixIcon={
|
|
||||||
<HardHat size={12} style={{ paddingRight: Spacing.PADDING_2 }} />
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{selectedView === VIEW_TYPES.OVERVIEW && (
|
{selectedView === VIEW_TYPES.OVERVIEW && (
|
||||||
<Overview
|
<Overview
|
||||||
@ -315,4 +351,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;
|
export default LogDetail;
|
||||||
|
|||||||
@ -410,18 +410,18 @@ export default function LogsFormatOptionsMenu({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="column-format">
|
<div className="column-format">
|
||||||
{addColumn?.value?.map(({ key, id }) => (
|
{addColumn?.value?.map(({ name }) => (
|
||||||
<div className="column-name" key={id}>
|
<div className="column-name" key={name}>
|
||||||
<div className="name">
|
<div className="name">
|
||||||
<Tooltip placement="left" title={key}>
|
<Tooltip placement="left" title={name}>
|
||||||
{key}
|
{name}
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
{addColumn?.value?.length > 1 && (
|
{addColumn?.value?.length > 1 && (
|
||||||
<X
|
<X
|
||||||
className="delete-btn"
|
className="delete-btn"
|
||||||
size={14}
|
size={14}
|
||||||
onClick={(): void => addColumn.onRemove(id as string)}
|
onClick={(): void => addColumn.onRemove(name)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -0,0 +1,7 @@
|
|||||||
|
.order-by-loading-container {
|
||||||
|
padding: 4px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
115
frontend/src/components/OrderBy/ListViewOrderBy.tsx
Normal file
115
frontend/src/components/OrderBy/ListViewOrderBy.tsx
Normal 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;
|
||||||
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
186
frontend/src/components/QueryBuilderV2/QueryBuilderV2.tsx
Normal file
186
frontend/src/components/QueryBuilderV2/QueryBuilderV2.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
});
|
||||||
@ -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;
|
||||||
|
};
|
||||||
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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;
|
||||||
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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>
|
||||||
|
);
|
||||||
|
});
|
||||||
@ -0,0 +1,378 @@
|
|||||||
|
/* 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 { githubLight } from '@uiw/codemirror-theme-github';
|
||||||
|
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 { useIsDarkMode } from 'hooks/useDarkMode';
|
||||||
|
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 isDarkMode = useIsDarkMode();
|
||||||
|
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={isDarkMode ? copilot : githubLight}
|
||||||
|
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;
|
||||||
@ -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-vanilla-300) !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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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;
|
||||||
@ -0,0 +1,343 @@
|
|||||||
|
.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-vanilla-300) !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-vanilla-100);
|
||||||
|
border: none;
|
||||||
|
box-shadow: 0px 4px 12px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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;
|
||||||
@ -0,0 +1,671 @@
|
|||||||
|
/* 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 { githubLight } from '@uiw/codemirror-theme-github';
|
||||||
|
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 { useIsDarkMode } from 'hooks/useDarkMode';
|
||||||
|
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: false, 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 isDarkMode = useIsDarkMode();
|
||||||
|
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={isDarkMode ? copilot : githubLight}
|
||||||
|
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;
|
||||||
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -0,0 +1,716 @@
|
|||||||
|
.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);
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.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: #b3d4fc !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
::selection {
|
||||||
|
background: #b3d4fc !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.cm-selectionBackground {
|
||||||
|
background: #b3d4fc !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
@ -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',
|
||||||
|
},
|
||||||
|
];
|
||||||
239
frontend/src/components/QueryBuilderV2/QueryV2/QueryV2.tsx
Normal file
239
frontend/src/components/QueryBuilderV2/QueryV2/QueryV2.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
});
|
||||||
670
frontend/src/components/QueryBuilderV2/utils.ts
Normal file
670
frontend/src/components/QueryBuilderV2/utils.ts
Normal 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;
|
||||||
|
};
|
||||||
@ -6,14 +6,13 @@ import './Checkbox.styles.scss';
|
|||||||
|
|
||||||
import { Button, Checkbox, Input, Skeleton, Typography } from 'antd';
|
import { Button, Checkbox, Input, Skeleton, Typography } from 'antd';
|
||||||
import cx from 'classnames';
|
import cx from 'classnames';
|
||||||
|
import { removeKeysFromExpression } from 'components/QueryBuilderV2/utils';
|
||||||
import {
|
import {
|
||||||
IQuickFiltersConfig,
|
IQuickFiltersConfig,
|
||||||
QuickFiltersSource,
|
QuickFiltersSource,
|
||||||
} from 'components/QuickFilters/types';
|
} from 'components/QuickFilters/types';
|
||||||
import {
|
import { OPERATORS } from 'constants/antlrQueryConstants';
|
||||||
DATA_TYPE_VS_ATTRIBUTE_VALUES_KEY,
|
import { DATA_TYPE_VS_ATTRIBUTE_VALUES_KEY } from 'constants/queryBuilder';
|
||||||
OPERATORS,
|
|
||||||
} from 'constants/queryBuilder';
|
|
||||||
import { DEBOUNCE_DELAY } from 'constants/queryBuilderFilterConfig';
|
import { DEBOUNCE_DELAY } from 'constants/queryBuilderFilterConfig';
|
||||||
import { getOperatorValue } from 'container/QueryBuilder/filters/QueryBuilderSearch/utils';
|
import { getOperatorValue } from 'container/QueryBuilder/filters/QueryBuilderSearch/utils';
|
||||||
import { useGetAggregateValues } from 'hooks/queryBuilder/useGetAggregateValues';
|
import { useGetAggregateValues } from 'hooks/queryBuilder/useGetAggregateValues';
|
||||||
@ -30,7 +29,7 @@ import { v4 as uuid } from 'uuid';
|
|||||||
import LogsQuickFilterEmptyState from './LogsQuickFilterEmptyState';
|
import LogsQuickFilterEmptyState from './LogsQuickFilterEmptyState';
|
||||||
|
|
||||||
const SELECTED_OPERATORS = [OPERATORS['='], 'in'];
|
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];
|
const SOURCES_WITH_EMPTY_STATE_ENABLED = [QuickFiltersSource.LOGS_EXPLORER];
|
||||||
|
|
||||||
@ -168,14 +167,20 @@ export default function CheckboxFilter(props: ICheckboxProps): JSX.Element {
|
|||||||
...currentQuery.builder,
|
...currentQuery.builder,
|
||||||
queryData: currentQuery.builder.queryData.map((item, idx) => ({
|
queryData: currentQuery.builder.queryData.map((item, idx) => ({
|
||||||
...item,
|
...item,
|
||||||
|
filter: {
|
||||||
|
expression: removeKeysFromExpression(item.filter?.expression ?? '', [
|
||||||
|
filter.attributeKey.key,
|
||||||
|
]),
|
||||||
|
},
|
||||||
filters: {
|
filters: {
|
||||||
...item.filters,
|
...item.filters,
|
||||||
items:
|
items:
|
||||||
idx === lastUsedQuery
|
idx === lastUsedQuery
|
||||||
? item.filters.items.filter(
|
? item.filters?.items?.filter(
|
||||||
(fil) => !isEqual(fil.key?.key, filter.attributeKey.key),
|
(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(
|
query.filters.items = query.filters.items.filter(
|
||||||
(q) => !isEqual(q.key?.key, filter.attributeKey.key),
|
(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') {
|
if (isOnlyOrAll === 'Only') {
|
||||||
const newFilterItem: TagFilterItem = {
|
const newFilterItem: TagFilterItem = {
|
||||||
id: uuid(),
|
id: uuid(),
|
||||||
@ -293,7 +306,7 @@ export default function CheckboxFilter(props: ICheckboxProps): JSX.Element {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 'nin':
|
case 'not in':
|
||||||
// if the current running operator is NIN then when unchecking the value it gets
|
// if the current running operator is NIN then when unchecking the value it gets
|
||||||
// added to the clause like key NIN [value1 , currentUnselectedValue]
|
// added to the clause like key NIN [value1 , currentUnselectedValue]
|
||||||
if (!checked) {
|
if (!checked) {
|
||||||
@ -372,7 +385,7 @@ export default function CheckboxFilter(props: ICheckboxProps): JSX.Element {
|
|||||||
if (!checked) {
|
if (!checked) {
|
||||||
const newFilter = {
|
const newFilter = {
|
||||||
...currentFilter,
|
...currentFilter,
|
||||||
op: getOperatorValue(OPERATORS.NIN),
|
op: getOperatorValue('NOT_IN'),
|
||||||
value: [currentFilter.value as string, value],
|
value: [currentFilter.value as string, value],
|
||||||
};
|
};
|
||||||
query.filters.items = query.filters.items.map((item) => {
|
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.
|
// case - when there is no filter for the current key that means all are selected right now.
|
||||||
const newFilterItem: TagFilterItem = {
|
const newFilterItem: TagFilterItem = {
|
||||||
id: uuid(),
|
id: uuid(),
|
||||||
op: getOperatorValue(OPERATORS.NIN),
|
op: getOperatorValue('NOT_IN'),
|
||||||
key: filter.attributeKey,
|
key: filter.attributeKey,
|
||||||
value,
|
value,
|
||||||
};
|
};
|
||||||
|
|||||||
@ -178,10 +178,12 @@ function Duration({
|
|||||||
...data,
|
...data,
|
||||||
filters: {
|
filters: {
|
||||||
...data.filters,
|
...data.filters,
|
||||||
items: data.filters?.items?.map((item) => ({
|
items:
|
||||||
|
data.filters?.items?.map((item) => ({
|
||||||
...item,
|
...item,
|
||||||
id: '',
|
id: '',
|
||||||
})),
|
})) || [],
|
||||||
|
op: data.filters?.op || 'AND',
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
return clonedQuery;
|
return clonedQuery;
|
||||||
@ -199,11 +201,12 @@ function Duration({
|
|||||||
...item.filters,
|
...item.filters,
|
||||||
items: props?.resetAll
|
items: props?.resetAll
|
||||||
? []
|
? []
|
||||||
: (unionTagFilterItems(item.filters?.items, preparePostData())
|
: (unionTagFilterItems(item.filters?.items || [], preparePostData())
|
||||||
.map((item) =>
|
.map((item) =>
|
||||||
item.key?.key === props?.clearByType ? undefined : item,
|
item.key?.key === props?.clearByType ? undefined : item,
|
||||||
)
|
)
|
||||||
.filter((i) => i) as TagFilterItem[]),
|
.filter((i) => i) as TagFilterItem[]),
|
||||||
|
op: item.filters?.op || 'AND',
|
||||||
},
|
},
|
||||||
})),
|
})),
|
||||||
},
|
},
|
||||||
|
|||||||
@ -90,9 +90,14 @@ export default function QuickFilters(props: IQuickFiltersProps): JSX.Element {
|
|||||||
...currentQuery.builder,
|
...currentQuery.builder,
|
||||||
queryData: currentQuery.builder.queryData.map((item, idx) => ({
|
queryData: currentQuery.builder.queryData.map((item, idx) => ({
|
||||||
...item,
|
...item,
|
||||||
|
filter: {
|
||||||
|
...item.filter,
|
||||||
|
expression: '',
|
||||||
|
},
|
||||||
filters: {
|
filters: {
|
||||||
...item.filters,
|
...item.filters,
|
||||||
items: idx === lastUsedQuery ? [] : [...item.filters.items],
|
items: idx === lastUsedQuery ? [] : [...(item.filters?.items || [])],
|
||||||
|
op: item.filters?.op || 'AND',
|
||||||
},
|
},
|
||||||
})),
|
})),
|
||||||
},
|
},
|
||||||
|
|||||||
78
frontend/src/constants/antlrQueryConstants.ts
Normal file
78
frontend/src/constants/antlrQueryConstants.ts
Normal 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,
|
||||||
|
];
|
||||||
@ -15,3 +15,4 @@ export const DASHBOARD_TIME_IN_DURATION = 'refreshInterval';
|
|||||||
|
|
||||||
export const DEFAULT_ENTITY_VERSION = 'v3';
|
export const DEFAULT_ENTITY_VERSION = 'v3';
|
||||||
export const ENTITY_VERSION_V4 = 'v4';
|
export const ENTITY_VERSION_V4 = 'v4';
|
||||||
|
export const ENTITY_VERSION_V5 = 'v5';
|
||||||
|
|||||||
@ -48,4 +48,5 @@ export enum QueryParams {
|
|||||||
kindString = 'kindString',
|
kindString = 'kindString',
|
||||||
tab = 'tab',
|
tab = 'tab',
|
||||||
thresholds = 'thresholds',
|
thresholds = 'thresholds',
|
||||||
|
selectedExplorerView = 'selectedExplorerView',
|
||||||
}
|
}
|
||||||
|
|||||||
@ -169,6 +169,16 @@ export const initialQueryBuilderFormValues: IBuilderQuery = {
|
|||||||
aggregateAttribute: initialAutocompleteData,
|
aggregateAttribute: initialAutocompleteData,
|
||||||
timeAggregation: MetricAggregateOperator.RATE,
|
timeAggregation: MetricAggregateOperator.RATE,
|
||||||
spaceAggregation: MetricAggregateOperator.SUM,
|
spaceAggregation: MetricAggregateOperator.SUM,
|
||||||
|
filter: { expression: '' },
|
||||||
|
aggregations: [
|
||||||
|
{
|
||||||
|
metricName: '',
|
||||||
|
temporality: '',
|
||||||
|
timeAggregation: MetricAggregateOperator.COUNT,
|
||||||
|
spaceAggregation: MetricAggregateOperator.SUM,
|
||||||
|
reduceTo: 'avg',
|
||||||
|
},
|
||||||
|
],
|
||||||
functions: [],
|
functions: [],
|
||||||
filters: { items: [], op: 'AND' },
|
filters: { items: [], op: 'AND' },
|
||||||
expression: createNewBuilderItemName({
|
expression: createNewBuilderItemName({
|
||||||
@ -176,7 +186,7 @@ export const initialQueryBuilderFormValues: IBuilderQuery = {
|
|||||||
sourceNames: alphabet,
|
sourceNames: alphabet,
|
||||||
}),
|
}),
|
||||||
disabled: false,
|
disabled: false,
|
||||||
stepInterval: 60,
|
stepInterval: undefined,
|
||||||
having: [],
|
having: [],
|
||||||
limit: null,
|
limit: null,
|
||||||
orderBy: [],
|
orderBy: [],
|
||||||
@ -188,12 +198,14 @@ export const initialQueryBuilderFormValues: IBuilderQuery = {
|
|||||||
const initialQueryBuilderFormLogsValues: IBuilderQuery = {
|
const initialQueryBuilderFormLogsValues: IBuilderQuery = {
|
||||||
...initialQueryBuilderFormValues,
|
...initialQueryBuilderFormValues,
|
||||||
aggregateOperator: LogsAggregatorOperator.COUNT,
|
aggregateOperator: LogsAggregatorOperator.COUNT,
|
||||||
|
aggregations: [{ expression: 'count() ' }],
|
||||||
dataSource: DataSource.LOGS,
|
dataSource: DataSource.LOGS,
|
||||||
};
|
};
|
||||||
|
|
||||||
const initialQueryBuilderFormTracesValues: IBuilderQuery = {
|
const initialQueryBuilderFormTracesValues: IBuilderQuery = {
|
||||||
...initialQueryBuilderFormValues,
|
...initialQueryBuilderFormValues,
|
||||||
aggregateOperator: TracesAggregatorOperator.COUNT,
|
aggregateOperator: TracesAggregatorOperator.COUNT,
|
||||||
|
aggregations: [{ expression: 'count() ' }],
|
||||||
dataSource: DataSource.TRACES,
|
dataSource: DataSource.TRACES,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -6,10 +6,6 @@ import {
|
|||||||
import { SelectOption } from 'types/common/select';
|
import { SelectOption } from 'types/common/select';
|
||||||
|
|
||||||
export const metricAggregateOperatorOptions: SelectOption<string, string>[] = [
|
export const metricAggregateOperatorOptions: SelectOption<string, string>[] = [
|
||||||
{
|
|
||||||
value: MetricAggregateOperator.NOOP,
|
|
||||||
label: 'NOOP',
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
value: MetricAggregateOperator.COUNT,
|
value: MetricAggregateOperator.COUNT,
|
||||||
label: 'Count',
|
label: 'Count',
|
||||||
@ -130,10 +126,6 @@ export const metricAggregateOperatorOptions: SelectOption<string, string>[] = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
export const tracesAggregateOperatorOptions: SelectOption<string, string>[] = [
|
export const tracesAggregateOperatorOptions: SelectOption<string, string>[] = [
|
||||||
{
|
|
||||||
value: TracesAggregatorOperator.NOOP,
|
|
||||||
label: 'NOOP',
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
value: TracesAggregatorOperator.COUNT,
|
value: TracesAggregatorOperator.COUNT,
|
||||||
label: 'Count',
|
label: 'Count',
|
||||||
@ -217,10 +209,6 @@ export const tracesAggregateOperatorOptions: SelectOption<string, string>[] = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
export const logsAggregateOperatorOptions: SelectOption<string, string>[] = [
|
export const logsAggregateOperatorOptions: SelectOption<string, string>[] = [
|
||||||
{
|
|
||||||
value: LogsAggregatorOperator.NOOP,
|
|
||||||
label: 'NOOP',
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
value: LogsAggregatorOperator.COUNT,
|
value: LogsAggregatorOperator.COUNT,
|
||||||
label: 'Count',
|
label: 'Count',
|
||||||
|
|||||||
@ -29,7 +29,7 @@ export const CompositeQueryOperatorsConfig: Array<{
|
|||||||
traceValue: 'In',
|
traceValue: 'In',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'nin',
|
label: 'not in',
|
||||||
metricValue: '!~',
|
metricValue: '!~',
|
||||||
traceValue: 'NotIn',
|
traceValue: 'NotIn',
|
||||||
},
|
},
|
||||||
@ -49,7 +49,7 @@ export const CompositeQueryOperatorsConfig: Array<{
|
|||||||
traceValue: 'Exists',
|
traceValue: 'Exists',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'nexists',
|
label: 'not exists',
|
||||||
metricValue: '!~',
|
metricValue: '!~',
|
||||||
traceValue: 'NotExists',
|
traceValue: 'NotExists',
|
||||||
},
|
},
|
||||||
@ -59,7 +59,7 @@ export const CompositeQueryOperatorsConfig: Array<{
|
|||||||
traceValue: 'Contains',
|
traceValue: 'Contains',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'ncontains',
|
label: 'not contains',
|
||||||
metricValue: '!~',
|
metricValue: '!~',
|
||||||
traceValue: 'NotContains',
|
traceValue: 'NotContains',
|
||||||
},
|
},
|
||||||
|
|||||||
@ -134,7 +134,7 @@ function AllErrors(): JSX.Element {
|
|||||||
exceptionType: getUpdatedExceptionType,
|
exceptionType: getUpdatedExceptionType,
|
||||||
serviceName: getUpdatedServiceName,
|
serviceName: getUpdatedServiceName,
|
||||||
tags: convertCompositeQueryToTraceSelectedTags(
|
tags: convertCompositeQueryToTraceSelectedTags(
|
||||||
compositeData?.builder.queryData?.[0]?.filters.items,
|
compositeData?.builder.queryData?.[0]?.filters?.items || [],
|
||||||
),
|
),
|
||||||
}),
|
}),
|
||||||
enabled: !loading,
|
enabled: !loading,
|
||||||
@ -155,7 +155,7 @@ function AllErrors(): JSX.Element {
|
|||||||
exceptionType: getUpdatedExceptionType,
|
exceptionType: getUpdatedExceptionType,
|
||||||
serviceName: getUpdatedServiceName,
|
serviceName: getUpdatedServiceName,
|
||||||
tags: convertCompositeQueryToTraceSelectedTags(
|
tags: convertCompositeQueryToTraceSelectedTags(
|
||||||
compositeData?.builder.queryData?.[0]?.filters.items,
|
compositeData?.builder.queryData?.[0]?.filters?.items || [],
|
||||||
),
|
),
|
||||||
}),
|
}),
|
||||||
enabled: !loading,
|
enabled: !loading,
|
||||||
@ -461,10 +461,11 @@ function AllErrors(): JSX.Element {
|
|||||||
logEvent('Exception: List page visited', {
|
logEvent('Exception: List page visited', {
|
||||||
numberOfExceptions: errorCountResponse?.data?.payload,
|
numberOfExceptions: errorCountResponse?.data?.payload,
|
||||||
selectedEnvironments,
|
selectedEnvironments,
|
||||||
resourceAttributeUsed: !!compositeData?.builder.queryData?.[0]?.filters
|
resourceAttributeUsed: !!(
|
||||||
.items?.length,
|
compositeData?.builder.queryData?.[0]?.filters?.items?.length || 0
|
||||||
|
),
|
||||||
tags: convertCompositeQueryToTraceSelectedTags(
|
tags: convertCompositeQueryToTraceSelectedTags(
|
||||||
compositeData?.builder.queryData?.[0]?.filters.items,
|
compositeData?.builder.queryData?.[0]?.filters?.items || [],
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@ -101,14 +101,14 @@ describe('API Monitoring Utils', () => {
|
|||||||
|
|
||||||
// Check that each query includes the domainName filter
|
// Check that each query includes the domainName filter
|
||||||
result.query.builder.queryData.forEach((query) => {
|
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,
|
(item) => item.key && item.key.key === SPAN_ATTRIBUTES.SERVER_NAME,
|
||||||
);
|
);
|
||||||
expect(serverNameFilter).toBeDefined();
|
expect(serverNameFilter).toBeDefined();
|
||||||
expect(serverNameFilter?.value).toBe(domainName);
|
expect(serverNameFilter?.value).toBe(domainName);
|
||||||
|
|
||||||
// Check that the custom filters were included
|
// Check that the custom filters were included
|
||||||
const testFilter = query.filters.items.find(
|
const testFilter = query.filters?.items?.find(
|
||||||
(item) => item.id === 'test-filter',
|
(item) => item.id === 'test-filter',
|
||||||
);
|
);
|
||||||
expect(testFilter).toBeDefined();
|
expect(testFilter).toBeDefined();
|
||||||
@ -210,13 +210,13 @@ describe('API Monitoring Utils', () => {
|
|||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
expect(result).toBeDefined();
|
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
|
// 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
|
// Verify each filter matches the corresponding groupBy
|
||||||
expect(
|
expect(
|
||||||
result.items.some(
|
result?.items?.some(
|
||||||
(item) =>
|
(item) =>
|
||||||
item.key &&
|
item.key &&
|
||||||
item.key.key === 'http.method' &&
|
item.key.key === 'http.method' &&
|
||||||
@ -226,7 +226,7 @@ describe('API Monitoring Utils', () => {
|
|||||||
).toBe(true);
|
).toBe(true);
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
result.items.some(
|
result?.items?.some(
|
||||||
(item) =>
|
(item) =>
|
||||||
item.key &&
|
item.key &&
|
||||||
item.key.key === 'http.status_code' &&
|
item.key.key === 'http.status_code' &&
|
||||||
@ -236,7 +236,7 @@ describe('API Monitoring Utils', () => {
|
|||||||
).toBe(true);
|
).toBe(true);
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
result.items.some(
|
result?.items?.some(
|
||||||
(item) =>
|
(item) =>
|
||||||
item.key &&
|
item.key &&
|
||||||
item.key.key === 'service.name' &&
|
item.key.key === 'service.name' &&
|
||||||
@ -272,10 +272,10 @@ describe('API Monitoring Utils', () => {
|
|||||||
// Assert
|
// Assert
|
||||||
expect(result).toBeDefined();
|
expect(result).toBeDefined();
|
||||||
// The implementation includes all keys from rowData, not just those in groupBy
|
// 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
|
// 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',
|
(item) => item.key && item.key.key === 'http.method',
|
||||||
);
|
);
|
||||||
expect(knownField).toBeDefined();
|
expect(knownField).toBeDefined();
|
||||||
@ -285,7 +285,7 @@ describe('API Monitoring Utils', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Should include the unknown field
|
// Should include the unknown field
|
||||||
const unknownField = result.items.find(
|
const unknownField = result?.items?.find(
|
||||||
(item) => item.key && item.key.key === 'unknown.field',
|
(item) => item.key && item.key.key === 'unknown.field',
|
||||||
);
|
);
|
||||||
expect(unknownField).toBeDefined();
|
expect(unknownField).toBeDefined();
|
||||||
@ -304,8 +304,8 @@ describe('API Monitoring Utils', () => {
|
|||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
expect(result).toBeDefined();
|
expect(result).toBeDefined();
|
||||||
expect(result.op).toBe('AND');
|
expect(result?.op).toBe('AND');
|
||||||
expect(result.items).toHaveLength(0);
|
expect(result?.items).toHaveLength(0);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -386,11 +386,11 @@ describe('API Monitoring Utils', () => {
|
|||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
expect(result).toBeDefined();
|
expect(result).toBeDefined();
|
||||||
expect(result.op).toBe('AND');
|
expect(result?.op).toBe('AND');
|
||||||
expect(result.items.length).toBeGreaterThanOrEqual(3);
|
expect(result?.items?.length).toBeGreaterThanOrEqual(3);
|
||||||
|
|
||||||
// Check domain filter
|
// Check domain filter
|
||||||
const domainFilter = result.items.find(
|
const domainFilter = result?.items?.find(
|
||||||
(item) =>
|
(item) =>
|
||||||
item.key &&
|
item.key &&
|
||||||
item.key.key === SPAN_ATTRIBUTES.SERVER_NAME &&
|
item.key.key === SPAN_ATTRIBUTES.SERVER_NAME &&
|
||||||
@ -399,7 +399,7 @@ describe('API Monitoring Utils', () => {
|
|||||||
expect(domainFilter).toBeDefined();
|
expect(domainFilter).toBeDefined();
|
||||||
|
|
||||||
// Check endpoint filter
|
// Check endpoint filter
|
||||||
const endpointFilter = result.items.find(
|
const endpointFilter = result?.items?.find(
|
||||||
(item) =>
|
(item) =>
|
||||||
item.key &&
|
item.key &&
|
||||||
item.key.key === SPAN_ATTRIBUTES.URL_PATH &&
|
item.key.key === SPAN_ATTRIBUTES.URL_PATH &&
|
||||||
@ -408,7 +408,7 @@ describe('API Monitoring Utils', () => {
|
|||||||
expect(endpointFilter).toBeDefined();
|
expect(endpointFilter).toBeDefined();
|
||||||
|
|
||||||
// Check status code filter
|
// Check status code filter
|
||||||
const statusFilter = result.items.find(
|
const statusFilter = result?.items?.find(
|
||||||
(item) =>
|
(item) =>
|
||||||
item.key &&
|
item.key &&
|
||||||
item.key.key === SPAN_ATTRIBUTES.RESPONSE_STATUS_CODE &&
|
item.key.key === SPAN_ATTRIBUTES.RESPONSE_STATUS_CODE &&
|
||||||
@ -469,7 +469,7 @@ describe('API Monitoring Utils', () => {
|
|||||||
expect(queryData.filters).toBeDefined();
|
expect(queryData.filters).toBeDefined();
|
||||||
|
|
||||||
// Check for domain filter
|
// Check for domain filter
|
||||||
const domainFilter = queryData.filters.items.find(
|
const domainFilter = queryData.filters?.items?.find(
|
||||||
// eslint-disable-next-line sonarjs/no-identical-functions
|
// eslint-disable-next-line sonarjs/no-identical-functions
|
||||||
(item) =>
|
(item) =>
|
||||||
item.key &&
|
item.key &&
|
||||||
@ -479,7 +479,7 @@ describe('API Monitoring Utils', () => {
|
|||||||
expect(domainFilter).toBeDefined();
|
expect(domainFilter).toBeDefined();
|
||||||
|
|
||||||
// Check that custom filters were included
|
// Check that custom filters were included
|
||||||
const testFilter = queryData.filters.items.find(
|
const testFilter = queryData.filters?.items?.find(
|
||||||
(item) => item.id === 'test-filter',
|
(item) => item.id === 'test-filter',
|
||||||
);
|
);
|
||||||
expect(testFilter).toBeDefined();
|
expect(testFilter).toBeDefined();
|
||||||
@ -583,7 +583,7 @@ describe('API Monitoring Utils', () => {
|
|||||||
} = query;
|
} = query;
|
||||||
queryData.forEach((qd) => {
|
queryData.forEach((qd) => {
|
||||||
if (qd.filters && qd.filters.items) {
|
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,
|
(item) => item.key && item.key.key === SPAN_ATTRIBUTES.SERVER_NAME,
|
||||||
);
|
);
|
||||||
expect(serverNameFilter).toBeDefined();
|
expect(serverNameFilter).toBeDefined();
|
||||||
@ -595,7 +595,7 @@ describe('API Monitoring Utils', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Should include our custom filter
|
// Should include our custom filter
|
||||||
const customFilter = qd.filters.items.find(
|
const customFilter = qd.filters?.items?.find(
|
||||||
(item) => item.id === 'test-filter',
|
(item) => item.id === 'test-filter',
|
||||||
);
|
);
|
||||||
expect(customFilter).toBeDefined();
|
expect(customFilter).toBeDefined();
|
||||||
@ -631,7 +631,7 @@ describe('API Monitoring Utils', () => {
|
|||||||
const queryData = result.query.builder.queryData[0];
|
const queryData = result.query.builder.queryData[0];
|
||||||
|
|
||||||
// Should have domain filter
|
// 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,
|
(item) => item.key && item.key.key === SPAN_ATTRIBUTES.SERVER_NAME,
|
||||||
);
|
);
|
||||||
expect(domainFilter).toBeDefined();
|
expect(domainFilter).toBeDefined();
|
||||||
@ -698,7 +698,7 @@ describe('API Monitoring Utils', () => {
|
|||||||
const queryData = result.query.builder.queryData[0];
|
const queryData = result.query.builder.queryData[0];
|
||||||
|
|
||||||
// Should have domain filter
|
// 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,
|
(item) => item.key && item.key.key === SPAN_ATTRIBUTES.SERVER_NAME,
|
||||||
);
|
);
|
||||||
expect(domainFilter).toBeDefined();
|
expect(domainFilter).toBeDefined();
|
||||||
@ -1375,7 +1375,7 @@ describe('API Monitoring Utils', () => {
|
|||||||
const queryData = result.query.builder.queryData[0];
|
const queryData = result.query.builder.queryData[0];
|
||||||
|
|
||||||
// Should have domain filter
|
// 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,
|
(item) => item.key && item.key.key === SPAN_ATTRIBUTES.SERVER_NAME,
|
||||||
);
|
);
|
||||||
expect(domainFilter).toBeDefined();
|
expect(domainFilter).toBeDefined();
|
||||||
@ -1384,7 +1384,7 @@ describe('API Monitoring Utils', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Should have endpoint filter if provided
|
// 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,
|
(item) => item.key && item.key.key === SPAN_ATTRIBUTES.URL_PATH,
|
||||||
);
|
);
|
||||||
expect(endpointFilter).toBeDefined();
|
expect(endpointFilter).toBeDefined();
|
||||||
@ -1422,7 +1422,7 @@ describe('API Monitoring Utils', () => {
|
|||||||
const queryData = result.query.builder.queryData[0];
|
const queryData = result.query.builder.queryData[0];
|
||||||
|
|
||||||
// Should include our custom filter
|
// Should include our custom filter
|
||||||
const includedFilter = queryData.filters.items.find(
|
const includedFilter = queryData.filters?.items?.find(
|
||||||
(item) => item.id === 'custom-filter',
|
(item) => item.id === 'custom-filter',
|
||||||
);
|
);
|
||||||
expect(includedFilter).toBeDefined();
|
expect(includedFilter).toBeDefined();
|
||||||
|
|||||||
@ -64,7 +64,7 @@ function AllEndPoints({
|
|||||||
return params.allEndpointsLocalFilters;
|
return params.allEndpointsLocalFilters;
|
||||||
}
|
}
|
||||||
// Initialize filters based on the initial endPointName prop
|
// Initialize filters based on the initial endPointName prop
|
||||||
const initialItems = [...initialFilters.items];
|
const initialItems = [...(initialFilters?.items || [])];
|
||||||
return { op: 'AND', items: initialItems };
|
return { op: 'AND', items: initialItems };
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -204,8 +204,8 @@ function AllEndPoints({
|
|||||||
setSelectedEndPointName(props[SPAN_ATTRIBUTES.URL_PATH] as string);
|
setSelectedEndPointName(props[SPAN_ATTRIBUTES.URL_PATH] as string);
|
||||||
setSelectedView(VIEWS.ENDPOINT_STATS);
|
setSelectedView(VIEWS.ENDPOINT_STATS);
|
||||||
const initialItems = [
|
const initialItems = [
|
||||||
...filters.items,
|
...(filters?.items || []),
|
||||||
...getGroupByFiltersFromGroupByValues(props, groupBy).items,
|
...(getGroupByFiltersFromGroupByValues(props, groupBy)?.items || []),
|
||||||
];
|
];
|
||||||
setInitialFiltersEndPointStats({
|
setInitialFiltersEndPointStats({
|
||||||
items: initialItems,
|
items: initialItems,
|
||||||
|
|||||||
@ -68,8 +68,8 @@ function EndPointDetails({
|
|||||||
const [filters, setFilters] = useState<IBuilderQuery['filters']>(() => {
|
const [filters, setFilters] = useState<IBuilderQuery['filters']>(() => {
|
||||||
// Initialize filters based on the initial endPointName prop
|
// Initialize filters based on the initial endPointName prop
|
||||||
const initialItems = params.endPointDetailsLocalFilters
|
const initialItems = params.endPointDetailsLocalFilters
|
||||||
? [...params.endPointDetailsLocalFilters.items]
|
? [...(params.endPointDetailsLocalFilters?.items || [])]
|
||||||
: [...initialFilters.items];
|
: [...(initialFilters?.items || [])];
|
||||||
if (endPointName) {
|
if (endPointName) {
|
||||||
initialItems.push({
|
initialItems.push({
|
||||||
id: '92b8a1c1',
|
id: '92b8a1c1',
|
||||||
@ -84,7 +84,7 @@ function EndPointDetails({
|
|||||||
// Effect to synchronize local filters when the endPointName prop changes (e.g., from dropdown)
|
// Effect to synchronize local filters when the endPointName prop changes (e.g., from dropdown)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setFilters((currentFilters) => {
|
setFilters((currentFilters) => {
|
||||||
const existingHttpUrlFilter = currentFilters.items.find(
|
const existingHttpUrlFilter = currentFilters?.items?.find(
|
||||||
(item) => item.key?.key === httpUrlKey.key,
|
(item) => item.key?.key === httpUrlKey.key,
|
||||||
);
|
);
|
||||||
const existingHttpUrlValue = (existingHttpUrlFilter?.value as string) || '';
|
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
|
// 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,
|
(item) => item.key?.key !== httpUrlKey.key,
|
||||||
);
|
);
|
||||||
const newItems = [...otherFilters];
|
const newItems = [...(otherFilters || [])];
|
||||||
if (endPointName) {
|
if (endPointName) {
|
||||||
newItems.push({
|
newItems.push({
|
||||||
id: '92b8a1c1',
|
id: '92b8a1c1',
|
||||||
@ -115,7 +115,8 @@ function EndPointDetails({
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const filtersWithoutHttpUrl = {
|
const filtersWithoutHttpUrl = {
|
||||||
op: 'AND',
|
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 });
|
setParams({ endPointDetailsLocalFilters: filtersWithoutHttpUrl });
|
||||||
}, [filters, setParams]);
|
}, [filters, setParams]);
|
||||||
@ -128,12 +129,14 @@ function EndPointDetails({
|
|||||||
// Filter out http.url filter before saving to params
|
// Filter out http.url filter before saving to params
|
||||||
const filteredNewFilters = {
|
const filteredNewFilters = {
|
||||||
op: 'AND',
|
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 });
|
setParams({ endPointDetailsLocalFilters: filteredNewFilters });
|
||||||
|
|
||||||
// 2. Derive the endpoint name from the *new* filters state
|
// 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,
|
(item) => item.key?.key === httpUrlKey.key,
|
||||||
);
|
);
|
||||||
const derivedEndPointName = (httpUrlFilter?.value as string) || '';
|
const derivedEndPointName = (httpUrlFilter?.value as string) || '';
|
||||||
@ -168,7 +171,7 @@ function EndPointDetails({
|
|||||||
const query = updatedCurrentQuery?.builder?.queryData[0] || null;
|
const query = updatedCurrentQuery?.builder?.queryData[0] || null;
|
||||||
|
|
||||||
const isServicesFilterApplied = useMemo(
|
const isServicesFilterApplied = useMemo(
|
||||||
() => filters.items.some((item) => item.key?.key === 'service.name'),
|
() => filters?.items?.some((item) => item.key?.key === 'service.name'),
|
||||||
[filters],
|
[filters],
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -182,7 +185,7 @@ function EndPointDetails({
|
|||||||
queryKey: [
|
queryKey: [
|
||||||
END_POINT_DETAILS_QUERY_KEYS_ARRAY[index],
|
END_POINT_DETAILS_QUERY_KEYS_ARRAY[index],
|
||||||
payload,
|
payload,
|
||||||
filters.items, // Include filters.items in queryKey for better caching
|
filters?.items, // Include filters.items in queryKey for better caching
|
||||||
ENTITY_VERSION_V4,
|
ENTITY_VERSION_V4,
|
||||||
],
|
],
|
||||||
queryFn: (): Promise<SuccessResponse<MetricRangePayloadProps>> =>
|
queryFn: (): Promise<SuccessResponse<MetricRangePayloadProps>> =>
|
||||||
|
|||||||
@ -66,9 +66,9 @@ function TopErrors({
|
|||||||
op: '=',
|
op: '=',
|
||||||
value: endPointName,
|
value: endPointName,
|
||||||
},
|
},
|
||||||
...initialFilters.items,
|
...(initialFilters?.items || []),
|
||||||
]
|
]
|
||||||
: [...initialFilters.items],
|
: [...(initialFilters?.items || [])],
|
||||||
op: 'AND',
|
op: 'AND',
|
||||||
},
|
},
|
||||||
showStatusCodeErrors,
|
showStatusCodeErrors,
|
||||||
@ -236,7 +236,7 @@ function TopErrors({
|
|||||||
record.statusCode,
|
record.statusCode,
|
||||||
);
|
);
|
||||||
navigateToExplorer({
|
navigateToExplorer({
|
||||||
filters: [...filters.items],
|
filters: [...(filters?.items || [])],
|
||||||
dataSource: DataSource.TRACES,
|
dataSource: DataSource.TRACES,
|
||||||
startTime: minTime,
|
startTime: minTime,
|
||||||
endTime: maxTime,
|
endTime: maxTime,
|
||||||
|
|||||||
@ -63,7 +63,7 @@ function ExpandedRow({
|
|||||||
(queryData) => ({
|
(queryData) => ({
|
||||||
...queryData,
|
...queryData,
|
||||||
filters: {
|
filters: {
|
||||||
items: [...(queryData.filters?.items || []), ...filters.items],
|
items: [...(queryData.filters?.items || []), ...(filters?.items || [])],
|
||||||
op: 'AND',
|
op: 'AND',
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
|||||||
@ -14,6 +14,7 @@ import {
|
|||||||
import { handleGraphClick } from 'container/GridCardLayout/GridCard/utils';
|
import { handleGraphClick } from 'container/GridCardLayout/GridCard/utils';
|
||||||
import { useGraphClickToShowButton } from 'container/GridCardLayout/useGraphClickToShowButton';
|
import { useGraphClickToShowButton } from 'container/GridCardLayout/useGraphClickToShowButton';
|
||||||
import useNavigateToExplorerPages from 'container/GridCardLayout/useNavigateToExplorerPages';
|
import useNavigateToExplorerPages from 'container/GridCardLayout/useNavigateToExplorerPages';
|
||||||
|
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||||
import { useResizeObserver } from 'hooks/useDimensions';
|
import { useResizeObserver } from 'hooks/useDimensions';
|
||||||
import { useNotifications } from 'hooks/useNotifications';
|
import { useNotifications } from 'hooks/useNotifications';
|
||||||
@ -112,6 +113,7 @@ function StatusCodeBarCharts({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const navigateToExplorer = useNavigateToExplorer();
|
const navigateToExplorer = useNavigateToExplorer();
|
||||||
|
const { currentQuery } = useQueryBuilder();
|
||||||
|
|
||||||
const navigateToExplorerPages = useNavigateToExplorerPages();
|
const navigateToExplorerPages = useNavigateToExplorerPages();
|
||||||
const { notifications } = useNotifications();
|
const { notifications } = useNotifications();
|
||||||
@ -136,8 +138,8 @@ function StatusCodeBarCharts({
|
|||||||
const widget = useMemo<Widgets>(
|
const widget = useMemo<Widgets>(
|
||||||
() =>
|
() =>
|
||||||
getStatusCodeBarChartWidgetData(domainName, endPointName, {
|
getStatusCodeBarChartWidgetData(domainName, endPointName, {
|
||||||
items: [...filters.items],
|
items: [...(filters?.items || [])],
|
||||||
op: filters.op,
|
op: filters?.op || 'AND',
|
||||||
}),
|
}),
|
||||||
[domainName, endPointName, filters],
|
[domainName, endPointName, filters],
|
||||||
);
|
);
|
||||||
@ -204,6 +206,7 @@ function StatusCodeBarCharts({
|
|||||||
customSeries: getCustomSeries,
|
customSeries: getCustomSeries,
|
||||||
onDragSelect,
|
onDragSelect,
|
||||||
colorMapping,
|
colorMapping,
|
||||||
|
query: currentQuery,
|
||||||
}),
|
}),
|
||||||
[
|
[
|
||||||
minTime,
|
minTime,
|
||||||
@ -217,6 +220,7 @@ function StatusCodeBarCharts({
|
|||||||
getCustomSeries,
|
getCustomSeries,
|
||||||
onDragSelect,
|
onDragSelect,
|
||||||
colorMapping,
|
colorMapping,
|
||||||
|
currentQuery,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@ -21,6 +21,7 @@ import { useQuery } from 'react-query';
|
|||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
import { AppState } from 'store/reducers';
|
import { AppState } from 'store/reducers';
|
||||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||||
|
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||||
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
||||||
import { DataSource } from 'types/common/queryBuilder';
|
import { DataSource } from 'types/common/queryBuilder';
|
||||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||||
@ -54,6 +55,7 @@ function DomainList(): JSX.Element {
|
|||||||
|
|
||||||
// initialise tab with default query.
|
// initialise tab with default query.
|
||||||
useShareBuilderUrl({
|
useShareBuilderUrl({
|
||||||
|
defaultValue: {
|
||||||
...initialQueriesMap.traces,
|
...initialQueriesMap.traces,
|
||||||
builder: {
|
builder: {
|
||||||
...initialQueriesMap.traces.builder,
|
...initialQueriesMap.traces.builder,
|
||||||
@ -63,11 +65,13 @@ function DomainList(): JSX.Element {
|
|||||||
dataSource: DataSource.TRACES,
|
dataSource: DataSource.TRACES,
|
||||||
aggregateOperator: 'noop',
|
aggregateOperator: 'noop',
|
||||||
aggregateAttribute: {
|
aggregateAttribute: {
|
||||||
...initialQueriesMap.traces.builder.queryData[0].aggregateAttribute,
|
...(initialQueriesMap.traces.builder.queryData[0]
|
||||||
|
.aggregateAttribute as BaseAutocompleteData),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const compositeData = useGetCompositeQueryParam();
|
const compositeData = useGetCompositeQueryParam();
|
||||||
@ -101,7 +105,7 @@ function DomainList(): JSX.Element {
|
|||||||
op: '=',
|
op: '=',
|
||||||
value: 'Client',
|
value: 'Client',
|
||||||
},
|
},
|
||||||
...(compositeData?.builder?.queryData[0]?.filters.items || []),
|
...(compositeData?.builder?.queryData[0]?.filters?.items || []),
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@ -374,7 +374,7 @@ export const getDomainMetricsQueryPayload = (
|
|||||||
op: '=',
|
op: '=',
|
||||||
value: domainName,
|
value: domainName,
|
||||||
},
|
},
|
||||||
...filters.items,
|
...(filters?.items || []),
|
||||||
],
|
],
|
||||||
op: 'AND',
|
op: 'AND',
|
||||||
},
|
},
|
||||||
@ -416,7 +416,7 @@ export const getDomainMetricsQueryPayload = (
|
|||||||
op: '=',
|
op: '=',
|
||||||
value: domainName,
|
value: domainName,
|
||||||
},
|
},
|
||||||
...filters.items,
|
...(filters?.items || []),
|
||||||
],
|
],
|
||||||
op: 'AND',
|
op: 'AND',
|
||||||
},
|
},
|
||||||
@ -470,7 +470,7 @@ export const getDomainMetricsQueryPayload = (
|
|||||||
op: '=',
|
op: '=',
|
||||||
value: 'true',
|
value: 'true',
|
||||||
},
|
},
|
||||||
...filters.items,
|
...(filters?.items || []),
|
||||||
],
|
],
|
||||||
op: 'AND',
|
op: 'AND',
|
||||||
},
|
},
|
||||||
@ -512,7 +512,7 @@ export const getDomainMetricsQueryPayload = (
|
|||||||
op: '=',
|
op: '=',
|
||||||
value: domainName,
|
value: domainName,
|
||||||
},
|
},
|
||||||
...filters.items,
|
...(filters?.items || []),
|
||||||
],
|
],
|
||||||
op: 'AND',
|
op: 'AND',
|
||||||
},
|
},
|
||||||
@ -993,7 +993,7 @@ export const getTopErrorsQueryPayload = (
|
|||||||
op: '=',
|
op: '=',
|
||||||
value: true,
|
value: true,
|
||||||
},
|
},
|
||||||
...filters.items,
|
...(filters?.items || []),
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
expression: 'A',
|
expression: 'A',
|
||||||
@ -1586,7 +1586,7 @@ export const getEndPointDetailsQueryPayload = (
|
|||||||
op: '=',
|
op: '=',
|
||||||
value: 'Client',
|
value: 'Client',
|
||||||
},
|
},
|
||||||
...filters.items,
|
...(filters?.items || []),
|
||||||
],
|
],
|
||||||
op: 'AND',
|
op: 'AND',
|
||||||
},
|
},
|
||||||
@ -1640,7 +1640,7 @@ export const getEndPointDetailsQueryPayload = (
|
|||||||
op: '=',
|
op: '=',
|
||||||
value: 'Client',
|
value: 'Client',
|
||||||
},
|
},
|
||||||
...filters.items,
|
...(filters?.items || []),
|
||||||
],
|
],
|
||||||
op: 'AND',
|
op: 'AND',
|
||||||
},
|
},
|
||||||
@ -1706,7 +1706,7 @@ export const getEndPointDetailsQueryPayload = (
|
|||||||
op: '=',
|
op: '=',
|
||||||
value: 'Client',
|
value: 'Client',
|
||||||
},
|
},
|
||||||
...filters.items,
|
...(filters?.items || []),
|
||||||
],
|
],
|
||||||
op: 'AND',
|
op: 'AND',
|
||||||
},
|
},
|
||||||
@ -1760,7 +1760,7 @@ export const getEndPointDetailsQueryPayload = (
|
|||||||
op: '=',
|
op: '=',
|
||||||
value: 'Client',
|
value: 'Client',
|
||||||
},
|
},
|
||||||
...filters.items,
|
...(filters?.items || []),
|
||||||
],
|
],
|
||||||
op: 'AND',
|
op: 'AND',
|
||||||
},
|
},
|
||||||
@ -1814,7 +1814,7 @@ export const getEndPointDetailsQueryPayload = (
|
|||||||
op: '=',
|
op: '=',
|
||||||
value: 'Client',
|
value: 'Client',
|
||||||
},
|
},
|
||||||
...filters.items,
|
...(filters?.items || []),
|
||||||
],
|
],
|
||||||
op: 'AND',
|
op: 'AND',
|
||||||
},
|
},
|
||||||
@ -1909,7 +1909,7 @@ export const getEndPointDetailsQueryPayload = (
|
|||||||
op: '=',
|
op: '=',
|
||||||
value: 'Client',
|
value: 'Client',
|
||||||
},
|
},
|
||||||
...filters.items,
|
...(filters?.items || []),
|
||||||
],
|
],
|
||||||
op: 'AND',
|
op: 'AND',
|
||||||
},
|
},
|
||||||
@ -1971,7 +1971,7 @@ export const getEndPointDetailsQueryPayload = (
|
|||||||
op: '=',
|
op: '=',
|
||||||
value: 'Client',
|
value: 'Client',
|
||||||
},
|
},
|
||||||
...filters.items,
|
...(filters?.items || []),
|
||||||
],
|
],
|
||||||
op: 'AND',
|
op: 'AND',
|
||||||
},
|
},
|
||||||
@ -2036,7 +2036,7 @@ export const getEndPointDetailsQueryPayload = (
|
|||||||
op: '=',
|
op: '=',
|
||||||
value: 'Client',
|
value: 'Client',
|
||||||
},
|
},
|
||||||
...filters.items,
|
...(filters?.items || []),
|
||||||
],
|
],
|
||||||
op: 'AND',
|
op: 'AND',
|
||||||
},
|
},
|
||||||
@ -2132,7 +2132,7 @@ export const getEndPointDetailsQueryPayload = (
|
|||||||
op: '=',
|
op: '=',
|
||||||
value: 'Client',
|
value: 'Client',
|
||||||
},
|
},
|
||||||
...filters.items,
|
...(filters?.items || []),
|
||||||
],
|
],
|
||||||
op: 'AND',
|
op: 'AND',
|
||||||
},
|
},
|
||||||
@ -2226,7 +2226,7 @@ export const getEndPointDetailsQueryPayload = (
|
|||||||
op: '=',
|
op: '=',
|
||||||
value: 'Client',
|
value: 'Client',
|
||||||
},
|
},
|
||||||
...filters.items,
|
...(filters?.items || []),
|
||||||
],
|
],
|
||||||
op: 'AND',
|
op: 'AND',
|
||||||
},
|
},
|
||||||
@ -2289,7 +2289,7 @@ export const getEndPointDetailsQueryPayload = (
|
|||||||
op: '=',
|
op: '=',
|
||||||
value: 'Client',
|
value: 'Client',
|
||||||
},
|
},
|
||||||
...filters.items,
|
...(filters?.items || []),
|
||||||
],
|
],
|
||||||
op: 'AND',
|
op: 'AND',
|
||||||
},
|
},
|
||||||
@ -2352,7 +2352,7 @@ export const getEndPointDetailsQueryPayload = (
|
|||||||
op: '=',
|
op: '=',
|
||||||
value: 'Client',
|
value: 'Client',
|
||||||
},
|
},
|
||||||
...filters.items,
|
...(filters?.items || []),
|
||||||
],
|
],
|
||||||
op: 'AND',
|
op: 'AND',
|
||||||
},
|
},
|
||||||
@ -2427,7 +2427,7 @@ export const getEndPointDetailsQueryPayload = (
|
|||||||
op: '=',
|
op: '=',
|
||||||
value: 'Client',
|
value: 'Client',
|
||||||
},
|
},
|
||||||
...filters.items,
|
...(filters?.items || []),
|
||||||
],
|
],
|
||||||
op: 'AND',
|
op: 'AND',
|
||||||
},
|
},
|
||||||
@ -2531,7 +2531,7 @@ export const getEndPointDetailsQueryPayload = (
|
|||||||
op: '=',
|
op: '=',
|
||||||
value: 'Client',
|
value: 'Client',
|
||||||
},
|
},
|
||||||
...filters.items,
|
...(filters?.items || []),
|
||||||
],
|
],
|
||||||
op: 'AND',
|
op: 'AND',
|
||||||
},
|
},
|
||||||
@ -2627,7 +2627,7 @@ export const getEndPointDetailsQueryPayload = (
|
|||||||
op: '=',
|
op: '=',
|
||||||
value: 'Client',
|
value: 'Client',
|
||||||
},
|
},
|
||||||
...filters.items,
|
...(filters?.items || []),
|
||||||
],
|
],
|
||||||
op: 'AND',
|
op: 'AND',
|
||||||
},
|
},
|
||||||
@ -3318,7 +3318,7 @@ export const getStatusCodeBarChartWidgetData = (
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
: []),
|
: []),
|
||||||
...filters.items,
|
...(filters?.items || []),
|
||||||
],
|
],
|
||||||
op: 'AND',
|
op: 'AND',
|
||||||
},
|
},
|
||||||
@ -3462,7 +3462,7 @@ export const getAllEndpointsWidgetData = (
|
|||||||
op: '=',
|
op: '=',
|
||||||
value: 'Client',
|
value: 'Client',
|
||||||
},
|
},
|
||||||
...filters.items,
|
...(filters?.items || []),
|
||||||
],
|
],
|
||||||
op: 'AND',
|
op: 'AND',
|
||||||
},
|
},
|
||||||
@ -3518,7 +3518,7 @@ export const getAllEndpointsWidgetData = (
|
|||||||
op: '=',
|
op: '=',
|
||||||
value: 'Client',
|
value: 'Client',
|
||||||
},
|
},
|
||||||
...filters.items,
|
...(filters?.items || []),
|
||||||
],
|
],
|
||||||
op: 'AND',
|
op: 'AND',
|
||||||
},
|
},
|
||||||
@ -3574,7 +3574,7 @@ export const getAllEndpointsWidgetData = (
|
|||||||
op: '=',
|
op: '=',
|
||||||
value: 'Client',
|
value: 'Client',
|
||||||
},
|
},
|
||||||
...filters.items,
|
...(filters?.items || []),
|
||||||
],
|
],
|
||||||
op: 'AND',
|
op: 'AND',
|
||||||
},
|
},
|
||||||
@ -3642,7 +3642,7 @@ export const getAllEndpointsWidgetData = (
|
|||||||
op: '=',
|
op: '=',
|
||||||
value: 'Client',
|
value: 'Client',
|
||||||
},
|
},
|
||||||
...filters.items,
|
...(filters?.items || []),
|
||||||
],
|
],
|
||||||
op: 'AND',
|
op: 'AND',
|
||||||
},
|
},
|
||||||
@ -3832,7 +3832,7 @@ export const getRateOverTimeWidgetData = (
|
|||||||
op: '=',
|
op: '=',
|
||||||
value: domainName,
|
value: domainName,
|
||||||
},
|
},
|
||||||
...filters.items,
|
...(filters?.items || []),
|
||||||
],
|
],
|
||||||
op: 'AND',
|
op: 'AND',
|
||||||
},
|
},
|
||||||
@ -3897,7 +3897,7 @@ export const getLatencyOverTimeWidgetData = (
|
|||||||
op: '=',
|
op: '=',
|
||||||
value: domainName,
|
value: domainName,
|
||||||
},
|
},
|
||||||
...filters.items,
|
...(filters?.items || []),
|
||||||
],
|
],
|
||||||
op: 'AND',
|
op: 'AND',
|
||||||
},
|
},
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import {
|
|||||||
notOfTrailResponse,
|
notOfTrailResponse,
|
||||||
trialConvertedToSubscriptionResponse,
|
trialConvertedToSubscriptionResponse,
|
||||||
} from 'mocks-server/__mockdata__/licenses';
|
} from 'mocks-server/__mockdata__/licenses';
|
||||||
import { act, render, screen, waitFor } from 'tests/test-utils';
|
import { act, render, screen } from 'tests/test-utils';
|
||||||
import { getFormattedDate } from 'utils/timeUtils';
|
import { getFormattedDate } from 'utils/timeUtils';
|
||||||
|
|
||||||
import BillingContainer from './BillingContainer';
|
import BillingContainer from './BillingContainer';
|
||||||
@ -34,6 +34,8 @@ window.ResizeObserver =
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
describe('BillingContainer', () => {
|
describe('BillingContainer', () => {
|
||||||
|
jest.setTimeout(30000);
|
||||||
|
|
||||||
test('Component should render', async () => {
|
test('Component should render', async () => {
|
||||||
render(<BillingContainer />);
|
render(<BillingContainer />);
|
||||||
|
|
||||||
@ -45,7 +47,7 @@ describe('BillingContainer', () => {
|
|||||||
name: /price per unit/i,
|
name: /price per unit/i,
|
||||||
});
|
});
|
||||||
expect(pricePerUnit).toBeInTheDocument();
|
expect(pricePerUnit).toBeInTheDocument();
|
||||||
const cost = screen.getByRole('columnheader', {
|
const cost = await screen.findByRole('columnheader', {
|
||||||
name: /cost \(billing period to date\)/i,
|
name: /cost \(billing period to date\)/i,
|
||||||
});
|
});
|
||||||
expect(cost).toBeInTheDocument();
|
expect(cost).toBeInTheDocument();
|
||||||
@ -58,15 +60,15 @@ describe('BillingContainer', () => {
|
|||||||
const upgradePlanButton = screen.getByTestId('upgrade-plan-button');
|
const upgradePlanButton = screen.getByTestId('upgrade-plan-button');
|
||||||
expect(upgradePlanButton).toBeInTheDocument();
|
expect(upgradePlanButton).toBeInTheDocument();
|
||||||
|
|
||||||
const dollar = screen.getByText(/\$1,278.3/i);
|
const dollar = await screen.findByText(/\$1,278.3/i);
|
||||||
await waitFor(() => expect(dollar).toBeInTheDocument());
|
expect(dollar).toBeInTheDocument();
|
||||||
|
|
||||||
const currentBill = screen.getByText('billing');
|
const currentBill = await screen.findByText('billing');
|
||||||
expect(currentBill).toBeInTheDocument();
|
expect(currentBill).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('OnTrail', async () => {
|
test('OnTrail', async () => {
|
||||||
act(() => {
|
await act(async () => {
|
||||||
render(<BillingContainer />, undefined, undefined, {
|
render(<BillingContainer />, undefined, undefined, {
|
||||||
trialInfo: licensesSuccessResponse.data,
|
trialInfo: licensesSuccessResponse.data,
|
||||||
});
|
});
|
||||||
@ -75,7 +77,7 @@ describe('BillingContainer', () => {
|
|||||||
const freeTrailText = await screen.findByText('Free Trial');
|
const freeTrailText = await screen.findByText('Free Trial');
|
||||||
expect(freeTrailText).toBeInTheDocument();
|
expect(freeTrailText).toBeInTheDocument();
|
||||||
|
|
||||||
const currentBill = screen.getByText('billing');
|
const currentBill = await screen.findByText('billing');
|
||||||
expect(currentBill).toBeInTheDocument();
|
expect(currentBill).toBeInTheDocument();
|
||||||
|
|
||||||
const dollar0 = await screen.findByText(/\$0/i);
|
const dollar0 = await screen.findByText(/\$0/i);
|
||||||
@ -95,18 +97,18 @@ describe('BillingContainer', () => {
|
|||||||
const checkPaidPlan = await screen.findByText(/checkout_plans/i);
|
const checkPaidPlan = await screen.findByText(/checkout_plans/i);
|
||||||
expect(checkPaidPlan).toBeInTheDocument();
|
expect(checkPaidPlan).toBeInTheDocument();
|
||||||
|
|
||||||
const link = screen.getByRole('link', { name: /here/i });
|
const link = await screen.findByRole('link', { name: /here/i });
|
||||||
expect(link).toBeInTheDocument();
|
expect(link).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('OnTrail but trialConvertedToSubscription', async () => {
|
test('OnTrail but trialConvertedToSubscription', async () => {
|
||||||
act(() => {
|
await act(async () => {
|
||||||
render(<BillingContainer />, undefined, undefined, {
|
render(<BillingContainer />, undefined, undefined, {
|
||||||
trialInfo: trialConvertedToSubscriptionResponse.data,
|
trialInfo: trialConvertedToSubscriptionResponse.data,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
const currentBill = screen.getByText('billing');
|
const currentBill = await screen.findByText('billing');
|
||||||
expect(currentBill).toBeInTheDocument();
|
expect(currentBill).toBeInTheDocument();
|
||||||
|
|
||||||
const dollar0 = await screen.findByText(/\$0/i);
|
const dollar0 = await screen.findByText(/\$0/i);
|
||||||
@ -145,7 +147,7 @@ describe('BillingContainer', () => {
|
|||||||
const billingPeriod = await findByText(billingPeriodText);
|
const billingPeriod = await findByText(billingPeriodText);
|
||||||
expect(billingPeriod).toBeInTheDocument();
|
expect(billingPeriod).toBeInTheDocument();
|
||||||
|
|
||||||
const currentBill = screen.getByText('billing');
|
const currentBill = await screen.findByText('billing');
|
||||||
expect(currentBill).toBeInTheDocument();
|
expect(currentBill).toBeInTheDocument();
|
||||||
|
|
||||||
const dollar0 = await screen.findByText(/\$1,278.3/i);
|
const dollar0 = await screen.findByText(/\$1,278.3/i);
|
||||||
|
|||||||
@ -69,7 +69,7 @@ function Download({ data, isLoading, fileName }: DownloadProps): JSX.Element {
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
className="periscope-btn"
|
className="periscope-btn ghost"
|
||||||
loading={isLoading}
|
loading={isLoading}
|
||||||
icon={<FileDown size={14} />}
|
icon={<FileDown size={14} />}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -257,6 +257,10 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.lightMode {
|
.lightMode {
|
||||||
|
.explorer-options-container {
|
||||||
|
background: var(--bg-vanilla-100);
|
||||||
|
}
|
||||||
|
|
||||||
.explorer-options {
|
.explorer-options {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
|
|||||||
@ -15,6 +15,7 @@ import {
|
|||||||
Typography,
|
Typography,
|
||||||
} from 'antd';
|
} from 'antd';
|
||||||
import logEvent from 'api/common/logEvent';
|
import logEvent from 'api/common/logEvent';
|
||||||
|
import { TelemetryFieldKey } from 'api/v5/v5';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import cx from 'classnames';
|
import cx from 'classnames';
|
||||||
import { getViewDetailsUsingViewKey } from 'components/ExplorerCard/utils';
|
import { getViewDetailsUsingViewKey } from 'components/ExplorerCard/utils';
|
||||||
@ -67,7 +68,6 @@ import {
|
|||||||
} from 'react';
|
} from 'react';
|
||||||
import { useHistory } from 'react-router-dom';
|
import { useHistory } from 'react-router-dom';
|
||||||
import { Dashboard } from 'types/api/dashboard/getAll';
|
import { Dashboard } from 'types/api/dashboard/getAll';
|
||||||
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
|
||||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||||
import { ViewProps } from 'types/api/saveViews/types';
|
import { ViewProps } from 'types/api/saveViews/types';
|
||||||
import { DataSource, StringOperators } from 'types/common/queryBuilder';
|
import { DataSource, StringOperators } from 'types/common/queryBuilder';
|
||||||
@ -270,7 +270,7 @@ function ExplorerOptions({
|
|||||||
|
|
||||||
const getUpdatedExtraData = (
|
const getUpdatedExtraData = (
|
||||||
extraData: string | undefined,
|
extraData: string | undefined,
|
||||||
newSelectedColumns: BaseAutocompleteData[],
|
newSelectedColumns: TelemetryFieldKey[],
|
||||||
formattingOptions?: FormattingOptions,
|
formattingOptions?: FormattingOptions,
|
||||||
): string => {
|
): string => {
|
||||||
let updatedExtraData;
|
let updatedExtraData;
|
||||||
@ -354,7 +354,7 @@ function ExplorerOptions({
|
|||||||
const { handleExplorerTabChange } = useHandleExplorerTabChange();
|
const { handleExplorerTabChange } = useHandleExplorerTabChange();
|
||||||
|
|
||||||
type ExtraData = {
|
type ExtraData = {
|
||||||
selectColumns?: BaseAutocompleteData[];
|
selectColumns?: TelemetryFieldKey[];
|
||||||
version?: number;
|
version?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -589,13 +589,6 @@ function ExplorerOptions({
|
|||||||
[isDarkMode],
|
[isDarkMode],
|
||||||
);
|
);
|
||||||
|
|
||||||
const hideToolbar = (): void => {
|
|
||||||
setExplorerToolBarVisibility(false, sourcepage);
|
|
||||||
if (setIsExplorerOptionHidden) {
|
|
||||||
setIsExplorerOptionHidden(true);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const isEditDeleteSupported = allowedRoles.includes(user.role as string);
|
const isEditDeleteSupported = allowedRoles.includes(user.role as string);
|
||||||
|
|
||||||
const [
|
const [
|
||||||
@ -782,6 +775,13 @@ function ExplorerOptions({
|
|||||||
);
|
);
|
||||||
}, [disabled, isOneChartPerQuery, onAddToDashboard, splitedQueries]);
|
}, [disabled, isOneChartPerQuery, onAddToDashboard, splitedQueries]);
|
||||||
|
|
||||||
|
const hideToolbar = (): void => {
|
||||||
|
setExplorerToolBarVisibility(false, sourcepage);
|
||||||
|
if (setIsExplorerOptionHidden) {
|
||||||
|
setIsExplorerOptionHidden(true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="explorer-options-container">
|
<div className="explorer-options-container">
|
||||||
{
|
{
|
||||||
|
|||||||
@ -20,9 +20,9 @@ function ExplorerOrderBy({ query, onChange }: OrderByFilterProps): JSX.Element {
|
|||||||
|
|
||||||
const { data, isFetching } = useGetAggregateKeys(
|
const { data, isFetching } = useGetAggregateKeys(
|
||||||
{
|
{
|
||||||
aggregateAttribute: query.aggregateAttribute.key,
|
aggregateAttribute: query.aggregateAttribute?.key || '',
|
||||||
dataSource: query.dataSource,
|
dataSource: query.dataSource,
|
||||||
aggregateOperator: query.aggregateOperator,
|
aggregateOperator: query.aggregateOperator || '',
|
||||||
searchText: debouncedSearchText,
|
searchText: debouncedSearchText,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { Button, Typography } from 'antd';
|
import { Button, Typography } from 'antd';
|
||||||
import createDashboard from 'api/v1/dashboards/create';
|
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 { useGetAllDashboard } from 'hooks/dashboard/useGetAllDashboard';
|
||||||
import { useErrorModal } from 'providers/ErrorModalProvider';
|
import { useErrorModal } from 'providers/ErrorModalProvider';
|
||||||
import { useCallback, useMemo, useState } from 'react';
|
import { useCallback, useMemo, useState } from 'react';
|
||||||
@ -75,7 +75,7 @@ function ExportPanelContainer({
|
|||||||
ns: 'dashboard',
|
ns: 'dashboard',
|
||||||
}),
|
}),
|
||||||
uploadedGrafana: false,
|
uploadedGrafana: false,
|
||||||
version: ENTITY_VERSION_V4,
|
version: ENTITY_VERSION_V5,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
showErrorModal(error as APIError);
|
showErrorModal(error as APIError);
|
||||||
|
|||||||
@ -2,12 +2,13 @@ import './ChartPreview.styles.scss';
|
|||||||
|
|
||||||
import { InfoCircleOutlined } from '@ant-design/icons';
|
import { InfoCircleOutlined } from '@ant-design/icons';
|
||||||
import Spinner from 'components/Spinner';
|
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 { FeatureKeys } from 'constants/features';
|
||||||
import { QueryParams } from 'constants/query';
|
import { QueryParams } from 'constants/query';
|
||||||
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
|
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
|
||||||
import AnomalyAlertEvaluationView from 'container/AnomalyAlertEvaluationView';
|
import AnomalyAlertEvaluationView from 'container/AnomalyAlertEvaluationView';
|
||||||
import GridPanelSwitch from 'container/GridPanelSwitch';
|
import GridPanelSwitch from 'container/GridPanelSwitch';
|
||||||
|
import { populateMultipleResults } from 'container/NewWidget/LeftContainer/WidgetGraph/util';
|
||||||
import { getFormatNameByOptionId } from 'container/NewWidget/RightContainer/alertFomatCategories';
|
import { getFormatNameByOptionId } from 'container/NewWidget/RightContainer/alertFomatCategories';
|
||||||
import { timePreferenceType } from 'container/NewWidget/RightContainer/timeItems';
|
import { timePreferenceType } from 'container/NewWidget/RightContainer/timeItems';
|
||||||
import { Time } from 'container/TopNav/DateTimeSelection/config';
|
import { Time } from 'container/TopNav/DateTimeSelection/config';
|
||||||
@ -16,6 +17,7 @@ import {
|
|||||||
Time as TimeV2,
|
Time as TimeV2,
|
||||||
} from 'container/TopNav/DateTimeSelectionV2/config';
|
} from 'container/TopNav/DateTimeSelectionV2/config';
|
||||||
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
|
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
|
||||||
|
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||||
import { useResizeObserver } from 'hooks/useDimensions';
|
import { useResizeObserver } from 'hooks/useDimensions';
|
||||||
import useUrlQuery from 'hooks/useUrlQuery';
|
import useUrlQuery from 'hooks/useUrlQuery';
|
||||||
@ -78,6 +80,7 @@ function ChartPreview({
|
|||||||
const threshold = alertDef?.condition.target || 0;
|
const threshold = alertDef?.condition.target || 0;
|
||||||
const [minTimeScale, setMinTimeScale] = useState<number>();
|
const [minTimeScale, setMinTimeScale] = useState<number>();
|
||||||
const [maxTimeScale, setMaxTimeScale] = useState<number>();
|
const [maxTimeScale, setMaxTimeScale] = useState<number>();
|
||||||
|
const { currentQuery } = useQueryBuilder();
|
||||||
|
|
||||||
const { minTime, maxTime, selectedTime: globalSelectedInterval } = useSelector<
|
const { minTime, maxTime, selectedTime: globalSelectedInterval } = useSelector<
|
||||||
AppState,
|
AppState,
|
||||||
@ -144,7 +147,8 @@ function ChartPreview({
|
|||||||
},
|
},
|
||||||
originalGraphType: graphType,
|
originalGraphType: graphType,
|
||||||
},
|
},
|
||||||
alertDef?.version || DEFAULT_ENTITY_VERSION,
|
// alertDef?.version || DEFAULT_ENTITY_VERSION,
|
||||||
|
ENTITY_VERSION_V5,
|
||||||
{
|
{
|
||||||
queryKey: [
|
queryKey: [
|
||||||
'chartPreview',
|
'chartPreview',
|
||||||
@ -154,7 +158,6 @@ function ChartPreview({
|
|||||||
maxTime,
|
maxTime,
|
||||||
alertDef?.ruleType,
|
alertDef?.ruleType,
|
||||||
],
|
],
|
||||||
retry: false,
|
|
||||||
enabled: canQuery,
|
enabled: canQuery,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@ -175,6 +178,12 @@ function ChartPreview({
|
|||||||
queryResponse.data.payload.data.result = sortedSeriesData;
|
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 containerDimensions = useResizeObserver(graphRef);
|
||||||
|
|
||||||
const isDarkMode = useIsDarkMode();
|
const isDarkMode = useIsDarkMode();
|
||||||
@ -246,6 +255,8 @@ function ChartPreview({
|
|||||||
tzDate: (timestamp: number) =>
|
tzDate: (timestamp: number) =>
|
||||||
uPlot.tzDate(new Date(timestamp * 1e3), timezone.value),
|
uPlot.tzDate(new Date(timestamp * 1e3), timezone.value),
|
||||||
timezone: timezone.value,
|
timezone: timezone.value,
|
||||||
|
currentQuery,
|
||||||
|
query: query || currentQuery,
|
||||||
}),
|
}),
|
||||||
[
|
[
|
||||||
yAxisUnit,
|
yAxisUnit,
|
||||||
@ -261,6 +272,8 @@ function ChartPreview({
|
|||||||
alertDef?.condition.targetUnit,
|
alertDef?.condition.targetUnit,
|
||||||
graphType,
|
graphType,
|
||||||
timezone.value,
|
timezone.value,
|
||||||
|
currentQuery,
|
||||||
|
query,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -309,7 +322,7 @@ function ChartPreview({
|
|||||||
{chartDataAvailable &&
|
{chartDataAvailable &&
|
||||||
isAnomalyDetectionAlert &&
|
isAnomalyDetectionAlert &&
|
||||||
isAnomalyDetectionEnabled &&
|
isAnomalyDetectionEnabled &&
|
||||||
queryResponse?.data?.payload?.data?.resultType === 'anomaly' && (
|
queryResponse?.data?.payload?.data?.resultType === 'time_series' && (
|
||||||
<AnomalyAlertEvaluationView
|
<AnomalyAlertEvaluationView
|
||||||
data={queryResponse?.data?.payload}
|
data={queryResponse?.data?.payload}
|
||||||
yAxisUnit={yAxisUnit}
|
yAxisUnit={yAxisUnit}
|
||||||
|
|||||||
@ -1,4 +1,6 @@
|
|||||||
.alert-tabs {
|
.alert-tabs {
|
||||||
|
padding: 0px 8px;
|
||||||
|
|
||||||
.ant-tabs-tab {
|
.ant-tabs-tab {
|
||||||
border: none !important;
|
border: none !important;
|
||||||
margin-left: 0px !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 {
|
.lightMode {
|
||||||
.alert-tabs {
|
.alert-tabs {
|
||||||
.ant-tabs-nav-list {
|
.ant-tabs-nav-list {
|
||||||
|
|||||||
@ -4,11 +4,11 @@ import { Color } from '@signozhq/design-tokens';
|
|||||||
import { Button, Tabs, Tooltip } from 'antd';
|
import { Button, Tabs, Tooltip } from 'antd';
|
||||||
import logEvent from 'api/common/logEvent';
|
import logEvent from 'api/common/logEvent';
|
||||||
import PromQLIcon from 'assets/Dashboard/PromQl';
|
import PromQLIcon from 'assets/Dashboard/PromQl';
|
||||||
|
import { QueryBuilderV2 } from 'components/QueryBuilderV2/QueryBuilderV2';
|
||||||
import { ALERTS_DATA_SOURCE_MAP } from 'constants/alerts';
|
import { ALERTS_DATA_SOURCE_MAP } from 'constants/alerts';
|
||||||
import { ENTITY_VERSION_V4 } from 'constants/app';
|
import { ENTITY_VERSION_V4 } from 'constants/app';
|
||||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||||
import { QBShortcuts } from 'constants/shortcuts/QBShortcuts';
|
import { QBShortcuts } from 'constants/shortcuts/QBShortcuts';
|
||||||
import { QueryBuilder } from 'container/QueryBuilder';
|
|
||||||
import { useKeyboardHotkeys } from 'hooks/hotkeys/useKeyboardHotkeys';
|
import { useKeyboardHotkeys } from 'hooks/hotkeys/useKeyboardHotkeys';
|
||||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||||
import { isEmpty } from 'lodash-es';
|
import { isEmpty } from 'lodash-es';
|
||||||
@ -48,7 +48,7 @@ function QuerySection({
|
|||||||
const isDarkMode = useIsDarkMode();
|
const isDarkMode = useIsDarkMode();
|
||||||
|
|
||||||
const renderMetricUI = (): JSX.Element => (
|
const renderMetricUI = (): JSX.Element => (
|
||||||
<QueryBuilder
|
<QueryBuilderV2
|
||||||
panelType={panelType}
|
panelType={panelType}
|
||||||
config={{
|
config={{
|
||||||
queryVariant: 'static',
|
queryVariant: 'static',
|
||||||
@ -144,7 +144,7 @@ function QuerySection({
|
|||||||
<div className="alert-tabs">
|
<div className="alert-tabs">
|
||||||
<Tabs
|
<Tabs
|
||||||
type="card"
|
type="card"
|
||||||
style={{ width: '100%' }}
|
style={{ width: '100%', padding: '0px 8px' }}
|
||||||
defaultActiveKey={currentTab}
|
defaultActiveKey={currentTab}
|
||||||
activeKey={currentTab}
|
activeKey={currentTab}
|
||||||
onChange={handleQueryCategoryChange}
|
onChange={handleQueryCategoryChange}
|
||||||
@ -178,7 +178,7 @@ function QuerySection({
|
|||||||
<div className="alert-tabs">
|
<div className="alert-tabs">
|
||||||
<Tabs
|
<Tabs
|
||||||
type="card"
|
type="card"
|
||||||
style={{ width: '100%' }}
|
style={{ width: '100%', padding: '0px 8px' }}
|
||||||
defaultActiveKey={currentTab}
|
defaultActiveKey={currentTab}
|
||||||
activeKey={currentTab}
|
activeKey={currentTab}
|
||||||
onChange={handleQueryCategoryChange}
|
onChange={handleQueryCategoryChange}
|
||||||
@ -218,7 +218,7 @@ function QuerySection({
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<StepHeading> {t('alert_form_step2', { step: step2Label })}</StepHeading>
|
<StepHeading> {t('alert_form_step2', { step: step2Label })}</StepHeading>
|
||||||
<FormContainer>
|
<FormContainer className="alert-query-section-container">
|
||||||
<div>{renderTabs(alertType)}</div>
|
<div>{renderTabs(alertType)}</div>
|
||||||
{renderQuerySection(currentTab)}
|
{renderQuerySection(currentTab)}
|
||||||
</FormContainer>
|
</FormContainer>
|
||||||
|
|||||||
@ -45,6 +45,7 @@ import {
|
|||||||
import { EQueryType } from 'types/common/dashboard';
|
import { EQueryType } from 'types/common/dashboard';
|
||||||
import { DataSource } from 'types/common/queryBuilder';
|
import { DataSource } from 'types/common/queryBuilder';
|
||||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||||
|
import { compositeQueryToQueryEnvelope } from 'utils/compositeQueryToQueryEnvelope';
|
||||||
|
|
||||||
import BasicInfo from './BasicInfo';
|
import BasicInfo from './BasicInfo';
|
||||||
import ChartPreview from './ChartPreview';
|
import ChartPreview from './ChartPreview';
|
||||||
@ -166,7 +167,7 @@ function FormAlertRules({
|
|||||||
|
|
||||||
const sq = useMemo(() => mapQueryDataFromApi(initQuery), [initQuery]);
|
const sq = useMemo(() => mapQueryDataFromApi(initQuery), [initQuery]);
|
||||||
|
|
||||||
useShareBuilderUrl(sq);
|
useShareBuilderUrl({ defaultValue: sq });
|
||||||
|
|
||||||
const handleDetectionMethodChange = (value: string): void => {
|
const handleDetectionMethodChange = (value: string): void => {
|
||||||
setAlertDef((def) => ({
|
setAlertDef((def) => ({
|
||||||
@ -272,6 +273,9 @@ function FormAlertRules({
|
|||||||
ruleType,
|
ruleType,
|
||||||
condition: {
|
condition: {
|
||||||
...initialValue.condition,
|
...initialValue.condition,
|
||||||
|
compositeQuery: compositeQueryToQueryEnvelope(
|
||||||
|
initialValue.condition.compositeQuery,
|
||||||
|
),
|
||||||
matchType: initialValue.condition.matchType ?? matchType ?? '',
|
matchType: initialValue.condition.matchType ?? matchType ?? '',
|
||||||
op: initialValue.condition.op ?? op ?? '',
|
op: initialValue.condition.op ?? op ?? '',
|
||||||
target: initialValue.condition.target ?? target ?? 0,
|
target: initialValue.condition.target ?? target ?? 0,
|
||||||
@ -447,7 +451,7 @@ function FormAlertRules({
|
|||||||
: alertDef.ruleType,
|
: alertDef.ruleType,
|
||||||
condition: {
|
condition: {
|
||||||
...alertDef.condition,
|
...alertDef.condition,
|
||||||
compositeQuery: {
|
compositeQuery: compositeQueryToQueryEnvelope({
|
||||||
builderQueries: {
|
builderQueries: {
|
||||||
...mapQueryDataToApi(currentQuery.builder.queryData, 'queryName').data,
|
...mapQueryDataToApi(currentQuery.builder.queryData, 'queryName').data,
|
||||||
...mapQueryDataToApi(currentQuery.builder.queryFormulas, 'queryName')
|
...mapQueryDataToApi(currentQuery.builder.queryFormulas, 'queryName')
|
||||||
@ -458,7 +462,7 @@ function FormAlertRules({
|
|||||||
queryType: currentQuery.queryType,
|
queryType: currentQuery.queryType,
|
||||||
panelType: panelType || initQuery.panelType,
|
panelType: panelType || initQuery.panelType,
|
||||||
unit: currentQuery.unit,
|
unit: currentQuery.unit,
|
||||||
},
|
}),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -848,7 +852,7 @@ function FormAlertRules({
|
|||||||
queryCategory={currentQuery.queryType}
|
queryCategory={currentQuery.queryType}
|
||||||
setQueryCategory={onQueryCategoryChange}
|
setQueryCategory={onQueryCategoryChange}
|
||||||
alertType={alertType || AlertTypes.METRICS_BASED_ALERT}
|
alertType={alertType || AlertTypes.METRICS_BASED_ALERT}
|
||||||
runQuery={(): void => handleRunQuery(true)}
|
runQuery={(): void => handleRunQuery(true, true)}
|
||||||
alertDef={alertDef}
|
alertDef={alertDef}
|
||||||
panelType={panelType || PANEL_TYPES.TIME_SERIES}
|
panelType={panelType || PANEL_TYPES.TIME_SERIES}
|
||||||
key={currentQuery.queryType}
|
key={currentQuery.queryType}
|
||||||
|
|||||||
@ -20,10 +20,10 @@ export const usePrefillAlertConditions = (
|
|||||||
if (!stagedQuery) return null;
|
if (!stagedQuery) return null;
|
||||||
const isSameTimeAggregation = stagedQuery.builder.queryData.every(
|
const isSameTimeAggregation = stagedQuery.builder.queryData.every(
|
||||||
(queryData) =>
|
(queryData) =>
|
||||||
queryData.reduceTo === stagedQuery.builder.queryData[0].reduceTo,
|
queryData?.reduceTo === stagedQuery.builder.queryData[0]?.reduceTo,
|
||||||
);
|
);
|
||||||
return isSameTimeAggregation
|
return isSameTimeAggregation
|
||||||
? stagedQuery.builder.queryData[0].reduceTo
|
? stagedQuery.builder.queryData[0]?.reduceTo
|
||||||
: null;
|
: null;
|
||||||
}, [stagedQuery]);
|
}, [stagedQuery]);
|
||||||
|
|
||||||
|
|||||||
@ -10,9 +10,10 @@ import cx from 'classnames';
|
|||||||
import { ToggleGraphProps } from 'components/Graph/types';
|
import { ToggleGraphProps } from 'components/Graph/types';
|
||||||
import Spinner from 'components/Spinner';
|
import Spinner from 'components/Spinner';
|
||||||
import TimePreference from 'components/TimePreferenceDropDown';
|
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 { QueryParams } from 'constants/query';
|
||||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||||
|
import { populateMultipleResults } from 'container/NewWidget/LeftContainer/WidgetGraph/util';
|
||||||
import {
|
import {
|
||||||
timeItems,
|
timeItems,
|
||||||
timePreferance,
|
timePreferance,
|
||||||
@ -122,7 +123,8 @@ function FullView({
|
|||||||
|
|
||||||
const response = useGetQueryRange(
|
const response = useGetQueryRange(
|
||||||
requestData,
|
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],
|
queryKey: [widget?.query, widget?.panelTypes, requestData, version],
|
||||||
enabled: !isDependedDataLoaded,
|
enabled: !isDependedDataLoaded,
|
||||||
@ -178,6 +180,12 @@ function FullView({
|
|||||||
response.data.payload.data.result = sortedSeriesData;
|
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(() => {
|
useEffect(() => {
|
||||||
graphsVisibilityStates?.forEach((e, i) => {
|
graphsVisibilityStates?.forEach((e, i) => {
|
||||||
fullViewChartRef?.current?.toggleGraph(i, e);
|
fullViewChartRef?.current?.toggleGraph(i, e);
|
||||||
|
|||||||
@ -1,7 +1,8 @@
|
|||||||
import logEvent from 'api/common/logEvent';
|
import logEvent from 'api/common/logEvent';
|
||||||
import { DEFAULT_ENTITY_VERSION } from 'constants/app';
|
import { DEFAULT_ENTITY_VERSION, ENTITY_VERSION_V5 } from 'constants/app';
|
||||||
import { QueryParams } from 'constants/query';
|
import { QueryParams } from 'constants/query';
|
||||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||||
|
import { populateMultipleResults } from 'container/NewWidget/LeftContainer/WidgetGraph/util';
|
||||||
import { CustomTimeType } from 'container/TopNav/DateTimeSelectionV2/config';
|
import { CustomTimeType } from 'container/TopNav/DateTimeSelectionV2/config';
|
||||||
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
|
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
|
||||||
import { useIntersectionObserver } from 'hooks/useIntersectionObserver';
|
import { useIntersectionObserver } from 'hooks/useIntersectionObserver';
|
||||||
@ -16,6 +17,7 @@ import { useQueryClient } from 'react-query';
|
|||||||
import { useDispatch, useSelector } from 'react-redux';
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
import { UpdateTimeInterval } from 'store/actions';
|
import { UpdateTimeInterval } from 'store/actions';
|
||||||
import { AppState } from 'store/reducers';
|
import { AppState } from 'store/reducers';
|
||||||
|
import APIError from 'types/api/error';
|
||||||
import { DataSource } from 'types/common/queryBuilder';
|
import { DataSource } from 'types/common/queryBuilder';
|
||||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||||
import { getGraphType } from 'utils/getGraphType';
|
import { getGraphType } from 'utils/getGraphType';
|
||||||
@ -24,7 +26,7 @@ import { getSortedSeriesData } from 'utils/getSortedSeriesData';
|
|||||||
import EmptyWidget from '../EmptyWidget';
|
import EmptyWidget from '../EmptyWidget';
|
||||||
import { MenuItemKeys } from '../WidgetHeader/contants';
|
import { MenuItemKeys } from '../WidgetHeader/contants';
|
||||||
import { GridCardGraphProps } from './types';
|
import { GridCardGraphProps } from './types';
|
||||||
import { isDataAvailableByPanelType } from './utils';
|
import { errorDetails, isDataAvailableByPanelType } from './utils';
|
||||||
import WidgetGraphComponent from './WidgetGraphComponent';
|
import WidgetGraphComponent from './WidgetGraphComponent';
|
||||||
|
|
||||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||||
@ -136,6 +138,7 @@ function GridCardGraph({
|
|||||||
formatForWeb: widget.panelTypes === PANEL_TYPES.TABLE,
|
formatForWeb: widget.panelTypes === PANEL_TYPES.TABLE,
|
||||||
start: customTimeRange?.startTime || start,
|
start: customTimeRange?.startTime || start,
|
||||||
end: customTimeRange?.endTime || end,
|
end: customTimeRange?.endTime || end,
|
||||||
|
originalGraphType: widget.panelTypes,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
updatedQuery.builder.queryData[0].pageSize = 10;
|
updatedQuery.builder.queryData[0].pageSize = 10;
|
||||||
@ -239,7 +242,12 @@ function GridCardGraph({
|
|||||||
enabled: queryEnabledCondition,
|
enabled: queryEnabledCondition,
|
||||||
refetchOnMount: false,
|
refetchOnMount: false,
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
setErrorMessage(error.message);
|
const errorMessage =
|
||||||
|
version === ENTITY_VERSION_V5
|
||||||
|
? errorDetails(error as APIError)
|
||||||
|
: error.message;
|
||||||
|
|
||||||
|
setErrorMessage(errorMessage);
|
||||||
if (customErrorMessage) {
|
if (customErrorMessage) {
|
||||||
setIsInternalServerError(
|
setIsInternalServerError(
|
||||||
String(error.message).includes('API responded with 500'),
|
String(error.message).includes('API responded with 500'),
|
||||||
@ -259,6 +267,7 @@ function GridCardGraph({
|
|||||||
getGraphData?.(data?.payload?.data);
|
getGraphData?.(data?.payload?.data);
|
||||||
setDashboardQueryRangeCalled(true);
|
setDashboardQueryRangeCalled(true);
|
||||||
},
|
},
|
||||||
|
showErrorModal: false,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -271,6 +280,12 @@ function GridCardGraph({
|
|||||||
queryResponse.data.payload.data.result = sortedSeriesData;
|
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 =
|
const menuList =
|
||||||
widget.panelTypes === PANEL_TYPES.TABLE ||
|
widget.panelTypes === PANEL_TYPES.TABLE ||
|
||||||
widget.panelTypes === PANEL_TYPES.LIST ||
|
widget.panelTypes === PANEL_TYPES.LIST ||
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import { LOCALSTORAGE } from 'constants/localStorage';
|
|||||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||||
import getLabelName from 'lib/getLabelName';
|
import getLabelName from 'lib/getLabelName';
|
||||||
import { Widgets } from 'types/api/dashboard/getAll';
|
import { Widgets } from 'types/api/dashboard/getAll';
|
||||||
|
import APIError from 'types/api/error';
|
||||||
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
|
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
|
||||||
import { TagFilterItem } from 'types/api/queryBuilder/queryBuilderData';
|
import { TagFilterItem } from 'types/api/queryBuilder/queryBuilderData';
|
||||||
import { QueryData } from 'types/api/widgets/getQuery';
|
import { QueryData } from 'types/api/widgets/getQuery';
|
||||||
@ -246,3 +247,14 @@ export const handleGraphClick = async ({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const errorDetails = (error: APIError): string => {
|
||||||
|
const { message, errors } = error.getErrorDetails()?.error || {};
|
||||||
|
|
||||||
|
const details =
|
||||||
|
errors?.length > 0
|
||||||
|
? `\n\nDetails: ${errors.map((e) => e.message).join('\n')}`
|
||||||
|
: '';
|
||||||
|
const errorDetails = `${message} ${details}`;
|
||||||
|
return errorDetails || 'Unknown error occurred';
|
||||||
|
};
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import { Button, Form, Input, Modal, Typography } from 'antd';
|
|||||||
import { useForm } from 'antd/es/form/Form';
|
import { useForm } from 'antd/es/form/Form';
|
||||||
import logEvent from 'api/common/logEvent';
|
import logEvent from 'api/common/logEvent';
|
||||||
import cx from 'classnames';
|
import cx from 'classnames';
|
||||||
|
import { ENTITY_VERSION_V5 } from 'constants/app';
|
||||||
import { QueryParams } from 'constants/query';
|
import { QueryParams } from 'constants/query';
|
||||||
import { PANEL_GROUP_TYPES, PANEL_TYPES } from 'constants/queryBuilder';
|
import { PANEL_GROUP_TYPES, PANEL_TYPES } from 'constants/queryBuilder';
|
||||||
import { themeColors } from 'constants/theme';
|
import { themeColors } from 'constants/theme';
|
||||||
@ -579,7 +580,8 @@ function GraphLayout(props: GraphLayoutProps): JSX.Element {
|
|||||||
widget={(currentWidget as Widgets) || ({ id, query: {} } as Widgets)}
|
widget={(currentWidget as Widgets) || ({ id, query: {} } as Widgets)}
|
||||||
headerMenuList={widgetActions}
|
headerMenuList={widgetActions}
|
||||||
variables={variables}
|
variables={variables}
|
||||||
version={selectedDashboard?.data?.version}
|
// version={selectedDashboard?.data?.version}
|
||||||
|
version={ENTITY_VERSION_V5}
|
||||||
onDragSelect={onDragSelect}
|
onDragSelect={onDragSelect}
|
||||||
dataAvailable={checkIfDataExists}
|
dataAvailable={checkIfDataExists}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -211,6 +211,15 @@ function WidgetHeader({
|
|||||||
maxLength: 100,
|
maxLength: 100,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const renderErrorMessage = useMemo(
|
||||||
|
() =>
|
||||||
|
errorMessage
|
||||||
|
?.split('\n')
|
||||||
|
// eslint-disable-next-line react/no-array-index-key
|
||||||
|
.map((item, i) => <p key={i}>{item}</p>),
|
||||||
|
[errorMessage],
|
||||||
|
);
|
||||||
|
|
||||||
if (widget.id === PANEL_TYPES.EMPTY_WIDGET) {
|
if (widget.id === PANEL_TYPES.EMPTY_WIDGET) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@ -270,7 +279,7 @@ function WidgetHeader({
|
|||||||
)}
|
)}
|
||||||
{queryResponse.isError && (
|
{queryResponse.isError && (
|
||||||
<Tooltip
|
<Tooltip
|
||||||
title={errorMessage}
|
title={renderErrorMessage}
|
||||||
placement={errorTooltipPosition}
|
placement={errorTooltipPosition}
|
||||||
className="widget-api-actions"
|
className="widget-api-actions"
|
||||||
>
|
>
|
||||||
|
|||||||
@ -46,6 +46,7 @@ function useUpdatedQuery(): UseUpdatedQueryResult {
|
|||||||
selectedTime: widgetConfig.timePreferance,
|
selectedTime: widgetConfig.timePreferance,
|
||||||
globalSelectedInterval,
|
globalSelectedInterval,
|
||||||
variables: getDashboardVariables(selectedDashboard?.data?.variables),
|
variables: getDashboardVariables(selectedDashboard?.data?.variables),
|
||||||
|
originalGraphType: widgetConfig.panelTypes,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Execute query and process results
|
// Execute query and process results
|
||||||
|
|||||||
@ -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
|
// 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 =>
|
const getSteps = (queryData: IBuilderQuery): number =>
|
||||||
queryData.stepInterval === 60
|
queryData?.stepInterval === 60
|
||||||
? stepIntervalPoints || 60
|
? stepIntervalPoints || 60
|
||||||
: queryData?.stepInterval || 60;
|
: queryData?.stepInterval || 60;
|
||||||
|
|
||||||
|
|||||||
@ -4,16 +4,19 @@ export const tableDataMultipleQueriesSuccessResponse = {
|
|||||||
name: 'service_name',
|
name: 'service_name',
|
||||||
queryName: '',
|
queryName: '',
|
||||||
isValueColumn: false,
|
isValueColumn: false,
|
||||||
|
id: 'service_name',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'A',
|
name: 'A',
|
||||||
queryName: 'A',
|
queryName: 'A',
|
||||||
isValueColumn: true,
|
isValueColumn: true,
|
||||||
|
id: 'A',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'B',
|
name: 'B',
|
||||||
queryName: 'B',
|
queryName: 'B',
|
||||||
isValueColumn: true,
|
isValueColumn: true,
|
||||||
|
id: 'B',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
rows: [
|
rows: [
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import './GridTableComponent.styles.scss';
|
|||||||
|
|
||||||
import { ExclamationCircleFilled } from '@ant-design/icons';
|
import { ExclamationCircleFilled } from '@ant-design/icons';
|
||||||
import { Space, Tooltip } from 'antd';
|
import { Space, Tooltip } from 'antd';
|
||||||
|
import { ColumnType } from 'antd/es/table';
|
||||||
import { getYAxisFormattedValue } from 'components/Graph/yAxisConfig';
|
import { getYAxisFormattedValue } from 'components/Graph/yAxisConfig';
|
||||||
import { Events } from 'constants/events';
|
import { Events } from 'constants/events';
|
||||||
import { QueryTable } from 'container/QueryTable';
|
import { QueryTable } from 'container/QueryTable';
|
||||||
@ -84,11 +85,12 @@ function GridTableComponent({
|
|||||||
const newValue = { ...val };
|
const newValue = { ...val };
|
||||||
Object.keys(val).forEach((k) => {
|
Object.keys(val).forEach((k) => {
|
||||||
if (columnUnits[k]) {
|
if (columnUnits[k]) {
|
||||||
// the check below takes care of not adding units for rows that have n/a values
|
// the check below takes care of not adding units for rows that have n/a or null values
|
||||||
newValue[k] =
|
if (val[k] !== 'n/a' && val[k] !== null) {
|
||||||
val[k] !== 'n/a'
|
newValue[k] = getYAxisFormattedValue(String(val[k]), columnUnits[k]);
|
||||||
? getYAxisFormattedValue(String(val[k]), columnUnits[k])
|
} else if (val[k] === null) {
|
||||||
: val[k];
|
newValue[k] = 'n/a';
|
||||||
|
}
|
||||||
newValue[`${k}_without_unit`] = val[k];
|
newValue[`${k}_without_unit`] = val[k];
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -113,25 +115,27 @@ function GridTableComponent({
|
|||||||
}
|
}
|
||||||
}, [createDataInCorrectFormat, dataSource, tableProcessedDataRef]);
|
}, [createDataInCorrectFormat, dataSource, tableProcessedDataRef]);
|
||||||
|
|
||||||
|
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||||
const newColumnData = columns.map((e) => ({
|
const newColumnData = columns.map((e) => ({
|
||||||
...e,
|
...e,
|
||||||
render: (text: string, ...rest: any): ReactNode => {
|
render: (text: string, ...rest: any): ReactNode => {
|
||||||
let textForThreshold = text;
|
let textForThreshold = text;
|
||||||
if (columnUnits && columnUnits?.[e.title as string]) {
|
const dataIndex = (e as ColumnType<RowData>)?.dataIndex || e.title;
|
||||||
textForThreshold = rest[0][`${e.title}_without_unit`];
|
if (columnUnits && columnUnits?.[dataIndex as string]) {
|
||||||
|
textForThreshold = rest[0][`${dataIndex}_without_unit`];
|
||||||
}
|
}
|
||||||
const isNumber = !Number.isNaN(Number(textForThreshold));
|
const isNumber = !Number.isNaN(Number(textForThreshold));
|
||||||
|
|
||||||
if (thresholds && isNumber) {
|
if (thresholds && isNumber) {
|
||||||
const { hasMultipleMatches, threshold } = findMatchingThreshold(
|
const { hasMultipleMatches, threshold } = findMatchingThreshold(
|
||||||
thresholds,
|
thresholds,
|
||||||
e.title as string,
|
dataIndex as string,
|
||||||
Number(textForThreshold),
|
Number(textForThreshold),
|
||||||
columnUnits?.[e.title as string],
|
columnUnits?.[dataIndex as string],
|
||||||
);
|
);
|
||||||
|
|
||||||
const idx = thresholds.findIndex(
|
const idx = thresholds.findIndex(
|
||||||
(t) => t.thresholdTableOptions === e.title,
|
(t) => t.thresholdTableOptions === dataIndex,
|
||||||
);
|
);
|
||||||
if (threshold && idx !== -1) {
|
if (threshold && idx !== -1) {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@ -98,7 +98,12 @@ export function findMatchingThreshold(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface TableData {
|
export interface TableData {
|
||||||
columns: { name: string; queryName: string; isValueColumn: boolean }[];
|
columns: {
|
||||||
|
name: string;
|
||||||
|
queryName: string;
|
||||||
|
isValueColumn: boolean;
|
||||||
|
id: string;
|
||||||
|
}[];
|
||||||
rows: { data: any }[];
|
rows: { data: any }[];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -183,10 +188,15 @@ export function createColumnsAndDataSource(
|
|||||||
? getQueryLegend(currentQuery, item.queryName)
|
? getQueryLegend(currentQuery, item.queryName)
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
|
const isNewAggregation =
|
||||||
|
currentQuery?.builder?.queryData?.find(
|
||||||
|
(query) => query.queryName === item.queryName,
|
||||||
|
)?.aggregations?.length || 0;
|
||||||
|
|
||||||
const column: ColumnType<RowData> = {
|
const column: ColumnType<RowData> = {
|
||||||
dataIndex: item.name,
|
dataIndex: item.id || item.name,
|
||||||
// if no legend present then rely on the column name value
|
// 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,
|
width: QUERY_TABLE_CONFIG.width,
|
||||||
render: renderColumnCell && renderColumnCell[item.name],
|
render: renderColumnCell && renderColumnCell[item.name],
|
||||||
sorter: (a: RowData, b: RowData): number => sortFunction(a, b, item),
|
sorter: (a: RowData, b: RowData): number => sortFunction(a, b, item),
|
||||||
|
|||||||
@ -9,7 +9,7 @@ import { K8sPodsListPayload } from 'api/infraMonitoring/getK8sPodsList';
|
|||||||
import listUserPreferences from 'api/v1/user/preferences/list';
|
import listUserPreferences from 'api/v1/user/preferences/list';
|
||||||
import updateUserPreferenceAPI from 'api/v1/user/preferences/name/update';
|
import updateUserPreferenceAPI from 'api/v1/user/preferences/name/update';
|
||||||
import Header from 'components/Header/Header';
|
import Header from 'components/Header/Header';
|
||||||
import { DEFAULT_ENTITY_VERSION } from 'constants/app';
|
import { ENTITY_VERSION_V5 } from 'constants/app';
|
||||||
import { FeatureKeys } from 'constants/features';
|
import { FeatureKeys } from 'constants/features';
|
||||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||||
import { ORG_PREFERENCES } from 'constants/orgPreferences';
|
import { ORG_PREFERENCES } from 'constants/orgPreferences';
|
||||||
@ -33,6 +33,7 @@ import { useMutation, useQuery } from 'react-query';
|
|||||||
import { UserPreference } from 'types/api/preferences/preference';
|
import { UserPreference } from 'types/api/preferences/preference';
|
||||||
import { DataSource } from 'types/common/queryBuilder';
|
import { DataSource } from 'types/common/queryBuilder';
|
||||||
import { USER_ROLES } from 'types/roles';
|
import { USER_ROLES } from 'types/roles';
|
||||||
|
import { isIngestionActive } from 'utils/app';
|
||||||
import { popupContainer } from 'utils/selectPopupContainer';
|
import { popupContainer } from 'utils/selectPopupContainer';
|
||||||
|
|
||||||
import AlertRules from './AlertRules/AlertRules';
|
import AlertRules from './AlertRules/AlertRules';
|
||||||
@ -85,14 +86,15 @@ export default function Home(): JSX.Element {
|
|||||||
const { data: logsData, isLoading: isLogsLoading } = useGetQueryRange(
|
const { data: logsData, isLoading: isLogsLoading } = useGetQueryRange(
|
||||||
{
|
{
|
||||||
query: initialQueriesMap[DataSource.LOGS],
|
query: initialQueriesMap[DataSource.LOGS],
|
||||||
graphType: PANEL_TYPES.TABLE,
|
graphType: PANEL_TYPES.VALUE,
|
||||||
selectedTime: 'GLOBAL_TIME',
|
selectedTime: 'GLOBAL_TIME',
|
||||||
globalSelectedInterval: '30m',
|
globalSelectedInterval: '30m',
|
||||||
params: {
|
params: {
|
||||||
dataSource: DataSource.LOGS,
|
dataSource: DataSource.LOGS,
|
||||||
},
|
},
|
||||||
|
formatForWeb: false,
|
||||||
},
|
},
|
||||||
DEFAULT_ENTITY_VERSION,
|
ENTITY_VERSION_V5,
|
||||||
{
|
{
|
||||||
queryKey: [
|
queryKey: [
|
||||||
REACT_QUERY_KEY.GET_QUERY_RANGE,
|
REACT_QUERY_KEY.GET_QUERY_RANGE,
|
||||||
@ -109,14 +111,15 @@ export default function Home(): JSX.Element {
|
|||||||
const { data: tracesData, isLoading: isTracesLoading } = useGetQueryRange(
|
const { data: tracesData, isLoading: isTracesLoading } = useGetQueryRange(
|
||||||
{
|
{
|
||||||
query: initialQueriesMap[DataSource.TRACES],
|
query: initialQueriesMap[DataSource.TRACES],
|
||||||
graphType: PANEL_TYPES.TABLE,
|
graphType: PANEL_TYPES.VALUE,
|
||||||
selectedTime: 'GLOBAL_TIME',
|
selectedTime: 'GLOBAL_TIME',
|
||||||
globalSelectedInterval: '30m',
|
globalSelectedInterval: '30m',
|
||||||
params: {
|
params: {
|
||||||
dataSource: DataSource.TRACES,
|
dataSource: DataSource.TRACES,
|
||||||
},
|
},
|
||||||
|
formatForWeb: false,
|
||||||
},
|
},
|
||||||
DEFAULT_ENTITY_VERSION,
|
ENTITY_VERSION_V5,
|
||||||
{
|
{
|
||||||
queryKey: [
|
queryKey: [
|
||||||
REACT_QUERY_KEY.GET_QUERY_RANGE,
|
REACT_QUERY_KEY.GET_QUERY_RANGE,
|
||||||
@ -282,13 +285,9 @@ export default function Home(): JSX.Element {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const logsDataTotal = parseInt(
|
const isLogsIngestionActive = isIngestionActive(logsData?.payload);
|
||||||
logsData?.payload?.data?.newResult?.data?.result?.[0]?.series?.[0]
|
|
||||||
?.values?.[0]?.value || '0',
|
|
||||||
10,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (logsDataTotal > 0) {
|
if (isLogsIngestionActive) {
|
||||||
setIsLogsIngestionActive(true);
|
setIsLogsIngestionActive(true);
|
||||||
handleUpdateChecklistDoneItem('SEND_LOGS');
|
handleUpdateChecklistDoneItem('SEND_LOGS');
|
||||||
handleUpdateChecklistDoneItem('ADD_DATA_SOURCE');
|
handleUpdateChecklistDoneItem('ADD_DATA_SOURCE');
|
||||||
@ -296,13 +295,9 @@ export default function Home(): JSX.Element {
|
|||||||
}, [logsData, handleUpdateChecklistDoneItem]);
|
}, [logsData, handleUpdateChecklistDoneItem]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const tracesDataTotal = parseInt(
|
const isTracesIngestionActive = isIngestionActive(tracesData?.payload);
|
||||||
tracesData?.payload?.data?.newResult?.data?.result?.[0]?.series?.[0]
|
|
||||||
?.values?.[0]?.value || '0',
|
|
||||||
10,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (tracesDataTotal > 0) {
|
if (isTracesIngestionActive) {
|
||||||
setIsTracesIngestionActive(true);
|
setIsTracesIngestionActive(true);
|
||||||
handleUpdateChecklistDoneItem('SEND_TRACES');
|
handleUpdateChecklistDoneItem('SEND_TRACES');
|
||||||
handleUpdateChecklistDoneItem('ADD_DATA_SOURCE');
|
handleUpdateChecklistDoneItem('ADD_DATA_SOURCE');
|
||||||
|
|||||||
@ -153,7 +153,7 @@ function HostsList(): JSX.Element {
|
|||||||
|
|
||||||
const handleFiltersChange = useCallback(
|
const handleFiltersChange = useCallback(
|
||||||
(value: IBuilderQuery['filters']): void => {
|
(value: IBuilderQuery['filters']): void => {
|
||||||
const isNewFilterAdded = value.items.length !== filters.items.length;
|
const isNewFilterAdded = value?.items?.length !== filters?.items?.length;
|
||||||
setFilters(value);
|
setFilters(value);
|
||||||
handleChangeQueryData('filters', value);
|
handleChangeQueryData('filters', value);
|
||||||
setSearchParams({
|
setSearchParams({
|
||||||
@ -163,7 +163,7 @@ function HostsList(): JSX.Element {
|
|||||||
if (isNewFilterAdded) {
|
if (isNewFilterAdded) {
|
||||||
setCurrentPage(1);
|
setCurrentPage(1);
|
||||||
|
|
||||||
if (value.items.length > 0) {
|
if (value?.items && value?.items?.length > 0) {
|
||||||
logEvent(InfraMonitoringEvents.FilterApplied, {
|
logEvent(InfraMonitoringEvents.FilterApplied, {
|
||||||
entity: InfraMonitoringEvents.HostEntity,
|
entity: InfraMonitoringEvents.HostEntity,
|
||||||
page: InfraMonitoringEvents.ListPage,
|
page: InfraMonitoringEvents.ListPage,
|
||||||
@ -251,7 +251,7 @@ function HostsList(): JSX.Element {
|
|||||||
isError={isError}
|
isError={isError}
|
||||||
tableData={data}
|
tableData={data}
|
||||||
hostMetricsData={hostMetricsData}
|
hostMetricsData={hostMetricsData}
|
||||||
filters={filters}
|
filters={filters || { items: [], op: 'AND' }}
|
||||||
currentPage={currentPage}
|
currentPage={currentPage}
|
||||||
setCurrentPage={setCurrentPage}
|
setCurrentPage={setCurrentPage}
|
||||||
onHostClick={handleHostClick}
|
onHostClick={handleHostClick}
|
||||||
|
|||||||
@ -50,7 +50,7 @@ function HostsListControls({
|
|||||||
<div className="hosts-list-controls">
|
<div className="hosts-list-controls">
|
||||||
<div className="hosts-list-controls-left">
|
<div className="hosts-list-controls-left">
|
||||||
<QueryBuilderSearch
|
<QueryBuilderSearch
|
||||||
query={query}
|
query={query as IBuilderQuery}
|
||||||
onChange={handleChangeTagFilters}
|
onChange={handleChangeTagFilters}
|
||||||
isInfraMonitoring
|
isInfraMonitoring
|
||||||
disableNavigationShortcuts
|
disableNavigationShortcuts
|
||||||
|
|||||||
@ -263,16 +263,18 @@ function ClusterDetails({
|
|||||||
const handleChangeLogFilters = useCallback(
|
const handleChangeLogFilters = useCallback(
|
||||||
(value: IBuilderQuery['filters'], view: VIEWS) => {
|
(value: IBuilderQuery['filters'], view: VIEWS) => {
|
||||||
setLogsAndTracesFilters((prevFilters) => {
|
setLogsAndTracesFilters((prevFilters) => {
|
||||||
const primaryFilters = prevFilters.items.filter((item) =>
|
const primaryFilters = prevFilters?.items?.filter((item) =>
|
||||||
[QUERY_KEYS.K8S_CLUSTER_NAME].includes(item.key?.key ?? ''),
|
[QUERY_KEYS.K8S_CLUSTER_NAME].includes(item.key?.key ?? ''),
|
||||||
);
|
);
|
||||||
const paginationFilter = value.items.find((item) => item.key?.key === 'id');
|
const paginationFilter = value?.items?.find(
|
||||||
const newFilters = value.items.filter(
|
(item) => item.key?.key === 'id',
|
||||||
|
);
|
||||||
|
const newFilters = value?.items?.filter(
|
||||||
(item) =>
|
(item) =>
|
||||||
item.key?.key !== 'id' && item.key?.key !== QUERY_KEYS.K8S_CLUSTER_NAME,
|
item.key?.key !== 'id' && item.key?.key !== QUERY_KEYS.K8S_CLUSTER_NAME,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (newFilters.length > 0) {
|
if (newFilters && newFilters?.length > 0) {
|
||||||
logEvent(InfraMonitoringEvents.FilterApplied, {
|
logEvent(InfraMonitoringEvents.FilterApplied, {
|
||||||
entity: InfraMonitoringEvents.K8sEntity,
|
entity: InfraMonitoringEvents.K8sEntity,
|
||||||
page: InfraMonitoringEvents.DetailedPage,
|
page: InfraMonitoringEvents.DetailedPage,
|
||||||
@ -285,8 +287,8 @@ function ClusterDetails({
|
|||||||
op: 'AND',
|
op: 'AND',
|
||||||
items: filterDuplicateFilters(
|
items: filterDuplicateFilters(
|
||||||
[
|
[
|
||||||
...primaryFilters,
|
...(primaryFilters || []),
|
||||||
...newFilters,
|
...(newFilters || []),
|
||||||
...(paginationFilter ? [paginationFilter] : []),
|
...(paginationFilter ? [paginationFilter] : []),
|
||||||
].filter((item): item is TagFilterItem => item !== undefined),
|
].filter((item): item is TagFilterItem => item !== undefined),
|
||||||
),
|
),
|
||||||
@ -310,11 +312,11 @@ function ClusterDetails({
|
|||||||
const handleChangeTracesFilters = useCallback(
|
const handleChangeTracesFilters = useCallback(
|
||||||
(value: IBuilderQuery['filters'], view: VIEWS) => {
|
(value: IBuilderQuery['filters'], view: VIEWS) => {
|
||||||
setLogsAndTracesFilters((prevFilters) => {
|
setLogsAndTracesFilters((prevFilters) => {
|
||||||
const primaryFilters = prevFilters.items.filter((item) =>
|
const primaryFilters = prevFilters?.items?.filter((item) =>
|
||||||
[QUERY_KEYS.K8S_CLUSTER_NAME].includes(item.key?.key ?? ''),
|
[QUERY_KEYS.K8S_CLUSTER_NAME].includes(item.key?.key ?? ''),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (value.items.length > 0) {
|
if (value?.items && value?.items?.length > 0) {
|
||||||
logEvent(InfraMonitoringEvents.FilterApplied, {
|
logEvent(InfraMonitoringEvents.FilterApplied, {
|
||||||
entity: InfraMonitoringEvents.K8sEntity,
|
entity: InfraMonitoringEvents.K8sEntity,
|
||||||
page: InfraMonitoringEvents.DetailedPage,
|
page: InfraMonitoringEvents.DetailedPage,
|
||||||
@ -327,10 +329,10 @@ function ClusterDetails({
|
|||||||
op: 'AND',
|
op: 'AND',
|
||||||
items: filterDuplicateFilters(
|
items: filterDuplicateFilters(
|
||||||
[
|
[
|
||||||
...primaryFilters,
|
...(primaryFilters || []),
|
||||||
...value.items.filter(
|
...(value?.items?.filter(
|
||||||
(item) => item.key?.key !== QUERY_KEYS.K8S_CLUSTER_NAME,
|
(item) => item.key?.key !== QUERY_KEYS.K8S_CLUSTER_NAME,
|
||||||
),
|
) || []),
|
||||||
].filter((item): item is TagFilterItem => item !== undefined),
|
].filter((item): item is TagFilterItem => item !== undefined),
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
@ -353,14 +355,14 @@ function ClusterDetails({
|
|||||||
const handleChangeEventsFilters = useCallback(
|
const handleChangeEventsFilters = useCallback(
|
||||||
(value: IBuilderQuery['filters'], view: VIEWS) => {
|
(value: IBuilderQuery['filters'], view: VIEWS) => {
|
||||||
setEventsFilters((prevFilters) => {
|
setEventsFilters((prevFilters) => {
|
||||||
const clusterKindFilter = prevFilters.items.find(
|
const clusterKindFilter = prevFilters?.items?.find(
|
||||||
(item) => item.key?.key === QUERY_KEYS.K8S_OBJECT_KIND,
|
(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,
|
(item) => item.key?.key === QUERY_KEYS.K8S_OBJECT_NAME,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (value.items.length > 0) {
|
if (value?.items && value?.items?.length > 0) {
|
||||||
logEvent(InfraMonitoringEvents.FilterApplied, {
|
logEvent(InfraMonitoringEvents.FilterApplied, {
|
||||||
entity: InfraMonitoringEvents.K8sEntity,
|
entity: InfraMonitoringEvents.K8sEntity,
|
||||||
page: InfraMonitoringEvents.DetailedPage,
|
page: InfraMonitoringEvents.DetailedPage,
|
||||||
@ -375,11 +377,11 @@ function ClusterDetails({
|
|||||||
[
|
[
|
||||||
clusterKindFilter,
|
clusterKindFilter,
|
||||||
clusterNameFilter,
|
clusterNameFilter,
|
||||||
...value.items.filter(
|
...(value?.items?.filter(
|
||||||
(item) =>
|
(item) =>
|
||||||
item.key?.key !== QUERY_KEYS.K8S_OBJECT_KIND &&
|
item.key?.key !== QUERY_KEYS.K8S_OBJECT_KIND &&
|
||||||
item.key?.key !== QUERY_KEYS.K8S_OBJECT_NAME,
|
item.key?.key !== QUERY_KEYS.K8S_OBJECT_NAME,
|
||||||
),
|
) || []),
|
||||||
].filter((item): item is TagFilterItem => item !== undefined),
|
].filter((item): item is TagFilterItem => item !== undefined),
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
@ -418,7 +420,9 @@ function ClusterDetails({
|
|||||||
if (selectedView === VIEW_TYPES.LOGS) {
|
if (selectedView === VIEW_TYPES.LOGS) {
|
||||||
const filtersWithoutPagination = {
|
const filtersWithoutPagination = {
|
||||||
...logsAndTracesFilters,
|
...logsAndTracesFilters,
|
||||||
items: logsAndTracesFilters.items.filter((item) => item.key?.key !== 'id'),
|
items:
|
||||||
|
logsAndTracesFilters?.items?.filter((item) => item.key?.key !== 'id') ||
|
||||||
|
[],
|
||||||
};
|
};
|
||||||
|
|
||||||
const compositeQuery = {
|
const compositeQuery = {
|
||||||
|
|||||||
@ -404,7 +404,7 @@ function K8sClustersList({
|
|||||||
setFiltersInitialised(true);
|
setFiltersInitialised(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (value.items.length > 0) {
|
if (value?.items && value?.items?.length > 0) {
|
||||||
logEvent(InfraMonitoringEvents.FilterApplied, {
|
logEvent(InfraMonitoringEvents.FilterApplied, {
|
||||||
entity: InfraMonitoringEvents.K8sEntity,
|
entity: InfraMonitoringEvents.K8sEntity,
|
||||||
category: InfraMonitoringEvents.Cluster,
|
category: InfraMonitoringEvents.Cluster,
|
||||||
|
|||||||
@ -279,19 +279,21 @@ function DaemonSetDetails({
|
|||||||
const handleChangeLogFilters = useCallback(
|
const handleChangeLogFilters = useCallback(
|
||||||
(value: IBuilderQuery['filters'], view: VIEWS) => {
|
(value: IBuilderQuery['filters'], view: VIEWS) => {
|
||||||
setLogAndTracesFilters((prevFilters) => {
|
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(
|
[QUERY_KEYS.K8S_DAEMON_SET_NAME, QUERY_KEYS.K8S_NAMESPACE_NAME].includes(
|
||||||
item.key?.key ?? '',
|
item.key?.key ?? '',
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
const paginationFilter = value.items.find((item) => item.key?.key === 'id');
|
const paginationFilter = value?.items?.find(
|
||||||
const newFilters = value.items.filter(
|
(item) => item.key?.key === 'id',
|
||||||
|
);
|
||||||
|
const newFilters = value?.items?.filter(
|
||||||
(item) =>
|
(item) =>
|
||||||
item.key?.key !== 'id' &&
|
item.key?.key !== 'id' &&
|
||||||
item.key?.key !== QUERY_KEYS.K8S_DAEMON_SET_NAME,
|
item.key?.key !== QUERY_KEYS.K8S_DAEMON_SET_NAME,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (newFilters.length > 0) {
|
if (newFilters && newFilters?.length > 0) {
|
||||||
logEvent(InfraMonitoringEvents.FilterApplied, {
|
logEvent(InfraMonitoringEvents.FilterApplied, {
|
||||||
entity: InfraMonitoringEvents.K8sEntity,
|
entity: InfraMonitoringEvents.K8sEntity,
|
||||||
page: InfraMonitoringEvents.DetailedPage,
|
page: InfraMonitoringEvents.DetailedPage,
|
||||||
@ -303,8 +305,8 @@ function DaemonSetDetails({
|
|||||||
const updatedFilters = {
|
const updatedFilters = {
|
||||||
op: 'AND',
|
op: 'AND',
|
||||||
items: [
|
items: [
|
||||||
...primaryFilters,
|
...(primaryFilters || []),
|
||||||
...newFilters,
|
...(newFilters || []),
|
||||||
...(paginationFilter ? [paginationFilter] : []),
|
...(paginationFilter ? [paginationFilter] : []),
|
||||||
].filter((item): item is TagFilterItem => item !== undefined),
|
].filter((item): item is TagFilterItem => item !== undefined),
|
||||||
};
|
};
|
||||||
@ -326,13 +328,13 @@ function DaemonSetDetails({
|
|||||||
const handleChangeTracesFilters = useCallback(
|
const handleChangeTracesFilters = useCallback(
|
||||||
(value: IBuilderQuery['filters'], view: VIEWS) => {
|
(value: IBuilderQuery['filters'], view: VIEWS) => {
|
||||||
setLogAndTracesFilters((prevFilters) => {
|
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(
|
[QUERY_KEYS.K8S_DAEMON_SET_NAME, QUERY_KEYS.K8S_NAMESPACE_NAME].includes(
|
||||||
item.key?.key ?? '',
|
item.key?.key ?? '',
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (value.items.length > 0) {
|
if (value?.items && value?.items?.length > 0) {
|
||||||
logEvent(InfraMonitoringEvents.FilterApplied, {
|
logEvent(InfraMonitoringEvents.FilterApplied, {
|
||||||
entity: InfraMonitoringEvents.K8sEntity,
|
entity: InfraMonitoringEvents.K8sEntity,
|
||||||
page: InfraMonitoringEvents.DetailedPage,
|
page: InfraMonitoringEvents.DetailedPage,
|
||||||
@ -344,10 +346,10 @@ function DaemonSetDetails({
|
|||||||
const updatedFilters = {
|
const updatedFilters = {
|
||||||
op: 'AND',
|
op: 'AND',
|
||||||
items: [
|
items: [
|
||||||
...primaryFilters,
|
...(primaryFilters || []),
|
||||||
...value.items.filter(
|
...(value?.items?.filter(
|
||||||
(item) => item.key?.key !== QUERY_KEYS.K8S_DAEMON_SET_NAME,
|
(item) => item.key?.key !== QUERY_KEYS.K8S_DAEMON_SET_NAME,
|
||||||
),
|
) || []),
|
||||||
].filter((item): item is TagFilterItem => item !== undefined),
|
].filter((item): item is TagFilterItem => item !== undefined),
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -369,14 +371,14 @@ function DaemonSetDetails({
|
|||||||
const handleChangeEventsFilters = useCallback(
|
const handleChangeEventsFilters = useCallback(
|
||||||
(value: IBuilderQuery['filters'], view: VIEWS) => {
|
(value: IBuilderQuery['filters'], view: VIEWS) => {
|
||||||
setEventsFilters((prevFilters) => {
|
setEventsFilters((prevFilters) => {
|
||||||
const daemonSetKindFilter = prevFilters.items.find(
|
const daemonSetKindFilter = prevFilters?.items?.find(
|
||||||
(item) => item.key?.key === QUERY_KEYS.K8S_OBJECT_KIND,
|
(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,
|
(item) => item.key?.key === QUERY_KEYS.K8S_OBJECT_NAME,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (value.items.length > 0) {
|
if (value?.items && value?.items?.length > 0) {
|
||||||
logEvent(InfraMonitoringEvents.FilterApplied, {
|
logEvent(InfraMonitoringEvents.FilterApplied, {
|
||||||
entity: InfraMonitoringEvents.K8sEntity,
|
entity: InfraMonitoringEvents.K8sEntity,
|
||||||
page: InfraMonitoringEvents.DetailedPage,
|
page: InfraMonitoringEvents.DetailedPage,
|
||||||
@ -390,11 +392,11 @@ function DaemonSetDetails({
|
|||||||
items: [
|
items: [
|
||||||
daemonSetKindFilter,
|
daemonSetKindFilter,
|
||||||
daemonSetNameFilter,
|
daemonSetNameFilter,
|
||||||
...value.items.filter(
|
...(value?.items?.filter(
|
||||||
(item) =>
|
(item) =>
|
||||||
item.key?.key !== QUERY_KEYS.K8S_OBJECT_KIND &&
|
item.key?.key !== QUERY_KEYS.K8S_OBJECT_KIND &&
|
||||||
item.key?.key !== QUERY_KEYS.K8S_OBJECT_NAME,
|
item.key?.key !== QUERY_KEYS.K8S_OBJECT_NAME,
|
||||||
),
|
) || []),
|
||||||
].filter((item): item is TagFilterItem => item !== undefined),
|
].filter((item): item is TagFilterItem => item !== undefined),
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -432,7 +434,7 @@ function DaemonSetDetails({
|
|||||||
if (selectedView === VIEW_TYPES.LOGS) {
|
if (selectedView === VIEW_TYPES.LOGS) {
|
||||||
const filtersWithoutPagination = {
|
const filtersWithoutPagination = {
|
||||||
...logAndTracesFilters,
|
...logAndTracesFilters,
|
||||||
items: logAndTracesFilters.items.filter((item) => item.key?.key !== 'id'),
|
items: logAndTracesFilters?.items?.filter((item) => item.key?.key !== 'id'),
|
||||||
};
|
};
|
||||||
|
|
||||||
const compositeQuery = {
|
const compositeQuery = {
|
||||||
|
|||||||
@ -408,7 +408,7 @@ function K8sDaemonSetsList({
|
|||||||
setFiltersInitialised(true);
|
setFiltersInitialised(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (value.items.length > 0) {
|
if (value?.items && value?.items?.length > 0) {
|
||||||
logEvent(InfraMonitoringEvents.FilterApplied, {
|
logEvent(InfraMonitoringEvents.FilterApplied, {
|
||||||
entity: InfraMonitoringEvents.K8sEntity,
|
entity: InfraMonitoringEvents.K8sEntity,
|
||||||
page: InfraMonitoringEvents.ListPage,
|
page: InfraMonitoringEvents.ListPage,
|
||||||
|
|||||||
@ -283,19 +283,21 @@ function DeploymentDetails({
|
|||||||
const handleChangeLogFilters = useCallback(
|
const handleChangeLogFilters = useCallback(
|
||||||
(value: IBuilderQuery['filters'], view: VIEWS) => {
|
(value: IBuilderQuery['filters'], view: VIEWS) => {
|
||||||
setLogAndTracesFilters((prevFilters) => {
|
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(
|
[QUERY_KEYS.K8S_DEPLOYMENT_NAME, QUERY_KEYS.K8S_NAMESPACE_NAME].includes(
|
||||||
item.key?.key ?? '',
|
item.key?.key ?? '',
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
const paginationFilter = value.items.find((item) => item.key?.key === 'id');
|
const paginationFilter = value?.items?.find(
|
||||||
const newFilters = value.items.filter(
|
(item) => item.key?.key === 'id',
|
||||||
|
);
|
||||||
|
const newFilters = value?.items?.filter(
|
||||||
(item) =>
|
(item) =>
|
||||||
item.key?.key !== 'id' &&
|
item.key?.key !== 'id' &&
|
||||||
item.key?.key !== QUERY_KEYS.K8S_DEPLOYMENT_NAME,
|
item.key?.key !== QUERY_KEYS.K8S_DEPLOYMENT_NAME,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (value.items.length > 0) {
|
if (value?.items && value?.items?.length > 0) {
|
||||||
logEvent(InfraMonitoringEvents.FilterApplied, {
|
logEvent(InfraMonitoringEvents.FilterApplied, {
|
||||||
entity: InfraMonitoringEvents.K8sEntity,
|
entity: InfraMonitoringEvents.K8sEntity,
|
||||||
page: InfraMonitoringEvents.DetailedPage,
|
page: InfraMonitoringEvents.DetailedPage,
|
||||||
@ -308,8 +310,8 @@ function DeploymentDetails({
|
|||||||
op: 'AND',
|
op: 'AND',
|
||||||
items: filterDuplicateFilters(
|
items: filterDuplicateFilters(
|
||||||
[
|
[
|
||||||
...primaryFilters,
|
...(primaryFilters || []),
|
||||||
...newFilters,
|
...(newFilters || []),
|
||||||
...(paginationFilter ? [paginationFilter] : []),
|
...(paginationFilter ? [paginationFilter] : []),
|
||||||
].filter((item): item is TagFilterItem => item !== undefined),
|
].filter((item): item is TagFilterItem => item !== undefined),
|
||||||
),
|
),
|
||||||
@ -333,13 +335,13 @@ function DeploymentDetails({
|
|||||||
const handleChangeTracesFilters = useCallback(
|
const handleChangeTracesFilters = useCallback(
|
||||||
(value: IBuilderQuery['filters'], view: VIEWS) => {
|
(value: IBuilderQuery['filters'], view: VIEWS) => {
|
||||||
setLogAndTracesFilters((prevFilters) => {
|
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(
|
[QUERY_KEYS.K8S_DEPLOYMENT_NAME, QUERY_KEYS.K8S_NAMESPACE_NAME].includes(
|
||||||
item.key?.key ?? '',
|
item.key?.key ?? '',
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (value.items.length > 0) {
|
if (value?.items && value?.items?.length > 0) {
|
||||||
logEvent(InfraMonitoringEvents.FilterApplied, {
|
logEvent(InfraMonitoringEvents.FilterApplied, {
|
||||||
entity: InfraMonitoringEvents.K8sEntity,
|
entity: InfraMonitoringEvents.K8sEntity,
|
||||||
page: InfraMonitoringEvents.DetailedPage,
|
page: InfraMonitoringEvents.DetailedPage,
|
||||||
@ -352,10 +354,10 @@ function DeploymentDetails({
|
|||||||
op: 'AND',
|
op: 'AND',
|
||||||
items: filterDuplicateFilters(
|
items: filterDuplicateFilters(
|
||||||
[
|
[
|
||||||
...primaryFilters,
|
...(primaryFilters || []),
|
||||||
...value.items.filter(
|
...(value?.items?.filter(
|
||||||
(item) => item.key?.key !== QUERY_KEYS.K8S_DEPLOYMENT_NAME,
|
(item) => item.key?.key !== QUERY_KEYS.K8S_DEPLOYMENT_NAME,
|
||||||
),
|
) || []),
|
||||||
].filter((item): item is TagFilterItem => item !== undefined),
|
].filter((item): item is TagFilterItem => item !== undefined),
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
@ -378,14 +380,14 @@ function DeploymentDetails({
|
|||||||
const handleChangeEventsFilters = useCallback(
|
const handleChangeEventsFilters = useCallback(
|
||||||
(value: IBuilderQuery['filters'], view: VIEWS) => {
|
(value: IBuilderQuery['filters'], view: VIEWS) => {
|
||||||
setEventsFilters((prevFilters) => {
|
setEventsFilters((prevFilters) => {
|
||||||
const deploymentKindFilter = prevFilters.items.find(
|
const deploymentKindFilter = prevFilters?.items?.find(
|
||||||
(item) => item.key?.key === QUERY_KEYS.K8S_OBJECT_KIND,
|
(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,
|
(item) => item.key?.key === QUERY_KEYS.K8S_OBJECT_NAME,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (value.items.length > 0) {
|
if (value?.items && value?.items?.length > 0) {
|
||||||
logEvent(InfraMonitoringEvents.FilterApplied, {
|
logEvent(InfraMonitoringEvents.FilterApplied, {
|
||||||
entity: InfraMonitoringEvents.K8sEntity,
|
entity: InfraMonitoringEvents.K8sEntity,
|
||||||
page: InfraMonitoringEvents.DetailedPage,
|
page: InfraMonitoringEvents.DetailedPage,
|
||||||
@ -400,11 +402,11 @@ function DeploymentDetails({
|
|||||||
[
|
[
|
||||||
deploymentKindFilter,
|
deploymentKindFilter,
|
||||||
deploymentNameFilter,
|
deploymentNameFilter,
|
||||||
...value.items.filter(
|
...(value?.items?.filter(
|
||||||
(item) =>
|
(item) =>
|
||||||
item.key?.key !== QUERY_KEYS.K8S_OBJECT_KIND &&
|
item.key?.key !== QUERY_KEYS.K8S_OBJECT_KIND &&
|
||||||
item.key?.key !== QUERY_KEYS.K8S_OBJECT_NAME,
|
item.key?.key !== QUERY_KEYS.K8S_OBJECT_NAME,
|
||||||
),
|
) || []),
|
||||||
].filter((item): item is TagFilterItem => item !== undefined),
|
].filter((item): item is TagFilterItem => item !== undefined),
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
@ -443,7 +445,7 @@ function DeploymentDetails({
|
|||||||
if (selectedView === VIEW_TYPES.LOGS) {
|
if (selectedView === VIEW_TYPES.LOGS) {
|
||||||
const filtersWithoutPagination = {
|
const filtersWithoutPagination = {
|
||||||
...logAndTracesFilters,
|
...logAndTracesFilters,
|
||||||
items: logAndTracesFilters.items.filter((item) => item.key?.key !== 'id'),
|
items: logAndTracesFilters?.items?.filter((item) => item.key?.key !== 'id'),
|
||||||
};
|
};
|
||||||
|
|
||||||
const compositeQuery = {
|
const compositeQuery = {
|
||||||
|
|||||||
@ -411,7 +411,7 @@ function K8sDeploymentsList({
|
|||||||
setFiltersInitialised(true);
|
setFiltersInitialised(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (value.items.length > 0) {
|
if (value?.items && value?.items?.length > 0) {
|
||||||
logEvent(InfraMonitoringEvents.FilterApplied, {
|
logEvent(InfraMonitoringEvents.FilterApplied, {
|
||||||
entity: InfraMonitoringEvents.K8sEntity,
|
entity: InfraMonitoringEvents.K8sEntity,
|
||||||
page: InfraMonitoringEvents.ListPage,
|
page: InfraMonitoringEvents.ListPage,
|
||||||
|
|||||||
@ -108,7 +108,7 @@ export default function Events({
|
|||||||
...currentQuery.builder.queryData[0].aggregateAttribute,
|
...currentQuery.builder.queryData[0].aggregateAttribute,
|
||||||
},
|
},
|
||||||
filters: {
|
filters: {
|
||||||
items: filters.items.filter(
|
items: filters?.items?.filter(
|
||||||
(item) =>
|
(item) =>
|
||||||
item.key?.key !== QUERY_KEYS.K8S_OBJECT_KIND &&
|
item.key?.key !== QUERY_KEYS.K8S_OBJECT_KIND &&
|
||||||
item.key?.key !== QUERY_KEYS.K8S_OBJECT_NAME,
|
item.key?.key !== QUERY_KEYS.K8S_OBJECT_NAME,
|
||||||
@ -251,7 +251,7 @@ export default function Events({
|
|||||||
<div className="filter-section">
|
<div className="filter-section">
|
||||||
{query && (
|
{query && (
|
||||||
<QueryBuilderSearch
|
<QueryBuilderSearch
|
||||||
query={query}
|
query={query as IBuilderQuery}
|
||||||
onChange={(value): void => handleChangeEventFilters(value, VIEWS.EVENTS)}
|
onChange={(value): void => handleChangeEventFilters(value, VIEWS.EVENTS)}
|
||||||
disableNavigationShortcuts
|
disableNavigationShortcuts
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -60,14 +60,14 @@ function EntityLogsDetailedView({
|
|||||||
...currentQuery.builder.queryData[0].aggregateAttribute,
|
...currentQuery.builder.queryData[0].aggregateAttribute,
|
||||||
},
|
},
|
||||||
filters: {
|
filters: {
|
||||||
items: filterOutPrimaryFilters(logFilters.items, queryKeyFilters),
|
items: filterOutPrimaryFilters(logFilters?.items || [], queryKeyFilters),
|
||||||
op: 'AND',
|
op: 'AND',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
[currentQuery, logFilters.items, queryKeyFilters],
|
[currentQuery, logFilters?.items, queryKeyFilters],
|
||||||
);
|
);
|
||||||
|
|
||||||
const query = updatedCurrentQuery?.builder?.queryData[0] || null;
|
const query = updatedCurrentQuery?.builder?.queryData[0] || null;
|
||||||
@ -78,7 +78,7 @@ function EntityLogsDetailedView({
|
|||||||
<div className="filter-section">
|
<div className="filter-section">
|
||||||
{query && (
|
{query && (
|
||||||
<QueryBuilderSearch
|
<QueryBuilderSearch
|
||||||
query={query}
|
query={query as IBuilderQuery}
|
||||||
onChange={(value): void => handleChangeLogFilters(value, VIEWS.LOGS)}
|
onChange={(value): void => handleChangeLogFilters(value, VIEWS.LOGS)}
|
||||||
disableNavigationShortcuts
|
disableNavigationShortcuts
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -1,9 +1,6 @@
|
|||||||
import { ENVIRONMENT } from 'constants/env';
|
import { ENVIRONMENT } from 'constants/env';
|
||||||
import { K8sCategory } from 'container/InfraMonitoringK8s/constants';
|
import { K8sCategory } from 'container/InfraMonitoringK8s/constants';
|
||||||
import {
|
import { verifyFiltersAndOrderBy } from 'container/LogsExplorerViews/tests/LogsExplorerPagination.test';
|
||||||
verifyFiltersAndOrderBy,
|
|
||||||
verifyPayload,
|
|
||||||
} from 'container/LogsExplorerViews/tests/LogsExplorerPagination.test';
|
|
||||||
import { logsPaginationQueryRangeSuccessResponse } from 'mocks-server/__mockdata__/logs_query_range';
|
import { logsPaginationQueryRangeSuccessResponse } from 'mocks-server/__mockdata__/logs_query_range';
|
||||||
import { server } from 'mocks-server/server';
|
import { server } from 'mocks-server/server';
|
||||||
import { rest } from 'msw';
|
import { rest } from 'msw';
|
||||||
@ -21,6 +18,32 @@ import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
|||||||
|
|
||||||
import EntityLogs from '../EntityLogs';
|
import EntityLogs from '../EntityLogs';
|
||||||
|
|
||||||
|
// Custom verifyPayload function for EntityLogs that works with the correct payload structure
|
||||||
|
const verifyEntityLogsPayload = ({
|
||||||
|
payload,
|
||||||
|
expectedOffset,
|
||||||
|
initialTimeRange,
|
||||||
|
}: {
|
||||||
|
payload: QueryRangePayload;
|
||||||
|
expectedOffset: number;
|
||||||
|
initialTimeRange?: { start: number; end: number };
|
||||||
|
}): IBuilderQuery => {
|
||||||
|
// Extract the builder query data from the correct path
|
||||||
|
const queryData = payload?.compositeQuery?.builderQueries?.A as IBuilderQuery;
|
||||||
|
|
||||||
|
expect(queryData).toBeDefined();
|
||||||
|
// Assert that the offset in the payload matches the expected offset
|
||||||
|
expect(queryData.offset).toBe(expectedOffset);
|
||||||
|
|
||||||
|
// If initial time range is provided, assert that the payload start and end match
|
||||||
|
if (initialTimeRange) {
|
||||||
|
expect(payload.start).toBe(initialTimeRange.start);
|
||||||
|
expect(payload.end).toBe(initialTimeRange.end);
|
||||||
|
}
|
||||||
|
|
||||||
|
return queryData;
|
||||||
|
};
|
||||||
|
|
||||||
jest.mock('uplot', () => {
|
jest.mock('uplot', () => {
|
||||||
const paths = {
|
const paths = {
|
||||||
spline: jest.fn(),
|
spline: jest.fn(),
|
||||||
@ -61,7 +84,7 @@ describe('EntityLogs', () => {
|
|||||||
const lastPayload =
|
const lastPayload =
|
||||||
capturedQueryRangePayloads[capturedQueryRangePayloads.length - 1];
|
capturedQueryRangePayloads[capturedQueryRangePayloads.length - 1];
|
||||||
|
|
||||||
const queryData = lastPayload?.compositeQuery.builderQueries
|
const queryData = (lastPayload as any)?.compositeQuery?.builderQueries
|
||||||
?.A as IBuilderQuery;
|
?.A as IBuilderQuery;
|
||||||
|
|
||||||
const offset = queryData?.offset ?? 0;
|
const offset = queryData?.offset ?? 0;
|
||||||
@ -126,7 +149,7 @@ describe('EntityLogs', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const firstPayload = capturedQueryRangePayloads[0];
|
const firstPayload = capturedQueryRangePayloads[0];
|
||||||
verifyPayload({
|
verifyEntityLogsPayload({
|
||||||
payload: firstPayload,
|
payload: firstPayload,
|
||||||
expectedOffset: 0,
|
expectedOffset: 0,
|
||||||
});
|
});
|
||||||
@ -138,7 +161,7 @@ describe('EntityLogs', () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const secondPayload = capturedQueryRangePayloads[1];
|
const secondPayload = capturedQueryRangePayloads[1];
|
||||||
const secondQueryData = verifyPayload({
|
const secondQueryData = verifyEntityLogsPayload({
|
||||||
payload: secondPayload,
|
payload: secondPayload,
|
||||||
expectedOffset: 100,
|
expectedOffset: 100,
|
||||||
initialTimeRange,
|
initialTimeRange,
|
||||||
@ -169,7 +192,7 @@ describe('EntityLogs', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const thirdPayload = capturedQueryRangePayloads[2];
|
const thirdPayload = capturedQueryRangePayloads[2];
|
||||||
const thirdQueryData = verifyPayload({
|
const thirdQueryData = verifyEntityLogsPayload({
|
||||||
payload: thirdPayload,
|
payload: thirdPayload,
|
||||||
expectedOffset: 200,
|
expectedOffset: 200,
|
||||||
initialTimeRange,
|
initialTimeRange,
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user