Compare commits
70 Commits
v0.101.0
...
feat/drill
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b23820f392 | ||
|
|
32b087d2ae | ||
|
|
bf4def51e7 | ||
|
|
4200bf0a93 | ||
|
|
2886220122 | ||
|
|
6c6793ed78 | ||
|
|
9941a0032a | ||
|
|
db1bb6b6fb | ||
|
|
43f296600d | ||
|
|
97aeaf4363 | ||
|
|
5829a9506c | ||
|
|
e642273486 | ||
|
|
da0934fafa | ||
|
|
6a3044ff65 | ||
|
|
e69bec34ed | ||
|
|
6f9f9dc843 | ||
|
|
cf247b98c8 | ||
|
|
fbbbd9883a | ||
|
|
cd7473f218 | ||
|
|
e437525185 | ||
|
|
f8dbe60aca | ||
|
|
0a5cbfadd9 | ||
|
|
ad82dcf308 | ||
|
|
af55325403 | ||
|
|
db807d5836 | ||
|
|
803aa5bb85 | ||
|
|
97b10046ee | ||
|
|
479d32d09e | ||
|
|
0809ee0518 | ||
|
|
3609cc92f7 | ||
|
|
e18a78a013 | ||
|
|
3bd867ce88 | ||
|
|
3071e331f3 | ||
|
|
46a143a747 | ||
|
|
8eec2f79ac | ||
|
|
d5f476b73d | ||
|
|
e59bd7a698 | ||
|
|
b5952fcce1 | ||
|
|
b5ab1e683b | ||
|
|
cb3cb6a55b | ||
|
|
b96fd03342 | ||
|
|
afbd177b66 | ||
|
|
885b79e581 | ||
|
|
b6fd4bf882 | ||
|
|
72f5f86f69 | ||
|
|
94179e567e | ||
|
|
7af9b1aff1 | ||
|
|
e840138d7b | ||
|
|
130ed51776 | ||
|
|
0169c7d6f8 | ||
|
|
9dd53e93e1 | ||
|
|
1fcfdf84b7 | ||
|
|
839636617d | ||
|
|
6308c17bdf | ||
|
|
4822198759 | ||
|
|
8a4f23bd4d | ||
|
|
fe097ac6d9 | ||
|
|
56a4c47127 | ||
|
|
203333729b | ||
|
|
309ace824e | ||
|
|
4544813e64 | ||
|
|
f9dcdca64d | ||
|
|
4fd917c752 | ||
|
|
5f434b7031 | ||
|
|
e98597fecb | ||
|
|
5c6a49eb90 | ||
|
|
a7170ef928 | ||
|
|
665c815e73 | ||
|
|
269ef25682 | ||
|
|
ec54e5c0b2 |
@@ -1,4 +1,5 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
|
ignorePatterns: ['src/antlr-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, HASNONE)
|
||||||
|
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",
|
||||||
|
|||||||
183
frontend/src/antlr-parser/FilterQuery.g4
Normal file
183
frontend/src/antlr-parser/FilterQuery.g4
Normal file
@@ -0,0 +1,183 @@
|
|||||||
|
grammar FilterQuery;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Parser Rules
|
||||||
|
*/
|
||||||
|
|
||||||
|
query
|
||||||
|
: expression ( (AND | OR) expression | expression )*
|
||||||
|
EOF
|
||||||
|
;
|
||||||
|
|
||||||
|
expression
|
||||||
|
: orExpression
|
||||||
|
;
|
||||||
|
|
||||||
|
orExpression
|
||||||
|
: andExpression ( OR andExpression )*
|
||||||
|
;
|
||||||
|
|
||||||
|
andExpression
|
||||||
|
: unaryExpression ( AND unaryExpression | unaryExpression )*
|
||||||
|
;
|
||||||
|
|
||||||
|
unaryExpression
|
||||||
|
: NOT? primary
|
||||||
|
;
|
||||||
|
|
||||||
|
primary
|
||||||
|
: LPAREN orExpression RPAREN
|
||||||
|
| comparison
|
||||||
|
| functionCall
|
||||||
|
| fullText
|
||||||
|
;
|
||||||
|
|
||||||
|
comparison
|
||||||
|
: key EQUALS value
|
||||||
|
| key (NOT_EQUALS | NEQ) value
|
||||||
|
| key LT value
|
||||||
|
| key LE value
|
||||||
|
| key GT value
|
||||||
|
| key GE value
|
||||||
|
| key (LIKE | ILIKE) value
|
||||||
|
| key (NOT_LIKE | NOT_ILIKE) value
|
||||||
|
| key BETWEEN value AND value
|
||||||
|
| key NOT_BETWEEN value AND value
|
||||||
|
| key inClause
|
||||||
|
| key notInClause
|
||||||
|
| key EXISTS
|
||||||
|
| key NOT_EXISTS
|
||||||
|
| key REGEXP value
|
||||||
|
| key NOT_REGEXP value
|
||||||
|
| key CONTAINS value
|
||||||
|
| key NOT_CONTAINS value
|
||||||
|
;
|
||||||
|
|
||||||
|
inClause
|
||||||
|
: IN LPAREN valueList RPAREN
|
||||||
|
| IN LBRACK valueList RBRACK
|
||||||
|
;
|
||||||
|
|
||||||
|
notInClause
|
||||||
|
: NOT_IN LPAREN valueList RPAREN
|
||||||
|
| NOT_IN LBRACK valueList RBRACK
|
||||||
|
;
|
||||||
|
|
||||||
|
valueList
|
||||||
|
: value ( COMMA value )*
|
||||||
|
;
|
||||||
|
|
||||||
|
fullText
|
||||||
|
: QUOTED_TEXT
|
||||||
|
;
|
||||||
|
|
||||||
|
functionCall
|
||||||
|
: (HAS | HASANY | HASALL | HASNONE) LPAREN functionParamList RPAREN
|
||||||
|
;
|
||||||
|
|
||||||
|
functionParamList
|
||||||
|
: functionParam ( COMMA functionParam )*
|
||||||
|
;
|
||||||
|
|
||||||
|
functionParam
|
||||||
|
: key
|
||||||
|
| value
|
||||||
|
| array
|
||||||
|
;
|
||||||
|
|
||||||
|
array
|
||||||
|
: LBRACK valueList RBRACK
|
||||||
|
;
|
||||||
|
|
||||||
|
value
|
||||||
|
: QUOTED_TEXT
|
||||||
|
| NUMBER
|
||||||
|
| BOOL
|
||||||
|
;
|
||||||
|
|
||||||
|
key
|
||||||
|
: KEY
|
||||||
|
;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Lexer Rules
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Common punctuation / symbols
|
||||||
|
LPAREN : '(' ;
|
||||||
|
RPAREN : ')' ;
|
||||||
|
LBRACK : '[' ;
|
||||||
|
RBRACK : ']' ;
|
||||||
|
COMMA : ',' ;
|
||||||
|
|
||||||
|
EQUALS : '=' | '==' ;
|
||||||
|
NOT_EQUALS : '!=' ;
|
||||||
|
NEQ : '<>' ;
|
||||||
|
LT : '<' ;
|
||||||
|
LE : '<=' ;
|
||||||
|
GT : '>' ;
|
||||||
|
GE : '>=' ;
|
||||||
|
|
||||||
|
// Multi-keyword operators
|
||||||
|
LIKE : [Ll][Ii][Kk][Ee] ;
|
||||||
|
NOT_LIKE : [Nn][Oo][Tt] [ \t]+ [Ll][Ii][Kk][Ee] ;
|
||||||
|
ILIKE : [Ii][Ll][Ii][Kk][Ee] ;
|
||||||
|
NOT_ILIKE : [Nn][Oo][Tt] [ \t]+ [Ii][Ll][Ii][Kk][Ee] ;
|
||||||
|
BETWEEN : [Bb][Ee][Tt][Ww][Ee][Ee][Nn] ;
|
||||||
|
NOT_BETWEEN : [Nn][Oo][Tt] [ \t]+ [Bb][Ee][Tt][Ww][Ee][Ee][Nn] ;
|
||||||
|
|
||||||
|
EXISTS : [Ee][Xx][Ii][Ss][Tt][Ss]? ;
|
||||||
|
NOT_EXISTS : [Nn][Oo][Tt] [ \t]+ [Ee][Xx][Ii][Ss][Tt][Ss]? ;
|
||||||
|
|
||||||
|
REGEXP : [Rr][Ee][Gg][Ee][Xx][Pp] ;
|
||||||
|
NOT_REGEXP : [Nn][Oo][Tt] [ \t]+ [Rr][Ee][Gg][Ee][Xx][Pp] ;
|
||||||
|
|
||||||
|
CONTAINS : [Cc][Oo][Nn][Tt][Aa][Ii][Nn][Ss]? ;
|
||||||
|
NOT_CONTAINS: [Nn][Oo][Tt] [ \t]+ [Cc][Oo][Nn][Tt][Aa][Ii][Nn][Ss]? ;
|
||||||
|
|
||||||
|
IN : [Ii][Nn] ;
|
||||||
|
NOT_IN : [Nn][Oo][Tt] [ \t]+ [Ii][Nn] ;
|
||||||
|
|
||||||
|
// Boolean logic
|
||||||
|
NOT : [Nn][Oo][Tt] ;
|
||||||
|
AND : [Aa][Nn][Dd] ;
|
||||||
|
OR : [Oo][Rr] ;
|
||||||
|
|
||||||
|
// Functions
|
||||||
|
HAS : [Hh][Aa][Ss] ;
|
||||||
|
HASANY : [Hh][Aa][Ss][Aa][Nn][Yy] ;
|
||||||
|
HASALL : [Hh][Aa][Ss][Aa][Ll][Ll] ;
|
||||||
|
HASNONE : [Hh][Aa][Ss][Nn][Oo][Nn][Ee] ;
|
||||||
|
|
||||||
|
// Boolean values
|
||||||
|
BOOL
|
||||||
|
: [Tt][Rr][Uu][Ee]
|
||||||
|
| [Ff][Aa][Ll][Ss][Ee]
|
||||||
|
;
|
||||||
|
|
||||||
|
// Numbers
|
||||||
|
NUMBER
|
||||||
|
: DIGIT+ ( '.' DIGIT+ )?
|
||||||
|
;
|
||||||
|
|
||||||
|
// Quoted text
|
||||||
|
QUOTED_TEXT
|
||||||
|
: ( '"' ( ~["\\] | '\\' . )* '"' // double-quoted
|
||||||
|
| '\'' ( ~['\\] | '\\' . )* '\'' // single-quoted
|
||||||
|
)
|
||||||
|
;
|
||||||
|
|
||||||
|
// Keys
|
||||||
|
KEY
|
||||||
|
: [a-zA-Z0-9_] [a-zA-Z0-9_.[\]]*
|
||||||
|
;
|
||||||
|
|
||||||
|
// Whitespace
|
||||||
|
WS
|
||||||
|
: [ \t\r\n]+ -> skip
|
||||||
|
;
|
||||||
|
|
||||||
|
// Digits fragment
|
||||||
|
fragment DIGIT
|
||||||
|
: [0-9]
|
||||||
|
;
|
||||||
104
frontend/src/antlr-parser/FilterQuery.interp
Normal file
104
frontend/src/antlr-parser/FilterQuery.interp
Normal file
File diff suppressed because one or more lines are too long
49
frontend/src/antlr-parser/FilterQuery.tokens
Normal file
49
frontend/src/antlr-parser/FilterQuery.tokens
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
LPAREN=1
|
||||||
|
RPAREN=2
|
||||||
|
LBRACK=3
|
||||||
|
RBRACK=4
|
||||||
|
COMMA=5
|
||||||
|
EQUALS=6
|
||||||
|
NOT_EQUALS=7
|
||||||
|
NEQ=8
|
||||||
|
LT=9
|
||||||
|
LE=10
|
||||||
|
GT=11
|
||||||
|
GE=12
|
||||||
|
LIKE=13
|
||||||
|
NOT_LIKE=14
|
||||||
|
ILIKE=15
|
||||||
|
NOT_ILIKE=16
|
||||||
|
BETWEEN=17
|
||||||
|
NOT_BETWEEN=18
|
||||||
|
EXISTS=19
|
||||||
|
NOT_EXISTS=20
|
||||||
|
REGEXP=21
|
||||||
|
NOT_REGEXP=22
|
||||||
|
CONTAINS=23
|
||||||
|
NOT_CONTAINS=24
|
||||||
|
IN=25
|
||||||
|
NOT_IN=26
|
||||||
|
NOT=27
|
||||||
|
AND=28
|
||||||
|
OR=29
|
||||||
|
HAS=30
|
||||||
|
HASANY=31
|
||||||
|
HASALL=32
|
||||||
|
HASNONE=33
|
||||||
|
BOOL=34
|
||||||
|
NUMBER=35
|
||||||
|
QUOTED_TEXT=36
|
||||||
|
KEY=37
|
||||||
|
WS=38
|
||||||
|
'('=1
|
||||||
|
')'=2
|
||||||
|
'['=3
|
||||||
|
']'=4
|
||||||
|
','=5
|
||||||
|
'!='=7
|
||||||
|
'<>'=8
|
||||||
|
'<'=9
|
||||||
|
'<='=10
|
||||||
|
'>'=11
|
||||||
|
'>='=12
|
||||||
132
frontend/src/antlr-parser/FilterQueryLexer.interp
Normal file
132
frontend/src/antlr-parser/FilterQueryLexer.interp
Normal file
File diff suppressed because one or more lines are too long
49
frontend/src/antlr-parser/FilterQueryLexer.tokens
Normal file
49
frontend/src/antlr-parser/FilterQueryLexer.tokens
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
LPAREN=1
|
||||||
|
RPAREN=2
|
||||||
|
LBRACK=3
|
||||||
|
RBRACK=4
|
||||||
|
COMMA=5
|
||||||
|
EQUALS=6
|
||||||
|
NOT_EQUALS=7
|
||||||
|
NEQ=8
|
||||||
|
LT=9
|
||||||
|
LE=10
|
||||||
|
GT=11
|
||||||
|
GE=12
|
||||||
|
LIKE=13
|
||||||
|
NOT_LIKE=14
|
||||||
|
ILIKE=15
|
||||||
|
NOT_ILIKE=16
|
||||||
|
BETWEEN=17
|
||||||
|
NOT_BETWEEN=18
|
||||||
|
EXISTS=19
|
||||||
|
NOT_EXISTS=20
|
||||||
|
REGEXP=21
|
||||||
|
NOT_REGEXP=22
|
||||||
|
CONTAINS=23
|
||||||
|
NOT_CONTAINS=24
|
||||||
|
IN=25
|
||||||
|
NOT_IN=26
|
||||||
|
NOT=27
|
||||||
|
AND=28
|
||||||
|
OR=29
|
||||||
|
HAS=30
|
||||||
|
HASANY=31
|
||||||
|
HASALL=32
|
||||||
|
HASNONE=33
|
||||||
|
BOOL=34
|
||||||
|
NUMBER=35
|
||||||
|
QUOTED_TEXT=36
|
||||||
|
KEY=37
|
||||||
|
WS=38
|
||||||
|
'('=1
|
||||||
|
')'=2
|
||||||
|
'['=3
|
||||||
|
']'=4
|
||||||
|
','=5
|
||||||
|
'!='=7
|
||||||
|
'<>'=8
|
||||||
|
'<'=9
|
||||||
|
'<='=10
|
||||||
|
'>'=11
|
||||||
|
'>='=12
|
||||||
249
frontend/src/antlr-parser/FilterQueryLexer.ts
Normal file
249
frontend/src/antlr-parser/FilterQueryLexer.ts
Normal file
@@ -0,0 +1,249 @@
|
|||||||
|
// Generated from src/antlr-parser/FilterQuery.g4 by ANTLR 4.13.1
|
||||||
|
// noinspection ES6UnusedImports,JSUnusedGlobalSymbols,JSUnusedLocalSymbols
|
||||||
|
import {
|
||||||
|
ATN,
|
||||||
|
ATNDeserializer,
|
||||||
|
CharStream,
|
||||||
|
DecisionState, DFA,
|
||||||
|
Lexer,
|
||||||
|
LexerATNSimulator,
|
||||||
|
RuleContext,
|
||||||
|
PredictionContextCache,
|
||||||
|
Token
|
||||||
|
} from "antlr4";
|
||||||
|
export default class FilterQueryLexer extends Lexer {
|
||||||
|
public static readonly LPAREN = 1;
|
||||||
|
public static readonly RPAREN = 2;
|
||||||
|
public static readonly LBRACK = 3;
|
||||||
|
public static readonly RBRACK = 4;
|
||||||
|
public static readonly COMMA = 5;
|
||||||
|
public static readonly EQUALS = 6;
|
||||||
|
public static readonly NOT_EQUALS = 7;
|
||||||
|
public static readonly NEQ = 8;
|
||||||
|
public static readonly LT = 9;
|
||||||
|
public static readonly LE = 10;
|
||||||
|
public static readonly GT = 11;
|
||||||
|
public static readonly GE = 12;
|
||||||
|
public static readonly LIKE = 13;
|
||||||
|
public static readonly NOT_LIKE = 14;
|
||||||
|
public static readonly ILIKE = 15;
|
||||||
|
public static readonly NOT_ILIKE = 16;
|
||||||
|
public static readonly BETWEEN = 17;
|
||||||
|
public static readonly NOT_BETWEEN = 18;
|
||||||
|
public static readonly EXISTS = 19;
|
||||||
|
public static readonly NOT_EXISTS = 20;
|
||||||
|
public static readonly REGEXP = 21;
|
||||||
|
public static readonly NOT_REGEXP = 22;
|
||||||
|
public static readonly CONTAINS = 23;
|
||||||
|
public static readonly NOT_CONTAINS = 24;
|
||||||
|
public static readonly IN = 25;
|
||||||
|
public static readonly NOT_IN = 26;
|
||||||
|
public static readonly NOT = 27;
|
||||||
|
public static readonly AND = 28;
|
||||||
|
public static readonly OR = 29;
|
||||||
|
public static readonly HAS = 30;
|
||||||
|
public static readonly HASANY = 31;
|
||||||
|
public static readonly HASALL = 32;
|
||||||
|
public static readonly HASNONE = 33;
|
||||||
|
public static readonly BOOL = 34;
|
||||||
|
public static readonly NUMBER = 35;
|
||||||
|
public static readonly QUOTED_TEXT = 36;
|
||||||
|
public static readonly KEY = 37;
|
||||||
|
public static readonly WS = 38;
|
||||||
|
public static readonly EOF = Token.EOF;
|
||||||
|
|
||||||
|
public static readonly channelNames: string[] = [ "DEFAULT_TOKEN_CHANNEL", "HIDDEN" ];
|
||||||
|
public static readonly literalNames: (string | null)[] = [ null, "'('",
|
||||||
|
"')'", "'['",
|
||||||
|
"']'", "','",
|
||||||
|
null, "'!='",
|
||||||
|
"'<>'", "'<'",
|
||||||
|
"'<='", "'>'",
|
||||||
|
"'>='" ];
|
||||||
|
public static readonly symbolicNames: (string | null)[] = [ null, "LPAREN",
|
||||||
|
"RPAREN", "LBRACK",
|
||||||
|
"RBRACK", "COMMA",
|
||||||
|
"EQUALS", "NOT_EQUALS",
|
||||||
|
"NEQ", "LT",
|
||||||
|
"LE", "GT",
|
||||||
|
"GE", "LIKE",
|
||||||
|
"NOT_LIKE",
|
||||||
|
"ILIKE", "NOT_ILIKE",
|
||||||
|
"BETWEEN",
|
||||||
|
"NOT_BETWEEN",
|
||||||
|
"EXISTS", "NOT_EXISTS",
|
||||||
|
"REGEXP", "NOT_REGEXP",
|
||||||
|
"CONTAINS",
|
||||||
|
"NOT_CONTAINS",
|
||||||
|
"IN", "NOT_IN",
|
||||||
|
"NOT", "AND",
|
||||||
|
"OR", "HAS",
|
||||||
|
"HASANY", "HASALL",
|
||||||
|
"HASNONE",
|
||||||
|
"BOOL", "NUMBER",
|
||||||
|
"QUOTED_TEXT",
|
||||||
|
"KEY", "WS" ];
|
||||||
|
public static readonly modeNames: string[] = [ "DEFAULT_MODE", ];
|
||||||
|
|
||||||
|
public static readonly ruleNames: string[] = [
|
||||||
|
"LPAREN", "RPAREN", "LBRACK", "RBRACK", "COMMA", "EQUALS", "NOT_EQUALS",
|
||||||
|
"NEQ", "LT", "LE", "GT", "GE", "LIKE", "NOT_LIKE", "ILIKE", "NOT_ILIKE",
|
||||||
|
"BETWEEN", "NOT_BETWEEN", "EXISTS", "NOT_EXISTS", "REGEXP", "NOT_REGEXP",
|
||||||
|
"CONTAINS", "NOT_CONTAINS", "IN", "NOT_IN", "NOT", "AND", "OR", "HAS",
|
||||||
|
"HASANY", "HASALL", "HASNONE", "BOOL", "NUMBER", "QUOTED_TEXT", "KEY",
|
||||||
|
"WS", "DIGIT",
|
||||||
|
];
|
||||||
|
|
||||||
|
|
||||||
|
constructor(input: CharStream) {
|
||||||
|
super(input);
|
||||||
|
this._interp = new LexerATNSimulator(this, FilterQueryLexer._ATN, FilterQueryLexer.DecisionsToDFA, new PredictionContextCache());
|
||||||
|
}
|
||||||
|
|
||||||
|
public get grammarFileName(): string { return "FilterQuery.g4"; }
|
||||||
|
|
||||||
|
public get literalNames(): (string | null)[] { return FilterQueryLexer.literalNames; }
|
||||||
|
public get symbolicNames(): (string | null)[] { return FilterQueryLexer.symbolicNames; }
|
||||||
|
public get ruleNames(): string[] { return FilterQueryLexer.ruleNames; }
|
||||||
|
|
||||||
|
public get serializedATN(): number[] { return FilterQueryLexer._serializedATN; }
|
||||||
|
|
||||||
|
public get channelNames(): string[] { return FilterQueryLexer.channelNames; }
|
||||||
|
|
||||||
|
public get modeNames(): string[] { return FilterQueryLexer.modeNames; }
|
||||||
|
|
||||||
|
public static readonly _serializedATN: number[] = [4,0,38,359,6,-1,2,0,
|
||||||
|
7,0,2,1,7,1,2,2,7,2,2,3,7,3,2,4,7,4,2,5,7,5,2,6,7,6,2,7,7,7,2,8,7,8,2,9,
|
||||||
|
7,9,2,10,7,10,2,11,7,11,2,12,7,12,2,13,7,13,2,14,7,14,2,15,7,15,2,16,7,
|
||||||
|
16,2,17,7,17,2,18,7,18,2,19,7,19,2,20,7,20,2,21,7,21,2,22,7,22,2,23,7,23,
|
||||||
|
2,24,7,24,2,25,7,25,2,26,7,26,2,27,7,27,2,28,7,28,2,29,7,29,2,30,7,30,2,
|
||||||
|
31,7,31,2,32,7,32,2,33,7,33,2,34,7,34,2,35,7,35,2,36,7,36,2,37,7,37,2,38,
|
||||||
|
7,38,1,0,1,0,1,1,1,1,1,2,1,2,1,3,1,3,1,4,1,4,1,5,1,5,1,5,3,5,93,8,5,1,6,
|
||||||
|
1,6,1,6,1,7,1,7,1,7,1,8,1,8,1,9,1,9,1,9,1,10,1,10,1,11,1,11,1,11,1,12,1,
|
||||||
|
12,1,12,1,12,1,12,1,13,1,13,1,13,1,13,4,13,120,8,13,11,13,12,13,121,1,13,
|
||||||
|
1,13,1,13,1,13,1,13,1,14,1,14,1,14,1,14,1,14,1,14,1,15,1,15,1,15,1,15,4,
|
||||||
|
15,139,8,15,11,15,12,15,140,1,15,1,15,1,15,1,15,1,15,1,15,1,16,1,16,1,16,
|
||||||
|
1,16,1,16,1,16,1,16,1,16,1,17,1,17,1,17,1,17,4,17,161,8,17,11,17,12,17,
|
||||||
|
162,1,17,1,17,1,17,1,17,1,17,1,17,1,17,1,17,1,18,1,18,1,18,1,18,1,18,1,
|
||||||
|
18,3,18,179,8,18,1,19,1,19,1,19,1,19,4,19,185,8,19,11,19,12,19,186,1,19,
|
||||||
|
1,19,1,19,1,19,1,19,1,19,3,19,195,8,19,1,20,1,20,1,20,1,20,1,20,1,20,1,
|
||||||
|
20,1,21,1,21,1,21,1,21,4,21,208,8,21,11,21,12,21,209,1,21,1,21,1,21,1,21,
|
||||||
|
1,21,1,21,1,21,1,22,1,22,1,22,1,22,1,22,1,22,1,22,1,22,3,22,227,8,22,1,
|
||||||
|
23,1,23,1,23,1,23,4,23,233,8,23,11,23,12,23,234,1,23,1,23,1,23,1,23,1,23,
|
||||||
|
1,23,1,23,1,23,3,23,245,8,23,1,24,1,24,1,24,1,25,1,25,1,25,1,25,4,25,254,
|
||||||
|
8,25,11,25,12,25,255,1,25,1,25,1,25,1,26,1,26,1,26,1,26,1,27,1,27,1,27,
|
||||||
|
1,27,1,28,1,28,1,28,1,29,1,29,1,29,1,29,1,30,1,30,1,30,1,30,1,30,1,30,1,
|
||||||
|
30,1,31,1,31,1,31,1,31,1,31,1,31,1,31,1,32,1,32,1,32,1,32,1,32,1,32,1,32,
|
||||||
|
1,32,1,33,1,33,1,33,1,33,1,33,1,33,1,33,1,33,1,33,3,33,307,8,33,1,34,4,
|
||||||
|
34,310,8,34,11,34,12,34,311,1,34,1,34,4,34,316,8,34,11,34,12,34,317,3,34,
|
||||||
|
320,8,34,1,35,1,35,1,35,1,35,5,35,326,8,35,10,35,12,35,329,9,35,1,35,1,
|
||||||
|
35,1,35,1,35,1,35,5,35,336,8,35,10,35,12,35,339,9,35,1,35,3,35,342,8,35,
|
||||||
|
1,36,1,36,5,36,346,8,36,10,36,12,36,349,9,36,1,37,4,37,352,8,37,11,37,12,
|
||||||
|
37,353,1,37,1,37,1,38,1,38,0,0,39,1,1,3,2,5,3,7,4,9,5,11,6,13,7,15,8,17,
|
||||||
|
9,19,10,21,11,23,12,25,13,27,14,29,15,31,16,33,17,35,18,37,19,39,20,41,
|
||||||
|
21,43,22,45,23,47,24,49,25,51,26,53,27,55,28,57,29,59,30,61,31,63,32,65,
|
||||||
|
33,67,34,69,35,71,36,73,37,75,38,77,0,1,0,28,2,0,76,76,108,108,2,0,73,73,
|
||||||
|
105,105,2,0,75,75,107,107,2,0,69,69,101,101,2,0,78,78,110,110,2,0,79,79,
|
||||||
|
111,111,2,0,84,84,116,116,2,0,9,9,32,32,2,0,66,66,98,98,2,0,87,87,119,119,
|
||||||
|
2,0,88,88,120,120,2,0,83,83,115,115,2,0,82,82,114,114,2,0,71,71,103,103,
|
||||||
|
2,0,80,80,112,112,2,0,67,67,99,99,2,0,65,65,97,97,2,0,68,68,100,100,2,0,
|
||||||
|
72,72,104,104,2,0,89,89,121,121,2,0,85,85,117,117,2,0,70,70,102,102,2,0,
|
||||||
|
34,34,92,92,2,0,39,39,92,92,4,0,48,57,65,90,95,95,97,122,6,0,46,46,48,57,
|
||||||
|
65,91,93,93,95,95,97,122,3,0,9,10,13,13,32,32,1,0,48,57,380,0,1,1,0,0,0,
|
||||||
|
0,3,1,0,0,0,0,5,1,0,0,0,0,7,1,0,0,0,0,9,1,0,0,0,0,11,1,0,0,0,0,13,1,0,0,
|
||||||
|
0,0,15,1,0,0,0,0,17,1,0,0,0,0,19,1,0,0,0,0,21,1,0,0,0,0,23,1,0,0,0,0,25,
|
||||||
|
1,0,0,0,0,27,1,0,0,0,0,29,1,0,0,0,0,31,1,0,0,0,0,33,1,0,0,0,0,35,1,0,0,
|
||||||
|
0,0,37,1,0,0,0,0,39,1,0,0,0,0,41,1,0,0,0,0,43,1,0,0,0,0,45,1,0,0,0,0,47,
|
||||||
|
1,0,0,0,0,49,1,0,0,0,0,51,1,0,0,0,0,53,1,0,0,0,0,55,1,0,0,0,0,57,1,0,0,
|
||||||
|
0,0,59,1,0,0,0,0,61,1,0,0,0,0,63,1,0,0,0,0,65,1,0,0,0,0,67,1,0,0,0,0,69,
|
||||||
|
1,0,0,0,0,71,1,0,0,0,0,73,1,0,0,0,0,75,1,0,0,0,1,79,1,0,0,0,3,81,1,0,0,
|
||||||
|
0,5,83,1,0,0,0,7,85,1,0,0,0,9,87,1,0,0,0,11,92,1,0,0,0,13,94,1,0,0,0,15,
|
||||||
|
97,1,0,0,0,17,100,1,0,0,0,19,102,1,0,0,0,21,105,1,0,0,0,23,107,1,0,0,0,
|
||||||
|
25,110,1,0,0,0,27,115,1,0,0,0,29,128,1,0,0,0,31,134,1,0,0,0,33,148,1,0,
|
||||||
|
0,0,35,156,1,0,0,0,37,172,1,0,0,0,39,180,1,0,0,0,41,196,1,0,0,0,43,203,
|
||||||
|
1,0,0,0,45,218,1,0,0,0,47,228,1,0,0,0,49,246,1,0,0,0,51,249,1,0,0,0,53,
|
||||||
|
260,1,0,0,0,55,264,1,0,0,0,57,268,1,0,0,0,59,271,1,0,0,0,61,275,1,0,0,0,
|
||||||
|
63,282,1,0,0,0,65,289,1,0,0,0,67,306,1,0,0,0,69,309,1,0,0,0,71,341,1,0,
|
||||||
|
0,0,73,343,1,0,0,0,75,351,1,0,0,0,77,357,1,0,0,0,79,80,5,40,0,0,80,2,1,
|
||||||
|
0,0,0,81,82,5,41,0,0,82,4,1,0,0,0,83,84,5,91,0,0,84,6,1,0,0,0,85,86,5,93,
|
||||||
|
0,0,86,8,1,0,0,0,87,88,5,44,0,0,88,10,1,0,0,0,89,93,5,61,0,0,90,91,5,61,
|
||||||
|
0,0,91,93,5,61,0,0,92,89,1,0,0,0,92,90,1,0,0,0,93,12,1,0,0,0,94,95,5,33,
|
||||||
|
0,0,95,96,5,61,0,0,96,14,1,0,0,0,97,98,5,60,0,0,98,99,5,62,0,0,99,16,1,
|
||||||
|
0,0,0,100,101,5,60,0,0,101,18,1,0,0,0,102,103,5,60,0,0,103,104,5,61,0,0,
|
||||||
|
104,20,1,0,0,0,105,106,5,62,0,0,106,22,1,0,0,0,107,108,5,62,0,0,108,109,
|
||||||
|
5,61,0,0,109,24,1,0,0,0,110,111,7,0,0,0,111,112,7,1,0,0,112,113,7,2,0,0,
|
||||||
|
113,114,7,3,0,0,114,26,1,0,0,0,115,116,7,4,0,0,116,117,7,5,0,0,117,119,
|
||||||
|
7,6,0,0,118,120,7,7,0,0,119,118,1,0,0,0,120,121,1,0,0,0,121,119,1,0,0,0,
|
||||||
|
121,122,1,0,0,0,122,123,1,0,0,0,123,124,7,0,0,0,124,125,7,1,0,0,125,126,
|
||||||
|
7,2,0,0,126,127,7,3,0,0,127,28,1,0,0,0,128,129,7,1,0,0,129,130,7,0,0,0,
|
||||||
|
130,131,7,1,0,0,131,132,7,2,0,0,132,133,7,3,0,0,133,30,1,0,0,0,134,135,
|
||||||
|
7,4,0,0,135,136,7,5,0,0,136,138,7,6,0,0,137,139,7,7,0,0,138,137,1,0,0,0,
|
||||||
|
139,140,1,0,0,0,140,138,1,0,0,0,140,141,1,0,0,0,141,142,1,0,0,0,142,143,
|
||||||
|
7,1,0,0,143,144,7,0,0,0,144,145,7,1,0,0,145,146,7,2,0,0,146,147,7,3,0,0,
|
||||||
|
147,32,1,0,0,0,148,149,7,8,0,0,149,150,7,3,0,0,150,151,7,6,0,0,151,152,
|
||||||
|
7,9,0,0,152,153,7,3,0,0,153,154,7,3,0,0,154,155,7,4,0,0,155,34,1,0,0,0,
|
||||||
|
156,157,7,4,0,0,157,158,7,5,0,0,158,160,7,6,0,0,159,161,7,7,0,0,160,159,
|
||||||
|
1,0,0,0,161,162,1,0,0,0,162,160,1,0,0,0,162,163,1,0,0,0,163,164,1,0,0,0,
|
||||||
|
164,165,7,8,0,0,165,166,7,3,0,0,166,167,7,6,0,0,167,168,7,9,0,0,168,169,
|
||||||
|
7,3,0,0,169,170,7,3,0,0,170,171,7,4,0,0,171,36,1,0,0,0,172,173,7,3,0,0,
|
||||||
|
173,174,7,10,0,0,174,175,7,1,0,0,175,176,7,11,0,0,176,178,7,6,0,0,177,179,
|
||||||
|
7,11,0,0,178,177,1,0,0,0,178,179,1,0,0,0,179,38,1,0,0,0,180,181,7,4,0,0,
|
||||||
|
181,182,7,5,0,0,182,184,7,6,0,0,183,185,7,7,0,0,184,183,1,0,0,0,185,186,
|
||||||
|
1,0,0,0,186,184,1,0,0,0,186,187,1,0,0,0,187,188,1,0,0,0,188,189,7,3,0,0,
|
||||||
|
189,190,7,10,0,0,190,191,7,1,0,0,191,192,7,11,0,0,192,194,7,6,0,0,193,195,
|
||||||
|
7,11,0,0,194,193,1,0,0,0,194,195,1,0,0,0,195,40,1,0,0,0,196,197,7,12,0,
|
||||||
|
0,197,198,7,3,0,0,198,199,7,13,0,0,199,200,7,3,0,0,200,201,7,10,0,0,201,
|
||||||
|
202,7,14,0,0,202,42,1,0,0,0,203,204,7,4,0,0,204,205,7,5,0,0,205,207,7,6,
|
||||||
|
0,0,206,208,7,7,0,0,207,206,1,0,0,0,208,209,1,0,0,0,209,207,1,0,0,0,209,
|
||||||
|
210,1,0,0,0,210,211,1,0,0,0,211,212,7,12,0,0,212,213,7,3,0,0,213,214,7,
|
||||||
|
13,0,0,214,215,7,3,0,0,215,216,7,10,0,0,216,217,7,14,0,0,217,44,1,0,0,0,
|
||||||
|
218,219,7,15,0,0,219,220,7,5,0,0,220,221,7,4,0,0,221,222,7,6,0,0,222,223,
|
||||||
|
7,16,0,0,223,224,7,1,0,0,224,226,7,4,0,0,225,227,7,11,0,0,226,225,1,0,0,
|
||||||
|
0,226,227,1,0,0,0,227,46,1,0,0,0,228,229,7,4,0,0,229,230,7,5,0,0,230,232,
|
||||||
|
7,6,0,0,231,233,7,7,0,0,232,231,1,0,0,0,233,234,1,0,0,0,234,232,1,0,0,0,
|
||||||
|
234,235,1,0,0,0,235,236,1,0,0,0,236,237,7,15,0,0,237,238,7,5,0,0,238,239,
|
||||||
|
7,4,0,0,239,240,7,6,0,0,240,241,7,16,0,0,241,242,7,1,0,0,242,244,7,4,0,
|
||||||
|
0,243,245,7,11,0,0,244,243,1,0,0,0,244,245,1,0,0,0,245,48,1,0,0,0,246,247,
|
||||||
|
7,1,0,0,247,248,7,4,0,0,248,50,1,0,0,0,249,250,7,4,0,0,250,251,7,5,0,0,
|
||||||
|
251,253,7,6,0,0,252,254,7,7,0,0,253,252,1,0,0,0,254,255,1,0,0,0,255,253,
|
||||||
|
1,0,0,0,255,256,1,0,0,0,256,257,1,0,0,0,257,258,7,1,0,0,258,259,7,4,0,0,
|
||||||
|
259,52,1,0,0,0,260,261,7,4,0,0,261,262,7,5,0,0,262,263,7,6,0,0,263,54,1,
|
||||||
|
0,0,0,264,265,7,16,0,0,265,266,7,4,0,0,266,267,7,17,0,0,267,56,1,0,0,0,
|
||||||
|
268,269,7,5,0,0,269,270,7,12,0,0,270,58,1,0,0,0,271,272,7,18,0,0,272,273,
|
||||||
|
7,16,0,0,273,274,7,11,0,0,274,60,1,0,0,0,275,276,7,18,0,0,276,277,7,16,
|
||||||
|
0,0,277,278,7,11,0,0,278,279,7,16,0,0,279,280,7,4,0,0,280,281,7,19,0,0,
|
||||||
|
281,62,1,0,0,0,282,283,7,18,0,0,283,284,7,16,0,0,284,285,7,11,0,0,285,286,
|
||||||
|
7,16,0,0,286,287,7,0,0,0,287,288,7,0,0,0,288,64,1,0,0,0,289,290,7,18,0,
|
||||||
|
0,290,291,7,16,0,0,291,292,7,11,0,0,292,293,7,4,0,0,293,294,7,5,0,0,294,
|
||||||
|
295,7,4,0,0,295,296,7,3,0,0,296,66,1,0,0,0,297,298,7,6,0,0,298,299,7,12,
|
||||||
|
0,0,299,300,7,20,0,0,300,307,7,3,0,0,301,302,7,21,0,0,302,303,7,16,0,0,
|
||||||
|
303,304,7,0,0,0,304,305,7,11,0,0,305,307,7,3,0,0,306,297,1,0,0,0,306,301,
|
||||||
|
1,0,0,0,307,68,1,0,0,0,308,310,3,77,38,0,309,308,1,0,0,0,310,311,1,0,0,
|
||||||
|
0,311,309,1,0,0,0,311,312,1,0,0,0,312,319,1,0,0,0,313,315,5,46,0,0,314,
|
||||||
|
316,3,77,38,0,315,314,1,0,0,0,316,317,1,0,0,0,317,315,1,0,0,0,317,318,1,
|
||||||
|
0,0,0,318,320,1,0,0,0,319,313,1,0,0,0,319,320,1,0,0,0,320,70,1,0,0,0,321,
|
||||||
|
327,5,34,0,0,322,326,8,22,0,0,323,324,5,92,0,0,324,326,9,0,0,0,325,322,
|
||||||
|
1,0,0,0,325,323,1,0,0,0,326,329,1,0,0,0,327,325,1,0,0,0,327,328,1,0,0,0,
|
||||||
|
328,330,1,0,0,0,329,327,1,0,0,0,330,342,5,34,0,0,331,337,5,39,0,0,332,336,
|
||||||
|
8,23,0,0,333,334,5,92,0,0,334,336,9,0,0,0,335,332,1,0,0,0,335,333,1,0,0,
|
||||||
|
0,336,339,1,0,0,0,337,335,1,0,0,0,337,338,1,0,0,0,338,340,1,0,0,0,339,337,
|
||||||
|
1,0,0,0,340,342,5,39,0,0,341,321,1,0,0,0,341,331,1,0,0,0,342,72,1,0,0,0,
|
||||||
|
343,347,7,24,0,0,344,346,7,25,0,0,345,344,1,0,0,0,346,349,1,0,0,0,347,345,
|
||||||
|
1,0,0,0,347,348,1,0,0,0,348,74,1,0,0,0,349,347,1,0,0,0,350,352,7,26,0,0,
|
||||||
|
351,350,1,0,0,0,352,353,1,0,0,0,353,351,1,0,0,0,353,354,1,0,0,0,354,355,
|
||||||
|
1,0,0,0,355,356,6,37,0,0,356,76,1,0,0,0,357,358,7,27,0,0,358,78,1,0,0,0,
|
||||||
|
24,0,92,121,140,162,178,186,194,209,226,234,244,255,306,311,317,319,325,
|
||||||
|
327,335,337,341,347,353,1,6,0,0];
|
||||||
|
|
||||||
|
private static __ATN: ATN;
|
||||||
|
public static get _ATN(): ATN {
|
||||||
|
if (!FilterQueryLexer.__ATN) {
|
||||||
|
FilterQueryLexer.__ATN = new ATNDeserializer().deserialize(FilterQueryLexer._serializedATN);
|
||||||
|
}
|
||||||
|
|
||||||
|
return FilterQueryLexer.__ATN;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static DecisionsToDFA = FilterQueryLexer._ATN.decisionToState.map( (ds: DecisionState, index: number) => new DFA(ds, index) );
|
||||||
|
}
|
||||||
201
frontend/src/antlr-parser/FilterQueryListener.ts
Normal file
201
frontend/src/antlr-parser/FilterQueryListener.ts
Normal file
@@ -0,0 +1,201 @@
|
|||||||
|
// Generated from src/antlr-parser/FilterQuery.g4 by ANTLR 4.13.1
|
||||||
|
|
||||||
|
import {ParseTreeListener} from "antlr4";
|
||||||
|
|
||||||
|
|
||||||
|
import { QueryContext } from "./FilterQueryParser";
|
||||||
|
import { ExpressionContext } from "./FilterQueryParser";
|
||||||
|
import { OrExpressionContext } from "./FilterQueryParser";
|
||||||
|
import { AndExpressionContext } from "./FilterQueryParser";
|
||||||
|
import { UnaryExpressionContext } from "./FilterQueryParser";
|
||||||
|
import { PrimaryContext } from "./FilterQueryParser";
|
||||||
|
import { ComparisonContext } from "./FilterQueryParser";
|
||||||
|
import { InClauseContext } from "./FilterQueryParser";
|
||||||
|
import { NotInClauseContext } from "./FilterQueryParser";
|
||||||
|
import { ValueListContext } from "./FilterQueryParser";
|
||||||
|
import { FullTextContext } from "./FilterQueryParser";
|
||||||
|
import { FunctionCallContext } from "./FilterQueryParser";
|
||||||
|
import { FunctionParamListContext } from "./FilterQueryParser";
|
||||||
|
import { FunctionParamContext } from "./FilterQueryParser";
|
||||||
|
import { ArrayContext } from "./FilterQueryParser";
|
||||||
|
import { ValueContext } from "./FilterQueryParser";
|
||||||
|
import { KeyContext } from "./FilterQueryParser";
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This interface defines a complete listener for a parse tree produced by
|
||||||
|
* `FilterQueryParser`.
|
||||||
|
*/
|
||||||
|
export default class FilterQueryListener extends ParseTreeListener {
|
||||||
|
/**
|
||||||
|
* Enter a parse tree produced by `FilterQueryParser.query`.
|
||||||
|
* @param ctx the parse tree
|
||||||
|
*/
|
||||||
|
enterQuery?: (ctx: QueryContext) => void;
|
||||||
|
/**
|
||||||
|
* Exit a parse tree produced by `FilterQueryParser.query`.
|
||||||
|
* @param ctx the parse tree
|
||||||
|
*/
|
||||||
|
exitQuery?: (ctx: QueryContext) => void;
|
||||||
|
/**
|
||||||
|
* Enter a parse tree produced by `FilterQueryParser.expression`.
|
||||||
|
* @param ctx the parse tree
|
||||||
|
*/
|
||||||
|
enterExpression?: (ctx: ExpressionContext) => void;
|
||||||
|
/**
|
||||||
|
* Exit a parse tree produced by `FilterQueryParser.expression`.
|
||||||
|
* @param ctx the parse tree
|
||||||
|
*/
|
||||||
|
exitExpression?: (ctx: ExpressionContext) => void;
|
||||||
|
/**
|
||||||
|
* Enter a parse tree produced by `FilterQueryParser.orExpression`.
|
||||||
|
* @param ctx the parse tree
|
||||||
|
*/
|
||||||
|
enterOrExpression?: (ctx: OrExpressionContext) => void;
|
||||||
|
/**
|
||||||
|
* Exit a parse tree produced by `FilterQueryParser.orExpression`.
|
||||||
|
* @param ctx the parse tree
|
||||||
|
*/
|
||||||
|
exitOrExpression?: (ctx: OrExpressionContext) => void;
|
||||||
|
/**
|
||||||
|
* Enter a parse tree produced by `FilterQueryParser.andExpression`.
|
||||||
|
* @param ctx the parse tree
|
||||||
|
*/
|
||||||
|
enterAndExpression?: (ctx: AndExpressionContext) => void;
|
||||||
|
/**
|
||||||
|
* Exit a parse tree produced by `FilterQueryParser.andExpression`.
|
||||||
|
* @param ctx the parse tree
|
||||||
|
*/
|
||||||
|
exitAndExpression?: (ctx: AndExpressionContext) => void;
|
||||||
|
/**
|
||||||
|
* Enter a parse tree produced by `FilterQueryParser.unaryExpression`.
|
||||||
|
* @param ctx the parse tree
|
||||||
|
*/
|
||||||
|
enterUnaryExpression?: (ctx: UnaryExpressionContext) => void;
|
||||||
|
/**
|
||||||
|
* Exit a parse tree produced by `FilterQueryParser.unaryExpression`.
|
||||||
|
* @param ctx the parse tree
|
||||||
|
*/
|
||||||
|
exitUnaryExpression?: (ctx: UnaryExpressionContext) => void;
|
||||||
|
/**
|
||||||
|
* Enter a parse tree produced by `FilterQueryParser.primary`.
|
||||||
|
* @param ctx the parse tree
|
||||||
|
*/
|
||||||
|
enterPrimary?: (ctx: PrimaryContext) => void;
|
||||||
|
/**
|
||||||
|
* Exit a parse tree produced by `FilterQueryParser.primary`.
|
||||||
|
* @param ctx the parse tree
|
||||||
|
*/
|
||||||
|
exitPrimary?: (ctx: PrimaryContext) => void;
|
||||||
|
/**
|
||||||
|
* Enter a parse tree produced by `FilterQueryParser.comparison`.
|
||||||
|
* @param ctx the parse tree
|
||||||
|
*/
|
||||||
|
enterComparison?: (ctx: ComparisonContext) => void;
|
||||||
|
/**
|
||||||
|
* Exit a parse tree produced by `FilterQueryParser.comparison`.
|
||||||
|
* @param ctx the parse tree
|
||||||
|
*/
|
||||||
|
exitComparison?: (ctx: ComparisonContext) => void;
|
||||||
|
/**
|
||||||
|
* Enter a parse tree produced by `FilterQueryParser.inClause`.
|
||||||
|
* @param ctx the parse tree
|
||||||
|
*/
|
||||||
|
enterInClause?: (ctx: InClauseContext) => void;
|
||||||
|
/**
|
||||||
|
* Exit a parse tree produced by `FilterQueryParser.inClause`.
|
||||||
|
* @param ctx the parse tree
|
||||||
|
*/
|
||||||
|
exitInClause?: (ctx: InClauseContext) => void;
|
||||||
|
/**
|
||||||
|
* Enter a parse tree produced by `FilterQueryParser.notInClause`.
|
||||||
|
* @param ctx the parse tree
|
||||||
|
*/
|
||||||
|
enterNotInClause?: (ctx: NotInClauseContext) => void;
|
||||||
|
/**
|
||||||
|
* Exit a parse tree produced by `FilterQueryParser.notInClause`.
|
||||||
|
* @param ctx the parse tree
|
||||||
|
*/
|
||||||
|
exitNotInClause?: (ctx: NotInClauseContext) => void;
|
||||||
|
/**
|
||||||
|
* Enter a parse tree produced by `FilterQueryParser.valueList`.
|
||||||
|
* @param ctx the parse tree
|
||||||
|
*/
|
||||||
|
enterValueList?: (ctx: ValueListContext) => void;
|
||||||
|
/**
|
||||||
|
* Exit a parse tree produced by `FilterQueryParser.valueList`.
|
||||||
|
* @param ctx the parse tree
|
||||||
|
*/
|
||||||
|
exitValueList?: (ctx: ValueListContext) => void;
|
||||||
|
/**
|
||||||
|
* Enter a parse tree produced by `FilterQueryParser.fullText`.
|
||||||
|
* @param ctx the parse tree
|
||||||
|
*/
|
||||||
|
enterFullText?: (ctx: FullTextContext) => void;
|
||||||
|
/**
|
||||||
|
* Exit a parse tree produced by `FilterQueryParser.fullText`.
|
||||||
|
* @param ctx the parse tree
|
||||||
|
*/
|
||||||
|
exitFullText?: (ctx: FullTextContext) => void;
|
||||||
|
/**
|
||||||
|
* Enter a parse tree produced by `FilterQueryParser.functionCall`.
|
||||||
|
* @param ctx the parse tree
|
||||||
|
*/
|
||||||
|
enterFunctionCall?: (ctx: FunctionCallContext) => void;
|
||||||
|
/**
|
||||||
|
* Exit a parse tree produced by `FilterQueryParser.functionCall`.
|
||||||
|
* @param ctx the parse tree
|
||||||
|
*/
|
||||||
|
exitFunctionCall?: (ctx: FunctionCallContext) => void;
|
||||||
|
/**
|
||||||
|
* Enter a parse tree produced by `FilterQueryParser.functionParamList`.
|
||||||
|
* @param ctx the parse tree
|
||||||
|
*/
|
||||||
|
enterFunctionParamList?: (ctx: FunctionParamListContext) => void;
|
||||||
|
/**
|
||||||
|
* Exit a parse tree produced by `FilterQueryParser.functionParamList`.
|
||||||
|
* @param ctx the parse tree
|
||||||
|
*/
|
||||||
|
exitFunctionParamList?: (ctx: FunctionParamListContext) => void;
|
||||||
|
/**
|
||||||
|
* Enter a parse tree produced by `FilterQueryParser.functionParam`.
|
||||||
|
* @param ctx the parse tree
|
||||||
|
*/
|
||||||
|
enterFunctionParam?: (ctx: FunctionParamContext) => void;
|
||||||
|
/**
|
||||||
|
* Exit a parse tree produced by `FilterQueryParser.functionParam`.
|
||||||
|
* @param ctx the parse tree
|
||||||
|
*/
|
||||||
|
exitFunctionParam?: (ctx: FunctionParamContext) => void;
|
||||||
|
/**
|
||||||
|
* Enter a parse tree produced by `FilterQueryParser.array`.
|
||||||
|
* @param ctx the parse tree
|
||||||
|
*/
|
||||||
|
enterArray?: (ctx: ArrayContext) => void;
|
||||||
|
/**
|
||||||
|
* Exit a parse tree produced by `FilterQueryParser.array`.
|
||||||
|
* @param ctx the parse tree
|
||||||
|
*/
|
||||||
|
exitArray?: (ctx: ArrayContext) => void;
|
||||||
|
/**
|
||||||
|
* Enter a parse tree produced by `FilterQueryParser.value`.
|
||||||
|
* @param ctx the parse tree
|
||||||
|
*/
|
||||||
|
enterValue?: (ctx: ValueContext) => void;
|
||||||
|
/**
|
||||||
|
* Exit a parse tree produced by `FilterQueryParser.value`.
|
||||||
|
* @param ctx the parse tree
|
||||||
|
*/
|
||||||
|
exitValue?: (ctx: ValueContext) => void;
|
||||||
|
/**
|
||||||
|
* Enter a parse tree produced by `FilterQueryParser.key`.
|
||||||
|
* @param ctx the parse tree
|
||||||
|
*/
|
||||||
|
enterKey?: (ctx: KeyContext) => void;
|
||||||
|
/**
|
||||||
|
* Exit a parse tree produced by `FilterQueryParser.key`.
|
||||||
|
* @param ctx the parse tree
|
||||||
|
*/
|
||||||
|
exitKey?: (ctx: KeyContext) => void;
|
||||||
|
}
|
||||||
|
|
||||||
1895
frontend/src/antlr-parser/FilterQueryParser.ts
Normal file
1895
frontend/src/antlr-parser/FilterQueryParser.ts
Normal file
File diff suppressed because it is too large
Load Diff
136
frontend/src/antlr-parser/FilterQueryVisitor.ts
Normal file
136
frontend/src/antlr-parser/FilterQueryVisitor.ts
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
// Generated from src/antlr-parser/FilterQuery.g4 by ANTLR 4.13.1
|
||||||
|
|
||||||
|
import {ParseTreeVisitor} from 'antlr4';
|
||||||
|
|
||||||
|
|
||||||
|
import { QueryContext } from "./FilterQueryParser";
|
||||||
|
import { ExpressionContext } from "./FilterQueryParser";
|
||||||
|
import { OrExpressionContext } from "./FilterQueryParser";
|
||||||
|
import { AndExpressionContext } from "./FilterQueryParser";
|
||||||
|
import { UnaryExpressionContext } from "./FilterQueryParser";
|
||||||
|
import { PrimaryContext } from "./FilterQueryParser";
|
||||||
|
import { ComparisonContext } from "./FilterQueryParser";
|
||||||
|
import { InClauseContext } from "./FilterQueryParser";
|
||||||
|
import { NotInClauseContext } from "./FilterQueryParser";
|
||||||
|
import { ValueListContext } from "./FilterQueryParser";
|
||||||
|
import { FullTextContext } from "./FilterQueryParser";
|
||||||
|
import { FunctionCallContext } from "./FilterQueryParser";
|
||||||
|
import { FunctionParamListContext } from "./FilterQueryParser";
|
||||||
|
import { FunctionParamContext } from "./FilterQueryParser";
|
||||||
|
import { ArrayContext } from "./FilterQueryParser";
|
||||||
|
import { ValueContext } from "./FilterQueryParser";
|
||||||
|
import { KeyContext } from "./FilterQueryParser";
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This interface defines a complete generic visitor for a parse tree produced
|
||||||
|
* by `FilterQueryParser`.
|
||||||
|
*
|
||||||
|
* @param <Result> The return type of the visit operation. Use `void` for
|
||||||
|
* operations with no return type.
|
||||||
|
*/
|
||||||
|
export default class FilterQueryVisitor<Result> extends ParseTreeVisitor<Result> {
|
||||||
|
/**
|
||||||
|
* Visit a parse tree produced by `FilterQueryParser.query`.
|
||||||
|
* @param ctx the parse tree
|
||||||
|
* @return the visitor result
|
||||||
|
*/
|
||||||
|
visitQuery?: (ctx: QueryContext) => Result;
|
||||||
|
/**
|
||||||
|
* Visit a parse tree produced by `FilterQueryParser.expression`.
|
||||||
|
* @param ctx the parse tree
|
||||||
|
* @return the visitor result
|
||||||
|
*/
|
||||||
|
visitExpression?: (ctx: ExpressionContext) => Result;
|
||||||
|
/**
|
||||||
|
* Visit a parse tree produced by `FilterQueryParser.orExpression`.
|
||||||
|
* @param ctx the parse tree
|
||||||
|
* @return the visitor result
|
||||||
|
*/
|
||||||
|
visitOrExpression?: (ctx: OrExpressionContext) => Result;
|
||||||
|
/**
|
||||||
|
* Visit a parse tree produced by `FilterQueryParser.andExpression`.
|
||||||
|
* @param ctx the parse tree
|
||||||
|
* @return the visitor result
|
||||||
|
*/
|
||||||
|
visitAndExpression?: (ctx: AndExpressionContext) => Result;
|
||||||
|
/**
|
||||||
|
* Visit a parse tree produced by `FilterQueryParser.unaryExpression`.
|
||||||
|
* @param ctx the parse tree
|
||||||
|
* @return the visitor result
|
||||||
|
*/
|
||||||
|
visitUnaryExpression?: (ctx: UnaryExpressionContext) => Result;
|
||||||
|
/**
|
||||||
|
* Visit a parse tree produced by `FilterQueryParser.primary`.
|
||||||
|
* @param ctx the parse tree
|
||||||
|
* @return the visitor result
|
||||||
|
*/
|
||||||
|
visitPrimary?: (ctx: PrimaryContext) => Result;
|
||||||
|
/**
|
||||||
|
* Visit a parse tree produced by `FilterQueryParser.comparison`.
|
||||||
|
* @param ctx the parse tree
|
||||||
|
* @return the visitor result
|
||||||
|
*/
|
||||||
|
visitComparison?: (ctx: ComparisonContext) => Result;
|
||||||
|
/**
|
||||||
|
* Visit a parse tree produced by `FilterQueryParser.inClause`.
|
||||||
|
* @param ctx the parse tree
|
||||||
|
* @return the visitor result
|
||||||
|
*/
|
||||||
|
visitInClause?: (ctx: InClauseContext) => Result;
|
||||||
|
/**
|
||||||
|
* Visit a parse tree produced by `FilterQueryParser.notInClause`.
|
||||||
|
* @param ctx the parse tree
|
||||||
|
* @return the visitor result
|
||||||
|
*/
|
||||||
|
visitNotInClause?: (ctx: NotInClauseContext) => Result;
|
||||||
|
/**
|
||||||
|
* Visit a parse tree produced by `FilterQueryParser.valueList`.
|
||||||
|
* @param ctx the parse tree
|
||||||
|
* @return the visitor result
|
||||||
|
*/
|
||||||
|
visitValueList?: (ctx: ValueListContext) => Result;
|
||||||
|
/**
|
||||||
|
* Visit a parse tree produced by `FilterQueryParser.fullText`.
|
||||||
|
* @param ctx the parse tree
|
||||||
|
* @return the visitor result
|
||||||
|
*/
|
||||||
|
visitFullText?: (ctx: FullTextContext) => Result;
|
||||||
|
/**
|
||||||
|
* Visit a parse tree produced by `FilterQueryParser.functionCall`.
|
||||||
|
* @param ctx the parse tree
|
||||||
|
* @return the visitor result
|
||||||
|
*/
|
||||||
|
visitFunctionCall?: (ctx: FunctionCallContext) => Result;
|
||||||
|
/**
|
||||||
|
* Visit a parse tree produced by `FilterQueryParser.functionParamList`.
|
||||||
|
* @param ctx the parse tree
|
||||||
|
* @return the visitor result
|
||||||
|
*/
|
||||||
|
visitFunctionParamList?: (ctx: FunctionParamListContext) => Result;
|
||||||
|
/**
|
||||||
|
* Visit a parse tree produced by `FilterQueryParser.functionParam`.
|
||||||
|
* @param ctx the parse tree
|
||||||
|
* @return the visitor result
|
||||||
|
*/
|
||||||
|
visitFunctionParam?: (ctx: FunctionParamContext) => Result;
|
||||||
|
/**
|
||||||
|
* Visit a parse tree produced by `FilterQueryParser.array`.
|
||||||
|
* @param ctx the parse tree
|
||||||
|
* @return the visitor result
|
||||||
|
*/
|
||||||
|
visitArray?: (ctx: ArrayContext) => Result;
|
||||||
|
/**
|
||||||
|
* Visit a parse tree produced by `FilterQueryParser.value`.
|
||||||
|
* @param ctx the parse tree
|
||||||
|
* @return the visitor result
|
||||||
|
*/
|
||||||
|
visitValue?: (ctx: ValueContext) => Result;
|
||||||
|
/**
|
||||||
|
* Visit a parse tree produced by `FilterQueryParser.key`.
|
||||||
|
* @param ctx the parse tree
|
||||||
|
* @return the visitor result
|
||||||
|
*/
|
||||||
|
visitKey?: (ctx: KeyContext) => Result;
|
||||||
|
}
|
||||||
|
|
||||||
@@ -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;
|
||||||
358
frontend/src/api/v5/queryRange/convertV5Response.ts
Normal file
358
frontend/src/api/v5/queryRange/convertV5Response.ts
Normal file
@@ -0,0 +1,358 @@
|
|||||||
|
import { isEmpty } from 'lodash-es';
|
||||||
|
import { SuccessResponse } from 'types/api';
|
||||||
|
import { MetricRangePayloadV3 } from 'types/api/metrics/getQueryRange';
|
||||||
|
import {
|
||||||
|
DistributionData,
|
||||||
|
MetricRangePayloadV5,
|
||||||
|
RawData,
|
||||||
|
ScalarData,
|
||||||
|
TimeSeriesData,
|
||||||
|
} from 'types/api/v5/queryRange';
|
||||||
|
import { QueryDataV3 } from 'types/api/widgets/getQuery';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts V5 TimeSeriesData to legacy format
|
||||||
|
*/
|
||||||
|
function convertTimeSeriesData(
|
||||||
|
timeSeriesData: TimeSeriesData,
|
||||||
|
legendMap: Record<string, string>,
|
||||||
|
): QueryDataV3 {
|
||||||
|
// Convert V5 time series format to legacy QueryDataV3 format
|
||||||
|
return {
|
||||||
|
queryName: timeSeriesData.queryName,
|
||||||
|
legend: legendMap[timeSeriesData.queryName] || timeSeriesData.queryName,
|
||||||
|
series: timeSeriesData?.aggregations?.flatMap((aggregation) =>
|
||||||
|
aggregation.series.map((series) => ({
|
||||||
|
labels: series.labels
|
||||||
|
? Object.fromEntries(
|
||||||
|
series.labels.map((label) => [label.key.name, label.value]),
|
||||||
|
)
|
||||||
|
: {},
|
||||||
|
labelsArray: series.labels
|
||||||
|
? series.labels.map((label) => ({ [label.key.name]: label.value }))
|
||||||
|
: [],
|
||||||
|
values: series.values.map((value) => ({
|
||||||
|
timestamp: value.timestamp,
|
||||||
|
value: String(value.value),
|
||||||
|
})),
|
||||||
|
})),
|
||||||
|
),
|
||||||
|
list: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper function to collect columns from scalar data
|
||||||
|
*/
|
||||||
|
function collectColumnsFromScalarData(
|
||||||
|
scalarData: ScalarData[],
|
||||||
|
): { name: string; queryName: string; isValueColumn: boolean }[] {
|
||||||
|
const columnMap = new Map<
|
||||||
|
string,
|
||||||
|
{ name: string; queryName: string; isValueColumn: boolean }
|
||||||
|
>();
|
||||||
|
|
||||||
|
scalarData.forEach((scalar) => {
|
||||||
|
scalar.columns.forEach((col) => {
|
||||||
|
if (col.columnType === 'group') {
|
||||||
|
// For group columns, use the column name as-is
|
||||||
|
const key = `${col.name}_group`;
|
||||||
|
if (!columnMap.has(key)) {
|
||||||
|
columnMap.set(key, {
|
||||||
|
name: col.name,
|
||||||
|
queryName: '', // Group columns don't have query names
|
||||||
|
isValueColumn: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else if (col.columnType === 'aggregation') {
|
||||||
|
// For aggregation columns, use the query name as the column name
|
||||||
|
const key = `${col.queryName}_aggregation`;
|
||||||
|
if (!columnMap.has(key)) {
|
||||||
|
columnMap.set(key, {
|
||||||
|
name: col.queryName, // Use query name as column name (A, B, etc.)
|
||||||
|
queryName: col.queryName,
|
||||||
|
isValueColumn: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return Array.from(columnMap.values()).sort((a, b) => {
|
||||||
|
if (a.isValueColumn !== b.isValueColumn) {
|
||||||
|
return a.isValueColumn ? 1 : -1;
|
||||||
|
}
|
||||||
|
return a.name.localeCompare(b.name);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper function to process scalar data rows with unified table structure
|
||||||
|
*/
|
||||||
|
function processScalarDataRows(
|
||||||
|
scalarData: ScalarData[],
|
||||||
|
): { data: Record<string, any> }[] {
|
||||||
|
// First, identify all group columns and all value columns
|
||||||
|
const allGroupColumns = new Set<string>();
|
||||||
|
const allValueColumns = new Set<string>();
|
||||||
|
|
||||||
|
scalarData.forEach((scalar) => {
|
||||||
|
scalar.columns.forEach((col) => {
|
||||||
|
if (col.columnType === 'group') {
|
||||||
|
allGroupColumns.add(col.name);
|
||||||
|
} else if (col.columnType === 'aggregation') {
|
||||||
|
// Use query name for value columns to match expected format
|
||||||
|
allValueColumns.add(col.queryName);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create a unified row structure
|
||||||
|
const unifiedRows = new Map<string, Record<string, any>>();
|
||||||
|
|
||||||
|
// Process each scalar result
|
||||||
|
scalarData.forEach((scalar) => {
|
||||||
|
scalar.data.forEach((dataRow) => {
|
||||||
|
const groupColumns = scalar.columns.filter(
|
||||||
|
(col) => col.columnType === 'group',
|
||||||
|
);
|
||||||
|
|
||||||
|
// Create row key based on group columns
|
||||||
|
let rowKey: string;
|
||||||
|
const groupValues: Record<string, any> = {};
|
||||||
|
|
||||||
|
if (groupColumns.length > 0) {
|
||||||
|
const keyParts: string[] = [];
|
||||||
|
groupColumns.forEach((col, index) => {
|
||||||
|
const value = dataRow[index];
|
||||||
|
keyParts.push(String(value));
|
||||||
|
groupValues[col.name] = value;
|
||||||
|
});
|
||||||
|
rowKey = keyParts.join('|');
|
||||||
|
} else {
|
||||||
|
// For scalar values without grouping, create a default row
|
||||||
|
rowKey = 'default_row';
|
||||||
|
// Set all group columns to 'n/a' for this row
|
||||||
|
Array.from(allGroupColumns).forEach((groupCol) => {
|
||||||
|
groupValues[groupCol] = 'n/a';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get or create the unified row
|
||||||
|
if (!unifiedRows.has(rowKey)) {
|
||||||
|
const newRow: Record<string, any> = { ...groupValues };
|
||||||
|
// Initialize all value columns to 'n/a'
|
||||||
|
Array.from(allValueColumns).forEach((valueCol) => {
|
||||||
|
newRow[valueCol] = 'n/a';
|
||||||
|
});
|
||||||
|
unifiedRows.set(rowKey, newRow);
|
||||||
|
}
|
||||||
|
|
||||||
|
const row = unifiedRows.get(rowKey)!;
|
||||||
|
|
||||||
|
// Fill in the aggregation values using query name as column name
|
||||||
|
scalar.columns.forEach((col, colIndex) => {
|
||||||
|
if (col.columnType === 'aggregation') {
|
||||||
|
row[col.queryName] = dataRow[colIndex];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return Array.from(unifiedRows.values()).map((rowData) => ({
|
||||||
|
data: rowData,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts V5 ScalarData array to legacy format with table structure
|
||||||
|
*/
|
||||||
|
function convertScalarDataArrayToTable(
|
||||||
|
scalarDataArray: ScalarData[],
|
||||||
|
legendMap: Record<string, string>,
|
||||||
|
): QueryDataV3 {
|
||||||
|
// If no scalar data, return empty structure
|
||||||
|
if (!scalarDataArray || scalarDataArray.length === 0) {
|
||||||
|
return {
|
||||||
|
queryName: '',
|
||||||
|
legend: '',
|
||||||
|
series: null,
|
||||||
|
list: null,
|
||||||
|
table: {
|
||||||
|
columns: [],
|
||||||
|
rows: [],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Collect columns and process rows
|
||||||
|
const columns = collectColumnsFromScalarData(scalarDataArray);
|
||||||
|
const rows = processScalarDataRows(scalarDataArray);
|
||||||
|
|
||||||
|
// Get the primary query name
|
||||||
|
const primaryQuery = scalarDataArray.find((s) =>
|
||||||
|
s.columns.some((c) => c.columnType === 'aggregation'),
|
||||||
|
);
|
||||||
|
const queryName =
|
||||||
|
primaryQuery?.columns.find((c) => c.columnType === 'aggregation')
|
||||||
|
?.queryName ||
|
||||||
|
scalarDataArray[0]?.columns[0]?.queryName ||
|
||||||
|
'';
|
||||||
|
|
||||||
|
return {
|
||||||
|
queryName,
|
||||||
|
legend: legendMap[queryName] || queryName,
|
||||||
|
series: null,
|
||||||
|
list: null,
|
||||||
|
table: {
|
||||||
|
columns,
|
||||||
|
rows,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts V5 RawData to legacy format
|
||||||
|
*/
|
||||||
|
function convertRawData(
|
||||||
|
rawData: RawData,
|
||||||
|
legendMap: Record<string, string>,
|
||||||
|
): QueryDataV3 {
|
||||||
|
// Convert V5 raw format to legacy QueryDataV3 format
|
||||||
|
return {
|
||||||
|
queryName: rawData.queryName,
|
||||||
|
legend: legendMap[rawData.queryName] || rawData.queryName,
|
||||||
|
series: null,
|
||||||
|
list: rawData.rows?.map((row) => ({
|
||||||
|
timestamp: row.timestamp,
|
||||||
|
data: {
|
||||||
|
// Map raw data to ILog structure - spread row.data first to include all properties
|
||||||
|
...row.data,
|
||||||
|
date: row.timestamp,
|
||||||
|
} as any,
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts V5 DistributionData to legacy format
|
||||||
|
*/
|
||||||
|
function convertDistributionData(
|
||||||
|
distributionData: DistributionData,
|
||||||
|
legendMap: Record<string, string>,
|
||||||
|
): any {
|
||||||
|
// eslint-disable-line @typescript-eslint/no-explicit-any
|
||||||
|
// Convert V5 distribution format to legacy histogram format
|
||||||
|
return {
|
||||||
|
...distributionData,
|
||||||
|
legendMap,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper function to convert V5 data based on type
|
||||||
|
*/
|
||||||
|
function convertV5DataByType(
|
||||||
|
v5Data: any,
|
||||||
|
legendMap: Record<string, string>,
|
||||||
|
): MetricRangePayloadV3['data'] {
|
||||||
|
switch (v5Data?.type) {
|
||||||
|
case 'time_series': {
|
||||||
|
const timeSeriesData = v5Data.data.results as TimeSeriesData[];
|
||||||
|
return {
|
||||||
|
resultType: 'time_series',
|
||||||
|
result: timeSeriesData.map((timeSeries) =>
|
||||||
|
convertTimeSeriesData(timeSeries, legendMap),
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
case 'scalar': {
|
||||||
|
const scalarData = v5Data.data.results as ScalarData[];
|
||||||
|
// For scalar data, combine all results into a single table
|
||||||
|
const combinedTable = convertScalarDataArrayToTable(scalarData, legendMap);
|
||||||
|
return {
|
||||||
|
resultType: 'scalar',
|
||||||
|
result: [combinedTable],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
case 'raw': {
|
||||||
|
const rawData = v5Data.data.results as RawData[];
|
||||||
|
return {
|
||||||
|
resultType: 'raw',
|
||||||
|
result: rawData.map((raw) => convertRawData(raw, legendMap)),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
case 'distribution': {
|
||||||
|
const distributionData = v5Data.data.results as DistributionData[];
|
||||||
|
return {
|
||||||
|
resultType: 'distribution',
|
||||||
|
result: distributionData.map((distribution) =>
|
||||||
|
convertDistributionData(distribution, legendMap),
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return {
|
||||||
|
resultType: '',
|
||||||
|
result: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts V5 API response to legacy format expected by frontend components
|
||||||
|
*/
|
||||||
|
export function convertV5ResponseToLegacy(
|
||||||
|
v5Response: SuccessResponse<MetricRangePayloadV5>,
|
||||||
|
legendMap: Record<string, string>,
|
||||||
|
// formatForWeb?: boolean,
|
||||||
|
): SuccessResponse<MetricRangePayloadV3> {
|
||||||
|
const { payload } = v5Response;
|
||||||
|
const v5Data = payload?.data;
|
||||||
|
|
||||||
|
// todo - sagar
|
||||||
|
// If formatForWeb is true, return as-is (like existing logic)
|
||||||
|
// Exception: scalar data should always be converted to table format
|
||||||
|
// if (formatForWeb && v5Data?.type !== 'scalar') {
|
||||||
|
// return v5Response as any;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// Convert based on V5 response type
|
||||||
|
const convertedData = convertV5DataByType(v5Data, legendMap);
|
||||||
|
|
||||||
|
// Create legacy-compatible response structure
|
||||||
|
const legacyResponse: SuccessResponse<MetricRangePayloadV3> = {
|
||||||
|
...v5Response,
|
||||||
|
payload: {
|
||||||
|
data: convertedData,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Apply legend mapping (similar to existing logic)
|
||||||
|
if (legacyResponse.payload?.data?.result) {
|
||||||
|
legacyResponse.payload.data.result = legacyResponse.payload.data.result.map(
|
||||||
|
(queryData: any) => {
|
||||||
|
// eslint-disable-line @typescript-eslint/no-explicit-any
|
||||||
|
const newQueryData = queryData;
|
||||||
|
newQueryData.legend = legendMap[queryData.queryName];
|
||||||
|
|
||||||
|
// If metric names is an empty object
|
||||||
|
if (isEmpty(queryData.metric)) {
|
||||||
|
// If metrics list is empty && the user haven't defined a legend then add the legend equal to the name of the query.
|
||||||
|
if (!newQueryData.legend) {
|
||||||
|
newQueryData.legend = queryData.queryName;
|
||||||
|
}
|
||||||
|
// If name of the query and the legend if inserted is same then add the same to the metrics object.
|
||||||
|
if (queryData.queryName === newQueryData.legend) {
|
||||||
|
newQueryData.metric = newQueryData.metric || {};
|
||||||
|
newQueryData.metric[queryData.queryName] = queryData.queryName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return newQueryData;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return legacyResponse;
|
||||||
|
}
|
||||||
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;
|
||||||
387
frontend/src/api/v5/queryRange/prepareQueryRangePayloadV5.ts
Normal file
387
frontend/src/api/v5/queryRange/prepareQueryRangePayloadV5.ts
Normal file
@@ -0,0 +1,387 @@
|
|||||||
|
/* eslint-disable sonarjs/cognitive-complexity */
|
||||||
|
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||||
|
import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults';
|
||||||
|
import getStartEndRangeTime from 'lib/getStartEndRangeTime';
|
||||||
|
import { mapQueryDataToApi } from 'lib/newQueryBuilder/queryBuilderMappers/mapQueryDataToApi';
|
||||||
|
import { isEmpty } from 'lodash-es';
|
||||||
|
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||||
|
import {
|
||||||
|
IBuilderQuery,
|
||||||
|
QueryFunctionProps,
|
||||||
|
} from 'types/api/queryBuilder/queryBuilderData';
|
||||||
|
import {
|
||||||
|
BaseBuilderQuery,
|
||||||
|
FieldContext,
|
||||||
|
FieldDataType,
|
||||||
|
FunctionName,
|
||||||
|
GroupByKey,
|
||||||
|
LogAggregation,
|
||||||
|
MetricAggregation,
|
||||||
|
OrderBy,
|
||||||
|
QueryEnvelope,
|
||||||
|
QueryFunction,
|
||||||
|
QueryRangePayloadV5,
|
||||||
|
QueryType,
|
||||||
|
RequestType,
|
||||||
|
TelemetryFieldKey,
|
||||||
|
TraceAggregation,
|
||||||
|
} from 'types/api/v5/queryRange';
|
||||||
|
import { EQueryType } from 'types/common/dashboard';
|
||||||
|
import { DataSource } from 'types/common/queryBuilder';
|
||||||
|
|
||||||
|
type PrepareQueryRangePayloadV5Result = {
|
||||||
|
queryPayload: QueryRangePayloadV5;
|
||||||
|
legendMap: Record<string, string>;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maps panel types to V5 request types
|
||||||
|
*/
|
||||||
|
function mapPanelTypeToRequestType(panelType: PANEL_TYPES): RequestType {
|
||||||
|
switch (panelType) {
|
||||||
|
case PANEL_TYPES.TIME_SERIES:
|
||||||
|
case PANEL_TYPES.BAR:
|
||||||
|
return 'time_series';
|
||||||
|
case PANEL_TYPES.TABLE:
|
||||||
|
case PANEL_TYPES.PIE:
|
||||||
|
case PANEL_TYPES.VALUE:
|
||||||
|
case PANEL_TYPES.TRACE:
|
||||||
|
return 'scalar';
|
||||||
|
case PANEL_TYPES.LIST:
|
||||||
|
return 'raw';
|
||||||
|
case PANEL_TYPES.HISTOGRAM:
|
||||||
|
return 'distribution';
|
||||||
|
default:
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets signal type from data source
|
||||||
|
*/
|
||||||
|
function getSignalType(dataSource: string): 'traces' | 'logs' | 'metrics' {
|
||||||
|
if (dataSource === 'traces') return 'traces';
|
||||||
|
if (dataSource === 'logs') return 'logs';
|
||||||
|
return 'metrics';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates base spec for builder queries
|
||||||
|
*/
|
||||||
|
function createBaseSpec(
|
||||||
|
queryData: IBuilderQuery,
|
||||||
|
requestType: RequestType,
|
||||||
|
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,
|
||||||
|
}: GetQueryResultsProps): PrepareQueryRangePayloadV5Result => {
|
||||||
|
let legendMap: Record<string, string> = {};
|
||||||
|
const requestType = mapPanelTypeToRequestType(graphType);
|
||||||
|
let queries: QueryEnvelope[] = [];
|
||||||
|
|
||||||
|
console.log('query', query);
|
||||||
|
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
variables,
|
||||||
|
};
|
||||||
|
|
||||||
|
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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,359 @@
|
|||||||
|
/* 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 ' },
|
||||||
|
];
|
||||||
|
|
||||||
|
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 || '',
|
||||||
|
);
|
||||||
|
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 }),
|
||||||
|
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,73 @@
|
|||||||
|
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 : '60'}
|
||||||
|
className="query-aggregation-interval-input"
|
||||||
|
label="Seconds"
|
||||||
|
placeholder="60"
|
||||||
|
type="number"
|
||||||
|
onChange={handleAggregationIntervalChange}
|
||||||
|
labelAfter
|
||||||
|
onClose={(): void => {}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
QueryAggregationOptions.defaultProps = {
|
||||||
|
panelType: null,
|
||||||
|
onChange: undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default QueryAggregationOptions;
|
||||||
@@ -0,0 +1,498 @@
|
|||||||
|
/* 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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(' ') || '',
|
||||||
|
);
|
||||||
|
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,
|
||||||
|
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',
|
||||||
|
},
|
||||||
|
];
|
||||||
275
frontend/src/components/QueryBuilderV2/QueryV2/QueryV2.tsx
Normal file
275
frontend/src/components/QueryBuilderV2/QueryV2/QueryV2.tsx
Normal file
@@ -0,0 +1,275 @@
|
|||||||
|
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, useEffect, 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 {
|
||||||
|
convertAggregationToExpression,
|
||||||
|
convertFiltersToExpression,
|
||||||
|
convertHavingToExpression,
|
||||||
|
} from '../utils';
|
||||||
|
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,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Convert old format to new format and update query when component mounts or query changes
|
||||||
|
const performQueryConversions = useCallback(() => {
|
||||||
|
// Convert filters if needed
|
||||||
|
if (query.filters?.items?.length > 0 && !query.filter?.expression) {
|
||||||
|
const convertedFilter = convertFiltersToExpression(query.filters);
|
||||||
|
handleChangeQueryData('filter', convertedFilter);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert having if needed
|
||||||
|
if (query.having?.length > 0 && !query.havingExpression?.expression) {
|
||||||
|
const convertedHaving = convertHavingToExpression(query.having);
|
||||||
|
handleChangeQueryData('havingExpression', convertedHaving);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert aggregation if needed
|
||||||
|
if (!query.aggregations && query.aggregateOperator) {
|
||||||
|
const convertedAggregation = convertAggregationToExpression(
|
||||||
|
query.aggregateOperator,
|
||||||
|
query.aggregateAttribute,
|
||||||
|
query.dataSource,
|
||||||
|
query.timeAggregation,
|
||||||
|
query.spaceAggregation,
|
||||||
|
) as any; // Type assertion to handle union type
|
||||||
|
handleChangeQueryData('aggregations', convertedAggregation);
|
||||||
|
}
|
||||||
|
}, [query, handleChangeQueryData]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const needsConversion =
|
||||||
|
(query.filters?.items?.length > 0 && !query.filter?.expression) ||
|
||||||
|
(query.having?.length > 0 && !query.havingExpression?.expression) ||
|
||||||
|
(!query.aggregations && query.aggregateOperator);
|
||||||
|
|
||||||
|
if (needsConversion) {
|
||||||
|
performQueryConversions();
|
||||||
|
}
|
||||||
|
}, [performQueryConversions, query]);
|
||||||
|
|
||||||
|
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>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="qb-elements-container">
|
||||||
|
<div className="qb-search-container">
|
||||||
|
{dataSource === DataSource.METRICS && (
|
||||||
|
<div className="metrics-select-container">
|
||||||
|
<MetricsSelect query={query} index={0} 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 queryName={query.queryName} />
|
||||||
|
</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={0}
|
||||||
|
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',
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
@@ -41,14 +39,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 {
|
import {
|
||||||
CSSProperties,
|
CSSProperties,
|
||||||
@@ -73,10 +64,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];
|
||||||
@@ -241,12 +230,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
|
||||||
@@ -552,13 +535,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 [
|
||||||
@@ -610,27 +586,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}`;
|
||||||
@@ -643,11 +598,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
|
||||||
@@ -676,12 +630,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,
|
||||||
@@ -695,14 +648,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
|
||||||
@@ -734,14 +684,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]);
|
||||||
|
|
||||||
@@ -760,41 +707,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 && (
|
||||||
<>
|
|
||||||
<Divider
|
|
||||||
type="vertical"
|
|
||||||
className={isEditDeleteSupported ? '' : 'hidden'}
|
|
||||||
/>
|
|
||||||
<Tooltip title="Update this view" placement="top">
|
<Tooltip title="Update this view" placement="top">
|
||||||
<Button
|
<Button
|
||||||
className={cx('action-icon', isEditDeleteSupported ? ' ' : 'hidden')}
|
className={cx(
|
||||||
|
'periscope-btn ghost',
|
||||||
|
isEditDeleteSupported ? '' : 'hidden',
|
||||||
|
)}
|
||||||
disabled={isViewUpdating}
|
disabled={isViewUpdating}
|
||||||
onClick={onUpdateQueryHandler}
|
onClick={onUpdateQueryHandler}
|
||||||
icon={<Disc3 size={14} />}
|
icon={<Disc3 size={16} />}
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</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
|
||||||
@@ -835,49 +772,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,7 +2,7 @@ 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';
|
||||||
@@ -144,7 +144,8 @@ function ChartPreview({
|
|||||||
},
|
},
|
||||||
originalGraphType: graphType,
|
originalGraphType: graphType,
|
||||||
},
|
},
|
||||||
alertDef?.version || DEFAULT_ENTITY_VERSION,
|
// alertDef?.version || DEFAULT_ENTITY_VERSION,
|
||||||
|
ENTITY_VERSION_V5,
|
||||||
{
|
{
|
||||||
queryKey: [
|
queryKey: [
|
||||||
'chartPreview',
|
'chartPreview',
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -4,7 +4,9 @@
|
|||||||
overflow-y: hidden;
|
overflow-y: hidden;
|
||||||
|
|
||||||
.full-view-header-container {
|
.full-view-header-container {
|
||||||
height: 40px;
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.graph-container {
|
.graph-container {
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
/* eslint-disable sonarjs/cognitive-complexity */
|
||||||
import './WidgetFullView.styles.scss';
|
import './WidgetFullView.styles.scss';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -8,11 +9,14 @@ import {
|
|||||||
import { Button, Input, Spin } from 'antd';
|
import { Button, Input, Spin } from 'antd';
|
||||||
import cx from 'classnames';
|
import cx from 'classnames';
|
||||||
import { ToggleGraphProps } from 'components/Graph/types';
|
import { ToggleGraphProps } from 'components/Graph/types';
|
||||||
|
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
|
||||||
|
import { QueryBuilderV2 } from 'components/QueryBuilderV2/QueryBuilderV2';
|
||||||
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 useDrilldown from 'container/GridCardLayout/GridCard/FullView/useDrilldown';
|
||||||
import {
|
import {
|
||||||
timeItems,
|
timeItems,
|
||||||
timePreferance,
|
timePreferance,
|
||||||
@@ -51,6 +55,7 @@ function FullView({
|
|||||||
onClickHandler,
|
onClickHandler,
|
||||||
customOnDragSelect,
|
customOnDragSelect,
|
||||||
setCurrentGraphRef,
|
setCurrentGraphRef,
|
||||||
|
enableDrillDown = false,
|
||||||
}: FullViewProps): JSX.Element {
|
}: FullViewProps): JSX.Element {
|
||||||
const { safeNavigate } = useSafeNavigate();
|
const { safeNavigate } = useSafeNavigate();
|
||||||
const { selectedTime: globalSelectedTime } = useSelector<
|
const { selectedTime: globalSelectedTime } = useSelector<
|
||||||
@@ -113,6 +118,13 @@ function FullView({
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { dashboardEditView } = useDrilldown({
|
||||||
|
enableDrillDown,
|
||||||
|
widget,
|
||||||
|
setRequestData,
|
||||||
|
selectedDashboard,
|
||||||
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setRequestData((prev) => ({
|
setRequestData((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
@@ -122,7 +134,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,
|
||||||
@@ -196,9 +209,23 @@ function FullView({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="full-view-container">
|
<div className="full-view-container">
|
||||||
|
<OverlayScrollbar>
|
||||||
|
<>
|
||||||
<div className="full-view-header-container">
|
<div className="full-view-header-container">
|
||||||
{fullViewOptions && (
|
{fullViewOptions && (
|
||||||
<TimeContainer $panelType={widget.panelTypes}>
|
<TimeContainer $panelType={widget.panelTypes}>
|
||||||
|
{enableDrillDown && (
|
||||||
|
<Button
|
||||||
|
className="switch-edit-btn"
|
||||||
|
disabled={response.isFetching || response.isLoading}
|
||||||
|
onClick={(): void => {
|
||||||
|
safeNavigate(dashboardEditView);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Switch to Edit Mode
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
<div className="time-container">
|
||||||
{response.isFetching && (
|
{response.isFetching && (
|
||||||
<Spin spinning indicator={<LoadingOutlined spin />} />
|
<Spin spinning indicator={<LoadingOutlined spin />} />
|
||||||
)}
|
)}
|
||||||
@@ -216,14 +243,25 @@ function FullView({
|
|||||||
type="primary"
|
type="primary"
|
||||||
icon={<SyncOutlined />}
|
icon={<SyncOutlined />}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
</TimeContainer>
|
</TimeContainer>
|
||||||
)}
|
)}
|
||||||
|
{enableDrillDown && (
|
||||||
|
<QueryBuilderV2
|
||||||
|
panelType={widget.panelTypes}
|
||||||
|
version={selectedDashboard?.data?.version || 'v3'}
|
||||||
|
isListViewPanel={widget.panelTypes === PANEL_TYPES.LIST}
|
||||||
|
// filterConfigs={filterConfigs}
|
||||||
|
// queryComponents={queryComponents}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className={cx('graph-container', {
|
className={cx('graph-container', {
|
||||||
disabled: isDashboardLocked,
|
disabled: isDashboardLocked,
|
||||||
'height-widget': widget?.mergeAllActiveQueries || widget?.stackedBarChart,
|
'height-widget':
|
||||||
|
widget?.mergeAllActiveQueries || widget?.stackedBarChart,
|
||||||
'list-graph-container': isListView,
|
'list-graph-container': isListView,
|
||||||
})}
|
})}
|
||||||
ref={fullViewRef}
|
ref={fullViewRef}
|
||||||
@@ -261,6 +299,8 @@ function FullView({
|
|||||||
/>
|
/>
|
||||||
</GraphContainer>
|
</GraphContainer>
|
||||||
</div>
|
</div>
|
||||||
|
</>
|
||||||
|
</OverlayScrollbar>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ export const NotFoundContainer = styled.div`
|
|||||||
export const TimeContainer = styled.div<Props>`
|
export const TimeContainer = styled.div<Props>`
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
|
gap: 16px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
${({ $panelType }): FlattenSimpleInterpolation =>
|
${({ $panelType }): FlattenSimpleInterpolation =>
|
||||||
$panelType === PANEL_TYPES.TABLE
|
$panelType === PANEL_TYPES.TABLE
|
||||||
@@ -25,6 +26,10 @@ export const TimeContainer = styled.div<Props>`
|
|||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
`
|
`
|
||||||
: css``}
|
: css``}
|
||||||
|
|
||||||
|
.time-container {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const GraphContainer = styled.div<GraphContainerProps>`
|
export const GraphContainer = styled.div<GraphContainerProps>`
|
||||||
|
|||||||
@@ -59,6 +59,7 @@ export interface FullViewProps {
|
|||||||
isDependedDataLoaded?: boolean;
|
isDependedDataLoaded?: boolean;
|
||||||
onToggleModelHandler?: GraphManagerProps['onToggleModelHandler'];
|
onToggleModelHandler?: GraphManagerProps['onToggleModelHandler'];
|
||||||
setCurrentGraphRef: Dispatch<SetStateAction<RefObject<HTMLDivElement> | null>>;
|
setCurrentGraphRef: Dispatch<SetStateAction<RefObject<HTMLDivElement> | null>>;
|
||||||
|
enableDrillDown?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface GraphManagerProps extends UplotProps {
|
export interface GraphManagerProps extends UplotProps {
|
||||||
|
|||||||
@@ -0,0 +1,65 @@
|
|||||||
|
import { useGetCompositeQueryParam } from 'hooks/queryBuilder/useGetCompositeQueryParam';
|
||||||
|
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||||
|
import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults';
|
||||||
|
import { Dispatch, SetStateAction, useEffect, useRef } from 'react';
|
||||||
|
import { Dashboard, Widgets } from 'types/api/dashboard/getAll';
|
||||||
|
import { generateExportToDashboardLink } from 'utils/dashboard/generateExportToDashboardLink';
|
||||||
|
|
||||||
|
export interface DrilldownQueryProps {
|
||||||
|
widget: Widgets;
|
||||||
|
setRequestData: Dispatch<SetStateAction<GetQueryResultsProps>>;
|
||||||
|
enableDrillDown: boolean;
|
||||||
|
selectedDashboard: Dashboard | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UseDrilldownReturn {
|
||||||
|
dashboardEditView: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const useDrilldown = ({
|
||||||
|
enableDrillDown,
|
||||||
|
widget,
|
||||||
|
setRequestData,
|
||||||
|
selectedDashboard,
|
||||||
|
}: DrilldownQueryProps): UseDrilldownReturn => {
|
||||||
|
const isMounted = useRef(false);
|
||||||
|
const { redirectWithQueryBuilderData, currentQuery } = useQueryBuilder();
|
||||||
|
const compositeQueryExists = !!useGetCompositeQueryParam();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (enableDrillDown && compositeQueryExists) {
|
||||||
|
setRequestData((prev) => ({
|
||||||
|
...prev,
|
||||||
|
query: currentQuery,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [currentQuery]);
|
||||||
|
|
||||||
|
// update composite query with widget query if composite query is not present in url.
|
||||||
|
// Composite query should be in the url if switch to edit mode is clicked or drilldown happens from dashboard.
|
||||||
|
useEffect(() => {
|
||||||
|
if (enableDrillDown && !compositeQueryExists && !isMounted.current) {
|
||||||
|
redirectWithQueryBuilderData(widget.query);
|
||||||
|
}
|
||||||
|
isMounted.current = true;
|
||||||
|
}, [
|
||||||
|
widget,
|
||||||
|
enableDrillDown,
|
||||||
|
compositeQueryExists,
|
||||||
|
redirectWithQueryBuilderData,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const dashboardEditView = generateExportToDashboardLink({
|
||||||
|
query: currentQuery,
|
||||||
|
panelType: widget.panelTypes,
|
||||||
|
dashboardId: selectedDashboard?.id || '',
|
||||||
|
widgetId: widget.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
dashboardEditView,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useDrilldown;
|
||||||
@@ -32,6 +32,7 @@ import {
|
|||||||
import { useLocation } from 'react-router-dom';
|
import { useLocation } from 'react-router-dom';
|
||||||
import { Props } from 'types/api/dashboard/update';
|
import { Props } from 'types/api/dashboard/update';
|
||||||
import { DataSource } from 'types/common/queryBuilder';
|
import { DataSource } from 'types/common/queryBuilder';
|
||||||
|
import { shouldEnableDrilldown } from 'utils/dashboard/generateExportToDashboardLink';
|
||||||
import { v4 } from 'uuid';
|
import { v4 } from 'uuid';
|
||||||
|
|
||||||
import { useGraphClickToShowButton } from '../useGraphClickToShowButton';
|
import { useGraphClickToShowButton } from '../useGraphClickToShowButton';
|
||||||
@@ -226,6 +227,7 @@ function WidgetGraphComponent({
|
|||||||
const onToggleModelHandler = (): void => {
|
const onToggleModelHandler = (): void => {
|
||||||
const existingSearchParams = new URLSearchParams(search);
|
const existingSearchParams = new URLSearchParams(search);
|
||||||
existingSearchParams.delete(QueryParams.expandedWidgetId);
|
existingSearchParams.delete(QueryParams.expandedWidgetId);
|
||||||
|
existingSearchParams.delete(QueryParams.compositeQuery);
|
||||||
const updatedQueryParams = Object.fromEntries(existingSearchParams.entries());
|
const updatedQueryParams = Object.fromEntries(existingSearchParams.entries());
|
||||||
if (queryResponse.data?.payload) {
|
if (queryResponse.data?.payload) {
|
||||||
const {
|
const {
|
||||||
@@ -354,6 +356,7 @@ function WidgetGraphComponent({
|
|||||||
onClickHandler={onClickHandler ?? graphClickHandler}
|
onClickHandler={onClickHandler ?? graphClickHandler}
|
||||||
customOnDragSelect={customOnDragSelect}
|
customOnDragSelect={customOnDragSelect}
|
||||||
setCurrentGraphRef={setCurrentGraphRef}
|
setCurrentGraphRef={setCurrentGraphRef}
|
||||||
|
enableDrillDown={shouldEnableDrilldown(widget.panelTypes)}
|
||||||
/>
|
/>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import logEvent from 'api/common/logEvent';
|
import logEvent from 'api/common/logEvent';
|
||||||
import { DEFAULT_ENTITY_VERSION } from 'constants/app';
|
import { ENTITY_VERSION_V5 } from 'constants/app';
|
||||||
import { QueryParams } from 'constants/query';
|
import { QueryParams } from 'constants/query';
|
||||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||||
import { CustomTimeType } from 'container/TopNav/DateTimeSelectionV2/config';
|
import { CustomTimeType } from 'container/TopNav/DateTimeSelectionV2/config';
|
||||||
@@ -209,7 +209,8 @@ function GridCardGraph({
|
|||||||
end: customTimeRange?.endTime || end,
|
end: customTimeRange?.endTime || end,
|
||||||
originalGraphType: widget?.panelTypes,
|
originalGraphType: widget?.panelTypes,
|
||||||
},
|
},
|
||||||
version || DEFAULT_ENTITY_VERSION,
|
ENTITY_VERSION_V5,
|
||||||
|
// version || DEFAULT_ENTITY_VERSION,
|
||||||
{
|
{
|
||||||
queryKey: [
|
queryKey: [
|
||||||
maxTime,
|
maxTime,
|
||||||
|
|||||||
@@ -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,45 +1,38 @@
|
|||||||
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(
|
() =>
|
||||||
|
updateAllQueriesOperators(
|
||||||
initialQueriesMap.logs,
|
initialQueriesMap.logs,
|
||||||
PANEL_TYPES.LIST,
|
PANEL_TYPES.LIST,
|
||||||
DataSource.LOGS,
|
DataSource.LOGS,
|
||||||
|
),
|
||||||
|
[updateAllQueriesOperators],
|
||||||
);
|
);
|
||||||
return prepareQueryWithDefaultTimestamp(updatedQuery);
|
|
||||||
}, [updateAllQueriesOperators]);
|
|
||||||
|
|
||||||
useShareBuilderUrl(defaultValue);
|
useShareBuilderUrl(defaultValue);
|
||||||
|
|
||||||
@@ -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">
|
|
||||||
<QueryBuilderSearchV2
|
|
||||||
query={query}
|
|
||||||
onChange={handleChangeTagFilters}
|
|
||||||
whereClauseConfig={filterConfigs?.filters}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{selectedView === SELECTED_VIEWS.QUERY_BUILDER && (
|
|
||||||
<QueryBuilder
|
|
||||||
panelType={panelTypes}
|
|
||||||
config={{ initialDataSource: DataSource.LOGS, queryVariant: 'static' }}
|
config={{ initialDataSource: DataSource.LOGS, queryVariant: 'static' }}
|
||||||
|
panelType={panelTypes}
|
||||||
filterConfigs={filterConfigs}
|
filterConfigs={filterConfigs}
|
||||||
queryComponents={queryComponents}
|
queryComponents={queryComponents}
|
||||||
|
showOnlyWhereClause={selectedView === ExplorerViews.LIST}
|
||||||
version="v3" // setting this to v3 as we this is rendered in logs explorer
|
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,
|
||||||
@@ -79,15 +79,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>;
|
||||||
@@ -95,6 +93,7 @@ function LogsExplorerViews({
|
|||||||
chartQueryKeyRef: MutableRefObject<any>;
|
chartQueryKeyRef: MutableRefObject<any>;
|
||||||
}): JSX.Element {
|
}): JSX.Element {
|
||||||
const { safeNavigate } = useSafeNavigate();
|
const { safeNavigate } = useSafeNavigate();
|
||||||
|
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);
|
||||||
@@ -136,6 +135,8 @@ function LogsExplorerViews({
|
|||||||
const [queryId, setQueryId] = useState<string>(v4());
|
const [queryId, setQueryId] = useState<string>(v4());
|
||||||
const [queryStats, setQueryStats] = useState<WsDataEvent>();
|
const [queryStats, setQueryStats] = useState<WsDataEvent>();
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
@@ -234,15 +235,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,
|
||||||
@@ -250,7 +242,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,
|
||||||
},
|
},
|
||||||
@@ -268,7 +261,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,
|
||||||
@@ -329,14 +323,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,
|
||||||
|
...(selectedPanelType !== PANEL_TYPES.LIST ? { order: [] } : {}),
|
||||||
|
}))
|
||||||
: [
|
: [
|
||||||
{
|
{
|
||||||
...(listQuery || initialQueryBuilderFormValues),
|
...(listQuery || initialQueryBuilderFormValues),
|
||||||
...paginateData,
|
...paginateData,
|
||||||
...(updatedFilters ? { filters: updatedFilters } : {}),
|
...(updatedFilters ? { filters: updatedFilters } : {}),
|
||||||
|
...(selectedPanelType === PANEL_TYPES.LIST
|
||||||
|
? { order: orderBy }
|
||||||
|
: { order: [] }),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -350,7 +356,7 @@ function LogsExplorerViews({
|
|||||||
|
|
||||||
return data;
|
return data;
|
||||||
},
|
},
|
||||||
[listQuery, activeLogId],
|
[activeLogId, orderDirection, listQuery, selectedPanelType],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleEndReached = useCallback(() => {
|
const handleEndReached = useCallback(() => {
|
||||||
@@ -452,8 +458,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);
|
||||||
@@ -473,11 +478,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
|
||||||
@@ -498,10 +499,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
|
||||||
) {
|
) {
|
||||||
const newRequestData = getRequestData(stagedQuery, {
|
const newRequestData = getRequestData(stagedQuery, {
|
||||||
filters: listQuery?.filters || initialFilters,
|
filters: listQuery?.filters || initialFilters,
|
||||||
@@ -524,6 +534,8 @@ function LogsExplorerViews({
|
|||||||
activeLogId,
|
activeLogId,
|
||||||
panelType,
|
panelType,
|
||||||
selectedView,
|
selectedView,
|
||||||
|
orderDirection,
|
||||||
|
selectedPanelType,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const chartData = useMemo(() => {
|
const chartData = useMemo(() => {
|
||||||
@@ -630,60 +642,43 @@ 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">
|
|
||||||
<Button.Group className="views-tabs">
|
|
||||||
<Button
|
|
||||||
value={PANEL_TYPES.LIST}
|
|
||||||
className={
|
|
||||||
// eslint-disable-next-line sonarjs/no-duplicate-string
|
|
||||||
selectedPanelType === PANEL_TYPES.LIST ? 'selected_view tab' : 'tab'
|
|
||||||
}
|
|
||||||
disabled={
|
|
||||||
(isMultipleQueries || isGroupByExist) && selectedView !== 'search'
|
|
||||||
}
|
|
||||||
onClick={(): void => handleModeChange(PANEL_TYPES.LIST)}
|
|
||||||
data-testid="logs-list-view"
|
|
||||||
>
|
|
||||||
List view
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
value={PANEL_TYPES.TIME_SERIES}
|
|
||||||
className={
|
|
||||||
// eslint-disable-next-line sonarjs/no-duplicate-string
|
|
||||||
selectedPanelType === PANEL_TYPES.TIME_SERIES
|
|
||||||
? 'selected_view tab'
|
|
||||||
: 'tab'
|
|
||||||
}
|
|
||||||
onClick={(): void => handleModeChange(PANEL_TYPES.TIME_SERIES)}
|
|
||||||
data-testid="time-series-view"
|
|
||||||
>
|
|
||||||
Time series
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
value={PANEL_TYPES.TABLE}
|
|
||||||
className={
|
|
||||||
// eslint-disable-next-line sonarjs/no-duplicate-string
|
|
||||||
selectedPanelType === PANEL_TYPES.TABLE ? 'selected_view tab' : 'tab'
|
|
||||||
}
|
|
||||||
onClick={(): void => handleModeChange(PANEL_TYPES.TABLE)}
|
|
||||||
data-testid="table-view"
|
|
||||||
>
|
|
||||||
Table
|
|
||||||
</Button>
|
|
||||||
</Button.Group>
|
|
||||||
<div className="logs-actions-container">
|
<div className="logs-actions-container">
|
||||||
{selectedPanelType === PANEL_TYPES.LIST && (
|
|
||||||
<div className="tab-options">
|
<div className="tab-options">
|
||||||
|
<div className="tab-options-left">
|
||||||
|
{selectedPanelType === PANEL_TYPES.LIST && (
|
||||||
|
<div className="frequency-chart-view-controller">
|
||||||
|
<Typography>Frequency chart</Typography>
|
||||||
|
<Switch
|
||||||
|
size="small"
|
||||||
|
checked={showFrequencyChart}
|
||||||
|
defaultChecked
|
||||||
|
onChange={(): void => setShowFrequencyChart(!showFrequencyChart)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="tab-options-right">
|
||||||
|
{selectedPanelType === PANEL_TYPES.LIST && (
|
||||||
|
<>
|
||||||
|
<div className="order-by-container">
|
||||||
|
<div className="order-by-label">
|
||||||
|
Order by <Minus size={14} /> <ArrowUp10 size={14} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Select
|
||||||
|
placeholder="Select order by"
|
||||||
|
className="order-by-select"
|
||||||
|
style={{ width: 100 }}
|
||||||
|
value={orderDirection}
|
||||||
|
onChange={(value): void => setOrderDirection(value)}
|
||||||
|
options={[
|
||||||
|
{ label: 'Ascending', value: 'asc' },
|
||||||
|
{ label: 'Descending', value: 'desc' },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<Download
|
<Download
|
||||||
data={flattenLogData}
|
data={flattenLogData}
|
||||||
isLoading={isFetching}
|
isLoading={isFetching}
|
||||||
@@ -691,7 +686,7 @@ function LogsExplorerViews({
|
|||||||
/>
|
/>
|
||||||
<div className="format-options-container" ref={menuRef}>
|
<div className="format-options-container" ref={menuRef}>
|
||||||
<Button
|
<Button
|
||||||
className="periscope-btn"
|
className="periscope-btn ghost"
|
||||||
onClick={handleToggleShowFormatOptions}
|
onClick={handleToggleShowFormatOptions}
|
||||||
icon={<Sliders size={14} />}
|
icon={<Sliders size={14} />}
|
||||||
data-testid="periscope-btn"
|
data-testid="periscope-btn"
|
||||||
@@ -706,8 +701,9 @@ function LogsExplorerViews({
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{(selectedPanelType === PANEL_TYPES.TIME_SERIES ||
|
{(selectedPanelType === PANEL_TYPES.TIME_SERIES ||
|
||||||
selectedPanelType === PANEL_TYPES.TABLE) && (
|
selectedPanelType === PANEL_TYPES.TABLE) && (
|
||||||
<div className="query-stats">
|
<div className="query-stats">
|
||||||
@@ -716,12 +712,14 @@ function LogsExplorerViews({
|
|||||||
error={isError}
|
error={isError}
|
||||||
success={isSuccess}
|
success={isSuccess}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{queryStats?.read_rows && (
|
{queryStats?.read_rows && (
|
||||||
<Typography.Text className="rows">
|
<Typography.Text className="rows">
|
||||||
{getYAxisFormattedValue(queryStats.read_rows?.toString(), 'short')}{' '}
|
{getYAxisFormattedValue(queryStats.read_rows?.toString(), 'short')}{' '}
|
||||||
rows
|
rows
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{queryStats?.elapsed_ms && (
|
{queryStats?.elapsed_ms && (
|
||||||
<>
|
<>
|
||||||
<div className="divider" />
|
<div className="divider" />
|
||||||
@@ -734,6 +732,16 @@ function LogsExplorerViews({
|
|||||||
)}
|
)}
|
||||||
</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 && (
|
||||||
@@ -780,4 +788,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 { QueryBuilderContext } from 'providers/QueryBuilder';
|
import { QueryBuilderContext } from 'providers/QueryBuilder';
|
||||||
import { VirtuosoMockContext } from 'react-virtuoso';
|
import { VirtuosoMockContext } from 'react-virtuoso';
|
||||||
import { fireEvent, render, RenderResult } from 'tests/test-utils';
|
import { fireEvent, render, RenderResult } from 'tests/test-utils';
|
||||||
@@ -106,8 +106,7 @@ const renderer = (): RenderResult =>
|
|||||||
value={{ viewportHeight: 300, itemHeight: 100 }}
|
value={{ viewportHeight: 300, itemHeight: 100 }}
|
||||||
>
|
>
|
||||||
<LogsExplorerViews
|
<LogsExplorerViews
|
||||||
selectedView={SELECTED_VIEWS.SEARCH}
|
selectedView={ExplorerViews.LIST}
|
||||||
showFrequencyChart
|
|
||||||
setIsLoadingQueries={(): void => {}}
|
setIsLoadingQueries={(): void => {}}
|
||||||
listQueryKeyRef={{ current: {} }}
|
listQueryKeyRef={{ current: {} }}
|
||||||
chartQueryKeyRef={{ current: {} }}
|
chartQueryKeyRef={{ current: {} }}
|
||||||
@@ -185,8 +184,7 @@ describe('LogsExplorerViews -', () => {
|
|||||||
render(
|
render(
|
||||||
<QueryBuilderContext.Provider value={mockQueryBuilderContextValue}>
|
<QueryBuilderContext.Provider value={mockQueryBuilderContextValue}>
|
||||||
<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;
|
||||||
|
|||||||
@@ -2,9 +2,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 { 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';
|
||||||
@@ -18,7 +20,7 @@ import { DataSource } from 'types/common/queryBuilder';
|
|||||||
import { generateExportToDashboardLink } from 'utils/dashboard/generateExportToDashboardLink';
|
import { generateExportToDashboardLink } from 'utils/dashboard/generateExportToDashboardLink';
|
||||||
import { v4 as uuid } from 'uuid';
|
import { v4 as uuid } from 'uuid';
|
||||||
|
|
||||||
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';
|
||||||
@@ -51,6 +53,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(
|
||||||
@@ -61,7 +73,7 @@ function Explorer(): JSX.Element {
|
|||||||
[currentQuery, updateAllQueriesOperators],
|
[currentQuery, updateAllQueriesOperators],
|
||||||
);
|
);
|
||||||
|
|
||||||
useShareBuilderUrl(exportDefaultQuery);
|
useShareBuilderUrl(defaultQuery);
|
||||||
|
|
||||||
const handleExport = useCallback(
|
const handleExport = useCallback(
|
||||||
(
|
(
|
||||||
@@ -93,6 +105,11 @@ function Explorer(): JSX.Element {
|
|||||||
[stagedQuery],
|
[stagedQuery],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
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">
|
||||||
@@ -110,7 +127,14 @@ function Explorer(): JSX.Element {
|
|||||||
<RightToolbarActions onStageRunQuery={handleRunQuery} />
|
<RightToolbarActions onStageRunQuery={handleRunQuery} />
|
||||||
</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';
|
||||||
@@ -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">
|
||||||
|
<QueryBuilderV2
|
||||||
panelType={selectedGraph}
|
panelType={selectedGraph}
|
||||||
filterConfigs={filterConfigs}
|
filterConfigs={filterConfigs}
|
||||||
version={selectedDashboard?.data?.version || 'v3'}
|
version={selectedDashboard?.data?.version || 'v3'}
|
||||||
isListViewPanel={selectedGraph === PANEL_TYPES.LIST}
|
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,
|
||||||
|
|||||||
@@ -43,6 +43,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 +53,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 (
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -36,6 +36,11 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.right-header {
|
||||||
|
display: flex;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
.save-btn {
|
.save-btn {
|
||||||
display: flex;
|
display: flex;
|
||||||
height: 32px;
|
height: 32px;
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
|||||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||||
import { useSafeNavigate } from 'hooks/useSafeNavigate';
|
import { useSafeNavigate } from 'hooks/useSafeNavigate';
|
||||||
import useUrlQuery from 'hooks/useUrlQuery';
|
import useUrlQuery from 'hooks/useUrlQuery';
|
||||||
|
import createQueryParams from 'lib/createQueryParams';
|
||||||
import { getDashboardVariables } from 'lib/dashbaordVariables/getDashboardVariables';
|
import { getDashboardVariables } from 'lib/dashbaordVariables/getDashboardVariables';
|
||||||
import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults';
|
import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults';
|
||||||
import { cloneDeep, defaultTo, isEmpty, isUndefined } from 'lodash-es';
|
import { cloneDeep, defaultTo, isEmpty, isUndefined } from 'lodash-es';
|
||||||
@@ -71,7 +72,10 @@ import {
|
|||||||
placeWidgetBetweenRows,
|
placeWidgetBetweenRows,
|
||||||
} from './utils';
|
} from './utils';
|
||||||
|
|
||||||
function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
|
function NewWidget({
|
||||||
|
selectedGraph,
|
||||||
|
enableDrilldown = false,
|
||||||
|
}: NewWidgetProps): JSX.Element {
|
||||||
const { safeNavigate } = useSafeNavigate();
|
const { safeNavigate } = useSafeNavigate();
|
||||||
const {
|
const {
|
||||||
selectedDashboard,
|
selectedDashboard,
|
||||||
@@ -682,6 +686,26 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
|
|||||||
}
|
}
|
||||||
}, [selectedLogFields, selectedTracesFields, currentQuery, selectedGraph]);
|
}, [selectedLogFields, selectedTracesFields, currentQuery, selectedGraph]);
|
||||||
|
|
||||||
|
const showSwitchToViewModeButton =
|
||||||
|
enableDrilldown && !isNewDashboard && !!query.get('widgetId');
|
||||||
|
|
||||||
|
const handleSwitchToViewMode = useCallback(() => {
|
||||||
|
if (!query.get('widgetId')) return;
|
||||||
|
const widgetId = query.get('widgetId') || '';
|
||||||
|
const queryParams = {
|
||||||
|
[QueryParams.expandedWidgetId]: widgetId,
|
||||||
|
[QueryParams.compositeQuery]: encodeURIComponent(
|
||||||
|
JSON.stringify(currentQuery),
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
const updatedSearch = createQueryParams(queryParams);
|
||||||
|
safeNavigate({
|
||||||
|
pathname: generatePath(ROUTES.DASHBOARD, { dashboardId }),
|
||||||
|
search: updatedSearch,
|
||||||
|
});
|
||||||
|
}, [query, safeNavigate, dashboardId, currentQuery]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container>
|
<Container>
|
||||||
<div className="edit-header">
|
<div className="edit-header">
|
||||||
@@ -698,6 +722,16 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
|
|||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
</Flex>
|
</Flex>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="right-header">
|
||||||
|
{showSwitchToViewModeButton && (
|
||||||
|
<Button
|
||||||
|
data-testid="switch-to-view-mode"
|
||||||
|
disabled={isSaveDisabled || !currentQuery}
|
||||||
|
onClick={handleSwitchToViewMode}
|
||||||
|
>
|
||||||
|
Switch to View Mode
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
{isSaveDisabled && (
|
{isSaveDisabled && (
|
||||||
<Button
|
<Button
|
||||||
type="primary"
|
type="primary"
|
||||||
@@ -724,6 +758,7 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
|
|||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<PanelContainer>
|
<PanelContainer>
|
||||||
<LeftContainerWrapper isDarkMode={useIsDarkMode()}>
|
<LeftContainerWrapper isDarkMode={useIsDarkMode()}>
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ export interface NewWidgetProps {
|
|||||||
selectedGraph: PANEL_TYPES;
|
selectedGraph: PANEL_TYPES;
|
||||||
yAxisUnit: Widgets['yAxisUnit'];
|
yAxisUnit: Widgets['yAxisUnit'];
|
||||||
fillSpans: Widgets['fillSpans'];
|
fillSpans: Widgets['fillSpans'];
|
||||||
|
enableDrilldown?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface WidgetGraphProps {
|
export interface WidgetGraphProps {
|
||||||
|
|||||||
@@ -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 && (
|
||||||
|
<Tooltip title="List View">
|
||||||
<Button
|
<Button
|
||||||
disabled={search.disabled}
|
disabled={list.disabled}
|
||||||
className={cx(
|
className={cx(
|
||||||
'search',
|
'list-view-tab',
|
||||||
actionBtn,
|
'explorer-view-option',
|
||||||
selectedView === 'search' ? activeTab : '',
|
selectedView === list.key ? activeTab : '',
|
||||||
)}
|
)}
|
||||||
onClick={(): void => onChangeSelectedView(SELECTED_VIEWS.SEARCH)}
|
onClick={(): void => onChangeSelectedView(list.key)}
|
||||||
>
|
>
|
||||||
<SquareMousePointer size={14} data-testid="search-view" />
|
<SquareMousePointer size={14} data-testid="search-view" />
|
||||||
|
List View
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Tooltip title="Query Builder">
|
|
||||||
<Button
|
|
||||||
disabled={QB.disabled}
|
|
||||||
className={cx(
|
|
||||||
queryBuilder,
|
|
||||||
actionBtn,
|
|
||||||
selectedView === queryBuilder ? activeTab : '',
|
|
||||||
)}
|
)}
|
||||||
onClick={(): void => onChangeSelectedView(SELECTED_VIEWS.QUERY_BUILDER)}
|
|
||||||
|
{trace?.show && (
|
||||||
|
<Tooltip title="Trace View">
|
||||||
|
<Button
|
||||||
|
disabled={trace.disabled}
|
||||||
|
className={cx(
|
||||||
|
'trace-view-tab',
|
||||||
|
'explorer-view-option',
|
||||||
|
selectedView === trace.key ? activeTab : '',
|
||||||
|
)}
|
||||||
|
onClick={(): void => onChangeSelectedView(trace.key)}
|
||||||
|
>
|
||||||
|
<SquareMousePointer size={14} data-testid="trace-view" />
|
||||||
|
Trace View
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{timeseries?.show && (
|
||||||
|
<Tooltip title="Time Series">
|
||||||
|
<Button
|
||||||
|
disabled={timeseries.disabled}
|
||||||
|
className={cx(
|
||||||
|
'timeseries-view-tab',
|
||||||
|
'explorer-view-option',
|
||||||
|
selectedView === timeseries.key ? activeTab : '',
|
||||||
|
)}
|
||||||
|
onClick={(): void => onChangeSelectedView(timeseries.key)}
|
||||||
>
|
>
|
||||||
<Atom size={14} data-testid="query-builder-view" />
|
<Atom size={14} data-testid="query-builder-view" />
|
||||||
|
Time Series
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
|
||||||
{clickhouse?.show && (
|
{clickhouse?.show && (
|
||||||
|
<Tooltip title="Clickhouse">
|
||||||
<Button
|
<Button
|
||||||
disabled={clickhouse.disabled}
|
disabled={clickhouse.disabled}
|
||||||
className={cx(
|
className={cx(
|
||||||
SELECTED_VIEWS.CLICKHOUSE,
|
'clickhouse-view-tab',
|
||||||
actionBtn,
|
'explorer-view-option',
|
||||||
selectedView === SELECTED_VIEWS.CLICKHOUSE ? activeTab : '',
|
selectedView === clickhouse.key ? activeTab : '',
|
||||||
)}
|
)}
|
||||||
onClick={(): void => onChangeSelectedView(SELECTED_VIEWS.CLICKHOUSE)}
|
onClick={(): void => onChangeSelectedView(clickhouse.key)}
|
||||||
>
|
>
|
||||||
<Terminal size={14} data-testid="clickhouse-view" />
|
<Terminal size={14} data-testid="clickhouse-view" />
|
||||||
|
Clickhouse
|
||||||
</Button>
|
</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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&:first-child {
|
||||||
|
border-left: 1px solid transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: transparent !important;
|
||||||
|
border-left: 1px solid transparent !important;
|
||||||
|
color: var(--bg-vanilla-100);
|
||||||
}
|
}
|
||||||
.action-btn + .action-btn {
|
|
||||||
border-left: 1px solid var(--bg-slate-400);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -7,10 +7,12 @@ import {
|
|||||||
createTableColumnsFromQuery,
|
createTableColumnsFromQuery,
|
||||||
RowData,
|
RowData,
|
||||||
} from 'lib/query/createTableColumnsFromQuery';
|
} from 'lib/query/createTableColumnsFromQuery';
|
||||||
|
import ContextMenu, { useCoordinates } from 'periscope/components/ContextMenu';
|
||||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
import { useParams } from 'react-router-dom';
|
import { useParams } from 'react-router-dom';
|
||||||
|
|
||||||
import { QueryTableProps } from './QueryTable.intefaces';
|
import { QueryTableProps } from './QueryTable.intefaces';
|
||||||
|
import useTableContextMenu from './useTableContextMenu';
|
||||||
import { createDownloadableData } from './utils';
|
import { createDownloadableData } from './utils';
|
||||||
|
|
||||||
export function QueryTable({
|
export function QueryTable({
|
||||||
@@ -31,6 +33,21 @@ export function QueryTable({
|
|||||||
const { servicename: encodedServiceName } = useParams<IServiceName>();
|
const { servicename: encodedServiceName } = useParams<IServiceName>();
|
||||||
const servicename = decodeURIComponent(encodedServiceName);
|
const servicename = decodeURIComponent(encodedServiceName);
|
||||||
const { loading } = props;
|
const { loading } = props;
|
||||||
|
|
||||||
|
const {
|
||||||
|
coordinates,
|
||||||
|
popoverPosition,
|
||||||
|
clickedData,
|
||||||
|
onClose,
|
||||||
|
onClick,
|
||||||
|
} = useCoordinates();
|
||||||
|
const { menuItemsConfig } = useTableContextMenu({
|
||||||
|
widgetId: widgetId || '',
|
||||||
|
clickedData,
|
||||||
|
onClose,
|
||||||
|
coordinates,
|
||||||
|
});
|
||||||
|
|
||||||
const { columns: newColumns, dataSource: newDataSource } = useMemo(() => {
|
const { columns: newColumns, dataSource: newDataSource } = useMemo(() => {
|
||||||
if (columns && dataSource) {
|
if (columns && dataSource) {
|
||||||
return { columns, dataSource };
|
return { columns, dataSource };
|
||||||
@@ -54,6 +71,44 @@ export function QueryTable({
|
|||||||
|
|
||||||
const tableColumns = modifyColumns ? modifyColumns(newColumns) : newColumns;
|
const tableColumns = modifyColumns ? modifyColumns(newColumns) : newColumns;
|
||||||
|
|
||||||
|
// Add click handlers to columns to capture clicked data
|
||||||
|
const columnsWithClickHandlers = useMemo(
|
||||||
|
() =>
|
||||||
|
tableColumns.map((column: any): any => ({
|
||||||
|
...column,
|
||||||
|
render: (text: any, record: RowData, index: number): JSX.Element => {
|
||||||
|
const originalRender = column.render;
|
||||||
|
const renderedContent = originalRender
|
||||||
|
? originalRender(text, record, index)
|
||||||
|
: text;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
// have its dimension equal to the column width
|
||||||
|
onClick={(e): void => {
|
||||||
|
e.stopPropagation();
|
||||||
|
console.log('@record:', { record, column });
|
||||||
|
onClick(e, { record, column });
|
||||||
|
}}
|
||||||
|
onKeyDown={(e): void => {
|
||||||
|
if (e.key === 'Enter' || e.key === ' ') {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
onClick(e as any, { record, column });
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
style={{ cursor: 'pointer' }}
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
>
|
||||||
|
{renderedContent}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
[tableColumns, onClick],
|
||||||
|
);
|
||||||
|
|
||||||
const paginationConfig = {
|
const paginationConfig = {
|
||||||
pageSize: 10,
|
pageSize: 10,
|
||||||
showSizeChanger: false,
|
showSizeChanger: false,
|
||||||
@@ -82,6 +137,7 @@ export function QueryTable({
|
|||||||
}, [newDataSource, onTableSearch, searchTerm]);
|
}, [newDataSource, onTableSearch, searchTerm]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<>
|
||||||
<div className="query-table">
|
<div className="query-table">
|
||||||
{isDownloadEnabled && (
|
{isDownloadEnabled && (
|
||||||
<div className="query-table--download">
|
<div className="query-table--download">
|
||||||
@@ -93,7 +149,7 @@ export function QueryTable({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<ResizeTable
|
<ResizeTable
|
||||||
columns={tableColumns}
|
columns={columnsWithClickHandlers}
|
||||||
tableLayout="fixed"
|
tableLayout="fixed"
|
||||||
dataSource={filterTable === null ? newDataSource : filterTable}
|
dataSource={filterTable === null ? newDataSource : filterTable}
|
||||||
scroll={{ x: 'max-content' }}
|
scroll={{ x: 'max-content' }}
|
||||||
@@ -105,5 +161,21 @@ export function QueryTable({
|
|||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<ContextMenu
|
||||||
|
coordinates={coordinates}
|
||||||
|
popoverPosition={popoverPosition}
|
||||||
|
title={menuItemsConfig.header}
|
||||||
|
items={menuItemsConfig.items}
|
||||||
|
onClose={onClose}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* <ContextMenuV2
|
||||||
|
coordinates={coordinates}
|
||||||
|
popoverPosition={popoverPosition}
|
||||||
|
title={menuItemsConfig.header}
|
||||||
|
items={menuItemsConfig.items}
|
||||||
|
onClose={onClose}
|
||||||
|
/> */}
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
95
frontend/src/container/QueryTable/contextConfig.tsx
Normal file
95
frontend/src/container/QueryTable/contextConfig.tsx
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
import { ColumnType } from 'antd/lib/table';
|
||||||
|
import { RowData } from 'lib/query/createTableColumnsFromQuery';
|
||||||
|
import { ReactNode } from 'react';
|
||||||
|
|
||||||
|
export type ContextMenuItem = ReactNode;
|
||||||
|
|
||||||
|
interface ClickedData {
|
||||||
|
record: RowData;
|
||||||
|
column: ColumnType<RowData>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ColumnClickData {
|
||||||
|
record: RowData;
|
||||||
|
column: ColumnType<RowData>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getContextMenuConfig(
|
||||||
|
clickedData: ClickedData | null,
|
||||||
|
panelType: string,
|
||||||
|
onColumnClick: (operator: string, data: ColumnClickData) => void,
|
||||||
|
): { header?: string; items?: ContextMenuItem } {
|
||||||
|
if (
|
||||||
|
panelType === 'table' &&
|
||||||
|
clickedData?.column &&
|
||||||
|
!(clickedData.column as any).queryName
|
||||||
|
) {
|
||||||
|
const columnName = clickedData.column.title || clickedData.column.dataIndex;
|
||||||
|
return {
|
||||||
|
header: `Filter by ${columnName}`,
|
||||||
|
items: (
|
||||||
|
<>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: 8,
|
||||||
|
padding: '8px 16px',
|
||||||
|
cursor: 'pointer',
|
||||||
|
}}
|
||||||
|
onClick={(): void =>
|
||||||
|
onColumnClick('=', {
|
||||||
|
record: clickedData.record,
|
||||||
|
column: clickedData.column,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
onKeyDown={(e): void => {
|
||||||
|
if (e.key === 'Enter' || e.key === ' ') {
|
||||||
|
e.preventDefault();
|
||||||
|
onColumnClick('=', {
|
||||||
|
record: clickedData.record,
|
||||||
|
column: clickedData.column,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
>
|
||||||
|
<span style={{ color: '#3B5AFB', fontSize: 18 }}>=</span>
|
||||||
|
<span style={{ fontWeight: 600, color: '#2B2B43' }}>Is this</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: 8,
|
||||||
|
padding: '8px 16px',
|
||||||
|
cursor: 'pointer',
|
||||||
|
}}
|
||||||
|
onClick={(): void =>
|
||||||
|
onColumnClick('!=', {
|
||||||
|
record: clickedData.record,
|
||||||
|
column: clickedData.column,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
onKeyDown={(e): void => {
|
||||||
|
if (e.key === 'Enter' || e.key === ' ') {
|
||||||
|
e.preventDefault();
|
||||||
|
onColumnClick('!=', {
|
||||||
|
record: clickedData.record,
|
||||||
|
column: clickedData.column,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
>
|
||||||
|
<span style={{ color: '#3B5AFB', fontSize: 18 }}>≠</span>
|
||||||
|
<span style={{ fontWeight: 600, color: '#2B2B43' }}>Is not this</span>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
66
frontend/src/container/QueryTable/useTableContextMenu.tsx
Normal file
66
frontend/src/container/QueryTable/useTableContextMenu.tsx
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
import { ColumnType } from 'antd/lib/table';
|
||||||
|
import { QueryParams } from 'constants/query';
|
||||||
|
import { useSafeNavigate } from 'hooks/useSafeNavigate';
|
||||||
|
import createQueryParams from 'lib/createQueryParams';
|
||||||
|
import { RowData } from 'lib/query/createTableColumnsFromQuery';
|
||||||
|
import { useCallback, useMemo } from 'react';
|
||||||
|
import { useLocation } from 'react-router-dom-v5-compat';
|
||||||
|
|
||||||
|
import { getContextMenuConfig } from './contextConfig';
|
||||||
|
|
||||||
|
interface ClickedData {
|
||||||
|
record: RowData;
|
||||||
|
column: ColumnType<RowData>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UseTableContextMenuProps {
|
||||||
|
widgetId: string;
|
||||||
|
clickedData: ClickedData | null;
|
||||||
|
onClose: () => void;
|
||||||
|
coordinates: { x: number; y: number } | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useTableContextMenu({
|
||||||
|
widgetId,
|
||||||
|
clickedData,
|
||||||
|
onClose,
|
||||||
|
coordinates,
|
||||||
|
}: UseTableContextMenuProps): {
|
||||||
|
menuItemsConfig: { header?: string; items?: React.ReactNode };
|
||||||
|
} {
|
||||||
|
const { pathname, search } = useLocation();
|
||||||
|
const { safeNavigate } = useSafeNavigate();
|
||||||
|
|
||||||
|
const onColumnClick = useCallback((): void => {
|
||||||
|
const queryParams = {
|
||||||
|
[QueryParams.expandedWidgetId]: widgetId,
|
||||||
|
};
|
||||||
|
const updatedSearch = createQueryParams(queryParams);
|
||||||
|
const existingSearch = new URLSearchParams(search);
|
||||||
|
const isExpandedWidgetIdPresent = existingSearch.has(
|
||||||
|
QueryParams.expandedWidgetId,
|
||||||
|
);
|
||||||
|
if (isExpandedWidgetIdPresent) {
|
||||||
|
existingSearch.delete(QueryParams.expandedWidgetId);
|
||||||
|
}
|
||||||
|
const separator = existingSearch.toString() ? '&' : '';
|
||||||
|
const newSearch = `${existingSearch}${separator}${updatedSearch}`;
|
||||||
|
|
||||||
|
safeNavigate({
|
||||||
|
pathname,
|
||||||
|
search: newSearch,
|
||||||
|
});
|
||||||
|
onClose();
|
||||||
|
}, [widgetId, search, pathname, safeNavigate, onClose]);
|
||||||
|
|
||||||
|
const menuItemsConfig = useMemo(() => {
|
||||||
|
if (coordinates) {
|
||||||
|
return getContextMenuConfig(clickedData, 'table', onColumnClick);
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}, [clickedData, onColumnClick, coordinates]);
|
||||||
|
|
||||||
|
return { menuItemsConfig };
|
||||||
|
}
|
||||||
|
|
||||||
|
export default useTableContextMenu;
|
||||||
@@ -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="rightActions">
|
||||||
<div className="timeRange">
|
<div className="timeRange">
|
||||||
{showOldCTA && <NewExplorerCTA />}
|
{showOldCTA && <NewExplorerCTA />}
|
||||||
<DateTimeSelectionV2
|
<DateTimeSelectionV2
|
||||||
showAutoRefresh={showAutoRefresh}
|
showAutoRefresh={showAutoRefresh}
|
||||||
showRefreshText={!isLogsExplorerPage && !isApiMonitoringPage}
|
showRefreshText={!isLogsExplorerPage && !isApiMonitoringPage}
|
||||||
|
hideShareModal
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="rightActions">{rightActions}</div>
|
|
||||||
|
{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 && (
|
||||||
|
<div className="refresh-text-container">
|
||||||
<RefreshText
|
<RefreshText
|
||||||
{...{
|
{...{
|
||||||
onLastRefreshHandler,
|
onLastRefreshHandler,
|
||||||
}}
|
}}
|
||||||
refreshButtonHidden={refreshButtonHidden}
|
refreshButtonHidden={refreshButtonHidden}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
<Form
|
<Form
|
||||||
form={formSelector}
|
form={formSelector}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user