mirror of
https://github.com/SigNoz/signoz.git
synced 2026-05-04 17:30:34 +01:00
Compare commits
98 Commits
v0.87.0-4e
...
fix/query-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f9414d2e8a | ||
|
|
6d110870a4 | ||
|
|
5bebc815e3 | ||
|
|
b6fe0619b0 | ||
|
|
bb8f6b9eb1 | ||
|
|
c783c5c7ee | ||
|
|
6f08aff58b | ||
|
|
86229dd55b | ||
|
|
d9519d6896 | ||
|
|
3da7ce5f03 | ||
|
|
e2498863e7 | ||
|
|
25c94dfa93 | ||
|
|
abe617bcd0 | ||
|
|
d8dd3cddd8 | ||
|
|
6ccb44a3df | ||
|
|
e30953fc93 | ||
|
|
9010220b44 | ||
|
|
48cdf439a9 | ||
|
|
52656b2ee8 | ||
|
|
51a7dae5d9 | ||
|
|
b27901cd70 | ||
|
|
e0c30a0e4b | ||
|
|
cd29cfb609 | ||
|
|
0325a5f64d | ||
|
|
af2814a535 | ||
|
|
8d49c7934b | ||
|
|
8c992d10be | ||
|
|
aa6008649d | ||
|
|
8d73f79299 | ||
|
|
62ecc44510 | ||
|
|
1e7b2a7734 | ||
|
|
35c49c45da | ||
|
|
e9bed072e4 | ||
|
|
ab32765dec | ||
|
|
959e518907 | ||
|
|
255fe1b70a | ||
|
|
44eb60fef9 | ||
|
|
14e4273ae9 | ||
|
|
7a98bed694 | ||
|
|
231b9072b1 | ||
|
|
5eca9efcd5 | ||
|
|
a9303d3482 | ||
|
|
5da45d8ade | ||
|
|
c07ada0d71 | ||
|
|
47ca071d36 | ||
|
|
51d697c91a | ||
|
|
00ce2e3034 | ||
|
|
1104eca146 | ||
|
|
ab594cd9ba | ||
|
|
de3497d439 | ||
|
|
e319394ac4 | ||
|
|
559be9a571 | ||
|
|
9c685837eb | ||
|
|
bc3d162016 | ||
|
|
6b406ec9de | ||
|
|
b7e08a746c | ||
|
|
2119bc4c26 | ||
|
|
6b936ea5e1 | ||
|
|
39eaa722ac | ||
|
|
5c2b7e545e | ||
|
|
0da6e76ba6 | ||
|
|
4f82e9cd9c | ||
|
|
6cfaba6bd7 | ||
|
|
2c177232b1 | ||
|
|
efb82abee7 | ||
|
|
159ff84768 | ||
|
|
2961581a52 | ||
|
|
91a9ca8cf8 | ||
|
|
b43f7cfb87 | ||
|
|
aa321a82e1 | ||
|
|
6e09bca806 | ||
|
|
c9e61e3d66 | ||
|
|
e7d17c54a7 | ||
|
|
abc934d88f | ||
|
|
19eec82770 | ||
|
|
b43891193e | ||
|
|
36003324f3 | ||
|
|
3e5ff4cdda | ||
|
|
d174038dce | ||
|
|
78d09e2940 | ||
|
|
6cb7f152e1 | ||
|
|
f6730d3d09 | ||
|
|
899a6ab70a | ||
|
|
a4b852bb99 | ||
|
|
92cd108c0d | ||
|
|
34c116fc7e | ||
|
|
250646a354 | ||
|
|
00191d5774 | ||
|
|
525a0d7a1a | ||
|
|
564edc7430 | ||
|
|
78f396b94a | ||
|
|
9e53c150b8 | ||
|
|
f80a6c3014 | ||
|
|
1eff6d82c9 | ||
|
|
f138eff26c | ||
|
|
50f3fc0ff9 | ||
|
|
ebcb172614 | ||
|
|
133c0deaa8 |
@@ -10,7 +10,6 @@ import (
|
||||
"github.com/gorilla/handlers"
|
||||
|
||||
"github.com/SigNoz/signoz/ee/query-service/app/api"
|
||||
"github.com/SigNoz/signoz/ee/query-service/constants"
|
||||
"github.com/SigNoz/signoz/ee/query-service/integrations/gateway"
|
||||
"github.com/SigNoz/signoz/ee/query-service/rules"
|
||||
"github.com/SigNoz/signoz/ee/query-service/usage"
|
||||
@@ -39,7 +38,6 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/query-service/healthcheck"
|
||||
baseint "github.com/SigNoz/signoz/pkg/query-service/interfaces"
|
||||
baserules "github.com/SigNoz/signoz/pkg/query-service/rules"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/telemetry"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/utils"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
@@ -157,15 +155,6 @@ func NewServer(config signoz.Config, signoz *signoz.SigNoz, jwt *authtypes.JWT)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
telemetry.GetInstance().SetReader(reader)
|
||||
telemetry.GetInstance().SetSqlStore(signoz.SQLStore)
|
||||
telemetry.GetInstance().SetSaasOperator(constants.SaasSegmentKey)
|
||||
telemetry.GetInstance().SetSavedViewsInfoCallback(telemetry.GetSavedViewsInfo)
|
||||
telemetry.GetInstance().SetAlertsInfoCallback(telemetry.GetAlertsInfo)
|
||||
telemetry.GetInstance().SetGetUsersCallback(telemetry.GetUsers)
|
||||
telemetry.GetInstance().SetUserCountCallback(telemetry.GetUserCount)
|
||||
telemetry.GetInstance().SetDashboardsInfoCallback(telemetry.GetDashboardsInfo)
|
||||
|
||||
apiOpts := api.APIHandlerOptions{
|
||||
DataConnector: reader,
|
||||
RulesManager: rm,
|
||||
@@ -211,7 +200,7 @@ func NewServer(config signoz.Config, signoz *signoz.SigNoz, jwt *authtypes.JWT)
|
||||
s.privateHTTP = privateServer
|
||||
|
||||
s.opampServer = opamp.InitializeServer(
|
||||
&opAmpModel.AllAgents, agentConfMgr,
|
||||
&opAmpModel.AllAgents, agentConfMgr, signoz.Instrumentation,
|
||||
)
|
||||
|
||||
orgs, err := apiHandler.Signoz.Modules.OrgGetter.ListByOwnedKeyRange(context.Background())
|
||||
@@ -243,7 +232,6 @@ func (s *Server) createPrivateServer(apiHandler *api.APIHandler) (*http.Server,
|
||||
s.config.APIServer.Timeout.Default,
|
||||
s.config.APIServer.Timeout.Max,
|
||||
).Wrap)
|
||||
r.Use(middleware.NewAnalytics().Wrap)
|
||||
r.Use(middleware.NewLogging(s.signoz.Instrumentation.Logger(), s.config.APIServer.Logging.ExcludedRoutes).Wrap)
|
||||
|
||||
apiHandler.RegisterPrivateRoutes(r)
|
||||
@@ -275,7 +263,6 @@ func (s *Server) createPublicServer(apiHandler *api.APIHandler, web web.Web) (*h
|
||||
s.config.APIServer.Timeout.Default,
|
||||
s.config.APIServer.Timeout.Max,
|
||||
).Wrap)
|
||||
r.Use(middleware.NewAnalytics().Wrap)
|
||||
r.Use(middleware.NewLogging(s.signoz.Instrumentation.Logger(), s.config.APIServer.Logging.ExcludedRoutes).Wrap)
|
||||
|
||||
apiHandler.RegisterRoutes(r, am)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
module.exports = {
|
||||
ignorePatterns: ['src/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)
|
||||
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",
|
||||
|
||||
@@ -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}`);
|
||||
@@ -1,21 +0,0 @@
|
||||
import axios from 'api';
|
||||
import { Props } from 'types/api/userFeedback/sendResponse';
|
||||
|
||||
const sendFeedback = async (props: Props): Promise<number> => {
|
||||
const response = await axios.post(
|
||||
'/feedback',
|
||||
{
|
||||
email: props.email,
|
||||
message: props.message,
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
return response.status;
|
||||
};
|
||||
|
||||
export default sendFeedback;
|
||||
168
frontend/src/api/v5/queryRange/constants.ts
Normal file
168
frontend/src/api/v5/queryRange/constants.ts
Normal file
@@ -0,0 +1,168 @@
|
||||
// V5 Query Range Constants
|
||||
|
||||
import { ENTITY_VERSION_V5 } from 'constants/app';
|
||||
import {
|
||||
FunctionName,
|
||||
RequestType,
|
||||
SignalType,
|
||||
Step,
|
||||
} from 'types/api/v5/queryRange';
|
||||
|
||||
// ===================== Schema and Version Constants =====================
|
||||
|
||||
export const SCHEMA_VERSION_V5 = ENTITY_VERSION_V5;
|
||||
export const API_VERSION_V5 = 'v5';
|
||||
|
||||
// ===================== Default Values =====================
|
||||
|
||||
export const DEFAULT_STEP_INTERVAL: Step = '60s';
|
||||
export const DEFAULT_LIMIT = 100;
|
||||
export const DEFAULT_OFFSET = 0;
|
||||
|
||||
// ===================== Request Type Constants =====================
|
||||
|
||||
export const REQUEST_TYPES: Record<string, RequestType> = {
|
||||
SCALAR: 'scalar',
|
||||
TIME_SERIES: 'time_series',
|
||||
RAW: 'raw',
|
||||
DISTRIBUTION: 'distribution',
|
||||
} as const;
|
||||
|
||||
// ===================== Signal Type Constants =====================
|
||||
|
||||
export const SIGNAL_TYPES: Record<string, SignalType> = {
|
||||
TRACES: 'traces',
|
||||
LOGS: 'logs',
|
||||
METRICS: 'metrics',
|
||||
} as const;
|
||||
|
||||
// ===================== Common Aggregation Expressions =====================
|
||||
|
||||
export const TRACE_AGGREGATIONS = {
|
||||
COUNT: 'count()',
|
||||
COUNT_DISTINCT_TRACE_ID: 'count_distinct(traceID)',
|
||||
AVG_DURATION: 'avg(duration_nano)',
|
||||
P50_DURATION: 'p50(duration_nano)',
|
||||
P95_DURATION: 'p95(duration_nano)',
|
||||
P99_DURATION: 'p99(duration_nano)',
|
||||
MAX_DURATION: 'max(duration_nano)',
|
||||
MIN_DURATION: 'min(duration_nano)',
|
||||
SUM_DURATION: 'sum(duration_nano)',
|
||||
} as const;
|
||||
|
||||
export const LOG_AGGREGATIONS = {
|
||||
COUNT: 'count()',
|
||||
COUNT_DISTINCT_HOST: 'count_distinct(host.name)',
|
||||
COUNT_DISTINCT_SERVICE: 'count_distinct(service.name)',
|
||||
COUNT_DISTINCT_CONTAINER: 'count_distinct(container.name)',
|
||||
} as const;
|
||||
|
||||
// ===================== Common Filter Expressions =====================
|
||||
|
||||
export const COMMON_FILTERS = {
|
||||
// Trace filters
|
||||
SERVER_SPANS: "kind_string = 'Server'",
|
||||
CLIENT_SPANS: "kind_string = 'Client'",
|
||||
INTERNAL_SPANS: "kind_string = 'Internal'",
|
||||
ERROR_SPANS: 'http.status_code >= 400',
|
||||
SUCCESS_SPANS: 'http.status_code < 400',
|
||||
|
||||
// Common service filters
|
||||
EXCLUDE_HEALTH_CHECKS: "http.route != '/health' AND http.route != '/ping'",
|
||||
HTTP_REQUESTS: "http.method != ''",
|
||||
|
||||
// Log filters
|
||||
ERROR_LOGS: "severity_text = 'ERROR'",
|
||||
WARN_LOGS: "severity_text = 'WARN'",
|
||||
INFO_LOGS: "severity_text = 'INFO'",
|
||||
DEBUG_LOGS: "severity_text = 'DEBUG'",
|
||||
} as const;
|
||||
|
||||
// ===================== Common Group By Fields =====================
|
||||
|
||||
export const COMMON_GROUP_BY_FIELDS = {
|
||||
SERVICE_NAME: {
|
||||
name: 'service.name',
|
||||
fieldDataType: 'string' as const,
|
||||
fieldContext: 'resource' as const,
|
||||
},
|
||||
HTTP_METHOD: {
|
||||
name: 'http.method',
|
||||
fieldDataType: 'string' as const,
|
||||
fieldContext: 'attribute' as const,
|
||||
},
|
||||
HTTP_ROUTE: {
|
||||
name: 'http.route',
|
||||
fieldDataType: 'string' as const,
|
||||
fieldContext: 'attribute' as const,
|
||||
},
|
||||
HTTP_STATUS_CODE: {
|
||||
name: 'http.status_code',
|
||||
fieldDataType: 'int64' as const,
|
||||
fieldContext: 'attribute' as const,
|
||||
},
|
||||
HOST_NAME: {
|
||||
name: 'host.name',
|
||||
fieldDataType: 'string' as const,
|
||||
fieldContext: 'resource' as const,
|
||||
},
|
||||
CONTAINER_NAME: {
|
||||
name: 'container.name',
|
||||
fieldDataType: 'string' as const,
|
||||
fieldContext: 'resource' as const,
|
||||
},
|
||||
} as const;
|
||||
|
||||
// ===================== Function Names =====================
|
||||
|
||||
export const FUNCTION_NAMES: Record<string, FunctionName> = {
|
||||
CUT_OFF_MIN: 'cutOffMin',
|
||||
CUT_OFF_MAX: 'cutOffMax',
|
||||
CLAMP_MIN: 'clampMin',
|
||||
CLAMP_MAX: 'clampMax',
|
||||
ABSOLUTE: 'absolute',
|
||||
RUNNING_DIFF: 'runningDiff',
|
||||
LOG2: 'log2',
|
||||
LOG10: 'log10',
|
||||
CUM_SUM: 'cumSum',
|
||||
EWMA3: 'ewma3',
|
||||
EWMA5: 'ewma5',
|
||||
EWMA7: 'ewma7',
|
||||
MEDIAN3: 'median3',
|
||||
MEDIAN5: 'median5',
|
||||
MEDIAN7: 'median7',
|
||||
TIME_SHIFT: 'timeShift',
|
||||
ANOMALY: 'anomaly',
|
||||
} as const;
|
||||
|
||||
// ===================== Common Step Intervals =====================
|
||||
|
||||
export const STEP_INTERVALS = {
|
||||
FIFTEEN_SECONDS: '15s',
|
||||
THIRTY_SECONDS: '30s',
|
||||
ONE_MINUTE: '60s',
|
||||
FIVE_MINUTES: '300s',
|
||||
TEN_MINUTES: '600s',
|
||||
FIFTEEN_MINUTES: '900s',
|
||||
THIRTY_MINUTES: '1800s',
|
||||
ONE_HOUR: '3600s',
|
||||
TWO_HOURS: '7200s',
|
||||
SIX_HOURS: '21600s',
|
||||
TWELVE_HOURS: '43200s',
|
||||
ONE_DAY: '86400s',
|
||||
} as const;
|
||||
|
||||
// ===================== Time Range Presets =====================
|
||||
|
||||
export const TIME_RANGE_PRESETS = {
|
||||
LAST_5_MINUTES: 5 * 60 * 1000,
|
||||
LAST_15_MINUTES: 15 * 60 * 1000,
|
||||
LAST_30_MINUTES: 30 * 60 * 1000,
|
||||
LAST_HOUR: 60 * 60 * 1000,
|
||||
LAST_3_HOURS: 3 * 60 * 60 * 1000,
|
||||
LAST_6_HOURS: 6 * 60 * 60 * 1000,
|
||||
LAST_12_HOURS: 12 * 60 * 60 * 1000,
|
||||
LAST_24_HOURS: 24 * 60 * 60 * 1000,
|
||||
LAST_3_DAYS: 3 * 24 * 60 * 60 * 1000,
|
||||
LAST_7_DAYS: 7 * 24 * 60 * 60 * 1000,
|
||||
} as const;
|
||||
239
frontend/src/api/v5/queryRange/convertV5Response.ts
Normal file
239
frontend/src/api/v5/queryRange/convertV5Response.ts
Normal file
@@ -0,0 +1,239 @@
|
||||
import { isEmpty } from 'lodash-es';
|
||||
import { SuccessResponse } from 'types/api';
|
||||
import { MetricRangePayloadV3 } from 'types/api/metrics/getQueryRange';
|
||||
import {
|
||||
DistributionData,
|
||||
MetricRangePayloadV5,
|
||||
RawData,
|
||||
ScalarData,
|
||||
TimeSeriesData,
|
||||
} from 'types/api/v5/queryRange';
|
||||
import { QueryDataV3 } from 'types/api/widgets/getQuery';
|
||||
|
||||
/**
|
||||
* Converts V5 TimeSeriesData to legacy format
|
||||
*/
|
||||
function convertTimeSeriesData(
|
||||
timeSeriesData: TimeSeriesData,
|
||||
legendMap: Record<string, string>,
|
||||
): QueryDataV3 {
|
||||
// Convert V5 time series format to legacy QueryDataV3 format
|
||||
return {
|
||||
queryName: timeSeriesData.queryName,
|
||||
legend: legendMap[timeSeriesData.queryName] || timeSeriesData.queryName,
|
||||
series: timeSeriesData?.aggregations?.flatMap((aggregation) =>
|
||||
aggregation.series.map((series) => ({
|
||||
labels: series.labels
|
||||
? Object.fromEntries(
|
||||
series.labels.map((label) => [label.key.name, label.value]),
|
||||
)
|
||||
: {},
|
||||
labelsArray: series.labels
|
||||
? series.labels.map((label) => ({ [label.key.name]: label.value }))
|
||||
: [],
|
||||
values: series.values.map((value) => ({
|
||||
timestamp: value.timestamp,
|
||||
value: String(value.value),
|
||||
})),
|
||||
})),
|
||||
),
|
||||
list: null,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts V5 ScalarData array to legacy format with table structure
|
||||
*/
|
||||
function convertScalarDataArrayToTable(
|
||||
scalarDataArray: ScalarData[],
|
||||
legendMap: Record<string, string>,
|
||||
): QueryDataV3[] {
|
||||
// If no scalar data, return empty structure
|
||||
|
||||
if (!scalarDataArray || scalarDataArray.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Process each scalar data separately to maintain query separation
|
||||
return scalarDataArray?.map((scalarData) => {
|
||||
// Get query name from the first column
|
||||
const queryName = scalarData?.columns?.[0]?.queryName || '';
|
||||
|
||||
// Collect columns for this specific query
|
||||
const columns = scalarData?.columns?.map((col) => ({
|
||||
name: col.columnType === 'aggregation' ? col.queryName : col.name,
|
||||
queryName: col.queryName,
|
||||
isValueColumn: col.columnType === 'aggregation',
|
||||
}));
|
||||
|
||||
// Process rows for this specific query
|
||||
const rows = scalarData?.data?.map((dataRow) => {
|
||||
const rowData: Record<string, any> = {};
|
||||
|
||||
scalarData?.columns?.forEach((col, colIndex) => {
|
||||
const columnName =
|
||||
col.columnType === 'aggregation' ? col.queryName : col.name;
|
||||
rowData[columnName] = dataRow[colIndex];
|
||||
});
|
||||
|
||||
return { data: rowData };
|
||||
});
|
||||
|
||||
return {
|
||||
queryName,
|
||||
legend: legendMap[queryName] || '',
|
||||
series: null,
|
||||
list: null,
|
||||
table: {
|
||||
columns,
|
||||
rows,
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts V5 RawData to legacy format
|
||||
*/
|
||||
function convertRawData(
|
||||
rawData: RawData,
|
||||
legendMap: Record<string, string>,
|
||||
): QueryDataV3 {
|
||||
// Convert V5 raw format to legacy QueryDataV3 format
|
||||
return {
|
||||
queryName: rawData.queryName,
|
||||
legend: legendMap[rawData.queryName] || rawData.queryName,
|
||||
series: null,
|
||||
list: rawData.rows?.map((row) => ({
|
||||
timestamp: row.timestamp,
|
||||
data: {
|
||||
// Map raw data to ILog structure - spread row.data first to include all properties
|
||||
...row.data,
|
||||
date: row.timestamp,
|
||||
} as any,
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts V5 DistributionData to legacy format
|
||||
*/
|
||||
function convertDistributionData(
|
||||
distributionData: DistributionData,
|
||||
legendMap: Record<string, string>,
|
||||
): any {
|
||||
// eslint-disable-line @typescript-eslint/no-explicit-any
|
||||
// Convert V5 distribution format to legacy histogram format
|
||||
return {
|
||||
...distributionData,
|
||||
legendMap,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to convert V5 data based on type
|
||||
*/
|
||||
function convertV5DataByType(
|
||||
v5Data: any,
|
||||
legendMap: Record<string, string>,
|
||||
): MetricRangePayloadV3['data'] {
|
||||
switch (v5Data?.type) {
|
||||
case 'time_series': {
|
||||
const timeSeriesData = v5Data.data.results as TimeSeriesData[];
|
||||
return {
|
||||
resultType: 'time_series',
|
||||
result: timeSeriesData.map((timeSeries) =>
|
||||
convertTimeSeriesData(timeSeries, legendMap),
|
||||
),
|
||||
};
|
||||
}
|
||||
case 'scalar': {
|
||||
const scalarData = v5Data.data.results as ScalarData[];
|
||||
// For scalar data, combine all results into separate table entries
|
||||
const combinedTables = convertScalarDataArrayToTable(scalarData, legendMap);
|
||||
return {
|
||||
resultType: 'scalar',
|
||||
result: combinedTables,
|
||||
};
|
||||
}
|
||||
case 'raw': {
|
||||
const rawData = v5Data.data.results as RawData[];
|
||||
return {
|
||||
resultType: 'raw',
|
||||
result: rawData.map((raw) => convertRawData(raw, legendMap)),
|
||||
};
|
||||
}
|
||||
case 'distribution': {
|
||||
const distributionData = v5Data.data.results as DistributionData[];
|
||||
return {
|
||||
resultType: 'distribution',
|
||||
result: distributionData.map((distribution) =>
|
||||
convertDistributionData(distribution, legendMap),
|
||||
),
|
||||
};
|
||||
}
|
||||
default:
|
||||
return {
|
||||
resultType: '',
|
||||
result: [],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts V5 API response to legacy format expected by frontend components
|
||||
*/
|
||||
export function convertV5ResponseToLegacy(
|
||||
v5Response: SuccessResponse<MetricRangePayloadV5>,
|
||||
legendMap: Record<string, string>,
|
||||
// formatForWeb?: boolean,
|
||||
): SuccessResponse<MetricRangePayloadV3> {
|
||||
const { payload } = v5Response;
|
||||
const v5Data = payload?.data;
|
||||
|
||||
// todo - sagar
|
||||
// If formatForWeb is true, return as-is (like existing logic)
|
||||
// Exception: scalar data should always be converted to table format
|
||||
// if (formatForWeb && v5Data?.type !== 'scalar') {
|
||||
// return v5Response as any;
|
||||
// }
|
||||
|
||||
// Convert based on V5 response type
|
||||
const convertedData = convertV5DataByType(v5Data, legendMap);
|
||||
|
||||
// Create legacy-compatible response structure
|
||||
const legacyResponse: SuccessResponse<MetricRangePayloadV3> = {
|
||||
...v5Response,
|
||||
payload: {
|
||||
data: convertedData,
|
||||
},
|
||||
};
|
||||
|
||||
// Apply legend mapping (similar to existing logic)
|
||||
if (legacyResponse.payload?.data?.result) {
|
||||
legacyResponse.payload.data.result = legacyResponse.payload.data.result.map(
|
||||
(queryData: any) => {
|
||||
// eslint-disable-line @typescript-eslint/no-explicit-any
|
||||
const newQueryData = queryData;
|
||||
newQueryData.legend = legendMap[queryData.queryName];
|
||||
|
||||
// If metric names is an empty object
|
||||
if (isEmpty(queryData.metric)) {
|
||||
// If metrics list is empty && the user haven't defined a legend then add the legend equal to the name of the query.
|
||||
if (newQueryData.legend === undefined || newQueryData.legend === null) {
|
||||
newQueryData.legend = queryData.queryName;
|
||||
}
|
||||
// If name of the query and the legend if inserted is same then add the same to the metrics object.
|
||||
if (queryData.queryName === newQueryData.legend) {
|
||||
newQueryData.metric = newQueryData.metric || {};
|
||||
newQueryData.metric[queryData.queryName] = queryData.queryName;
|
||||
}
|
||||
}
|
||||
|
||||
return newQueryData;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
return legacyResponse;
|
||||
}
|
||||
51
frontend/src/api/v5/queryRange/getQueryRange.ts
Normal file
51
frontend/src/api/v5/queryRange/getQueryRange.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import { ApiV5Instance } from 'api';
|
||||
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
||||
import { AxiosError } from 'axios';
|
||||
import { ENTITY_VERSION_V5 } from 'constants/app';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import {
|
||||
MetricRangePayloadV5,
|
||||
QueryRangePayloadV5,
|
||||
} from 'types/api/v5/queryRange';
|
||||
|
||||
export const getQueryRangeV5 = async (
|
||||
props: QueryRangePayloadV5,
|
||||
version: string,
|
||||
signal: AbortSignal,
|
||||
headers?: Record<string, string>,
|
||||
): Promise<SuccessResponse<MetricRangePayloadV5> | ErrorResponse> => {
|
||||
try {
|
||||
if (version && version === ENTITY_VERSION_V5) {
|
||||
const response = await ApiV5Instance.post('/query_range', props, {
|
||||
signal,
|
||||
headers,
|
||||
});
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
error: null,
|
||||
message: response.data.status,
|
||||
payload: response.data,
|
||||
params: props,
|
||||
};
|
||||
}
|
||||
|
||||
// Default V5 behavior
|
||||
const response = await ApiV5Instance.post('/query_range', props, {
|
||||
signal,
|
||||
headers,
|
||||
});
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
error: null,
|
||||
message: response.data.status,
|
||||
payload: response.data,
|
||||
params: props,
|
||||
};
|
||||
} catch (error) {
|
||||
return ErrorResponseHandler(error as AxiosError);
|
||||
}
|
||||
};
|
||||
|
||||
export default getQueryRangeV5;
|
||||
393
frontend/src/api/v5/queryRange/prepareQueryRangePayloadV5.ts
Normal file
393
frontend/src/api/v5/queryRange/prepareQueryRangePayloadV5.ts
Normal file
@@ -0,0 +1,393 @@
|
||||
/* eslint-disable sonarjs/cognitive-complexity */
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults';
|
||||
import getStartEndRangeTime from 'lib/getStartEndRangeTime';
|
||||
import { mapQueryDataToApi } from 'lib/newQueryBuilder/queryBuilderMappers/mapQueryDataToApi';
|
||||
import { isEmpty } from 'lodash-es';
|
||||
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import {
|
||||
IBuilderQuery,
|
||||
QueryFunctionProps,
|
||||
} from 'types/api/queryBuilder/queryBuilderData';
|
||||
import {
|
||||
BaseBuilderQuery,
|
||||
FieldContext,
|
||||
FieldDataType,
|
||||
FunctionName,
|
||||
GroupByKey,
|
||||
LogAggregation,
|
||||
MetricAggregation,
|
||||
OrderBy,
|
||||
QueryEnvelope,
|
||||
QueryFunction,
|
||||
QueryRangePayloadV5,
|
||||
QueryType,
|
||||
RequestType,
|
||||
TelemetryFieldKey,
|
||||
TraceAggregation,
|
||||
VariableItem,
|
||||
} from 'types/api/v5/queryRange';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
type PrepareQueryRangePayloadV5Result = {
|
||||
queryPayload: QueryRangePayloadV5;
|
||||
legendMap: Record<string, string>;
|
||||
};
|
||||
|
||||
/**
|
||||
* Maps panel types to V5 request types
|
||||
*/
|
||||
function mapPanelTypeToRequestType(panelType: PANEL_TYPES): RequestType {
|
||||
switch (panelType) {
|
||||
case PANEL_TYPES.TIME_SERIES:
|
||||
case PANEL_TYPES.BAR:
|
||||
return 'time_series';
|
||||
case PANEL_TYPES.TABLE:
|
||||
case PANEL_TYPES.PIE:
|
||||
case PANEL_TYPES.VALUE:
|
||||
case PANEL_TYPES.TRACE:
|
||||
return 'scalar';
|
||||
case PANEL_TYPES.LIST:
|
||||
return 'raw';
|
||||
case PANEL_TYPES.HISTOGRAM:
|
||||
return 'distribution';
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets signal type from data source
|
||||
*/
|
||||
function getSignalType(dataSource: string): 'traces' | 'logs' | 'metrics' {
|
||||
if (dataSource === 'traces') return 'traces';
|
||||
if (dataSource === 'logs') return 'logs';
|
||||
return 'metrics';
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates base spec for builder queries
|
||||
*/
|
||||
function createBaseSpec(
|
||||
queryData: IBuilderQuery,
|
||||
requestType: RequestType,
|
||||
panelType?: PANEL_TYPES,
|
||||
): BaseBuilderQuery {
|
||||
return {
|
||||
stepInterval: queryData.stepInterval,
|
||||
disabled: queryData.disabled,
|
||||
filter: queryData?.filter?.expression ? queryData.filter : undefined,
|
||||
groupBy:
|
||||
queryData.groupBy?.length > 0
|
||||
? queryData.groupBy.map(
|
||||
(item: any): GroupByKey => ({
|
||||
name: item.key,
|
||||
fieldDataType: item?.dataType,
|
||||
fieldContext: item?.type,
|
||||
description: item?.description,
|
||||
unit: item?.unit,
|
||||
signal: item?.signal,
|
||||
materialized: item?.materialized,
|
||||
}),
|
||||
)
|
||||
: undefined,
|
||||
limit:
|
||||
panelType === PANEL_TYPES.TABLE || panelType === PANEL_TYPES.LIST
|
||||
? queryData.limit || queryData.pageSize || undefined
|
||||
: queryData.limit || undefined,
|
||||
offset: requestType === 'raw' ? queryData.offset : undefined,
|
||||
order:
|
||||
queryData.orderBy.length > 0
|
||||
? queryData.orderBy.map(
|
||||
(order: any): OrderBy => ({
|
||||
key: {
|
||||
name: order.columnName,
|
||||
},
|
||||
direction: order.order,
|
||||
}),
|
||||
)
|
||||
: undefined,
|
||||
// legend: isEmpty(queryData.legend) ? undefined : queryData.legend,
|
||||
having: isEmpty(queryData.havingExpression)
|
||||
? undefined
|
||||
: queryData?.havingExpression,
|
||||
functions: isEmpty(queryData.functions)
|
||||
? undefined
|
||||
: queryData.functions.map(
|
||||
(func: QueryFunctionProps): QueryFunction => ({
|
||||
name: func.name as FunctionName,
|
||||
args: func.args.map((arg) => ({
|
||||
// name: arg.name,
|
||||
value: arg,
|
||||
})),
|
||||
}),
|
||||
),
|
||||
selectFields: isEmpty(queryData.selectColumns)
|
||||
? undefined
|
||||
: queryData.selectColumns?.map(
|
||||
(column: BaseAutocompleteData): TelemetryFieldKey => ({
|
||||
name: column.key,
|
||||
fieldDataType: column?.dataType as FieldDataType,
|
||||
fieldContext: column?.type as FieldContext,
|
||||
}),
|
||||
),
|
||||
};
|
||||
}
|
||||
// Utility to parse aggregation expressions with optional alias
|
||||
export function parseAggregations(
|
||||
expression: string,
|
||||
): { expression: string; alias?: string }[] {
|
||||
const result: { expression: string; alias?: string }[] = [];
|
||||
const regex = /([a-zA-Z0-9_]+\([^)]*\))(?:\s*as\s+([a-zA-Z0-9_]+))?/g;
|
||||
let match = regex.exec(expression);
|
||||
while (match !== null) {
|
||||
const expr = match[1];
|
||||
const alias = match[2];
|
||||
if (alias) {
|
||||
result.push({ expression: expr, alias });
|
||||
} else {
|
||||
result.push({ expression: expr });
|
||||
}
|
||||
match = regex.exec(expression);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export function createAggregation(
|
||||
queryData: any,
|
||||
): TraceAggregation[] | LogAggregation[] | MetricAggregation[] {
|
||||
if (queryData.dataSource === DataSource.METRICS) {
|
||||
return [
|
||||
{
|
||||
metricName: queryData?.aggregateAttribute?.key,
|
||||
temporality: queryData?.aggregateAttribute?.temporality,
|
||||
timeAggregation: queryData?.timeAggregation,
|
||||
spaceAggregation: queryData?.spaceAggregation,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
if (queryData.aggregations?.length > 0) {
|
||||
return isEmpty(parseAggregations(queryData.aggregations?.[0].expression))
|
||||
? [{ expression: 'count()' }]
|
||||
: parseAggregations(queryData.aggregations?.[0].expression);
|
||||
}
|
||||
|
||||
return [{ expression: 'count()' }];
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts query builder data to V5 builder queries
|
||||
*/
|
||||
function convertBuilderQueriesToV5(
|
||||
builderQueries: Record<string, any>, // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||
requestType: RequestType,
|
||||
panelType?: PANEL_TYPES,
|
||||
): QueryEnvelope[] {
|
||||
return Object.entries(builderQueries).map(
|
||||
([queryName, queryData]): QueryEnvelope => {
|
||||
const signal = getSignalType(queryData.dataSource);
|
||||
const baseSpec = createBaseSpec(queryData, requestType, panelType);
|
||||
let spec: QueryEnvelope['spec'];
|
||||
|
||||
const aggregations = createAggregation(queryData);
|
||||
|
||||
switch (signal) {
|
||||
case 'traces':
|
||||
spec = {
|
||||
name: queryName,
|
||||
signal: 'traces' as const,
|
||||
...baseSpec,
|
||||
aggregations: aggregations as TraceAggregation[],
|
||||
};
|
||||
break;
|
||||
case 'logs':
|
||||
spec = {
|
||||
name: queryName,
|
||||
signal: 'logs' as const,
|
||||
...baseSpec,
|
||||
aggregations: aggregations as LogAggregation[],
|
||||
};
|
||||
break;
|
||||
case 'metrics':
|
||||
default:
|
||||
spec = {
|
||||
name: queryName,
|
||||
signal: 'metrics' as const,
|
||||
...baseSpec,
|
||||
aggregations: aggregations as MetricAggregation[],
|
||||
// reduceTo: queryData.reduceTo,
|
||||
};
|
||||
break;
|
||||
}
|
||||
|
||||
return {
|
||||
type: 'builder_query' as QueryType,
|
||||
spec,
|
||||
};
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts PromQL queries to V5 format
|
||||
*/
|
||||
function convertPromQueriesToV5(
|
||||
promQueries: Record<string, any>, // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||
): QueryEnvelope[] {
|
||||
return Object.entries(promQueries).map(
|
||||
([queryName, queryData]): QueryEnvelope => ({
|
||||
type: 'promql' as QueryType,
|
||||
spec: {
|
||||
name: queryName,
|
||||
query: queryData.query,
|
||||
disabled: queryData.disabled || false,
|
||||
step: queryData.stepInterval,
|
||||
stats: false, // PromQL specific field
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts ClickHouse queries to V5 format
|
||||
*/
|
||||
function convertClickHouseQueriesToV5(
|
||||
chQueries: Record<string, any>, // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||
): QueryEnvelope[] {
|
||||
return Object.entries(chQueries).map(
|
||||
([queryName, queryData]): QueryEnvelope => ({
|
||||
type: 'clickhouse_sql' as QueryType,
|
||||
spec: {
|
||||
name: queryName,
|
||||
query: queryData.query,
|
||||
disabled: queryData.disabled || false,
|
||||
// ClickHouse doesn't have step or stats like PromQL
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts query formulas to V5 format
|
||||
*/
|
||||
function convertFormulasToV5(
|
||||
formulas: Record<string, any>, // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||
): QueryEnvelope[] {
|
||||
return Object.entries(formulas).map(
|
||||
([queryName, formulaData]): QueryEnvelope => ({
|
||||
type: 'builder_formula' as QueryType,
|
||||
spec: {
|
||||
name: queryName,
|
||||
expression: formulaData.expression || '',
|
||||
functions: formulaData.functions,
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to reduce query arrays to objects
|
||||
*/
|
||||
function reduceQueriesToObject(
|
||||
queryArray: any[], // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||
): { queries: Record<string, any>; legends: Record<string, string> } {
|
||||
// eslint-disable-line @typescript-eslint/no-explicit-any
|
||||
const legends: Record<string, string> = {};
|
||||
const queries = queryArray.reduce((acc, queryItem) => {
|
||||
if (!queryItem.query) return acc;
|
||||
acc[queryItem.name] = queryItem;
|
||||
legends[queryItem.name] = queryItem.legend;
|
||||
return acc;
|
||||
}, {} as Record<string, any>); // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||
|
||||
return { queries, legends };
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares V5 query range payload from GetQueryResultsProps
|
||||
*/
|
||||
export const prepareQueryRangePayloadV5 = ({
|
||||
query,
|
||||
globalSelectedInterval,
|
||||
graphType,
|
||||
selectedTime,
|
||||
tableParams,
|
||||
variables = {},
|
||||
start: startTime,
|
||||
end: endTime,
|
||||
formatForWeb,
|
||||
}: GetQueryResultsProps): PrepareQueryRangePayloadV5Result => {
|
||||
let legendMap: Record<string, string> = {};
|
||||
const requestType = mapPanelTypeToRequestType(graphType);
|
||||
let queries: QueryEnvelope[] = [];
|
||||
|
||||
switch (query.queryType) {
|
||||
case EQueryType.QUERY_BUILDER: {
|
||||
const { queryData: data, queryFormulas } = query.builder;
|
||||
const currentQueryData = mapQueryDataToApi(data, 'queryName', tableParams);
|
||||
const currentFormulas = mapQueryDataToApi(queryFormulas, 'queryName');
|
||||
|
||||
// Combine legend maps
|
||||
legendMap = {
|
||||
...currentQueryData.newLegendMap,
|
||||
...currentFormulas.newLegendMap,
|
||||
};
|
||||
|
||||
// Convert builder queries
|
||||
const builderQueries = convertBuilderQueriesToV5(
|
||||
currentQueryData.data,
|
||||
requestType,
|
||||
graphType,
|
||||
);
|
||||
|
||||
// Convert formulas as separate query type
|
||||
const formulaQueries = convertFormulasToV5(currentFormulas.data);
|
||||
|
||||
// Combine both types
|
||||
queries = [...builderQueries, ...formulaQueries];
|
||||
break;
|
||||
}
|
||||
case EQueryType.PROM: {
|
||||
const promQueries = reduceQueriesToObject(query[query.queryType]);
|
||||
queries = convertPromQueriesToV5(promQueries.queries);
|
||||
legendMap = promQueries.legends;
|
||||
break;
|
||||
}
|
||||
case EQueryType.CLICKHOUSE: {
|
||||
const chQueries = reduceQueriesToObject(query[query.queryType]);
|
||||
queries = convertClickHouseQueriesToV5(chQueries.queries);
|
||||
legendMap = chQueries.legends;
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
// Calculate time range
|
||||
const { start, end } = getStartEndRangeTime({
|
||||
type: selectedTime,
|
||||
interval: globalSelectedInterval,
|
||||
});
|
||||
|
||||
// Create V5 payload
|
||||
const queryPayload: QueryRangePayloadV5 = {
|
||||
schemaVersion: 'v1',
|
||||
start: startTime ? startTime * 1e3 : parseInt(start, 10) * 1e3,
|
||||
end: endTime ? endTime * 1e3 : parseInt(end, 10) * 1e3,
|
||||
requestType,
|
||||
compositeQuery: {
|
||||
queries,
|
||||
},
|
||||
formatOptions: {
|
||||
formatTableResultForUI: !!formatForWeb,
|
||||
},
|
||||
variables: Object.entries(variables).reduce((acc, [key, value]) => {
|
||||
acc[key] = { value };
|
||||
return acc;
|
||||
}, {} as Record<string, VariableItem>),
|
||||
};
|
||||
|
||||
return { legendMap, queryPayload };
|
||||
};
|
||||
8
frontend/src/api/v5/v5.ts
Normal file
8
frontend/src/api/v5/v5.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
// V5 API exports
|
||||
export * from './queryRange/constants';
|
||||
export { convertV5ResponseToLegacy } from './queryRange/convertV5Response';
|
||||
export { getQueryRangeV5 } from './queryRange/getQueryRange';
|
||||
export { prepareQueryRangePayloadV5 } from './queryRange/prepareQueryRangePayloadV5';
|
||||
|
||||
// Export types from proper location
|
||||
export * from 'types/api/v5/queryRange';
|
||||
@@ -169,6 +169,7 @@
|
||||
box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
.ant-drawer-close {
|
||||
padding: 0px;
|
||||
}
|
||||
|
||||
@@ -430,9 +430,13 @@ function HostMetricsDetails({
|
||||
>
|
||||
{host.active ? 'ACTIVE' : 'INACTIVE'}
|
||||
</Tag>
|
||||
<Tag className="infra-monitoring-tags" bordered>
|
||||
{host.os}
|
||||
</Tag>
|
||||
{host.os ? (
|
||||
<Tag className="infra-monitoring-tags" bordered>
|
||||
{host.os}
|
||||
</Tag>
|
||||
) : (
|
||||
<Typography.Text>-</Typography.Text>
|
||||
)}
|
||||
<div className="progress-container">
|
||||
<Progress
|
||||
percent={Number((host.cpu * 100).toFixed(1))}
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
.input-with-label {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
border-radius: 2px 0px 0px 2px;
|
||||
|
||||
.label {
|
||||
color: var(--bg-vanilla-400);
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
line-height: 18px; /* 128.571% */
|
||||
letter-spacing: 0.56px;
|
||||
|
||||
max-width: 150px;
|
||||
min-width: 120px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
padding: 0px 8px;
|
||||
|
||||
border-radius: 2px 0px 0px 2px;
|
||||
border: 1px solid var(--bg-slate-400);
|
||||
background: var(--bg-ink-300);
|
||||
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
font-weight: var(--font-weight-light);
|
||||
}
|
||||
|
||||
.input {
|
||||
flex: 1;
|
||||
min-width: 150px;
|
||||
font-family: 'Space Mono', monospace !important;
|
||||
|
||||
border-radius: 2px 0px 0px 2px;
|
||||
border: 1px solid var(--bg-slate-400);
|
||||
background: var(--bg-ink-300);
|
||||
|
||||
border-right: none;
|
||||
border-left: none;
|
||||
border-top-right-radius: 0px;
|
||||
border-bottom-right-radius: 0px;
|
||||
border-top-left-radius: 0px;
|
||||
border-bottom-left-radius: 0px;
|
||||
}
|
||||
|
||||
.close-btn {
|
||||
border-radius: 0px 2px 2px 0px;
|
||||
border: 1px solid var(--bg-slate-400);
|
||||
background: var(--bg-ink-300);
|
||||
height: 38px;
|
||||
width: 38px;
|
||||
}
|
||||
|
||||
&.labelAfter {
|
||||
.input {
|
||||
border-radius: 0px 2px 2px 0px;
|
||||
border: 1px solid var(--bg-slate-400);
|
||||
background: var(--bg-ink-300);
|
||||
border-top-right-radius: 0px;
|
||||
border-bottom-right-radius: 0px;
|
||||
}
|
||||
|
||||
.label {
|
||||
border-left: none;
|
||||
border-top-left-radius: 0px;
|
||||
border-bottom-left-radius: 0px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.lightMode {
|
||||
.input-with-label {
|
||||
.label {
|
||||
color: var(--bg-ink-500) !important;
|
||||
|
||||
border: 1px solid var(--bg-vanilla-300) !important;
|
||||
background: var(--bg-vanilla-100) !important;
|
||||
}
|
||||
|
||||
.input {
|
||||
border: 1px solid var(--bg-vanilla-300) !important;
|
||||
background: var(--bg-vanilla-100) !important;
|
||||
}
|
||||
|
||||
.close-btn {
|
||||
border: 1px solid var(--bg-vanilla-300) !important;
|
||||
background: var(--bg-vanilla-100) !important;
|
||||
}
|
||||
|
||||
&.labelAfter {
|
||||
.input {
|
||||
border: 1px solid var(--bg-vanilla-300) !important;
|
||||
background: var(--bg-vanilla-100) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
71
frontend/src/components/InputWithLabel/InputWithLabel.tsx
Normal file
71
frontend/src/components/InputWithLabel/InputWithLabel.tsx
Normal file
@@ -0,0 +1,71 @@
|
||||
import './InputWithLabel.styles.scss';
|
||||
|
||||
import { Button, Input, Typography } from 'antd';
|
||||
import cx from 'classnames';
|
||||
import { X } from 'lucide-react';
|
||||
import { useState } from 'react';
|
||||
|
||||
function InputWithLabel({
|
||||
label,
|
||||
initialValue,
|
||||
placeholder,
|
||||
type,
|
||||
onClose,
|
||||
labelAfter,
|
||||
onChange,
|
||||
className,
|
||||
}: {
|
||||
label: string;
|
||||
initialValue?: string | number;
|
||||
placeholder: string;
|
||||
type?: string;
|
||||
onClose?: () => void;
|
||||
labelAfter?: boolean;
|
||||
onChange: (value: string) => void;
|
||||
className?: string;
|
||||
}): JSX.Element {
|
||||
const [inputValue, setInputValue] = useState<string>(
|
||||
initialValue ? initialValue.toString() : '',
|
||||
);
|
||||
|
||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement>): void => {
|
||||
setInputValue(e.target.value);
|
||||
onChange?.(e.target.value);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cx('input-with-label', className, {
|
||||
labelAfter,
|
||||
})}
|
||||
>
|
||||
{!labelAfter && <Typography.Text className="label">{label}</Typography.Text>}
|
||||
<Input
|
||||
className="input"
|
||||
placeholder={placeholder}
|
||||
type={type}
|
||||
value={inputValue}
|
||||
onChange={handleChange}
|
||||
name={label.toLowerCase()}
|
||||
/>
|
||||
{labelAfter && <Typography.Text className="label">{label}</Typography.Text>}
|
||||
{onClose && (
|
||||
<Button
|
||||
className="periscope-btn ghost close-btn"
|
||||
icon={<X size={16} />}
|
||||
onClick={onClose}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
InputWithLabel.defaultProps = {
|
||||
type: 'text',
|
||||
onClose: undefined,
|
||||
labelAfter: false,
|
||||
initialValue: undefined,
|
||||
className: undefined,
|
||||
};
|
||||
|
||||
export default InputWithLabel;
|
||||
@@ -20,6 +20,7 @@ export function getDefaultCellStyle(isDarkMode?: boolean): CSSProperties {
|
||||
|
||||
export const defaultTableStyle: CSSProperties = {
|
||||
minWidth: '40rem',
|
||||
maxWidth: '60rem',
|
||||
};
|
||||
|
||||
export const defaultListViewPanelStyle: CSSProperties = {
|
||||
|
||||
@@ -0,0 +1,553 @@
|
||||
.query-builder-v2 {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 4px;
|
||||
|
||||
width: 100%;
|
||||
|
||||
border-bottom: 1px solid var(--bg-slate-400);
|
||||
border-top: 1px solid var(--bg-slate-400);
|
||||
|
||||
font-family: Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto,
|
||||
'Helvetica Neue', sans-serif;
|
||||
|
||||
border-right: none;
|
||||
border-left: none;
|
||||
|
||||
.qb-content-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: calc(100% - 44px);
|
||||
|
||||
flex: 1;
|
||||
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.qb-content-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
padding: 8px;
|
||||
|
||||
flex: 1;
|
||||
|
||||
.qb-header-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
margin-left: 32px;
|
||||
|
||||
.query-actions-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 8px;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.qb-elements-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
|
||||
margin-left: 108px;
|
||||
|
||||
.code-mirror-where-clause,
|
||||
.query-aggregation-container,
|
||||
.query-add-ons,
|
||||
.metrics-aggregation-section-content {
|
||||
position: relative;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: -10px;
|
||||
top: 12px;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-left: 6px dotted #1d212d;
|
||||
}
|
||||
|
||||
/* Horizontal line pointing from vertical to the item */
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: -28px;
|
||||
top: 15px;
|
||||
width: 24px;
|
||||
height: 1px;
|
||||
background: repeating-linear-gradient(
|
||||
to right,
|
||||
#1d212d,
|
||||
#1d212d 4px,
|
||||
transparent 4px,
|
||||
transparent 8px
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.where-clause-view {
|
||||
.qb-content-section {
|
||||
.qb-elements-container {
|
||||
margin-left: 0px;
|
||||
|
||||
.code-mirror-where-clause,
|
||||
.query-aggregation-container,
|
||||
.query-add-ons,
|
||||
.metrics-aggregation-section-content {
|
||||
&::before {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&::after {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.query-names-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
|
||||
width: 44px;
|
||||
padding: 8px;
|
||||
|
||||
border-left: 1px solid var(--bg-slate-400);
|
||||
|
||||
.query-name {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
|
||||
padding: 4px;
|
||||
|
||||
border-radius: 0px 2px 2px 0px;
|
||||
|
||||
border-radius: 2px;
|
||||
border: 1px solid rgba(242, 71, 105, 0.2);
|
||||
background: rgba(242, 71, 105, 0.1);
|
||||
|
||||
color: var(--Sakura-400, #f56c87);
|
||||
font-family: 'Space Mono';
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 16px; /* 128.571% */
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.formula-name {
|
||||
display: flex;
|
||||
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
|
||||
padding: 4px;
|
||||
|
||||
border-radius: 0px 2px 2px 0px;
|
||||
|
||||
border-radius: 2px;
|
||||
border: 1px solid rgba(173, 127, 88, 0.2);
|
||||
background: rgba(173, 127, 88, 0.1);
|
||||
|
||||
color: var(--Sienna-500, #ad7f58);
|
||||
font-family: 'Space Mono';
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 16px; /* 128.571% */
|
||||
text-transform: uppercase;
|
||||
}
|
||||
}
|
||||
|
||||
.qb-formulas-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
|
||||
margin-left: 32px;
|
||||
padding-bottom: 16px;
|
||||
padding-left: 8px;
|
||||
|
||||
.qb-formula {
|
||||
.ant-row {
|
||||
row-gap: 0px !important;
|
||||
}
|
||||
|
||||
.qb-entity-options {
|
||||
margin-left: 8px;
|
||||
|
||||
padding-right: 8px;
|
||||
}
|
||||
|
||||
.formula-container {
|
||||
margin-left: 82px;
|
||||
padding: 4px 0px;
|
||||
|
||||
.ant-col {
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: -10px;
|
||||
top: 12px;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-left: 6px dotted #1d212d;
|
||||
}
|
||||
|
||||
/* Horizontal line pointing from vertical to the item */
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: -28px;
|
||||
top: 15px;
|
||||
width: 24px;
|
||||
height: 1px;
|
||||
background: repeating-linear-gradient(
|
||||
to right,
|
||||
#1d212d,
|
||||
#1d212d 4px,
|
||||
transparent 4px,
|
||||
transparent 8px
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
.formula-expression {
|
||||
border-bottom-left-radius: 0px !important;
|
||||
border-bottom-right-radius: 0px !important;
|
||||
|
||||
font-family: 'Space Mono';
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 16px; /* 128.571% */
|
||||
|
||||
resize: none;
|
||||
}
|
||||
|
||||
.formula-legend {
|
||||
border-top-left-radius: 0px !important;
|
||||
border-top-right-radius: 0px !important;
|
||||
|
||||
.ant-input-group-addon {
|
||||
border-top-left-radius: 0px !important;
|
||||
border-top-right-radius: 0px !important;
|
||||
}
|
||||
|
||||
.ant-input {
|
||||
border-top-left-radius: 0px !important;
|
||||
border-top-right-radius: 0px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.qb-footer {
|
||||
padding: 0 8px 16px 8px;
|
||||
|
||||
.qb-footer-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 8px;
|
||||
|
||||
margin-left: 32px;
|
||||
|
||||
.qb-add-new-query {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 8px;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
height: calc(100% - 82px);
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 56px;
|
||||
top: 31px;
|
||||
bottom: 0;
|
||||
width: 1px;
|
||||
background: repeating-linear-gradient(
|
||||
to bottom,
|
||||
#1d212d,
|
||||
#1d212d 4px,
|
||||
transparent 4px,
|
||||
transparent 8px
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.qb-entity-options {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 8px;
|
||||
|
||||
.options {
|
||||
.query-name {
|
||||
border-radius: 0px 2px 2px 0px !important;
|
||||
border: 1px solid rgba(242, 71, 105, 0.2) !important;
|
||||
background: rgba(242, 71, 105, 0.1) !important;
|
||||
|
||||
color: var(--Sakura-400, #f56c87) !important;
|
||||
font-family: 'Space Mono';
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 18px; /* 128.571% */
|
||||
text-transform: uppercase;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
height: 120px;
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 31px;
|
||||
bottom: 0;
|
||||
width: 1px;
|
||||
background: repeating-linear-gradient(
|
||||
to bottom,
|
||||
#1d212d,
|
||||
#1d212d 4px,
|
||||
transparent 4px,
|
||||
transparent 8px
|
||||
);
|
||||
left: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
.formula-name {
|
||||
border-radius: 0px 2px 2px 0px;
|
||||
border: 1px solid rgba(173, 127, 88, 0.2);
|
||||
background: rgba(173, 127, 88, 0.1);
|
||||
|
||||
font-family: 'Space Mono';
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 18px; /* 128.571% */
|
||||
text-transform: uppercase;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
height: 65px;
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 31px;
|
||||
bottom: 0;
|
||||
width: 1px;
|
||||
background: repeating-linear-gradient(
|
||||
to bottom,
|
||||
#1d212d,
|
||||
#1d212d 4px,
|
||||
transparent 4px,
|
||||
transparent 8px
|
||||
);
|
||||
left: 15px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.query-data-source {
|
||||
margin-left: 8px;
|
||||
|
||||
.ant-select-selector {
|
||||
min-width: 120px;
|
||||
|
||||
border-radius: 2px;
|
||||
border: 1px solid var(--Slate-400, #1d212d);
|
||||
background: var(--Ink-300, #16181d);
|
||||
box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.qb-search-container {
|
||||
.metrics-select-container {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.qb-search-filter-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
|
||||
flex-wrap: wrap;
|
||||
|
||||
.query-search-container {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.traces-search-filter-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
width: 180px;
|
||||
}
|
||||
|
||||
.ant-select {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.ant-select-selector {
|
||||
border-radius: 2px;
|
||||
border: 1px solid var(--Slate-400, #1d212d) !important;
|
||||
background: var(--Ink-300, #16181d) !important;
|
||||
height: 34px !important;
|
||||
box-sizing: border-box !important;
|
||||
}
|
||||
|
||||
.ant-select-arrow {
|
||||
color: var(--bg-vanilla-400) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.query-actions-dropdown {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.lightMode {
|
||||
.query-builder-v2 {
|
||||
border-bottom: 1px solid var(--bg-vanilla-300);
|
||||
border-top: 1px solid var(--bg-vanilla-300);
|
||||
|
||||
.qb-content-section {
|
||||
.qb-elements-container {
|
||||
.code-mirror-where-clause,
|
||||
.query-aggregation-container,
|
||||
.query-add-ons,
|
||||
.metrics-aggregation-section-content {
|
||||
&::before {
|
||||
border-left: 6px dotted var(--bg-vanilla-300);
|
||||
}
|
||||
|
||||
/* Horizontal line pointing from vertical to the item */
|
||||
&::after {
|
||||
background: repeating-linear-gradient(
|
||||
to right,
|
||||
var(--bg-vanilla-300),
|
||||
var(--bg-vanilla-300) 4px,
|
||||
transparent 4px,
|
||||
transparent 8px
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.query-names-section {
|
||||
border-left: 1px solid var(--bg-vanilla-300);
|
||||
}
|
||||
|
||||
.qb-formulas-container {
|
||||
.qb-formula {
|
||||
.formula-container {
|
||||
.ant-col {
|
||||
&::before {
|
||||
border-left: 6px dotted var(--bg-vanilla-300);
|
||||
}
|
||||
|
||||
/* Horizontal line pointing from vertical to the item */
|
||||
&::after {
|
||||
background: repeating-linear-gradient(
|
||||
to right,
|
||||
var(--bg-vanilla-300),
|
||||
var(--bg-vanilla-300) 4px,
|
||||
transparent 4px,
|
||||
transparent 8px
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.qb-footer {
|
||||
.qb-footer-container {
|
||||
.qb-add-new-query {
|
||||
&::before {
|
||||
background: repeating-linear-gradient(
|
||||
to bottom,
|
||||
var(--bg-vanilla-300),
|
||||
var(--bg-vanilla-300) 4px,
|
||||
transparent 4px,
|
||||
transparent 8px
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.qb-entity-options {
|
||||
.options {
|
||||
.query-name {
|
||||
&::before {
|
||||
background: repeating-linear-gradient(
|
||||
to bottom,
|
||||
var(--bg-vanilla-300),
|
||||
var(--bg-vanilla-300) 4px,
|
||||
transparent 4px,
|
||||
transparent 8px
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
.formula-name {
|
||||
&::before {
|
||||
background: repeating-linear-gradient(
|
||||
to bottom,
|
||||
var(--bg-vanilla-300),
|
||||
var(--bg-vanilla-300) 4px,
|
||||
transparent 4px,
|
||||
transparent 8px
|
||||
);
|
||||
left: 15px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.query-data-source {
|
||||
.ant-select-selector {
|
||||
border: 1px solid var(--bg-vanilla-300) !important;
|
||||
background: var(--bg-vanilla-100) !important;
|
||||
box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.1) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.qb-search-filter-container {
|
||||
.ant-select-selector {
|
||||
border: 1px solid var(--bg-vanilla-300) !important;
|
||||
background: var(--bg-vanilla-100) !important;
|
||||
box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.1) !important;
|
||||
}
|
||||
|
||||
.ant-select-arrow {
|
||||
color: var(--bg-vanilla-400) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
185
frontend/src/components/QueryBuilderV2/QueryBuilderV2.tsx
Normal file
185
frontend/src/components/QueryBuilderV2/QueryBuilderV2.tsx
Normal file
@@ -0,0 +1,185 @@
|
||||
import './QueryBuilderV2.styles.scss';
|
||||
|
||||
import { OPERATORS, PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { Formula } from 'container/QueryBuilder/components/Formula';
|
||||
import { QueryBuilderProps } from 'container/QueryBuilder/QueryBuilder.interfaces';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { memo, useEffect, useMemo, useRef } from 'react';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
import { QueryBuilderV2Provider } from './QueryBuilderV2Context';
|
||||
import QueryFooter from './QueryV2/QueryFooter/QueryFooter';
|
||||
import { QueryV2 } from './QueryV2/QueryV2';
|
||||
|
||||
export const QueryBuilderV2 = memo(function QueryBuilderV2({
|
||||
config,
|
||||
panelType: newPanelType,
|
||||
filterConfigs = {},
|
||||
queryComponents,
|
||||
isListViewPanel = false,
|
||||
showOnlyWhereClause = false,
|
||||
version,
|
||||
}: QueryBuilderProps): JSX.Element {
|
||||
const {
|
||||
currentQuery,
|
||||
addNewBuilderQuery,
|
||||
addNewFormula,
|
||||
handleSetConfig,
|
||||
panelType,
|
||||
initialDataSource,
|
||||
} = useQueryBuilder();
|
||||
|
||||
const containerRef = useRef(null);
|
||||
|
||||
const currentDataSource = useMemo(
|
||||
() =>
|
||||
(config && config.queryVariant === 'static' && config.initialDataSource) ||
|
||||
null,
|
||||
[config],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (currentDataSource !== initialDataSource || newPanelType !== panelType) {
|
||||
if (newPanelType === PANEL_TYPES.BAR) {
|
||||
handleSetConfig(PANEL_TYPES.BAR, DataSource.METRICS);
|
||||
return;
|
||||
}
|
||||
handleSetConfig(newPanelType, currentDataSource);
|
||||
}
|
||||
}, [
|
||||
handleSetConfig,
|
||||
panelType,
|
||||
initialDataSource,
|
||||
currentDataSource,
|
||||
newPanelType,
|
||||
]);
|
||||
|
||||
const listViewLogFilterConfigs: QueryBuilderProps['filterConfigs'] = useMemo(() => {
|
||||
const config: QueryBuilderProps['filterConfigs'] = {
|
||||
stepInterval: { isHidden: true, isDisabled: true },
|
||||
having: { isHidden: true, isDisabled: true },
|
||||
filters: {
|
||||
customKey: 'body',
|
||||
customOp: OPERATORS.CONTAINS,
|
||||
},
|
||||
};
|
||||
|
||||
return config;
|
||||
}, []);
|
||||
|
||||
const listViewTracesFilterConfigs: QueryBuilderProps['filterConfigs'] = useMemo(() => {
|
||||
const config: QueryBuilderProps['filterConfigs'] = {
|
||||
stepInterval: { isHidden: true, isDisabled: true },
|
||||
having: { isHidden: true, isDisabled: true },
|
||||
limit: { isHidden: true, isDisabled: true },
|
||||
filters: {
|
||||
customKey: 'body',
|
||||
customOp: OPERATORS.CONTAINS,
|
||||
},
|
||||
};
|
||||
|
||||
return config;
|
||||
}, []);
|
||||
|
||||
const queryFilterConfigs = useMemo(() => {
|
||||
if (isListViewPanel) {
|
||||
return currentQuery.builder.queryData[0].dataSource === DataSource.TRACES
|
||||
? listViewTracesFilterConfigs
|
||||
: listViewLogFilterConfigs;
|
||||
}
|
||||
|
||||
return filterConfigs;
|
||||
}, [
|
||||
isListViewPanel,
|
||||
filterConfigs,
|
||||
currentQuery.builder.queryData,
|
||||
listViewLogFilterConfigs,
|
||||
listViewTracesFilterConfigs,
|
||||
]);
|
||||
|
||||
return (
|
||||
<QueryBuilderV2Provider>
|
||||
<div className="query-builder-v2">
|
||||
<div className="qb-content-container">
|
||||
{isListViewPanel && (
|
||||
<QueryV2
|
||||
ref={containerRef}
|
||||
key={currentQuery.builder.queryData[0].queryName}
|
||||
index={0}
|
||||
query={currentQuery.builder.queryData[0]}
|
||||
filterConfigs={queryFilterConfigs}
|
||||
queryComponents={queryComponents}
|
||||
version={version}
|
||||
isAvailableToDisable={false}
|
||||
queryVariant={config?.queryVariant || 'dropdown'}
|
||||
showOnlyWhereClause={showOnlyWhereClause}
|
||||
isListViewPanel={isListViewPanel}
|
||||
/>
|
||||
)}
|
||||
|
||||
{!isListViewPanel &&
|
||||
currentQuery.builder.queryData.map((query, index) => (
|
||||
<QueryV2
|
||||
ref={containerRef}
|
||||
key={query.queryName}
|
||||
index={index}
|
||||
query={query}
|
||||
filterConfigs={queryFilterConfigs}
|
||||
queryComponents={queryComponents}
|
||||
version={version}
|
||||
isAvailableToDisable={false}
|
||||
queryVariant={config?.queryVariant || 'dropdown'}
|
||||
showOnlyWhereClause={showOnlyWhereClause}
|
||||
isListViewPanel={isListViewPanel}
|
||||
/>
|
||||
))}
|
||||
|
||||
{!showOnlyWhereClause && currentQuery.builder.queryFormulas.length > 0 && (
|
||||
<div className="qb-formulas-container">
|
||||
{currentQuery.builder.queryFormulas.map((formula, index) => {
|
||||
const query =
|
||||
currentQuery.builder.queryData[index] ||
|
||||
currentQuery.builder.queryData[0];
|
||||
|
||||
return (
|
||||
<div key={formula.queryName} className="qb-formula">
|
||||
<Formula
|
||||
filterConfigs={filterConfigs}
|
||||
query={query}
|
||||
formula={formula}
|
||||
index={index}
|
||||
isAdditionalFilterEnable={false}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!showOnlyWhereClause && !isListViewPanel && (
|
||||
<QueryFooter
|
||||
addNewBuilderQuery={addNewBuilderQuery}
|
||||
addNewFormula={addNewFormula}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{!showOnlyWhereClause && !isListViewPanel && (
|
||||
<div className="query-names-section">
|
||||
{currentQuery.builder.queryData.map((query) => (
|
||||
<div key={query.queryName} className="query-name">
|
||||
{query.queryName}
|
||||
</div>
|
||||
))}
|
||||
|
||||
{currentQuery.builder.queryFormulas.map((formula) => (
|
||||
<div key={formula.queryName} className="formula-name">
|
||||
{formula.queryName}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</QueryBuilderV2Provider>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,62 @@
|
||||
import { createContext, ReactNode, useContext, useMemo, useState } from 'react';
|
||||
|
||||
// Types for the context state
|
||||
export type AggregationOption = { func: string; arg: string };
|
||||
|
||||
interface QueryBuilderV2ContextType {
|
||||
searchText: string;
|
||||
setSearchText: (text: string) => void;
|
||||
aggregationOptions: AggregationOption[];
|
||||
setAggregationOptions: (options: AggregationOption[]) => void;
|
||||
aggregationInterval: string;
|
||||
setAggregationInterval: (interval: string) => void;
|
||||
queryAddValues: any; // Replace 'any' with a more specific type if available
|
||||
setQueryAddValues: (values: any) => void;
|
||||
}
|
||||
|
||||
const QueryBuilderV2Context = createContext<
|
||||
QueryBuilderV2ContextType | undefined
|
||||
>(undefined);
|
||||
|
||||
export function QueryBuilderV2Provider({
|
||||
children,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
}): JSX.Element {
|
||||
const [searchText, setSearchText] = useState('');
|
||||
const [aggregationOptions, setAggregationOptions] = useState<
|
||||
AggregationOption[]
|
||||
>([]);
|
||||
const [aggregationInterval, setAggregationInterval] = useState('');
|
||||
const [queryAddValues, setQueryAddValues] = useState<any>(null); // Replace 'any' if you have a type
|
||||
|
||||
return (
|
||||
<QueryBuilderV2Context.Provider
|
||||
value={useMemo(
|
||||
() => ({
|
||||
searchText,
|
||||
setSearchText,
|
||||
aggregationOptions,
|
||||
setAggregationOptions,
|
||||
aggregationInterval,
|
||||
setAggregationInterval,
|
||||
queryAddValues,
|
||||
setQueryAddValues,
|
||||
}),
|
||||
[searchText, aggregationOptions, aggregationInterval, queryAddValues],
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</QueryBuilderV2Context.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export const useQueryBuilderV2Context = (): QueryBuilderV2ContextType => {
|
||||
const context = useContext(QueryBuilderV2Context);
|
||||
if (!context) {
|
||||
throw new Error(
|
||||
'useQueryBuilderV2Context must be used within a QueryBuilderV2Provider',
|
||||
);
|
||||
}
|
||||
return context;
|
||||
};
|
||||
@@ -0,0 +1,97 @@
|
||||
.metrics-aggregate-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
margin: 4px 0;
|
||||
|
||||
.metrics-time-aggregation-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
|
||||
.metrics-time-aggregation-section-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
|
||||
color: var(--Slate-50, #62687c);
|
||||
font-family: 'Geist Mono';
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
line-height: 18px; /* 150% */
|
||||
letter-spacing: 0.48px;
|
||||
}
|
||||
}
|
||||
|
||||
.metrics-space-aggregation-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
|
||||
.metrics-space-aggregation-section-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
|
||||
color: var(--Slate-50, #62687c);
|
||||
font-family: 'Geist Mono';
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
line-height: 18px; /* 150% */
|
||||
letter-spacing: 0.48px;
|
||||
}
|
||||
}
|
||||
|
||||
.metrics-aggregation-section-content {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
|
||||
.metrics-aggregation-section-content-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
|
||||
.metrics-aggregation-section-content-item-label {
|
||||
color: var(--Vanilla-400, #c0c1c3);
|
||||
font-family: 'Geist Mono';
|
||||
font-size: 13px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 20px; /* 142.857% */
|
||||
letter-spacing: -0.07px;
|
||||
}
|
||||
|
||||
.metrics-aggregation-section-content-item-value {
|
||||
min-width: 320px;
|
||||
|
||||
.ant-select {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.ant-select-selector {
|
||||
border-radius: 2px;
|
||||
border: 1.005px solid var(--Slate-400, #1d212d);
|
||||
background: var(--Ink-300, #16181d);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.metrics-operators-select {
|
||||
border-radius: 2px;
|
||||
border: 1.005px solid var(--Slate-400, #1d212d);
|
||||
background: var(--Ink-300, #16181d);
|
||||
|
||||
color: var(--Vanilla-400, #c0c1c3);
|
||||
font-family: 'Geist Mono';
|
||||
font-size: 13px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 20px; /* 142.857% */
|
||||
letter-spacing: -0.07px;
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
import './MetricsAggregateSection.styles.scss';
|
||||
|
||||
import { Tooltip } from 'antd';
|
||||
import InputWithLabel from 'components/InputWithLabel/InputWithLabel';
|
||||
import { ATTRIBUTE_TYPES, PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import SpaceAggregationOptions from 'container/QueryBuilder/components/SpaceAggregationOptions/SpaceAggregationOptions';
|
||||
import { GroupByFilter, OperatorsSelect } from 'container/QueryBuilder/filters';
|
||||
import { useQueryOperations } from 'hooks/queryBuilder/useQueryBuilderOperations';
|
||||
import { Info } from 'lucide-react';
|
||||
import { memo, useCallback, useMemo } from 'react';
|
||||
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
const MetricsAggregateSection = memo(function MetricsAggregateSection({
|
||||
query,
|
||||
index,
|
||||
version,
|
||||
panelType,
|
||||
}: {
|
||||
query: IBuilderQuery;
|
||||
index: number;
|
||||
version: string;
|
||||
panelType: PANEL_TYPES | null;
|
||||
}): JSX.Element {
|
||||
const {
|
||||
operators,
|
||||
spaceAggregationOptions,
|
||||
handleChangeQueryData,
|
||||
handleChangeOperator,
|
||||
handleSpaceAggregationChange,
|
||||
} = useQueryOperations({
|
||||
index,
|
||||
query,
|
||||
entityVersion: version,
|
||||
});
|
||||
|
||||
const handleChangeGroupByKeys = useCallback(
|
||||
(value: IBuilderQuery['groupBy']) => {
|
||||
handleChangeQueryData('groupBy', value);
|
||||
},
|
||||
[handleChangeQueryData],
|
||||
);
|
||||
|
||||
const handleChangeAggregateEvery = useCallback(
|
||||
(value: string) => {
|
||||
handleChangeQueryData('stepInterval', Number(value));
|
||||
},
|
||||
[handleChangeQueryData],
|
||||
);
|
||||
|
||||
const showAggregationInterval = useMemo(() => {
|
||||
// eslint-disable-next-line sonarjs/prefer-single-boolean-return
|
||||
if (panelType === PANEL_TYPES.VALUE) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}, [panelType]);
|
||||
|
||||
const disableOperatorSelector =
|
||||
!query?.aggregateAttribute.key || query?.aggregateAttribute.key === '';
|
||||
|
||||
return (
|
||||
<div className="metrics-aggregate-section">
|
||||
<div className="metrics-time-aggregation-section">
|
||||
<div className="metrics-time-aggregation-section-title">
|
||||
AGGREGATE BY TIME{' '}
|
||||
<Tooltip title="AGGREGATE BY TIME">
|
||||
<Info size={12} />
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
<div className="metrics-aggregation-section-content">
|
||||
<div className="metrics-aggregation-section-content-item">
|
||||
<div className="metrics-aggregation-section-content-item-label">
|
||||
Align with
|
||||
</div>
|
||||
|
||||
<div className="metrics-aggregation-section-content-item-value">
|
||||
<OperatorsSelect
|
||||
value={query.aggregateOperator}
|
||||
onChange={handleChangeOperator}
|
||||
operators={operators}
|
||||
className="metrics-operators-select"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{showAggregationInterval && (
|
||||
<div className="metrics-aggregation-section-content-item">
|
||||
<div className="metrics-aggregation-section-content-item-label">
|
||||
aggregated every
|
||||
</div>
|
||||
|
||||
<div className="metrics-aggregation-section-content-item-value">
|
||||
<InputWithLabel
|
||||
onChange={handleChangeAggregateEvery}
|
||||
label="Seconds"
|
||||
placeholder="Enter a number"
|
||||
labelAfter
|
||||
initialValue={query?.stepInterval ?? undefined}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="metrics-space-aggregation-section">
|
||||
<div className="metrics-space-aggregation-section-title">
|
||||
AGGREGATE LABELS
|
||||
<Tooltip title="AGGREGATE LABELS">
|
||||
<Info size={12} />
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
<div className="metrics-aggregation-section-content">
|
||||
<div className="metrics-aggregation-section-content-item">
|
||||
<div className="metrics-aggregation-section-content-item-value space-aggregation-select">
|
||||
<SpaceAggregationOptions
|
||||
panelType={panelType}
|
||||
key={`${panelType}${query.spaceAggregation}${query.timeAggregation}`}
|
||||
aggregatorAttributeType={
|
||||
query?.aggregateAttribute.type as ATTRIBUTE_TYPES
|
||||
}
|
||||
selectedValue={query.spaceAggregation}
|
||||
disabled={disableOperatorSelector}
|
||||
onSelect={handleSpaceAggregationChange}
|
||||
operators={spaceAggregationOptions}
|
||||
qbVersion="v3"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="metrics-aggregation-section-content-item">
|
||||
<div className="metrics-aggregation-section-content-item-label">by</div>
|
||||
|
||||
<div className="metrics-aggregation-section-content-item-value">
|
||||
<GroupByFilter
|
||||
disabled={!query.aggregateAttribute.key}
|
||||
query={query}
|
||||
onChange={handleChangeGroupByKeys}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
export default MetricsAggregateSection;
|
||||
@@ -0,0 +1,42 @@
|
||||
.metrics-select-container {
|
||||
margin-bottom: 8px;
|
||||
|
||||
.ant-select-selector {
|
||||
width: 100%;
|
||||
border-radius: 2px;
|
||||
border: 1px solid #1d212d !important;
|
||||
background: #16181d;
|
||||
color: #fff;
|
||||
font-family: 'Geist Mono';
|
||||
font-size: 13px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 20px; /* 142.857% */
|
||||
min-height: 36px;
|
||||
}
|
||||
|
||||
.ant-select-dropdown {
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--bg-slate-400);
|
||||
background: linear-gradient(
|
||||
139deg,
|
||||
rgba(18, 19, 23, 0.8) 0%,
|
||||
rgba(18, 19, 23, 0.9) 98.68%
|
||||
);
|
||||
box-shadow: 4px 10px 16px 2px rgba(0, 0, 0, 0.2);
|
||||
backdrop-filter: blur(20px);
|
||||
|
||||
.ant-select-item {
|
||||
color: #fff;
|
||||
font-family: 'Geist Mono';
|
||||
font-size: 13px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 20px; /* 142.857% */
|
||||
|
||||
&:hover {
|
||||
background: rgba(171, 189, 255, 0.04) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import './MetricsSelect.styles.scss';
|
||||
|
||||
import { AggregatorFilter } from 'container/QueryBuilder/filters';
|
||||
import { useQueryOperations } from 'hooks/queryBuilder/useQueryBuilderOperations';
|
||||
import { memo } from 'react';
|
||||
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
export const MetricsSelect = memo(function MetricsSelect({
|
||||
query,
|
||||
index,
|
||||
version,
|
||||
}: {
|
||||
query: IBuilderQuery;
|
||||
index: number;
|
||||
version: string;
|
||||
}): JSX.Element {
|
||||
const { handleChangeAggregatorAttribute } = useQueryOperations({
|
||||
index,
|
||||
query,
|
||||
entityVersion: version,
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="metrics-select-container">
|
||||
<AggregatorFilter onChange={handleChangeAggregatorAttribute} query={query} />
|
||||
</div>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,389 @@
|
||||
/* eslint-disable import/no-extraneous-dependencies */
|
||||
/* eslint-disable sonarjs/cognitive-complexity */
|
||||
import {
|
||||
autocompletion,
|
||||
closeCompletion,
|
||||
Completion,
|
||||
CompletionContext,
|
||||
completionKeymap,
|
||||
CompletionResult,
|
||||
startCompletion,
|
||||
} from '@codemirror/autocomplete';
|
||||
import { javascript } from '@codemirror/lang-javascript';
|
||||
import { copilot } from '@uiw/codemirror-theme-copilot';
|
||||
import CodeMirror, { EditorView, keymap } from '@uiw/react-codemirror';
|
||||
import { Button } from 'antd';
|
||||
import { useQueryBuilderV2Context } from 'components/QueryBuilderV2/QueryBuilderV2Context';
|
||||
import { X } from 'lucide-react';
|
||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
const havingOperators = [
|
||||
{
|
||||
label: '=',
|
||||
value: '=',
|
||||
},
|
||||
{
|
||||
label: '!=',
|
||||
value: '!=',
|
||||
},
|
||||
{
|
||||
label: '>',
|
||||
value: '>',
|
||||
},
|
||||
{
|
||||
label: '<',
|
||||
value: '<',
|
||||
},
|
||||
{
|
||||
label: '>=',
|
||||
value: '>=',
|
||||
},
|
||||
{
|
||||
label: '<=',
|
||||
value: '<=',
|
||||
},
|
||||
{
|
||||
label: 'IN',
|
||||
value: 'IN',
|
||||
},
|
||||
{
|
||||
label: 'NOT_IN',
|
||||
value: 'NOT_IN',
|
||||
},
|
||||
];
|
||||
|
||||
// Add common value suggestions
|
||||
const commonValues = [
|
||||
{ label: '0', value: '0 ' },
|
||||
{ label: '1', value: '1 ' },
|
||||
{ label: '5', value: '5 ' },
|
||||
{ label: '10', value: '10 ' },
|
||||
{ label: '50', value: '50 ' },
|
||||
{ label: '100', value: '100 ' },
|
||||
{ label: '1000', value: '1000 ' },
|
||||
];
|
||||
|
||||
const conjunctions = [
|
||||
{ label: 'AND', value: 'AND ' },
|
||||
{ label: 'OR', value: 'OR ' },
|
||||
];
|
||||
|
||||
// Custom extension to stop events from propagating to global shortcuts
|
||||
const stopEventsExtension = EditorView.domEventHandlers({
|
||||
keydown: (event) => {
|
||||
// Stop all keyboard events from propagating to global shortcuts
|
||||
event.stopPropagation();
|
||||
event.stopImmediatePropagation();
|
||||
return false; // Important for CM to know you handled it
|
||||
},
|
||||
input: (event) => {
|
||||
event.stopPropagation();
|
||||
return false;
|
||||
},
|
||||
focus: (event) => {
|
||||
// Ensure focus events don't interfere with global shortcuts
|
||||
event.stopPropagation();
|
||||
return false;
|
||||
},
|
||||
blur: (event) => {
|
||||
// Ensure blur events don't interfere with global shortcuts
|
||||
event.stopPropagation();
|
||||
return false;
|
||||
},
|
||||
});
|
||||
|
||||
function HavingFilter({
|
||||
onClose,
|
||||
onChange,
|
||||
queryData,
|
||||
}: {
|
||||
onClose: () => void;
|
||||
onChange: (value: string) => void;
|
||||
queryData: IBuilderQuery;
|
||||
}): JSX.Element {
|
||||
const { aggregationOptions } = useQueryBuilderV2Context();
|
||||
const [input, setInput] = useState(
|
||||
queryData?.havingExpression?.expression || '',
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setInput(queryData?.havingExpression?.expression || '');
|
||||
}, [queryData?.havingExpression?.expression]);
|
||||
|
||||
const [isFocused, setIsFocused] = useState(false);
|
||||
|
||||
const editorRef = useRef<EditorView | null>(null);
|
||||
|
||||
const [options, setOptions] = useState<{ label: string; value: string }[]>([]);
|
||||
|
||||
const handleChange = (value: string): void => {
|
||||
setInput(value);
|
||||
onChange(value);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (isFocused && editorRef.current && options.length > 0) {
|
||||
startCompletion(editorRef.current);
|
||||
}
|
||||
}, [isFocused, options]);
|
||||
|
||||
// Update options when aggregation options change
|
||||
useEffect(() => {
|
||||
const newOptions = [];
|
||||
for (let i = 0; i < aggregationOptions.length; i++) {
|
||||
const opt = aggregationOptions[i];
|
||||
for (let j = 0; j < havingOperators.length; j++) {
|
||||
const operator = havingOperators[j];
|
||||
newOptions.push({
|
||||
label: `${opt.func}(${opt.arg}) ${operator.label}`,
|
||||
value: `${opt.func}(${opt.arg}) ${operator.label} `,
|
||||
apply: (
|
||||
view: EditorView,
|
||||
completion: { label: string; value: string },
|
||||
from: number,
|
||||
to: number,
|
||||
): void => {
|
||||
view.dispatch({
|
||||
changes: { from, to, insert: completion.value },
|
||||
selection: { anchor: from + completion.value.length },
|
||||
});
|
||||
// Trigger value suggestions immediately after operator
|
||||
setTimeout(() => {
|
||||
startCompletion(view);
|
||||
}, 0);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
setOptions(newOptions);
|
||||
}, [aggregationOptions]);
|
||||
|
||||
// Helper to check if a string is a number
|
||||
const isNumber = (token: string): boolean => /^-?\d+(\.\d+)?$/.test(token);
|
||||
|
||||
// Helper to check if we're after an operator
|
||||
const isAfterOperator = (tokens: string[]): boolean => {
|
||||
if (tokens.length === 0) return false;
|
||||
const lastToken = tokens[tokens.length - 1];
|
||||
// Check if the last token is exactly an operator or ends with an operator and space
|
||||
return havingOperators.some((op) => {
|
||||
const opWithSpace = `${op.value} `;
|
||||
return lastToken === op.value || lastToken.endsWith(opWithSpace);
|
||||
});
|
||||
};
|
||||
|
||||
// Helper function for applying completion with space
|
||||
const applyCompletionWithSpace = (
|
||||
view: EditorView,
|
||||
completion: Completion,
|
||||
from: number,
|
||||
to: number,
|
||||
): void => {
|
||||
const insertValue =
|
||||
typeof completion.apply === 'string' ? completion.apply : completion.label;
|
||||
const newText = `${insertValue} `;
|
||||
const newPos = from + newText.length;
|
||||
|
||||
view.dispatch({
|
||||
changes: { from, to, insert: newText },
|
||||
selection: { anchor: newPos, head: newPos },
|
||||
effects: EditorView.scrollIntoView(newPos),
|
||||
});
|
||||
};
|
||||
|
||||
const havingAutocomplete = useMemo(() => {
|
||||
// Helper functions for applying completions
|
||||
const forceCompletion = (view: EditorView): void => {
|
||||
setTimeout(() => {
|
||||
if (view) {
|
||||
startCompletion(view);
|
||||
}
|
||||
}, 0);
|
||||
};
|
||||
|
||||
const applyValueCompletion = (
|
||||
view: EditorView,
|
||||
completion: Completion,
|
||||
from: number,
|
||||
to: number,
|
||||
): void => {
|
||||
applyCompletionWithSpace(view, completion, from, to);
|
||||
forceCompletion(view);
|
||||
};
|
||||
|
||||
const applyOperatorCompletion = (
|
||||
view: EditorView,
|
||||
completion: Completion,
|
||||
from: number,
|
||||
to: number,
|
||||
): void => {
|
||||
const insertValue =
|
||||
typeof completion.apply === 'string' ? completion.apply : completion.label;
|
||||
const insertWithSpace = `${insertValue} `;
|
||||
view.dispatch({
|
||||
changes: { from, to, insert: insertWithSpace },
|
||||
selection: { anchor: from + insertWithSpace.length },
|
||||
});
|
||||
forceCompletion(view);
|
||||
};
|
||||
|
||||
return autocompletion({
|
||||
override: [
|
||||
(context: CompletionContext): CompletionResult | null => {
|
||||
const text = context.state.sliceDoc(0, context.pos);
|
||||
const trimmedText = text.trim();
|
||||
const tokens = trimmedText.split(/\s+/).filter(Boolean);
|
||||
|
||||
// Handle empty state when no aggregation options are available
|
||||
if (options.length === 0) {
|
||||
return {
|
||||
from: context.pos,
|
||||
options: [
|
||||
{
|
||||
label:
|
||||
'No aggregation functions available. Please add aggregation functions first.',
|
||||
type: 'text',
|
||||
apply: (): boolean => true,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
// Show value suggestions after operator
|
||||
if (isAfterOperator(tokens)) {
|
||||
return {
|
||||
from: context.pos,
|
||||
options: [
|
||||
...commonValues.map((value) => ({
|
||||
...value,
|
||||
apply: applyValueCompletion,
|
||||
})),
|
||||
{
|
||||
label: 'Enter a custom number value',
|
||||
type: 'text',
|
||||
apply: applyValueCompletion,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
// Suggest key/operator pairs and ( for grouping
|
||||
if (
|
||||
tokens.length === 0 ||
|
||||
conjunctions.some((c) => tokens[tokens.length - 1] === c.value.trim()) ||
|
||||
tokens[tokens.length - 1] === '('
|
||||
) {
|
||||
return {
|
||||
from: context.pos,
|
||||
options: options.map((opt) => ({
|
||||
...opt,
|
||||
apply: applyOperatorCompletion,
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
// Show suggestions when typing
|
||||
if (tokens.length > 0) {
|
||||
const lastToken = tokens[tokens.length - 1];
|
||||
const filteredOptions = options.filter((opt) =>
|
||||
opt.label.toLowerCase().includes(lastToken.toLowerCase()),
|
||||
);
|
||||
if (filteredOptions.length > 0) {
|
||||
return {
|
||||
from: context.pos - lastToken.length,
|
||||
options: filteredOptions.map((opt) => ({
|
||||
...opt,
|
||||
apply: applyOperatorCompletion,
|
||||
})),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Suggest conjunctions after a value and a space
|
||||
if (
|
||||
tokens.length > 0 &&
|
||||
(isNumber(tokens[tokens.length - 1]) ||
|
||||
tokens[tokens.length - 1] === ')') &&
|
||||
text.endsWith(' ')
|
||||
) {
|
||||
return {
|
||||
from: context.pos,
|
||||
options: conjunctions.map((conj) => ({
|
||||
...conj,
|
||||
apply: applyValueCompletion,
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
// Show all options if no other condition matches
|
||||
return {
|
||||
from: context.pos,
|
||||
options: options.map((opt) => ({
|
||||
...opt,
|
||||
apply: applyOperatorCompletion,
|
||||
})),
|
||||
};
|
||||
},
|
||||
],
|
||||
defaultKeymap: true,
|
||||
closeOnBlur: true,
|
||||
maxRenderedOptions: 200,
|
||||
activateOnTyping: true,
|
||||
});
|
||||
}, [options]);
|
||||
|
||||
return (
|
||||
<div className="having-filter-container">
|
||||
<div className="having-filter-select-container">
|
||||
<CodeMirror
|
||||
value={input}
|
||||
onChange={handleChange}
|
||||
theme={copilot}
|
||||
className="having-filter-select-editor"
|
||||
width="100%"
|
||||
extensions={[
|
||||
havingAutocomplete,
|
||||
javascript({ jsx: false, typescript: false }),
|
||||
stopEventsExtension,
|
||||
keymap.of([
|
||||
...completionKeymap,
|
||||
{
|
||||
key: 'Escape',
|
||||
run: closeCompletion,
|
||||
},
|
||||
]),
|
||||
]}
|
||||
placeholder="Type Having query like count() > 10 ..."
|
||||
basicSetup={{
|
||||
lineNumbers: false,
|
||||
autocompletion: true,
|
||||
completionKeymap: true,
|
||||
}}
|
||||
onCreateEditor={(view: EditorView): void => {
|
||||
editorRef.current = view;
|
||||
}}
|
||||
onFocus={(): void => {
|
||||
setIsFocused(true);
|
||||
if (editorRef.current) {
|
||||
startCompletion(editorRef.current);
|
||||
}
|
||||
}}
|
||||
onBlur={(): void => {
|
||||
setIsFocused(false);
|
||||
if (editorRef.current) {
|
||||
closeCompletion(editorRef.current);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
className="close-btn periscope-btn ghost"
|
||||
icon={<X size={16} />}
|
||||
onClick={onClose}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default HavingFilter;
|
||||
@@ -0,0 +1,377 @@
|
||||
.add-ons-list {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
.add-ons-tabs {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
||||
.add-on-tab-title {
|
||||
display: flex;
|
||||
gap: var(--margin-2);
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: var(--font-size-xs);
|
||||
font-style: normal;
|
||||
font-weight: var(--font-weight-normal);
|
||||
|
||||
color: var(--Vanilla-400, #c0c1c3);
|
||||
}
|
||||
|
||||
.tab {
|
||||
border: 1px solid var(--bg-slate-400);
|
||||
border-left: none;
|
||||
min-width: 120px;
|
||||
height: 36px;
|
||||
line-height: 36px;
|
||||
|
||||
&:first-child {
|
||||
border-left: 1px solid var(--bg-slate-400);
|
||||
}
|
||||
}
|
||||
|
||||
.tab::before {
|
||||
background: var(--bg-slate-400);
|
||||
}
|
||||
|
||||
.selected-view {
|
||||
color: var(--text-robin-500);
|
||||
border: 1px solid var(--bg-slate-400);
|
||||
|
||||
display: none;
|
||||
}
|
||||
|
||||
.selected-view::before {
|
||||
background: var(--bg-slate-400);
|
||||
}
|
||||
}
|
||||
|
||||
.compass-button {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
|
||||
border-radius: 2px;
|
||||
border: 1px solid var(--bg-slate-400);
|
||||
background: var(--bg-ink-300);
|
||||
box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
.having-filter-container {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
|
||||
.having-filter-select-container {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
|
||||
.having-filter-select-editor {
|
||||
border-radius: 2px;
|
||||
flex: 1;
|
||||
width: calc(100% - 40px);
|
||||
|
||||
.cm-content {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.cm-editor {
|
||||
border-radius: 2px;
|
||||
background-color: transparent !important;
|
||||
position: relative !important;
|
||||
|
||||
&:focus-within {
|
||||
border-color: var(--bg-robin-500);
|
||||
}
|
||||
|
||||
&.cm-focused {
|
||||
outline: none !important;
|
||||
}
|
||||
|
||||
.cm-content {
|
||||
border-radius: 2px;
|
||||
border: 1px solid var(--Slate-400, #1d212d);
|
||||
border-top-right-radius: 0px;
|
||||
border-bottom-right-radius: 0px;
|
||||
padding: 0px !important;
|
||||
background-color: #121317 !important;
|
||||
|
||||
&:focus-within {
|
||||
border-color: var(--bg-ink-200);
|
||||
}
|
||||
}
|
||||
|
||||
.cm-tooltip-autocomplete {
|
||||
background: var(--bg-ink-300) !important;
|
||||
color: var(--bg-ink-500) !important;
|
||||
border-radius: 2px !important;
|
||||
font-size: 12px !important;
|
||||
font-weight: 500 !important;
|
||||
margin-top: -2px !important;
|
||||
width: 100% !important;
|
||||
position: absolute !important;
|
||||
top: 38px !important;
|
||||
left: 0px !important;
|
||||
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--bg-slate-200, #1d212d);
|
||||
border-top: none !important;
|
||||
border-top-left-radius: 0px !important;
|
||||
border-top-right-radius: 0px !important;
|
||||
background: linear-gradient(
|
||||
139deg,
|
||||
rgba(18, 19, 23, 0.8) 0%,
|
||||
rgba(18, 19, 23, 0.9) 98.68%
|
||||
) !important;
|
||||
backdrop-filter: blur(20px);
|
||||
box-sizing: border-box;
|
||||
font-family: 'Space Mono', monospace !important;
|
||||
|
||||
ul {
|
||||
width: 100% !important;
|
||||
max-width: 100% !important;
|
||||
font-family: 'Space Mono', monospace !important;
|
||||
min-height: 200px !important;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 0.3rem;
|
||||
}
|
||||
&::-webkit-scrollbar-corner {
|
||||
background: transparent;
|
||||
}
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: rgb(136, 136, 136);
|
||||
border-radius: 0.625rem;
|
||||
}
|
||||
&::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
li {
|
||||
width: 100% !important;
|
||||
max-width: 100% !important;
|
||||
line-height: 36px !important;
|
||||
height: 36px !important;
|
||||
padding: 4px 8px !important;
|
||||
|
||||
display: flex !important;
|
||||
align-items: center !important;
|
||||
gap: 8px !important;
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
|
||||
font-family: 'Space Mono', monospace !important;
|
||||
color: var(--bg-vanilla-100) !important;
|
||||
|
||||
.cm-completionIcon {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
&[aria-selected='true'] {
|
||||
// background-color: rgba(78, 116, 248, 0.7) !important;
|
||||
background: rgba(171, 189, 255, 0.04) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.cm-gutters {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.cm-scroller {
|
||||
scrollbar-width: none;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-corner {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.cm-line {
|
||||
line-height: 36px !important;
|
||||
font-family: 'Space Mono', monospace !important;
|
||||
background-color: #121317 !important;
|
||||
|
||||
::-moz-selection {
|
||||
background: var(--bg-ink-100) !important;
|
||||
opacity: 0.5 !important;
|
||||
}
|
||||
|
||||
::selection {
|
||||
background: var(--bg-ink-100) !important;
|
||||
opacity: 0.5 !important;
|
||||
}
|
||||
|
||||
.cm-function {
|
||||
color: var(--bg-robin-500) !important;
|
||||
}
|
||||
|
||||
.chip-decorator {
|
||||
background: rgba(36, 40, 52, 1) !important;
|
||||
color: var(--bg-vanilla-100) !important;
|
||||
border-radius: 4px;
|
||||
padding: 2px 4px;
|
||||
margin-right: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.cm-selectionBackground {
|
||||
background: var(--bg-ink-100) !important;
|
||||
opacity: 0.5 !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.close-btn {
|
||||
border-radius: 0px 2px 2px 0px;
|
||||
border: 1px solid var(--bg-slate-400);
|
||||
background: var(--bg-ink-300);
|
||||
height: 38px;
|
||||
width: 38px;
|
||||
|
||||
border-left: transparent;
|
||||
border-top-left-radius: 0px;
|
||||
border-bottom-left-radius: 0px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.selected-add-ons-content {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(420px, 1fr));
|
||||
gap: 8px;
|
||||
padding-bottom: 8px;
|
||||
position: relative;
|
||||
|
||||
.add-on-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
|
||||
max-width: 100%;
|
||||
min-width: 100%;
|
||||
min-width: 420px;
|
||||
|
||||
box-sizing: border-box;
|
||||
position: relative;
|
||||
}
|
||||
}
|
||||
|
||||
.lightMode {
|
||||
.add-ons-list {
|
||||
.add-ons-tabs {
|
||||
.add-on-tab-title {
|
||||
color: var(--bg-ink-500) !important;
|
||||
}
|
||||
|
||||
.tab {
|
||||
border: 1px solid var(--bg-vanilla-300) !important;
|
||||
background: var(--bg-vanilla-100) !important;
|
||||
|
||||
&:first-child {
|
||||
border-left: 1px solid var(--bg-vanilla-300) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.tab::before {
|
||||
background: var(--bg-vanilla-300) !important;
|
||||
}
|
||||
|
||||
.selected-view {
|
||||
color: var(--bg-robin-500) !important;
|
||||
border: 1px solid var(--bg-vanilla-300) !important;
|
||||
}
|
||||
|
||||
.selected-view::before {
|
||||
background: var(--bg-vanilla-300) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.compass-button {
|
||||
border: 1px solid var(--bg-vanilla-300) !important;
|
||||
background: var(--bg-vanilla-100) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.having-filter-container {
|
||||
.having-filter-select-container {
|
||||
.having-filter-select-editor {
|
||||
.cm-editor {
|
||||
&:focus-within {
|
||||
border-color: var(--bg-vanilla-300) !important;
|
||||
}
|
||||
|
||||
.cm-content {
|
||||
border: 1px solid var(--bg-vanilla-300) !important;
|
||||
background: var(--bg-vanilla-100) !important;
|
||||
|
||||
&:focus-within {
|
||||
border-color: var(--bg-vanilla-300) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.cm-tooltip-autocomplete {
|
||||
background: var(--bg-vanilla-100) !important;
|
||||
border: 1px solid var(--bg-vanilla-300) !important;
|
||||
color: var(--bg-ink-500) !important;
|
||||
|
||||
ul {
|
||||
li {
|
||||
&:hover {
|
||||
background: var(--bg-vanilla-300) !important;
|
||||
}
|
||||
|
||||
&[aria-selected='true'] {
|
||||
color: var(--bg-ink-500) !important;
|
||||
background: var(--bg-vanilla-300) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.cm-line {
|
||||
background-color: var(--bg-vanilla-100) !important;
|
||||
|
||||
::-moz-selection {
|
||||
background: var(--bg-vanilla-100) !important;
|
||||
}
|
||||
|
||||
::selection {
|
||||
background: var(--bg-ink-100) !important;
|
||||
}
|
||||
|
||||
.chip-decorator {
|
||||
background: var(--bg-robin-100) !important;
|
||||
color: var(--bg-ink-400) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.cm-selectionBackground {
|
||||
background: var(--bg-vanilla-100) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.close-btn {
|
||||
border: 1px solid var(--bg-vanilla-300) !important;
|
||||
background: var(--bg-vanilla-100) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,340 @@
|
||||
import './QueryAddOns.styles.scss';
|
||||
|
||||
import { Button, Radio, RadioChangeEvent } from 'antd';
|
||||
import InputWithLabel from 'components/InputWithLabel/InputWithLabel';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { GroupByFilter } from 'container/QueryBuilder/filters/GroupByFilter/GroupByFilter';
|
||||
import { OrderByFilter } from 'container/QueryBuilder/filters/OrderByFilter/OrderByFilter';
|
||||
import { ReduceToFilter } from 'container/QueryBuilder/filters/ReduceToFilter/ReduceToFilter';
|
||||
import { useQueryOperations } from 'hooks/queryBuilder/useQueryBuilderOperations';
|
||||
import { isEmpty } from 'lodash-es';
|
||||
import { BarChart2, ScrollText, X } from 'lucide-react';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
import HavingFilter from './HavingFilter/HavingFilter';
|
||||
|
||||
interface AddOn {
|
||||
icon: React.ReactNode;
|
||||
label: string;
|
||||
key: string;
|
||||
}
|
||||
|
||||
const ADD_ONS_KEYS = {
|
||||
GROUP_BY: 'group_by',
|
||||
HAVING: 'having',
|
||||
ORDER_BY: 'order_by',
|
||||
LIMIT: 'limit',
|
||||
LEGEND_FORMAT: 'legend_format',
|
||||
};
|
||||
|
||||
const ADD_ONS = [
|
||||
{
|
||||
icon: <BarChart2 size={14} />,
|
||||
label: 'Group By',
|
||||
key: 'group_by',
|
||||
},
|
||||
{
|
||||
icon: <ScrollText size={14} />,
|
||||
label: 'Having',
|
||||
key: 'having',
|
||||
},
|
||||
{
|
||||
icon: <ScrollText size={14} />,
|
||||
label: 'Order By',
|
||||
key: 'order_by',
|
||||
},
|
||||
{
|
||||
icon: <ScrollText size={14} />,
|
||||
label: 'Limit',
|
||||
key: 'limit',
|
||||
},
|
||||
{
|
||||
icon: <ScrollText size={14} />,
|
||||
label: 'Legend format',
|
||||
key: 'legend_format',
|
||||
},
|
||||
];
|
||||
|
||||
const REDUCE_TO = {
|
||||
icon: <ScrollText size={14} />,
|
||||
label: 'Reduce to',
|
||||
key: 'reduce_to',
|
||||
};
|
||||
|
||||
function QueryAddOns({
|
||||
query,
|
||||
version,
|
||||
isListViewPanel,
|
||||
showReduceTo,
|
||||
panelType,
|
||||
index,
|
||||
}: {
|
||||
query: IBuilderQuery;
|
||||
version: string;
|
||||
isListViewPanel: boolean;
|
||||
showReduceTo: boolean;
|
||||
panelType: PANEL_TYPES | null;
|
||||
index: number;
|
||||
}): JSX.Element {
|
||||
const [addOns, setAddOns] = useState<AddOn[]>(ADD_ONS);
|
||||
|
||||
const [selectedViews, setSelectedViews] = useState<AddOn[]>([]);
|
||||
|
||||
const { handleChangeQueryData } = useQueryOperations({
|
||||
index,
|
||||
query,
|
||||
entityVersion: '',
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (isListViewPanel) {
|
||||
setAddOns([]);
|
||||
|
||||
setSelectedViews([
|
||||
ADD_ONS.find((addOn) => addOn.key === ADD_ONS_KEYS.ORDER_BY) as AddOn,
|
||||
]);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
let filteredAddOns: AddOn[];
|
||||
if (panelType === PANEL_TYPES.VALUE) {
|
||||
// Filter out all add-ons except legend format
|
||||
filteredAddOns = ADD_ONS.filter(
|
||||
(addOn) => addOn.key === ADD_ONS_KEYS.LEGEND_FORMAT,
|
||||
);
|
||||
} else {
|
||||
filteredAddOns = Object.values(ADD_ONS);
|
||||
|
||||
// Filter out group_by for metrics data source
|
||||
if (query.dataSource === DataSource.METRICS) {
|
||||
filteredAddOns = filteredAddOns.filter(
|
||||
(addOn) => addOn.key !== ADD_ONS_KEYS.GROUP_BY,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// add reduce to if showReduceTo is true
|
||||
if (showReduceTo) {
|
||||
filteredAddOns = [...filteredAddOns, REDUCE_TO];
|
||||
}
|
||||
|
||||
setAddOns(filteredAddOns);
|
||||
|
||||
// Filter selectedViews to only include add-ons present in filteredAddOns
|
||||
setSelectedViews((prevSelectedViews) =>
|
||||
prevSelectedViews.filter((view) =>
|
||||
filteredAddOns.some((addOn) => addOn.key === view.key),
|
||||
),
|
||||
);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [panelType, isListViewPanel, query.dataSource]);
|
||||
|
||||
const handleOptionClick = (e: RadioChangeEvent): void => {
|
||||
if (selectedViews.find((view) => view.key === e.target.value.key)) {
|
||||
setSelectedViews(
|
||||
selectedViews.filter((view) => view.key !== e.target.value.key),
|
||||
);
|
||||
} else {
|
||||
setSelectedViews([...selectedViews, e.target.value]);
|
||||
}
|
||||
};
|
||||
|
||||
const handleChangeGroupByKeys = useCallback(
|
||||
(value: IBuilderQuery['groupBy']) => {
|
||||
handleChangeQueryData('groupBy', value);
|
||||
},
|
||||
[handleChangeQueryData],
|
||||
);
|
||||
|
||||
const handleChangeOrderByKeys = useCallback(
|
||||
(value: IBuilderQuery['orderBy']) => {
|
||||
handleChangeQueryData('orderBy', value);
|
||||
},
|
||||
[handleChangeQueryData],
|
||||
);
|
||||
|
||||
const handleChangeReduceTo = useCallback(
|
||||
(value: IBuilderQuery['reduceTo']) => {
|
||||
handleChangeQueryData('reduceTo', value);
|
||||
},
|
||||
[handleChangeQueryData],
|
||||
);
|
||||
|
||||
const handleRemoveView = useCallback(
|
||||
(key: string): void => {
|
||||
setSelectedViews(selectedViews.filter((view) => view.key !== key));
|
||||
},
|
||||
[selectedViews],
|
||||
);
|
||||
|
||||
const handleChangeQueryLegend = useCallback(
|
||||
(value: string) => {
|
||||
handleChangeQueryData('legend', value);
|
||||
},
|
||||
[handleChangeQueryData],
|
||||
);
|
||||
|
||||
const handleChangeLimit = useCallback(
|
||||
(value: string) => {
|
||||
handleChangeQueryData('limit', Number(value) || null);
|
||||
},
|
||||
[handleChangeQueryData],
|
||||
);
|
||||
|
||||
const handleChangeHaving = useCallback(
|
||||
(value: string) => {
|
||||
handleChangeQueryData('havingExpression', {
|
||||
expression: value,
|
||||
});
|
||||
},
|
||||
[handleChangeQueryData],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="query-add-ons">
|
||||
{selectedViews.length > 0 && (
|
||||
<div className="selected-add-ons-content">
|
||||
{selectedViews.find((view) => view.key === 'group_by') && (
|
||||
<div className="add-on-content">
|
||||
<div className="periscope-input-with-label">
|
||||
<div className="label">Group By</div>
|
||||
<div className="input">
|
||||
<GroupByFilter
|
||||
disabled={
|
||||
query.dataSource === DataSource.METRICS &&
|
||||
!query.aggregateAttribute.key
|
||||
}
|
||||
query={query}
|
||||
onChange={handleChangeGroupByKeys}
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
className="close-btn periscope-btn ghost"
|
||||
icon={<X size={16} />}
|
||||
onClick={(): void => handleRemoveView('group_by')}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{selectedViews.find((view) => view.key === 'having') && (
|
||||
<div className="add-on-content">
|
||||
<div className="periscope-input-with-label">
|
||||
<div className="label">Having</div>
|
||||
<div className="input">
|
||||
<HavingFilter
|
||||
onClose={(): void => {
|
||||
setSelectedViews(
|
||||
selectedViews.filter((view) => view.key !== 'having'),
|
||||
);
|
||||
}}
|
||||
onChange={handleChangeHaving}
|
||||
queryData={query}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{selectedViews.find((view) => view.key === 'limit') && (
|
||||
<div className="add-on-content">
|
||||
<InputWithLabel
|
||||
label="Limit"
|
||||
onChange={handleChangeLimit}
|
||||
initialValue={query?.limit ?? undefined}
|
||||
placeholder="Enter limit"
|
||||
onClose={(): void => {
|
||||
setSelectedViews(selectedViews.filter((view) => view.key !== 'limit'));
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{selectedViews.find((view) => view.key === 'order_by') && (
|
||||
<div className="add-on-content">
|
||||
<div className="periscope-input-with-label">
|
||||
<div className="label">Order By</div>
|
||||
<div className="input">
|
||||
<OrderByFilter
|
||||
entityVersion={version}
|
||||
query={query}
|
||||
onChange={handleChangeOrderByKeys}
|
||||
isListViewPanel={isListViewPanel}
|
||||
isNewQueryV2
|
||||
/>
|
||||
</div>
|
||||
{!isListViewPanel && (
|
||||
<Button
|
||||
className="close-btn periscope-btn ghost"
|
||||
icon={<X size={16} />}
|
||||
onClick={(): void => handleRemoveView('order_by')}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{selectedViews.find((view) => view.key === 'reduce_to') && showReduceTo && (
|
||||
<div className="add-on-content">
|
||||
<div className="periscope-input-with-label">
|
||||
<div className="label">Reduce to</div>
|
||||
<div className="input">
|
||||
<ReduceToFilter query={query} onChange={handleChangeReduceTo} />
|
||||
</div>
|
||||
|
||||
<Button
|
||||
className="close-btn periscope-btn ghost"
|
||||
icon={<X size={16} />}
|
||||
onClick={(): void => handleRemoveView('reduce_to')}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{selectedViews.find((view) => view.key === 'legend_format') && (
|
||||
<div className="add-on-content">
|
||||
<InputWithLabel
|
||||
label="Legend format"
|
||||
placeholder="Write legend format"
|
||||
onChange={handleChangeQueryLegend}
|
||||
initialValue={isEmpty(query?.legend) ? undefined : query?.legend}
|
||||
onClose={(): void => {
|
||||
setSelectedViews(
|
||||
selectedViews.filter((view) => view.key !== 'legend_format'),
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="add-ons-list">
|
||||
<Radio.Group
|
||||
className="add-ons-tabs"
|
||||
onChange={handleOptionClick}
|
||||
value={selectedViews}
|
||||
>
|
||||
{addOns.map((addOn) => (
|
||||
<Radio.Button
|
||||
key={addOn.label}
|
||||
className={
|
||||
selectedViews.find((view) => view.key === addOn.key)
|
||||
? 'selected-view tab'
|
||||
: 'tab'
|
||||
}
|
||||
value={addOn}
|
||||
>
|
||||
<div className="add-on-tab-title">
|
||||
{addOn.icon}
|
||||
{addOn.label}
|
||||
</div>
|
||||
</Radio.Button>
|
||||
))}
|
||||
</Radio.Group>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default QueryAddOns;
|
||||
@@ -0,0 +1,300 @@
|
||||
.query-aggregation-container {
|
||||
display: block;
|
||||
position: relative;
|
||||
|
||||
.aggregation-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 8px;
|
||||
align-items: flex-start;
|
||||
flex-wrap: wrap;
|
||||
|
||||
.query-aggregation-select-container {
|
||||
flex: 1;
|
||||
min-width: 400px;
|
||||
}
|
||||
|
||||
.query-aggregation-options-input {
|
||||
width: 100%;
|
||||
height: 36px;
|
||||
line-height: 36px;
|
||||
border-radius: 2px;
|
||||
border: 1px solid var(--bg-slate-400);
|
||||
box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.1);
|
||||
|
||||
font-family: 'Space Mono', monospace !important;
|
||||
|
||||
&::placeholder {
|
||||
color: var(--bg-vanilla-100);
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
.query-aggregation-interval {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
max-width: 360px;
|
||||
|
||||
.query-aggregation-interval-input-container {
|
||||
.query-aggregation-interval-input {
|
||||
input {
|
||||
max-width: 120px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.query-aggregation-select-container {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
min-width: 400px;
|
||||
|
||||
.query-aggregation-select-editor {
|
||||
border-radius: 2px;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
|
||||
.cm-content {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.cm-editor {
|
||||
border-radius: 2px;
|
||||
background-color: transparent !important;
|
||||
position: relative !important;
|
||||
|
||||
&.cm-focused {
|
||||
outline: none !important;
|
||||
}
|
||||
|
||||
&:focus-within {
|
||||
border-color: var(--bg-robin-500);
|
||||
}
|
||||
|
||||
.cm-content {
|
||||
border-radius: 2px;
|
||||
border: 1px solid var(--Slate-400, #1d212d);
|
||||
border-top-right-radius: 0px;
|
||||
border-bottom-right-radius: 0px;
|
||||
padding: 0px !important;
|
||||
background-color: #121317 !important;
|
||||
|
||||
&:focus-within {
|
||||
border-color: var(--bg-ink-200);
|
||||
}
|
||||
}
|
||||
|
||||
.cm-tooltip-autocomplete {
|
||||
background: var(--bg-ink-300) !important;
|
||||
border-radius: 2px !important;
|
||||
font-size: 12px !important;
|
||||
font-weight: 500 !important;
|
||||
margin-top: 8px !important;
|
||||
min-width: 400px !important;
|
||||
position: absolute !important;
|
||||
left: 0px !important;
|
||||
width: 100% !important;
|
||||
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--bg-slate-200, #1d212d);
|
||||
border-top: none !important;
|
||||
border-top-left-radius: 0px !important;
|
||||
border-top-right-radius: 0px !important;
|
||||
background: linear-gradient(
|
||||
139deg,
|
||||
rgba(18, 19, 23, 0.8) 0%,
|
||||
rgba(18, 19, 23, 0.9) 98.68%
|
||||
) !important;
|
||||
backdrop-filter: blur(20px);
|
||||
box-sizing: border-box;
|
||||
font-family: 'Space Mono', monospace !important;
|
||||
|
||||
ul {
|
||||
width: 100% !important;
|
||||
max-width: 100% !important;
|
||||
font-family: 'Space Mono', monospace !important;
|
||||
min-height: 200px !important;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 0.3rem;
|
||||
}
|
||||
&::-webkit-scrollbar-corner {
|
||||
background: transparent;
|
||||
}
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: rgb(136, 136, 136);
|
||||
border-radius: 0.625rem;
|
||||
}
|
||||
&::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
li {
|
||||
width: 100% !important;
|
||||
max-width: 100% !important;
|
||||
line-height: 36px !important;
|
||||
height: 36px !important;
|
||||
padding: 4px 8px !important;
|
||||
|
||||
display: flex !important;
|
||||
align-items: center !important;
|
||||
gap: 8px !important;
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
|
||||
font-family: 'Space Mono', monospace !important;
|
||||
|
||||
.cm-completionIcon {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
&[aria-selected='true'] {
|
||||
// background-color: rgba(78, 116, 248, 0.7) !important;
|
||||
background: rgba(171, 189, 255, 0.04) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.cm-gutters {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.cm-line {
|
||||
line-height: 36px !important;
|
||||
font-family: 'Space Mono', monospace !important;
|
||||
background-color: #121317 !important;
|
||||
|
||||
::-moz-selection {
|
||||
background: var(--bg-ink-100) !important;
|
||||
opacity: 0.5 !important;
|
||||
}
|
||||
|
||||
::selection {
|
||||
background: var(--bg-ink-100) !important;
|
||||
opacity: 0.5 !important;
|
||||
}
|
||||
|
||||
.cm-function {
|
||||
color: var(--bg-robin-500) !important;
|
||||
}
|
||||
|
||||
.chip-decorator {
|
||||
background: rgba(36, 40, 52, 1) !important;
|
||||
color: var(--bg-vanilla-100) !important;
|
||||
border-radius: 4px;
|
||||
padding: 2px 4px;
|
||||
margin-right: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.cm-selectionBackground {
|
||||
background: var(--bg-ink-100) !important;
|
||||
opacity: 0.5 !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.close-btn {
|
||||
border-radius: 0px 2px 2px 0px;
|
||||
border: 1px solid var(--bg-slate-400);
|
||||
background: var(--bg-ink-300);
|
||||
height: 38px;
|
||||
width: 38px;
|
||||
|
||||
border-left: transparent;
|
||||
border-top-left-radius: 0px;
|
||||
border-bottom-left-radius: 0px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.lightMode {
|
||||
.query-aggregation-container {
|
||||
.aggregation-container {
|
||||
.query-aggregation-options-input {
|
||||
border-color: var(--bg-vanilla-300) !important;
|
||||
|
||||
&::placeholder {
|
||||
color: var(--bg-ink-400) !important;
|
||||
opacity: 0.5 !important;
|
||||
}
|
||||
}
|
||||
|
||||
.query-aggregation-select-container {
|
||||
.query-aggregation-select-editor {
|
||||
.cm-editor {
|
||||
.cm-content {
|
||||
border: 1px solid var(--bg-vanilla-300) !important;
|
||||
background: var(--bg-vanilla-100) !important;
|
||||
box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.1) !important;
|
||||
|
||||
&:focus-within {
|
||||
border-color: var(--bg-vanilla-300) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.cm-tooltip-autocomplete {
|
||||
background: var(--bg-vanilla-100) !important;
|
||||
border: 1px solid var(--bg-vanilla-300) !important;
|
||||
color: var(--bg-ink-500) !important;
|
||||
|
||||
ul {
|
||||
li {
|
||||
&:hover {
|
||||
background-color: var(--bg-vanilla-300) !important;
|
||||
color: var(--bg-ink-500) !important;
|
||||
}
|
||||
|
||||
&[aria-selected='true'] {
|
||||
background: var(--bg-vanilla-300) !important;
|
||||
color: var(--bg-ink-500) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.cm-line {
|
||||
background-color: var(--bg-vanilla-100) !important;
|
||||
|
||||
::-moz-selection {
|
||||
background: var(--bg-vanilla-100) !important;
|
||||
opacity: 0.5 !important;
|
||||
}
|
||||
|
||||
::selection {
|
||||
background: var(--bg-vanilla-100) !important;
|
||||
opacity: 0.5 !important;
|
||||
}
|
||||
|
||||
.cm-function {
|
||||
color: var(--bg-robin-500) !important;
|
||||
}
|
||||
|
||||
.chip-decorator {
|
||||
background: var(--bg-robin-500) !important;
|
||||
color: var(--bg-ink-400) !important;
|
||||
}
|
||||
}
|
||||
|
||||
// .cm-selectionBackground {
|
||||
// background: var(--bg-vanilla-100) !important;
|
||||
// opacity: 0.5 !important;
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
.close-btn {
|
||||
border-color: var(--bg-vanilla-300) !important;
|
||||
background: var(--bg-vanilla-100) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
import './QueryAggregation.styles.scss';
|
||||
|
||||
import InputWithLabel from 'components/InputWithLabel/InputWithLabel';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { useMemo } from 'react';
|
||||
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
import QueryAggregationSelect from './QueryAggregationSelect';
|
||||
|
||||
function QueryAggregationOptions({
|
||||
dataSource,
|
||||
panelType,
|
||||
onAggregationIntervalChange,
|
||||
onChange,
|
||||
queryData,
|
||||
}: {
|
||||
dataSource: DataSource;
|
||||
panelType?: string;
|
||||
onAggregationIntervalChange: (value: number) => void;
|
||||
onChange?: (value: string) => void;
|
||||
queryData: IBuilderQuery;
|
||||
}): JSX.Element {
|
||||
const showAggregationInterval = useMemo(() => {
|
||||
// eslint-disable-next-line sonarjs/prefer-single-boolean-return
|
||||
if (panelType === PANEL_TYPES.VALUE) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (dataSource === DataSource.TRACES || dataSource === DataSource.LOGS) {
|
||||
return !(panelType === PANEL_TYPES.TABLE || panelType === PANEL_TYPES.PIE);
|
||||
}
|
||||
|
||||
return true;
|
||||
}, [dataSource, panelType]);
|
||||
|
||||
const handleAggregationIntervalChange = (value: string): void => {
|
||||
onAggregationIntervalChange(Number(value));
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="query-aggregation-container">
|
||||
<div className="aggregation-container">
|
||||
<QueryAggregationSelect onChange={onChange} queryData={queryData} />
|
||||
|
||||
{showAggregationInterval && (
|
||||
<div className="query-aggregation-interval">
|
||||
<div className="query-aggregation-interval-label">every</div>
|
||||
<div className="query-aggregation-interval-input-container">
|
||||
<InputWithLabel
|
||||
initialValue={
|
||||
queryData.stepInterval ? queryData.stepInterval : undefined
|
||||
}
|
||||
className="query-aggregation-interval-input"
|
||||
label="Seconds"
|
||||
placeholder="Auto"
|
||||
type="number"
|
||||
onChange={handleAggregationIntervalChange}
|
||||
labelAfter
|
||||
onClose={(): void => {}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
QueryAggregationOptions.defaultProps = {
|
||||
panelType: null,
|
||||
onChange: undefined,
|
||||
};
|
||||
|
||||
export default QueryAggregationOptions;
|
||||
@@ -0,0 +1,530 @@
|
||||
/* eslint-disable import/no-extraneous-dependencies */
|
||||
/* eslint-disable no-cond-assign */
|
||||
/* eslint-disable no-restricted-syntax */
|
||||
/* eslint-disable class-methods-use-this */
|
||||
/* eslint-disable react/no-this-in-sfc */
|
||||
/* eslint-disable sonarjs/cognitive-complexity */
|
||||
import './QueryAggregation.styles.scss';
|
||||
|
||||
import {
|
||||
autocompletion,
|
||||
closeCompletion,
|
||||
Completion,
|
||||
CompletionContext,
|
||||
completionKeymap,
|
||||
CompletionResult,
|
||||
startCompletion,
|
||||
} from '@codemirror/autocomplete';
|
||||
import { javascript } from '@codemirror/lang-javascript';
|
||||
import { RangeSetBuilder } from '@codemirror/state';
|
||||
import { copilot } from '@uiw/codemirror-theme-copilot';
|
||||
import CodeMirror, {
|
||||
Decoration,
|
||||
EditorView,
|
||||
keymap,
|
||||
ViewPlugin,
|
||||
ViewUpdate,
|
||||
} from '@uiw/react-codemirror';
|
||||
import { getAggregateAttribute } from 'api/queryBuilder/getAggregateAttribute';
|
||||
import { QueryBuilderKeys } from 'constants/queryBuilder';
|
||||
import { tracesAggregateOperatorOptions } from 'constants/queryBuilderOperators';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useQuery } from 'react-query';
|
||||
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { TracesAggregatorOperator } from 'types/common/queryBuilder';
|
||||
|
||||
import { useQueryBuilderV2Context } from '../../QueryBuilderV2Context';
|
||||
|
||||
const chipDecoration = Decoration.mark({
|
||||
class: 'chip-decorator',
|
||||
});
|
||||
|
||||
const operatorArgMeta: Record<
|
||||
string,
|
||||
{ acceptsArgs: boolean; multiple: boolean }
|
||||
> = {
|
||||
[TracesAggregatorOperator.NOOP]: { acceptsArgs: false, multiple: false },
|
||||
[TracesAggregatorOperator.COUNT]: { acceptsArgs: false, multiple: false },
|
||||
[TracesAggregatorOperator.COUNT_DISTINCT]: {
|
||||
acceptsArgs: true,
|
||||
multiple: true,
|
||||
},
|
||||
[TracesAggregatorOperator.SUM]: { acceptsArgs: true, multiple: false },
|
||||
[TracesAggregatorOperator.AVG]: { acceptsArgs: true, multiple: false },
|
||||
[TracesAggregatorOperator.MAX]: { acceptsArgs: true, multiple: false },
|
||||
[TracesAggregatorOperator.MIN]: { acceptsArgs: true, multiple: false },
|
||||
[TracesAggregatorOperator.P05]: { acceptsArgs: true, multiple: false },
|
||||
[TracesAggregatorOperator.P10]: { acceptsArgs: true, multiple: false },
|
||||
[TracesAggregatorOperator.P20]: { acceptsArgs: true, multiple: false },
|
||||
[TracesAggregatorOperator.P25]: { acceptsArgs: true, multiple: false },
|
||||
[TracesAggregatorOperator.P50]: { acceptsArgs: true, multiple: false },
|
||||
[TracesAggregatorOperator.P75]: { acceptsArgs: true, multiple: false },
|
||||
[TracesAggregatorOperator.P90]: { acceptsArgs: true, multiple: false },
|
||||
[TracesAggregatorOperator.P95]: { acceptsArgs: true, multiple: false },
|
||||
[TracesAggregatorOperator.P99]: { acceptsArgs: true, multiple: false },
|
||||
[TracesAggregatorOperator.RATE]: { acceptsArgs: true, multiple: false },
|
||||
[TracesAggregatorOperator.RATE_SUM]: { acceptsArgs: true, multiple: false },
|
||||
[TracesAggregatorOperator.RATE_AVG]: { acceptsArgs: true, multiple: false },
|
||||
[TracesAggregatorOperator.RATE_MIN]: { acceptsArgs: true, multiple: false },
|
||||
[TracesAggregatorOperator.RATE_MAX]: { acceptsArgs: true, multiple: false },
|
||||
};
|
||||
|
||||
function getFunctionContextAtCursor(
|
||||
text: string,
|
||||
cursorPos: number,
|
||||
): string | null {
|
||||
// Find the nearest function name to the left of the nearest unmatched '('
|
||||
let openParenIndex = -1;
|
||||
let funcName: string | null = null;
|
||||
let parenStack = 0;
|
||||
for (let i = cursorPos - 1; i >= 0; i--) {
|
||||
if (text[i] === ')') parenStack++;
|
||||
else if (text[i] === '(') {
|
||||
if (parenStack === 0) {
|
||||
openParenIndex = i;
|
||||
const before = text.slice(0, i);
|
||||
const match = before.match(/(\w+)\s*$/);
|
||||
if (match) funcName = match[1].toLowerCase();
|
||||
break;
|
||||
}
|
||||
parenStack--;
|
||||
}
|
||||
}
|
||||
if (openParenIndex === -1 || !funcName) return null;
|
||||
// Scan forwards to find the matching closing parenthesis
|
||||
let closeParenIndex = -1;
|
||||
let depth = 1;
|
||||
for (let j = openParenIndex + 1; j < text.length; j++) {
|
||||
if (text[j] === '(') depth++;
|
||||
else if (text[j] === ')') depth--;
|
||||
if (depth === 0) {
|
||||
closeParenIndex = j;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (
|
||||
cursorPos > openParenIndex &&
|
||||
(closeParenIndex === -1 || cursorPos <= closeParenIndex)
|
||||
) {
|
||||
return funcName;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// Custom extension to stop events from propagating to global shortcuts
|
||||
const stopEventsExtension = EditorView.domEventHandlers({
|
||||
keydown: (event) => {
|
||||
// Stop all keyboard events from propagating to global shortcuts
|
||||
event.stopPropagation();
|
||||
event.stopImmediatePropagation();
|
||||
return false; // Important for CM to know you handled it
|
||||
},
|
||||
input: (event) => {
|
||||
event.stopPropagation();
|
||||
return false;
|
||||
},
|
||||
focus: (event) => {
|
||||
// Ensure focus events don't interfere with global shortcuts
|
||||
event.stopPropagation();
|
||||
return false;
|
||||
},
|
||||
blur: (event) => {
|
||||
// Ensure blur events don't interfere with global shortcuts
|
||||
event.stopPropagation();
|
||||
return false;
|
||||
},
|
||||
});
|
||||
|
||||
// eslint-disable-next-line react/no-this-in-sfc
|
||||
function QueryAggregationSelect({
|
||||
onChange,
|
||||
queryData,
|
||||
}: {
|
||||
onChange?: (value: string) => void;
|
||||
queryData: IBuilderQuery;
|
||||
}): JSX.Element {
|
||||
const { setAggregationOptions } = useQueryBuilderV2Context();
|
||||
|
||||
const [input, setInput] = useState(
|
||||
queryData?.aggregations?.map((i: any) => i.expression).join(' ') || '',
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setInput(
|
||||
queryData?.aggregations?.map((i: any) => i.expression).join(' ') || '',
|
||||
);
|
||||
}, [queryData?.aggregations]);
|
||||
|
||||
const [cursorPos, setCursorPos] = useState(0);
|
||||
const [functionArgPairs, setFunctionArgPairs] = useState<
|
||||
{ func: string; arg: string }[]
|
||||
>([]);
|
||||
const editorRef = useRef<EditorView | null>(null);
|
||||
const [isFocused, setIsFocused] = useState(false);
|
||||
|
||||
// Helper function to safely start completion
|
||||
const safeStartCompletion = useCallback((): void => {
|
||||
requestAnimationFrame(() => {
|
||||
if (editorRef.current) {
|
||||
startCompletion(editorRef.current);
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
|
||||
// Update cursor position on every editor update
|
||||
const handleUpdate = (update: { view: EditorView }): void => {
|
||||
const pos = update.view.state.selection.main.from;
|
||||
setCursorPos(pos);
|
||||
};
|
||||
|
||||
// Effect to handle focus state and trigger suggestions
|
||||
useEffect(() => {
|
||||
if (isFocused) {
|
||||
safeStartCompletion();
|
||||
}
|
||||
}, [isFocused, safeStartCompletion]);
|
||||
|
||||
// Extract all valid function-argument pairs from the input
|
||||
useEffect(() => {
|
||||
const pairs: { func: string; arg: string }[] = [];
|
||||
const regex = /([a-zA-Z_][\w]*)\s*\(([^)]*)\)/g;
|
||||
let match;
|
||||
while ((match = regex.exec(input)) !== null) {
|
||||
const func = match[1].toLowerCase();
|
||||
const args = match[2]
|
||||
.split(',')
|
||||
.map((arg) => arg.trim())
|
||||
.filter((arg) => arg.length > 0);
|
||||
|
||||
if (args.length === 0) {
|
||||
// For functions with no arguments, add a pair with empty string as arg
|
||||
pairs.push({ func, arg: '' });
|
||||
} else {
|
||||
args.forEach((arg) => {
|
||||
pairs.push({ func, arg });
|
||||
});
|
||||
}
|
||||
}
|
||||
setFunctionArgPairs(pairs);
|
||||
setAggregationOptions(pairs);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [input]);
|
||||
|
||||
// Find function context for fetching suggestions
|
||||
const functionContextForFetch = getFunctionContextAtCursor(input, cursorPos);
|
||||
|
||||
const { data: aggregateAttributeData, isLoading: isLoadingFields } = useQuery(
|
||||
[
|
||||
QueryBuilderKeys.GET_AGGREGATE_ATTRIBUTE,
|
||||
functionContextForFetch,
|
||||
queryData.dataSource,
|
||||
],
|
||||
() =>
|
||||
getAggregateAttribute({
|
||||
searchText: '',
|
||||
aggregateOperator: functionContextForFetch as string,
|
||||
dataSource: queryData.dataSource,
|
||||
}),
|
||||
{
|
||||
enabled:
|
||||
!!functionContextForFetch &&
|
||||
!!operatorArgMeta[functionContextForFetch]?.acceptsArgs,
|
||||
},
|
||||
);
|
||||
|
||||
// Get valid function names (lowercase)
|
||||
const validFunctions = useMemo(
|
||||
() => tracesAggregateOperatorOptions.map((op) => op.value.toLowerCase()),
|
||||
[],
|
||||
);
|
||||
|
||||
// Memoized chipPlugin that highlights valid function calls like count(), max(arg), min(arg)
|
||||
const chipPlugin = useMemo(
|
||||
() =>
|
||||
ViewPlugin.fromClass(
|
||||
class {
|
||||
decorations: import('@codemirror/view').DecorationSet;
|
||||
|
||||
constructor(view: EditorView) {
|
||||
this.decorations = this.buildDecorations(view);
|
||||
}
|
||||
|
||||
update(update: ViewUpdate): void {
|
||||
if (update.docChanged || update.viewportChanged) {
|
||||
this.decorations = this.buildDecorations(update.view);
|
||||
}
|
||||
}
|
||||
|
||||
buildDecorations(
|
||||
view: EditorView,
|
||||
): import('@codemirror/view').DecorationSet {
|
||||
const builder = new RangeSetBuilder<Decoration>();
|
||||
for (const { from, to } of view.visibleRanges) {
|
||||
const text = view.state.doc.sliceString(from, to);
|
||||
|
||||
const regex = /\b([a-zA-Z_][\w]*)\s*\(([^)]*)\)/g;
|
||||
let match;
|
||||
|
||||
while ((match = regex.exec(text)) !== null) {
|
||||
const func = match[1].toLowerCase();
|
||||
|
||||
if (validFunctions.includes(func)) {
|
||||
const start = from + match.index;
|
||||
const end = start + match[0].length;
|
||||
builder.add(start, end, chipDecoration);
|
||||
}
|
||||
}
|
||||
}
|
||||
return builder.finish();
|
||||
}
|
||||
},
|
||||
{
|
||||
decorations: (v: any): import('@codemirror/view').DecorationSet =>
|
||||
v.decorations,
|
||||
},
|
||||
),
|
||||
[validFunctions],
|
||||
) as any;
|
||||
|
||||
const operatorCompletions: Completion[] = tracesAggregateOperatorOptions.map(
|
||||
(op) => ({
|
||||
label: op.value,
|
||||
type: 'function',
|
||||
info: op.label,
|
||||
apply: (
|
||||
view: EditorView,
|
||||
completion: Completion,
|
||||
from: number,
|
||||
to: number,
|
||||
): void => {
|
||||
const acceptsArgs = operatorArgMeta[op.value]?.acceptsArgs;
|
||||
|
||||
let insertText: string;
|
||||
let cursorPos: number;
|
||||
|
||||
if (!acceptsArgs) {
|
||||
insertText = `${op.value}() `;
|
||||
cursorPos = from + insertText.length; // Use insertText.length instead of hardcoded values
|
||||
} else {
|
||||
insertText = `${op.value}(`;
|
||||
cursorPos = from + insertText.length; // Use insertText.length instead of hardcoded values
|
||||
}
|
||||
|
||||
view.dispatch({
|
||||
changes: { from, to, insert: insertText },
|
||||
selection: { anchor: cursorPos },
|
||||
});
|
||||
|
||||
// Trigger suggestions after a small delay
|
||||
setTimeout(() => {
|
||||
safeStartCompletion();
|
||||
}, 50);
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
// Memoize field suggestions from API (no filtering here)
|
||||
const fieldSuggestions = useMemo(
|
||||
() =>
|
||||
aggregateAttributeData?.payload?.attributeKeys?.map(
|
||||
(attributeKey: BaseAutocompleteData) => ({
|
||||
label: attributeKey.key,
|
||||
type: 'variable',
|
||||
info: attributeKey.dataType,
|
||||
apply: (
|
||||
view: EditorView,
|
||||
completion: Completion,
|
||||
from: number,
|
||||
to: number,
|
||||
): void => {
|
||||
const text = view.state.sliceDoc(0, from);
|
||||
const funcName = getFunctionContextAtCursor(text, from);
|
||||
const multiple = funcName ? operatorArgMeta[funcName]?.multiple : false;
|
||||
|
||||
// Insert the selected key followed by either a comma or closing parenthesis
|
||||
const insertText = multiple
|
||||
? `${completion.label},`
|
||||
: `${completion.label}) `;
|
||||
const cursorPos = from + insertText.length; // Use insertText.length instead of hardcoded values
|
||||
|
||||
view.dispatch({
|
||||
changes: { from, to, insert: insertText },
|
||||
selection: { anchor: cursorPos },
|
||||
});
|
||||
|
||||
// Trigger next suggestions after a small delay
|
||||
setTimeout(() => {
|
||||
safeStartCompletion();
|
||||
}, 50);
|
||||
},
|
||||
}),
|
||||
) || [],
|
||||
[aggregateAttributeData, safeStartCompletion],
|
||||
);
|
||||
|
||||
const aggregatorAutocomplete = useMemo(
|
||||
() =>
|
||||
autocompletion({
|
||||
override: [
|
||||
(context: CompletionContext): CompletionResult | null => {
|
||||
const text = context.state.sliceDoc(0, context.state.doc.length);
|
||||
const cursorPos = context.pos;
|
||||
const funcName = getFunctionContextAtCursor(text, cursorPos);
|
||||
|
||||
// Do not show suggestions if inside count()
|
||||
if (
|
||||
funcName === TracesAggregatorOperator.COUNT &&
|
||||
cursorPos > 0 &&
|
||||
text[cursorPos - 1] !== ')'
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// If inside a function that accepts args, show field suggestions
|
||||
if (funcName && operatorArgMeta[funcName]?.acceptsArgs) {
|
||||
if (isLoadingFields) {
|
||||
return {
|
||||
from: cursorPos,
|
||||
options: [
|
||||
{
|
||||
label: 'Loading suggestions...',
|
||||
type: 'text',
|
||||
apply: (): void => {},
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
const doc = context.state.sliceDoc(0, cursorPos);
|
||||
const lastOpenParen = doc.lastIndexOf('(');
|
||||
const lastComma = doc.lastIndexOf(',', cursorPos - 1);
|
||||
const startOfArg =
|
||||
lastComma > lastOpenParen ? lastComma + 1 : lastOpenParen + 1;
|
||||
const inputText = doc.slice(startOfArg, cursorPos).trim();
|
||||
|
||||
// Parse arguments already present in the function call (before the cursor)
|
||||
const usedArgs = new Set<string>();
|
||||
if (lastOpenParen !== -1) {
|
||||
const argsString = doc.slice(lastOpenParen + 1, cursorPos);
|
||||
argsString.split(',').forEach((arg) => {
|
||||
const trimmed = arg.trim();
|
||||
if (trimmed) usedArgs.add(trimmed);
|
||||
});
|
||||
}
|
||||
|
||||
// Exclude arguments already paired with this function elsewhere in the input
|
||||
const globalUsedArgs = new Set(
|
||||
functionArgPairs
|
||||
.filter((pair) => pair.func === funcName)
|
||||
.map((pair) => pair.arg),
|
||||
);
|
||||
|
||||
const availableSuggestions = fieldSuggestions.filter(
|
||||
(suggestion) =>
|
||||
!usedArgs.has(suggestion.label) &&
|
||||
!globalUsedArgs.has(suggestion.label),
|
||||
);
|
||||
|
||||
const filteredSuggestions =
|
||||
inputText === ''
|
||||
? availableSuggestions
|
||||
: availableSuggestions.filter((suggestion) =>
|
||||
suggestion.label.toLowerCase().includes(inputText.toLowerCase()),
|
||||
);
|
||||
|
||||
return {
|
||||
from: startOfArg,
|
||||
options: filteredSuggestions,
|
||||
};
|
||||
}
|
||||
|
||||
// Show operator suggestions if no function context or not accepting args
|
||||
if (!funcName || !operatorArgMeta[funcName]?.acceptsArgs) {
|
||||
// Check if 'count(' is present in the current input (case-insensitive)
|
||||
const hasCount = text.toLowerCase().includes('count(');
|
||||
const availableOperators = hasCount
|
||||
? operatorCompletions.filter((op) => op.label.toLowerCase() !== 'count')
|
||||
: operatorCompletions;
|
||||
|
||||
// Get the word before cursor if any
|
||||
const word = context.matchBefore(/[\w\d_]+/);
|
||||
|
||||
// Show suggestions if:
|
||||
// 1. There's a word match
|
||||
// 2. The input is empty (cursor at start)
|
||||
// 3. The user explicitly triggered completion
|
||||
if (word || cursorPos === 0 || context.explicit) {
|
||||
return {
|
||||
from: word ? word.from : cursorPos,
|
||||
options: availableOperators,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
],
|
||||
defaultKeymap: true,
|
||||
closeOnBlur: true,
|
||||
maxRenderedOptions: 50,
|
||||
activateOnTyping: true,
|
||||
}),
|
||||
[operatorCompletions, isLoadingFields, fieldSuggestions, functionArgPairs],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="query-aggregation-select-container">
|
||||
<CodeMirror
|
||||
value={input}
|
||||
onChange={(value): void => {
|
||||
setInput(value);
|
||||
onChange?.(value);
|
||||
}}
|
||||
className="query-aggregation-select-editor"
|
||||
theme={copilot}
|
||||
extensions={[
|
||||
chipPlugin,
|
||||
aggregatorAutocomplete,
|
||||
javascript({ jsx: false, typescript: false }),
|
||||
EditorView.lineWrapping,
|
||||
stopEventsExtension,
|
||||
keymap.of([
|
||||
...completionKeymap,
|
||||
{
|
||||
key: 'Escape',
|
||||
run: closeCompletion,
|
||||
},
|
||||
]),
|
||||
]}
|
||||
placeholder="Type aggregator functions like sum(), count_distinct(...), etc."
|
||||
basicSetup={{
|
||||
lineNumbers: false,
|
||||
autocompletion: true,
|
||||
completionKeymap: true,
|
||||
}}
|
||||
onUpdate={handleUpdate}
|
||||
onCreateEditor={(view: EditorView): void => {
|
||||
editorRef.current = view;
|
||||
}}
|
||||
onFocus={(): void => {
|
||||
setIsFocused(true);
|
||||
safeStartCompletion();
|
||||
}}
|
||||
onBlur={(): void => {
|
||||
setIsFocused(false);
|
||||
|
||||
if (editorRef.current) {
|
||||
closeCompletion(editorRef.current);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
QueryAggregationSelect.defaultProps = {
|
||||
onChange: undefined,
|
||||
};
|
||||
|
||||
export default QueryAggregationSelect;
|
||||
@@ -0,0 +1,35 @@
|
||||
import { Button } from 'antd';
|
||||
import { Plus, Sigma } from 'lucide-react';
|
||||
|
||||
export default function QueryFooter({
|
||||
addNewBuilderQuery,
|
||||
addNewFormula,
|
||||
}: {
|
||||
addNewBuilderQuery: () => void;
|
||||
addNewFormula: () => void;
|
||||
}): JSX.Element {
|
||||
return (
|
||||
<div className="qb-footer">
|
||||
<div className="qb-footer-container">
|
||||
<div className="qb-add-new-query">
|
||||
<Button
|
||||
className="add-new-query-button periscope-btn secondary"
|
||||
type="text"
|
||||
icon={<Plus size={16} />}
|
||||
onClick={addNewBuilderQuery}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="qb-add-formula">
|
||||
<Button
|
||||
className="add-formula-button periscope-btn secondary"
|
||||
icon={<Sigma size={16} />}
|
||||
onClick={addNewFormula}
|
||||
>
|
||||
Add Formula
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,713 @@
|
||||
.code-mirror-where-clause {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
font-family: Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto,
|
||||
'Helvetica Neue', sans-serif;
|
||||
|
||||
.query-where-clause-editor-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
.query-where-clause-editor {
|
||||
flex: 1;
|
||||
min-width: 400px;
|
||||
}
|
||||
|
||||
.query-status-container {
|
||||
width: 32px;
|
||||
|
||||
background-color: #121317 !important;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border: 1px solid var(--bg-slate-200);
|
||||
border-radius: 2px;
|
||||
border-top-left-radius: 0px !important;
|
||||
border-bottom-left-radius: 0px !important;
|
||||
border-left: none !important;
|
||||
|
||||
&.hasErrors {
|
||||
border-color: var(--bg-cherry-500);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.query-where-clause-editor {
|
||||
&.hasErrors {
|
||||
.cm-editor {
|
||||
.cm-content {
|
||||
border-color: var(--bg-cherry-500);
|
||||
border-top-right-radius: 0px !important;
|
||||
border-bottom-right-radius: 0px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.cm-editor {
|
||||
border-radius: 2px;
|
||||
overflow: hidden;
|
||||
background-color: transparent !important;
|
||||
|
||||
&:focus-within {
|
||||
border-color: var(--bg-robin-500);
|
||||
}
|
||||
|
||||
.cm-content {
|
||||
border-radius: 2px;
|
||||
border: 1px solid var(--Slate-400, #1d212d);
|
||||
padding: 0px !important;
|
||||
background-color: #121317 !important;
|
||||
|
||||
&:focus-within {
|
||||
border-color: var(--bg-ink-200);
|
||||
}
|
||||
}
|
||||
|
||||
&.cm-focused {
|
||||
outline: 1px solid var(--bg-slate-200);
|
||||
}
|
||||
|
||||
.cm-tooltip-autocomplete {
|
||||
background: var(--bg-ink-300) !important;
|
||||
border-radius: 2px !important;
|
||||
font-size: 12px !important;
|
||||
font-weight: 500 !important;
|
||||
margin-top: -2px !important;
|
||||
min-width: 400px !important;
|
||||
position: relative !important;
|
||||
top: 0px !important;
|
||||
left: 0px !important;
|
||||
|
||||
border-radius: 4px;
|
||||
border: 0px;
|
||||
background: linear-gradient(
|
||||
139deg,
|
||||
rgba(18, 19, 23, 0.8) 0%,
|
||||
rgba(18, 19, 23, 0.9) 98.68%
|
||||
) !important;
|
||||
backdrop-filter: blur(20px);
|
||||
box-sizing: border-box;
|
||||
font-family: 'Space Mono', monospace !important;
|
||||
|
||||
ul {
|
||||
width: 100% !important;
|
||||
max-width: 100% !important;
|
||||
font-family: 'Space Mono', monospace !important;
|
||||
min-height: 200px !important;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 0.3rem;
|
||||
}
|
||||
&::-webkit-scrollbar-corner {
|
||||
background: transparent;
|
||||
}
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: rgb(136, 136, 136);
|
||||
border-radius: 0.625rem;
|
||||
}
|
||||
&::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
li {
|
||||
width: 100% !important;
|
||||
max-width: 100% !important;
|
||||
line-height: 36px !important;
|
||||
height: 36px !important;
|
||||
padding: 4px 8px !important;
|
||||
|
||||
display: flex !important;
|
||||
align-items: center !important;
|
||||
gap: 8px !important;
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
|
||||
font-family: 'Space Mono', monospace !important;
|
||||
|
||||
&:hover {
|
||||
background: var(--bg-ink-100) !important;
|
||||
}
|
||||
|
||||
.cm-completionIcon {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
&[aria-selected='true'] {
|
||||
// background-color: rgba(78, 116, 248, 0.7) !important;
|
||||
background: rgba(171, 189, 255, 0.04) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.cm-gutters {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.cm-line {
|
||||
line-height: 34px !important;
|
||||
font-family: 'Space Mono', monospace !important;
|
||||
background-color: #121317 !important;
|
||||
|
||||
::-moz-selection {
|
||||
background: var(--bg-ink-100) !important;
|
||||
opacity: 0.5 !important;
|
||||
}
|
||||
|
||||
::selection {
|
||||
background: var(--bg-ink-100) !important;
|
||||
opacity: 0.5 !important;
|
||||
}
|
||||
}
|
||||
|
||||
.cm-selectionBackground {
|
||||
background: var(--bg-ink-100) !important;
|
||||
opacity: 0.5 !important;
|
||||
}
|
||||
}
|
||||
|
||||
.cursor-position {
|
||||
font-size: 12px;
|
||||
color: var(--bg-ink-200);
|
||||
padding: 6px;
|
||||
background-color: var(--bg-vanilla-200);
|
||||
border-radius: 4px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.query-validation {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
margin-bottom: 8px;
|
||||
margin-top: 16px;
|
||||
|
||||
.valid,
|
||||
.invalid {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
font-weight: 500;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.valid {
|
||||
background-color: rgba(39, 174, 96, 0.1);
|
||||
color: #27ae60;
|
||||
}
|
||||
|
||||
.invalid {
|
||||
background-color: rgba(235, 87, 87, 0.1);
|
||||
color: #eb5757;
|
||||
}
|
||||
|
||||
.query-validation-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.query-validation-errors {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
|
||||
.query-validation-error {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 16px;
|
||||
|
||||
font-size: 12px;
|
||||
font-family: 'Space Mono', monospace !important;
|
||||
color: var(--bg-cherry-500);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.query-context {
|
||||
padding: 12px;
|
||||
background-color: var(--bg-ink-400);
|
||||
border-radius: 4px;
|
||||
border-left: 3px solid var(--bg-robin-500);
|
||||
color: var(--bg-ink-300) !important;
|
||||
|
||||
.ant-card-head {
|
||||
color: var(--bg-vanilla-300) !important;
|
||||
}
|
||||
|
||||
.context-details {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
font-size: 13px;
|
||||
|
||||
strong {
|
||||
color: var(--bg-vanilla-300);
|
||||
margin-right: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.code-mirror-card {
|
||||
.ant-card-body {
|
||||
padding: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.query-text-preview-title {
|
||||
font-size: 13px;
|
||||
color: var(--bg-vanilla-100);
|
||||
background-color: var(--bg-robin-500);
|
||||
padding: 2px 6px;
|
||||
border-radius: 2px;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.query-text-preview {
|
||||
font-family: 'Space Mono', monospace;
|
||||
font-size: 13px;
|
||||
color: var(--bg-vanilla-200);
|
||||
padding: 2px 6px;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.query-examples-card {
|
||||
background-color: var(--bg-ink-400);
|
||||
border: 1px solid var(--bg-slate-200);
|
||||
|
||||
.ant-card-body {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.query-examples {
|
||||
.ant-collapse-header {
|
||||
padding: 8px 16px !important;
|
||||
color: var(--bg-vanilla-300) !important;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.ant-collapse-content {
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
.query-examples-list {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.query-example-tag {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
padding: 8px 12px;
|
||||
background-color: var(--bg-ink-400);
|
||||
border: 1px solid var(--bg-slate-200);
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
outline: none;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--bg-ink-300);
|
||||
border-color: var(--bg-robin-500);
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline: 2px solid var(--bg-robin-500);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.query-example-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.query-example-label {
|
||||
font-weight: 500;
|
||||
color: var(--bg-vanilla-300);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.query-example-query {
|
||||
font-family: 'Space Mono', monospace;
|
||||
font-size: 12px;
|
||||
color: var(--bg-vanilla-200);
|
||||
background-color: var(--bg-ink-300);
|
||||
padding: 2px 6px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.query-example-description {
|
||||
font-size: 12px;
|
||||
color: var(--bg-vanilla-200);
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
|
||||
.query-example-content {
|
||||
display: inline-flex;
|
||||
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Context indicator styles
|
||||
.context-indicator {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
padding: 8px 12px;
|
||||
margin-bottom: 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 13px;
|
||||
background-color: #f5f5f5;
|
||||
border-left: 4px solid #1890ff;
|
||||
|
||||
display: none;
|
||||
|
||||
.triplet-info {
|
||||
margin-left: 16px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.query-pair-info {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
border-left: 1px solid rgba(0, 0, 0, 0.1);
|
||||
padding-left: 8px;
|
||||
background-color: rgba(0, 0, 0, 0.03);
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
// Color variations based on context
|
||||
&.context-indicator-key {
|
||||
border-left-color: #1890ff; // blue
|
||||
background-color: rgba(24, 144, 255, 0.1);
|
||||
}
|
||||
|
||||
&.context-indicator-operator {
|
||||
border-left-color: #722ed1; // purple
|
||||
background-color: rgba(114, 46, 209, 0.1);
|
||||
}
|
||||
|
||||
&.context-indicator-value {
|
||||
border-left-color: #52c41a; // green
|
||||
background-color: rgba(82, 196, 26, 0.1);
|
||||
}
|
||||
|
||||
&.context-indicator-conjunction {
|
||||
border-left-color: #fa8c16; // orange
|
||||
background-color: rgba(250, 140, 22, 0.1);
|
||||
}
|
||||
|
||||
&.context-indicator-function {
|
||||
border-left-color: #13c2c2; // cyan
|
||||
background-color: rgba(19, 194, 194, 0.1);
|
||||
}
|
||||
|
||||
&.context-indicator-parenthesis {
|
||||
border-left-color: #eb2f96; // magenta
|
||||
background-color: rgba(235, 47, 150, 0.1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.query-status-popover {
|
||||
.ant-popover-arrow {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.ant-popover-content {
|
||||
background: linear-gradient(
|
||||
139deg,
|
||||
rgba(18, 19, 23, 0.8) 0%,
|
||||
rgba(18, 19, 23, 0.9) 98.68%
|
||||
);
|
||||
box-shadow: 4px 10px 16px 2px rgba(0, 0, 0, 0.2);
|
||||
backdrop-filter: blur(20px);
|
||||
|
||||
margin-top: -6px !important;
|
||||
}
|
||||
}
|
||||
|
||||
// /* Dark mode support */
|
||||
// :global(.darkMode) {
|
||||
// .code-mirror-where-clause {
|
||||
// .cm-editor {
|
||||
// border-color: var(--bg-slate-500);
|
||||
// background-color: var(--bg-ink-400);
|
||||
// }
|
||||
|
||||
// .cursor-position {
|
||||
// background-color: var(--bg-ink-400);
|
||||
// color: var(--bg-vanilla-100);
|
||||
// }
|
||||
|
||||
// .query-context {
|
||||
// background-color: var(--bg-ink-400);
|
||||
// color: var(--bg-vanilla-100);
|
||||
|
||||
// h3 {
|
||||
// color: var(--bg-vanilla-100);
|
||||
// }
|
||||
|
||||
// .context-details {
|
||||
// p {
|
||||
// strong {
|
||||
// color: var(--bg-vanilla-200);
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// .query-examples-card {
|
||||
// background-color: var(--bg-ink-400);
|
||||
// border-color: var(--bg-slate-500);
|
||||
|
||||
// .ant-collapse-header {
|
||||
// color: var(--bg-vanilla-100) !important;
|
||||
// }
|
||||
|
||||
// .query-example-tag {
|
||||
// background-color: var(--bg-ink-400);
|
||||
// border-color: var(--bg-slate-500);
|
||||
|
||||
// &:hover {
|
||||
// background-color: var(--bg-ink-300);
|
||||
// border-color: var(--bg-robin-500);
|
||||
// }
|
||||
|
||||
// .query-example-label {
|
||||
// color: var(--bg-vanilla-100);
|
||||
// }
|
||||
|
||||
// .query-example-query {
|
||||
// color: var(--bg-vanilla-100);
|
||||
// background-color: var(--bg-ink-300);
|
||||
// }
|
||||
|
||||
// .query-example-description {
|
||||
// color: var(--bg-vanilla-100);
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// .context-indicator {
|
||||
// background-color: var(--bg-ink-300);
|
||||
// color: var(--bg-vanilla-100);
|
||||
|
||||
// .query-pair-info {
|
||||
// border-left: 1px solid rgba(255, 255, 255, 0.1);
|
||||
// background-color: rgba(255, 255, 255, 0.05);
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
.lightMode {
|
||||
.code-mirror-where-clause {
|
||||
.query-where-clause-editor-container {
|
||||
.query-status-container {
|
||||
background-color: var(--bg-vanilla-100) !important;
|
||||
border: 1px solid var(--bg-vanilla-300);
|
||||
|
||||
&.hasErrors {
|
||||
border-color: var(--bg-cherry-500);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.query-where-clause-editor {
|
||||
&.hasErrors {
|
||||
.cm-editor {
|
||||
.cm-content {
|
||||
border-color: var(--bg-cherry-500);
|
||||
border-top-right-radius: 0px !important;
|
||||
border-bottom-right-radius: 0px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.cm-editor {
|
||||
&:focus-within {
|
||||
border-color: var(--bg-robin-500);
|
||||
}
|
||||
|
||||
&.cm-focused {
|
||||
outline: 1px solid var(--bg-vanilla-300);
|
||||
}
|
||||
|
||||
.cm-content {
|
||||
border-radius: 2px;
|
||||
border: 1px solid var(--bg-vanilla-300);
|
||||
padding: 0px !important;
|
||||
background-color: var(--bg-vanilla-100) !important;
|
||||
|
||||
&:focus-within {
|
||||
border-color: var(--bg-vanilla-200);
|
||||
}
|
||||
}
|
||||
|
||||
.cm-tooltip-autocomplete {
|
||||
background: var(--bg-vanilla-100) !important;
|
||||
|
||||
border: 0px;
|
||||
backdrop-filter: blur(20px);
|
||||
|
||||
ul {
|
||||
li {
|
||||
background-color: var(--bg-vanilla-100) !important;
|
||||
color: var(--bg-ink-300) !important;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--bg-vanilla-200) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.cm-line {
|
||||
background-color: var(--bg-vanilla-100) !important;
|
||||
|
||||
::-moz-selection {
|
||||
background: var(--bg-vanilla-100) !important;
|
||||
}
|
||||
|
||||
::selection {
|
||||
background: var(--bg-vanilla-100) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.cm-selectionBackground {
|
||||
background: var(--bg-vanilla-100) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.cursor-position {
|
||||
color: var(--bg-vanilla-200);
|
||||
background-color: var(--bg-vanilla-100);
|
||||
}
|
||||
|
||||
.query-context {
|
||||
background-color: var(--bg-vanilla-100);
|
||||
border-left: 3px solid var(--bg-vanilla-300);
|
||||
color: var(--bg-vanilla-300) !important;
|
||||
|
||||
.ant-card-head {
|
||||
color: var(--bg-ink-300) !important;
|
||||
}
|
||||
|
||||
.context-details {
|
||||
p {
|
||||
strong {
|
||||
color: var(--bg-ink-300);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.query-examples-card {
|
||||
background-color: var(--bg-vanilla-100);
|
||||
border: 1px solid var(--bg-vanilla-300);
|
||||
|
||||
.query-examples {
|
||||
.ant-collapse-header {
|
||||
color: var(--bg-ink-300) !important;
|
||||
}
|
||||
|
||||
.query-example-tag {
|
||||
background-color: var(--bg-vanilla-100);
|
||||
border: 1px solid var(--bg-vanilla-300);
|
||||
|
||||
&:hover {
|
||||
background-color: var(--bg-vanilla-200);
|
||||
border-color: var(--bg-vanilla-300);
|
||||
}
|
||||
|
||||
.query-example-label {
|
||||
color: var(--bg-ink-300);
|
||||
}
|
||||
|
||||
.query-example-query {
|
||||
color: var(--bg-ink-300);
|
||||
background-color: var(--bg-vanilla-100);
|
||||
}
|
||||
|
||||
.query-example-description {
|
||||
color: var(--bg-ink-300);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.context-indicator {
|
||||
background-color: var(--bg-vanilla-100);
|
||||
border-left: 4px solid var(--bg-vanilla-300);
|
||||
|
||||
display: none;
|
||||
|
||||
.query-pair-info {
|
||||
border-left: 1px solid rgba(255, 255, 255, 0.1);
|
||||
background-color: rgba(255, 255, 255, 0.03);
|
||||
}
|
||||
|
||||
// Color variations based on context
|
||||
&.context-indicator-key {
|
||||
border-left-color: #1890ff; // blue
|
||||
background-color: rgba(24, 144, 255, 0.1);
|
||||
}
|
||||
|
||||
&.context-indicator-operator {
|
||||
border-left-color: #722ed1; // purple
|
||||
background-color: rgba(114, 46, 209, 0.1);
|
||||
}
|
||||
|
||||
&.context-indicator-value {
|
||||
border-left-color: #52c41a; // green
|
||||
background-color: rgba(82, 196, 26, 0.1);
|
||||
}
|
||||
|
||||
&.context-indicator-conjunction {
|
||||
border-left-color: #fa8c16; // orange
|
||||
background-color: rgba(250, 140, 22, 0.1);
|
||||
}
|
||||
|
||||
&.context-indicator-function {
|
||||
border-left-color: #13c2c2; // cyan
|
||||
background-color: rgba(19, 194, 194, 0.1);
|
||||
}
|
||||
|
||||
&.context-indicator-parenthesis {
|
||||
border-left-color: #eb2f96; // magenta
|
||||
background-color: rgba(235, 47, 150, 0.1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.query-status-popover {
|
||||
.ant-popover-content {
|
||||
background: var(--bg-vanilla-100);
|
||||
box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,79 @@
|
||||
export const queryExamples = [
|
||||
{
|
||||
label: 'Basic Query',
|
||||
query: "status = 'error'",
|
||||
description: 'Find all errors',
|
||||
},
|
||||
{
|
||||
label: 'Multiple Conditions',
|
||||
query: "status = 'error' AND service = 'frontend'",
|
||||
description: 'Find errors from frontend service',
|
||||
},
|
||||
{
|
||||
label: 'IN Operator',
|
||||
query: "status IN ['error', 'warning']",
|
||||
description: 'Find items with specific statuses',
|
||||
},
|
||||
{
|
||||
label: 'Function Usage',
|
||||
query: "HAS(service, 'frontend')",
|
||||
description: 'Use HAS function',
|
||||
},
|
||||
{
|
||||
label: 'Numeric Comparison',
|
||||
query: 'duration > 1000',
|
||||
description: 'Find items with duration greater than 1000ms',
|
||||
},
|
||||
{
|
||||
label: 'Range Query',
|
||||
query: 'duration BETWEEN 100 AND 1000',
|
||||
description: 'Find items with duration between 100ms and 1000ms',
|
||||
},
|
||||
{
|
||||
label: 'Pattern Matching',
|
||||
query: "service LIKE 'front%'",
|
||||
description: 'Find services starting with "front"',
|
||||
},
|
||||
{
|
||||
label: 'Complex Conditions',
|
||||
query: "(status = 'error' OR status = 'warning') AND service = 'frontend'",
|
||||
description: 'Find errors or warnings from frontend service',
|
||||
},
|
||||
{
|
||||
label: 'Multiple Functions',
|
||||
query: "HAS(service, 'frontend') AND HAS(status, 'error')",
|
||||
description: 'Use multiple HAS functions',
|
||||
},
|
||||
{
|
||||
label: 'NOT Operator',
|
||||
query: "NOT status = 'success'",
|
||||
description: 'Find items that are not successful',
|
||||
},
|
||||
{
|
||||
label: 'Array Contains',
|
||||
query: "tags CONTAINS 'production'",
|
||||
description: 'Find items with production tag',
|
||||
},
|
||||
{
|
||||
label: 'Regex Pattern',
|
||||
query: "service REGEXP '^prod-.*'",
|
||||
description: 'Find services matching regex pattern',
|
||||
},
|
||||
{
|
||||
label: 'Null Check',
|
||||
query: 'error IS NULL',
|
||||
description: 'Find items without errors',
|
||||
},
|
||||
{
|
||||
label: 'Multiple Attributes',
|
||||
query:
|
||||
"service = 'frontend' AND environment = 'production' AND status = 'error'",
|
||||
description: 'Find production frontend errors',
|
||||
},
|
||||
{
|
||||
label: 'Nested Conditions',
|
||||
query:
|
||||
"(service = 'frontend' OR service = 'backend') AND (status = 'error' OR status = 'warning')",
|
||||
description: 'Find errors or warnings from frontend or backend',
|
||||
},
|
||||
];
|
||||
234
frontend/src/components/QueryBuilderV2/QueryV2/QueryV2.tsx
Normal file
234
frontend/src/components/QueryBuilderV2/QueryV2/QueryV2.tsx
Normal file
@@ -0,0 +1,234 @@
|
||||
import { Dropdown } from 'antd';
|
||||
import cx from 'classnames';
|
||||
import { ENTITY_VERSION_V4 } from 'constants/app';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import QBEntityOptions from 'container/QueryBuilder/components/QBEntityOptions/QBEntityOptions';
|
||||
import { QueryProps } from 'container/QueryBuilder/components/Query/Query.interfaces';
|
||||
import SpanScopeSelector from 'container/QueryBuilder/filters/QueryBuilderSearchV2/SpanScopeSelector';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { useQueryOperations } from 'hooks/queryBuilder/useQueryBuilderOperations';
|
||||
import { Copy, Ellipsis, Trash } from 'lucide-react';
|
||||
import { memo, useCallback, useMemo, useState } from 'react';
|
||||
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { HandleChangeQueryDataV5 } from 'types/common/operations.types';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
import MetricsAggregateSection from './MerticsAggregateSection/MetricsAggregateSection';
|
||||
import { MetricsSelect } from './MetricsSelect/MetricsSelect';
|
||||
import QueryAddOns from './QueryAddOns/QueryAddOns';
|
||||
import QueryAggregation from './QueryAggregation/QueryAggregation';
|
||||
import QuerySearch from './QuerySearch/QuerySearch';
|
||||
|
||||
export const QueryV2 = memo(function QueryV2({
|
||||
ref,
|
||||
index,
|
||||
queryVariant,
|
||||
query,
|
||||
filterConfigs,
|
||||
isListViewPanel = false,
|
||||
version,
|
||||
showOnlyWhereClause = false,
|
||||
}: QueryProps & { ref: React.RefObject<HTMLDivElement> }): JSX.Element {
|
||||
const { cloneQuery, panelType } = useQueryBuilder();
|
||||
|
||||
const showFunctions = query?.functions?.length > 0;
|
||||
const { dataSource } = query;
|
||||
|
||||
const [isCollapsed, setIsCollapsed] = useState(false);
|
||||
|
||||
const {
|
||||
handleChangeQueryData,
|
||||
handleDeleteQuery,
|
||||
handleQueryFunctionsUpdates,
|
||||
handleChangeDataSource,
|
||||
} = useQueryOperations({
|
||||
index,
|
||||
query,
|
||||
filterConfigs,
|
||||
isListViewPanel,
|
||||
entityVersion: version,
|
||||
});
|
||||
|
||||
const handleToggleDisableQuery = useCallback(() => {
|
||||
handleChangeQueryData('disabled', !query.disabled);
|
||||
}, [handleChangeQueryData, query]);
|
||||
|
||||
const handleToggleCollapsQuery = (): void => {
|
||||
setIsCollapsed(!isCollapsed);
|
||||
};
|
||||
|
||||
const handleCloneEntity = (): void => {
|
||||
cloneQuery('query', query);
|
||||
};
|
||||
|
||||
const showReduceTo = useMemo(
|
||||
() =>
|
||||
dataSource === DataSource.METRICS &&
|
||||
(panelType === PANEL_TYPES.TABLE ||
|
||||
panelType === PANEL_TYPES.PIE ||
|
||||
panelType === PANEL_TYPES.VALUE),
|
||||
[dataSource, panelType],
|
||||
);
|
||||
|
||||
const showSpanScopeSelector = useMemo(() => dataSource === DataSource.TRACES, [
|
||||
dataSource,
|
||||
]);
|
||||
|
||||
const handleChangeAggregateEvery = useCallback(
|
||||
(value: IBuilderQuery['stepInterval']) => {
|
||||
handleChangeQueryData('stepInterval', value);
|
||||
},
|
||||
[handleChangeQueryData],
|
||||
);
|
||||
|
||||
const handleSearchChange = useCallback(
|
||||
(value: string) => {
|
||||
(handleChangeQueryData as HandleChangeQueryDataV5)('filter', {
|
||||
expression: value,
|
||||
});
|
||||
},
|
||||
[handleChangeQueryData],
|
||||
);
|
||||
|
||||
const handleChangeAggregation = useCallback(
|
||||
(value: string) => {
|
||||
(handleChangeQueryData as HandleChangeQueryDataV5)('aggregations', [
|
||||
{
|
||||
expression: value,
|
||||
},
|
||||
]);
|
||||
},
|
||||
[handleChangeQueryData],
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cx('query-v2', { 'where-clause-view': showOnlyWhereClause })}
|
||||
ref={ref}
|
||||
>
|
||||
<div className="qb-content-section">
|
||||
{!showOnlyWhereClause && (
|
||||
<div className="qb-header-container">
|
||||
<div className="query-actions-container">
|
||||
<div className="query-actions-left-container">
|
||||
<QBEntityOptions
|
||||
isMetricsDataSource={dataSource === DataSource.METRICS}
|
||||
showFunctions={
|
||||
(version && version === ENTITY_VERSION_V4) ||
|
||||
query.dataSource === DataSource.LOGS ||
|
||||
showFunctions ||
|
||||
false
|
||||
}
|
||||
isCollapsed={isCollapsed}
|
||||
entityType="query"
|
||||
entityData={query}
|
||||
onToggleVisibility={handleToggleDisableQuery}
|
||||
onDelete={handleDeleteQuery}
|
||||
onCloneQuery={cloneQuery}
|
||||
onCollapseEntity={handleToggleCollapsQuery}
|
||||
query={query}
|
||||
onQueryFunctionsUpdates={handleQueryFunctionsUpdates}
|
||||
showDeleteButton={false}
|
||||
showCloneOption={false}
|
||||
isListViewPanel={isListViewPanel}
|
||||
index={index}
|
||||
queryVariant={queryVariant}
|
||||
onChangeDataSource={handleChangeDataSource}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{!isListViewPanel && (
|
||||
<Dropdown
|
||||
className="query-actions-dropdown"
|
||||
menu={{
|
||||
items: [
|
||||
{
|
||||
label: 'Clone',
|
||||
key: 'clone-query',
|
||||
icon: <Copy size={14} />,
|
||||
onClick: handleCloneEntity,
|
||||
},
|
||||
{
|
||||
label: 'Delete',
|
||||
key: 'delete-query',
|
||||
icon: <Trash size={14} />,
|
||||
onClick: handleDeleteQuery,
|
||||
},
|
||||
],
|
||||
}}
|
||||
placement="bottomRight"
|
||||
>
|
||||
<Ellipsis size={16} />
|
||||
</Dropdown>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!isCollapsed && (
|
||||
<div className="qb-elements-container">
|
||||
<div className="qb-search-container">
|
||||
{dataSource === DataSource.METRICS && (
|
||||
<div className="metrics-select-container">
|
||||
<MetricsSelect query={query} index={index} version="v4" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="qb-search-filter-container">
|
||||
<div className="query-search-container">
|
||||
<QuerySearch
|
||||
key={`query-search-${query.queryName}-${query.dataSource}`}
|
||||
onChange={handleSearchChange}
|
||||
queryData={query}
|
||||
dataSource={dataSource}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{showSpanScopeSelector && (
|
||||
<div className="traces-search-filter-container">
|
||||
<div className="traces-search-filter-in">in</div>
|
||||
<SpanScopeSelector query={query} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{!showOnlyWhereClause &&
|
||||
!isListViewPanel &&
|
||||
dataSource !== DataSource.METRICS && (
|
||||
<QueryAggregation
|
||||
dataSource={dataSource}
|
||||
key={`query-search-${query.queryName}-${query.dataSource}`}
|
||||
panelType={panelType || undefined}
|
||||
onAggregationIntervalChange={handleChangeAggregateEvery}
|
||||
onChange={handleChangeAggregation}
|
||||
queryData={query}
|
||||
/>
|
||||
)}
|
||||
|
||||
{!showOnlyWhereClause && dataSource === DataSource.METRICS && (
|
||||
<MetricsAggregateSection
|
||||
panelType={panelType}
|
||||
query={query}
|
||||
index={index}
|
||||
key={`metrics-aggregate-section-${query.queryName}-${query.dataSource}`}
|
||||
version="v4"
|
||||
/>
|
||||
)}
|
||||
|
||||
{!showOnlyWhereClause && (
|
||||
<QueryAddOns
|
||||
index={index}
|
||||
query={query}
|
||||
version="v3"
|
||||
isListViewPanel={isListViewPanel}
|
||||
showReduceTo={showReduceTo}
|
||||
panelType={panelType}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
185
frontend/src/components/QueryBuilderV2/utils.ts
Normal file
185
frontend/src/components/QueryBuilderV2/utils.ts
Normal file
@@ -0,0 +1,185 @@
|
||||
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import { Having, TagFilter } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import {
|
||||
LogAggregation,
|
||||
MetricAggregation,
|
||||
TraceAggregation,
|
||||
} from 'types/api/v5/queryRange';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
/**
|
||||
* Check if an operator requires array values (like IN, NOT IN)
|
||||
* @param operator - The operator to check
|
||||
* @returns True if the operator requires array values
|
||||
*/
|
||||
const isArrayOperator = (operator: string): boolean => {
|
||||
const arrayOperators = ['in', 'nin', 'IN', 'NOT IN'];
|
||||
return arrayOperators.includes(operator);
|
||||
};
|
||||
|
||||
/**
|
||||
* Format a value for the expression string
|
||||
* @param value - The value to format
|
||||
* @param operator - The operator being used (to determine if array is needed)
|
||||
* @returns Formatted value string
|
||||
*/
|
||||
const formatValueForExpression = (
|
||||
value: string[] | string | number | boolean,
|
||||
operator?: string,
|
||||
): string => {
|
||||
// For IN operators, ensure value is always an array
|
||||
if (isArrayOperator(operator || '')) {
|
||||
const arrayValue = Array.isArray(value) ? value : [value];
|
||||
return `[${arrayValue
|
||||
.map((v) =>
|
||||
typeof v === 'string' ? `'${v.replace(/'/g, "\\'")}'` : String(v),
|
||||
)
|
||||
.join(', ')}]`;
|
||||
}
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
// Handle array values (e.g., for IN operations)
|
||||
return `[${value
|
||||
.map((v) =>
|
||||
typeof v === 'string' ? `'${v.replace(/'/g, "\\'")}'` : String(v),
|
||||
)
|
||||
.join(', ')}]`;
|
||||
}
|
||||
|
||||
if (typeof value === 'string') {
|
||||
// Add single quotes around all string values and escape internal single quotes
|
||||
return `'${value.replace(/'/g, "\\'")}'`;
|
||||
}
|
||||
|
||||
return String(value);
|
||||
};
|
||||
|
||||
export const convertFiltersToExpression = (
|
||||
filters: TagFilter,
|
||||
): { expression: string } => {
|
||||
if (!filters?.items || filters.items.length === 0) {
|
||||
return { expression: '' };
|
||||
}
|
||||
|
||||
const expressions = filters.items
|
||||
.map((filter) => {
|
||||
const { key, op, value } = filter;
|
||||
|
||||
// Skip if key is not defined
|
||||
if (!key?.key) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const formattedValue = formatValueForExpression(value, op);
|
||||
return `${key.key} ${op} ${formattedValue}`;
|
||||
})
|
||||
.filter((expression) => expression !== ''); // Remove empty expressions
|
||||
|
||||
return {
|
||||
expression: expressions.join(' AND '),
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Convert old having format to new having format
|
||||
* @param having - Array of old having objects with columnName, op, and value
|
||||
* @returns New having format with expression string
|
||||
*/
|
||||
export const convertHavingToExpression = (
|
||||
having: Having[],
|
||||
): { expression: string } => {
|
||||
if (!having || having.length === 0) {
|
||||
return { expression: '' };
|
||||
}
|
||||
|
||||
const expressions = having
|
||||
.map((havingItem) => {
|
||||
const { columnName, op, value } = havingItem;
|
||||
|
||||
// Skip if columnName is not defined
|
||||
if (!columnName) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// Format value based on its type
|
||||
let formattedValue: string;
|
||||
if (Array.isArray(value)) {
|
||||
// For array values, format as [val1, val2, ...]
|
||||
formattedValue = `[${value.join(', ')}]`;
|
||||
} else {
|
||||
// For single values, just convert to string
|
||||
formattedValue = String(value);
|
||||
}
|
||||
|
||||
return `${columnName} ${op} ${formattedValue}`;
|
||||
})
|
||||
.filter((expression) => expression !== ''); // Remove empty expressions
|
||||
|
||||
return {
|
||||
expression: expressions.join(' AND '),
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Convert old aggregation format to new aggregation format
|
||||
* @param aggregateOperator - The aggregate operator (e.g., 'sum', 'count', 'avg')
|
||||
* @param aggregateAttribute - The attribute to aggregate
|
||||
* @param dataSource - The data source type
|
||||
* @param timeAggregation - Time aggregation for metrics (optional)
|
||||
* @param spaceAggregation - Space aggregation for metrics (optional)
|
||||
* @param alias - Optional alias for the aggregation
|
||||
* @returns New aggregation format based on data source
|
||||
*
|
||||
*/
|
||||
export const convertAggregationToExpression = (
|
||||
aggregateOperator: string,
|
||||
aggregateAttribute: BaseAutocompleteData,
|
||||
dataSource: DataSource,
|
||||
timeAggregation?: string,
|
||||
spaceAggregation?: string,
|
||||
alias?: string,
|
||||
): (TraceAggregation | LogAggregation | MetricAggregation)[] | undefined => {
|
||||
// Skip if no operator or attribute key
|
||||
if (!aggregateOperator) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Replace noop with count as default
|
||||
const normalizedOperator =
|
||||
aggregateOperator === 'noop' ? 'count' : aggregateOperator;
|
||||
const normalizedTimeAggregation =
|
||||
timeAggregation === 'noop' ? 'count' : timeAggregation;
|
||||
const normalizedSpaceAggregation =
|
||||
spaceAggregation === 'noop' ? 'count' : spaceAggregation;
|
||||
|
||||
// For metrics, use the MetricAggregation format
|
||||
if (dataSource === DataSource.METRICS) {
|
||||
return [
|
||||
{
|
||||
metricName: aggregateAttribute.key,
|
||||
timeAggregation: (normalizedTimeAggregation || normalizedOperator) as any,
|
||||
spaceAggregation: (normalizedSpaceAggregation || normalizedOperator) as any,
|
||||
} as MetricAggregation,
|
||||
];
|
||||
}
|
||||
|
||||
// For traces and logs, use expression format
|
||||
const expression = `${normalizedOperator}(${aggregateAttribute.key})`;
|
||||
|
||||
if (dataSource === DataSource.TRACES) {
|
||||
return [
|
||||
{
|
||||
expression,
|
||||
...(alias && { alias }),
|
||||
} as TraceAggregation,
|
||||
];
|
||||
}
|
||||
|
||||
// For logs
|
||||
return [
|
||||
{
|
||||
expression,
|
||||
...(alias && { alias }),
|
||||
} as LogAggregation,
|
||||
];
|
||||
};
|
||||
@@ -15,3 +15,4 @@ export const DASHBOARD_TIME_IN_DURATION = 'refreshInterval';
|
||||
|
||||
export const DEFAULT_ENTITY_VERSION = 'v3';
|
||||
export const 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',
|
||||
}
|
||||
|
||||
@@ -169,6 +169,9 @@ export const initialQueryBuilderFormValues: IBuilderQuery = {
|
||||
aggregateAttribute: initialAutocompleteData,
|
||||
timeAggregation: MetricAggregateOperator.RATE,
|
||||
spaceAggregation: MetricAggregateOperator.SUM,
|
||||
filter: { expression: '' },
|
||||
aggregations: [{ expression: 'count() ' }],
|
||||
havingExpression: { expression: '' },
|
||||
functions: [],
|
||||
filters: { items: [], op: 'AND' },
|
||||
expression: createNewBuilderItemName({
|
||||
|
||||
@@ -54,19 +54,21 @@ function DomainList(): JSX.Element {
|
||||
|
||||
// initialise tab with default query.
|
||||
useShareBuilderUrl({
|
||||
...initialQueriesMap.traces,
|
||||
builder: {
|
||||
...initialQueriesMap.traces.builder,
|
||||
queryData: [
|
||||
{
|
||||
...initialQueriesMap.traces.builder.queryData[0],
|
||||
dataSource: DataSource.TRACES,
|
||||
aggregateOperator: 'noop',
|
||||
aggregateAttribute: {
|
||||
...initialQueriesMap.traces.builder.queryData[0].aggregateAttribute,
|
||||
defaultValue: {
|
||||
...initialQueriesMap.traces,
|
||||
builder: {
|
||||
...initialQueriesMap.traces.builder,
|
||||
queryData: [
|
||||
{
|
||||
...initialQueriesMap.traces.builder.queryData[0],
|
||||
dataSource: DataSource.TRACES,
|
||||
aggregateOperator: 'noop',
|
||||
aggregateAttribute: {
|
||||
...initialQueriesMap.traces.builder.queryData[0].aggregateAttribute,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ function CloudIntegrationPage(): JSX.Element {
|
||||
<HeroSection />
|
||||
<RequestIntegrationBtn
|
||||
type={IntegrationType.AWS_SERVICES}
|
||||
message="Cannot find the AWS service you're looking for? Request more integrations"
|
||||
message="Can't find the AWS service you're looking for? Request more integrations"
|
||||
/>
|
||||
<ServicesTabs />
|
||||
</div>
|
||||
|
||||
@@ -60,26 +60,30 @@ function CloudServiceDataCollected({
|
||||
|
||||
return (
|
||||
<div className="cloud-service-data-collected">
|
||||
<div className="cloud-service-data-collected__table">
|
||||
<div className="cloud-service-data-collected__table-heading">Logs</div>
|
||||
<Table
|
||||
columns={logsColumns}
|
||||
dataSource={logsData}
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
{...tableProps}
|
||||
className="cloud-service-data-collected__table-logs"
|
||||
/>
|
||||
</div>
|
||||
<div className="cloud-service-data-collected__table">
|
||||
<div className="cloud-service-data-collected__table-heading">Metrics</div>
|
||||
<Table
|
||||
columns={metricsColumns}
|
||||
dataSource={metricsData}
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
{...tableProps}
|
||||
className="cloud-service-data-collected__table-metrics"
|
||||
/>
|
||||
</div>
|
||||
{logsData && logsData.length > 0 && (
|
||||
<div className="cloud-service-data-collected__table">
|
||||
<div className="cloud-service-data-collected__table-heading">Logs</div>
|
||||
<Table
|
||||
columns={logsColumns}
|
||||
dataSource={logsData}
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
{...tableProps}
|
||||
className="cloud-service-data-collected__table-logs"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{metricsData && metricsData.length > 0 && (
|
||||
<div className="cloud-service-data-collected__table">
|
||||
<div className="cloud-service-data-collected__table-heading">Metrics</div>
|
||||
<Table
|
||||
columns={metricsColumns}
|
||||
dataSource={metricsData}
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
{...tableProps}
|
||||
className="cloud-service-data-collected__table-metrics"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -51,6 +51,33 @@ function ServiceStatus({
|
||||
return <div className={`service-status ${className}`}>{text}</div>;
|
||||
}
|
||||
|
||||
function getTabItems(serviceDetailsData: any): TabsProps['items'] {
|
||||
const dashboards = serviceDetailsData?.assets.dashboards || [];
|
||||
const dataCollected = serviceDetailsData?.data_collected || {};
|
||||
const items: TabsProps['items'] = [];
|
||||
|
||||
if (dashboards.length) {
|
||||
items.push({
|
||||
key: 'dashboards',
|
||||
label: `Dashboards (${dashboards.length})`,
|
||||
children: <CloudServiceDashboards service={serviceDetailsData} />,
|
||||
});
|
||||
}
|
||||
|
||||
items.push({
|
||||
key: 'data-collected',
|
||||
label: 'Data Collected',
|
||||
children: (
|
||||
<CloudServiceDataCollected
|
||||
logsData={dataCollected.logs || []}
|
||||
metricsData={dataCollected.metrics || []}
|
||||
/>
|
||||
),
|
||||
});
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
function ServiceDetails(): JSX.Element | null {
|
||||
const urlQuery = useUrlQuery();
|
||||
const cloudAccountId = urlQuery.get('cloudAccountId');
|
||||
@@ -106,23 +133,7 @@ function ServiceDetails(): JSX.Element | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
const tabItems: TabsProps['items'] = [
|
||||
{
|
||||
key: 'dashboards',
|
||||
label: `Dashboards (${serviceDetailsData?.assets.dashboards.length})`,
|
||||
children: <CloudServiceDashboards service={serviceDetailsData} />,
|
||||
},
|
||||
{
|
||||
key: 'data-collected',
|
||||
label: 'Data Collected',
|
||||
children: (
|
||||
<CloudServiceDataCollected
|
||||
logsData={serviceDetailsData?.data_collected.logs || []}
|
||||
metricsData={serviceDetailsData?.data_collected.metrics || []}
|
||||
/>
|
||||
),
|
||||
},
|
||||
];
|
||||
const tabItems = getTabItems(serviceDetailsData);
|
||||
|
||||
return (
|
||||
<div className="service-details">
|
||||
|
||||
@@ -34,7 +34,7 @@ describe('Request AWS integration', () => {
|
||||
|
||||
expect(
|
||||
screen.getByText(
|
||||
/cannot find what you’re looking for\? request more integrations/i,
|
||||
/can't find what you’re looking for\? request more integrations/i,
|
||||
),
|
||||
).toBeInTheDocument();
|
||||
|
||||
|
||||
@@ -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,
|
||||
@@ -45,14 +43,7 @@ import { useHandleExplorerTabChange } from 'hooks/useHandleExplorerTabChange';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import { mapCompositeQueryFromQuery } from 'lib/newQueryBuilder/queryBuilderMappers/mapCompositeQueryFromQuery';
|
||||
import { cloneDeep, isEqual, omit } from 'lodash-es';
|
||||
import {
|
||||
Check,
|
||||
ConciergeBell,
|
||||
Disc3,
|
||||
PanelBottomClose,
|
||||
Plus,
|
||||
X,
|
||||
} from 'lucide-react';
|
||||
import { Check, ConciergeBell, Disc3, Plus, X } from 'lucide-react';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
import { FormattingOptions } from 'providers/preferences/types';
|
||||
import {
|
||||
@@ -78,10 +69,8 @@ import ExplorerOptionsHideArea from './ExplorerOptionsHideArea';
|
||||
import { PreservedViewsInLocalStorage } from './types';
|
||||
import {
|
||||
DATASOURCE_VS_ROUTES,
|
||||
generateRGBAFromHex,
|
||||
getRandomColor,
|
||||
saveNewViewHandler,
|
||||
setExplorerToolBarVisibility,
|
||||
} from './utils';
|
||||
|
||||
const allowedRoles = [USER_ROLES.ADMIN, USER_ROLES.AUTHOR, USER_ROLES.EDITOR];
|
||||
@@ -253,12 +242,6 @@ function ExplorerOptions({
|
||||
const extraData = viewsData?.data?.data?.find((view) => view.id === viewKey)
|
||||
?.extraData;
|
||||
|
||||
const extraDataColor = extraData ? JSON.parse(extraData).color : '';
|
||||
const rgbaColor = generateRGBAFromHex(
|
||||
extraDataColor || Color.BG_SIENNA_500,
|
||||
0.08,
|
||||
);
|
||||
|
||||
const { options, handleOptionsChange } = useOptionsMenu({
|
||||
storageKey:
|
||||
sourcepage === DataSource.TRACES
|
||||
@@ -589,13 +572,6 @@ function ExplorerOptions({
|
||||
[isDarkMode],
|
||||
);
|
||||
|
||||
const hideToolbar = (): void => {
|
||||
setExplorerToolBarVisibility(false, sourcepage);
|
||||
if (setIsExplorerOptionHidden) {
|
||||
setIsExplorerOptionHidden(true);
|
||||
}
|
||||
};
|
||||
|
||||
const isEditDeleteSupported = allowedRoles.includes(user.role as string);
|
||||
|
||||
const [
|
||||
@@ -647,27 +623,6 @@ function ExplorerOptions({
|
||||
viewsData?.data?.data,
|
||||
]);
|
||||
|
||||
const infoIconText = useMemo(() => {
|
||||
if (isLogsExplorer) {
|
||||
return 'Learn more about Logs explorer';
|
||||
}
|
||||
if (isMetricsExplorer) {
|
||||
return 'Learn more about Metrics explorer';
|
||||
}
|
||||
return 'Learn more about Traces explorer';
|
||||
}, [isLogsExplorer, isMetricsExplorer]);
|
||||
|
||||
const infoIconLink = useMemo(() => {
|
||||
if (isLogsExplorer) {
|
||||
return 'https://signoz.io/docs/product-features/logs-explorer/?utm_source=product&utm_medium=logs-explorer-toolbar';
|
||||
}
|
||||
// TODO: Add metrics explorer info icon link
|
||||
if (isMetricsExplorer) {
|
||||
return '';
|
||||
}
|
||||
return 'https://signoz.io/docs/product-features/trace-explorer/?utm_source=product&utm_medium=trace-explorer-toolbar';
|
||||
}, [isLogsExplorer, isMetricsExplorer]);
|
||||
|
||||
const getQueryName = (query: Query): string => {
|
||||
if (query.builder.queryFormulas.length > 0) {
|
||||
return `Formula ${query.builder.queryFormulas[0].queryName}`;
|
||||
@@ -680,11 +635,10 @@ function ExplorerOptions({
|
||||
const selectLabel = (
|
||||
<Button
|
||||
disabled={disabled}
|
||||
shape="round"
|
||||
className="periscope-btn ghost"
|
||||
shape="default"
|
||||
icon={<ConciergeBell size={16} />}
|
||||
>
|
||||
Create an Alert
|
||||
</Button>
|
||||
/>
|
||||
);
|
||||
return (
|
||||
<Select
|
||||
@@ -713,12 +667,11 @@ function ExplorerOptions({
|
||||
return (
|
||||
<Button
|
||||
disabled={disabled}
|
||||
shape="round"
|
||||
shape="default"
|
||||
className="periscope-btn ghost"
|
||||
onClick={(): void => onCreateAlertsHandler(query)}
|
||||
icon={<ConciergeBell size={16} />}
|
||||
>
|
||||
Create an Alert
|
||||
</Button>
|
||||
/>
|
||||
);
|
||||
}, [
|
||||
disabled,
|
||||
@@ -732,14 +685,11 @@ function ExplorerOptions({
|
||||
if (isOneChartPerQuery) {
|
||||
const selectLabel = (
|
||||
<Button
|
||||
type="primary"
|
||||
className="periscope-btn ghost"
|
||||
disabled={disabled}
|
||||
shape="round"
|
||||
onClick={onAddToDashboard}
|
||||
icon={<Plus size={16} />}
|
||||
>
|
||||
Add to Dashboard
|
||||
</Button>
|
||||
icon={<Plus size={12} />}
|
||||
/>
|
||||
);
|
||||
return (
|
||||
<Select
|
||||
@@ -771,14 +721,11 @@ function ExplorerOptions({
|
||||
}
|
||||
return (
|
||||
<Button
|
||||
type="primary"
|
||||
className="periscope-btn ghost"
|
||||
disabled={disabled}
|
||||
shape="round"
|
||||
onClick={onAddToDashboard}
|
||||
icon={<Plus size={16} />}
|
||||
>
|
||||
Add to Dashboard
|
||||
</Button>
|
||||
/>
|
||||
);
|
||||
}, [disabled, isOneChartPerQuery, onAddToDashboard, splitedQueries]);
|
||||
|
||||
@@ -797,41 +744,31 @@ function ExplorerOptions({
|
||||
>
|
||||
<Tooltip title="Clear this view" placement="top">
|
||||
<Button
|
||||
className="action-icon"
|
||||
className="periscope-btn ghost"
|
||||
onClick={handleClearSelect}
|
||||
icon={<X size={14} />}
|
||||
icon={<X size={16} />}
|
||||
/>
|
||||
</Tooltip>
|
||||
{
|
||||
// only show the update view option when the query is updated
|
||||
}
|
||||
{isQueryUpdated && (
|
||||
<>
|
||||
<Divider
|
||||
type="vertical"
|
||||
className={isEditDeleteSupported ? '' : 'hidden'}
|
||||
<Tooltip title="Update this view" placement="top">
|
||||
<Button
|
||||
className={cx(
|
||||
'periscope-btn ghost',
|
||||
isEditDeleteSupported ? '' : 'hidden',
|
||||
)}
|
||||
disabled={isViewUpdating}
|
||||
onClick={onUpdateQueryHandler}
|
||||
icon={<Disc3 size={16} />}
|
||||
/>
|
||||
<Tooltip title="Update this view" placement="top">
|
||||
<Button
|
||||
className={cx('action-icon', isEditDeleteSupported ? ' ' : 'hidden')}
|
||||
disabled={isViewUpdating}
|
||||
onClick={onUpdateQueryHandler}
|
||||
icon={<Disc3 size={14} />}
|
||||
/>
|
||||
</Tooltip>
|
||||
</>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{!isExplorerOptionHidden && (
|
||||
<div
|
||||
className="explorer-options"
|
||||
style={{
|
||||
background: extraData
|
||||
? `linear-gradient(90deg, rgba(0,0,0,0) -5%, ${rgbaColor} 9%, rgba(0,0,0,0) 30%)`
|
||||
: 'transparent',
|
||||
}}
|
||||
>
|
||||
<div className="explorer-options">
|
||||
<div className="view-options">
|
||||
<Select<string, { key: string; value: string }>
|
||||
showSearch
|
||||
@@ -872,49 +809,23 @@ function ExplorerOptions({
|
||||
</Select>
|
||||
|
||||
<Button
|
||||
shape="round"
|
||||
shape="default"
|
||||
className={cx(
|
||||
'periscope-btn secondary',
|
||||
isEditDeleteSupported ? '' : 'hidden',
|
||||
)}
|
||||
onClick={handleSaveViewModalToggle}
|
||||
className={isEditDeleteSupported ? '' : 'hidden'}
|
||||
disabled={viewsIsLoading || isRefetching}
|
||||
icon={<Disc3 size={16} />}
|
||||
icon={<Disc3 size={12} />}
|
||||
>
|
||||
Save this view
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<hr className={isEditDeleteSupported ? '' : 'hidden'} />
|
||||
|
||||
<div className={cx('actions', isEditDeleteSupported ? '' : 'hidden')}>
|
||||
{alertButton}
|
||||
{dashboardButton}
|
||||
</div>
|
||||
<div className="actions">
|
||||
{/* Hide the info icon for metrics explorer until we get the docs link */}
|
||||
{!isMetricsExplorer && (
|
||||
<Tooltip
|
||||
title={
|
||||
<div>
|
||||
{infoIconText}
|
||||
<Typography.Link href={infoIconLink} target="_blank">
|
||||
{' '}
|
||||
here
|
||||
</Typography.Link>{' '}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<InfoCircleOutlined className="info-icon" />
|
||||
</Tooltip>
|
||||
)}
|
||||
<Tooltip title="Hide">
|
||||
<Button
|
||||
disabled={disabled}
|
||||
shape="circle"
|
||||
onClick={hideToolbar}
|
||||
icon={<PanelBottomClose size={16} />}
|
||||
data-testid="hide-toolbar"
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<ExplorerOptionsHideArea
|
||||
|
||||
@@ -2,12 +2,13 @@ import './ChartPreview.styles.scss';
|
||||
|
||||
import { InfoCircleOutlined } from '@ant-design/icons';
|
||||
import Spinner from 'components/Spinner';
|
||||
import { DEFAULT_ENTITY_VERSION } from 'constants/app';
|
||||
import { ENTITY_VERSION_V5 } from 'constants/app';
|
||||
import { FeatureKeys } from 'constants/features';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import AnomalyAlertEvaluationView from 'container/AnomalyAlertEvaluationView';
|
||||
import GridPanelSwitch from 'container/GridPanelSwitch';
|
||||
import { populateMultipleResults } from 'container/NewWidget/LeftContainer/WidgetGraph/util';
|
||||
import { getFormatNameByOptionId } from 'container/NewWidget/RightContainer/alertFomatCategories';
|
||||
import { timePreferenceType } from 'container/NewWidget/RightContainer/timeItems';
|
||||
import { Time } from 'container/TopNav/DateTimeSelection/config';
|
||||
@@ -144,7 +145,8 @@ function ChartPreview({
|
||||
},
|
||||
originalGraphType: graphType,
|
||||
},
|
||||
alertDef?.version || DEFAULT_ENTITY_VERSION,
|
||||
// alertDef?.version || DEFAULT_ENTITY_VERSION,
|
||||
ENTITY_VERSION_V5,
|
||||
{
|
||||
queryKey: [
|
||||
'chartPreview',
|
||||
@@ -175,6 +177,12 @@ function ChartPreview({
|
||||
queryResponse.data.payload.data.result = sortedSeriesData;
|
||||
}
|
||||
|
||||
if (queryResponse.data && graphType === PANEL_TYPES.PIE) {
|
||||
const transformedData = populateMultipleResults(queryResponse?.data);
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
queryResponse.data = transformedData;
|
||||
}
|
||||
|
||||
const containerDimensions = useResizeObserver(graphRef);
|
||||
|
||||
const isDarkMode = useIsDarkMode();
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -162,7 +162,7 @@ function FormAlertRules({
|
||||
|
||||
const sq = useMemo(() => mapQueryDataFromApi(initQuery), [initQuery]);
|
||||
|
||||
useShareBuilderUrl(sq);
|
||||
useShareBuilderUrl({ defaultValue: sq });
|
||||
|
||||
const handleDetectionMethodChange = (value: string): void => {
|
||||
setAlertDef((def) => ({
|
||||
@@ -832,7 +832,7 @@ function FormAlertRules({
|
||||
queryCategory={currentQuery.queryType}
|
||||
setQueryCategory={onQueryCategoryChange}
|
||||
alertType={alertType || AlertTypes.METRICS_BASED_ALERT}
|
||||
runQuery={(): void => handleRunQuery(true)}
|
||||
runQuery={(): void => handleRunQuery(true, true)}
|
||||
alertDef={alertDef}
|
||||
panelType={panelType || PANEL_TYPES.TIME_SERIES}
|
||||
key={currentQuery.queryType}
|
||||
|
||||
@@ -10,9 +10,10 @@ import cx from 'classnames';
|
||||
import { ToggleGraphProps } from 'components/Graph/types';
|
||||
import Spinner from 'components/Spinner';
|
||||
import TimePreference from 'components/TimePreferenceDropDown';
|
||||
import { DEFAULT_ENTITY_VERSION } from 'constants/app';
|
||||
import { ENTITY_VERSION_V5 } from 'constants/app';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { populateMultipleResults } from 'container/NewWidget/LeftContainer/WidgetGraph/util';
|
||||
import {
|
||||
timeItems,
|
||||
timePreferance,
|
||||
@@ -122,7 +123,8 @@ function FullView({
|
||||
|
||||
const response = useGetQueryRange(
|
||||
requestData,
|
||||
selectedDashboard?.data?.version || version || DEFAULT_ENTITY_VERSION,
|
||||
// selectedDashboard?.data?.version || version || DEFAULT_ENTITY_VERSION,
|
||||
ENTITY_VERSION_V5,
|
||||
{
|
||||
queryKey: [widget?.query, widget?.panelTypes, requestData, version],
|
||||
enabled: !isDependedDataLoaded,
|
||||
@@ -178,6 +180,12 @@ function FullView({
|
||||
response.data.payload.data.result = sortedSeriesData;
|
||||
}
|
||||
|
||||
if (response.data && widget.panelTypes === PANEL_TYPES.PIE) {
|
||||
const transformedData = populateMultipleResults(response?.data);
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
response.data = transformedData;
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
graphsVisibilityStates?.forEach((e, i) => {
|
||||
fullViewChartRef?.current?.toggleGraph(i, e);
|
||||
|
||||
@@ -2,6 +2,7 @@ import logEvent from 'api/common/logEvent';
|
||||
import { DEFAULT_ENTITY_VERSION } from 'constants/app';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { populateMultipleResults } from 'container/NewWidget/LeftContainer/WidgetGraph/util';
|
||||
import { CustomTimeType } from 'container/TopNav/DateTimeSelectionV2/config';
|
||||
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
|
||||
import { useIntersectionObserver } from 'hooks/useIntersectionObserver';
|
||||
@@ -271,6 +272,12 @@ function GridCardGraph({
|
||||
queryResponse.data.payload.data.result = sortedSeriesData;
|
||||
}
|
||||
|
||||
if (queryResponse.data && widget.panelTypes === PANEL_TYPES.PIE) {
|
||||
const transformedData = populateMultipleResults(queryResponse?.data);
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
queryResponse.data = transformedData;
|
||||
}
|
||||
|
||||
const menuList =
|
||||
widget.panelTypes === PANEL_TYPES.TABLE ||
|
||||
widget.panelTypes === PANEL_TYPES.LIST ||
|
||||
|
||||
@@ -6,6 +6,7 @@ import { Button, Form, Input, Modal, Typography } from 'antd';
|
||||
import { useForm } from 'antd/es/form/Form';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import cx from 'classnames';
|
||||
import { ENTITY_VERSION_V5 } from 'constants/app';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import { PANEL_GROUP_TYPES, PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { themeColors } from 'constants/theme';
|
||||
@@ -579,7 +580,8 @@ function GraphLayout(props: GraphLayoutProps): JSX.Element {
|
||||
widget={(currentWidget as Widgets) || ({ id, query: {} } as Widgets)}
|
||||
headerMenuList={widgetActions}
|
||||
variables={variables}
|
||||
version={selectedDashboard?.data?.version}
|
||||
// version={selectedDashboard?.data?.version}
|
||||
version={ENTITY_VERSION_V5}
|
||||
onDragSelect={onDragSelect}
|
||||
dataAvailable={checkIfDataExists}
|
||||
/>
|
||||
|
||||
@@ -109,7 +109,7 @@ describe('GridCardLayout Utils', () => {
|
||||
builder: {
|
||||
queryData: [
|
||||
{
|
||||
stepInterval: 30,
|
||||
stepInterval: 60,
|
||||
aggregateOperator: 'avg',
|
||||
dataSource: DataSource.METRICS,
|
||||
queryName: 'A',
|
||||
@@ -181,7 +181,7 @@ describe('GridCardLayout Utils', () => {
|
||||
|
||||
expect(result.builder.queryData).toHaveLength(2);
|
||||
expect(result.builder.queryData[0].stepInterval).toBe(180);
|
||||
expect(result.builder.queryData[1].stepInterval).toBe(180);
|
||||
expect(result.builder.queryData[1].stepInterval).toBe(45); // 45 is the stepInterval of the second query - custom value
|
||||
});
|
||||
|
||||
it('should use calculated stepInterval when original is undefined', () => {
|
||||
|
||||
@@ -2,7 +2,7 @@ import { FORMULA_REGEXP } from 'constants/regExp';
|
||||
import { isEmpty, isEqual } from 'lodash-es';
|
||||
import { Layout } from 'react-grid-layout';
|
||||
import { Dashboard, Widgets } from 'types/api/dashboard/getAll';
|
||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { IBuilderQuery, Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
export const removeUndefinedValuesFromLayout = (layout: Layout[]): Layout[] =>
|
||||
layout.map((obj) =>
|
||||
@@ -99,6 +99,12 @@ export function updateStepInterval(
|
||||
): Query {
|
||||
const stepIntervalPoints = getStepIntervalPoints(minTime, maxTime);
|
||||
|
||||
// if user haven't enter anything manually, that is we have default value of 60 then do the interval adjustment for bar otherwise apply the user's value
|
||||
const getSteps = (queryData: IBuilderQuery): number =>
|
||||
queryData.stepInterval === 60
|
||||
? stepIntervalPoints || 60
|
||||
: queryData?.stepInterval || 60;
|
||||
|
||||
return {
|
||||
...query,
|
||||
builder: {
|
||||
@@ -106,7 +112,7 @@ export function updateStepInterval(
|
||||
queryData: [
|
||||
...(query?.builder?.queryData ?? []).map((queryData) => ({
|
||||
...queryData,
|
||||
stepInterval: stepIntervalPoints || queryData?.stepInterval || 60,
|
||||
stepInterval: getSteps(queryData),
|
||||
})),
|
||||
],
|
||||
},
|
||||
|
||||
@@ -172,6 +172,13 @@
|
||||
.ant-table-cell:nth-child(n + 3) {
|
||||
padding-right: 24px;
|
||||
}
|
||||
.memory-usage-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 4px;
|
||||
margin-right: 4px;
|
||||
}
|
||||
.column-header-right {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import './InfraMonitoring.styles.scss';
|
||||
|
||||
import { InfoCircleOutlined } from '@ant-design/icons';
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { Progress, TabsProps, Tag } from 'antd';
|
||||
import { Progress, TabsProps, Tag, Tooltip } from 'antd';
|
||||
import { ColumnType } from 'antd/es/table';
|
||||
import {
|
||||
HostData,
|
||||
@@ -93,7 +94,14 @@ export const getHostsListColumns = (): ColumnType<HostRowData>[] => [
|
||||
align: 'right',
|
||||
},
|
||||
{
|
||||
title: <div className="column-header-right">Memory Usage</div>,
|
||||
title: (
|
||||
<div className="column-header-right memory-usage-header">
|
||||
Memory Usage
|
||||
<Tooltip title="Excluding cache memory">
|
||||
<InfoCircleOutlined />
|
||||
</Tooltip>
|
||||
</div>
|
||||
),
|
||||
dataIndex: 'memory',
|
||||
key: 'memory',
|
||||
width: 100,
|
||||
@@ -210,6 +218,10 @@ export function GetHostsQuickFiltersConfig(
|
||||
? 'system.cpu.load_average.15m'
|
||||
: 'system_cpu_load_average_15m';
|
||||
|
||||
const environmentKey = dotMetricsEnabled
|
||||
? 'deployment.environment'
|
||||
: 'deployment_environment';
|
||||
|
||||
return [
|
||||
{
|
||||
type: FiltersType.CHECKBOX,
|
||||
@@ -241,5 +253,17 @@ export function GetHostsQuickFiltersConfig(
|
||||
dataSource: DataSource.METRICS,
|
||||
defaultOpen: true,
|
||||
},
|
||||
{
|
||||
type: FiltersType.CHECKBOX,
|
||||
title: 'Environment',
|
||||
attributeKey: {
|
||||
key: environmentKey,
|
||||
dataType: DataTypes.String,
|
||||
type: 'resource',
|
||||
isColumn: false,
|
||||
isJSON: false,
|
||||
},
|
||||
defaultOpen: true,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
@@ -90,6 +90,9 @@ export function GetPodsQuickFiltersConfig(
|
||||
? 'k8s.daemonset.name'
|
||||
: 'k8s_daemonset_name';
|
||||
const jobKey = dotMetricsEnabled ? 'k8s.job.name' : 'k8s_job_name';
|
||||
const environmentKey = dotMetricsEnabled
|
||||
? 'deployment.environment'
|
||||
: 'deployment_environment';
|
||||
|
||||
// Define aggregate attribute (metric) name
|
||||
const cpuUtilizationMetric = dotMetricsEnabled
|
||||
@@ -225,6 +228,18 @@ export function GetPodsQuickFiltersConfig(
|
||||
dataSource: DataSource.METRICS,
|
||||
defaultOpen: false,
|
||||
},
|
||||
{
|
||||
type: FiltersType.CHECKBOX,
|
||||
title: 'Environment',
|
||||
attributeKey: {
|
||||
key: environmentKey,
|
||||
dataType: DataTypes.String,
|
||||
type: 'resource',
|
||||
isColumn: false,
|
||||
isJSON: false,
|
||||
},
|
||||
defaultOpen: true,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
@@ -239,6 +254,9 @@ export function GetNodesQuickFiltersConfig(
|
||||
const cpuUtilMetric = dotMetricsEnabled
|
||||
? 'k8s.node.cpu.utilization'
|
||||
: 'k8s_node_cpu_utilization';
|
||||
const environmentKey = dotMetricsEnabled
|
||||
? 'deployment.environment'
|
||||
: 'deployment_environment';
|
||||
|
||||
return [
|
||||
{
|
||||
@@ -273,6 +291,18 @@ export function GetNodesQuickFiltersConfig(
|
||||
dataSource: DataSource.METRICS,
|
||||
defaultOpen: true,
|
||||
},
|
||||
{
|
||||
type: FiltersType.CHECKBOX,
|
||||
title: 'Environment',
|
||||
attributeKey: {
|
||||
key: environmentKey,
|
||||
dataType: DataTypes.String,
|
||||
type: 'resource',
|
||||
isColumn: false,
|
||||
isJSON: false,
|
||||
},
|
||||
defaultOpen: true,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
@@ -286,6 +316,9 @@ export function GetNamespaceQuickFiltersConfig(
|
||||
const cpuUtilMetric = dotMetricsEnabled
|
||||
? 'k8s.pod.cpu.utilization'
|
||||
: 'k8s_pod_cpu_utilization';
|
||||
const environmentKey = dotMetricsEnabled
|
||||
? 'deployment.environment'
|
||||
: 'deployment_environment';
|
||||
|
||||
return [
|
||||
{
|
||||
@@ -320,6 +353,18 @@ export function GetNamespaceQuickFiltersConfig(
|
||||
dataSource: DataSource.METRICS,
|
||||
defaultOpen: true,
|
||||
},
|
||||
{
|
||||
type: FiltersType.CHECKBOX,
|
||||
title: 'Environment',
|
||||
attributeKey: {
|
||||
key: environmentKey,
|
||||
dataType: DataTypes.String,
|
||||
type: 'resource',
|
||||
isColumn: false,
|
||||
isJSON: false,
|
||||
},
|
||||
defaultOpen: true,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
@@ -330,6 +375,9 @@ export function GetClustersQuickFiltersConfig(
|
||||
const cpuUtilMetric = dotMetricsEnabled
|
||||
? 'k8s.node.cpu.utilization'
|
||||
: 'k8s_node_cpu_utilization';
|
||||
const environmentKey = dotMetricsEnabled
|
||||
? 'deployment.environment'
|
||||
: 'deployment_environment';
|
||||
|
||||
return [
|
||||
{
|
||||
@@ -348,6 +396,18 @@ export function GetClustersQuickFiltersConfig(
|
||||
dataSource: DataSource.METRICS,
|
||||
defaultOpen: true,
|
||||
},
|
||||
{
|
||||
type: FiltersType.CHECKBOX,
|
||||
title: 'Environment',
|
||||
attributeKey: {
|
||||
key: environmentKey,
|
||||
dataType: DataTypes.String,
|
||||
type: 'resource',
|
||||
isColumn: false,
|
||||
isJSON: false,
|
||||
},
|
||||
defaultOpen: true,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
@@ -357,6 +417,9 @@ export function GetContainersQuickFiltersConfig(
|
||||
const containerKey = dotMetricsEnabled
|
||||
? 'k8s.container.name'
|
||||
: 'k8s_container_name';
|
||||
const environmentKey = dotMetricsEnabled
|
||||
? 'deployment.environment'
|
||||
: 'deployment_environment';
|
||||
|
||||
return [
|
||||
{
|
||||
@@ -372,6 +435,18 @@ export function GetContainersQuickFiltersConfig(
|
||||
},
|
||||
defaultOpen: true,
|
||||
},
|
||||
{
|
||||
type: FiltersType.CHECKBOX,
|
||||
title: 'Environment',
|
||||
attributeKey: {
|
||||
key: environmentKey,
|
||||
dataType: DataTypes.String,
|
||||
type: 'resource',
|
||||
isColumn: false,
|
||||
isJSON: false,
|
||||
},
|
||||
defaultOpen: true,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
@@ -388,6 +463,9 @@ export function GetVolumesQuickFiltersConfig(
|
||||
const volumeMetric = dotMetricsEnabled
|
||||
? 'k8s.volume.capacity'
|
||||
: 'k8s_volume_capacity';
|
||||
const environmentKey = dotMetricsEnabled
|
||||
? 'deployment.environment'
|
||||
: 'deployment_environment';
|
||||
|
||||
return [
|
||||
{
|
||||
@@ -438,6 +516,18 @@ export function GetVolumesQuickFiltersConfig(
|
||||
dataSource: DataSource.METRICS,
|
||||
defaultOpen: true,
|
||||
},
|
||||
{
|
||||
type: FiltersType.CHECKBOX,
|
||||
title: 'Environment',
|
||||
attributeKey: {
|
||||
key: environmentKey,
|
||||
dataType: DataTypes.String,
|
||||
type: 'resource',
|
||||
isColumn: false,
|
||||
isJSON: false,
|
||||
},
|
||||
defaultOpen: true,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
@@ -454,6 +544,9 @@ export function GetDeploymentsQuickFiltersConfig(
|
||||
const metric = dotMetricsEnabled
|
||||
? 'k8s.pod.cpu.utilization'
|
||||
: 'k8s_pod_cpu_utilization';
|
||||
const environmentKey = dotMetricsEnabled
|
||||
? 'deployment.environment'
|
||||
: 'deployment_environment';
|
||||
|
||||
return [
|
||||
{
|
||||
@@ -504,6 +597,18 @@ export function GetDeploymentsQuickFiltersConfig(
|
||||
dataSource: DataSource.METRICS,
|
||||
defaultOpen: true,
|
||||
},
|
||||
{
|
||||
type: FiltersType.CHECKBOX,
|
||||
title: 'Environment',
|
||||
attributeKey: {
|
||||
key: environmentKey,
|
||||
dataType: DataTypes.String,
|
||||
type: 'resource',
|
||||
isColumn: false,
|
||||
isJSON: false,
|
||||
},
|
||||
defaultOpen: true,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
@@ -520,6 +625,9 @@ export function GetStatefulsetsQuickFiltersConfig(
|
||||
const metric = dotMetricsEnabled
|
||||
? 'k8s.pod.cpu.utilization'
|
||||
: 'k8s_pod_cpu_utilization';
|
||||
const environmentKey = dotMetricsEnabled
|
||||
? 'deployment.environment'
|
||||
: 'deployment_environment';
|
||||
|
||||
return [
|
||||
{
|
||||
@@ -570,6 +678,18 @@ export function GetStatefulsetsQuickFiltersConfig(
|
||||
dataSource: DataSource.METRICS,
|
||||
defaultOpen: true,
|
||||
},
|
||||
{
|
||||
type: FiltersType.CHECKBOX,
|
||||
title: 'Environment',
|
||||
attributeKey: {
|
||||
key: environmentKey,
|
||||
dataType: DataTypes.String,
|
||||
type: 'resource',
|
||||
isColumn: false,
|
||||
isJSON: false,
|
||||
},
|
||||
defaultOpen: true,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
@@ -586,6 +706,9 @@ export function GetDaemonsetsQuickFiltersConfig(
|
||||
const metricName = dotMetricsEnabled
|
||||
? 'k8s.pod.cpu.utilization'
|
||||
: 'k8s_pod_cpu_utilization';
|
||||
const environmentKey = dotMetricsEnabled
|
||||
? 'deployment.environment'
|
||||
: 'deployment_environment';
|
||||
|
||||
return [
|
||||
{
|
||||
@@ -634,6 +757,18 @@ export function GetDaemonsetsQuickFiltersConfig(
|
||||
dataSource: DataSource.METRICS,
|
||||
defaultOpen: true,
|
||||
},
|
||||
{
|
||||
type: FiltersType.CHECKBOX,
|
||||
title: 'Environment',
|
||||
attributeKey: {
|
||||
key: environmentKey,
|
||||
dataType: DataTypes.String,
|
||||
type: 'resource',
|
||||
isColumn: false,
|
||||
isJSON: false,
|
||||
},
|
||||
defaultOpen: true,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
@@ -648,6 +783,9 @@ export function GetJobsQuickFiltersConfig(
|
||||
const metricName = dotMetricsEnabled
|
||||
? 'k8s.pod.cpu.utilization'
|
||||
: 'k8s_pod_cpu_utilization';
|
||||
const environmentKey = dotMetricsEnabled
|
||||
? 'deployment.environment'
|
||||
: 'deployment_environment';
|
||||
|
||||
return [
|
||||
{
|
||||
@@ -696,6 +834,18 @@ export function GetJobsQuickFiltersConfig(
|
||||
dataSource: DataSource.METRICS,
|
||||
defaultOpen: true,
|
||||
},
|
||||
{
|
||||
type: FiltersType.CHECKBOX,
|
||||
title: 'Environment',
|
||||
attributeKey: {
|
||||
key: environmentKey,
|
||||
dataType: DataTypes.String,
|
||||
type: 'resource',
|
||||
isColumn: false,
|
||||
isJSON: false,
|
||||
},
|
||||
defaultOpen: true,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -71,7 +71,7 @@ import {
|
||||
PaginationProps,
|
||||
} from 'types/api/ingestionKeys/types';
|
||||
import { USER_ROLES } from 'types/roles';
|
||||
import { hasDatePassed } from 'utils/timeUtils';
|
||||
import { getDaysUntilExpiry } from 'utils/timeUtils';
|
||||
|
||||
const { Option } = Select;
|
||||
|
||||
@@ -1242,23 +1242,37 @@ function MultiIngestionSettings(): JSX.Element {
|
||||
|
||||
<div className="ingestion-key-details">
|
||||
<div className="ingestion-key-last-used-at">
|
||||
<CalendarClock size={14} />
|
||||
{((): JSX.Element | null => {
|
||||
const daysToExpiry = getDaysUntilExpiry(expiresOn);
|
||||
const isNoExpiry = expiresOn === 'No Expiry';
|
||||
|
||||
{hasDatePassed(expiresOn) ? (
|
||||
<>
|
||||
Expired on <Minus size={12} />
|
||||
<Typography.Text>{expiresOn}</Typography.Text>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{expiresOn !== 'No Expiry' && (
|
||||
<>
|
||||
Expires on <Minus size={12} />
|
||||
</>
|
||||
)}
|
||||
<Typography.Text>{expiresOn}</Typography.Text>
|
||||
</>
|
||||
)}
|
||||
if (!isNoExpiry && daysToExpiry < 0) {
|
||||
return (
|
||||
<div className="ingestion-key-expires-in danger">
|
||||
<CalendarClock size={14} /> Expired on
|
||||
<Minus size={12} /> {expiresOn}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (!isNoExpiry && daysToExpiry <= 3) {
|
||||
return (
|
||||
<div className="ingestion-key-expires-in warning">
|
||||
<CalendarClock size={14} /> Expires on
|
||||
<Minus size={12} /> {expiresOn}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<>
|
||||
{!isNoExpiry && (
|
||||
<>
|
||||
<CalendarClock size={14} /> Expires on <Minus size={12} />
|
||||
</>
|
||||
)}
|
||||
<Typography.Text>{expiresOn}</Typography.Text>
|
||||
</>
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -25,7 +25,7 @@ import logEvent from 'api/common/logEvent';
|
||||
import createDashboard from 'api/v1/dashboards/create';
|
||||
import { AxiosError } from 'axios';
|
||||
import cx from 'classnames';
|
||||
import { ENTITY_VERSION_V4 } from 'constants/app';
|
||||
import { ENTITY_VERSION_V5 } from 'constants/app';
|
||||
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { sanitizeDashboardData } from 'container/NewDashboard/DashboardDescription';
|
||||
@@ -293,7 +293,7 @@ function DashboardsList(): JSX.Element {
|
||||
ns: 'dashboard',
|
||||
}),
|
||||
uploadedGrafana: false,
|
||||
version: ENTITY_VERSION_V4,
|
||||
version: ENTITY_VERSION_V5,
|
||||
});
|
||||
|
||||
safeNavigate(
|
||||
|
||||
@@ -58,7 +58,14 @@ export function RequestDashboardBtn(): JSX.Element {
|
||||
return (
|
||||
<div className="request-entity-container">
|
||||
<Typography.Text>
|
||||
Can't find the dashboard you need? Request a new Dashboard.
|
||||
<a
|
||||
href="https://github.com/SigNoz/dashboards"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Browse dashboard templates
|
||||
</a>{' '}
|
||||
or Request new template →
|
||||
</Typography.Text>
|
||||
|
||||
<div className="form-section">
|
||||
|
||||
@@ -2463,7 +2463,7 @@ export const getHostQueryPayload = (
|
||||
functions: [],
|
||||
groupBy: [],
|
||||
having: [],
|
||||
legend: '',
|
||||
legend: 'system disk io',
|
||||
limit: null,
|
||||
orderBy: [],
|
||||
queryName: 'A',
|
||||
|
||||
@@ -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,47 +1,40 @@
|
||||
import './LogsExplorerQuerySection.styles.scss';
|
||||
|
||||
import { QueryBuilderV2 } from 'components/QueryBuilderV2/QueryBuilderV2';
|
||||
import {
|
||||
initialQueriesMap,
|
||||
OPERATORS,
|
||||
PANEL_TYPES,
|
||||
} from 'constants/queryBuilder';
|
||||
import ExplorerOrderBy from 'container/ExplorerOrderBy';
|
||||
import { QueryBuilder } from 'container/QueryBuilder';
|
||||
import { OrderByFilterProps } from 'container/QueryBuilder/filters/OrderByFilter/OrderByFilter.interfaces';
|
||||
import QueryBuilderSearchV2 from 'container/QueryBuilder/filters/QueryBuilderSearchV2/QueryBuilderSearchV2';
|
||||
import { QueryBuilderProps } from 'container/QueryBuilder/QueryBuilder.interfaces';
|
||||
import { useGetPanelTypesQueryParam } from 'hooks/queryBuilder/useGetPanelTypesQueryParam';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { useQueryOperations } from 'hooks/queryBuilder/useQueryBuilderOperations';
|
||||
import { useShareBuilderUrl } from 'hooks/queryBuilder/useShareBuilderUrl';
|
||||
import {
|
||||
prepareQueryWithDefaultTimestamp,
|
||||
SELECTED_VIEWS,
|
||||
} from 'pages/LogsExplorer/utils';
|
||||
import { ExplorerViews } from 'pages/LogsExplorer/utils';
|
||||
import { memo, useCallback, useMemo } from 'react';
|
||||
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
function LogExplorerQuerySection({
|
||||
selectedView,
|
||||
}: {
|
||||
selectedView: SELECTED_VIEWS;
|
||||
selectedView: ExplorerViews;
|
||||
}): JSX.Element {
|
||||
const { currentQuery, updateAllQueriesOperators } = useQueryBuilder();
|
||||
|
||||
const query = currentQuery?.builder?.queryData[0] || null;
|
||||
const { updateAllQueriesOperators } = useQueryBuilder();
|
||||
|
||||
const panelTypes = useGetPanelTypesQueryParam(PANEL_TYPES.LIST);
|
||||
const defaultValue = useMemo(() => {
|
||||
const updatedQuery = updateAllQueriesOperators(
|
||||
initialQueriesMap.logs,
|
||||
PANEL_TYPES.LIST,
|
||||
DataSource.LOGS,
|
||||
);
|
||||
return prepareQueryWithDefaultTimestamp(updatedQuery);
|
||||
}, [updateAllQueriesOperators]);
|
||||
const defaultValue = useMemo(
|
||||
() =>
|
||||
updateAllQueriesOperators(
|
||||
initialQueriesMap.logs,
|
||||
PANEL_TYPES.LIST,
|
||||
DataSource.LOGS,
|
||||
),
|
||||
[updateAllQueriesOperators],
|
||||
);
|
||||
|
||||
useShareBuilderUrl(defaultValue);
|
||||
useShareBuilderUrl({ defaultValue });
|
||||
|
||||
const filterConfigs: QueryBuilderProps['filterConfigs'] = useMemo(() => {
|
||||
const isTable = panelTypes === PANEL_TYPES.TABLE;
|
||||
@@ -58,13 +51,6 @@ function LogExplorerQuerySection({
|
||||
return config;
|
||||
}, [panelTypes]);
|
||||
|
||||
const { handleChangeQueryData } = useQueryOperations({
|
||||
index: 0,
|
||||
query,
|
||||
filterConfigs,
|
||||
entityVersion: '',
|
||||
});
|
||||
|
||||
const renderOrderBy = useCallback(
|
||||
({ query, onChange }: OrderByFilterProps): JSX.Element => (
|
||||
<ExplorerOrderBy query={query} onChange={onChange} />
|
||||
@@ -79,35 +65,16 @@ function LogExplorerQuerySection({
|
||||
[panelTypes, renderOrderBy],
|
||||
);
|
||||
|
||||
const handleChangeTagFilters = useCallback(
|
||||
(value: IBuilderQuery['filters']) => {
|
||||
handleChangeQueryData('filters', value);
|
||||
},
|
||||
[handleChangeQueryData],
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{selectedView === SELECTED_VIEWS.SEARCH && (
|
||||
<div className="qb-search-view-container">
|
||||
<QueryBuilderSearchV2
|
||||
query={query}
|
||||
onChange={handleChangeTagFilters}
|
||||
whereClauseConfig={filterConfigs?.filters}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{selectedView === SELECTED_VIEWS.QUERY_BUILDER && (
|
||||
<QueryBuilder
|
||||
panelType={panelTypes}
|
||||
config={{ initialDataSource: DataSource.LOGS, queryVariant: 'static' }}
|
||||
filterConfigs={filterConfigs}
|
||||
queryComponents={queryComponents}
|
||||
version="v3" // setting this to v3 as we this is rendered in logs explorer
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
<QueryBuilderV2
|
||||
isListViewPanel={panelTypes === PANEL_TYPES.LIST}
|
||||
config={{ initialDataSource: DataSource.LOGS, queryVariant: 'static' }}
|
||||
panelType={panelTypes}
|
||||
filterConfigs={filterConfigs}
|
||||
queryComponents={queryComponents}
|
||||
showOnlyWhereClause={selectedView === ExplorerViews.LIST}
|
||||
version="v3" // setting this to v3 as we this is rendered in logs explorer
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
@@ -80,15 +80,13 @@ import { v4 } from 'uuid';
|
||||
|
||||
import QueryStatus from './QueryStatus';
|
||||
|
||||
function LogsExplorerViews({
|
||||
function LogsExplorerViewsContainer({
|
||||
selectedView,
|
||||
showFrequencyChart,
|
||||
setIsLoadingQueries,
|
||||
listQueryKeyRef,
|
||||
chartQueryKeyRef,
|
||||
}: {
|
||||
selectedView: SELECTED_VIEWS;
|
||||
showFrequencyChart: boolean;
|
||||
selectedView: ExplorerViews;
|
||||
setIsLoadingQueries: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
listQueryKeyRef: MutableRefObject<any>;
|
||||
@@ -97,6 +95,7 @@ function LogsExplorerViews({
|
||||
}): JSX.Element {
|
||||
const { safeNavigate } = useSafeNavigate();
|
||||
const dispatch = useDispatch();
|
||||
const [showFrequencyChart, setShowFrequencyChart] = useState(true);
|
||||
|
||||
// this is to respect the panel type present in the URL rather than defaulting it to list always.
|
||||
const panelTypes = useGetPanelTypesQueryParam(PANEL_TYPES.LIST);
|
||||
@@ -139,6 +138,8 @@ function LogsExplorerViews({
|
||||
const [queryStats, setQueryStats] = useState<WsDataEvent>();
|
||||
const [listChartQuery, setListChartQuery] = useState<Query | null>(null);
|
||||
|
||||
const [orderDirection, setOrderDirection] = useState<string>('desc');
|
||||
|
||||
const listQuery = useMemo(() => {
|
||||
if (!stagedQuery || stagedQuery.builder.queryData.length < 1) return null;
|
||||
|
||||
@@ -240,15 +241,6 @@ function LogsExplorerViews({
|
||||
[currentQuery, selectedPanelType, updateAllQueriesOperators],
|
||||
);
|
||||
|
||||
const handleModeChange = (panelType: PANEL_TYPES): void => {
|
||||
if (selectedView === SELECTED_VIEWS.SEARCH) {
|
||||
handleSetConfig(panelType, DataSource.LOGS);
|
||||
}
|
||||
|
||||
setShowFormatMenuItems(false);
|
||||
handleExplorerTabChange(panelType);
|
||||
};
|
||||
|
||||
const {
|
||||
data: listChartData,
|
||||
isFetching: isFetchingListChartData,
|
||||
@@ -256,7 +248,8 @@ function LogsExplorerViews({
|
||||
} = useGetExplorerQueryRange(
|
||||
listChartQuery,
|
||||
PANEL_TYPES.TIME_SERIES,
|
||||
ENTITY_VERSION_V4,
|
||||
// ENTITY_VERSION_V4,
|
||||
ENTITY_VERSION_V5,
|
||||
{
|
||||
enabled: !!listChartQuery && panelType === PANEL_TYPES.LIST,
|
||||
},
|
||||
@@ -276,7 +269,8 @@ function LogsExplorerViews({
|
||||
} = useGetExplorerQueryRange(
|
||||
requestData,
|
||||
panelType,
|
||||
ENTITY_VERSION_V4,
|
||||
// ENTITY_VERSION_V4,
|
||||
ENTITY_VERSION_V5,
|
||||
{
|
||||
keepPreviousData: true,
|
||||
enabled: !isLimit && !!requestData,
|
||||
@@ -337,14 +331,26 @@ function LogsExplorerViews({
|
||||
};
|
||||
}
|
||||
|
||||
// Create orderBy array based on orderDirection
|
||||
const orderBy = [
|
||||
{ columnName: 'timestamp', order: orderDirection },
|
||||
{ columnName: 'id', order: orderDirection },
|
||||
];
|
||||
|
||||
const queryData: IBuilderQuery[] =
|
||||
query.builder.queryData.length > 1
|
||||
? query.builder.queryData
|
||||
? query.builder.queryData.map((item) => ({
|
||||
...item,
|
||||
...(selectedView !== ExplorerViews.LIST ? { order: [] } : {}),
|
||||
}))
|
||||
: [
|
||||
{
|
||||
...(listQuery || initialQueryBuilderFormValues),
|
||||
...paginateData,
|
||||
...(updatedFilters ? { filters: updatedFilters } : {}),
|
||||
...(selectedView === ExplorerViews.LIST
|
||||
? { order: orderBy, orderBy }
|
||||
: { order: [] }),
|
||||
},
|
||||
];
|
||||
|
||||
@@ -358,7 +364,7 @@ function LogsExplorerViews({
|
||||
|
||||
return data;
|
||||
},
|
||||
[listQuery, activeLogId],
|
||||
[activeLogId, orderDirection, listQuery, selectedView],
|
||||
);
|
||||
|
||||
const handleEndReached = useCallback(() => {
|
||||
@@ -460,8 +466,7 @@ function LogsExplorerViews({
|
||||
|
||||
useEffect(() => {
|
||||
const shouldChangeView =
|
||||
(isMultipleQueries || isGroupByExist) &&
|
||||
selectedView !== SELECTED_VIEWS.SEARCH;
|
||||
(isMultipleQueries || isGroupByExist) && selectedView !== ExplorerViews.LIST;
|
||||
|
||||
if (selectedPanelType === PANEL_TYPES.LIST && shouldChangeView) {
|
||||
handleExplorerTabChange(PANEL_TYPES.TIME_SERIES);
|
||||
@@ -481,11 +486,7 @@ function LogsExplorerViews({
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
selectedView &&
|
||||
selectedView === SELECTED_VIEWS.SEARCH &&
|
||||
handleSetConfig
|
||||
) {
|
||||
if (selectedView && selectedView === ExplorerViews.LIST && handleSetConfig) {
|
||||
handleSetConfig(defaultTo(panelTypes, PANEL_TYPES.LIST), DataSource.LOGS);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
@@ -506,10 +507,19 @@ function LogsExplorerViews({
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [data]);
|
||||
|
||||
// Store previous orderDirection to detect changes
|
||||
const prevOrderDirectionRef = useRef(orderDirection);
|
||||
|
||||
useEffect(() => {
|
||||
const orderDirectionChanged =
|
||||
prevOrderDirectionRef.current !== orderDirection &&
|
||||
selectedPanelType === PANEL_TYPES.LIST;
|
||||
prevOrderDirectionRef.current = orderDirection;
|
||||
|
||||
if (
|
||||
requestData?.id !== stagedQuery?.id ||
|
||||
currentMinTimeRef.current !== minTime
|
||||
currentMinTimeRef.current !== minTime ||
|
||||
orderDirectionChanged
|
||||
) {
|
||||
// Recalculate global time when query changes i.e. stage and run query clicked
|
||||
if (
|
||||
@@ -545,6 +555,8 @@ function LogsExplorerViews({
|
||||
dispatch,
|
||||
selectedTime,
|
||||
maxTime,
|
||||
orderDirection,
|
||||
selectedPanelType,
|
||||
]);
|
||||
|
||||
const chartData = useMemo(() => {
|
||||
@@ -651,111 +663,107 @@ function LogsExplorerViews({
|
||||
|
||||
return (
|
||||
<div className="logs-explorer-views-container">
|
||||
{showFrequencyChart && (
|
||||
<LogsExplorerChart
|
||||
className="logs-histogram"
|
||||
isLoading={isFetchingListChartData || isLoadingListChartData}
|
||||
data={chartData}
|
||||
isLogsExplorerViews={panelType === PANEL_TYPES.LIST}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="logs-explorer-views-types">
|
||||
<div className="views-tabs-container">
|
||||
<Button.Group className="views-tabs">
|
||||
<Button
|
||||
value={PANEL_TYPES.LIST}
|
||||
className={
|
||||
// eslint-disable-next-line sonarjs/no-duplicate-string
|
||||
selectedPanelType === PANEL_TYPES.LIST ? 'selected_view tab' : 'tab'
|
||||
}
|
||||
disabled={
|
||||
(isMultipleQueries || isGroupByExist) && selectedView !== 'search'
|
||||
}
|
||||
onClick={(): void => handleModeChange(PANEL_TYPES.LIST)}
|
||||
data-testid="logs-list-view"
|
||||
>
|
||||
List view
|
||||
</Button>
|
||||
<Button
|
||||
value={PANEL_TYPES.TIME_SERIES}
|
||||
className={
|
||||
// eslint-disable-next-line sonarjs/no-duplicate-string
|
||||
selectedPanelType === PANEL_TYPES.TIME_SERIES
|
||||
? 'selected_view tab'
|
||||
: 'tab'
|
||||
}
|
||||
onClick={(): void => handleModeChange(PANEL_TYPES.TIME_SERIES)}
|
||||
data-testid="time-series-view"
|
||||
>
|
||||
Time series
|
||||
</Button>
|
||||
<Button
|
||||
value={PANEL_TYPES.TABLE}
|
||||
className={
|
||||
// eslint-disable-next-line sonarjs/no-duplicate-string
|
||||
selectedPanelType === PANEL_TYPES.TABLE ? 'selected_view tab' : 'tab'
|
||||
}
|
||||
onClick={(): void => handleModeChange(PANEL_TYPES.TABLE)}
|
||||
data-testid="table-view"
|
||||
>
|
||||
Table
|
||||
</Button>
|
||||
</Button.Group>
|
||||
<div className="logs-actions-container">
|
||||
{selectedPanelType === PANEL_TYPES.LIST && (
|
||||
<div className="tab-options">
|
||||
<Download
|
||||
data={flattenLogData}
|
||||
isLoading={isFetching}
|
||||
fileName="log_data"
|
||||
/>
|
||||
<div className="format-options-container" ref={menuRef}>
|
||||
<Button
|
||||
className="periscope-btn"
|
||||
onClick={handleToggleShowFormatOptions}
|
||||
icon={<Sliders size={14} />}
|
||||
data-testid="periscope-btn"
|
||||
<div className="logs-actions-container">
|
||||
<div className="tab-options">
|
||||
<div className="tab-options-left">
|
||||
{selectedPanelType === PANEL_TYPES.LIST && (
|
||||
<div className="frequency-chart-view-controller">
|
||||
<Typography>Frequency chart</Typography>
|
||||
<Switch
|
||||
size="small"
|
||||
checked={showFrequencyChart}
|
||||
defaultChecked
|
||||
onChange={(): void => setShowFrequencyChart(!showFrequencyChart)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="tab-options-right">
|
||||
{selectedPanelType === PANEL_TYPES.LIST && (
|
||||
<>
|
||||
<div className="order-by-container">
|
||||
<div className="order-by-label">
|
||||
Order by <Minus size={14} /> <ArrowUp10 size={14} />
|
||||
</div>
|
||||
|
||||
<Select
|
||||
placeholder="Select order by"
|
||||
className="order-by-select"
|
||||
style={{ width: 100 }}
|
||||
value={orderDirection}
|
||||
onChange={(value): void => setOrderDirection(value)}
|
||||
options={[
|
||||
{ label: 'Ascending', value: 'asc' },
|
||||
{ label: 'Descending', value: 'desc' },
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
<Download
|
||||
data={flattenLogData}
|
||||
isLoading={isFetching}
|
||||
fileName="log_data"
|
||||
/>
|
||||
<div className="format-options-container" ref={menuRef}>
|
||||
<Button
|
||||
className="periscope-btn ghost"
|
||||
onClick={handleToggleShowFormatOptions}
|
||||
icon={<Sliders size={14} />}
|
||||
data-testid="periscope-btn"
|
||||
/>
|
||||
|
||||
{showFormatMenuItems && (
|
||||
<LogsFormatOptionsMenu
|
||||
title="FORMAT"
|
||||
items={formatItems}
|
||||
selectedOptionFormat={options.format}
|
||||
config={config}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{(selectedPanelType === PANEL_TYPES.TIME_SERIES ||
|
||||
selectedPanelType === PANEL_TYPES.TABLE) && (
|
||||
<div className="query-stats">
|
||||
<QueryStatus
|
||||
loading={isLoading || isFetching}
|
||||
error={isError}
|
||||
success={isSuccess}
|
||||
/>
|
||||
|
||||
{showFormatMenuItems && (
|
||||
<LogsFormatOptionsMenu
|
||||
title="FORMAT"
|
||||
items={formatItems}
|
||||
selectedOptionFormat={options.format}
|
||||
config={config}
|
||||
/>
|
||||
{queryStats?.read_rows && (
|
||||
<Typography.Text className="rows">
|
||||
{getYAxisFormattedValue(queryStats.read_rows?.toString(), 'short')}{' '}
|
||||
rows
|
||||
</Typography.Text>
|
||||
)}
|
||||
|
||||
{queryStats?.elapsed_ms && (
|
||||
<>
|
||||
<div className="divider" />
|
||||
<Typography.Text className="time">
|
||||
{getYAxisFormattedValue(queryStats?.elapsed_ms?.toString(), 'ms')}
|
||||
</Typography.Text>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{(selectedPanelType === PANEL_TYPES.TIME_SERIES ||
|
||||
selectedPanelType === PANEL_TYPES.TABLE) && (
|
||||
<div className="query-stats">
|
||||
<QueryStatus
|
||||
loading={isLoading || isFetching}
|
||||
error={isError}
|
||||
success={isSuccess}
|
||||
/>
|
||||
{queryStats?.read_rows && (
|
||||
<Typography.Text className="rows">
|
||||
{getYAxisFormattedValue(queryStats.read_rows?.toString(), 'short')}{' '}
|
||||
rows
|
||||
</Typography.Text>
|
||||
)}
|
||||
{queryStats?.elapsed_ms && (
|
||||
<>
|
||||
<div className="divider" />
|
||||
<Typography.Text className="time">
|
||||
{getYAxisFormattedValue(queryStats?.elapsed_ms?.toString(), 'ms')}
|
||||
</Typography.Text>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{selectedPanelType === PANEL_TYPES.LIST && showFrequencyChart && (
|
||||
<LogsExplorerChart
|
||||
className="logs-histogram"
|
||||
isLoading={isFetchingListChartData || isLoadingListChartData}
|
||||
data={chartData}
|
||||
isLogsExplorerViews={panelType === PANEL_TYPES.LIST}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="logs-explorer-views-type-content">
|
||||
{selectedPanelType === PANEL_TYPES.LIST && (
|
||||
<LogsExplorerList
|
||||
@@ -801,4 +809,4 @@ function LogsExplorerViews({
|
||||
);
|
||||
}
|
||||
|
||||
export default memo(LogsExplorerViews);
|
||||
export default memo(LogsExplorerViewsContainer);
|
||||
|
||||
@@ -4,7 +4,7 @@ import { useGetExplorerQueryRange } from 'hooks/queryBuilder/useGetExplorerQuery
|
||||
import { logsQueryRangeSuccessResponse } from 'mocks-server/__mockdata__/logs_query_range';
|
||||
import { server } from 'mocks-server/server';
|
||||
import { rest } from 'msw';
|
||||
import { SELECTED_VIEWS } from 'pages/LogsExplorer/utils';
|
||||
import { ExplorerViews } from 'pages/LogsExplorer/utils';
|
||||
import { PreferenceContextProvider } from 'providers/preferences/context/PreferenceContextProvider';
|
||||
import { QueryBuilderContext } from 'providers/QueryBuilder';
|
||||
import { VirtuosoMockContext } from 'react-virtuoso';
|
||||
@@ -127,8 +127,7 @@ const renderer = (): RenderResult =>
|
||||
>
|
||||
<PreferenceContextProvider>
|
||||
<LogsExplorerViews
|
||||
selectedView={SELECTED_VIEWS.SEARCH}
|
||||
showFrequencyChart
|
||||
selectedView={ExplorerViews.LIST}
|
||||
setIsLoadingQueries={(): void => {}}
|
||||
listQueryKeyRef={{ current: {} }}
|
||||
chartQueryKeyRef={{ current: {} }}
|
||||
@@ -212,8 +211,7 @@ describe('LogsExplorerViews -', () => {
|
||||
<QueryBuilderContext.Provider value={mockQueryBuilderContextValue}>
|
||||
<PreferenceContextProvider>
|
||||
<LogsExplorerViews
|
||||
selectedView={SELECTED_VIEWS.SEARCH}
|
||||
showFrequencyChart
|
||||
selectedView={ExplorerViews.LIST}
|
||||
setIsLoadingQueries={(): void => {}}
|
||||
listQueryKeyRef={{ current: {} }}
|
||||
chartQueryKeyRef={{ current: {} }}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -3,9 +3,11 @@ import './Explorer.styles.scss';
|
||||
import * as Sentry from '@sentry/react';
|
||||
import { Switch } from 'antd';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { QueryBuilderV2 } from 'components/QueryBuilderV2/QueryBuilderV2';
|
||||
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import ExplorerOptionWrapper from 'container/ExplorerOptions/ExplorerOptionWrapper';
|
||||
import RightToolbarActions from 'container/QueryBuilder/components/ToolbarActions/RightToolbarActions';
|
||||
import { QueryBuilderProps } from 'container/QueryBuilder/QueryBuilder.interfaces';
|
||||
import DateTimeSelector from 'container/TopNav/DateTimeSelectionV2';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { useShareBuilderUrl } from 'hooks/queryBuilder/useShareBuilderUrl';
|
||||
@@ -20,7 +22,7 @@ import { generateExportToDashboardLink } from 'utils/dashboard/generateExportToD
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
import { MetricsExplorerEventKeys, MetricsExplorerEvents } from '../events';
|
||||
import QuerySection from './QuerySection';
|
||||
// import QuerySection from './QuerySection';
|
||||
import TimeSeries from './TimeSeries';
|
||||
import { ExplorerTabs } from './types';
|
||||
import { splitQueryIntoOneChartPerQuery } from './utils';
|
||||
@@ -53,6 +55,16 @@ function Explorer(): JSX.Element {
|
||||
});
|
||||
};
|
||||
|
||||
const defaultQuery = useMemo(
|
||||
() =>
|
||||
updateAllQueriesOperators(
|
||||
initialQueriesMap[DataSource.METRICS],
|
||||
PANEL_TYPES.TIME_SERIES,
|
||||
DataSource.METRICS,
|
||||
),
|
||||
[updateAllQueriesOperators],
|
||||
);
|
||||
|
||||
const exportDefaultQuery = useMemo(
|
||||
() =>
|
||||
updateAllQueriesOperators(
|
||||
@@ -63,7 +75,7 @@ function Explorer(): JSX.Element {
|
||||
[currentQuery, updateAllQueriesOperators],
|
||||
);
|
||||
|
||||
useShareBuilderUrl(exportDefaultQuery);
|
||||
useShareBuilderUrl({ defaultValue: defaultQuery });
|
||||
|
||||
const handleExport = useCallback(
|
||||
(
|
||||
@@ -101,6 +113,11 @@ function Explorer(): JSX.Element {
|
||||
});
|
||||
}, []);
|
||||
|
||||
const queryComponents = useMemo(
|
||||
(): QueryBuilderProps['queryComponents'] => ({}),
|
||||
[],
|
||||
);
|
||||
|
||||
return (
|
||||
<Sentry.ErrorBoundary fallback={<ErrorBoundaryFallback />}>
|
||||
<div className="metrics-explorer-explore-container">
|
||||
@@ -115,10 +132,19 @@ function Explorer(): JSX.Element {
|
||||
</div>
|
||||
<div className="explore-header-right-actions">
|
||||
<DateTimeSelector showAutoRefresh />
|
||||
<RightToolbarActions onStageRunQuery={handleRunQuery} />
|
||||
<RightToolbarActions
|
||||
onStageRunQuery={(): void => handleRunQuery(true, true)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<QuerySection />
|
||||
{/* <QuerySection /> */}
|
||||
<QueryBuilderV2
|
||||
config={{ initialDataSource: DataSource.METRICS, queryVariant: 'static' }}
|
||||
panelType={PANEL_TYPES.TIME_SERIES}
|
||||
queryComponents={queryComponents}
|
||||
showFunctions={false}
|
||||
version="v3"
|
||||
/>
|
||||
{/* TODO: Enable once we have resolved all related metrics issues */}
|
||||
{/* <Button.Group className="explore-tabs">
|
||||
<Button
|
||||
|
||||
@@ -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,
|
||||
})),
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
import { Checkbox, Empty } from 'antd';
|
||||
import Spinner from 'components/Spinner';
|
||||
|
||||
type ExplorerAttributeColumnsProps = {
|
||||
isLoading: boolean;
|
||||
data: any;
|
||||
searchText: string;
|
||||
isAttributeKeySelected: (key: string) => boolean;
|
||||
handleCheckboxChange: (key: string) => void;
|
||||
};
|
||||
|
||||
function ExplorerAttributeColumns({
|
||||
isLoading,
|
||||
data,
|
||||
searchText,
|
||||
isAttributeKeySelected,
|
||||
handleCheckboxChange,
|
||||
}: ExplorerAttributeColumnsProps): JSX.Element {
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="attribute-columns">
|
||||
<Spinner size="large" tip="Loading..." height="2vh" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const filteredAttributeKeys =
|
||||
data?.payload?.attributeKeys?.filter((attributeKey: any) =>
|
||||
attributeKey.key.toLowerCase().includes(searchText.toLowerCase()),
|
||||
) || [];
|
||||
if (filteredAttributeKeys.length === 0) {
|
||||
return (
|
||||
<div className="attribute-columns">
|
||||
<Empty description="No columns found" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="attribute-columns">
|
||||
{filteredAttributeKeys.map((attributeKey: any) => (
|
||||
<Checkbox
|
||||
checked={isAttributeKeySelected(attributeKey.key)}
|
||||
onChange={(): void => handleCheckboxChange(attributeKey.key)}
|
||||
style={{ padding: 0 }}
|
||||
key={attributeKey.key}
|
||||
>
|
||||
{attributeKey.key}
|
||||
</Checkbox>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ExplorerAttributeColumns;
|
||||
@@ -3,21 +3,13 @@
|
||||
import './ExplorerColumnsRenderer.styles.scss';
|
||||
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import {
|
||||
Button,
|
||||
Checkbox,
|
||||
Divider,
|
||||
Dropdown,
|
||||
Input,
|
||||
Tooltip,
|
||||
Typography,
|
||||
} from 'antd';
|
||||
import { Button, Divider, Dropdown, Input, Tooltip, Typography } from 'antd';
|
||||
import { MenuProps } from 'antd/lib';
|
||||
import Spinner from 'components/Spinner';
|
||||
import { SOMETHING_WENT_WRONG } from 'constants/api';
|
||||
import { useGetAggregateKeys } from 'hooks/queryBuilder/useGetAggregateKeys';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import useDebouncedFn from 'hooks/useDebouncedFunction';
|
||||
import {
|
||||
AlertCircle,
|
||||
GripVertical,
|
||||
@@ -25,7 +17,7 @@ import {
|
||||
Search,
|
||||
Trash2,
|
||||
} from 'lucide-react';
|
||||
import { useState } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import {
|
||||
DragDropContext,
|
||||
Draggable,
|
||||
@@ -35,6 +27,7 @@ import {
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
import { WidgetGraphProps } from '../types';
|
||||
import ExplorerAttributeColumns from './ExplorerAttributeColumns';
|
||||
|
||||
type LogColumnsRendererProps = {
|
||||
setSelectedLogFields: WidgetGraphProps['setSelectedLogFields'];
|
||||
@@ -51,6 +44,7 @@ function ExplorerColumnsRenderer({
|
||||
}: LogColumnsRendererProps): JSX.Element {
|
||||
const { currentQuery } = useQueryBuilder();
|
||||
const [searchText, setSearchText] = useState<string>('');
|
||||
const [querySearchText, setQuerySearchText] = useState<string>('');
|
||||
const [open, setOpen] = useState<boolean>(false);
|
||||
|
||||
const initialDataSource = currentQuery.builder.queryData[0].dataSource;
|
||||
@@ -60,13 +54,14 @@ function ExplorerColumnsRenderer({
|
||||
aggregateAttribute: '',
|
||||
dataSource: currentQuery.builder.queryData[0].dataSource,
|
||||
aggregateOperator: currentQuery.builder.queryData[0].aggregateOperator,
|
||||
searchText: '',
|
||||
searchText: querySearchText,
|
||||
tagType: '',
|
||||
},
|
||||
{
|
||||
queryKey: [
|
||||
currentQuery.builder.queryData[0].dataSource,
|
||||
currentQuery.builder.queryData[0].aggregateOperator,
|
||||
querySearchText,
|
||||
],
|
||||
},
|
||||
);
|
||||
@@ -120,8 +115,20 @@ function ExplorerColumnsRenderer({
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
const debouncedSetQuerySearchText = useDebouncedFn((value) => {
|
||||
setQuerySearchText(value as string);
|
||||
}, 400);
|
||||
|
||||
useEffect(
|
||||
() => (): void => {
|
||||
debouncedSetQuerySearchText.cancel();
|
||||
},
|
||||
[debouncedSetQuerySearchText],
|
||||
);
|
||||
|
||||
const handleSearchChange = (e: React.ChangeEvent<HTMLInputElement>): void => {
|
||||
setSearchText(e.target.value);
|
||||
debouncedSetQuerySearchText(e.target.value);
|
||||
};
|
||||
|
||||
const items: MenuProps['items'] = [
|
||||
@@ -141,22 +148,13 @@ function ExplorerColumnsRenderer({
|
||||
{
|
||||
key: 'columns',
|
||||
label: (
|
||||
<div className="attribute-columns">
|
||||
{data?.payload?.attributeKeys
|
||||
?.filter((attributeKey) =>
|
||||
attributeKey.key.toLowerCase().includes(searchText.toLowerCase()),
|
||||
)
|
||||
?.map((attributeKey) => (
|
||||
<Checkbox
|
||||
checked={isAttributeKeySelected(attributeKey.key)}
|
||||
onChange={(): void => handleCheckboxChange(attributeKey.key)}
|
||||
style={{ padding: 0 }}
|
||||
key={attributeKey.key}
|
||||
>
|
||||
{attributeKey.key}
|
||||
</Checkbox>
|
||||
))}
|
||||
</div>
|
||||
<ExplorerAttributeColumns
|
||||
isLoading={isLoading}
|
||||
data={data}
|
||||
searchText={searchText}
|
||||
isAttributeKeySelected={isAttributeKeySelected}
|
||||
handleCheckboxChange={handleCheckboxChange}
|
||||
/>
|
||||
),
|
||||
},
|
||||
];
|
||||
@@ -220,17 +218,13 @@ function ExplorerColumnsRenderer({
|
||||
|
||||
const isDarkMode = useIsDarkMode();
|
||||
|
||||
if (isLoading) {
|
||||
return <Spinner size="large" tip="Loading..." height="4vh" />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="explorer-columns-renderer">
|
||||
<div className="title">
|
||||
<Typography.Text>Columns</Typography.Text>
|
||||
{isError && (
|
||||
<Tooltip title={SOMETHING_WENT_WRONG}>
|
||||
<AlertCircle size={16} />
|
||||
<AlertCircle size={16} data-testid="alert-circle-icon" />
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
@@ -265,6 +259,7 @@ function ExplorerColumnsRenderer({
|
||||
size={12}
|
||||
color="red"
|
||||
onClick={(): void => removeSelectedLogField(field.name)}
|
||||
data-testid="trash-icon"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
@@ -290,6 +285,7 @@ function ExplorerColumnsRenderer({
|
||||
size={12}
|
||||
color="red"
|
||||
onClick={(): void => removeSelectedLogField(field.key)}
|
||||
data-testid="trash-icon"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { Color } from '@signozhq/design-tokens';
|
||||
import { Button, Tabs, Typography } from 'antd';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import PromQLIcon from 'assets/Dashboard/PromQl';
|
||||
import { QueryBuilderV2 } from 'components/QueryBuilderV2/QueryBuilderV2';
|
||||
import TextToolTip from 'components/TextToolTip';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { QBShortcuts } from 'constants/shortcuts/QBShortcuts';
|
||||
@@ -11,7 +12,7 @@ import {
|
||||
getDefaultWidgetData,
|
||||
PANEL_TYPE_TO_QUERY_TYPES,
|
||||
} from 'container/NewWidget/utils';
|
||||
import { QueryBuilder } from 'container/QueryBuilder';
|
||||
// import { QueryBuilder } from 'container/QueryBuilder';
|
||||
import { QueryBuilderProps } from 'container/QueryBuilder/QueryBuilder.interfaces';
|
||||
import { useKeyboardHotkeys } from 'hooks/hotkeys/useKeyboardHotkeys';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
@@ -63,7 +64,7 @@ function QuerySection({
|
||||
|
||||
const { query } = selectedWidget;
|
||||
|
||||
useShareBuilderUrl(query);
|
||||
useShareBuilderUrl({ defaultValue: query });
|
||||
|
||||
const handleStageQuery = useCallback(
|
||||
(query: Query): void => {
|
||||
@@ -142,6 +143,11 @@ function QuerySection({
|
||||
return config;
|
||||
}, []);
|
||||
|
||||
const queryComponents = useMemo(
|
||||
(): QueryBuilderProps['queryComponents'] => ({}),
|
||||
[],
|
||||
);
|
||||
|
||||
const items = useMemo(() => {
|
||||
const supportedQueryTypes = PANEL_TYPE_TO_QUERY_TYPES[selectedGraph] || [];
|
||||
|
||||
@@ -150,12 +156,15 @@ function QuerySection({
|
||||
icon: <Atom size={14} />,
|
||||
label: 'Query Builder',
|
||||
component: (
|
||||
<QueryBuilder
|
||||
panelType={selectedGraph}
|
||||
filterConfigs={filterConfigs}
|
||||
version={selectedDashboard?.data?.version || 'v3'}
|
||||
isListViewPanel={selectedGraph === PANEL_TYPES.LIST}
|
||||
/>
|
||||
<div className="query-builder-v2-container">
|
||||
<QueryBuilderV2
|
||||
panelType={selectedGraph}
|
||||
filterConfigs={filterConfigs}
|
||||
version={selectedDashboard?.data?.version || 'v3'}
|
||||
isListViewPanel={selectedGraph === PANEL_TYPES.LIST}
|
||||
queryComponents={queryComponents}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
[EQueryType.CLICKHOUSE]: {
|
||||
@@ -186,6 +195,7 @@ function QuerySection({
|
||||
children: queryTypeComponents[queryType].component,
|
||||
}));
|
||||
}, [
|
||||
queryComponents,
|
||||
selectedGraph,
|
||||
filterConfigs,
|
||||
selectedDashboard?.data?.version,
|
||||
|
||||
@@ -5,6 +5,7 @@ import { WidgetGraphContainerProps } from 'container/NewWidget/types';
|
||||
import { getSortedSeriesData } from 'utils/getSortedSeriesData';
|
||||
|
||||
import { NotFoundContainer } from './styles';
|
||||
import { populateMultipleResults } from './util';
|
||||
import WidgetGraph from './WidgetGraphs';
|
||||
|
||||
function WidgetGraphContainer({
|
||||
@@ -22,6 +23,12 @@ function WidgetGraphContainer({
|
||||
queryResponse.data.payload.data.result = sortedSeriesData;
|
||||
}
|
||||
|
||||
if (queryResponse.data && selectedGraph === PANEL_TYPES.PIE) {
|
||||
const transformedData = populateMultipleResults(queryResponse?.data);
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
queryResponse.data = transformedData;
|
||||
}
|
||||
|
||||
if (selectedWidget === undefined) {
|
||||
return <Card>Invalid widget</Card>;
|
||||
}
|
||||
@@ -43,6 +50,7 @@ function WidgetGraphContainer({
|
||||
|
||||
if (
|
||||
selectedGraph !== PANEL_TYPES.LIST &&
|
||||
selectedGraph !== PANEL_TYPES.VALUE &&
|
||||
queryResponse.data?.payload.data?.result?.length === 0
|
||||
) {
|
||||
return (
|
||||
@@ -52,7 +60,7 @@ function WidgetGraphContainer({
|
||||
);
|
||||
}
|
||||
if (
|
||||
selectedGraph === PANEL_TYPES.LIST &&
|
||||
(selectedGraph === PANEL_TYPES.LIST || selectedGraph === PANEL_TYPES.VALUE) &&
|
||||
queryResponse.data?.payload?.data?.newResult?.data?.result?.length === 0
|
||||
) {
|
||||
return (
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
import { SuccessResponse } from 'types/api';
|
||||
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
|
||||
import { QueryData, QueryDataV3 } from 'types/api/widgets/getQuery';
|
||||
|
||||
export function populateMultipleResults(
|
||||
responseData: SuccessResponse<MetricRangePayloadProps, unknown>,
|
||||
): SuccessResponse<MetricRangePayloadProps, unknown> {
|
||||
const queryResults = responseData?.payload?.data?.newResult?.data?.result;
|
||||
const allFormattedResults: QueryData[] = [];
|
||||
|
||||
queryResults?.forEach((query: QueryDataV3) => {
|
||||
const { queryName, legend, table } = query;
|
||||
if (!table) return;
|
||||
|
||||
const { columns, rows } = table;
|
||||
|
||||
const valueCol = columns?.find((c) => c.isValueColumn);
|
||||
const labelCols = columns?.filter((c) => !c.isValueColumn);
|
||||
|
||||
rows?.forEach((row) => {
|
||||
const metric: Record<string, string> = {};
|
||||
labelCols?.forEach((col) => {
|
||||
metric[col.name] = String(row.data[col.name]);
|
||||
});
|
||||
|
||||
allFormattedResults.push({
|
||||
metric,
|
||||
values: [[0, String(row.data[valueCol!.name])]],
|
||||
queryName,
|
||||
legend: legend || '',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Create a copy instead of mutating the original
|
||||
const updatedResponseData: SuccessResponse<
|
||||
MetricRangePayloadProps,
|
||||
unknown
|
||||
> = {
|
||||
...responseData,
|
||||
payload: {
|
||||
...responseData.payload,
|
||||
data: {
|
||||
...responseData.payload.data,
|
||||
result: allFormattedResults,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
return updatedResponseData;
|
||||
}
|
||||
@@ -0,0 +1,394 @@
|
||||
/* eslint-disable sonarjs/no-duplicate-string */
|
||||
/* eslint-disable sonarjs/cognitive-complexity */
|
||||
/* eslint-disable react/jsx-props-no-spreading */
|
||||
import { render, screen, waitFor } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { useGetAggregateKeys } from 'hooks/queryBuilder/useGetAggregateKeys';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import React from 'react';
|
||||
import { DropResult } from 'react-beautiful-dnd';
|
||||
import {
|
||||
BaseAutocompleteData,
|
||||
DataTypes,
|
||||
} from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
import ExplorerColumnsRenderer from '../ExplorerColumnsRenderer';
|
||||
|
||||
// Mock hooks
|
||||
jest.mock('hooks/queryBuilder/useQueryBuilder');
|
||||
jest.mock('hooks/queryBuilder/useGetAggregateKeys');
|
||||
|
||||
// Mock react-beautiful-dnd
|
||||
let onDragEndMock: ((result: DropResult) => void) | undefined;
|
||||
|
||||
jest.mock('react-beautiful-dnd', () => ({
|
||||
DragDropContext: jest.fn(
|
||||
({
|
||||
children,
|
||||
onDragEnd,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
onDragEnd: (result: any) => void;
|
||||
}) => {
|
||||
onDragEndMock = onDragEnd;
|
||||
return children;
|
||||
},
|
||||
),
|
||||
Droppable: jest.fn(
|
||||
({ children }: { children: (provided: any) => React.ReactNode }) =>
|
||||
children({
|
||||
draggableProps: { style: {} },
|
||||
innerRef: jest.fn(),
|
||||
placeholder: null,
|
||||
}),
|
||||
),
|
||||
Draggable: jest.fn(
|
||||
({ children }: { children: (provided: any) => React.ReactNode }) =>
|
||||
children({
|
||||
draggableProps: { style: {} },
|
||||
innerRef: jest.fn(),
|
||||
dragHandleProps: {},
|
||||
}),
|
||||
),
|
||||
}));
|
||||
|
||||
describe('ExplorerColumnsRenderer', () => {
|
||||
const mockSetSelectedLogFields = jest.fn();
|
||||
const mockSetSelectedTracesFields = jest.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
// Reset mock implementations for useQueryBuilder and useGetAggregateKeys before each test
|
||||
// to ensure a clean state for each test case unless explicitly overridden.
|
||||
(useQueryBuilder as jest.Mock).mockReturnValue({
|
||||
currentQuery: {
|
||||
builder: {
|
||||
queryData: [
|
||||
{
|
||||
dataSource: DataSource.LOGS,
|
||||
aggregateOperator: 'count',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
(useGetAggregateKeys as jest.Mock).mockReturnValue({
|
||||
data: {
|
||||
payload: {
|
||||
attributeKeys: [
|
||||
{ key: 'attribute1', dataType: 'string', type: '' },
|
||||
{ key: 'attribute2', dataType: 'string', type: '' },
|
||||
{ key: 'another_attribute', dataType: 'string', type: '' },
|
||||
],
|
||||
},
|
||||
},
|
||||
isLoading: false,
|
||||
isError: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('renders correctly with default props and displays "Columns" title', () => {
|
||||
render(
|
||||
<ExplorerColumnsRenderer
|
||||
selectedLogFields={[]}
|
||||
setSelectedLogFields={mockSetSelectedLogFields}
|
||||
selectedTracesFields={[]}
|
||||
setSelectedTracesFields={mockSetSelectedTracesFields}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByText('Columns')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('add-columns-button')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('displays error message when data fetching fails', () => {
|
||||
(useGetAggregateKeys as jest.Mock).mockReturnValueOnce({
|
||||
data: undefined,
|
||||
isLoading: false,
|
||||
isError: true,
|
||||
});
|
||||
|
||||
render(
|
||||
<ExplorerColumnsRenderer
|
||||
selectedLogFields={[]}
|
||||
setSelectedLogFields={mockSetSelectedLogFields}
|
||||
selectedTracesFields={[]}
|
||||
setSelectedTracesFields={mockSetSelectedTracesFields}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByTestId('alert-circle-icon')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('opens and closes the dropdown', async () => {
|
||||
render(
|
||||
<ExplorerColumnsRenderer
|
||||
selectedLogFields={[]}
|
||||
setSelectedLogFields={mockSetSelectedLogFields}
|
||||
selectedTracesFields={[]}
|
||||
setSelectedTracesFields={mockSetSelectedTracesFields}
|
||||
/>,
|
||||
);
|
||||
|
||||
const addButton = screen.getByTestId('add-columns-button');
|
||||
await userEvent.click(addButton);
|
||||
|
||||
expect(screen.getByPlaceholderText('Search')).toBeInTheDocument();
|
||||
expect(screen.getByText('attribute1')).toBeInTheDocument();
|
||||
|
||||
await userEvent.click(addButton);
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByRole('menu')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('filters attribute keys based on search text', async () => {
|
||||
render(
|
||||
<ExplorerColumnsRenderer
|
||||
selectedLogFields={[]}
|
||||
setSelectedLogFields={mockSetSelectedLogFields}
|
||||
selectedTracesFields={[]}
|
||||
setSelectedTracesFields={mockSetSelectedTracesFields}
|
||||
/>,
|
||||
);
|
||||
|
||||
await userEvent.click(screen.getByTestId('add-columns-button'));
|
||||
|
||||
const searchInput = screen.getByPlaceholderText('Search');
|
||||
await userEvent.type(searchInput, 'another');
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText('attribute1')).not.toBeInTheDocument();
|
||||
expect(screen.getByText('another_attribute')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
await userEvent.clear(searchInput);
|
||||
await userEvent.type(searchInput, 'attribute');
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('attribute1')).toBeInTheDocument();
|
||||
expect(screen.getByText('attribute2')).toBeInTheDocument();
|
||||
expect(screen.getByText('another_attribute')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Log Data Source', () => {
|
||||
it('adds a log field when checkbox is checked', async () => {
|
||||
render(
|
||||
<ExplorerColumnsRenderer
|
||||
selectedLogFields={[]}
|
||||
setSelectedLogFields={mockSetSelectedLogFields}
|
||||
selectedTracesFields={[]}
|
||||
setSelectedTracesFields={mockSetSelectedTracesFields}
|
||||
/>,
|
||||
);
|
||||
|
||||
await userEvent.click(screen.getByTestId('add-columns-button'));
|
||||
const checkbox = screen.getByLabelText('attribute1');
|
||||
await userEvent.click(checkbox);
|
||||
|
||||
expect(mockSetSelectedLogFields).toHaveBeenCalledWith([
|
||||
{ dataType: 'string', name: 'attribute1', type: '' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('removes a log field when checkbox is unchecked', async () => {
|
||||
render(
|
||||
<ExplorerColumnsRenderer
|
||||
selectedLogFields={[{ dataType: 'string', name: 'attribute1', type: '' }]}
|
||||
setSelectedLogFields={mockSetSelectedLogFields}
|
||||
selectedTracesFields={[]}
|
||||
setSelectedTracesFields={mockSetSelectedTracesFields}
|
||||
/>,
|
||||
);
|
||||
|
||||
await userEvent.click(screen.getByTestId('add-columns-button'));
|
||||
const checkbox = screen.getByLabelText('attribute1');
|
||||
await userEvent.click(checkbox);
|
||||
|
||||
expect(mockSetSelectedLogFields).toHaveBeenCalledWith([]);
|
||||
});
|
||||
|
||||
it('removes a log field using the trash icon', async () => {
|
||||
render(
|
||||
<ExplorerColumnsRenderer
|
||||
selectedLogFields={[{ dataType: 'string', name: 'attribute1', type: '' }]}
|
||||
setSelectedLogFields={mockSetSelectedLogFields}
|
||||
selectedTracesFields={[]}
|
||||
setSelectedTracesFields={mockSetSelectedTracesFields}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByText('attribute1')).toBeInTheDocument();
|
||||
const trashIcon = screen.getByTestId('trash-icon');
|
||||
await userEvent.click(trashIcon);
|
||||
|
||||
expect(mockSetSelectedLogFields).toHaveBeenCalledWith([]);
|
||||
});
|
||||
|
||||
it('reorders log fields on drag and drop', () => {
|
||||
const initialSelectedFields = [
|
||||
{ dataType: 'string', name: 'field1', type: '' },
|
||||
{ dataType: 'string', name: 'field2', type: '' },
|
||||
];
|
||||
|
||||
render(
|
||||
<ExplorerColumnsRenderer
|
||||
selectedLogFields={initialSelectedFields}
|
||||
setSelectedLogFields={mockSetSelectedLogFields}
|
||||
selectedTracesFields={[]}
|
||||
setSelectedTracesFields={mockSetSelectedTracesFields}
|
||||
/>,
|
||||
);
|
||||
|
||||
const field1Element = screen.getByText('field1');
|
||||
const dragDropContext = field1Element.closest('.explorer-columns');
|
||||
|
||||
if (dragDropContext && onDragEndMock) {
|
||||
// Simulate onDragEnd directly
|
||||
onDragEndMock({
|
||||
source: { index: 0, droppableId: 'drag-drop-list' },
|
||||
destination: { index: 1, droppableId: 'drag-drop-list' },
|
||||
draggableId: '0',
|
||||
type: 'DEFAULT',
|
||||
reason: 'DROP',
|
||||
combine: undefined,
|
||||
mode: 'FLUID',
|
||||
});
|
||||
|
||||
expect(mockSetSelectedLogFields).toHaveBeenCalledWith([
|
||||
{ dataType: 'string', name: 'field2', type: '' },
|
||||
{ dataType: 'string', name: 'field1', type: '' },
|
||||
]);
|
||||
} else {
|
||||
fail('DragDropContext or onDragEndMock not found');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('Trace Data Source', () => {
|
||||
beforeEach(() => {
|
||||
(useQueryBuilder as jest.Mock).mockReturnValue({
|
||||
currentQuery: {
|
||||
builder: {
|
||||
queryData: [
|
||||
{
|
||||
dataSource: DataSource.TRACES,
|
||||
aggregateOperator: 'count',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
(useGetAggregateKeys as jest.Mock).mockReturnValue({
|
||||
data: {
|
||||
payload: {
|
||||
attributeKeys: [
|
||||
{ key: 'trace_attribute1', dataType: 'string', type: 'tag' },
|
||||
{ key: 'trace_attribute2', dataType: 'string', type: 'tag' },
|
||||
],
|
||||
},
|
||||
},
|
||||
isLoading: false,
|
||||
isError: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('adds a trace field when checkbox is checked', async () => {
|
||||
render(
|
||||
<ExplorerColumnsRenderer
|
||||
selectedLogFields={[]}
|
||||
setSelectedLogFields={mockSetSelectedLogFields}
|
||||
selectedTracesFields={[]}
|
||||
setSelectedTracesFields={mockSetSelectedTracesFields}
|
||||
/>,
|
||||
);
|
||||
|
||||
await userEvent.click(screen.getByTestId('add-columns-button'));
|
||||
const checkbox = screen.getByLabelText('trace_attribute1');
|
||||
await userEvent.click(checkbox);
|
||||
|
||||
expect(mockSetSelectedTracesFields).toHaveBeenCalledWith([
|
||||
{ key: 'trace_attribute1', dataType: 'string', type: 'tag' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('removes a trace field when checkbox is unchecked', async () => {
|
||||
render(
|
||||
<ExplorerColumnsRenderer
|
||||
selectedLogFields={[]}
|
||||
setSelectedLogFields={mockSetSelectedLogFields}
|
||||
selectedTracesFields={[
|
||||
{ key: 'trace_attribute1', dataType: DataTypes.String, type: 'tag' },
|
||||
]}
|
||||
setSelectedTracesFields={mockSetSelectedTracesFields}
|
||||
/>,
|
||||
);
|
||||
|
||||
await userEvent.click(screen.getByTestId('add-columns-button'));
|
||||
const checkbox = screen.getByLabelText('trace_attribute1');
|
||||
await userEvent.click(checkbox);
|
||||
|
||||
expect(mockSetSelectedTracesFields).toHaveBeenCalledWith([]);
|
||||
});
|
||||
|
||||
it('removes a trace field using the trash icon', async () => {
|
||||
render(
|
||||
<ExplorerColumnsRenderer
|
||||
selectedLogFields={[]}
|
||||
setSelectedLogFields={mockSetSelectedLogFields}
|
||||
selectedTracesFields={[
|
||||
{ key: 'trace_attribute1', dataType: DataTypes.String, type: 'tag' },
|
||||
]}
|
||||
setSelectedTracesFields={mockSetSelectedTracesFields}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByText('trace_attribute1')).toBeInTheDocument();
|
||||
const trashIcon = screen.getByTestId('trash-icon');
|
||||
await userEvent.click(trashIcon);
|
||||
|
||||
expect(mockSetSelectedTracesFields).toHaveBeenCalledWith([]);
|
||||
});
|
||||
|
||||
it('reorders trace fields on drag and drop', () => {
|
||||
const initialSelectedFields = [
|
||||
{ key: 'trace_field1', dataType: 'string', type: 'tag' },
|
||||
{ key: 'trace_field2', dataType: 'string', type: 'tag' },
|
||||
];
|
||||
|
||||
render(
|
||||
<ExplorerColumnsRenderer
|
||||
selectedLogFields={[]}
|
||||
setSelectedLogFields={mockSetSelectedLogFields}
|
||||
selectedTracesFields={initialSelectedFields as BaseAutocompleteData[]}
|
||||
setSelectedTracesFields={mockSetSelectedTracesFields}
|
||||
/>,
|
||||
);
|
||||
|
||||
const traceField1Element = screen.getByText('trace_field1');
|
||||
const dragDropContext = traceField1Element.closest('.explorer-columns');
|
||||
if (dragDropContext && onDragEndMock) {
|
||||
// Simulate onDragEnd directly
|
||||
onDragEndMock({
|
||||
source: { index: 0, droppableId: 'drag-drop-list' },
|
||||
destination: { index: 1, droppableId: 'drag-drop-list' },
|
||||
draggableId: '0',
|
||||
type: 'DEFAULT',
|
||||
reason: 'DROP',
|
||||
combine: undefined,
|
||||
mode: 'FLUID',
|
||||
});
|
||||
|
||||
expect(mockSetSelectedTracesFields).toHaveBeenCalledWith([
|
||||
{ key: 'trace_field2', dataType: 'string', type: 'tag' },
|
||||
{ key: 'trace_field1', dataType: 'string', type: 'tag' },
|
||||
]);
|
||||
} else {
|
||||
fail('DragDropContext or onDragEndMock not found');
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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,
|
||||
|
||||
@@ -421,6 +421,7 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
|
||||
formatForWeb:
|
||||
getGraphTypeForFormat(selectedGraph || selectedWidget.panelTypes) ===
|
||||
PANEL_TYPES.TABLE,
|
||||
originalGraphType: selectedGraph || selectedWidget.panelTypes,
|
||||
}));
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
@@ -580,7 +581,11 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
|
||||
|
||||
const setGraphHandler = (type: PANEL_TYPES): void => {
|
||||
setIsLoadingPanelData(true);
|
||||
const updatedQuery = handleQueryChange(type as any, supersetQuery);
|
||||
const updatedQuery = handleQueryChange(
|
||||
type as any,
|
||||
supersetQuery,
|
||||
selectedGraph,
|
||||
);
|
||||
setGraphType(type);
|
||||
redirectWithQueryBuilderData(
|
||||
updatedQuery,
|
||||
|
||||
@@ -56,9 +56,11 @@ export const panelTypeDataSourceFormValuesMap: Record<
|
||||
'aggregateAttribute',
|
||||
'aggregateOperator',
|
||||
'filters',
|
||||
'filter',
|
||||
'groupBy',
|
||||
'limit',
|
||||
'having',
|
||||
'havingExpression',
|
||||
'orderBy',
|
||||
'functions',
|
||||
'stepInterval',
|
||||
@@ -66,6 +68,7 @@ export const panelTypeDataSourceFormValuesMap: Record<
|
||||
'queryName',
|
||||
'legend',
|
||||
'expression',
|
||||
'aggregations',
|
||||
],
|
||||
},
|
||||
},
|
||||
@@ -76,10 +79,12 @@ export const panelTypeDataSourceFormValuesMap: Record<
|
||||
'aggregateOperator',
|
||||
'timeAggregation',
|
||||
'filters',
|
||||
'filter',
|
||||
'spaceAggregation',
|
||||
'groupBy',
|
||||
'limit',
|
||||
'having',
|
||||
'havingExpression',
|
||||
'orderBy',
|
||||
'stepInterval',
|
||||
'legend',
|
||||
@@ -87,6 +92,7 @@ export const panelTypeDataSourceFormValuesMap: Record<
|
||||
'disabled',
|
||||
'functions',
|
||||
'expression',
|
||||
'aggregations',
|
||||
],
|
||||
},
|
||||
},
|
||||
@@ -96,9 +102,11 @@ export const panelTypeDataSourceFormValuesMap: Record<
|
||||
'aggregateAttribute',
|
||||
'aggregateOperator',
|
||||
'filters',
|
||||
'filter',
|
||||
'groupBy',
|
||||
'limit',
|
||||
'having',
|
||||
'havingExpression',
|
||||
'orderBy',
|
||||
'functions',
|
||||
'stepInterval',
|
||||
@@ -106,6 +114,7 @@ export const panelTypeDataSourceFormValuesMap: Record<
|
||||
'queryName',
|
||||
'legend',
|
||||
'expression',
|
||||
'aggregations',
|
||||
],
|
||||
},
|
||||
},
|
||||
@@ -117,9 +126,11 @@ export const panelTypeDataSourceFormValuesMap: Record<
|
||||
'aggregateAttribute',
|
||||
'aggregateOperator',
|
||||
'filters',
|
||||
'filter',
|
||||
'groupBy',
|
||||
'limit',
|
||||
'having',
|
||||
'havingExpression',
|
||||
'orderBy',
|
||||
'functions',
|
||||
'stepInterval',
|
||||
@@ -127,6 +138,7 @@ export const panelTypeDataSourceFormValuesMap: Record<
|
||||
'queryName',
|
||||
'legend',
|
||||
'expression',
|
||||
'aggregations',
|
||||
],
|
||||
},
|
||||
},
|
||||
@@ -137,10 +149,12 @@ export const panelTypeDataSourceFormValuesMap: Record<
|
||||
'aggregateOperator',
|
||||
'timeAggregation',
|
||||
'filters',
|
||||
'filter',
|
||||
'spaceAggregation',
|
||||
'groupBy',
|
||||
'limit',
|
||||
'having',
|
||||
'havingExpression',
|
||||
'orderBy',
|
||||
'stepInterval',
|
||||
'legend',
|
||||
@@ -148,6 +162,7 @@ export const panelTypeDataSourceFormValuesMap: Record<
|
||||
'disabled',
|
||||
'functions',
|
||||
'expression',
|
||||
'aggregations',
|
||||
],
|
||||
},
|
||||
},
|
||||
@@ -157,9 +172,11 @@ export const panelTypeDataSourceFormValuesMap: Record<
|
||||
'aggregateAttribute',
|
||||
'aggregateOperator',
|
||||
'filters',
|
||||
'filter',
|
||||
'groupBy',
|
||||
'limit',
|
||||
'having',
|
||||
'havingExpression',
|
||||
'orderBy',
|
||||
'functions',
|
||||
'stepInterval',
|
||||
@@ -167,6 +184,7 @@ export const panelTypeDataSourceFormValuesMap: Record<
|
||||
'queryName',
|
||||
'legend',
|
||||
'expression',
|
||||
'aggregations',
|
||||
],
|
||||
},
|
||||
},
|
||||
@@ -178,9 +196,11 @@ export const panelTypeDataSourceFormValuesMap: Record<
|
||||
'aggregateAttribute',
|
||||
'aggregateOperator',
|
||||
'filters',
|
||||
'filter',
|
||||
'groupBy',
|
||||
'limit',
|
||||
'having',
|
||||
'havingExpression',
|
||||
'orderBy',
|
||||
'functions',
|
||||
'stepInterval',
|
||||
@@ -188,6 +208,7 @@ export const panelTypeDataSourceFormValuesMap: Record<
|
||||
'queryName',
|
||||
'legend',
|
||||
'expression',
|
||||
'aggregations',
|
||||
],
|
||||
},
|
||||
},
|
||||
@@ -198,10 +219,12 @@ export const panelTypeDataSourceFormValuesMap: Record<
|
||||
'aggregateOperator',
|
||||
'timeAggregation',
|
||||
'filters',
|
||||
'filter',
|
||||
'spaceAggregation',
|
||||
'groupBy',
|
||||
'limit',
|
||||
'having',
|
||||
'havingExpression',
|
||||
'orderBy',
|
||||
'stepInterval',
|
||||
'legend',
|
||||
@@ -209,6 +232,7 @@ export const panelTypeDataSourceFormValuesMap: Record<
|
||||
'disabled',
|
||||
'functions',
|
||||
'expression',
|
||||
'aggregations',
|
||||
],
|
||||
},
|
||||
},
|
||||
@@ -218,9 +242,11 @@ export const panelTypeDataSourceFormValuesMap: Record<
|
||||
'aggregateAttribute',
|
||||
'aggregateOperator',
|
||||
'filters',
|
||||
'filter',
|
||||
'groupBy',
|
||||
'limit',
|
||||
'having',
|
||||
'havingExpression',
|
||||
'orderBy',
|
||||
'functions',
|
||||
'stepInterval',
|
||||
@@ -228,6 +254,7 @@ export const panelTypeDataSourceFormValuesMap: Record<
|
||||
'queryName',
|
||||
'legend',
|
||||
'expression',
|
||||
'aggregations',
|
||||
],
|
||||
},
|
||||
},
|
||||
@@ -239,9 +266,11 @@ export const panelTypeDataSourceFormValuesMap: Record<
|
||||
'aggregateAttribute',
|
||||
'aggregateOperator',
|
||||
'filters',
|
||||
'filter',
|
||||
'groupBy',
|
||||
'limit',
|
||||
'having',
|
||||
'havingExpression',
|
||||
'orderBy',
|
||||
'functions',
|
||||
'stepInterval',
|
||||
@@ -249,6 +278,7 @@ export const panelTypeDataSourceFormValuesMap: Record<
|
||||
'queryName',
|
||||
'expression',
|
||||
'legend',
|
||||
'aggregations',
|
||||
],
|
||||
},
|
||||
},
|
||||
@@ -259,11 +289,13 @@ export const panelTypeDataSourceFormValuesMap: Record<
|
||||
'aggregateOperator',
|
||||
'timeAggregation',
|
||||
'filters',
|
||||
'filter',
|
||||
'spaceAggregation',
|
||||
'groupBy',
|
||||
'reduceTo',
|
||||
'limit',
|
||||
'having',
|
||||
'havingExpression',
|
||||
'orderBy',
|
||||
'stepInterval',
|
||||
'legend',
|
||||
@@ -271,6 +303,7 @@ export const panelTypeDataSourceFormValuesMap: Record<
|
||||
'expression',
|
||||
'disabled',
|
||||
'functions',
|
||||
'aggregations',
|
||||
],
|
||||
},
|
||||
},
|
||||
@@ -280,9 +313,11 @@ export const panelTypeDataSourceFormValuesMap: Record<
|
||||
'aggregateAttribute',
|
||||
'aggregateOperator',
|
||||
'filters',
|
||||
'filter',
|
||||
'groupBy',
|
||||
'limit',
|
||||
'having',
|
||||
'havingExpression',
|
||||
'orderBy',
|
||||
'functions',
|
||||
'stepInterval',
|
||||
@@ -290,6 +325,7 @@ export const panelTypeDataSourceFormValuesMap: Record<
|
||||
'queryName',
|
||||
'expression',
|
||||
'legend',
|
||||
'aggregations',
|
||||
],
|
||||
},
|
||||
},
|
||||
@@ -301,9 +337,11 @@ export const panelTypeDataSourceFormValuesMap: Record<
|
||||
'aggregateAttribute',
|
||||
'aggregateOperator',
|
||||
'filters',
|
||||
'filter',
|
||||
'groupBy',
|
||||
'limit',
|
||||
'having',
|
||||
'havingExpression',
|
||||
'orderBy',
|
||||
'functions',
|
||||
'stepInterval',
|
||||
@@ -311,6 +349,7 @@ export const panelTypeDataSourceFormValuesMap: Record<
|
||||
'queryName',
|
||||
'expression',
|
||||
'legend',
|
||||
'aggregations',
|
||||
],
|
||||
},
|
||||
},
|
||||
@@ -321,11 +360,13 @@ export const panelTypeDataSourceFormValuesMap: Record<
|
||||
'aggregateOperator',
|
||||
'timeAggregation',
|
||||
'filters',
|
||||
'filter',
|
||||
'spaceAggregation',
|
||||
'groupBy',
|
||||
'reduceTo',
|
||||
'limit',
|
||||
'having',
|
||||
'havingExpression',
|
||||
'orderBy',
|
||||
'stepInterval',
|
||||
'legend',
|
||||
@@ -333,6 +374,7 @@ export const panelTypeDataSourceFormValuesMap: Record<
|
||||
'expression',
|
||||
'disabled',
|
||||
'functions',
|
||||
'aggregations',
|
||||
],
|
||||
},
|
||||
},
|
||||
@@ -342,9 +384,11 @@ export const panelTypeDataSourceFormValuesMap: Record<
|
||||
'aggregateAttribute',
|
||||
'aggregateOperator',
|
||||
'filters',
|
||||
'filter',
|
||||
'groupBy',
|
||||
'limit',
|
||||
'having',
|
||||
'havingExpression',
|
||||
'orderBy',
|
||||
'functions',
|
||||
'stepInterval',
|
||||
@@ -352,6 +396,7 @@ export const panelTypeDataSourceFormValuesMap: Record<
|
||||
'queryName',
|
||||
'expression',
|
||||
'legend',
|
||||
'aggregations',
|
||||
],
|
||||
},
|
||||
},
|
||||
@@ -359,17 +404,31 @@ export const panelTypeDataSourceFormValuesMap: Record<
|
||||
[PANEL_TYPES.LIST]: {
|
||||
[DataSource.LOGS]: {
|
||||
builder: {
|
||||
queryData: ['filters', 'limit', 'orderBy', 'functions'],
|
||||
queryData: [
|
||||
'filters',
|
||||
'filter',
|
||||
'limit',
|
||||
'orderBy',
|
||||
'functions',
|
||||
'aggregations',
|
||||
],
|
||||
},
|
||||
},
|
||||
[DataSource.METRICS]: {
|
||||
builder: {
|
||||
queryData: [],
|
||||
queryData: ['filters', 'filter', 'aggregations'],
|
||||
},
|
||||
},
|
||||
[DataSource.TRACES]: {
|
||||
builder: {
|
||||
queryData: ['filters', 'limit', 'orderBy', 'functions'],
|
||||
queryData: [
|
||||
'filters',
|
||||
'filter',
|
||||
'limit',
|
||||
'orderBy',
|
||||
'functions',
|
||||
'aggregations',
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -380,14 +439,17 @@ export const panelTypeDataSourceFormValuesMap: Record<
|
||||
'aggregateAttribute',
|
||||
'aggregateOperator',
|
||||
'filters',
|
||||
'filter',
|
||||
'reduceTo',
|
||||
'having',
|
||||
'havingExpression',
|
||||
'functions',
|
||||
'stepInterval',
|
||||
'queryName',
|
||||
'expression',
|
||||
'disabled',
|
||||
'legend',
|
||||
'aggregations',
|
||||
],
|
||||
},
|
||||
},
|
||||
@@ -398,8 +460,10 @@ export const panelTypeDataSourceFormValuesMap: Record<
|
||||
'aggregateOperator',
|
||||
'timeAggregation',
|
||||
'filters',
|
||||
'filter',
|
||||
'spaceAggregation',
|
||||
'having',
|
||||
'havingExpression',
|
||||
'reduceTo',
|
||||
'stepInterval',
|
||||
'legend',
|
||||
@@ -407,6 +471,7 @@ export const panelTypeDataSourceFormValuesMap: Record<
|
||||
'expression',
|
||||
'disabled',
|
||||
'functions',
|
||||
'aggregations',
|
||||
],
|
||||
},
|
||||
},
|
||||
@@ -416,14 +481,17 @@ export const panelTypeDataSourceFormValuesMap: Record<
|
||||
'aggregateAttribute',
|
||||
'aggregateOperator',
|
||||
'filters',
|
||||
'filter',
|
||||
'reduceTo',
|
||||
'having',
|
||||
'havingExpression',
|
||||
'functions',
|
||||
'stepInterval',
|
||||
'queryName',
|
||||
'expression',
|
||||
'disabled',
|
||||
'legend',
|
||||
'aggregations',
|
||||
],
|
||||
},
|
||||
},
|
||||
@@ -433,7 +501,9 @@ export const panelTypeDataSourceFormValuesMap: Record<
|
||||
export function handleQueryChange(
|
||||
newPanelType: keyof PartialPanelTypes,
|
||||
supersetQuery: Query,
|
||||
currentPanelType: PANEL_TYPES,
|
||||
): Query {
|
||||
console.log('supersetQuery', supersetQuery);
|
||||
return {
|
||||
...supersetQuery,
|
||||
builder: {
|
||||
@@ -454,6 +524,7 @@ export function handleQueryChange(
|
||||
set(tempQuery, 'aggregateOperator', 'noop');
|
||||
set(tempQuery, 'offset', 0);
|
||||
set(tempQuery, 'pageSize', 10);
|
||||
set(tempQuery, 'orderBy', undefined);
|
||||
} else if (tempQuery.aggregateOperator === 'noop') {
|
||||
// this condition takes care of the part where we start with the list panel type and then shift to other panels
|
||||
// because in other cases we never set list operator and other fields in superset query rather just update in the current / staged query
|
||||
@@ -462,6 +533,13 @@ export function handleQueryChange(
|
||||
unset(tempQuery, 'pageSize');
|
||||
}
|
||||
|
||||
if (
|
||||
currentPanelType === PANEL_TYPES.LIST &&
|
||||
newPanelType !== PANEL_TYPES.LIST
|
||||
) {
|
||||
set(tempQuery, 'orderBy', undefined);
|
||||
}
|
||||
|
||||
return tempQuery;
|
||||
}),
|
||||
},
|
||||
|
||||
@@ -3,15 +3,15 @@ import '../OnboardingQuestionaire.styles.scss';
|
||||
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { Button, Input, Typography } from 'antd';
|
||||
import TextArea from 'antd/lib/input/TextArea';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { ArrowLeft, ArrowRight, CheckCircle } from 'lucide-react';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
export interface SignozDetails {
|
||||
hearAboutSignoz: string | null;
|
||||
interestInSignoz: string | null;
|
||||
otherInterestInSignoz: string | null;
|
||||
otherAboutSignoz: string | null;
|
||||
discoverSignoz: string | null;
|
||||
}
|
||||
|
||||
interface AboutSigNozQuestionsProps {
|
||||
@@ -21,15 +21,6 @@ interface AboutSigNozQuestionsProps {
|
||||
onBack: () => void;
|
||||
}
|
||||
|
||||
const hearAboutSignozOptions: Record<string, string> = {
|
||||
search: 'Google / Search',
|
||||
hackerNews: 'Hacker News',
|
||||
linkedin: 'LinkedIn',
|
||||
twitter: 'Twitter',
|
||||
reddit: 'Reddit',
|
||||
colleaguesFriends: 'Colleagues / Friends',
|
||||
};
|
||||
|
||||
const interestedInOptions: Record<string, string> = {
|
||||
savingCosts: 'Saving costs',
|
||||
otelNativeStack: 'Interested in Otel-native stack',
|
||||
@@ -42,24 +33,20 @@ export function AboutSigNozQuestions({
|
||||
onNext,
|
||||
onBack,
|
||||
}: AboutSigNozQuestionsProps): JSX.Element {
|
||||
const [hearAboutSignoz, setHearAboutSignoz] = useState<string | null>(
|
||||
signozDetails?.hearAboutSignoz || null,
|
||||
);
|
||||
const [otherAboutSignoz, setOtherAboutSignoz] = useState<string>(
|
||||
signozDetails?.otherAboutSignoz || '',
|
||||
);
|
||||
const [interestInSignoz, setInterestInSignoz] = useState<string | null>(
|
||||
signozDetails?.interestInSignoz || null,
|
||||
);
|
||||
const [otherInterestInSignoz, setOtherInterestInSignoz] = useState<string>(
|
||||
signozDetails?.otherInterestInSignoz || '',
|
||||
);
|
||||
const [discoverSignoz, setDiscoverSignoz] = useState<string>(
|
||||
signozDetails?.discoverSignoz || '',
|
||||
);
|
||||
const [isNextDisabled, setIsNextDisabled] = useState<boolean>(true);
|
||||
|
||||
useEffect((): void => {
|
||||
if (
|
||||
hearAboutSignoz !== null &&
|
||||
(hearAboutSignoz !== 'Others' || otherAboutSignoz !== '') &&
|
||||
discoverSignoz !== '' &&
|
||||
interestInSignoz !== null &&
|
||||
(interestInSignoz !== 'Others' || otherInterestInSignoz !== '')
|
||||
) {
|
||||
@@ -67,24 +54,17 @@ export function AboutSigNozQuestions({
|
||||
} else {
|
||||
setIsNextDisabled(true);
|
||||
}
|
||||
}, [
|
||||
hearAboutSignoz,
|
||||
otherAboutSignoz,
|
||||
interestInSignoz,
|
||||
otherInterestInSignoz,
|
||||
]);
|
||||
}, [interestInSignoz, otherInterestInSignoz, discoverSignoz]);
|
||||
|
||||
const handleOnNext = (): void => {
|
||||
setSignozDetails({
|
||||
hearAboutSignoz,
|
||||
otherAboutSignoz,
|
||||
discoverSignoz,
|
||||
interestInSignoz,
|
||||
otherInterestInSignoz,
|
||||
});
|
||||
|
||||
logEvent('Org Onboarding: Answered', {
|
||||
hearAboutSignoz,
|
||||
otherAboutSignoz,
|
||||
discoverSignoz,
|
||||
interestInSignoz,
|
||||
otherInterestInSignoz,
|
||||
});
|
||||
@@ -94,8 +74,7 @@ export function AboutSigNozQuestions({
|
||||
|
||||
const handleOnBack = (): void => {
|
||||
setSignozDetails({
|
||||
hearAboutSignoz,
|
||||
otherAboutSignoz,
|
||||
discoverSignoz,
|
||||
interestInSignoz,
|
||||
otherInterestInSignoz,
|
||||
});
|
||||
@@ -115,52 +94,16 @@ export function AboutSigNozQuestions({
|
||||
<div className="questions-form-container">
|
||||
<div className="questions-form">
|
||||
<div className="form-group">
|
||||
<div className="question">Where did you hear about SigNoz?</div>
|
||||
<div className="two-column-grid">
|
||||
{Object.keys(hearAboutSignozOptions).map((option: string) => (
|
||||
<Button
|
||||
key={option}
|
||||
type="primary"
|
||||
className={`onboarding-questionaire-button ${
|
||||
hearAboutSignoz === option ? 'active' : ''
|
||||
}`}
|
||||
onClick={(): void => setHearAboutSignoz(option)}
|
||||
>
|
||||
{hearAboutSignozOptions[option]}
|
||||
{hearAboutSignoz === option && (
|
||||
<CheckCircle size={12} color={Color.BG_FOREST_500} />
|
||||
)}
|
||||
</Button>
|
||||
))}
|
||||
<div className="question">How did you first come across SigNoz?</div>
|
||||
|
||||
{hearAboutSignoz === 'Others' ? (
|
||||
<Input
|
||||
type="text"
|
||||
className="onboarding-questionaire-other-input"
|
||||
placeholder="How you got to know about us"
|
||||
value={otherAboutSignoz}
|
||||
autoFocus
|
||||
addonAfter={
|
||||
otherAboutSignoz !== '' ? (
|
||||
<CheckCircle size={12} color={Color.BG_FOREST_500} />
|
||||
) : (
|
||||
''
|
||||
)
|
||||
}
|
||||
onChange={(e): void => setOtherAboutSignoz(e.target.value)}
|
||||
/>
|
||||
) : (
|
||||
<Button
|
||||
type="primary"
|
||||
className={`onboarding-questionaire-button ${
|
||||
hearAboutSignoz === 'Others' ? 'active' : ''
|
||||
}`}
|
||||
onClick={(): void => setHearAboutSignoz('Others')}
|
||||
>
|
||||
Others
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<TextArea
|
||||
className="discover-signoz-input"
|
||||
placeholder="e.g., Google Search, Hacker News, Reddit, a friend, ChatGPT, a blog post, a conference, etc."
|
||||
value={discoverSignoz}
|
||||
autoFocus
|
||||
rows={4}
|
||||
onChange={(e): void => setDiscoverSignoz(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
|
||||
@@ -84,6 +84,23 @@
|
||||
}
|
||||
}
|
||||
|
||||
.discover-signoz-input {
|
||||
width: 100%;
|
||||
height: 100px;
|
||||
resize: none;
|
||||
border: 1px solid var(--bg-slate-400);
|
||||
background: var(--bg-ink-300);
|
||||
color: var(--bg-vanilla-100);
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
padding: 12px;
|
||||
|
||||
&::placeholder {
|
||||
color: var(--bg-vanilla-400);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
&.invite-team-members-form {
|
||||
min-height: calc(420px - 24px);
|
||||
max-height: calc(420px - 24px);
|
||||
@@ -123,7 +140,7 @@
|
||||
|
||||
height: 32px;
|
||||
background: var(--Ink-300, #16181d);
|
||||
border: 1px solid var(--bg-slate-400);
|
||||
border: 1px solid var(--Greyscale-Slate-400, #1d212d);
|
||||
color: var(--bg-vanilla-400);
|
||||
}
|
||||
|
||||
@@ -445,6 +462,17 @@
|
||||
}
|
||||
}
|
||||
|
||||
.discover-signoz-input {
|
||||
border: 1px solid var(--bg-vanilla-300);
|
||||
background: var(--bg-vanilla-100);
|
||||
color: var(--text-ink-300);
|
||||
|
||||
&::placeholder {
|
||||
color: var(--bg-slate-400);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
&.invite-team-members-form {
|
||||
.invite-team-members-container {
|
||||
max-height: 260px;
|
||||
|
||||
@@ -21,7 +21,7 @@ export interface OrgDetails {
|
||||
usesObservability: boolean | null;
|
||||
observabilityTool: string | null;
|
||||
otherTool: string | null;
|
||||
familiarity: string | null;
|
||||
usesOtel: boolean | null;
|
||||
}
|
||||
|
||||
interface OrgQuestionsProps {
|
||||
@@ -40,13 +40,6 @@ const observabilityTools = {
|
||||
Honeycomb: 'Honeycomb',
|
||||
};
|
||||
|
||||
const o11yFamiliarityOptions: Record<string, string> = {
|
||||
beginner: 'Beginner',
|
||||
intermediate: 'Intermediate',
|
||||
expert: 'Expert',
|
||||
notFamiliar: "I'm not familiar with it",
|
||||
};
|
||||
|
||||
function OrgQuestions({
|
||||
currentOrgData,
|
||||
orgDetails,
|
||||
@@ -69,9 +62,6 @@ function OrgQuestions({
|
||||
const [otherTool, setOtherTool] = useState<string>(
|
||||
orgDetails?.otherTool || '',
|
||||
);
|
||||
const [familiarity, setFamiliarity] = useState<string | null>(
|
||||
orgDetails?.familiarity || null,
|
||||
);
|
||||
const [isNextDisabled, setIsNextDisabled] = useState<boolean>(true);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -80,6 +70,10 @@ function OrgQuestions({
|
||||
|
||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||
|
||||
const [usesOtel, setUsesOtel] = useState<boolean | null>(
|
||||
orgDetails?.usesOtel || null,
|
||||
);
|
||||
|
||||
const handleOrgNameUpdate = async (): Promise<void> => {
|
||||
/* Early bailout if orgData is not set or if the organisation name is not set or if the organisation name is empty or if the organisation name is the same as the one in the orgData */
|
||||
if (
|
||||
@@ -92,7 +86,7 @@ function OrgQuestions({
|
||||
usesObservability,
|
||||
observabilityTool,
|
||||
otherTool,
|
||||
familiarity,
|
||||
usesOtel,
|
||||
});
|
||||
|
||||
onNext({
|
||||
@@ -100,7 +94,7 @@ function OrgQuestions({
|
||||
usesObservability,
|
||||
observabilityTool,
|
||||
otherTool,
|
||||
familiarity,
|
||||
usesOtel,
|
||||
});
|
||||
|
||||
return;
|
||||
@@ -123,7 +117,7 @@ function OrgQuestions({
|
||||
usesObservability,
|
||||
observabilityTool,
|
||||
otherTool,
|
||||
familiarity,
|
||||
usesOtel,
|
||||
});
|
||||
|
||||
onNext({
|
||||
@@ -131,7 +125,7 @@ function OrgQuestions({
|
||||
usesObservability,
|
||||
observabilityTool,
|
||||
otherTool,
|
||||
familiarity,
|
||||
usesOtel,
|
||||
});
|
||||
} else {
|
||||
logEvent('Org Onboarding: Org Name Update Failed', {
|
||||
@@ -177,7 +171,7 @@ function OrgQuestions({
|
||||
useEffect(() => {
|
||||
const isValidObservability = isValidUsesObservability();
|
||||
|
||||
if (organisationName !== '' && familiarity !== null && isValidObservability) {
|
||||
if (organisationName !== '' && usesOtel !== null && isValidObservability) {
|
||||
setIsNextDisabled(false);
|
||||
} else {
|
||||
setIsNextDisabled(true);
|
||||
@@ -186,7 +180,7 @@ function OrgQuestions({
|
||||
}, [
|
||||
organisationName,
|
||||
usesObservability,
|
||||
familiarity,
|
||||
usesOtel,
|
||||
observabilityTool,
|
||||
otherTool,
|
||||
]);
|
||||
@@ -317,25 +311,37 @@ function OrgQuestions({
|
||||
)}
|
||||
|
||||
<div className="form-group">
|
||||
<div className="question">
|
||||
Are you familiar with setting up observability (o11y)?
|
||||
</div>
|
||||
<div className="question">Do you already use OpenTelemetry?</div>
|
||||
<div className="two-column-grid">
|
||||
{Object.keys(o11yFamiliarityOptions).map((option: string) => (
|
||||
<Button
|
||||
key={option}
|
||||
type="primary"
|
||||
className={`onboarding-questionaire-button ${
|
||||
familiarity === option ? 'active' : ''
|
||||
}`}
|
||||
onClick={(): void => setFamiliarity(option)}
|
||||
>
|
||||
{o11yFamiliarityOptions[option]}
|
||||
{familiarity === option && (
|
||||
<CheckCircle size={12} color={Color.BG_FOREST_500} />
|
||||
)}
|
||||
</Button>
|
||||
))}
|
||||
<Button
|
||||
type="primary"
|
||||
name="usesObservability"
|
||||
className={`onboarding-questionaire-button ${
|
||||
usesOtel === true ? 'active' : ''
|
||||
}`}
|
||||
onClick={(): void => {
|
||||
setUsesOtel(true);
|
||||
}}
|
||||
>
|
||||
Yes{' '}
|
||||
{usesOtel === true && (
|
||||
<CheckCircle size={12} color={Color.BG_FOREST_500} />
|
||||
)}
|
||||
</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
className={`onboarding-questionaire-button ${
|
||||
usesOtel === false ? 'active' : ''
|
||||
}`}
|
||||
onClick={(): void => {
|
||||
setUsesOtel(false);
|
||||
}}
|
||||
>
|
||||
No{' '}
|
||||
{usesOtel === false && (
|
||||
<CheckCircle size={12} color={Color.BG_FOREST_500} />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -42,14 +42,13 @@ const INITIAL_ORG_DETAILS: OrgDetails = {
|
||||
usesObservability: true,
|
||||
observabilityTool: '',
|
||||
otherTool: '',
|
||||
familiarity: '',
|
||||
usesOtel: null,
|
||||
};
|
||||
|
||||
const INITIAL_SIGNOZ_DETAILS: SignozDetails = {
|
||||
hearAboutSignoz: '',
|
||||
interestInSignoz: '',
|
||||
otherInterestInSignoz: '',
|
||||
otherAboutSignoz: '',
|
||||
discoverSignoz: '',
|
||||
};
|
||||
|
||||
const INITIAL_OPTIMISE_SIGNOZ_DETAILS: OptimiseSignozDetails = {
|
||||
@@ -168,22 +167,17 @@ function OnboardingQuestionaire(): JSX.Element {
|
||||
});
|
||||
|
||||
updateProfile({
|
||||
familiarity_with_observability: orgDetails?.familiarity as string,
|
||||
uses_otel: orgDetails?.usesOtel as boolean,
|
||||
has_existing_observability_tool: orgDetails?.usesObservability as boolean,
|
||||
existing_observability_tool:
|
||||
orgDetails?.observabilityTool === 'Others'
|
||||
? (orgDetails?.otherTool as string)
|
||||
: (orgDetails?.observabilityTool as string),
|
||||
|
||||
where_did_you_discover_signoz: signozDetails?.discoverSignoz as string,
|
||||
reasons_for_interest_in_signoz:
|
||||
signozDetails?.interestInSignoz === 'Others'
|
||||
? (signozDetails?.otherInterestInSignoz as string)
|
||||
: (signozDetails?.interestInSignoz as string),
|
||||
where_did_you_hear_about_signoz:
|
||||
signozDetails?.hearAboutSignoz === 'Others'
|
||||
? (signozDetails?.otherAboutSignoz as string)
|
||||
: (signozDetails?.hearAboutSignoz as string),
|
||||
|
||||
logs_scale_per_day_in_gb: optimiseSignozDetails?.logsPerDay as number,
|
||||
number_of_hosts: optimiseSignozDetails?.hostsPerDay as number,
|
||||
number_of_services: optimiseSignozDetails?.services as number,
|
||||
|
||||
@@ -1631,7 +1631,7 @@
|
||||
"docker metrics to signoz"
|
||||
],
|
||||
"imgUrl": "/Logos/docker.svg",
|
||||
"link": "https://signoz.io/docs/userguide/k8s-metrics/"
|
||||
"link": "https://signoz.io/docs/metrics-management/docker-container-metrics/"
|
||||
},
|
||||
{
|
||||
"dataSource": "ec2-application-logs",
|
||||
|
||||
@@ -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 => {}}
|
||||
/>,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user