mirror of
https://github.com/SigNoz/signoz.git
synced 2026-02-18 23:12:36 +00:00
Compare commits
1 Commits
chore/supp
...
chore/curs
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
92f3f51eb6 |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -229,5 +229,3 @@ cython_debug/
|
||||
pyrightconfig.json
|
||||
|
||||
|
||||
# cursor files
|
||||
frontend/.cursor/
|
||||
|
||||
74
frontend/.cursor/rules/testing-conventions.mdc
Normal file
74
frontend/.cursor/rules/testing-conventions.mdc
Normal file
@@ -0,0 +1,74 @@
|
||||
---
|
||||
description: Core testing conventions - imports, rendering, MSW, interactions, queries
|
||||
globs: **/*.test.{ts,tsx}
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
# Testing Conventions
|
||||
|
||||
## Imports
|
||||
|
||||
Always import from the test harness, never directly from `@testing-library/react`:
|
||||
|
||||
```ts
|
||||
import { render, screen, userEvent, waitFor } from 'tests/test-utils';
|
||||
import { server, rest } from 'mocks-server/server';
|
||||
```
|
||||
|
||||
## Router
|
||||
|
||||
Use the built-in router in `render`:
|
||||
|
||||
```ts
|
||||
render(<Page />, undefined, { initialRoute: '/traces-explorer' });
|
||||
```
|
||||
|
||||
Only mock `useLocation` / `useParams` if the test depends on their values.
|
||||
|
||||
## MSW
|
||||
|
||||
Global MSW server runs automatically. Override per-test:
|
||||
|
||||
```ts
|
||||
server.use(
|
||||
rest.get('*/api/v1/foo', (_req, res, ctx) =>
|
||||
res(ctx.status(200), ctx.json({ ok: true })))
|
||||
);
|
||||
```
|
||||
|
||||
Keep large response fixtures in `mocks-server/__mockdata_`.
|
||||
|
||||
## Interactions
|
||||
|
||||
- Prefer `userEvent` for real user interactions (click, type, select, tab).
|
||||
- Use `fireEvent` only for low-level events not covered by `userEvent` (e.g., scroll, resize). Wrap in `act(...)` if needed.
|
||||
- Always `await` interactions:
|
||||
|
||||
```ts
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
await user.click(screen.getByRole('button', { name: /save/i }));
|
||||
```
|
||||
|
||||
## Timers
|
||||
|
||||
No global fake timers. Per-test only, for debounce/throttle:
|
||||
|
||||
```ts
|
||||
jest.useFakeTimers();
|
||||
const user = userEvent.setup({ advanceTimers: (ms) => jest.advanceTimersByTime(ms) });
|
||||
await user.type(screen.getByRole('textbox'), 'query');
|
||||
jest.advanceTimersByTime(400);
|
||||
jest.useRealTimers();
|
||||
```
|
||||
|
||||
## Queries
|
||||
|
||||
Prefer accessible queries: `getByRole` > `findByRole` > `getByLabelText` > visible text > `data-testid` (last resort).
|
||||
|
||||
## Anti-patterns
|
||||
|
||||
- Never import from `@testing-library/react` directly
|
||||
- Never use global fake timers
|
||||
- Never wrap `render` in `act(...)`
|
||||
- Never mock infra dependencies locally (router, react-query)
|
||||
- Limit to 3-5 focused tests per file
|
||||
54
frontend/.cursor/rules/testing-mock-strategy.mdc
Normal file
54
frontend/.cursor/rules/testing-mock-strategy.mdc
Normal file
@@ -0,0 +1,54 @@
|
||||
---
|
||||
description: When to use global vs local mocks in tests
|
||||
globs: **/*.test.{ts,tsx}
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
# Mock Strategy
|
||||
|
||||
## Use Global Mocks For
|
||||
|
||||
High-frequency dependencies (20+ test files):
|
||||
- Core infrastructure: react-router-dom, react-query, antd
|
||||
- Browser APIs: ResizeObserver, matchMedia, localStorage
|
||||
- Utility libraries: date-fns, lodash
|
||||
|
||||
Available global mock files (from jest.config.ts):
|
||||
- `uplot` -> `__mocks__/uplotMock.ts`
|
||||
|
||||
## Use Local Mocks For
|
||||
|
||||
- Business logic dependencies (API endpoints, custom hooks, domain components)
|
||||
- Test-specific behavior (different data per test, error scenarios, loading states)
|
||||
|
||||
## Decision Tree
|
||||
|
||||
```
|
||||
Used in 20+ test files?
|
||||
YES -> Global mock
|
||||
NO -> Business logic or test-specific?
|
||||
YES -> Local mock
|
||||
NO -> Consider global if usage grows
|
||||
```
|
||||
|
||||
## Correct Usage
|
||||
|
||||
```ts
|
||||
// Global mocks are already available - just import
|
||||
import { useLocation } from 'react-router-dom';
|
||||
|
||||
// Local mocks for business logic
|
||||
jest.mock('../api/tracesService', () => ({
|
||||
getTraces: jest.fn(() => mockTracesData),
|
||||
}));
|
||||
```
|
||||
|
||||
## Anti-patterns
|
||||
|
||||
```ts
|
||||
// Never re-mock globally mocked dependencies locally
|
||||
jest.mock('react-router-dom', () => ({ ... }));
|
||||
|
||||
// Never put test-specific data in global mocks
|
||||
jest.mock('../api/tracesService', () => ({ getTraces: jest.fn(() => specificTestData) }));
|
||||
```
|
||||
54
frontend/.cursor/rules/testing-type-safety.mdc
Normal file
54
frontend/.cursor/rules/testing-type-safety.mdc
Normal file
@@ -0,0 +1,54 @@
|
||||
---
|
||||
description: TypeScript type safety requirements for Jest tests
|
||||
globs: **/*.test.{ts,tsx}
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
# TypeScript Type Safety in Tests
|
||||
|
||||
All Jest tests must be fully type-safe. Never use `any`.
|
||||
|
||||
## Mock Function Typing
|
||||
|
||||
```ts
|
||||
// Use jest.mocked for module mocks
|
||||
import useFoo from 'hooks/useFoo';
|
||||
jest.mock('hooks/useFoo');
|
||||
const mockUseFoo = jest.mocked(useFoo);
|
||||
|
||||
// Use jest.MockedFunction for standalone mocks
|
||||
const mockFetch = jest.fn() as jest.MockedFunction<(id: number) => Promise<User>>;
|
||||
```
|
||||
|
||||
## Mock Data
|
||||
|
||||
Define interfaces for all mock data:
|
||||
|
||||
```ts
|
||||
const mockUser: User = { id: 1, name: 'John', email: 'john@example.com' };
|
||||
|
||||
const mockProps: ComponentProps = {
|
||||
title: 'Test',
|
||||
data: [mockUser],
|
||||
onSelect: jest.fn() as jest.MockedFunction<(user: User) => void>,
|
||||
};
|
||||
```
|
||||
|
||||
## Hook Mocking Pattern
|
||||
|
||||
```ts
|
||||
import useFoo from 'hooks/useFoo';
|
||||
jest.mock('hooks/useFoo');
|
||||
const mockUseFoo = jest.mocked(useFoo);
|
||||
mockUseFoo.mockReturnValue(/* minimal shape */);
|
||||
```
|
||||
|
||||
Prefer helpers (`rqSuccess`, `rqLoading`, `rqError`) for React Query results.
|
||||
|
||||
## Checklist
|
||||
|
||||
- All mock functions use `jest.MockedFunction<T>` or `jest.mocked()`
|
||||
- All mock data has proper interfaces
|
||||
- No `any` types in test files
|
||||
- Component props are typed
|
||||
- API response types are defined
|
||||
@@ -1,484 +0,0 @@
|
||||
# Persona
|
||||
You are an expert developer with deep knowledge of Jest, React Testing Library, MSW, and TypeScript, tasked with creating unit tests for this repository.
|
||||
|
||||
# Auto-detect TypeScript Usage
|
||||
Check for TypeScript in the project through tsconfig.json or package.json dependencies.
|
||||
Adjust syntax based on this detection.
|
||||
|
||||
# TypeScript Type Safety for Jest Tests
|
||||
**CRITICAL**: All Jest tests MUST be fully type-safe with proper TypeScript types.
|
||||
|
||||
**Type Safety Requirements:**
|
||||
- Use proper TypeScript interfaces for all mock data
|
||||
- Type all Jest mock functions with `jest.MockedFunction<T>`
|
||||
- Use generic types for React components and hooks
|
||||
- Define proper return types for mock functions
|
||||
- Use `as const` for literal types when needed
|
||||
- Avoid `any` type – use proper typing instead
|
||||
|
||||
# Unit Testing Focus
|
||||
Focus on critical functionality (business logic, utility functions, component behavior)
|
||||
Mock dependencies (API calls, external modules) before imports
|
||||
Test multiple data scenarios (valid inputs, invalid inputs, edge cases)
|
||||
Write maintainable tests with descriptive names grouped in describe blocks
|
||||
|
||||
# Global vs Local Mocks
|
||||
**Use Global Mocks for:**
|
||||
- High-frequency dependencies (20+ test files)
|
||||
- Core infrastructure (react-router-dom, react-query, antd)
|
||||
- Standard implementations across the app
|
||||
- Browser APIs (ResizeObserver, matchMedia, localStorage)
|
||||
- Utility libraries (date-fns, lodash)
|
||||
|
||||
**Use Local Mocks for:**
|
||||
- Business logic dependencies (5-15 test files)
|
||||
- Test-specific behavior (different data per test)
|
||||
- API endpoints with specific responses
|
||||
- Domain-specific components
|
||||
- Error scenarios and edge cases
|
||||
|
||||
**Global Mock Files Available (from jest.config.ts):**
|
||||
- `uplot` → `__mocks__/uplotMock.ts`
|
||||
|
||||
# Repo-specific Testing Conventions
|
||||
|
||||
## Imports
|
||||
Always import from our harness:
|
||||
```ts
|
||||
import { render, screen, userEvent, waitFor } from 'tests/test-utils';
|
||||
```
|
||||
For API mocks:
|
||||
```ts
|
||||
import { server, rest } from 'mocks-server/server';
|
||||
```
|
||||
Do not import directly from `@testing-library/react`.
|
||||
|
||||
## Router
|
||||
Use the router built into render:
|
||||
```ts
|
||||
render(<Page />, undefined, { initialRoute: '/traces-explorer' });
|
||||
```
|
||||
Only mock `useLocation` / `useParams` if the test depends on them.
|
||||
|
||||
## Hook Mocks
|
||||
Pattern:
|
||||
```ts
|
||||
import useFoo from 'hooks/useFoo';
|
||||
jest.mock('hooks/useFoo');
|
||||
const mockUseFoo = jest.mocked(useFoo);
|
||||
mockUseFoo.mockReturnValue(/* minimal shape */ as any);
|
||||
```
|
||||
Prefer helpers (`rqSuccess`, `rqLoading`, `rqError`) for React Query results.
|
||||
|
||||
## MSW
|
||||
Global MSW server runs automatically.
|
||||
Override per-test:
|
||||
```ts
|
||||
server.use(
|
||||
rest.get('*/api/v1/foo', (_req, res, ctx) => res(ctx.status(200), ctx.json({ ok: true })))
|
||||
);
|
||||
```
|
||||
Keep large responses in `mocks-server/__mockdata_`.
|
||||
|
||||
## Interactions
|
||||
- Prefer `userEvent` for real user interactions (click, type, select, tab).
|
||||
- Use `fireEvent` only for low-level/programmatic events not covered by `userEvent` (e.g., scroll, resize, setting `element.scrollTop` for virtualization). Wrap in `act(...)` if needed.
|
||||
- Always await interactions:
|
||||
```ts
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
await user.click(screen.getByRole('button', { name: /save/i }));
|
||||
```
|
||||
|
||||
```ts
|
||||
// Example: virtualized list scroll (no userEvent helper)
|
||||
const scroller = container.querySelector('[data-test-id="virtuoso-scroller"]') as HTMLElement;
|
||||
scroller.scrollTop = targetScrollTop;
|
||||
act(() => { fireEvent.scroll(scroller); });
|
||||
```
|
||||
|
||||
## Timers
|
||||
❌ No global fake timers.
|
||||
✅ Per-test only, for debounce/throttle:
|
||||
```ts
|
||||
jest.useFakeTimers();
|
||||
const user = userEvent.setup({ advanceTimers: (ms) => jest.advanceTimersByTime(ms) });
|
||||
await user.type(screen.getByRole('textbox'), 'query');
|
||||
jest.advanceTimersByTime(400);
|
||||
jest.useRealTimers();
|
||||
```
|
||||
|
||||
## Queries
|
||||
Prefer accessible queries (`getByRole`, `findByRole`, `getByLabelText`).
|
||||
Fallback: visible text.
|
||||
Last resort: `data-testid`.
|
||||
|
||||
# Example Test (using only configured global mocks)
|
||||
```ts
|
||||
import { render, screen, userEvent, waitFor } from 'tests/test-utils';
|
||||
import { server, rest } from 'mocks-server/server';
|
||||
import MyComponent from '../MyComponent';
|
||||
|
||||
describe('MyComponent', () => {
|
||||
it('renders and interacts', async () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
|
||||
server.use(
|
||||
rest.get('*/api/v1/example', (_req, res, ctx) => res(ctx.status(200), ctx.json({ value: 42 })))
|
||||
);
|
||||
|
||||
render(<MyComponent />, undefined, { initialRoute: '/foo' });
|
||||
|
||||
expect(await screen.findByText(/value: 42/i)).toBeInTheDocument();
|
||||
await user.click(screen.getByRole('button', { name: /refresh/i }));
|
||||
await waitFor(() => expect(screen.getByText(/loading/i)).toBeInTheDocument());
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
# Anti-patterns
|
||||
❌ Importing RTL directly
|
||||
❌ Using global fake timers
|
||||
❌ Wrapping render in `act(...)`
|
||||
❌ Mocking infra dependencies locally (router, react-query)
|
||||
✅ Use our harness (`tests/test-utils`)
|
||||
✅ Use MSW for API overrides
|
||||
✅ Use userEvent + await
|
||||
✅ Pin time only in tests that assert relative dates
|
||||
|
||||
# Best Practices
|
||||
- **Critical Functionality**: Prioritize testing business logic and utilities
|
||||
- **Dependency Mocking**: Global mocks for infra, local mocks for business logic
|
||||
- **Data Scenarios**: Always test valid, invalid, and edge cases
|
||||
- **Descriptive Names**: Make test intent clear
|
||||
- **Organization**: Group related tests in describe
|
||||
- **Consistency**: Match repo conventions
|
||||
- **Edge Cases**: Test null, undefined, unexpected values
|
||||
- **Limit Scope**: 3–5 focused tests per file
|
||||
- **Use Helpers**: `rqSuccess`, `makeUser`, etc.
|
||||
- **No Any**: Enforce type safety
|
||||
|
||||
# Example Test
|
||||
```ts
|
||||
import { render, screen, userEvent, waitFor } from 'tests/test-utils';
|
||||
import { server, rest } from 'mocks-server/server';
|
||||
import MyComponent from '../MyComponent';
|
||||
|
||||
describe('MyComponent', () => {
|
||||
it('renders and interacts', async () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
|
||||
server.use(
|
||||
rest.get('*/api/v1/example', (_req, res, ctx) => res(ctx.status(200), ctx.json({ value: 42 })))
|
||||
);
|
||||
|
||||
render(<MyComponent />, undefined, { initialRoute: '/foo' });
|
||||
|
||||
expect(await screen.findByText(/value: 42/i)).toBeInTheDocument();
|
||||
await user.click(screen.getByRole('button', { name: /refresh/i }));
|
||||
await waitFor(() => expect(screen.getByText(/loading/i)).toBeInTheDocument());
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
# Anti-patterns
|
||||
❌ Importing RTL directly
|
||||
❌ Using global fake timers
|
||||
❌ Wrapping render in `act(...)`
|
||||
❌ Mocking infra dependencies locally (router, react-query)
|
||||
✅ Use our harness (`tests/test-utils`)
|
||||
✅ Use MSW for API overrides
|
||||
✅ Use userEvent + await
|
||||
✅ Pin time only in tests that assert relative dates
|
||||
|
||||
# TypeScript Type Safety Examples
|
||||
|
||||
## Proper Mock Typing
|
||||
```ts
|
||||
// ✅ GOOD - Properly typed mocks
|
||||
interface User {
|
||||
id: number;
|
||||
name: string;
|
||||
email: string;
|
||||
}
|
||||
|
||||
interface ApiResponse<T> {
|
||||
data: T;
|
||||
status: number;
|
||||
message: string;
|
||||
}
|
||||
|
||||
// Type the mock functions
|
||||
const mockFetchUser = jest.fn() as jest.MockedFunction<(id: number) => Promise<ApiResponse<User>>>;
|
||||
const mockUpdateUser = jest.fn() as jest.MockedFunction<(user: User) => Promise<ApiResponse<User>>>;
|
||||
|
||||
// Mock implementation with proper typing
|
||||
mockFetchUser.mockResolvedValue({
|
||||
data: { id: 1, name: 'John Doe', email: 'john@example.com' },
|
||||
status: 200,
|
||||
message: 'Success'
|
||||
});
|
||||
|
||||
// ❌ BAD - Using any type
|
||||
const mockFetchUser = jest.fn() as any; // Don't do this
|
||||
```
|
||||
|
||||
## React Component Testing with Types
|
||||
```ts
|
||||
// ✅ GOOD - Properly typed component testing
|
||||
interface ComponentProps {
|
||||
title: string;
|
||||
data: User[];
|
||||
onUserSelect: (user: User) => void;
|
||||
isLoading?: boolean;
|
||||
}
|
||||
|
||||
const TestComponent: React.FC<ComponentProps> = ({ title, data, onUserSelect, isLoading = false }) => {
|
||||
// Component implementation
|
||||
};
|
||||
|
||||
describe('TestComponent', () => {
|
||||
it('should render with proper props', () => {
|
||||
// Arrange - Type the props properly
|
||||
const mockProps: ComponentProps = {
|
||||
title: 'Test Title',
|
||||
data: [{ id: 1, name: 'John', email: 'john@example.com' }],
|
||||
onUserSelect: jest.fn() as jest.MockedFunction<(user: User) => void>,
|
||||
isLoading: false
|
||||
};
|
||||
|
||||
// Act
|
||||
render(<TestComponent {...mockProps} />);
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText('Test Title')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## Hook Testing with Types
|
||||
```ts
|
||||
// ✅ GOOD - Properly typed hook testing
|
||||
interface UseUserDataReturn {
|
||||
user: User | null;
|
||||
loading: boolean;
|
||||
error: string | null;
|
||||
refetch: () => void;
|
||||
}
|
||||
|
||||
const useUserData = (id: number): UseUserDataReturn => {
|
||||
// Hook implementation
|
||||
};
|
||||
|
||||
describe('useUserData', () => {
|
||||
it('should return user data with proper typing', () => {
|
||||
// Arrange
|
||||
const mockUser: User = { id: 1, name: 'John', email: 'john@example.com' };
|
||||
mockFetchUser.mockResolvedValue({
|
||||
data: mockUser,
|
||||
status: 200,
|
||||
message: 'Success'
|
||||
});
|
||||
|
||||
// Act
|
||||
const { result } = renderHook(() => useUserData(1));
|
||||
|
||||
// Assert
|
||||
expect(result.current.user).toEqual(mockUser);
|
||||
expect(result.current.loading).toBe(false);
|
||||
expect(result.current.error).toBeNull();
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## Global Mock Type Safety
|
||||
```ts
|
||||
// ✅ GOOD - Type-safe global mocks
|
||||
// In __mocks__/routerMock.ts
|
||||
export const mockUseLocation = (overrides: Partial<Location> = {}): Location => ({
|
||||
pathname: '/traces',
|
||||
search: '',
|
||||
hash: '',
|
||||
state: null,
|
||||
key: 'test-key',
|
||||
...overrides,
|
||||
});
|
||||
|
||||
// In test files
|
||||
const location = useLocation(); // Properly typed from global mock
|
||||
expect(location.pathname).toBe('/traces');
|
||||
```
|
||||
|
||||
# TypeScript Configuration for Jest
|
||||
|
||||
## Required Jest Configuration
|
||||
```json
|
||||
// jest.config.ts
|
||||
{
|
||||
"preset": "ts-jest/presets/js-with-ts-esm",
|
||||
"globals": {
|
||||
"ts-jest": {
|
||||
"useESM": true,
|
||||
"isolatedModules": true,
|
||||
"tsconfig": "<rootDir>/tsconfig.jest.json"
|
||||
}
|
||||
},
|
||||
"extensionsToTreatAsEsm": [".ts", ".tsx"],
|
||||
"moduleFileExtensions": ["ts", "tsx", "js", "json"]
|
||||
}
|
||||
```
|
||||
|
||||
## TypeScript Jest Configuration
|
||||
```json
|
||||
// tsconfig.jest.json
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"types": ["jest", "@testing-library/jest-dom"],
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"moduleResolution": "node"
|
||||
},
|
||||
"include": [
|
||||
"src/**/*",
|
||||
"**/*.test.ts",
|
||||
"**/*.test.tsx",
|
||||
"__mocks__/**/*"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Common Type Safety Patterns
|
||||
|
||||
### Mock Function Typing
|
||||
```ts
|
||||
// ✅ GOOD - Proper mock function typing
|
||||
const mockApiCall = jest.fn() as jest.MockedFunction<typeof apiCall>;
|
||||
const mockEventHandler = jest.fn() as jest.MockedFunction<(event: Event) => void>;
|
||||
|
||||
// ❌ BAD - Using any
|
||||
const mockApiCall = jest.fn() as any;
|
||||
```
|
||||
|
||||
### Generic Mock Typing
|
||||
```ts
|
||||
// ✅ GOOD - Generic mock typing
|
||||
interface MockApiResponse<T> {
|
||||
data: T;
|
||||
status: number;
|
||||
}
|
||||
|
||||
const mockFetchData = jest.fn() as jest.MockedFunction<
|
||||
<T>(endpoint: string) => Promise<MockApiResponse<T>>
|
||||
>;
|
||||
|
||||
// Usage
|
||||
mockFetchData<User>('/users').mockResolvedValue({
|
||||
data: { id: 1, name: 'John' },
|
||||
status: 200
|
||||
});
|
||||
```
|
||||
|
||||
### React Testing Library with Types
|
||||
```ts
|
||||
// ✅ GOOD - Typed testing utilities
|
||||
import { render, screen, RenderResult } from '@testing-library/react';
|
||||
import { ComponentProps } from 'react';
|
||||
|
||||
type TestComponentProps = ComponentProps<typeof TestComponent>;
|
||||
|
||||
const renderTestComponent = (props: Partial<TestComponentProps> = {}): RenderResult => {
|
||||
const defaultProps: TestComponentProps = {
|
||||
title: 'Test',
|
||||
data: [],
|
||||
onSelect: jest.fn(),
|
||||
...props
|
||||
};
|
||||
|
||||
return render(<TestComponent {...defaultProps} />);
|
||||
};
|
||||
```
|
||||
|
||||
### Error Handling with Types
|
||||
```ts
|
||||
// ✅ GOOD - Typed error handling
|
||||
interface ApiError {
|
||||
message: string;
|
||||
code: number;
|
||||
details?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
const mockApiError: ApiError = {
|
||||
message: 'API Error',
|
||||
code: 500,
|
||||
details: { endpoint: '/users' }
|
||||
};
|
||||
|
||||
mockFetchUser.mockRejectedValue(new Error(JSON.stringify(mockApiError)));
|
||||
```
|
||||
|
||||
## Type Safety Checklist
|
||||
- [ ] All mock functions use `jest.MockedFunction<T>`
|
||||
- [ ] All mock data has proper interfaces
|
||||
- [ ] No `any` types in test files
|
||||
- [ ] Generic types are used where appropriate
|
||||
- [ ] Error types are properly defined
|
||||
- [ ] Component props are typed
|
||||
- [ ] Hook return types are defined
|
||||
- [ ] API response types are defined
|
||||
- [ ] Global mocks are type-safe
|
||||
- [ ] Test utilities are properly typed
|
||||
|
||||
# Mock Decision Tree
|
||||
```
|
||||
Is it used in 20+ test files?
|
||||
├─ YES → Use Global Mock
|
||||
│ ├─ react-router-dom
|
||||
│ ├─ react-query
|
||||
│ ├─ antd components
|
||||
│ └─ browser APIs
|
||||
│
|
||||
└─ NO → Is it business logic?
|
||||
├─ YES → Use Local Mock
|
||||
│ ├─ API endpoints
|
||||
│ ├─ Custom hooks
|
||||
│ └─ Domain components
|
||||
│
|
||||
└─ NO → Is it test-specific?
|
||||
├─ YES → Use Local Mock
|
||||
│ ├─ Error scenarios
|
||||
│ ├─ Loading states
|
||||
│ └─ Specific data
|
||||
│
|
||||
└─ NO → Consider Global Mock
|
||||
└─ If it becomes frequently used
|
||||
```
|
||||
|
||||
# Common Anti-Patterns to Avoid
|
||||
|
||||
❌ **Don't mock global dependencies locally:**
|
||||
```js
|
||||
// BAD - This is already globally mocked
|
||||
jest.mock('react-router-dom', () => ({ ... }));
|
||||
```
|
||||
|
||||
❌ **Don't create global mocks for test-specific data:**
|
||||
```js
|
||||
// BAD - This should be local
|
||||
jest.mock('../api/tracesService', () => ({
|
||||
getTraces: jest.fn(() => specificTestData)
|
||||
}));
|
||||
```
|
||||
|
||||
✅ **Do use global mocks for infrastructure:**
|
||||
```js
|
||||
// GOOD - Use global mock
|
||||
import { useLocation } from 'react-router-dom';
|
||||
```
|
||||
|
||||
✅ **Do create local mocks for business logic:**
|
||||
```js
|
||||
// GOOD - Local mock for specific test needs
|
||||
jest.mock('../api/tracesService', () => ({
|
||||
getTraces: jest.fn(() => mockTracesData)
|
||||
}));
|
||||
```
|
||||
@@ -2,8 +2,6 @@
|
||||
interface SafeNavigateOptions {
|
||||
replace?: boolean;
|
||||
state?: unknown;
|
||||
newTab?: boolean;
|
||||
event?: MouseEvent | React.MouseEvent;
|
||||
}
|
||||
|
||||
interface SafeNavigateTo {
|
||||
@@ -22,7 +20,9 @@ interface UseSafeNavigateReturn {
|
||||
|
||||
export const useSafeNavigate = (): UseSafeNavigateReturn => ({
|
||||
safeNavigate: jest.fn(
|
||||
(_to: SafeNavigateToType, _options?: SafeNavigateOptions) => {},
|
||||
(to: SafeNavigateToType, options?: SafeNavigateOptions) => {
|
||||
console.log(`Mock safeNavigate called with:`, to, options);
|
||||
},
|
||||
) as jest.MockedFunction<
|
||||
(to: SafeNavigateToType, options?: SafeNavigateOptions) => void
|
||||
>,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/* eslint-disable sonarjs/cognitive-complexity */
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useCopyToClipboard, useLocation } from 'react-use';
|
||||
import { Color, Spacing } from '@signozhq/design-tokens';
|
||||
@@ -214,7 +214,7 @@ function LogDetailInner({
|
||||
};
|
||||
|
||||
// Go to logs explorer page with the log data
|
||||
const handleOpenInExplorer = (e?: React.MouseEvent): void => {
|
||||
const handleOpenInExplorer = (): void => {
|
||||
const queryParams = {
|
||||
[QueryParams.activeLogId]: `"${log?.id}"`,
|
||||
[QueryParams.startTime]: minTime?.toString() || '',
|
||||
@@ -227,9 +227,7 @@ function LogDetailInner({
|
||||
),
|
||||
),
|
||||
};
|
||||
safeNavigate(`${ROUTES.LOGS_EXPLORER}?${createQueryParams(queryParams)}`, {
|
||||
event: e,
|
||||
});
|
||||
safeNavigate(`${ROUTES.LOGS_EXPLORER}?${createQueryParams(queryParams)}`);
|
||||
};
|
||||
|
||||
const handleQueryExpressionChange = useCallback(
|
||||
|
||||
@@ -70,8 +70,8 @@ function SelectAlertType({ onSelect }: SelectAlertTypeProps): JSX.Element {
|
||||
</Tag>
|
||||
) : undefined
|
||||
}
|
||||
onClick={(e): void => {
|
||||
onSelect(option.selection, e);
|
||||
onClick={(): void => {
|
||||
onSelect(option.selection);
|
||||
}}
|
||||
data-testid={`alert-type-card-${option.selection}`}
|
||||
>
|
||||
@@ -108,7 +108,7 @@ function SelectAlertType({ onSelect }: SelectAlertTypeProps): JSX.Element {
|
||||
}
|
||||
|
||||
interface SelectAlertTypeProps {
|
||||
onSelect: (type: AlertTypes, event?: React.MouseEvent) => void;
|
||||
onSelect: (typ: AlertTypes) => void;
|
||||
}
|
||||
|
||||
export default SelectAlertType;
|
||||
|
||||
@@ -33,9 +33,9 @@ function Footer(): JSX.Element {
|
||||
const { currentQuery } = useQueryBuilder();
|
||||
const { safeNavigate } = useSafeNavigate();
|
||||
|
||||
const handleDiscard = (e: React.MouseEvent): void => {
|
||||
const handleDiscard = (): void => {
|
||||
discardAlertRule();
|
||||
safeNavigate('/alerts', { event: e });
|
||||
safeNavigate('/alerts');
|
||||
};
|
||||
|
||||
const alertValidationMessage = useMemo(
|
||||
|
||||
@@ -161,7 +161,6 @@ describe('Dashboard landing page actions header tests', () => {
|
||||
// Verify navigation was called with correct URL
|
||||
expect(mockSafeNavigate).toHaveBeenCalledWith(
|
||||
'/dashboard?columnKey=updatedAt&order=descend&page=1&search=',
|
||||
expect.objectContaining({ event: expect.any(Object) }),
|
||||
);
|
||||
|
||||
// Ensure the URL contains only essential dashboard list params
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import { useCallback } from 'react';
|
||||
import { Button } from 'antd';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { useSafeNavigate } from 'hooks/useSafeNavigate';
|
||||
@@ -23,19 +23,16 @@ function DashboardBreadcrumbs(): JSX.Element {
|
||||
|
||||
const { title = '', image = Base64Icons[0] } = selectedData || {};
|
||||
|
||||
const goToListPage = useCallback(
|
||||
(e?: React.MouseEvent) => {
|
||||
const urlParams = new URLSearchParams();
|
||||
urlParams.set('columnKey', listSortOrder.columnKey as string);
|
||||
urlParams.set('order', listSortOrder.order as string);
|
||||
urlParams.set('page', listSortOrder.pagination as string);
|
||||
urlParams.set('search', listSortOrder.search as string);
|
||||
const goToListPage = useCallback(() => {
|
||||
const urlParams = new URLSearchParams();
|
||||
urlParams.set('columnKey', listSortOrder.columnKey as string);
|
||||
urlParams.set('order', listSortOrder.order as string);
|
||||
urlParams.set('page', listSortOrder.pagination as string);
|
||||
urlParams.set('search', listSortOrder.search as string);
|
||||
|
||||
const generatedUrl = `${ROUTES.ALL_DASHBOARD}?${urlParams.toString()}`;
|
||||
safeNavigate(generatedUrl, { event: e });
|
||||
},
|
||||
[listSortOrder, safeNavigate],
|
||||
);
|
||||
const generatedUrl = `${ROUTES.ALL_DASHBOARD}?${urlParams.toString()}`;
|
||||
safeNavigate(generatedUrl);
|
||||
}, [listSortOrder, safeNavigate]);
|
||||
|
||||
return (
|
||||
<div className="dashboard-breadcrumbs">
|
||||
|
||||
@@ -16,7 +16,6 @@ import { isUndefined } from 'lodash-es';
|
||||
import { urlKey } from 'pages/ErrorDetails/utils';
|
||||
import { useTimezone } from 'providers/Timezone';
|
||||
import { PayloadProps as GetByErrorTypeAndServicePayload } from 'types/api/errors/getByErrorTypeAndService';
|
||||
import { isModifierKeyPressed, openInNewTab } from 'utils/navigation';
|
||||
|
||||
import { keyToExclude } from './config';
|
||||
import { DashedContainer, EditorContainer, EventContainer } from './styles';
|
||||
@@ -112,19 +111,14 @@ function ErrorDetails(props: ErrorDetailsProps): JSX.Element {
|
||||
value: errorDetail[key as keyof GetByErrorTypeAndServicePayload],
|
||||
}));
|
||||
|
||||
const onClickTraceHandler = (event: React.MouseEvent): void => {
|
||||
const onClickTraceHandler = (): void => {
|
||||
logEvent('Exception: Navigate to trace detail page', {
|
||||
groupId: errorDetail?.groupID,
|
||||
spanId: errorDetail.spanID,
|
||||
traceId: errorDetail.traceID,
|
||||
exceptionId: errorDetail?.errorId,
|
||||
});
|
||||
const path = `/trace/${errorDetail.traceID}?spanId=${errorDetail.spanID}`;
|
||||
if (isModifierKeyPressed(event)) {
|
||||
openInNewTab(path);
|
||||
} else {
|
||||
history.push(path);
|
||||
}
|
||||
history.push(`/trace/${errorDetail.traceID}?spanId=${errorDetail.spanID}`);
|
||||
};
|
||||
|
||||
const logEventCalledRef = useRef(false);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useQueryClient } from 'react-query';
|
||||
import { useSelector } from 'react-redux';
|
||||
@@ -329,18 +329,13 @@ function FormAlertRules({
|
||||
}
|
||||
}, [alertDef, currentQuery?.queryType, queryOptions]);
|
||||
|
||||
const onCancelHandler = useCallback(
|
||||
(e?: React.MouseEvent) => {
|
||||
urlQuery.delete(QueryParams.compositeQuery);
|
||||
urlQuery.delete(QueryParams.panelTypes);
|
||||
urlQuery.delete(QueryParams.ruleId);
|
||||
urlQuery.delete(QueryParams.relativeTime);
|
||||
safeNavigate(`${ROUTES.LIST_ALL_ALERT}?${urlQuery.toString()}`, {
|
||||
event: e,
|
||||
});
|
||||
},
|
||||
[safeNavigate, urlQuery],
|
||||
);
|
||||
const onCancelHandler = useCallback(() => {
|
||||
urlQuery.delete(QueryParams.compositeQuery);
|
||||
urlQuery.delete(QueryParams.panelTypes);
|
||||
urlQuery.delete(QueryParams.ruleId);
|
||||
urlQuery.delete(QueryParams.relativeTime);
|
||||
safeNavigate(`${ROUTES.LIST_ALL_ALERT}?${urlQuery.toString()}`);
|
||||
}, [safeNavigate, urlQuery]);
|
||||
|
||||
// onQueryCategoryChange handles changes to query category
|
||||
// in state as well as sets additional defaults
|
||||
|
||||
@@ -1,11 +1,5 @@
|
||||
/* eslint-disable sonarjs/cognitive-complexity */
|
||||
import React, {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import {
|
||||
LoadingOutlined,
|
||||
@@ -285,9 +279,9 @@ function FullView({
|
||||
<Button
|
||||
className="switch-edit-btn"
|
||||
disabled={response.isFetching || response.isLoading}
|
||||
onClick={(e: React.MouseEvent): void => {
|
||||
onClick={(): void => {
|
||||
if (dashboardEditView) {
|
||||
safeNavigate(dashboardEditView, { event: e });
|
||||
safeNavigate(dashboardEditView);
|
||||
}
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/* eslint-disable sonarjs/no-duplicate-string */
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useMutation, useQuery } from 'react-query';
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { Button, Popover } from 'antd';
|
||||
@@ -28,7 +28,6 @@ import { UserPreference } from 'types/api/preferences/preference';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
import { USER_ROLES } from 'types/roles';
|
||||
import { isIngestionActive } from 'utils/app';
|
||||
import { navigateToPage } from 'utils/navigation';
|
||||
import { popupContainer } from 'utils/selectPopupContainer';
|
||||
|
||||
import AlertRules from './AlertRules/AlertRules';
|
||||
@@ -414,12 +413,12 @@ export default function Home(): JSX.Element {
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
className="active-ingestion-card-actions"
|
||||
onClick={(e: React.MouseEvent): void => {
|
||||
onClick={(): void => {
|
||||
// eslint-disable-next-line sonarjs/no-duplicate-string
|
||||
logEvent('Homepage: Ingestion Active Explore clicked', {
|
||||
source: 'Logs',
|
||||
});
|
||||
navigateToPage(ROUTES.LOGS_EXPLORER, history.push, e);
|
||||
history.push(ROUTES.LOGS_EXPLORER);
|
||||
}}
|
||||
onKeyDown={(e): void => {
|
||||
if (e.key === 'Enter') {
|
||||
@@ -456,11 +455,11 @@ export default function Home(): JSX.Element {
|
||||
className="active-ingestion-card-actions"
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={(e: React.MouseEvent): void => {
|
||||
onClick={(): void => {
|
||||
logEvent('Homepage: Ingestion Active Explore clicked', {
|
||||
source: 'Traces',
|
||||
});
|
||||
navigateToPage(ROUTES.TRACES_EXPLORER, history.push, e);
|
||||
history.push(ROUTES.TRACES_EXPLORER);
|
||||
}}
|
||||
onKeyDown={(e): void => {
|
||||
if (e.key === 'Enter') {
|
||||
@@ -497,11 +496,11 @@ export default function Home(): JSX.Element {
|
||||
className="active-ingestion-card-actions"
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={(e: React.MouseEvent): void => {
|
||||
onClick={(): void => {
|
||||
logEvent('Homepage: Ingestion Active Explore clicked', {
|
||||
source: 'Metrics',
|
||||
});
|
||||
navigateToPage(ROUTES.METRICS_EXPLORER, history.push, e);
|
||||
history.push(ROUTES.METRICS_EXPLORER);
|
||||
}}
|
||||
onKeyDown={(e): void => {
|
||||
if (e.key === 'Enter') {
|
||||
@@ -551,11 +550,11 @@ export default function Home(): JSX.Element {
|
||||
type="default"
|
||||
className="periscope-btn secondary"
|
||||
icon={<Wrench size={14} />}
|
||||
onClick={(e: React.MouseEvent): void => {
|
||||
onClick={(): void => {
|
||||
logEvent('Homepage: Explore clicked', {
|
||||
source: 'Logs',
|
||||
});
|
||||
navigateToPage(ROUTES.LOGS_EXPLORER, history.push, e);
|
||||
history.push(ROUTES.LOGS_EXPLORER);
|
||||
}}
|
||||
>
|
||||
Open Logs Explorer
|
||||
@@ -565,11 +564,11 @@ export default function Home(): JSX.Element {
|
||||
type="default"
|
||||
className="periscope-btn secondary"
|
||||
icon={<Wrench size={14} />}
|
||||
onClick={(e: React.MouseEvent): void => {
|
||||
onClick={(): void => {
|
||||
logEvent('Homepage: Explore clicked', {
|
||||
source: 'Traces',
|
||||
});
|
||||
navigateToPage(ROUTES.TRACES_EXPLORER, history.push, e);
|
||||
history.push(ROUTES.TRACES_EXPLORER);
|
||||
}}
|
||||
>
|
||||
Open Traces Explorer
|
||||
@@ -579,11 +578,11 @@ export default function Home(): JSX.Element {
|
||||
type="default"
|
||||
className="periscope-btn secondary"
|
||||
icon={<Wrench size={14} />}
|
||||
onClick={(e: React.MouseEvent): void => {
|
||||
onClick={(): void => {
|
||||
logEvent('Homepage: Explore clicked', {
|
||||
source: 'Metrics',
|
||||
});
|
||||
navigateToPage(ROUTES.METRICS_EXPLORER_EXPLORER, history.push, e);
|
||||
history.push(ROUTES.METRICS_EXPLORER_EXPLORER);
|
||||
}}
|
||||
>
|
||||
Open Metrics Explorer
|
||||
@@ -620,11 +619,11 @@ export default function Home(): JSX.Element {
|
||||
type="default"
|
||||
className="periscope-btn secondary"
|
||||
icon={<Plus size={14} />}
|
||||
onClick={(e: React.MouseEvent): void => {
|
||||
onClick={(): void => {
|
||||
logEvent('Homepage: Explore clicked', {
|
||||
source: 'Dashboards',
|
||||
});
|
||||
navigateToPage(ROUTES.ALL_DASHBOARD, history.push, e);
|
||||
history.push(ROUTES.ALL_DASHBOARD);
|
||||
}}
|
||||
>
|
||||
Create dashboard
|
||||
@@ -662,11 +661,11 @@ export default function Home(): JSX.Element {
|
||||
type="default"
|
||||
className="periscope-btn secondary"
|
||||
icon={<Plus size={14} />}
|
||||
onClick={(e: React.MouseEvent): void => {
|
||||
onClick={(): void => {
|
||||
logEvent('Homepage: Explore clicked', {
|
||||
source: 'Alerts',
|
||||
});
|
||||
navigateToPage(ROUTES.ALERTS_NEW, history.push, e);
|
||||
history.push(ROUTES.ALERTS_NEW);
|
||||
}}
|
||||
>
|
||||
Create an alert
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { memo, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { memo, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { QueryKey } from 'react-query';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { Link } from 'react-router-dom';
|
||||
@@ -116,7 +116,7 @@ const ServicesListTable = memo(
|
||||
onRowClick,
|
||||
}: {
|
||||
services: ServicesList[];
|
||||
onRowClick: (record: ServicesList, event: React.MouseEvent) => void;
|
||||
onRowClick: (record: ServicesList) => void;
|
||||
}): JSX.Element => (
|
||||
<div className="services-list-container home-data-item-container metrics-services-list">
|
||||
<div className="services-list">
|
||||
@@ -125,8 +125,8 @@ const ServicesListTable = memo(
|
||||
dataSource={services}
|
||||
pagination={false}
|
||||
className="services-table"
|
||||
onRow={(record: ServicesList): Record<string, unknown> => ({
|
||||
onClick: (event: React.MouseEvent): void => onRowClick(record, event),
|
||||
onRow={(record): { onClick: () => void } => ({
|
||||
onClick: (): void => onRowClick(record),
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
@@ -284,11 +284,11 @@ function ServiceMetrics({
|
||||
}, [onUpdateChecklistDoneItem, loadingUserPreferences, servicesExist]);
|
||||
|
||||
const handleRowClick = useCallback(
|
||||
(record: ServicesList, event: React.MouseEvent) => {
|
||||
(record: ServicesList) => {
|
||||
logEvent('Homepage: Service clicked', {
|
||||
serviceName: record.serviceName,
|
||||
});
|
||||
safeNavigate(`${ROUTES.APPLICATION}/${record.serviceName}`, { event });
|
||||
safeNavigate(`${ROUTES.APPLICATION}/${record.serviceName}`);
|
||||
},
|
||||
[safeNavigate],
|
||||
);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Button, Select, Skeleton, Table } from 'antd';
|
||||
@@ -172,13 +172,13 @@ export default function ServiceTraces({
|
||||
dataSource={top5Services}
|
||||
pagination={false}
|
||||
className="services-table"
|
||||
onRow={(record: ServicesList): Record<string, unknown> => ({
|
||||
onClick: (event: React.MouseEvent): void => {
|
||||
onRow={(record): { onClick: () => void } => ({
|
||||
onClick: (): void => {
|
||||
logEvent('Homepage: Service clicked', {
|
||||
serviceName: record.serviceName,
|
||||
});
|
||||
|
||||
safeNavigate(`${ROUTES.APPLICATION}/${record.serviceName}`, { event });
|
||||
safeNavigate(`${ROUTES.APPLICATION}/${record.serviceName}`);
|
||||
},
|
||||
})}
|
||||
/>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { LoadingOutlined } from '@ant-design/icons';
|
||||
import {
|
||||
Skeleton,
|
||||
@@ -11,7 +11,6 @@ import {
|
||||
import { SorterResult } from 'antd/es/table/interface';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { InfraMonitoringEvents } from 'constants/events';
|
||||
import { isModifierKeyPressed, openInNewTab } from 'utils/navigation';
|
||||
|
||||
import HostsEmptyOrIncorrectMetrics from './HostsEmptyOrIncorrectMetrics';
|
||||
import {
|
||||
@@ -77,16 +76,7 @@ export default function HostsListTable({
|
||||
[],
|
||||
);
|
||||
|
||||
const handleRowClick = (
|
||||
record: HostRowData,
|
||||
event: React.MouseEvent,
|
||||
): void => {
|
||||
if (isModifierKeyPressed(event)) {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
params.set('hostName', record.hostName);
|
||||
openInNewTab(`${window.location.pathname}?${params.toString()}`);
|
||||
return;
|
||||
}
|
||||
const handleRowClick = (record: HostRowData): void => {
|
||||
onHostClick(record.hostName);
|
||||
logEvent(InfraMonitoringEvents.ItemClicked, {
|
||||
entity: InfraMonitoringEvents.HostEntity,
|
||||
@@ -190,8 +180,8 @@ export default function HostsListTable({
|
||||
tableLayout="fixed"
|
||||
rowKey={(record): string => record.hostName}
|
||||
onChange={handleTableChange}
|
||||
onRow={(record: HostRowData): Record<string, unknown> => ({
|
||||
onClick: (event: React.MouseEvent): void => handleRowClick(record, event),
|
||||
onRow={(record): { onClick: () => void; className: string } => ({
|
||||
onClick: (): void => handleRowClick(record),
|
||||
className: 'clickable-row',
|
||||
})}
|
||||
/>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/* eslint-disable no-restricted-syntax */
|
||||
/* eslint-disable @typescript-eslint/explicit-function-return-type */
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useSearchParams } from 'react-router-dom-v5-compat';
|
||||
import { LoadingOutlined } from '@ant-design/icons';
|
||||
@@ -24,7 +24,6 @@ import { ChevronDown, ChevronRight } from 'lucide-react';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
import { isModifierKeyPressed, openInNewTab } from 'utils/navigation';
|
||||
|
||||
import { FeatureKeys } from '../../../constants/features';
|
||||
import { useAppContext } from '../../../providers/App/App';
|
||||
@@ -452,19 +451,7 @@ function K8sClustersList({
|
||||
);
|
||||
}, [selectedClusterName, groupBy.length, clustersData, nestedClustersData]);
|
||||
|
||||
const handleRowClick = (
|
||||
record: K8sClustersRowData,
|
||||
event: React.MouseEvent,
|
||||
): void => {
|
||||
if (event && isModifierKeyPressed(event)) {
|
||||
const newParams = new URLSearchParams(searchParams);
|
||||
newParams.set(
|
||||
INFRA_MONITORING_K8S_PARAMS_KEYS.CLUSTER_NAME,
|
||||
record.clusterUID,
|
||||
);
|
||||
openInNewTab(`${window.location.pathname}?${newParams.toString()}`);
|
||||
return;
|
||||
}
|
||||
const handleRowClick = (record: K8sClustersRowData): void => {
|
||||
if (groupBy.length === 0) {
|
||||
setSelectedRowData(null);
|
||||
setselectedClusterName(record.clusterUID);
|
||||
@@ -528,19 +515,8 @@ function K8sClustersList({
|
||||
indicator: <Spin indicator={<LoadingOutlined size={14} spin />} />,
|
||||
}}
|
||||
showHeader={false}
|
||||
onRow={(
|
||||
record,
|
||||
): { onClick: (event: React.MouseEvent) => void; className: string } => ({
|
||||
onClick: (event: React.MouseEvent): void => {
|
||||
if (event && isModifierKeyPressed(event)) {
|
||||
const newParams = new URLSearchParams(searchParams);
|
||||
newParams.set(
|
||||
INFRA_MONITORING_K8S_PARAMS_KEYS.CLUSTER_NAME,
|
||||
record.clusterUID,
|
||||
);
|
||||
openInNewTab(`${window.location.pathname}?${newParams.toString()}`);
|
||||
return;
|
||||
}
|
||||
onRow={(record): { onClick: () => void; className: string } => ({
|
||||
onClick: (): void => {
|
||||
setselectedClusterName(record.clusterUID);
|
||||
},
|
||||
className: 'expanded-clickable-row',
|
||||
@@ -731,10 +707,8 @@ function K8sClustersList({
|
||||
}}
|
||||
tableLayout="fixed"
|
||||
onChange={handleTableChange}
|
||||
onRow={(
|
||||
record,
|
||||
): { onClick: (event: React.MouseEvent) => void; className: string } => ({
|
||||
onClick: (event: React.MouseEvent): void => handleRowClick(record, event),
|
||||
onRow={(record): { onClick: () => void; className: string } => ({
|
||||
onClick: (): void => handleRowClick(record),
|
||||
className: 'clickable-row',
|
||||
})}
|
||||
expandable={{
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/* eslint-disable no-restricted-syntax */
|
||||
/* eslint-disable @typescript-eslint/explicit-function-return-type */
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useSearchParams } from 'react-router-dom-v5-compat';
|
||||
import { LoadingOutlined } from '@ant-design/icons';
|
||||
@@ -25,7 +25,6 @@ import { ChevronDown, ChevronRight } from 'lucide-react';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
import { isModifierKeyPressed, openInNewTab } from 'utils/navigation';
|
||||
|
||||
import { FeatureKeys } from '../../../constants/features';
|
||||
import { useAppContext } from '../../../providers/App/App';
|
||||
@@ -458,19 +457,7 @@ function K8sDaemonSetsList({
|
||||
nestedDaemonSetsData,
|
||||
]);
|
||||
|
||||
const handleRowClick = (
|
||||
record: K8sDaemonSetsRowData,
|
||||
event?: React.MouseEvent,
|
||||
): void => {
|
||||
if (event && isModifierKeyPressed(event)) {
|
||||
const newParams = new URLSearchParams(searchParams);
|
||||
newParams.set(
|
||||
INFRA_MONITORING_K8S_PARAMS_KEYS.DAEMONSET_UID,
|
||||
record.daemonsetUID,
|
||||
);
|
||||
openInNewTab(`${window.location.pathname}?${newParams.toString()}`);
|
||||
return;
|
||||
}
|
||||
const handleRowClick = (record: K8sDaemonSetsRowData): void => {
|
||||
if (groupBy.length === 0) {
|
||||
setSelectedRowData(null);
|
||||
setSelectedDaemonSetUID(record.daemonsetUID);
|
||||
@@ -534,19 +521,8 @@ function K8sDaemonSetsList({
|
||||
indicator: <Spin indicator={<LoadingOutlined size={14} spin />} />,
|
||||
}}
|
||||
showHeader={false}
|
||||
onRow={(
|
||||
record,
|
||||
): { onClick: (event: React.MouseEvent) => void; className: string } => ({
|
||||
onClick: (event: React.MouseEvent): void => {
|
||||
if (event && isModifierKeyPressed(event)) {
|
||||
const newParams = new URLSearchParams(searchParams);
|
||||
newParams.set(
|
||||
INFRA_MONITORING_K8S_PARAMS_KEYS.DAEMONSET_UID,
|
||||
record.daemonsetUID,
|
||||
);
|
||||
openInNewTab(`${window.location.pathname}?${newParams.toString()}`);
|
||||
return;
|
||||
}
|
||||
onRow={(record): { onClick: () => void; className: string } => ({
|
||||
onClick: (): void => {
|
||||
setSelectedDaemonSetUID(record.daemonsetUID);
|
||||
},
|
||||
className: 'expanded-clickable-row',
|
||||
@@ -739,10 +715,8 @@ function K8sDaemonSetsList({
|
||||
}}
|
||||
tableLayout="fixed"
|
||||
onChange={handleTableChange}
|
||||
onRow={(
|
||||
record,
|
||||
): { onClick: (event: React.MouseEvent) => void; className: string } => ({
|
||||
onClick: (event: React.MouseEvent): void => handleRowClick(record, event),
|
||||
onRow={(record): { onClick: () => void; className: string } => ({
|
||||
onClick: (): void => handleRowClick(record),
|
||||
className: 'clickable-row',
|
||||
})}
|
||||
expandable={{
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/* eslint-disable no-restricted-syntax */
|
||||
/* eslint-disable @typescript-eslint/explicit-function-return-type */
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useSearchParams } from 'react-router-dom-v5-compat';
|
||||
import { LoadingOutlined } from '@ant-design/icons';
|
||||
@@ -25,7 +25,6 @@ import { ChevronDown, ChevronRight } from 'lucide-react';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
import { isModifierKeyPressed, openInNewTab } from 'utils/navigation';
|
||||
|
||||
import { FeatureKeys } from '../../../constants/features';
|
||||
import { useAppContext } from '../../../providers/App/App';
|
||||
@@ -464,19 +463,7 @@ function K8sDeploymentsList({
|
||||
nestedDeploymentsData,
|
||||
]);
|
||||
|
||||
const handleRowClick = (
|
||||
record: K8sDeploymentsRowData,
|
||||
event?: React.MouseEvent,
|
||||
): void => {
|
||||
if (event && isModifierKeyPressed(event)) {
|
||||
const newParams = new URLSearchParams(searchParams);
|
||||
newParams.set(
|
||||
INFRA_MONITORING_K8S_PARAMS_KEYS.DEPLOYMENT_UID,
|
||||
record.deploymentUID,
|
||||
);
|
||||
openInNewTab(`${window.location.pathname}?${newParams.toString()}`);
|
||||
return;
|
||||
}
|
||||
const handleRowClick = (record: K8sDeploymentsRowData): void => {
|
||||
if (groupBy.length === 0) {
|
||||
setSelectedRowData(null);
|
||||
setselectedDeploymentUID(record.deploymentUID);
|
||||
@@ -540,19 +527,8 @@ function K8sDeploymentsList({
|
||||
indicator: <Spin indicator={<LoadingOutlined size={14} spin />} />,
|
||||
}}
|
||||
showHeader={false}
|
||||
onRow={(
|
||||
record,
|
||||
): { onClick: (event: React.MouseEvent) => void; className: string } => ({
|
||||
onClick: (event: React.MouseEvent): void => {
|
||||
if (event && isModifierKeyPressed(event)) {
|
||||
const newParams = new URLSearchParams(searchParams);
|
||||
newParams.set(
|
||||
INFRA_MONITORING_K8S_PARAMS_KEYS.DEPLOYMENT_UID,
|
||||
record.deploymentUID,
|
||||
);
|
||||
openInNewTab(`${window.location.pathname}?${newParams.toString()}`);
|
||||
return;
|
||||
}
|
||||
onRow={(record): { onClick: () => void; className: string } => ({
|
||||
onClick: (): void => {
|
||||
setselectedDeploymentUID(record.deploymentUID);
|
||||
},
|
||||
className: 'expanded-clickable-row',
|
||||
@@ -746,10 +722,8 @@ function K8sDeploymentsList({
|
||||
}}
|
||||
tableLayout="fixed"
|
||||
onChange={handleTableChange}
|
||||
onRow={(
|
||||
record,
|
||||
): { onClick: (event: React.MouseEvent) => void; className: string } => ({
|
||||
onClick: (event: React.MouseEvent): void => handleRowClick(record, event),
|
||||
onRow={(record): { onClick: () => void; className: string } => ({
|
||||
onClick: (): void => handleRowClick(record),
|
||||
className: 'clickable-row',
|
||||
})}
|
||||
expandable={{
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/* eslint-disable no-restricted-syntax */
|
||||
/* eslint-disable @typescript-eslint/explicit-function-return-type */
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useSearchParams } from 'react-router-dom-v5-compat';
|
||||
import { LoadingOutlined } from '@ant-design/icons';
|
||||
@@ -25,7 +25,6 @@ import { ChevronDown, ChevronRight } from 'lucide-react';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
import { isModifierKeyPressed, openInNewTab } from 'utils/navigation';
|
||||
|
||||
import { FeatureKeys } from '../../../constants/features';
|
||||
import { useAppContext } from '../../../providers/App/App';
|
||||
@@ -429,16 +428,7 @@ function K8sJobsList({
|
||||
return jobsData.find((job) => job.jobName === selectedJobUID) || null;
|
||||
}, [selectedJobUID, groupBy.length, jobsData, nestedJobsData]);
|
||||
|
||||
const handleRowClick = (
|
||||
record: K8sJobsRowData,
|
||||
event: React.MouseEvent,
|
||||
): void => {
|
||||
if (event && isModifierKeyPressed(event)) {
|
||||
const newParams = new URLSearchParams(searchParams);
|
||||
newParams.set(INFRA_MONITORING_K8S_PARAMS_KEYS.JOB_UID, record.jobUID);
|
||||
openInNewTab(`${window.location.pathname}?${newParams.toString()}`);
|
||||
return;
|
||||
}
|
||||
const handleRowClick = (record: K8sJobsRowData): void => {
|
||||
if (groupBy.length === 0) {
|
||||
setSelectedRowData(null);
|
||||
setselectedJobUID(record.jobUID);
|
||||
@@ -502,16 +492,8 @@ function K8sJobsList({
|
||||
indicator: <Spin indicator={<LoadingOutlined size={14} spin />} />,
|
||||
}}
|
||||
showHeader={false}
|
||||
onRow={(
|
||||
record,
|
||||
): { onClick: (event: React.MouseEvent) => void; className: string } => ({
|
||||
onClick: (event: React.MouseEvent): void => {
|
||||
if (event && isModifierKeyPressed(event)) {
|
||||
const newParams = new URLSearchParams(searchParams);
|
||||
newParams.set(INFRA_MONITORING_K8S_PARAMS_KEYS.JOB_UID, record.jobUID);
|
||||
openInNewTab(`${window.location.pathname}?${newParams.toString()}`);
|
||||
return;
|
||||
}
|
||||
onRow={(record): { onClick: () => void; className: string } => ({
|
||||
onClick: (): void => {
|
||||
setselectedJobUID(record.jobUID);
|
||||
},
|
||||
className: 'expanded-clickable-row',
|
||||
@@ -702,10 +684,8 @@ function K8sJobsList({
|
||||
}}
|
||||
tableLayout="fixed"
|
||||
onChange={handleTableChange}
|
||||
onRow={(
|
||||
record,
|
||||
): { onClick: (event: React.MouseEvent) => void; className: string } => ({
|
||||
onClick: (event: React.MouseEvent): void => handleRowClick(record, event),
|
||||
onRow={(record): { onClick: () => void; className: string } => ({
|
||||
onClick: (): void => handleRowClick(record),
|
||||
className: 'clickable-row',
|
||||
})}
|
||||
expandable={{
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/* eslint-disable no-restricted-syntax */
|
||||
/* eslint-disable @typescript-eslint/explicit-function-return-type */
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useSearchParams } from 'react-router-dom-v5-compat';
|
||||
import { LoadingOutlined } from '@ant-design/icons';
|
||||
@@ -24,7 +24,6 @@ import { ChevronDown, ChevronRight } from 'lucide-react';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
import { isModifierKeyPressed, openInNewTab } from 'utils/navigation';
|
||||
|
||||
import { FeatureKeys } from '../../../constants/features';
|
||||
import { useAppContext } from '../../../providers/App/App';
|
||||
@@ -460,19 +459,7 @@ function K8sNamespacesList({
|
||||
nestedNamespacesData,
|
||||
]);
|
||||
|
||||
const handleRowClick = (
|
||||
record: K8sNamespacesRowData,
|
||||
event: React.MouseEvent,
|
||||
): void => {
|
||||
if (event && isModifierKeyPressed(event)) {
|
||||
const newParams = new URLSearchParams(searchParams);
|
||||
newParams.set(
|
||||
INFRA_MONITORING_K8S_PARAMS_KEYS.NAMESPACE_UID,
|
||||
record.namespaceUID,
|
||||
);
|
||||
openInNewTab(`${window.location.pathname}?${newParams.toString()}`);
|
||||
return;
|
||||
}
|
||||
const handleRowClick = (record: K8sNamespacesRowData): void => {
|
||||
if (groupBy.length === 0) {
|
||||
setSelectedRowData(null);
|
||||
setselectedNamespaceUID(record.namespaceUID);
|
||||
@@ -536,19 +523,8 @@ function K8sNamespacesList({
|
||||
indicator: <Spin indicator={<LoadingOutlined size={14} spin />} />,
|
||||
}}
|
||||
showHeader={false}
|
||||
onRow={(
|
||||
record,
|
||||
): { onClick: (event: React.MouseEvent) => void; className: string } => ({
|
||||
onClick: (event: React.MouseEvent): void => {
|
||||
if (isModifierKeyPressed(event)) {
|
||||
const newParams = new URLSearchParams(searchParams);
|
||||
newParams.set(
|
||||
INFRA_MONITORING_K8S_PARAMS_KEYS.NAMESPACE_UID,
|
||||
record.namespaceUID,
|
||||
);
|
||||
openInNewTab(`${window.location.pathname}?${newParams.toString()}`);
|
||||
return;
|
||||
}
|
||||
onRow={(record): { onClick: () => void; className: string } => ({
|
||||
onClick: (): void => {
|
||||
setselectedNamespaceUID(record.namespaceUID);
|
||||
},
|
||||
className: 'expanded-clickable-row',
|
||||
@@ -740,10 +716,8 @@ function K8sNamespacesList({
|
||||
}}
|
||||
tableLayout="fixed"
|
||||
onChange={handleTableChange}
|
||||
onRow={(
|
||||
record,
|
||||
): { onClick: (event: React.MouseEvent) => void; className: string } => ({
|
||||
onClick: (event: React.MouseEvent): void => handleRowClick(record, event),
|
||||
onRow={(record): { onClick: () => void; className: string } => ({
|
||||
onClick: (): void => handleRowClick(record),
|
||||
className: 'clickable-row',
|
||||
})}
|
||||
expandable={{
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/* eslint-disable no-restricted-syntax */
|
||||
/* eslint-disable @typescript-eslint/explicit-function-return-type */
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useSearchParams } from 'react-router-dom-v5-compat';
|
||||
import { LoadingOutlined } from '@ant-design/icons';
|
||||
@@ -24,7 +24,6 @@ import { ChevronDown, ChevronRight } from 'lucide-react';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
import { isModifierKeyPressed, openInNewTab } from 'utils/navigation';
|
||||
|
||||
import { FeatureKeys } from '../../../constants/features';
|
||||
import { useAppContext } from '../../../providers/App/App';
|
||||
@@ -439,16 +438,7 @@ function K8sNodesList({
|
||||
return nodesData.find((node) => node.nodeUID === selectedNodeUID) || null;
|
||||
}, [selectedNodeUID, groupBy.length, nodesData, nestedNodesData]);
|
||||
|
||||
const handleRowClick = (
|
||||
record: K8sNodesRowData,
|
||||
event: React.MouseEvent,
|
||||
): void => {
|
||||
if (event && isModifierKeyPressed(event)) {
|
||||
const newParams = new URLSearchParams(searchParams);
|
||||
newParams.set(INFRA_MONITORING_K8S_PARAMS_KEYS.NODE_UID, record.nodeUID);
|
||||
openInNewTab(`${window.location.pathname}?${newParams.toString()}`);
|
||||
return;
|
||||
}
|
||||
const handleRowClick = (record: K8sNodesRowData): void => {
|
||||
if (groupBy.length === 0) {
|
||||
setSelectedRowData(null);
|
||||
setSelectedNodeUID(record.nodeUID);
|
||||
@@ -513,19 +503,8 @@ function K8sNodesList({
|
||||
indicator: <Spin indicator={<LoadingOutlined size={14} spin />} />,
|
||||
}}
|
||||
showHeader={false}
|
||||
onRow={(
|
||||
record,
|
||||
): { onClick: (event: React.MouseEvent) => void; className: string } => ({
|
||||
onClick: (event: React.MouseEvent): void => {
|
||||
if (isModifierKeyPressed(event)) {
|
||||
const newParams = new URLSearchParams(searchParams);
|
||||
newParams.set(
|
||||
INFRA_MONITORING_K8S_PARAMS_KEYS.NODE_UID,
|
||||
record.nodeUID,
|
||||
);
|
||||
openInNewTab(`${window.location.pathname}?${newParams.toString()}`);
|
||||
return;
|
||||
}
|
||||
onRow={(record): { onClick: () => void; className: string } => ({
|
||||
onClick: (): void => {
|
||||
setSelectedNodeUID(record.nodeUID);
|
||||
},
|
||||
className: 'expanded-clickable-row',
|
||||
@@ -716,10 +695,8 @@ function K8sNodesList({
|
||||
}}
|
||||
tableLayout="fixed"
|
||||
onChange={handleTableChange}
|
||||
onRow={(
|
||||
record,
|
||||
): { onClick: (event: React.MouseEvent) => void; className: string } => ({
|
||||
onClick: (event: React.MouseEvent): void => handleRowClick(record, event),
|
||||
onRow={(record): { onClick: () => void; className: string } => ({
|
||||
onClick: (): void => handleRowClick(record),
|
||||
className: 'clickable-row',
|
||||
})}
|
||||
expandable={{
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/* eslint-disable no-restricted-syntax */
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useSearchParams } from 'react-router-dom-v5-compat';
|
||||
import { LoadingOutlined } from '@ant-design/icons';
|
||||
@@ -26,7 +26,6 @@ import { ChevronDown, ChevronRight, CornerDownRight } from 'lucide-react';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
import { isModifierKeyPressed, openInNewTab } from 'utils/navigation';
|
||||
|
||||
import { FeatureKeys } from '../../../constants/features';
|
||||
import { useAppContext } from '../../../providers/App/App';
|
||||
@@ -496,16 +495,7 @@ function K8sPodsList({
|
||||
}
|
||||
}, [selectedRowData, fetchGroupedByRowData]);
|
||||
|
||||
const handleRowClick = (
|
||||
record: K8sPodsRowData,
|
||||
event: React.MouseEvent,
|
||||
): void => {
|
||||
if (event && isModifierKeyPressed(event)) {
|
||||
const newParams = new URLSearchParams(searchParams);
|
||||
newParams.set(INFRA_MONITORING_K8S_PARAMS_KEYS.POD_UID, record.podUID);
|
||||
openInNewTab(`${window.location.pathname}?${newParams.toString()}`);
|
||||
return;
|
||||
}
|
||||
const handleRowClick = (record: K8sPodsRowData): void => {
|
||||
if (groupBy.length === 0) {
|
||||
setSelectedPodUID(record.podUID);
|
||||
setSearchParams({
|
||||
@@ -625,14 +615,8 @@ function K8sPodsList({
|
||||
spinning: isFetchingGroupedByRowData || isLoadingGroupedByRowData,
|
||||
indicator: <Spin indicator={<LoadingOutlined size={14} spin />} />,
|
||||
}}
|
||||
onRow={(record: K8sPodsRowData): Record<string, unknown> => ({
|
||||
onClick: (event: React.MouseEvent): void => {
|
||||
if (isModifierKeyPressed(event)) {
|
||||
const newParams = new URLSearchParams(searchParams);
|
||||
newParams.set(INFRA_MONITORING_K8S_PARAMS_KEYS.POD_UID, record.podUID);
|
||||
openInNewTab(`${window.location.pathname}?${newParams.toString()}`);
|
||||
return;
|
||||
}
|
||||
onRow={(record): { onClick: () => void; className: string } => ({
|
||||
onClick: (): void => {
|
||||
setSelectedPodUID(record.podUID);
|
||||
},
|
||||
className: 'expanded-clickable-row',
|
||||
@@ -768,8 +752,8 @@ function K8sPodsList({
|
||||
scroll={{ x: true }}
|
||||
tableLayout="fixed"
|
||||
onChange={handleTableChange}
|
||||
onRow={(record: K8sPodsRowData): Record<string, unknown> => ({
|
||||
onClick: (event: React.MouseEvent): void => handleRowClick(record, event),
|
||||
onRow={(record): { onClick: () => void; className: string } => ({
|
||||
onClick: (): void => handleRowClick(record),
|
||||
className: 'clickable-row',
|
||||
})}
|
||||
expandable={{
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/* eslint-disable no-restricted-syntax */
|
||||
/* eslint-disable @typescript-eslint/explicit-function-return-type */
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useSearchParams } from 'react-router-dom-v5-compat';
|
||||
import { LoadingOutlined } from '@ant-design/icons';
|
||||
@@ -25,7 +25,6 @@ import { ChevronDown, ChevronRight } from 'lucide-react';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
import { isModifierKeyPressed, openInNewTab } from 'utils/navigation';
|
||||
|
||||
import { FeatureKeys } from '../../../constants/features';
|
||||
import { useAppContext } from '../../../providers/App/App';
|
||||
@@ -461,19 +460,7 @@ function K8sStatefulSetsList({
|
||||
nestedStatefulSetsData,
|
||||
]);
|
||||
|
||||
const handleRowClick = (
|
||||
record: K8sStatefulSetsRowData,
|
||||
event: React.MouseEvent,
|
||||
): void => {
|
||||
if (event && isModifierKeyPressed(event)) {
|
||||
const newParams = new URLSearchParams(searchParams);
|
||||
newParams.set(
|
||||
INFRA_MONITORING_K8S_PARAMS_KEYS.STATEFULSET_UID,
|
||||
record.statefulsetUID,
|
||||
);
|
||||
openInNewTab(`${window.location.pathname}?${newParams.toString()}`);
|
||||
return;
|
||||
}
|
||||
const handleRowClick = (record: K8sStatefulSetsRowData): void => {
|
||||
if (groupBy.length === 0) {
|
||||
setSelectedRowData(null);
|
||||
setselectedStatefulSetUID(record.statefulsetUID);
|
||||
@@ -537,19 +524,8 @@ function K8sStatefulSetsList({
|
||||
indicator: <Spin indicator={<LoadingOutlined size={14} spin />} />,
|
||||
}}
|
||||
showHeader={false}
|
||||
onRow={(
|
||||
record,
|
||||
): { onClick: (event: React.MouseEvent) => void; className: string } => ({
|
||||
onClick: (event: React.MouseEvent): void => {
|
||||
if (event && isModifierKeyPressed(event)) {
|
||||
const newParams = new URLSearchParams(searchParams);
|
||||
newParams.set(
|
||||
INFRA_MONITORING_K8S_PARAMS_KEYS.STATEFULSET_UID,
|
||||
record.statefulsetUID,
|
||||
);
|
||||
openInNewTab(`${window.location.pathname}?${newParams.toString()}`);
|
||||
return;
|
||||
}
|
||||
onRow={(record): { onClick: () => void; className: string } => ({
|
||||
onClick: (): void => {
|
||||
setselectedStatefulSetUID(record.statefulsetUID);
|
||||
},
|
||||
className: 'expanded-clickable-row',
|
||||
@@ -742,8 +718,8 @@ function K8sStatefulSetsList({
|
||||
}}
|
||||
tableLayout="fixed"
|
||||
onChange={handleTableChange}
|
||||
onRow={(record) => ({
|
||||
onClick: (event: React.MouseEvent): void => handleRowClick(record, event),
|
||||
onRow={(record): { onClick: () => void; className: string } => ({
|
||||
onClick: (): void => handleRowClick(record),
|
||||
className: 'clickable-row',
|
||||
})}
|
||||
expandable={{
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/* eslint-disable no-restricted-syntax */
|
||||
/* eslint-disable @typescript-eslint/explicit-function-return-type */
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useSearchParams } from 'react-router-dom-v5-compat';
|
||||
import { LoadingOutlined } from '@ant-design/icons';
|
||||
@@ -25,7 +25,6 @@ import { ChevronDown, ChevronRight } from 'lucide-react';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
import { isModifierKeyPressed, openInNewTab } from 'utils/navigation';
|
||||
|
||||
import { FeatureKeys } from '../../../constants/features';
|
||||
import { useAppContext } from '../../../providers/App/App';
|
||||
@@ -391,16 +390,7 @@ function K8sVolumesList({
|
||||
);
|
||||
}, [selectedVolumeUID, volumesData, groupBy.length, nestedVolumesData]);
|
||||
|
||||
const handleRowClick = (
|
||||
record: K8sVolumesRowData,
|
||||
event: React.MouseEvent,
|
||||
): void => {
|
||||
if (event && isModifierKeyPressed(event)) {
|
||||
const newParams = new URLSearchParams(searchParams);
|
||||
newParams.set(INFRA_MONITORING_K8S_PARAMS_KEYS.VOLUME_UID, record.volumeUID);
|
||||
openInNewTab(`${window.location.pathname}?${newParams.toString()}`);
|
||||
return;
|
||||
}
|
||||
const handleRowClick = (record: K8sVolumesRowData): void => {
|
||||
if (groupBy.length === 0) {
|
||||
setSelectedRowData(null);
|
||||
setselectedVolumeUID(record.volumeUID);
|
||||
@@ -464,19 +454,8 @@ function K8sVolumesList({
|
||||
indicator: <Spin indicator={<LoadingOutlined size={14} spin />} />,
|
||||
}}
|
||||
showHeader={false}
|
||||
onRow={(
|
||||
record,
|
||||
): { onClick: (event: React.MouseEvent) => void; className: string } => ({
|
||||
onClick: (event: React.MouseEvent): void => {
|
||||
if (event && isModifierKeyPressed(event)) {
|
||||
const newParams = new URLSearchParams(searchParams);
|
||||
newParams.set(
|
||||
INFRA_MONITORING_K8S_PARAMS_KEYS.VOLUME_UID,
|
||||
record.volumeUID,
|
||||
);
|
||||
openInNewTab(`${window.location.pathname}?${newParams.toString()}`);
|
||||
return;
|
||||
}
|
||||
onRow={(record): { onClick: () => void; className: string } => ({
|
||||
onClick: (): void => {
|
||||
setselectedVolumeUID(record.volumeUID);
|
||||
},
|
||||
className: 'expanded-clickable-row',
|
||||
@@ -662,10 +641,8 @@ function K8sVolumesList({
|
||||
}}
|
||||
tableLayout="fixed"
|
||||
onChange={handleTableChange}
|
||||
onRow={(
|
||||
record,
|
||||
): { onClick: (event: React.MouseEvent) => void; className: string } => ({
|
||||
onClick: (event: React.MouseEvent): void => handleRowClick(record, event),
|
||||
onRow={(record): { onClick: () => void; className: string } => ({
|
||||
onClick: (): void => handleRowClick(record),
|
||||
className: 'clickable-row',
|
||||
})}
|
||||
expandable={{
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import { useCallback, useState } from 'react';
|
||||
import { PlusOutlined } from '@ant-design/icons';
|
||||
import { Button, Divider, Typography } from 'antd';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
@@ -7,7 +7,6 @@ import useComponentPermission from 'hooks/useComponentPermission';
|
||||
import history from 'lib/history';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
import { isModifierKeyPressed, openInNewTab } from 'utils/navigation';
|
||||
|
||||
import AlertInfoCard from './AlertInfoCard';
|
||||
import { ALERT_CARDS, ALERT_INFO_LINKS } from './alertLinks';
|
||||
@@ -37,13 +36,9 @@ export function AlertsEmptyState(): JSX.Element {
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const onClickNewAlertHandler = useCallback((e: React.MouseEvent) => {
|
||||
const onClickNewAlertHandler = useCallback(() => {
|
||||
setLoading(false);
|
||||
if (isModifierKeyPressed(e)) {
|
||||
openInNewTab(ROUTES.ALERTS_NEW);
|
||||
} else {
|
||||
history.push(ROUTES.ALERTS_NEW);
|
||||
}
|
||||
history.push(ROUTES.ALERTS_NEW);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/* eslint-disable react/display-name */
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import { useCallback, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { UseQueryResult } from 'react-query';
|
||||
import { PlusOutlined } from '@ant-design/icons';
|
||||
@@ -100,22 +100,16 @@ function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element {
|
||||
});
|
||||
}, [notificationsApi, t]);
|
||||
|
||||
const onClickNewAlertHandler = useCallback(
|
||||
(e: React.MouseEvent): void => {
|
||||
logEvent('Alert: New alert button clicked', {
|
||||
number: allAlertRules?.length,
|
||||
layout: 'new',
|
||||
});
|
||||
safeNavigate(ROUTES.ALERT_TYPE_SELECTION, { event: e });
|
||||
},
|
||||
const onClickNewAlertHandler = useCallback(() => {
|
||||
logEvent('Alert: New alert button clicked', {
|
||||
number: allAlertRules?.length,
|
||||
layout: 'new',
|
||||
});
|
||||
safeNavigate(ROUTES.ALERT_TYPE_SELECTION);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[],
|
||||
);
|
||||
}, []);
|
||||
|
||||
const onEditHandler = (
|
||||
record: GettableAlert,
|
||||
options?: { event?: React.MouseEvent; newTab?: boolean },
|
||||
): void => {
|
||||
const onEditHandler = (record: GettableAlert, openInNewTab: boolean): void => {
|
||||
const compositeQuery = sanitizeDefaultAlertQuery(
|
||||
mapQueryDataFromApi(record.condition.compositeQuery),
|
||||
record.alertType as AlertTypes,
|
||||
@@ -131,10 +125,11 @@ function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element {
|
||||
|
||||
setEditLoader(false);
|
||||
|
||||
safeNavigate(`${ROUTES.ALERT_OVERVIEW}?${params.toString()}`, {
|
||||
event: options?.event,
|
||||
newTab: options?.newTab,
|
||||
});
|
||||
if (openInNewTab) {
|
||||
window.open(`${ROUTES.ALERT_OVERVIEW}?${params.toString()}`, '_blank');
|
||||
} else {
|
||||
safeNavigate(`${ROUTES.ALERT_OVERVIEW}?${params.toString()}`);
|
||||
}
|
||||
};
|
||||
|
||||
const onCloneHandler = (
|
||||
@@ -271,7 +266,7 @@ function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element {
|
||||
const onClickHandler = (e: React.MouseEvent<HTMLElement>): void => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
onEditHandler(record, { event: e });
|
||||
onEditHandler(record, e.metaKey || e.ctrlKey);
|
||||
};
|
||||
|
||||
return <Typography.Link onClick={onClickHandler}>{value}</Typography.Link>;
|
||||
@@ -336,9 +331,7 @@ function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element {
|
||||
/>,
|
||||
<ColumnButton
|
||||
key="2"
|
||||
onClick={(e: React.MouseEvent): void =>
|
||||
onEditHandler(record, { event: e })
|
||||
}
|
||||
onClick={(): void => onEditHandler(record, false)}
|
||||
type="link"
|
||||
loading={editLoader}
|
||||
>
|
||||
@@ -346,7 +339,7 @@ function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element {
|
||||
</ColumnButton>,
|
||||
<ColumnButton
|
||||
key="3"
|
||||
onClick={(): void => onEditHandler(record, { newTab: true })}
|
||||
onClick={(): void => onEditHandler(record, true)}
|
||||
type="link"
|
||||
loading={editLoader}
|
||||
>
|
||||
|
||||
@@ -416,7 +416,11 @@ function DashboardsList(): JSX.Element {
|
||||
|
||||
const onClickHandler = (event: React.MouseEvent<HTMLElement>): void => {
|
||||
event.stopPropagation();
|
||||
safeNavigate(getLink(), { event });
|
||||
if (event.metaKey || event.ctrlKey) {
|
||||
window.open(getLink(), '_blank');
|
||||
} else {
|
||||
safeNavigate(getLink());
|
||||
}
|
||||
logEvent('Dashboard List: Clicked on dashboard', {
|
||||
dashboardId: dashboard.id,
|
||||
dashboardName: dashboard.name,
|
||||
|
||||
@@ -31,7 +31,6 @@ import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
import { isModifierKeyPressed, openInNewTab } from 'utils/navigation';
|
||||
import { secondsToMilliseconds } from 'utils/timeUtils';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
@@ -236,7 +235,7 @@ function Application(): JSX.Element {
|
||||
timestamp: number,
|
||||
apmToTraceQuery: Query,
|
||||
isViewLogsClicked?: boolean,
|
||||
): ((e: React.MouseEvent) => void) => (e: React.MouseEvent): void => {
|
||||
): (() => void) => (): void => {
|
||||
const endTime = secondsToMilliseconds(timestamp);
|
||||
const startTime = secondsToMilliseconds(timestamp - stepInterval);
|
||||
|
||||
@@ -260,11 +259,7 @@ function Application(): JSX.Element {
|
||||
queryString,
|
||||
);
|
||||
|
||||
if (isModifierKeyPressed(e)) {
|
||||
openInNewTab(newPath);
|
||||
} else {
|
||||
history.push(newPath);
|
||||
}
|
||||
history.push(newPath);
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[stepInterval],
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import React from 'react';
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { Button } from 'antd';
|
||||
import { Binoculars, DraftingCompass, ScrollText } from 'lucide-react';
|
||||
@@ -7,9 +6,9 @@ import './GraphControlsPanel.styles.scss';
|
||||
|
||||
interface GraphControlsPanelProps {
|
||||
id: string;
|
||||
onViewLogsClick?: (e: React.MouseEvent) => void;
|
||||
onViewTracesClick: (e: React.MouseEvent) => void;
|
||||
onViewAPIMonitoringClick?: (e: React.MouseEvent) => void;
|
||||
onViewLogsClick?: () => void;
|
||||
onViewTracesClick: () => void;
|
||||
onViewAPIMonitoringClick?: () => void;
|
||||
}
|
||||
|
||||
function GraphControlsPanel({
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { Dispatch, SetStateAction, useMemo, useRef } from 'react';
|
||||
import { Dispatch, SetStateAction, useMemo, useRef } from 'react';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import ROUTES from 'constants/routes';
|
||||
@@ -42,10 +42,7 @@ interface OnViewTracePopupClickProps {
|
||||
apmToTraceQuery: Query;
|
||||
isViewLogsClicked?: boolean;
|
||||
stepInterval?: number;
|
||||
safeNavigate: (
|
||||
url: string,
|
||||
options?: { event?: React.MouseEvent | MouseEvent },
|
||||
) => void;
|
||||
safeNavigate: (url: string) => void;
|
||||
}
|
||||
|
||||
interface OnViewAPIMonitoringPopupClickProps {
|
||||
@@ -54,10 +51,8 @@ interface OnViewAPIMonitoringPopupClickProps {
|
||||
stepInterval?: number;
|
||||
domainName: string;
|
||||
isError: boolean;
|
||||
safeNavigate: (
|
||||
url: string,
|
||||
options?: { event?: React.MouseEvent | MouseEvent },
|
||||
) => void;
|
||||
|
||||
safeNavigate: (url: string) => void;
|
||||
}
|
||||
|
||||
export function generateExplorerPath(
|
||||
@@ -98,8 +93,8 @@ export function onViewTracePopupClick({
|
||||
isViewLogsClicked,
|
||||
stepInterval,
|
||||
safeNavigate,
|
||||
}: OnViewTracePopupClickProps): (e?: React.MouseEvent) => void {
|
||||
return (e?: React.MouseEvent): void => {
|
||||
}: OnViewTracePopupClickProps): VoidFunction {
|
||||
return (): void => {
|
||||
const endTime = secondsToMilliseconds(timestamp);
|
||||
const startTime = secondsToMilliseconds(timestamp - (stepInterval || 60));
|
||||
|
||||
@@ -123,7 +118,7 @@ export function onViewTracePopupClick({
|
||||
queryString,
|
||||
);
|
||||
|
||||
safeNavigate(newPath, { event: e });
|
||||
safeNavigate(newPath);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -154,8 +149,8 @@ export function onViewAPIMonitoringPopupClick({
|
||||
isError,
|
||||
stepInterval,
|
||||
safeNavigate,
|
||||
}: OnViewAPIMonitoringPopupClickProps): (e?: React.MouseEvent) => void {
|
||||
return (e?: React.MouseEvent): void => {
|
||||
}: OnViewAPIMonitoringPopupClickProps): VoidFunction {
|
||||
return (): void => {
|
||||
const endTime = timestamp + (stepInterval || 60);
|
||||
const startTime = timestamp - (stepInterval || 60);
|
||||
const filters = {
|
||||
@@ -195,7 +190,7 @@ export function onViewAPIMonitoringPopupClick({
|
||||
filters,
|
||||
);
|
||||
|
||||
safeNavigate(newPath, { event: e });
|
||||
safeNavigate(newPath);
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,6 @@ import { useSafeNavigate } from 'hooks/useSafeNavigate';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
import history from 'lib/history';
|
||||
import { Bell, Grid } from 'lucide-react';
|
||||
import { navigateToPage } from 'utils/navigation';
|
||||
|
||||
import { DashboardsAndAlertsPopoverProps } from './types';
|
||||
|
||||
@@ -26,10 +25,9 @@ function DashboardsAndAlertsPopover({
|
||||
label: (
|
||||
<Typography.Link
|
||||
key={alert.alert_id}
|
||||
onClick={(e): void => {
|
||||
onClick={(): void => {
|
||||
params.set(QueryParams.ruleId, alert.alert_id);
|
||||
const path = `${ROUTES.ALERT_OVERVIEW}?${params.toString()}`;
|
||||
navigateToPage(path, history.push, e);
|
||||
history.push(`${ROUTES.ALERT_OVERVIEW}?${params.toString()}`);
|
||||
}}
|
||||
className="dashboards-popover-content-item"
|
||||
>
|
||||
@@ -57,12 +55,11 @@ function DashboardsAndAlertsPopover({
|
||||
label: (
|
||||
<Typography.Link
|
||||
key={dashboard.dashboard_id}
|
||||
onClick={(e): void => {
|
||||
onClick={(): void => {
|
||||
safeNavigate(
|
||||
generatePath(ROUTES.DASHBOARD, {
|
||||
dashboardId: dashboard.dashboard_id,
|
||||
}),
|
||||
{ event: e },
|
||||
);
|
||||
}}
|
||||
className="dashboards-popover-content-item"
|
||||
|
||||
@@ -118,7 +118,6 @@ describe('DashboardsAndAlertsPopover', () => {
|
||||
// Should navigate to the dashboard
|
||||
expect(mockSafeNavigate).toHaveBeenCalledWith(
|
||||
`/dashboard/${mockDashboard1.dashboard_id}`,
|
||||
expect.objectContaining({ event: expect.any(Object) }),
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import { useCallback } from 'react';
|
||||
import { LoadingOutlined } from '@ant-design/icons';
|
||||
import {
|
||||
Spin,
|
||||
@@ -107,9 +107,8 @@ function MetricsTable({
|
||||
onChange: onPaginationChange,
|
||||
total: totalCount,
|
||||
}}
|
||||
onRow={(record: MetricsListItemRowData): Record<string, unknown> => ({
|
||||
onClick: (event: React.MouseEvent): void =>
|
||||
openMetricDetails(record.key, 'list', event),
|
||||
onRow={(record): { onClick: () => void; className: string } => ({
|
||||
onClick: (): void => openMetricDetails(record.key, 'list'),
|
||||
className: 'clickable-row',
|
||||
})}
|
||||
/>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/* eslint-disable no-nested-ternary */
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useSearchParams } from 'react-router-dom-v5-compat';
|
||||
import * as Sentry from '@sentry/react';
|
||||
@@ -14,7 +14,6 @@ import { AppState } from 'store/reducers';
|
||||
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
import { isModifierKeyPressed, openInNewTab } from 'utils/navigation';
|
||||
|
||||
import { MetricsExplorerEventKeys, MetricsExplorerEvents } from '../events';
|
||||
import InspectModal from '../Inspect';
|
||||
@@ -210,15 +209,7 @@ function Summary(): JSX.Element {
|
||||
const openMetricDetails = (
|
||||
metricName: string,
|
||||
view: 'list' | 'treemap',
|
||||
event?: React.MouseEvent,
|
||||
): void => {
|
||||
if (event && isModifierKeyPressed(event)) {
|
||||
const newParams = new URLSearchParams(searchParams);
|
||||
newParams.set(IS_METRIC_DETAILS_OPEN_KEY, 'true');
|
||||
newParams.set(SELECTED_METRIC_NAME_KEY, metricName);
|
||||
openInNewTab(`${window.location.pathname}?${newParams.toString()}`);
|
||||
return;
|
||||
}
|
||||
setSelectedMetricName(metricName);
|
||||
setIsMetricDetailsOpen(true);
|
||||
setSearchParams({
|
||||
|
||||
@@ -194,11 +194,7 @@ describe('MetricsTable', () => {
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByText('Metric 1'));
|
||||
expect(mockOpenMetricDetails).toHaveBeenCalledWith(
|
||||
'metric1',
|
||||
'list',
|
||||
expect.any(Object),
|
||||
);
|
||||
expect(mockOpenMetricDetails).toHaveBeenCalledWith('metric1', 'list');
|
||||
});
|
||||
|
||||
it('calls setOrderBy when column header is clicked', () => {
|
||||
|
||||
@@ -14,11 +14,7 @@ export interface MetricsTableProps {
|
||||
onPaginationChange: (page: number, pageSize: number) => void;
|
||||
setOrderBy: (orderBy: OrderByPayload) => void;
|
||||
totalCount: number;
|
||||
openMetricDetails: (
|
||||
metricName: string,
|
||||
view: 'list' | 'treemap',
|
||||
event?: React.MouseEvent,
|
||||
) => void;
|
||||
openMetricDetails: (metricName: string, view: 'list' | 'treemap') => void;
|
||||
queryFilters: TagFilter;
|
||||
}
|
||||
|
||||
@@ -32,11 +28,7 @@ export interface MetricsTreemapProps {
|
||||
isLoading: boolean;
|
||||
isError: boolean;
|
||||
viewType: TreemapViewType;
|
||||
openMetricDetails: (
|
||||
metricName: string,
|
||||
view: 'list' | 'treemap',
|
||||
event?: React.MouseEvent,
|
||||
) => void;
|
||||
openMetricDetails: (metricName: string, view: 'list' | 'treemap') => void;
|
||||
setHeatmapView: (value: TreemapViewType) => void;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { Badge, Button } from 'antd';
|
||||
import ROUTES from 'constants/routes';
|
||||
import history from 'lib/history';
|
||||
import { Undo } from 'lucide-react';
|
||||
import { navigateToPage } from 'utils/navigation';
|
||||
|
||||
import { buttonText, RIBBON_STYLES } from './config';
|
||||
|
||||
@@ -22,30 +21,23 @@ function NewExplorerCTA(): JSX.Element | null {
|
||||
[location.pathname],
|
||||
);
|
||||
|
||||
const onClickHandler = useCallback(
|
||||
(e?: React.MouseEvent): void => {
|
||||
let targetPath: string;
|
||||
if (location.pathname === ROUTES.LOGS_EXPLORER) {
|
||||
targetPath = ROUTES.OLD_LOGS_EXPLORER;
|
||||
} else if (location.pathname === ROUTES.TRACE) {
|
||||
targetPath = ROUTES.TRACES_EXPLORER;
|
||||
} else if (location.pathname === ROUTES.OLD_LOGS_EXPLORER) {
|
||||
targetPath = ROUTES.LOGS_EXPLORER;
|
||||
} else if (location.pathname === ROUTES.TRACES_EXPLORER) {
|
||||
targetPath = ROUTES.TRACE;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
navigateToPage(targetPath, history.push, e);
|
||||
},
|
||||
[location.pathname],
|
||||
);
|
||||
const onClickHandler = useCallback((): void => {
|
||||
if (location.pathname === ROUTES.LOGS_EXPLORER) {
|
||||
history.push(ROUTES.OLD_LOGS_EXPLORER);
|
||||
} else if (location.pathname === ROUTES.TRACE) {
|
||||
history.push(ROUTES.TRACES_EXPLORER);
|
||||
} else if (location.pathname === ROUTES.OLD_LOGS_EXPLORER) {
|
||||
history.push(ROUTES.LOGS_EXPLORER);
|
||||
} else if (location.pathname === ROUTES.TRACES_EXPLORER) {
|
||||
history.push(ROUTES.TRACE);
|
||||
}
|
||||
}, [location.pathname]);
|
||||
|
||||
const button = useMemo(
|
||||
() => (
|
||||
<Button
|
||||
icon={<Undo size={16} />}
|
||||
onClick={(e): void => onClickHandler(e)}
|
||||
onClick={onClickHandler}
|
||||
data-testid="newExplorerCTA"
|
||||
type="text"
|
||||
className="periscope-btn link"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/* eslint-disable jsx-a11y/no-static-element-interactions */
|
||||
/* eslint-disable jsx-a11y/click-events-have-key-events */
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useQuery } from 'react-query';
|
||||
import { useEffectOnce } from 'react-use';
|
||||
@@ -17,7 +17,6 @@ import { InviteMemberFormValues } from 'container/OrganizationSettings/PendingIn
|
||||
import history from 'lib/history';
|
||||
import { UserPlus } from 'lucide-react';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
import { navigateToPage } from 'utils/navigation';
|
||||
|
||||
import ModuleStepsContainer from './common/ModuleStepsContainer/ModuleStepsContainer';
|
||||
import { stepsMap } from './constants/stepsConfig';
|
||||
@@ -253,13 +252,9 @@ export default function Onboarding(): JSX.Element {
|
||||
}
|
||||
};
|
||||
|
||||
const handleNext = (e?: React.MouseEvent): void => {
|
||||
const handleNext = (): void => {
|
||||
if (activeStep <= 3) {
|
||||
navigateToPage(
|
||||
moduleRouteMap[selectedModule.id as ModulesMap],
|
||||
history.push,
|
||||
e,
|
||||
);
|
||||
history.push(moduleRouteMap[selectedModule.id as ModulesMap]);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -322,9 +317,9 @@ export default function Onboarding(): JSX.Element {
|
||||
{activeStep === 1 && (
|
||||
<div className="onboarding-page">
|
||||
<div
|
||||
onClick={(e): void => {
|
||||
onClick={(): void => {
|
||||
logEvent('Onboarding V2: Skip Button Clicked', {});
|
||||
navigateToPage(ROUTES.APPLICATION, history.push, e);
|
||||
history.push(ROUTES.APPLICATION);
|
||||
}}
|
||||
className="skip-to-console"
|
||||
>
|
||||
@@ -360,11 +355,7 @@ export default function Onboarding(): JSX.Element {
|
||||
</div>
|
||||
</div>
|
||||
<div className="continue-to-next-step">
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<ArrowRightOutlined />}
|
||||
onClick={(e): void => handleNext(e)}
|
||||
>
|
||||
<Button type="primary" icon={<ArrowRightOutlined />} onClick={handleNext}>
|
||||
{t('get_started')}
|
||||
</Button>
|
||||
</div>
|
||||
@@ -395,16 +386,17 @@ export default function Onboarding(): JSX.Element {
|
||||
{activeStep > 1 && (
|
||||
<div className="stepsContainer">
|
||||
<ModuleStepsContainer
|
||||
onReselectModule={(e?: React.MouseEvent): void => {
|
||||
onReselectModule={(): void => {
|
||||
setCurrent(current - 1);
|
||||
setActiveStep(activeStep - 1);
|
||||
setSelectedModule(useCases.APM);
|
||||
resetProgress();
|
||||
|
||||
const path = isOnboardingV3Enabled
|
||||
? ROUTES.GET_STARTED_WITH_CLOUD
|
||||
: ROUTES.GET_STARTED;
|
||||
navigateToPage(path, history.push, e);
|
||||
if (isOnboardingV3Enabled) {
|
||||
history.push(ROUTES.GET_STARTED_WITH_CLOUD);
|
||||
} else {
|
||||
history.push(ROUTES.GET_STARTED);
|
||||
}
|
||||
}}
|
||||
selectedModule={selectedModule}
|
||||
selectedModuleSteps={selectedModuleSteps}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/* eslint-disable jsx-a11y/no-static-element-interactions */
|
||||
/* eslint-disable jsx-a11y/click-events-have-key-events */
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { LoadingOutlined } from '@ant-design/icons';
|
||||
@@ -23,7 +23,6 @@ import {
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
import { Blocks, Check } from 'lucide-react';
|
||||
import { navigateToPage } from 'utils/navigation';
|
||||
import { popupContainer } from 'utils/selectPopupContainer';
|
||||
|
||||
import './DataSource.styles.scss';
|
||||
@@ -142,13 +141,13 @@ export default function DataSource(): JSX.Element {
|
||||
}
|
||||
};
|
||||
|
||||
const goToIntegrationsPage = (e?: React.MouseEvent): void => {
|
||||
const goToIntegrationsPage = (): void => {
|
||||
logEvent('Onboarding V2: Go to integrations', {
|
||||
module: selectedModule?.id,
|
||||
dataSource: selectedDataSource?.name,
|
||||
framework: selectedFramework,
|
||||
});
|
||||
navigateToPage(ROUTES.INTEGRATIONS, history.push, e);
|
||||
history.push(ROUTES.INTEGRATIONS);
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -250,7 +249,7 @@ export default function DataSource(): JSX.Element {
|
||||
page which allows more sources of sending data
|
||||
</Typography.Text>
|
||||
<Button
|
||||
onClick={(e): void => goToIntegrationsPage(e)}
|
||||
onClick={goToIntegrationsPage}
|
||||
icon={<Blocks size={14} />}
|
||||
className="navigate-integrations-page-btn"
|
||||
>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
/* eslint-disable jsx-a11y/click-events-have-key-events */
|
||||
/* eslint-disable react/jsx-no-comment-textnodes */
|
||||
/* eslint-disable sonarjs/prefer-single-boolean-return */
|
||||
import React, { SetStateAction, useState } from 'react';
|
||||
import { SetStateAction, useState } from 'react';
|
||||
import {
|
||||
ArrowLeftOutlined,
|
||||
ArrowRightOutlined,
|
||||
@@ -19,7 +19,6 @@ import { hasFrameworks } from 'container/OnboardingContainer/utils/dataSourceUti
|
||||
import history from 'lib/history';
|
||||
import { isEmpty, isNull } from 'lodash-es';
|
||||
import { UserPlus } from 'lucide-react';
|
||||
import { navigateToPage } from 'utils/navigation';
|
||||
|
||||
import { useOnboardingContext } from '../../context/OnboardingContext';
|
||||
import {
|
||||
@@ -143,7 +142,7 @@ export default function ModuleStepsContainer({
|
||||
return true;
|
||||
};
|
||||
|
||||
const redirectToModules = (event?: React.MouseEvent): void => {
|
||||
const redirectToModules = (): void => {
|
||||
logEvent('Onboarding V2 Complete', {
|
||||
module: selectedModule.id,
|
||||
dataSource: selectedDataSource?.id,
|
||||
@@ -153,28 +152,26 @@ export default function ModuleStepsContainer({
|
||||
serviceName,
|
||||
});
|
||||
|
||||
let targetPath: string;
|
||||
if (selectedModule.id === ModulesMap.APM) {
|
||||
targetPath = ROUTES.APPLICATION;
|
||||
history.push(ROUTES.APPLICATION);
|
||||
} else if (selectedModule.id === ModulesMap.LogsManagement) {
|
||||
targetPath = ROUTES.LOGS_EXPLORER;
|
||||
history.push(ROUTES.LOGS_EXPLORER);
|
||||
} else if (selectedModule.id === ModulesMap.InfrastructureMonitoring) {
|
||||
targetPath = ROUTES.APPLICATION;
|
||||
history.push(ROUTES.APPLICATION);
|
||||
} else if (selectedModule.id === ModulesMap.AwsMonitoring) {
|
||||
targetPath = ROUTES.APPLICATION;
|
||||
history.push(ROUTES.APPLICATION);
|
||||
} else {
|
||||
targetPath = ROUTES.APPLICATION;
|
||||
history.push(ROUTES.APPLICATION);
|
||||
}
|
||||
navigateToPage(targetPath, history.push, event);
|
||||
};
|
||||
|
||||
const handleNext = (event?: React.MouseEvent): void => {
|
||||
const handleNext = (): void => {
|
||||
const isValid = isValidForm();
|
||||
|
||||
if (isValid) {
|
||||
if (current === lastStepIndex) {
|
||||
resetProgress();
|
||||
redirectToModules(event);
|
||||
redirectToModules();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -382,8 +379,8 @@ export default function ModuleStepsContainer({
|
||||
}
|
||||
};
|
||||
|
||||
const handleLogoClick = (e: React.MouseEvent): void => {
|
||||
navigateToPage('/home', history.push, e);
|
||||
const handleLogoClick = (): void => {
|
||||
history.push('/home');
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -403,7 +400,7 @@ export default function ModuleStepsContainer({
|
||||
style={{ display: 'flex', alignItems: 'center' }}
|
||||
type="default"
|
||||
icon={<LeftCircleOutlined />}
|
||||
onClick={(e): void => onReselectModule(e)}
|
||||
onClick={onReselectModule}
|
||||
>
|
||||
{selectedModule.title}
|
||||
</Button>
|
||||
@@ -473,11 +470,7 @@ export default function ModuleStepsContainer({
|
||||
>
|
||||
Back
|
||||
</Button>
|
||||
<Button
|
||||
onClick={(e): void => handleNext(e)}
|
||||
type="primary"
|
||||
icon={<ArrowRightOutlined />}
|
||||
>
|
||||
<Button onClick={handleNext} type="primary" icon={<ArrowRightOutlined />}>
|
||||
{current < lastStepIndex ? 'Continue to next step' : 'Done'}
|
||||
</Button>
|
||||
<LaunchChatSupport
|
||||
|
||||
@@ -21,7 +21,6 @@ import history from 'lib/history';
|
||||
import { isEmpty } from 'lodash-es';
|
||||
import { CheckIcon, Goal, UserPlus, X } from 'lucide-react';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
import { navigateToPage } from 'utils/navigation';
|
||||
|
||||
import OnboardingIngestionDetails from '../IngestionDetails/IngestionDetails';
|
||||
import InviteTeamMembers from '../InviteTeamMembers/InviteTeamMembers';
|
||||
@@ -414,10 +413,7 @@ function OnboardingAddDataSource(): JSX.Element {
|
||||
]);
|
||||
}, [org]);
|
||||
|
||||
const handleUpdateCurrentStep = (
|
||||
step: number,
|
||||
event?: React.MouseEvent,
|
||||
): void => {
|
||||
const handleUpdateCurrentStep = (step: number): void => {
|
||||
setCurrentStep(step);
|
||||
|
||||
if (step === 1) {
|
||||
@@ -447,45 +443,43 @@ function OnboardingAddDataSource(): JSX.Element {
|
||||
...setupStepItemsBase.slice(2),
|
||||
]);
|
||||
} else if (step === 3) {
|
||||
let targetPath: string;
|
||||
switch (selectedDataSource?.module) {
|
||||
case 'apm':
|
||||
targetPath = ROUTES.APPLICATION;
|
||||
history.push(ROUTES.APPLICATION);
|
||||
break;
|
||||
case 'logs':
|
||||
targetPath = ROUTES.LOGS;
|
||||
history.push(ROUTES.LOGS);
|
||||
break;
|
||||
case 'metrics':
|
||||
targetPath = ROUTES.METRICS_EXPLORER;
|
||||
history.push(ROUTES.METRICS_EXPLORER);
|
||||
break;
|
||||
case 'dashboards':
|
||||
targetPath = ROUTES.ALL_DASHBOARD;
|
||||
history.push(ROUTES.ALL_DASHBOARD);
|
||||
break;
|
||||
case 'infra-monitoring-hosts':
|
||||
targetPath = ROUTES.INFRASTRUCTURE_MONITORING_HOSTS;
|
||||
history.push(ROUTES.INFRASTRUCTURE_MONITORING_HOSTS);
|
||||
break;
|
||||
case 'infra-monitoring-k8s':
|
||||
targetPath = ROUTES.INFRASTRUCTURE_MONITORING_KUBERNETES;
|
||||
history.push(ROUTES.INFRASTRUCTURE_MONITORING_KUBERNETES);
|
||||
break;
|
||||
case 'messaging-queues-kafka':
|
||||
targetPath = ROUTES.MESSAGING_QUEUES_KAFKA;
|
||||
history.push(ROUTES.MESSAGING_QUEUES_KAFKA);
|
||||
break;
|
||||
case 'messaging-queues-celery':
|
||||
targetPath = ROUTES.MESSAGING_QUEUES_CELERY_TASK;
|
||||
history.push(ROUTES.MESSAGING_QUEUES_CELERY_TASK);
|
||||
break;
|
||||
case 'integrations':
|
||||
targetPath = ROUTES.INTEGRATIONS;
|
||||
history.push(ROUTES.INTEGRATIONS);
|
||||
break;
|
||||
case 'home':
|
||||
targetPath = ROUTES.HOME;
|
||||
history.push(ROUTES.HOME);
|
||||
break;
|
||||
case 'api-monitoring':
|
||||
targetPath = ROUTES.API_MONITORING;
|
||||
history.push(ROUTES.API_MONITORING);
|
||||
break;
|
||||
default:
|
||||
targetPath = ROUTES.APPLICATION;
|
||||
history.push(ROUTES.APPLICATION);
|
||||
}
|
||||
navigateToPage(targetPath, history.push, event);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -634,7 +628,7 @@ function OnboardingAddDataSource(): JSX.Element {
|
||||
<X
|
||||
size={14}
|
||||
className="onboarding-header-container-close-icon"
|
||||
onClick={(e): void => {
|
||||
onClick={(): void => {
|
||||
logEvent(
|
||||
`${ONBOARDING_V3_ANALYTICS_EVENTS_MAP?.BASE}: ${ONBOARDING_V3_ANALYTICS_EVENTS_MAP?.CLOSE_ONBOARDING_CLICKED}`,
|
||||
{
|
||||
@@ -642,7 +636,7 @@ function OnboardingAddDataSource(): JSX.Element {
|
||||
},
|
||||
);
|
||||
|
||||
navigateToPage(ROUTES.HOME, history.push, e);
|
||||
history.push(ROUTES.HOME);
|
||||
}}
|
||||
/>
|
||||
<Typography.Text>Get Started (2/4)</Typography.Text>
|
||||
@@ -969,7 +963,7 @@ function OnboardingAddDataSource(): JSX.Element {
|
||||
type="primary"
|
||||
disabled={!selectedDataSource}
|
||||
shape="round"
|
||||
onClick={(e): void => {
|
||||
onClick={(): void => {
|
||||
logEvent(
|
||||
`${ONBOARDING_V3_ANALYTICS_EVENTS_MAP?.BASE}: ${ONBOARDING_V3_ANALYTICS_EVENTS_MAP?.CONFIGURED_PRODUCT}`,
|
||||
{
|
||||
@@ -983,7 +977,7 @@ function OnboardingAddDataSource(): JSX.Element {
|
||||
selectedEnvironment || selectedFramework || selectedDataSource;
|
||||
|
||||
if (currentEntity?.internalRedirect && currentEntity?.link) {
|
||||
navigateToPage(currentEntity.link, history.push, e);
|
||||
history.push(currentEntity.link);
|
||||
} else {
|
||||
handleUpdateCurrentStep(2);
|
||||
}
|
||||
@@ -1054,7 +1048,7 @@ function OnboardingAddDataSource(): JSX.Element {
|
||||
<Button
|
||||
type="primary"
|
||||
shape="round"
|
||||
onClick={(e): void => {
|
||||
onClick={(): void => {
|
||||
logEvent(
|
||||
`${ONBOARDING_V3_ANALYTICS_EVENTS_MAP?.BASE}: ${ONBOARDING_V3_ANALYTICS_EVENTS_MAP?.CONTINUE_BUTTON_CLICKED}`,
|
||||
{
|
||||
@@ -1066,7 +1060,7 @@ function OnboardingAddDataSource(): JSX.Element {
|
||||
);
|
||||
|
||||
handleFilterByCategory('All');
|
||||
handleUpdateCurrentStep(3, e);
|
||||
handleUpdateCurrentStep(3);
|
||||
}}
|
||||
>
|
||||
Continue
|
||||
|
||||
@@ -65,7 +65,6 @@ import AppReducer from 'types/reducer/app';
|
||||
import { USER_ROLES } from 'types/roles';
|
||||
import { checkVersionState } from 'utils/app';
|
||||
import { showErrorNotification } from 'utils/error';
|
||||
import { isModifierKeyPressed, openInNewTab } from 'utils/navigation';
|
||||
|
||||
import { useCmdK } from '../../providers/cmdKProvider';
|
||||
import { routeConfig } from './config';
|
||||
@@ -307,6 +306,8 @@ function SideNav({ isPinned }: { isPinned: boolean }): JSX.Element {
|
||||
icon: <Cog size={16} />,
|
||||
};
|
||||
|
||||
const isCtrlMetaKey = (e: MouseEvent): boolean => e.ctrlKey || e.metaKey;
|
||||
|
||||
const isLatestVersion = checkVersionState(currentVersion, latestVersion);
|
||||
|
||||
const [
|
||||
@@ -435,6 +436,10 @@ function SideNav({ isPinned }: { isPinned: boolean }): JSX.Element {
|
||||
|
||||
const isWorkspaceBlocked = trialInfo?.workSpaceBlock || false;
|
||||
|
||||
const openInNewTab = (path: string): void => {
|
||||
window.open(path, '_blank');
|
||||
};
|
||||
|
||||
const onClickGetStarted = (event: MouseEvent): void => {
|
||||
logEvent('Sidebar: Menu clicked', {
|
||||
menuRoute: '/get-started',
|
||||
@@ -445,7 +450,7 @@ function SideNav({ isPinned }: { isPinned: boolean }): JSX.Element {
|
||||
? ROUTES.GET_STARTED_WITH_CLOUD
|
||||
: ROUTES.GET_STARTED;
|
||||
|
||||
if (isModifierKeyPressed(event)) {
|
||||
if (isCtrlMetaKey(event)) {
|
||||
openInNewTab(onboaringRoute);
|
||||
} else {
|
||||
history.push(onboaringRoute);
|
||||
@@ -460,7 +465,7 @@ function SideNav({ isPinned }: { isPinned: boolean }): JSX.Element {
|
||||
const queryString = getQueryString(availableParams || [], params);
|
||||
|
||||
if (pathname !== key) {
|
||||
if (event && isModifierKeyPressed(event)) {
|
||||
if (event && isCtrlMetaKey(event)) {
|
||||
openInNewTab(`${key}?${queryString.join('&')}`);
|
||||
} else {
|
||||
history.push(`${key}?${queryString.join('&')}`, {
|
||||
@@ -659,7 +664,7 @@ function SideNav({ isPinned }: { isPinned: boolean }): JSX.Element {
|
||||
|
||||
const handleMenuItemClick = (event: MouseEvent, item: SidebarItem): void => {
|
||||
if (item.key === ROUTES.SETTINGS) {
|
||||
if (isModifierKeyPressed(event)) {
|
||||
if (isCtrlMetaKey(event)) {
|
||||
openInNewTab(settingsRoute);
|
||||
} else {
|
||||
history.push(settingsRoute);
|
||||
@@ -837,7 +842,6 @@ function SideNav({ isPinned }: { isPinned: boolean }): JSX.Element {
|
||||
}
|
||||
};
|
||||
|
||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||
const handleHelpSupportMenuItemClick = (info: SidebarItem): void => {
|
||||
const item = helpSupportDropdownMenuItems.find(
|
||||
(item) => !('type' in item) && item.key === info.key,
|
||||
@@ -847,8 +851,6 @@ function SideNav({ isPinned }: { isPinned: boolean }): JSX.Element {
|
||||
window.open(item.url, '_blank');
|
||||
}
|
||||
|
||||
const event = (info as SidebarItem & { domEvent?: MouseEvent }).domEvent;
|
||||
|
||||
if (item && !('type' in item)) {
|
||||
logEvent('Help Popover: Item clicked', {
|
||||
menuRoute: item.key,
|
||||
@@ -857,18 +859,10 @@ function SideNav({ isPinned }: { isPinned: boolean }): JSX.Element {
|
||||
|
||||
switch (item.key) {
|
||||
case ROUTES.SHORTCUTS:
|
||||
if (event && isModifierKeyPressed(event)) {
|
||||
openInNewTab(ROUTES.SHORTCUTS);
|
||||
} else {
|
||||
history.push(ROUTES.SHORTCUTS);
|
||||
}
|
||||
history.push(ROUTES.SHORTCUTS);
|
||||
break;
|
||||
case 'invite-collaborators':
|
||||
if (event && isModifierKeyPressed(event)) {
|
||||
openInNewTab(`${ROUTES.ORG_SETTINGS}#invite-team-members`);
|
||||
} else {
|
||||
history.push(`${ROUTES.ORG_SETTINGS}#invite-team-members`);
|
||||
}
|
||||
history.push(`${ROUTES.ORG_SETTINGS}#invite-team-members`);
|
||||
break;
|
||||
case 'chat-support':
|
||||
if (window.pylon) {
|
||||
@@ -887,7 +881,6 @@ function SideNav({ isPinned }: { isPinned: boolean }): JSX.Element {
|
||||
}
|
||||
};
|
||||
|
||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||
const handleSettingsMenuItemClick = (info: SidebarItem): void => {
|
||||
const item = userSettingsDropdownMenuItems.find(
|
||||
(item) => item?.key === info.key,
|
||||
@@ -905,30 +898,15 @@ function SideNav({ isPinned }: { isPinned: boolean }): JSX.Element {
|
||||
menuRoute: item?.key,
|
||||
menuLabel,
|
||||
});
|
||||
|
||||
const event = (info as SidebarItem & { domEvent?: MouseEvent }).domEvent;
|
||||
|
||||
switch (info.key) {
|
||||
case 'account':
|
||||
if (event && isModifierKeyPressed(event)) {
|
||||
openInNewTab(ROUTES.MY_SETTINGS);
|
||||
} else {
|
||||
history.push(ROUTES.MY_SETTINGS);
|
||||
}
|
||||
history.push(ROUTES.MY_SETTINGS);
|
||||
break;
|
||||
case 'workspace':
|
||||
if (event && isModifierKeyPressed(event)) {
|
||||
openInNewTab(ROUTES.SETTINGS);
|
||||
} else {
|
||||
history.push(ROUTES.SETTINGS);
|
||||
}
|
||||
history.push(ROUTES.SETTINGS);
|
||||
break;
|
||||
case 'license':
|
||||
if (event && isModifierKeyPressed(event)) {
|
||||
openInNewTab(ROUTES.LIST_LICENSES);
|
||||
} else {
|
||||
history.push(ROUTES.LIST_LICENSES);
|
||||
}
|
||||
history.push(ROUTES.LIST_LICENSES);
|
||||
break;
|
||||
case 'logout':
|
||||
Logout();
|
||||
|
||||
@@ -1,106 +0,0 @@
|
||||
/**
|
||||
* Tests for useSafeNavigate's mock contract.
|
||||
*
|
||||
* The real useSafeNavigate hook is globally replaced by a mock via
|
||||
* jest.config.ts moduleNameMapper, so we cannot test the real
|
||||
* implementation here. Instead we verify:
|
||||
*
|
||||
* 1. The mock accepts the new `event` and `newTab` options without
|
||||
* type errors — ensuring component tests that pass these options
|
||||
* won't break.
|
||||
* 2. The shouldOpenNewTab decision logic (extracted inline below)
|
||||
* matches the real hook's behaviour.
|
||||
*/
|
||||
|
||||
import { useSafeNavigate } from 'hooks/useSafeNavigate';
|
||||
import { isModifierKeyPressed } from 'utils/navigation';
|
||||
|
||||
/**
|
||||
* Mirrors the shouldOpenNewTab logic from the real useSafeNavigate hook.
|
||||
* Kept in sync manually — any drift will be caught by integration tests.
|
||||
*/
|
||||
interface NavigateOptions {
|
||||
newTab?: boolean;
|
||||
event?: MouseEvent | React.MouseEvent;
|
||||
}
|
||||
|
||||
const shouldOpenNewTab = (options?: NavigateOptions): boolean =>
|
||||
Boolean(
|
||||
options?.newTab || (options?.event && isModifierKeyPressed(options.event)),
|
||||
);
|
||||
|
||||
describe('useSafeNavigate mock contract', () => {
|
||||
it('mock returns a safeNavigate function', () => {
|
||||
const { safeNavigate } = useSafeNavigate();
|
||||
expect(typeof safeNavigate).toBe('function');
|
||||
});
|
||||
|
||||
it('safeNavigate accepts string path with event option', () => {
|
||||
const { safeNavigate } = useSafeNavigate();
|
||||
const event = { metaKey: true, ctrlKey: false } as MouseEvent;
|
||||
|
||||
expect(() => {
|
||||
safeNavigate('/alerts', { event });
|
||||
}).not.toThrow();
|
||||
|
||||
expect(safeNavigate).toHaveBeenCalledWith('/alerts', { event });
|
||||
});
|
||||
|
||||
it('safeNavigate accepts string path with newTab option', () => {
|
||||
const { safeNavigate } = useSafeNavigate();
|
||||
|
||||
expect(() => {
|
||||
safeNavigate('/dashboard', { newTab: true });
|
||||
}).not.toThrow();
|
||||
|
||||
expect(safeNavigate).toHaveBeenCalledWith('/dashboard', { newTab: true });
|
||||
});
|
||||
|
||||
it('safeNavigate accepts SafeNavigateParams with event option', () => {
|
||||
const { safeNavigate } = useSafeNavigate();
|
||||
const event = { metaKey: false, ctrlKey: true } as MouseEvent;
|
||||
|
||||
expect(() => {
|
||||
safeNavigate({ pathname: '/settings', search: '?tab=general' }, { event });
|
||||
}).not.toThrow();
|
||||
|
||||
expect(safeNavigate).toHaveBeenCalledWith(
|
||||
{ pathname: '/settings', search: '?tab=general' },
|
||||
{ event },
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('shouldOpenNewTab decision logic', () => {
|
||||
it('returns true when newTab is true', () => {
|
||||
expect(shouldOpenNewTab({ newTab: true })).toBe(true);
|
||||
});
|
||||
|
||||
it('returns true when event has metaKey pressed', () => {
|
||||
const event = { metaKey: true, ctrlKey: false } as MouseEvent;
|
||||
expect(shouldOpenNewTab({ event })).toBe(true);
|
||||
});
|
||||
|
||||
it('returns true when event has ctrlKey pressed', () => {
|
||||
const event = { metaKey: false, ctrlKey: true } as MouseEvent;
|
||||
expect(shouldOpenNewTab({ event })).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false when event has no modifier keys', () => {
|
||||
const event = { metaKey: false, ctrlKey: false } as MouseEvent;
|
||||
expect(shouldOpenNewTab({ event })).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false when no options provided', () => {
|
||||
expect(shouldOpenNewTab()).toBe(false);
|
||||
expect(shouldOpenNewTab(undefined)).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false when options provided without event or newTab', () => {
|
||||
expect(shouldOpenNewTab({})).toBe(false);
|
||||
});
|
||||
|
||||
it('newTab takes precedence even without event', () => {
|
||||
expect(shouldOpenNewTab({ newTab: true })).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -1,13 +1,11 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import { useCallback } from 'react';
|
||||
import { useLocation, useNavigate } from 'react-router-dom-v5-compat';
|
||||
import { cloneDeep, isEqual } from 'lodash-es';
|
||||
import { isModifierKeyPressed } from 'utils/navigation';
|
||||
|
||||
interface NavigateOptions {
|
||||
replace?: boolean;
|
||||
state?: any;
|
||||
newTab?: boolean;
|
||||
event?: MouseEvent | React.MouseEvent;
|
||||
}
|
||||
|
||||
interface SafeNavigateParams {
|
||||
@@ -107,7 +105,6 @@ export const useSafeNavigate = (
|
||||
const location = useLocation();
|
||||
|
||||
const safeNavigate = useCallback(
|
||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||
(to: string | SafeNavigateParams, options?: NavigateOptions) => {
|
||||
const currentUrl = new URL(
|
||||
`${location.pathname}${location.search}`,
|
||||
@@ -125,10 +122,8 @@ export const useSafeNavigate = (
|
||||
);
|
||||
}
|
||||
|
||||
const shouldOpenNewTab =
|
||||
options?.newTab || (options?.event && isModifierKeyPressed(options.event));
|
||||
|
||||
if (shouldOpenNewTab) {
|
||||
// If newTab is true, open in new tab and return early
|
||||
if (options?.newTab) {
|
||||
const targetPath =
|
||||
typeof to === 'string'
|
||||
? to
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useEffect, useMemo } from 'react';
|
||||
import { useEffect, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { Breadcrumb, Button, Divider } from 'antd';
|
||||
@@ -18,7 +18,6 @@ import {
|
||||
NEW_ALERT_SCHEMA_VERSION,
|
||||
PostableAlertRuleV2,
|
||||
} from 'types/api/alerts/alertTypesV2';
|
||||
import { navigateToPage } from 'utils/navigation';
|
||||
|
||||
import AlertHeader from './AlertHeader/AlertHeader';
|
||||
import AlertNotFound from './AlertNotFound';
|
||||
@@ -59,11 +58,11 @@ function BreadCrumbItem({
|
||||
if (isLast) {
|
||||
return <div className="breadcrumb-item breadcrumb-item--last">{title}</div>;
|
||||
}
|
||||
const handleNavigate = (e: React.MouseEvent): void => {
|
||||
const handleNavigate = (): void => {
|
||||
if (!route) {
|
||||
return;
|
||||
}
|
||||
navigateToPage(ROUTES.LIST_ALL_ALERT, history.push, e);
|
||||
history.push(ROUTES.LIST_ALL_ALERT);
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import React from 'react';
|
||||
import { Button, Typography } from 'antd';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { useGetTenantLicense } from 'hooks/useGetTenantLicense';
|
||||
@@ -16,8 +15,8 @@ function AlertNotFound({ isTestAlert }: AlertNotFoundProps): JSX.Element {
|
||||
const { isCloudUser: isCloudUserVal } = useGetTenantLicense();
|
||||
const { safeNavigate } = useSafeNavigate();
|
||||
|
||||
const checkAllRulesHandler = (e?: React.MouseEvent): void => {
|
||||
safeNavigate(ROUTES.LIST_ALL_ALERT, { event: e });
|
||||
const checkAllRulesHandler = (): void => {
|
||||
safeNavigate(ROUTES.LIST_ALL_ALERT);
|
||||
};
|
||||
|
||||
const contactSupportHandler = (): void => {
|
||||
|
||||
@@ -69,10 +69,7 @@ describe('AlertNotFound', () => {
|
||||
const user = userEvent.setup();
|
||||
render(<AlertNotFound isTestAlert={false} />);
|
||||
await user.click(screen.getByText('Check all rules'));
|
||||
expect(mockSafeNavigate).toHaveBeenCalledWith(
|
||||
ROUTES.LIST_ALL_ALERT,
|
||||
expect.objectContaining({ event: expect.any(Object) }),
|
||||
);
|
||||
expect(mockSafeNavigate).toHaveBeenCalledWith(ROUTES.LIST_ALL_ALERT);
|
||||
});
|
||||
|
||||
it('should navigate to the correct support page for cloud users when button is clicked', async () => {
|
||||
|
||||
@@ -18,7 +18,7 @@ function AlertTypeSelectionPage(): JSX.Element {
|
||||
}, []);
|
||||
|
||||
const handleSelectType = useCallback(
|
||||
(type: AlertTypes, event?: React.MouseEvent): void => {
|
||||
(type: AlertTypes): void => {
|
||||
// For anamoly based alert, we need to set the ruleType to anomaly_rule
|
||||
// and alertType to metrics_based_alert
|
||||
if (type === AlertTypes.ANOMALY_BASED_ALERT) {
|
||||
@@ -41,7 +41,7 @@ function AlertTypeSelectionPage(): JSX.Element {
|
||||
queryParams.set(QueryParams.showClassicCreateAlertsPage, 'true');
|
||||
}
|
||||
|
||||
safeNavigate(`${ROUTES.ALERTS_NEW}?${queryParams.toString()}`, { event });
|
||||
safeNavigate(`${ROUTES.ALERTS_NEW}?${queryParams.toString()}`);
|
||||
},
|
||||
[queryParams, safeNavigate],
|
||||
);
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { Button, Flex, Typography } from 'antd';
|
||||
import ROUTES from 'constants/routes';
|
||||
import history from 'lib/history';
|
||||
import { ArrowRight } from 'lucide-react';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
import { navigateToPage } from 'utils/navigation';
|
||||
import { routePermission } from 'utils/permission';
|
||||
|
||||
import './Integrations.styles.scss';
|
||||
|
||||
function Header(): JSX.Element {
|
||||
const history = useHistory();
|
||||
const { user } = useAppContext();
|
||||
|
||||
const isGetStartedWithCloudAllowed = routePermission.GET_STARTED_WITH_CLOUD.includes(
|
||||
@@ -30,9 +30,7 @@ function Header(): JSX.Element {
|
||||
<Button
|
||||
className="periscope-btn primary view-data-sources-btn"
|
||||
type="primary"
|
||||
onClick={(e): void =>
|
||||
navigateToPage(ROUTES.GET_STARTED_WITH_CLOUD, history.push, e)
|
||||
}
|
||||
onClick={(): void => history.push(ROUTES.GET_STARTED_WITH_CLOUD)}
|
||||
>
|
||||
<span>View 150+ Data Sources</span>
|
||||
<ArrowRight size={14} />
|
||||
|
||||
@@ -7,7 +7,6 @@ import { QueryParams } from 'constants/query';
|
||||
import ROUTES from 'constants/routes';
|
||||
import DateTimeSelectionV2 from 'container/TopNav/DateTimeSelectionV2';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
import { isModifierKeyPressed, openInNewTab } from 'utils/navigation';
|
||||
|
||||
import {
|
||||
MessagingQueuesViewType,
|
||||
@@ -65,14 +64,8 @@ function MQDetailPage(): JSX.Element {
|
||||
selectedView !== MessagingQueuesViewType.dropRate.value &&
|
||||
selectedView !== MessagingQueuesViewType.metricPage.value;
|
||||
|
||||
const handleBackClick = (
|
||||
event?: React.MouseEvent | React.KeyboardEvent,
|
||||
): void => {
|
||||
if (event && isModifierKeyPressed(event as React.MouseEvent)) {
|
||||
openInNewTab(ROUTES.MESSAGING_QUEUES_KAFKA);
|
||||
} else {
|
||||
history.push(ROUTES.MESSAGING_QUEUES_KAFKA);
|
||||
}
|
||||
const handleBackClick = (): void => {
|
||||
history.push(ROUTES.MESSAGING_QUEUES_KAFKA);
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -84,7 +77,7 @@ function MQDetailPage(): JSX.Element {
|
||||
className="message-queue-text"
|
||||
onKeyDown={(e): void => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
handleBackClick(e);
|
||||
handleBackClick();
|
||||
}
|
||||
}}
|
||||
role="button"
|
||||
|
||||
@@ -30,7 +30,6 @@ import {
|
||||
setConfigDetail,
|
||||
} from 'pages/MessagingQueues/MessagingQueuesUtils';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import { isModifierKeyPressed, openInNewTab } from 'utils/navigation';
|
||||
import { formatNumericValue } from 'utils/numericUtils';
|
||||
|
||||
import { getTableDataForProducerLatencyOverview } from './MQTableUtils';
|
||||
@@ -81,12 +80,7 @@ export function getColumns(
|
||||
onClick={(e): void => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
const path = `/services/${encodeURIComponent(text)}`;
|
||||
if (isModifierKeyPressed(e)) {
|
||||
openInNewTab(path);
|
||||
} else {
|
||||
history.push(path);
|
||||
}
|
||||
history.push(`/services/${encodeURIComponent(text)}`);
|
||||
}}
|
||||
>
|
||||
{text}
|
||||
|
||||
@@ -10,7 +10,6 @@ import { QueryParams } from 'constants/query';
|
||||
import ROUTES from 'constants/routes';
|
||||
import DateTimeSelectionV2 from 'container/TopNav/DateTimeSelectionV2';
|
||||
import { useGetTenantLicense } from 'hooks/useGetTenantLicense';
|
||||
import { isModifierKeyPressed, openInNewTab } from 'utils/navigation';
|
||||
|
||||
import {
|
||||
KAFKA_SETUP_DOC_LINK,
|
||||
@@ -24,40 +23,26 @@ function MessagingQueues(): JSX.Element {
|
||||
const history = useHistory();
|
||||
const { t } = useTranslation('messagingQueuesKafkaOverview');
|
||||
|
||||
const redirectToDetailsPage = (
|
||||
callerView?: string,
|
||||
event?: React.MouseEvent,
|
||||
): void => {
|
||||
const redirectToDetailsPage = (callerView?: string): void => {
|
||||
logEvent('Messaging Queues: View details clicked', {
|
||||
page: 'Messaging Queues Overview',
|
||||
source: callerView,
|
||||
});
|
||||
|
||||
const path = `${ROUTES.MESSAGING_QUEUES_KAFKA_DETAIL}?${QueryParams.mqServiceView}=${callerView}`;
|
||||
if (event && isModifierKeyPressed(event)) {
|
||||
openInNewTab(path);
|
||||
} else {
|
||||
history.push(path);
|
||||
}
|
||||
history.push(
|
||||
`${ROUTES.MESSAGING_QUEUES_KAFKA_DETAIL}?${QueryParams.mqServiceView}=${callerView}`,
|
||||
);
|
||||
};
|
||||
|
||||
const { isCloudUser: isCloudUserVal } = useGetTenantLicense();
|
||||
|
||||
const getStartedRedirect = (
|
||||
link: string,
|
||||
sourceCard: string,
|
||||
event?: React.MouseEvent,
|
||||
): void => {
|
||||
const getStartedRedirect = (link: string, sourceCard: string): void => {
|
||||
logEvent('Messaging Queues: Get started clicked', {
|
||||
source: sourceCard,
|
||||
link: isCloudUserVal ? link : KAFKA_SETUP_DOC_LINK,
|
||||
});
|
||||
if (isCloudUserVal) {
|
||||
if (event && isModifierKeyPressed(event)) {
|
||||
openInNewTab(link);
|
||||
} else {
|
||||
history.push(link);
|
||||
}
|
||||
history.push(link);
|
||||
} else {
|
||||
window.open(KAFKA_SETUP_DOC_LINK, '_blank');
|
||||
}
|
||||
@@ -94,11 +79,10 @@ function MessagingQueues(): JSX.Element {
|
||||
<div className="button-grp">
|
||||
<Button
|
||||
type="default"
|
||||
onClick={(e): void =>
|
||||
onClick={(): void =>
|
||||
getStartedRedirect(
|
||||
`${ROUTES.GET_STARTED_APPLICATION_MONITORING}?${QueryParams.getStartedSource}=kafka&${QueryParams.getStartedSourceService}=${MessagingQueueHealthCheckService.Consumers}`,
|
||||
'Configure Consumer',
|
||||
e,
|
||||
)
|
||||
}
|
||||
>
|
||||
@@ -114,11 +98,10 @@ function MessagingQueues(): JSX.Element {
|
||||
<div className="button-grp">
|
||||
<Button
|
||||
type="default"
|
||||
onClick={(e): void =>
|
||||
onClick={(): void =>
|
||||
getStartedRedirect(
|
||||
`${ROUTES.GET_STARTED_APPLICATION_MONITORING}?${QueryParams.getStartedSource}=kafka&${QueryParams.getStartedSourceService}=${MessagingQueueHealthCheckService.Producers}`,
|
||||
'Configure Producer',
|
||||
e,
|
||||
)
|
||||
}
|
||||
>
|
||||
@@ -134,11 +117,10 @@ function MessagingQueues(): JSX.Element {
|
||||
<div className="button-grp">
|
||||
<Button
|
||||
type="default"
|
||||
onClick={(e): void =>
|
||||
onClick={(): void =>
|
||||
getStartedRedirect(
|
||||
`${ROUTES.GET_STARTED_INFRASTRUCTURE_MONITORING}?${QueryParams.getStartedSource}=kafka&${QueryParams.getStartedSourceService}=${MessagingQueueHealthCheckService.Kafka}`,
|
||||
'Monitor kafka',
|
||||
e,
|
||||
)
|
||||
}
|
||||
>
|
||||
@@ -161,8 +143,8 @@ function MessagingQueues(): JSX.Element {
|
||||
<div className="button-grp">
|
||||
<Button
|
||||
type="default"
|
||||
onClick={(e): void =>
|
||||
redirectToDetailsPage(MessagingQueuesViewType.consumerLag.value, e)
|
||||
onClick={(): void =>
|
||||
redirectToDetailsPage(MessagingQueuesViewType.consumerLag.value)
|
||||
}
|
||||
>
|
||||
{t('summarySection.viewDetailsButton')}
|
||||
@@ -179,8 +161,8 @@ function MessagingQueues(): JSX.Element {
|
||||
<div className="button-grp">
|
||||
<Button
|
||||
type="default"
|
||||
onClick={(e): void =>
|
||||
redirectToDetailsPage(MessagingQueuesViewType.producerLatency.value, e)
|
||||
onClick={(): void =>
|
||||
redirectToDetailsPage(MessagingQueuesViewType.producerLatency.value)
|
||||
}
|
||||
>
|
||||
{t('summarySection.viewDetailsButton')}
|
||||
@@ -197,11 +179,8 @@ function MessagingQueues(): JSX.Element {
|
||||
<div className="button-grp">
|
||||
<Button
|
||||
type="default"
|
||||
onClick={(e): void =>
|
||||
redirectToDetailsPage(
|
||||
MessagingQueuesViewType.partitionLatency.value,
|
||||
e,
|
||||
)
|
||||
onClick={(): void =>
|
||||
redirectToDetailsPage(MessagingQueuesViewType.partitionLatency.value)
|
||||
}
|
||||
>
|
||||
{t('summarySection.viewDetailsButton')}
|
||||
@@ -218,8 +197,8 @@ function MessagingQueues(): JSX.Element {
|
||||
<div className="button-grp">
|
||||
<Button
|
||||
type="default"
|
||||
onClick={(e): void =>
|
||||
redirectToDetailsPage(MessagingQueuesViewType.dropRate.value, e)
|
||||
onClick={(): void =>
|
||||
redirectToDetailsPage(MessagingQueuesViewType.dropRate.value)
|
||||
}
|
||||
>
|
||||
{t('summarySection.viewDetailsButton')}
|
||||
@@ -236,8 +215,8 @@ function MessagingQueues(): JSX.Element {
|
||||
<div className="button-grp">
|
||||
<Button
|
||||
type="default"
|
||||
onClick={(e): void =>
|
||||
redirectToDetailsPage(MessagingQueuesViewType.metricPage.value, e)
|
||||
onClick={(): void =>
|
||||
redirectToDetailsPage(MessagingQueuesViewType.metricPage.value)
|
||||
}
|
||||
>
|
||||
{t('summarySection.viewDetailsButton')}
|
||||
|
||||
@@ -16,7 +16,6 @@ import history from 'lib/history';
|
||||
import { Cog } from 'lucide-react';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
import { USER_ROLES } from 'types/roles';
|
||||
import { isModifierKeyPressed, openInNewTab } from 'utils/navigation';
|
||||
|
||||
import { getRoutes } from './utils';
|
||||
|
||||
@@ -185,6 +184,12 @@ function SettingsPage(): JSX.Element {
|
||||
],
|
||||
);
|
||||
|
||||
const isCtrlMetaKey = (e: MouseEvent): boolean => e.ctrlKey || e.metaKey;
|
||||
|
||||
const openInNewTab = (path: string): void => {
|
||||
window.open(path, '_blank');
|
||||
};
|
||||
|
||||
const onClickHandler = useCallback(
|
||||
(key: string, event: MouseEvent | null) => {
|
||||
const params = new URLSearchParams(search);
|
||||
@@ -193,7 +198,7 @@ function SettingsPage(): JSX.Element {
|
||||
const queryString = getQueryString(availableParams || [], params);
|
||||
|
||||
if (pathname !== key) {
|
||||
if (event && isModifierKeyPressed(event)) {
|
||||
if (event && isCtrlMetaKey(event)) {
|
||||
openInNewTab(`${key}?${queryString.join('&')}`);
|
||||
} else {
|
||||
history.push(`${key}?${queryString.join('&')}`, {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/* eslint-disable react/no-unescaped-entities */
|
||||
import React, { useCallback, useEffect } from 'react';
|
||||
import { useCallback, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useMutation } from 'react-query';
|
||||
import type { TabsProps } from 'antd';
|
||||
@@ -27,7 +27,6 @@ import { CircleArrowRight } from 'lucide-react';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
import APIError from 'types/api/error';
|
||||
import { LicensePlatform } from 'types/api/licensesV3/getActive';
|
||||
import { navigateToPage } from 'utils/navigation';
|
||||
import { getFormattedDate } from 'utils/timeUtils';
|
||||
|
||||
import CustomerStoryCard from './CustomerStoryCard';
|
||||
@@ -134,10 +133,10 @@ export default function WorkspaceBlocked(): JSX.Element {
|
||||
});
|
||||
};
|
||||
|
||||
const handleViewBilling = (e?: React.MouseEvent): void => {
|
||||
const handleViewBilling = (): void => {
|
||||
logEvent('Workspace Blocked: User Clicked View Billing', {});
|
||||
|
||||
navigateToPage(ROUTES.BILLING, history.push, e);
|
||||
history.push(ROUTES.BILLING);
|
||||
};
|
||||
|
||||
const renderCustomerStories = (
|
||||
@@ -297,7 +296,7 @@ export default function WorkspaceBlocked(): JSX.Element {
|
||||
type="link"
|
||||
size="small"
|
||||
role="button"
|
||||
onClick={(e): void => handleViewBilling(e)}
|
||||
onClick={handleViewBilling}
|
||||
>
|
||||
View Billing
|
||||
</Button>
|
||||
|
||||
@@ -1,123 +0,0 @@
|
||||
import {
|
||||
isModifierKeyPressed,
|
||||
navigateToPage,
|
||||
openInNewTab,
|
||||
} from '../navigation';
|
||||
|
||||
describe('navigation utilities', () => {
|
||||
const originalWindowOpen = window.open;
|
||||
|
||||
beforeEach(() => {
|
||||
window.open = jest.fn();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
window.open = originalWindowOpen;
|
||||
});
|
||||
|
||||
describe('isModifierKeyPressed', () => {
|
||||
const createMouseEvent = (overrides: Partial<MouseEvent> = {}): MouseEvent =>
|
||||
({
|
||||
metaKey: false,
|
||||
ctrlKey: false,
|
||||
...overrides,
|
||||
} as MouseEvent);
|
||||
|
||||
it('returns true when metaKey is pressed (Cmd on Mac)', () => {
|
||||
const event = createMouseEvent({ metaKey: true });
|
||||
expect(isModifierKeyPressed(event)).toBe(true);
|
||||
});
|
||||
|
||||
it('returns true when ctrlKey is pressed (Ctrl on Windows/Linux)', () => {
|
||||
const event = createMouseEvent({ ctrlKey: true });
|
||||
expect(isModifierKeyPressed(event)).toBe(true);
|
||||
});
|
||||
|
||||
it('returns true when both metaKey and ctrlKey are pressed', () => {
|
||||
const event = createMouseEvent({ metaKey: true, ctrlKey: true });
|
||||
expect(isModifierKeyPressed(event)).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false when neither modifier key is pressed', () => {
|
||||
const event = createMouseEvent();
|
||||
expect(isModifierKeyPressed(event)).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false when only shiftKey or altKey are pressed', () => {
|
||||
const event = createMouseEvent({
|
||||
shiftKey: true,
|
||||
altKey: true,
|
||||
} as Partial<MouseEvent>);
|
||||
expect(isModifierKeyPressed(event)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('openInNewTab', () => {
|
||||
it('calls window.open with the given path and _blank target', () => {
|
||||
openInNewTab('/dashboard');
|
||||
expect(window.open).toHaveBeenCalledWith('/dashboard', '_blank');
|
||||
});
|
||||
|
||||
it('handles full URLs', () => {
|
||||
openInNewTab('https://example.com/page');
|
||||
expect(window.open).toHaveBeenCalledWith(
|
||||
'https://example.com/page',
|
||||
'_blank',
|
||||
);
|
||||
});
|
||||
|
||||
it('handles paths with query strings', () => {
|
||||
openInNewTab('/alerts?tab=AlertRules&relativeTime=30m');
|
||||
expect(window.open).toHaveBeenCalledWith(
|
||||
'/alerts?tab=AlertRules&relativeTime=30m',
|
||||
'_blank',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('navigateToPage', () => {
|
||||
const mockNavigate = jest.fn() as jest.MockedFunction<(path: string) => void>;
|
||||
|
||||
beforeEach(() => {
|
||||
mockNavigate.mockClear();
|
||||
});
|
||||
|
||||
it('opens new tab when metaKey is pressed', () => {
|
||||
const event = { metaKey: true, ctrlKey: false } as MouseEvent;
|
||||
navigateToPage('/services', mockNavigate, event);
|
||||
|
||||
expect(window.open).toHaveBeenCalledWith('/services', '_blank');
|
||||
expect(mockNavigate).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('opens new tab when ctrlKey is pressed', () => {
|
||||
const event = { metaKey: false, ctrlKey: true } as MouseEvent;
|
||||
navigateToPage('/services', mockNavigate, event);
|
||||
|
||||
expect(window.open).toHaveBeenCalledWith('/services', '_blank');
|
||||
expect(mockNavigate).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('calls navigate callback when no modifier key is pressed', () => {
|
||||
const event = { metaKey: false, ctrlKey: false } as MouseEvent;
|
||||
navigateToPage('/services', mockNavigate, event);
|
||||
|
||||
expect(mockNavigate).toHaveBeenCalledWith('/services');
|
||||
expect(window.open).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('calls navigate callback when no event is provided', () => {
|
||||
navigateToPage('/services', mockNavigate);
|
||||
|
||||
expect(mockNavigate).toHaveBeenCalledWith('/services');
|
||||
expect(window.open).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('calls navigate callback when event is undefined', () => {
|
||||
navigateToPage('/dashboard', mockNavigate, undefined);
|
||||
|
||||
expect(mockNavigate).toHaveBeenCalledWith('/dashboard');
|
||||
expect(window.open).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,37 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
/**
|
||||
* Returns true if the user is holding Cmd (Mac) or Ctrl (Windows/Linux)
|
||||
* during a click event — the universal "open in new tab" modifier.
|
||||
*/
|
||||
export const isModifierKeyPressed = (
|
||||
event: MouseEvent | React.MouseEvent,
|
||||
): boolean => event.metaKey || event.ctrlKey;
|
||||
|
||||
/**
|
||||
* Opens the given path in a new browser tab.
|
||||
*/
|
||||
export const openInNewTab = (path: string): void => {
|
||||
window.open(path, '_blank');
|
||||
};
|
||||
|
||||
/**
|
||||
* Navigates to a path, respecting modifier keys. If Cmd/Ctrl is held,
|
||||
* the path is opened in a new tab. Otherwise, the provided `navigate`
|
||||
* callback is invoked for SPA navigation.
|
||||
*
|
||||
* @param path - The target URL path
|
||||
* @param navigate - SPA navigation callback (e.g. history.push, safeNavigate)
|
||||
* @param event - Optional mouse event to check for modifier keys
|
||||
*/
|
||||
export const navigateToPage = (
|
||||
path: string,
|
||||
navigate: (path: string) => void,
|
||||
event?: MouseEvent | React.MouseEvent,
|
||||
): void => {
|
||||
if (event && isModifierKeyPressed(event)) {
|
||||
openInNewTab(path);
|
||||
} else {
|
||||
navigate(path);
|
||||
}
|
||||
};
|
||||
@@ -61,11 +61,13 @@ var (
|
||||
}
|
||||
)
|
||||
|
||||
type fieldMapper struct {}
|
||||
type fieldMapper struct {
|
||||
}
|
||||
|
||||
func NewFieldMapper() qbtypes.FieldMapper {
|
||||
return &fieldMapper{}
|
||||
}
|
||||
|
||||
func (m *fieldMapper) getColumn(_ context.Context, key *telemetrytypes.TelemetryFieldKey) (*schema.Column, error) {
|
||||
switch key.FieldContext {
|
||||
case telemetrytypes.FieldContextResource:
|
||||
@@ -252,27 +254,12 @@ func (m *fieldMapper) buildFieldForJSON(key *telemetrytypes.TelemetryFieldKey) (
|
||||
"plan length is less than 2 for promoted path: %s", key.Name)
|
||||
}
|
||||
|
||||
node := plan[1]
|
||||
promotedExpr := fmt.Sprintf(
|
||||
"dynamicElement(%s, '%s')",
|
||||
node.FieldPath(),
|
||||
node.TerminalConfig.ElemType.StringValue(),
|
||||
// promoted column first then body_json column
|
||||
// TODO(Piyush): Change this in future for better performance
|
||||
expr = fmt.Sprintf("coalesce(%s, %s)",
|
||||
fmt.Sprintf("dynamicElement(%s, '%s')", plan[1].FieldPath(), plan[1].TerminalConfig.ElemType.StringValue()),
|
||||
expr,
|
||||
)
|
||||
|
||||
// dynamicElement returns NULL for scalar types or an empty array for array types.
|
||||
if node.TerminalConfig.ElemType.IsArray {
|
||||
expr = fmt.Sprintf(
|
||||
"if(length(%s) > 0, %s, %s)",
|
||||
promotedExpr,
|
||||
promotedExpr,
|
||||
expr,
|
||||
)
|
||||
} else {
|
||||
// promoted column first then body_json column
|
||||
// TODO(Piyush): Change this in future for better performance
|
||||
expr = fmt.Sprintf("coalesce(%s, %s)", promotedExpr, expr)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return expr, nil
|
||||
@@ -294,7 +281,8 @@ func (m *fieldMapper) buildArrayConcat(plan telemetrytypes.JSONAccessPlan) (stri
|
||||
}
|
||||
|
||||
// Build arrayMap expressions for ALL available branches at the root level.
|
||||
// Iterate branches in deterministic order (JSON then Dynamic)
|
||||
// Iterate branches in deterministic order (JSON then Dynamic) so generated SQL
|
||||
// is stable across environments; map iteration order is random in Go.
|
||||
var arrayMapExpressions []string
|
||||
for _, node := range plan {
|
||||
for _, branchType := range node.BranchesInOrder() {
|
||||
|
||||
@@ -32,6 +32,7 @@ func NewJSONConditionBuilder(key *telemetrytypes.TelemetryFieldKey, valueType te
|
||||
|
||||
// BuildCondition builds the full WHERE condition for body_json JSON paths
|
||||
func (c *jsonConditionBuilder) buildJSONCondition(operator qbtypes.FilterOperator, value any, sb *sqlbuilder.SelectBuilder) (string, error) {
|
||||
|
||||
conditions := []string{}
|
||||
for _, node := range c.key.JSONPlan {
|
||||
condition, err := c.emitPlannedCondition(node, operator, value, sb)
|
||||
@@ -72,9 +73,9 @@ func (c *jsonConditionBuilder) buildTerminalCondition(node *telemetrytypes.JSONA
|
||||
|
||||
// switch operator for array membership checks
|
||||
switch operator {
|
||||
case qbtypes.FilterOperatorContains:
|
||||
case qbtypes.FilterOperatorContains, qbtypes.FilterOperatorIn:
|
||||
operator = qbtypes.FilterOperatorEqual
|
||||
case qbtypes.FilterOperatorNotContains:
|
||||
case qbtypes.FilterOperatorNotContains, qbtypes.FilterOperatorNotIn:
|
||||
operator = qbtypes.FilterOperatorNotEqual
|
||||
}
|
||||
}
|
||||
@@ -190,14 +191,13 @@ func (c *jsonConditionBuilder) buildArrayMembershipCondition(node *telemetrytype
|
||||
arrayExpr = typedArrayExpr()
|
||||
}
|
||||
|
||||
key := "x"
|
||||
fieldExpr, value := querybuilder.DataTypeCollisionHandledFieldName(&localKeyCopy, value, key, operator)
|
||||
fieldExpr, value := querybuilder.DataTypeCollisionHandledFieldName(&localKeyCopy, value, "x", operator)
|
||||
op, err := c.applyOperator(sb, fieldExpr, operator, value)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return fmt.Sprintf("arrayExists(%s -> %s, %s)", key, op, arrayExpr), nil
|
||||
return fmt.Sprintf("arrayExists(%s -> %s, %s)", fieldExpr, op, arrayExpr), nil
|
||||
}
|
||||
|
||||
// recurseArrayHops recursively builds array traversal conditions
|
||||
@@ -279,31 +279,27 @@ func (c *jsonConditionBuilder) applyOperator(sb *sqlbuilder.SelectBuilder, field
|
||||
case qbtypes.FilterOperatorNotContains:
|
||||
return sb.NotILike(fieldExpr, fmt.Sprintf("%%%v%%", value)), nil
|
||||
case qbtypes.FilterOperatorIn, qbtypes.FilterOperatorNotIn:
|
||||
// emulate IN/NOT IN using OR/AND over equals to leverage indexes consistently
|
||||
values, ok := value.([]any)
|
||||
if !ok {
|
||||
values = []any{value}
|
||||
}
|
||||
if operator == qbtypes.FilterOperatorIn {
|
||||
return sb.In(fieldExpr, values...), nil
|
||||
conds := []string{}
|
||||
for _, v := range values {
|
||||
if operator == qbtypes.FilterOperatorIn {
|
||||
conds = append(conds, sb.E(fieldExpr, v))
|
||||
} else {
|
||||
conds = append(conds, sb.NE(fieldExpr, v))
|
||||
}
|
||||
}
|
||||
return sb.NotIn(fieldExpr, values...), nil
|
||||
if operator == qbtypes.FilterOperatorIn {
|
||||
return sb.Or(conds...), nil
|
||||
}
|
||||
return sb.And(conds...), nil
|
||||
case qbtypes.FilterOperatorExists:
|
||||
return fmt.Sprintf("%s IS NOT NULL", fieldExpr), nil
|
||||
case qbtypes.FilterOperatorNotExists:
|
||||
return fmt.Sprintf("%s IS NULL", fieldExpr), nil
|
||||
// between and not between
|
||||
case qbtypes.FilterOperatorBetween, qbtypes.FilterOperatorNotBetween:
|
||||
values, ok := value.([]any)
|
||||
if !ok {
|
||||
return "", qbtypes.ErrBetweenValues
|
||||
}
|
||||
if len(values) != 2 {
|
||||
return "", qbtypes.ErrBetweenValues
|
||||
}
|
||||
if operator == qbtypes.FilterOperatorBetween {
|
||||
return sb.Between(fieldExpr, values[0], values[1]), nil
|
||||
}
|
||||
return sb.NotBetween(fieldExpr, values[0], values[1]), nil
|
||||
default:
|
||||
return "", qbtypes.ErrUnsupportedOperator
|
||||
}
|
||||
|
||||
@@ -316,7 +316,7 @@ func TestStatementBuilderListQueryBody(t *testing.T) {
|
||||
Limit: 10,
|
||||
},
|
||||
expected: qbtypes.Statement{
|
||||
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_logs.distributed_logs_v2_resource WHERE true AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?) SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, scope_name, scope_version, body, body_json, body_json_promoted, attributes_string, attributes_number, attributes_bool, resources_string, scope_string FROM signoz_logs.distributed_logs_v2 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND ((arrayExists(`body_json.education`-> (arrayExists(x -> LOWER(toString(x)) LIKE LOWER(?), dynamicElement(`body_json.education`.`parameters`, 'Array(Nullable(Float64))')) OR arrayExists(x -> toFloat64(x) = ?, dynamicElement(`body_json.education`.`parameters`, 'Array(Nullable(Float64))'))), dynamicElement(body_json.`education`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=0))'))) OR (arrayExists(`body_json.education`-> (arrayExists(x -> LOWER(toString(x)) LIKE LOWER(?), arrayMap(x->dynamicElement(x, 'Float64'), arrayFilter(x->(dynamicType(x) = 'Float64'), dynamicElement(`body_json.education`.`parameters`, 'Array(Dynamic)')))) OR arrayExists(x -> toFloat64(x) = ?, arrayMap(x->dynamicElement(x, 'Float64'), arrayFilter(x->(dynamicType(x) = 'Float64'), dynamicElement(`body_json.education`.`parameters`, 'Array(Dynamic)'))))), dynamicElement(body_json.`education`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=0))')))) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? LIMIT ?",
|
||||
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_logs.distributed_logs_v2_resource WHERE true AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?) SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, scope_name, scope_version, body, body_json, body_json_promoted, attributes_string, attributes_number, attributes_bool, resources_string, scope_string FROM signoz_logs.distributed_logs_v2 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND ((arrayExists(`body_json.education`-> (arrayExists(toString(x) -> LOWER(toString(x)) LIKE LOWER(?), dynamicElement(`body_json.education`.`parameters`, 'Array(Nullable(Float64))')) OR arrayExists(toFloat64(x) -> toFloat64(x) = ?, dynamicElement(`body_json.education`.`parameters`, 'Array(Nullable(Float64))'))), dynamicElement(body_json.`education`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=0))'))) OR (arrayExists(`body_json.education`-> (arrayExists(toString(x) -> LOWER(toString(x)) LIKE LOWER(?), arrayMap(x->dynamicElement(x, 'Float64'), arrayFilter(x->(dynamicType(x) = 'Float64'), dynamicElement(`body_json.education`.`parameters`, 'Array(Dynamic)')))) OR arrayExists(toFloat64(x) -> toFloat64(x) = ?, arrayMap(x->dynamicElement(x, 'Float64'), arrayFilter(x->(dynamicType(x) = 'Float64'), dynamicElement(`body_json.education`.`parameters`, 'Array(Dynamic)'))))), dynamicElement(body_json.`education`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=0))')))) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? LIMIT ?",
|
||||
Args: []any{uint64(1747945619), uint64(1747983448), "%1.65%", 1.65, "%1.65%", 1.65, "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 10},
|
||||
Warnings: []string{"Key `education[].parameters` is ambiguous, found 2 different combinations of field context / data type: [name=education[].parameters,context=body,datatype=[]float64,jsondatatype=Array(Nullable(Float64)) name=education[].parameters,context=body,datatype=[]dynamic,jsondatatype=Array(Dynamic)]."},
|
||||
},
|
||||
@@ -345,7 +345,7 @@ func TestStatementBuilderListQueryBody(t *testing.T) {
|
||||
Limit: 10,
|
||||
},
|
||||
expected: qbtypes.Statement{
|
||||
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_logs.distributed_logs_v2_resource WHERE true AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?) SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, scope_name, scope_version, body, body_json, body_json_promoted, attributes_string, attributes_number, attributes_bool, resources_string, scope_string FROM signoz_logs.distributed_logs_v2 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND ((arrayExists(`body_json.education`-> (arrayExists(x -> LOWER(toString(x)) LIKE LOWER(?), dynamicElement(`body_json.education`.`parameters`, 'Array(Nullable(Float64))')) OR arrayExists(x -> x = ?, dynamicElement(`body_json.education`.`parameters`, 'Array(Nullable(Float64))'))), dynamicElement(body_json.`education`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=0))'))) OR (arrayExists(`body_json.education`-> (arrayExists(x -> LOWER(toString(x)) LIKE LOWER(?), arrayMap(x->dynamicElement(x, 'Bool'), arrayFilter(x->(dynamicType(x) = 'Bool'), dynamicElement(`body_json.education`.`parameters`, 'Array(Dynamic)')))) OR arrayExists(x -> x = ?, arrayMap(x->dynamicElement(x, 'Bool'), arrayFilter(x->(dynamicType(x) = 'Bool'), dynamicElement(`body_json.education`.`parameters`, 'Array(Dynamic)'))))), dynamicElement(body_json.`education`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=0))')))) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? LIMIT ?",
|
||||
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_logs.distributed_logs_v2_resource WHERE true AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?) SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, scope_name, scope_version, body, body_json, body_json_promoted, attributes_string, attributes_number, attributes_bool, resources_string, scope_string FROM signoz_logs.distributed_logs_v2 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND ((arrayExists(`body_json.education`-> (arrayExists(toString(x) -> LOWER(toString(x)) LIKE LOWER(?), dynamicElement(`body_json.education`.`parameters`, 'Array(Nullable(Float64))')) OR arrayExists(x -> x = ?, dynamicElement(`body_json.education`.`parameters`, 'Array(Nullable(Float64))'))), dynamicElement(body_json.`education`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=0))'))) OR (arrayExists(`body_json.education`-> (arrayExists(toString(x) -> LOWER(toString(x)) LIKE LOWER(?), arrayMap(x->dynamicElement(x, 'Bool'), arrayFilter(x->(dynamicType(x) = 'Bool'), dynamicElement(`body_json.education`.`parameters`, 'Array(Dynamic)')))) OR arrayExists(x -> x = ?, arrayMap(x->dynamicElement(x, 'Bool'), arrayFilter(x->(dynamicType(x) = 'Bool'), dynamicElement(`body_json.education`.`parameters`, 'Array(Dynamic)'))))), dynamicElement(body_json.`education`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=0))')))) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? LIMIT ?",
|
||||
Args: []any{uint64(1747945619), uint64(1747983448), "%true%", true, "%true%", true, "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 10},
|
||||
Warnings: []string{"Key `education[].parameters` is ambiguous, found 2 different combinations of field context / data type: [name=education[].parameters,context=body,datatype=[]float64,jsondatatype=Array(Nullable(Float64)) name=education[].parameters,context=body,datatype=[]dynamic,jsondatatype=Array(Dynamic)]."},
|
||||
},
|
||||
@@ -360,55 +360,12 @@ func TestStatementBuilderListQueryBody(t *testing.T) {
|
||||
Limit: 10,
|
||||
},
|
||||
expected: qbtypes.Statement{
|
||||
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_logs.distributed_logs_v2_resource WHERE true AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?) SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, scope_name, scope_version, body, body_json, body_json_promoted, attributes_string, attributes_number, attributes_bool, resources_string, scope_string FROM signoz_logs.distributed_logs_v2 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND ((arrayExists(`body_json.education`-> (arrayExists(x -> LOWER(toString(x)) LIKE LOWER(?), dynamicElement(`body_json.education`.`parameters`, 'Array(Nullable(Float64))')) OR arrayExists(x -> toString(x) = ?, dynamicElement(`body_json.education`.`parameters`, 'Array(Nullable(Float64))'))), dynamicElement(body_json.`education`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=0))'))) OR (arrayExists(`body_json.education`-> (arrayExists(x -> LOWER(x) LIKE LOWER(?), arrayMap(x->dynamicElement(x, 'String'), arrayFilter(x->(dynamicType(x) = 'String'), dynamicElement(`body_json.education`.`parameters`, 'Array(Dynamic)')))) OR arrayExists(x -> x = ?, arrayMap(x->dynamicElement(x, 'String'), arrayFilter(x->(dynamicType(x) = 'String'), dynamicElement(`body_json.education`.`parameters`, 'Array(Dynamic)'))))), dynamicElement(body_json.`education`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=0))')))) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? LIMIT ?",
|
||||
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_logs.distributed_logs_v2_resource WHERE true AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?) SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, scope_name, scope_version, body, body_json, body_json_promoted, attributes_string, attributes_number, attributes_bool, resources_string, scope_string FROM signoz_logs.distributed_logs_v2 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND ((arrayExists(`body_json.education`-> (arrayExists(toString(x) -> LOWER(toString(x)) LIKE LOWER(?), dynamicElement(`body_json.education`.`parameters`, 'Array(Nullable(Float64))')) OR arrayExists(toString(x) -> toString(x) = ?, dynamicElement(`body_json.education`.`parameters`, 'Array(Nullable(Float64))'))), dynamicElement(body_json.`education`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=0))'))) OR (arrayExists(`body_json.education`-> (arrayExists(x -> LOWER(x) LIKE LOWER(?), arrayMap(x->dynamicElement(x, 'String'), arrayFilter(x->(dynamicType(x) = 'String'), dynamicElement(`body_json.education`.`parameters`, 'Array(Dynamic)')))) OR arrayExists(x -> x = ?, arrayMap(x->dynamicElement(x, 'String'), arrayFilter(x->(dynamicType(x) = 'String'), dynamicElement(`body_json.education`.`parameters`, 'Array(Dynamic)'))))), dynamicElement(body_json.`education`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=0))')))) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? LIMIT ?",
|
||||
Args: []any{uint64(1747945619), uint64(1747983448), "%passed%", "passed", "%passed%", "passed", "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 10},
|
||||
Warnings: []string{"Key `education[].parameters` is ambiguous, found 2 different combinations of field context / data type: [name=education[].parameters,context=body,datatype=[]float64,jsondatatype=Array(Nullable(Float64)) name=education[].parameters,context=body,datatype=[]dynamic,jsondatatype=Array(Dynamic)]."},
|
||||
},
|
||||
expectedErr: nil,
|
||||
},
|
||||
{
|
||||
name: "Dynamic array IN Operator",
|
||||
requestType: qbtypes.RequestTypeRaw,
|
||||
query: qbtypes.QueryBuilderQuery[qbtypes.LogAggregation]{
|
||||
Signal: telemetrytypes.SignalLogs,
|
||||
Filter: &qbtypes.Filter{Expression: "body.education[].parameters IN [1.65, 1.99]"},
|
||||
Limit: 10,
|
||||
},
|
||||
expected: qbtypes.Statement{
|
||||
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_logs.distributed_logs_v2_resource WHERE true AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?) SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, scope_name, scope_version, body, body_json, body_json_promoted, attributes_string, attributes_number, attributes_bool, resources_string, scope_string FROM signoz_logs.distributed_logs_v2 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND ((arrayExists(`body_json.education`-> (arrayExists(x -> toFloat64(x) IN (?, ?), dynamicElement(`body_json.education`.`parameters`, 'Array(Nullable(Float64))'))), dynamicElement(body_json.`education`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=0))'))) OR (arrayExists(`body_json.education`-> (arrayExists(x -> x IN (?, ?), arrayMap(x->dynamicElement(x, 'Array(Nullable(Float64))'), arrayFilter(x->(dynamicType(x) = 'Array(Nullable(Float64))'), dynamicElement(`body_json.education`.`parameters`, 'Array(Dynamic)'))))), dynamicElement(body_json.`education`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=0))')))) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? LIMIT ?",
|
||||
Args: []any{uint64(1747945619), uint64(1747983448), 1.65, 1.99, 1.65, 1.99, "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 10},
|
||||
Warnings: []string{"Key `education[].parameters` is ambiguous, found 2 different combinations of field context / data type: [name=education[].parameters,context=body,datatype=[]float64,jsondatatype=Array(Nullable(Float64)) name=education[].parameters,context=body,datatype=[]dynamic,jsondatatype=Array(Dynamic)]."},
|
||||
},
|
||||
expectedErr: nil,
|
||||
},
|
||||
{
|
||||
name: "Integer BETWEEN Operator",
|
||||
requestType: qbtypes.RequestTypeRaw,
|
||||
query: qbtypes.QueryBuilderQuery[qbtypes.LogAggregation]{
|
||||
Signal: telemetrytypes.SignalLogs,
|
||||
Filter: &qbtypes.Filter{Expression: "education[].awards[].semester BETWEEN 2 AND 4"},
|
||||
Limit: 10,
|
||||
},
|
||||
expected: qbtypes.Statement{
|
||||
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_logs.distributed_logs_v2_resource WHERE (arrayExists(`body_json.education`-> (arrayExists(`body_json.education[].awards`-> toFloat64(dynamicElement(`body_json.education[].awards`.`semester`, 'Int64')) BETWEEN ? AND ?, dynamicElement(`body_json.education`.`awards`, 'Array(JSON(max_dynamic_types=8, max_dynamic_paths=0))')) OR arrayExists(`body_json.education[].awards`-> toFloat64(dynamicElement(`body_json.education[].awards`.`semester`, 'Int64')) BETWEEN ? AND ?, arrayMap(x->dynamicElement(x, 'JSON'), arrayFilter(x->(dynamicType(x) = 'JSON'), dynamicElement(`body_json.education`.`awards`, 'Array(Dynamic)'))))), dynamicElement(body_json.`education`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=0))'))) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?) SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, scope_name, scope_version, body, body_json, body_json_promoted, attributes_string, attributes_number, attributes_bool, resources_string, scope_string FROM signoz_logs.distributed_logs_v2 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND (arrayExists(`body_json.education`-> (arrayExists(`body_json.education[].awards`-> toFloat64(dynamicElement(`body_json.education[].awards`.`semester`, 'Int64')) BETWEEN ? AND ?, dynamicElement(`body_json.education`.`awards`, 'Array(JSON(max_dynamic_types=8, max_dynamic_paths=0))')) OR arrayExists(`body_json.education[].awards`-> toFloat64(dynamicElement(`body_json.education[].awards`.`semester`, 'Int64')) BETWEEN ? AND ?, arrayMap(x->dynamicElement(x, 'JSON'), arrayFilter(x->(dynamicType(x) = 'JSON'), dynamicElement(`body_json.education`.`awards`, 'Array(Dynamic)'))))), dynamicElement(body_json.`education`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=0))'))) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? LIMIT ?",
|
||||
Args: []any{float64(2), float64(4), float64(2), float64(4), uint64(1747945619), uint64(1747983448), float64(2), float64(4), float64(2), float64(4), "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 10},
|
||||
},
|
||||
expectedErr: nil,
|
||||
},
|
||||
{
|
||||
name: "Integer IN Operator",
|
||||
requestType: qbtypes.RequestTypeRaw,
|
||||
query: qbtypes.QueryBuilderQuery[qbtypes.LogAggregation]{
|
||||
Signal: telemetrytypes.SignalLogs,
|
||||
Filter: &qbtypes.Filter{Expression: "education[].awards[].semester IN [2, 4]"},
|
||||
Limit: 10,
|
||||
},
|
||||
expected: qbtypes.Statement{
|
||||
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_logs.distributed_logs_v2_resource WHERE (arrayExists(`body_json.education`-> (arrayExists(`body_json.education[].awards`-> toFloat64(dynamicElement(`body_json.education[].awards`.`semester`, 'Int64')) IN (?, ?), dynamicElement(`body_json.education`.`awards`, 'Array(JSON(max_dynamic_types=8, max_dynamic_paths=0))')) OR arrayExists(`body_json.education[].awards`-> toFloat64(dynamicElement(`body_json.education[].awards`.`semester`, 'Int64')) IN (?, ?), arrayMap(x->dynamicElement(x, 'JSON'), arrayFilter(x->(dynamicType(x) = 'JSON'), dynamicElement(`body_json.education`.`awards`, 'Array(Dynamic)'))))), dynamicElement(body_json.`education`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=0))'))) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?) SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, scope_name, scope_version, body, body_json, body_json_promoted, attributes_string, attributes_number, attributes_bool, resources_string, scope_string FROM signoz_logs.distributed_logs_v2 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND (arrayExists(`body_json.education`-> (arrayExists(`body_json.education[].awards`-> toFloat64(dynamicElement(`body_json.education[].awards`.`semester`, 'Int64')) IN (?, ?), dynamicElement(`body_json.education`.`awards`, 'Array(JSON(max_dynamic_types=8, max_dynamic_paths=0))')) OR arrayExists(`body_json.education[].awards`-> toFloat64(dynamicElement(`body_json.education[].awards`.`semester`, 'Int64')) IN (?, ?), arrayMap(x->dynamicElement(x, 'JSON'), arrayFilter(x->(dynamicType(x) = 'JSON'), dynamicElement(`body_json.education`.`awards`, 'Array(Dynamic)'))))), dynamicElement(body_json.`education`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=0))'))) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? LIMIT ?",
|
||||
Args: []any{float64(2), float64(4), float64(2), float64(4), uint64(1747945619), uint64(1747983448), float64(2), float64(4), float64(2), float64(4), "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 10},
|
||||
},
|
||||
expectedErr: nil,
|
||||
},
|
||||
{
|
||||
name: "Equals to 'sports' inside array of awards",
|
||||
requestType: qbtypes.RequestTypeRaw,
|
||||
@@ -432,7 +389,7 @@ func TestStatementBuilderListQueryBody(t *testing.T) {
|
||||
Limit: 10,
|
||||
},
|
||||
expected: qbtypes.Statement{
|
||||
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_logs.distributed_logs_v2_resource WHERE true AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?) SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, scope_name, scope_version, body, body_json, body_json_promoted, attributes_string, attributes_number, attributes_bool, resources_string, scope_string FROM signoz_logs.distributed_logs_v2 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND ((arrayExists(`body_json.interests`-> arrayExists(`body_json.interests[].entities`-> arrayExists(`body_json.interests[].entities[].reviews`-> arrayExists(`body_json.interests[].entities[].reviews[].entries`-> arrayExists(`body_json.interests[].entities[].reviews[].entries[].metadata`-> arrayExists(`body_json.interests[].entities[].reviews[].entries[].metadata[].positions`-> (arrayExists(x -> LOWER(toString(x)) LIKE LOWER(?), dynamicElement(`body_json.interests[].entities[].reviews[].entries[].metadata[].positions`.`ratings`, 'Array(Nullable(Int64))')) OR arrayExists(x -> toFloat64(x) = ?, dynamicElement(`body_json.interests[].entities[].reviews[].entries[].metadata[].positions`.`ratings`, 'Array(Nullable(Int64))'))), dynamicElement(`body_json.interests[].entities[].reviews[].entries[].metadata`.`positions`, 'Array(JSON(max_dynamic_types=0, max_dynamic_paths=0))')), dynamicElement(`body_json.interests[].entities[].reviews[].entries`.`metadata`, 'Array(JSON(max_dynamic_types=1, max_dynamic_paths=0))')), dynamicElement(`body_json.interests[].entities[].reviews`.`entries`, 'Array(JSON(max_dynamic_types=2, max_dynamic_paths=0))')), dynamicElement(`body_json.interests[].entities`.`reviews`, 'Array(JSON(max_dynamic_types=4, max_dynamic_paths=0))')), dynamicElement(`body_json.interests`.`entities`, 'Array(JSON(max_dynamic_types=8, max_dynamic_paths=0))')), dynamicElement(body_json.`interests`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=0))'))) OR (arrayExists(`body_json.interests`-> arrayExists(`body_json.interests[].entities`-> arrayExists(`body_json.interests[].entities[].reviews`-> arrayExists(`body_json.interests[].entities[].reviews[].entries`-> arrayExists(`body_json.interests[].entities[].reviews[].entries[].metadata`-> arrayExists(`body_json.interests[].entities[].reviews[].entries[].metadata[].positions`-> (arrayExists(x -> LOWER(x) LIKE LOWER(?), dynamicElement(`body_json.interests[].entities[].reviews[].entries[].metadata[].positions`.`ratings`, 'Array(Nullable(String))')) OR arrayExists(x -> toFloat64OrNull(x) = ?, dynamicElement(`body_json.interests[].entities[].reviews[].entries[].metadata[].positions`.`ratings`, 'Array(Nullable(String))'))), dynamicElement(`body_json.interests[].entities[].reviews[].entries[].metadata`.`positions`, 'Array(JSON(max_dynamic_types=0, max_dynamic_paths=0))')), dynamicElement(`body_json.interests[].entities[].reviews[].entries`.`metadata`, 'Array(JSON(max_dynamic_types=1, max_dynamic_paths=0))')), dynamicElement(`body_json.interests[].entities[].reviews`.`entries`, 'Array(JSON(max_dynamic_types=2, max_dynamic_paths=0))')), dynamicElement(`body_json.interests[].entities`.`reviews`, 'Array(JSON(max_dynamic_types=4, max_dynamic_paths=0))')), dynamicElement(`body_json.interests`.`entities`, 'Array(JSON(max_dynamic_types=8, max_dynamic_paths=0))')), dynamicElement(body_json.`interests`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=0))')))) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? LIMIT ?",
|
||||
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_logs.distributed_logs_v2_resource WHERE true AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?) SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, scope_name, scope_version, body, body_json, body_json_promoted, attributes_string, attributes_number, attributes_bool, resources_string, scope_string FROM signoz_logs.distributed_logs_v2 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND ((arrayExists(`body_json.interests`-> arrayExists(`body_json.interests[].entities`-> arrayExists(`body_json.interests[].entities[].reviews`-> arrayExists(`body_json.interests[].entities[].reviews[].entries`-> arrayExists(`body_json.interests[].entities[].reviews[].entries[].metadata`-> arrayExists(`body_json.interests[].entities[].reviews[].entries[].metadata[].positions`-> (arrayExists(toString(x) -> LOWER(toString(x)) LIKE LOWER(?), dynamicElement(`body_json.interests[].entities[].reviews[].entries[].metadata[].positions`.`ratings`, 'Array(Nullable(Int64))')) OR arrayExists(toFloat64(x) -> toFloat64(x) = ?, dynamicElement(`body_json.interests[].entities[].reviews[].entries[].metadata[].positions`.`ratings`, 'Array(Nullable(Int64))'))), dynamicElement(`body_json.interests[].entities[].reviews[].entries[].metadata`.`positions`, 'Array(JSON(max_dynamic_types=0, max_dynamic_paths=0))')), dynamicElement(`body_json.interests[].entities[].reviews[].entries`.`metadata`, 'Array(JSON(max_dynamic_types=1, max_dynamic_paths=0))')), dynamicElement(`body_json.interests[].entities[].reviews`.`entries`, 'Array(JSON(max_dynamic_types=2, max_dynamic_paths=0))')), dynamicElement(`body_json.interests[].entities`.`reviews`, 'Array(JSON(max_dynamic_types=4, max_dynamic_paths=0))')), dynamicElement(`body_json.interests`.`entities`, 'Array(JSON(max_dynamic_types=8, max_dynamic_paths=0))')), dynamicElement(body_json.`interests`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=0))'))) OR (arrayExists(`body_json.interests`-> arrayExists(`body_json.interests[].entities`-> arrayExists(`body_json.interests[].entities[].reviews`-> arrayExists(`body_json.interests[].entities[].reviews[].entries`-> arrayExists(`body_json.interests[].entities[].reviews[].entries[].metadata`-> arrayExists(`body_json.interests[].entities[].reviews[].entries[].metadata[].positions`-> (arrayExists(x -> LOWER(x) LIKE LOWER(?), dynamicElement(`body_json.interests[].entities[].reviews[].entries[].metadata[].positions`.`ratings`, 'Array(Nullable(String))')) OR arrayExists(toFloat64OrNull(x) -> toFloat64OrNull(x) = ?, dynamicElement(`body_json.interests[].entities[].reviews[].entries[].metadata[].positions`.`ratings`, 'Array(Nullable(String))'))), dynamicElement(`body_json.interests[].entities[].reviews[].entries[].metadata`.`positions`, 'Array(JSON(max_dynamic_types=0, max_dynamic_paths=0))')), dynamicElement(`body_json.interests[].entities[].reviews[].entries`.`metadata`, 'Array(JSON(max_dynamic_types=1, max_dynamic_paths=0))')), dynamicElement(`body_json.interests[].entities[].reviews`.`entries`, 'Array(JSON(max_dynamic_types=2, max_dynamic_paths=0))')), dynamicElement(`body_json.interests[].entities`.`reviews`, 'Array(JSON(max_dynamic_types=4, max_dynamic_paths=0))')), dynamicElement(`body_json.interests`.`entities`, 'Array(JSON(max_dynamic_types=8, max_dynamic_paths=0))')), dynamicElement(body_json.`interests`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=0))')))) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? LIMIT ?",
|
||||
Args: []any{uint64(1747945619), uint64(1747983448), "%4%", float64(4), "%4%", float64(4), "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 10},
|
||||
Warnings: []string{"Key `interests[].entities[].reviews[].entries[].metadata[].positions[].ratings` is ambiguous, found 2 different combinations of field context / data type: [name=interests[].entities[].reviews[].entries[].metadata[].positions[].ratings,context=body,datatype=[]int64,jsondatatype=Array(Nullable(Int64)) name=interests[].entities[].reviews[].entries[].metadata[].positions[].ratings,context=body,datatype=[]string,jsondatatype=Array(Nullable(String))]."},
|
||||
},
|
||||
@@ -447,7 +404,7 @@ func TestStatementBuilderListQueryBody(t *testing.T) {
|
||||
Limit: 10,
|
||||
},
|
||||
expected: qbtypes.Statement{
|
||||
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_logs.distributed_logs_v2_resource WHERE true AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?) SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, scope_name, scope_version, body, body_json, body_json_promoted, attributes_string, attributes_number, attributes_bool, resources_string, scope_string FROM signoz_logs.distributed_logs_v2 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND ((arrayExists(`body_json.interests`-> arrayExists(`body_json.interests[].entities`-> arrayExists(`body_json.interests[].entities[].reviews`-> arrayExists(`body_json.interests[].entities[].reviews[].entries`-> arrayExists(`body_json.interests[].entities[].reviews[].entries[].metadata`-> arrayExists(`body_json.interests[].entities[].reviews[].entries[].metadata[].positions`-> (arrayExists(x -> LOWER(toString(x)) LIKE LOWER(?), dynamicElement(`body_json.interests[].entities[].reviews[].entries[].metadata[].positions`.`ratings`, 'Array(Nullable(Int64))')) OR arrayExists(x -> toString(x) = ?, dynamicElement(`body_json.interests[].entities[].reviews[].entries[].metadata[].positions`.`ratings`, 'Array(Nullable(Int64))'))), dynamicElement(`body_json.interests[].entities[].reviews[].entries[].metadata`.`positions`, 'Array(JSON(max_dynamic_types=0, max_dynamic_paths=0))')), dynamicElement(`body_json.interests[].entities[].reviews[].entries`.`metadata`, 'Array(JSON(max_dynamic_types=1, max_dynamic_paths=0))')), dynamicElement(`body_json.interests[].entities[].reviews`.`entries`, 'Array(JSON(max_dynamic_types=2, max_dynamic_paths=0))')), dynamicElement(`body_json.interests[].entities`.`reviews`, 'Array(JSON(max_dynamic_types=4, max_dynamic_paths=0))')), dynamicElement(`body_json.interests`.`entities`, 'Array(JSON(max_dynamic_types=8, max_dynamic_paths=0))')), dynamicElement(body_json.`interests`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=0))'))) OR (arrayExists(`body_json.interests`-> arrayExists(`body_json.interests[].entities`-> arrayExists(`body_json.interests[].entities[].reviews`-> arrayExists(`body_json.interests[].entities[].reviews[].entries`-> arrayExists(`body_json.interests[].entities[].reviews[].entries[].metadata`-> arrayExists(`body_json.interests[].entities[].reviews[].entries[].metadata[].positions`-> (arrayExists(x -> LOWER(x) LIKE LOWER(?), dynamicElement(`body_json.interests[].entities[].reviews[].entries[].metadata[].positions`.`ratings`, 'Array(Nullable(String))')) OR arrayExists(x -> x = ?, dynamicElement(`body_json.interests[].entities[].reviews[].entries[].metadata[].positions`.`ratings`, 'Array(Nullable(String))'))), dynamicElement(`body_json.interests[].entities[].reviews[].entries[].metadata`.`positions`, 'Array(JSON(max_dynamic_types=0, max_dynamic_paths=0))')), dynamicElement(`body_json.interests[].entities[].reviews[].entries`.`metadata`, 'Array(JSON(max_dynamic_types=1, max_dynamic_paths=0))')), dynamicElement(`body_json.interests[].entities[].reviews`.`entries`, 'Array(JSON(max_dynamic_types=2, max_dynamic_paths=0))')), dynamicElement(`body_json.interests[].entities`.`reviews`, 'Array(JSON(max_dynamic_types=4, max_dynamic_paths=0))')), dynamicElement(`body_json.interests`.`entities`, 'Array(JSON(max_dynamic_types=8, max_dynamic_paths=0))')), dynamicElement(body_json.`interests`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=0))')))) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? LIMIT ?",
|
||||
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_logs.distributed_logs_v2_resource WHERE true AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?) SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, scope_name, scope_version, body, body_json, body_json_promoted, attributes_string, attributes_number, attributes_bool, resources_string, scope_string FROM signoz_logs.distributed_logs_v2 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND ((arrayExists(`body_json.interests`-> arrayExists(`body_json.interests[].entities`-> arrayExists(`body_json.interests[].entities[].reviews`-> arrayExists(`body_json.interests[].entities[].reviews[].entries`-> arrayExists(`body_json.interests[].entities[].reviews[].entries[].metadata`-> arrayExists(`body_json.interests[].entities[].reviews[].entries[].metadata[].positions`-> (arrayExists(toString(x) -> LOWER(toString(x)) LIKE LOWER(?), dynamicElement(`body_json.interests[].entities[].reviews[].entries[].metadata[].positions`.`ratings`, 'Array(Nullable(Int64))')) OR arrayExists(toString(x) -> toString(x) = ?, dynamicElement(`body_json.interests[].entities[].reviews[].entries[].metadata[].positions`.`ratings`, 'Array(Nullable(Int64))'))), dynamicElement(`body_json.interests[].entities[].reviews[].entries[].metadata`.`positions`, 'Array(JSON(max_dynamic_types=0, max_dynamic_paths=0))')), dynamicElement(`body_json.interests[].entities[].reviews[].entries`.`metadata`, 'Array(JSON(max_dynamic_types=1, max_dynamic_paths=0))')), dynamicElement(`body_json.interests[].entities[].reviews`.`entries`, 'Array(JSON(max_dynamic_types=2, max_dynamic_paths=0))')), dynamicElement(`body_json.interests[].entities`.`reviews`, 'Array(JSON(max_dynamic_types=4, max_dynamic_paths=0))')), dynamicElement(`body_json.interests`.`entities`, 'Array(JSON(max_dynamic_types=8, max_dynamic_paths=0))')), dynamicElement(body_json.`interests`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=0))'))) OR (arrayExists(`body_json.interests`-> arrayExists(`body_json.interests[].entities`-> arrayExists(`body_json.interests[].entities[].reviews`-> arrayExists(`body_json.interests[].entities[].reviews[].entries`-> arrayExists(`body_json.interests[].entities[].reviews[].entries[].metadata`-> arrayExists(`body_json.interests[].entities[].reviews[].entries[].metadata[].positions`-> (arrayExists(x -> LOWER(x) LIKE LOWER(?), dynamicElement(`body_json.interests[].entities[].reviews[].entries[].metadata[].positions`.`ratings`, 'Array(Nullable(String))')) OR arrayExists(x -> x = ?, dynamicElement(`body_json.interests[].entities[].reviews[].entries[].metadata[].positions`.`ratings`, 'Array(Nullable(String))'))), dynamicElement(`body_json.interests[].entities[].reviews[].entries[].metadata`.`positions`, 'Array(JSON(max_dynamic_types=0, max_dynamic_paths=0))')), dynamicElement(`body_json.interests[].entities[].reviews[].entries`.`metadata`, 'Array(JSON(max_dynamic_types=1, max_dynamic_paths=0))')), dynamicElement(`body_json.interests[].entities[].reviews`.`entries`, 'Array(JSON(max_dynamic_types=2, max_dynamic_paths=0))')), dynamicElement(`body_json.interests[].entities`.`reviews`, 'Array(JSON(max_dynamic_types=4, max_dynamic_paths=0))')), dynamicElement(`body_json.interests`.`entities`, 'Array(JSON(max_dynamic_types=8, max_dynamic_paths=0))')), dynamicElement(body_json.`interests`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=0))')))) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? LIMIT ?",
|
||||
Args: []any{uint64(1747945619), uint64(1747983448), "%Good%", "Good", "%Good%", "Good", "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 10},
|
||||
Warnings: []string{"Key `interests[].entities[].reviews[].entries[].metadata[].positions[].ratings` is ambiguous, found 2 different combinations of field context / data type: [name=interests[].entities[].reviews[].entries[].metadata[].positions[].ratings,context=body,datatype=[]int64,jsondatatype=Array(Nullable(Int64)) name=interests[].entities[].reviews[].entries[].metadata[].positions[].ratings,context=body,datatype=[]string,jsondatatype=Array(Nullable(String))]."},
|
||||
},
|
||||
@@ -535,7 +492,7 @@ func TestStatementBuilderListQueryBodyPromoted(t *testing.T) {
|
||||
disableBodyJSONQuery(t)
|
||||
}()
|
||||
|
||||
statementBuilder := buildJSONTestStatementBuilder(t, "education", "tags")
|
||||
statementBuilder := buildJSONTestStatementBuilder(t, "education")
|
||||
cases := []struct {
|
||||
name string
|
||||
requestType qbtypes.RequestType
|
||||
@@ -543,20 +500,6 @@ func TestStatementBuilderListQueryBodyPromoted(t *testing.T) {
|
||||
expected qbtypes.Statement
|
||||
expectedErr error
|
||||
}{
|
||||
{
|
||||
name: "Has Array promoted uses body fallback",
|
||||
requestType: qbtypes.RequestTypeRaw,
|
||||
query: qbtypes.QueryBuilderQuery[qbtypes.LogAggregation]{
|
||||
Signal: telemetrytypes.SignalLogs,
|
||||
Filter: &qbtypes.Filter{Expression: "has(body.tags, 'production')"},
|
||||
Limit: 10,
|
||||
},
|
||||
expected: qbtypes.Statement{
|
||||
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_logs.distributed_logs_v2_resource WHERE true AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?) SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, scope_name, scope_version, body, body_json, body_json_promoted, attributes_string, attributes_number, attributes_bool, resources_string, scope_string FROM signoz_logs.distributed_logs_v2 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND has(if(length(dynamicElement(body_json_promoted.`tags`, 'Array(Nullable(String))')) > 0, dynamicElement(body_json_promoted.`tags`, 'Array(Nullable(String))'), dynamicElement(body_json.`tags`, 'Array(Nullable(String))')), ?) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? LIMIT ?",
|
||||
Args: []any{uint64(1747945619), uint64(1747983448), "production", "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 10},
|
||||
},
|
||||
expectedErr: nil,
|
||||
},
|
||||
{
|
||||
name: "Key inside Array(JSON) exists",
|
||||
requestType: qbtypes.RequestTypeRaw,
|
||||
@@ -608,7 +551,7 @@ func TestStatementBuilderListQueryBodyPromoted(t *testing.T) {
|
||||
Limit: 10,
|
||||
},
|
||||
expected: qbtypes.Statement{
|
||||
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_logs.distributed_logs_v2_resource WHERE true AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?) SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, scope_name, scope_version, body, body_json, body_json_promoted, attributes_string, attributes_number, attributes_bool, resources_string, scope_string FROM signoz_logs.distributed_logs_v2 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND ((arrayExists(`body_json.education`-> (arrayExists(x -> LOWER(toString(x)) LIKE LOWER(?), dynamicElement(`body_json.education`.`parameters`, 'Array(Nullable(Float64))')) OR arrayExists(x -> toFloat64(x) = ?, dynamicElement(`body_json.education`.`parameters`, 'Array(Nullable(Float64))'))), dynamicElement(body_json.`education`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=0))')) OR arrayExists(`body_json_promoted.education`-> (arrayExists(x -> LOWER(toString(x)) LIKE LOWER(?), dynamicElement(`body_json_promoted.education`.`parameters`, 'Array(Nullable(Float64))')) OR arrayExists(x -> toFloat64(x) = ?, dynamicElement(`body_json_promoted.education`.`parameters`, 'Array(Nullable(Float64))'))), dynamicElement(body_json_promoted.`education`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=256))'))) OR (arrayExists(`body_json.education`-> (arrayExists(x -> LOWER(toString(x)) LIKE LOWER(?), arrayMap(x->dynamicElement(x, 'Float64'), arrayFilter(x->(dynamicType(x) = 'Float64'), dynamicElement(`body_json.education`.`parameters`, 'Array(Dynamic)')))) OR arrayExists(x -> toFloat64(x) = ?, arrayMap(x->dynamicElement(x, 'Float64'), arrayFilter(x->(dynamicType(x) = 'Float64'), dynamicElement(`body_json.education`.`parameters`, 'Array(Dynamic)'))))), dynamicElement(body_json.`education`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=0))')) OR arrayExists(`body_json_promoted.education`-> (arrayExists(x -> LOWER(toString(x)) LIKE LOWER(?), arrayMap(x->dynamicElement(x, 'Float64'), arrayFilter(x->(dynamicType(x) = 'Float64'), dynamicElement(`body_json_promoted.education`.`parameters`, 'Array(Dynamic)')))) OR arrayExists(x -> toFloat64(x) = ?, arrayMap(x->dynamicElement(x, 'Float64'), arrayFilter(x->(dynamicType(x) = 'Float64'), dynamicElement(`body_json_promoted.education`.`parameters`, 'Array(Dynamic)'))))), dynamicElement(body_json_promoted.`education`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=256))')))) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? LIMIT ?",
|
||||
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_logs.distributed_logs_v2_resource WHERE true AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?) SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, scope_name, scope_version, body, body_json, body_json_promoted, attributes_string, attributes_number, attributes_bool, resources_string, scope_string FROM signoz_logs.distributed_logs_v2 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND ((arrayExists(`body_json.education`-> (arrayExists(toString(x) -> LOWER(toString(x)) LIKE LOWER(?), dynamicElement(`body_json.education`.`parameters`, 'Array(Nullable(Float64))')) OR arrayExists(toFloat64(x) -> toFloat64(x) = ?, dynamicElement(`body_json.education`.`parameters`, 'Array(Nullable(Float64))'))), dynamicElement(body_json.`education`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=0))')) OR arrayExists(`body_json_promoted.education`-> (arrayExists(toString(x) -> LOWER(toString(x)) LIKE LOWER(?), dynamicElement(`body_json_promoted.education`.`parameters`, 'Array(Nullable(Float64))')) OR arrayExists(toFloat64(x) -> toFloat64(x) = ?, dynamicElement(`body_json_promoted.education`.`parameters`, 'Array(Nullable(Float64))'))), dynamicElement(body_json_promoted.`education`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=256))'))) OR (arrayExists(`body_json.education`-> (arrayExists(toString(x) -> LOWER(toString(x)) LIKE LOWER(?), arrayMap(x->dynamicElement(x, 'Float64'), arrayFilter(x->(dynamicType(x) = 'Float64'), dynamicElement(`body_json.education`.`parameters`, 'Array(Dynamic)')))) OR arrayExists(toFloat64(x) -> toFloat64(x) = ?, arrayMap(x->dynamicElement(x, 'Float64'), arrayFilter(x->(dynamicType(x) = 'Float64'), dynamicElement(`body_json.education`.`parameters`, 'Array(Dynamic)'))))), dynamicElement(body_json.`education`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=0))')) OR arrayExists(`body_json_promoted.education`-> (arrayExists(toString(x) -> LOWER(toString(x)) LIKE LOWER(?), arrayMap(x->dynamicElement(x, 'Float64'), arrayFilter(x->(dynamicType(x) = 'Float64'), dynamicElement(`body_json_promoted.education`.`parameters`, 'Array(Dynamic)')))) OR arrayExists(toFloat64(x) -> toFloat64(x) = ?, arrayMap(x->dynamicElement(x, 'Float64'), arrayFilter(x->(dynamicType(x) = 'Float64'), dynamicElement(`body_json_promoted.education`.`parameters`, 'Array(Dynamic)'))))), dynamicElement(body_json_promoted.`education`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=256))')))) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? LIMIT ?",
|
||||
Args: []any{uint64(1747945619), uint64(1747983448), "%1.65%", 1.65, "%1.65%", 1.65, "%1.65%", 1.65, "%1.65%", 1.65, "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 10},
|
||||
Warnings: []string{"Key `education[].parameters` is ambiguous, found 2 different combinations of field context / data type: [name=education[].parameters,context=body,datatype=[]float64,materialized=true,jsondatatype=Array(Nullable(Float64)) name=education[].parameters,context=body,datatype=[]dynamic,materialized=true,jsondatatype=Array(Dynamic)]."},
|
||||
},
|
||||
@@ -637,7 +580,7 @@ func TestStatementBuilderListQueryBodyPromoted(t *testing.T) {
|
||||
Limit: 10,
|
||||
},
|
||||
expected: qbtypes.Statement{
|
||||
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_logs.distributed_logs_v2_resource WHERE true AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?) SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, scope_name, scope_version, body, body_json, body_json_promoted, attributes_string, attributes_number, attributes_bool, resources_string, scope_string FROM signoz_logs.distributed_logs_v2 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND ((arrayExists(`body_json.education`-> (arrayExists(x -> LOWER(toString(x)) LIKE LOWER(?), dynamicElement(`body_json.education`.`parameters`, 'Array(Nullable(Float64))')) OR arrayExists(x -> x = ?, dynamicElement(`body_json.education`.`parameters`, 'Array(Nullable(Float64))'))), dynamicElement(body_json.`education`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=0))')) OR arrayExists(`body_json_promoted.education`-> (arrayExists(x -> LOWER(toString(x)) LIKE LOWER(?), dynamicElement(`body_json_promoted.education`.`parameters`, 'Array(Nullable(Float64))')) OR arrayExists(x -> x = ?, dynamicElement(`body_json_promoted.education`.`parameters`, 'Array(Nullable(Float64))'))), dynamicElement(body_json_promoted.`education`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=256))'))) OR (arrayExists(`body_json.education`-> (arrayExists(x -> LOWER(toString(x)) LIKE LOWER(?), arrayMap(x->dynamicElement(x, 'Bool'), arrayFilter(x->(dynamicType(x) = 'Bool'), dynamicElement(`body_json.education`.`parameters`, 'Array(Dynamic)')))) OR arrayExists(x -> x = ?, arrayMap(x->dynamicElement(x, 'Bool'), arrayFilter(x->(dynamicType(x) = 'Bool'), dynamicElement(`body_json.education`.`parameters`, 'Array(Dynamic)'))))), dynamicElement(body_json.`education`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=0))')) OR arrayExists(`body_json_promoted.education`-> (arrayExists(x -> LOWER(toString(x)) LIKE LOWER(?), arrayMap(x->dynamicElement(x, 'Bool'), arrayFilter(x->(dynamicType(x) = 'Bool'), dynamicElement(`body_json_promoted.education`.`parameters`, 'Array(Dynamic)')))) OR arrayExists(x -> x = ?, arrayMap(x->dynamicElement(x, 'Bool'), arrayFilter(x->(dynamicType(x) = 'Bool'), dynamicElement(`body_json_promoted.education`.`parameters`, 'Array(Dynamic)'))))), dynamicElement(body_json_promoted.`education`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=256))')))) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? LIMIT ?",
|
||||
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_logs.distributed_logs_v2_resource WHERE true AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?) SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, scope_name, scope_version, body, body_json, body_json_promoted, attributes_string, attributes_number, attributes_bool, resources_string, scope_string FROM signoz_logs.distributed_logs_v2 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND ((arrayExists(`body_json.education`-> (arrayExists(toString(x) -> LOWER(toString(x)) LIKE LOWER(?), dynamicElement(`body_json.education`.`parameters`, 'Array(Nullable(Float64))')) OR arrayExists(x -> x = ?, dynamicElement(`body_json.education`.`parameters`, 'Array(Nullable(Float64))'))), dynamicElement(body_json.`education`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=0))')) OR arrayExists(`body_json_promoted.education`-> (arrayExists(toString(x) -> LOWER(toString(x)) LIKE LOWER(?), dynamicElement(`body_json_promoted.education`.`parameters`, 'Array(Nullable(Float64))')) OR arrayExists(x -> x = ?, dynamicElement(`body_json_promoted.education`.`parameters`, 'Array(Nullable(Float64))'))), dynamicElement(body_json_promoted.`education`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=256))'))) OR (arrayExists(`body_json.education`-> (arrayExists(toString(x) -> LOWER(toString(x)) LIKE LOWER(?), arrayMap(x->dynamicElement(x, 'Bool'), arrayFilter(x->(dynamicType(x) = 'Bool'), dynamicElement(`body_json.education`.`parameters`, 'Array(Dynamic)')))) OR arrayExists(x -> x = ?, arrayMap(x->dynamicElement(x, 'Bool'), arrayFilter(x->(dynamicType(x) = 'Bool'), dynamicElement(`body_json.education`.`parameters`, 'Array(Dynamic)'))))), dynamicElement(body_json.`education`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=0))')) OR arrayExists(`body_json_promoted.education`-> (arrayExists(toString(x) -> LOWER(toString(x)) LIKE LOWER(?), arrayMap(x->dynamicElement(x, 'Bool'), arrayFilter(x->(dynamicType(x) = 'Bool'), dynamicElement(`body_json_promoted.education`.`parameters`, 'Array(Dynamic)')))) OR arrayExists(x -> x = ?, arrayMap(x->dynamicElement(x, 'Bool'), arrayFilter(x->(dynamicType(x) = 'Bool'), dynamicElement(`body_json_promoted.education`.`parameters`, 'Array(Dynamic)'))))), dynamicElement(body_json_promoted.`education`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=256))')))) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? LIMIT ?",
|
||||
Args: []any{uint64(1747945619), uint64(1747983448), "%true%", true, "%true%", true, "%true%", true, "%true%", true, "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 10},
|
||||
Warnings: []string{"Key `education[].parameters` is ambiguous, found 2 different combinations of field context / data type: [name=education[].parameters,context=body,datatype=[]float64,materialized=true,jsondatatype=Array(Nullable(Float64)) name=education[].parameters,context=body,datatype=[]dynamic,materialized=true,jsondatatype=Array(Dynamic)]."},
|
||||
},
|
||||
@@ -652,7 +595,7 @@ func TestStatementBuilderListQueryBodyPromoted(t *testing.T) {
|
||||
Limit: 10,
|
||||
},
|
||||
expected: qbtypes.Statement{
|
||||
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_logs.distributed_logs_v2_resource WHERE true AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?) SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, scope_name, scope_version, body, body_json, body_json_promoted, attributes_string, attributes_number, attributes_bool, resources_string, scope_string FROM signoz_logs.distributed_logs_v2 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND ((arrayExists(`body_json.education`-> (arrayExists(x -> LOWER(toString(x)) LIKE LOWER(?), dynamicElement(`body_json.education`.`parameters`, 'Array(Nullable(Float64))')) OR arrayExists(x -> toString(x) = ?, dynamicElement(`body_json.education`.`parameters`, 'Array(Nullable(Float64))'))), dynamicElement(body_json.`education`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=0))')) OR arrayExists(`body_json_promoted.education`-> (arrayExists(x -> LOWER(toString(x)) LIKE LOWER(?), dynamicElement(`body_json_promoted.education`.`parameters`, 'Array(Nullable(Float64))')) OR arrayExists(x -> toString(x) = ?, dynamicElement(`body_json_promoted.education`.`parameters`, 'Array(Nullable(Float64))'))), dynamicElement(body_json_promoted.`education`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=256))'))) OR (arrayExists(`body_json.education`-> (arrayExists(x -> LOWER(x) LIKE LOWER(?), arrayMap(x->dynamicElement(x, 'String'), arrayFilter(x->(dynamicType(x) = 'String'), dynamicElement(`body_json.education`.`parameters`, 'Array(Dynamic)')))) OR arrayExists(x -> x = ?, arrayMap(x->dynamicElement(x, 'String'), arrayFilter(x->(dynamicType(x) = 'String'), dynamicElement(`body_json.education`.`parameters`, 'Array(Dynamic)'))))), dynamicElement(body_json.`education`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=0))')) OR arrayExists(`body_json_promoted.education`-> (arrayExists(x -> LOWER(x) LIKE LOWER(?), arrayMap(x->dynamicElement(x, 'String'), arrayFilter(x->(dynamicType(x) = 'String'), dynamicElement(`body_json_promoted.education`.`parameters`, 'Array(Dynamic)')))) OR arrayExists(x -> x = ?, arrayMap(x->dynamicElement(x, 'String'), arrayFilter(x->(dynamicType(x) = 'String'), dynamicElement(`body_json_promoted.education`.`parameters`, 'Array(Dynamic)'))))), dynamicElement(body_json_promoted.`education`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=256))')))) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? LIMIT ?",
|
||||
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_logs.distributed_logs_v2_resource WHERE true AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?) SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, scope_name, scope_version, body, body_json, body_json_promoted, attributes_string, attributes_number, attributes_bool, resources_string, scope_string FROM signoz_logs.distributed_logs_v2 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND ((arrayExists(`body_json.education`-> (arrayExists(toString(x) -> LOWER(toString(x)) LIKE LOWER(?), dynamicElement(`body_json.education`.`parameters`, 'Array(Nullable(Float64))')) OR arrayExists(toString(x) -> toString(x) = ?, dynamicElement(`body_json.education`.`parameters`, 'Array(Nullable(Float64))'))), dynamicElement(body_json.`education`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=0))')) OR arrayExists(`body_json_promoted.education`-> (arrayExists(toString(x) -> LOWER(toString(x)) LIKE LOWER(?), dynamicElement(`body_json_promoted.education`.`parameters`, 'Array(Nullable(Float64))')) OR arrayExists(toString(x) -> toString(x) = ?, dynamicElement(`body_json_promoted.education`.`parameters`, 'Array(Nullable(Float64))'))), dynamicElement(body_json_promoted.`education`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=256))'))) OR (arrayExists(`body_json.education`-> (arrayExists(x -> LOWER(x) LIKE LOWER(?), arrayMap(x->dynamicElement(x, 'String'), arrayFilter(x->(dynamicType(x) = 'String'), dynamicElement(`body_json.education`.`parameters`, 'Array(Dynamic)')))) OR arrayExists(x -> x = ?, arrayMap(x->dynamicElement(x, 'String'), arrayFilter(x->(dynamicType(x) = 'String'), dynamicElement(`body_json.education`.`parameters`, 'Array(Dynamic)'))))), dynamicElement(body_json.`education`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=0))')) OR arrayExists(`body_json_promoted.education`-> (arrayExists(x -> LOWER(x) LIKE LOWER(?), arrayMap(x->dynamicElement(x, 'String'), arrayFilter(x->(dynamicType(x) = 'String'), dynamicElement(`body_json_promoted.education`.`parameters`, 'Array(Dynamic)')))) OR arrayExists(x -> x = ?, arrayMap(x->dynamicElement(x, 'String'), arrayFilter(x->(dynamicType(x) = 'String'), dynamicElement(`body_json_promoted.education`.`parameters`, 'Array(Dynamic)'))))), dynamicElement(body_json_promoted.`education`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=256))')))) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? LIMIT ?",
|
||||
Args: []any{uint64(1747945619), uint64(1747983448), "%passed%", "passed", "%passed%", "passed", "%passed%", "passed", "%passed%", "passed", "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 10},
|
||||
Warnings: []string{"Key `education[].parameters` is ambiguous, found 2 different combinations of field context / data type: [name=education[].parameters,context=body,datatype=[]float64,materialized=true,jsondatatype=Array(Nullable(Float64)) name=education[].parameters,context=body,datatype=[]dynamic,materialized=true,jsondatatype=Array(Dynamic)]."},
|
||||
},
|
||||
|
||||
@@ -3,12 +3,12 @@ package telemetrylogs
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
||||
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
)
|
||||
|
||||
func parseStrValue(valueStr string, operator qbtypes.FilterOperator) (telemetrytypes.FieldDataType, any) {
|
||||
@@ -41,55 +41,31 @@ func parseStrValue(valueStr string, operator qbtypes.FilterOperator) (telemetryt
|
||||
}
|
||||
|
||||
func InferDataType(value any, operator qbtypes.FilterOperator, key *telemetrytypes.TelemetryFieldKey) (telemetrytypes.FieldDataType, any) {
|
||||
if operator.IsArrayOperator() && reflect.ValueOf(value).Kind() != reflect.Slice {
|
||||
value = []any{value}
|
||||
}
|
||||
|
||||
// closure to calculate the data type of the value
|
||||
var closure func(value any, key *telemetrytypes.TelemetryFieldKey) (telemetrytypes.FieldDataType, any)
|
||||
closure = func(value any, key *telemetrytypes.TelemetryFieldKey) (telemetrytypes.FieldDataType, any) {
|
||||
// check if the value is a int, float, string, bool
|
||||
valueType := telemetrytypes.FieldDataTypeUnspecified
|
||||
switch v := value.(type) {
|
||||
case []any:
|
||||
// take the first element and infer the type
|
||||
var scalerType telemetrytypes.FieldDataType
|
||||
if len(v) > 0 {
|
||||
// Note: [[...]] Slices inside Slices are not handled yet
|
||||
if reflect.ValueOf(v[0]).Kind() == reflect.Slice {
|
||||
return telemetrytypes.FieldDataTypeUnspecified, value
|
||||
}
|
||||
|
||||
scalerType, _ = closure(v[0], key)
|
||||
}
|
||||
|
||||
arrayType := telemetrytypes.ScalerFieldTypeToArrayFieldType[scalerType]
|
||||
switch {
|
||||
// decide on the field data type based on the key
|
||||
case key.FieldDataType.IsArray():
|
||||
return arrayType, v
|
||||
default:
|
||||
// TODO(Piyush): backward compatibility for the old String based JSON QB queries
|
||||
if strings.HasSuffix(key.Name, telemetrytypes.ArrayAnyIndexSuffix) {
|
||||
return arrayType, v
|
||||
}
|
||||
return scalerType, v
|
||||
}
|
||||
case uint8, uint16, uint32, uint64, int, int8, int16, int32, int64:
|
||||
valueType = telemetrytypes.FieldDataTypeInt64
|
||||
case float32, float64:
|
||||
valueType = telemetrytypes.FieldDataTypeFloat64
|
||||
case string:
|
||||
valueType, value = parseStrValue(v, operator)
|
||||
case bool:
|
||||
valueType = telemetrytypes.FieldDataTypeBool
|
||||
// check if the value is a int, float, string, bool
|
||||
valueType := telemetrytypes.FieldDataTypeUnspecified
|
||||
switch v := value.(type) {
|
||||
case []any:
|
||||
// take the first element and infer the type
|
||||
if len(v) > 0 {
|
||||
valueType, _ = InferDataType(v[0], operator, key)
|
||||
}
|
||||
|
||||
return valueType, value
|
||||
return valueType, v
|
||||
case uint8, uint16, uint32, uint64, int, int8, int16, int32, int64:
|
||||
valueType = telemetrytypes.FieldDataTypeInt64
|
||||
case float32, float64:
|
||||
valueType = telemetrytypes.FieldDataTypeFloat64
|
||||
case string:
|
||||
valueType, value = parseStrValue(v, operator)
|
||||
case bool:
|
||||
valueType = telemetrytypes.FieldDataTypeBool
|
||||
}
|
||||
|
||||
// calculate the data type of the value
|
||||
return closure(value, key)
|
||||
// check if it is array
|
||||
if strings.HasSuffix(key.Name, "[*]") || strings.HasSuffix(key.Name, "[]") {
|
||||
valueType = telemetrytypes.FieldDataType{String: valuer.NewString(fmt.Sprintf("[]%s", valueType.StringValue()))}
|
||||
}
|
||||
|
||||
return valueType, value
|
||||
}
|
||||
|
||||
func getBodyJSONPath(key *telemetrytypes.TelemetryFieldKey) string {
|
||||
|
||||
@@ -421,38 +421,6 @@ func TestStatementBuilderListQueryResourceTests(t *testing.T) {
|
||||
},
|
||||
expectedErr: nil,
|
||||
},
|
||||
{
|
||||
name: "IN operator with json search",
|
||||
requestType: qbtypes.RequestTypeRaw,
|
||||
query: qbtypes.QueryBuilderQuery[qbtypes.LogAggregation]{
|
||||
Signal: telemetrytypes.SignalLogs,
|
||||
Filter: &qbtypes.Filter{
|
||||
Expression: "body.user_names[*] IN 'john_doe'",
|
||||
},
|
||||
Limit: 10,
|
||||
},
|
||||
expected: qbtypes.Statement{
|
||||
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_logs.distributed_logs_v2_resource WHERE true AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?) SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, scope_name, scope_version, body, attributes_string, attributes_number, attributes_bool, resources_string, scope_string FROM signoz_logs.distributed_logs_v2 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND ((JSONExtract(JSON_QUERY(body, '$.\"user_names\"[*]'), 'Array(String)') = ?) AND JSON_EXISTS(body, '$.\"user_names\"[*]')) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? LIMIT ?",
|
||||
Args: []any{uint64(1747945619), uint64(1747983448), "john_doe", "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 10},
|
||||
},
|
||||
expectedErr: nil,
|
||||
},
|
||||
{
|
||||
name: "has with json search",
|
||||
requestType: qbtypes.RequestTypeRaw,
|
||||
query: qbtypes.QueryBuilderQuery[qbtypes.LogAggregation]{
|
||||
Signal: telemetrytypes.SignalLogs,
|
||||
Filter: &qbtypes.Filter{
|
||||
Expression: "has(body.user_names[*], 'john_doe')",
|
||||
},
|
||||
Limit: 10,
|
||||
},
|
||||
expected: qbtypes.Statement{
|
||||
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_logs.distributed_logs_v2_resource WHERE true AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?) SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, scope_name, scope_version, body, attributes_string, attributes_number, attributes_bool, resources_string, scope_string FROM signoz_logs.distributed_logs_v2 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND has(JSONExtract(JSON_QUERY(body, '$.\"user_names\"[*]'), 'Array(String)'), ?) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? LIMIT ?",
|
||||
Args: []any{uint64(1747945619), uint64(1747983448), "john_doe", "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 10},
|
||||
},
|
||||
expectedErr: nil,
|
||||
},
|
||||
}
|
||||
|
||||
mockMetadataStore := telemetrytypestest.NewMockMetadataStore()
|
||||
|
||||
@@ -112,8 +112,7 @@ func (t *telemetryMetaStore) buildBodyJSONPaths(ctx context.Context,
|
||||
}
|
||||
|
||||
for _, fieldKey := range fieldKeys {
|
||||
promotedKey := strings.Split(fieldKey.Name, telemetrytypes.ArraySep)[0]
|
||||
fieldKey.Materialized = promoted.Contains(promotedKey)
|
||||
fieldKey.Materialized = promoted.Contains(fieldKey.Name)
|
||||
fieldKey.Indexes = indexes[fieldKey.Name]
|
||||
}
|
||||
|
||||
@@ -502,8 +501,7 @@ func (t *telemetryMetaStore) GetPromotedPaths(ctx context.Context, paths ...stri
|
||||
sb := sqlbuilder.Select("path").From(fmt.Sprintf("%s.%s", DBName, PromotedPathsTableName))
|
||||
pathConditions := []string{}
|
||||
for _, path := range paths {
|
||||
split := strings.Split(path, telemetrytypes.ArraySep)
|
||||
pathConditions = append(pathConditions, sb.Equal("path", split[0]))
|
||||
pathConditions = append(pathConditions, sb.Equal("path", path))
|
||||
}
|
||||
sb.Where(sb.Or(pathConditions...))
|
||||
|
||||
|
||||
@@ -161,17 +161,6 @@ func (f FilterOperator) IsStringSearchOperator() bool {
|
||||
}
|
||||
}
|
||||
|
||||
// IsArrayOperator returns true if the operator works with array values only
|
||||
func (f FilterOperator) IsArrayOperator() bool {
|
||||
switch f {
|
||||
case FilterOperatorIn, FilterOperatorNotIn,
|
||||
FilterOperatorBetween, FilterOperatorNotBetween:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
type OrderDirection struct {
|
||||
valuer.String
|
||||
}
|
||||
|
||||
@@ -23,10 +23,8 @@ const (
|
||||
// e.g., "body.status" where "body." is the prefix
|
||||
BodyJSONStringSearchPrefix = "body."
|
||||
ArraySep = jsontypeexporter.ArraySeparator
|
||||
ArraySepSuffix = "[]"
|
||||
// TODO(Piyush): Remove once we've migrated to the new array syntax
|
||||
ArrayAnyIndex = "[*]."
|
||||
ArrayAnyIndexSuffix = "[*]"
|
||||
ArrayAnyIndex = "[*]."
|
||||
)
|
||||
|
||||
type TelemetryFieldKey struct {
|
||||
|
||||
@@ -93,14 +93,6 @@ var (
|
||||
FieldDataTypeArrayFloat64: "Array(Float64)",
|
||||
FieldDataTypeArrayBool: "Array(Bool)",
|
||||
}
|
||||
|
||||
ScalerFieldTypeToArrayFieldType = map[FieldDataType]FieldDataType{
|
||||
FieldDataTypeString: FieldDataTypeArrayString,
|
||||
FieldDataTypeBool: FieldDataTypeArrayBool,
|
||||
FieldDataTypeNumber: FieldDataTypeArrayNumber,
|
||||
FieldDataTypeInt64: FieldDataTypeArrayInt64,
|
||||
FieldDataTypeFloat64: FieldDataTypeArrayFloat64,
|
||||
}
|
||||
)
|
||||
|
||||
func (f FieldDataType) CHDataType() string {
|
||||
|
||||
@@ -65,7 +65,6 @@ func TestJSONTypeSet() (map[string][]JSONDataType, MetadataStore) {
|
||||
"interests[].entities[].reviews[].entries[].metadata[].positions[].unit": {String},
|
||||
"interests[].entities[].reviews[].entries[].metadata[].positions[].ratings": {ArrayInt64, ArrayString},
|
||||
"message": {String},
|
||||
"tags": {ArrayString},
|
||||
}
|
||||
|
||||
return types, nil
|
||||
|
||||
Reference in New Issue
Block a user