Compare commits
85 Commits
v0.89.0-cl
...
fix/query-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f9414d2e8a | ||
|
|
6d110870a4 | ||
|
|
5bebc815e3 | ||
|
|
b6fe0619b0 | ||
|
|
bb8f6b9eb1 | ||
|
|
c783c5c7ee | ||
|
|
6f08aff58b | ||
|
|
86229dd55b | ||
|
|
d9519d6896 | ||
|
|
3da7ce5f03 | ||
|
|
e2498863e7 | ||
|
|
25c94dfa93 | ||
|
|
abe617bcd0 | ||
|
|
d8dd3cddd8 | ||
|
|
6ccb44a3df | ||
|
|
e30953fc93 | ||
|
|
9010220b44 | ||
|
|
48cdf439a9 | ||
|
|
52656b2ee8 | ||
|
|
51a7dae5d9 | ||
|
|
b27901cd70 | ||
|
|
e0c30a0e4b | ||
|
|
cd29cfb609 | ||
|
|
0325a5f64d | ||
|
|
af2814a535 | ||
|
|
8d49c7934b | ||
|
|
8c992d10be | ||
|
|
aa6008649d | ||
|
|
8d73f79299 | ||
|
|
62ecc44510 | ||
|
|
1e7b2a7734 | ||
|
|
35c49c45da | ||
|
|
e9bed072e4 | ||
|
|
ab32765dec | ||
|
|
959e518907 | ||
|
|
255fe1b70a | ||
|
|
44eb60fef9 | ||
|
|
14e4273ae9 | ||
|
|
7a98bed694 | ||
|
|
231b9072b1 | ||
|
|
5eca9efcd5 | ||
|
|
a9303d3482 | ||
|
|
5da45d8ade | ||
|
|
c07ada0d71 | ||
|
|
47ca071d36 | ||
|
|
51d697c91a | ||
|
|
00ce2e3034 | ||
|
|
1104eca146 | ||
|
|
ab594cd9ba | ||
|
|
de3497d439 | ||
|
|
e319394ac4 | ||
|
|
559be9a571 | ||
|
|
9c685837eb | ||
|
|
bc3d162016 | ||
|
|
6b406ec9de | ||
|
|
b7e08a746c | ||
|
|
2119bc4c26 | ||
|
|
6b936ea5e1 | ||
|
|
39eaa722ac | ||
|
|
5c2b7e545e | ||
|
|
0da6e76ba6 | ||
|
|
4f82e9cd9c | ||
|
|
6cfaba6bd7 | ||
|
|
2c177232b1 | ||
|
|
efb82abee7 | ||
|
|
159ff84768 | ||
|
|
2961581a52 | ||
|
|
91a9ca8cf8 | ||
|
|
b43f7cfb87 | ||
|
|
aa321a82e1 | ||
|
|
6e09bca806 | ||
|
|
c9e61e3d66 | ||
|
|
e7d17c54a7 | ||
|
|
abc934d88f | ||
|
|
19eec82770 | ||
|
|
b43891193e | ||
|
|
36003324f3 | ||
|
|
3e5ff4cdda | ||
|
|
d174038dce | ||
|
|
78d09e2940 | ||
|
|
6cb7f152e1 | ||
|
|
f6730d3d09 | ||
|
|
899a6ab70a | ||
|
|
a4b852bb99 | ||
|
|
92cd108c0d |
@@ -200,7 +200,7 @@ func NewServer(config signoz.Config, signoz *signoz.SigNoz, jwt *authtypes.JWT)
|
|||||||
s.privateHTTP = privateServer
|
s.privateHTTP = privateServer
|
||||||
|
|
||||||
s.opampServer = opamp.InitializeServer(
|
s.opampServer = opamp.InitializeServer(
|
||||||
&opAmpModel.AllAgents, agentConfMgr,
|
&opAmpModel.AllAgents, agentConfMgr, signoz.Instrumentation,
|
||||||
)
|
)
|
||||||
|
|
||||||
orgs, err := apiHandler.Signoz.Modules.OrgGetter.ListByOwnedKeyRange(context.Background())
|
orgs, err := apiHandler.Signoz.Modules.OrgGetter.ListByOwnedKeyRange(context.Background())
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
|
ignorePatterns: ['src/parser/*.ts'],
|
||||||
env: {
|
env: {
|
||||||
browser: true,
|
browser: true,
|
||||||
es2021: true,
|
es2021: true,
|
||||||
|
|||||||
154
frontend/docs/QuerySearch.md
Normal file
154
frontend/docs/QuerySearch.md
Normal 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
|
||||||
@@ -28,6 +28,8 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ant-design/colors": "6.0.0",
|
"@ant-design/colors": "6.0.0",
|
||||||
"@ant-design/icons": "4.8.0",
|
"@ant-design/icons": "4.8.0",
|
||||||
|
"@codemirror/autocomplete": "6.18.6",
|
||||||
|
"@codemirror/lang-javascript": "6.2.3",
|
||||||
"@dnd-kit/core": "6.1.0",
|
"@dnd-kit/core": "6.1.0",
|
||||||
"@dnd-kit/modifiers": "7.0.0",
|
"@dnd-kit/modifiers": "7.0.0",
|
||||||
"@dnd-kit/sortable": "8.0.0",
|
"@dnd-kit/sortable": "8.0.0",
|
||||||
@@ -43,6 +45,8 @@
|
|||||||
"@signozhq/design-tokens": "1.1.4",
|
"@signozhq/design-tokens": "1.1.4",
|
||||||
"@tanstack/react-table": "8.20.6",
|
"@tanstack/react-table": "8.20.6",
|
||||||
"@tanstack/react-virtual": "3.11.2",
|
"@tanstack/react-virtual": "3.11.2",
|
||||||
|
"@uiw/codemirror-theme-copilot": "4.23.11",
|
||||||
|
"@uiw/react-codemirror": "4.23.10",
|
||||||
"@uiw/react-md-editor": "3.23.5",
|
"@uiw/react-md-editor": "3.23.5",
|
||||||
"@visx/group": "3.3.0",
|
"@visx/group": "3.3.0",
|
||||||
"@visx/hierarchy": "3.12.0",
|
"@visx/hierarchy": "3.12.0",
|
||||||
@@ -53,6 +57,7 @@
|
|||||||
"antd": "5.11.0",
|
"antd": "5.11.0",
|
||||||
"antd-table-saveas-excel": "2.2.1",
|
"antd-table-saveas-excel": "2.2.1",
|
||||||
"axios": "1.8.2",
|
"axios": "1.8.2",
|
||||||
|
"antlr4": "4.13.2",
|
||||||
"babel-eslint": "^10.1.0",
|
"babel-eslint": "^10.1.0",
|
||||||
"babel-jest": "^29.6.4",
|
"babel-jest": "^29.6.4",
|
||||||
"babel-loader": "9.1.3",
|
"babel-loader": "9.1.3",
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ const apiV1 = '/api/v1/';
|
|||||||
export const apiV2 = '/api/v2/';
|
export const apiV2 = '/api/v2/';
|
||||||
export const apiV3 = '/api/v3/';
|
export const apiV3 = '/api/v3/';
|
||||||
export const apiV4 = '/api/v4/';
|
export const apiV4 = '/api/v4/';
|
||||||
|
export const apiV5 = '/api/v5/';
|
||||||
export const gatewayApiV1 = '/api/gateway/v1/';
|
export const gatewayApiV1 = '/api/gateway/v1/';
|
||||||
export const gatewayApiV2 = '/api/gateway/v2/';
|
export const gatewayApiV2 = '/api/gateway/v2/';
|
||||||
export const apiAlertManager = '/api/alertmanager/';
|
export const apiAlertManager = '/api/alertmanager/';
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import apiV1, {
|
|||||||
apiV2,
|
apiV2,
|
||||||
apiV3,
|
apiV3,
|
||||||
apiV4,
|
apiV4,
|
||||||
|
apiV5,
|
||||||
gatewayApiV1,
|
gatewayApiV1,
|
||||||
gatewayApiV2,
|
gatewayApiV2,
|
||||||
} from './apiV1';
|
} from './apiV1';
|
||||||
@@ -171,6 +172,18 @@ ApiV4Instance.interceptors.response.use(
|
|||||||
ApiV4Instance.interceptors.request.use(interceptorsRequestResponse);
|
ApiV4Instance.interceptors.request.use(interceptorsRequestResponse);
|
||||||
//
|
//
|
||||||
|
|
||||||
|
// axios V5
|
||||||
|
export const ApiV5Instance = axios.create({
|
||||||
|
baseURL: `${ENVIRONMENT.baseURL}${apiV5}`,
|
||||||
|
});
|
||||||
|
|
||||||
|
ApiV5Instance.interceptors.response.use(
|
||||||
|
interceptorsResponse,
|
||||||
|
interceptorRejected,
|
||||||
|
);
|
||||||
|
ApiV5Instance.interceptors.request.use(interceptorsRequestResponse);
|
||||||
|
//
|
||||||
|
|
||||||
// axios Base
|
// axios Base
|
||||||
export const ApiBaseInstance = axios.create({
|
export const ApiBaseInstance = axios.create({
|
||||||
baseURL: `${ENVIRONMENT.baseURL}${apiV1}`,
|
baseURL: `${ENVIRONMENT.baseURL}${apiV1}`,
|
||||||
|
|||||||
11
frontend/src/api/querySuggestions/getKeySuggestions.ts
Normal file
11
frontend/src/api/querySuggestions/getKeySuggestions.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
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}&name=${props.name}`);
|
||||||
11
frontend/src/api/querySuggestions/getValueSuggestion.ts
Normal file
11
frontend/src/api/querySuggestions/getValueSuggestion.ts
Normal 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}`);
|
||||||
168
frontend/src/api/v5/queryRange/constants.ts
Normal file
168
frontend/src/api/v5/queryRange/constants.ts
Normal file
@@ -0,0 +1,168 @@
|
|||||||
|
// V5 Query Range Constants
|
||||||
|
|
||||||
|
import { ENTITY_VERSION_V5 } from 'constants/app';
|
||||||
|
import {
|
||||||
|
FunctionName,
|
||||||
|
RequestType,
|
||||||
|
SignalType,
|
||||||
|
Step,
|
||||||
|
} from 'types/api/v5/queryRange';
|
||||||
|
|
||||||
|
// ===================== Schema and Version Constants =====================
|
||||||
|
|
||||||
|
export const SCHEMA_VERSION_V5 = ENTITY_VERSION_V5;
|
||||||
|
export const API_VERSION_V5 = 'v5';
|
||||||
|
|
||||||
|
// ===================== Default Values =====================
|
||||||
|
|
||||||
|
export const DEFAULT_STEP_INTERVAL: Step = '60s';
|
||||||
|
export const DEFAULT_LIMIT = 100;
|
||||||
|
export const DEFAULT_OFFSET = 0;
|
||||||
|
|
||||||
|
// ===================== Request Type Constants =====================
|
||||||
|
|
||||||
|
export const REQUEST_TYPES: Record<string, RequestType> = {
|
||||||
|
SCALAR: 'scalar',
|
||||||
|
TIME_SERIES: 'time_series',
|
||||||
|
RAW: 'raw',
|
||||||
|
DISTRIBUTION: 'distribution',
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
// ===================== Signal Type Constants =====================
|
||||||
|
|
||||||
|
export const SIGNAL_TYPES: Record<string, SignalType> = {
|
||||||
|
TRACES: 'traces',
|
||||||
|
LOGS: 'logs',
|
||||||
|
METRICS: 'metrics',
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
// ===================== Common Aggregation Expressions =====================
|
||||||
|
|
||||||
|
export const TRACE_AGGREGATIONS = {
|
||||||
|
COUNT: 'count()',
|
||||||
|
COUNT_DISTINCT_TRACE_ID: 'count_distinct(traceID)',
|
||||||
|
AVG_DURATION: 'avg(duration_nano)',
|
||||||
|
P50_DURATION: 'p50(duration_nano)',
|
||||||
|
P95_DURATION: 'p95(duration_nano)',
|
||||||
|
P99_DURATION: 'p99(duration_nano)',
|
||||||
|
MAX_DURATION: 'max(duration_nano)',
|
||||||
|
MIN_DURATION: 'min(duration_nano)',
|
||||||
|
SUM_DURATION: 'sum(duration_nano)',
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const LOG_AGGREGATIONS = {
|
||||||
|
COUNT: 'count()',
|
||||||
|
COUNT_DISTINCT_HOST: 'count_distinct(host.name)',
|
||||||
|
COUNT_DISTINCT_SERVICE: 'count_distinct(service.name)',
|
||||||
|
COUNT_DISTINCT_CONTAINER: 'count_distinct(container.name)',
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
// ===================== Common Filter Expressions =====================
|
||||||
|
|
||||||
|
export const COMMON_FILTERS = {
|
||||||
|
// Trace filters
|
||||||
|
SERVER_SPANS: "kind_string = 'Server'",
|
||||||
|
CLIENT_SPANS: "kind_string = 'Client'",
|
||||||
|
INTERNAL_SPANS: "kind_string = 'Internal'",
|
||||||
|
ERROR_SPANS: 'http.status_code >= 400',
|
||||||
|
SUCCESS_SPANS: 'http.status_code < 400',
|
||||||
|
|
||||||
|
// Common service filters
|
||||||
|
EXCLUDE_HEALTH_CHECKS: "http.route != '/health' AND http.route != '/ping'",
|
||||||
|
HTTP_REQUESTS: "http.method != ''",
|
||||||
|
|
||||||
|
// Log filters
|
||||||
|
ERROR_LOGS: "severity_text = 'ERROR'",
|
||||||
|
WARN_LOGS: "severity_text = 'WARN'",
|
||||||
|
INFO_LOGS: "severity_text = 'INFO'",
|
||||||
|
DEBUG_LOGS: "severity_text = 'DEBUG'",
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
// ===================== Common Group By Fields =====================
|
||||||
|
|
||||||
|
export const COMMON_GROUP_BY_FIELDS = {
|
||||||
|
SERVICE_NAME: {
|
||||||
|
name: 'service.name',
|
||||||
|
fieldDataType: 'string' as const,
|
||||||
|
fieldContext: 'resource' as const,
|
||||||
|
},
|
||||||
|
HTTP_METHOD: {
|
||||||
|
name: 'http.method',
|
||||||
|
fieldDataType: 'string' as const,
|
||||||
|
fieldContext: 'attribute' as const,
|
||||||
|
},
|
||||||
|
HTTP_ROUTE: {
|
||||||
|
name: 'http.route',
|
||||||
|
fieldDataType: 'string' as const,
|
||||||
|
fieldContext: 'attribute' as const,
|
||||||
|
},
|
||||||
|
HTTP_STATUS_CODE: {
|
||||||
|
name: 'http.status_code',
|
||||||
|
fieldDataType: 'int64' as const,
|
||||||
|
fieldContext: 'attribute' as const,
|
||||||
|
},
|
||||||
|
HOST_NAME: {
|
||||||
|
name: 'host.name',
|
||||||
|
fieldDataType: 'string' as const,
|
||||||
|
fieldContext: 'resource' as const,
|
||||||
|
},
|
||||||
|
CONTAINER_NAME: {
|
||||||
|
name: 'container.name',
|
||||||
|
fieldDataType: 'string' as const,
|
||||||
|
fieldContext: 'resource' as const,
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
// ===================== Function Names =====================
|
||||||
|
|
||||||
|
export const FUNCTION_NAMES: Record<string, FunctionName> = {
|
||||||
|
CUT_OFF_MIN: 'cutOffMin',
|
||||||
|
CUT_OFF_MAX: 'cutOffMax',
|
||||||
|
CLAMP_MIN: 'clampMin',
|
||||||
|
CLAMP_MAX: 'clampMax',
|
||||||
|
ABSOLUTE: 'absolute',
|
||||||
|
RUNNING_DIFF: 'runningDiff',
|
||||||
|
LOG2: 'log2',
|
||||||
|
LOG10: 'log10',
|
||||||
|
CUM_SUM: 'cumSum',
|
||||||
|
EWMA3: 'ewma3',
|
||||||
|
EWMA5: 'ewma5',
|
||||||
|
EWMA7: 'ewma7',
|
||||||
|
MEDIAN3: 'median3',
|
||||||
|
MEDIAN5: 'median5',
|
||||||
|
MEDIAN7: 'median7',
|
||||||
|
TIME_SHIFT: 'timeShift',
|
||||||
|
ANOMALY: 'anomaly',
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
// ===================== Common Step Intervals =====================
|
||||||
|
|
||||||
|
export const STEP_INTERVALS = {
|
||||||
|
FIFTEEN_SECONDS: '15s',
|
||||||
|
THIRTY_SECONDS: '30s',
|
||||||
|
ONE_MINUTE: '60s',
|
||||||
|
FIVE_MINUTES: '300s',
|
||||||
|
TEN_MINUTES: '600s',
|
||||||
|
FIFTEEN_MINUTES: '900s',
|
||||||
|
THIRTY_MINUTES: '1800s',
|
||||||
|
ONE_HOUR: '3600s',
|
||||||
|
TWO_HOURS: '7200s',
|
||||||
|
SIX_HOURS: '21600s',
|
||||||
|
TWELVE_HOURS: '43200s',
|
||||||
|
ONE_DAY: '86400s',
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
// ===================== Time Range Presets =====================
|
||||||
|
|
||||||
|
export const TIME_RANGE_PRESETS = {
|
||||||
|
LAST_5_MINUTES: 5 * 60 * 1000,
|
||||||
|
LAST_15_MINUTES: 15 * 60 * 1000,
|
||||||
|
LAST_30_MINUTES: 30 * 60 * 1000,
|
||||||
|
LAST_HOUR: 60 * 60 * 1000,
|
||||||
|
LAST_3_HOURS: 3 * 60 * 60 * 1000,
|
||||||
|
LAST_6_HOURS: 6 * 60 * 60 * 1000,
|
||||||
|
LAST_12_HOURS: 12 * 60 * 60 * 1000,
|
||||||
|
LAST_24_HOURS: 24 * 60 * 60 * 1000,
|
||||||
|
LAST_3_DAYS: 3 * 24 * 60 * 60 * 1000,
|
||||||
|
LAST_7_DAYS: 7 * 24 * 60 * 60 * 1000,
|
||||||
|
} as const;
|
||||||
239
frontend/src/api/v5/queryRange/convertV5Response.ts
Normal file
239
frontend/src/api/v5/queryRange/convertV5Response.ts
Normal 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;
|
||||||
|
}
|
||||||
51
frontend/src/api/v5/queryRange/getQueryRange.ts
Normal file
51
frontend/src/api/v5/queryRange/getQueryRange.ts
Normal 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;
|
||||||
393
frontend/src/api/v5/queryRange/prepareQueryRangePayloadV5.ts
Normal file
393
frontend/src/api/v5/queryRange/prepareQueryRangePayloadV5.ts
Normal file
@@ -0,0 +1,393 @@
|
|||||||
|
/* eslint-disable sonarjs/cognitive-complexity */
|
||||||
|
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||||
|
import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults';
|
||||||
|
import getStartEndRangeTime from 'lib/getStartEndRangeTime';
|
||||||
|
import { mapQueryDataToApi } from 'lib/newQueryBuilder/queryBuilderMappers/mapQueryDataToApi';
|
||||||
|
import { isEmpty } from 'lodash-es';
|
||||||
|
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||||
|
import {
|
||||||
|
IBuilderQuery,
|
||||||
|
QueryFunctionProps,
|
||||||
|
} from 'types/api/queryBuilder/queryBuilderData';
|
||||||
|
import {
|
||||||
|
BaseBuilderQuery,
|
||||||
|
FieldContext,
|
||||||
|
FieldDataType,
|
||||||
|
FunctionName,
|
||||||
|
GroupByKey,
|
||||||
|
LogAggregation,
|
||||||
|
MetricAggregation,
|
||||||
|
OrderBy,
|
||||||
|
QueryEnvelope,
|
||||||
|
QueryFunction,
|
||||||
|
QueryRangePayloadV5,
|
||||||
|
QueryType,
|
||||||
|
RequestType,
|
||||||
|
TelemetryFieldKey,
|
||||||
|
TraceAggregation,
|
||||||
|
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: BaseAutocompleteData): TelemetryFieldKey => ({
|
||||||
|
name: column.key,
|
||||||
|
fieldDataType: column?.dataType as FieldDataType,
|
||||||
|
fieldContext: column?.type as FieldContext,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// Utility to parse aggregation expressions with optional alias
|
||||||
|
export function parseAggregations(
|
||||||
|
expression: string,
|
||||||
|
): { expression: string; alias?: string }[] {
|
||||||
|
const result: { expression: string; alias?: string }[] = [];
|
||||||
|
const regex = /([a-zA-Z0-9_]+\([^)]*\))(?:\s*as\s+([a-zA-Z0-9_]+))?/g;
|
||||||
|
let match = regex.exec(expression);
|
||||||
|
while (match !== null) {
|
||||||
|
const expr = match[1];
|
||||||
|
const alias = match[2];
|
||||||
|
if (alias) {
|
||||||
|
result.push({ expression: expr, alias });
|
||||||
|
} else {
|
||||||
|
result.push({ expression: expr });
|
||||||
|
}
|
||||||
|
match = regex.exec(expression);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 };
|
||||||
|
};
|
||||||
8
frontend/src/api/v5/v5.ts
Normal file
8
frontend/src/api/v5/v5.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
// V5 API exports
|
||||||
|
export * from './queryRange/constants';
|
||||||
|
export { convertV5ResponseToLegacy } from './queryRange/convertV5Response';
|
||||||
|
export { getQueryRangeV5 } from './queryRange/getQueryRange';
|
||||||
|
export { prepareQueryRangePayloadV5 } from './queryRange/prepareQueryRangePayloadV5';
|
||||||
|
|
||||||
|
// Export types from proper location
|
||||||
|
export * from 'types/api/v5/queryRange';
|
||||||
@@ -169,6 +169,7 @@
|
|||||||
box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.1);
|
box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.ant-drawer-close {
|
.ant-drawer-close {
|
||||||
padding: 0px;
|
padding: 0px;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -430,9 +430,13 @@ function HostMetricsDetails({
|
|||||||
>
|
>
|
||||||
{host.active ? 'ACTIVE' : 'INACTIVE'}
|
{host.active ? 'ACTIVE' : 'INACTIVE'}
|
||||||
</Tag>
|
</Tag>
|
||||||
<Tag className="infra-monitoring-tags" bordered>
|
{host.os ? (
|
||||||
{host.os}
|
<Tag className="infra-monitoring-tags" bordered>
|
||||||
</Tag>
|
{host.os}
|
||||||
|
</Tag>
|
||||||
|
) : (
|
||||||
|
<Typography.Text>-</Typography.Text>
|
||||||
|
)}
|
||||||
<div className="progress-container">
|
<div className="progress-container">
|
||||||
<Progress
|
<Progress
|
||||||
percent={Number((host.cpu * 100).toFixed(1))}
|
percent={Number((host.cpu * 100).toFixed(1))}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
71
frontend/src/components/InputWithLabel/InputWithLabel.tsx
Normal file
71
frontend/src/components/InputWithLabel/InputWithLabel.tsx
Normal 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;
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
185
frontend/src/components/QueryBuilderV2/QueryBuilderV2.tsx
Normal file
185
frontend/src/components/QueryBuilderV2/QueryBuilderV2.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
});
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
import { createContext, ReactNode, useContext, useMemo, useState } from 'react';
|
||||||
|
|
||||||
|
// Types for the context state
|
||||||
|
export type AggregationOption = { func: string; arg: string };
|
||||||
|
|
||||||
|
interface QueryBuilderV2ContextType {
|
||||||
|
searchText: string;
|
||||||
|
setSearchText: (text: string) => void;
|
||||||
|
aggregationOptions: AggregationOption[];
|
||||||
|
setAggregationOptions: (options: AggregationOption[]) => void;
|
||||||
|
aggregationInterval: string;
|
||||||
|
setAggregationInterval: (interval: string) => void;
|
||||||
|
queryAddValues: any; // Replace 'any' with a more specific type if available
|
||||||
|
setQueryAddValues: (values: any) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QueryBuilderV2Context = createContext<
|
||||||
|
QueryBuilderV2ContextType | undefined
|
||||||
|
>(undefined);
|
||||||
|
|
||||||
|
export function QueryBuilderV2Provider({
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
children: ReactNode;
|
||||||
|
}): JSX.Element {
|
||||||
|
const [searchText, setSearchText] = useState('');
|
||||||
|
const [aggregationOptions, setAggregationOptions] = useState<
|
||||||
|
AggregationOption[]
|
||||||
|
>([]);
|
||||||
|
const [aggregationInterval, setAggregationInterval] = useState('');
|
||||||
|
const [queryAddValues, setQueryAddValues] = useState<any>(null); // Replace 'any' if you have a type
|
||||||
|
|
||||||
|
return (
|
||||||
|
<QueryBuilderV2Context.Provider
|
||||||
|
value={useMemo(
|
||||||
|
() => ({
|
||||||
|
searchText,
|
||||||
|
setSearchText,
|
||||||
|
aggregationOptions,
|
||||||
|
setAggregationOptions,
|
||||||
|
aggregationInterval,
|
||||||
|
setAggregationInterval,
|
||||||
|
queryAddValues,
|
||||||
|
setQueryAddValues,
|
||||||
|
}),
|
||||||
|
[searchText, aggregationOptions, aggregationInterval, queryAddValues],
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</QueryBuilderV2Context.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useQueryBuilderV2Context = (): QueryBuilderV2ContextType => {
|
||||||
|
const context = useContext(QueryBuilderV2Context);
|
||||||
|
if (!context) {
|
||||||
|
throw new Error(
|
||||||
|
'useQueryBuilderV2Context must be used within a QueryBuilderV2Provider',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
};
|
||||||
@@ -0,0 +1,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;
|
||||||
|
}
|
||||||
@@ -0,0 +1,151 @@
|
|||||||
|
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, useMemo } from 'react';
|
||||||
|
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
||||||
|
|
||||||
|
const MetricsAggregateSection = memo(function MetricsAggregateSection({
|
||||||
|
query,
|
||||||
|
index,
|
||||||
|
version,
|
||||||
|
panelType,
|
||||||
|
}: {
|
||||||
|
query: IBuilderQuery;
|
||||||
|
index: number;
|
||||||
|
version: string;
|
||||||
|
panelType: PANEL_TYPES | null;
|
||||||
|
}): JSX.Element {
|
||||||
|
const {
|
||||||
|
operators,
|
||||||
|
spaceAggregationOptions,
|
||||||
|
handleChangeQueryData,
|
||||||
|
handleChangeOperator,
|
||||||
|
handleSpaceAggregationChange,
|
||||||
|
} = useQueryOperations({
|
||||||
|
index,
|
||||||
|
query,
|
||||||
|
entityVersion: version,
|
||||||
|
});
|
||||||
|
|
||||||
|
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;
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import './MetricsSelect.styles.scss';
|
||||||
|
|
||||||
|
import { AggregatorFilter } from 'container/QueryBuilder/filters';
|
||||||
|
import { useQueryOperations } from 'hooks/queryBuilder/useQueryBuilderOperations';
|
||||||
|
import { memo } from 'react';
|
||||||
|
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
||||||
|
|
||||||
|
export const MetricsSelect = memo(function MetricsSelect({
|
||||||
|
query,
|
||||||
|
index,
|
||||||
|
version,
|
||||||
|
}: {
|
||||||
|
query: IBuilderQuery;
|
||||||
|
index: number;
|
||||||
|
version: string;
|
||||||
|
}): JSX.Element {
|
||||||
|
const { handleChangeAggregatorAttribute } = useQueryOperations({
|
||||||
|
index,
|
||||||
|
query,
|
||||||
|
entityVersion: version,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="metrics-select-container">
|
||||||
|
<AggregatorFilter onChange={handleChangeAggregatorAttribute} query={query} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
@@ -0,0 +1,389 @@
|
|||||||
|
/* 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',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// Add common value suggestions
|
||||||
|
const commonValues = [
|
||||||
|
{ label: '0', value: '0 ' },
|
||||||
|
{ label: '1', value: '1 ' },
|
||||||
|
{ label: '5', value: '5 ' },
|
||||||
|
{ label: '10', value: '10 ' },
|
||||||
|
{ label: '50', value: '50 ' },
|
||||||
|
{ label: '100', value: '100 ' },
|
||||||
|
{ label: '1000', value: '1000 ' },
|
||||||
|
];
|
||||||
|
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show value suggestions after operator
|
||||||
|
if (isAfterOperator(tokens)) {
|
||||||
|
return {
|
||||||
|
from: context.pos,
|
||||||
|
options: [
|
||||||
|
...commonValues.map((value) => ({
|
||||||
|
...value,
|
||||||
|
apply: applyValueCompletion,
|
||||||
|
})),
|
||||||
|
{
|
||||||
|
label: 'Enter a custom number value',
|
||||||
|
type: 'text',
|
||||||
|
apply: applyValueCompletion,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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,
|
||||||
|
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;
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
@@ -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;
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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
@@ -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',
|
||||||
|
},
|
||||||
|
];
|
||||||
234
frontend/src/components/QueryBuilderV2/QueryV2/QueryV2.tsx
Normal file
234
frontend/src/components/QueryBuilderV2/QueryV2/QueryV2.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
});
|
||||||
185
frontend/src/components/QueryBuilderV2/utils.ts
Normal file
185
frontend/src/components/QueryBuilderV2/utils.ts
Normal 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,
|
||||||
|
];
|
||||||
|
};
|
||||||
@@ -15,3 +15,4 @@ export const DASHBOARD_TIME_IN_DURATION = 'refreshInterval';
|
|||||||
|
|
||||||
export const DEFAULT_ENTITY_VERSION = 'v3';
|
export const DEFAULT_ENTITY_VERSION = 'v3';
|
||||||
export const ENTITY_VERSION_V4 = 'v4';
|
export const ENTITY_VERSION_V4 = 'v4';
|
||||||
|
export const ENTITY_VERSION_V5 = 'v5';
|
||||||
|
|||||||
@@ -46,4 +46,5 @@ export enum QueryParams {
|
|||||||
msgSystem = 'msgSystem',
|
msgSystem = 'msgSystem',
|
||||||
destination = 'destination',
|
destination = 'destination',
|
||||||
kindString = 'kindString',
|
kindString = 'kindString',
|
||||||
|
selectedExplorerView = 'selectedExplorerView',
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -169,6 +169,9 @@ export const initialQueryBuilderFormValues: IBuilderQuery = {
|
|||||||
aggregateAttribute: initialAutocompleteData,
|
aggregateAttribute: initialAutocompleteData,
|
||||||
timeAggregation: MetricAggregateOperator.RATE,
|
timeAggregation: MetricAggregateOperator.RATE,
|
||||||
spaceAggregation: MetricAggregateOperator.SUM,
|
spaceAggregation: MetricAggregateOperator.SUM,
|
||||||
|
filter: { expression: '' },
|
||||||
|
aggregations: [{ expression: 'count() ' }],
|
||||||
|
havingExpression: { expression: '' },
|
||||||
functions: [],
|
functions: [],
|
||||||
filters: { items: [], op: 'AND' },
|
filters: { items: [], op: 'AND' },
|
||||||
expression: createNewBuilderItemName({
|
expression: createNewBuilderItemName({
|
||||||
|
|||||||
@@ -54,19 +54,21 @@ function DomainList(): JSX.Element {
|
|||||||
|
|
||||||
// initialise tab with default query.
|
// initialise tab with default query.
|
||||||
useShareBuilderUrl({
|
useShareBuilderUrl({
|
||||||
...initialQueriesMap.traces,
|
defaultValue: {
|
||||||
builder: {
|
...initialQueriesMap.traces,
|
||||||
...initialQueriesMap.traces.builder,
|
builder: {
|
||||||
queryData: [
|
...initialQueriesMap.traces.builder,
|
||||||
{
|
queryData: [
|
||||||
...initialQueriesMap.traces.builder.queryData[0],
|
{
|
||||||
dataSource: DataSource.TRACES,
|
...initialQueriesMap.traces.builder.queryData[0],
|
||||||
aggregateOperator: 'noop',
|
dataSource: DataSource.TRACES,
|
||||||
aggregateAttribute: {
|
aggregateOperator: 'noop',
|
||||||
...initialQueriesMap.traces.builder.queryData[0].aggregateAttribute,
|
aggregateAttribute: {
|
||||||
|
...initialQueriesMap.traces.builder.queryData[0].aggregateAttribute,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
],
|
||||||
],
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ function Download({ data, isLoading, fileName }: DownloadProps): JSX.Element {
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
className="periscope-btn"
|
className="periscope-btn ghost"
|
||||||
loading={isLoading}
|
loading={isLoading}
|
||||||
icon={<FileDown size={14} />}
|
icon={<FileDown size={14} />}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
.explorer-options-container {
|
.explorer-options-container {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
bottom: 24px;
|
bottom: 8px;
|
||||||
left: calc(50% + 240px);
|
left: calc(50% + 240px);
|
||||||
transform: translate(calc(-50% - 120px), 0);
|
transform: translate(calc(-50% - 120px), 0);
|
||||||
transition: left 0.2s linear;
|
transition: left 0.2s linear;
|
||||||
|
|
||||||
|
border-radius: 6px;
|
||||||
|
background: var(--Ink-300, #16181d);
|
||||||
|
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 16px;
|
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
|
|
||||||
.multi-alert-button,
|
.multi-alert-button,
|
||||||
@@ -32,19 +34,15 @@
|
|||||||
.explorer-update {
|
.explorer-update {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 12px;
|
gap: 4px;
|
||||||
padding: 10px 10px;
|
padding: 8px;
|
||||||
border-radius: 50px;
|
background: var(--Ink-300, #16181d);
|
||||||
border: 1px solid var(--bg-slate-400);
|
|
||||||
background: rgba(22, 24, 29, 0.6);
|
|
||||||
backdrop-filter: blur(20px);
|
|
||||||
|
|
||||||
.action-icon {
|
.action-icon {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 8px;
|
padding: 6px;
|
||||||
border-radius: 50px;
|
|
||||||
border: 1px solid var(--bg-slate-400);
|
border: 1px solid var(--bg-slate-400);
|
||||||
background: var(--bg-slate-500);
|
background: var(--bg-slate-500);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
@@ -64,10 +62,8 @@
|
|||||||
|
|
||||||
.explorer-options {
|
.explorer-options {
|
||||||
padding: 10px 12px;
|
padding: 10px 12px;
|
||||||
border: 1px solid var(--bg-slate-400);
|
background: var(--Ink-300, #16181d);
|
||||||
border-radius: 50px;
|
border-radius: 2px;
|
||||||
background: rgba(22, 24, 29, 0.6);
|
|
||||||
backdrop-filter: blur(20px);
|
|
||||||
|
|
||||||
cursor: default;
|
cursor: default;
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -96,27 +92,6 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
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 {
|
.ant-select-focused {
|
||||||
border-color: transparent !important;
|
border-color: transparent !important;
|
||||||
|
|
||||||
@@ -257,6 +232,10 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.lightMode {
|
.lightMode {
|
||||||
|
.explorer-options-container {
|
||||||
|
background: var(--bg-vanilla-100);
|
||||||
|
}
|
||||||
|
|
||||||
.explorer-options {
|
.explorer-options {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
|
|||||||
@@ -1,12 +1,10 @@
|
|||||||
/* eslint-disable react/jsx-props-no-spreading */
|
/* eslint-disable react/jsx-props-no-spreading */
|
||||||
import './ExplorerOptions.styles.scss';
|
import './ExplorerOptions.styles.scss';
|
||||||
|
|
||||||
import { InfoCircleOutlined } from '@ant-design/icons';
|
|
||||||
import { Color } from '@signozhq/design-tokens';
|
import { Color } from '@signozhq/design-tokens';
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
ColorPicker,
|
ColorPicker,
|
||||||
Divider,
|
|
||||||
Input,
|
Input,
|
||||||
Modal,
|
Modal,
|
||||||
RefSelectProps,
|
RefSelectProps,
|
||||||
@@ -45,14 +43,7 @@ import { useHandleExplorerTabChange } from 'hooks/useHandleExplorerTabChange';
|
|||||||
import { useNotifications } from 'hooks/useNotifications';
|
import { useNotifications } from 'hooks/useNotifications';
|
||||||
import { mapCompositeQueryFromQuery } from 'lib/newQueryBuilder/queryBuilderMappers/mapCompositeQueryFromQuery';
|
import { mapCompositeQueryFromQuery } from 'lib/newQueryBuilder/queryBuilderMappers/mapCompositeQueryFromQuery';
|
||||||
import { cloneDeep, isEqual, omit } from 'lodash-es';
|
import { cloneDeep, isEqual, omit } from 'lodash-es';
|
||||||
import {
|
import { Check, ConciergeBell, Disc3, Plus, X } from 'lucide-react';
|
||||||
Check,
|
|
||||||
ConciergeBell,
|
|
||||||
Disc3,
|
|
||||||
PanelBottomClose,
|
|
||||||
Plus,
|
|
||||||
X,
|
|
||||||
} from 'lucide-react';
|
|
||||||
import { useAppContext } from 'providers/App/App';
|
import { useAppContext } from 'providers/App/App';
|
||||||
import { FormattingOptions } from 'providers/preferences/types';
|
import { FormattingOptions } from 'providers/preferences/types';
|
||||||
import {
|
import {
|
||||||
@@ -78,10 +69,8 @@ import ExplorerOptionsHideArea from './ExplorerOptionsHideArea';
|
|||||||
import { PreservedViewsInLocalStorage } from './types';
|
import { PreservedViewsInLocalStorage } from './types';
|
||||||
import {
|
import {
|
||||||
DATASOURCE_VS_ROUTES,
|
DATASOURCE_VS_ROUTES,
|
||||||
generateRGBAFromHex,
|
|
||||||
getRandomColor,
|
getRandomColor,
|
||||||
saveNewViewHandler,
|
saveNewViewHandler,
|
||||||
setExplorerToolBarVisibility,
|
|
||||||
} from './utils';
|
} from './utils';
|
||||||
|
|
||||||
const allowedRoles = [USER_ROLES.ADMIN, USER_ROLES.AUTHOR, USER_ROLES.EDITOR];
|
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)
|
const extraData = viewsData?.data?.data?.find((view) => view.id === viewKey)
|
||||||
?.extraData;
|
?.extraData;
|
||||||
|
|
||||||
const extraDataColor = extraData ? JSON.parse(extraData).color : '';
|
|
||||||
const rgbaColor = generateRGBAFromHex(
|
|
||||||
extraDataColor || Color.BG_SIENNA_500,
|
|
||||||
0.08,
|
|
||||||
);
|
|
||||||
|
|
||||||
const { options, handleOptionsChange } = useOptionsMenu({
|
const { options, handleOptionsChange } = useOptionsMenu({
|
||||||
storageKey:
|
storageKey:
|
||||||
sourcepage === DataSource.TRACES
|
sourcepage === DataSource.TRACES
|
||||||
@@ -589,13 +572,6 @@ function ExplorerOptions({
|
|||||||
[isDarkMode],
|
[isDarkMode],
|
||||||
);
|
);
|
||||||
|
|
||||||
const hideToolbar = (): void => {
|
|
||||||
setExplorerToolBarVisibility(false, sourcepage);
|
|
||||||
if (setIsExplorerOptionHidden) {
|
|
||||||
setIsExplorerOptionHidden(true);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const isEditDeleteSupported = allowedRoles.includes(user.role as string);
|
const isEditDeleteSupported = allowedRoles.includes(user.role as string);
|
||||||
|
|
||||||
const [
|
const [
|
||||||
@@ -647,27 +623,6 @@ function ExplorerOptions({
|
|||||||
viewsData?.data?.data,
|
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 => {
|
const getQueryName = (query: Query): string => {
|
||||||
if (query.builder.queryFormulas.length > 0) {
|
if (query.builder.queryFormulas.length > 0) {
|
||||||
return `Formula ${query.builder.queryFormulas[0].queryName}`;
|
return `Formula ${query.builder.queryFormulas[0].queryName}`;
|
||||||
@@ -680,11 +635,10 @@ function ExplorerOptions({
|
|||||||
const selectLabel = (
|
const selectLabel = (
|
||||||
<Button
|
<Button
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
shape="round"
|
className="periscope-btn ghost"
|
||||||
|
shape="default"
|
||||||
icon={<ConciergeBell size={16} />}
|
icon={<ConciergeBell size={16} />}
|
||||||
>
|
/>
|
||||||
Create an Alert
|
|
||||||
</Button>
|
|
||||||
);
|
);
|
||||||
return (
|
return (
|
||||||
<Select
|
<Select
|
||||||
@@ -713,12 +667,11 @@ function ExplorerOptions({
|
|||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
shape="round"
|
shape="default"
|
||||||
|
className="periscope-btn ghost"
|
||||||
onClick={(): void => onCreateAlertsHandler(query)}
|
onClick={(): void => onCreateAlertsHandler(query)}
|
||||||
icon={<ConciergeBell size={16} />}
|
icon={<ConciergeBell size={16} />}
|
||||||
>
|
/>
|
||||||
Create an Alert
|
|
||||||
</Button>
|
|
||||||
);
|
);
|
||||||
}, [
|
}, [
|
||||||
disabled,
|
disabled,
|
||||||
@@ -732,14 +685,11 @@ function ExplorerOptions({
|
|||||||
if (isOneChartPerQuery) {
|
if (isOneChartPerQuery) {
|
||||||
const selectLabel = (
|
const selectLabel = (
|
||||||
<Button
|
<Button
|
||||||
type="primary"
|
className="periscope-btn ghost"
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
shape="round"
|
|
||||||
onClick={onAddToDashboard}
|
onClick={onAddToDashboard}
|
||||||
icon={<Plus size={16} />}
|
icon={<Plus size={12} />}
|
||||||
>
|
/>
|
||||||
Add to Dashboard
|
|
||||||
</Button>
|
|
||||||
);
|
);
|
||||||
return (
|
return (
|
||||||
<Select
|
<Select
|
||||||
@@ -771,14 +721,11 @@ function ExplorerOptions({
|
|||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
type="primary"
|
className="periscope-btn ghost"
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
shape="round"
|
|
||||||
onClick={onAddToDashboard}
|
onClick={onAddToDashboard}
|
||||||
icon={<Plus size={16} />}
|
icon={<Plus size={16} />}
|
||||||
>
|
/>
|
||||||
Add to Dashboard
|
|
||||||
</Button>
|
|
||||||
);
|
);
|
||||||
}, [disabled, isOneChartPerQuery, onAddToDashboard, splitedQueries]);
|
}, [disabled, isOneChartPerQuery, onAddToDashboard, splitedQueries]);
|
||||||
|
|
||||||
@@ -797,41 +744,31 @@ function ExplorerOptions({
|
|||||||
>
|
>
|
||||||
<Tooltip title="Clear this view" placement="top">
|
<Tooltip title="Clear this view" placement="top">
|
||||||
<Button
|
<Button
|
||||||
className="action-icon"
|
className="periscope-btn ghost"
|
||||||
onClick={handleClearSelect}
|
onClick={handleClearSelect}
|
||||||
icon={<X size={14} />}
|
icon={<X size={16} />}
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
{
|
{
|
||||||
// only show the update view option when the query is updated
|
// only show the update view option when the query is updated
|
||||||
}
|
}
|
||||||
{isQueryUpdated && (
|
{isQueryUpdated && (
|
||||||
<>
|
<Tooltip title="Update this view" placement="top">
|
||||||
<Divider
|
<Button
|
||||||
type="vertical"
|
className={cx(
|
||||||
className={isEditDeleteSupported ? '' : 'hidden'}
|
'periscope-btn ghost',
|
||||||
|
isEditDeleteSupported ? '' : 'hidden',
|
||||||
|
)}
|
||||||
|
disabled={isViewUpdating}
|
||||||
|
onClick={onUpdateQueryHandler}
|
||||||
|
icon={<Disc3 size={16} />}
|
||||||
/>
|
/>
|
||||||
<Tooltip title="Update this view" placement="top">
|
</Tooltip>
|
||||||
<Button
|
|
||||||
className={cx('action-icon', isEditDeleteSupported ? ' ' : 'hidden')}
|
|
||||||
disabled={isViewUpdating}
|
|
||||||
onClick={onUpdateQueryHandler}
|
|
||||||
icon={<Disc3 size={14} />}
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{!isExplorerOptionHidden && (
|
{!isExplorerOptionHidden && (
|
||||||
<div
|
<div className="explorer-options">
|
||||||
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="view-options">
|
<div className="view-options">
|
||||||
<Select<string, { key: string; value: string }>
|
<Select<string, { key: string; value: string }>
|
||||||
showSearch
|
showSearch
|
||||||
@@ -872,49 +809,23 @@ function ExplorerOptions({
|
|||||||
</Select>
|
</Select>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
shape="round"
|
shape="default"
|
||||||
|
className={cx(
|
||||||
|
'periscope-btn secondary',
|
||||||
|
isEditDeleteSupported ? '' : 'hidden',
|
||||||
|
)}
|
||||||
onClick={handleSaveViewModalToggle}
|
onClick={handleSaveViewModalToggle}
|
||||||
className={isEditDeleteSupported ? '' : 'hidden'}
|
|
||||||
disabled={viewsIsLoading || isRefetching}
|
disabled={viewsIsLoading || isRefetching}
|
||||||
icon={<Disc3 size={16} />}
|
icon={<Disc3 size={12} />}
|
||||||
>
|
>
|
||||||
Save this view
|
Save this view
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<hr className={isEditDeleteSupported ? '' : 'hidden'} />
|
|
||||||
|
|
||||||
<div className={cx('actions', isEditDeleteSupported ? '' : 'hidden')}>
|
<div className={cx('actions', isEditDeleteSupported ? '' : 'hidden')}>
|
||||||
{alertButton}
|
{alertButton}
|
||||||
{dashboardButton}
|
{dashboardButton}
|
||||||
</div>
|
</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>
|
</div>
|
||||||
)}
|
)}
|
||||||
<ExplorerOptionsHideArea
|
<ExplorerOptionsHideArea
|
||||||
|
|||||||
@@ -2,12 +2,13 @@ import './ChartPreview.styles.scss';
|
|||||||
|
|
||||||
import { InfoCircleOutlined } from '@ant-design/icons';
|
import { InfoCircleOutlined } from '@ant-design/icons';
|
||||||
import Spinner from 'components/Spinner';
|
import Spinner from 'components/Spinner';
|
||||||
import { DEFAULT_ENTITY_VERSION } from 'constants/app';
|
import { ENTITY_VERSION_V5 } from 'constants/app';
|
||||||
import { FeatureKeys } from 'constants/features';
|
import { FeatureKeys } from 'constants/features';
|
||||||
import { QueryParams } from 'constants/query';
|
import { QueryParams } from 'constants/query';
|
||||||
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
|
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
|
||||||
import AnomalyAlertEvaluationView from 'container/AnomalyAlertEvaluationView';
|
import AnomalyAlertEvaluationView from 'container/AnomalyAlertEvaluationView';
|
||||||
import GridPanelSwitch from 'container/GridPanelSwitch';
|
import GridPanelSwitch from 'container/GridPanelSwitch';
|
||||||
|
import { populateMultipleResults } from 'container/NewWidget/LeftContainer/WidgetGraph/util';
|
||||||
import { getFormatNameByOptionId } from 'container/NewWidget/RightContainer/alertFomatCategories';
|
import { getFormatNameByOptionId } from 'container/NewWidget/RightContainer/alertFomatCategories';
|
||||||
import { timePreferenceType } from 'container/NewWidget/RightContainer/timeItems';
|
import { timePreferenceType } from 'container/NewWidget/RightContainer/timeItems';
|
||||||
import { Time } from 'container/TopNav/DateTimeSelection/config';
|
import { Time } from 'container/TopNav/DateTimeSelection/config';
|
||||||
@@ -144,7 +145,8 @@ function ChartPreview({
|
|||||||
},
|
},
|
||||||
originalGraphType: graphType,
|
originalGraphType: graphType,
|
||||||
},
|
},
|
||||||
alertDef?.version || DEFAULT_ENTITY_VERSION,
|
// alertDef?.version || DEFAULT_ENTITY_VERSION,
|
||||||
|
ENTITY_VERSION_V5,
|
||||||
{
|
{
|
||||||
queryKey: [
|
queryKey: [
|
||||||
'chartPreview',
|
'chartPreview',
|
||||||
@@ -175,6 +177,12 @@ function ChartPreview({
|
|||||||
queryResponse.data.payload.data.result = sortedSeriesData;
|
queryResponse.data.payload.data.result = sortedSeriesData;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (queryResponse.data && graphType === PANEL_TYPES.PIE) {
|
||||||
|
const transformedData = populateMultipleResults(queryResponse?.data);
|
||||||
|
// eslint-disable-next-line no-param-reassign
|
||||||
|
queryResponse.data = transformedData;
|
||||||
|
}
|
||||||
|
|
||||||
const containerDimensions = useResizeObserver(graphRef);
|
const containerDimensions = useResizeObserver(graphRef);
|
||||||
|
|
||||||
const isDarkMode = useIsDarkMode();
|
const isDarkMode = useIsDarkMode();
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
.alert-tabs {
|
.alert-tabs {
|
||||||
|
padding: 0px 8px;
|
||||||
|
|
||||||
.ant-tabs-tab {
|
.ant-tabs-tab {
|
||||||
border: none !important;
|
border: none !important;
|
||||||
margin-left: 0px !important;
|
margin-left: 0px !important;
|
||||||
@@ -48,6 +50,30 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.alert-query-section-container {
|
||||||
|
.ant-card-body {
|
||||||
|
padding: 0px;
|
||||||
|
|
||||||
|
.alert-tabs {
|
||||||
|
padding: 0px;
|
||||||
|
|
||||||
|
.ant-tabs {
|
||||||
|
.ant-tabs-nav {
|
||||||
|
padding: 8px 0;
|
||||||
|
|
||||||
|
.ant-tabs-nav-wrap {
|
||||||
|
padding: 0px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-tabs-extra-content {
|
||||||
|
padding: 0px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.lightMode {
|
.lightMode {
|
||||||
.alert-tabs {
|
.alert-tabs {
|
||||||
.ant-tabs-nav-list {
|
.ant-tabs-nav-list {
|
||||||
|
|||||||
@@ -4,11 +4,11 @@ import { Color } from '@signozhq/design-tokens';
|
|||||||
import { Button, Tabs, Tooltip } from 'antd';
|
import { Button, Tabs, Tooltip } from 'antd';
|
||||||
import logEvent from 'api/common/logEvent';
|
import logEvent from 'api/common/logEvent';
|
||||||
import PromQLIcon from 'assets/Dashboard/PromQl';
|
import PromQLIcon from 'assets/Dashboard/PromQl';
|
||||||
|
import { QueryBuilderV2 } from 'components/QueryBuilderV2/QueryBuilderV2';
|
||||||
import { ALERTS_DATA_SOURCE_MAP } from 'constants/alerts';
|
import { ALERTS_DATA_SOURCE_MAP } from 'constants/alerts';
|
||||||
import { ENTITY_VERSION_V4 } from 'constants/app';
|
import { ENTITY_VERSION_V4 } from 'constants/app';
|
||||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||||
import { QBShortcuts } from 'constants/shortcuts/QBShortcuts';
|
import { QBShortcuts } from 'constants/shortcuts/QBShortcuts';
|
||||||
import { QueryBuilder } from 'container/QueryBuilder';
|
|
||||||
import { useKeyboardHotkeys } from 'hooks/hotkeys/useKeyboardHotkeys';
|
import { useKeyboardHotkeys } from 'hooks/hotkeys/useKeyboardHotkeys';
|
||||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||||
import { isEmpty } from 'lodash-es';
|
import { isEmpty } from 'lodash-es';
|
||||||
@@ -48,7 +48,7 @@ function QuerySection({
|
|||||||
const isDarkMode = useIsDarkMode();
|
const isDarkMode = useIsDarkMode();
|
||||||
|
|
||||||
const renderMetricUI = (): JSX.Element => (
|
const renderMetricUI = (): JSX.Element => (
|
||||||
<QueryBuilder
|
<QueryBuilderV2
|
||||||
panelType={panelType}
|
panelType={panelType}
|
||||||
config={{
|
config={{
|
||||||
queryVariant: 'static',
|
queryVariant: 'static',
|
||||||
@@ -144,7 +144,7 @@ function QuerySection({
|
|||||||
<div className="alert-tabs">
|
<div className="alert-tabs">
|
||||||
<Tabs
|
<Tabs
|
||||||
type="card"
|
type="card"
|
||||||
style={{ width: '100%' }}
|
style={{ width: '100%', padding: '0px 8px' }}
|
||||||
defaultActiveKey={currentTab}
|
defaultActiveKey={currentTab}
|
||||||
activeKey={currentTab}
|
activeKey={currentTab}
|
||||||
onChange={handleQueryCategoryChange}
|
onChange={handleQueryCategoryChange}
|
||||||
@@ -178,7 +178,7 @@ function QuerySection({
|
|||||||
<div className="alert-tabs">
|
<div className="alert-tabs">
|
||||||
<Tabs
|
<Tabs
|
||||||
type="card"
|
type="card"
|
||||||
style={{ width: '100%' }}
|
style={{ width: '100%', padding: '0px 8px' }}
|
||||||
defaultActiveKey={currentTab}
|
defaultActiveKey={currentTab}
|
||||||
activeKey={currentTab}
|
activeKey={currentTab}
|
||||||
onChange={handleQueryCategoryChange}
|
onChange={handleQueryCategoryChange}
|
||||||
@@ -215,7 +215,7 @@ function QuerySection({
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<StepHeading> {t('alert_form_step2')}</StepHeading>
|
<StepHeading> {t('alert_form_step2')}</StepHeading>
|
||||||
<FormContainer>
|
<FormContainer className="alert-query-section-container">
|
||||||
<div>{renderTabs(alertType)}</div>
|
<div>{renderTabs(alertType)}</div>
|
||||||
{renderQuerySection(currentTab)}
|
{renderQuerySection(currentTab)}
|
||||||
</FormContainer>
|
</FormContainer>
|
||||||
|
|||||||
@@ -162,7 +162,7 @@ function FormAlertRules({
|
|||||||
|
|
||||||
const sq = useMemo(() => mapQueryDataFromApi(initQuery), [initQuery]);
|
const sq = useMemo(() => mapQueryDataFromApi(initQuery), [initQuery]);
|
||||||
|
|
||||||
useShareBuilderUrl(sq);
|
useShareBuilderUrl({ defaultValue: sq });
|
||||||
|
|
||||||
const handleDetectionMethodChange = (value: string): void => {
|
const handleDetectionMethodChange = (value: string): void => {
|
||||||
setAlertDef((def) => ({
|
setAlertDef((def) => ({
|
||||||
@@ -832,7 +832,7 @@ function FormAlertRules({
|
|||||||
queryCategory={currentQuery.queryType}
|
queryCategory={currentQuery.queryType}
|
||||||
setQueryCategory={onQueryCategoryChange}
|
setQueryCategory={onQueryCategoryChange}
|
||||||
alertType={alertType || AlertTypes.METRICS_BASED_ALERT}
|
alertType={alertType || AlertTypes.METRICS_BASED_ALERT}
|
||||||
runQuery={(): void => handleRunQuery(true)}
|
runQuery={(): void => handleRunQuery(true, true)}
|
||||||
alertDef={alertDef}
|
alertDef={alertDef}
|
||||||
panelType={panelType || PANEL_TYPES.TIME_SERIES}
|
panelType={panelType || PANEL_TYPES.TIME_SERIES}
|
||||||
key={currentQuery.queryType}
|
key={currentQuery.queryType}
|
||||||
|
|||||||
@@ -10,9 +10,10 @@ import cx from 'classnames';
|
|||||||
import { ToggleGraphProps } from 'components/Graph/types';
|
import { ToggleGraphProps } from 'components/Graph/types';
|
||||||
import Spinner from 'components/Spinner';
|
import Spinner from 'components/Spinner';
|
||||||
import TimePreference from 'components/TimePreferenceDropDown';
|
import TimePreference from 'components/TimePreferenceDropDown';
|
||||||
import { DEFAULT_ENTITY_VERSION } from 'constants/app';
|
import { ENTITY_VERSION_V5 } from 'constants/app';
|
||||||
import { QueryParams } from 'constants/query';
|
import { QueryParams } from 'constants/query';
|
||||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||||
|
import { populateMultipleResults } from 'container/NewWidget/LeftContainer/WidgetGraph/util';
|
||||||
import {
|
import {
|
||||||
timeItems,
|
timeItems,
|
||||||
timePreferance,
|
timePreferance,
|
||||||
@@ -122,7 +123,8 @@ function FullView({
|
|||||||
|
|
||||||
const response = useGetQueryRange(
|
const response = useGetQueryRange(
|
||||||
requestData,
|
requestData,
|
||||||
selectedDashboard?.data?.version || version || DEFAULT_ENTITY_VERSION,
|
// selectedDashboard?.data?.version || version || DEFAULT_ENTITY_VERSION,
|
||||||
|
ENTITY_VERSION_V5,
|
||||||
{
|
{
|
||||||
queryKey: [widget?.query, widget?.panelTypes, requestData, version],
|
queryKey: [widget?.query, widget?.panelTypes, requestData, version],
|
||||||
enabled: !isDependedDataLoaded,
|
enabled: !isDependedDataLoaded,
|
||||||
@@ -178,6 +180,12 @@ function FullView({
|
|||||||
response.data.payload.data.result = sortedSeriesData;
|
response.data.payload.data.result = sortedSeriesData;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (response.data && widget.panelTypes === PANEL_TYPES.PIE) {
|
||||||
|
const transformedData = populateMultipleResults(response?.data);
|
||||||
|
// eslint-disable-next-line no-param-reassign
|
||||||
|
response.data = transformedData;
|
||||||
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
graphsVisibilityStates?.forEach((e, i) => {
|
graphsVisibilityStates?.forEach((e, i) => {
|
||||||
fullViewChartRef?.current?.toggleGraph(i, e);
|
fullViewChartRef?.current?.toggleGraph(i, e);
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import logEvent from 'api/common/logEvent';
|
|||||||
import { DEFAULT_ENTITY_VERSION } from 'constants/app';
|
import { DEFAULT_ENTITY_VERSION } from 'constants/app';
|
||||||
import { QueryParams } from 'constants/query';
|
import { QueryParams } from 'constants/query';
|
||||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||||
|
import { populateMultipleResults } from 'container/NewWidget/LeftContainer/WidgetGraph/util';
|
||||||
import { CustomTimeType } from 'container/TopNav/DateTimeSelectionV2/config';
|
import { CustomTimeType } from 'container/TopNav/DateTimeSelectionV2/config';
|
||||||
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
|
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
|
||||||
import { useIntersectionObserver } from 'hooks/useIntersectionObserver';
|
import { useIntersectionObserver } from 'hooks/useIntersectionObserver';
|
||||||
@@ -271,6 +272,12 @@ function GridCardGraph({
|
|||||||
queryResponse.data.payload.data.result = sortedSeriesData;
|
queryResponse.data.payload.data.result = sortedSeriesData;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (queryResponse.data && widget.panelTypes === PANEL_TYPES.PIE) {
|
||||||
|
const transformedData = populateMultipleResults(queryResponse?.data);
|
||||||
|
// eslint-disable-next-line no-param-reassign
|
||||||
|
queryResponse.data = transformedData;
|
||||||
|
}
|
||||||
|
|
||||||
const menuList =
|
const menuList =
|
||||||
widget.panelTypes === PANEL_TYPES.TABLE ||
|
widget.panelTypes === PANEL_TYPES.TABLE ||
|
||||||
widget.panelTypes === PANEL_TYPES.LIST ||
|
widget.panelTypes === PANEL_TYPES.LIST ||
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { Button, Form, Input, Modal, Typography } from 'antd';
|
|||||||
import { useForm } from 'antd/es/form/Form';
|
import { useForm } from 'antd/es/form/Form';
|
||||||
import logEvent from 'api/common/logEvent';
|
import logEvent from 'api/common/logEvent';
|
||||||
import cx from 'classnames';
|
import cx from 'classnames';
|
||||||
|
import { ENTITY_VERSION_V5 } from 'constants/app';
|
||||||
import { QueryParams } from 'constants/query';
|
import { QueryParams } from 'constants/query';
|
||||||
import { PANEL_GROUP_TYPES, PANEL_TYPES } from 'constants/queryBuilder';
|
import { PANEL_GROUP_TYPES, PANEL_TYPES } from 'constants/queryBuilder';
|
||||||
import { themeColors } from 'constants/theme';
|
import { themeColors } from 'constants/theme';
|
||||||
@@ -579,7 +580,8 @@ function GraphLayout(props: GraphLayoutProps): JSX.Element {
|
|||||||
widget={(currentWidget as Widgets) || ({ id, query: {} } as Widgets)}
|
widget={(currentWidget as Widgets) || ({ id, query: {} } as Widgets)}
|
||||||
headerMenuList={widgetActions}
|
headerMenuList={widgetActions}
|
||||||
variables={variables}
|
variables={variables}
|
||||||
version={selectedDashboard?.data?.version}
|
// version={selectedDashboard?.data?.version}
|
||||||
|
version={ENTITY_VERSION_V5}
|
||||||
onDragSelect={onDragSelect}
|
onDragSelect={onDragSelect}
|
||||||
dataAvailable={checkIfDataExists}
|
dataAvailable={checkIfDataExists}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -172,6 +172,13 @@
|
|||||||
.ant-table-cell:nth-child(n + 3) {
|
.ant-table-cell:nth-child(n + 3) {
|
||||||
padding-right: 24px;
|
padding-right: 24px;
|
||||||
}
|
}
|
||||||
|
.memory-usage-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 4px;
|
||||||
|
margin-right: 4px;
|
||||||
|
}
|
||||||
.column-header-right {
|
.column-header-right {
|
||||||
text-align: right;
|
text-align: right;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import './InfraMonitoring.styles.scss';
|
import './InfraMonitoring.styles.scss';
|
||||||
|
|
||||||
|
import { InfoCircleOutlined } from '@ant-design/icons';
|
||||||
import { Color } from '@signozhq/design-tokens';
|
import { Color } from '@signozhq/design-tokens';
|
||||||
import { Progress, TabsProps, Tag } from 'antd';
|
import { Progress, TabsProps, Tag, Tooltip } from 'antd';
|
||||||
import { ColumnType } from 'antd/es/table';
|
import { ColumnType } from 'antd/es/table';
|
||||||
import {
|
import {
|
||||||
HostData,
|
HostData,
|
||||||
@@ -93,7 +94,14 @@ export const getHostsListColumns = (): ColumnType<HostRowData>[] => [
|
|||||||
align: 'right',
|
align: 'right',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: <div className="column-header-right">Memory Usage</div>,
|
title: (
|
||||||
|
<div className="column-header-right memory-usage-header">
|
||||||
|
Memory Usage
|
||||||
|
<Tooltip title="Excluding cache memory">
|
||||||
|
<InfoCircleOutlined />
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
dataIndex: 'memory',
|
dataIndex: 'memory',
|
||||||
key: 'memory',
|
key: 'memory',
|
||||||
width: 100,
|
width: 100,
|
||||||
@@ -255,7 +263,6 @@ export function GetHostsQuickFiltersConfig(
|
|||||||
isColumn: false,
|
isColumn: false,
|
||||||
isJSON: false,
|
isJSON: false,
|
||||||
},
|
},
|
||||||
dataSource: DataSource.METRICS,
|
|
||||||
defaultOpen: true,
|
defaultOpen: true,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -238,7 +238,6 @@ export function GetPodsQuickFiltersConfig(
|
|||||||
isColumn: false,
|
isColumn: false,
|
||||||
isJSON: false,
|
isJSON: false,
|
||||||
},
|
},
|
||||||
dataSource: DataSource.METRICS,
|
|
||||||
defaultOpen: true,
|
defaultOpen: true,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@@ -302,7 +301,6 @@ export function GetNodesQuickFiltersConfig(
|
|||||||
isColumn: false,
|
isColumn: false,
|
||||||
isJSON: false,
|
isJSON: false,
|
||||||
},
|
},
|
||||||
dataSource: DataSource.METRICS,
|
|
||||||
defaultOpen: true,
|
defaultOpen: true,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@@ -365,7 +363,6 @@ export function GetNamespaceQuickFiltersConfig(
|
|||||||
isColumn: false,
|
isColumn: false,
|
||||||
isJSON: false,
|
isJSON: false,
|
||||||
},
|
},
|
||||||
dataSource: DataSource.METRICS,
|
|
||||||
defaultOpen: true,
|
defaultOpen: true,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@@ -409,7 +406,6 @@ export function GetClustersQuickFiltersConfig(
|
|||||||
isColumn: false,
|
isColumn: false,
|
||||||
isJSON: false,
|
isJSON: false,
|
||||||
},
|
},
|
||||||
dataSource: DataSource.METRICS,
|
|
||||||
defaultOpen: true,
|
defaultOpen: true,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@@ -449,7 +445,6 @@ export function GetContainersQuickFiltersConfig(
|
|||||||
isColumn: false,
|
isColumn: false,
|
||||||
isJSON: false,
|
isJSON: false,
|
||||||
},
|
},
|
||||||
dataSource: DataSource.METRICS,
|
|
||||||
defaultOpen: true,
|
defaultOpen: true,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@@ -531,7 +526,6 @@ export function GetVolumesQuickFiltersConfig(
|
|||||||
isColumn: false,
|
isColumn: false,
|
||||||
isJSON: false,
|
isJSON: false,
|
||||||
},
|
},
|
||||||
dataSource: DataSource.METRICS,
|
|
||||||
defaultOpen: true,
|
defaultOpen: true,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@@ -613,7 +607,6 @@ export function GetDeploymentsQuickFiltersConfig(
|
|||||||
isColumn: false,
|
isColumn: false,
|
||||||
isJSON: false,
|
isJSON: false,
|
||||||
},
|
},
|
||||||
dataSource: DataSource.METRICS,
|
|
||||||
defaultOpen: true,
|
defaultOpen: true,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@@ -695,7 +688,6 @@ export function GetStatefulsetsQuickFiltersConfig(
|
|||||||
isColumn: false,
|
isColumn: false,
|
||||||
isJSON: false,
|
isJSON: false,
|
||||||
},
|
},
|
||||||
dataSource: DataSource.METRICS,
|
|
||||||
defaultOpen: true,
|
defaultOpen: true,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@@ -775,7 +767,6 @@ export function GetDaemonsetsQuickFiltersConfig(
|
|||||||
isColumn: false,
|
isColumn: false,
|
||||||
isJSON: false,
|
isJSON: false,
|
||||||
},
|
},
|
||||||
dataSource: DataSource.METRICS,
|
|
||||||
defaultOpen: true,
|
defaultOpen: true,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@@ -853,7 +844,6 @@ export function GetJobsQuickFiltersConfig(
|
|||||||
isColumn: false,
|
isColumn: false,
|
||||||
isJSON: false,
|
isJSON: false,
|
||||||
},
|
},
|
||||||
dataSource: DataSource.METRICS,
|
|
||||||
defaultOpen: true,
|
defaultOpen: true,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ import logEvent from 'api/common/logEvent';
|
|||||||
import createDashboard from 'api/v1/dashboards/create';
|
import createDashboard from 'api/v1/dashboards/create';
|
||||||
import { AxiosError } from 'axios';
|
import { AxiosError } from 'axios';
|
||||||
import cx from 'classnames';
|
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 { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
|
||||||
import ROUTES from 'constants/routes';
|
import ROUTES from 'constants/routes';
|
||||||
import { sanitizeDashboardData } from 'container/NewDashboard/DashboardDescription';
|
import { sanitizeDashboardData } from 'container/NewDashboard/DashboardDescription';
|
||||||
@@ -293,7 +293,7 @@ function DashboardsList(): JSX.Element {
|
|||||||
ns: 'dashboard',
|
ns: 'dashboard',
|
||||||
}),
|
}),
|
||||||
uploadedGrafana: false,
|
uploadedGrafana: false,
|
||||||
version: ENTITY_VERSION_V4,
|
version: ENTITY_VERSION_V5,
|
||||||
});
|
});
|
||||||
|
|
||||||
safeNavigate(
|
safeNavigate(
|
||||||
|
|||||||
@@ -2463,7 +2463,7 @@ export const getHostQueryPayload = (
|
|||||||
functions: [],
|
functions: [],
|
||||||
groupBy: [],
|
groupBy: [],
|
||||||
having: [],
|
having: [],
|
||||||
legend: '',
|
legend: 'system disk io',
|
||||||
limit: null,
|
limit: null,
|
||||||
orderBy: [],
|
orderBy: [],
|
||||||
queryName: 'A',
|
queryName: 'A',
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
.qb-search-view-container {
|
.qb-search-view-container {
|
||||||
padding: 8px 16px;
|
padding: 8px 16px;
|
||||||
border-top: 1px solid var(--bg-slate-400, #1d212d);
|
|
||||||
border-bottom: 1px solid var(--bg-slate-400, #1d212d);
|
|
||||||
|
|
||||||
.ant-select-selector {
|
.ant-select-selector {
|
||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
@@ -20,9 +18,6 @@
|
|||||||
|
|
||||||
.lightMode {
|
.lightMode {
|
||||||
.qb-search-view-container {
|
.qb-search-view-container {
|
||||||
border-top: 1px solid var(--bg-vanilla-300);
|
|
||||||
border-bottom: 1px solid var(--bg-vanilla-300);
|
|
||||||
|
|
||||||
.ant-select-selector {
|
.ant-select-selector {
|
||||||
border-color: var(--bg-vanilla-300) !important;
|
border-color: var(--bg-vanilla-300) !important;
|
||||||
background-color: var(--bg-vanilla-100) !important;
|
background-color: var(--bg-vanilla-100) !important;
|
||||||
|
|||||||
@@ -1,47 +1,40 @@
|
|||||||
import './LogsExplorerQuerySection.styles.scss';
|
import './LogsExplorerQuerySection.styles.scss';
|
||||||
|
|
||||||
|
import { QueryBuilderV2 } from 'components/QueryBuilderV2/QueryBuilderV2';
|
||||||
import {
|
import {
|
||||||
initialQueriesMap,
|
initialQueriesMap,
|
||||||
OPERATORS,
|
OPERATORS,
|
||||||
PANEL_TYPES,
|
PANEL_TYPES,
|
||||||
} from 'constants/queryBuilder';
|
} from 'constants/queryBuilder';
|
||||||
import ExplorerOrderBy from 'container/ExplorerOrderBy';
|
import ExplorerOrderBy from 'container/ExplorerOrderBy';
|
||||||
import { QueryBuilder } from 'container/QueryBuilder';
|
|
||||||
import { OrderByFilterProps } from 'container/QueryBuilder/filters/OrderByFilter/OrderByFilter.interfaces';
|
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 { QueryBuilderProps } from 'container/QueryBuilder/QueryBuilder.interfaces';
|
||||||
import { useGetPanelTypesQueryParam } from 'hooks/queryBuilder/useGetPanelTypesQueryParam';
|
import { useGetPanelTypesQueryParam } from 'hooks/queryBuilder/useGetPanelTypesQueryParam';
|
||||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||||
import { useQueryOperations } from 'hooks/queryBuilder/useQueryBuilderOperations';
|
|
||||||
import { useShareBuilderUrl } from 'hooks/queryBuilder/useShareBuilderUrl';
|
import { useShareBuilderUrl } from 'hooks/queryBuilder/useShareBuilderUrl';
|
||||||
import {
|
import { ExplorerViews } from 'pages/LogsExplorer/utils';
|
||||||
prepareQueryWithDefaultTimestamp,
|
|
||||||
SELECTED_VIEWS,
|
|
||||||
} from 'pages/LogsExplorer/utils';
|
|
||||||
import { memo, useCallback, useMemo } from 'react';
|
import { memo, useCallback, useMemo } from 'react';
|
||||||
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
|
||||||
import { DataSource } from 'types/common/queryBuilder';
|
import { DataSource } from 'types/common/queryBuilder';
|
||||||
|
|
||||||
function LogExplorerQuerySection({
|
function LogExplorerQuerySection({
|
||||||
selectedView,
|
selectedView,
|
||||||
}: {
|
}: {
|
||||||
selectedView: SELECTED_VIEWS;
|
selectedView: ExplorerViews;
|
||||||
}): JSX.Element {
|
}): JSX.Element {
|
||||||
const { currentQuery, updateAllQueriesOperators } = useQueryBuilder();
|
const { updateAllQueriesOperators } = useQueryBuilder();
|
||||||
|
|
||||||
const query = currentQuery?.builder?.queryData[0] || null;
|
|
||||||
|
|
||||||
const panelTypes = useGetPanelTypesQueryParam(PANEL_TYPES.LIST);
|
const panelTypes = useGetPanelTypesQueryParam(PANEL_TYPES.LIST);
|
||||||
const defaultValue = useMemo(() => {
|
const defaultValue = useMemo(
|
||||||
const updatedQuery = updateAllQueriesOperators(
|
() =>
|
||||||
initialQueriesMap.logs,
|
updateAllQueriesOperators(
|
||||||
PANEL_TYPES.LIST,
|
initialQueriesMap.logs,
|
||||||
DataSource.LOGS,
|
PANEL_TYPES.LIST,
|
||||||
);
|
DataSource.LOGS,
|
||||||
return prepareQueryWithDefaultTimestamp(updatedQuery);
|
),
|
||||||
}, [updateAllQueriesOperators]);
|
[updateAllQueriesOperators],
|
||||||
|
);
|
||||||
|
|
||||||
useShareBuilderUrl(defaultValue);
|
useShareBuilderUrl({ defaultValue });
|
||||||
|
|
||||||
const filterConfigs: QueryBuilderProps['filterConfigs'] = useMemo(() => {
|
const filterConfigs: QueryBuilderProps['filterConfigs'] = useMemo(() => {
|
||||||
const isTable = panelTypes === PANEL_TYPES.TABLE;
|
const isTable = panelTypes === PANEL_TYPES.TABLE;
|
||||||
@@ -58,13 +51,6 @@ function LogExplorerQuerySection({
|
|||||||
return config;
|
return config;
|
||||||
}, [panelTypes]);
|
}, [panelTypes]);
|
||||||
|
|
||||||
const { handleChangeQueryData } = useQueryOperations({
|
|
||||||
index: 0,
|
|
||||||
query,
|
|
||||||
filterConfigs,
|
|
||||||
entityVersion: '',
|
|
||||||
});
|
|
||||||
|
|
||||||
const renderOrderBy = useCallback(
|
const renderOrderBy = useCallback(
|
||||||
({ query, onChange }: OrderByFilterProps): JSX.Element => (
|
({ query, onChange }: OrderByFilterProps): JSX.Element => (
|
||||||
<ExplorerOrderBy query={query} onChange={onChange} />
|
<ExplorerOrderBy query={query} onChange={onChange} />
|
||||||
@@ -79,35 +65,16 @@ function LogExplorerQuerySection({
|
|||||||
[panelTypes, renderOrderBy],
|
[panelTypes, renderOrderBy],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleChangeTagFilters = useCallback(
|
|
||||||
(value: IBuilderQuery['filters']) => {
|
|
||||||
handleChangeQueryData('filters', value);
|
|
||||||
},
|
|
||||||
[handleChangeQueryData],
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<QueryBuilderV2
|
||||||
{selectedView === SELECTED_VIEWS.SEARCH && (
|
isListViewPanel={panelTypes === PANEL_TYPES.LIST}
|
||||||
<div className="qb-search-view-container">
|
config={{ initialDataSource: DataSource.LOGS, queryVariant: 'static' }}
|
||||||
<QueryBuilderSearchV2
|
panelType={panelTypes}
|
||||||
query={query}
|
filterConfigs={filterConfigs}
|
||||||
onChange={handleChangeTagFilters}
|
queryComponents={queryComponents}
|
||||||
whereClauseConfig={filterConfigs?.filters}
|
showOnlyWhereClause={selectedView === ExplorerViews.LIST}
|
||||||
/>
|
version="v3" // setting this to v3 as we this is rendered in logs explorer
|
||||||
</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
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -45,7 +45,6 @@ function LogsExplorerList({
|
|||||||
}: LogsExplorerListProps): JSX.Element {
|
}: LogsExplorerListProps): JSX.Element {
|
||||||
const ref = useRef<VirtuosoHandle>(null);
|
const ref = useRef<VirtuosoHandle>(null);
|
||||||
const { initialDataSource } = useQueryBuilder();
|
const { initialDataSource } = useQueryBuilder();
|
||||||
|
|
||||||
const { activeLogId } = useCopyLogLink();
|
const { activeLogId } = useCopyLogLink();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
|
|||||||
@@ -76,14 +76,92 @@
|
|||||||
border: 1px solid var(--bg-slate-400);
|
border: 1px solid var(--bg-slate-400);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.order-by-container {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
.format-options-container {
|
.format-options-container {
|
||||||
position: relative;
|
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 {
|
.query-stats {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
|
|
||||||
|
align-self: flex-end;
|
||||||
|
|
||||||
.rows {
|
.rows {
|
||||||
color: var(--bg-vanilla-400);
|
color: var(--bg-vanilla-400);
|
||||||
font-family: 'Geist Mono';
|
font-family: 'Geist Mono';
|
||||||
@@ -110,13 +188,6 @@
|
|||||||
letter-spacing: 0.36px;
|
letter-spacing: 0.36px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
.logs-actions-container {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
|
|
||||||
.ant-btn {
|
.ant-btn {
|
||||||
border: none;
|
border: none;
|
||||||
@@ -186,6 +257,14 @@
|
|||||||
background: var(--bg-robin-400);
|
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 {
|
.query-stats {
|
||||||
.rows {
|
.rows {
|
||||||
color: var(--bg-ink-400);
|
color: var(--bg-ink-400);
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
/* eslint-disable sonarjs/cognitive-complexity */
|
/* eslint-disable sonarjs/cognitive-complexity */
|
||||||
import './LogsExplorerViews.styles.scss';
|
import './LogsExplorerViews.styles.scss';
|
||||||
|
|
||||||
import { Button, Typography } from 'antd';
|
import { Button, Select, Switch, Typography } from 'antd';
|
||||||
import { getQueryStats, WsDataEvent } from 'api/common/getQueryStats';
|
import { getQueryStats, WsDataEvent } from 'api/common/getQueryStats';
|
||||||
import logEvent from 'api/common/logEvent';
|
import logEvent from 'api/common/logEvent';
|
||||||
import { getYAxisFormattedValue } from 'components/Graph/yAxisConfig';
|
import { getYAxisFormattedValue } from 'components/Graph/yAxisConfig';
|
||||||
import LogsFormatOptionsMenu from 'components/LogsFormatOptionsMenu/LogsFormatOptionsMenu';
|
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 { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
|
||||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||||
import { AVAILABLE_EXPORT_PANEL_TYPES } from 'constants/panelTypes';
|
import { AVAILABLE_EXPORT_PANEL_TYPES } from 'constants/panelTypes';
|
||||||
@@ -46,8 +46,8 @@ import {
|
|||||||
omit,
|
omit,
|
||||||
set,
|
set,
|
||||||
} from 'lodash-es';
|
} from 'lodash-es';
|
||||||
import { Sliders } from 'lucide-react';
|
import { ArrowUp10, Minus, Sliders } from 'lucide-react';
|
||||||
import { SELECTED_VIEWS } from 'pages/LogsExplorer/utils';
|
import { ExplorerViews } from 'pages/LogsExplorer/utils';
|
||||||
import { useTimezone } from 'providers/Timezone';
|
import { useTimezone } from 'providers/Timezone';
|
||||||
import {
|
import {
|
||||||
memo,
|
memo,
|
||||||
@@ -80,15 +80,13 @@ import { v4 } from 'uuid';
|
|||||||
|
|
||||||
import QueryStatus from './QueryStatus';
|
import QueryStatus from './QueryStatus';
|
||||||
|
|
||||||
function LogsExplorerViews({
|
function LogsExplorerViewsContainer({
|
||||||
selectedView,
|
selectedView,
|
||||||
showFrequencyChart,
|
|
||||||
setIsLoadingQueries,
|
setIsLoadingQueries,
|
||||||
listQueryKeyRef,
|
listQueryKeyRef,
|
||||||
chartQueryKeyRef,
|
chartQueryKeyRef,
|
||||||
}: {
|
}: {
|
||||||
selectedView: SELECTED_VIEWS;
|
selectedView: ExplorerViews;
|
||||||
showFrequencyChart: boolean;
|
|
||||||
setIsLoadingQueries: React.Dispatch<React.SetStateAction<boolean>>;
|
setIsLoadingQueries: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
listQueryKeyRef: MutableRefObject<any>;
|
listQueryKeyRef: MutableRefObject<any>;
|
||||||
@@ -97,6 +95,7 @@ function LogsExplorerViews({
|
|||||||
}): JSX.Element {
|
}): JSX.Element {
|
||||||
const { safeNavigate } = useSafeNavigate();
|
const { safeNavigate } = useSafeNavigate();
|
||||||
const dispatch = useDispatch();
|
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.
|
// this is to respect the panel type present in the URL rather than defaulting it to list always.
|
||||||
const panelTypes = useGetPanelTypesQueryParam(PANEL_TYPES.LIST);
|
const panelTypes = useGetPanelTypesQueryParam(PANEL_TYPES.LIST);
|
||||||
@@ -139,6 +138,8 @@ function LogsExplorerViews({
|
|||||||
const [queryStats, setQueryStats] = useState<WsDataEvent>();
|
const [queryStats, setQueryStats] = useState<WsDataEvent>();
|
||||||
const [listChartQuery, setListChartQuery] = useState<Query | null>(null);
|
const [listChartQuery, setListChartQuery] = useState<Query | null>(null);
|
||||||
|
|
||||||
|
const [orderDirection, setOrderDirection] = useState<string>('desc');
|
||||||
|
|
||||||
const listQuery = useMemo(() => {
|
const listQuery = useMemo(() => {
|
||||||
if (!stagedQuery || stagedQuery.builder.queryData.length < 1) return null;
|
if (!stagedQuery || stagedQuery.builder.queryData.length < 1) return null;
|
||||||
|
|
||||||
@@ -240,15 +241,6 @@ function LogsExplorerViews({
|
|||||||
[currentQuery, selectedPanelType, updateAllQueriesOperators],
|
[currentQuery, selectedPanelType, updateAllQueriesOperators],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleModeChange = (panelType: PANEL_TYPES): void => {
|
|
||||||
if (selectedView === SELECTED_VIEWS.SEARCH) {
|
|
||||||
handleSetConfig(panelType, DataSource.LOGS);
|
|
||||||
}
|
|
||||||
|
|
||||||
setShowFormatMenuItems(false);
|
|
||||||
handleExplorerTabChange(panelType);
|
|
||||||
};
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data: listChartData,
|
data: listChartData,
|
||||||
isFetching: isFetchingListChartData,
|
isFetching: isFetchingListChartData,
|
||||||
@@ -256,7 +248,8 @@ function LogsExplorerViews({
|
|||||||
} = useGetExplorerQueryRange(
|
} = useGetExplorerQueryRange(
|
||||||
listChartQuery,
|
listChartQuery,
|
||||||
PANEL_TYPES.TIME_SERIES,
|
PANEL_TYPES.TIME_SERIES,
|
||||||
ENTITY_VERSION_V4,
|
// ENTITY_VERSION_V4,
|
||||||
|
ENTITY_VERSION_V5,
|
||||||
{
|
{
|
||||||
enabled: !!listChartQuery && panelType === PANEL_TYPES.LIST,
|
enabled: !!listChartQuery && panelType === PANEL_TYPES.LIST,
|
||||||
},
|
},
|
||||||
@@ -276,7 +269,8 @@ function LogsExplorerViews({
|
|||||||
} = useGetExplorerQueryRange(
|
} = useGetExplorerQueryRange(
|
||||||
requestData,
|
requestData,
|
||||||
panelType,
|
panelType,
|
||||||
ENTITY_VERSION_V4,
|
// ENTITY_VERSION_V4,
|
||||||
|
ENTITY_VERSION_V5,
|
||||||
{
|
{
|
||||||
keepPreviousData: true,
|
keepPreviousData: true,
|
||||||
enabled: !isLimit && !!requestData,
|
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[] =
|
const queryData: IBuilderQuery[] =
|
||||||
query.builder.queryData.length > 1
|
query.builder.queryData.length > 1
|
||||||
? query.builder.queryData
|
? query.builder.queryData.map((item) => ({
|
||||||
|
...item,
|
||||||
|
...(selectedView !== ExplorerViews.LIST ? { order: [] } : {}),
|
||||||
|
}))
|
||||||
: [
|
: [
|
||||||
{
|
{
|
||||||
...(listQuery || initialQueryBuilderFormValues),
|
...(listQuery || initialQueryBuilderFormValues),
|
||||||
...paginateData,
|
...paginateData,
|
||||||
...(updatedFilters ? { filters: updatedFilters } : {}),
|
...(updatedFilters ? { filters: updatedFilters } : {}),
|
||||||
|
...(selectedView === ExplorerViews.LIST
|
||||||
|
? { order: orderBy, orderBy }
|
||||||
|
: { order: [] }),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -358,7 +364,7 @@ function LogsExplorerViews({
|
|||||||
|
|
||||||
return data;
|
return data;
|
||||||
},
|
},
|
||||||
[listQuery, activeLogId],
|
[activeLogId, orderDirection, listQuery, selectedView],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleEndReached = useCallback(() => {
|
const handleEndReached = useCallback(() => {
|
||||||
@@ -460,8 +466,7 @@ function LogsExplorerViews({
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const shouldChangeView =
|
const shouldChangeView =
|
||||||
(isMultipleQueries || isGroupByExist) &&
|
(isMultipleQueries || isGroupByExist) && selectedView !== ExplorerViews.LIST;
|
||||||
selectedView !== SELECTED_VIEWS.SEARCH;
|
|
||||||
|
|
||||||
if (selectedPanelType === PANEL_TYPES.LIST && shouldChangeView) {
|
if (selectedPanelType === PANEL_TYPES.LIST && shouldChangeView) {
|
||||||
handleExplorerTabChange(PANEL_TYPES.TIME_SERIES);
|
handleExplorerTabChange(PANEL_TYPES.TIME_SERIES);
|
||||||
@@ -481,11 +486,7 @@ function LogsExplorerViews({
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (
|
if (selectedView && selectedView === ExplorerViews.LIST && handleSetConfig) {
|
||||||
selectedView &&
|
|
||||||
selectedView === SELECTED_VIEWS.SEARCH &&
|
|
||||||
handleSetConfig
|
|
||||||
) {
|
|
||||||
handleSetConfig(defaultTo(panelTypes, PANEL_TYPES.LIST), DataSource.LOGS);
|
handleSetConfig(defaultTo(panelTypes, PANEL_TYPES.LIST), DataSource.LOGS);
|
||||||
}
|
}
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
@@ -506,10 +507,19 @@ function LogsExplorerViews({
|
|||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [data]);
|
}, [data]);
|
||||||
|
|
||||||
|
// Store previous orderDirection to detect changes
|
||||||
|
const prevOrderDirectionRef = useRef(orderDirection);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
const orderDirectionChanged =
|
||||||
|
prevOrderDirectionRef.current !== orderDirection &&
|
||||||
|
selectedPanelType === PANEL_TYPES.LIST;
|
||||||
|
prevOrderDirectionRef.current = orderDirection;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
requestData?.id !== stagedQuery?.id ||
|
requestData?.id !== stagedQuery?.id ||
|
||||||
currentMinTimeRef.current !== minTime
|
currentMinTimeRef.current !== minTime ||
|
||||||
|
orderDirectionChanged
|
||||||
) {
|
) {
|
||||||
// Recalculate global time when query changes i.e. stage and run query clicked
|
// Recalculate global time when query changes i.e. stage and run query clicked
|
||||||
if (
|
if (
|
||||||
@@ -545,6 +555,8 @@ function LogsExplorerViews({
|
|||||||
dispatch,
|
dispatch,
|
||||||
selectedTime,
|
selectedTime,
|
||||||
maxTime,
|
maxTime,
|
||||||
|
orderDirection,
|
||||||
|
selectedPanelType,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const chartData = useMemo(() => {
|
const chartData = useMemo(() => {
|
||||||
@@ -651,111 +663,107 @@ function LogsExplorerViews({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="logs-explorer-views-container">
|
<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="logs-explorer-views-types">
|
||||||
<div className="views-tabs-container">
|
<div className="logs-actions-container">
|
||||||
<Button.Group className="views-tabs">
|
<div className="tab-options">
|
||||||
<Button
|
<div className="tab-options-left">
|
||||||
value={PANEL_TYPES.LIST}
|
{selectedPanelType === PANEL_TYPES.LIST && (
|
||||||
className={
|
<div className="frequency-chart-view-controller">
|
||||||
// eslint-disable-next-line sonarjs/no-duplicate-string
|
<Typography>Frequency chart</Typography>
|
||||||
selectedPanelType === PANEL_TYPES.LIST ? 'selected_view tab' : 'tab'
|
<Switch
|
||||||
}
|
size="small"
|
||||||
disabled={
|
checked={showFrequencyChart}
|
||||||
(isMultipleQueries || isGroupByExist) && selectedView !== 'search'
|
defaultChecked
|
||||||
}
|
onChange={(): void => setShowFrequencyChart(!showFrequencyChart)}
|
||||||
onClick={(): void => handleModeChange(PANEL_TYPES.LIST)}
|
/>
|
||||||
data-testid="logs-list-view"
|
</div>
|
||||||
>
|
)}
|
||||||
List view
|
</div>
|
||||||
</Button>
|
|
||||||
<Button
|
<div className="tab-options-right">
|
||||||
value={PANEL_TYPES.TIME_SERIES}
|
{selectedPanelType === PANEL_TYPES.LIST && (
|
||||||
className={
|
<>
|
||||||
// eslint-disable-next-line sonarjs/no-duplicate-string
|
<div className="order-by-container">
|
||||||
selectedPanelType === PANEL_TYPES.TIME_SERIES
|
<div className="order-by-label">
|
||||||
? 'selected_view tab'
|
Order by <Minus size={14} /> <ArrowUp10 size={14} />
|
||||||
: 'tab'
|
</div>
|
||||||
}
|
|
||||||
onClick={(): void => handleModeChange(PANEL_TYPES.TIME_SERIES)}
|
<Select
|
||||||
data-testid="time-series-view"
|
placeholder="Select order by"
|
||||||
>
|
className="order-by-select"
|
||||||
Time series
|
style={{ width: 100 }}
|
||||||
</Button>
|
value={orderDirection}
|
||||||
<Button
|
onChange={(value): void => setOrderDirection(value)}
|
||||||
value={PANEL_TYPES.TABLE}
|
options={[
|
||||||
className={
|
{ label: 'Ascending', value: 'asc' },
|
||||||
// eslint-disable-next-line sonarjs/no-duplicate-string
|
{ label: 'Descending', value: 'desc' },
|
||||||
selectedPanelType === PANEL_TYPES.TABLE ? 'selected_view tab' : 'tab'
|
]}
|
||||||
}
|
/>
|
||||||
onClick={(): void => handleModeChange(PANEL_TYPES.TABLE)}
|
</div>
|
||||||
data-testid="table-view"
|
<Download
|
||||||
>
|
data={flattenLogData}
|
||||||
Table
|
isLoading={isFetching}
|
||||||
</Button>
|
fileName="log_data"
|
||||||
</Button.Group>
|
/>
|
||||||
<div className="logs-actions-container">
|
<div className="format-options-container" ref={menuRef}>
|
||||||
{selectedPanelType === PANEL_TYPES.LIST && (
|
<Button
|
||||||
<div className="tab-options">
|
className="periscope-btn ghost"
|
||||||
<Download
|
onClick={handleToggleShowFormatOptions}
|
||||||
data={flattenLogData}
|
icon={<Sliders size={14} />}
|
||||||
isLoading={isFetching}
|
data-testid="periscope-btn"
|
||||||
fileName="log_data"
|
/>
|
||||||
/>
|
|
||||||
<div className="format-options-container" ref={menuRef}>
|
{showFormatMenuItems && (
|
||||||
<Button
|
<LogsFormatOptionsMenu
|
||||||
className="periscope-btn"
|
title="FORMAT"
|
||||||
onClick={handleToggleShowFormatOptions}
|
items={formatItems}
|
||||||
icon={<Sliders size={14} />}
|
selectedOptionFormat={options.format}
|
||||||
data-testid="periscope-btn"
|
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 && (
|
{queryStats?.read_rows && (
|
||||||
<LogsFormatOptionsMenu
|
<Typography.Text className="rows">
|
||||||
title="FORMAT"
|
{getYAxisFormattedValue(queryStats.read_rows?.toString(), 'short')}{' '}
|
||||||
items={formatItems}
|
rows
|
||||||
selectedOptionFormat={options.format}
|
</Typography.Text>
|
||||||
config={config}
|
)}
|
||||||
/>
|
|
||||||
|
{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.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>
|
</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">
|
<div className="logs-explorer-views-type-content">
|
||||||
{selectedPanelType === PANEL_TYPES.LIST && (
|
{selectedPanelType === PANEL_TYPES.LIST && (
|
||||||
<LogsExplorerList
|
<LogsExplorerList
|
||||||
@@ -801,4 +809,4 @@ function LogsExplorerViews({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default memo(LogsExplorerViews);
|
export default memo(LogsExplorerViewsContainer);
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { useGetExplorerQueryRange } from 'hooks/queryBuilder/useGetExplorerQuery
|
|||||||
import { logsQueryRangeSuccessResponse } from 'mocks-server/__mockdata__/logs_query_range';
|
import { logsQueryRangeSuccessResponse } from 'mocks-server/__mockdata__/logs_query_range';
|
||||||
import { server } from 'mocks-server/server';
|
import { server } from 'mocks-server/server';
|
||||||
import { rest } from 'msw';
|
import { rest } from 'msw';
|
||||||
import { SELECTED_VIEWS } from 'pages/LogsExplorer/utils';
|
import { ExplorerViews } from 'pages/LogsExplorer/utils';
|
||||||
import { PreferenceContextProvider } from 'providers/preferences/context/PreferenceContextProvider';
|
import { PreferenceContextProvider } from 'providers/preferences/context/PreferenceContextProvider';
|
||||||
import { QueryBuilderContext } from 'providers/QueryBuilder';
|
import { QueryBuilderContext } from 'providers/QueryBuilder';
|
||||||
import { VirtuosoMockContext } from 'react-virtuoso';
|
import { VirtuosoMockContext } from 'react-virtuoso';
|
||||||
@@ -127,8 +127,7 @@ const renderer = (): RenderResult =>
|
|||||||
>
|
>
|
||||||
<PreferenceContextProvider>
|
<PreferenceContextProvider>
|
||||||
<LogsExplorerViews
|
<LogsExplorerViews
|
||||||
selectedView={SELECTED_VIEWS.SEARCH}
|
selectedView={ExplorerViews.LIST}
|
||||||
showFrequencyChart
|
|
||||||
setIsLoadingQueries={(): void => {}}
|
setIsLoadingQueries={(): void => {}}
|
||||||
listQueryKeyRef={{ current: {} }}
|
listQueryKeyRef={{ current: {} }}
|
||||||
chartQueryKeyRef={{ current: {} }}
|
chartQueryKeyRef={{ current: {} }}
|
||||||
@@ -212,8 +211,7 @@ describe('LogsExplorerViews -', () => {
|
|||||||
<QueryBuilderContext.Provider value={mockQueryBuilderContextValue}>
|
<QueryBuilderContext.Provider value={mockQueryBuilderContextValue}>
|
||||||
<PreferenceContextProvider>
|
<PreferenceContextProvider>
|
||||||
<LogsExplorerViews
|
<LogsExplorerViews
|
||||||
selectedView={SELECTED_VIEWS.SEARCH}
|
selectedView={ExplorerViews.LIST}
|
||||||
showFrequencyChart
|
|
||||||
setIsLoadingQueries={(): void => {}}
|
setIsLoadingQueries={(): void => {}}
|
||||||
listQueryKeyRef={{ current: {} }}
|
listQueryKeyRef={{ current: {} }}
|
||||||
chartQueryKeyRef={{ current: {} }}
|
chartQueryKeyRef={{ current: {} }}
|
||||||
|
|||||||
@@ -5,7 +5,8 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
margin: 10px 0;
|
margin: 4px 0;
|
||||||
|
padding: 0 1rem;
|
||||||
|
|
||||||
.explore-header-left-actions {
|
.explore-header-left-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -55,7 +56,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.explore-content {
|
.explore-content {
|
||||||
margin-top: 10px;
|
padding: 0 8px;
|
||||||
|
|
||||||
.ant-space {
|
.ant-space {
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
|
|||||||
@@ -3,9 +3,11 @@ import './Explorer.styles.scss';
|
|||||||
import * as Sentry from '@sentry/react';
|
import * as Sentry from '@sentry/react';
|
||||||
import { Switch } from 'antd';
|
import { Switch } from 'antd';
|
||||||
import logEvent from 'api/common/logEvent';
|
import logEvent from 'api/common/logEvent';
|
||||||
|
import { QueryBuilderV2 } from 'components/QueryBuilderV2/QueryBuilderV2';
|
||||||
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
|
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
|
||||||
import ExplorerOptionWrapper from 'container/ExplorerOptions/ExplorerOptionWrapper';
|
import ExplorerOptionWrapper from 'container/ExplorerOptions/ExplorerOptionWrapper';
|
||||||
import RightToolbarActions from 'container/QueryBuilder/components/ToolbarActions/RightToolbarActions';
|
import RightToolbarActions from 'container/QueryBuilder/components/ToolbarActions/RightToolbarActions';
|
||||||
|
import { QueryBuilderProps } from 'container/QueryBuilder/QueryBuilder.interfaces';
|
||||||
import DateTimeSelector from 'container/TopNav/DateTimeSelectionV2';
|
import DateTimeSelector from 'container/TopNav/DateTimeSelectionV2';
|
||||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||||
import { useShareBuilderUrl } from 'hooks/queryBuilder/useShareBuilderUrl';
|
import { useShareBuilderUrl } from 'hooks/queryBuilder/useShareBuilderUrl';
|
||||||
@@ -20,7 +22,7 @@ import { generateExportToDashboardLink } from 'utils/dashboard/generateExportToD
|
|||||||
import { v4 as uuid } from 'uuid';
|
import { v4 as uuid } from 'uuid';
|
||||||
|
|
||||||
import { MetricsExplorerEventKeys, MetricsExplorerEvents } from '../events';
|
import { MetricsExplorerEventKeys, MetricsExplorerEvents } from '../events';
|
||||||
import QuerySection from './QuerySection';
|
// import QuerySection from './QuerySection';
|
||||||
import TimeSeries from './TimeSeries';
|
import TimeSeries from './TimeSeries';
|
||||||
import { ExplorerTabs } from './types';
|
import { ExplorerTabs } from './types';
|
||||||
import { splitQueryIntoOneChartPerQuery } from './utils';
|
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(
|
const exportDefaultQuery = useMemo(
|
||||||
() =>
|
() =>
|
||||||
updateAllQueriesOperators(
|
updateAllQueriesOperators(
|
||||||
@@ -63,7 +75,7 @@ function Explorer(): JSX.Element {
|
|||||||
[currentQuery, updateAllQueriesOperators],
|
[currentQuery, updateAllQueriesOperators],
|
||||||
);
|
);
|
||||||
|
|
||||||
useShareBuilderUrl(exportDefaultQuery);
|
useShareBuilderUrl({ defaultValue: defaultQuery });
|
||||||
|
|
||||||
const handleExport = useCallback(
|
const handleExport = useCallback(
|
||||||
(
|
(
|
||||||
@@ -101,6 +113,11 @@ function Explorer(): JSX.Element {
|
|||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const queryComponents = useMemo(
|
||||||
|
(): QueryBuilderProps['queryComponents'] => ({}),
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Sentry.ErrorBoundary fallback={<ErrorBoundaryFallback />}>
|
<Sentry.ErrorBoundary fallback={<ErrorBoundaryFallback />}>
|
||||||
<div className="metrics-explorer-explore-container">
|
<div className="metrics-explorer-explore-container">
|
||||||
@@ -115,10 +132,19 @@ function Explorer(): JSX.Element {
|
|||||||
</div>
|
</div>
|
||||||
<div className="explore-header-right-actions">
|
<div className="explore-header-right-actions">
|
||||||
<DateTimeSelector showAutoRefresh />
|
<DateTimeSelector showAutoRefresh />
|
||||||
<RightToolbarActions onStageRunQuery={handleRunQuery} />
|
<RightToolbarActions
|
||||||
|
onStageRunQuery={(): void => handleRunQuery(true, true)}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</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 */}
|
{/* TODO: Enable once we have resolved all related metrics issues */}
|
||||||
{/* <Button.Group className="explore-tabs">
|
{/* <Button.Group className="explore-tabs">
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import classNames from 'classnames';
|
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 { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
|
||||||
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||||
import { BuilderUnitsFilter } from 'container/QueryBuilder/filters/BuilderUnitsFilter/BuilderUnits';
|
import { BuilderUnitsFilter } from 'container/QueryBuilder/filters/BuilderUnitsFilter/BuilderUnits';
|
||||||
@@ -63,7 +63,7 @@ function TimeSeries({ showOneChartPerQuery }: TimeSeriesProps): JSX.Element {
|
|||||||
queryKey: [
|
queryKey: [
|
||||||
REACT_QUERY_KEY.GET_QUERY_RANGE,
|
REACT_QUERY_KEY.GET_QUERY_RANGE,
|
||||||
payload,
|
payload,
|
||||||
ENTITY_VERSION_V4,
|
ENTITY_VERSION_V5,
|
||||||
globalSelectedTime,
|
globalSelectedTime,
|
||||||
maxTime,
|
maxTime,
|
||||||
minTime,
|
minTime,
|
||||||
@@ -80,7 +80,8 @@ function TimeSeries({ showOneChartPerQuery }: TimeSeriesProps): JSX.Element {
|
|||||||
dataSource: DataSource.METRICS,
|
dataSource: DataSource.METRICS,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
ENTITY_VERSION_V4,
|
// ENTITY_VERSION_V4,
|
||||||
|
ENTITY_VERSION_V5,
|
||||||
),
|
),
|
||||||
enabled: !!payload,
|
enabled: !!payload,
|
||||||
})),
|
})),
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { Color } from '@signozhq/design-tokens';
|
|||||||
import { Button, Tabs, Typography } from 'antd';
|
import { Button, Tabs, Typography } from 'antd';
|
||||||
import logEvent from 'api/common/logEvent';
|
import logEvent from 'api/common/logEvent';
|
||||||
import PromQLIcon from 'assets/Dashboard/PromQl';
|
import PromQLIcon from 'assets/Dashboard/PromQl';
|
||||||
|
import { QueryBuilderV2 } from 'components/QueryBuilderV2/QueryBuilderV2';
|
||||||
import TextToolTip from 'components/TextToolTip';
|
import TextToolTip from 'components/TextToolTip';
|
||||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||||
import { QBShortcuts } from 'constants/shortcuts/QBShortcuts';
|
import { QBShortcuts } from 'constants/shortcuts/QBShortcuts';
|
||||||
@@ -11,7 +12,7 @@ import {
|
|||||||
getDefaultWidgetData,
|
getDefaultWidgetData,
|
||||||
PANEL_TYPE_TO_QUERY_TYPES,
|
PANEL_TYPE_TO_QUERY_TYPES,
|
||||||
} from 'container/NewWidget/utils';
|
} from 'container/NewWidget/utils';
|
||||||
import { QueryBuilder } from 'container/QueryBuilder';
|
// import { QueryBuilder } from 'container/QueryBuilder';
|
||||||
import { QueryBuilderProps } from 'container/QueryBuilder/QueryBuilder.interfaces';
|
import { QueryBuilderProps } from 'container/QueryBuilder/QueryBuilder.interfaces';
|
||||||
import { useKeyboardHotkeys } from 'hooks/hotkeys/useKeyboardHotkeys';
|
import { useKeyboardHotkeys } from 'hooks/hotkeys/useKeyboardHotkeys';
|
||||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||||
@@ -63,7 +64,7 @@ function QuerySection({
|
|||||||
|
|
||||||
const { query } = selectedWidget;
|
const { query } = selectedWidget;
|
||||||
|
|
||||||
useShareBuilderUrl(query);
|
useShareBuilderUrl({ defaultValue: query });
|
||||||
|
|
||||||
const handleStageQuery = useCallback(
|
const handleStageQuery = useCallback(
|
||||||
(query: Query): void => {
|
(query: Query): void => {
|
||||||
@@ -142,6 +143,11 @@ function QuerySection({
|
|||||||
return config;
|
return config;
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const queryComponents = useMemo(
|
||||||
|
(): QueryBuilderProps['queryComponents'] => ({}),
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
const items = useMemo(() => {
|
const items = useMemo(() => {
|
||||||
const supportedQueryTypes = PANEL_TYPE_TO_QUERY_TYPES[selectedGraph] || [];
|
const supportedQueryTypes = PANEL_TYPE_TO_QUERY_TYPES[selectedGraph] || [];
|
||||||
|
|
||||||
@@ -150,12 +156,15 @@ function QuerySection({
|
|||||||
icon: <Atom size={14} />,
|
icon: <Atom size={14} />,
|
||||||
label: 'Query Builder',
|
label: 'Query Builder',
|
||||||
component: (
|
component: (
|
||||||
<QueryBuilder
|
<div className="query-builder-v2-container">
|
||||||
panelType={selectedGraph}
|
<QueryBuilderV2
|
||||||
filterConfigs={filterConfigs}
|
panelType={selectedGraph}
|
||||||
version={selectedDashboard?.data?.version || 'v3'}
|
filterConfigs={filterConfigs}
|
||||||
isListViewPanel={selectedGraph === PANEL_TYPES.LIST}
|
version={selectedDashboard?.data?.version || 'v3'}
|
||||||
/>
|
isListViewPanel={selectedGraph === PANEL_TYPES.LIST}
|
||||||
|
queryComponents={queryComponents}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
[EQueryType.CLICKHOUSE]: {
|
[EQueryType.CLICKHOUSE]: {
|
||||||
@@ -186,6 +195,7 @@ function QuerySection({
|
|||||||
children: queryTypeComponents[queryType].component,
|
children: queryTypeComponents[queryType].component,
|
||||||
}));
|
}));
|
||||||
}, [
|
}, [
|
||||||
|
queryComponents,
|
||||||
selectedGraph,
|
selectedGraph,
|
||||||
filterConfigs,
|
filterConfigs,
|
||||||
selectedDashboard?.data?.version,
|
selectedDashboard?.data?.version,
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { WidgetGraphContainerProps } from 'container/NewWidget/types';
|
|||||||
import { getSortedSeriesData } from 'utils/getSortedSeriesData';
|
import { getSortedSeriesData } from 'utils/getSortedSeriesData';
|
||||||
|
|
||||||
import { NotFoundContainer } from './styles';
|
import { NotFoundContainer } from './styles';
|
||||||
|
import { populateMultipleResults } from './util';
|
||||||
import WidgetGraph from './WidgetGraphs';
|
import WidgetGraph from './WidgetGraphs';
|
||||||
|
|
||||||
function WidgetGraphContainer({
|
function WidgetGraphContainer({
|
||||||
@@ -22,6 +23,12 @@ function WidgetGraphContainer({
|
|||||||
queryResponse.data.payload.data.result = sortedSeriesData;
|
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) {
|
if (selectedWidget === undefined) {
|
||||||
return <Card>Invalid widget</Card>;
|
return <Card>Invalid widget</Card>;
|
||||||
}
|
}
|
||||||
@@ -43,6 +50,7 @@ function WidgetGraphContainer({
|
|||||||
|
|
||||||
if (
|
if (
|
||||||
selectedGraph !== PANEL_TYPES.LIST &&
|
selectedGraph !== PANEL_TYPES.LIST &&
|
||||||
|
selectedGraph !== PANEL_TYPES.VALUE &&
|
||||||
queryResponse.data?.payload.data?.result?.length === 0
|
queryResponse.data?.payload.data?.result?.length === 0
|
||||||
) {
|
) {
|
||||||
return (
|
return (
|
||||||
@@ -52,7 +60,7 @@ function WidgetGraphContainer({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
selectedGraph === PANEL_TYPES.LIST &&
|
(selectedGraph === PANEL_TYPES.LIST || selectedGraph === PANEL_TYPES.VALUE) &&
|
||||||
queryResponse.data?.payload?.data?.newResult?.data?.result?.length === 0
|
queryResponse.data?.payload?.data?.newResult?.data?.result?.length === 0
|
||||||
) {
|
) {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -1,11 +1,10 @@
|
|||||||
import './LeftContainer.styles.scss';
|
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 { PANEL_TYPES } from 'constants/queryBuilder';
|
||||||
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||||
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
|
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
|
||||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||||
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
|
||||||
import { memo, useEffect } from 'react';
|
import { memo, useEffect } from 'react';
|
||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
import { AppState } from 'store/reducers';
|
import { AppState } from 'store/reducers';
|
||||||
@@ -30,7 +29,7 @@ function LeftContainer({
|
|||||||
setQueryResponse,
|
setQueryResponse,
|
||||||
}: WidgetGraphProps): JSX.Element {
|
}: WidgetGraphProps): JSX.Element {
|
||||||
const { stagedQuery } = useQueryBuilder();
|
const { stagedQuery } = useQueryBuilder();
|
||||||
const { selectedDashboard } = useDashboard();
|
// const { selectedDashboard } = useDashboard();
|
||||||
|
|
||||||
const { selectedTime: globalSelectedInterval } = useSelector<
|
const { selectedTime: globalSelectedInterval } = useSelector<
|
||||||
AppState,
|
AppState,
|
||||||
@@ -38,7 +37,8 @@ function LeftContainer({
|
|||||||
>((state) => state.globalTime);
|
>((state) => state.globalTime);
|
||||||
const queryResponse = useGetQueryRange(
|
const queryResponse = useGetQueryRange(
|
||||||
requestData,
|
requestData,
|
||||||
selectedDashboard?.data?.version || DEFAULT_ENTITY_VERSION,
|
// selectedDashboard?.data?.version || DEFAULT_ENTITY_VERSION,
|
||||||
|
ENTITY_VERSION_V5,
|
||||||
{
|
{
|
||||||
enabled: !!stagedQuery,
|
enabled: !!stagedQuery,
|
||||||
retry: false,
|
retry: false,
|
||||||
|
|||||||
@@ -581,7 +581,11 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
|
|||||||
|
|
||||||
const setGraphHandler = (type: PANEL_TYPES): void => {
|
const setGraphHandler = (type: PANEL_TYPES): void => {
|
||||||
setIsLoadingPanelData(true);
|
setIsLoadingPanelData(true);
|
||||||
const updatedQuery = handleQueryChange(type as any, supersetQuery);
|
const updatedQuery = handleQueryChange(
|
||||||
|
type as any,
|
||||||
|
supersetQuery,
|
||||||
|
selectedGraph,
|
||||||
|
);
|
||||||
setGraphType(type);
|
setGraphType(type);
|
||||||
redirectWithQueryBuilderData(
|
redirectWithQueryBuilderData(
|
||||||
updatedQuery,
|
updatedQuery,
|
||||||
|
|||||||
@@ -56,9 +56,11 @@ export const panelTypeDataSourceFormValuesMap: Record<
|
|||||||
'aggregateAttribute',
|
'aggregateAttribute',
|
||||||
'aggregateOperator',
|
'aggregateOperator',
|
||||||
'filters',
|
'filters',
|
||||||
|
'filter',
|
||||||
'groupBy',
|
'groupBy',
|
||||||
'limit',
|
'limit',
|
||||||
'having',
|
'having',
|
||||||
|
'havingExpression',
|
||||||
'orderBy',
|
'orderBy',
|
||||||
'functions',
|
'functions',
|
||||||
'stepInterval',
|
'stepInterval',
|
||||||
@@ -66,6 +68,7 @@ export const panelTypeDataSourceFormValuesMap: Record<
|
|||||||
'queryName',
|
'queryName',
|
||||||
'legend',
|
'legend',
|
||||||
'expression',
|
'expression',
|
||||||
|
'aggregations',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -76,10 +79,12 @@ export const panelTypeDataSourceFormValuesMap: Record<
|
|||||||
'aggregateOperator',
|
'aggregateOperator',
|
||||||
'timeAggregation',
|
'timeAggregation',
|
||||||
'filters',
|
'filters',
|
||||||
|
'filter',
|
||||||
'spaceAggregation',
|
'spaceAggregation',
|
||||||
'groupBy',
|
'groupBy',
|
||||||
'limit',
|
'limit',
|
||||||
'having',
|
'having',
|
||||||
|
'havingExpression',
|
||||||
'orderBy',
|
'orderBy',
|
||||||
'stepInterval',
|
'stepInterval',
|
||||||
'legend',
|
'legend',
|
||||||
@@ -87,6 +92,7 @@ export const panelTypeDataSourceFormValuesMap: Record<
|
|||||||
'disabled',
|
'disabled',
|
||||||
'functions',
|
'functions',
|
||||||
'expression',
|
'expression',
|
||||||
|
'aggregations',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -96,9 +102,11 @@ export const panelTypeDataSourceFormValuesMap: Record<
|
|||||||
'aggregateAttribute',
|
'aggregateAttribute',
|
||||||
'aggregateOperator',
|
'aggregateOperator',
|
||||||
'filters',
|
'filters',
|
||||||
|
'filter',
|
||||||
'groupBy',
|
'groupBy',
|
||||||
'limit',
|
'limit',
|
||||||
'having',
|
'having',
|
||||||
|
'havingExpression',
|
||||||
'orderBy',
|
'orderBy',
|
||||||
'functions',
|
'functions',
|
||||||
'stepInterval',
|
'stepInterval',
|
||||||
@@ -106,6 +114,7 @@ export const panelTypeDataSourceFormValuesMap: Record<
|
|||||||
'queryName',
|
'queryName',
|
||||||
'legend',
|
'legend',
|
||||||
'expression',
|
'expression',
|
||||||
|
'aggregations',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -117,9 +126,11 @@ export const panelTypeDataSourceFormValuesMap: Record<
|
|||||||
'aggregateAttribute',
|
'aggregateAttribute',
|
||||||
'aggregateOperator',
|
'aggregateOperator',
|
||||||
'filters',
|
'filters',
|
||||||
|
'filter',
|
||||||
'groupBy',
|
'groupBy',
|
||||||
'limit',
|
'limit',
|
||||||
'having',
|
'having',
|
||||||
|
'havingExpression',
|
||||||
'orderBy',
|
'orderBy',
|
||||||
'functions',
|
'functions',
|
||||||
'stepInterval',
|
'stepInterval',
|
||||||
@@ -127,6 +138,7 @@ export const panelTypeDataSourceFormValuesMap: Record<
|
|||||||
'queryName',
|
'queryName',
|
||||||
'legend',
|
'legend',
|
||||||
'expression',
|
'expression',
|
||||||
|
'aggregations',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -137,10 +149,12 @@ export const panelTypeDataSourceFormValuesMap: Record<
|
|||||||
'aggregateOperator',
|
'aggregateOperator',
|
||||||
'timeAggregation',
|
'timeAggregation',
|
||||||
'filters',
|
'filters',
|
||||||
|
'filter',
|
||||||
'spaceAggregation',
|
'spaceAggregation',
|
||||||
'groupBy',
|
'groupBy',
|
||||||
'limit',
|
'limit',
|
||||||
'having',
|
'having',
|
||||||
|
'havingExpression',
|
||||||
'orderBy',
|
'orderBy',
|
||||||
'stepInterval',
|
'stepInterval',
|
||||||
'legend',
|
'legend',
|
||||||
@@ -148,6 +162,7 @@ export const panelTypeDataSourceFormValuesMap: Record<
|
|||||||
'disabled',
|
'disabled',
|
||||||
'functions',
|
'functions',
|
||||||
'expression',
|
'expression',
|
||||||
|
'aggregations',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -157,9 +172,11 @@ export const panelTypeDataSourceFormValuesMap: Record<
|
|||||||
'aggregateAttribute',
|
'aggregateAttribute',
|
||||||
'aggregateOperator',
|
'aggregateOperator',
|
||||||
'filters',
|
'filters',
|
||||||
|
'filter',
|
||||||
'groupBy',
|
'groupBy',
|
||||||
'limit',
|
'limit',
|
||||||
'having',
|
'having',
|
||||||
|
'havingExpression',
|
||||||
'orderBy',
|
'orderBy',
|
||||||
'functions',
|
'functions',
|
||||||
'stepInterval',
|
'stepInterval',
|
||||||
@@ -167,6 +184,7 @@ export const panelTypeDataSourceFormValuesMap: Record<
|
|||||||
'queryName',
|
'queryName',
|
||||||
'legend',
|
'legend',
|
||||||
'expression',
|
'expression',
|
||||||
|
'aggregations',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -178,9 +196,11 @@ export const panelTypeDataSourceFormValuesMap: Record<
|
|||||||
'aggregateAttribute',
|
'aggregateAttribute',
|
||||||
'aggregateOperator',
|
'aggregateOperator',
|
||||||
'filters',
|
'filters',
|
||||||
|
'filter',
|
||||||
'groupBy',
|
'groupBy',
|
||||||
'limit',
|
'limit',
|
||||||
'having',
|
'having',
|
||||||
|
'havingExpression',
|
||||||
'orderBy',
|
'orderBy',
|
||||||
'functions',
|
'functions',
|
||||||
'stepInterval',
|
'stepInterval',
|
||||||
@@ -188,6 +208,7 @@ export const panelTypeDataSourceFormValuesMap: Record<
|
|||||||
'queryName',
|
'queryName',
|
||||||
'legend',
|
'legend',
|
||||||
'expression',
|
'expression',
|
||||||
|
'aggregations',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -198,10 +219,12 @@ export const panelTypeDataSourceFormValuesMap: Record<
|
|||||||
'aggregateOperator',
|
'aggregateOperator',
|
||||||
'timeAggregation',
|
'timeAggregation',
|
||||||
'filters',
|
'filters',
|
||||||
|
'filter',
|
||||||
'spaceAggregation',
|
'spaceAggregation',
|
||||||
'groupBy',
|
'groupBy',
|
||||||
'limit',
|
'limit',
|
||||||
'having',
|
'having',
|
||||||
|
'havingExpression',
|
||||||
'orderBy',
|
'orderBy',
|
||||||
'stepInterval',
|
'stepInterval',
|
||||||
'legend',
|
'legend',
|
||||||
@@ -209,6 +232,7 @@ export const panelTypeDataSourceFormValuesMap: Record<
|
|||||||
'disabled',
|
'disabled',
|
||||||
'functions',
|
'functions',
|
||||||
'expression',
|
'expression',
|
||||||
|
'aggregations',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -218,9 +242,11 @@ export const panelTypeDataSourceFormValuesMap: Record<
|
|||||||
'aggregateAttribute',
|
'aggregateAttribute',
|
||||||
'aggregateOperator',
|
'aggregateOperator',
|
||||||
'filters',
|
'filters',
|
||||||
|
'filter',
|
||||||
'groupBy',
|
'groupBy',
|
||||||
'limit',
|
'limit',
|
||||||
'having',
|
'having',
|
||||||
|
'havingExpression',
|
||||||
'orderBy',
|
'orderBy',
|
||||||
'functions',
|
'functions',
|
||||||
'stepInterval',
|
'stepInterval',
|
||||||
@@ -228,6 +254,7 @@ export const panelTypeDataSourceFormValuesMap: Record<
|
|||||||
'queryName',
|
'queryName',
|
||||||
'legend',
|
'legend',
|
||||||
'expression',
|
'expression',
|
||||||
|
'aggregations',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -239,9 +266,11 @@ export const panelTypeDataSourceFormValuesMap: Record<
|
|||||||
'aggregateAttribute',
|
'aggregateAttribute',
|
||||||
'aggregateOperator',
|
'aggregateOperator',
|
||||||
'filters',
|
'filters',
|
||||||
|
'filter',
|
||||||
'groupBy',
|
'groupBy',
|
||||||
'limit',
|
'limit',
|
||||||
'having',
|
'having',
|
||||||
|
'havingExpression',
|
||||||
'orderBy',
|
'orderBy',
|
||||||
'functions',
|
'functions',
|
||||||
'stepInterval',
|
'stepInterval',
|
||||||
@@ -249,6 +278,7 @@ export const panelTypeDataSourceFormValuesMap: Record<
|
|||||||
'queryName',
|
'queryName',
|
||||||
'expression',
|
'expression',
|
||||||
'legend',
|
'legend',
|
||||||
|
'aggregations',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -259,11 +289,13 @@ export const panelTypeDataSourceFormValuesMap: Record<
|
|||||||
'aggregateOperator',
|
'aggregateOperator',
|
||||||
'timeAggregation',
|
'timeAggregation',
|
||||||
'filters',
|
'filters',
|
||||||
|
'filter',
|
||||||
'spaceAggregation',
|
'spaceAggregation',
|
||||||
'groupBy',
|
'groupBy',
|
||||||
'reduceTo',
|
'reduceTo',
|
||||||
'limit',
|
'limit',
|
||||||
'having',
|
'having',
|
||||||
|
'havingExpression',
|
||||||
'orderBy',
|
'orderBy',
|
||||||
'stepInterval',
|
'stepInterval',
|
||||||
'legend',
|
'legend',
|
||||||
@@ -271,6 +303,7 @@ export const panelTypeDataSourceFormValuesMap: Record<
|
|||||||
'expression',
|
'expression',
|
||||||
'disabled',
|
'disabled',
|
||||||
'functions',
|
'functions',
|
||||||
|
'aggregations',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -280,9 +313,11 @@ export const panelTypeDataSourceFormValuesMap: Record<
|
|||||||
'aggregateAttribute',
|
'aggregateAttribute',
|
||||||
'aggregateOperator',
|
'aggregateOperator',
|
||||||
'filters',
|
'filters',
|
||||||
|
'filter',
|
||||||
'groupBy',
|
'groupBy',
|
||||||
'limit',
|
'limit',
|
||||||
'having',
|
'having',
|
||||||
|
'havingExpression',
|
||||||
'orderBy',
|
'orderBy',
|
||||||
'functions',
|
'functions',
|
||||||
'stepInterval',
|
'stepInterval',
|
||||||
@@ -290,6 +325,7 @@ export const panelTypeDataSourceFormValuesMap: Record<
|
|||||||
'queryName',
|
'queryName',
|
||||||
'expression',
|
'expression',
|
||||||
'legend',
|
'legend',
|
||||||
|
'aggregations',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -301,9 +337,11 @@ export const panelTypeDataSourceFormValuesMap: Record<
|
|||||||
'aggregateAttribute',
|
'aggregateAttribute',
|
||||||
'aggregateOperator',
|
'aggregateOperator',
|
||||||
'filters',
|
'filters',
|
||||||
|
'filter',
|
||||||
'groupBy',
|
'groupBy',
|
||||||
'limit',
|
'limit',
|
||||||
'having',
|
'having',
|
||||||
|
'havingExpression',
|
||||||
'orderBy',
|
'orderBy',
|
||||||
'functions',
|
'functions',
|
||||||
'stepInterval',
|
'stepInterval',
|
||||||
@@ -311,6 +349,7 @@ export const panelTypeDataSourceFormValuesMap: Record<
|
|||||||
'queryName',
|
'queryName',
|
||||||
'expression',
|
'expression',
|
||||||
'legend',
|
'legend',
|
||||||
|
'aggregations',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -321,11 +360,13 @@ export const panelTypeDataSourceFormValuesMap: Record<
|
|||||||
'aggregateOperator',
|
'aggregateOperator',
|
||||||
'timeAggregation',
|
'timeAggregation',
|
||||||
'filters',
|
'filters',
|
||||||
|
'filter',
|
||||||
'spaceAggregation',
|
'spaceAggregation',
|
||||||
'groupBy',
|
'groupBy',
|
||||||
'reduceTo',
|
'reduceTo',
|
||||||
'limit',
|
'limit',
|
||||||
'having',
|
'having',
|
||||||
|
'havingExpression',
|
||||||
'orderBy',
|
'orderBy',
|
||||||
'stepInterval',
|
'stepInterval',
|
||||||
'legend',
|
'legend',
|
||||||
@@ -333,6 +374,7 @@ export const panelTypeDataSourceFormValuesMap: Record<
|
|||||||
'expression',
|
'expression',
|
||||||
'disabled',
|
'disabled',
|
||||||
'functions',
|
'functions',
|
||||||
|
'aggregations',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -342,9 +384,11 @@ export const panelTypeDataSourceFormValuesMap: Record<
|
|||||||
'aggregateAttribute',
|
'aggregateAttribute',
|
||||||
'aggregateOperator',
|
'aggregateOperator',
|
||||||
'filters',
|
'filters',
|
||||||
|
'filter',
|
||||||
'groupBy',
|
'groupBy',
|
||||||
'limit',
|
'limit',
|
||||||
'having',
|
'having',
|
||||||
|
'havingExpression',
|
||||||
'orderBy',
|
'orderBy',
|
||||||
'functions',
|
'functions',
|
||||||
'stepInterval',
|
'stepInterval',
|
||||||
@@ -352,6 +396,7 @@ export const panelTypeDataSourceFormValuesMap: Record<
|
|||||||
'queryName',
|
'queryName',
|
||||||
'expression',
|
'expression',
|
||||||
'legend',
|
'legend',
|
||||||
|
'aggregations',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -359,17 +404,31 @@ export const panelTypeDataSourceFormValuesMap: Record<
|
|||||||
[PANEL_TYPES.LIST]: {
|
[PANEL_TYPES.LIST]: {
|
||||||
[DataSource.LOGS]: {
|
[DataSource.LOGS]: {
|
||||||
builder: {
|
builder: {
|
||||||
queryData: ['filters', 'limit', 'orderBy', 'functions'],
|
queryData: [
|
||||||
|
'filters',
|
||||||
|
'filter',
|
||||||
|
'limit',
|
||||||
|
'orderBy',
|
||||||
|
'functions',
|
||||||
|
'aggregations',
|
||||||
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
[DataSource.METRICS]: {
|
[DataSource.METRICS]: {
|
||||||
builder: {
|
builder: {
|
||||||
queryData: [],
|
queryData: ['filters', 'filter', 'aggregations'],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
[DataSource.TRACES]: {
|
[DataSource.TRACES]: {
|
||||||
builder: {
|
builder: {
|
||||||
queryData: ['filters', 'limit', 'orderBy', 'functions'],
|
queryData: [
|
||||||
|
'filters',
|
||||||
|
'filter',
|
||||||
|
'limit',
|
||||||
|
'orderBy',
|
||||||
|
'functions',
|
||||||
|
'aggregations',
|
||||||
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -380,14 +439,17 @@ export const panelTypeDataSourceFormValuesMap: Record<
|
|||||||
'aggregateAttribute',
|
'aggregateAttribute',
|
||||||
'aggregateOperator',
|
'aggregateOperator',
|
||||||
'filters',
|
'filters',
|
||||||
|
'filter',
|
||||||
'reduceTo',
|
'reduceTo',
|
||||||
'having',
|
'having',
|
||||||
|
'havingExpression',
|
||||||
'functions',
|
'functions',
|
||||||
'stepInterval',
|
'stepInterval',
|
||||||
'queryName',
|
'queryName',
|
||||||
'expression',
|
'expression',
|
||||||
'disabled',
|
'disabled',
|
||||||
'legend',
|
'legend',
|
||||||
|
'aggregations',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -398,8 +460,10 @@ export const panelTypeDataSourceFormValuesMap: Record<
|
|||||||
'aggregateOperator',
|
'aggregateOperator',
|
||||||
'timeAggregation',
|
'timeAggregation',
|
||||||
'filters',
|
'filters',
|
||||||
|
'filter',
|
||||||
'spaceAggregation',
|
'spaceAggregation',
|
||||||
'having',
|
'having',
|
||||||
|
'havingExpression',
|
||||||
'reduceTo',
|
'reduceTo',
|
||||||
'stepInterval',
|
'stepInterval',
|
||||||
'legend',
|
'legend',
|
||||||
@@ -407,6 +471,7 @@ export const panelTypeDataSourceFormValuesMap: Record<
|
|||||||
'expression',
|
'expression',
|
||||||
'disabled',
|
'disabled',
|
||||||
'functions',
|
'functions',
|
||||||
|
'aggregations',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -416,14 +481,17 @@ export const panelTypeDataSourceFormValuesMap: Record<
|
|||||||
'aggregateAttribute',
|
'aggregateAttribute',
|
||||||
'aggregateOperator',
|
'aggregateOperator',
|
||||||
'filters',
|
'filters',
|
||||||
|
'filter',
|
||||||
'reduceTo',
|
'reduceTo',
|
||||||
'having',
|
'having',
|
||||||
|
'havingExpression',
|
||||||
'functions',
|
'functions',
|
||||||
'stepInterval',
|
'stepInterval',
|
||||||
'queryName',
|
'queryName',
|
||||||
'expression',
|
'expression',
|
||||||
'disabled',
|
'disabled',
|
||||||
'legend',
|
'legend',
|
||||||
|
'aggregations',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -433,7 +501,9 @@ export const panelTypeDataSourceFormValuesMap: Record<
|
|||||||
export function handleQueryChange(
|
export function handleQueryChange(
|
||||||
newPanelType: keyof PartialPanelTypes,
|
newPanelType: keyof PartialPanelTypes,
|
||||||
supersetQuery: Query,
|
supersetQuery: Query,
|
||||||
|
currentPanelType: PANEL_TYPES,
|
||||||
): Query {
|
): Query {
|
||||||
|
console.log('supersetQuery', supersetQuery);
|
||||||
return {
|
return {
|
||||||
...supersetQuery,
|
...supersetQuery,
|
||||||
builder: {
|
builder: {
|
||||||
@@ -454,6 +524,7 @@ export function handleQueryChange(
|
|||||||
set(tempQuery, 'aggregateOperator', 'noop');
|
set(tempQuery, 'aggregateOperator', 'noop');
|
||||||
set(tempQuery, 'offset', 0);
|
set(tempQuery, 'offset', 0);
|
||||||
set(tempQuery, 'pageSize', 10);
|
set(tempQuery, 'pageSize', 10);
|
||||||
|
set(tempQuery, 'orderBy', undefined);
|
||||||
} else if (tempQuery.aggregateOperator === 'noop') {
|
} 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
|
// 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
|
// 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 +533,13 @@ export function handleQueryChange(
|
|||||||
unset(tempQuery, 'pageSize');
|
unset(tempQuery, 'pageSize');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
currentPanelType === PANEL_TYPES.LIST &&
|
||||||
|
newPanelType !== PANEL_TYPES.LIST
|
||||||
|
) {
|
||||||
|
set(tempQuery, 'orderBy', undefined);
|
||||||
|
}
|
||||||
|
|
||||||
return tempQuery;
|
return tempQuery;
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1631,7 +1631,7 @@
|
|||||||
"docker metrics to signoz"
|
"docker metrics to signoz"
|
||||||
],
|
],
|
||||||
"imgUrl": "/Logos/docker.svg",
|
"imgUrl": "/Logos/docker.svg",
|
||||||
"link": "https://signoz.io/docs/userguide/k8s-metrics/"
|
"link": "https://signoz.io/docs/metrics-management/docker-container-metrics/"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"dataSource": "ec2-application-logs",
|
"dataSource": "ec2-application-logs",
|
||||||
|
|||||||
@@ -9,9 +9,17 @@ function ValuePanelWrapper({
|
|||||||
}: PanelWrapperProps): JSX.Element {
|
}: PanelWrapperProps): JSX.Element {
|
||||||
const { yAxisUnit, thresholds } = widget;
|
const { yAxisUnit, thresholds } = widget;
|
||||||
const data = getUPlotChartData(queryResponse?.data?.payload);
|
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 (
|
return (
|
||||||
<GridValueComponent
|
<GridValueComponent
|
||||||
data={data}
|
data={gridValueData}
|
||||||
yAxisUnit={yAxisUnit}
|
yAxisUnit={yAxisUnit}
|
||||||
thresholds={thresholds}
|
thresholds={thresholds}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -28,5 +28,6 @@ export type QueryBuilderProps = {
|
|||||||
queryComponents?: { renderOrderBy?: (props: OrderByFilterProps) => ReactNode };
|
queryComponents?: { renderOrderBy?: (props: OrderByFilterProps) => ReactNode };
|
||||||
isListViewPanel?: boolean;
|
isListViewPanel?: boolean;
|
||||||
showFunctions?: boolean;
|
showFunctions?: boolean;
|
||||||
|
showOnlyWhereClause?: boolean;
|
||||||
version: string;
|
version: string;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -78,11 +78,11 @@
|
|||||||
color: var(--bg-sakura-400);
|
color: var(--bg-sakura-400);
|
||||||
}
|
}
|
||||||
|
|
||||||
&.sync-btn {
|
// &.sync-btn {
|
||||||
border: 1px solid rgba(78, 116, 248, 0.2);
|
// border: 1px solid rgba(78, 116, 248, 0.2);
|
||||||
background: rgba(78, 116, 248, 0.1);
|
// background: rgba(78, 116, 248, 0.1);
|
||||||
color: var(--bg-robin-500);
|
// color: var(--bg-robin-500);
|
||||||
}
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
&.formula-btn {
|
&.formula-btn {
|
||||||
|
|||||||
@@ -161,15 +161,18 @@ export function Formula({
|
|||||||
<Col span={24}>
|
<Col span={24}>
|
||||||
<Input.TextArea
|
<Input.TextArea
|
||||||
name="expression"
|
name="expression"
|
||||||
|
className="formula-expression"
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
size="middle"
|
size="middle"
|
||||||
value={formula.expression}
|
value={formula.expression}
|
||||||
|
placeholder="Enter formula"
|
||||||
rows={2}
|
rows={2}
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
<Col span={24}>
|
<Col span={24}>
|
||||||
<Input
|
<Input
|
||||||
name="legend"
|
name="legend"
|
||||||
|
className="formula-legend"
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
size="middle"
|
size="middle"
|
||||||
value={formula.legend}
|
value={formula.legend}
|
||||||
|
|||||||
@@ -28,9 +28,12 @@
|
|||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
|
|
||||||
.periscope-btn {
|
.periscope-btn {
|
||||||
border: 1px solid var(--bg-slate-200);
|
|
||||||
background: var(--bg-ink-200);
|
|
||||||
min-width: 32px;
|
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 {
|
.query-name {
|
||||||
@@ -44,11 +47,11 @@
|
|||||||
border: 1px solid rgba(242, 71, 105, 0.2) !important;
|
border: 1px solid rgba(242, 71, 105, 0.2) !important;
|
||||||
background: rgba(242, 71, 105, 0.1) !important;
|
background: rgba(242, 71, 105, 0.1) !important;
|
||||||
|
|
||||||
&.sync-btn {
|
// &.sync-btn {
|
||||||
border: 1px solid rgba(78, 116, 248, 0.2) !important;
|
// border: 1px solid rgba(78, 116, 248, 0.2) !important;
|
||||||
background: rgba(78, 116, 248, 0.1) !important;
|
// background: rgba(78, 116, 248, 0.1) !important;
|
||||||
color: var(--bg-robin-500) !important;
|
// color: var(--bg-robin-500) !important;
|
||||||
}
|
// }
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
border: 1px solid rgba(242, 71, 105, 0.4) !important;
|
border: 1px solid rgba(242, 71, 105, 0.4) !important;
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import {
|
|||||||
} from 'types/api/queryBuilder/queryBuilderData';
|
} from 'types/api/queryBuilder/queryBuilderData';
|
||||||
import { DataSource } from 'types/common/queryBuilder';
|
import { DataSource } from 'types/common/queryBuilder';
|
||||||
|
|
||||||
|
import { DataSourceDropdown } from '..';
|
||||||
import QueryFunctions from '../QueryFunctions/QueryFunctions';
|
import QueryFunctions from '../QueryFunctions/QueryFunctions';
|
||||||
|
|
||||||
interface QBEntityOptionsProps {
|
interface QBEntityOptionsProps {
|
||||||
@@ -31,16 +32,20 @@ interface QBEntityOptionsProps {
|
|||||||
isCollapsed: boolean;
|
isCollapsed: boolean;
|
||||||
entityType: string;
|
entityType: string;
|
||||||
entityData: any;
|
entityData: any;
|
||||||
onDelete: () => void;
|
onDelete?: () => void;
|
||||||
onCloneQuery?: (type: string, query: IBuilderQuery) => void;
|
onCloneQuery?: (type: string, query: IBuilderQuery) => void;
|
||||||
onToggleVisibility: () => void;
|
onToggleVisibility: () => void;
|
||||||
onCollapseEntity: () => void;
|
onCollapseEntity: () => void;
|
||||||
onQueryFunctionsUpdates?: (functions: QueryFunctionProps[]) => void;
|
onQueryFunctionsUpdates?: (functions: QueryFunctionProps[]) => void;
|
||||||
showDeleteButton: boolean;
|
showDeleteButton?: boolean;
|
||||||
|
showCloneOption?: boolean;
|
||||||
isListViewPanel?: boolean;
|
isListViewPanel?: boolean;
|
||||||
index?: number;
|
index?: number;
|
||||||
|
queryVariant?: 'dropdown' | 'static';
|
||||||
|
onChangeDataSource?: (value: DataSource) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||||
export default function QBEntityOptions({
|
export default function QBEntityOptions({
|
||||||
query,
|
query,
|
||||||
isMetricsDataSource,
|
isMetricsDataSource,
|
||||||
@@ -48,14 +53,17 @@ export default function QBEntityOptions({
|
|||||||
showFunctions,
|
showFunctions,
|
||||||
entityType,
|
entityType,
|
||||||
entityData,
|
entityData,
|
||||||
onDelete,
|
|
||||||
onCloneQuery,
|
|
||||||
onToggleVisibility,
|
onToggleVisibility,
|
||||||
onCollapseEntity,
|
onCollapseEntity,
|
||||||
showDeleteButton,
|
|
||||||
onQueryFunctionsUpdates,
|
onQueryFunctionsUpdates,
|
||||||
isListViewPanel,
|
isListViewPanel,
|
||||||
|
onDelete,
|
||||||
|
showDeleteButton,
|
||||||
|
showCloneOption,
|
||||||
|
onCloneQuery,
|
||||||
index,
|
index,
|
||||||
|
queryVariant,
|
||||||
|
onChangeDataSource,
|
||||||
}: QBEntityOptionsProps): JSX.Element {
|
}: QBEntityOptionsProps): JSX.Element {
|
||||||
const handleCloneEntity = (): void => {
|
const handleCloneEntity = (): void => {
|
||||||
if (isFunction(onCloneQuery)) {
|
if (isFunction(onCloneQuery)) {
|
||||||
@@ -97,7 +105,7 @@ export default function QBEntityOptions({
|
|||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
{entityType === 'query' && (
|
{entityType === 'query' && showCloneOption && (
|
||||||
<Tooltip title={`Clone Query ${entityData.queryName}`}>
|
<Tooltip title={`Clone Query ${entityData.queryName}`}>
|
||||||
<Button className={cx('periscope-btn')} onClick={handleCloneEntity}>
|
<Button className={cx('periscope-btn')} onClick={handleCloneEntity}>
|
||||||
<Copy size={14} />
|
<Copy size={14} />
|
||||||
@@ -115,7 +123,23 @@ export default function QBEntityOptions({
|
|||||||
{entityData.queryName}
|
{entityData.queryName}
|
||||||
</Button>
|
</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 &&
|
{showFunctions &&
|
||||||
|
!isListViewPanel &&
|
||||||
(isMetricsDataSource || isLogsDataSource) &&
|
(isMetricsDataSource || isLogsDataSource) &&
|
||||||
query &&
|
query &&
|
||||||
onQueryFunctionsUpdates && (
|
onQueryFunctionsUpdates && (
|
||||||
@@ -138,7 +162,7 @@ export default function QBEntityOptions({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{showDeleteButton && (
|
{showDeleteButton && !isListViewPanel && (
|
||||||
<Button className="periscope-btn ghost" onClick={onDelete}>
|
<Button className="periscope-btn ghost" onClick={onDelete}>
|
||||||
<Trash2 size={14} />
|
<Trash2 size={14} />
|
||||||
</Button>
|
</Button>
|
||||||
@@ -156,4 +180,9 @@ QBEntityOptions.defaultProps = {
|
|||||||
showFunctions: false,
|
showFunctions: false,
|
||||||
onCloneQuery: noop,
|
onCloneQuery: noop,
|
||||||
index: 0,
|
index: 0,
|
||||||
|
onDelete: noop,
|
||||||
|
showDeleteButton: false,
|
||||||
|
showCloneOption: true,
|
||||||
|
queryVariant: 'static',
|
||||||
|
onChangeDataSource: noop,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -5,8 +5,10 @@ export type QueryProps = {
|
|||||||
index: number;
|
index: number;
|
||||||
isAvailableToDisable: boolean;
|
isAvailableToDisable: boolean;
|
||||||
query: IBuilderQuery;
|
query: IBuilderQuery;
|
||||||
queryVariant: 'static' | 'dropdown';
|
queryVariant?: 'static' | 'dropdown';
|
||||||
isListViewPanel?: boolean;
|
isListViewPanel?: boolean;
|
||||||
showFunctions?: boolean;
|
showFunctions?: boolean;
|
||||||
version: string;
|
version: string;
|
||||||
|
showSpanScopeSelector?: boolean;
|
||||||
|
showOnlyWhereClause?: boolean;
|
||||||
} & Pick<QueryBuilderProps, 'filterConfigs' | 'queryComponents'>;
|
} & Pick<QueryBuilderProps, 'filterConfigs' | 'queryComponents'>;
|
||||||
|
|||||||
@@ -350,6 +350,7 @@ export const Query = memo(function Query({
|
|||||||
showDeleteButton={currentQuery.builder.queryData.length > 1}
|
showDeleteButton={currentQuery.builder.queryData.length > 1}
|
||||||
isListViewPanel={isListViewPanel}
|
isListViewPanel={isListViewPanel}
|
||||||
index={index}
|
index={index}
|
||||||
|
queryVariant={queryVariant}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{!isCollapse && (
|
{!isCollapse && (
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ interface SpaceAggregationOptionsProps {
|
|||||||
disabled: boolean;
|
disabled: boolean;
|
||||||
onSelect: (value: string) => void;
|
onSelect: (value: string) => void;
|
||||||
operators: any[];
|
operators: any[];
|
||||||
|
qbVersion?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function SpaceAggregationOptions({
|
export default function SpaceAggregationOptions({
|
||||||
@@ -19,8 +20,10 @@ export default function SpaceAggregationOptions({
|
|||||||
disabled,
|
disabled,
|
||||||
onSelect,
|
onSelect,
|
||||||
operators,
|
operators,
|
||||||
|
qbVersion,
|
||||||
}: SpaceAggregationOptionsProps): JSX.Element {
|
}: 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(
|
const [defaultValue, setDefaultValue] = useState(
|
||||||
selectedValue || placeHolderText,
|
selectedValue || placeHolderText,
|
||||||
);
|
);
|
||||||
@@ -58,10 +61,15 @@ export default function SpaceAggregationOptions({
|
|||||||
>
|
>
|
||||||
{operators.map((operator) => (
|
{operators.map((operator) => (
|
||||||
<Select.Option key={operator.value} value={operator.value}>
|
<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.Option>
|
||||||
))}
|
))}
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SpaceAggregationOptions.defaultProps = {
|
||||||
|
qbVersion: 'v2',
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,35 +1,31 @@
|
|||||||
|
/* eslint-disable sonarjs/no-duplicate-string */
|
||||||
import './ToolbarActions.styles.scss';
|
import './ToolbarActions.styles.scss';
|
||||||
|
|
||||||
import { FilterOutlined } from '@ant-design/icons';
|
import { FilterOutlined } from '@ant-design/icons';
|
||||||
import { Button, Switch, Tooltip, Typography } from 'antd';
|
import { Button, Tooltip } from 'antd';
|
||||||
import cx from 'classnames';
|
import cx from 'classnames';
|
||||||
import { Atom, SquareMousePointer, Terminal } from 'lucide-react';
|
import { Atom, Binoculars, SquareMousePointer, Terminal } from 'lucide-react';
|
||||||
import { SELECTED_VIEWS } from 'pages/LogsExplorer/utils';
|
import { ExplorerViews } from 'pages/LogsExplorer/utils';
|
||||||
|
|
||||||
interface LeftToolbarActionsProps {
|
interface LeftToolbarActionsProps {
|
||||||
items: any;
|
items: any;
|
||||||
selectedView: string;
|
selectedView: string;
|
||||||
onToggleHistrogramVisibility: () => void;
|
onChangeSelectedView: (view: ExplorerViews) => void;
|
||||||
onChangeSelectedView: (view: SELECTED_VIEWS) => void;
|
|
||||||
showFrequencyChart: boolean;
|
|
||||||
showFilter: boolean;
|
showFilter: boolean;
|
||||||
handleFilterVisibilityChange: () => void;
|
handleFilterVisibilityChange: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const activeTab = 'active-tab';
|
const activeTab = 'active-tab';
|
||||||
const actionBtn = 'action-btn';
|
|
||||||
export const queryBuilder = 'query-builder';
|
|
||||||
|
|
||||||
export default function LeftToolbarActions({
|
export default function LeftToolbarActions({
|
||||||
items,
|
items,
|
||||||
selectedView,
|
selectedView,
|
||||||
onToggleHistrogramVisibility,
|
|
||||||
onChangeSelectedView,
|
onChangeSelectedView,
|
||||||
showFrequencyChart,
|
|
||||||
showFilter,
|
showFilter,
|
||||||
handleFilterVisibilityChange,
|
handleFilterVisibilityChange,
|
||||||
}: LeftToolbarActionsProps): JSX.Element {
|
}: LeftToolbarActionsProps): JSX.Element {
|
||||||
const { clickhouse, search, queryBuilder: QB } = items;
|
const { clickhouse, list, timeseries, table, trace } = items;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="left-toolbar">
|
<div className="left-toolbar">
|
||||||
@@ -41,56 +37,90 @@ export default function LeftToolbarActions({
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
<div className="left-toolbar-query-actions">
|
<div className="left-toolbar-query-actions">
|
||||||
<Tooltip title="Search">
|
{list?.show && (
|
||||||
<Button
|
<Tooltip title="List View">
|
||||||
disabled={search.disabled}
|
<Button
|
||||||
className={cx(
|
disabled={list.disabled}
|
||||||
'search',
|
className={cx(
|
||||||
actionBtn,
|
'list-view-tab',
|
||||||
selectedView === 'search' ? activeTab : '',
|
'explorer-view-option',
|
||||||
)}
|
selectedView === list.key ? activeTab : '',
|
||||||
onClick={(): void => onChangeSelectedView(SELECTED_VIEWS.SEARCH)}
|
)}
|
||||||
>
|
onClick={(): void => onChangeSelectedView(list.key)}
|
||||||
<SquareMousePointer size={14} data-testid="search-view" />
|
>
|
||||||
</Button>
|
<SquareMousePointer size={14} data-testid="search-view" />
|
||||||
</Tooltip>
|
List View
|
||||||
<Tooltip title="Query Builder">
|
</Button>
|
||||||
<Button
|
</Tooltip>
|
||||||
disabled={QB.disabled}
|
)}
|
||||||
className={cx(
|
|
||||||
queryBuilder,
|
{trace?.show && (
|
||||||
actionBtn,
|
<Tooltip title="Trace View">
|
||||||
selectedView === queryBuilder ? activeTab : '',
|
<Button
|
||||||
)}
|
disabled={trace.disabled}
|
||||||
onClick={(): void => onChangeSelectedView(SELECTED_VIEWS.QUERY_BUILDER)}
|
className={cx(
|
||||||
>
|
'trace-view-tab',
|
||||||
<Atom size={14} data-testid="query-builder-view" />
|
'explorer-view-option',
|
||||||
</Button>
|
selectedView === trace.key ? activeTab : '',
|
||||||
</Tooltip>
|
)}
|
||||||
|
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 && (
|
{clickhouse?.show && (
|
||||||
<Button
|
<Tooltip title="Clickhouse">
|
||||||
disabled={clickhouse.disabled}
|
<Button
|
||||||
className={cx(
|
disabled={clickhouse.disabled}
|
||||||
SELECTED_VIEWS.CLICKHOUSE,
|
className={cx(
|
||||||
actionBtn,
|
'clickhouse-view-tab',
|
||||||
selectedView === SELECTED_VIEWS.CLICKHOUSE ? activeTab : '',
|
'explorer-view-option',
|
||||||
)}
|
selectedView === clickhouse.key ? activeTab : '',
|
||||||
onClick={(): void => onChangeSelectedView(SELECTED_VIEWS.CLICKHOUSE)}
|
)}
|
||||||
>
|
onClick={(): void => onChangeSelectedView(clickhouse.key)}
|
||||||
<Terminal size={14} data-testid="clickhouse-view" />
|
>
|
||||||
</Button>
|
<Terminal size={14} data-testid="clickhouse-view" />
|
||||||
|
Clickhouse
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="frequency-chart-view-controller">
|
{table?.show && (
|
||||||
<Typography>Frequency chart</Typography>
|
<Tooltip title="Table">
|
||||||
<Switch
|
<Button
|
||||||
size="small"
|
disabled={table.disabled}
|
||||||
checked={showFrequencyChart}
|
className={cx(
|
||||||
defaultChecked
|
'table-view-tab',
|
||||||
onChange={onToggleHistrogramVisibility}
|
'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>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -19,32 +19,51 @@
|
|||||||
border: 1px solid var(--bg-slate-400);
|
border: 1px solid var(--bg-slate-400);
|
||||||
background: var(--bg-ink-300);
|
background: var(--bg-ink-300);
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
border-bottom: none;
|
||||||
|
margin-bottom: -1px;
|
||||||
|
|
||||||
.prom-ql-icon {
|
.prom-ql-icon {
|
||||||
height: 14px;
|
height: 14px;
|
||||||
width: 14px;
|
width: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ant-btn {
|
.explorer-view-option {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
flex-direction: row;
|
||||||
border: none;
|
border: none;
|
||||||
padding: 9px;
|
padding: 9px;
|
||||||
box-shadow: none;
|
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 {
|
&.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 {
|
&:disabled {
|
||||||
background-color: #121317;
|
background-color: var(--bg-ink-300);
|
||||||
opacity: 0.6;
|
opacity: 0.6;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
.action-btn + .action-btn {
|
&:first-child {
|
||||||
border-left: 1px solid var(--bg-slate-400);
|
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 {
|
.lightMode {
|
||||||
.left-toolbar {
|
.left-toolbar {
|
||||||
|
.filter-btn {
|
||||||
|
border-color: var(--bg-vanilla-300) !important;
|
||||||
|
}
|
||||||
|
|
||||||
.left-toolbar-query-actions {
|
.left-toolbar-query-actions {
|
||||||
border-color: var(--bg-vanilla-300);
|
border-color: var(--bg-vanilla-300) !important;
|
||||||
background: var(--bg-vanilla-100);
|
background: var(--bg-vanilla-100) !important;
|
||||||
|
|
||||||
.ant-btn {
|
.ant-btn {
|
||||||
border-color: var(--bg-vanilla-300);
|
border-color: var(--bg-vanilla-300) !important;
|
||||||
background: var(--bg-vanilla-100);
|
background: var(--bg-vanilla-100) !important;
|
||||||
color: var(--bg-ink-200);
|
color: var(--bg-ink-200) !important;
|
||||||
|
|
||||||
&.active-tab {
|
&.active-tab {
|
||||||
background-color: var(--bg-robin-100);
|
background-color: var(--bg-robin-100) !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.loading-container {
|
.loading-container {
|
||||||
.loading-btn {
|
.loading-btn {
|
||||||
background: var(--bg-vanilla-300);
|
background: var(--bg-vanilla-300) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cancel-run {
|
.cancel-run {
|
||||||
color: var(--bg-vanilla-100);
|
color: var(--bg-vanilla-100) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cancel-run:hover {
|
.cancel-run:hover {
|
||||||
background-color: #ff7875;
|
background-color: #ff7875 !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { render, screen } from '@testing-library/react';
|
import { render, screen } from '@testing-library/react';
|
||||||
import userEvent from '@testing-library/user-event';
|
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 MockQueryClientProvider from 'providers/test/MockQueryClientProvider';
|
||||||
|
|
||||||
import LeftToolbarActions from '../LeftToolbarActions';
|
import LeftToolbarActions from '../LeftToolbarActions';
|
||||||
@@ -9,7 +9,6 @@ import RightToolbarActions from '../RightToolbarActions';
|
|||||||
describe('ToolbarActions', () => {
|
describe('ToolbarActions', () => {
|
||||||
it('LeftToolbarActions - renders correctly with default props', async () => {
|
it('LeftToolbarActions - renders correctly with default props', async () => {
|
||||||
const handleChangeSelectedView = jest.fn();
|
const handleChangeSelectedView = jest.fn();
|
||||||
const handleToggleShowFrequencyChart = jest.fn();
|
|
||||||
const { queryByTestId } = render(
|
const { queryByTestId } = render(
|
||||||
<LeftToolbarActions
|
<LeftToolbarActions
|
||||||
items={{
|
items={{
|
||||||
@@ -31,10 +30,8 @@ describe('ToolbarActions', () => {
|
|||||||
disabled: false,
|
disabled: false,
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
selectedView={SELECTED_VIEWS.SEARCH}
|
selectedView={ExplorerViews.LIST}
|
||||||
onChangeSelectedView={handleChangeSelectedView}
|
onChangeSelectedView={handleChangeSelectedView}
|
||||||
onToggleHistrogramVisibility={handleToggleShowFrequencyChart}
|
|
||||||
showFrequencyChart
|
|
||||||
showFilter
|
showFilter
|
||||||
handleFilterVisibilityChange={(): void => {}}
|
handleFilterVisibilityChange={(): void => {}}
|
||||||
/>,
|
/>,
|
||||||
@@ -77,10 +74,8 @@ describe('ToolbarActions', () => {
|
|||||||
show: true,
|
show: true,
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
selectedView={SELECTED_VIEWS.QUERY_BUILDER}
|
selectedView={ExplorerViews.TIMESERIES}
|
||||||
onChangeSelectedView={handleChangeSelectedView}
|
onChangeSelectedView={handleChangeSelectedView}
|
||||||
onToggleHistrogramVisibility={handleToggleShowFrequencyChart}
|
|
||||||
showFrequencyChart
|
|
||||||
showFilter
|
showFilter
|
||||||
handleFilterVisibilityChange={(): void => {}}
|
handleFilterVisibilityChange={(): void => {}}
|
||||||
/>,
|
/>,
|
||||||
|
|||||||
@@ -5,13 +5,13 @@ export function removePrefix(str: string, type: string): string {
|
|||||||
const resourcePrefix = `${MetricsType.Resource}_`;
|
const resourcePrefix = `${MetricsType.Resource}_`;
|
||||||
const scopePrefix = `${MetricsType.Scope}_`;
|
const scopePrefix = `${MetricsType.Scope}_`;
|
||||||
|
|
||||||
if (str.startsWith(tagPrefix)) {
|
if (str?.startsWith(tagPrefix)) {
|
||||||
return str.slice(tagPrefix.length);
|
return str.slice(tagPrefix.length);
|
||||||
}
|
}
|
||||||
if (str.startsWith(resourcePrefix)) {
|
if (str?.startsWith(resourcePrefix)) {
|
||||||
return str.slice(resourcePrefix.length);
|
return str.slice(resourcePrefix.length);
|
||||||
}
|
}
|
||||||
if (str.startsWith(scopePrefix) && type === MetricsType.Scope) {
|
if (str?.startsWith(scopePrefix) && type === MetricsType.Scope) {
|
||||||
return str.slice(scopePrefix.length);
|
return str.slice(scopePrefix.length);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ export const OperatorsSelect = memo(function OperatorsSelect({
|
|||||||
operators,
|
operators,
|
||||||
value,
|
value,
|
||||||
onChange,
|
onChange,
|
||||||
|
className,
|
||||||
...props
|
...props
|
||||||
}: OperatorsSelectProps): JSX.Element {
|
}: OperatorsSelectProps): JSX.Element {
|
||||||
return (
|
return (
|
||||||
@@ -20,6 +21,7 @@ export const OperatorsSelect = memo(function OperatorsSelect({
|
|||||||
showSearch
|
showSearch
|
||||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||||
{...props}
|
{...props}
|
||||||
|
popupClassName={className}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ export type OrderByFilterProps = {
|
|||||||
onChange: (values: OrderByPayload[]) => void;
|
onChange: (values: OrderByPayload[]) => void;
|
||||||
isListViewPanel?: boolean;
|
isListViewPanel?: boolean;
|
||||||
entityVersion?: string;
|
entityVersion?: string;
|
||||||
|
isNewQueryV2?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type OrderByFilterValue = {
|
export type OrderByFilterValue = {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { Select, Spin } from 'antd';
|
|||||||
import { useGetAggregateKeys } from 'hooks/queryBuilder/useGetAggregateKeys';
|
import { useGetAggregateKeys } from 'hooks/queryBuilder/useGetAggregateKeys';
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { DataSource, MetricAggregateOperator } from 'types/common/queryBuilder';
|
import { DataSource, MetricAggregateOperator } from 'types/common/queryBuilder';
|
||||||
|
import { getParsedAggregationOptionsForOrderBy } from 'utils/aggregationConverter';
|
||||||
import { popupContainer } from 'utils/selectPopupContainer';
|
import { popupContainer } from 'utils/selectPopupContainer';
|
||||||
|
|
||||||
import { selectStyle } from '../QueryBuilderSearch/config';
|
import { selectStyle } from '../QueryBuilderSearch/config';
|
||||||
@@ -13,6 +14,7 @@ export function OrderByFilter({
|
|||||||
onChange,
|
onChange,
|
||||||
isListViewPanel = false,
|
isListViewPanel = false,
|
||||||
entityVersion,
|
entityVersion,
|
||||||
|
isNewQueryV2 = false,
|
||||||
}: OrderByFilterProps): JSX.Element {
|
}: OrderByFilterProps): JSX.Element {
|
||||||
const {
|
const {
|
||||||
debouncedSearchText,
|
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 optionsData = useMemo(() => {
|
||||||
const keyOptions = createOptions(data?.payload?.attributeKeys || []);
|
const keyOptions = createOptions(data?.payload?.attributeKeys || []);
|
||||||
const groupByOptions = createOptions(query.groupBy);
|
const groupByOptions = createOptions(query.groupBy);
|
||||||
|
const aggregationOptionsFromParsed = createOptions(parsedAggregationOptions);
|
||||||
|
|
||||||
const options =
|
const options =
|
||||||
query.aggregateOperator === MetricAggregateOperator.NOOP
|
query.aggregateOperator === MetricAggregateOperator.NOOP
|
||||||
? keyOptions
|
? keyOptions
|
||||||
: [...groupByOptions, ...aggregationOptions];
|
: [
|
||||||
|
...groupByOptions,
|
||||||
|
...(isNewQueryV2 ? aggregationOptionsFromParsed : aggregationOptions),
|
||||||
|
];
|
||||||
|
|
||||||
return generateOptions(options);
|
return generateOptions(options);
|
||||||
}, [
|
}, [
|
||||||
aggregationOptions,
|
|
||||||
createOptions,
|
createOptions,
|
||||||
data?.payload?.attributeKeys,
|
data?.payload?.attributeKeys,
|
||||||
generateOptions,
|
|
||||||
query.aggregateOperator,
|
|
||||||
query.groupBy,
|
query.groupBy,
|
||||||
|
query.aggregateOperator,
|
||||||
|
parsedAggregationOptions,
|
||||||
|
aggregationOptions,
|
||||||
|
generateOptions,
|
||||||
|
isNewQueryV2,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const isDisabledSelect =
|
const isDisabledSelect =
|
||||||
|
|||||||
@@ -17,6 +17,7 @@
|
|||||||
.query-builder-search-v2 {
|
.query-builder-search-v2 {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 4px;
|
gap: 4px;
|
||||||
|
flex: 1;
|
||||||
|
|
||||||
.ant-select-dropdown {
|
.ant-select-dropdown {
|
||||||
padding: 0px;
|
padding: 0px;
|
||||||
|
|||||||
@@ -450,7 +450,7 @@ function QueryBuilderSearchV2(
|
|||||||
if ((event.ctrlKey || event.metaKey) && event.key === 'Enter') {
|
if ((event.ctrlKey || event.metaKey) && event.key === 'Enter') {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
handleRunQuery();
|
handleRunQuery(false, true);
|
||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -21,21 +21,23 @@ function ResourceAttributesFilter(): JSX.Element | null {
|
|||||||
|
|
||||||
// initialise tab with default query.
|
// initialise tab with default query.
|
||||||
useShareBuilderUrl({
|
useShareBuilderUrl({
|
||||||
...initialQueriesMap.traces,
|
defaultValue: {
|
||||||
builder: {
|
...initialQueriesMap.traces,
|
||||||
...initialQueriesMap.traces.builder,
|
builder: {
|
||||||
queryData: [
|
...initialQueriesMap.traces.builder,
|
||||||
{
|
queryData: [
|
||||||
...initialQueriesMap.traces.builder.queryData[0],
|
{
|
||||||
dataSource: DataSource.TRACES,
|
...initialQueriesMap.traces.builder.queryData[0],
|
||||||
aggregateOperator: 'noop',
|
dataSource: DataSource.TRACES,
|
||||||
aggregateAttribute: {
|
aggregateOperator: 'noop',
|
||||||
...initialQueriesMap.traces.builder.queryData[0].aggregateAttribute,
|
aggregateAttribute: {
|
||||||
type: 'resource',
|
...initialQueriesMap.traces.builder.queryData[0].aggregateAttribute,
|
||||||
|
type: 'resource',
|
||||||
|
},
|
||||||
|
queryName: '',
|
||||||
},
|
},
|
||||||
queryName: '',
|
],
|
||||||
},
|
},
|
||||||
],
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -33,8 +33,6 @@ import { GlobalReducer } from 'types/reducer/globalTime';
|
|||||||
import uPlot from 'uplot';
|
import uPlot from 'uplot';
|
||||||
import { getTimeRange } from 'utils/getTimeRange';
|
import { getTimeRange } from 'utils/getTimeRange';
|
||||||
|
|
||||||
import { Container } from './styles';
|
|
||||||
|
|
||||||
function TimeSeriesView({
|
function TimeSeriesView({
|
||||||
data,
|
data,
|
||||||
isLoading,
|
isLoading,
|
||||||
@@ -162,7 +160,7 @@ function TimeSeriesView({
|
|||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container>
|
<div className="time-series-view">
|
||||||
{isError && <LogsError />}
|
{isError && <LogsError />}
|
||||||
<div
|
<div
|
||||||
className="graph-container"
|
className="graph-container"
|
||||||
@@ -204,7 +202,7 @@ function TimeSeriesView({
|
|||||||
!isEmpty(chartData?.[0]) &&
|
!isEmpty(chartData?.[0]) &&
|
||||||
chartOptions && <Uplot data={chartData} options={chartOptions} />}
|
chartOptions && <Uplot data={chartData} options={chartOptions} />}
|
||||||
</div>
|
</div>
|
||||||
</Container>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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 { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
|
||||||
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||||
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
|
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
|
||||||
@@ -52,7 +54,8 @@ function TimeSeriesViewContainer({
|
|||||||
dataSource,
|
dataSource,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
ENTITY_VERSION_V4,
|
// ENTITY_VERSION_V4,
|
||||||
|
ENTITY_VERSION_V5,
|
||||||
{
|
{
|
||||||
queryKey: [
|
queryKey: [
|
||||||
REACT_QUERY_KEY.GET_QUERY_RANGE,
|
REACT_QUERY_KEY.GET_QUERY_RANGE,
|
||||||
|
|||||||
@@ -1,12 +1,23 @@
|
|||||||
.toolbar {
|
.toolbar {
|
||||||
display: grid;
|
display: flex;
|
||||||
grid-template-columns: 3fr auto auto;
|
justify-content: space-between;
|
||||||
padding: 8px 16px;
|
align-items: flex-end;
|
||||||
|
|
||||||
|
border-top: 1px solid var(--bg-slate-400);
|
||||||
|
border-bottom: 1px solid var(--bg-slate-400);
|
||||||
|
|
||||||
|
padding: 0px 8px;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
|
|
||||||
|
.rightActions {
|
||||||
|
margin: 4px 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
.timeRange {
|
.timeRange {
|
||||||
display: flex;
|
display: flex;
|
||||||
padding-right: 8px;
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 16px;
|
gap: 16px;
|
||||||
}
|
}
|
||||||
@@ -27,3 +38,10 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.lightMode {
|
||||||
|
.toolbar {
|
||||||
|
border-top: 1px solid var(--bg-vanilla-300);
|
||||||
|
border-bottom: 1px solid var(--bg-vanilla-300);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -32,14 +32,19 @@ export default function Toolbar({
|
|||||||
return (
|
return (
|
||||||
<div className="toolbar">
|
<div className="toolbar">
|
||||||
<div className="leftActions">{leftActions}</div>
|
<div className="leftActions">{leftActions}</div>
|
||||||
<div className="timeRange">
|
|
||||||
{showOldCTA && <NewExplorerCTA />}
|
<div className="rightActions">
|
||||||
<DateTimeSelectionV2
|
<div className="timeRange">
|
||||||
showAutoRefresh={showAutoRefresh}
|
{showOldCTA && <NewExplorerCTA />}
|
||||||
showRefreshText={!isLogsExplorerPage && !isApiMonitoringPage}
|
<DateTimeSelectionV2
|
||||||
/>
|
showAutoRefresh={showAutoRefresh}
|
||||||
|
showRefreshText={!isLogsExplorerPage && !isApiMonitoringPage}
|
||||||
|
hideShareModal
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{rightActions}
|
||||||
</div>
|
</div>
|
||||||
<div className="rightActions">{rightActions}</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,6 +31,10 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.refresh-text-container {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
.refresh-actions {
|
.refresh-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
@@ -309,3 +313,11 @@
|
|||||||
border-color: var(--bg-vanilla-300);
|
border-color: var(--bg-vanilla-300);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (min-width: 1400px) {
|
||||||
|
.date-time-selector {
|
||||||
|
.refresh-text-container {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -782,18 +782,22 @@ function DateTimeSelection({
|
|||||||
</Button>
|
</Button>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{showOldExplorerCTA && (
|
{showOldExplorerCTA && (
|
||||||
<div style={{ marginRight: 12 }}>
|
<div style={{ marginRight: 12 }}>
|
||||||
<NewExplorerCTA />
|
<NewExplorerCTA />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{!hasSelectedTimeError && !refreshButtonHidden && showRefreshText && (
|
{!hasSelectedTimeError && !refreshButtonHidden && showRefreshText && (
|
||||||
<RefreshText
|
<div className="refresh-text-container">
|
||||||
{...{
|
<RefreshText
|
||||||
onLastRefreshHandler,
|
{...{
|
||||||
}}
|
onLastRefreshHandler,
|
||||||
refreshButtonHidden={refreshButtonHidden}
|
}}
|
||||||
/>
|
refreshButtonHidden={refreshButtonHidden}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
<Form
|
<Form
|
||||||
form={formSelector}
|
form={formSelector}
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
.trace-explorer-controls {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,9 @@
|
|||||||
|
import './ListView.styles.scss';
|
||||||
|
|
||||||
|
import { Select } from 'antd';
|
||||||
import logEvent from 'api/common/logEvent';
|
import logEvent from 'api/common/logEvent';
|
||||||
import { ResizeTable } from 'components/ResizeTable';
|
import { ResizeTable } from 'components/ResizeTable';
|
||||||
import { ENTITY_VERSION_V4 } from 'constants/app';
|
import { ENTITY_VERSION_V5 } from 'constants/app';
|
||||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||||
import { QueryParams } from 'constants/query';
|
import { QueryParams } from 'constants/query';
|
||||||
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
|
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
|
||||||
@@ -18,8 +21,9 @@ import useDragColumns from 'hooks/useDragColumns';
|
|||||||
import { getDraggedColumns } from 'hooks/useDragColumns/utils';
|
import { getDraggedColumns } from 'hooks/useDragColumns/utils';
|
||||||
import useUrlQueryData from 'hooks/useUrlQueryData';
|
import useUrlQueryData from 'hooks/useUrlQueryData';
|
||||||
import { RowData } from 'lib/query/createTableColumnsFromQuery';
|
import { RowData } from 'lib/query/createTableColumnsFromQuery';
|
||||||
|
import { ArrowUp10, Minus } from 'lucide-react';
|
||||||
import { useTimezone } from 'providers/Timezone';
|
import { useTimezone } from 'providers/Timezone';
|
||||||
import { memo, useCallback, useEffect, useMemo } from 'react';
|
import { memo, useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
import { AppState } from 'store/reducers';
|
import { AppState } from 'store/reducers';
|
||||||
import { DataSource } from 'types/common/queryBuilder';
|
import { DataSource } from 'types/common/queryBuilder';
|
||||||
@@ -42,6 +46,8 @@ function ListView({ isFilterApplied }: ListViewProps): JSX.Element {
|
|||||||
|
|
||||||
const panelType = panelTypeFromQueryBuilder || PANEL_TYPES.LIST;
|
const panelType = panelTypeFromQueryBuilder || PANEL_TYPES.LIST;
|
||||||
|
|
||||||
|
const [orderDirection, setOrderDirection] = useState<string>('asc');
|
||||||
|
|
||||||
const {
|
const {
|
||||||
selectedTime: globalSelectedTime,
|
selectedTime: globalSelectedTime,
|
||||||
maxTime,
|
maxTime,
|
||||||
@@ -104,7 +110,8 @@ function ListView({ isFilterApplied }: ListViewProps): JSX.Element {
|
|||||||
selectColumns: options?.selectColumns,
|
selectColumns: options?.selectColumns,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
ENTITY_VERSION_V4,
|
// ENTITY_VERSION_V4,
|
||||||
|
ENTITY_VERSION_V5,
|
||||||
{
|
{
|
||||||
queryKey,
|
queryKey,
|
||||||
enabled:
|
enabled:
|
||||||
@@ -167,12 +174,32 @@ function ListView({ isFilterApplied }: ListViewProps): JSX.Element {
|
|||||||
return (
|
return (
|
||||||
<Container>
|
<Container>
|
||||||
{transformedQueryTableData.length !== 0 && (
|
{transformedQueryTableData.length !== 0 && (
|
||||||
<TraceExplorerControls
|
<div className="trace-explorer-controls">
|
||||||
isLoading={isFetching}
|
<div className="order-by-container">
|
||||||
totalCount={totalCount}
|
<div className="order-by-label">
|
||||||
config={config}
|
Order by <Minus size={14} /> <ArrowUp10 size={14} />
|
||||||
perPageOptions={PER_PAGE_OPTIONS}
|
</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>
|
||||||
|
|
||||||
|
<TraceExplorerControls
|
||||||
|
isLoading={isFetching}
|
||||||
|
totalCount={totalCount}
|
||||||
|
config={config}
|
||||||
|
perPageOptions={PER_PAGE_OPTIONS}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{isError && <ErrorText>{data?.error || 'Something went wrong'}</ErrorText>}
|
{isError && <ErrorText>{data?.error || 'Something went wrong'}</ErrorText>}
|
||||||
|
|||||||
@@ -1,19 +1,13 @@
|
|||||||
import { Button } from 'antd';
|
import { QueryBuilderV2 } from 'components/QueryBuilderV2/QueryBuilderV2';
|
||||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||||
import ExplorerOrderBy from 'container/ExplorerOrderBy';
|
import ExplorerOrderBy from 'container/ExplorerOrderBy';
|
||||||
import { QueryBuilder } from 'container/QueryBuilder';
|
|
||||||
import { OrderByFilterProps } from 'container/QueryBuilder/filters/OrderByFilter/OrderByFilter.interfaces';
|
import { OrderByFilterProps } from 'container/QueryBuilder/filters/OrderByFilter/OrderByFilter.interfaces';
|
||||||
import { QueryBuilderProps } from 'container/QueryBuilder/QueryBuilder.interfaces';
|
import { QueryBuilderProps } from 'container/QueryBuilder/QueryBuilder.interfaces';
|
||||||
import { useGetPanelTypesQueryParam } from 'hooks/queryBuilder/useGetPanelTypesQueryParam';
|
import { useGetPanelTypesQueryParam } from 'hooks/queryBuilder/useGetPanelTypesQueryParam';
|
||||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
|
||||||
import { memo, useCallback, useMemo } from 'react';
|
import { memo, useCallback, useMemo } from 'react';
|
||||||
import { DataSource } from 'types/common/queryBuilder';
|
import { DataSource } from 'types/common/queryBuilder';
|
||||||
|
|
||||||
import { ButtonWrapper, Container } from './styles';
|
|
||||||
|
|
||||||
function QuerySection(): JSX.Element {
|
function QuerySection(): JSX.Element {
|
||||||
const { handleRunQuery } = useQueryBuilder();
|
|
||||||
|
|
||||||
const panelTypes = useGetPanelTypesQueryParam(PANEL_TYPES.LIST);
|
const panelTypes = useGetPanelTypesQueryParam(PANEL_TYPES.LIST);
|
||||||
|
|
||||||
const filterConfigs: QueryBuilderProps['filterConfigs'] = useMemo(() => {
|
const filterConfigs: QueryBuilderProps['filterConfigs'] = useMemo(() => {
|
||||||
@@ -44,25 +38,19 @@ function QuerySection(): JSX.Element {
|
|||||||
}, [panelTypes, renderOrderBy]);
|
}, [panelTypes, renderOrderBy]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container>
|
<QueryBuilderV2
|
||||||
<QueryBuilder
|
isListViewPanel={
|
||||||
panelType={panelTypes}
|
panelTypes === PANEL_TYPES.LIST || panelTypes === PANEL_TYPES.TRACE
|
||||||
config={{
|
}
|
||||||
queryVariant: 'static',
|
config={{ initialDataSource: DataSource.TRACES, queryVariant: 'static' }}
|
||||||
initialDataSource: DataSource.TRACES,
|
queryComponents={queryComponents}
|
||||||
}}
|
panelType={panelTypes}
|
||||||
filterConfigs={filterConfigs}
|
filterConfigs={filterConfigs}
|
||||||
queryComponents={queryComponents}
|
showOnlyWhereClause={
|
||||||
version="v3" // setting this to v3 as we this is rendered in logs explorer
|
panelTypes === PANEL_TYPES.LIST || panelTypes === PANEL_TYPES.TRACE
|
||||||
actions={
|
}
|
||||||
<ButtonWrapper>
|
version="v3" // setting this to v3 as we this is rendered in logs explorer
|
||||||
<Button onClick={(): void => handleRunQuery()} type="primary">
|
/>
|
||||||
Run Query
|
|
||||||
</Button>
|
|
||||||
</ButtonWrapper>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</Container>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user