Compare commits
70 Commits
main
...
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 = {
|
||||
ignorePatterns: ['src/antlr-parser/*.ts'],
|
||||
env: {
|
||||
browser: 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": {
|
||||
"@ant-design/colors": "6.0.0",
|
||||
"@ant-design/icons": "4.8.0",
|
||||
"@codemirror/autocomplete": "6.18.6",
|
||||
"@codemirror/lang-javascript": "6.2.3",
|
||||
"@dnd-kit/core": "6.1.0",
|
||||
"@dnd-kit/modifiers": "7.0.0",
|
||||
"@dnd-kit/sortable": "8.0.0",
|
||||
@@ -43,6 +45,8 @@
|
||||
"@signozhq/design-tokens": "1.1.4",
|
||||
"@tanstack/react-table": "8.20.6",
|
||||
"@tanstack/react-virtual": "3.11.2",
|
||||
"@uiw/codemirror-theme-copilot": "4.23.11",
|
||||
"@uiw/react-codemirror": "4.23.10",
|
||||
"@uiw/react-md-editor": "3.23.5",
|
||||
"@visx/group": "3.3.0",
|
||||
"@visx/hierarchy": "3.12.0",
|
||||
@@ -53,6 +57,7 @@
|
||||
"antd": "5.11.0",
|
||||
"antd-table-saveas-excel": "2.2.1",
|
||||
"axios": "1.8.2",
|
||||
"antlr4": "4.13.2",
|
||||
"babel-eslint": "^10.1.0",
|
||||
"babel-jest": "^29.6.4",
|
||||
"babel-loader": "9.1.3",
|
||||
|
||||
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 apiV3 = '/api/v3/';
|
||||
export const apiV4 = '/api/v4/';
|
||||
export const apiV5 = '/api/v5/';
|
||||
export const gatewayApiV1 = '/api/gateway/v1/';
|
||||
export const gatewayApiV2 = '/api/gateway/v2/';
|
||||
export const apiAlertManager = '/api/alertmanager/';
|
||||
|
||||
@@ -19,6 +19,7 @@ import apiV1, {
|
||||
apiV2,
|
||||
apiV3,
|
||||
apiV4,
|
||||
apiV5,
|
||||
gatewayApiV1,
|
||||
gatewayApiV2,
|
||||
} from './apiV1';
|
||||
@@ -171,6 +172,18 @@ ApiV4Instance.interceptors.response.use(
|
||||
ApiV4Instance.interceptors.request.use(interceptorsRequestResponse);
|
||||
//
|
||||
|
||||
// axios V5
|
||||
export const ApiV5Instance = axios.create({
|
||||
baseURL: `${ENVIRONMENT.baseURL}${apiV5}`,
|
||||
});
|
||||
|
||||
ApiV5Instance.interceptors.response.use(
|
||||
interceptorsResponse,
|
||||
interceptorRejected,
|
||||
);
|
||||
ApiV5Instance.interceptors.request.use(interceptorsRequestResponse);
|
||||
//
|
||||
|
||||
// axios Base
|
||||
export const ApiBaseInstance = axios.create({
|
||||
baseURL: `${ENVIRONMENT.baseURL}${apiV1}`,
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
.ant-drawer-close {
|
||||
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 ENTITY_VERSION_V4 = 'v4';
|
||||
export const ENTITY_VERSION_V5 = 'v5';
|
||||
|
||||
@@ -46,4 +46,5 @@ export enum QueryParams {
|
||||
msgSystem = 'msgSystem',
|
||||
destination = 'destination',
|
||||
kindString = 'kindString',
|
||||
selectedExplorerView = 'selectedExplorerView',
|
||||
}
|
||||
|
||||
@@ -69,7 +69,7 @@ function Download({ data, isLoading, fileName }: DownloadProps): JSX.Element {
|
||||
}
|
||||
>
|
||||
<Button
|
||||
className="periscope-btn"
|
||||
className="periscope-btn ghost"
|
||||
loading={isLoading}
|
||||
icon={<FileDown size={14} />}
|
||||
/>
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
.explorer-options-container {
|
||||
position: fixed;
|
||||
bottom: 24px;
|
||||
bottom: 8px;
|
||||
left: calc(50% + 240px);
|
||||
transform: translate(calc(-50% - 120px), 0);
|
||||
transition: left 0.2s linear;
|
||||
|
||||
border-radius: 6px;
|
||||
background: var(--Ink-300, #16181d);
|
||||
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
background-color: transparent;
|
||||
|
||||
.multi-alert-button,
|
||||
@@ -32,19 +34,15 @@
|
||||
.explorer-update {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 10px 10px;
|
||||
border-radius: 50px;
|
||||
border: 1px solid var(--bg-slate-400);
|
||||
background: rgba(22, 24, 29, 0.6);
|
||||
backdrop-filter: blur(20px);
|
||||
gap: 4px;
|
||||
padding: 8px;
|
||||
background: var(--Ink-300, #16181d);
|
||||
|
||||
.action-icon {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 8px;
|
||||
border-radius: 50px;
|
||||
padding: 6px;
|
||||
border: 1px solid var(--bg-slate-400);
|
||||
background: var(--bg-slate-500);
|
||||
cursor: pointer;
|
||||
@@ -64,10 +62,8 @@
|
||||
|
||||
.explorer-options {
|
||||
padding: 10px 12px;
|
||||
border: 1px solid var(--bg-slate-400);
|
||||
border-radius: 50px;
|
||||
background: rgba(22, 24, 29, 0.6);
|
||||
backdrop-filter: blur(20px);
|
||||
background: var(--Ink-300, #16181d);
|
||||
border-radius: 2px;
|
||||
|
||||
cursor: default;
|
||||
display: flex;
|
||||
@@ -96,27 +92,6 @@
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
button {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border: none;
|
||||
|
||||
border: 1px solid #1d2023;
|
||||
|
||||
box-shadow: none !important;
|
||||
|
||||
&.ant-btn-round {
|
||||
padding: 8px 12px 8px 10px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
&.ant-btn-round:disabled {
|
||||
background-color: rgba(209, 209, 209, 0.074);
|
||||
color: #5f5f5f;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-select-focused {
|
||||
border-color: transparent !important;
|
||||
|
||||
@@ -257,6 +232,10 @@
|
||||
}
|
||||
|
||||
.lightMode {
|
||||
.explorer-options-container {
|
||||
background: var(--bg-vanilla-100);
|
||||
}
|
||||
|
||||
.explorer-options {
|
||||
background: transparent;
|
||||
box-shadow: none;
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
/* eslint-disable react/jsx-props-no-spreading */
|
||||
import './ExplorerOptions.styles.scss';
|
||||
|
||||
import { InfoCircleOutlined } from '@ant-design/icons';
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import {
|
||||
Button,
|
||||
ColorPicker,
|
||||
Divider,
|
||||
Input,
|
||||
Modal,
|
||||
RefSelectProps,
|
||||
@@ -41,14 +39,7 @@ import { useHandleExplorerTabChange } from 'hooks/useHandleExplorerTabChange';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import { mapCompositeQueryFromQuery } from 'lib/newQueryBuilder/queryBuilderMappers/mapCompositeQueryFromQuery';
|
||||
import { cloneDeep, isEqual, omit } from 'lodash-es';
|
||||
import {
|
||||
Check,
|
||||
ConciergeBell,
|
||||
Disc3,
|
||||
PanelBottomClose,
|
||||
Plus,
|
||||
X,
|
||||
} from 'lucide-react';
|
||||
import { Check, ConciergeBell, Disc3, Plus, X } from 'lucide-react';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
import {
|
||||
CSSProperties,
|
||||
@@ -73,10 +64,8 @@ import ExplorerOptionsHideArea from './ExplorerOptionsHideArea';
|
||||
import { PreservedViewsInLocalStorage } from './types';
|
||||
import {
|
||||
DATASOURCE_VS_ROUTES,
|
||||
generateRGBAFromHex,
|
||||
getRandomColor,
|
||||
saveNewViewHandler,
|
||||
setExplorerToolBarVisibility,
|
||||
} from './utils';
|
||||
|
||||
const allowedRoles = [USER_ROLES.ADMIN, USER_ROLES.AUTHOR, USER_ROLES.EDITOR];
|
||||
@@ -241,12 +230,6 @@ function ExplorerOptions({
|
||||
const extraData = viewsData?.data?.data?.find((view) => view.id === viewKey)
|
||||
?.extraData;
|
||||
|
||||
const extraDataColor = extraData ? JSON.parse(extraData).color : '';
|
||||
const rgbaColor = generateRGBAFromHex(
|
||||
extraDataColor || Color.BG_SIENNA_500,
|
||||
0.08,
|
||||
);
|
||||
|
||||
const { options, handleOptionsChange } = useOptionsMenu({
|
||||
storageKey:
|
||||
sourcepage === DataSource.TRACES
|
||||
@@ -552,13 +535,6 @@ function ExplorerOptions({
|
||||
[isDarkMode],
|
||||
);
|
||||
|
||||
const hideToolbar = (): void => {
|
||||
setExplorerToolBarVisibility(false, sourcepage);
|
||||
if (setIsExplorerOptionHidden) {
|
||||
setIsExplorerOptionHidden(true);
|
||||
}
|
||||
};
|
||||
|
||||
const isEditDeleteSupported = allowedRoles.includes(user.role as string);
|
||||
|
||||
const [
|
||||
@@ -610,27 +586,6 @@ function ExplorerOptions({
|
||||
viewsData?.data?.data,
|
||||
]);
|
||||
|
||||
const infoIconText = useMemo(() => {
|
||||
if (isLogsExplorer) {
|
||||
return 'Learn more about Logs explorer';
|
||||
}
|
||||
if (isMetricsExplorer) {
|
||||
return 'Learn more about Metrics explorer';
|
||||
}
|
||||
return 'Learn more about Traces explorer';
|
||||
}, [isLogsExplorer, isMetricsExplorer]);
|
||||
|
||||
const infoIconLink = useMemo(() => {
|
||||
if (isLogsExplorer) {
|
||||
return 'https://signoz.io/docs/product-features/logs-explorer/?utm_source=product&utm_medium=logs-explorer-toolbar';
|
||||
}
|
||||
// TODO: Add metrics explorer info icon link
|
||||
if (isMetricsExplorer) {
|
||||
return '';
|
||||
}
|
||||
return 'https://signoz.io/docs/product-features/trace-explorer/?utm_source=product&utm_medium=trace-explorer-toolbar';
|
||||
}, [isLogsExplorer, isMetricsExplorer]);
|
||||
|
||||
const getQueryName = (query: Query): string => {
|
||||
if (query.builder.queryFormulas.length > 0) {
|
||||
return `Formula ${query.builder.queryFormulas[0].queryName}`;
|
||||
@@ -643,11 +598,10 @@ function ExplorerOptions({
|
||||
const selectLabel = (
|
||||
<Button
|
||||
disabled={disabled}
|
||||
shape="round"
|
||||
className="periscope-btn ghost"
|
||||
shape="default"
|
||||
icon={<ConciergeBell size={16} />}
|
||||
>
|
||||
Create an Alert
|
||||
</Button>
|
||||
/>
|
||||
);
|
||||
return (
|
||||
<Select
|
||||
@@ -676,12 +630,11 @@ function ExplorerOptions({
|
||||
return (
|
||||
<Button
|
||||
disabled={disabled}
|
||||
shape="round"
|
||||
shape="default"
|
||||
className="periscope-btn ghost"
|
||||
onClick={(): void => onCreateAlertsHandler(query)}
|
||||
icon={<ConciergeBell size={16} />}
|
||||
>
|
||||
Create an Alert
|
||||
</Button>
|
||||
/>
|
||||
);
|
||||
}, [
|
||||
disabled,
|
||||
@@ -695,14 +648,11 @@ function ExplorerOptions({
|
||||
if (isOneChartPerQuery) {
|
||||
const selectLabel = (
|
||||
<Button
|
||||
type="primary"
|
||||
className="periscope-btn ghost"
|
||||
disabled={disabled}
|
||||
shape="round"
|
||||
onClick={onAddToDashboard}
|
||||
icon={<Plus size={16} />}
|
||||
>
|
||||
Add to Dashboard
|
||||
</Button>
|
||||
icon={<Plus size={12} />}
|
||||
/>
|
||||
);
|
||||
return (
|
||||
<Select
|
||||
@@ -734,14 +684,11 @@ function ExplorerOptions({
|
||||
}
|
||||
return (
|
||||
<Button
|
||||
type="primary"
|
||||
className="periscope-btn ghost"
|
||||
disabled={disabled}
|
||||
shape="round"
|
||||
onClick={onAddToDashboard}
|
||||
icon={<Plus size={16} />}
|
||||
>
|
||||
Add to Dashboard
|
||||
</Button>
|
||||
/>
|
||||
);
|
||||
}, [disabled, isOneChartPerQuery, onAddToDashboard, splitedQueries]);
|
||||
|
||||
@@ -760,41 +707,31 @@ function ExplorerOptions({
|
||||
>
|
||||
<Tooltip title="Clear this view" placement="top">
|
||||
<Button
|
||||
className="action-icon"
|
||||
className="periscope-btn ghost"
|
||||
onClick={handleClearSelect}
|
||||
icon={<X size={14} />}
|
||||
icon={<X size={16} />}
|
||||
/>
|
||||
</Tooltip>
|
||||
{
|
||||
// only show the update view option when the query is updated
|
||||
}
|
||||
{isQueryUpdated && (
|
||||
<>
|
||||
<Divider
|
||||
type="vertical"
|
||||
className={isEditDeleteSupported ? '' : 'hidden'}
|
||||
<Tooltip title="Update this view" placement="top">
|
||||
<Button
|
||||
className={cx(
|
||||
'periscope-btn ghost',
|
||||
isEditDeleteSupported ? '' : 'hidden',
|
||||
)}
|
||||
disabled={isViewUpdating}
|
||||
onClick={onUpdateQueryHandler}
|
||||
icon={<Disc3 size={16} />}
|
||||
/>
|
||||
<Tooltip title="Update this view" placement="top">
|
||||
<Button
|
||||
className={cx('action-icon', isEditDeleteSupported ? ' ' : 'hidden')}
|
||||
disabled={isViewUpdating}
|
||||
onClick={onUpdateQueryHandler}
|
||||
icon={<Disc3 size={14} />}
|
||||
/>
|
||||
</Tooltip>
|
||||
</>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{!isExplorerOptionHidden && (
|
||||
<div
|
||||
className="explorer-options"
|
||||
style={{
|
||||
background: extraData
|
||||
? `linear-gradient(90deg, rgba(0,0,0,0) -5%, ${rgbaColor} 9%, rgba(0,0,0,0) 30%)`
|
||||
: 'transparent',
|
||||
}}
|
||||
>
|
||||
<div className="explorer-options">
|
||||
<div className="view-options">
|
||||
<Select<string, { key: string; value: string }>
|
||||
showSearch
|
||||
@@ -835,49 +772,23 @@ function ExplorerOptions({
|
||||
</Select>
|
||||
|
||||
<Button
|
||||
shape="round"
|
||||
shape="default"
|
||||
className={cx(
|
||||
'periscope-btn secondary',
|
||||
isEditDeleteSupported ? '' : 'hidden',
|
||||
)}
|
||||
onClick={handleSaveViewModalToggle}
|
||||
className={isEditDeleteSupported ? '' : 'hidden'}
|
||||
disabled={viewsIsLoading || isRefetching}
|
||||
icon={<Disc3 size={16} />}
|
||||
icon={<Disc3 size={12} />}
|
||||
>
|
||||
Save this view
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<hr className={isEditDeleteSupported ? '' : 'hidden'} />
|
||||
|
||||
<div className={cx('actions', isEditDeleteSupported ? '' : 'hidden')}>
|
||||
{alertButton}
|
||||
{dashboardButton}
|
||||
</div>
|
||||
<div className="actions">
|
||||
{/* Hide the info icon for metrics explorer until we get the docs link */}
|
||||
{!isMetricsExplorer && (
|
||||
<Tooltip
|
||||
title={
|
||||
<div>
|
||||
{infoIconText}
|
||||
<Typography.Link href={infoIconLink} target="_blank">
|
||||
{' '}
|
||||
here
|
||||
</Typography.Link>{' '}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<InfoCircleOutlined className="info-icon" />
|
||||
</Tooltip>
|
||||
)}
|
||||
<Tooltip title="Hide">
|
||||
<Button
|
||||
disabled={disabled}
|
||||
shape="circle"
|
||||
onClick={hideToolbar}
|
||||
icon={<PanelBottomClose size={16} />}
|
||||
data-testid="hide-toolbar"
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<ExplorerOptionsHideArea
|
||||
|
||||
@@ -2,7 +2,7 @@ import './ChartPreview.styles.scss';
|
||||
|
||||
import { InfoCircleOutlined } from '@ant-design/icons';
|
||||
import Spinner from 'components/Spinner';
|
||||
import { DEFAULT_ENTITY_VERSION } from 'constants/app';
|
||||
import { ENTITY_VERSION_V5 } from 'constants/app';
|
||||
import { FeatureKeys } from 'constants/features';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
|
||||
@@ -144,7 +144,8 @@ function ChartPreview({
|
||||
},
|
||||
originalGraphType: graphType,
|
||||
},
|
||||
alertDef?.version || DEFAULT_ENTITY_VERSION,
|
||||
// alertDef?.version || DEFAULT_ENTITY_VERSION,
|
||||
ENTITY_VERSION_V5,
|
||||
{
|
||||
queryKey: [
|
||||
'chartPreview',
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
.alert-tabs {
|
||||
padding: 0px 8px;
|
||||
|
||||
.ant-tabs-tab {
|
||||
border: none !important;
|
||||
margin-left: 0px !important;
|
||||
@@ -48,6 +50,30 @@
|
||||
}
|
||||
}
|
||||
|
||||
.alert-query-section-container {
|
||||
.ant-card-body {
|
||||
padding: 0px;
|
||||
|
||||
.alert-tabs {
|
||||
padding: 0px;
|
||||
|
||||
.ant-tabs {
|
||||
.ant-tabs-nav {
|
||||
padding: 8px 0;
|
||||
|
||||
.ant-tabs-nav-wrap {
|
||||
padding: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-tabs-extra-content {
|
||||
padding: 0px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.lightMode {
|
||||
.alert-tabs {
|
||||
.ant-tabs-nav-list {
|
||||
|
||||
@@ -4,11 +4,11 @@ import { Color } from '@signozhq/design-tokens';
|
||||
import { Button, Tabs, Tooltip } from 'antd';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import PromQLIcon from 'assets/Dashboard/PromQl';
|
||||
import { QueryBuilderV2 } from 'components/QueryBuilderV2/QueryBuilderV2';
|
||||
import { ALERTS_DATA_SOURCE_MAP } from 'constants/alerts';
|
||||
import { ENTITY_VERSION_V4 } from 'constants/app';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { QBShortcuts } from 'constants/shortcuts/QBShortcuts';
|
||||
import { QueryBuilder } from 'container/QueryBuilder';
|
||||
import { useKeyboardHotkeys } from 'hooks/hotkeys/useKeyboardHotkeys';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import { isEmpty } from 'lodash-es';
|
||||
@@ -48,7 +48,7 @@ function QuerySection({
|
||||
const isDarkMode = useIsDarkMode();
|
||||
|
||||
const renderMetricUI = (): JSX.Element => (
|
||||
<QueryBuilder
|
||||
<QueryBuilderV2
|
||||
panelType={panelType}
|
||||
config={{
|
||||
queryVariant: 'static',
|
||||
@@ -144,7 +144,7 @@ function QuerySection({
|
||||
<div className="alert-tabs">
|
||||
<Tabs
|
||||
type="card"
|
||||
style={{ width: '100%' }}
|
||||
style={{ width: '100%', padding: '0px 8px' }}
|
||||
defaultActiveKey={currentTab}
|
||||
activeKey={currentTab}
|
||||
onChange={handleQueryCategoryChange}
|
||||
@@ -178,7 +178,7 @@ function QuerySection({
|
||||
<div className="alert-tabs">
|
||||
<Tabs
|
||||
type="card"
|
||||
style={{ width: '100%' }}
|
||||
style={{ width: '100%', padding: '0px 8px' }}
|
||||
defaultActiveKey={currentTab}
|
||||
activeKey={currentTab}
|
||||
onChange={handleQueryCategoryChange}
|
||||
@@ -215,7 +215,7 @@ function QuerySection({
|
||||
return (
|
||||
<>
|
||||
<StepHeading> {t('alert_form_step2')}</StepHeading>
|
||||
<FormContainer>
|
||||
<FormContainer className="alert-query-section-container">
|
||||
<div>{renderTabs(alertType)}</div>
|
||||
{renderQuerySection(currentTab)}
|
||||
</FormContainer>
|
||||
|
||||
@@ -4,7 +4,9 @@
|
||||
overflow-y: hidden;
|
||||
|
||||
.full-view-header-container {
|
||||
height: 40px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.graph-container {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable sonarjs/cognitive-complexity */
|
||||
import './WidgetFullView.styles.scss';
|
||||
|
||||
import {
|
||||
@@ -8,11 +9,14 @@ import {
|
||||
import { Button, Input, Spin } from 'antd';
|
||||
import cx from 'classnames';
|
||||
import { ToggleGraphProps } from 'components/Graph/types';
|
||||
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
|
||||
import { QueryBuilderV2 } from 'components/QueryBuilderV2/QueryBuilderV2';
|
||||
import Spinner from 'components/Spinner';
|
||||
import TimePreference from 'components/TimePreferenceDropDown';
|
||||
import { DEFAULT_ENTITY_VERSION } from 'constants/app';
|
||||
import { ENTITY_VERSION_V5 } from 'constants/app';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import useDrilldown from 'container/GridCardLayout/GridCard/FullView/useDrilldown';
|
||||
import {
|
||||
timeItems,
|
||||
timePreferance,
|
||||
@@ -51,6 +55,7 @@ function FullView({
|
||||
onClickHandler,
|
||||
customOnDragSelect,
|
||||
setCurrentGraphRef,
|
||||
enableDrillDown = false,
|
||||
}: FullViewProps): JSX.Element {
|
||||
const { safeNavigate } = useSafeNavigate();
|
||||
const { selectedTime: globalSelectedTime } = useSelector<
|
||||
@@ -113,6 +118,13 @@ function FullView({
|
||||
};
|
||||
});
|
||||
|
||||
const { dashboardEditView } = useDrilldown({
|
||||
enableDrillDown,
|
||||
widget,
|
||||
setRequestData,
|
||||
selectedDashboard,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
setRequestData((prev) => ({
|
||||
...prev,
|
||||
@@ -122,7 +134,8 @@ function FullView({
|
||||
|
||||
const response = useGetQueryRange(
|
||||
requestData,
|
||||
selectedDashboard?.data?.version || version || DEFAULT_ENTITY_VERSION,
|
||||
// selectedDashboard?.data?.version || version || DEFAULT_ENTITY_VERSION,
|
||||
ENTITY_VERSION_V5,
|
||||
{
|
||||
queryKey: [widget?.query, widget?.panelTypes, requestData, version],
|
||||
enabled: !isDependedDataLoaded,
|
||||
@@ -196,71 +209,98 @@ function FullView({
|
||||
|
||||
return (
|
||||
<div className="full-view-container">
|
||||
<div className="full-view-header-container">
|
||||
{fullViewOptions && (
|
||||
<TimeContainer $panelType={widget.panelTypes}>
|
||||
{response.isFetching && (
|
||||
<Spin spinning indicator={<LoadingOutlined spin />} />
|
||||
<OverlayScrollbar>
|
||||
<>
|
||||
<div className="full-view-header-container">
|
||||
{fullViewOptions && (
|
||||
<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 && (
|
||||
<Spin spinning indicator={<LoadingOutlined spin />} />
|
||||
)}
|
||||
<TimePreference
|
||||
selectedTime={selectedTime}
|
||||
setSelectedTime={setSelectedTime}
|
||||
/>
|
||||
<Button
|
||||
style={{
|
||||
marginLeft: '4px',
|
||||
}}
|
||||
onClick={(): void => {
|
||||
response.refetch();
|
||||
}}
|
||||
type="primary"
|
||||
icon={<SyncOutlined />}
|
||||
/>
|
||||
</div>
|
||||
</TimeContainer>
|
||||
)}
|
||||
<TimePreference
|
||||
selectedTime={selectedTime}
|
||||
setSelectedTime={setSelectedTime}
|
||||
/>
|
||||
<Button
|
||||
style={{
|
||||
marginLeft: '4px',
|
||||
}}
|
||||
onClick={(): void => {
|
||||
response.refetch();
|
||||
}}
|
||||
type="primary"
|
||||
icon={<SyncOutlined />}
|
||||
/>
|
||||
</TimeContainer>
|
||||
)}
|
||||
</div>
|
||||
{enableDrillDown && (
|
||||
<QueryBuilderV2
|
||||
panelType={widget.panelTypes}
|
||||
version={selectedDashboard?.data?.version || 'v3'}
|
||||
isListViewPanel={widget.panelTypes === PANEL_TYPES.LIST}
|
||||
// filterConfigs={filterConfigs}
|
||||
// queryComponents={queryComponents}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={cx('graph-container', {
|
||||
disabled: isDashboardLocked,
|
||||
'height-widget': widget?.mergeAllActiveQueries || widget?.stackedBarChart,
|
||||
'list-graph-container': isListView,
|
||||
})}
|
||||
ref={fullViewRef}
|
||||
>
|
||||
<GraphContainer
|
||||
style={{
|
||||
height: isListView ? '100%' : '90%',
|
||||
}}
|
||||
isGraphLegendToggleAvailable={canModifyChart}
|
||||
>
|
||||
{isTablePanel && (
|
||||
<Input
|
||||
addonBefore={<SearchOutlined size={14} />}
|
||||
className="global-search"
|
||||
placeholder="Search..."
|
||||
allowClear
|
||||
key={widget.id}
|
||||
onChange={(e): void => {
|
||||
setSearchTerm(e.target.value || '');
|
||||
<div
|
||||
className={cx('graph-container', {
|
||||
disabled: isDashboardLocked,
|
||||
'height-widget':
|
||||
widget?.mergeAllActiveQueries || widget?.stackedBarChart,
|
||||
'list-graph-container': isListView,
|
||||
})}
|
||||
ref={fullViewRef}
|
||||
>
|
||||
<GraphContainer
|
||||
style={{
|
||||
height: isListView ? '100%' : '90%',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<PanelWrapper
|
||||
queryResponse={response}
|
||||
widget={widget}
|
||||
setRequestData={setRequestData}
|
||||
isFullViewMode
|
||||
onToggleModelHandler={onToggleModelHandler}
|
||||
setGraphVisibility={setGraphsVisibilityStates}
|
||||
graphVisibility={graphsVisibilityStates}
|
||||
onDragSelect={customOnDragSelect ?? onDragSelect}
|
||||
tableProcessedDataRef={tableProcessedDataRef}
|
||||
searchTerm={searchTerm}
|
||||
onClickHandler={onClickHandler}
|
||||
/>
|
||||
</GraphContainer>
|
||||
</div>
|
||||
isGraphLegendToggleAvailable={canModifyChart}
|
||||
>
|
||||
{isTablePanel && (
|
||||
<Input
|
||||
addonBefore={<SearchOutlined size={14} />}
|
||||
className="global-search"
|
||||
placeholder="Search..."
|
||||
allowClear
|
||||
key={widget.id}
|
||||
onChange={(e): void => {
|
||||
setSearchTerm(e.target.value || '');
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<PanelWrapper
|
||||
queryResponse={response}
|
||||
widget={widget}
|
||||
setRequestData={setRequestData}
|
||||
isFullViewMode
|
||||
onToggleModelHandler={onToggleModelHandler}
|
||||
setGraphVisibility={setGraphsVisibilityStates}
|
||||
graphVisibility={graphsVisibilityStates}
|
||||
onDragSelect={customOnDragSelect ?? onDragSelect}
|
||||
tableProcessedDataRef={tableProcessedDataRef}
|
||||
searchTerm={searchTerm}
|
||||
onClickHandler={onClickHandler}
|
||||
/>
|
||||
</GraphContainer>
|
||||
</div>
|
||||
</>
|
||||
</OverlayScrollbar>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ export const NotFoundContainer = styled.div`
|
||||
export const TimeContainer = styled.div<Props>`
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 16px;
|
||||
align-items: center;
|
||||
${({ $panelType }): FlattenSimpleInterpolation =>
|
||||
$panelType === PANEL_TYPES.TABLE
|
||||
@@ -25,6 +26,10 @@ export const TimeContainer = styled.div<Props>`
|
||||
margin-bottom: 1rem;
|
||||
`
|
||||
: css``}
|
||||
|
||||
.time-container {
|
||||
display: flex;
|
||||
}
|
||||
`;
|
||||
|
||||
export const GraphContainer = styled.div<GraphContainerProps>`
|
||||
|
||||
@@ -59,6 +59,7 @@ export interface FullViewProps {
|
||||
isDependedDataLoaded?: boolean;
|
||||
onToggleModelHandler?: GraphManagerProps['onToggleModelHandler'];
|
||||
setCurrentGraphRef: Dispatch<SetStateAction<RefObject<HTMLDivElement> | null>>;
|
||||
enableDrillDown?: boolean;
|
||||
}
|
||||
|
||||
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 { Props } from 'types/api/dashboard/update';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
import { shouldEnableDrilldown } from 'utils/dashboard/generateExportToDashboardLink';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import { useGraphClickToShowButton } from '../useGraphClickToShowButton';
|
||||
@@ -226,6 +227,7 @@ function WidgetGraphComponent({
|
||||
const onToggleModelHandler = (): void => {
|
||||
const existingSearchParams = new URLSearchParams(search);
|
||||
existingSearchParams.delete(QueryParams.expandedWidgetId);
|
||||
existingSearchParams.delete(QueryParams.compositeQuery);
|
||||
const updatedQueryParams = Object.fromEntries(existingSearchParams.entries());
|
||||
if (queryResponse.data?.payload) {
|
||||
const {
|
||||
@@ -354,6 +356,7 @@ function WidgetGraphComponent({
|
||||
onClickHandler={onClickHandler ?? graphClickHandler}
|
||||
customOnDragSelect={customOnDragSelect}
|
||||
setCurrentGraphRef={setCurrentGraphRef}
|
||||
enableDrillDown={shouldEnableDrilldown(widget.panelTypes)}
|
||||
/>
|
||||
</Modal>
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
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 { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { CustomTimeType } from 'container/TopNav/DateTimeSelectionV2/config';
|
||||
@@ -209,7 +209,8 @@ function GridCardGraph({
|
||||
end: customTimeRange?.endTime || end,
|
||||
originalGraphType: widget?.panelTypes,
|
||||
},
|
||||
version || DEFAULT_ENTITY_VERSION,
|
||||
ENTITY_VERSION_V5,
|
||||
// version || DEFAULT_ENTITY_VERSION,
|
||||
{
|
||||
queryKey: [
|
||||
maxTime,
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
.qb-search-view-container {
|
||||
padding: 8px 16px;
|
||||
border-top: 1px solid var(--bg-slate-400, #1d212d);
|
||||
border-bottom: 1px solid var(--bg-slate-400, #1d212d);
|
||||
|
||||
.ant-select-selector {
|
||||
border-radius: 2px;
|
||||
@@ -20,9 +18,6 @@
|
||||
|
||||
.lightMode {
|
||||
.qb-search-view-container {
|
||||
border-top: 1px solid var(--bg-vanilla-300);
|
||||
border-bottom: 1px solid var(--bg-vanilla-300);
|
||||
|
||||
.ant-select-selector {
|
||||
border-color: var(--bg-vanilla-300) !important;
|
||||
background-color: var(--bg-vanilla-100) !important;
|
||||
|
||||
@@ -1,45 +1,38 @@
|
||||
import './LogsExplorerQuerySection.styles.scss';
|
||||
|
||||
import { QueryBuilderV2 } from 'components/QueryBuilderV2/QueryBuilderV2';
|
||||
import {
|
||||
initialQueriesMap,
|
||||
OPERATORS,
|
||||
PANEL_TYPES,
|
||||
} from 'constants/queryBuilder';
|
||||
import ExplorerOrderBy from 'container/ExplorerOrderBy';
|
||||
import { QueryBuilder } from 'container/QueryBuilder';
|
||||
import { OrderByFilterProps } from 'container/QueryBuilder/filters/OrderByFilter/OrderByFilter.interfaces';
|
||||
import QueryBuilderSearchV2 from 'container/QueryBuilder/filters/QueryBuilderSearchV2/QueryBuilderSearchV2';
|
||||
import { QueryBuilderProps } from 'container/QueryBuilder/QueryBuilder.interfaces';
|
||||
import { useGetPanelTypesQueryParam } from 'hooks/queryBuilder/useGetPanelTypesQueryParam';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { useQueryOperations } from 'hooks/queryBuilder/useQueryBuilderOperations';
|
||||
import { useShareBuilderUrl } from 'hooks/queryBuilder/useShareBuilderUrl';
|
||||
import {
|
||||
prepareQueryWithDefaultTimestamp,
|
||||
SELECTED_VIEWS,
|
||||
} from 'pages/LogsExplorer/utils';
|
||||
import { ExplorerViews } from 'pages/LogsExplorer/utils';
|
||||
import { memo, useCallback, useMemo } from 'react';
|
||||
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
function LogExplorerQuerySection({
|
||||
selectedView,
|
||||
}: {
|
||||
selectedView: SELECTED_VIEWS;
|
||||
selectedView: ExplorerViews;
|
||||
}): JSX.Element {
|
||||
const { currentQuery, updateAllQueriesOperators } = useQueryBuilder();
|
||||
|
||||
const query = currentQuery?.builder?.queryData[0] || null;
|
||||
const { updateAllQueriesOperators } = useQueryBuilder();
|
||||
|
||||
const panelTypes = useGetPanelTypesQueryParam(PANEL_TYPES.LIST);
|
||||
const defaultValue = useMemo(() => {
|
||||
const updatedQuery = updateAllQueriesOperators(
|
||||
initialQueriesMap.logs,
|
||||
PANEL_TYPES.LIST,
|
||||
DataSource.LOGS,
|
||||
);
|
||||
return prepareQueryWithDefaultTimestamp(updatedQuery);
|
||||
}, [updateAllQueriesOperators]);
|
||||
const defaultValue = useMemo(
|
||||
() =>
|
||||
updateAllQueriesOperators(
|
||||
initialQueriesMap.logs,
|
||||
PANEL_TYPES.LIST,
|
||||
DataSource.LOGS,
|
||||
),
|
||||
[updateAllQueriesOperators],
|
||||
);
|
||||
|
||||
useShareBuilderUrl(defaultValue);
|
||||
|
||||
@@ -58,13 +51,6 @@ function LogExplorerQuerySection({
|
||||
return config;
|
||||
}, [panelTypes]);
|
||||
|
||||
const { handleChangeQueryData } = useQueryOperations({
|
||||
index: 0,
|
||||
query,
|
||||
filterConfigs,
|
||||
entityVersion: '',
|
||||
});
|
||||
|
||||
const renderOrderBy = useCallback(
|
||||
({ query, onChange }: OrderByFilterProps): JSX.Element => (
|
||||
<ExplorerOrderBy query={query} onChange={onChange} />
|
||||
@@ -79,35 +65,16 @@ function LogExplorerQuerySection({
|
||||
[panelTypes, renderOrderBy],
|
||||
);
|
||||
|
||||
const handleChangeTagFilters = useCallback(
|
||||
(value: IBuilderQuery['filters']) => {
|
||||
handleChangeQueryData('filters', value);
|
||||
},
|
||||
[handleChangeQueryData],
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{selectedView === SELECTED_VIEWS.SEARCH && (
|
||||
<div className="qb-search-view-container">
|
||||
<QueryBuilderSearchV2
|
||||
query={query}
|
||||
onChange={handleChangeTagFilters}
|
||||
whereClauseConfig={filterConfigs?.filters}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{selectedView === SELECTED_VIEWS.QUERY_BUILDER && (
|
||||
<QueryBuilder
|
||||
panelType={panelTypes}
|
||||
config={{ initialDataSource: DataSource.LOGS, queryVariant: 'static' }}
|
||||
filterConfigs={filterConfigs}
|
||||
queryComponents={queryComponents}
|
||||
version="v3" // setting this to v3 as we this is rendered in logs explorer
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
<QueryBuilderV2
|
||||
isListViewPanel={panelTypes === PANEL_TYPES.LIST}
|
||||
config={{ initialDataSource: DataSource.LOGS, queryVariant: 'static' }}
|
||||
panelType={panelTypes}
|
||||
filterConfigs={filterConfigs}
|
||||
queryComponents={queryComponents}
|
||||
showOnlyWhereClause={selectedView === ExplorerViews.LIST}
|
||||
version="v3" // setting this to v3 as we this is rendered in logs explorer
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -45,7 +45,6 @@ function LogsExplorerList({
|
||||
}: LogsExplorerListProps): JSX.Element {
|
||||
const ref = useRef<VirtuosoHandle>(null);
|
||||
const { initialDataSource } = useQueryBuilder();
|
||||
|
||||
const { activeLogId } = useCopyLogLink();
|
||||
|
||||
const {
|
||||
|
||||
@@ -76,14 +76,92 @@
|
||||
border: 1px solid var(--bg-slate-400);
|
||||
}
|
||||
|
||||
.order-by-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.format-options-container {
|
||||
position: relative;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.logs-actions-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
height: 40px;
|
||||
padding: 4px 8px;
|
||||
width: 100%;
|
||||
|
||||
border-top: 1px solid var(--Slate-500, #161922);
|
||||
border-bottom: 1px solid var(--Slate-500, #161922);
|
||||
box-shadow: 0px 8px 6px 0px #0b0c0e;
|
||||
|
||||
.tab-options {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
.tab-options-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.tab-options-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
|
||||
.order-by-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
.order-by-label {
|
||||
color: var(--text-vanilla-400);
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 16px; /* 133.333% */
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.order-by-select {
|
||||
width: 100px;
|
||||
|
||||
.ant-select-selector {
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.frequency-chart-view-controller {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.query-stats {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
|
||||
align-self: flex-end;
|
||||
|
||||
.rows {
|
||||
color: var(--bg-vanilla-400);
|
||||
font-family: 'Geist Mono';
|
||||
@@ -110,13 +188,6 @@
|
||||
letter-spacing: 0.36px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.logs-actions-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
.ant-btn {
|
||||
border: none;
|
||||
@@ -186,6 +257,14 @@
|
||||
background: var(--bg-robin-400);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.logs-actions-container {
|
||||
.tab-options {
|
||||
border-top: 1px solid var(--text-vanilla-300);
|
||||
border-bottom: 1px solid var(--text-vanilla-300);
|
||||
}
|
||||
|
||||
.query-stats {
|
||||
.rows {
|
||||
color: var(--bg-ink-400);
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
/* eslint-disable sonarjs/cognitive-complexity */
|
||||
import './LogsExplorerViews.styles.scss';
|
||||
|
||||
import { Button, Typography } from 'antd';
|
||||
import { Button, Select, Switch, Typography } from 'antd';
|
||||
import { getQueryStats, WsDataEvent } from 'api/common/getQueryStats';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { getYAxisFormattedValue } from 'components/Graph/yAxisConfig';
|
||||
import LogsFormatOptionsMenu from 'components/LogsFormatOptionsMenu/LogsFormatOptionsMenu';
|
||||
import { ENTITY_VERSION_V4 } from 'constants/app';
|
||||
import { ENTITY_VERSION_V5 } from 'constants/app';
|
||||
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
|
||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||
import { AVAILABLE_EXPORT_PANEL_TYPES } from 'constants/panelTypes';
|
||||
@@ -46,8 +46,8 @@ import {
|
||||
omit,
|
||||
set,
|
||||
} from 'lodash-es';
|
||||
import { Sliders } from 'lucide-react';
|
||||
import { SELECTED_VIEWS } from 'pages/LogsExplorer/utils';
|
||||
import { ArrowUp10, Minus, Sliders } from 'lucide-react';
|
||||
import { ExplorerViews } from 'pages/LogsExplorer/utils';
|
||||
import { useTimezone } from 'providers/Timezone';
|
||||
import {
|
||||
memo,
|
||||
@@ -79,15 +79,13 @@ import { v4 } from 'uuid';
|
||||
|
||||
import QueryStatus from './QueryStatus';
|
||||
|
||||
function LogsExplorerViews({
|
||||
function LogsExplorerViewsContainer({
|
||||
selectedView,
|
||||
showFrequencyChart,
|
||||
setIsLoadingQueries,
|
||||
listQueryKeyRef,
|
||||
chartQueryKeyRef,
|
||||
}: {
|
||||
selectedView: SELECTED_VIEWS;
|
||||
showFrequencyChart: boolean;
|
||||
selectedView: ExplorerViews;
|
||||
setIsLoadingQueries: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
listQueryKeyRef: MutableRefObject<any>;
|
||||
@@ -95,6 +93,7 @@ function LogsExplorerViews({
|
||||
chartQueryKeyRef: MutableRefObject<any>;
|
||||
}): JSX.Element {
|
||||
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.
|
||||
const panelTypes = useGetPanelTypesQueryParam(PANEL_TYPES.LIST);
|
||||
@@ -136,6 +135,8 @@ function LogsExplorerViews({
|
||||
const [queryId, setQueryId] = useState<string>(v4());
|
||||
const [queryStats, setQueryStats] = useState<WsDataEvent>();
|
||||
|
||||
const [orderDirection, setOrderDirection] = useState<string>('desc');
|
||||
|
||||
const listQuery = useMemo(() => {
|
||||
if (!stagedQuery || stagedQuery.builder.queryData.length < 1) return null;
|
||||
|
||||
@@ -234,15 +235,6 @@ function LogsExplorerViews({
|
||||
[currentQuery, selectedPanelType, updateAllQueriesOperators],
|
||||
);
|
||||
|
||||
const handleModeChange = (panelType: PANEL_TYPES): void => {
|
||||
if (selectedView === SELECTED_VIEWS.SEARCH) {
|
||||
handleSetConfig(panelType, DataSource.LOGS);
|
||||
}
|
||||
|
||||
setShowFormatMenuItems(false);
|
||||
handleExplorerTabChange(panelType);
|
||||
};
|
||||
|
||||
const {
|
||||
data: listChartData,
|
||||
isFetching: isFetchingListChartData,
|
||||
@@ -250,7 +242,8 @@ function LogsExplorerViews({
|
||||
} = useGetExplorerQueryRange(
|
||||
listChartQuery,
|
||||
PANEL_TYPES.TIME_SERIES,
|
||||
ENTITY_VERSION_V4,
|
||||
// ENTITY_VERSION_V4,
|
||||
ENTITY_VERSION_V5,
|
||||
{
|
||||
enabled: !!listChartQuery && panelType === PANEL_TYPES.LIST,
|
||||
},
|
||||
@@ -268,7 +261,8 @@ function LogsExplorerViews({
|
||||
} = useGetExplorerQueryRange(
|
||||
requestData,
|
||||
panelType,
|
||||
ENTITY_VERSION_V4,
|
||||
// ENTITY_VERSION_V4,
|
||||
ENTITY_VERSION_V5,
|
||||
{
|
||||
keepPreviousData: true,
|
||||
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[] =
|
||||
query.builder.queryData.length > 1
|
||||
? query.builder.queryData
|
||||
? query.builder.queryData.map((item) => ({
|
||||
...item,
|
||||
...(selectedPanelType !== PANEL_TYPES.LIST ? { order: [] } : {}),
|
||||
}))
|
||||
: [
|
||||
{
|
||||
...(listQuery || initialQueryBuilderFormValues),
|
||||
...paginateData,
|
||||
...(updatedFilters ? { filters: updatedFilters } : {}),
|
||||
...(selectedPanelType === PANEL_TYPES.LIST
|
||||
? { order: orderBy }
|
||||
: { order: [] }),
|
||||
},
|
||||
];
|
||||
|
||||
@@ -350,7 +356,7 @@ function LogsExplorerViews({
|
||||
|
||||
return data;
|
||||
},
|
||||
[listQuery, activeLogId],
|
||||
[activeLogId, orderDirection, listQuery, selectedPanelType],
|
||||
);
|
||||
|
||||
const handleEndReached = useCallback(() => {
|
||||
@@ -452,8 +458,7 @@ function LogsExplorerViews({
|
||||
|
||||
useEffect(() => {
|
||||
const shouldChangeView =
|
||||
(isMultipleQueries || isGroupByExist) &&
|
||||
selectedView !== SELECTED_VIEWS.SEARCH;
|
||||
(isMultipleQueries || isGroupByExist) && selectedView !== ExplorerViews.LIST;
|
||||
|
||||
if (selectedPanelType === PANEL_TYPES.LIST && shouldChangeView) {
|
||||
handleExplorerTabChange(PANEL_TYPES.TIME_SERIES);
|
||||
@@ -473,11 +478,7 @@ function LogsExplorerViews({
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
selectedView &&
|
||||
selectedView === SELECTED_VIEWS.SEARCH &&
|
||||
handleSetConfig
|
||||
) {
|
||||
if (selectedView && selectedView === ExplorerViews.LIST && handleSetConfig) {
|
||||
handleSetConfig(defaultTo(panelTypes, PANEL_TYPES.LIST), DataSource.LOGS);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
@@ -498,10 +499,19 @@ function LogsExplorerViews({
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [data]);
|
||||
|
||||
// Store previous orderDirection to detect changes
|
||||
const prevOrderDirectionRef = useRef(orderDirection);
|
||||
|
||||
useEffect(() => {
|
||||
const orderDirectionChanged =
|
||||
prevOrderDirectionRef.current !== orderDirection &&
|
||||
selectedPanelType === PANEL_TYPES.LIST;
|
||||
prevOrderDirectionRef.current = orderDirection;
|
||||
|
||||
if (
|
||||
requestData?.id !== stagedQuery?.id ||
|
||||
currentMinTimeRef.current !== minTime
|
||||
currentMinTimeRef.current !== minTime ||
|
||||
orderDirectionChanged
|
||||
) {
|
||||
const newRequestData = getRequestData(stagedQuery, {
|
||||
filters: listQuery?.filters || initialFilters,
|
||||
@@ -524,6 +534,8 @@ function LogsExplorerViews({
|
||||
activeLogId,
|
||||
panelType,
|
||||
selectedView,
|
||||
orderDirection,
|
||||
selectedPanelType,
|
||||
]);
|
||||
|
||||
const chartData = useMemo(() => {
|
||||
@@ -630,111 +642,107 @@ function LogsExplorerViews({
|
||||
|
||||
return (
|
||||
<div className="logs-explorer-views-container">
|
||||
{showFrequencyChart && (
|
||||
<LogsExplorerChart
|
||||
className="logs-histogram"
|
||||
isLoading={isFetchingListChartData || isLoadingListChartData}
|
||||
data={chartData}
|
||||
isLogsExplorerViews={panelType === PANEL_TYPES.LIST}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="logs-explorer-views-types">
|
||||
<div className="views-tabs-container">
|
||||
<Button.Group className="views-tabs">
|
||||
<Button
|
||||
value={PANEL_TYPES.LIST}
|
||||
className={
|
||||
// eslint-disable-next-line sonarjs/no-duplicate-string
|
||||
selectedPanelType === PANEL_TYPES.LIST ? 'selected_view tab' : 'tab'
|
||||
}
|
||||
disabled={
|
||||
(isMultipleQueries || isGroupByExist) && selectedView !== 'search'
|
||||
}
|
||||
onClick={(): void => handleModeChange(PANEL_TYPES.LIST)}
|
||||
data-testid="logs-list-view"
|
||||
>
|
||||
List view
|
||||
</Button>
|
||||
<Button
|
||||
value={PANEL_TYPES.TIME_SERIES}
|
||||
className={
|
||||
// eslint-disable-next-line sonarjs/no-duplicate-string
|
||||
selectedPanelType === PANEL_TYPES.TIME_SERIES
|
||||
? 'selected_view tab'
|
||||
: 'tab'
|
||||
}
|
||||
onClick={(): void => handleModeChange(PANEL_TYPES.TIME_SERIES)}
|
||||
data-testid="time-series-view"
|
||||
>
|
||||
Time series
|
||||
</Button>
|
||||
<Button
|
||||
value={PANEL_TYPES.TABLE}
|
||||
className={
|
||||
// eslint-disable-next-line sonarjs/no-duplicate-string
|
||||
selectedPanelType === PANEL_TYPES.TABLE ? 'selected_view tab' : 'tab'
|
||||
}
|
||||
onClick={(): void => handleModeChange(PANEL_TYPES.TABLE)}
|
||||
data-testid="table-view"
|
||||
>
|
||||
Table
|
||||
</Button>
|
||||
</Button.Group>
|
||||
<div className="logs-actions-container">
|
||||
{selectedPanelType === PANEL_TYPES.LIST && (
|
||||
<div className="tab-options">
|
||||
<Download
|
||||
data={flattenLogData}
|
||||
isLoading={isFetching}
|
||||
fileName="log_data"
|
||||
/>
|
||||
<div className="format-options-container" ref={menuRef}>
|
||||
<Button
|
||||
className="periscope-btn"
|
||||
onClick={handleToggleShowFormatOptions}
|
||||
icon={<Sliders size={14} />}
|
||||
data-testid="periscope-btn"
|
||||
<div className="logs-actions-container">
|
||||
<div className="tab-options">
|
||||
<div className="tab-options-left">
|
||||
{selectedPanelType === PANEL_TYPES.LIST && (
|
||||
<div className="frequency-chart-view-controller">
|
||||
<Typography>Frequency chart</Typography>
|
||||
<Switch
|
||||
size="small"
|
||||
checked={showFrequencyChart}
|
||||
defaultChecked
|
||||
onChange={(): void => setShowFrequencyChart(!showFrequencyChart)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="tab-options-right">
|
||||
{selectedPanelType === PANEL_TYPES.LIST && (
|
||||
<>
|
||||
<div className="order-by-container">
|
||||
<div className="order-by-label">
|
||||
Order by <Minus size={14} /> <ArrowUp10 size={14} />
|
||||
</div>
|
||||
|
||||
<Select
|
||||
placeholder="Select order by"
|
||||
className="order-by-select"
|
||||
style={{ width: 100 }}
|
||||
value={orderDirection}
|
||||
onChange={(value): void => setOrderDirection(value)}
|
||||
options={[
|
||||
{ label: 'Ascending', value: 'asc' },
|
||||
{ label: 'Descending', value: 'desc' },
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
<Download
|
||||
data={flattenLogData}
|
||||
isLoading={isFetching}
|
||||
fileName="log_data"
|
||||
/>
|
||||
<div className="format-options-container" ref={menuRef}>
|
||||
<Button
|
||||
className="periscope-btn ghost"
|
||||
onClick={handleToggleShowFormatOptions}
|
||||
icon={<Sliders size={14} />}
|
||||
data-testid="periscope-btn"
|
||||
/>
|
||||
|
||||
{showFormatMenuItems && (
|
||||
<LogsFormatOptionsMenu
|
||||
title="FORMAT"
|
||||
items={formatItems}
|
||||
selectedOptionFormat={options.format}
|
||||
config={config}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{(selectedPanelType === PANEL_TYPES.TIME_SERIES ||
|
||||
selectedPanelType === PANEL_TYPES.TABLE) && (
|
||||
<div className="query-stats">
|
||||
<QueryStatus
|
||||
loading={isLoading || isFetching}
|
||||
error={isError}
|
||||
success={isSuccess}
|
||||
/>
|
||||
|
||||
{showFormatMenuItems && (
|
||||
<LogsFormatOptionsMenu
|
||||
title="FORMAT"
|
||||
items={formatItems}
|
||||
selectedOptionFormat={options.format}
|
||||
config={config}
|
||||
/>
|
||||
{queryStats?.read_rows && (
|
||||
<Typography.Text className="rows">
|
||||
{getYAxisFormattedValue(queryStats.read_rows?.toString(), 'short')}{' '}
|
||||
rows
|
||||
</Typography.Text>
|
||||
)}
|
||||
|
||||
{queryStats?.elapsed_ms && (
|
||||
<>
|
||||
<div className="divider" />
|
||||
<Typography.Text className="time">
|
||||
{getYAxisFormattedValue(queryStats?.elapsed_ms?.toString(), 'ms')}
|
||||
</Typography.Text>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{(selectedPanelType === PANEL_TYPES.TIME_SERIES ||
|
||||
selectedPanelType === PANEL_TYPES.TABLE) && (
|
||||
<div className="query-stats">
|
||||
<QueryStatus
|
||||
loading={isLoading || isFetching}
|
||||
error={isError}
|
||||
success={isSuccess}
|
||||
/>
|
||||
{queryStats?.read_rows && (
|
||||
<Typography.Text className="rows">
|
||||
{getYAxisFormattedValue(queryStats.read_rows?.toString(), 'short')}{' '}
|
||||
rows
|
||||
</Typography.Text>
|
||||
)}
|
||||
{queryStats?.elapsed_ms && (
|
||||
<>
|
||||
<div className="divider" />
|
||||
<Typography.Text className="time">
|
||||
{getYAxisFormattedValue(queryStats?.elapsed_ms?.toString(), 'ms')}
|
||||
</Typography.Text>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{selectedPanelType === PANEL_TYPES.LIST && showFrequencyChart && (
|
||||
<LogsExplorerChart
|
||||
className="logs-histogram"
|
||||
isLoading={isFetchingListChartData || isLoadingListChartData}
|
||||
data={chartData}
|
||||
isLogsExplorerViews={panelType === PANEL_TYPES.LIST}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="logs-explorer-views-type-content">
|
||||
{selectedPanelType === PANEL_TYPES.LIST && (
|
||||
<LogsExplorerList
|
||||
@@ -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 { server } from 'mocks-server/server';
|
||||
import { rest } from 'msw';
|
||||
import { SELECTED_VIEWS } from 'pages/LogsExplorer/utils';
|
||||
import { ExplorerViews } from 'pages/LogsExplorer/utils';
|
||||
import { QueryBuilderContext } from 'providers/QueryBuilder';
|
||||
import { VirtuosoMockContext } from 'react-virtuoso';
|
||||
import { fireEvent, render, RenderResult } from 'tests/test-utils';
|
||||
@@ -106,8 +106,7 @@ const renderer = (): RenderResult =>
|
||||
value={{ viewportHeight: 300, itemHeight: 100 }}
|
||||
>
|
||||
<LogsExplorerViews
|
||||
selectedView={SELECTED_VIEWS.SEARCH}
|
||||
showFrequencyChart
|
||||
selectedView={ExplorerViews.LIST}
|
||||
setIsLoadingQueries={(): void => {}}
|
||||
listQueryKeyRef={{ current: {} }}
|
||||
chartQueryKeyRef={{ current: {} }}
|
||||
@@ -185,8 +184,7 @@ describe('LogsExplorerViews -', () => {
|
||||
render(
|
||||
<QueryBuilderContext.Provider value={mockQueryBuilderContextValue}>
|
||||
<LogsExplorerViews
|
||||
selectedView={SELECTED_VIEWS.SEARCH}
|
||||
showFrequencyChart
|
||||
selectedView={ExplorerViews.LIST}
|
||||
setIsLoadingQueries={(): void => {}}
|
||||
listQueryKeyRef={{ current: {} }}
|
||||
chartQueryKeyRef={{ current: {} }}
|
||||
|
||||
@@ -5,7 +5,8 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin: 10px 0;
|
||||
margin: 4px 0;
|
||||
padding: 0 1rem;
|
||||
|
||||
.explore-header-left-actions {
|
||||
display: flex;
|
||||
@@ -55,7 +56,7 @@
|
||||
}
|
||||
|
||||
.explore-content {
|
||||
margin-top: 10px;
|
||||
padding: 0 8px;
|
||||
|
||||
.ant-space {
|
||||
margin-top: 10px;
|
||||
|
||||
@@ -2,9 +2,11 @@ import './Explorer.styles.scss';
|
||||
|
||||
import * as Sentry from '@sentry/react';
|
||||
import { Switch } from 'antd';
|
||||
import { QueryBuilderV2 } from 'components/QueryBuilderV2/QueryBuilderV2';
|
||||
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import ExplorerOptionWrapper from 'container/ExplorerOptions/ExplorerOptionWrapper';
|
||||
import RightToolbarActions from 'container/QueryBuilder/components/ToolbarActions/RightToolbarActions';
|
||||
import { QueryBuilderProps } from 'container/QueryBuilder/QueryBuilder.interfaces';
|
||||
import DateTimeSelector from 'container/TopNav/DateTimeSelectionV2';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { useShareBuilderUrl } from 'hooks/queryBuilder/useShareBuilderUrl';
|
||||
@@ -18,7 +20,7 @@ import { DataSource } from 'types/common/queryBuilder';
|
||||
import { generateExportToDashboardLink } from 'utils/dashboard/generateExportToDashboardLink';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
import QuerySection from './QuerySection';
|
||||
// import QuerySection from './QuerySection';
|
||||
import TimeSeries from './TimeSeries';
|
||||
import { ExplorerTabs } from './types';
|
||||
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(
|
||||
() =>
|
||||
updateAllQueriesOperators(
|
||||
@@ -61,7 +73,7 @@ function Explorer(): JSX.Element {
|
||||
[currentQuery, updateAllQueriesOperators],
|
||||
);
|
||||
|
||||
useShareBuilderUrl(exportDefaultQuery);
|
||||
useShareBuilderUrl(defaultQuery);
|
||||
|
||||
const handleExport = useCallback(
|
||||
(
|
||||
@@ -93,6 +105,11 @@ function Explorer(): JSX.Element {
|
||||
[stagedQuery],
|
||||
);
|
||||
|
||||
const queryComponents = useMemo(
|
||||
(): QueryBuilderProps['queryComponents'] => ({}),
|
||||
[],
|
||||
);
|
||||
|
||||
return (
|
||||
<Sentry.ErrorBoundary fallback={<ErrorBoundaryFallback />}>
|
||||
<div className="metrics-explorer-explore-container">
|
||||
@@ -110,7 +127,14 @@ function Explorer(): JSX.Element {
|
||||
<RightToolbarActions onStageRunQuery={handleRunQuery} />
|
||||
</div>
|
||||
</div>
|
||||
<QuerySection />
|
||||
{/* <QuerySection /> */}
|
||||
<QueryBuilderV2
|
||||
config={{ initialDataSource: DataSource.METRICS, queryVariant: 'static' }}
|
||||
panelType={PANEL_TYPES.TIME_SERIES}
|
||||
queryComponents={queryComponents}
|
||||
showFunctions={false}
|
||||
version="v3"
|
||||
/>
|
||||
{/* TODO: Enable once we have resolved all related metrics issues */}
|
||||
{/* <Button.Group className="explore-tabs">
|
||||
<Button
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import classNames from 'classnames';
|
||||
import { ENTITY_VERSION_V4 } from 'constants/app';
|
||||
import { ENTITY_VERSION_V5 } from 'constants/app';
|
||||
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||
import { BuilderUnitsFilter } from 'container/QueryBuilder/filters/BuilderUnitsFilter/BuilderUnits';
|
||||
@@ -63,7 +63,7 @@ function TimeSeries({ showOneChartPerQuery }: TimeSeriesProps): JSX.Element {
|
||||
queryKey: [
|
||||
REACT_QUERY_KEY.GET_QUERY_RANGE,
|
||||
payload,
|
||||
ENTITY_VERSION_V4,
|
||||
ENTITY_VERSION_V5,
|
||||
globalSelectedTime,
|
||||
maxTime,
|
||||
minTime,
|
||||
@@ -80,7 +80,8 @@ function TimeSeries({ showOneChartPerQuery }: TimeSeriesProps): JSX.Element {
|
||||
dataSource: DataSource.METRICS,
|
||||
},
|
||||
},
|
||||
ENTITY_VERSION_V4,
|
||||
// ENTITY_VERSION_V4,
|
||||
ENTITY_VERSION_V5,
|
||||
),
|
||||
enabled: !!payload,
|
||||
})),
|
||||
|
||||
@@ -4,6 +4,7 @@ import { Color } from '@signozhq/design-tokens';
|
||||
import { Button, Tabs, Typography } from 'antd';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import PromQLIcon from 'assets/Dashboard/PromQl';
|
||||
import { QueryBuilderV2 } from 'components/QueryBuilderV2/QueryBuilderV2';
|
||||
import TextToolTip from 'components/TextToolTip';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { QBShortcuts } from 'constants/shortcuts/QBShortcuts';
|
||||
@@ -11,7 +12,7 @@ import {
|
||||
getDefaultWidgetData,
|
||||
PANEL_TYPE_TO_QUERY_TYPES,
|
||||
} from 'container/NewWidget/utils';
|
||||
import { QueryBuilder } from 'container/QueryBuilder';
|
||||
// import { QueryBuilder } from 'container/QueryBuilder';
|
||||
import { QueryBuilderProps } from 'container/QueryBuilder/QueryBuilder.interfaces';
|
||||
import { useKeyboardHotkeys } from 'hooks/hotkeys/useKeyboardHotkeys';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
@@ -142,6 +143,11 @@ function QuerySection({
|
||||
return config;
|
||||
}, []);
|
||||
|
||||
const queryComponents = useMemo(
|
||||
(): QueryBuilderProps['queryComponents'] => ({}),
|
||||
[],
|
||||
);
|
||||
|
||||
const items = useMemo(() => {
|
||||
const supportedQueryTypes = PANEL_TYPE_TO_QUERY_TYPES[selectedGraph] || [];
|
||||
|
||||
@@ -150,12 +156,15 @@ function QuerySection({
|
||||
icon: <Atom size={14} />,
|
||||
label: 'Query Builder',
|
||||
component: (
|
||||
<QueryBuilder
|
||||
panelType={selectedGraph}
|
||||
filterConfigs={filterConfigs}
|
||||
version={selectedDashboard?.data?.version || 'v3'}
|
||||
isListViewPanel={selectedGraph === PANEL_TYPES.LIST}
|
||||
/>
|
||||
<div className="query-builder-v2-container">
|
||||
<QueryBuilderV2
|
||||
panelType={selectedGraph}
|
||||
filterConfigs={filterConfigs}
|
||||
version={selectedDashboard?.data?.version || 'v3'}
|
||||
isListViewPanel={selectedGraph === PANEL_TYPES.LIST}
|
||||
queryComponents={queryComponents}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
[EQueryType.CLICKHOUSE]: {
|
||||
@@ -186,6 +195,7 @@ function QuerySection({
|
||||
children: queryTypeComponents[queryType].component,
|
||||
}));
|
||||
}, [
|
||||
queryComponents,
|
||||
selectedGraph,
|
||||
filterConfigs,
|
||||
selectedDashboard?.data?.version,
|
||||
|
||||
@@ -43,6 +43,7 @@ function WidgetGraphContainer({
|
||||
|
||||
if (
|
||||
selectedGraph !== PANEL_TYPES.LIST &&
|
||||
selectedGraph !== PANEL_TYPES.VALUE &&
|
||||
queryResponse.data?.payload.data?.result?.length === 0
|
||||
) {
|
||||
return (
|
||||
@@ -52,7 +53,7 @@ function WidgetGraphContainer({
|
||||
);
|
||||
}
|
||||
if (
|
||||
selectedGraph === PANEL_TYPES.LIST &&
|
||||
(selectedGraph === PANEL_TYPES.LIST || selectedGraph === PANEL_TYPES.VALUE) &&
|
||||
queryResponse.data?.payload?.data?.newResult?.data?.result?.length === 0
|
||||
) {
|
||||
return (
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import './LeftContainer.styles.scss';
|
||||
|
||||
import { DEFAULT_ENTITY_VERSION } from 'constants/app';
|
||||
import { ENTITY_VERSION_V5 } from 'constants/app';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
||||
import { memo, useEffect } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { AppState } from 'store/reducers';
|
||||
@@ -30,7 +29,7 @@ function LeftContainer({
|
||||
setQueryResponse,
|
||||
}: WidgetGraphProps): JSX.Element {
|
||||
const { stagedQuery } = useQueryBuilder();
|
||||
const { selectedDashboard } = useDashboard();
|
||||
// const { selectedDashboard } = useDashboard();
|
||||
|
||||
const { selectedTime: globalSelectedInterval } = useSelector<
|
||||
AppState,
|
||||
@@ -38,7 +37,8 @@ function LeftContainer({
|
||||
>((state) => state.globalTime);
|
||||
const queryResponse = useGetQueryRange(
|
||||
requestData,
|
||||
selectedDashboard?.data?.version || DEFAULT_ENTITY_VERSION,
|
||||
// selectedDashboard?.data?.version || DEFAULT_ENTITY_VERSION,
|
||||
ENTITY_VERSION_V5,
|
||||
{
|
||||
enabled: !!stagedQuery,
|
||||
retry: false,
|
||||
|
||||
@@ -36,6 +36,11 @@
|
||||
}
|
||||
}
|
||||
|
||||
.right-header {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.save-btn {
|
||||
display: flex;
|
||||
height: 32px;
|
||||
|
||||
@@ -20,6 +20,7 @@ import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import { useSafeNavigate } from 'hooks/useSafeNavigate';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
import createQueryParams from 'lib/createQueryParams';
|
||||
import { getDashboardVariables } from 'lib/dashbaordVariables/getDashboardVariables';
|
||||
import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults';
|
||||
import { cloneDeep, defaultTo, isEmpty, isUndefined } from 'lodash-es';
|
||||
@@ -71,7 +72,10 @@ import {
|
||||
placeWidgetBetweenRows,
|
||||
} from './utils';
|
||||
|
||||
function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
|
||||
function NewWidget({
|
||||
selectedGraph,
|
||||
enableDrilldown = false,
|
||||
}: NewWidgetProps): JSX.Element {
|
||||
const { safeNavigate } = useSafeNavigate();
|
||||
const {
|
||||
selectedDashboard,
|
||||
@@ -682,6 +686,26 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
|
||||
}
|
||||
}, [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 (
|
||||
<Container>
|
||||
<div className="edit-header">
|
||||
@@ -698,31 +722,42 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
|
||||
</Typography.Text>
|
||||
</Flex>
|
||||
</div>
|
||||
{isSaveDisabled && (
|
||||
<Button
|
||||
type="primary"
|
||||
data-testid="new-widget-save"
|
||||
loading={updateDashboardMutation.isLoading}
|
||||
disabled={isSaveDisabled}
|
||||
onClick={onSaveDashboard}
|
||||
className="save-btn"
|
||||
>
|
||||
Save Changes
|
||||
</Button>
|
||||
)}
|
||||
{!isSaveDisabled && (
|
||||
<Button
|
||||
type="primary"
|
||||
data-testid="new-widget-save"
|
||||
loading={updateDashboardMutation.isLoading}
|
||||
disabled={isSaveDisabled}
|
||||
onClick={onSaveDashboard}
|
||||
icon={<Check size={14} />}
|
||||
className="save-btn"
|
||||
>
|
||||
Save Changes
|
||||
</Button>
|
||||
)}
|
||||
<div className="right-header">
|
||||
{showSwitchToViewModeButton && (
|
||||
<Button
|
||||
data-testid="switch-to-view-mode"
|
||||
disabled={isSaveDisabled || !currentQuery}
|
||||
onClick={handleSwitchToViewMode}
|
||||
>
|
||||
Switch to View Mode
|
||||
</Button>
|
||||
)}
|
||||
{isSaveDisabled && (
|
||||
<Button
|
||||
type="primary"
|
||||
data-testid="new-widget-save"
|
||||
loading={updateDashboardMutation.isLoading}
|
||||
disabled={isSaveDisabled}
|
||||
onClick={onSaveDashboard}
|
||||
className="save-btn"
|
||||
>
|
||||
Save Changes
|
||||
</Button>
|
||||
)}
|
||||
{!isSaveDisabled && (
|
||||
<Button
|
||||
type="primary"
|
||||
data-testid="new-widget-save"
|
||||
loading={updateDashboardMutation.isLoading}
|
||||
disabled={isSaveDisabled}
|
||||
onClick={onSaveDashboard}
|
||||
icon={<Check size={14} />}
|
||||
className="save-btn"
|
||||
>
|
||||
Save Changes
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<PanelContainer>
|
||||
|
||||
@@ -12,6 +12,7 @@ export interface NewWidgetProps {
|
||||
selectedGraph: PANEL_TYPES;
|
||||
yAxisUnit: Widgets['yAxisUnit'];
|
||||
fillSpans: Widgets['fillSpans'];
|
||||
enableDrilldown?: boolean;
|
||||
}
|
||||
|
||||
export interface WidgetGraphProps {
|
||||
|
||||
@@ -9,9 +9,17 @@ function ValuePanelWrapper({
|
||||
}: PanelWrapperProps): JSX.Element {
|
||||
const { yAxisUnit, thresholds } = widget;
|
||||
const data = getUPlotChartData(queryResponse?.data?.payload);
|
||||
const dataNew = Object.values(
|
||||
queryResponse?.data?.payload?.data?.newResult?.data?.result[0]?.table
|
||||
?.rows?.[0]?.data || {},
|
||||
);
|
||||
|
||||
// this is for handling both query_range v3 and v5 responses
|
||||
const gridValueData = data?.[0].length > 0 ? data : [[0], dataNew];
|
||||
|
||||
return (
|
||||
<GridValueComponent
|
||||
data={data}
|
||||
data={gridValueData}
|
||||
yAxisUnit={yAxisUnit}
|
||||
thresholds={thresholds}
|
||||
/>
|
||||
|
||||
@@ -28,5 +28,6 @@ export type QueryBuilderProps = {
|
||||
queryComponents?: { renderOrderBy?: (props: OrderByFilterProps) => ReactNode };
|
||||
isListViewPanel?: boolean;
|
||||
showFunctions?: boolean;
|
||||
showOnlyWhereClause?: boolean;
|
||||
version: string;
|
||||
};
|
||||
|
||||
@@ -78,11 +78,11 @@
|
||||
color: var(--bg-sakura-400);
|
||||
}
|
||||
|
||||
&.sync-btn {
|
||||
border: 1px solid rgba(78, 116, 248, 0.2);
|
||||
background: rgba(78, 116, 248, 0.1);
|
||||
color: var(--bg-robin-500);
|
||||
}
|
||||
// &.sync-btn {
|
||||
// border: 1px solid rgba(78, 116, 248, 0.2);
|
||||
// background: rgba(78, 116, 248, 0.1);
|
||||
// color: var(--bg-robin-500);
|
||||
// }
|
||||
}
|
||||
|
||||
&.formula-btn {
|
||||
|
||||
@@ -161,15 +161,18 @@ export function Formula({
|
||||
<Col span={24}>
|
||||
<Input.TextArea
|
||||
name="expression"
|
||||
className="formula-expression"
|
||||
onChange={handleChange}
|
||||
size="middle"
|
||||
value={formula.expression}
|
||||
placeholder="Enter formula"
|
||||
rows={2}
|
||||
/>
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<Input
|
||||
name="legend"
|
||||
className="formula-legend"
|
||||
onChange={handleChange}
|
||||
size="middle"
|
||||
value={formula.legend}
|
||||
|
||||
@@ -28,9 +28,12 @@
|
||||
border-radius: 2px;
|
||||
|
||||
.periscope-btn {
|
||||
border: 1px solid var(--bg-slate-200);
|
||||
background: var(--bg-ink-200);
|
||||
min-width: 32px;
|
||||
|
||||
border-radius: 2px;
|
||||
border: 1px solid var(--Slate-400, #1d212d);
|
||||
background: var(--Ink-300, #16181d);
|
||||
box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.query-name {
|
||||
@@ -44,11 +47,11 @@
|
||||
border: 1px solid rgba(242, 71, 105, 0.2) !important;
|
||||
background: rgba(242, 71, 105, 0.1) !important;
|
||||
|
||||
&.sync-btn {
|
||||
border: 1px solid rgba(78, 116, 248, 0.2) !important;
|
||||
background: rgba(78, 116, 248, 0.1) !important;
|
||||
color: var(--bg-robin-500) !important;
|
||||
}
|
||||
// &.sync-btn {
|
||||
// border: 1px solid rgba(78, 116, 248, 0.2) !important;
|
||||
// background: rgba(78, 116, 248, 0.1) !important;
|
||||
// color: var(--bg-robin-500) !important;
|
||||
// }
|
||||
|
||||
&:hover {
|
||||
border: 1px solid rgba(242, 71, 105, 0.4) !important;
|
||||
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
} from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
import { DataSourceDropdown } from '..';
|
||||
import QueryFunctions from '../QueryFunctions/QueryFunctions';
|
||||
|
||||
interface QBEntityOptionsProps {
|
||||
@@ -31,16 +32,20 @@ interface QBEntityOptionsProps {
|
||||
isCollapsed: boolean;
|
||||
entityType: string;
|
||||
entityData: any;
|
||||
onDelete: () => void;
|
||||
onDelete?: () => void;
|
||||
onCloneQuery?: (type: string, query: IBuilderQuery) => void;
|
||||
onToggleVisibility: () => void;
|
||||
onCollapseEntity: () => void;
|
||||
onQueryFunctionsUpdates?: (functions: QueryFunctionProps[]) => void;
|
||||
showDeleteButton: boolean;
|
||||
showDeleteButton?: boolean;
|
||||
showCloneOption?: boolean;
|
||||
isListViewPanel?: boolean;
|
||||
index?: number;
|
||||
queryVariant?: 'dropdown' | 'static';
|
||||
onChangeDataSource?: (value: DataSource) => void;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||
export default function QBEntityOptions({
|
||||
query,
|
||||
isMetricsDataSource,
|
||||
@@ -48,14 +53,17 @@ export default function QBEntityOptions({
|
||||
showFunctions,
|
||||
entityType,
|
||||
entityData,
|
||||
onDelete,
|
||||
onCloneQuery,
|
||||
onToggleVisibility,
|
||||
onCollapseEntity,
|
||||
showDeleteButton,
|
||||
onQueryFunctionsUpdates,
|
||||
isListViewPanel,
|
||||
onDelete,
|
||||
showDeleteButton,
|
||||
showCloneOption,
|
||||
onCloneQuery,
|
||||
index,
|
||||
queryVariant,
|
||||
onChangeDataSource,
|
||||
}: QBEntityOptionsProps): JSX.Element {
|
||||
const handleCloneEntity = (): void => {
|
||||
if (isFunction(onCloneQuery)) {
|
||||
@@ -97,7 +105,7 @@ export default function QBEntityOptions({
|
||||
</Button>
|
||||
</Tooltip>
|
||||
|
||||
{entityType === 'query' && (
|
||||
{entityType === 'query' && showCloneOption && (
|
||||
<Tooltip title={`Clone Query ${entityData.queryName}`}>
|
||||
<Button className={cx('periscope-btn')} onClick={handleCloneEntity}>
|
||||
<Copy size={14} />
|
||||
@@ -115,7 +123,23 @@ export default function QBEntityOptions({
|
||||
{entityData.queryName}
|
||||
</Button>
|
||||
|
||||
{queryVariant === 'dropdown' && (
|
||||
<div className="query-data-source">
|
||||
<DataSourceDropdown
|
||||
onChange={(value): void => {
|
||||
if (onChangeDataSource) {
|
||||
onChangeDataSource(value);
|
||||
}
|
||||
}}
|
||||
value={query?.dataSource || DataSource.METRICS}
|
||||
isListViewPanel={isListViewPanel}
|
||||
className="query-data-source-dropdown"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showFunctions &&
|
||||
!isListViewPanel &&
|
||||
(isMetricsDataSource || isLogsDataSource) &&
|
||||
query &&
|
||||
onQueryFunctionsUpdates && (
|
||||
@@ -138,7 +162,7 @@ export default function QBEntityOptions({
|
||||
)}
|
||||
</div>
|
||||
|
||||
{showDeleteButton && (
|
||||
{showDeleteButton && !isListViewPanel && (
|
||||
<Button className="periscope-btn ghost" onClick={onDelete}>
|
||||
<Trash2 size={14} />
|
||||
</Button>
|
||||
@@ -156,4 +180,9 @@ QBEntityOptions.defaultProps = {
|
||||
showFunctions: false,
|
||||
onCloneQuery: noop,
|
||||
index: 0,
|
||||
onDelete: noop,
|
||||
showDeleteButton: false,
|
||||
showCloneOption: true,
|
||||
queryVariant: 'static',
|
||||
onChangeDataSource: noop,
|
||||
};
|
||||
|
||||
@@ -5,8 +5,10 @@ export type QueryProps = {
|
||||
index: number;
|
||||
isAvailableToDisable: boolean;
|
||||
query: IBuilderQuery;
|
||||
queryVariant: 'static' | 'dropdown';
|
||||
queryVariant?: 'static' | 'dropdown';
|
||||
isListViewPanel?: boolean;
|
||||
showFunctions?: boolean;
|
||||
version: string;
|
||||
showSpanScopeSelector?: boolean;
|
||||
showOnlyWhereClause?: boolean;
|
||||
} & Pick<QueryBuilderProps, 'filterConfigs' | 'queryComponents'>;
|
||||
|
||||
@@ -350,6 +350,7 @@ export const Query = memo(function Query({
|
||||
showDeleteButton={currentQuery.builder.queryData.length > 1}
|
||||
isListViewPanel={isListViewPanel}
|
||||
index={index}
|
||||
queryVariant={queryVariant}
|
||||
/>
|
||||
|
||||
{!isCollapse && (
|
||||
|
||||
@@ -10,6 +10,7 @@ interface SpaceAggregationOptionsProps {
|
||||
disabled: boolean;
|
||||
onSelect: (value: string) => void;
|
||||
operators: any[];
|
||||
qbVersion?: string;
|
||||
}
|
||||
|
||||
export default function SpaceAggregationOptions({
|
||||
@@ -19,8 +20,10 @@ export default function SpaceAggregationOptions({
|
||||
disabled,
|
||||
onSelect,
|
||||
operators,
|
||||
qbVersion,
|
||||
}: SpaceAggregationOptionsProps): JSX.Element {
|
||||
const placeHolderText = panelType === PANEL_TYPES.VALUE ? 'Sum' : 'Sum By';
|
||||
const placeHolderText =
|
||||
panelType === PANEL_TYPES.VALUE || qbVersion === 'v3' ? 'Sum' : 'Sum By';
|
||||
const [defaultValue, setDefaultValue] = useState(
|
||||
selectedValue || placeHolderText,
|
||||
);
|
||||
@@ -58,10 +61,15 @@ export default function SpaceAggregationOptions({
|
||||
>
|
||||
{operators.map((operator) => (
|
||||
<Select.Option key={operator.value} value={operator.value}>
|
||||
{operator.label} {panelType !== PANEL_TYPES.VALUE ? ' By' : ''}
|
||||
{operator.label}{' '}
|
||||
{panelType !== PANEL_TYPES.VALUE && qbVersion === 'v2' ? ' By' : ''}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
SpaceAggregationOptions.defaultProps = {
|
||||
qbVersion: 'v2',
|
||||
};
|
||||
|
||||
@@ -1,35 +1,31 @@
|
||||
/* eslint-disable sonarjs/no-duplicate-string */
|
||||
import './ToolbarActions.styles.scss';
|
||||
|
||||
import { FilterOutlined } from '@ant-design/icons';
|
||||
import { Button, Switch, Tooltip, Typography } from 'antd';
|
||||
import { Button, Tooltip } from 'antd';
|
||||
import cx from 'classnames';
|
||||
import { Atom, SquareMousePointer, Terminal } from 'lucide-react';
|
||||
import { SELECTED_VIEWS } from 'pages/LogsExplorer/utils';
|
||||
import { Atom, Binoculars, SquareMousePointer, Terminal } from 'lucide-react';
|
||||
import { ExplorerViews } from 'pages/LogsExplorer/utils';
|
||||
|
||||
interface LeftToolbarActionsProps {
|
||||
items: any;
|
||||
selectedView: string;
|
||||
onToggleHistrogramVisibility: () => void;
|
||||
onChangeSelectedView: (view: SELECTED_VIEWS) => void;
|
||||
showFrequencyChart: boolean;
|
||||
onChangeSelectedView: (view: ExplorerViews) => void;
|
||||
showFilter: boolean;
|
||||
handleFilterVisibilityChange: () => void;
|
||||
}
|
||||
|
||||
const activeTab = 'active-tab';
|
||||
const actionBtn = 'action-btn';
|
||||
export const queryBuilder = 'query-builder';
|
||||
|
||||
export default function LeftToolbarActions({
|
||||
items,
|
||||
selectedView,
|
||||
onToggleHistrogramVisibility,
|
||||
|
||||
onChangeSelectedView,
|
||||
showFrequencyChart,
|
||||
showFilter,
|
||||
handleFilterVisibilityChange,
|
||||
}: LeftToolbarActionsProps): JSX.Element {
|
||||
const { clickhouse, search, queryBuilder: QB } = items;
|
||||
const { clickhouse, list, timeseries, table, trace } = items;
|
||||
|
||||
return (
|
||||
<div className="left-toolbar">
|
||||
@@ -41,56 +37,90 @@ export default function LeftToolbarActions({
|
||||
</Tooltip>
|
||||
)}
|
||||
<div className="left-toolbar-query-actions">
|
||||
<Tooltip title="Search">
|
||||
<Button
|
||||
disabled={search.disabled}
|
||||
className={cx(
|
||||
'search',
|
||||
actionBtn,
|
||||
selectedView === 'search' ? activeTab : '',
|
||||
)}
|
||||
onClick={(): void => onChangeSelectedView(SELECTED_VIEWS.SEARCH)}
|
||||
>
|
||||
<SquareMousePointer size={14} data-testid="search-view" />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<Tooltip title="Query Builder">
|
||||
<Button
|
||||
disabled={QB.disabled}
|
||||
className={cx(
|
||||
queryBuilder,
|
||||
actionBtn,
|
||||
selectedView === queryBuilder ? activeTab : '',
|
||||
)}
|
||||
onClick={(): void => onChangeSelectedView(SELECTED_VIEWS.QUERY_BUILDER)}
|
||||
>
|
||||
<Atom size={14} data-testid="query-builder-view" />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
{list?.show && (
|
||||
<Tooltip title="List View">
|
||||
<Button
|
||||
disabled={list.disabled}
|
||||
className={cx(
|
||||
'list-view-tab',
|
||||
'explorer-view-option',
|
||||
selectedView === list.key ? activeTab : '',
|
||||
)}
|
||||
onClick={(): void => onChangeSelectedView(list.key)}
|
||||
>
|
||||
<SquareMousePointer size={14} data-testid="search-view" />
|
||||
List View
|
||||
</Button>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
{trace?.show && (
|
||||
<Tooltip title="Trace View">
|
||||
<Button
|
||||
disabled={trace.disabled}
|
||||
className={cx(
|
||||
'trace-view-tab',
|
||||
'explorer-view-option',
|
||||
selectedView === trace.key ? activeTab : '',
|
||||
)}
|
||||
onClick={(): void => onChangeSelectedView(trace.key)}
|
||||
>
|
||||
<SquareMousePointer size={14} data-testid="trace-view" />
|
||||
Trace View
|
||||
</Button>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
{timeseries?.show && (
|
||||
<Tooltip title="Time Series">
|
||||
<Button
|
||||
disabled={timeseries.disabled}
|
||||
className={cx(
|
||||
'timeseries-view-tab',
|
||||
'explorer-view-option',
|
||||
selectedView === timeseries.key ? activeTab : '',
|
||||
)}
|
||||
onClick={(): void => onChangeSelectedView(timeseries.key)}
|
||||
>
|
||||
<Atom size={14} data-testid="query-builder-view" />
|
||||
Time Series
|
||||
</Button>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
{clickhouse?.show && (
|
||||
<Button
|
||||
disabled={clickhouse.disabled}
|
||||
className={cx(
|
||||
SELECTED_VIEWS.CLICKHOUSE,
|
||||
actionBtn,
|
||||
selectedView === SELECTED_VIEWS.CLICKHOUSE ? activeTab : '',
|
||||
)}
|
||||
onClick={(): void => onChangeSelectedView(SELECTED_VIEWS.CLICKHOUSE)}
|
||||
>
|
||||
<Terminal size={14} data-testid="clickhouse-view" />
|
||||
</Button>
|
||||
<Tooltip title="Clickhouse">
|
||||
<Button
|
||||
disabled={clickhouse.disabled}
|
||||
className={cx(
|
||||
'clickhouse-view-tab',
|
||||
'explorer-view-option',
|
||||
selectedView === clickhouse.key ? activeTab : '',
|
||||
)}
|
||||
onClick={(): void => onChangeSelectedView(clickhouse.key)}
|
||||
>
|
||||
<Terminal size={14} data-testid="clickhouse-view" />
|
||||
Clickhouse
|
||||
</Button>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="frequency-chart-view-controller">
|
||||
<Typography>Frequency chart</Typography>
|
||||
<Switch
|
||||
size="small"
|
||||
checked={showFrequencyChart}
|
||||
defaultChecked
|
||||
onChange={onToggleHistrogramVisibility}
|
||||
/>
|
||||
{table?.show && (
|
||||
<Tooltip title="Table">
|
||||
<Button
|
||||
disabled={table.disabled}
|
||||
className={cx(
|
||||
'table-view-tab',
|
||||
'explorer-view-option',
|
||||
selectedView === table.key ? activeTab : '',
|
||||
)}
|
||||
onClick={(): void => onChangeSelectedView(table.key)}
|
||||
>
|
||||
<Binoculars size={14} data-testid="query-builder-view-v2" />
|
||||
Table
|
||||
</Button>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -19,32 +19,51 @@
|
||||
border: 1px solid var(--bg-slate-400);
|
||||
background: var(--bg-ink-300);
|
||||
flex-direction: row;
|
||||
border-bottom: none;
|
||||
margin-bottom: -1px;
|
||||
|
||||
.prom-ql-icon {
|
||||
height: 14px;
|
||||
width: 14px;
|
||||
}
|
||||
|
||||
.ant-btn {
|
||||
.explorer-view-option {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-direction: row;
|
||||
border: none;
|
||||
padding: 9px;
|
||||
box-shadow: none;
|
||||
border-radius: 0;
|
||||
border-radius: 0px;
|
||||
border-left: 1px solid var(--bg-slate-400);
|
||||
border-bottom: 1px solid var(--bg-slate-400);
|
||||
|
||||
gap: 8px;
|
||||
|
||||
&.active-tab {
|
||||
background-color: var(--bg-slate-400);
|
||||
background-color: var(--bg-ink-500);
|
||||
border-bottom: 1px solid var(--bg-ink-500);
|
||||
|
||||
&:hover {
|
||||
background-color: var(--bg-ink-500) !important;
|
||||
}
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
background-color: #121317;
|
||||
background-color: var(--bg-ink-300);
|
||||
opacity: 0.6;
|
||||
}
|
||||
}
|
||||
.action-btn + .action-btn {
|
||||
border-left: 1px solid var(--bg-slate-400);
|
||||
|
||||
&:first-child {
|
||||
border-left: 1px solid transparent;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: transparent !important;
|
||||
border-left: 1px solid transparent !important;
|
||||
color: var(--bg-vanilla-100);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -108,32 +127,36 @@
|
||||
|
||||
.lightMode {
|
||||
.left-toolbar {
|
||||
.filter-btn {
|
||||
border-color: var(--bg-vanilla-300) !important;
|
||||
}
|
||||
|
||||
.left-toolbar-query-actions {
|
||||
border-color: var(--bg-vanilla-300);
|
||||
background: var(--bg-vanilla-100);
|
||||
border-color: var(--bg-vanilla-300) !important;
|
||||
background: var(--bg-vanilla-100) !important;
|
||||
|
||||
.ant-btn {
|
||||
border-color: var(--bg-vanilla-300);
|
||||
background: var(--bg-vanilla-100);
|
||||
color: var(--bg-ink-200);
|
||||
border-color: var(--bg-vanilla-300) !important;
|
||||
background: var(--bg-vanilla-100) !important;
|
||||
color: var(--bg-ink-200) !important;
|
||||
|
||||
&.active-tab {
|
||||
background-color: var(--bg-robin-100);
|
||||
background-color: var(--bg-robin-100) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.loading-container {
|
||||
.loading-btn {
|
||||
background: var(--bg-vanilla-300);
|
||||
background: var(--bg-vanilla-300) !important;
|
||||
}
|
||||
|
||||
.cancel-run {
|
||||
color: var(--bg-vanilla-100);
|
||||
color: var(--bg-vanilla-100) !important;
|
||||
}
|
||||
|
||||
.cancel-run:hover {
|
||||
background-color: #ff7875;
|
||||
background-color: #ff7875 !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { SELECTED_VIEWS } from 'pages/LogsExplorer/utils';
|
||||
import { ExplorerViews } from 'pages/LogsExplorer/utils';
|
||||
import MockQueryClientProvider from 'providers/test/MockQueryClientProvider';
|
||||
|
||||
import LeftToolbarActions from '../LeftToolbarActions';
|
||||
@@ -9,7 +9,6 @@ import RightToolbarActions from '../RightToolbarActions';
|
||||
describe('ToolbarActions', () => {
|
||||
it('LeftToolbarActions - renders correctly with default props', async () => {
|
||||
const handleChangeSelectedView = jest.fn();
|
||||
const handleToggleShowFrequencyChart = jest.fn();
|
||||
const { queryByTestId } = render(
|
||||
<LeftToolbarActions
|
||||
items={{
|
||||
@@ -31,10 +30,8 @@ describe('ToolbarActions', () => {
|
||||
disabled: false,
|
||||
},
|
||||
}}
|
||||
selectedView={SELECTED_VIEWS.SEARCH}
|
||||
selectedView={ExplorerViews.LIST}
|
||||
onChangeSelectedView={handleChangeSelectedView}
|
||||
onToggleHistrogramVisibility={handleToggleShowFrequencyChart}
|
||||
showFrequencyChart
|
||||
showFilter
|
||||
handleFilterVisibilityChange={(): void => {}}
|
||||
/>,
|
||||
@@ -77,10 +74,8 @@ describe('ToolbarActions', () => {
|
||||
show: true,
|
||||
},
|
||||
}}
|
||||
selectedView={SELECTED_VIEWS.QUERY_BUILDER}
|
||||
selectedView={ExplorerViews.TIMESERIES}
|
||||
onChangeSelectedView={handleChangeSelectedView}
|
||||
onToggleHistrogramVisibility={handleToggleShowFrequencyChart}
|
||||
showFrequencyChart
|
||||
showFilter
|
||||
handleFilterVisibilityChange={(): void => {}}
|
||||
/>,
|
||||
|
||||
@@ -5,13 +5,13 @@ export function removePrefix(str: string, type: string): string {
|
||||
const resourcePrefix = `${MetricsType.Resource}_`;
|
||||
const scopePrefix = `${MetricsType.Scope}_`;
|
||||
|
||||
if (str.startsWith(tagPrefix)) {
|
||||
if (str?.startsWith(tagPrefix)) {
|
||||
return str.slice(tagPrefix.length);
|
||||
}
|
||||
if (str.startsWith(resourcePrefix)) {
|
||||
if (str?.startsWith(resourcePrefix)) {
|
||||
return str.slice(resourcePrefix.length);
|
||||
}
|
||||
if (str.startsWith(scopePrefix) && type === MetricsType.Scope) {
|
||||
if (str?.startsWith(scopePrefix) && type === MetricsType.Scope) {
|
||||
return str.slice(scopePrefix.length);
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ export const OperatorsSelect = memo(function OperatorsSelect({
|
||||
operators,
|
||||
value,
|
||||
onChange,
|
||||
className,
|
||||
...props
|
||||
}: OperatorsSelectProps): JSX.Element {
|
||||
return (
|
||||
@@ -20,6 +21,7 @@ export const OperatorsSelect = memo(function OperatorsSelect({
|
||||
showSearch
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
{...props}
|
||||
popupClassName={className}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -8,6 +8,7 @@ export type OrderByFilterProps = {
|
||||
onChange: (values: OrderByPayload[]) => void;
|
||||
isListViewPanel?: boolean;
|
||||
entityVersion?: string;
|
||||
isNewQueryV2?: boolean;
|
||||
};
|
||||
|
||||
export type OrderByFilterValue = {
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Select, Spin } from 'antd';
|
||||
import { useGetAggregateKeys } from 'hooks/queryBuilder/useGetAggregateKeys';
|
||||
import { useMemo } from 'react';
|
||||
import { DataSource, MetricAggregateOperator } from 'types/common/queryBuilder';
|
||||
import { getParsedAggregationOptionsForOrderBy } from 'utils/aggregationConverter';
|
||||
import { popupContainer } from 'utils/selectPopupContainer';
|
||||
|
||||
import { selectStyle } from '../QueryBuilderSearch/config';
|
||||
@@ -13,6 +14,7 @@ export function OrderByFilter({
|
||||
onChange,
|
||||
isListViewPanel = false,
|
||||
entityVersion,
|
||||
isNewQueryV2 = false,
|
||||
}: OrderByFilterProps): JSX.Element {
|
||||
const {
|
||||
debouncedSearchText,
|
||||
@@ -37,22 +39,35 @@ export function OrderByFilter({
|
||||
},
|
||||
);
|
||||
|
||||
// Get parsed aggregation options using createAggregation only for QueryV2
|
||||
const parsedAggregationOptions = useMemo(
|
||||
() => (isNewQueryV2 ? getParsedAggregationOptionsForOrderBy(query) : []),
|
||||
[query, isNewQueryV2],
|
||||
);
|
||||
|
||||
const optionsData = useMemo(() => {
|
||||
const keyOptions = createOptions(data?.payload?.attributeKeys || []);
|
||||
const groupByOptions = createOptions(query.groupBy);
|
||||
const aggregationOptionsFromParsed = createOptions(parsedAggregationOptions);
|
||||
|
||||
const options =
|
||||
query.aggregateOperator === MetricAggregateOperator.NOOP
|
||||
? keyOptions
|
||||
: [...groupByOptions, ...aggregationOptions];
|
||||
: [
|
||||
...groupByOptions,
|
||||
...(isNewQueryV2 ? aggregationOptionsFromParsed : aggregationOptions),
|
||||
];
|
||||
|
||||
return generateOptions(options);
|
||||
}, [
|
||||
aggregationOptions,
|
||||
createOptions,
|
||||
data?.payload?.attributeKeys,
|
||||
generateOptions,
|
||||
query.aggregateOperator,
|
||||
query.groupBy,
|
||||
query.aggregateOperator,
|
||||
parsedAggregationOptions,
|
||||
aggregationOptions,
|
||||
generateOptions,
|
||||
isNewQueryV2,
|
||||
]);
|
||||
|
||||
const isDisabledSelect =
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
.query-builder-search-v2 {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
flex: 1;
|
||||
|
||||
.ant-select-dropdown {
|
||||
padding: 0px;
|
||||
|
||||
@@ -7,10 +7,12 @@ import {
|
||||
createTableColumnsFromQuery,
|
||||
RowData,
|
||||
} from 'lib/query/createTableColumnsFromQuery';
|
||||
import ContextMenu, { useCoordinates } from 'periscope/components/ContextMenu';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
|
||||
import { QueryTableProps } from './QueryTable.intefaces';
|
||||
import useTableContextMenu from './useTableContextMenu';
|
||||
import { createDownloadableData } from './utils';
|
||||
|
||||
export function QueryTable({
|
||||
@@ -31,6 +33,21 @@ export function QueryTable({
|
||||
const { servicename: encodedServiceName } = useParams<IServiceName>();
|
||||
const servicename = decodeURIComponent(encodedServiceName);
|
||||
const { loading } = props;
|
||||
|
||||
const {
|
||||
coordinates,
|
||||
popoverPosition,
|
||||
clickedData,
|
||||
onClose,
|
||||
onClick,
|
||||
} = useCoordinates();
|
||||
const { menuItemsConfig } = useTableContextMenu({
|
||||
widgetId: widgetId || '',
|
||||
clickedData,
|
||||
onClose,
|
||||
coordinates,
|
||||
});
|
||||
|
||||
const { columns: newColumns, dataSource: newDataSource } = useMemo(() => {
|
||||
if (columns && dataSource) {
|
||||
return { columns, dataSource };
|
||||
@@ -54,6 +71,44 @@ export function QueryTable({
|
||||
|
||||
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 = {
|
||||
pageSize: 10,
|
||||
showSizeChanger: false,
|
||||
@@ -82,28 +137,45 @@ export function QueryTable({
|
||||
}, [newDataSource, onTableSearch, searchTerm]);
|
||||
|
||||
return (
|
||||
<div className="query-table">
|
||||
{isDownloadEnabled && (
|
||||
<div className="query-table--download">
|
||||
<Download
|
||||
data={downloadableData}
|
||||
fileName={`${fileName}-${servicename}`}
|
||||
isLoading={loading as boolean}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<ResizeTable
|
||||
columns={tableColumns}
|
||||
tableLayout="fixed"
|
||||
dataSource={filterTable === null ? newDataSource : filterTable}
|
||||
scroll={{ x: 'max-content' }}
|
||||
pagination={paginationConfig}
|
||||
widgetId={widgetId}
|
||||
shouldPersistColumnWidths
|
||||
sticky={sticky}
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
{...props}
|
||||
<>
|
||||
<div className="query-table">
|
||||
{isDownloadEnabled && (
|
||||
<div className="query-table--download">
|
||||
<Download
|
||||
data={downloadableData}
|
||||
fileName={`${fileName}-${servicename}`}
|
||||
isLoading={loading as boolean}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<ResizeTable
|
||||
columns={columnsWithClickHandlers}
|
||||
tableLayout="fixed"
|
||||
dataSource={filterTable === null ? newDataSource : filterTable}
|
||||
scroll={{ x: 'max-content' }}
|
||||
pagination={paginationConfig}
|
||||
widgetId={widgetId}
|
||||
shouldPersistColumnWidths
|
||||
sticky={sticky}
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
<ContextMenu
|
||||
coordinates={coordinates}
|
||||
popoverPosition={popoverPosition}
|
||||
title={menuItemsConfig.header}
|
||||
items={menuItemsConfig.items}
|
||||
onClose={onClose}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* <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 { getTimeRange } from 'utils/getTimeRange';
|
||||
|
||||
import { Container } from './styles';
|
||||
|
||||
function TimeSeriesView({
|
||||
data,
|
||||
isLoading,
|
||||
@@ -162,7 +160,7 @@ function TimeSeriesView({
|
||||
});
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<div className="time-series-view">
|
||||
{isError && <LogsError />}
|
||||
<div
|
||||
className="graph-container"
|
||||
@@ -204,7 +202,7 @@ function TimeSeriesView({
|
||||
!isEmpty(chartData?.[0]) &&
|
||||
chartOptions && <Uplot data={chartData} options={chartOptions} />}
|
||||
</div>
|
||||
</Container>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { ENTITY_VERSION_V4 } from 'constants/app';
|
||||
import './TimeSeriesView.styles.scss';
|
||||
|
||||
import { ENTITY_VERSION_V5 } from 'constants/app';
|
||||
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
|
||||
@@ -52,7 +54,8 @@ function TimeSeriesViewContainer({
|
||||
dataSource,
|
||||
},
|
||||
},
|
||||
ENTITY_VERSION_V4,
|
||||
// ENTITY_VERSION_V4,
|
||||
ENTITY_VERSION_V5,
|
||||
{
|
||||
queryKey: [
|
||||
REACT_QUERY_KEY.GET_QUERY_RANGE,
|
||||
|
||||
@@ -1,12 +1,23 @@
|
||||
.toolbar {
|
||||
display: grid;
|
||||
grid-template-columns: 3fr auto auto;
|
||||
padding: 8px 16px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
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;
|
||||
|
||||
.rightActions {
|
||||
margin: 4px 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.timeRange {
|
||||
display: flex;
|
||||
padding-right: 8px;
|
||||
align-items: center;
|
||||
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 (
|
||||
<div className="toolbar">
|
||||
<div className="leftActions">{leftActions}</div>
|
||||
<div className="timeRange">
|
||||
{showOldCTA && <NewExplorerCTA />}
|
||||
<DateTimeSelectionV2
|
||||
showAutoRefresh={showAutoRefresh}
|
||||
showRefreshText={!isLogsExplorerPage && !isApiMonitoringPage}
|
||||
/>
|
||||
|
||||
<div className="rightActions">
|
||||
<div className="timeRange">
|
||||
{showOldCTA && <NewExplorerCTA />}
|
||||
<DateTimeSelectionV2
|
||||
showAutoRefresh={showAutoRefresh}
|
||||
showRefreshText={!isLogsExplorerPage && !isApiMonitoringPage}
|
||||
hideShareModal
|
||||
/>
|
||||
</div>
|
||||
|
||||
{rightActions}
|
||||
</div>
|
||||
<div className="rightActions">{rightActions}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -31,6 +31,10 @@
|
||||
}
|
||||
}
|
||||
|
||||
.refresh-text-container {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.refresh-actions {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
@@ -309,3 +313,11 @@
|
||||
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>
|
||||
</FormItem>
|
||||
)}
|
||||
|
||||
{showOldExplorerCTA && (
|
||||
<div style={{ marginRight: 12 }}>
|
||||
<NewExplorerCTA />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!hasSelectedTimeError && !refreshButtonHidden && showRefreshText && (
|
||||
<RefreshText
|
||||
{...{
|
||||
onLastRefreshHandler,
|
||||
}}
|
||||
refreshButtonHidden={refreshButtonHidden}
|
||||
/>
|
||||
<div className="refresh-text-container">
|
||||
<RefreshText
|
||||
{...{
|
||||
onLastRefreshHandler,
|
||||
}}
|
||||
refreshButtonHidden={refreshButtonHidden}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<Form
|
||||
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