Compare commits

...

86 Commits

Author SHA1 Message Date
ahrefabhi
070decb102 fix(query): added fix for multi value context and in-place replacement 2025-07-01 01:51:22 +05:30
ahrefabhi
0fbe60d68f feat: enhance IQueryPair to support multi-value operators and update query context handling 2025-06-29 17:26:43 +05:30
SagarRajput-7
7e877790d0 feat: new qb selectedfields changes for logs and traces 2025-06-27 18:36:09 +05:30
ahrefabhi
3b2d3c3849 fix: simplify condition for wrapping string values in quotes and comment out query context display 2025-06-27 18:29:28 +05:30
ahrefabhi
815d3b8bc0 feat: enhance query processing to support negation context and improve space handling 2025-06-27 18:29:28 +05:30
ahrefabhi
e529ad8d57 chore: added grammer parity for frontend grammer with main grammer 2025-06-27 18:29:28 +05:30
ahrefabhi
c8f36de6fa feat: add support for negation context in query processing 2025-06-27 18:29:28 +05:30
ahrefabhi
85caefa945 feat: enhance query context to support detection of values wrapped in quotes 2025-06-27 18:29:28 +05:30
ahrefabhi
62d8dd929a chore: removed IS_NULL and IS_NOT_NULL and added NOT_EXISTS 2025-06-27 18:29:28 +05:30
ahrefabhi
afceff33d6 feat: added IS_NULL and IS_NOT_NULL operators and fixed support for not value operators 2025-06-27 18:29:28 +05:30
ahrefabhi
ed77c6abd0 chore: updated grammer for value, added parsetree for finding current context 2025-06-27 18:29:27 +05:30
SagarRajput-7
5575334893 fix: implemented the filter retention across view switch in explorer pages 2025-06-27 18:29:27 +05:30
SagarRajput-7
137a3f6d27 fix: added metricName to the metric where clause keys api 2025-06-27 18:29:27 +05:30
SagarRajput-7
c332a3c48a fix: removed hadrcoded values suggestion for Having Filter 2025-06-27 18:29:27 +05:30
SagarRajput-7
84ff35100a fix: fixed infinite loop of states around operator 2025-06-27 18:29:27 +05:30
SagarRajput-7
67ce050f53 fix: handled qb - order, group and having's vertical expansion 2025-06-27 18:29:27 +05:30
SagarRajput-7
ceefe50d82 fix: removed noop from suggestions and default values 2025-06-27 18:29:26 +05:30
SagarRajput-7
04c9e852e6 fix: fixed metric having clause and traces order sorting 2025-06-27 18:29:26 +05:30
SagarRajput-7
97cd377fa6 feat: new query builder misc fixes (#8359)
* feat: qb fixes

* feat: fixed handlerunquery props

* feat: fixes logs list order by

* feat: fix logs order by issue

* feat: safety check and order by correction

* feat: updated version in new create dashboards

* feat: added new formatOptions for table and fixed the pie chart plotting

* feat: keyboard shortcut overriding issue and pie ch correction in dashboard views

* feat: fixed dashboard data state management across datasource * paneltypes

* feat: fixed explorer pages data management issues

* feat: integrated new backend payload/request diff, to the UI types

* feat: fixed the collapse behaviour of QB - queries

* feat: fix order by and default aggregation to count()
2025-06-27 18:29:26 +05:30
SagarRajput-7
157213defc feat: resolved conflicts 2025-06-27 18:29:25 +05:30
SagarRajput-7
8092df8961 Query builder misc - fixes (#8295)
* feat: trace and logs explorer fixes

* fix: ui fixes

* fix: handle multi arg aggregation

* feat: explorer pages fixes

* feat: added fixes for order by for datasource

* feat: metric order by issue

* feat: support for paneltype selectedview tab switch

* feat: qb v2 compatiblity with url's composite query

* feat: conversion fixes

* feat: where clause and aggregation fix

---------

Co-authored-by: Yunus M <myounis.ar@live.com>
2025-06-27 18:29:25 +05:30
Yunus M
3c895981d9 feat: fetch more keys is complete list not already fetched 2025-06-27 18:29:25 +05:30
SagarRajput-7
a058dac45b feat: query_range migration from v3/v4 -> v5 (#8192)
* feat: query_range migration from v3/v4 -> v5

* feat: cleanup files

* feat: cleanup code

* feat: metric payload improvements

* feat: metric payload improvements

* feat: data retention and qb v2 for dashboard cleanup

* feat: corrected datasource change daata updatation in qb v2

* feat: fix value panel plotting with new query v5

* feat: alert migration

* feat: fixed aggregation css

* feat: explorer pages migration

* feat: trace and logs explorer fixes
2025-06-27 18:29:25 +05:30
Yunus M
a18106f5d8 fix: responsiveness issues 2025-06-27 18:29:24 +05:30
Yunus M
ea88177936 feat: where clause key updates 2025-06-27 18:29:24 +05:30
Yunus M
84a17dd376 feat: update styles for light mode 2025-06-27 18:29:24 +05:30
Yunus M
f699773aec feat: show errors 2025-06-27 18:29:24 +05:30
Yunus M
d46d1a0f24 feat: update context and show suggestions on select 2025-06-27 18:29:24 +05:30
Yunus M
a3b66935d8 feat: add a space after selecting a value from suggestion 2025-06-27 18:29:24 +05:30
Yunus M
1f8c97cd5b feat: improve suggestion ux in query search 2025-06-27 18:29:24 +05:30
Yunus M
2659e03564 feat: ui improvements 2025-06-27 18:29:24 +05:30
Yunus M
121696c1d7 feat: handle close on blur 2025-06-27 18:29:24 +05:30
Yunus M
20be9dd600 feat: query search component clean up 2025-06-27 18:29:24 +05:30
Yunus M
45e4c65c9f feat: handle having option autocomplete ux 2025-06-27 18:29:24 +05:30
Yunus M
b7490fcf68 feat: disable clicking on placeholder items in suggestions 2025-06-27 18:29:24 +05:30
Yunus M
4bfd4e536c feat: improve having suggestions 2025-06-27 18:29:24 +05:30
Yunus M
f7d5a26403 feat: handle add ons 2025-06-27 18:29:24 +05:30
Yunus M
f87594243e feat: handle list panel type options 2025-06-27 18:29:24 +05:30
Yunus M
dacc3d6d9e feat: pass index to query addons 2025-06-27 18:29:24 +05:30
Yunus M
6b28ec2f7f feat: update qb elements based on panel type 2025-06-27 18:29:24 +05:30
Yunus M
9b757af028 feat: hide extra qb elements 2025-06-27 18:29:24 +05:30
Yunus M
ce87bcae71 feat: use qb-v2 in explorers and alerts 2025-06-27 18:29:24 +05:30
Yunus M
25fb8b6561 feat: update explorer views 2025-06-27 18:29:24 +05:30
Yunus M
428a16326a feat: update logs, metrics and traces qb 2025-06-27 18:29:24 +05:30
Yunus M
78fec2188d feat: query builder layout updates 2025-06-27 18:29:24 +05:30
Yunus M
c5650cc131 fix: minor fixes 2025-06-27 18:29:24 +05:30
Yunus M
f67213096c feat: create separate containers for traces, logs and metrics qbs 2025-06-27 18:29:24 +05:30
Yunus M
d71f85a8ec feat: metrics qb 2025-06-27 18:29:24 +05:30
Yunus M
9335261314 fix: update dropdown css 2025-06-27 18:29:24 +05:30
Yunus M
0f5c54cabb feat: remove () from suggestions 2025-06-27 18:29:24 +05:30
Yunus M
0204337396 feat: handle parenthesis and conjunction operators 2025-06-27 18:29:24 +05:30
Yunus M
8101fef874 feat: support multiple having key value pairs 2025-06-27 18:29:24 +05:30
Yunus M
2d223fe9e8 feat: move state to context 2025-06-27 18:29:24 +05:30
Yunus M
de464e6042 feat: handle having options creation 2025-06-27 18:29:24 +05:30
Yunus M
ea42e4db6b feat: hide already used variables 2025-06-27 18:29:24 +05:30
Yunus M
2b5d2f0061 fix: show operator suggestions only on manual trigger or valid key 2025-06-27 18:29:24 +05:30
Yunus M
a013cc0fd3 fix: handle autocomplete 2025-06-27 18:29:24 +05:30
Yunus M
e68d860adf fix: update styles 2025-06-27 18:29:23 +05:30
Yunus M
9ed93ae5ac fix: update css 2025-06-27 18:29:23 +05:30
Yunus M
9989af10d6 feat: handle multie select functions 2025-06-27 18:29:23 +05:30
Yunus M
1bc89c9d1a feat: handle field suggestions for aggregate operators 2025-06-27 18:29:23 +05:30
Yunus M
3fbe111bc0 feat: support aggregation function with values 2025-06-27 18:29:23 +05:30
Yunus M
c449d1da8e feat: add groupBy, having, order by, limit and legend format 2025-06-27 18:29:23 +05:30
Yunus M
4635da0ee8 feat: handle multie select values better 2025-06-27 18:29:23 +05:30
Yunus M
67453e27f7 feat: improve suggestions 2025-06-27 18:29:23 +05:30
Yunus M
fdcc6a6c92 feat: console log context based on cursor position 2025-06-27 18:29:23 +05:30
Yunus M
62c71e6306 fix: handle . notation keywords better 2025-06-27 18:29:23 +05:30
Yunus M
e2e535eaca feat: remove card container above where clause 2025-06-27 18:29:23 +05:30
Yunus M
2520718afb feat: use new qb in logs explorer 2025-06-27 18:29:23 +05:30
Yunus M
0ffa666903 feat: handle parenthesis 2025-06-27 18:29:23 +05:30
Yunus M
c653e83461 feat: handle value selection 2025-06-27 18:29:23 +05:30
Yunus M
b80cf96faf feat: styling updates 2025-06-27 18:29:23 +05:30
Yunus M
a2126ad22c feat: handle string and number values correctly 2025-06-27 18:29:23 +05:30
Yunus M
5a75df30e2 feat: handle async value fetching 2025-06-27 18:29:23 +05:30
Yunus M
aeca98b6aa feat: update the context with additonal properties 2025-06-27 18:29:23 +05:30
Yunus M
53b31ae516 feat: styling updates 2025-06-27 18:29:23 +05:30
Yunus M
209828de01 feat: update theme and syntax highlighting 2025-06-27 18:29:23 +05:30
Yunus M
491a0140e3 feat: handle context switch 2025-06-27 18:29:23 +05:30
Yunus M
b9494a3375 feat: handle multiple spaces 2025-06-27 18:29:23 +05:30
Yunus M
d4b379ccc0 feat: integrate the apis 2025-06-27 18:29:23 +05:30
Yunus M
a7ff27d30c feat: update context logic and return auto-suggestions based on context 2025-06-27 18:29:23 +05:30
Yunus M
6008e8df72 feat: add apis and hooks 2025-06-27 18:29:23 +05:30
Yunus M
27d5e16d18 feat: update context to recognise conjunction operator 2025-06-27 18:29:23 +05:30
Yunus M
24d6b48ad4 feat: add codemirror 2025-06-27 18:29:23 +05:30
Yunus M
78af24b4df feat: add types, base components 2025-06-27 18:29:23 +05:30
Yunus M
45fcf746b0 feat: add antlr4, parser files and grammar 2025-06-27 18:29:23 +05:30
180 changed files with 17993 additions and 1213 deletions

View File

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

View File

@@ -0,0 +1,154 @@
# QuerySearch Component Documentation
## Overview
The QuerySearch component is a sophisticated query builder interface that allows users to construct complex search queries with real-time validation and autocomplete functionality.
## Dependencies
```typescript
// Core UI
import { Card, Collapse, Space, Tag, Typography } from 'antd';
// Code Editor
import {
autocompletion,
CompletionContext,
CompletionResult,
startCompletion,
} from '@codemirror/autocomplete';
import { javascript } from '@codemirror/lang-javascript';
import { ViewPlugin, ViewUpdate } from '@codemirror/view';
import { copilot } from '@uiw/codemirror-theme-copilot';
import CodeMirror, { EditorView, Extension } from '@uiw/react-codemirror';
// Custom Hooks and Utilities
import { useGetQueryKeySuggestions } from 'hooks/querySuggestions/useGetQueryKeySuggestions';
import { getValueSuggestions } from 'api/querySuggestions/getValueSuggestion';
import { queryOperatorSuggestions, validateQuery } from 'utils/antlrQueryUtils';
import { getQueryContextAtCursor } from 'utils/queryContextUtils';
```
## Key Features
1. Real-time query validation
2. Context-aware autocompletion
3. Support for various query operators (=, !=, IN, LIKE, etc.)
4. Support for complex conditions with AND/OR operators
5. Support for functions (HAS, HASANY, HASALL)
6. Support for parentheses and nested conditions
7. Query examples for common use cases
## State Management
```typescript
const [query, setQuery] = useState<string>('');
const [valueSuggestions, setValueSuggestions] = useState<any[]>([]);
const [activeKey, setActiveKey] = useState<string>('');
const [isLoadingSuggestions, setIsLoadingSuggestions] = useState(false);
const [queryContext, setQueryContext] = useState<IQueryContext | null>(null);
const [validation, setValidation] = useState<IValidationResult>({...});
const [editingMode, setEditingMode] = useState<'key' | 'operator' | 'value' | 'conjunction' | 'function' | 'parenthesis' | 'bracketList' | null>(null);
```
## Core Functions
### 1. Autocomplete Handler
```typescript
function myCompletions(context: CompletionContext): CompletionResult | null {
// Handles autocomplete suggestions based on context
// Supports different contexts: key, operator, value, function, etc.
}
```
### 2. Value Suggestions Fetcher
```typescript
const fetchValueSuggestions = useCallback(
async (key: string): Promise<void> => {
// Fetches value suggestions for a given key
// Handles loading states and error cases
},
[activeKey, isLoadingSuggestions],
);
```
### 3. Query Change Handler
```typescript
const handleQueryChange = useCallback(async (newQuery: string) => {
// Updates query and validates it
// Handles validation errors
}, []);
```
## Query Context Types
1. Key context: When editing a field name
2. Operator context: When selecting an operator
3. Value context: When entering a value
4. Conjunction context: When using AND/OR
5. Function context: When using functions
6. Parenthesis context: When using parentheses
7. Bracket list context: When using IN operator
## Example Queries
```typescript
const queryExamples = [
{ label: 'Basic Query', query: "status = 'error'" },
{ label: 'Multiple Conditions', query: "status = 'error' AND service = 'frontend'" },
{ label: 'IN Operator', query: "status IN ['error', 'warning']" },
{ label: 'Function Usage', query: "HAS(service, 'frontend')" },
{ label: 'Numeric Comparison', query: 'duration > 1000' },
// ... more examples
];
```
## Performance Optimizations
1. Uses `useCallback` for memoized functions
2. Tracks component mount state to prevent updates after unmount
3. Debounces suggestion fetching
4. Caches key suggestions
## Error Handling
```typescript
try {
const validationResponse = validateQuery(newQuery);
setValidation(validationResponse);
} catch (error) {
setValidation({
isValid: false,
message: 'Failed to process query',
errors: [error as IDetailedError],
});
}
```
## Usage Example
```typescript
<QuerySearch />
```
## Styling
- Uses SCSS for styling
- Custom classes for different components
- Theme integration with CodeMirror
## Best Practices
1. Always validate queries before submission
2. Handle loading states appropriately
3. Provide clear error messages
4. Use appropriate operators for different data types
5. Consider performance implications of complex queries
## Common Issues and Solutions
1. Query validation errors
- Check syntax and operator usage
- Verify data types match operator requirements
2. Performance issues
- Optimize suggestion fetching
- Cache frequently used values
3. UI/UX issues
- Ensure clear error messages
- Provide helpful suggestions
- Show appropriate loading states
## Future Improvements
1. Add more query examples
2. Enhance error messages
3. Improve performance for large datasets
4. Add more operator support
5. Enhance UI/UX features

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,17 @@
import axios from 'api';
import { AxiosResponse } from 'axios';
import {
QueryKeyRequestProps,
QueryKeySuggestionsResponseProps,
} from 'types/api/querySuggestions/types';
export const getKeySuggestions = (
props: QueryKeyRequestProps,
): Promise<AxiosResponse<QueryKeySuggestionsResponseProps>> =>
axios.get(
`/fields/keys?signal=${props.signal}&searchText=${
props.searchText
}&metricName=${props.metricName ?? ''}&fieldContext=${
props.fieldContext ?? ''
}&fieldDataType=${props.fieldDataType ?? ''}`,
);

View File

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

View File

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

View File

@@ -0,0 +1,239 @@
import { isEmpty } from 'lodash-es';
import { SuccessResponse } from 'types/api';
import { MetricRangePayloadV3 } from 'types/api/metrics/getQueryRange';
import {
DistributionData,
MetricRangePayloadV5,
RawData,
ScalarData,
TimeSeriesData,
} from 'types/api/v5/queryRange';
import { QueryDataV3 } from 'types/api/widgets/getQuery';
/**
* Converts V5 TimeSeriesData to legacy format
*/
function convertTimeSeriesData(
timeSeriesData: TimeSeriesData,
legendMap: Record<string, string>,
): QueryDataV3 {
// Convert V5 time series format to legacy QueryDataV3 format
return {
queryName: timeSeriesData.queryName,
legend: legendMap[timeSeriesData.queryName] || timeSeriesData.queryName,
series: timeSeriesData?.aggregations?.flatMap((aggregation) =>
aggregation.series.map((series) => ({
labels: series.labels
? Object.fromEntries(
series.labels.map((label) => [label.key.name, label.value]),
)
: {},
labelsArray: series.labels
? series.labels.map((label) => ({ [label.key.name]: label.value }))
: [],
values: series.values.map((value) => ({
timestamp: value.timestamp,
value: String(value.value),
})),
})),
),
list: null,
};
}
/**
* Converts V5 ScalarData array to legacy format with table structure
*/
function convertScalarDataArrayToTable(
scalarDataArray: ScalarData[],
legendMap: Record<string, string>,
): QueryDataV3[] {
// If no scalar data, return empty structure
if (!scalarDataArray || scalarDataArray.length === 0) {
return [];
}
// 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 || '';
// Collect columns for this specific query
const columns = scalarData?.columns?.map((col) => ({
name: col.columnType === 'aggregation' ? col.queryName : col.name,
queryName: col.queryName,
isValueColumn: col.columnType === 'aggregation',
}));
// Process rows for this specific query
const rows = scalarData?.data?.map((dataRow) => {
const rowData: Record<string, any> = {};
scalarData?.columns?.forEach((col, colIndex) => {
const columnName =
col.columnType === 'aggregation' ? col.queryName : col.name;
rowData[columnName] = dataRow[colIndex];
});
return { data: rowData };
});
return {
queryName,
legend: legendMap[queryName] || '',
series: null,
list: null,
table: {
columns,
rows,
},
};
});
}
/**
* Converts V5 RawData to legacy format
*/
function convertRawData(
rawData: RawData,
legendMap: Record<string, string>,
): QueryDataV3 {
// Convert V5 raw format to legacy QueryDataV3 format
return {
queryName: rawData.queryName,
legend: legendMap[rawData.queryName] || rawData.queryName,
series: null,
list: rawData.rows?.map((row) => ({
timestamp: row.timestamp,
data: {
// Map raw data to ILog structure - spread row.data first to include all properties
...row.data,
date: row.timestamp,
} as any,
})),
};
}
/**
* Converts V5 DistributionData to legacy format
*/
function convertDistributionData(
distributionData: DistributionData,
legendMap: Record<string, string>,
): any {
// eslint-disable-line @typescript-eslint/no-explicit-any
// Convert V5 distribution format to legacy histogram format
return {
...distributionData,
legendMap,
};
}
/**
* Helper function to convert V5 data based on type
*/
function convertV5DataByType(
v5Data: any,
legendMap: Record<string, string>,
): MetricRangePayloadV3['data'] {
switch (v5Data?.type) {
case 'time_series': {
const timeSeriesData = v5Data.data.results as TimeSeriesData[];
return {
resultType: 'time_series',
result: timeSeriesData.map((timeSeries) =>
convertTimeSeriesData(timeSeries, legendMap),
),
};
}
case 'scalar': {
const scalarData = v5Data.data.results as ScalarData[];
// For scalar data, combine all results into separate table entries
const combinedTables = convertScalarDataArrayToTable(scalarData, legendMap);
return {
resultType: 'scalar',
result: combinedTables,
};
}
case 'raw': {
const rawData = v5Data.data.results as RawData[];
return {
resultType: 'raw',
result: rawData.map((raw) => convertRawData(raw, legendMap)),
};
}
case 'distribution': {
const distributionData = v5Data.data.results as DistributionData[];
return {
resultType: 'distribution',
result: distributionData.map((distribution) =>
convertDistributionData(distribution, legendMap),
),
};
}
default:
return {
resultType: '',
result: [],
};
}
}
/**
* Converts V5 API response to legacy format expected by frontend components
*/
export function convertV5ResponseToLegacy(
v5Response: SuccessResponse<MetricRangePayloadV5>,
legendMap: Record<string, string>,
// formatForWeb?: boolean,
): SuccessResponse<MetricRangePayloadV3> {
const { payload } = v5Response;
const v5Data = payload?.data;
// todo - sagar
// If formatForWeb is true, return as-is (like existing logic)
// Exception: scalar data should always be converted to table format
// if (formatForWeb && v5Data?.type !== 'scalar') {
// return v5Response as any;
// }
// Convert based on V5 response type
const convertedData = convertV5DataByType(v5Data, legendMap);
// Create legacy-compatible response structure
const legacyResponse: SuccessResponse<MetricRangePayloadV3> = {
...v5Response,
payload: {
data: convertedData,
},
};
// Apply legend mapping (similar to existing logic)
if (legacyResponse.payload?.data?.result) {
legacyResponse.payload.data.result = legacyResponse.payload.data.result.map(
(queryData: any) => {
// eslint-disable-line @typescript-eslint/no-explicit-any
const newQueryData = queryData;
newQueryData.legend = legendMap[queryData.queryName];
// If metric names is an empty object
if (isEmpty(queryData.metric)) {
// If metrics list is empty && the user haven't defined a legend then add the legend equal to the name of the query.
if (newQueryData.legend === undefined || newQueryData.legend === null) {
newQueryData.legend = queryData.queryName;
}
// If name of the query and the legend if inserted is same then add the same to the metrics object.
if (queryData.queryName === newQueryData.legend) {
newQueryData.metric = newQueryData.metric || {};
newQueryData.metric[queryData.queryName] = queryData.queryName;
}
}
return newQueryData;
},
);
}
return legacyResponse;
}

View File

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

View File

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

View File

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

View File

@@ -169,6 +169,7 @@
box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.1);
}
}
.ant-drawer-close {
padding: 0px;
}

