mirror of
https://github.com/SigNoz/signoz.git
synced 2026-02-26 18:32:35 +00:00
Compare commits
17 Commits
feat/azure
...
feat/disab
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
745e4e7c0a | ||
|
|
887b3c7b3a | ||
|
|
476fe7a29a | ||
|
|
c1d38d86f1 | ||
|
|
4f2594c31d | ||
|
|
3e6d105e81 | ||
|
|
c9985b56bc | ||
|
|
f9868e2221 | ||
|
|
72b0398eaf | ||
|
|
5b75a39777 | ||
|
|
6948b69012 | ||
|
|
bc9701397e | ||
|
|
396cf3194e | ||
|
|
8be96a0ded | ||
|
|
82c54b1d36 | ||
|
|
39f5fb7290 | ||
|
|
6ec2989e5c |
@@ -320,3 +320,4 @@ user:
|
||||
# The name of the organization to create or look up for the root user.
|
||||
org:
|
||||
name: default
|
||||
id: 00000000-0000-0000-0000-000000000000
|
||||
|
||||
@@ -190,7 +190,7 @@ services:
|
||||
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
|
||||
signoz:
|
||||
!!merge <<: *db-depend
|
||||
image: signoz/signoz:v0.112.1
|
||||
image: signoz/signoz:v0.113.0
|
||||
ports:
|
||||
- "8080:8080" # signoz port
|
||||
# - "6060:6060" # pprof port
|
||||
@@ -213,7 +213,7 @@ services:
|
||||
retries: 3
|
||||
otel-collector:
|
||||
!!merge <<: *db-depend
|
||||
image: signoz/signoz-otel-collector:v0.142.1
|
||||
image: signoz/signoz-otel-collector:v0.144.1
|
||||
entrypoint:
|
||||
- /bin/sh
|
||||
command:
|
||||
@@ -241,7 +241,7 @@ services:
|
||||
replicas: 3
|
||||
signoz-telemetrystore-migrator:
|
||||
!!merge <<: *db-depend
|
||||
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-v0.142.0}
|
||||
image: signoz/signoz-otel-collector:v0.144.1
|
||||
environment:
|
||||
- SIGNOZ_OTEL_COLLECTOR_CLICKHOUSE_DSN=tcp://clickhouse:9000
|
||||
- SIGNOZ_OTEL_COLLECTOR_CLICKHOUSE_CLUSTER=cluster
|
||||
|
||||
@@ -117,7 +117,7 @@ services:
|
||||
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
|
||||
signoz:
|
||||
!!merge <<: *db-depend
|
||||
image: signoz/signoz:v0.112.1
|
||||
image: signoz/signoz:v0.113.0
|
||||
ports:
|
||||
- "8080:8080" # signoz port
|
||||
volumes:
|
||||
@@ -139,7 +139,7 @@ services:
|
||||
retries: 3
|
||||
otel-collector:
|
||||
!!merge <<: *db-depend
|
||||
image: signoz/signoz-otel-collector:v0.142.1
|
||||
image: signoz/signoz-otel-collector:v0.144.1
|
||||
entrypoint:
|
||||
- /bin/sh
|
||||
command:
|
||||
@@ -167,7 +167,7 @@ services:
|
||||
replicas: 3
|
||||
signoz-telemetrystore-migrator:
|
||||
!!merge <<: *db-depend
|
||||
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-v0.142.0}
|
||||
image: signoz/signoz-otel-collector:v0.144.1
|
||||
environment:
|
||||
- SIGNOZ_OTEL_COLLECTOR_CLICKHOUSE_DSN=tcp://clickhouse:9000
|
||||
- SIGNOZ_OTEL_COLLECTOR_CLICKHOUSE_CLUSTER=cluster
|
||||
|
||||
@@ -181,7 +181,7 @@ services:
|
||||
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
|
||||
signoz:
|
||||
!!merge <<: *db-depend
|
||||
image: signoz/signoz:${VERSION:-v0.112.1}
|
||||
image: signoz/signoz:${VERSION:-v0.113.0}
|
||||
container_name: signoz
|
||||
ports:
|
||||
- "8080:8080" # signoz port
|
||||
@@ -204,7 +204,7 @@ services:
|
||||
retries: 3
|
||||
otel-collector:
|
||||
!!merge <<: *db-depend
|
||||
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-v0.142.1}
|
||||
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-v0.144.1}
|
||||
container_name: signoz-otel-collector
|
||||
entrypoint:
|
||||
- /bin/sh
|
||||
@@ -229,7 +229,7 @@ services:
|
||||
- "4318:4318" # OTLP HTTP receiver
|
||||
signoz-telemetrystore-migrator:
|
||||
!!merge <<: *db-depend
|
||||
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-v0.142.0}
|
||||
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-v0.144.1}
|
||||
container_name: signoz-telemetrystore-migrator
|
||||
environment:
|
||||
- SIGNOZ_OTEL_COLLECTOR_CLICKHOUSE_DSN=tcp://clickhouse:9000
|
||||
|
||||
@@ -109,7 +109,7 @@ services:
|
||||
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
|
||||
signoz:
|
||||
!!merge <<: *db-depend
|
||||
image: signoz/signoz:${VERSION:-v0.112.1}
|
||||
image: signoz/signoz:${VERSION:-v0.113.0}
|
||||
container_name: signoz
|
||||
ports:
|
||||
- "8080:8080" # signoz port
|
||||
@@ -132,7 +132,7 @@ services:
|
||||
retries: 3
|
||||
otel-collector:
|
||||
!!merge <<: *db-depend
|
||||
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-v0.142.1}
|
||||
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-v0.144.1}
|
||||
container_name: signoz-otel-collector
|
||||
entrypoint:
|
||||
- /bin/sh
|
||||
@@ -157,7 +157,7 @@ services:
|
||||
- "4318:4318" # OTLP HTTP receiver
|
||||
signoz-telemetrystore-migrator:
|
||||
!!merge <<: *db-depend
|
||||
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-v0.142.0}
|
||||
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-v0.144.1}
|
||||
container_name: signoz-telemetrystore-migrator
|
||||
environment:
|
||||
- SIGNOZ_OTEL_COLLECTOR_CLICKHOUSE_DSN=tcp://clickhouse:9000
|
||||
|
||||
@@ -842,6 +842,17 @@ components:
|
||||
- temporality
|
||||
- isMonotonic
|
||||
type: object
|
||||
MetrictypesComparisonSpaceAggregationParam:
|
||||
properties:
|
||||
operator:
|
||||
type: string
|
||||
threshold:
|
||||
format: double
|
||||
type: number
|
||||
required:
|
||||
- operator
|
||||
- threshold
|
||||
type: object
|
||||
MetrictypesSpaceAggregation:
|
||||
enum:
|
||||
- sum
|
||||
@@ -1138,6 +1149,8 @@ components:
|
||||
type: object
|
||||
Querybuildertypesv5MetricAggregation:
|
||||
properties:
|
||||
comparisonSpaceAggregationParam:
|
||||
$ref: '#/components/schemas/MetrictypesComparisonSpaceAggregationParam'
|
||||
metricName:
|
||||
type: string
|
||||
reduceTo:
|
||||
|
||||
@@ -1,77 +0,0 @@
|
||||
---
|
||||
description: Global vs local mock strategy for Jest tests
|
||||
globs:
|
||||
- "**/*.test.ts"
|
||||
- "**/*.test.tsx"
|
||||
- "**/__tests__/**/*.ts"
|
||||
- "**/__tests__/**/*.tsx"
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
# Mock Decision Strategy
|
||||
|
||||
## Global Mocks (20+ test files)
|
||||
|
||||
- Core infrastructure: react-router-dom, react-query, antd
|
||||
- Browser APIs: ResizeObserver, matchMedia, localStorage
|
||||
- Utility libraries: date-fns, lodash
|
||||
- Available: `uplot` → `__mocks__/uplotMock.ts`
|
||||
|
||||
## Local Mocks (5–15 test files)
|
||||
|
||||
- Business logic dependencies
|
||||
- API endpoints with specific responses
|
||||
- Domain-specific components
|
||||
- Error scenarios and edge cases
|
||||
|
||||
## 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
|
||||
```
|
||||
|
||||
## Anti-patterns
|
||||
|
||||
❌ Don't mock global dependencies locally:
|
||||
```ts
|
||||
jest.mock('react-router-dom', () => ({ ... })); // Already globally mocked
|
||||
```
|
||||
|
||||
❌ Don't create global mocks for test-specific data:
|
||||
```ts
|
||||
jest.mock('../api/tracesService', () => ({
|
||||
getTraces: jest.fn(() => specificTestData) // BAD - should be local
|
||||
}));
|
||||
```
|
||||
|
||||
✅ Do use global mocks for infrastructure:
|
||||
```ts
|
||||
import { useLocation } from 'react-router-dom';
|
||||
```
|
||||
|
||||
✅ Do create local mocks for business logic:
|
||||
```ts
|
||||
jest.mock('../api/tracesService', () => ({
|
||||
getTraces: jest.fn(() => mockTracesData)
|
||||
}));
|
||||
```
|
||||
@@ -1,124 +0,0 @@
|
||||
---
|
||||
description: Core Jest/React Testing Library conventions - harness, MSW, interactions, timers
|
||||
globs:
|
||||
- "**/*.test.ts"
|
||||
- "**/*.test.tsx"
|
||||
- "**/__tests__/**/*.ts"
|
||||
- "**/__tests__/**/*.tsx"
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
# Jest Test Conventions
|
||||
|
||||
Expert developer with Jest, React Testing Library, MSW, and TypeScript. Focus on critical functionality, mock dependencies before imports, test multiple scenarios, write maintainable tests.
|
||||
|
||||
**Auto-detect TypeScript**: Check for TypeScript in the project through tsconfig.json or package.json dependencies. Adjust syntax based on this detection.
|
||||
|
||||
## 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
|
||||
|
||||
```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:
|
||||
```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`.
|
||||
|
||||
## 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
|
||||
|
||||
## Anti-patterns
|
||||
|
||||
❌ Importing RTL directly | ❌ Global fake timers | ❌ Wrapping render in `act(...)` | ❌ Mocking infra locally
|
||||
✅ Use harness | ✅ MSW for API | ✅ userEvent + await | ✅ Pin time only for relative-date tests
|
||||
|
||||
## Example
|
||||
|
||||
```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());
|
||||
});
|
||||
});
|
||||
```
|
||||
@@ -1,168 +0,0 @@
|
||||
---
|
||||
description: TypeScript type safety for Jest tests - mocks, interfaces, no any
|
||||
globs:
|
||||
- "**/*.test.ts"
|
||||
- "**/*.test.tsx"
|
||||
- "**/__tests__/**/*.ts"
|
||||
- "**/__tests__/**/*.tsx"
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
# TypeScript Type Safety for Jest Tests
|
||||
|
||||
**CRITICAL**: All Jest tests MUST be fully type-safe.
|
||||
|
||||
## 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
|
||||
|
||||
## Mock Function Typing
|
||||
|
||||
```ts
|
||||
// ✅ GOOD
|
||||
const mockFetchUser = jest.fn() as jest.MockedFunction<(id: number) => Promise<ApiResponse<User>>>;
|
||||
const mockEventHandler = jest.fn() as jest.MockedFunction<(event: Event) => void>;
|
||||
|
||||
// ❌ BAD
|
||||
const mockFetchUser = jest.fn() as any;
|
||||
```
|
||||
|
||||
## Mock Data with Interfaces
|
||||
|
||||
```ts
|
||||
interface User { id: number; name: string; email: string; }
|
||||
interface ApiResponse<T> { data: T; status: number; message: string; }
|
||||
|
||||
const mockUser: User = { id: 1, name: 'John Doe', email: 'john@example.com' };
|
||||
mockFetchUser.mockResolvedValue({ data: mockUser, status: 200, message: 'Success' });
|
||||
```
|
||||
|
||||
## Component Props Typing
|
||||
|
||||
```ts
|
||||
interface ComponentProps { title: string; data: User[]; onUserSelect: (user: User) => void; }
|
||||
|
||||
const mockProps: ComponentProps = {
|
||||
title: 'Test',
|
||||
data: [{ id: 1, name: 'John', email: 'john@example.com' }],
|
||||
onUserSelect: jest.fn() as jest.MockedFunction<(user: User) => void>,
|
||||
};
|
||||
render(<TestComponent {...mockProps} />);
|
||||
```
|
||||
|
||||
## Hook Testing with Types
|
||||
|
||||
```ts
|
||||
interface UseUserDataReturn {
|
||||
user: User | null;
|
||||
loading: boolean;
|
||||
error: string | null;
|
||||
refetch: () => void;
|
||||
}
|
||||
|
||||
describe('useUserData', () => {
|
||||
it('should return user data with proper typing', () => {
|
||||
const mockUser: User = { id: 1, name: 'John', email: 'john@example.com' };
|
||||
mockFetchUser.mockResolvedValue({ data: mockUser, status: 200, message: 'Success' });
|
||||
const { result } = renderHook(() => useUserData(1));
|
||||
expect(result.current.user).toEqual(mockUser);
|
||||
expect(result.current.loading).toBe(false);
|
||||
expect(result.current.error).toBeNull();
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## Generic Mock Typing
|
||||
|
||||
```ts
|
||||
interface MockApiResponse<T> { data: T; status: number; }
|
||||
|
||||
const mockFetchData = jest.fn() as jest.MockedFunction<
|
||||
<T>(endpoint: string) => Promise<MockApiResponse<T>>
|
||||
>;
|
||||
mockFetchData<User>('/users').mockResolvedValue({ data: { id: 1, name: 'John' }, status: 200 });
|
||||
```
|
||||
|
||||
## React Testing Library with Types
|
||||
|
||||
```ts
|
||||
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
|
||||
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)));
|
||||
```
|
||||
|
||||
## Global Mock Type Safety
|
||||
|
||||
```ts
|
||||
// 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
|
||||
```
|
||||
|
||||
## TypeScript Configuration for Jest
|
||||
|
||||
```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"]
|
||||
}
|
||||
```
|
||||
|
||||
```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__/**/*"]
|
||||
}
|
||||
```
|
||||
|
||||
## 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
|
||||
@@ -52,8 +52,6 @@
|
||||
"@signozhq/combobox": "0.0.2",
|
||||
"@signozhq/command": "0.0.0",
|
||||
"@signozhq/design-tokens": "2.1.1",
|
||||
"@signozhq/dialog": "0.0.2",
|
||||
"@signozhq/drawer": "0.0.4",
|
||||
"@signozhq/icons": "0.1.0",
|
||||
"@signozhq/input": "0.0.2",
|
||||
"@signozhq/popover": "0.0.0",
|
||||
@@ -62,12 +60,10 @@
|
||||
"@signozhq/sonner": "0.1.0",
|
||||
"@signozhq/switch": "0.0.2",
|
||||
"@signozhq/table": "0.3.7",
|
||||
"@signozhq/tabs": "0.0.11",
|
||||
"@signozhq/tooltip": "0.0.2",
|
||||
"@tanstack/react-table": "8.20.6",
|
||||
"@tanstack/react-virtual": "3.11.2",
|
||||
"@uiw/codemirror-theme-copilot": "4.23.11",
|
||||
"@uiw/codemirror-theme-dracula": "4.25.4",
|
||||
"@uiw/codemirror-theme-github": "4.24.1",
|
||||
"@uiw/react-codemirror": "4.23.10",
|
||||
"@uiw/react-md-editor": "3.23.5",
|
||||
@@ -121,6 +117,7 @@
|
||||
"lucide-react": "0.498.0",
|
||||
"mini-css-extract-plugin": "2.4.5",
|
||||
"motion": "12.4.13",
|
||||
"nuqs": "2.8.8",
|
||||
"overlayscrollbars": "^2.8.1",
|
||||
"overlayscrollbars-react": "^0.5.6",
|
||||
"papaparse": "5.4.1",
|
||||
@@ -134,7 +131,7 @@
|
||||
"react-dom": "18.2.0",
|
||||
"react-drag-listview": "2.0.0",
|
||||
"react-error-boundary": "4.0.11",
|
||||
"react-force-graph": "^1.43.0",
|
||||
"react-force-graph-2d": "^1.29.1",
|
||||
"react-full-screen": "1.1.1",
|
||||
"react-grid-layout": "^1.3.4",
|
||||
"react-helmet-async": "1.3.0",
|
||||
@@ -166,7 +163,8 @@
|
||||
"webpack": "5.94.0",
|
||||
"webpack-dev-server": "^5.2.1",
|
||||
"webpack-retry-chunk-load-plugin": "3.1.1",
|
||||
"xstate": "^4.31.0"
|
||||
"xstate": "^4.31.0",
|
||||
"zustand": "5.0.11"
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
@@ -291,4 +289,4 @@
|
||||
"on-headers": "^1.1.0",
|
||||
"tmp": "0.2.4"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,312 +0,0 @@
|
||||
<svg width="929" height="8" viewBox="0 0 929 8" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="12" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="18" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="24" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="30" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="36" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="42" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="48" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="54" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="60" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="66" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="72" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="78" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="84" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="90" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="96" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="102" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="108" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="114" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="120" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="126" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="132" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="138" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="144" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="150" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="156" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="162" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="168" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="174" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="180" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="186" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="192" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="198" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="204" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="210" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="216" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="222" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="228" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="234" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="240" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="246" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="252" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="258" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="264" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="270" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="276" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="282" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="288" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="294" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="300" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="306" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="312" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="318" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="324" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="330" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="336" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="342" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="348" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="354" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="360" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="366" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="372" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="378" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="384" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="390" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="396" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="402" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="408" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="414" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="420" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="426" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="432" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="438" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="444" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="450" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="456" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="462" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="468" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="474" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="480" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="486" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="492" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="498" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="504" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="510" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="516" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="522" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="528" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="534" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="540" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="546" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="552" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="558" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="564" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="570" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="576" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="582" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="588" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="594" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="600" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="606" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="612" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="618" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="624" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="630" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="636" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="642" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="648" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="654" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="660" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="666" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="672" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="678" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="684" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="690" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="696" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="702" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="708" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="714" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="720" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="726" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="732" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="738" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="744" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="750" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="756" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="762" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="768" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="774" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="780" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="786" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="792" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="798" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="804" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="810" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="816" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="822" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="828" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="834" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="840" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="846" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="852" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="858" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="864" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="870" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="876" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="882" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="888" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="894" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="900" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="906" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="912" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="918" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="924" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="6" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="12" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="18" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="24" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="30" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="36" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="42" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="48" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="54" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="60" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="66" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="72" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="78" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="84" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="90" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="96" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="102" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="108" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="114" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="120" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="126" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="132" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="138" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="144" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="150" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="156" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="162" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="168" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="174" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="180" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="186" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="192" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="198" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="204" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="210" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="216" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="222" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="228" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="234" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="240" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="246" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="252" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="258" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="264" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="270" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="276" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="282" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="288" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="294" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="300" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="306" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="312" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="318" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="324" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="330" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="336" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="342" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="348" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="354" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="360" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="366" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="372" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="378" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="384" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="390" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="396" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="402" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="408" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="414" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="420" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="426" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="432" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="438" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="444" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="450" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="456" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="462" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="468" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="474" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="480" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="486" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="492" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="498" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="504" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="510" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="516" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="522" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="528" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="534" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="540" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="546" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="552" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="558" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="564" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="570" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="576" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="582" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="588" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="594" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="600" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="606" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="612" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="618" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="624" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="630" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="636" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="642" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="648" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="654" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="660" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="666" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="672" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="678" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="684" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="690" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="696" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="702" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="708" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="714" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="720" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="726" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="732" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="738" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="744" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="750" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="756" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="762" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="768" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="774" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="780" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="786" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="792" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="798" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="804" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="810" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="816" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="822" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="828" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="834" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="840" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="846" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="852" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="858" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="864" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="870" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="876" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="882" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="888" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="894" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="900" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="906" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="912" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="918" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="924" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 19 KiB |
@@ -253,18 +253,12 @@ export const ShortcutsPage = Loadable(
|
||||
() => import(/* webpackChunkName: "ShortcutsPage" */ 'pages/Settings'),
|
||||
);
|
||||
|
||||
export const Integrations = Loadable(
|
||||
export const InstalledIntegrations = Loadable(
|
||||
() =>
|
||||
import(
|
||||
/* webpackChunkName: "InstalledIntegrations" */ 'pages/IntegrationsModulePage'
|
||||
),
|
||||
);
|
||||
export const IntegrationsDetailsPage = Loadable(
|
||||
() =>
|
||||
import(
|
||||
/* webpackChunkName: "IntegrationsDetailsPage" */ 'pages/IntegrationsDetailsPage'
|
||||
),
|
||||
);
|
||||
|
||||
export const MessagingQueuesMainPage = Loadable(
|
||||
() =>
|
||||
|
||||
@@ -18,8 +18,7 @@ import {
|
||||
ForgotPassword,
|
||||
Home,
|
||||
InfrastructureMonitoring,
|
||||
Integrations,
|
||||
IntegrationsDetailsPage,
|
||||
InstalledIntegrations,
|
||||
LicensePage,
|
||||
ListAllALertsPage,
|
||||
LiveLogs,
|
||||
@@ -390,17 +389,10 @@ const routes: AppRoutes[] = [
|
||||
isPrivate: true,
|
||||
key: 'WORKSPACE_ACCESS_RESTRICTED',
|
||||
},
|
||||
{
|
||||
path: ROUTES.INTEGRATIONS_DETAIL,
|
||||
exact: true,
|
||||
component: IntegrationsDetailsPage,
|
||||
isPrivate: true,
|
||||
key: 'INTEGRATIONS_DETAIL',
|
||||
},
|
||||
{
|
||||
path: ROUTES.INTEGRATIONS,
|
||||
exact: true,
|
||||
component: Integrations,
|
||||
component: InstalledIntegrations,
|
||||
isPrivate: true,
|
||||
key: 'INTEGRATIONS',
|
||||
},
|
||||
|
||||
@@ -1006,6 +1006,18 @@ export interface MetricsexplorertypesUpdateMetricMetadataRequestDTO {
|
||||
unit: string;
|
||||
}
|
||||
|
||||
export interface MetrictypesComparisonSpaceAggregationParamDTO {
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
operator: string;
|
||||
/**
|
||||
* @type number
|
||||
* @format double
|
||||
*/
|
||||
threshold: number;
|
||||
}
|
||||
|
||||
export enum MetrictypesSpaceAggregationDTO {
|
||||
sum = 'sum',
|
||||
avg = 'avg',
|
||||
@@ -1367,6 +1379,7 @@ export interface Querybuildertypesv5LogAggregationDTO {
|
||||
}
|
||||
|
||||
export interface Querybuildertypesv5MetricAggregationDTO {
|
||||
comparisonSpaceAggregationParam?: MetrictypesComparisonSpaceAggregationParamDTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
|
||||
@@ -50,6 +50,7 @@ export interface HostListResponse {
|
||||
total: number;
|
||||
sentAnyHostMetricsData: boolean;
|
||||
isSendingK8SAgentMetrics: boolean;
|
||||
endTimeBeforeRetention: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -5,13 +5,13 @@ import {
|
||||
ServiceData,
|
||||
UpdateServiceConfigPayload,
|
||||
UpdateServiceConfigResponse,
|
||||
} from 'container/Integrations/CloudIntegration/AmazonWebServices/types';
|
||||
} from 'container/CloudIntegrationPage/ServicesSection/types';
|
||||
import {
|
||||
AccountConfigPayload,
|
||||
AccountConfigResponse,
|
||||
AWSAccountConfigPayload,
|
||||
ConnectionParams,
|
||||
ConnectionUrlResponse,
|
||||
} from 'types/api/integrations/aws';
|
||||
import { ConnectionParams } from 'types/api/integrations/types';
|
||||
|
||||
export const getAwsAccounts = async (): Promise<CloudAccount[]> => {
|
||||
const response = await axios.get('/cloud-integrations/aws/accounts');
|
||||
@@ -60,7 +60,7 @@ export const generateConnectionUrl = async (params: {
|
||||
|
||||
export const updateAccountConfig = async (
|
||||
accountId: string,
|
||||
payload: AWSAccountConfigPayload,
|
||||
payload: AccountConfigPayload,
|
||||
): Promise<AccountConfigResponse> => {
|
||||
const response = await axios.post<AccountConfigResponse>(
|
||||
`/cloud-integrations/aws/accounts/${accountId}/config`,
|
||||
|
||||
@@ -1,122 +0,0 @@
|
||||
import axios from 'api';
|
||||
import {
|
||||
CloudAccount,
|
||||
ServiceData,
|
||||
} from 'container/Integrations/CloudIntegration/AmazonWebServices/types';
|
||||
import {
|
||||
AzureCloudAccountConfig,
|
||||
AzureService,
|
||||
AzureServiceConfigPayload,
|
||||
} from 'container/Integrations/types';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import {
|
||||
AccountConfigResponse,
|
||||
AWSAccountConfigPayload,
|
||||
} from 'types/api/integrations/aws';
|
||||
import {
|
||||
AzureAccountConfig,
|
||||
ConnectionParams,
|
||||
IAzureDeploymentCommands,
|
||||
} from 'types/api/integrations/types';
|
||||
|
||||
export const getCloudIntegrationAccounts = async (
|
||||
cloudServiceId: string,
|
||||
): Promise<CloudAccount[]> => {
|
||||
const response = await axios.get(
|
||||
`/cloud-integrations/${cloudServiceId}/accounts`,
|
||||
);
|
||||
|
||||
return response.data.data.accounts;
|
||||
};
|
||||
|
||||
export const getCloudIntegrationServices = async (
|
||||
cloudServiceId: string,
|
||||
cloudAccountId?: string,
|
||||
): Promise<AzureService[]> => {
|
||||
const params = cloudAccountId
|
||||
? { cloud_account_id: cloudAccountId }
|
||||
: undefined;
|
||||
|
||||
const response = await axios.get(
|
||||
`/cloud-integrations/${cloudServiceId}/services`,
|
||||
{
|
||||
params,
|
||||
},
|
||||
);
|
||||
|
||||
return response.data.data.services;
|
||||
};
|
||||
|
||||
export const getCloudIntegrationServiceDetails = async (
|
||||
cloudServiceId: string,
|
||||
serviceId: string,
|
||||
cloudAccountId?: string,
|
||||
): Promise<ServiceData> => {
|
||||
const params = cloudAccountId
|
||||
? { cloud_account_id: cloudAccountId }
|
||||
: undefined;
|
||||
const response = await axios.get(
|
||||
`/cloud-integrations/${cloudServiceId}/services/${serviceId}`,
|
||||
{ params },
|
||||
);
|
||||
return response.data.data;
|
||||
};
|
||||
|
||||
export const updateAccountConfig = async (
|
||||
cloudServiceId: string,
|
||||
accountId: string,
|
||||
payload: AWSAccountConfigPayload | AzureAccountConfig,
|
||||
): Promise<AccountConfigResponse> => {
|
||||
const response = await axios.post<AccountConfigResponse>(
|
||||
`/cloud-integrations/${cloudServiceId}/accounts/${accountId}/config`,
|
||||
payload,
|
||||
);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export const updateServiceConfig = async (
|
||||
cloudServiceId: string,
|
||||
serviceId: string,
|
||||
payload: AzureServiceConfigPayload,
|
||||
): Promise<AzureServiceConfigPayload> => {
|
||||
const response = await axios.post<AzureServiceConfigPayload>(
|
||||
`/cloud-integrations/${cloudServiceId}/services/${serviceId}/config`,
|
||||
payload,
|
||||
);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export const getConnectionParams = async (
|
||||
cloudServiceId: string,
|
||||
): Promise<ConnectionParams> => {
|
||||
const response = await axios.get(
|
||||
`/cloud-integrations/${cloudServiceId}/accounts/generate-connection-params`,
|
||||
);
|
||||
return response.data.data;
|
||||
};
|
||||
|
||||
export const getAzureDeploymentCommands = async (params: {
|
||||
agent_config: ConnectionParams;
|
||||
account_config: AzureCloudAccountConfig;
|
||||
}): Promise<IAzureDeploymentCommands> => {
|
||||
const response = await axios.post(
|
||||
`/cloud-integrations/azure/accounts/generate-connection-url`,
|
||||
params,
|
||||
);
|
||||
|
||||
return response.data.data;
|
||||
};
|
||||
|
||||
export const removeIntegrationAccount = async ({
|
||||
cloudServiceId,
|
||||
accountId,
|
||||
}: {
|
||||
cloudServiceId: string;
|
||||
accountId: string;
|
||||
}): Promise<SuccessResponse<Record<string, never>> | ErrorResponse> => {
|
||||
const response = await axios.post(
|
||||
`/cloud-integrations/${cloudServiceId}/accounts/${accountId}/disconnect`,
|
||||
);
|
||||
|
||||
return response.data;
|
||||
};
|
||||
@@ -1,26 +0,0 @@
|
||||
import { ApiV2Instance as axios } from 'api';
|
||||
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
|
||||
import { AxiosError } from 'axios';
|
||||
import { ErrorResponseV2, ErrorV2Resp, SuccessResponseV2 } from 'types/api';
|
||||
import { MetricMetadataResponse } from 'types/api/metricsExplorer/v2/getMetricMetadata';
|
||||
|
||||
export const getMetricMetadata = async (
|
||||
metricName: string,
|
||||
signal?: AbortSignal,
|
||||
headers?: Record<string, string>,
|
||||
): Promise<SuccessResponseV2<MetricMetadataResponse> | ErrorResponseV2> => {
|
||||
try {
|
||||
const encodedMetricName = encodeURIComponent(metricName);
|
||||
const response = await axios.get(`/metrics/${encodedMetricName}/metadata`, {
|
||||
signal,
|
||||
headers,
|
||||
});
|
||||
|
||||
return {
|
||||
httpStatusCode: response.status,
|
||||
data: response.data,
|
||||
};
|
||||
} catch (error) {
|
||||
return ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
|
||||
}
|
||||
};
|
||||
3
frontend/src/auto-import-registry.d.ts
vendored
3
frontend/src/auto-import-registry.d.ts
vendored
@@ -18,8 +18,6 @@ import '@signozhq/checkbox';
|
||||
import '@signozhq/combobox';
|
||||
import '@signozhq/command';
|
||||
import '@signozhq/design-tokens';
|
||||
import '@signozhq/dialog';
|
||||
import '@signozhq/drawer';
|
||||
import '@signozhq/icons';
|
||||
import '@signozhq/input';
|
||||
import '@signozhq/popover';
|
||||
@@ -28,5 +26,4 @@ import '@signozhq/resizable';
|
||||
import '@signozhq/sonner';
|
||||
import '@signozhq/switch';
|
||||
import '@signozhq/table';
|
||||
import '@signozhq/tabs';
|
||||
import '@signozhq/tooltip';
|
||||
|
||||
@@ -1,109 +0,0 @@
|
||||
.cloud-integration-accounts {
|
||||
padding: 0px 16px;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
|
||||
.selected-cloud-integration-account-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
|
||||
.selected-cloud-integration-account-section-header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--l3-background);
|
||||
background: var(--l1-background);
|
||||
|
||||
.selected-cloud-integration-account-section-header-title {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
.selected-cloud-integration-account-status {
|
||||
display: flex;
|
||||
border-right: 1px solid var(--l3-background);
|
||||
border-radius: none;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 4px;
|
||||
line-height: 32px;
|
||||
}
|
||||
|
||||
.selected-cloud-integration-account-section-header-title-text {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
|
||||
.azure-cloud-account-selector {
|
||||
.ant-select {
|
||||
background-color: transparent !important;
|
||||
border: none !important;
|
||||
box-shadow: none !important;
|
||||
|
||||
.ant-select-selector {
|
||||
background: transparent !important;
|
||||
border: none !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.selected-cloud-integration-account-settings {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 16px;
|
||||
line-height: 32px;
|
||||
margin-right: 8px;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.account-settings-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.add-new-cloud-integration-account-button {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 16px;
|
||||
line-height: 32px;
|
||||
margin-right: 8px;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.cloud-integration-accounts-drawer-content {
|
||||
height: 100%;
|
||||
max-height: calc(100vh - 120px); // Account for drawer header and padding
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 0.2rem;
|
||||
}
|
||||
|
||||
.edit-account-content,
|
||||
.add-new-account-content {
|
||||
flex: 1;
|
||||
min-height: 0; // Allows flex children to shrink below content size
|
||||
}
|
||||
}
|
||||
@@ -1,165 +0,0 @@
|
||||
import { useState } from 'react';
|
||||
import { Button } from '@signozhq/button';
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { DrawerWrapper } from '@signozhq/drawer';
|
||||
import { Select } from 'antd';
|
||||
import ConnectNewAzureAccount from 'container/Integrations/CloudIntegration/AzureServices/AzureAccount/ConnectNewAzureAccount';
|
||||
import EditAzureAccount from 'container/Integrations/CloudIntegration/AzureServices/AzureAccount/EditAzureAccount';
|
||||
import { INTEGRATION_TYPES } from 'container/Integrations/constants';
|
||||
import { CloudAccount } from 'container/Integrations/types';
|
||||
import { useGetConnectionParams } from 'hooks/integration/useGetConnectionParams';
|
||||
import useAxiosError from 'hooks/useAxiosError';
|
||||
import { Dot, PencilLine, Plus } from 'lucide-react';
|
||||
|
||||
import './CloudIntegrationAccounts.styles.scss';
|
||||
|
||||
export type DrawerMode = 'edit' | 'add';
|
||||
|
||||
export default function CloudIntegrationAccounts({
|
||||
selectedAccount,
|
||||
accounts,
|
||||
isLoadingAccounts,
|
||||
onSelectAccount,
|
||||
refetchAccounts,
|
||||
}: {
|
||||
selectedAccount: CloudAccount | null;
|
||||
accounts: CloudAccount[];
|
||||
isLoadingAccounts: boolean;
|
||||
onSelectAccount: (account: CloudAccount) => void;
|
||||
refetchAccounts: () => void;
|
||||
}): JSX.Element {
|
||||
const [isDrawerOpen, setIsDrawerOpen] = useState(false);
|
||||
const [mode, setMode] = useState<DrawerMode>('add');
|
||||
|
||||
const handleDrawerOpenChange = (open: boolean): void => {
|
||||
setIsDrawerOpen(open);
|
||||
};
|
||||
|
||||
const handleEditAccount = (): void => {
|
||||
setMode('edit');
|
||||
setIsDrawerOpen(true);
|
||||
};
|
||||
|
||||
const handleAddNewAccount = (): void => {
|
||||
setMode('add');
|
||||
setIsDrawerOpen(true);
|
||||
};
|
||||
|
||||
const handleError = useAxiosError();
|
||||
|
||||
const {
|
||||
data: connectionParams,
|
||||
isLoading: isConnectionParamsLoading,
|
||||
} = useGetConnectionParams({
|
||||
cloudServiceId: INTEGRATION_TYPES.AZURE,
|
||||
options: { onError: handleError },
|
||||
});
|
||||
|
||||
const handleSelectAccount = (value: string): void => {
|
||||
const account = accounts.find(
|
||||
(account) => account.cloud_account_id === value,
|
||||
);
|
||||
|
||||
if (account) {
|
||||
onSelectAccount(account);
|
||||
}
|
||||
};
|
||||
|
||||
const handleAccountConnected = (): void => {
|
||||
refetchAccounts();
|
||||
};
|
||||
|
||||
const handleAccountUpdated = (): void => {
|
||||
refetchAccounts();
|
||||
};
|
||||
|
||||
const renderDrawerContent = (): JSX.Element => {
|
||||
return (
|
||||
<div className="cloud-integration-accounts-drawer-content">
|
||||
{mode === 'edit' ? (
|
||||
<div className="edit-account-content">
|
||||
<EditAzureAccount
|
||||
selectedAccount={selectedAccount as CloudAccount}
|
||||
connectionParams={connectionParams || {}}
|
||||
isConnectionParamsLoading={isConnectionParamsLoading}
|
||||
onAccountUpdated={handleAccountUpdated}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="add-new-account-content">
|
||||
<ConnectNewAzureAccount
|
||||
connectionParams={connectionParams || {}}
|
||||
isConnectionParamsLoading={isConnectionParamsLoading}
|
||||
onAccountConnected={handleAccountConnected}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="cloud-integration-accounts">
|
||||
{selectedAccount && (
|
||||
<div className="selected-cloud-integration-account-section">
|
||||
<div className="selected-cloud-integration-account-section-header">
|
||||
<div className="selected-cloud-integration-account-section-header-title">
|
||||
<div className="selected-cloud-integration-account-status">
|
||||
<Dot size={24} color={Color.BG_FOREST_500} />
|
||||
</div>
|
||||
<div className="selected-cloud-integration-account-section-header-title-text">
|
||||
Subscription ID :
|
||||
<span className="azure-cloud-account-selector">
|
||||
<Select
|
||||
value={selectedAccount?.cloud_account_id}
|
||||
options={accounts.map((account) => ({
|
||||
label: account.cloud_account_id,
|
||||
value: account.cloud_account_id,
|
||||
}))}
|
||||
onChange={handleSelectAccount}
|
||||
loading={isLoadingAccounts}
|
||||
placeholder="Select Account"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="selected-cloud-integration-account-settings">
|
||||
<Button
|
||||
variant="link"
|
||||
color="secondary"
|
||||
prefixIcon={<PencilLine size={14} />}
|
||||
onClick={handleEditAccount}
|
||||
>
|
||||
Edit Account
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="link"
|
||||
color="secondary"
|
||||
prefixIcon={<Plus size={14} />}
|
||||
onClick={handleAddNewAccount}
|
||||
>
|
||||
Add New Account
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="account-settings-container">
|
||||
<DrawerWrapper
|
||||
open={isDrawerOpen}
|
||||
onOpenChange={handleDrawerOpenChange}
|
||||
type="panel"
|
||||
header={{
|
||||
title: mode === 'add' ? 'Connect with Azure' : 'Edit Azure Account',
|
||||
}}
|
||||
content={renderDrawerContent()}
|
||||
showCloseButton
|
||||
allowOutsideClick={mode === 'edit'}
|
||||
direction="right"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
import CloudIntegrationAccounts from './CloudIntegrationAccounts';
|
||||
|
||||
export default CloudIntegrationAccounts;
|
||||
@@ -1,50 +0,0 @@
|
||||
.cloud-integrations-header-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
border-bottom: 1px solid var(--l3-background);
|
||||
|
||||
.cloud-integrations-header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
padding: 16px;
|
||||
padding-bottom: 0px;
|
||||
gap: 16px;
|
||||
|
||||
.cloud-integrations-title-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
|
||||
.cloud-integrations-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
|
||||
.cloud-integrations-icon {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
color: var(--l1-foreground);
|
||||
font-family: Inter;
|
||||
font-size: 16px;
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
line-height: 32px; /* 200% */
|
||||
letter-spacing: -0.08px;
|
||||
}
|
||||
|
||||
.cloud-integrations-description {
|
||||
color: var(--l2-foreground);
|
||||
font-family: Inter;
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 20px; /* 142.857% */
|
||||
letter-spacing: -0.07px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
import {
|
||||
AWS_INTEGRATION,
|
||||
AZURE_INTEGRATION,
|
||||
} from 'container/Integrations/constants';
|
||||
import { CloudAccount, IntegrationType } from 'container/Integrations/types';
|
||||
|
||||
import CloudIntegrationAccounts from '../CloudIntegrationAccounts';
|
||||
|
||||
import './CloudIntegrationsHeader.styles.scss';
|
||||
|
||||
export default function CloudIntegrationsHeader({
|
||||
cloudServiceId,
|
||||
selectedAccount,
|
||||
accounts,
|
||||
isLoadingAccounts,
|
||||
onSelectAccount,
|
||||
refetchAccounts,
|
||||
}: {
|
||||
selectedAccount: CloudAccount | null;
|
||||
accounts: CloudAccount[] | [];
|
||||
isLoadingAccounts: boolean;
|
||||
onSelectAccount: (account: CloudAccount) => void;
|
||||
cloudServiceId: IntegrationType;
|
||||
refetchAccounts: () => void;
|
||||
}): JSX.Element {
|
||||
const INTEGRATION_DATA =
|
||||
cloudServiceId === IntegrationType.AWS_SERVICES
|
||||
? AWS_INTEGRATION
|
||||
: AZURE_INTEGRATION;
|
||||
|
||||
return (
|
||||
<div className="cloud-integrations-header-section">
|
||||
<div className="cloud-integrations-header">
|
||||
<div className="cloud-integrations-title-section">
|
||||
<div className="cloud-integrations-title">
|
||||
<img
|
||||
className="cloud-integrations-icon"
|
||||
src={INTEGRATION_DATA.icon}
|
||||
alt={INTEGRATION_DATA.icon_alt}
|
||||
/>
|
||||
|
||||
{INTEGRATION_DATA.title}
|
||||
</div>
|
||||
<div className="cloud-integrations-description">
|
||||
{INTEGRATION_DATA.description}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="cloud-integrations-accounts-list">
|
||||
<CloudIntegrationAccounts
|
||||
selectedAccount={selectedAccount}
|
||||
accounts={accounts}
|
||||
isLoadingAccounts={isLoadingAccounts}
|
||||
onSelectAccount={onSelectAccount}
|
||||
refetchAccounts={refetchAccounts}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
import CloudIntegrationsHeader from './CloudIntegrationsHeader';
|
||||
|
||||
export default CloudIntegrationsHeader;
|
||||
@@ -1,34 +0,0 @@
|
||||
.cloud-service-data-collected {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
|
||||
.cloud-service-data-collected-table {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
|
||||
.cloud-service-data-collected-table-heading {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
color: var(--l2-foreground);
|
||||
|
||||
/* Bifrost (Ancient)/Content/sm */
|
||||
font-family: Inter;
|
||||
font-size: 13px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 20px; /* 142.857% */
|
||||
letter-spacing: -0.07px;
|
||||
}
|
||||
|
||||
.cloud-service-data-collected-table-logs {
|
||||
border-radius: 6px;
|
||||
border: 1px solid var(--l3-background);
|
||||
background: var(--l1-background);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
.code-block-container {
|
||||
position: relative;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
|
||||
.code-block-copy-btn {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
right: 8px;
|
||||
transform: translateY(-50%);
|
||||
z-index: 10;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 6px 8px;
|
||||
color: var(--bg-vanilla-100);
|
||||
transition: color 0.15s ease;
|
||||
|
||||
&.copied {
|
||||
background-color: var(--bg-robin-500);
|
||||
}
|
||||
}
|
||||
|
||||
// CodeMirror wrapper
|
||||
.code-block-editor {
|
||||
border-radius: 4px;
|
||||
|
||||
.cm-editor {
|
||||
border-radius: 4px;
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
font-family: 'Space Mono', monospace;
|
||||
}
|
||||
|
||||
.cm-scroller {
|
||||
font-family: 'Space Mono', monospace;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,145 +0,0 @@
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import { CheckOutlined, CopyOutlined } from '@ant-design/icons';
|
||||
import { javascript } from '@codemirror/lang-javascript';
|
||||
import { Button } from '@signozhq/button';
|
||||
import { dracula } from '@uiw/codemirror-theme-dracula';
|
||||
import { githubLight } from '@uiw/codemirror-theme-github';
|
||||
import CodeMirror, {
|
||||
EditorState,
|
||||
EditorView,
|
||||
Extension,
|
||||
} from '@uiw/react-codemirror';
|
||||
import cx from 'classnames';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
|
||||
import './CodeBlock.styles.scss';
|
||||
|
||||
export type CodeBlockLanguage =
|
||||
| 'javascript'
|
||||
| 'typescript'
|
||||
| 'js'
|
||||
| 'ts'
|
||||
| 'json'
|
||||
| 'bash'
|
||||
| 'shell'
|
||||
| 'text';
|
||||
|
||||
export type CodeBlockTheme = 'light' | 'dark' | 'auto';
|
||||
|
||||
interface CodeBlockProps {
|
||||
/** The code content to display */
|
||||
value: string;
|
||||
/** Language for syntax highlighting */
|
||||
language?: CodeBlockLanguage;
|
||||
/** Theme: 'light' | 'dark' | 'auto' (follows app dark mode when 'auto') */
|
||||
theme?: CodeBlockTheme;
|
||||
/** Show line numbers */
|
||||
lineNumbers?: boolean;
|
||||
/** Show copy button */
|
||||
showCopyButton?: boolean;
|
||||
/** Custom class name for the container */
|
||||
className?: string;
|
||||
/** Max height in pixels - enables scrolling when content exceeds */
|
||||
maxHeight?: number | string;
|
||||
/** Callback when copy is clicked */
|
||||
onCopy?: (copiedText: string) => void;
|
||||
}
|
||||
|
||||
const LANGUAGE_EXTENSION_MAP: Record<
|
||||
CodeBlockLanguage,
|
||||
ReturnType<typeof javascript> | undefined
|
||||
> = {
|
||||
javascript: javascript({ jsx: true }),
|
||||
typescript: javascript({ jsx: true }),
|
||||
js: javascript({ jsx: true }),
|
||||
ts: javascript({ jsx: true }),
|
||||
json: javascript(), // JSON is valid JS; proper json() would require @codemirror/lang-json
|
||||
bash: undefined,
|
||||
shell: undefined,
|
||||
text: undefined,
|
||||
};
|
||||
|
||||
function CodeBlock({
|
||||
value,
|
||||
language = 'text',
|
||||
theme: themeProp = 'auto',
|
||||
lineNumbers = true,
|
||||
showCopyButton = true,
|
||||
className,
|
||||
maxHeight,
|
||||
onCopy,
|
||||
}: CodeBlockProps): JSX.Element {
|
||||
const isDarkMode = useIsDarkMode();
|
||||
const [isCopied, setIsCopied] = useState(false);
|
||||
|
||||
const resolvedDark = themeProp === 'auto' ? isDarkMode : themeProp === 'dark';
|
||||
const theme = resolvedDark ? dracula : githubLight;
|
||||
|
||||
const extensions = useMemo((): Extension[] => {
|
||||
const langExtension = LANGUAGE_EXTENSION_MAP[language];
|
||||
return [
|
||||
EditorState.readOnly.of(true),
|
||||
EditorView.editable.of(false),
|
||||
EditorView.lineWrapping,
|
||||
...(langExtension ? [langExtension] : []),
|
||||
];
|
||||
}, [language]);
|
||||
|
||||
const handleCopy = useCallback((): void => {
|
||||
navigator.clipboard.writeText(value).then(() => {
|
||||
setIsCopied(true);
|
||||
onCopy?.(value);
|
||||
setTimeout(() => setIsCopied(false), 2000);
|
||||
});
|
||||
}, [value, onCopy]);
|
||||
|
||||
return (
|
||||
<div className={cx('code-block-container', className)}>
|
||||
{showCopyButton && (
|
||||
<Button
|
||||
variant="solid"
|
||||
size="xs"
|
||||
color="secondary"
|
||||
className={cx('code-block-copy-btn', { copied: isCopied })}
|
||||
onClick={handleCopy}
|
||||
aria-label={isCopied ? 'Copied' : 'Copy code'}
|
||||
title={isCopied ? 'Copied' : 'Copy code'}
|
||||
>
|
||||
{isCopied ? <CheckOutlined /> : <CopyOutlined />}
|
||||
</Button>
|
||||
)}
|
||||
<CodeMirror
|
||||
className="code-block-editor"
|
||||
value={value}
|
||||
theme={theme}
|
||||
readOnly
|
||||
editable={false}
|
||||
extensions={extensions}
|
||||
basicSetup={{
|
||||
lineNumbers,
|
||||
highlightActiveLineGutter: false,
|
||||
highlightActiveLine: false,
|
||||
highlightSelectionMatches: true,
|
||||
drawSelection: true,
|
||||
syntaxHighlighting: true,
|
||||
bracketMatching: true,
|
||||
history: false,
|
||||
foldGutter: false,
|
||||
autocompletion: false,
|
||||
defaultKeymap: false,
|
||||
searchKeymap: true,
|
||||
historyKeymap: false,
|
||||
foldKeymap: false,
|
||||
completionKeymap: false,
|
||||
closeBrackets: false,
|
||||
indentOnInput: false,
|
||||
}}
|
||||
style={{
|
||||
maxHeight: maxHeight ?? 'auto',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default CodeBlock;
|
||||
@@ -1,2 +0,0 @@
|
||||
export type { CodeBlockLanguage, CodeBlockTheme } from './CodeBlock';
|
||||
export { default as CodeBlock } from './CodeBlock';
|
||||
@@ -248,6 +248,11 @@ declare module 'chart.js' {
|
||||
}
|
||||
}
|
||||
|
||||
const intlNumberFormatter = new Intl.NumberFormat('en-US', {
|
||||
useGrouping: false,
|
||||
maximumFractionDigits: 20,
|
||||
});
|
||||
|
||||
/**
|
||||
* Formats a number for display, preserving leading zeros after the decimal point
|
||||
* and showing up to DEFAULT_SIGNIFICANT_DIGITS digits after the first non-zero decimal digit.
|
||||
@@ -270,10 +275,7 @@ export const formatDecimalWithLeadingZeros = (
|
||||
}
|
||||
|
||||
// Use toLocaleString to get a full decimal representation without scientific notation.
|
||||
const numStr = value.toLocaleString('en-US', {
|
||||
useGrouping: false,
|
||||
maximumFractionDigits: 20,
|
||||
});
|
||||
const numStr = intlNumberFormatter.format(value);
|
||||
|
||||
const [integerPart, decimalPart = ''] = numStr.split('.');
|
||||
|
||||
|
||||
@@ -3,8 +3,8 @@ import { useLocation } from 'react-router-dom';
|
||||
import { toast } from '@signozhq/sonner';
|
||||
import { Button, Input, Radio, RadioChangeEvent, Typography } from 'antd';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { handleContactSupport } from 'container/Integrations/utils';
|
||||
import { useGetTenantLicense } from 'hooks/useGetTenantLicense';
|
||||
import { handleContactSupport } from 'pages/Integrations/utils';
|
||||
|
||||
function FeedbackModal({ onClose }: { onClose: () => void }): JSX.Element {
|
||||
const [activeTab, setActiveTab] = useState('feedback');
|
||||
|
||||
@@ -5,8 +5,8 @@ import { toast } from '@signozhq/sonner';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { handleContactSupport } from 'container/Integrations/utils';
|
||||
import { useGetTenantLicense } from 'hooks/useGetTenantLicense';
|
||||
import { handleContactSupport } from 'pages/Integrations/utils';
|
||||
|
||||
import FeedbackModal from '../FeedbackModal';
|
||||
|
||||
@@ -31,7 +31,7 @@ jest.mock('hooks/useGetTenantLicense', () => ({
|
||||
useGetTenantLicense: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('container/Integrations/utils', () => ({
|
||||
jest.mock('pages/Integrations/utils', () => ({
|
||||
handleContactSupport: jest.fn(),
|
||||
}));
|
||||
|
||||
|
||||
@@ -86,8 +86,13 @@ function LogDetailInner({
|
||||
const handleClickOutside = (e: MouseEvent): void => {
|
||||
const target = e.target as HTMLElement;
|
||||
|
||||
// Don't close if clicking on explicitly ignored regions
|
||||
if (target.closest('[data-log-detail-ignore="true"]')) {
|
||||
// Don't close if clicking on drawer content, overlays, or portal elements
|
||||
if (
|
||||
target.closest('[data-log-detail-ignore="true"]') ||
|
||||
target.closest('.cm-tooltip-autocomplete') ||
|
||||
target.closest('.drawer-popover') ||
|
||||
target.closest('.query-status-popover')
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -400,7 +405,11 @@ function LogDetailInner({
|
||||
<div className="log-detail-drawer__content" data-log-detail-ignore="true">
|
||||
<div className="log-detail-drawer__log">
|
||||
<Divider type="vertical" className={cx('log-type-indicator', logType)} />
|
||||
<Tooltip title={removeEscapeCharacters(log?.body)} placement="left">
|
||||
<Tooltip
|
||||
title={removeEscapeCharacters(log?.body)}
|
||||
placement="left"
|
||||
mouseLeaveDelay={0}
|
||||
>
|
||||
<div className="log-body" dangerouslySetInnerHTML={htmlBody} />
|
||||
</Tooltip>
|
||||
|
||||
@@ -466,6 +475,7 @@ function LogDetailInner({
|
||||
title="Show Filters"
|
||||
placement="topLeft"
|
||||
aria-label="Show Filters"
|
||||
mouseLeaveDelay={0}
|
||||
>
|
||||
<Button
|
||||
className="action-btn"
|
||||
@@ -481,6 +491,7 @@ function LogDetailInner({
|
||||
aria-label={
|
||||
selectedView === VIEW_TYPES.JSON ? 'Copy JSON' : 'Copy Log Link'
|
||||
}
|
||||
mouseLeaveDelay={0}
|
||||
>
|
||||
<Button
|
||||
className="action-btn"
|
||||
|
||||
@@ -27,7 +27,11 @@ function AddToQueryHOC({
|
||||
return (
|
||||
// eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions
|
||||
<div className={cx('addToQueryContainer', fontSize)} onClick={handleQueryAdd}>
|
||||
<Popover placement="top" content={popOverContent}>
|
||||
<Popover
|
||||
overlayClassName="drawer-popover"
|
||||
placement="top"
|
||||
content={popOverContent}
|
||||
>
|
||||
{children}
|
||||
</Popover>
|
||||
</div>
|
||||
|
||||
@@ -32,6 +32,7 @@ function CopyClipboardHOC({
|
||||
<span onClick={onClick} role="presentation" tabIndex={-1}>
|
||||
<Popover
|
||||
placement="top"
|
||||
overlayClassName="drawer-popover"
|
||||
content={<span style={{ fontSize: '0.9rem' }}>{tooltipText}</span>}
|
||||
>
|
||||
{children}
|
||||
|
||||
@@ -11,7 +11,6 @@ import {
|
||||
startCompletion,
|
||||
} from '@codemirror/autocomplete';
|
||||
import { javascript } from '@codemirror/lang-javascript';
|
||||
import * as Sentry from '@sentry/react';
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { copilot } from '@uiw/codemirror-theme-copilot';
|
||||
import { githubLight } from '@uiw/codemirror-theme-github';
|
||||
@@ -564,15 +563,7 @@ function QuerySearch({
|
||||
const lastPos = lastPosRef.current;
|
||||
|
||||
if (newPos.line !== lastPos.line || newPos.ch !== lastPos.ch) {
|
||||
setCursorPos((lastPos) => {
|
||||
if (newPos.ch !== lastPos.ch && newPos.ch === 0) {
|
||||
Sentry.captureEvent({
|
||||
message: `Cursor jumped to start of line from ${lastPos.ch} to ${newPos.ch}`,
|
||||
level: 'warning',
|
||||
});
|
||||
}
|
||||
return newPos;
|
||||
});
|
||||
setCursorPos(newPos);
|
||||
lastPosRef.current = newPos;
|
||||
|
||||
if (doc) {
|
||||
@@ -1328,7 +1319,10 @@ function QuerySearch({
|
||||
)}
|
||||
|
||||
<div className="query-where-clause-editor-container">
|
||||
<Tooltip title={getTooltipContent()} placement="left">
|
||||
<Tooltip
|
||||
title={<div data-log-detail-ignore="true">{getTooltipContent()}</div>}
|
||||
placement="left"
|
||||
>
|
||||
<a
|
||||
href="https://signoz.io/docs/userguide/search-syntax/"
|
||||
target="_blank"
|
||||
|
||||
@@ -40,6 +40,7 @@ function ValueGraph({
|
||||
value,
|
||||
rawValue,
|
||||
thresholds,
|
||||
yAxisUnit,
|
||||
}: ValueGraphProps): JSX.Element {
|
||||
const { t } = useTranslation(['valueGraph']);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
@@ -87,7 +88,7 @@ function ValueGraph({
|
||||
const {
|
||||
threshold,
|
||||
isConflictingThresholds,
|
||||
} = getBackgroundColorAndThresholdCheck(thresholds, rawValue);
|
||||
} = getBackgroundColorAndThresholdCheck(thresholds, rawValue, yAxisUnit);
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -155,6 +156,7 @@ interface ValueGraphProps {
|
||||
value: string;
|
||||
rawValue: number;
|
||||
thresholds: ThresholdProps[];
|
||||
yAxisUnit?: string;
|
||||
}
|
||||
|
||||
export default ValueGraph;
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { getYAxisFormattedValue } from 'components/Graph/yAxisConfig';
|
||||
import { evaluateThresholdWithConvertedValue } from 'container/GridTableComponent/utils';
|
||||
import { ThresholdProps } from 'container/NewWidget/RightContainer/Threshold/types';
|
||||
|
||||
function compareThreshold(
|
||||
function doesValueSatisfyThreshold(
|
||||
rawValue: number,
|
||||
threshold: ThresholdProps,
|
||||
yAxisUnit?: string,
|
||||
): boolean {
|
||||
if (
|
||||
threshold.thresholdOperator === undefined ||
|
||||
@@ -11,31 +12,14 @@ function compareThreshold(
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
switch (threshold.thresholdOperator) {
|
||||
case '>':
|
||||
return rawValue > threshold.thresholdValue;
|
||||
case '>=':
|
||||
return rawValue >= threshold.thresholdValue;
|
||||
case '<':
|
||||
return rawValue < threshold.thresholdValue;
|
||||
case '<=':
|
||||
return rawValue <= threshold.thresholdValue;
|
||||
case '=':
|
||||
return rawValue === threshold.thresholdValue;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function extractNumbersFromString(inputString: string): number[] {
|
||||
const regex = /[+-]?\d+(\.\d+)?/g;
|
||||
const matches = inputString.match(regex);
|
||||
|
||||
if (matches) {
|
||||
return matches.map(Number);
|
||||
}
|
||||
|
||||
return [];
|
||||
return evaluateThresholdWithConvertedValue(
|
||||
rawValue,
|
||||
threshold.thresholdValue,
|
||||
threshold.thresholdOperator,
|
||||
threshold.thresholdUnit,
|
||||
yAxisUnit,
|
||||
);
|
||||
}
|
||||
|
||||
function getHighestPrecedenceThreshold(
|
||||
@@ -60,21 +44,32 @@ function getHighestPrecedenceThreshold(
|
||||
return highestPrecedenceThreshold;
|
||||
}
|
||||
|
||||
function extractNumbersFromString(inputString: string): number[] {
|
||||
const regex = /[+-]?\d+(\.\d+)?/g;
|
||||
const matches = inputString.match(regex);
|
||||
|
||||
if (matches) {
|
||||
return matches.map(Number);
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
export function getBackgroundColorAndThresholdCheck(
|
||||
thresholds: ThresholdProps[],
|
||||
rawValue: number,
|
||||
yAxisUnit?: string,
|
||||
): {
|
||||
threshold: ThresholdProps;
|
||||
isConflictingThresholds: boolean;
|
||||
} {
|
||||
const matchingThresholds = thresholds.filter((threshold) =>
|
||||
compareThreshold(
|
||||
extractNumbersFromString(
|
||||
getYAxisFormattedValue(rawValue.toString(), threshold.thresholdUnit || ''),
|
||||
)[0],
|
||||
threshold,
|
||||
),
|
||||
);
|
||||
const matchingThresholds = thresholds.filter((threshold) => {
|
||||
const numbers = extractNumbersFromString(rawValue.toString());
|
||||
if (numbers.length === 0) {
|
||||
return false;
|
||||
}
|
||||
return doesValueSatisfyThreshold(numbers[0], threshold, yAxisUnit);
|
||||
});
|
||||
|
||||
if (matchingThresholds.length === 0) {
|
||||
return {
|
||||
|
||||
@@ -22,6 +22,8 @@ function YAxisUnitSelector({
|
||||
'data-testid': dataTestId,
|
||||
source,
|
||||
initialValue,
|
||||
categoriesOverride,
|
||||
containerClassName,
|
||||
}: YAxisUnitSelectorProps): JSX.Element {
|
||||
const universalUnit = mapMetricUnitToUniversalUnit(value);
|
||||
|
||||
@@ -66,10 +68,14 @@ function YAxisUnitSelector({
|
||||
return aliases.some((alias) => alias.toLowerCase().includes(search));
|
||||
};
|
||||
|
||||
const categories = getYAxisCategories(source);
|
||||
const categoriesToRender = useMemo(() => {
|
||||
return categoriesOverride || getYAxisCategories(source);
|
||||
}, [categoriesOverride, source]);
|
||||
|
||||
return (
|
||||
<div className="y-axis-unit-selector-component">
|
||||
<div
|
||||
className={classNames('y-axis-unit-selector-component', containerClassName)}
|
||||
>
|
||||
<Select
|
||||
showSearch
|
||||
value={universalUnit}
|
||||
@@ -90,7 +96,7 @@ function YAxisUnitSelector({
|
||||
data-testid={dataTestId}
|
||||
allowClear
|
||||
>
|
||||
{categories.map((category) => (
|
||||
{categoriesToRender.map((category) => (
|
||||
<Select.OptGroup key={category.name} label={category.name}>
|
||||
{category.units.map((unit) => (
|
||||
<Select.Option key={unit.id} value={unit.id}>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { fireEvent, render, screen } from '@testing-library/react';
|
||||
|
||||
import { YAxisSource } from '../types';
|
||||
import { YAxisCategoryNames } from '../constants';
|
||||
import { UniversalYAxisUnit, YAxisSource } from '../types';
|
||||
import YAxisUnitSelector from '../YAxisUnitSelector';
|
||||
|
||||
describe('YAxisUnitSelector', () => {
|
||||
@@ -123,4 +124,34 @@ describe('YAxisUnitSelector', () => {
|
||||
const warningIcon = screen.queryByLabelText('warning');
|
||||
expect(warningIcon).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('uses categories override to render custom units', () => {
|
||||
const customCategories = [
|
||||
{
|
||||
name: YAxisCategoryNames.Data,
|
||||
units: [
|
||||
{
|
||||
id: UniversalYAxisUnit.BYTES,
|
||||
name: 'Custom Bytes (B)',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
render(
|
||||
<YAxisUnitSelector
|
||||
value=""
|
||||
onChange={mockOnChange}
|
||||
source={YAxisSource.ALERTS}
|
||||
categoriesOverride={customCategories}
|
||||
/>,
|
||||
);
|
||||
|
||||
const select = screen.getByRole('combobox');
|
||||
|
||||
fireEvent.mouseDown(select);
|
||||
|
||||
expect(screen.getByText('Custom Bytes (B)')).toBeInTheDocument();
|
||||
expect(screen.queryByText('Bytes (B)')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -9,6 +9,8 @@ export interface YAxisUnitSelectorProps {
|
||||
'data-testid'?: string;
|
||||
source: YAxisSource;
|
||||
initialValue?: string;
|
||||
categoriesOverride?: YAxisCategory[];
|
||||
containerClassName?: string;
|
||||
}
|
||||
|
||||
export enum UniversalYAxisUnit {
|
||||
|
||||
@@ -1,19 +1,4 @@
|
||||
export const REACT_QUERY_KEY = {
|
||||
// Cloud Integration Query Keys
|
||||
CLOUD_INTEGRATION_ACCOUNTS: 'CLOUD_INTEGRATION_ACCOUNTS',
|
||||
CLOUD_INTEGRATION_SERVICES: 'CLOUD_INTEGRATION_SERVICES',
|
||||
CLOUD_INTEGRATION_SERVICE_DETAILS: 'CLOUD_INTEGRATION_SERVICE_DETAILS',
|
||||
CLOUD_INTEGRATION_ACCOUNT_STATUS: 'CLOUD_INTEGRATION_ACCOUNT_STATUS',
|
||||
CLOUD_INTEGRATION_UPDATE_ACCOUNT_CONFIG:
|
||||
'CLOUD_INTEGRATION_UPDATE_ACCOUNT_CONFIG',
|
||||
CLOUD_INTEGRATION_UPDATE_SERVICE_CONFIG:
|
||||
'CLOUD_INTEGRATION_UPDATE_SERVICE_CONFIG',
|
||||
CLOUD_INTEGRATION_GENERATE_CONNECTION_URL:
|
||||
'CLOUD_INTEGRATION_GENERATE_CONNECTION_URL',
|
||||
CLOUD_INTEGRATION_GET_CONNECTION_PARAMS:
|
||||
'CLOUD_INTEGRATION_GET_CONNECTION_PARAMS',
|
||||
CLOUD_INTEGRATION_GET_DEPLOYMENT_COMMANDS:
|
||||
'CLOUD_INTEGRATION_GET_DEPLOYMENT_COMMANDS',
|
||||
GET_PUBLIC_DASHBOARD: 'GET_PUBLIC_DASHBOARD',
|
||||
GET_PUBLIC_DASHBOARD_META: 'GET_PUBLIC_DASHBOARD_META',
|
||||
GET_PUBLIC_DASHBOARD_WIDGET_DATA: 'GET_PUBLIC_DASHBOARD_WIDGET_DATA',
|
||||
|
||||
@@ -65,7 +65,6 @@ const ROUTES = {
|
||||
WORKSPACE_SUSPENDED: '/workspace-suspended',
|
||||
SHORTCUTS: '/settings/shortcuts',
|
||||
INTEGRATIONS: '/integrations',
|
||||
INTEGRATIONS_DETAIL: '/integrations/:integrationId',
|
||||
MESSAGING_QUEUES_BASE: '/messaging-queues',
|
||||
MESSAGING_QUEUES_KAFKA: '/messaging-queues/kafka',
|
||||
MESSAGING_QUEUES_KAFKA_DETAIL: '/messaging-queues/kafka/detail',
|
||||
|
||||
@@ -117,7 +117,7 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
||||
const [showSlowApiWarning, setShowSlowApiWarning] = useState(false);
|
||||
const [slowApiWarningShown, setSlowApiWarningShown] = useState(false);
|
||||
|
||||
const { latestVersion } = useSelector<AppState, AppReducer>(
|
||||
const { currentVersion } = useSelector<AppState, AppReducer>(
|
||||
(state) => state.app,
|
||||
);
|
||||
|
||||
@@ -213,9 +213,9 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
||||
},
|
||||
{
|
||||
queryFn: (): Promise<SuccessResponse<ChangelogSchema> | ErrorResponse> =>
|
||||
getChangelogByVersion(latestVersion, changelogForTenant),
|
||||
queryKey: ['getChangelogByVersion', latestVersion, changelogForTenant],
|
||||
enabled: isLoggedIn && Boolean(latestVersion),
|
||||
getChangelogByVersion(currentVersion, changelogForTenant),
|
||||
queryKey: ['getChangelogByVersion', currentVersion, changelogForTenant],
|
||||
enabled: isLoggedIn && Boolean(currentVersion),
|
||||
},
|
||||
]);
|
||||
|
||||
@@ -226,7 +226,7 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
||||
!changelog &&
|
||||
!getChangelogByVersionResponse.isLoading &&
|
||||
isLoggedIn &&
|
||||
Boolean(latestVersion)
|
||||
Boolean(currentVersion)
|
||||
) {
|
||||
getChangelogByVersionResponse.refetch();
|
||||
}
|
||||
@@ -237,9 +237,9 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
||||
let timer: ReturnType<typeof setTimeout>;
|
||||
if (
|
||||
isCloudUserVal &&
|
||||
Boolean(latestVersion) &&
|
||||
Boolean(currentVersion) &&
|
||||
seenChangelogVersion != null &&
|
||||
latestVersion !== seenChangelogVersion &&
|
||||
currentVersion !== seenChangelogVersion &&
|
||||
daysSinceAccountCreation > MIN_ACCOUNT_AGE_FOR_CHANGELOG && // Show to only users older than 2 weeks
|
||||
!isWorkspaceAccessRestricted
|
||||
) {
|
||||
@@ -255,7 +255,7 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [
|
||||
isCloudUserVal,
|
||||
latestVersion,
|
||||
currentVersion,
|
||||
seenChangelogVersion,
|
||||
toggleChangelogModal,
|
||||
isWorkspaceAccessRestricted,
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import {
|
||||
IntegrationType,
|
||||
RequestIntegrationBtn,
|
||||
} from 'pages/Integrations/RequestIntegrationBtn';
|
||||
|
||||
import Header from './Header/Header';
|
||||
import HeroSection from './HeroSection/HeroSection';
|
||||
import ServicesTabs from './ServicesSection/ServicesTabs';
|
||||
|
||||
function CloudIntegrationPage(): JSX.Element {
|
||||
return (
|
||||
<div>
|
||||
<Header />
|
||||
<HeroSection />
|
||||
<RequestIntegrationBtn
|
||||
type={IntegrationType.AWS_SERVICES}
|
||||
message="Can't find the AWS service you're looking for? Request more integrations"
|
||||
/>
|
||||
<ServicesTabs />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default CloudIntegrationPage;
|
||||
@@ -48,7 +48,7 @@
|
||||
|
||||
.lightMode {
|
||||
.cloud-header {
|
||||
border-bottom: 1px solid var(--bg-vanilla-300);
|
||||
border-bottom: 1px solid var(--bg-slate-300);
|
||||
|
||||
&__breadcrumb-title {
|
||||
color: var(--bg-ink-400);
|
||||
@@ -1,13 +1,11 @@
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Button } from '@signozhq/button';
|
||||
import Breadcrumb from 'antd/es/breadcrumb';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { IntegrationType } from 'container/Integrations/types';
|
||||
import { Blocks, LifeBuoy } from 'lucide-react';
|
||||
|
||||
import './Header.styles.scss';
|
||||
|
||||
function Header({ title }: { title: IntegrationType }): JSX.Element {
|
||||
function Header(): JSX.Element {
|
||||
return (
|
||||
<div className="cloud-header">
|
||||
<div className="cloud-header__navigation">
|
||||
@@ -25,26 +23,25 @@ function Header({ title }: { title: IntegrationType }): JSX.Element {
|
||||
),
|
||||
},
|
||||
{
|
||||
title: <div className="cloud-header__breadcrumb-title">{title}</div>,
|
||||
title: (
|
||||
<div className="cloud-header__breadcrumb-title">
|
||||
Amazon Web Services
|
||||
</div>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
<div className="cloud-header__actions">
|
||||
<Button
|
||||
variant="solid"
|
||||
size="sm"
|
||||
color="secondary"
|
||||
onClick={(): void => {
|
||||
window.open(
|
||||
'https://signoz.io/blog/native-aws-integrations-with-autodiscovery/',
|
||||
'_blank',
|
||||
);
|
||||
}}
|
||||
prefixIcon={<LifeBuoy size={12} />}
|
||||
<a
|
||||
href="https://signoz.io/blog/native-aws-integrations-with-autodiscovery/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="cloud-header__help"
|
||||
>
|
||||
<LifeBuoy size={12} />
|
||||
Get Help
|
||||
</Button>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -1,4 +1,5 @@
|
||||
.hero-section {
|
||||
height: 308px;
|
||||
padding: 26px 16px;
|
||||
display: flex;
|
||||
gap: 24px;
|
||||
@@ -0,0 +1,34 @@
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
|
||||
import AccountActions from './components/AccountActions';
|
||||
|
||||
import './HeroSection.style.scss';
|
||||
|
||||
function HeroSection(): JSX.Element {
|
||||
const isDarkMode = useIsDarkMode();
|
||||
return (
|
||||
<div
|
||||
className="hero-section"
|
||||
style={
|
||||
isDarkMode
|
||||
? {
|
||||
backgroundImage: `url('/Images/integrations-hero-bg.png')`,
|
||||
}
|
||||
: {}
|
||||
}
|
||||
>
|
||||
<div className="hero-section__icon">
|
||||
<img src="/Logos/aws-dark.svg" alt="aws-logo" />
|
||||
</div>
|
||||
<div className="hero-section__details">
|
||||
<div className="title">Amazon Web Services</div>
|
||||
<div className="description">
|
||||
One-click setup for AWS monitoring with SigNoz
|
||||
</div>
|
||||
<AccountActions />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default HeroSection;
|
||||
@@ -11,6 +11,7 @@
|
||||
|
||||
&__input-skeleton {
|
||||
width: 300px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
&__new-account-button-skeleton {
|
||||
@@ -1,16 +1,14 @@
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom-v5-compat';
|
||||
import { Button } from '@signozhq/button';
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { Select, Skeleton } from 'antd';
|
||||
import { Button, Select, Skeleton } from 'antd';
|
||||
import { SelectProps } from 'antd/lib';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { getAccountById } from 'container/Integrations/CloudIntegration/utils';
|
||||
import { useAwsAccounts } from 'hooks/integration/aws/useAwsAccounts';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
import { Check, ChevronDown } from 'lucide-react';
|
||||
|
||||
import { CloudAccount } from '../../types';
|
||||
import { CloudAccount } from '../../ServicesSection/types';
|
||||
import AccountSettingsModal from './AccountSettingsModal';
|
||||
import CloudAccountSetupModal from './CloudAccountSetupModal';
|
||||
|
||||
@@ -50,6 +48,12 @@ function renderOption(
|
||||
);
|
||||
}
|
||||
|
||||
const getAccountById = (
|
||||
accounts: CloudAccount[],
|
||||
accountId: string,
|
||||
): CloudAccount | null =>
|
||||
accounts.find((account) => account.cloud_account_id === accountId) || null;
|
||||
|
||||
function AccountActionsRenderer({
|
||||
accounts,
|
||||
isLoading,
|
||||
@@ -70,7 +74,24 @@ function AccountActionsRenderer({
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="hero-section__actions-with-account">
|
||||
<Skeleton.Input active block className="hero-section__input-skeleton" />
|
||||
<Skeleton.Input
|
||||
active
|
||||
size="large"
|
||||
block
|
||||
className="hero-section__input-skeleton"
|
||||
/>
|
||||
<div className="hero-section__action-buttons">
|
||||
<Skeleton.Button
|
||||
active
|
||||
size="large"
|
||||
className="hero-section__new-account-button-skeleton"
|
||||
/>
|
||||
<Skeleton.Button
|
||||
active
|
||||
size="large"
|
||||
className="hero-section__account-settings-button-skeleton"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -89,12 +110,16 @@ function AccountActionsRenderer({
|
||||
onChange={onAccountChange}
|
||||
/>
|
||||
<div className="hero-section__action-buttons">
|
||||
<Button variant="solid" color="primary" onClick={onIntegrationModalOpen}>
|
||||
<Button
|
||||
type="primary"
|
||||
className="hero-section__action-button primary"
|
||||
onClick={onIntegrationModalOpen}
|
||||
>
|
||||
Add New AWS Account
|
||||
</Button>
|
||||
<Button
|
||||
variant="solid"
|
||||
color="primary"
|
||||
type="default"
|
||||
className="hero-section__action-button secondary"
|
||||
onClick={onAccountSettingsModalOpen}
|
||||
>
|
||||
Account Settings
|
||||
@@ -104,7 +129,10 @@ function AccountActionsRenderer({
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Button variant="solid" color="primary" onClick={onIntegrationModalOpen}>
|
||||
<Button
|
||||
className="hero-section__action-button primary"
|
||||
onClick={onIntegrationModalOpen}
|
||||
>
|
||||
Integrate Now
|
||||
</Button>
|
||||
);
|
||||
@@ -10,8 +10,8 @@ import {
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
import history from 'lib/history';
|
||||
|
||||
import logEvent from '../../../../../../api/common/logEvent';
|
||||
import { CloudAccount } from '../../types';
|
||||
import logEvent from '../../../../api/common/logEvent';
|
||||
import { CloudAccount } from '../../ServicesSection/types';
|
||||
import { RegionSelector } from './RegionSelector';
|
||||
import RemoveIntegrationAccount from './RemoveIntegrationAccount';
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import { useRef } from 'react';
|
||||
import { Form } from 'antd';
|
||||
import cx from 'classnames';
|
||||
import { INTEGRATION_TYPES } from 'container/Integrations/constants';
|
||||
import { useGetAccountStatus } from 'hooks/integration/useGetAccountStatus';
|
||||
import { AccountStatusResponse } from 'types/api/integrations/types';
|
||||
import { useAccountStatus } from 'hooks/integration/aws/useAccountStatus';
|
||||
import { AccountStatusResponse } from 'types/api/integrations/aws';
|
||||
import { regions } from 'utils/regions';
|
||||
|
||||
import logEvent from '../../../../../../api/common/logEvent';
|
||||
import logEvent from '../../../../api/common/logEvent';
|
||||
import { ModalStateEnum, RegionFormProps } from '../types';
|
||||
import AlertMessage from './AlertMessage';
|
||||
import {
|
||||
@@ -45,31 +44,27 @@ export function RegionForm({
|
||||
const refetchInterval = 10 * 1000;
|
||||
const errorTimeout = 10 * 60 * 1000;
|
||||
|
||||
const { isLoading: isAccountStatusLoading } = useGetAccountStatus(
|
||||
INTEGRATION_TYPES.AWS,
|
||||
accountId,
|
||||
{
|
||||
refetchInterval,
|
||||
enabled: !!accountId && modalState === ModalStateEnum.WAITING,
|
||||
onSuccess: (data: AccountStatusResponse) => {
|
||||
if (data.data.status.integration.last_heartbeat_ts_ms !== null) {
|
||||
setModalState(ModalStateEnum.SUCCESS);
|
||||
logEvent('AWS Integration: Account connected', {
|
||||
cloudAccountId: data?.data?.cloud_account_id,
|
||||
status: data?.data?.status,
|
||||
});
|
||||
} else if (Date.now() - startTimeRef.current >= errorTimeout) {
|
||||
setModalState(ModalStateEnum.ERROR);
|
||||
logEvent('AWS Integration: Account connection attempt timed out', {
|
||||
id: accountId,
|
||||
});
|
||||
}
|
||||
},
|
||||
onError: () => {
|
||||
const { isLoading: isAccountStatusLoading } = useAccountStatus(accountId, {
|
||||
refetchInterval,
|
||||
enabled: !!accountId && modalState === ModalStateEnum.WAITING,
|
||||
onSuccess: (data: AccountStatusResponse) => {
|
||||
if (data.data.status.integration.last_heartbeat_ts_ms !== null) {
|
||||
setModalState(ModalStateEnum.SUCCESS);
|
||||
logEvent('AWS Integration: Account connected', {
|
||||
cloudAccountId: data?.data?.cloud_account_id,
|
||||
status: data?.data?.status,
|
||||
});
|
||||
} else if (Date.now() - startTimeRef.current >= errorTimeout) {
|
||||
setModalState(ModalStateEnum.ERROR);
|
||||
},
|
||||
logEvent('AWS Integration: Account connection attempt timed out', {
|
||||
id: accountId,
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
onError: () => {
|
||||
setModalState(ModalStateEnum.ERROR);
|
||||
},
|
||||
});
|
||||
|
||||
const isFormDisabled =
|
||||
modalState === ModalStateEnum.WAITING || isAccountStatusLoading;
|
||||
@@ -2,11 +2,11 @@ import { useState } from 'react';
|
||||
import { useMutation } from 'react-query';
|
||||
import { Button, Modal } from 'antd';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import removeAwsIntegrationAccount from 'api/integration/aws/removeAwsIntegrationAccount';
|
||||
import removeAwsIntegrationAccount from 'api/Integrations/removeAwsIntegrationAccount';
|
||||
import { SOMETHING_WENT_WRONG } from 'constants/api';
|
||||
import { INTEGRATION_TELEMETRY_EVENTS } from 'container/Integrations/constants';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import { X } from 'lucide-react';
|
||||
import { INTEGRATION_TELEMETRY_EVENTS } from 'pages/Integrations/utils';
|
||||
|
||||
import './RemoveIntegrationAccount.scss';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Form, Input } from 'antd';
|
||||
import { ConnectionParams } from 'types/api/integrations/types';
|
||||
import { ConnectionParams } from 'types/api/integrations/aws';
|
||||
|
||||
function RenderConnectionFields({
|
||||
isConnectionParamsLoading,
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Dispatch, SetStateAction } from 'react';
|
||||
import { FormInstance } from 'antd';
|
||||
import { ConnectionParams } from 'types/api/integrations/types';
|
||||
import { ConnectionParams } from 'types/api/integrations/aws';
|
||||
|
||||
export enum ActiveViewEnum {
|
||||
SELECT_REGIONS = 'select-regions',
|
||||
@@ -1,8 +1,6 @@
|
||||
import { Table } from 'antd';
|
||||
import { ServiceData } from 'container/Integrations/CloudIntegration/AmazonWebServices/types';
|
||||
import { BarChart2, ScrollText } from 'lucide-react';
|
||||
|
||||
import './CloudServiceDataCollected.styles.scss';
|
||||
import { ServiceData } from './types';
|
||||
|
||||
function CloudServiceDataCollected({
|
||||
logsData,
|
||||
@@ -63,32 +61,26 @@ function CloudServiceDataCollected({
|
||||
return (
|
||||
<div className="cloud-service-data-collected">
|
||||
{logsData && logsData.length > 0 && (
|
||||
<div className="cloud-service-data-collected-table">
|
||||
<div className="cloud-service-data-collected-table-heading">
|
||||
<ScrollText size={14} />
|
||||
Logs
|
||||
</div>
|
||||
<div className="cloud-service-data-collected__table">
|
||||
<div className="cloud-service-data-collected__table-heading">Logs</div>
|
||||
<Table
|
||||
columns={logsColumns}
|
||||
dataSource={logsData}
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
{...tableProps}
|
||||
className="cloud-service-data-collected-table-logs"
|
||||
className="cloud-service-data-collected__table-logs"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{metricsData && metricsData.length > 0 && (
|
||||
<div className="cloud-service-data-collected-table">
|
||||
<div className="cloud-service-data-collected-table-heading">
|
||||
<BarChart2 size={14} />
|
||||
Metrics
|
||||
</div>
|
||||
<div className="cloud-service-data-collected__table">
|
||||
<div className="cloud-service-data-collected__table-heading">Metrics</div>
|
||||
<Table
|
||||
columns={metricsColumns}
|
||||
dataSource={metricsData}
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
{...tableProps}
|
||||
className="cloud-service-data-collected-table-metrics"
|
||||
className="cloud-service-data-collected__table-metrics"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
@@ -3,12 +3,14 @@ import { useQueryClient } from 'react-query';
|
||||
import { Form, Switch } from 'antd';
|
||||
import SignozModal from 'components/SignozModal/SignozModal';
|
||||
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||
import { AWSServiceConfig } from 'container/Integrations/CloudIntegration/AmazonWebServices/types';
|
||||
import { SupportedSignals } from 'container/Integrations/types';
|
||||
import {
|
||||
ServiceConfig,
|
||||
SupportedSignals,
|
||||
} from 'container/CloudIntegrationPage/ServicesSection/types';
|
||||
import { useUpdateServiceConfig } from 'hooks/integration/aws/useUpdateServiceConfig';
|
||||
import { isEqual } from 'lodash-es';
|
||||
|
||||
import logEvent from '../../../../api/common/logEvent';
|
||||
import logEvent from '../../../api/common/logEvent';
|
||||
import S3BucketsSelector from './S3BucketsSelector';
|
||||
|
||||
import './ConfigureServiceModal.styles.scss';
|
||||
@@ -20,7 +22,7 @@ export interface IConfigureServiceModalProps {
|
||||
serviceId: string;
|
||||
cloudAccountId: string;
|
||||
supportedSignals: SupportedSignals;
|
||||
initialConfig?: AWSServiceConfig;
|
||||
initialConfig?: ServiceConfig;
|
||||
}
|
||||
|
||||
function ConfigureServiceModal({
|
||||
@@ -1,16 +1,15 @@
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { Button, Tabs, TabsProps } from 'antd';
|
||||
import CloudServiceDataCollected from 'components/CloudIntegrations/CloudServiceDataCollected/CloudServiceDataCollected';
|
||||
import { MarkdownRenderer } from 'components/MarkdownRenderer/MarkdownRenderer';
|
||||
import Spinner from 'components/Spinner';
|
||||
import CloudServiceDashboards from 'container/Integrations/CloudIntegration/AmazonWebServices/CloudServiceDashboards';
|
||||
import { AWSServiceConfig } from 'container/Integrations/CloudIntegration/AmazonWebServices/types';
|
||||
import { IServiceStatus } from 'container/Integrations/types';
|
||||
import CloudServiceDashboards from 'container/CloudIntegrationPage/ServicesSection/CloudServiceDashboards';
|
||||
import CloudServiceDataCollected from 'container/CloudIntegrationPage/ServicesSection/CloudServiceDataCollected';
|
||||
import { IServiceStatus } from 'container/CloudIntegrationPage/ServicesSection/types';
|
||||
import dayjs from 'dayjs';
|
||||
import { useServiceDetails } from 'hooks/integration/aws/useServiceDetails';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
|
||||
import logEvent from '../../../../api/common/logEvent';
|
||||
import logEvent from '../../../api/common/logEvent';
|
||||
import ConfigureServiceModal from './ConfigureServiceModal';
|
||||
|
||||
const getStatus = (
|
||||
@@ -111,10 +110,9 @@ function ServiceDetails(): JSX.Element | null {
|
||||
[config],
|
||||
);
|
||||
|
||||
const awsConfig = config as AWSServiceConfig | undefined;
|
||||
const isAnySignalConfigured = useMemo(
|
||||
() => !!awsConfig?.logs?.enabled || !!awsConfig?.metrics?.enabled,
|
||||
[awsConfig],
|
||||
() => !!config?.logs?.enabled || !!config?.metrics?.enabled,
|
||||
[config],
|
||||
);
|
||||
|
||||
// log telemetry event on visiting details of a service.
|
||||
@@ -181,7 +179,7 @@ function ServiceDetails(): JSX.Element | null {
|
||||
serviceName={serviceDetailsData.title}
|
||||
serviceId={serviceId || ''}
|
||||
cloudAccountId={cloudAccountId || ''}
|
||||
initialConfig={awsConfig}
|
||||
initialConfig={serviceDetailsData.config}
|
||||
supportedSignals={serviceDetailsData.supported_signals || {}}
|
||||
/>
|
||||
)}
|
||||
@@ -20,19 +20,19 @@
|
||||
}
|
||||
.services-section {
|
||||
display: flex;
|
||||
|
||||
gap: 10px;
|
||||
&__sidebar {
|
||||
width: 240px;
|
||||
border-right: 1px solid var(--bg-slate-400);
|
||||
width: 16%;
|
||||
padding: 0 16px;
|
||||
}
|
||||
|
||||
&__content {
|
||||
flex: 1;
|
||||
width: 84%;
|
||||
padding: 16px;
|
||||
}
|
||||
}
|
||||
.services-filter {
|
||||
padding: 12px;
|
||||
|
||||
padding: 16px 0;
|
||||
.ant-select-selector {
|
||||
background-color: var(--bg-ink-300) !important;
|
||||
border: 1px solid var(--bg-slate-400) !important;
|
||||
@@ -63,19 +63,17 @@
|
||||
background-color: var(--bg-ink-100);
|
||||
}
|
||||
&__icon-wrapper {
|
||||
height: 32px;
|
||||
width: 32px;
|
||||
height: 40px;
|
||||
width: 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: var(--bg-ink-300);
|
||||
border: 1px solid var(--bg-slate-400);
|
||||
border-radius: 4px;
|
||||
|
||||
.service-item__icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
object-fit: contain;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
}
|
||||
&__title {
|
||||
@@ -92,13 +90,11 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
|
||||
&__title-bar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
height: 48px;
|
||||
padding: 8px 12px;
|
||||
border-bottom: 1px solid var(--bg-slate-400);
|
||||
|
||||
.service-details__details-title {
|
||||
@@ -109,7 +105,6 @@
|
||||
letter-spacing: -0.07px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.service-details__right-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -162,28 +157,21 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__overview {
|
||||
color: var(--bg-vanilla-400);
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
line-height: 20px; /* 142.857% */
|
||||
letter-spacing: -0.07px;
|
||||
width: 100%;
|
||||
|
||||
padding: 8px 12px;
|
||||
width: 800px;
|
||||
}
|
||||
|
||||
&__tabs {
|
||||
padding: 0px 12px 12px 8px;
|
||||
|
||||
.ant-tabs {
|
||||
&-ink-bar {
|
||||
background-color: transparent;
|
||||
}
|
||||
&-nav {
|
||||
padding: 0;
|
||||
|
||||
padding: 8px 0 18px;
|
||||
&-wrap {
|
||||
padding: 0;
|
||||
}
|
||||
@@ -1,14 +1,13 @@
|
||||
import { useMemo, useState } from 'react';
|
||||
import { useQuery } from 'react-query';
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import type { SelectProps } from 'antd';
|
||||
import { Select } from 'antd';
|
||||
import type { SelectProps, TabsProps } from 'antd';
|
||||
import { Select, Tabs } from 'antd';
|
||||
import { getAwsServices } from 'api/integration/aws';
|
||||
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
import { ChevronDown } from 'lucide-react';
|
||||
|
||||
import HeroSection from './HeroSection/HeroSection';
|
||||
import ServiceDetails from './ServiceDetails';
|
||||
import ServicesList from './ServicesList';
|
||||
|
||||
@@ -107,10 +106,17 @@ function ServicesSection(): JSX.Element {
|
||||
}
|
||||
|
||||
function ServicesTabs(): JSX.Element {
|
||||
const tabItems: TabsProps['items'] = [
|
||||
{
|
||||
key: 'services',
|
||||
label: 'Services For Integration',
|
||||
children: <ServicesSection />,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="services-tabs">
|
||||
<HeroSection />
|
||||
<ServicesSection />
|
||||
<Tabs defaultActiveKey="services" items={tabItems} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,40 +1,90 @@
|
||||
import { ServiceData } from 'container/Integrations/types';
|
||||
|
||||
interface Service {
|
||||
id: string;
|
||||
title: string;
|
||||
icon: string;
|
||||
config: AWSServiceConfig;
|
||||
config: ServiceConfig;
|
||||
}
|
||||
|
||||
interface S3BucketsByRegion {
|
||||
[region: string]: string[];
|
||||
interface Dashboard {
|
||||
id: string;
|
||||
url: string;
|
||||
title: string;
|
||||
description: string;
|
||||
image: string;
|
||||
}
|
||||
|
||||
interface LogField {
|
||||
name: string;
|
||||
path: string;
|
||||
type: string;
|
||||
}
|
||||
|
||||
interface Metric {
|
||||
name: string;
|
||||
type: string;
|
||||
unit: string;
|
||||
}
|
||||
|
||||
interface ConfigStatus {
|
||||
enabled: boolean;
|
||||
}
|
||||
|
||||
interface DataStatus {
|
||||
last_received_ts_ms: number;
|
||||
last_received_from: string;
|
||||
}
|
||||
|
||||
interface S3BucketsByRegion {
|
||||
[region: string]: string[];
|
||||
}
|
||||
|
||||
interface LogsConfig extends ConfigStatus {
|
||||
s3_buckets?: S3BucketsByRegion;
|
||||
}
|
||||
|
||||
interface AWSServiceConfig {
|
||||
interface ServiceConfig {
|
||||
logs: LogsConfig;
|
||||
metrics: ConfigStatus;
|
||||
s3_sync?: LogsConfig;
|
||||
}
|
||||
|
||||
interface IServiceStatus {
|
||||
logs: DataStatus | null;
|
||||
metrics: DataStatus | null;
|
||||
}
|
||||
|
||||
interface SupportedSignals {
|
||||
metrics: boolean;
|
||||
logs: boolean;
|
||||
}
|
||||
|
||||
interface ServiceData {
|
||||
id: string;
|
||||
title: string;
|
||||
icon: string;
|
||||
overview: string;
|
||||
supported_signals: SupportedSignals;
|
||||
assets: {
|
||||
dashboards: Dashboard[];
|
||||
};
|
||||
data_collected: {
|
||||
logs?: LogField[];
|
||||
metrics: Metric[];
|
||||
};
|
||||
config?: ServiceConfig;
|
||||
status?: IServiceStatus;
|
||||
}
|
||||
|
||||
interface ServiceDetailsResponse {
|
||||
status: 'success';
|
||||
data: ServiceData;
|
||||
}
|
||||
|
||||
export interface AWSCloudAccountConfig {
|
||||
interface CloudAccountConfig {
|
||||
regions: string[];
|
||||
}
|
||||
|
||||
export interface IntegrationStatus {
|
||||
interface IntegrationStatus {
|
||||
last_heartbeat_ts_ms: number;
|
||||
}
|
||||
|
||||
@@ -45,7 +95,7 @@ interface AccountStatus {
|
||||
interface CloudAccount {
|
||||
id: string;
|
||||
cloud_account_id: string;
|
||||
config: AWSCloudAccountConfig;
|
||||
config: CloudAccountConfig;
|
||||
status: AccountStatus;
|
||||
}
|
||||
|
||||
@@ -83,13 +133,15 @@ interface UpdateServiceConfigResponse {
|
||||
}
|
||||
|
||||
export type {
|
||||
AWSServiceConfig,
|
||||
CloudAccount,
|
||||
CloudAccountsData,
|
||||
IServiceStatus,
|
||||
S3BucketsByRegion,
|
||||
Service,
|
||||
ServiceConfig,
|
||||
ServiceData,
|
||||
ServiceDetailsResponse,
|
||||
SupportedSignals,
|
||||
UpdateServiceConfigPayload,
|
||||
UpdateServiceConfigResponse,
|
||||
};
|
||||
@@ -1,12 +1,14 @@
|
||||
import { I18nextProvider } from 'react-i18next';
|
||||
import { act, fireEvent, render, screen } from '@testing-library/react';
|
||||
import { RequestIntegrationBtn } from 'container/Integrations/RequestIntegrationBtn';
|
||||
import { IntegrationType } from 'container/Integrations/types';
|
||||
import { server } from 'mocks-server/server';
|
||||
import { rest } from 'msw';
|
||||
import {
|
||||
IntegrationType,
|
||||
RequestIntegrationBtn,
|
||||
} from 'pages/Integrations/RequestIntegrationBtn';
|
||||
import i18n from 'ReactI18';
|
||||
|
||||
describe.skip('Request AWS integration', () => {
|
||||
describe('Request AWS integration', () => {
|
||||
it('should render the request integration button', async () => {
|
||||
let capturedPayload: any;
|
||||
server.use(
|
||||
@@ -49,7 +49,7 @@ function evaluateCondition(
|
||||
* @param columnUnit - The current unit of the value.
|
||||
* @returns A boolean indicating whether the value meets the threshold condition.
|
||||
*/
|
||||
function evaluateThresholdWithConvertedValue(
|
||||
export function evaluateThresholdWithConvertedValue(
|
||||
value: number,
|
||||
thresholdValue: number,
|
||||
thresholdOperator?: string,
|
||||
|
||||
@@ -99,6 +99,7 @@ function GridValueComponent({
|
||||
<ValueGraph
|
||||
thresholds={thresholds || []}
|
||||
rawValue={value}
|
||||
yAxisUnit={yAxisUnit}
|
||||
value={
|
||||
yAxisUnit
|
||||
? getYAxisFormattedValue(
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import { LoadingOutlined } from '@ant-design/icons';
|
||||
import {
|
||||
Skeleton,
|
||||
@@ -14,12 +14,93 @@ import { InfraMonitoringEvents } from 'constants/events';
|
||||
|
||||
import HostsEmptyOrIncorrectMetrics from './HostsEmptyOrIncorrectMetrics';
|
||||
import {
|
||||
EmptyOrLoadingViewProps,
|
||||
formatDataForTable,
|
||||
getHostsListColumns,
|
||||
HostRowData,
|
||||
HostsListTableProps,
|
||||
} from './utils';
|
||||
|
||||
function EmptyOrLoadingView(
|
||||
viewState: EmptyOrLoadingViewProps,
|
||||
): React.ReactNode {
|
||||
const { isError, errorMessage } = viewState;
|
||||
if (isError) {
|
||||
return <Typography>{errorMessage || 'Something went wrong'}</Typography>;
|
||||
}
|
||||
if (viewState.showHostsEmptyState) {
|
||||
return (
|
||||
<HostsEmptyOrIncorrectMetrics
|
||||
noData={!viewState.sentAnyHostMetricsData}
|
||||
incorrectData={viewState.isSendingIncorrectK8SAgentMetrics}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (viewState.showEndTimeBeforeRetentionMessage) {
|
||||
return (
|
||||
<div className="hosts-empty-state-container">
|
||||
<div className="hosts-empty-state-container-content">
|
||||
<img className="eyes-emoji" src="/Images/eyesEmoji.svg" alt="eyes emoji" />
|
||||
<div className="no-hosts-message">
|
||||
<Typography.Title level={5} className="no-hosts-message-title">
|
||||
Queried time range is before earliest host metrics
|
||||
</Typography.Title>
|
||||
<Typography.Text className="no-hosts-message-text">
|
||||
Your requested end time is earlier than the earliest detected time of
|
||||
host metrics data, please adjust your end time.
|
||||
</Typography.Text>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (viewState.showNoRecordsInSelectedTimeRangeMessage) {
|
||||
return (
|
||||
<div className="no-filtered-hosts-message-container">
|
||||
<div className="no-filtered-hosts-message-content">
|
||||
<img
|
||||
src="/Icons/emptyState.svg"
|
||||
alt="thinking-emoji"
|
||||
className="empty-state-svg"
|
||||
/>
|
||||
<Typography.Title level={5} className="no-filtered-hosts-title">
|
||||
No host metrics found
|
||||
</Typography.Title>
|
||||
<Typography.Text className="no-filtered-hosts-message">
|
||||
No host metrics in the selected time range and filters. Please adjust your
|
||||
time range or filters.
|
||||
</Typography.Text>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (viewState.showTableLoadingState) {
|
||||
return (
|
||||
<div className="hosts-list-loading-state">
|
||||
<Skeleton.Input
|
||||
className="hosts-list-loading-state-item"
|
||||
size="large"
|
||||
block
|
||||
active
|
||||
/>
|
||||
<Skeleton.Input
|
||||
className="hosts-list-loading-state-item"
|
||||
size="large"
|
||||
block
|
||||
active
|
||||
/>
|
||||
<Skeleton.Input
|
||||
className="hosts-list-loading-state-item"
|
||||
size="large"
|
||||
block
|
||||
active
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export default function HostsListTable({
|
||||
isLoading,
|
||||
isFetching,
|
||||
@@ -46,6 +127,11 @@ export default function HostsListTable({
|
||||
[data],
|
||||
);
|
||||
|
||||
const endTimeBeforeRetention = useMemo(
|
||||
() => data?.payload?.data?.endTimeBeforeRetention || false,
|
||||
[data],
|
||||
);
|
||||
|
||||
const formattedHostMetricsData = useMemo(
|
||||
() => formatDataForTable(hostMetricsData),
|
||||
[hostMetricsData],
|
||||
@@ -84,12 +170,6 @@ export default function HostsListTable({
|
||||
});
|
||||
};
|
||||
|
||||
const showNoFilteredHostsMessage =
|
||||
!isFetching &&
|
||||
!isLoading &&
|
||||
formattedHostMetricsData.length === 0 &&
|
||||
filters.items.length > 0;
|
||||
|
||||
const showHostsEmptyState =
|
||||
!isFetching &&
|
||||
!isLoading &&
|
||||
@@ -97,63 +177,36 @@ export default function HostsListTable({
|
||||
(!sentAnyHostMetricsData || isSendingIncorrectK8SAgentMetrics) &&
|
||||
!filters.items.length;
|
||||
|
||||
const showEndTimeBeforeRetentionMessage =
|
||||
!isFetching &&
|
||||
!isLoading &&
|
||||
formattedHostMetricsData.length === 0 &&
|
||||
endTimeBeforeRetention &&
|
||||
!filters.items.length;
|
||||
|
||||
const showNoRecordsInSelectedTimeRangeMessage =
|
||||
!isFetching &&
|
||||
!isLoading &&
|
||||
formattedHostMetricsData.length === 0 &&
|
||||
!showEndTimeBeforeRetentionMessage &&
|
||||
!showHostsEmptyState;
|
||||
|
||||
const showTableLoadingState =
|
||||
(isLoading || isFetching) && formattedHostMetricsData.length === 0;
|
||||
|
||||
if (isError) {
|
||||
return <Typography>{data?.error || 'Something went wrong'}</Typography>;
|
||||
}
|
||||
const emptyOrLoadingView = EmptyOrLoadingView({
|
||||
isError,
|
||||
errorMessage: data?.error ?? '',
|
||||
showHostsEmptyState,
|
||||
sentAnyHostMetricsData,
|
||||
isSendingIncorrectK8SAgentMetrics,
|
||||
showEndTimeBeforeRetentionMessage,
|
||||
showNoRecordsInSelectedTimeRangeMessage,
|
||||
showTableLoadingState,
|
||||
});
|
||||
|
||||
if (showHostsEmptyState) {
|
||||
return (
|
||||
<HostsEmptyOrIncorrectMetrics
|
||||
noData={!sentAnyHostMetricsData}
|
||||
incorrectData={isSendingIncorrectK8SAgentMetrics}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (showNoFilteredHostsMessage) {
|
||||
return (
|
||||
<div className="no-filtered-hosts-message-container">
|
||||
<div className="no-filtered-hosts-message-content">
|
||||
<img
|
||||
src="/Icons/emptyState.svg"
|
||||
alt="thinking-emoji"
|
||||
className="empty-state-svg"
|
||||
/>
|
||||
|
||||
<Typography.Text className="no-filtered-hosts-message">
|
||||
This query had no results. Edit your query and try again!
|
||||
</Typography.Text>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (showTableLoadingState) {
|
||||
return (
|
||||
<div className="hosts-list-loading-state">
|
||||
<Skeleton.Input
|
||||
className="hosts-list-loading-state-item"
|
||||
size="large"
|
||||
block
|
||||
active
|
||||
/>
|
||||
<Skeleton.Input
|
||||
className="hosts-list-loading-state-item"
|
||||
size="large"
|
||||
block
|
||||
active
|
||||
/>
|
||||
<Skeleton.Input
|
||||
className="hosts-list-loading-state-item"
|
||||
size="large"
|
||||
block
|
||||
active
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
if (emptyOrLoadingView) {
|
||||
return <>{emptyOrLoadingView}</>;
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
/* eslint-disable react/jsx-props-no-spreading */
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { HostData, HostListResponse } from 'api/infraMonitoring/getHostLists';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
|
||||
import HostsListTable from '../HostsListTable';
|
||||
import { HostsListTableProps } from '../utils';
|
||||
|
||||
const EMPTY_STATE_CONTAINER_CLASS = '.hosts-empty-state-container';
|
||||
|
||||
describe('HostsListTable', () => {
|
||||
const mockHost = {
|
||||
const createMockHost = (): HostData =>
|
||||
({
|
||||
hostName: 'test-host-1',
|
||||
active: true,
|
||||
cpu: 0.75,
|
||||
@@ -14,20 +18,46 @@ describe('HostsListTable', () => {
|
||||
wait: 0.03,
|
||||
load15: 1.5,
|
||||
os: 'linux',
|
||||
};
|
||||
cpuTimeSeries: { labels: {}, labelsArray: [], values: [] },
|
||||
memoryTimeSeries: { labels: {}, labelsArray: [], values: [] },
|
||||
waitTimeSeries: { labels: {}, labelsArray: [], values: [] },
|
||||
load15TimeSeries: { labels: {}, labelsArray: [], values: [] },
|
||||
} as HostData);
|
||||
|
||||
const mockTableData = {
|
||||
const createMockTableData = (
|
||||
overrides: Partial<HostListResponse['data']> = {},
|
||||
): SuccessResponse<HostListResponse> => {
|
||||
const mockHost = createMockHost();
|
||||
return {
|
||||
statusCode: 200,
|
||||
message: 'Success',
|
||||
error: null,
|
||||
payload: {
|
||||
status: 'success',
|
||||
data: {
|
||||
hosts: [mockHost],
|
||||
type: 'list',
|
||||
records: [mockHost],
|
||||
groups: null,
|
||||
total: 1,
|
||||
sentAnyHostMetricsData: true,
|
||||
isSendingK8SAgentMetrics: false,
|
||||
endTimeBeforeRetention: false,
|
||||
...overrides,
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
describe('HostsListTable', () => {
|
||||
const mockHost = createMockHost();
|
||||
const mockTableData = createMockTableData();
|
||||
|
||||
const mockOnHostClick = jest.fn();
|
||||
const mockSetCurrentPage = jest.fn();
|
||||
const mockSetOrderBy = jest.fn();
|
||||
const mockSetPageSize = jest.fn();
|
||||
const mockProps = {
|
||||
|
||||
const mockProps: HostsListTableProps = {
|
||||
isLoading: false,
|
||||
isError: false,
|
||||
isFetching: false,
|
||||
@@ -43,7 +73,7 @@ describe('HostsListTable', () => {
|
||||
pageSize: 10,
|
||||
setOrderBy: mockSetOrderBy,
|
||||
setPageSize: mockSetPageSize,
|
||||
} as any;
|
||||
};
|
||||
|
||||
it('renders loading state if isLoading is true and tableData is empty', () => {
|
||||
const { container } = render(
|
||||
@@ -51,7 +81,7 @@ describe('HostsListTable', () => {
|
||||
{...mockProps}
|
||||
isLoading
|
||||
hostMetricsData={[]}
|
||||
tableData={{ payload: { data: { hosts: [] } } }}
|
||||
tableData={createMockTableData({ records: [] })}
|
||||
/>,
|
||||
);
|
||||
expect(container.querySelector('.hosts-list-loading-state')).toBeTruthy();
|
||||
@@ -63,7 +93,7 @@ describe('HostsListTable', () => {
|
||||
{...mockProps}
|
||||
isFetching
|
||||
hostMetricsData={[]}
|
||||
tableData={{ payload: { data: { hosts: [] } } }}
|
||||
tableData={createMockTableData({ records: [] })}
|
||||
/>,
|
||||
);
|
||||
expect(container.querySelector('.hosts-list-loading-state')).toBeTruthy();
|
||||
@@ -74,19 +104,56 @@ describe('HostsListTable', () => {
|
||||
expect(screen.getByText('Something went wrong')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('renders "Something went wrong" fallback when isError is true and error message is empty', () => {
|
||||
const tableDataWithEmptyError: ErrorResponse = {
|
||||
statusCode: 500,
|
||||
payload: null,
|
||||
error: '',
|
||||
message: null,
|
||||
};
|
||||
render(
|
||||
<HostsListTable
|
||||
{...mockProps}
|
||||
isError
|
||||
hostMetricsData={[]}
|
||||
tableData={tableDataWithEmptyError}
|
||||
/>,
|
||||
);
|
||||
expect(screen.getByText('Something went wrong')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders custom error message when isError is true and error message is provided', () => {
|
||||
const customErrorMessage = 'Failed to fetch host metrics';
|
||||
const tableDataWithError: ErrorResponse = {
|
||||
statusCode: 500,
|
||||
payload: null,
|
||||
error: customErrorMessage,
|
||||
message: null,
|
||||
};
|
||||
render(
|
||||
<HostsListTable
|
||||
{...mockProps}
|
||||
isError
|
||||
hostMetricsData={[]}
|
||||
tableData={tableDataWithError}
|
||||
/>,
|
||||
);
|
||||
expect(screen.getByText(customErrorMessage)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders empty state if no hosts are found', () => {
|
||||
const { container } = render(
|
||||
<HostsListTable
|
||||
{...mockProps}
|
||||
hostMetricsData={[]}
|
||||
tableData={{
|
||||
payload: {
|
||||
data: { hosts: [] },
|
||||
},
|
||||
}}
|
||||
tableData={createMockTableData({
|
||||
records: [],
|
||||
})}
|
||||
/>,
|
||||
);
|
||||
expect(container.querySelector(EMPTY_STATE_CONTAINER_CLASS)).toBeTruthy();
|
||||
expect(
|
||||
container.querySelector('.no-filtered-hosts-message-container'),
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
||||
it('renders empty state if sentAnyHostMetricsData is false', () => {
|
||||
@@ -94,58 +161,114 @@ describe('HostsListTable', () => {
|
||||
<HostsListTable
|
||||
{...mockProps}
|
||||
hostMetricsData={[]}
|
||||
tableData={{
|
||||
...mockTableData,
|
||||
payload: {
|
||||
...mockTableData.payload,
|
||||
data: {
|
||||
...mockTableData.payload.data,
|
||||
sentAnyHostMetricsData: false,
|
||||
hosts: [],
|
||||
},
|
||||
},
|
||||
}}
|
||||
tableData={createMockTableData({
|
||||
sentAnyHostMetricsData: false,
|
||||
records: [],
|
||||
})}
|
||||
/>,
|
||||
);
|
||||
expect(container.querySelector(EMPTY_STATE_CONTAINER_CLASS)).toBeTruthy();
|
||||
});
|
||||
|
||||
it('renders empty state if isSendingIncorrectK8SAgentMetrics is true', () => {
|
||||
it('renders empty state if isSendingK8SAgentMetrics is true', () => {
|
||||
const { container } = render(
|
||||
<HostsListTable
|
||||
{...mockProps}
|
||||
hostMetricsData={[]}
|
||||
tableData={{
|
||||
...mockTableData,
|
||||
payload: {
|
||||
...mockTableData.payload,
|
||||
data: {
|
||||
...mockTableData.payload.data,
|
||||
isSendingIncorrectK8SAgentMetrics: true,
|
||||
hosts: [],
|
||||
},
|
||||
},
|
||||
}}
|
||||
tableData={createMockTableData({
|
||||
isSendingK8SAgentMetrics: true,
|
||||
records: [],
|
||||
})}
|
||||
/>,
|
||||
);
|
||||
expect(container.querySelector(EMPTY_STATE_CONTAINER_CLASS)).toBeTruthy();
|
||||
});
|
||||
|
||||
it('renders end time before retention message when endTimeBeforeRetention is true', () => {
|
||||
const { container } = render(
|
||||
<HostsListTable
|
||||
{...mockProps}
|
||||
hostMetricsData={[]}
|
||||
tableData={createMockTableData({
|
||||
sentAnyHostMetricsData: true,
|
||||
isSendingK8SAgentMetrics: false,
|
||||
endTimeBeforeRetention: true,
|
||||
records: [],
|
||||
})}
|
||||
/>,
|
||||
);
|
||||
expect(container.querySelector(EMPTY_STATE_CONTAINER_CLASS)).toBeTruthy();
|
||||
expect(
|
||||
screen.getByText(
|
||||
/Your requested end time is earlier than the earliest detected time of host metrics data, please adjust your end time\./,
|
||||
),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders no records message when noRecordsInSelectedTimeRangeAndFilters is true', () => {
|
||||
const { container } = render(
|
||||
<HostsListTable
|
||||
{...mockProps}
|
||||
hostMetricsData={[]}
|
||||
tableData={createMockTableData({
|
||||
sentAnyHostMetricsData: true,
|
||||
isSendingK8SAgentMetrics: false,
|
||||
records: [],
|
||||
})}
|
||||
/>,
|
||||
);
|
||||
expect(
|
||||
container.querySelector('.no-filtered-hosts-message-container'),
|
||||
).toBeTruthy();
|
||||
expect(
|
||||
screen.getByText(/No host metrics in the selected time range and filters/),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders no filtered hosts message when filters are present and no hosts are found', () => {
|
||||
const { container } = render(
|
||||
<HostsListTable
|
||||
{...mockProps}
|
||||
hostMetricsData={[]}
|
||||
filters={{
|
||||
items: [
|
||||
{
|
||||
id: 'host_name',
|
||||
key: {
|
||||
key: 'host_name',
|
||||
dataType: DataTypes.String,
|
||||
type: 'tag',
|
||||
isIndexed: true,
|
||||
},
|
||||
op: '=',
|
||||
value: 'unknown',
|
||||
},
|
||||
],
|
||||
op: 'AND',
|
||||
}}
|
||||
tableData={createMockTableData({
|
||||
sentAnyHostMetricsData: true,
|
||||
isSendingK8SAgentMetrics: false,
|
||||
records: [],
|
||||
})}
|
||||
/>,
|
||||
);
|
||||
expect(container.querySelector('.no-filtered-hosts-message')).toBeTruthy();
|
||||
expect(
|
||||
screen.getByText(
|
||||
/No host metrics in the selected time range and filters\. Please adjust your time range or filters\./,
|
||||
),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders table data', () => {
|
||||
const { container } = render(
|
||||
<HostsListTable
|
||||
{...mockProps}
|
||||
tableData={{
|
||||
...mockTableData,
|
||||
payload: {
|
||||
...mockTableData.payload,
|
||||
data: {
|
||||
...mockTableData.payload.data,
|
||||
isSendingIncorrectK8SAgentMetrics: false,
|
||||
sentAnyHostMetricsData: true,
|
||||
},
|
||||
},
|
||||
}}
|
||||
tableData={createMockTableData({
|
||||
isSendingK8SAgentMetrics: false,
|
||||
sentAnyHostMetricsData: true,
|
||||
})}
|
||||
/>,
|
||||
);
|
||||
expect(container.querySelector('.hosts-list-table')).toBeTruthy();
|
||||
|
||||
@@ -107,6 +107,17 @@ export interface HostsListTableProps {
|
||||
setPageSize: (pageSize: number) => void;
|
||||
}
|
||||
|
||||
export interface EmptyOrLoadingViewProps {
|
||||
isError: boolean;
|
||||
errorMessage: string;
|
||||
showHostsEmptyState: boolean;
|
||||
sentAnyHostMetricsData: boolean;
|
||||
isSendingIncorrectK8SAgentMetrics: boolean;
|
||||
showEndTimeBeforeRetentionMessage: boolean;
|
||||
showNoRecordsInSelectedTimeRangeMessage: boolean;
|
||||
showTableLoadingState: boolean;
|
||||
}
|
||||
|
||||
export const getHostListsQuery = (): HostListPayload => ({
|
||||
filters: {
|
||||
items: [],
|
||||
|
||||
@@ -453,6 +453,9 @@ function K8sClustersList({
|
||||
|
||||
const handleRowClick = (record: K8sClustersRowData): void => {
|
||||
if (groupBy.length === 0) {
|
||||
if (!record.clusterNameRaw) {
|
||||
return;
|
||||
}
|
||||
setSelectedRowData(null);
|
||||
setselectedClusterName(record.clusterUID);
|
||||
setSearchParams({
|
||||
@@ -517,9 +520,13 @@ function K8sClustersList({
|
||||
showHeader={false}
|
||||
onRow={(record): { onClick: () => void; className: string } => ({
|
||||
onClick: (): void => {
|
||||
setselectedClusterName(record.clusterUID);
|
||||
if (record.clusterNameRaw) {
|
||||
setselectedClusterName(record.clusterUID);
|
||||
}
|
||||
},
|
||||
className: 'expanded-clickable-row',
|
||||
className: record.clusterNameRaw
|
||||
? 'expanded-clickable-row'
|
||||
: 'disabled-row',
|
||||
})}
|
||||
/>
|
||||
|
||||
@@ -709,7 +716,10 @@ function K8sClustersList({
|
||||
onChange={handleTableChange}
|
||||
onRow={(record): { onClick: () => void; className: string } => ({
|
||||
onClick: (): void => handleRowClick(record),
|
||||
className: 'clickable-row',
|
||||
className:
|
||||
groupBy.length > 0 || record.clusterNameRaw
|
||||
? 'clickable-row'
|
||||
: 'disabled-row',
|
||||
})}
|
||||
expandable={{
|
||||
expandedRowRender: isGroupedByAttribute ? expandedRowRender : undefined,
|
||||
|
||||
@@ -47,6 +47,7 @@ export const defaultAddedColumns: IEntityColumn[] = [
|
||||
export interface K8sClustersRowData {
|
||||
key: string;
|
||||
clusterUID: string;
|
||||
clusterNameRaw: string;
|
||||
clusterName: React.ReactNode;
|
||||
cpu: React.ReactNode;
|
||||
memory: React.ReactNode;
|
||||
@@ -175,6 +176,7 @@ export const formatDataForTable = (
|
||||
data.map((cluster, index) => ({
|
||||
key: index.toString(),
|
||||
clusterUID: cluster.meta.k8s_cluster_name,
|
||||
clusterNameRaw: cluster.meta.k8s_cluster_name || '',
|
||||
clusterName: (
|
||||
<Tooltip title={cluster.meta.k8s_cluster_name}>
|
||||
{cluster.meta.k8s_cluster_name}
|
||||
|
||||
@@ -459,6 +459,9 @@ function K8sDaemonSetsList({
|
||||
|
||||
const handleRowClick = (record: K8sDaemonSetsRowData): void => {
|
||||
if (groupBy.length === 0) {
|
||||
if (!record.daemonsetNameRaw) {
|
||||
return;
|
||||
}
|
||||
setSelectedRowData(null);
|
||||
setSelectedDaemonSetUID(record.daemonsetUID);
|
||||
setSearchParams({
|
||||
@@ -523,9 +526,13 @@ function K8sDaemonSetsList({
|
||||
showHeader={false}
|
||||
onRow={(record): { onClick: () => void; className: string } => ({
|
||||
onClick: (): void => {
|
||||
setSelectedDaemonSetUID(record.daemonsetUID);
|
||||
if (record.daemonsetNameRaw) {
|
||||
setSelectedDaemonSetUID(record.daemonsetUID);
|
||||
}
|
||||
},
|
||||
className: 'expanded-clickable-row',
|
||||
className: record.daemonsetNameRaw
|
||||
? 'expanded-clickable-row'
|
||||
: 'disabled-row',
|
||||
})}
|
||||
/>
|
||||
|
||||
@@ -717,7 +724,10 @@ function K8sDaemonSetsList({
|
||||
onChange={handleTableChange}
|
||||
onRow={(record): { onClick: () => void; className: string } => ({
|
||||
onClick: (): void => handleRowClick(record),
|
||||
className: 'clickable-row',
|
||||
className:
|
||||
groupBy.length > 0 || record.daemonsetNameRaw
|
||||
? 'clickable-row'
|
||||
: 'disabled-row',
|
||||
})}
|
||||
expandable={{
|
||||
expandedRowRender: isGroupedByAttribute ? expandedRowRender : undefined,
|
||||
|
||||
@@ -82,6 +82,7 @@ export const defaultAddedColumns: IEntityColumn[] = [
|
||||
export interface K8sDaemonSetsRowData {
|
||||
key: string;
|
||||
daemonsetUID: string;
|
||||
daemonsetNameRaw: string;
|
||||
daemonsetName: React.ReactNode;
|
||||
cpu_request: React.ReactNode;
|
||||
cpu_limit: React.ReactNode;
|
||||
@@ -276,6 +277,7 @@ export const formatDataForTable = (
|
||||
data.map((daemonSet, index) => ({
|
||||
key: index.toString(),
|
||||
daemonsetUID: daemonSet.daemonSetName,
|
||||
daemonsetNameRaw: daemonSet.meta.k8s_daemonset_name || '',
|
||||
daemonsetName: (
|
||||
<Tooltip title={daemonSet.meta.k8s_daemonset_name}>
|
||||
{daemonSet.meta.k8s_daemonset_name || ''}
|
||||
|
||||
@@ -465,6 +465,9 @@ function K8sDeploymentsList({
|
||||
|
||||
const handleRowClick = (record: K8sDeploymentsRowData): void => {
|
||||
if (groupBy.length === 0) {
|
||||
if (!record.deploymentNameRaw) {
|
||||
return;
|
||||
}
|
||||
setSelectedRowData(null);
|
||||
setselectedDeploymentUID(record.deploymentUID);
|
||||
setSearchParams({
|
||||
@@ -529,9 +532,13 @@ function K8sDeploymentsList({
|
||||
showHeader={false}
|
||||
onRow={(record): { onClick: () => void; className: string } => ({
|
||||
onClick: (): void => {
|
||||
setselectedDeploymentUID(record.deploymentUID);
|
||||
if (record.deploymentNameRaw) {
|
||||
setselectedDeploymentUID(record.deploymentUID);
|
||||
}
|
||||
},
|
||||
className: 'expanded-clickable-row',
|
||||
className: record.deploymentNameRaw
|
||||
? 'expanded-clickable-row'
|
||||
: 'disabled-row',
|
||||
})}
|
||||
/>
|
||||
|
||||
@@ -724,7 +731,10 @@ function K8sDeploymentsList({
|
||||
onChange={handleTableChange}
|
||||
onRow={(record): { onClick: () => void; className: string } => ({
|
||||
onClick: (): void => handleRowClick(record),
|
||||
className: 'clickable-row',
|
||||
className:
|
||||
groupBy.length > 0 || record.deploymentNameRaw
|
||||
? 'clickable-row'
|
||||
: 'disabled-row',
|
||||
})}
|
||||
expandable={{
|
||||
expandedRowRender: isGroupedByAttribute ? expandedRowRender : undefined,
|
||||
|
||||
@@ -81,6 +81,7 @@ export const defaultAddedColumns: IEntityColumn[] = [
|
||||
export interface K8sDeploymentsRowData {
|
||||
key: string;
|
||||
deploymentUID: string;
|
||||
deploymentNameRaw: string;
|
||||
deploymentName: React.ReactNode;
|
||||
available_pods: React.ReactNode;
|
||||
desired_pods: React.ReactNode;
|
||||
@@ -267,6 +268,7 @@ export const formatDataForTable = (
|
||||
data.map((deployment, index) => ({
|
||||
key: index.toString(),
|
||||
deploymentUID: deployment.meta.k8s_deployment_name,
|
||||
deploymentNameRaw: deployment.meta.k8s_deployment_name || '',
|
||||
deploymentName: (
|
||||
<Tooltip title={deployment.meta.k8s_deployment_name}>
|
||||
{deployment.meta.k8s_deployment_name}
|
||||
|
||||
@@ -337,6 +337,11 @@
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.disabled-row {
|
||||
cursor: default;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.k8s-list-table {
|
||||
.ant-table {
|
||||
.ant-table-thead > tr > th {
|
||||
|
||||
@@ -430,6 +430,9 @@ function K8sJobsList({
|
||||
|
||||
const handleRowClick = (record: K8sJobsRowData): void => {
|
||||
if (groupBy.length === 0) {
|
||||
if (!record.jobNameRaw) {
|
||||
return;
|
||||
}
|
||||
setSelectedRowData(null);
|
||||
setselectedJobUID(record.jobUID);
|
||||
setSearchParams({
|
||||
@@ -494,9 +497,11 @@ function K8sJobsList({
|
||||
showHeader={false}
|
||||
onRow={(record): { onClick: () => void; className: string } => ({
|
||||
onClick: (): void => {
|
||||
setselectedJobUID(record.jobUID);
|
||||
if (record.jobNameRaw) {
|
||||
setselectedJobUID(record.jobUID);
|
||||
}
|
||||
},
|
||||
className: 'expanded-clickable-row',
|
||||
className: record.jobNameRaw ? 'expanded-clickable-row' : 'disabled-row',
|
||||
})}
|
||||
/>
|
||||
|
||||
@@ -686,7 +691,10 @@ function K8sJobsList({
|
||||
onChange={handleTableChange}
|
||||
onRow={(record): { onClick: () => void; className: string } => ({
|
||||
onClick: (): void => handleRowClick(record),
|
||||
className: 'clickable-row',
|
||||
className:
|
||||
groupBy.length > 0 || record.jobNameRaw
|
||||
? 'clickable-row'
|
||||
: 'disabled-row',
|
||||
})}
|
||||
expandable={{
|
||||
expandedRowRender: isGroupedByAttribute ? expandedRowRender : undefined,
|
||||
|
||||
@@ -94,6 +94,7 @@ export const defaultAddedColumns: IEntityColumn[] = [
|
||||
export interface K8sJobsRowData {
|
||||
key: string;
|
||||
jobUID: string;
|
||||
jobNameRaw: string;
|
||||
jobName: React.ReactNode;
|
||||
namespaceName: React.ReactNode;
|
||||
successful_pods: React.ReactNode;
|
||||
@@ -303,6 +304,7 @@ export const formatDataForTable = (
|
||||
data.map((job, index) => ({
|
||||
key: index.toString(),
|
||||
jobUID: job.jobName,
|
||||
jobNameRaw: job.meta.k8s_job_name || '',
|
||||
jobName: (
|
||||
<Tooltip title={job.meta.k8s_job_name}>
|
||||
{job.meta.k8s_job_name || ''}
|
||||
|
||||
@@ -461,6 +461,9 @@ function K8sNamespacesList({
|
||||
|
||||
const handleRowClick = (record: K8sNamespacesRowData): void => {
|
||||
if (groupBy.length === 0) {
|
||||
if (!record.namespaceNameRaw) {
|
||||
return;
|
||||
}
|
||||
setSelectedRowData(null);
|
||||
setselectedNamespaceUID(record.namespaceUID);
|
||||
setSearchParams({
|
||||
@@ -525,9 +528,13 @@ function K8sNamespacesList({
|
||||
showHeader={false}
|
||||
onRow={(record): { onClick: () => void; className: string } => ({
|
||||
onClick: (): void => {
|
||||
setselectedNamespaceUID(record.namespaceUID);
|
||||
if (record.namespaceNameRaw) {
|
||||
setselectedNamespaceUID(record.namespaceUID);
|
||||
}
|
||||
},
|
||||
className: 'expanded-clickable-row',
|
||||
className: record.namespaceNameRaw
|
||||
? 'expanded-clickable-row'
|
||||
: 'disabled-row',
|
||||
})}
|
||||
/>
|
||||
|
||||
@@ -718,7 +725,10 @@ function K8sNamespacesList({
|
||||
onChange={handleTableChange}
|
||||
onRow={(record): { onClick: () => void; className: string } => ({
|
||||
onClick: (): void => handleRowClick(record),
|
||||
className: 'clickable-row',
|
||||
className:
|
||||
groupBy.length > 0 || record.namespaceNameRaw
|
||||
? 'clickable-row'
|
||||
: 'disabled-row',
|
||||
})}
|
||||
expandable={{
|
||||
expandedRowRender: isGroupedByAttribute ? expandedRowRender : undefined,
|
||||
|
||||
@@ -41,6 +41,7 @@ export const defaultAddedColumns: IEntityColumn[] = [
|
||||
export interface K8sNamespacesRowData {
|
||||
key: string;
|
||||
namespaceUID: string;
|
||||
namespaceNameRaw: string;
|
||||
namespaceName: string;
|
||||
clusterName: string;
|
||||
cpu: React.ReactNode;
|
||||
@@ -161,6 +162,7 @@ export const formatDataForTable = (
|
||||
data.map((namespace, index) => ({
|
||||
key: index.toString(),
|
||||
namespaceUID: namespace.namespaceName,
|
||||
namespaceNameRaw: namespace.namespaceName || '',
|
||||
namespaceName: namespace.namespaceName,
|
||||
clusterName: namespace.meta.k8s_cluster_name,
|
||||
cpu: (
|
||||
|
||||
@@ -440,6 +440,9 @@ function K8sNodesList({
|
||||
|
||||
const handleRowClick = (record: K8sNodesRowData): void => {
|
||||
if (groupBy.length === 0) {
|
||||
if (!record.nodeNameRaw) {
|
||||
return;
|
||||
}
|
||||
setSelectedRowData(null);
|
||||
setSelectedNodeUID(record.nodeUID);
|
||||
setSearchParams({
|
||||
@@ -505,9 +508,13 @@ function K8sNodesList({
|
||||
showHeader={false}
|
||||
onRow={(record): { onClick: () => void; className: string } => ({
|
||||
onClick: (): void => {
|
||||
setSelectedNodeUID(record.nodeUID);
|
||||
if (record.nodeNameRaw) {
|
||||
setSelectedNodeUID(record.nodeUID);
|
||||
}
|
||||
},
|
||||
className: 'expanded-clickable-row',
|
||||
className: record.nodeNameRaw
|
||||
? 'expanded-clickable-row'
|
||||
: 'disabled-row',
|
||||
})}
|
||||
/>
|
||||
|
||||
@@ -697,7 +704,10 @@ function K8sNodesList({
|
||||
onChange={handleTableChange}
|
||||
onRow={(record): { onClick: () => void; className: string } => ({
|
||||
onClick: (): void => handleRowClick(record),
|
||||
className: 'clickable-row',
|
||||
className:
|
||||
groupBy.length > 0 || record.nodeNameRaw
|
||||
? 'clickable-row'
|
||||
: 'disabled-row',
|
||||
})}
|
||||
expandable={{
|
||||
expandedRowRender: isGroupedByAttribute ? expandedRowRender : undefined,
|
||||
|
||||
@@ -53,6 +53,7 @@ export const defaultAddedColumns: IEntityColumn[] = [
|
||||
export interface K8sNodesRowData {
|
||||
key: string;
|
||||
nodeUID: string;
|
||||
nodeNameRaw: string;
|
||||
nodeName: React.ReactNode;
|
||||
clusterName: string;
|
||||
cpu: React.ReactNode;
|
||||
@@ -193,6 +194,7 @@ export const formatDataForTable = (
|
||||
data.map((node, index) => ({
|
||||
key: `${node.nodeUID}-${index}`,
|
||||
nodeUID: node.nodeUID || '',
|
||||
nodeNameRaw: node.meta.k8s_node_name || '',
|
||||
nodeName: (
|
||||
<Tooltip title={node.meta.k8s_node_name}>
|
||||
{node.meta.k8s_node_name || ''}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user