View File

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

View File

@@ -0,0 +1,71 @@
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,
}: {
label: string;
initialValue?: string | number;
placeholder: string;
type?: string;
onClose?: () => void;
labelAfter?: boolean;
onChange: (value: string) => void;
className?: string;
}): 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={<X size={16} />}
onClick={onClose}
/>
)}
</div>
);
}
InputWithLabel.defaultProps = {
type: 'text',
onClose: undefined,
labelAfter: false,
initialValue: undefined,
className: undefined,
};
export default InputWithLabel;

View File

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

View File

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

View File

@@ -0,0 +1,185 @@
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}
/>
</div>
);
})}
</div>
)}
{!showOnlyWhereClause && !isListViewPanel && (
<QueryFooter
addNewBuilderQuery={addNewBuilderQuery}
addNewFormula={addNewFormula}
/>
)}
</div>
{!showOnlyWhereClause && !isListViewPanel && (
<div className="query-names-section">
{currentQuery.builder.queryData.map((query) => (
<div key={query.queryName} className="query-name">
{query.queryName}
</div>
))}
{currentQuery.builder.queryFormulas.map((formula) => (
<div key={formula.queryName} className="formula-name">
{formula.queryName}
</div>
))}
</div>
)}
</div>
</QueryBuilderV2Provider>
);
});

View File

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

View File

@@ -0,0 +1,97 @@
.metrics-aggregate-section {
display: flex;
flex-direction: column;
gap: 16px;
margin: 4px 0;
.metrics-time-aggregation-section {
display: flex;
flex-direction: column;
gap: 12px;
.metrics-time-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-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;
.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;
}
.metrics-aggregation-section-content-item-value {
min-width: 320px;
.ant-select {
width: 100%;
}
.ant-select-selector {
border-radius: 2px;
border: 1.005px solid var(--Slate-400, #1d212d);
background: var(--Ink-300, #16181d);
}
}
}
}
}
.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;
}

View File

@@ -0,0 +1,168 @@
import './MetricsAggregateSection.styles.scss';
import { Tooltip } from 'antd';
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 { 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,
});
useEffect(() => {
setAggregationOptions([
{
func: query.spaceAggregation || 'count',
arg: query.aggregateAttribute.key || '',
},
]);
}, [
query.spaceAggregation,
query.aggregateAttribute.key,
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 =
!query?.aggregateAttribute.key || query?.aggregateAttribute.key === '';
return (
<div className="metrics-aggregate-section">
<div className="metrics-time-aggregation-section">
<div className="metrics-time-aggregation-section-title">
AGGREGATE BY TIME{' '}
<Tooltip title="AGGREGATE BY TIME">
<Info size={12} />
</Tooltip>
</div>
<div className="metrics-aggregation-section-content">
<div className="metrics-aggregation-section-content-item">
<div className="metrics-aggregation-section-content-item-label">
Align with
</div>
<div className="metrics-aggregation-section-content-item-value">
<OperatorsSelect
value={query.aggregateOperator}
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">
aggregated every
</div>
<div className="metrics-aggregation-section-content-item-value">
<InputWithLabel
onChange={handleChangeAggregateEvery}
label="Seconds"
placeholder="Enter a number"
labelAfter
initialValue={query?.stepInterval ?? undefined}
/>
</div>
</div>
)}
</div>
</div>
<div className="metrics-space-aggregation-section">
<div className="metrics-space-aggregation-section-title">
AGGREGATE LABELS
<Tooltip title="AGGREGATE LABELS">
<Info size={12} />
</Tooltip>
</div>
<div className="metrics-aggregation-section-content">
<div className="metrics-aggregation-section-content-item">
<div className="metrics-aggregation-section-content-item-value space-aggregation-select">
<SpaceAggregationOptions
panelType={panelType}
key={`${panelType}${query.spaceAggregation}${query.timeAggregation}`}
aggregatorAttributeType={
query?.aggregateAttribute.type as ATTRIBUTE_TYPES
}
selectedValue={query.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">
<GroupByFilter
disabled={!query.aggregateAttribute.key}
query={query}
onChange={handleChangeGroupByKeys}
/>
</div>
</div>
</div>
</div>
</div>
);
});
export default MetricsAggregateSection;

View File

@@ -0,0 +1,42 @@
.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;
}
}
}
}

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,340 @@
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 { useQueryOperations } from 'hooks/queryBuilder/useQueryBuilderOperations';
import { isEmpty } from 'lodash-es';
import { BarChart2, ScrollText, X } from 'lucide-react';
import { useCallback, useEffect, useState } from 'react';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import { DataSource } 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: '',
});
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 handleChangeReduceTo = useCallback(
(value: IBuilderQuery['reduceTo']) => {
handleChangeQueryData('reduceTo', value);
},
[handleChangeQueryData],
);
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('havingExpression', {
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.aggregateAttribute.key
}
query={query}
onChange={handleChangeGroupByKeys}
/>
</div>
<Button
className="close-btn periscope-btn ghost"
icon={<X 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'));
}}
/>
</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={<X 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={handleChangeReduceTo} />
</div>
<Button
className="close-btn periscope-btn ghost"
icon={<X 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'),
);
}}
/>
</div>
)}
</div>
)}
<div className="add-ons-list">
<Radio.Group
className="add-ons-tabs"
onChange={handleOptionClick}
value={selectedViews}
>
{addOns.map((addOn) => (
<Radio.Button
key={addOn.label}
className={
selectedViews.find((view) => view.key === addOn.key)
? 'selected-view tab'
: 'tab'
}
value={addOn}
>
<div className="add-on-tab-title">
{addOn.icon}
{addOn.label}
</div>
</Radio.Button>
))}
</Radio.Group>
</div>
</div>
);
}
export default QueryAddOns;

View File

@@ -0,0 +1,300 @@
.query-aggregation-container {
display: block;
position: relative;
.aggregation-container {
display: flex;
flex-direction: row;
gap: 8px;
align-items: flex-start;
flex-wrap: wrap;
.query-aggregation-select-container {
flex: 1;
min-width: 400px;
}
.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;
}
}
}
}
.query-aggregation-select-container {
width: 100%;
display: flex;
flex-direction: row;
align-items: center;
flex: 1;
min-width: 400px;
.query-aggregation-select-editor {
border-radius: 2px;
flex: 1;
min-width: 0;
.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;
}
}
}
.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;
}
}
}
}
.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;
}
&[aria-selected='true'] {
background: var(--bg-vanilla-300) !important;
color: var(--bg-ink-500) !important;
}
}
}
}
.cm-line {
background-color: var(--bg-vanilla-100) !important;
::-moz-selection {
background: var(--bg-vanilla-100) !important;
opacity: 0.5 !important;
}
::selection {
background: var(--bg-vanilla-100) !important;
opacity: 0.5 !important;
}
.cm-function {
color: var(--bg-robin-500) !important;
}
.chip-decorator {
background: var(--bg-robin-500) !important;
color: var(--bg-ink-400) !important;
}
}
// .cm-selectionBackground {
// background: var(--bg-vanilla-100) !important;
// opacity: 0.5 !important;
// }
}
}
.close-btn {
border-color: var(--bg-vanilla-300) !important;
background: var(--bg-vanilla-100) !important;
}
}
}
}
}

View File

@@ -0,0 +1,75 @@
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} />
{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
onClose={(): void => {}}
/>
</div>
</div>
)}
</div>
</div>
);
}
QueryAggregationOptions.defaultProps = {
panelType: null,
onChange: undefined,
};
export default QueryAggregationOptions;

View File

@@ -0,0 +1,530 @@
/* 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 { RangeSetBuilder } from '@codemirror/state';
import { copilot } from '@uiw/codemirror-theme-copilot';
import CodeMirror, {
Decoration,
EditorView,
keymap,
ViewPlugin,
ViewUpdate,
} from '@uiw/react-codemirror';
import { getAggregateAttribute } from 'api/queryBuilder/getAggregateAttribute';
import { QueryBuilderKeys } from 'constants/queryBuilder';
import { tracesAggregateOperatorOptions } from 'constants/queryBuilderOperators';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useQuery } from 'react-query';
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import { TracesAggregatorOperator } from 'types/common/queryBuilder';
import { useQueryBuilderV2Context } from '../../QueryBuilderV2Context';
const chipDecoration = Decoration.mark({
class: 'chip-decorator',
});
const operatorArgMeta: Record<
string,
{ acceptsArgs: boolean; multiple: boolean }
> = {
[TracesAggregatorOperator.NOOP]: { acceptsArgs: false, multiple: false },
[TracesAggregatorOperator.COUNT]: { acceptsArgs: false, multiple: false },
[TracesAggregatorOperator.COUNT_DISTINCT]: {
acceptsArgs: true,
multiple: true,
},
[TracesAggregatorOperator.SUM]: { acceptsArgs: true, multiple: false },
[TracesAggregatorOperator.AVG]: { acceptsArgs: true, multiple: false },
[TracesAggregatorOperator.MAX]: { acceptsArgs: true, multiple: false },
[TracesAggregatorOperator.MIN]: { acceptsArgs: true, multiple: false },
[TracesAggregatorOperator.P05]: { acceptsArgs: true, multiple: false },
[TracesAggregatorOperator.P10]: { acceptsArgs: true, multiple: false },
[TracesAggregatorOperator.P20]: { acceptsArgs: true, multiple: false },
[TracesAggregatorOperator.P25]: { acceptsArgs: true, multiple: false },
[TracesAggregatorOperator.P50]: { acceptsArgs: true, multiple: false },
[TracesAggregatorOperator.P75]: { acceptsArgs: true, multiple: false },
[TracesAggregatorOperator.P90]: { acceptsArgs: true, multiple: false },
[TracesAggregatorOperator.P95]: { acceptsArgs: true, multiple: false },
[TracesAggregatorOperator.P99]: { acceptsArgs: true, multiple: false },
[TracesAggregatorOperator.RATE]: { acceptsArgs: true, multiple: false },
[TracesAggregatorOperator.RATE_SUM]: { acceptsArgs: true, multiple: false },
[TracesAggregatorOperator.RATE_AVG]: { acceptsArgs: true, multiple: false },
[TracesAggregatorOperator.RATE_MIN]: { acceptsArgs: true, multiple: false },
[TracesAggregatorOperator.RATE_MAX]: { acceptsArgs: true, multiple: false },
};
function getFunctionContextAtCursor(
text: string,
cursorPos: number,
): string | null {
// Find the nearest function name to the left of the nearest unmatched '('
let openParenIndex = -1;
let funcName: string | null = null;
let parenStack = 0;
for (let i = cursorPos - 1; i >= 0; i--) {
if (text[i] === ')') parenStack++;
else if (text[i] === '(') {
if (parenStack === 0) {
openParenIndex = i;
const before = text.slice(0, i);
const match = before.match(/(\w+)\s*$/);
if (match) funcName = match[1].toLowerCase();
break;
}
parenStack--;
}
}
if (openParenIndex === -1 || !funcName) return null;
// Scan forwards to find the matching closing parenthesis
let closeParenIndex = -1;
let depth = 1;
for (let j = openParenIndex + 1; j < text.length; j++) {
if (text[j] === '(') depth++;
else if (text[j] === ')') depth--;
if (depth === 0) {
closeParenIndex = j;
break;
}
}
if (
cursorPos > openParenIndex &&
(closeParenIndex === -1 || cursorPos <= closeParenIndex)
) {
return funcName;
}
return null;
}
// Custom extension to stop events from propagating to global shortcuts
const stopEventsExtension = EditorView.domEventHandlers({
keydown: (event) => {
// Stop all keyboard events from propagating to global shortcuts
event.stopPropagation();
event.stopImmediatePropagation();
return false; // Important for CM to know you handled it
},
input: (event) => {
event.stopPropagation();
return false;
},
focus: (event) => {
// Ensure focus events don't interfere with global shortcuts
event.stopPropagation();
return false;
},
blur: (event) => {
// Ensure blur events don't interfere with global shortcuts
event.stopPropagation();
return false;
},
});
// eslint-disable-next-line react/no-this-in-sfc
function QueryAggregationSelect({
onChange,
queryData,
}: {
onChange?: (value: string) => void;
queryData: IBuilderQuery;
}): JSX.Element {
const { setAggregationOptions } = useQueryBuilderV2Context();
const [input, setInput] = useState(
queryData?.aggregations?.map((i: any) => i.expression).join(' ') || '',
);
useEffect(() => {
setInput(
queryData?.aggregations?.map((i: any) => i.expression).join(' ') || '',
);
}, [queryData?.aggregations]);
const [cursorPos, setCursorPos] = useState(0);
const [functionArgPairs, setFunctionArgPairs] = useState<
{ func: string; arg: string }[]
>([]);
const editorRef = useRef<EditorView | null>(null);
const [isFocused, setIsFocused] = useState(false);
// 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 });
});
}
}
setFunctionArgPairs(pairs);
setAggregationOptions(pairs);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [input]);
// Find function context for fetching suggestions
const functionContextForFetch = getFunctionContextAtCursor(input, cursorPos);
const { data: aggregateAttributeData, isLoading: isLoadingFields } = useQuery(
[
QueryBuilderKeys.GET_AGGREGATE_ATTRIBUTE,
functionContextForFetch,
queryData.dataSource,
],
() =>
getAggregateAttribute({
searchText: '',
aggregateOperator: functionContextForFetch as string,
dataSource: queryData.dataSource,
}),
{
enabled:
!!functionContextForFetch &&
!!operatorArgMeta[functionContextForFetch]?.acceptsArgs,
},
);
// Get valid function names (lowercase)
const validFunctions = useMemo(
() => tracesAggregateOperatorOptions.map((op) => op.value.toLowerCase()),
[],
);
// 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(
() =>
aggregateAttributeData?.payload?.attributeKeys?.map(
(attributeKey: BaseAutocompleteData) => ({
label: attributeKey.key,
type: 'variable',
info: attributeKey.dataType,
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);
// 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],
);
return (
<div className="query-aggregation-select-container">
<CodeMirror
value={input}
onChange={(value): void => {
setInput(value);
onChange?.(value);
}}
className="query-aggregation-select-editor"
theme={copilot}
extensions={[
chipPlugin,
aggregatorAutocomplete,
javascript({ jsx: false, typescript: false }),
EditorView.lineWrapping,
stopEventsExtension,
keymap.of([
...completionKeymap,
{
key: 'Escape',
run: closeCompletion,
},
]),
]}
placeholder="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);
}
}}
/>
</div>
);
}
QueryAggregationSelect.defaultProps = {
onChange: undefined,
};
export default QueryAggregationSelect;

View File

@@ -0,0 +1,35 @@
import { Button } 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">
<Button
className="add-new-query-button periscope-btn secondary"
type="text"
icon={<Plus size={16} />}
onClick={addNewBuilderQuery}
/>
</div>
<div className="qb-add-formula">
<Button
className="add-formula-button periscope-btn secondary"
icon={<Sigma size={16} />}
onClick={addNewFormula}
>
Add Formula
</Button>
</div>
</div>
</div>
);
}

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -0,0 +1,234 @@
import { Dropdown } from 'antd';
import cx from 'classnames';
import { ENTITY_VERSION_V4 } 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 ||
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="v4" />
</div>
)}
<div className="qb-search-filter-container">
<div className="query-search-container">
<QuerySearch
key={`query-search-${query.queryName}-${query.dataSource}`}
onChange={handleSearchChange}
queryData={query}
dataSource={dataSource}
/>
</div>
{showSpanScopeSelector && (
<div className="traces-search-filter-container">
<div className="traces-search-filter-in">in</div>
<SpanScopeSelector query={query} />
</div>
)}
</div>
</div>
{!showOnlyWhereClause &&
!isListViewPanel &&
dataSource !== DataSource.METRICS && (
<QueryAggregation
dataSource={dataSource}
key={`query-search-${query.queryName}-${query.dataSource}`}
panelType={panelType || undefined}
onAggregationIntervalChange={handleChangeAggregateEvery}
onChange={handleChangeAggregation}
queryData={query}
/>
)}
{!showOnlyWhereClause && dataSource === DataSource.METRICS && (
<MetricsAggregateSection
panelType={panelType}
query={query}
index={index}
key={`metrics-aggregate-section-${query.queryName}-${query.dataSource}`}
version="v4"
/>
)}
{!showOnlyWhereClause && (
<QueryAddOns
index={index}
query={query}
version="v3"
isListViewPanel={isListViewPanel}
showReduceTo={showReduceTo}
panelType={panelType}
/>
)}
</div>
)}
</div>
</div>
);
});

View File

@@ -0,0 +1,185 @@
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { Having, TagFilter } from 'types/api/queryBuilder/queryBuilderData';
import {
LogAggregation,
MetricAggregation,
TraceAggregation,
} from 'types/api/v5/queryRange';
import { DataSource } from 'types/common/queryBuilder';
/**
* 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', 'nin', '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 '),
};
};
/**
* 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,
];
};

View File

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

View File

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

View File

@@ -169,6 +169,9 @@ export const initialQueryBuilderFormValues: IBuilderQuery = {
aggregateAttribute: initialAutocompleteData,
timeAggregation: MetricAggregateOperator.RATE,
spaceAggregation: MetricAggregateOperator.SUM,
filter: { expression: '' },
aggregations: [{ expression: 'count() ' }],
havingExpression: { expression: '' },
functions: [],
filters: { items: [], op: 'AND' },
expression: createNewBuilderItemName({

View File

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

View File

@@ -54,19 +54,21 @@ function DomainList(): JSX.Element {
// initialise tab with default query.
useShareBuilderUrl({
...initialQueriesMap.traces,
builder: {
...initialQueriesMap.traces.builder,
queryData: [
{
...initialQueriesMap.traces.builder.queryData[0],
dataSource: DataSource.TRACES,
aggregateOperator: 'noop',
aggregateAttribute: {
...initialQueriesMap.traces.builder.queryData[0].aggregateAttribute,
defaultValue: {
...initialQueriesMap.traces,
builder: {
...initialQueriesMap.traces.builder,
queryData: [
{
...initialQueriesMap.traces.builder.queryData[0],
dataSource: DataSource.TRACES,
aggregateOperator: 'noop',
aggregateAttribute: {
...initialQueriesMap.traces.builder.queryData[0].aggregateAttribute,
},
},
},
],
],
},
},
});

View File

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

View File

@@ -1,12 +1,14 @@
.explorer-options-container {
position: fixed;
bottom: 24px;
bottom: 8px;
left: calc(50% + 240px);
transform: translate(calc(-50% - 120px), 0);
transition: left 0.2s linear;
border-radius: 6px;
background: var(--Ink-300, #16181d);
display: flex;
gap: 16px;
background-color: transparent;
.multi-alert-button,
@@ -32,19 +34,15 @@
.explorer-update {
display: inline-flex;
align-items: center;
gap: 12px;
padding: 10px 10px;
border-radius: 50px;
border: 1px solid var(--bg-slate-400);
background: rgba(22, 24, 29, 0.6);
backdrop-filter: blur(20px);
gap: 4px;
padding: 8px;
background: var(--Ink-300, #16181d);
.action-icon {
display: flex;
justify-content: center;
align-items: center;
padding: 8px;
border-radius: 50px;
padding: 6px;
border: 1px solid var(--bg-slate-400);
background: var(--bg-slate-500);
cursor: pointer;
@@ -64,10 +62,8 @@
.explorer-options {
padding: 10px 12px;
border: 1px solid var(--bg-slate-400);
border-radius: 50px;
background: rgba(22, 24, 29, 0.6);
backdrop-filter: blur(20px);
background: var(--Ink-300, #16181d);
border-radius: 2px;
cursor: default;
display: flex;
@@ -96,27 +92,6 @@
align-items: center;
gap: 8px;
button {
display: flex;
justify-content: center;
align-items: center;
border: none;
border: 1px solid #1d2023;
box-shadow: none !important;
&.ant-btn-round {
padding: 8px 12px 8px 10px;
font-weight: 500;
}
&.ant-btn-round:disabled {
background-color: rgba(209, 209, 209, 0.074);
color: #5f5f5f;
}
}
.ant-select-focused {
border-color: transparent !important;
@@ -257,6 +232,10 @@
}
.lightMode {
.explorer-options-container {
background: var(--bg-vanilla-100);
}
.explorer-options {
background: transparent;
box-shadow: none;

View File

@@ -1,12 +1,10 @@
/* eslint-disable react/jsx-props-no-spreading */
import './ExplorerOptions.styles.scss';
import { InfoCircleOutlined } from '@ant-design/icons';
import { Color } from '@signozhq/design-tokens';
import {
Button,
ColorPicker,
Divider,
Input,
Modal,
RefSelectProps,
@@ -15,6 +13,7 @@ import {
Typography,
} from 'antd';
import logEvent from 'api/common/logEvent';
import { TelemetryFieldKey } from 'api/v5/v5';
import axios from 'axios';
import cx from 'classnames';
import { getViewDetailsUsingViewKey } from 'components/ExplorerCard/utils';
@@ -45,14 +44,7 @@ import { useHandleExplorerTabChange } from 'hooks/useHandleExplorerTabChange';
import { useNotifications } from 'hooks/useNotifications';
import { mapCompositeQueryFromQuery } from 'lib/newQueryBuilder/queryBuilderMappers/mapCompositeQueryFromQuery';
import { cloneDeep, isEqual, omit } from 'lodash-es';
import {
Check,
ConciergeBell,
Disc3,
PanelBottomClose,
Plus,
X,
} from 'lucide-react';
import { Check, ConciergeBell, Disc3, Plus, X } from 'lucide-react';
import { useAppContext } from 'providers/App/App';
import { FormattingOptions } from 'providers/preferences/types';
import {
@@ -67,7 +59,6 @@ import {
} from 'react';
import { useHistory } from 'react-router-dom';
import { Dashboard } from 'types/api/dashboard/getAll';
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
import { ViewProps } from 'types/api/saveViews/types';
import { DataSource, StringOperators } from 'types/common/queryBuilder';
@@ -78,10 +69,8 @@ import ExplorerOptionsHideArea from './ExplorerOptionsHideArea';
import { PreservedViewsInLocalStorage } from './types';
import {
DATASOURCE_VS_ROUTES,
generateRGBAFromHex,
getRandomColor,
saveNewViewHandler,
setExplorerToolBarVisibility,
} from './utils';
const allowedRoles = [USER_ROLES.ADMIN, USER_ROLES.AUTHOR, USER_ROLES.EDITOR];
@@ -253,12 +242,6 @@ function ExplorerOptions({
const extraData = viewsData?.data?.data?.find((view) => view.id === viewKey)
?.extraData;
const extraDataColor = extraData ? JSON.parse(extraData).color : '';
const rgbaColor = generateRGBAFromHex(
extraDataColor || Color.BG_SIENNA_500,
0.08,
);
const { options, handleOptionsChange } = useOptionsMenu({
storageKey:
sourcepage === DataSource.TRACES
@@ -270,7 +253,7 @@ function ExplorerOptions({
const getUpdatedExtraData = (
extraData: string | undefined,
newSelectedColumns: BaseAutocompleteData[],
newSelectedColumns: TelemetryFieldKey[],
formattingOptions?: FormattingOptions,
): string => {
let updatedExtraData;
@@ -354,7 +337,7 @@ function ExplorerOptions({
const { handleExplorerTabChange } = useHandleExplorerTabChange();
type ExtraData = {
selectColumns?: BaseAutocompleteData[];
selectColumns?: TelemetryFieldKey[];
version?: number;
};
@@ -589,13 +572,6 @@ function ExplorerOptions({
[isDarkMode],
);
const hideToolbar = (): void => {
setExplorerToolBarVisibility(false, sourcepage);
if (setIsExplorerOptionHidden) {
setIsExplorerOptionHidden(true);
}
};
const isEditDeleteSupported = allowedRoles.includes(user.role as string);
const [
@@ -647,27 +623,6 @@ function ExplorerOptions({
viewsData?.data?.data,
]);
const infoIconText = useMemo(() => {
if (isLogsExplorer) {
return 'Learn more about Logs explorer';
}
if (isMetricsExplorer) {
return 'Learn more about Metrics explorer';
}
return 'Learn more about Traces explorer';
}, [isLogsExplorer, isMetricsExplorer]);
const infoIconLink = useMemo(() => {
if (isLogsExplorer) {
return 'https://signoz.io/docs/product-features/logs-explorer/?utm_source=product&utm_medium=logs-explorer-toolbar';
}
// TODO: Add metrics explorer info icon link
if (isMetricsExplorer) {
return '';
}
return 'https://signoz.io/docs/product-features/trace-explorer/?utm_source=product&utm_medium=trace-explorer-toolbar';
}, [isLogsExplorer, isMetricsExplorer]);
const getQueryName = (query: Query): string => {
if (query.builder.queryFormulas.length > 0) {
return `Formula ${query.builder.queryFormulas[0].queryName}`;
@@ -680,11 +635,10 @@ function ExplorerOptions({
const selectLabel = (
<Button
disabled={disabled}
shape="round"
className="periscope-btn ghost"
shape="default"
icon={<ConciergeBell size={16} />}
>
Create an Alert
</Button>
/>
);
return (
<Select
@@ -713,12 +667,11 @@ function ExplorerOptions({
return (
<Button
disabled={disabled}
shape="round"
shape="default"
className="periscope-btn ghost"
onClick={(): void => onCreateAlertsHandler(query)}
icon={<ConciergeBell size={16} />}
>
Create an Alert
</Button>
/>
);
}, [
disabled,
@@ -732,14 +685,11 @@ function ExplorerOptions({
if (isOneChartPerQuery) {
const selectLabel = (
<Button
type="primary"
className="periscope-btn ghost"
disabled={disabled}
shape="round"
onClick={onAddToDashboard}
icon={<Plus size={16} />}
>
Add to Dashboard
</Button>
icon={<Plus size={12} />}
/>
);
return (
<Select
@@ -771,14 +721,11 @@ function ExplorerOptions({
}
return (
<Button
type="primary"
className="periscope-btn ghost"
disabled={disabled}
shape="round"
onClick={onAddToDashboard}
icon={<Plus size={16} />}
>
Add to Dashboard
</Button>
/>
);
}, [disabled, isOneChartPerQuery, onAddToDashboard, splitedQueries]);
@@ -797,41 +744,31 @@ function ExplorerOptions({
>
<Tooltip title="Clear this view" placement="top">
<Button
className="action-icon"
className="periscope-btn ghost"
onClick={handleClearSelect}
icon={<X size={14} />}
icon={<X size={16} />}
/>
</Tooltip>
{
// only show the update view option when the query is updated
}
{isQueryUpdated && (
<>
<Divider
type="vertical"
className={isEditDeleteSupported ? '' : 'hidden'}
<Tooltip title="Update this view" placement="top">
<Button
className={cx(
'periscope-btn ghost',
isEditDeleteSupported ? '' : 'hidden',
)}
disabled={isViewUpdating}
onClick={onUpdateQueryHandler}
icon={<Disc3 size={16} />}
/>
<Tooltip title="Update this view" placement="top">
<Button
className={cx('action-icon', isEditDeleteSupported ? ' ' : 'hidden')}
disabled={isViewUpdating}
onClick={onUpdateQueryHandler}
icon={<Disc3 size={14} />}
/>
</Tooltip>
</>
</Tooltip>
)}
</div>
)}
{!isExplorerOptionHidden && (
<div
className="explorer-options"
style={{
background: extraData
? `linear-gradient(90deg, rgba(0,0,0,0) -5%, ${rgbaColor} 9%, rgba(0,0,0,0) 30%)`
: 'transparent',
}}
>
<div className="explorer-options">
<div className="view-options">
<Select<string, { key: string; value: string }>
showSearch
@@ -872,49 +809,23 @@ function ExplorerOptions({
</Select>
<Button
shape="round"
shape="default"
className={cx(
'periscope-btn secondary',
isEditDeleteSupported ? '' : 'hidden',
)}
onClick={handleSaveViewModalToggle}
className={isEditDeleteSupported ? '' : 'hidden'}
disabled={viewsIsLoading || isRefetching}
icon={<Disc3 size={16} />}
icon={<Disc3 size={12} />}
>
Save this view
</Button>
</div>
<hr className={isEditDeleteSupported ? '' : 'hidden'} />
<div className={cx('actions', isEditDeleteSupported ? '' : 'hidden')}>
{alertButton}
{dashboardButton}
</div>
<div className="actions">
{/* Hide the info icon for metrics explorer until we get the docs link */}
{!isMetricsExplorer && (
<Tooltip
title={
<div>
{infoIconText}
<Typography.Link href={infoIconLink} target="_blank">
{' '}
here
</Typography.Link>{' '}
</div>
}
>
<InfoCircleOutlined className="info-icon" />
</Tooltip>
)}
<Tooltip title="Hide">
<Button
disabled={disabled}
shape="circle"
onClick={hideToolbar}
icon={<PanelBottomClose size={16} />}
data-testid="hide-toolbar"
/>
</Tooltip>
</div>
</div>
)}
<ExplorerOptionsHideArea

View File

@@ -2,12 +2,13 @@ import './ChartPreview.styles.scss';
import { InfoCircleOutlined } from '@ant-design/icons';
import Spinner from 'components/Spinner';
import { DEFAULT_ENTITY_VERSION } from 'constants/app';
import { ENTITY_VERSION_V5 } from 'constants/app';
import { FeatureKeys } from 'constants/features';
import { QueryParams } from 'constants/query';
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
import AnomalyAlertEvaluationView from 'container/AnomalyAlertEvaluationView';
import GridPanelSwitch from 'container/GridPanelSwitch';
import { populateMultipleResults } from 'container/NewWidget/LeftContainer/WidgetGraph/util';
import { getFormatNameByOptionId } from 'container/NewWidget/RightContainer/alertFomatCategories';
import { timePreferenceType } from 'container/NewWidget/RightContainer/timeItems';
import { Time } from 'container/TopNav/DateTimeSelection/config';
@@ -144,7 +145,8 @@ function ChartPreview({
},
originalGraphType: graphType,
},
alertDef?.version || DEFAULT_ENTITY_VERSION,
// alertDef?.version || DEFAULT_ENTITY_VERSION,
ENTITY_VERSION_V5,
{
queryKey: [
'chartPreview',
@@ -175,6 +177,12 @@ function ChartPreview({
queryResponse.data.payload.data.result = sortedSeriesData;
}
if (queryResponse.data && graphType === PANEL_TYPES.PIE) {
const transformedData = populateMultipleResults(queryResponse?.data);
// eslint-disable-next-line no-param-reassign
queryResponse.data = transformedData;
}
const containerDimensions = useResizeObserver(graphRef);
const isDarkMode = useIsDarkMode();

View File

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

View File

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

View File

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

View File

@@ -10,9 +10,10 @@ import cx from 'classnames';
import { ToggleGraphProps } from 'components/Graph/types';
import Spinner from 'components/Spinner';
import TimePreference from 'components/TimePreferenceDropDown';
import { DEFAULT_ENTITY_VERSION } from 'constants/app';
import { ENTITY_VERSION_V5 } from 'constants/app';
import { QueryParams } from 'constants/query';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { populateMultipleResults } from 'container/NewWidget/LeftContainer/WidgetGraph/util';
import {
timeItems,
timePreferance,
@@ -122,7 +123,8 @@ function FullView({
const response = useGetQueryRange(
requestData,
selectedDashboard?.data?.version || version || DEFAULT_ENTITY_VERSION,
// selectedDashboard?.data?.version || version || DEFAULT_ENTITY_VERSION,
ENTITY_VERSION_V5,
{
queryKey: [widget?.query, widget?.panelTypes, requestData, version],
enabled: !isDependedDataLoaded,
@@ -178,6 +180,12 @@ function FullView({
response.data.payload.data.result = sortedSeriesData;
}
if (response.data && widget.panelTypes === PANEL_TYPES.PIE) {
const transformedData = populateMultipleResults(response?.data);
// eslint-disable-next-line no-param-reassign
response.data = transformedData;
}
useEffect(() => {
graphsVisibilityStates?.forEach((e, i) => {
fullViewChartRef?.current?.toggleGraph(i, e);

View File

@@ -2,6 +2,7 @@ import logEvent from 'api/common/logEvent';
import { DEFAULT_ENTITY_VERSION } from 'constants/app';
import { QueryParams } from 'constants/query';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { populateMultipleResults } from 'container/NewWidget/LeftContainer/WidgetGraph/util';
import { CustomTimeType } from 'container/TopNav/DateTimeSelectionV2/config';
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
import { useIntersectionObserver } from 'hooks/useIntersectionObserver';
@@ -271,6 +272,12 @@ function GridCardGraph({
queryResponse.data.payload.data.result = sortedSeriesData;
}
if (queryResponse.data && widget.panelTypes === PANEL_TYPES.PIE) {
const transformedData = populateMultipleResults(queryResponse?.data);
// eslint-disable-next-line no-param-reassign
queryResponse.data = transformedData;
}
const menuList =
widget.panelTypes === PANEL_TYPES.TABLE ||
widget.panelTypes === PANEL_TYPES.LIST ||

View File

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

View File

@@ -25,7 +25,7 @@ import logEvent from 'api/common/logEvent';
import createDashboard from 'api/v1/dashboards/create';
import { AxiosError } from 'axios';
import cx from 'classnames';
import { ENTITY_VERSION_V4 } from 'constants/app';
import { ENTITY_VERSION_V5 } from 'constants/app';
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
import ROUTES from 'constants/routes';
import { sanitizeDashboardData } from 'container/NewDashboard/DashboardDescription';
@@ -293,7 +293,7 @@ function DashboardsList(): JSX.Element {
ns: 'dashboard',
}),
uploadedGrafana: false,
version: ENTITY_VERSION_V4,
version: ENTITY_VERSION_V5,
});
safeNavigate(

View File

@@ -87,8 +87,9 @@ function TableView({
}
});
} else {
// eslint-disable-next-line sonarjs/no-identical-functions
selectedOptions.selectColumns.forEach((val) => {
const path = findKeyPath(logData, val.key, '');
const path = findKeyPath(logData, val.name, '');
if (path) {
pinnedAttributes[path] = true;
}

View File

@@ -1,7 +1,5 @@
.qb-search-view-container {
padding: 8px 16px;
border-top: 1px solid var(--bg-slate-400, #1d212d);
border-bottom: 1px solid var(--bg-slate-400, #1d212d);
.ant-select-selector {
border-radius: 2px;
@@ -20,9 +18,6 @@
.lightMode {
.qb-search-view-container {
border-top: 1px solid var(--bg-vanilla-300);
border-bottom: 1px solid var(--bg-vanilla-300);
.ant-select-selector {
border-color: var(--bg-vanilla-300) !important;
background-color: var(--bg-vanilla-100) !important;

View File

@@ -1,47 +1,40 @@
import './LogsExplorerQuerySection.styles.scss';
import { QueryBuilderV2 } from 'components/QueryBuilderV2/QueryBuilderV2';
import {
initialQueriesMap,
OPERATORS,
PANEL_TYPES,
} from 'constants/queryBuilder';
import ExplorerOrderBy from 'container/ExplorerOrderBy';
import { QueryBuilder } from 'container/QueryBuilder';
import { OrderByFilterProps } from 'container/QueryBuilder/filters/OrderByFilter/OrderByFilter.interfaces';
import QueryBuilderSearchV2 from 'container/QueryBuilder/filters/QueryBuilderSearchV2/QueryBuilderSearchV2';
import { QueryBuilderProps } from 'container/QueryBuilder/QueryBuilder.interfaces';
import { useGetPanelTypesQueryParam } from 'hooks/queryBuilder/useGetPanelTypesQueryParam';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useQueryOperations } from 'hooks/queryBuilder/useQueryBuilderOperations';
import { useShareBuilderUrl } from 'hooks/queryBuilder/useShareBuilderUrl';
import {
prepareQueryWithDefaultTimestamp,
SELECTED_VIEWS,
} from 'pages/LogsExplorer/utils';
import { ExplorerViews } from 'pages/LogsExplorer/utils';
import { memo, useCallback, useMemo } from 'react';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import { DataSource } from 'types/common/queryBuilder';
function LogExplorerQuerySection({
selectedView,
}: {
selectedView: SELECTED_VIEWS;
selectedView: ExplorerViews;
}): JSX.Element {
const { currentQuery, updateAllQueriesOperators } = useQueryBuilder();
const query = currentQuery?.builder?.queryData[0] || null;
const { updateAllQueriesOperators } = useQueryBuilder();
const panelTypes = useGetPanelTypesQueryParam(PANEL_TYPES.LIST);
const defaultValue = useMemo(() => {
const updatedQuery = updateAllQueriesOperators(
initialQueriesMap.logs,
PANEL_TYPES.LIST,
DataSource.LOGS,
);
return prepareQueryWithDefaultTimestamp(updatedQuery);
}, [updateAllQueriesOperators]);
const defaultValue = useMemo(
() =>
updateAllQueriesOperators(
initialQueriesMap.logs,
PANEL_TYPES.LIST,
DataSource.LOGS,
),
[updateAllQueriesOperators],
);
useShareBuilderUrl(defaultValue);
useShareBuilderUrl({ defaultValue });
const filterConfigs: QueryBuilderProps['filterConfigs'] = useMemo(() => {
const isTable = panelTypes === PANEL_TYPES.TABLE;
@@ -58,13 +51,6 @@ function LogExplorerQuerySection({
return config;
}, [panelTypes]);
const { handleChangeQueryData } = useQueryOperations({
index: 0,
query,
filterConfigs,
entityVersion: '',
});
const renderOrderBy = useCallback(
({ query, onChange }: OrderByFilterProps): JSX.Element => (
<ExplorerOrderBy query={query} onChange={onChange} />
@@ -79,35 +65,16 @@ function LogExplorerQuerySection({
[panelTypes, renderOrderBy],
);
const handleChangeTagFilters = useCallback(
(value: IBuilderQuery['filters']) => {
handleChangeQueryData('filters', value);
},
[handleChangeQueryData],
);
return (
<>
{selectedView === SELECTED_VIEWS.SEARCH && (
<div className="qb-search-view-container">
<QueryBuilderSearchV2
query={query}
onChange={handleChangeTagFilters}
whereClauseConfig={filterConfigs?.filters}
/>
</div>
)}
{selectedView === SELECTED_VIEWS.QUERY_BUILDER && (
<QueryBuilder
panelType={panelTypes}
config={{ initialDataSource: DataSource.LOGS, queryVariant: 'static' }}
filterConfigs={filterConfigs}
queryComponents={queryComponents}
version="v3" // setting this to v3 as we this is rendered in logs explorer
/>
)}
</>
<QueryBuilderV2
isListViewPanel={panelTypes === PANEL_TYPES.LIST}
config={{ initialDataSource: DataSource.LOGS, queryVariant: 'static' }}
panelType={panelTypes}
filterConfigs={filterConfigs}
queryComponents={queryComponents}
showOnlyWhereClause={selectedView === ExplorerViews.LIST}
version="v3" // setting this to v3 as we this is rendered in logs explorer
/>
);
}

View File

@@ -45,7 +45,6 @@ function LogsExplorerList({
}: LogsExplorerListProps): JSX.Element {
const ref = useRef<VirtuosoHandle>(null);
const { initialDataSource } = useQueryBuilder();
const { activeLogId } = useCopyLogLink();
const {

View File

@@ -1,11 +1,11 @@
import { TelemetryFieldKey } from 'api/v5/v5';
import { IField } from 'types/api/logs/fields';
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
export const convertKeysToColumnFields = (
keys: BaseAutocompleteData[],
keys: TelemetryFieldKey[],
): IField[] =>
keys.map((item) => ({
dataType: item.dataType as string,
name: item.key,
type: item.type as string,
dataType: item.fieldDataType ?? '',
name: item.name,
type: item.fieldContext ?? '',
}));

View File

@@ -76,14 +76,92 @@
border: 1px solid var(--bg-slate-400);
}
.order-by-container {
display: flex;
align-items: center;
gap: 8px;
}
.format-options-container {
position: relative;
}
}
}
.logs-actions-container {
display: flex;
justify-content: center;
align-items: center;
gap: 8px;
height: 40px;
padding: 4px 8px;
width: 100%;
border-top: 1px solid var(--Slate-500, #161922);
border-bottom: 1px solid var(--Slate-500, #161922);
box-shadow: 0px 8px 6px 0px #0b0c0e;
.tab-options {
display: flex;
flex: 1;
justify-content: space-between;
align-items: center;
.tab-options-left {
display: flex;
align-items: center;
gap: 8px;
}
.tab-options-right {
display: flex;
align-items: center;
gap: 4px;
.order-by-container {
display: flex;
align-items: center;
gap: 8px;
.order-by-label {
color: var(--text-vanilla-400);
font-size: 12px;
font-style: normal;
font-weight: 400;
line-height: 16px; /* 133.333% */
display: flex;
align-items: center;
gap: 4px;
}
.order-by-select {
width: 100px;
.ant-select-selector {
border: none;
box-shadow: none;
background-color: transparent;
}
}
}
}
.frequency-chart-view-controller {
display: flex;
align-items: center;
gap: 8px;
}
}
.query-stats {
display: flex;
align-items: center;
gap: 12px;
align-self: flex-end;
.rows {
color: var(--bg-vanilla-400);
font-family: 'Geist Mono';
@@ -110,13 +188,6 @@
letter-spacing: 0.36px;
}
}
}
.logs-actions-container {
display: flex;
justify-content: center;
align-items: center;
gap: 8px;
.ant-btn {
border: none;
@@ -186,6 +257,14 @@
background: var(--bg-robin-400);
}
}
}
.logs-actions-container {
.tab-options {
border-top: 1px solid var(--text-vanilla-300);
border-bottom: 1px solid var(--text-vanilla-300);
}
.query-stats {
.rows {
color: var(--bg-ink-400);

View File

@@ -1,12 +1,12 @@
/* eslint-disable sonarjs/cognitive-complexity */
import './LogsExplorerViews.styles.scss';
import { Button, Typography } from 'antd';
import { Button, Select, Switch, Typography } from 'antd';
import { getQueryStats, WsDataEvent } from 'api/common/getQueryStats';
import logEvent from 'api/common/logEvent';
import { getYAxisFormattedValue } from 'components/Graph/yAxisConfig';
import LogsFormatOptionsMenu from 'components/LogsFormatOptionsMenu/LogsFormatOptionsMenu';
import { ENTITY_VERSION_V4 } from 'constants/app';
import { ENTITY_VERSION_V5 } from 'constants/app';
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
import { LOCALSTORAGE } from 'constants/localStorage';
import { AVAILABLE_EXPORT_PANEL_TYPES } from 'constants/panelTypes';
@@ -46,8 +46,8 @@ import {
omit,
set,
} from 'lodash-es';
import { Sliders } from 'lucide-react';
import { SELECTED_VIEWS } from 'pages/LogsExplorer/utils';
import { ArrowUp10, Minus, Sliders } from 'lucide-react';
import { ExplorerViews } from 'pages/LogsExplorer/utils';
import { useTimezone } from 'providers/Timezone';
import {
memo,
@@ -80,15 +80,13 @@ import { v4 } from 'uuid';
import QueryStatus from './QueryStatus';
function LogsExplorerViews({
function LogsExplorerViewsContainer({
selectedView,
showFrequencyChart,
setIsLoadingQueries,
listQueryKeyRef,
chartQueryKeyRef,
}: {
selectedView: SELECTED_VIEWS;
showFrequencyChart: boolean;
selectedView: ExplorerViews;
setIsLoadingQueries: React.Dispatch<React.SetStateAction<boolean>>;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
listQueryKeyRef: MutableRefObject<any>;
@@ -97,6 +95,7 @@ function LogsExplorerViews({
}): JSX.Element {
const { safeNavigate } = useSafeNavigate();
const dispatch = useDispatch();
const [showFrequencyChart, setShowFrequencyChart] = useState(true);
// this is to respect the panel type present in the URL rather than defaulting it to list always.
const panelTypes = useGetPanelTypesQueryParam(PANEL_TYPES.LIST);
@@ -139,6 +138,8 @@ function LogsExplorerViews({
const [queryStats, setQueryStats] = useState<WsDataEvent>();
const [listChartQuery, setListChartQuery] = useState<Query | null>(null);
const [orderDirection, setOrderDirection] = useState<string>('desc');
const listQuery = useMemo(() => {
if (!stagedQuery || stagedQuery.builder.queryData.length < 1) return null;
@@ -240,15 +241,6 @@ function LogsExplorerViews({
[currentQuery, selectedPanelType, updateAllQueriesOperators],
);
const handleModeChange = (panelType: PANEL_TYPES): void => {
if (selectedView === SELECTED_VIEWS.SEARCH) {
handleSetConfig(panelType, DataSource.LOGS);
}
setShowFormatMenuItems(false);
handleExplorerTabChange(panelType);
};
const {
data: listChartData,
isFetching: isFetchingListChartData,
@@ -256,7 +248,8 @@ function LogsExplorerViews({
} = useGetExplorerQueryRange(
listChartQuery,
PANEL_TYPES.TIME_SERIES,
ENTITY_VERSION_V4,
// ENTITY_VERSION_V4,
ENTITY_VERSION_V5,
{
enabled: !!listChartQuery && panelType === PANEL_TYPES.LIST,
},
@@ -276,7 +269,8 @@ function LogsExplorerViews({
} = useGetExplorerQueryRange(
requestData,
panelType,
ENTITY_VERSION_V4,
// ENTITY_VERSION_V4,
ENTITY_VERSION_V5,
{
keepPreviousData: true,
enabled: !isLimit && !!requestData,
@@ -337,14 +331,26 @@ function LogsExplorerViews({
};
}
// Create orderBy array based on orderDirection
const orderBy = [
{ columnName: 'timestamp', order: orderDirection },
{ columnName: 'id', order: orderDirection },
];
const queryData: IBuilderQuery[] =
query.builder.queryData.length > 1
? query.builder.queryData
? query.builder.queryData.map((item) => ({
...item,
...(selectedView !== ExplorerViews.LIST ? { order: [] } : {}),
}))
: [
{
...(listQuery || initialQueryBuilderFormValues),
...paginateData,
...(updatedFilters ? { filters: updatedFilters } : {}),
...(selectedView === ExplorerViews.LIST
? { order: orderBy, orderBy }
: { order: [] }),
},
];
@@ -358,7 +364,7 @@ function LogsExplorerViews({
return data;
},
[listQuery, activeLogId],
[activeLogId, orderDirection, listQuery, selectedView],
);
const handleEndReached = useCallback(() => {
@@ -460,8 +466,7 @@ function LogsExplorerViews({
useEffect(() => {
const shouldChangeView =
(isMultipleQueries || isGroupByExist) &&
selectedView !== SELECTED_VIEWS.SEARCH;
(isMultipleQueries || isGroupByExist) && selectedView !== ExplorerViews.LIST;
if (selectedPanelType === PANEL_TYPES.LIST && shouldChangeView) {
handleExplorerTabChange(PANEL_TYPES.TIME_SERIES);
@@ -481,11 +486,7 @@ function LogsExplorerViews({
]);
useEffect(() => {
if (
selectedView &&
selectedView === SELECTED_VIEWS.SEARCH &&
handleSetConfig
) {
if (selectedView && selectedView === ExplorerViews.LIST && handleSetConfig) {
handleSetConfig(defaultTo(panelTypes, PANEL_TYPES.LIST), DataSource.LOGS);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
@@ -506,10 +507,19 @@ function LogsExplorerViews({
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [data]);
// Store previous orderDirection to detect changes
const prevOrderDirectionRef = useRef(orderDirection);
useEffect(() => {
const orderDirectionChanged =
prevOrderDirectionRef.current !== orderDirection &&
selectedPanelType === PANEL_TYPES.LIST;
prevOrderDirectionRef.current = orderDirection;
if (
requestData?.id !== stagedQuery?.id ||
currentMinTimeRef.current !== minTime
currentMinTimeRef.current !== minTime ||
orderDirectionChanged
) {
// Recalculate global time when query changes i.e. stage and run query clicked
if (
@@ -545,6 +555,8 @@ function LogsExplorerViews({
dispatch,
selectedTime,
maxTime,
orderDirection,
selectedPanelType,
]);
const chartData = useMemo(() => {
@@ -651,111 +663,107 @@ function LogsExplorerViews({
return (
<div className="logs-explorer-views-container">
{showFrequencyChart && (
<LogsExplorerChart
className="logs-histogram"
isLoading={isFetchingListChartData || isLoadingListChartData}
data={chartData}
isLogsExplorerViews={panelType === PANEL_TYPES.LIST}
/>
)}
<div className="logs-explorer-views-types">
<div className="views-tabs-container">
<Button.Group className="views-tabs">
<Button
value={PANEL_TYPES.LIST}
className={
// eslint-disable-next-line sonarjs/no-duplicate-string
selectedPanelType === PANEL_TYPES.LIST ? 'selected_view tab' : 'tab'
}
disabled={
(isMultipleQueries || isGroupByExist) && selectedView !== 'search'
}
onClick={(): void => handleModeChange(PANEL_TYPES.LIST)}
data-testid="logs-list-view"
>
List view
</Button>
<Button
value={PANEL_TYPES.TIME_SERIES}
className={
// eslint-disable-next-line sonarjs/no-duplicate-string
selectedPanelType === PANEL_TYPES.TIME_SERIES
? 'selected_view tab'
: 'tab'
}
onClick={(): void => handleModeChange(PANEL_TYPES.TIME_SERIES)}
data-testid="time-series-view"
>
Time series
</Button>
<Button
value={PANEL_TYPES.TABLE}
className={
// eslint-disable-next-line sonarjs/no-duplicate-string
selectedPanelType === PANEL_TYPES.TABLE ? 'selected_view tab' : 'tab'
}
onClick={(): void => handleModeChange(PANEL_TYPES.TABLE)}
data-testid="table-view"
>
Table
</Button>
</Button.Group>
<div className="logs-actions-container">
{selectedPanelType === PANEL_TYPES.LIST && (
<div className="tab-options">
<Download
data={flattenLogData}
isLoading={isFetching}
fileName="log_data"
/>
<div className="format-options-container" ref={menuRef}>
<Button
className="periscope-btn"
onClick={handleToggleShowFormatOptions}
icon={<Sliders size={14} />}
data-testid="periscope-btn"
<div className="logs-actions-container">
<div className="tab-options">
<div className="tab-options-left">
{selectedPanelType === PANEL_TYPES.LIST && (
<div className="frequency-chart-view-controller">
<Typography>Frequency chart</Typography>
<Switch
size="small"
checked={showFrequencyChart}
defaultChecked
onChange={(): void => setShowFrequencyChart(!showFrequencyChart)}
/>
</div>
)}
</div>
<div className="tab-options-right">
{selectedPanelType === PANEL_TYPES.LIST && (
<>
<div className="order-by-container">
<div className="order-by-label">
Order by <Minus size={14} /> <ArrowUp10 size={14} />
</div>
<Select
placeholder="Select order by"
className="order-by-select"
style={{ width: 100 }}
value={orderDirection}
onChange={(value): void => setOrderDirection(value)}
options={[
{ label: 'Ascending', value: 'asc' },
{ label: 'Descending', value: 'desc' },
]}
/>
</div>
<Download
data={flattenLogData}
isLoading={isFetching}
fileName="log_data"
/>
<div className="format-options-container" ref={menuRef}>
<Button
className="periscope-btn ghost"
onClick={handleToggleShowFormatOptions}
icon={<Sliders size={14} />}
data-testid="periscope-btn"
/>
{showFormatMenuItems && (
<LogsFormatOptionsMenu
title="FORMAT"
items={formatItems}
selectedOptionFormat={options.format}
config={config}
/>
)}
</div>
</>
)}
{(selectedPanelType === PANEL_TYPES.TIME_SERIES ||
selectedPanelType === PANEL_TYPES.TABLE) && (
<div className="query-stats">
<QueryStatus
loading={isLoading || isFetching}
error={isError}
success={isSuccess}
/>
{showFormatMenuItems && (
<LogsFormatOptionsMenu
title="FORMAT"
items={formatItems}
selectedOptionFormat={options.format}
config={config}
/>
{queryStats?.read_rows && (
<Typography.Text className="rows">
{getYAxisFormattedValue(queryStats.read_rows?.toString(), 'short')}{' '}
rows
</Typography.Text>
)}
{queryStats?.elapsed_ms && (
<>
<div className="divider" />
<Typography.Text className="time">
{getYAxisFormattedValue(queryStats?.elapsed_ms?.toString(), 'ms')}
</Typography.Text>
</>
)}
</div>
</div>
)}
{(selectedPanelType === PANEL_TYPES.TIME_SERIES ||
selectedPanelType === PANEL_TYPES.TABLE) && (
<div className="query-stats">
<QueryStatus
loading={isLoading || isFetching}
error={isError}
success={isSuccess}
/>
{queryStats?.read_rows && (
<Typography.Text className="rows">
{getYAxisFormattedValue(queryStats.read_rows?.toString(), 'short')}{' '}
rows
</Typography.Text>
)}
{queryStats?.elapsed_ms && (
<>
<div className="divider" />
<Typography.Text className="time">
{getYAxisFormattedValue(queryStats?.elapsed_ms?.toString(), 'ms')}
</Typography.Text>
</>
)}
</div>
)}
)}
</div>
</div>
</div>
{selectedPanelType === PANEL_TYPES.LIST && showFrequencyChart && (
<LogsExplorerChart
className="logs-histogram"
isLoading={isFetchingListChartData || isLoadingListChartData}
data={chartData}
isLogsExplorerViews={panelType === PANEL_TYPES.LIST}
/>
)}
<div className="logs-explorer-views-type-content">
{selectedPanelType === PANEL_TYPES.LIST && (
<LogsExplorerList
@@ -801,4 +809,4 @@ function LogsExplorerViews({
);
}
export default memo(LogsExplorerViews);
export default memo(LogsExplorerViewsContainer);

View File

@@ -4,7 +4,7 @@ import { useGetExplorerQueryRange } from 'hooks/queryBuilder/useGetExplorerQuery
import { logsQueryRangeSuccessResponse } from 'mocks-server/__mockdata__/logs_query_range';
import { server } from 'mocks-server/server';
import { rest } from 'msw';
import { SELECTED_VIEWS } from 'pages/LogsExplorer/utils';
import { ExplorerViews } from 'pages/LogsExplorer/utils';
import { PreferenceContextProvider } from 'providers/preferences/context/PreferenceContextProvider';
import { QueryBuilderContext } from 'providers/QueryBuilder';
import { VirtuosoMockContext } from 'react-virtuoso';
@@ -127,8 +127,7 @@ const renderer = (): RenderResult =>
>
<PreferenceContextProvider>
<LogsExplorerViews
selectedView={SELECTED_VIEWS.SEARCH}
showFrequencyChart
selectedView={ExplorerViews.LIST}
setIsLoadingQueries={(): void => {}}
listQueryKeyRef={{ current: {} }}
chartQueryKeyRef={{ current: {} }}
@@ -212,8 +211,7 @@ describe('LogsExplorerViews -', () => {
<QueryBuilderContext.Provider value={mockQueryBuilderContextValue}>
<PreferenceContextProvider>
<LogsExplorerViews
selectedView={SELECTED_VIEWS.SEARCH}
showFrequencyChart
selectedView={ExplorerViews.LIST}
setIsLoadingQueries={(): void => {}}
listQueryKeyRef={{ current: {} }}
chartQueryKeyRef={{ current: {} }}

View File

@@ -5,7 +5,8 @@
display: flex;
align-items: center;
justify-content: space-between;
margin: 10px 0;
margin: 4px 0;
padding: 0 1rem;
.explore-header-left-actions {
display: flex;
@@ -55,7 +56,7 @@
}
.explore-content {
margin-top: 10px;
padding: 0 8px;
.ant-space {
margin-top: 10px;

View File

@@ -3,9 +3,11 @@ import './Explorer.styles.scss';
import * as Sentry from '@sentry/react';
import { Switch } from 'antd';
import logEvent from 'api/common/logEvent';
import { QueryBuilderV2 } from 'components/QueryBuilderV2/QueryBuilderV2';
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
import ExplorerOptionWrapper from 'container/ExplorerOptions/ExplorerOptionWrapper';
import RightToolbarActions from 'container/QueryBuilder/components/ToolbarActions/RightToolbarActions';
import { QueryBuilderProps } from 'container/QueryBuilder/QueryBuilder.interfaces';
import DateTimeSelector from 'container/TopNav/DateTimeSelectionV2';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useShareBuilderUrl } from 'hooks/queryBuilder/useShareBuilderUrl';
@@ -20,7 +22,7 @@ import { generateExportToDashboardLink } from 'utils/dashboard/generateExportToD
import { v4 as uuid } from 'uuid';
import { MetricsExplorerEventKeys, MetricsExplorerEvents } from '../events';
import QuerySection from './QuerySection';
// import QuerySection from './QuerySection';
import TimeSeries from './TimeSeries';
import { ExplorerTabs } from './types';
import { splitQueryIntoOneChartPerQuery } from './utils';
@@ -53,6 +55,16 @@ function Explorer(): JSX.Element {
});
};
const defaultQuery = useMemo(
() =>
updateAllQueriesOperators(
initialQueriesMap[DataSource.METRICS],
PANEL_TYPES.TIME_SERIES,
DataSource.METRICS,
),
[updateAllQueriesOperators],
);
const exportDefaultQuery = useMemo(
() =>
updateAllQueriesOperators(
@@ -63,7 +75,7 @@ function Explorer(): JSX.Element {
[currentQuery, updateAllQueriesOperators],
);
useShareBuilderUrl(exportDefaultQuery);
useShareBuilderUrl({ defaultValue: defaultQuery });
const handleExport = useCallback(
(
@@ -101,6 +113,11 @@ function Explorer(): JSX.Element {
});
}, []);
const queryComponents = useMemo(
(): QueryBuilderProps['queryComponents'] => ({}),
[],
);
return (
<Sentry.ErrorBoundary fallback={<ErrorBoundaryFallback />}>
<div className="metrics-explorer-explore-container">
@@ -115,10 +132,19 @@ function Explorer(): JSX.Element {
</div>
<div className="explore-header-right-actions">
<DateTimeSelector showAutoRefresh />
<RightToolbarActions onStageRunQuery={handleRunQuery} />
<RightToolbarActions
onStageRunQuery={(): void => handleRunQuery(true, true)}
/>
</div>
</div>
<QuerySection />
{/* <QuerySection /> */}
<QueryBuilderV2
config={{ initialDataSource: DataSource.METRICS, queryVariant: 'static' }}
panelType={PANEL_TYPES.TIME_SERIES}
queryComponents={queryComponents}
showFunctions={false}
version="v3"
/>
{/* TODO: Enable once we have resolved all related metrics issues */}
{/* <Button.Group className="explore-tabs">
<Button

View File

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

View File

@@ -1,5 +1,5 @@
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { defaultTraceSelectedColumns } from 'container/OptionsMenu/constants';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
import { LogsAggregatorOperator } from 'types/common/queryBuilder';
@@ -43,48 +43,7 @@ export const listViewInitialTraceQuery = {
orderBy: [{ columnName: 'timestamp', order: 'desc' }],
offset: 0,
pageSize: 10,
selectColumns: [
{
key: 'serviceName',
dataType: 'string',
type: 'tag',
isColumn: true,
isJSON: false,
id: 'serviceName--string--tag--true',
},
{
key: 'name',
dataType: 'string',
type: 'tag',
isColumn: true,
isJSON: false,
id: 'name--string--tag--true',
},
{
key: 'durationNano',
dataType: 'float64',
type: 'tag',
isColumn: true,
isJSON: false,
id: 'durationNano--float64--tag--true',
},
{
key: 'httpMethod',
dataType: 'string',
type: 'tag',
isColumn: true,
isJSON: false,
id: 'httpMethod--string--tag--true',
},
{
key: 'responseStatusCode',
dataType: 'string',
type: 'tag',
isColumn: true,
isJSON: false,
id: 'responseStatusCode--string--tag--true',
},
] as BaseAutocompleteData[],
selectColumns: defaultTraceSelectedColumns,
},
],
},

View File

@@ -25,9 +25,11 @@ function ExplorerAttributeColumns({
}
const filteredAttributeKeys =
data?.payload?.attributeKeys?.filter((attributeKey: any) =>
attributeKey.key.toLowerCase().includes(searchText.toLowerCase()),
) || [];
Object.values(data?.data?.data?.keys || {})
?.flat()
?.filter((attributeKey: any) =>
attributeKey.name.toLowerCase().includes(searchText.toLowerCase()),
) || [];
if (filteredAttributeKeys.length === 0) {
return (
<div className="attribute-columns">
@@ -36,16 +38,17 @@ function ExplorerAttributeColumns({
);
}
console.log('filteredAttributeKeys', filteredAttributeKeys, data);
return (
<div className="attribute-columns">
{filteredAttributeKeys.map((attributeKey: any) => (
<Checkbox
checked={isAttributeKeySelected(attributeKey.key)}
onChange={(): void => handleCheckboxChange(attributeKey.key)}
checked={isAttributeKeySelected(attributeKey.name)}
onChange={(): void => handleCheckboxChange(attributeKey.name)}
style={{ padding: 0 }}
key={attributeKey.key}
key={attributeKey.name}
>
{attributeKey.key}
{attributeKey.name}
</Checkbox>
))}
</div>

View File

@@ -1,3 +1,4 @@
/* eslint-disable sonarjs/no-identical-functions */
/* eslint-disable sonarjs/cognitive-complexity */
/* eslint-disable react/jsx-props-no-spreading */
import './ExplorerColumnsRenderer.styles.scss';
@@ -5,9 +6,10 @@ import './ExplorerColumnsRenderer.styles.scss';
import { Color } from '@signozhq/design-tokens';
import { Button, Divider, Dropdown, Input, Tooltip, Typography } from 'antd';
import { MenuProps } from 'antd/lib';
import { FieldDataType } from 'api/v5/v5';
import { SOMETHING_WENT_WRONG } from 'constants/api';
import { useGetAggregateKeys } from 'hooks/queryBuilder/useGetAggregateKeys';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useGetQueryKeySuggestions } from 'hooks/querySuggestions/useGetQueryKeySuggestions';
import { useIsDarkMode } from 'hooks/useDarkMode';
import useDebouncedFn from 'hooks/useDebouncedFunction';
import {
@@ -49,13 +51,27 @@ function ExplorerColumnsRenderer({
const initialDataSource = currentQuery.builder.queryData[0].dataSource;
const { data, isLoading, isError } = useGetAggregateKeys(
// const { data, isLoading, isError } = useGetAggregateKeys(
// {
// aggregateAttribute: '',
// dataSource: currentQuery.builder.queryData[0].dataSource,
// aggregateOperator: currentQuery.builder.queryData[0].aggregateOperator,
// searchText: querySearchText,
// tagType: '',
// },
// {
// queryKey: [
// currentQuery.builder.queryData[0].dataSource,
// currentQuery.builder.queryData[0].aggregateOperator,
// querySearchText,
// ],
// },
// );
const { data, isLoading, isError } = useGetQueryKeySuggestions(
{
aggregateAttribute: '',
dataSource: currentQuery.builder.queryData[0].dataSource,
aggregateOperator: currentQuery.builder.queryData[0].aggregateOperator,
searchText: querySearchText,
tagType: '',
signal: currentQuery.builder.queryData[0].dataSource,
},
{
queryKey: [
@@ -71,7 +87,7 @@ function ExplorerColumnsRenderer({
return selectedLogFields.some((field) => field.name === key);
}
if (initialDataSource === DataSource.TRACES && selectedTracesFields) {
return selectedTracesFields.some((field) => field.key === key);
return selectedTracesFields.some((field) => field.name === key);
}
return false;
};
@@ -99,18 +115,31 @@ function ExplorerColumnsRenderer({
initialDataSource === DataSource.TRACES &&
setSelectedTracesFields !== undefined
) {
const selectedField = data?.payload?.attributeKeys?.find(
(attributeKey) => attributeKey.key === key,
);
const selectedField = Object.values(data?.data?.data?.keys || {})
?.flat()
?.find((attributeKey) => attributeKey.name === key);
console.log('selectedField', selectedField);
if (selectedTracesFields) {
if (isAttributeKeySelected(key)) {
setSelectedTracesFields(
selectedTracesFields.filter((field) => field.key !== key),
selectedTracesFields.filter((field) => field.name !== key),
);
} else if (selectedField) {
setSelectedTracesFields([...selectedTracesFields, selectedField]);
setSelectedTracesFields([
...selectedTracesFields,
{
...selectedField,
fieldDataType: selectedField.fieldDataType as FieldDataType,
},
]);
}
} else if (selectedField) setSelectedTracesFields([selectedField]);
} else if (selectedField)
setSelectedTracesFields([
{
...selectedField,
fieldDataType: selectedField.fieldDataType as FieldDataType,
},
]);
}
setOpen(false);
};
@@ -175,7 +204,7 @@ function ExplorerColumnsRenderer({
selectedTracesFields
) {
setSelectedTracesFields(
selectedTracesFields.filter((field) => field.key !== name),
selectedTracesFields.filter((field) => field.name !== name),
);
}
};
@@ -218,6 +247,8 @@ function ExplorerColumnsRenderer({
const isDarkMode = useIsDarkMode();
console.log('selectedTracesFields', selectedTracesFields, selectedLogFields);
return (
<div className="explorer-columns-renderer">
<div className="title">
@@ -279,12 +310,14 @@ function ExplorerColumnsRenderer({
>
<div className="explorer-column-title">
<GripVertical size={12} color="#5A5A5A" />
{field.key}
{field?.name || (field as any)?.key}
</div>
<Trash2
size={12}
color="red"
onClick={(): void => removeSelectedLogField(field.key)}
onClick={(): void =>
removeSelectedLogField(field?.name || (field as any)?.key)
}
data-testid="trash-icon"
/>
</div>

View File

@@ -4,6 +4,7 @@ import { Color } from '@signozhq/design-tokens';
import { Button, Tabs, Typography } from 'antd';
import logEvent from 'api/common/logEvent';
import PromQLIcon from 'assets/Dashboard/PromQl';
import { QueryBuilderV2 } from 'components/QueryBuilderV2/QueryBuilderV2';
import TextToolTip from 'components/TextToolTip';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { QBShortcuts } from 'constants/shortcuts/QBShortcuts';
@@ -11,7 +12,7 @@ import {
getDefaultWidgetData,
PANEL_TYPE_TO_QUERY_TYPES,
} from 'container/NewWidget/utils';
import { QueryBuilder } from 'container/QueryBuilder';
// import { QueryBuilder } from 'container/QueryBuilder';
import { QueryBuilderProps } from 'container/QueryBuilder/QueryBuilder.interfaces';
import { useKeyboardHotkeys } from 'hooks/hotkeys/useKeyboardHotkeys';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
@@ -63,7 +64,7 @@ function QuerySection({
const { query } = selectedWidget;
useShareBuilderUrl(query);
useShareBuilderUrl({ defaultValue: query });
const handleStageQuery = useCallback(
(query: Query): void => {
@@ -142,6 +143,11 @@ function QuerySection({
return config;
}, []);
const queryComponents = useMemo(
(): QueryBuilderProps['queryComponents'] => ({}),
[],
);
const items = useMemo(() => {
const supportedQueryTypes = PANEL_TYPE_TO_QUERY_TYPES[selectedGraph] || [];
@@ -150,12 +156,15 @@ function QuerySection({
icon: <Atom size={14} />,
label: 'Query Builder',
component: (
<QueryBuilder
panelType={selectedGraph}
filterConfigs={filterConfigs}
version={selectedDashboard?.data?.version || 'v3'}
isListViewPanel={selectedGraph === PANEL_TYPES.LIST}
/>
<div className="query-builder-v2-container">
<QueryBuilderV2
panelType={selectedGraph}
filterConfigs={filterConfigs}
version={selectedDashboard?.data?.version || 'v3'}
isListViewPanel={selectedGraph === PANEL_TYPES.LIST}
queryComponents={queryComponents}
/>
</div>
),
},
[EQueryType.CLICKHOUSE]: {
@@ -186,6 +195,7 @@ function QuerySection({
children: queryTypeComponents[queryType].component,
}));
}, [
queryComponents,
selectedGraph,
filterConfigs,
selectedDashboard?.data?.version,

View File

@@ -5,6 +5,7 @@ import { WidgetGraphContainerProps } from 'container/NewWidget/types';
import { getSortedSeriesData } from 'utils/getSortedSeriesData';
import { NotFoundContainer } from './styles';
import { populateMultipleResults } from './util';
import WidgetGraph from './WidgetGraphs';
function WidgetGraphContainer({
@@ -22,6 +23,12 @@ function WidgetGraphContainer({
queryResponse.data.payload.data.result = sortedSeriesData;
}
if (queryResponse.data && selectedGraph === PANEL_TYPES.PIE) {
const transformedData = populateMultipleResults(queryResponse?.data);
// eslint-disable-next-line no-param-reassign
queryResponse.data = transformedData;
}
if (selectedWidget === undefined) {
return <Card>Invalid widget</Card>;
}
@@ -43,6 +50,7 @@ function WidgetGraphContainer({
if (
selectedGraph !== PANEL_TYPES.LIST &&
selectedGraph !== PANEL_TYPES.VALUE &&
queryResponse.data?.payload.data?.result?.length === 0
) {
return (
@@ -52,7 +60,7 @@ function WidgetGraphContainer({
);
}
if (
selectedGraph === PANEL_TYPES.LIST &&
(selectedGraph === PANEL_TYPES.LIST || selectedGraph === PANEL_TYPES.VALUE) &&
queryResponse.data?.payload?.data?.newResult?.data?.result?.length === 0
) {
return (

View File

@@ -0,0 +1,51 @@
import { SuccessResponse } from 'types/api';
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
import { QueryData, QueryDataV3 } from 'types/api/widgets/getQuery';
export function populateMultipleResults(
responseData: SuccessResponse<MetricRangePayloadProps, unknown>,
): SuccessResponse<MetricRangePayloadProps, unknown> {
const queryResults = responseData?.payload?.data?.newResult?.data?.result;
const allFormattedResults: QueryData[] = [];
queryResults?.forEach((query: QueryDataV3) => {
const { queryName, legend, table } = query;
if (!table) return;
const { columns, rows } = table;
const valueCol = columns?.find((c) => c.isValueColumn);
const labelCols = columns?.filter((c) => !c.isValueColumn);
rows?.forEach((row) => {
const metric: Record<string, string> = {};
labelCols?.forEach((col) => {
metric[col.name] = String(row.data[col.name]);
});
allFormattedResults.push({
metric,
values: [[0, String(row.data[valueCol!.name])]],
queryName,
legend: legend || '',
});
});
});
// Create a copy instead of mutating the original
const updatedResponseData: SuccessResponse<
MetricRangePayloadProps,
unknown
> = {
...responseData,
payload: {
...responseData.payload,
data: {
...responseData.payload.data,
result: allFormattedResults,
},
},
};
return updatedResponseData;
}

View File

@@ -3,14 +3,12 @@
/* eslint-disable react/jsx-props-no-spreading */
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { TelemetryFieldKey } from 'api/v5/v5';
import { useGetAggregateKeys } from 'hooks/queryBuilder/useGetAggregateKeys';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import React from 'react';
import { DropResult } from 'react-beautiful-dnd';
import {
BaseAutocompleteData,
DataTypes,
} from 'types/api/queryBuilder/queryAutocompleteResponse';
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { DataSource } from 'types/common/queryBuilder';
import ExplorerColumnsRenderer from '../ExplorerColumnsRenderer';
@@ -321,7 +319,11 @@ describe('ExplorerColumnsRenderer', () => {
selectedLogFields={[]}
setSelectedLogFields={mockSetSelectedLogFields}
selectedTracesFields={[
{ key: 'trace_attribute1', dataType: DataTypes.String, type: 'tag' },
{
name: 'trace_attribute1',
fieldDataType: DataTypes.String,
fieldContext: '',
},
]}
setSelectedTracesFields={mockSetSelectedTracesFields}
/>,
@@ -340,7 +342,11 @@ describe('ExplorerColumnsRenderer', () => {
selectedLogFields={[]}
setSelectedLogFields={mockSetSelectedLogFields}
selectedTracesFields={[
{ key: 'trace_attribute1', dataType: DataTypes.String, type: 'tag' },
{
name: 'trace_attribute1',
fieldDataType: DataTypes.String,
fieldContext: '',
},
]}
setSelectedTracesFields={mockSetSelectedTracesFields}
/>,
@@ -355,15 +361,15 @@ describe('ExplorerColumnsRenderer', () => {
it('reorders trace fields on drag and drop', () => {
const initialSelectedFields = [
{ key: 'trace_field1', dataType: 'string', type: 'tag' },
{ key: 'trace_field2', dataType: 'string', type: 'tag' },
{ name: 'trace_field1', fieldDataType: 'string', fieldContext: 'tag' },
{ name: 'trace_field2', fieldDataType: 'string', fieldContext: 'tag' },
];
render(
<ExplorerColumnsRenderer
selectedLogFields={[]}
setSelectedLogFields={mockSetSelectedLogFields}
selectedTracesFields={initialSelectedFields as BaseAutocompleteData[]}
selectedTracesFields={initialSelectedFields as TelemetryFieldKey[]}
setSelectedTracesFields={mockSetSelectedTracesFields}
/>,
);
@@ -383,8 +389,8 @@ describe('ExplorerColumnsRenderer', () => {
});
expect(mockSetSelectedTracesFields).toHaveBeenCalledWith([
{ key: 'trace_field2', dataType: 'string', type: 'tag' },
{ key: 'trace_field1', dataType: 'string', type: 'tag' },
{ name: 'trace_field2', fieldDataType: 'string', fieldContext: 'tag' },
{ name: 'trace_field1', fieldDataType: 'string', fieldContext: 'tag' },
]);
} else {
fail('DragDropContext or onDragEndMock not found');

View File

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

View File

@@ -581,7 +581,11 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
const setGraphHandler = (type: PANEL_TYPES): void => {
setIsLoadingPanelData(true);
const updatedQuery = handleQueryChange(type as any, supersetQuery);
const updatedQuery = handleQueryChange(
type as any,
supersetQuery,
selectedGraph,
);
setGraphType(type);
redirectWithQueryBuilderData(
updatedQuery,

View File

@@ -6,9 +6,12 @@ import {
} from 'constants/queryBuilder';
import {
listViewInitialLogQuery,
listViewInitialTraceQuery,
PANEL_TYPES_INITIAL_QUERY,
} from 'container/NewDashboard/ComponentsSlider/constants';
import {
defaultLogsSelectedColumns,
defaultTraceSelectedColumns,
} from 'container/OptionsMenu/constants';
import { categoryToSupport } from 'container/QueryBuilder/filters/BuilderUnitsFilter/config';
import { cloneDeep, defaultTo, isEmpty, isEqual, set, unset } from 'lodash-es';
import { Layout } from 'react-grid-layout';
@@ -56,9 +59,11 @@ export const panelTypeDataSourceFormValuesMap: Record<
'aggregateAttribute',
'aggregateOperator',
'filters',
'filter',
'groupBy',
'limit',
'having',
'havingExpression',
'orderBy',
'functions',
'stepInterval',
@@ -66,6 +71,7 @@ export const panelTypeDataSourceFormValuesMap: Record<
'queryName',
'legend',
'expression',
'aggregations',
],
},
},
@@ -76,10 +82,12 @@ export const panelTypeDataSourceFormValuesMap: Record<
'aggregateOperator',
'timeAggregation',
'filters',
'filter',
'spaceAggregation',
'groupBy',
'limit',
'having',
'havingExpression',
'orderBy',
'stepInterval',
'legend',
@@ -87,6 +95,7 @@ export const panelTypeDataSourceFormValuesMap: Record<
'disabled',
'functions',
'expression',
'aggregations',
],
},
},
@@ -96,9 +105,11 @@ export const panelTypeDataSourceFormValuesMap: Record<
'aggregateAttribute',
'aggregateOperator',
'filters',
'filter',
'groupBy',
'limit',
'having',
'havingExpression',
'orderBy',
'functions',
'stepInterval',
@@ -106,6 +117,7 @@ export const panelTypeDataSourceFormValuesMap: Record<
'queryName',
'legend',
'expression',
'aggregations',
],
},
},
@@ -117,9 +129,11 @@ export const panelTypeDataSourceFormValuesMap: Record<
'aggregateAttribute',
'aggregateOperator',
'filters',
'filter',
'groupBy',
'limit',
'having',
'havingExpression',
'orderBy',
'functions',
'stepInterval',
@@ -127,6 +141,7 @@ export const panelTypeDataSourceFormValuesMap: Record<
'queryName',
'legend',
'expression',
'aggregations',
],
},
},
@@ -137,10 +152,12 @@ export const panelTypeDataSourceFormValuesMap: Record<
'aggregateOperator',
'timeAggregation',
'filters',
'filter',
'spaceAggregation',
'groupBy',
'limit',
'having',
'havingExpression',
'orderBy',
'stepInterval',
'legend',
@@ -148,6 +165,7 @@ export const panelTypeDataSourceFormValuesMap: Record<
'disabled',
'functions',
'expression',
'aggregations',
],
},
},
@@ -157,9 +175,11 @@ export const panelTypeDataSourceFormValuesMap: Record<
'aggregateAttribute',
'aggregateOperator',
'filters',
'filter',
'groupBy',
'limit',
'having',
'havingExpression',
'orderBy',
'functions',
'stepInterval',
@@ -167,6 +187,7 @@ export const panelTypeDataSourceFormValuesMap: Record<
'queryName',
'legend',
'expression',
'aggregations',
],
},
},
@@ -178,9 +199,11 @@ export const panelTypeDataSourceFormValuesMap: Record<
'aggregateAttribute',
'aggregateOperator',
'filters',
'filter',
'groupBy',
'limit',
'having',
'havingExpression',
'orderBy',
'functions',
'stepInterval',
@@ -188,6 +211,7 @@ export const panelTypeDataSourceFormValuesMap: Record<
'queryName',
'legend',
'expression',
'aggregations',
],
},
},
@@ -198,10 +222,12 @@ export const panelTypeDataSourceFormValuesMap: Record<
'aggregateOperator',
'timeAggregation',
'filters',
'filter',
'spaceAggregation',
'groupBy',
'limit',
'having',
'havingExpression',
'orderBy',
'stepInterval',
'legend',
@@ -209,6 +235,7 @@ export const panelTypeDataSourceFormValuesMap: Record<
'disabled',
'functions',
'expression',
'aggregations',
],
},
},
@@ -218,9 +245,11 @@ export const panelTypeDataSourceFormValuesMap: Record<
'aggregateAttribute',
'aggregateOperator',
'filters',
'filter',
'groupBy',
'limit',
'having',
'havingExpression',
'orderBy',
'functions',
'stepInterval',
@@ -228,6 +257,7 @@ export const panelTypeDataSourceFormValuesMap: Record<
'queryName',
'legend',
'expression',
'aggregations',
],
},
},
@@ -239,9 +269,11 @@ export const panelTypeDataSourceFormValuesMap: Record<
'aggregateAttribute',
'aggregateOperator',
'filters',
'filter',
'groupBy',
'limit',
'having',
'havingExpression',
'orderBy',
'functions',
'stepInterval',
@@ -249,6 +281,7 @@ export const panelTypeDataSourceFormValuesMap: Record<
'queryName',
'expression',
'legend',
'aggregations',
],
},
},
@@ -259,11 +292,13 @@ export const panelTypeDataSourceFormValuesMap: Record<
'aggregateOperator',
'timeAggregation',
'filters',
'filter',
'spaceAggregation',
'groupBy',
'reduceTo',
'limit',
'having',
'havingExpression',
'orderBy',
'stepInterval',
'legend',
@@ -271,6 +306,7 @@ export const panelTypeDataSourceFormValuesMap: Record<
'expression',
'disabled',
'functions',
'aggregations',
],
},
},
@@ -280,9 +316,11 @@ export const panelTypeDataSourceFormValuesMap: Record<
'aggregateAttribute',
'aggregateOperator',
'filters',
'filter',
'groupBy',
'limit',
'having',
'havingExpression',
'orderBy',
'functions',
'stepInterval',
@@ -290,6 +328,7 @@ export const panelTypeDataSourceFormValuesMap: Record<
'queryName',
'expression',
'legend',
'aggregations',
],
},
},
@@ -301,9 +340,11 @@ export const panelTypeDataSourceFormValuesMap: Record<
'aggregateAttribute',
'aggregateOperator',
'filters',
'filter',
'groupBy',
'limit',
'having',
'havingExpression',
'orderBy',
'functions',
'stepInterval',
@@ -311,6 +352,7 @@ export const panelTypeDataSourceFormValuesMap: Record<
'queryName',
'expression',
'legend',
'aggregations',
],
},
},
@@ -321,11 +363,13 @@ export const panelTypeDataSourceFormValuesMap: Record<
'aggregateOperator',
'timeAggregation',
'filters',
'filter',
'spaceAggregation',
'groupBy',
'reduceTo',
'limit',
'having',
'havingExpression',
'orderBy',
'stepInterval',
'legend',
@@ -333,6 +377,7 @@ export const panelTypeDataSourceFormValuesMap: Record<
'expression',
'disabled',
'functions',
'aggregations',
],
},
},
@@ -342,9 +387,11 @@ export const panelTypeDataSourceFormValuesMap: Record<
'aggregateAttribute',
'aggregateOperator',
'filters',
'filter',
'groupBy',
'limit',
'having',
'havingExpression',
'orderBy',
'functions',
'stepInterval',
@@ -352,6 +399,7 @@ export const panelTypeDataSourceFormValuesMap: Record<
'queryName',
'expression',
'legend',
'aggregations',
],
},
},
@@ -359,17 +407,31 @@ export const panelTypeDataSourceFormValuesMap: Record<
[PANEL_TYPES.LIST]: {
[DataSource.LOGS]: {
builder: {
queryData: ['filters', 'limit', 'orderBy', 'functions'],
queryData: [
'filters',
'filter',
'limit',
'orderBy',
'functions',
'aggregations',
],
},
},
[DataSource.METRICS]: {
builder: {
queryData: [],
queryData: ['filters', 'filter', 'aggregations'],
},
},
[DataSource.TRACES]: {
builder: {
queryData: ['filters', 'limit', 'orderBy', 'functions'],
queryData: [
'filters',
'filter',
'limit',
'orderBy',
'functions',
'aggregations',
],
},
},
},
@@ -380,14 +442,17 @@ export const panelTypeDataSourceFormValuesMap: Record<
'aggregateAttribute',
'aggregateOperator',
'filters',
'filter',
'reduceTo',
'having',
'havingExpression',
'functions',
'stepInterval',
'queryName',
'expression',
'disabled',
'legend',
'aggregations',
],
},
},
@@ -398,8 +463,10 @@ export const panelTypeDataSourceFormValuesMap: Record<
'aggregateOperator',
'timeAggregation',
'filters',
'filter',
'spaceAggregation',
'having',
'havingExpression',
'reduceTo',
'stepInterval',
'legend',
@@ -407,6 +474,7 @@ export const panelTypeDataSourceFormValuesMap: Record<
'expression',
'disabled',
'functions',
'aggregations',
],
},
},
@@ -416,14 +484,17 @@ export const panelTypeDataSourceFormValuesMap: Record<
'aggregateAttribute',
'aggregateOperator',
'filters',
'filter',
'reduceTo',
'having',
'havingExpression',
'functions',
'stepInterval',
'queryName',
'expression',
'disabled',
'legend',
'aggregations',
],
},
},
@@ -433,6 +504,7 @@ export const panelTypeDataSourceFormValuesMap: Record<
export function handleQueryChange(
newPanelType: keyof PartialPanelTypes,
supersetQuery: Query,
currentPanelType: PANEL_TYPES,
): Query {
return {
...supersetQuery,
@@ -454,6 +526,7 @@ export function handleQueryChange(
set(tempQuery, 'aggregateOperator', 'noop');
set(tempQuery, 'offset', 0);
set(tempQuery, 'pageSize', 10);
set(tempQuery, 'orderBy', undefined);
} else if (tempQuery.aggregateOperator === 'noop') {
// this condition takes care of the part where we start with the list panel type and then shift to other panels
// because in other cases we never set list operator and other fields in superset query rather just update in the current / staged query
@@ -462,6 +535,13 @@ export function handleQueryChange(
unset(tempQuery, 'pageSize');
}
if (
currentPanelType === PANEL_TYPES.LIST &&
newPanelType !== PANEL_TYPES.LIST
) {
set(tempQuery, 'orderBy', undefined);
}
return tempQuery;
}),
},
@@ -486,21 +566,12 @@ export const getDefaultWidgetData = (
timePreferance: 'GLOBAL_TIME',
softMax: null,
softMin: null,
selectedLogFields: [
{
dataType: 'string',
type: '',
name: 'body',
},
{
dataType: 'string',
type: '',
name: 'timestamp',
},
],
selectedTracesFields: [
...listViewInitialTraceQuery.builder.queryData[0].selectColumns,
],
selectedLogFields: defaultLogsSelectedColumns.map((field) => ({
...field,
type: field.fieldContext ?? '',
dataType: field.fieldDataType ?? '',
})),
selectedTracesFields: defaultTraceSelectedColumns,
});
export const PANEL_TYPE_TO_QUERY_TYPES: Record<PANEL_TYPES, EQueryType[]> = {

View File

@@ -42,10 +42,10 @@ function AddColumnField({ config }: AddColumnFieldProps): JSX.Element | null {
</SearchIconWrapper>
</Input.Group>
{config.value?.map(({ key, id }) => (
<AddColumnItem direction="horizontal" key={id}>
<Typography>{key}</Typography>
<DeleteOutlinedIcon onClick={(): void => config.onRemove(id as string)} />
{config.value?.map(({ name }) => (
<AddColumnItem direction="horizontal" key={name}>
<Typography>{name}</Typography>
<DeleteOutlinedIcon onClick={(): void => config.onRemove(name)} />
</AddColumnItem>
))}
</AddColumnWrapper>

View File

@@ -1,4 +1,4 @@
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { TelemetryFieldKey } from 'api/v5/v5';
import { FontSize, OptionsQuery } from './types';
@@ -11,71 +11,56 @@ export const defaultOptionsQuery: OptionsQuery = {
fontSize: FontSize.SMALL,
};
export const defaultLogsSelectedColumns = [
export const defaultLogsSelectedColumns: TelemetryFieldKey[] = [
{
key: 'timestamp',
dataType: DataTypes.String,
type: 'tag',
name: 'timestamp',
signal: 'logs',
fieldContext: 'log',
fieldDataType: '',
isColumn: true,
isJSON: false,
id: 'timestamp--string--tag--true',
isIndexed: false,
},
{
key: 'body',
dataType: DataTypes.String,
type: 'tag',
name: 'body',
signal: 'logs',
fieldContext: 'log',
fieldDataType: '',
isColumn: true,
isJSON: false,
id: 'body--string--tag--true',
isIndexed: false,
},
];
export const defaultTraceSelectedColumns = [
export const defaultTraceSelectedColumns: TelemetryFieldKey[] = [
{
key: 'serviceName',
dataType: DataTypes.String,
type: 'tag',
isColumn: true,
isJSON: false,
id: 'serviceName--string--tag--true',
isIndexed: false,
name: 'service.name',
signal: 'traces',
fieldContext: 'resource',
fieldDataType: 'string',
},
{
key: 'name',
dataType: DataTypes.String,
type: 'tag',
isColumn: true,
isJSON: false,
id: 'name--string--tag--true',
isIndexed: false,
name: 'name',
signal: 'traces',
fieldContext: 'span',
fieldDataType: 'string',
},
{
key: 'durationNano',
dataType: DataTypes.Float64,
type: 'tag',
isColumn: true,
isJSON: false,
id: 'durationNano--float64--tag--true',
isIndexed: false,
name: 'duration_nano',
signal: 'traces',
fieldContext: 'span',
fieldDataType: '',
},
{
key: 'httpMethod',
dataType: DataTypes.String,
type: 'tag',
isColumn: true,
isJSON: false,
id: 'httpMethod--string--tag--true',
isIndexed: false,
name: 'http_method',
signal: 'traces',
fieldContext: 'span',
fieldDataType: '',
},
{
key: 'responseStatusCode',
dataType: DataTypes.String,
type: 'tag',
isColumn: true,
isJSON: false,
id: 'responseStatusCode--string--tag--true',
isIndexed: false,
name: 'response_status_code',
signal: 'traces',
fieldContext: 'span',
fieldDataType: '',
},
];

View File

@@ -1,6 +1,6 @@
import { InputNumberProps, RadioProps, SelectProps } from 'antd';
import { TelemetryFieldKey } from 'api/v5/v5';
import { LogViewMode } from 'container/LogsTable';
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
export enum FontSize {
SMALL = 'small',
@@ -13,7 +13,7 @@ interface FontSizeProps {
onChange: (val: FontSize) => void;
}
export interface OptionsQuery {
selectColumns: BaseAutocompleteData[];
selectColumns: TelemetryFieldKey[];
maxLines: number;
format: LogViewMode;
fontSize: FontSize;
@@ -36,7 +36,7 @@ export type OptionsMenuConfig = {
'options' | 'onSelect' | 'onFocus' | 'onSearch' | 'onBlur'
> & {
isFetching: boolean;
value: BaseAutocompleteData[];
value: TelemetryFieldKey[];
onRemove: (key: string) => void;
};
};

View File

@@ -1,21 +1,24 @@
import { getAggregateKeys } from 'api/queryBuilder/getAttributeKeys';
import { getKeySuggestions } from 'api/querySuggestions/getKeySuggestions';
import { TelemetryFieldKey } from 'api/v5/v5';
import { AxiosResponse } from 'axios';
import { LogViewMode } from 'container/LogsTable';
import { useGetAggregateKeys } from 'hooks/queryBuilder/useGetAggregateKeys';
import { useGetQueryKeySuggestions } from 'hooks/querySuggestions/useGetQueryKeySuggestions';
import useDebounce from 'hooks/useDebounce';
import { useNotifications } from 'hooks/useNotifications';
import useUrlQueryData from 'hooks/useUrlQueryData';
import {
AllTraceFilterKeys,
AllTraceFilterKeyValue,
} from 'pages/TracesExplorer/Filter/filterUtils';
import { AllTraceFilterKeyValue } from 'pages/TracesExplorer/Filter/filterUtils';
import { usePreferenceContext } from 'providers/preferences/context/PreferenceContextProvider';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useQueries } from 'react-query';
import { ErrorResponse, SuccessResponse } from 'types/api';
import {
BaseAutocompleteData,
IQueryAutocompleteResponse,
} from 'types/api/queryBuilder/queryAutocompleteResponse';
QueryKeyRequestProps,
QueryKeySuggestionsResponseProps,
} from 'types/api/querySuggestions/types';
import {
FieldContext,
FieldDataType,
SignalType,
} from 'types/api/v5/queryRange';
import { DataSource } from 'types/common/queryBuilder';
import {
@@ -47,7 +50,7 @@ interface UseOptionsMenu {
const useOptionsMenu = ({
dataSource,
aggregateOperator,
// aggregateOperator,
initialOptions = {},
}: UseOptionsMenuProps): UseOptionsMenu => {
const { notifications } = useNotifications();
@@ -61,15 +64,23 @@ const useOptionsMenu = ({
const [isFocused, setIsFocused] = useState<boolean>(false);
const debouncedSearchText = useDebounce(searchText, 300);
const initialQueryParams = useMemo(
// const initialQueryParams = useMemo(
// () => ({
// searchText: '',
// aggregateAttribute: '',
// tagType: undefined,
// dataSource,
// aggregateOperator,
// }),
// [dataSource, aggregateOperator],
// );
const initialQueryParamsV5: QueryKeyRequestProps = useMemo(
() => ({
signal: dataSource,
searchText: '',
aggregateAttribute: '',
tagType: undefined,
dataSource,
aggregateOperator,
}),
[dataSource, aggregateOperator],
[dataSource],
);
const {
@@ -77,23 +88,37 @@ const useOptionsMenu = ({
redirectWithQuery: redirectWithOptionsData,
} = useUrlQueryData<OptionsQuery>(URL_OPTIONS, defaultOptionsQuery);
const initialQueries = useMemo(
// const initialQueries = useMemo(
// () =>
// initialOptions?.selectColumns?.map((column) => ({
// queryKey: column,
// queryFn: (): Promise<
// SuccessResponse<IQueryAutocompleteResponse> | ErrorResponse
// > =>
// getAggregateKeys({
// ...initialQueryParams,
// searchText: column,
// }),
// enabled: !!column && !optionsQuery,
// })) || [],
// [initialOptions?.selectColumns, initialQueryParams, optionsQuery],
// );
const initialQueriesV5 = useMemo(
() =>
initialOptions?.selectColumns?.map((column) => ({
queryKey: column,
queryFn: (): Promise<
SuccessResponse<IQueryAutocompleteResponse> | ErrorResponse
> =>
getAggregateKeys({
...initialQueryParams,
queryFn: (): Promise<AxiosResponse<QueryKeySuggestionsResponseProps>> =>
getKeySuggestions({
...initialQueryParamsV5,
searchText: column,
}),
enabled: !!column && !optionsQuery,
})) || [],
[initialOptions?.selectColumns, initialQueryParams, optionsQuery],
[initialOptions?.selectColumns, initialQueryParamsV5, optionsQuery],
);
const initialAttributesResult = useQueries(initialQueries);
const initialAttributesResult = useQueries(initialQueriesV5);
const isFetchedInitialAttributes = useMemo(
() => initialAttributesResult.every((result) => result.isFetched),
@@ -106,42 +131,52 @@ const useOptionsMenu = ({
}
const attributesData = initialAttributesResult?.reduce(
(acc, attributeResponse) => {
const data = attributeResponse?.data?.payload?.attributeKeys || [];
(acc: TelemetryFieldKey[], attributeResponse): TelemetryFieldKey[] => {
const suggestions =
Object.values(attributeResponse?.data?.data?.data?.keys || {}).flat() ||
[];
return [...acc, ...data];
const mappedSuggestions: TelemetryFieldKey[] = suggestions.map(
(suggestion) => ({
name: suggestion.name,
signal: suggestion.signal as SignalType,
fieldDataType: suggestion.fieldDataType as FieldDataType,
fieldContext: suggestion.fieldContext as FieldContext,
}),
);
return [...acc, ...mappedSuggestions];
},
[] as BaseAutocompleteData[],
[],
);
let initialSelected = initialOptions.selectColumns
?.map((column) => attributesData.find(({ key }) => key === column))
.filter(Boolean) as BaseAutocompleteData[];
let initialSelected: TelemetryFieldKey[] | undefined =
initialOptions.selectColumns
?.map((column) => attributesData.find(({ name }) => name === column))
.filter((e) => !!e) || [];
if (dataSource === DataSource.TRACES) {
initialSelected = initialSelected
?.map((col) => {
if (col && Object.keys(AllTraceFilterKeyValue).includes(col?.key)) {
if (col && Object.keys(AllTraceFilterKeyValue).includes(col?.name)) {
const metaData = defaultTraceSelectedColumns.find(
(coln) => coln.key === (col.key as AllTraceFilterKeys),
(coln) => coln.name === col.name,
);
return {
...metaData,
key: metaData?.key,
dataType: metaData?.dataType,
type: metaData?.type,
isColumn: metaData?.isColumn,
isJSON: metaData?.isJSON,
id: metaData?.id,
name: metaData?.name || '',
};
}
return col;
})
.filter(Boolean) as BaseAutocompleteData[];
.filter((e) => !!e);
if (!initialSelected || !initialSelected?.length) {
initialSelected = defaultTraceSelectedColumns;
initialSelected = defaultTraceSelectedColumns.map((e) => ({
...e,
name: e.name,
}));
}
}
@@ -154,41 +189,91 @@ const useOptionsMenu = ({
]);
const {
data: searchedAttributesData,
isFetching: isSearchedAttributesFetching,
} = useGetAggregateKeys(
data: searchedAttributesDataV5,
isFetching: isSearchedAttributesFetchingV5,
} = useGetQueryKeySuggestions(
{
...initialQueryParams,
...initialQueryParamsV5,
searchText: debouncedSearchText,
},
{ queryKey: [debouncedSearchText, isFocused], enabled: isFocused },
);
const searchedAttributeKeys = useMemo(() => {
if (searchedAttributesData?.payload?.attributeKeys?.length) {
// const {
// data: searchedAttributesData,
// isFetching: isSearchedAttributesFetching,
// } = useGetAggregateKeys(
// {
// ...initialQueryParams,
// searchText: debouncedSearchText,
// },
// { queryKey: [debouncedSearchText, isFocused], enabled: isFocused },
// );
const searchedAttributeKeys: TelemetryFieldKey[] = useMemo(() => {
const searchedAttributesDataList = Object.values(
searchedAttributesDataV5?.data.data.keys || {},
).flat();
if (searchedAttributesDataList.length) {
if (dataSource === DataSource.LOGS) {
const logsSelectedColumns: TelemetryFieldKey[] = defaultLogsSelectedColumns.map(
(e) => ({
...e,
name: e.name,
signal: e.signal as SignalType,
fieldContext: e.fieldContext as FieldContext,
fieldDataType: e.fieldDataType as FieldDataType,
}),
);
return [
...defaultLogsSelectedColumns,
...searchedAttributesData.payload.attributeKeys.filter(
(attribute) => attribute.key !== 'body',
),
...logsSelectedColumns,
...searchedAttributesDataList
.filter((attribute) => attribute.name !== 'body')
// eslint-disable-next-line sonarjs/no-identical-functions
.map((e) => ({
...e,
name: e.name,
signal: e.signal as SignalType,
fieldContext: e.fieldContext as FieldContext,
fieldDataType: e.fieldDataType as FieldDataType,
})),
];
}
return searchedAttributesData.payload.attributeKeys;
// eslint-disable-next-line sonarjs/no-identical-functions
return searchedAttributesDataList.map((e) => ({
...e,
name: e.name,
signal: e.signal as SignalType,
fieldContext: e.fieldContext as FieldContext,
fieldDataType: e.fieldDataType as FieldDataType,
}));
}
if (dataSource === DataSource.TRACES) {
return defaultTraceSelectedColumns;
return defaultTraceSelectedColumns.map((e) => ({
...e,
name: e.name,
}));
}
return [];
}, [dataSource, searchedAttributesData?.payload?.attributeKeys]);
}, [dataSource, searchedAttributesDataV5?.data.data.keys]);
const initialOptionsQuery: OptionsQuery = useMemo(() => {
let defaultColumns = defaultOptionsQuery.selectColumns;
let defaultColumns: TelemetryFieldKey[] = defaultOptionsQuery.selectColumns;
if (dataSource === DataSource.TRACES) {
defaultColumns = defaultTraceSelectedColumns;
defaultColumns = defaultTraceSelectedColumns.map((e) => ({
...e,
name: e.name,
}));
} else if (dataSource === DataSource.LOGS) {
defaultColumns = defaultLogsSelectedColumns;
// eslint-disable-next-line sonarjs/no-identical-functions
defaultColumns = defaultLogsSelectedColumns.map((e) => ({
...e,
name: e.name,
signal: e.signal as SignalType,
fieldContext: e.fieldContext as FieldContext,
fieldDataType: e.fieldDataType as FieldDataType,
}));
}
const finalSelectColumns = initialOptions?.selectColumns
@@ -203,14 +288,14 @@ const useOptionsMenu = ({
}, [dataSource, initialOptions, initialSelectedColumns]);
const selectedColumnKeys = useMemo(
() => preferences?.columns?.map(({ id }) => id) || [],
() => preferences?.columns?.map(({ name }) => name) || [],
[preferences?.columns],
);
const optionsFromAttributeKeys = useMemo(() => {
const filteredAttributeKeys = searchedAttributeKeys.filter((item) => {
if (dataSource !== DataSource.LOGS) {
return item.key !== 'body';
return item.name !== 'body';
}
return true;
});
@@ -232,11 +317,11 @@ const useOptionsMenu = ({
const column = [
...searchedAttributeKeys,
...(preferences?.columns || []),
].find(({ id }) => id === key);
].find(({ name }) => name === key);
if (!column) return acc;
return [...acc, column];
}, [] as BaseAutocompleteData[]);
}, [] as TelemetryFieldKey[]);
const optionsData: OptionsQuery = {
...defaultOptionsQuery,
@@ -261,7 +346,7 @@ const useOptionsMenu = ({
const handleRemoveSelectedColumn = useCallback(
(columnKey: string) => {
const newSelectedColumns = preferences?.columns?.filter(
({ id }) => id !== columnKey,
({ name }) => name !== columnKey,
);
if (!newSelectedColumns?.length && dataSource !== DataSource.LOGS) {
@@ -367,7 +452,7 @@ const useOptionsMenu = ({
const optionsMenuConfig: Required<OptionsMenuConfig> = useMemo(
() => ({
addColumn: {
isFetching: isSearchedAttributesFetching,
isFetching: isSearchedAttributesFetchingV5,
value: preferences?.columns || defaultOptionsQuery.selectColumns,
options: optionsFromAttributeKeys || [],
onFocus: handleFocus,
@@ -390,7 +475,7 @@ const useOptionsMenu = ({
},
}),
[
isSearchedAttributesFetching,
isSearchedAttributesFetchingV5,
preferences,
optionsFromAttributeKeys,
handleSelectColumns,

View File

@@ -1,13 +1,13 @@
import { SelectProps } from 'antd';
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { TelemetryFieldKey } from 'api/v5/v5';
export const getOptionsFromKeys = (
keys: BaseAutocompleteData[],
keys: TelemetryFieldKey[],
selectedKeys: (string | undefined)[],
): SelectProps['options'] => {
const options = keys.map(({ id, key }) => ({
label: key,
value: id,
const options = keys.map(({ name }) => ({
label: name,
value: name,
}));
return options.filter(

View File

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

View File

@@ -28,5 +28,6 @@ export type QueryBuilderProps = {
queryComponents?: { renderOrderBy?: (props: OrderByFilterProps) => ReactNode };
isListViewPanel?: boolean;
showFunctions?: boolean;
showOnlyWhereClause?: boolean;
version: string;
};

View File

@@ -78,11 +78,11 @@
color: var(--bg-sakura-400);
}
&.sync-btn {
border: 1px solid rgba(78, 116, 248, 0.2);
background: rgba(78, 116, 248, 0.1);
color: var(--bg-robin-500);
}
// &.sync-btn {
// border: 1px solid rgba(78, 116, 248, 0.2);
// background: rgba(78, 116, 248, 0.1);
// color: var(--bg-robin-500);
// }
}
&.formula-btn {

View File

@@ -161,15 +161,18 @@ export function Formula({
<Col span={24}>
<Input.TextArea
name="expression"
className="formula-expression"
onChange={handleChange}
size="middle"
value={formula.expression}
placeholder="Enter formula"
rows={2}
/>
</Col>
<Col span={24}>
<Input
name="legend"
className="formula-legend"
onChange={handleChange}
size="middle"
value={formula.legend}

View File

@@ -28,9 +28,12 @@
border-radius: 2px;
.periscope-btn {
border: 1px solid var(--bg-slate-200);
background: var(--bg-ink-200);
min-width: 32px;
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);
}
.query-name {
@@ -44,11 +47,11 @@
border: 1px solid rgba(242, 71, 105, 0.2) !important;
background: rgba(242, 71, 105, 0.1) !important;
&.sync-btn {
border: 1px solid rgba(78, 116, 248, 0.2) !important;
background: rgba(78, 116, 248, 0.1) !important;
color: var(--bg-robin-500) !important;
}
// &.sync-btn {
// border: 1px solid rgba(78, 116, 248, 0.2) !important;
// background: rgba(78, 116, 248, 0.1) !important;
// color: var(--bg-robin-500) !important;
// }
&:hover {
border: 1px solid rgba(242, 71, 105, 0.4) !important;

View File

@@ -22,6 +22,7 @@ import {
} from 'types/api/queryBuilder/queryBuilderData';
import { DataSource } from 'types/common/queryBuilder';
import { DataSourceDropdown } from '..';
import QueryFunctions from '../QueryFunctions/QueryFunctions';
interface QBEntityOptionsProps {
@@ -31,16 +32,20 @@ interface QBEntityOptionsProps {
isCollapsed: boolean;
entityType: string;
entityData: any;
onDelete: () => void;
onDelete?: () => void;
onCloneQuery?: (type: string, query: IBuilderQuery) => void;
onToggleVisibility: () => void;
onCollapseEntity: () => void;
onQueryFunctionsUpdates?: (functions: QueryFunctionProps[]) => void;
showDeleteButton: boolean;
showDeleteButton?: boolean;
showCloneOption?: boolean;
isListViewPanel?: boolean;
index?: number;
queryVariant?: 'dropdown' | 'static';
onChangeDataSource?: (value: DataSource) => void;
}
// eslint-disable-next-line sonarjs/cognitive-complexity
export default function QBEntityOptions({
query,
isMetricsDataSource,
@@ -48,14 +53,17 @@ export default function QBEntityOptions({
showFunctions,
entityType,
entityData,
onDelete,
onCloneQuery,
onToggleVisibility,
onCollapseEntity,
showDeleteButton,
onQueryFunctionsUpdates,
isListViewPanel,
onDelete,
showDeleteButton,
showCloneOption,
onCloneQuery,
index,
queryVariant,
onChangeDataSource,
}: QBEntityOptionsProps): JSX.Element {
const handleCloneEntity = (): void => {
if (isFunction(onCloneQuery)) {
@@ -97,7 +105,7 @@ export default function QBEntityOptions({
</Button>
</Tooltip>
{entityType === 'query' && (
{entityType === 'query' && showCloneOption && (
<Tooltip title={`Clone Query ${entityData.queryName}`}>
<Button className={cx('periscope-btn')} onClick={handleCloneEntity}>
<Copy size={14} />
@@ -115,7 +123,23 @@ export default function QBEntityOptions({
{entityData.queryName}
</Button>
{queryVariant === 'dropdown' && (
<div className="query-data-source">
<DataSourceDropdown
onChange={(value): void => {
if (onChangeDataSource) {
onChangeDataSource(value);
}
}}
value={query?.dataSource || DataSource.METRICS}
isListViewPanel={isListViewPanel}
className="query-data-source-dropdown"
/>
</div>
)}
{showFunctions &&
!isListViewPanel &&
(isMetricsDataSource || isLogsDataSource) &&
query &&
onQueryFunctionsUpdates && (
@@ -138,7 +162,7 @@ export default function QBEntityOptions({
)}
</div>
{showDeleteButton && (
{showDeleteButton && !isListViewPanel && (
<Button className="periscope-btn ghost" onClick={onDelete}>
<Trash2 size={14} />
</Button>
@@ -156,4 +180,9 @@ QBEntityOptions.defaultProps = {
showFunctions: false,
onCloneQuery: noop,
index: 0,
onDelete: noop,
showDeleteButton: false,
showCloneOption: true,
queryVariant: 'static',
onChangeDataSource: noop,
};

View File

@@ -5,8 +5,10 @@ export type QueryProps = {
index: number;
isAvailableToDisable: boolean;
query: IBuilderQuery;
queryVariant: 'static' | 'dropdown';
queryVariant?: 'static' | 'dropdown';
isListViewPanel?: boolean;
showFunctions?: boolean;
version: string;
showSpanScopeSelector?: boolean;
showOnlyWhereClause?: boolean;
} & Pick<QueryBuilderProps, 'filterConfigs' | 'queryComponents'>;

View File

@@ -350,6 +350,7 @@ export const Query = memo(function Query({
showDeleteButton={currentQuery.builder.queryData.length > 1}
isListViewPanel={isListViewPanel}
index={index}
queryVariant={queryVariant}
/>
{!isCollapse && (

View File

@@ -10,6 +10,7 @@ interface SpaceAggregationOptionsProps {
disabled: boolean;
onSelect: (value: string) => void;
operators: any[];
qbVersion?: string;
}
export default function SpaceAggregationOptions({
@@ -19,8 +20,10 @@ export default function SpaceAggregationOptions({
disabled,
onSelect,
operators,
qbVersion,
}: SpaceAggregationOptionsProps): JSX.Element {
const placeHolderText = panelType === PANEL_TYPES.VALUE ? 'Sum' : 'Sum By';
const placeHolderText =
panelType === PANEL_TYPES.VALUE || qbVersion === 'v3' ? 'Sum' : 'Sum By';
const [defaultValue, setDefaultValue] = useState(
selectedValue || placeHolderText,
);
@@ -58,10 +61,15 @@ export default function SpaceAggregationOptions({
>
{operators.map((operator) => (
<Select.Option key={operator.value} value={operator.value}>
{operator.label} {panelType !== PANEL_TYPES.VALUE ? ' By' : ''}
{operator.label}{' '}
{panelType !== PANEL_TYPES.VALUE && qbVersion === 'v2' ? ' By' : ''}
</Select.Option>
))}
</Select>
</div>
);
}
SpaceAggregationOptions.defaultProps = {
qbVersion: 'v2',
};

View File

@@ -1,35 +1,31 @@
/* eslint-disable sonarjs/no-duplicate-string */
import './ToolbarActions.styles.scss';
import { FilterOutlined } from '@ant-design/icons';
import { Button, Switch, Tooltip, Typography } from 'antd';
import { Button, Tooltip } from 'antd';
import cx from 'classnames';
import { Atom, SquareMousePointer, Terminal } from 'lucide-react';
import { SELECTED_VIEWS } from 'pages/LogsExplorer/utils';
import { Atom, Binoculars, SquareMousePointer, Terminal } from 'lucide-react';
import { ExplorerViews } from 'pages/LogsExplorer/utils';
interface LeftToolbarActionsProps {
items: any;
selectedView: string;
onToggleHistrogramVisibility: () => void;
onChangeSelectedView: (view: SELECTED_VIEWS) => void;
showFrequencyChart: boolean;
onChangeSelectedView: (view: ExplorerViews) => void;
showFilter: boolean;
handleFilterVisibilityChange: () => void;
}
const activeTab = 'active-tab';
const actionBtn = 'action-btn';
export const queryBuilder = 'query-builder';
export default function LeftToolbarActions({
items,
selectedView,
onToggleHistrogramVisibility,
onChangeSelectedView,
showFrequencyChart,
showFilter,
handleFilterVisibilityChange,
}: LeftToolbarActionsProps): JSX.Element {
const { clickhouse, search, queryBuilder: QB } = items;
const { clickhouse, list, timeseries, table, trace } = items;
return (
<div className="left-toolbar">
@@ -41,56 +37,90 @@ export default function LeftToolbarActions({
</Tooltip>
)}
<div className="left-toolbar-query-actions">
<Tooltip title="Search">
<Button
disabled={search.disabled}
className={cx(
'search',
actionBtn,
selectedView === 'search' ? activeTab : '',
)}
onClick={(): void => onChangeSelectedView(SELECTED_VIEWS.SEARCH)}
>
<SquareMousePointer size={14} data-testid="search-view" />
</Button>
</Tooltip>
<Tooltip title="Query Builder">
<Button
disabled={QB.disabled}
className={cx(
queryBuilder,
actionBtn,
selectedView === queryBuilder ? activeTab : '',
)}
onClick={(): void => onChangeSelectedView(SELECTED_VIEWS.QUERY_BUILDER)}
>
<Atom size={14} data-testid="query-builder-view" />
</Button>
</Tooltip>
{list?.show && (
<Tooltip title="List View">
<Button
disabled={list.disabled}
className={cx(
'list-view-tab',
'explorer-view-option',
selectedView === list.key ? activeTab : '',
)}
onClick={(): void => onChangeSelectedView(list.key)}
>
<SquareMousePointer size={14} data-testid="search-view" />
List View
</Button>
</Tooltip>
)}
{trace?.show && (
<Tooltip title="Trace View">
<Button
disabled={trace.disabled}
className={cx(
'trace-view-tab',
'explorer-view-option',
selectedView === trace.key ? activeTab : '',
)}
onClick={(): void => onChangeSelectedView(trace.key)}
>
<SquareMousePointer size={14} data-testid="trace-view" />
Trace View
</Button>
</Tooltip>
)}
{timeseries?.show && (
<Tooltip title="Time Series">
<Button
disabled={timeseries.disabled}
className={cx(
'timeseries-view-tab',
'explorer-view-option',
selectedView === timeseries.key ? activeTab : '',
)}
onClick={(): void => onChangeSelectedView(timeseries.key)}
>
<Atom size={14} data-testid="query-builder-view" />
Time Series
</Button>
</Tooltip>
)}
{clickhouse?.show && (
<Button
disabled={clickhouse.disabled}
className={cx(
SELECTED_VIEWS.CLICKHOUSE,
actionBtn,
selectedView === SELECTED_VIEWS.CLICKHOUSE ? activeTab : '',
)}
onClick={(): void => onChangeSelectedView(SELECTED_VIEWS.CLICKHOUSE)}
>
<Terminal size={14} data-testid="clickhouse-view" />
</Button>
<Tooltip title="Clickhouse">
<Button
disabled={clickhouse.disabled}
className={cx(
'clickhouse-view-tab',
'explorer-view-option',
selectedView === clickhouse.key ? activeTab : '',
)}
onClick={(): void => onChangeSelectedView(clickhouse.key)}
>
<Terminal size={14} data-testid="clickhouse-view" />
Clickhouse
</Button>
</Tooltip>
)}
</div>
<div className="frequency-chart-view-controller">
<Typography>Frequency chart</Typography>
<Switch
size="small"
checked={showFrequencyChart}
defaultChecked
onChange={onToggleHistrogramVisibility}
/>
{table?.show && (
<Tooltip title="Table">
<Button
disabled={table.disabled}
className={cx(
'table-view-tab',
'explorer-view-option',
selectedView === table.key ? activeTab : '',
)}
onClick={(): void => onChangeSelectedView(table.key)}
>
<Binoculars size={14} data-testid="query-builder-view-v2" />
Table
</Button>
</Tooltip>
)}
</div>
</div>
);

View File

@@ -19,32 +19,51 @@
border: 1px solid var(--bg-slate-400);
background: var(--bg-ink-300);
flex-direction: row;
border-bottom: none;
margin-bottom: -1px;
.prom-ql-icon {
height: 14px;
width: 14px;
}
.ant-btn {
.explorer-view-option {
display: flex;
align-items: center;
justify-content: center;
flex-direction: row;
border: none;
padding: 9px;
box-shadow: none;
border-radius: 0;
border-radius: 0px;
border-left: 1px solid var(--bg-slate-400);
border-bottom: 1px solid var(--bg-slate-400);
gap: 8px;
&.active-tab {
background-color: var(--bg-slate-400);
background-color: var(--bg-ink-500);
border-bottom: 1px solid var(--bg-ink-500);
&:hover {
background-color: var(--bg-ink-500) !important;
}
}
&:disabled {
background-color: #121317;
background-color: var(--bg-ink-300);
opacity: 0.6;
}
}
.action-btn + .action-btn {
border-left: 1px solid var(--bg-slate-400);
&:first-child {
border-left: 1px solid transparent;
}
&:hover {
background-color: transparent !important;
border-left: 1px solid transparent !important;
color: var(--bg-vanilla-100);
}
}
}
@@ -108,32 +127,36 @@
.lightMode {
.left-toolbar {
.filter-btn {
border-color: var(--bg-vanilla-300) !important;
}
.left-toolbar-query-actions {
border-color: var(--bg-vanilla-300);
background: var(--bg-vanilla-100);
border-color: var(--bg-vanilla-300) !important;
background: var(--bg-vanilla-100) !important;
.ant-btn {
border-color: var(--bg-vanilla-300);
background: var(--bg-vanilla-100);
color: var(--bg-ink-200);
border-color: var(--bg-vanilla-300) !important;
background: var(--bg-vanilla-100) !important;
color: var(--bg-ink-200) !important;
&.active-tab {
background-color: var(--bg-robin-100);
background-color: var(--bg-robin-100) !important;
}
}
}
}
.loading-container {
.loading-btn {
background: var(--bg-vanilla-300);
background: var(--bg-vanilla-300) !important;
}
.cancel-run {
color: var(--bg-vanilla-100);
color: var(--bg-vanilla-100) !important;
}
.cancel-run:hover {
background-color: #ff7875;
background-color: #ff7875 !important;
}
}
}

View File

@@ -1,6 +1,6 @@
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { SELECTED_VIEWS } from 'pages/LogsExplorer/utils';
import { ExplorerViews } from 'pages/LogsExplorer/utils';
import MockQueryClientProvider from 'providers/test/MockQueryClientProvider';
import LeftToolbarActions from '../LeftToolbarActions';
@@ -9,7 +9,6 @@ import RightToolbarActions from '../RightToolbarActions';
describe('ToolbarActions', () => {
it('LeftToolbarActions - renders correctly with default props', async () => {
const handleChangeSelectedView = jest.fn();
const handleToggleShowFrequencyChart = jest.fn();
const { queryByTestId } = render(
<LeftToolbarActions
items={{
@@ -31,10 +30,8 @@ describe('ToolbarActions', () => {
disabled: false,
},
}}
selectedView={SELECTED_VIEWS.SEARCH}
selectedView={ExplorerViews.LIST}
onChangeSelectedView={handleChangeSelectedView}
onToggleHistrogramVisibility={handleToggleShowFrequencyChart}
showFrequencyChart
showFilter
handleFilterVisibilityChange={(): void => {}}
/>,
@@ -77,10 +74,8 @@ describe('ToolbarActions', () => {
show: true,
},
}}
selectedView={SELECTED_VIEWS.QUERY_BUILDER}
selectedView={ExplorerViews.TIMESERIES}
onChangeSelectedView={handleChangeSelectedView}
onToggleHistrogramVisibility={handleToggleShowFrequencyChart}
showFrequencyChart
showFilter
handleFilterVisibilityChange={(): void => {}}
/>,

View File

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

View File

@@ -9,6 +9,7 @@ export const OperatorsSelect = memo(function OperatorsSelect({
operators,
value,
onChange,
className,
...props
}: OperatorsSelectProps): JSX.Element {
return (
@@ -20,6 +21,7 @@ export const OperatorsSelect = memo(function OperatorsSelect({
showSearch
// eslint-disable-next-line react/jsx-props-no-spreading
{...props}
popupClassName={className}
/>
);
});

View File

@@ -8,6 +8,7 @@ export type OrderByFilterProps = {
onChange: (values: OrderByPayload[]) => void;
isListViewPanel?: boolean;
entityVersion?: string;
isNewQueryV2?: boolean;
};
export type OrderByFilterValue = {

View File

@@ -2,6 +2,7 @@ import { Select, Spin } from 'antd';
import { useGetAggregateKeys } from 'hooks/queryBuilder/useGetAggregateKeys';
import { useMemo } from 'react';
import { DataSource, MetricAggregateOperator } from 'types/common/queryBuilder';
import { getParsedAggregationOptionsForOrderBy } from 'utils/aggregationConverter';
import { popupContainer } from 'utils/selectPopupContainer';
import { selectStyle } from '../QueryBuilderSearch/config';
@@ -13,6 +14,7 @@ export function OrderByFilter({
onChange,
isListViewPanel = false,
entityVersion,
isNewQueryV2 = false,
}: OrderByFilterProps): JSX.Element {
const {
debouncedSearchText,
@@ -37,22 +39,35 @@ export function OrderByFilter({
},
);
// Get parsed aggregation options using createAggregation only for QueryV2
const parsedAggregationOptions = useMemo(
() => (isNewQueryV2 ? getParsedAggregationOptionsForOrderBy(query) : []),
[query, isNewQueryV2],
);
const optionsData = useMemo(() => {
const keyOptions = createOptions(data?.payload?.attributeKeys || []);
const groupByOptions = createOptions(query.groupBy);
const aggregationOptionsFromParsed = createOptions(parsedAggregationOptions);
const options =
query.aggregateOperator === MetricAggregateOperator.NOOP
? keyOptions
: [...groupByOptions, ...aggregationOptions];
: [
...groupByOptions,
...(isNewQueryV2 ? aggregationOptionsFromParsed : aggregationOptions),
];
return generateOptions(options);
}, [
aggregationOptions,
createOptions,
data?.payload?.attributeKeys,
generateOptions,
query.aggregateOperator,
query.groupBy,
query.aggregateOperator,
parsedAggregationOptions,
aggregationOptions,
generateOptions,
isNewQueryV2,
]);
const isDisabledSelect =

View File

@@ -1 +1,5 @@
export const selectStyle = { width: '100%', minWidth: '7.7rem' };
export const selectStyle = {
width: '100%',
minWidth: '7.7rem',
height: '100%',
};

View File

@@ -17,6 +17,7 @@
.query-builder-search-v2 {
display: flex;
gap: 4px;
flex: 1;
.ant-select-dropdown {
padding: 0px;

View File

@@ -450,7 +450,7 @@ function QueryBuilderSearchV2(
if ((event.ctrlKey || event.metaKey) && event.key === 'Enter') {
event.preventDefault();
event.stopPropagation();
handleRunQuery();
handleRunQuery(false, true);
setIsOpen(false);
}
},

View File

@@ -21,21 +21,23 @@ function ResourceAttributesFilter(): JSX.Element | null {
// initialise tab with default query.
useShareBuilderUrl({
...initialQueriesMap.traces,
builder: {
...initialQueriesMap.traces.builder,
queryData: [
{
...initialQueriesMap.traces.builder.queryData[0],
dataSource: DataSource.TRACES,
aggregateOperator: 'noop',
aggregateAttribute: {
...initialQueriesMap.traces.builder.queryData[0].aggregateAttribute,
type: 'resource',
defaultValue: {
...initialQueriesMap.traces,
builder: {
...initialQueriesMap.traces.builder,
queryData: [
{
...initialQueriesMap.traces.builder.queryData[0],
dataSource: DataSource.TRACES,
aggregateOperator: 'noop',
aggregateAttribute: {
...initialQueriesMap.traces.builder.queryData[0].aggregateAttribute,
type: 'resource',
},
queryName: '',
},
queryName: '',
},
],
],
},
},
});

View File

@@ -0,0 +1,11 @@
.time-series-view {
height: 50vh;
min-height: 350px;
padding: 0px 12px;
.ant-card-body {
height: 50vh;
min-height: 350px;
padding: 0px 12px;
}
}

View File

@@ -33,8 +33,6 @@ import { GlobalReducer } from 'types/reducer/globalTime';
import uPlot from 'uplot';
import { getTimeRange } from 'utils/getTimeRange';
import { Container } from './styles';
function TimeSeriesView({
data,
isLoading,
@@ -162,7 +160,7 @@ function TimeSeriesView({
});
return (
<Container>
<div className="time-series-view">
{isError && <LogsError />}
<div
className="graph-container"
@@ -204,7 +202,7 @@ function TimeSeriesView({
!isEmpty(chartData?.[0]) &&
chartOptions && <Uplot data={chartData} options={chartOptions} />}
</div>
</Container>
</div>
);
}

View File

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

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