Compare commits

...

20 Commits

Author SHA1 Message Date
Yunus M
048de52246 chore: skip request integration service test in aws 2026-02-15 02:14:32 +05:30
Yunus M
644480c4c3 chore: remove cursor rules from gitignore 2026-02-15 02:13:34 +05:30
Yunus M
08748dfe7f chore: move cursor rules to folder to follow the current format 2026-02-15 02:08:16 +05:30
Yunus M
9dce854255 fix: update integrations util path to fix test case 2026-02-15 01:53:56 +05:30
Yunus M
b2539b337e feat: integrate disconnect integration api 2026-02-15 01:39:19 +05:30
Yunus M
9d45e75d52 feat: add search functionality and no results UI for integrations 2026-02-15 00:52:42 +05:30
Yunus M
9c7a54b549 fix: aws integration - minor ui improvements 2026-02-15 00:35:05 +05:30
Yunus M
763e13df21 Merge branch 'main' into feat/azure-integration-ui 2026-02-15 00:13:34 +05:30
Yunus M
5cb81fe17a fix: sorting logic for enabled and not enabled services 2026-02-15 00:02:38 +05:30
Yunus M
3ecd0a662c feat: integrate service update api 2026-02-14 23:54:18 +05:30
Yunus M
b062a8a463 feat: integrate azure account connect / edit APIs 2026-02-14 22:22:40 +05:30
Yunus M
82a67b62e2 refactor: update integration types and improve imports 2026-02-11 20:06:14 +05:30
Yunus M
9a70da858f refactor: update integration types and improve imports 2026-02-11 20:02:18 +05:30
Yunus M
74a548e2a2 feat: add new Azure integration components and update existing ones 2026-02-11 19:27:13 +05:30
Yunus M
be68b71bd8 feat: improve light mode styles 2026-02-09 21:57:50 +05:30
Yunus M
5119a62a77 feat: improve light mode styles 2026-02-09 21:54:16 +05:30
Yunus M
5203a9f177 feat: enhance IntegrationDetailHeader with loading state and styles 2026-02-09 21:38:11 +05:30
Yunus M
09ac5abe33 refactor: reorganize AWS integration components and update imports
- Moved AWS-related components to a new directory structure for better organization.
- Updated import paths to reflect the new structure.
- Removed unused components and styles related to the previous integration setup.
- Adjusted constants and integration logic to ensure compatibility with the new structure.
2026-02-09 19:54:08 +05:30
Yunus M
bea4f32fe9 feat: render integration in new route 2026-02-04 15:44:47 +05:30
Yunus M
1e07714075 chore: clean up integrations code for better code organisation and extensibility 2026-02-02 19:51:31 +05:30
140 changed files with 5648 additions and 1833 deletions

3
.gitignore vendored
View File

@@ -228,6 +228,3 @@ cython_debug/
# LSP config files
pyrightconfig.json
# cursor files
frontend/.cursor/

View File

@@ -0,0 +1,77 @@
---
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 (515 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)
}));
```

View File

@@ -0,0 +1,124 @@
---
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**: 35 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());
});
});
```

View File

@@ -0,0 +1,168 @@
---
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

View File

@@ -1,484 +0,0 @@
# Persona
You are an expert developer with deep knowledge of Jest, React Testing Library, MSW, and TypeScript, tasked with creating unit tests for this repository.
# Auto-detect TypeScript Usage
Check for TypeScript in the project through tsconfig.json or package.json dependencies.
Adjust syntax based on this detection.
# TypeScript Type Safety for Jest Tests
**CRITICAL**: All Jest tests MUST be fully type-safe with proper TypeScript types.
**Type Safety Requirements:**
- Use proper TypeScript interfaces for all mock data
- Type all Jest mock functions with `jest.MockedFunction<T>`
- Use generic types for React components and hooks
- Define proper return types for mock functions
- Use `as const` for literal types when needed
- Avoid `any` type use proper typing instead
# Unit Testing Focus
Focus on critical functionality (business logic, utility functions, component behavior)
Mock dependencies (API calls, external modules) before imports
Test multiple data scenarios (valid inputs, invalid inputs, edge cases)
Write maintainable tests with descriptive names grouped in describe blocks
# Global vs Local Mocks
**Use Global Mocks for:**
- High-frequency dependencies (20+ test files)
- Core infrastructure (react-router-dom, react-query, antd)
- Standard implementations across the app
- Browser APIs (ResizeObserver, matchMedia, localStorage)
- Utility libraries (date-fns, lodash)
**Use Local Mocks for:**
- Business logic dependencies (5-15 test files)
- Test-specific behavior (different data per test)
- API endpoints with specific responses
- Domain-specific components
- Error scenarios and edge cases
**Global Mock Files Available (from jest.config.ts):**
- `uplot` → `__mocks__/uplotMock.ts`
# Repo-specific Testing Conventions
## Imports
Always import from our harness:
```ts
import { render, screen, userEvent, waitFor } from 'tests/test-utils';
```
For API mocks:
```ts
import { server, rest } from 'mocks-server/server';
```
Do not import directly from `@testing-library/react`.
## Router
Use the router built into render:
```ts
render(<Page />, undefined, { initialRoute: '/traces-explorer' });
```
Only mock `useLocation` / `useParams` if the test depends on them.
## Hook Mocks
Pattern:
```ts
import useFoo from 'hooks/useFoo';
jest.mock('hooks/useFoo');
const mockUseFoo = jest.mocked(useFoo);
mockUseFoo.mockReturnValue(/* minimal shape */ as any);
```
Prefer helpers (`rqSuccess`, `rqLoading`, `rqError`) for React Query results.
## MSW
Global MSW server runs automatically.
Override per-test:
```ts
server.use(
rest.get('*/api/v1/foo', (_req, res, ctx) => res(ctx.status(200), ctx.json({ ok: true })))
);
```
Keep large responses in `mocks-server/__mockdata_`.
## Interactions
- Prefer `userEvent` for real user interactions (click, type, select, tab).
- Use `fireEvent` only for low-level/programmatic events not covered by `userEvent` (e.g., scroll, resize, setting `element.scrollTop` for virtualization). Wrap in `act(...)` if needed.
- Always await interactions:
```ts
const user = userEvent.setup({ pointerEventsCheck: 0 });
await user.click(screen.getByRole('button', { name: /save/i }));
```
```ts
// Example: virtualized list scroll (no userEvent helper)
const scroller = container.querySelector('[data-test-id="virtuoso-scroller"]') as HTMLElement;
scroller.scrollTop = targetScrollTop;
act(() => { fireEvent.scroll(scroller); });
```
## Timers
❌ No global fake timers.
✅ Per-test only, for debounce/throttle:
```ts
jest.useFakeTimers();
const user = userEvent.setup({ advanceTimers: (ms) => jest.advanceTimersByTime(ms) });
await user.type(screen.getByRole('textbox'), 'query');
jest.advanceTimersByTime(400);
jest.useRealTimers();
```
## Queries
Prefer accessible queries (`getByRole`, `findByRole`, `getByLabelText`).
Fallback: visible text.
Last resort: `data-testid`.
# Example Test (using only configured global mocks)
```ts
import { render, screen, userEvent, waitFor } from 'tests/test-utils';
import { server, rest } from 'mocks-server/server';
import MyComponent from '../MyComponent';
describe('MyComponent', () => {
it('renders and interacts', async () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
server.use(
rest.get('*/api/v1/example', (_req, res, ctx) => res(ctx.status(200), ctx.json({ value: 42 })))
);
render(<MyComponent />, undefined, { initialRoute: '/foo' });
expect(await screen.findByText(/value: 42/i)).toBeInTheDocument();
await user.click(screen.getByRole('button', { name: /refresh/i }));
await waitFor(() => expect(screen.getByText(/loading/i)).toBeInTheDocument());
});
});
```
# Anti-patterns
❌ Importing RTL directly
❌ Using global fake timers
❌ Wrapping render in `act(...)`
❌ Mocking infra dependencies locally (router, react-query)
✅ Use our harness (`tests/test-utils`)
✅ Use MSW for API overrides
✅ Use userEvent + await
✅ Pin time only in tests that assert relative dates
# Best Practices
- **Critical Functionality**: Prioritize testing business logic and utilities
- **Dependency Mocking**: Global mocks for infra, local mocks for business logic
- **Data Scenarios**: Always test valid, invalid, and edge cases
- **Descriptive Names**: Make test intent clear
- **Organization**: Group related tests in describe
- **Consistency**: Match repo conventions
- **Edge Cases**: Test null, undefined, unexpected values
- **Limit Scope**: 35 focused tests per file
- **Use Helpers**: `rqSuccess`, `makeUser`, etc.
- **No Any**: Enforce type safety
# Example Test
```ts
import { render, screen, userEvent, waitFor } from 'tests/test-utils';
import { server, rest } from 'mocks-server/server';
import MyComponent from '../MyComponent';
describe('MyComponent', () => {
it('renders and interacts', async () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
server.use(
rest.get('*/api/v1/example', (_req, res, ctx) => res(ctx.status(200), ctx.json({ value: 42 })))
);
render(<MyComponent />, undefined, { initialRoute: '/foo' });
expect(await screen.findByText(/value: 42/i)).toBeInTheDocument();
await user.click(screen.getByRole('button', { name: /refresh/i }));
await waitFor(() => expect(screen.getByText(/loading/i)).toBeInTheDocument());
});
});
```
# Anti-patterns
❌ Importing RTL directly
❌ Using global fake timers
❌ Wrapping render in `act(...)`
❌ Mocking infra dependencies locally (router, react-query)
✅ Use our harness (`tests/test-utils`)
✅ Use MSW for API overrides
✅ Use userEvent + await
✅ Pin time only in tests that assert relative dates
# TypeScript Type Safety Examples
## Proper Mock Typing
```ts
// ✅ GOOD - Properly typed mocks
interface User {
id: number;
name: string;
email: string;
}
interface ApiResponse<T> {
data: T;
status: number;
message: string;
}
// Type the mock functions
const mockFetchUser = jest.fn() as jest.MockedFunction<(id: number) => Promise<ApiResponse<User>>>;
const mockUpdateUser = jest.fn() as jest.MockedFunction<(user: User) => Promise<ApiResponse<User>>>;
// Mock implementation with proper typing
mockFetchUser.mockResolvedValue({
data: { id: 1, name: 'John Doe', email: 'john@example.com' },
status: 200,
message: 'Success'
});
// ❌ BAD - Using any type
const mockFetchUser = jest.fn() as any; // Don't do this
```
## React Component Testing with Types
```ts
// ✅ GOOD - Properly typed component testing
interface ComponentProps {
title: string;
data: User[];
onUserSelect: (user: User) => void;
isLoading?: boolean;
}
const TestComponent: React.FC<ComponentProps> = ({ title, data, onUserSelect, isLoading = false }) => {
// Component implementation
};
describe('TestComponent', () => {
it('should render with proper props', () => {
// Arrange - Type the props properly
const mockProps: ComponentProps = {
title: 'Test Title',
data: [{ id: 1, name: 'John', email: 'john@example.com' }],
onUserSelect: jest.fn() as jest.MockedFunction<(user: User) => void>,
isLoading: false
};
// Act
render(<TestComponent {...mockProps} />);
// Assert
expect(screen.getByText('Test Title')).toBeInTheDocument();
});
});
```
## Hook Testing with Types
```ts
// ✅ GOOD - Properly typed hook testing
interface UseUserDataReturn {
user: User | null;
loading: boolean;
error: string | null;
refetch: () => void;
}
const useUserData = (id: number): UseUserDataReturn => {
// Hook implementation
};
describe('useUserData', () => {
it('should return user data with proper typing', () => {
// Arrange
const mockUser: User = { id: 1, name: 'John', email: 'john@example.com' };
mockFetchUser.mockResolvedValue({
data: mockUser,
status: 200,
message: 'Success'
});
// Act
const { result } = renderHook(() => useUserData(1));
// Assert
expect(result.current.user).toEqual(mockUser);
expect(result.current.loading).toBe(false);
expect(result.current.error).toBeNull();
});
});
```
## Global Mock Type Safety
```ts
// ✅ GOOD - Type-safe global mocks
// In __mocks__/routerMock.ts
export const mockUseLocation = (overrides: Partial<Location> = {}): Location => ({
pathname: '/traces',
search: '',
hash: '',
state: null,
key: 'test-key',
...overrides,
});
// In test files
const location = useLocation(); // Properly typed from global mock
expect(location.pathname).toBe('/traces');
```
# TypeScript Configuration for Jest
## Required Jest Configuration
```json
// jest.config.ts
{
"preset": "ts-jest/presets/js-with-ts-esm",
"globals": {
"ts-jest": {
"useESM": true,
"isolatedModules": true,
"tsconfig": "<rootDir>/tsconfig.jest.json"
}
},
"extensionsToTreatAsEsm": [".ts", ".tsx"],
"moduleFileExtensions": ["ts", "tsx", "js", "json"]
}
```
## TypeScript Jest Configuration
```json
// tsconfig.jest.json
{
"extends": "./tsconfig.json",
"compilerOptions": {
"types": ["jest", "@testing-library/jest-dom"],
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"moduleResolution": "node"
},
"include": [
"src/**/*",
"**/*.test.ts",
"**/*.test.tsx",
"__mocks__/**/*"
]
}
```
## Common Type Safety Patterns
### Mock Function Typing
```ts
// ✅ GOOD - Proper mock function typing
const mockApiCall = jest.fn() as jest.MockedFunction<typeof apiCall>;
const mockEventHandler = jest.fn() as jest.MockedFunction<(event: Event) => void>;
// ❌ BAD - Using any
const mockApiCall = jest.fn() as any;
```
### Generic Mock Typing
```ts
// ✅ GOOD - Generic mock typing
interface MockApiResponse<T> {
data: T;
status: number;
}
const mockFetchData = jest.fn() as jest.MockedFunction<
<T>(endpoint: string) => Promise<MockApiResponse<T>>
>;
// Usage
mockFetchData<User>('/users').mockResolvedValue({
data: { id: 1, name: 'John' },
status: 200
});
```
### React Testing Library with Types
```ts
// ✅ GOOD - Typed testing utilities
import { render, screen, RenderResult } from '@testing-library/react';
import { ComponentProps } from 'react';
type TestComponentProps = ComponentProps<typeof TestComponent>;
const renderTestComponent = (props: Partial<TestComponentProps> = {}): RenderResult => {
const defaultProps: TestComponentProps = {
title: 'Test',
data: [],
onSelect: jest.fn(),
...props
};
return render(<TestComponent {...defaultProps} />);
};
```
### Error Handling with Types
```ts
// ✅ GOOD - Typed error handling
interface ApiError {
message: string;
code: number;
details?: Record<string, unknown>;
}
const mockApiError: ApiError = {
message: 'API Error',
code: 500,
details: { endpoint: '/users' }
};
mockFetchUser.mockRejectedValue(new Error(JSON.stringify(mockApiError)));
```
## Type Safety Checklist
- [ ] All mock functions use `jest.MockedFunction<T>`
- [ ] All mock data has proper interfaces
- [ ] No `any` types in test files
- [ ] Generic types are used where appropriate
- [ ] Error types are properly defined
- [ ] Component props are typed
- [ ] Hook return types are defined
- [ ] API response types are defined
- [ ] Global mocks are type-safe
- [ ] Test utilities are properly typed
# Mock Decision Tree
```
Is it used in 20+ test files?
├─ YES → Use Global Mock
│ ├─ react-router-dom
│ ├─ react-query
│ ├─ antd components
│ └─ browser APIs
└─ NO → Is it business logic?
├─ YES → Use Local Mock
│ ├─ API endpoints
│ ├─ Custom hooks
│ └─ Domain components
└─ NO → Is it test-specific?
├─ YES → Use Local Mock
│ ├─ Error scenarios
│ ├─ Loading states
│ └─ Specific data
└─ NO → Consider Global Mock
└─ If it becomes frequently used
```
# Common Anti-Patterns to Avoid
❌ **Don't mock global dependencies locally:**
```js
// BAD - This is already globally mocked
jest.mock('react-router-dom', () => ({ ... }));
```
❌ **Don't create global mocks for test-specific data:**
```js
// BAD - This should be local
jest.mock('../api/tracesService', () => ({
getTraces: jest.fn(() => specificTestData)
}));
```
✅ **Do use global mocks for infrastructure:**
```js
// GOOD - Use global mock
import { useLocation } from 'react-router-dom';
```
✅ **Do create local mocks for business logic:**
```js
// GOOD - Local mock for specific test needs
jest.mock('../api/tracesService', () => ({
getTraces: jest.fn(() => mockTracesData)
}));
```

View File

@@ -51,6 +51,7 @@
"@signozhq/checkbox": "0.0.2",
"@signozhq/combobox": "0.0.2",
"@signozhq/command": "0.0.0",
"@signozhq/drawer": "0.0.4",
"@signozhq/design-tokens": "2.1.1",
"@signozhq/icons": "0.1.0",
"@signozhq/input": "0.0.2",
@@ -58,10 +59,12 @@
"@signozhq/resizable": "0.0.0",
"@signozhq/sonner": "0.1.0",
"@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",

View File

@@ -0,0 +1,312 @@
<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>

After

Width:  |  Height:  |  Size: 19 KiB

View File

@@ -253,12 +253,18 @@ export const ShortcutsPage = Loadable(
() => import(/* webpackChunkName: "ShortcutsPage" */ 'pages/Settings'),
);
export const InstalledIntegrations = Loadable(
export const Integrations = Loadable(
() =>
import(
/* webpackChunkName: "InstalledIntegrations" */ 'pages/IntegrationsModulePage'
),
);
export const IntegrationsDetailsPage = Loadable(
() =>
import(
/* webpackChunkName: "IntegrationsDetailsPage" */ 'pages/IntegrationsDetailsPage'
),
);
export const MessagingQueuesMainPage = Loadable(
() =>

View File

@@ -20,7 +20,8 @@ import {
ForgotPassword,
Home,
InfrastructureMonitoring,
InstalledIntegrations,
Integrations,
IntegrationsDetailsPage,
LicensePage,
ListAllALertsPage,
LiveLogs,
@@ -389,10 +390,17 @@ 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: InstalledIntegrations,
component: Integrations,
isPrivate: true,
key: 'INTEGRATIONS',
},

View File

@@ -5,13 +5,13 @@ import {
ServiceData,
UpdateServiceConfigPayload,
UpdateServiceConfigResponse,
} from 'container/CloudIntegrationPage/ServicesSection/types';
} from 'container/Integrations/CloudIntegration/AmazonWebServices/types';
import {
AccountConfigPayload,
AccountConfigResponse,
ConnectionParams,
AWSAccountConfigPayload,
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: AccountConfigPayload,
payload: AWSAccountConfigPayload,
): Promise<AccountConfigResponse> => {
const response = await axios.post<AccountConfigResponse>(
`/cloud-integrations/aws/accounts/${accountId}/config`,

View File

@@ -0,0 +1,122 @@
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;
};

View File

@@ -17,6 +17,7 @@ import '@signozhq/callout';
import '@signozhq/checkbox';
import '@signozhq/combobox';
import '@signozhq/command';
import '@signozhq/drawer';
import '@signozhq/design-tokens';
import '@signozhq/icons';
import '@signozhq/input';
@@ -24,4 +25,5 @@ import '@signozhq/popover';
import '@signozhq/resizable';
import '@signozhq/sonner';
import '@signozhq/table';
import '@signozhq/tabs';
import '@signozhq/tooltip';

View File

@@ -0,0 +1,89 @@
.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(--Slate-400, #1d212d);
background: var(--Ink-400, #121317);
.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(--Slate-400, #1d212d);
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;
}
}
}

View File

@@ -0,0 +1,165 @@
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>
);
}

View File

@@ -0,0 +1,3 @@
import CloudIntegrationAccounts from './CloudIntegrationAccounts';
export default CloudIntegrationAccounts;

View File

@@ -0,0 +1,62 @@
.cloud-integrations-header-section {
display: flex;
flex-direction: column;
gap: 16px;
border-bottom: 1px solid var(--bg-slate-500);
.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(--Vanilla-100, #fff);
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(--Vanilla-400, #c0c1c3);
font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
}
}
}
}
.lightMode {
.cloud-integrations-header {
.cloud-integrations-title {
color: var(--bg-vanilla-100);
}
.cloud-integrations-description {
color: var(--bg-vanilla-400);
}
}
}

View File

@@ -0,0 +1,61 @@
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>
);
}

View File

@@ -0,0 +1,3 @@
import CloudIntegrationsHeader from './CloudIntegrationsHeader';
export default CloudIntegrationsHeader;

View File

@@ -0,0 +1,35 @@
.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(--Vanilla-400, #c0c1c3);
/* Bifrost (Ancient)/Content/sm */
font-family: Inter;
font-size: 14px;
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(--Slate-400, #1d212d);
background: var(--Ink-400, #121317);
box-shadow: 0 4px 12px 0 rgba(0, 0, 0, 0.1);
}
}
}

View File

@@ -1,6 +1,8 @@
import { Table } from 'antd';
import { ServiceData } from 'container/Integrations/CloudIntegration/AmazonWebServices/types';
import { BarChart2, ScrollText } from 'lucide-react';
import { ServiceData } from './types';
import './CloudServiceDataCollected.styles.scss';
function CloudServiceDataCollected({
logsData,
@@ -61,26 +63,32 @@ 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">Logs</div>
<div className="cloud-service-data-collected-table">
<div className="cloud-service-data-collected-table-heading">
<ScrollText size={14} />
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">Metrics</div>
<div className="cloud-service-data-collected-table">
<div className="cloud-service-data-collected-table-heading">
<BarChart2 size={14} />
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>
)}

View File

@@ -0,0 +1,39 @@
.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;
}
}
}

View File

@@ -0,0 +1,145 @@
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;

View File

@@ -0,0 +1,2 @@
export type { CodeBlockLanguage, CodeBlockTheme } from './CodeBlock';
export { default as CodeBlock } from './CodeBlock';

View File

@@ -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');

View File

@@ -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('pages/Integrations/utils', () => ({
jest.mock('container/Integrations/utils', () => ({
handleContactSupport: jest.fn(),
}));

View File

@@ -1,4 +1,19 @@
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',

View File

@@ -64,6 +64,7 @@ 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',

View File

@@ -1,24 +0,0 @@
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;

View File

@@ -1,34 +0,0 @@
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;

View File

@@ -3,14 +3,12 @@ import { useQueryClient } from 'react-query';
import { Form, Switch } from 'antd';
import SignozModal from 'components/SignozModal/SignozModal';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import {
ServiceConfig,
SupportedSignals,
} from 'container/CloudIntegrationPage/ServicesSection/types';
import { AWSServiceConfig } from 'container/Integrations/CloudIntegration/AmazonWebServices/types';
import { SupportedSignals } from 'container/Integrations/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';
@@ -22,7 +20,7 @@ export interface IConfigureServiceModalProps {
serviceId: string;
cloudAccountId: string;
supportedSignals: SupportedSignals;
initialConfig?: ServiceConfig;
initialConfig?: AWSServiceConfig;
}
function ConfigureServiceModal({

View File

@@ -0,0 +1,23 @@
import AccountActions from './components/AccountActions';
import './HeroSection.style.scss';
function HeroSection(): JSX.Element {
return (
<div className="hero-section">
<div className="hero-section__icon">
<img src="/Logos/aws-dark.svg" alt="AWS" />
</div>
<div className="hero-section__details">
<div className="title">AWS</div>
<div className="description">
AWS is a cloud computing platform that provides a range of services for
building and running applications.
</div>
<AccountActions />
</div>
</div>
);
}
export default HeroSection;

View File

@@ -1,14 +1,16 @@
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 { Button, Select, Skeleton } from 'antd';
import { 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 '../../ServicesSection/types';
import { CloudAccount } from '../../types';
import AccountSettingsModal from './AccountSettingsModal';
import CloudAccountSetupModal from './CloudAccountSetupModal';
@@ -48,12 +50,6 @@ function renderOption(
);
}
const getAccountById = (
accounts: CloudAccount[],
accountId: string,
): CloudAccount | null =>
accounts.find((account) => account.cloud_account_id === accountId) || null;
function AccountActionsRenderer({
accounts,
isLoading,
@@ -74,24 +70,7 @@ function AccountActionsRenderer({
if (isLoading) {
return (
<div className="hero-section__actions-with-account">
<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>
<Skeleton.Input active block className="hero-section__input-skeleton" />
</div>
);
}
@@ -110,16 +89,12 @@ function AccountActionsRenderer({
onChange={onAccountChange}
/>
<div className="hero-section__action-buttons">
<Button
type="primary"
className="hero-section__action-button primary"
onClick={onIntegrationModalOpen}
>
<Button variant="solid" color="primary" onClick={onIntegrationModalOpen}>
Add New AWS Account
</Button>
<Button
type="default"
className="hero-section__action-button secondary"
variant="solid"
color="primary"
onClick={onAccountSettingsModalOpen}
>
Account Settings
@@ -129,10 +104,7 @@ function AccountActionsRenderer({
);
}
return (
<Button
className="hero-section__action-button primary"
onClick={onIntegrationModalOpen}
>
<Button variant="solid" color="primary" onClick={onIntegrationModalOpen}>
Integrate Now
</Button>
);

View File

@@ -10,8 +10,8 @@ import {
import useUrlQuery from 'hooks/useUrlQuery';
import history from 'lib/history';
import logEvent from '../../../../api/common/logEvent';
import { CloudAccount } from '../../ServicesSection/types';
import logEvent from '../../../../../../api/common/logEvent';
import { CloudAccount } from '../../types';
import { RegionSelector } from './RegionSelector';
import RemoveIntegrationAccount from './RemoveIntegrationAccount';

View File

@@ -1,11 +1,12 @@
import { useRef } from 'react';
import { Form } from 'antd';
import cx from 'classnames';
import { useAccountStatus } from 'hooks/integration/aws/useAccountStatus';
import { AccountStatusResponse } from 'types/api/integrations/aws';
import { INTEGRATION_TYPES } from 'container/Integrations/constants';
import { useGetAccountStatus } from 'hooks/integration/useGetAccountStatus';
import { AccountStatusResponse } from 'types/api/integrations/types';
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 {
@@ -44,27 +45,31 @@ export function RegionForm({
const refetchInterval = 10 * 1000;
const errorTimeout = 10 * 60 * 1000;
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) {
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: () => {
setModalState(ModalStateEnum.ERROR);
logEvent('AWS Integration: Account connection attempt timed out', {
id: accountId,
});
}
},
},
onError: () => {
setModalState(ModalStateEnum.ERROR);
},
});
);
const isFormDisabled =
modalState === ModalStateEnum.WAITING || isAccountStatusLoading;

View File

@@ -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/Integrations/removeAwsIntegrationAccount';
import removeAwsIntegrationAccount from 'api/integration/aws/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';

View File

@@ -1,5 +1,5 @@
import { Form, Input } from 'antd';
import { ConnectionParams } from 'types/api/integrations/aws';
import { ConnectionParams } from 'types/api/integrations/types';
function RenderConnectionFields({
isConnectionParamsLoading,

View File

@@ -1,6 +1,6 @@
import { Dispatch, SetStateAction } from 'react';
import { FormInstance } from 'antd';
import { ConnectionParams } from 'types/api/integrations/aws';
import { ConnectionParams } from 'types/api/integrations/types';
export enum ActiveViewEnum {
SELECT_REGIONS = 'select-regions',

View File

@@ -1,15 +1,16 @@
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/CloudIntegrationPage/ServicesSection/CloudServiceDashboards';
import CloudServiceDataCollected from 'container/CloudIntegrationPage/ServicesSection/CloudServiceDataCollected';
import { IServiceStatus } from 'container/CloudIntegrationPage/ServicesSection/types';
import CloudServiceDashboards from 'container/Integrations/CloudIntegration/AmazonWebServices/CloudServiceDashboards';
import { AWSServiceConfig } from 'container/Integrations/CloudIntegration/AmazonWebServices/types';
import { IServiceStatus } from 'container/Integrations/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 = (
@@ -110,9 +111,10 @@ function ServiceDetails(): JSX.Element | null {
[config],
);
const awsConfig = config as AWSServiceConfig | undefined;
const isAnySignalConfigured = useMemo(
() => !!config?.logs?.enabled || !!config?.metrics?.enabled,
[config],
() => !!awsConfig?.logs?.enabled || !!awsConfig?.metrics?.enabled,
[awsConfig],
);
// log telemetry event on visiting details of a service.
@@ -179,7 +181,7 @@ function ServiceDetails(): JSX.Element | null {
serviceName={serviceDetailsData.title}
serviceId={serviceId || ''}
cloudAccountId={cloudAccountId || ''}
initialConfig={serviceDetailsData.config}
initialConfig={awsConfig}
supportedSignals={serviceDetailsData.supported_signals || {}}
/>
)}

View File

@@ -20,19 +20,19 @@
}
.services-section {
display: flex;
gap: 10px;
&__sidebar {
width: 16%;
padding: 0 16px;
width: 240px;
border-right: 1px solid var(--bg-slate-400);
}
&__content {
width: 84%;
padding: 16px;
flex: 1;
}
}
.services-filter {
padding: 16px 0;
padding: 12px;
.ant-select-selector {
background-color: var(--bg-ink-300) !important;
border: 1px solid var(--bg-slate-400) !important;
@@ -63,17 +63,19 @@
background-color: var(--bg-ink-100);
}
&__icon-wrapper {
height: 40px;
width: 40px;
height: 32px;
width: 32px;
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: 24px;
height: 24px;
width: 16px;
height: 16px;
object-fit: contain;
}
}
&__title {
@@ -90,11 +92,13 @@
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 {
@@ -105,6 +109,7 @@
letter-spacing: -0.07px;
text-align: left;
}
.service-details__right-actions {
display: flex;
align-items: center;
@@ -157,21 +162,28 @@
}
}
}
&__overview {
color: var(--bg-vanilla-400);
font-size: 14px;
font-weight: 400;
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
width: 800px;
width: 100%;
padding: 8px 12px;
}
&__tabs {
padding: 0px 12px 12px 8px;
.ant-tabs {
&-ink-bar {
background-color: transparent;
}
&-nav {
padding: 8px 0 18px;
padding: 0;
&-wrap {
padding: 0;
}

View File

@@ -1,13 +1,14 @@
import { useMemo, useState } from 'react';
import { useQuery } from 'react-query';
import { Color } from '@signozhq/design-tokens';
import type { SelectProps, TabsProps } from 'antd';
import { Select, Tabs } from 'antd';
import type { SelectProps } from 'antd';
import { Select } 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';
@@ -106,17 +107,10 @@ 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">
<Tabs defaultActiveKey="services" items={tabItems} />
<HeroSection />
<ServicesSection />
</div>
);
}

View File

@@ -1,90 +1,40 @@
import { ServiceData } from 'container/Integrations/types';
interface Service {
id: string;
title: string;
icon: string;
config: ServiceConfig;
}
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;
config: AWSServiceConfig;
}
interface S3BucketsByRegion {
[region: string]: string[];
}
interface ConfigStatus {
enabled: boolean;
}
interface LogsConfig extends ConfigStatus {
s3_buckets?: S3BucketsByRegion;
}
interface ServiceConfig {
interface AWSServiceConfig {
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;
}
interface CloudAccountConfig {
export interface AWSCloudAccountConfig {
regions: string[];
}
interface IntegrationStatus {
export interface IntegrationStatus {
last_heartbeat_ts_ms: number;
}
@@ -95,7 +45,7 @@ interface AccountStatus {
interface CloudAccount {
id: string;
cloud_account_id: string;
config: CloudAccountConfig;
config: AWSCloudAccountConfig;
status: AccountStatus;
}
@@ -133,15 +83,13 @@ interface UpdateServiceConfigResponse {
}
export type {
AWSServiceConfig,
CloudAccount,
CloudAccountsData,
IServiceStatus,
S3BucketsByRegion,
Service,
ServiceConfig,
ServiceData,
ServiceDetailsResponse,
SupportedSignals,
UpdateServiceConfigPayload,
UpdateServiceConfigResponse,
};

View File

@@ -0,0 +1,326 @@
.azure-account-container {
padding: 16px 8px;
.azure-account-title {
font-size: 24px;
font-weight: 600;
color: #000;
}
.azure-account-prerequisites-step {
display: flex;
flex-direction: column;
gap: 16px;
padding: 16px 0px;
.azure-account-prerequisites-step-description {
display: flex;
flex-direction: column;
gap: 16px;
.azure-account-prerequisites-step-description-item {
color: var(--Vanilla-400, #c0c1c3);
font-family: Inter;
font-size: 13px;
font-style: normal;
font-weight: 400;
line-height: 20px;
display: flex;
align-items: center;
gap: 8px;
.azure-account-prerequisites-step-description-item-bullet {
color: var(--Robin-500, #4e74f8);
font-family: Inter;
font-size: 13px;
font-style: normal;
font-weight: 400;
line-height: 20px; /* 142.857% */
}
}
}
.azure-account-prerequisites-step-how-it-works {
.azure-account-prerequisites-step-how-it-works-title {
display: flex;
flex-direction: row;
gap: 8px;
align-items: center;
cursor: pointer;
padding: 8px 16px;
border: 1px solid var(--Slate-400, #1d212d);
border-radius: 4px;
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
background: var(--Ink-400, #121317);
box-shadow: 0 4px 12px 0 rgba(0, 0, 0, 0.1);
.azure-account-prerequisites-step-how-it-works-title-icon {
display: flex;
align-items: center;
justify-content: center;
width: 20px;
height: 20px;
color: var(--Vanilla-100, #fff);
}
.azure-account-prerequisites-step-how-it-works-title-text {
color: var(--Vanilla-100, #fff);
font-family: Inter;
font-size: 13px;
font-style: normal;
font-weight: 400;
line-height: 20px; /* 166.667% */
letter-spacing: -0.06px;
}
}
.azure-account-prerequisites-step-how-it-works-description {
padding: 16px;
color: var(--Vanilla-400, #c0c1c3);
font-family: Inter;
font-size: 13px;
font-style: normal;
font-weight: 400;
line-height: 20px; /* 150% */
letter-spacing: -0.06px;
border: 1px solid var(--Slate-400, #1d212d);
border-top: none;
border-radius: 4px;
border-top-left-radius: 0;
border-top-right-radius: 0;
background: var(--Ink-400, #121317);
box-shadow: 0 4px 12px 0 rgba(0, 0, 0, 0.1);
display: flex;
flex-direction: column;
gap: 16px;
}
}
}
.azure-account-configure-agent-step {
display: flex;
flex-direction: column;
gap: 16px;
padding: 16px 0px;
}
.azure-account-deploy-agent-step {
display: flex;
flex-direction: column;
gap: 16px;
padding: 16px 0px;
.azure-account-deploy-agent-step-subtitle {
color: var(--Vanilla-400, #c0c1c3);
font-family: Inter;
font-size: 13px;
font-style: normal;
font-weight: 400;
line-height: 20px; /* 142.857% */
}
.azure-account-deploy-agent-step-commands {
display: flex;
flex-direction: column;
gap: 8px;
.azure-account-deploy-agent-step-commands-tabs {
width: 100%;
border-radius: 6px;
border: 1px solid var(--Slate-500, #161922);
background: var(--Ink-400, #121317);
box-shadow: 0 4px 12px 0 rgba(0, 0, 0, 0.1);
padding: 8px;
// attribute - role="tabpanel"
[role='tabpanel'] {
width: 100%;
}
.azure-account-deploy-agent-step-commands-tabs-content {
width: 100%;
}
}
}
.azure-account-connection-status-container {
display: flex;
flex-direction: column;
gap: 16px;
.azure-account-connection-status-content {
width: 100%;
.azure-account-connection-status-callout {
width: 100%;
[data-slot='callout-title'] {
font-size: 13px;
font-weight: 400 !important;
line-height: 22px; /* 157.143% */
letter-spacing: -0.07px;
}
}
}
.azure-account-connection-status-close-disclosure {
color: var(--Vanilla-400, #c0c1c3);
font-family: Inter;
font-size: 12px;
font-style: normal;
font-weight: 400;
line-height: 20px; /* 142.857% */
}
}
}
.azure-account-form {
.azure-account-configure-agent-step-primary-region {
display: flex;
flex-direction: column;
gap: 8px;
.azure-account-configure-agent-step-primary-region-title {
color: var(--Vanilla-100, #fff);
font-family: Inter;
font-size: 13px;
font-style: normal;
font-weight: 400;
line-height: 20px; /* 150% */
letter-spacing: -0.06px;
}
.azure-account-configure-agent-step-primary-region-select {
display: flex;
flex-direction: column;
gap: 8px;
}
}
.azure-account-configure-agent-step-resource-groups {
display: flex;
flex-direction: column;
gap: 8px;
.azure-account-configure-agent-step-resource-groups-title {
color: var(--Vanilla-100, #fff);
font-family: Inter;
font-size: 13px;
font-style: normal;
font-weight: 400;
line-height: 20px; /* 150% */
letter-spacing: -0.06px;
}
.azure-account-configure-agent-step-resource-groups-select {
display: flex;
flex-direction: column;
gap: 8px;
}
}
.azure-account-actions-container {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
gap: 8px;
}
}
.ant-select {
.ant-select-selector {
background: var(--Ink-400, #121317);
border: 1px solid var(--Slate-500, #161922);
border-radius: 6px;
box-shadow: 0 4px 12px 0 rgba(0, 0, 0, 0.1);
}
.ant-select-selection-item {
color: var(--Vanilla-100, #fff);
}
.ant-select-arrow {
color: var(--Vanilla-100, #fff);
}
.ant-select-dropdown {
background: var(--Ink-400, #121317);
border: 1px solid var(--Slate-500, #161922);
border-radius: 6px;
box-shadow: 0 4px 12px 0 rgba(0, 0, 0, 0.1);
}
.ant-select-item {
color: var(--Vanilla-100, #fff);
}
.ant-select-item-option-active {
background: var(--Ink-400, #121317);
}
}
.azure-account-disconnect-container {
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: center;
gap: 8px;
padding: 16px 0px;
}
}
.azure-account-disconnect-modal {
.ant-modal-content {
width: 480px;
min-height: 200px;
flex-shrink: 0;
background: var(--bg-ink-400);
color: var(--bg-vanilla-100);
border: 1px solid var(--bg-slate-500);
border-radius: 4px;
box-shadow: 0px -4px 16px 2px rgba(0, 0, 0, 0.2);
padding: 12px;
}
.ant-modal-header {
background: var(--bg-ink-400);
color: var(--bg-vanilla-100);
.ant-modal-title {
color: var(--bg-vanilla-100);
}
}
.ant-modal-body {
margin-top: 16px;
}
.ant-modal-close {
color: var(--bg-vanilla-100);
}
.ant-modal-footer {
margin-top: 32px;
}
}
.lightMode {
.azure-account-disconnect-container {
border-color: var(--bg-vanilla-300);
background: var(--bg-vanilla-100);
}
}

View File

@@ -0,0 +1,218 @@
import { useCallback, useState } from 'react';
import { useMutation, useQueryClient } from 'react-query';
import { Button } from '@signozhq/button';
import { toast } from '@signozhq/sonner';
import { Form, Select } from 'antd';
import { Modal } from 'antd/lib';
import { removeIntegrationAccount } from 'api/integration';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import {
AZURE_REGIONS,
INTEGRATION_TYPES,
} from 'container/Integrations/constants';
import {
AzureCloudAccountConfig,
CloudAccount,
} from 'container/Integrations/types';
import { CornerDownRight, Unlink } from 'lucide-react';
import { ConnectionParams } from 'types/api/integrations/types';
interface AzureAccountFormProps {
mode?: 'add' | 'edit';
selectedAccount: CloudAccount | null;
connectionParams: ConnectionParams;
isConnectionParamsLoading: boolean;
isLoading: boolean;
onSubmit: (values: {
primaryRegion: string;
resourceGroups: string[];
}) => void;
submitButtonText?: string;
showDisconnectAccountButton?: boolean;
}
export const AzureAccountForm = ({
mode = 'add',
selectedAccount,
connectionParams,
isConnectionParamsLoading,
isLoading,
onSubmit,
submitButtonText = 'Fetch Deployment Command',
showDisconnectAccountButton = false,
}: AzureAccountFormProps): JSX.Element => {
const [azureAccountForm] = Form.useForm();
const queryClient = useQueryClient();
const [isModalOpen, setIsModalOpen] = useState(false);
const handleSubmit = useCallback((): void => {
azureAccountForm
.validateFields()
.then((values) => {
onSubmit({
primaryRegion: values.primaryRegion,
resourceGroups: values.resourceGroups,
});
})
.catch((error) => {
console.error('Form submission failed:', error);
});
}, [azureAccountForm, onSubmit]);
const handleDisconnect = (): void => {
setIsModalOpen(true);
};
const {
mutate: removeIntegration,
isLoading: isRemoveIntegrationLoading,
} = useMutation(removeIntegrationAccount, {
onSuccess: () => {
toast.success('Azure account disconnected successfully', {
description: 'Azure account disconnected successfully',
position: 'top-right',
duration: 3000,
});
queryClient.invalidateQueries([REACT_QUERY_KEY.CLOUD_INTEGRATION_ACCOUNTS]);
setIsModalOpen(false);
},
onError: (error) => {
console.error('Failed to remove integration:', error);
},
});
const handleOk = (): void => {
removeIntegration({
cloudServiceId: INTEGRATION_TYPES.AZURE,
accountId: selectedAccount?.id as string,
});
};
const handleCancel = (): void => {
setIsModalOpen(false);
};
return (
<Form
name="azure-account-form"
className="azure-account-form"
form={azureAccountForm}
layout="vertical"
autoComplete="off"
initialValues={{
primaryRegion:
(selectedAccount?.config as AzureCloudAccountConfig)?.deployment_region ||
undefined,
resourceGroups:
(selectedAccount?.config as AzureCloudAccountConfig)?.resource_groups ||
[],
}}
>
<div className="azure-account-configure-agent-step-primary-region">
<div className="azure-account-configure-agent-step-primary-region-title">
Select primary region
</div>
<div className="azure-account-configure-agent-step-primary-region-select">
<Form.Item
name="primaryRegion"
rules={[{ required: true, message: 'Please select a primary region' }]}
>
<Select
disabled={mode === 'edit'}
placeholder="Select primary region"
options={AZURE_REGIONS}
showSearch
filterOption={(input, option): boolean =>
option?.label?.toLowerCase().includes(input.toLowerCase()) ||
option?.value?.toLowerCase().includes(input.toLowerCase()) ||
false
}
notFoundContent={null}
/>
</Form.Item>
</div>
</div>
<div className="azure-account-configure-agent-step-resource-groups">
<div className="azure-account-configure-agent-step-resource-groups-title">
Enter resource groups you want to monitor
</div>
<div className="azure-account-configure-agent-step-resource-groups-select">
<Form.Item
name="resourceGroups"
rules={[
{
required: true,
message: 'Please enter resource groups you want to monitor',
},
]}
>
<Select
placeholder="Enter resource groups you want to monitor"
options={[]}
mode="tags"
notFoundContent={null}
filterOption={false}
showSearch={false}
/>
</Form.Item>
</div>
</div>
<div className="azure-account-actions-container">
{showDisconnectAccountButton && (
<div className="azure-account-disconnect-container">
<Button
variant="solid"
color="destructive"
prefixIcon={<Unlink size={14} />}
size="sm"
onClick={handleDisconnect}
disabled={isRemoveIntegrationLoading}
>
Disconnect
</Button>
</div>
)}
<Button
variant="solid"
color="primary"
onClick={handleSubmit}
size="sm"
prefixIcon={<CornerDownRight size={12} />}
loading={
isConnectionParamsLoading || isLoading || isRemoveIntegrationLoading
}
disabled={
isConnectionParamsLoading ||
!connectionParams ||
isLoading ||
isRemoveIntegrationLoading
}
>
{submitButtonText}
</Button>
</div>
<Modal
className="azure-account-disconnect-modal"
open={isModalOpen}
title="Remove integration"
onOk={handleOk}
onCancel={handleCancel}
okText="Remove Integration"
okButtonProps={{
danger: true,
}}
>
<div className="remove-integration-modal-content">
Removing this account will remove all components created for sending
telemetry to SigNoz in your Azure account within the next ~15 minutes
</div>
</Modal>
</Form>
);
};

View File

@@ -0,0 +1,379 @@
import { useEffect, useMemo, useRef, useState } from 'react';
import { Callout } from '@signozhq/callout';
import { toast } from '@signozhq/sonner';
import Tabs from '@signozhq/tabs';
import { Steps } from 'antd';
import { StepsProps } from 'antd/lib';
import logEvent from 'api/common/logEvent';
import { getAzureDeploymentCommands } from 'api/integration';
import { CodeBlock } from 'components/CodeBlock';
import { INTEGRATION_TYPES } from 'container/Integrations/constants';
import { useGetAccountStatus } from 'hooks/integration/useGetAccountStatus';
import { ChevronDown, ChevronRight } from 'lucide-react';
import {
AccountStatusResponse,
ConnectionParams,
IAzureDeploymentCommands,
} from 'types/api/integrations/types';
import { AzureAccountForm } from './AzureAccountForm';
import './AzureAccount.styles.scss';
interface ConnectNewAzureAccountProps {
connectionParams: ConnectionParams;
isConnectionParamsLoading: boolean;
onAccountConnected: () => void;
}
const PrerequisitesStep = (): JSX.Element => {
const [isHowItWorksOpen, setIsHowItWorksOpen] = useState(false);
const handleHowItWorksClick = (): void => {
setIsHowItWorksOpen(!isHowItWorksOpen);
};
return (
<div className="azure-account-prerequisites-step">
<div className="azure-account-prerequisites-step-description">
<div className="azure-account-prerequisites-step-description-item">
<span className="azure-account-prerequisites-step-description-item-bullet">
</span>{' '}
Ensure that youre logged in to the Azure portal or Azure CLI is setup for
your subscription
</div>
<div className="azure-account-prerequisites-step-description-item">
<span className="azure-account-prerequisites-step-description-item-bullet">
</span>{' '}
Ensure that you either have the OWNER role OR
</div>
<div className="azure-account-prerequisites-step-description-item">
<span className="azure-account-prerequisites-step-description-item-bullet">
</span>{' '}
Both the CONTRIBUTOR and USER ACCESS ADMIN roles.
</div>
</div>
<div className="azure-account-prerequisites-step-how-it-works">
<div
className="azure-account-prerequisites-step-how-it-works-title"
onClick={handleHowItWorksClick}
>
<div className="azure-account-prerequisites-step-how-it-works-title-icon">
{isHowItWorksOpen ? (
<ChevronDown size={16} />
) : (
<ChevronRight size={16} />
)}
</div>
<div className="azure-account-prerequisites-step-how-it-works-title-text">
How it works
</div>
</div>
{isHowItWorksOpen && (
<div className="azure-account-prerequisites-step-how-it-works-description">
<p>
SigNoz will create new resource-group to manage the resources required
for this integration. The following steps will create a User-Assigned
Managed Identity with the necessary permissions and follows the Principle
of Least Privilege.
</p>
<p>
Once the Integration template is deployed, you can enable the services
you want to monitor right here in SigNoz dashboard.
</p>
</div>
)}
</div>
</div>
);
};
const ConnectionSuccess = {
type: 'success' as const,
title: 'Agent has been deployed successfully.',
description: 'You can now safely close this panel.',
};
const ConnectionWarning = {
type: 'warning' as const,
title: 'Listening for data...',
description:
'Do not close this panel until the agent stack is deployed successfully.',
};
export const ConfigureAgentStep = ({
connectionParams,
isConnectionParamsLoading,
setDeploymentCommands,
setAccountId,
}: {
connectionParams: ConnectionParams;
isConnectionParamsLoading: boolean;
setDeploymentCommands: (deploymentCommands: IAzureDeploymentCommands) => void;
setAccountId: (accountId: string) => void;
}): JSX.Element => {
const [isFetchingDeploymentCommand, setIsFetchingDeploymentCommand] = useState(
false,
);
const getDeploymentCommand = async ({
primaryRegion,
resourceGroups,
}: {
primaryRegion: string;
resourceGroups: string[];
}): Promise<IAzureDeploymentCommands> => {
setIsFetchingDeploymentCommand(true);
return await getAzureDeploymentCommands({
agent_config: connectionParams,
account_config: {
deployment_region: primaryRegion,
resource_groups: resourceGroups,
},
});
};
const handleFetchDeploymentCommand = async ({
primaryRegion,
resourceGroups,
}: {
primaryRegion: string;
resourceGroups: string[];
}): Promise<void> => {
const deploymentCommands = await getDeploymentCommand({
primaryRegion,
resourceGroups,
});
setDeploymentCommands(deploymentCommands);
if (deploymentCommands.account_id) {
setAccountId(deploymentCommands.account_id);
}
setIsFetchingDeploymentCommand(false);
};
return (
<div className="azure-account-configure-agent-step">
<AzureAccountForm
selectedAccount={null}
connectionParams={connectionParams}
isConnectionParamsLoading={isConnectionParamsLoading}
onSubmit={handleFetchDeploymentCommand}
isLoading={isFetchingDeploymentCommand}
/>
</div>
);
};
const DeployAgentStep = ({
deploymentCommands,
accountId,
onAccountConnected,
}: {
deploymentCommands: IAzureDeploymentCommands | null;
accountId: string | null;
onAccountConnected: () => void;
}): JSX.Element => {
const [showConnectionStatus, setShowConnectionStatus] = useState(false);
const [isAccountConnected, setIsAccountConnected] = useState(false);
const COMMAND_PLACEHOLDER =
'// Select Primary Region and Resource Groups to fetch the deployment commands\n';
const handleCopyDeploymentCommand = (): void => {
setShowConnectionStatus(true);
};
const startTimeRef = useRef(Date.now());
const refetchInterval = 10 * 1000;
const errorTimeout = 10 * 60 * 1000;
useGetAccountStatus(INTEGRATION_TYPES.AZURE, accountId ?? undefined, {
refetchInterval,
enabled: !!accountId,
onSuccess: (data: AccountStatusResponse) => {
if (data.data.status.integration.last_heartbeat_ts_ms !== null) {
setIsAccountConnected(true);
setShowConnectionStatus(true);
onAccountConnected();
// setModalState(ModalStateEnum.SUCCESS);
toast.success('Azure Integration: Account connected', {
description: 'Azure Integration: Account connected',
position: 'top-right',
duration: 3000,
});
logEvent('Azure Integration: Account connected', {
cloudAccountId: data?.data?.cloud_account_id,
status: data?.data?.status,
});
} else if (Date.now() - startTimeRef.current >= errorTimeout) {
// setModalState(ModalStateEnum.ERROR);
toast.error('Azure Integration: Account connection attempt timed out', {
description: 'Azure Integration: Account connection attempt timed out',
position: 'top-right',
duration: 3000,
});
logEvent('Azure Integration: Account connection attempt timed out', {
id: deploymentCommands?.account_id,
});
}
},
onError: () => {
toast.error('Azure Integration: Account connection attempt timed out', {
description: 'Azure Integration: Account connection attempt timed out',
position: 'top-right',
duration: 3000,
});
},
});
useEffect(() => {
if (
deploymentCommands &&
(deploymentCommands.az_shell_connection_command ||
deploymentCommands.az_cli_connection_command)
) {
setTimeout(() => {
setShowConnectionStatus(true);
}, 3000);
}
}, [deploymentCommands]);
return (
<div className="azure-account-deploy-agent-step">
<div className="azure-account-deploy-agent-step-subtitle">
Copy the command and then use it to create the deployment stack.
</div>
<div className="azure-account-deploy-agent-step-commands">
<Tabs
className="azure-account-deploy-agent-step-commands-tabs"
defaultValue="azure-shell"
items={[
{
key: 'azure-shell',
label: 'Azure Shell',
children: (
<div className="azure-account-deploy-agent-step-commands-tabs-content">
<CodeBlock
language="typescript"
value={
deploymentCommands?.az_shell_connection_command ||
COMMAND_PLACEHOLDER
}
onCopy={handleCopyDeploymentCommand}
/>
</div>
),
},
{
key: 'azure-sdk',
label: 'Azure SDK',
children: (
<div className="azure-account-deploy-agent-step-commands-tabs-content">
<CodeBlock
language="typescript"
value={
deploymentCommands?.az_cli_connection_command || COMMAND_PLACEHOLDER
}
onCopy={handleCopyDeploymentCommand}
/>
</div>
),
},
]}
variant="primary"
/>
</div>
{showConnectionStatus && (
<div className="azure-account-connection-status-container">
<div className="azure-account-connection-status-content">
<Callout
className="azure-account-connection-status-callout"
type={
isAccountConnected ? ConnectionSuccess.type : ConnectionWarning.type
}
size="small"
showIcon
message={
isAccountConnected ? ConnectionSuccess.title : ConnectionWarning.title
}
/>
</div>
<div className="azure-account-connection-status-close-disclosure">
{isAccountConnected
? ConnectionSuccess.description
: ConnectionWarning.description}
</div>
</div>
)}
</div>
);
};
export default function ConnectNewAzureAccount({
connectionParams,
isConnectionParamsLoading,
onAccountConnected,
}: ConnectNewAzureAccountProps): JSX.Element {
const [
deploymentCommands,
setDeploymentCommands,
] = useState<IAzureDeploymentCommands | null>(null);
const [accountId, setAccountId] = useState<string | null>(null);
const steps = useMemo(() => {
const steps: StepsProps['items'] = [
{
title: 'Prerequisites',
description: <PrerequisitesStep />,
},
{
title: 'Configure Agent',
description: (
<ConfigureAgentStep
connectionParams={connectionParams}
isConnectionParamsLoading={isConnectionParamsLoading}
setDeploymentCommands={setDeploymentCommands}
setAccountId={setAccountId}
/>
),
},
{
title: 'Deploy Agent',
description: (
<DeployAgentStep
deploymentCommands={deploymentCommands}
accountId={accountId}
onAccountConnected={onAccountConnected}
/>
),
},
];
return steps;
}, [
connectionParams,
isConnectionParamsLoading,
deploymentCommands,
accountId,
onAccountConnected,
]);
return (
<div className="azure-account-container">
<Steps direction="vertical" current={1} items={steps} />
</div>
);
}

View File

@@ -0,0 +1,90 @@
import { useCallback } from 'react';
import { toast } from '@signozhq/sonner';
import { INTEGRATION_TYPES } from 'container/Integrations/constants';
import { CloudAccount } from 'container/Integrations/types';
import { useUpdateAccountConfig } from 'hooks/integration/useUpdateAccountConfig';
import {
AzureAccountConfig,
ConnectionParams,
} from 'types/api/integrations/types';
import { AzureAccountForm } from './AzureAccountForm';
import './AzureAccount.styles.scss';
interface EditAzureAccountProps {
selectedAccount: CloudAccount;
connectionParams: ConnectionParams;
isConnectionParamsLoading: boolean;
onAccountUpdated: () => void;
}
function EditAzureAccount({
selectedAccount,
connectionParams,
isConnectionParamsLoading,
onAccountUpdated,
}: EditAzureAccountProps): JSX.Element {
const {
mutate: updateAzureAccountConfig,
isLoading,
} = useUpdateAccountConfig();
const handleSubmit = useCallback(
async ({
primaryRegion,
resourceGroups,
}: {
primaryRegion: string;
resourceGroups: string[];
}): Promise<void> => {
try {
const payload: AzureAccountConfig = {
config: {
deployment_region: primaryRegion,
resource_groups: resourceGroups,
},
};
updateAzureAccountConfig(
{
cloudServiceId: INTEGRATION_TYPES.AZURE,
accountId: selectedAccount?.id,
payload,
},
{
onSuccess: () => {
toast.success('Success', {
description: 'Azure account updated successfully',
position: 'top-right',
duration: 3000,
});
onAccountUpdated();
},
},
);
} catch (error) {
console.error('Form submission failed:', error);
}
},
[updateAzureAccountConfig, selectedAccount?.id, onAccountUpdated],
);
return (
<div className="azure-account-container">
<AzureAccountForm
mode="edit"
selectedAccount={selectedAccount}
connectionParams={connectionParams}
isConnectionParamsLoading={isConnectionParamsLoading}
onSubmit={handleSubmit}
isLoading={isLoading}
submitButtonText="Save Changes"
showDisconnectAccountButton
/>
</div>
);
}
export default EditAzureAccount;

View File

@@ -0,0 +1,178 @@
.azure-service-details-container {
display: flex;
flex-direction: column;
width: 100%;
.azure-service-details-tabs {
margin-top: 8px;
// remove the padding left from the first div of the tabs component
// this needs to be handled in the tabs component
> div:first-child {
padding-left: 0;
}
.azure-service-details-data-collected-content-logs,
.azure-service-details-data-collected-content-metrics {
display: flex;
flex-direction: row;
gap: 8px;
.azure-service-details-data-collected-content-title {
display: flex;
flex-direction: row;
align-items: center;
gap: 8px;
color: var(--Vanilla-400, #c0c1c3);
/* Bifrost (Ancient)/Content/sm */
font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
}
}
.azure-service-details-overview {
display: flex;
flex-direction: column;
gap: 16px;
.azure-service-details-overview-configuration {
display: flex;
flex-direction: column;
border-radius: 4px;
border: 1px solid var(--Slate-400, #1d212d);
background: var(--Ink-400, #121317);
.azure-service-details-overview-configuration-title {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
gap: 8px;
width: 100%;
border-radius: 4px 4px 0 0;
border-bottom: 1px solid var(--Slate-400, #1d212d);
background: rgba(171, 189, 255, 0.04);
padding: 8px 12px;
.azure-service-details-overview-configuration-title-text {
color: var(--Vanilla-100, #fff);
font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 22px; /* 157.143% */
letter-spacing: -0.07px;
}
.configuration-action {
display: flex;
flex-direction: row;
align-items: center;
gap: 8px;
}
}
.azure-service-details-overview-configuration-content {
display: flex;
flex-direction: column;
gap: 16px;
padding: 12px;
background: var(--Ink-400, #121317);
.azure-service-details-overview-configuration-content-item {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
gap: 8px;
}
}
.azure-service-details-overview-configuration-actions {
display: flex;
flex-direction: row;
justify-content: flex-end;
gap: 8px;
padding: 8px 12px;
border-top: 1px solid var(--Slate-400, #1d212d);
background: var(--Ink-400, #121317);
}
}
.azure-service-details-actions {
display: flex;
flex-direction: row;
gap: 8px;
padding: 12px 0;
}
.azure-service-dashboards {
border-radius: 6px;
border: 1px solid var(--Slate-400, #1d212d);
background: var(--Ink-400, #121317);
box-shadow: 0 4px 12px 0 rgba(0, 0, 0, 0.1);
.azure-service-dashboards-title {
display: flex;
flex-direction: row;
align-items: center;
gap: 8px;
color: var(--Vanilla-400, #c0c1c3);
font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 500;
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
padding: 8px 16px;
border-bottom: 1px solid var(--Slate-400, #1d212d);
}
.azure-service-dashboards-items {
display: flex;
flex-direction: column;
gap: 16px;
.azure-service-dashboard-item {
display: flex;
flex-direction: column;
gap: 8px;
padding: 12px 16px 12px 16px;
.azure-service-dashboard-item-title {
color: var(--Vanilla-100, #f0f1f2);
font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 500;
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
}
.azure-service-dashboard-item-description {
color: var(--Vanilla-400, #c0c1c3);
font-family: Inter;
font-size: 12px;
font-style: normal;
font-weight: 400;
line-height: 18px; /* 150% */
}
}
}
}
}
}
}

View File

@@ -0,0 +1,393 @@
import { useEffect, useState } from 'react';
import { useQueryClient } from 'react-query';
import { Button } from '@signozhq/button';
import Tabs from '@signozhq/tabs';
import { Checkbox, Skeleton } from 'antd';
import CloudServiceDataCollected from 'components/CloudIntegrations/CloudServiceDataCollected/CloudServiceDataCollected';
import { MarkdownRenderer } from 'components/MarkdownRenderer/MarkdownRenderer';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import { INTEGRATION_TYPES } from 'container/Integrations/constants';
import { AzureConfig, AzureService } from 'container/Integrations/types';
import { useGetCloudIntegrationServiceDetails } from 'hooks/integration/useServiceDetails';
import { useUpdateServiceConfig } from 'hooks/integration/useUpdateServiceConfig';
import { isEqual } from 'lodash-es';
import { Save, X } from 'lucide-react';
import './AzureServiceDetails.styles.scss';
interface AzureServiceDetailsProps {
selectedService: AzureService | null;
cloudAccountId: string;
}
function configToMap(
config: AzureConfig[] | undefined,
): { [key: string]: boolean } {
return (config || []).reduce(
(acc: { [key: string]: boolean }, item: AzureConfig) => {
acc[item.name] = item.enabled;
return acc;
},
{},
);
}
export default function AzureServiceDetails({
selectedService,
cloudAccountId,
}: AzureServiceDetailsProps): JSX.Element {
const queryClient = useQueryClient();
const {
data: serviceDetailsData,
isLoading,
refetch: refetchServiceDetails,
} = useGetCloudIntegrationServiceDetails(
INTEGRATION_TYPES.AZURE,
selectedService?.id || '',
cloudAccountId || undefined,
);
const {
mutate: updateAzureServiceConfig,
isLoading: isUpdating,
} = useUpdateServiceConfig();
// Last saved/committed config — updated when data loads and on save success.
// Used for hasChanges and Discard so buttons hide immediately after save.
const [lastSavedSnapshot, setLastSavedSnapshot] = useState<{
logs: { [key: string]: boolean };
metrics: { [key: string]: boolean };
}>({ logs: {}, metrics: {} });
// Editable state
const [azureLogsEnabledAll, setAzureLogsEnabledAll] = useState<boolean>(false);
const [azureMetricsEnabledAll, setAzureMetricsEnabledAll] = useState<boolean>(
false,
);
const [logsConfig, updateLogsConfig] = useState<{ [key: string]: boolean }>(
{},
);
const [metricsConfigs, updateMetricsConfigs] = useState<{
[key: string]: boolean;
}>({});
// Sync state when serviceDetailsData loads
useEffect(() => {
if (!serviceDetailsData?.config) {
return;
}
const logs = configToMap(serviceDetailsData.config.logs as AzureConfig[]);
const metrics = configToMap(
serviceDetailsData.config.metrics as AzureConfig[],
);
if (Object.keys(logs).length > 0) {
updateLogsConfig(logs);
setAzureLogsEnabledAll(
!(serviceDetailsData.config.logs as AzureConfig[])?.some(
(log: AzureConfig) => !log.enabled,
),
);
}
if (Object.keys(metrics).length > 0) {
updateMetricsConfigs(metrics);
setAzureMetricsEnabledAll(
!(serviceDetailsData.config.metrics as AzureConfig[])?.some(
(metric: AzureConfig) => !metric.enabled,
),
);
}
setLastSavedSnapshot({ logs, metrics });
}, [serviceDetailsData]);
const hasChanges =
!isEqual(logsConfig, lastSavedSnapshot.logs) ||
!isEqual(metricsConfigs, lastSavedSnapshot.metrics);
const handleSave = (): void => {
if (!selectedService?.id) {
return;
}
updateAzureServiceConfig(
{
cloudServiceId: INTEGRATION_TYPES.AZURE,
serviceId: selectedService?.id,
payload: {
cloud_account_id: cloudAccountId,
config: {
logs: Object.entries(logsConfig).map(([name, enabled]) => ({
name,
enabled,
})),
metrics: Object.entries(metricsConfigs).map(([name, enabled]) => ({
name,
enabled,
})),
},
},
},
{
onSuccess: (_, variables) => {
// Update snapshot immediately from what we saved (not current state)
const saved = variables.payload.config;
setLastSavedSnapshot({
logs: configToMap(saved.logs),
metrics: configToMap(saved.metrics),
});
queryClient.invalidateQueries([
REACT_QUERY_KEY.AWS_SERVICE_DETAILS,
selectedService?.id,
cloudAccountId,
]);
// Invalidate services list so Enabled/Not Enabled stays in sync
queryClient.invalidateQueries([INTEGRATION_TYPES.AZURE]);
refetchServiceDetails();
},
},
);
};
const handleDiscard = (): void => {
updateLogsConfig(lastSavedSnapshot.logs);
updateMetricsConfigs(lastSavedSnapshot.metrics);
setAzureLogsEnabledAll(
Object.values(lastSavedSnapshot.logs).every(Boolean) &&
Object.keys(lastSavedSnapshot.logs).length > 0,
);
setAzureMetricsEnabledAll(
Object.values(lastSavedSnapshot.metrics).every(Boolean) &&
Object.keys(lastSavedSnapshot.metrics).length > 0,
);
};
const handleAzureLogsEnableAllChange = (checked: boolean): void => {
setAzureLogsEnabledAll(checked);
updateLogsConfig((prev) =>
Object.fromEntries(Object.keys(prev).map((key) => [key, checked])),
);
};
const handleAzureMetricsEnableAllChange = (checked: boolean): void => {
setAzureMetricsEnabledAll(checked);
updateMetricsConfigs((prev) =>
Object.fromEntries(Object.keys(prev).map((key) => [key, checked])),
);
};
const handleAzureLogsEnabledChange = (
logName: string,
checked: boolean,
): void => {
updateLogsConfig((prev) => ({ ...prev, [logName]: checked }));
};
const handleAzureMetricsEnabledChange = (
metricName: string,
checked: boolean,
): void => {
updateMetricsConfigs((prev) => ({ ...prev, [metricName]: checked }));
};
// Keep "enable all" in sync when individual items change
useEffect(() => {
if (Object.keys(logsConfig).length > 0) {
const allEnabled = Object.values(logsConfig).every(Boolean);
setAzureLogsEnabledAll(allEnabled);
}
}, [logsConfig]);
useEffect(() => {
if (Object.keys(metricsConfigs).length > 0) {
const allEnabled = Object.values(metricsConfigs).every(Boolean);
setAzureMetricsEnabledAll(allEnabled);
}
}, [metricsConfigs]);
const renderOverview = (): JSX.Element => {
const dashboards = serviceDetailsData?.assets?.dashboards || [];
if (isLoading) {
return (
<div className="azure-service-details-overview-loading">
<Skeleton active />
</div>
);
}
return (
<div className="azure-service-details-overview">
{!isLoading && (
<div className="azure-service-details-overview-configuration">
<div className="azure-service-details-overview-configuration-logs">
<div className="azure-service-details-overview-configuration-title">
<div className="azure-service-details-overview-configuration-title-text">
Azure Logs
</div>
<div className="configuration-action">
<Checkbox
checked={azureLogsEnabledAll}
indeterminate={
Object.values(logsConfig).some(Boolean) &&
!Object.values(logsConfig).every(Boolean)
}
onChange={(e): void =>
handleAzureLogsEnableAllChange(e.target.checked)
}
disabled={isUpdating}
/>
</div>
</div>
<div className="azure-service-details-overview-configuration-content">
{logsConfig &&
Object.keys(logsConfig).length > 0 &&
Object.keys(logsConfig).map((logName: string) => (
<div
key={logName}
className="azure-service-details-overview-configuration-content-item"
>
<div className="azure-service-details-overview-configuration-content-item-text">
{logName}
</div>
<Checkbox
checked={logsConfig[logName]}
onChange={(e): void =>
handleAzureLogsEnabledChange(logName, e.target.checked)
}
disabled={isUpdating}
/>
</div>
))}
</div>
</div>
<div className="azure-service-details-overview-configuration-metrics">
<div className="azure-service-details-overview-configuration-title">
<div className="azure-service-details-overview-configuration-title-text">
Azure Metrics
</div>
<div className="configuration-action">
<Checkbox
checked={azureMetricsEnabledAll}
indeterminate={
Object.values(metricsConfigs).some(Boolean) &&
!Object.values(metricsConfigs).every(Boolean)
}
onChange={(e): void =>
handleAzureMetricsEnableAllChange(e.target.checked)
}
disabled={isUpdating}
/>
</div>
</div>
<div className="azure-service-details-overview-configuration-content">
{metricsConfigs &&
Object.keys(metricsConfigs).length > 0 &&
Object.keys(metricsConfigs).map((metricName: string) => (
<div
key={metricName}
className="azure-service-details-overview-configuration-content-item"
>
<div className="azure-service-details-overview-configuration-content-item-text">
{metricName}
</div>
<Checkbox
checked={metricsConfigs[metricName]}
onChange={(e): void =>
handleAzureMetricsEnabledChange(metricName, e.target.checked)
}
disabled={isUpdating}
/>
</div>
))}
</div>
</div>
{hasChanges && (
<div className="azure-service-details-overview-configuration-actions">
<Button
variant="solid"
color="secondary"
onClick={handleDiscard}
disabled={isUpdating}
size="xs"
prefixIcon={<X size={14} />}
>
Discard
</Button>
<Button
variant="solid"
color="primary"
onClick={handleSave}
loading={isUpdating}
disabled={isUpdating}
size="xs"
prefixIcon={<Save size={14} />}
>
Save
</Button>
</div>
)}
</div>
)}
<MarkdownRenderer
variables={{}}
markdownContent={serviceDetailsData?.overview}
/>
<div className="azure-service-dashboards">
<div className="azure-service-dashboards-title">Dashboards</div>
<div className="azure-service-dashboards-items">
{dashboards.map((dashboard) => (
<div key={dashboard.id} className="azure-service-dashboard-item">
<div className="azure-service-dashboard-item-title">
{dashboard.title}
</div>
<div className="azure-service-dashboard-item-description">
{dashboard.description}
</div>
</div>
))}
</div>
</div>
</div>
);
};
const renderDataCollected = (): JSX.Element => {
return (
<div className="azure-service-details-data-collected-table">
<CloudServiceDataCollected
logsData={serviceDetailsData?.data_collected?.logs || []}
metricsData={serviceDetailsData?.data_collected?.metrics || []}
/>
</div>
);
};
return (
<div className="azure-service-details-container">
<Tabs
defaultValue="overview"
className="azure-service-details-tabs"
items={[
{
children: renderOverview(),
key: 'overview',
label: 'Overview',
},
{
children: renderDataCollected(),
key: 'data-collected',
label: 'Data Collected',
},
]}
variant="secondary"
/>
</div>
);
}

View File

@@ -0,0 +1,178 @@
.azure-services-container {
display: flex;
flex-direction: column;
height: calc(100% - 52px);
position: relative;
.azure-services-content {
height: 100%;
position: relative;
.azure-services-list-header {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
padding: 6px 10px;
height: 40px;
border-bottom: 1px solid var(--bg-slate-400);
.azure-services-views-btn-group {
border: 1px solid var(--bg-slate-400);
border-radius: 4px;
display: flex;
align-items: center;
.azure-services-views-btn {
&:first-child {
border-radius: 4px 0px 0px 4px;
}
&:last-child {
border-radius: 0px 4px 4px 0px;
}
&:hover {
background-color: var(--bg-slate-400);
}
&.active {
background-color: var(--bg-slate-400);
}
}
}
}
.azure-services-list-section {
height: 100%;
position: relative;
.azure-services-list-section-content {
display: flex;
flex-direction: row;
width: 100%;
height: 100%;
}
.azure-services-list-section-loading-skeleton {
display: flex;
flex-direction: row;
width: 100%;
.azure-services-list-section-loading-skeleton-sidebar {
width: 240px;
padding: 12px;
display: flex;
flex-direction: column;
gap: 24px;
}
.azure-services-list-section-loading-skeleton-main {
flex: 1;
padding: 12px;
display: flex;
flex-direction: column;
gap: 24px;
}
}
.azure-services-list-view {
height: 100%;
.azure-services-list-view-sidebar {
width: 240px;
height: 100%;
border-right: 1px solid var(--Slate-500, #161922);
padding: 12px;
.azure-services-list-view-sidebar-content {
display: flex;
flex-direction: column;
gap: 8px;
.azure-services-enabled {
display: flex;
flex-direction: column;
gap: 8px;
}
.azure-services-not-enabled {
display: flex;
flex-direction: column;
gap: 8px;
}
.azure-services-list-view-sidebar-content-header {
color: var(--Slate-50, #62687c);
font-family: Inter;
font-size: 11px;
font-style: normal;
font-weight: 600;
line-height: 18px; /* 163.636% */
letter-spacing: 0.44px;
text-transform: uppercase;
}
.azure-services-list-view-sidebar-content-item {
display: flex;
flex-direction: row;
align-items: center;
gap: 10px;
padding: 4px 12px;
border-radius: 4px;
cursor: pointer;
.azure-services-list-view-sidebar-content-item-icon {
width: 20px;
height: 20px;
}
.azure-services-list-view-sidebar-content-item-title {
color: var(--Vanilla-100, #fff);
font-family: Inter;
font-size: 13px;
font-style: normal;
font-weight: 400;
line-height: 18px; /* 128.571% */
}
&:hover {
color: var(--Vanilla-100, #fff);
font-family: Inter;
font-size: 13px;
font-style: normal;
font-weight: 400;
line-height: 18px; /* 128.571% */
}
&.active {
color: var(--Vanilla-100, #fff);
font-family: Inter;
font-size: 13px;
font-style: normal;
font-weight: 400;
line-height: 18px; /* 128.571% */
background-color: var(--bg-slate-400);
}
}
}
}
.azure-services-list-view-main {
flex: 1;
padding: 12px;
}
}
.azure-services-grid-view {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
}
}
}
}

View File

@@ -0,0 +1,70 @@
import cx from 'classnames';
import { AzureService } from 'container/Integrations/types';
interface AzureServicesListViewProps {
selectedService: AzureService | null;
enabledServices: AzureService[];
notEnabledServices: AzureService[];
onSelectService: (service: AzureService) => void;
}
export default function AzureServicesListView({
selectedService,
enabledServices,
notEnabledServices,
onSelectService,
}: AzureServicesListViewProps): JSX.Element {
const isEnabledServicesEmpty = enabledServices.length === 0;
const isNotEnabledServicesEmpty = notEnabledServices.length === 0;
const renderServiceItem = (service: AzureService): JSX.Element => {
return (
<div
className={cx('azure-services-list-view-sidebar-content-item', {
active: service.id === selectedService?.id,
})}
key={service.id}
onClick={(): void => onSelectService(service)}
>
<img
src={service.icon}
alt={service.title}
className="azure-services-list-view-sidebar-content-item-icon"
/>
<div className="azure-services-list-view-sidebar-content-item-title">
{service.title}
</div>
</div>
);
};
return (
<div className="azure-services-list-view">
<div className="azure-services-list-view-sidebar">
<div className="azure-services-list-view-sidebar-content">
<div className="azure-services-enabled">
<div className="azure-services-list-view-sidebar-content-header">
Enabled
</div>
{enabledServices.map((service) => renderServiceItem(service))}
{isEnabledServicesEmpty && (
<div className="azure-services-list-view-sidebar-content-item-empty-message">
No enabled services
</div>
)}
</div>
{!isNotEnabledServicesEmpty && (
<div className="azure-services-not-enabled">
<div className="azure-services-list-view-sidebar-content-header">
Not Enabled
</div>
{notEnabledServices.map((service) => renderServiceItem(service))}
</div>
)}
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,137 @@
import { useEffect, useMemo, useState } from 'react';
import { Skeleton } from 'antd';
import CloudIntegrationsHeader from 'components/CloudIntegrations/CloudIntegrationsHeader';
import { INTEGRATION_TYPES } from 'container/Integrations/constants';
import {
AzureConfig,
AzureService,
CloudAccount,
IntegrationType,
} from 'container/Integrations/types';
import { useGetAccountServices } from 'hooks/integration/useGetAccountServices';
import { useGetCloudIntegrationAccounts } from 'hooks/integration/useGetCloudIntegrationAccounts';
import useUrlQuery from 'hooks/useUrlQuery';
import { getAccountById } from '../utils';
import AzureServiceDetails from './AzureServiceDetails/AzureServiceDetails';
import AzureServicesListView from './AzureServicesListView';
import './AzureServices.styles.scss';
/** Service is enabled if even one sub item (log or metric) is enabled */
function hasAnySubItemEnabled(service: AzureService): boolean {
const logs = service.config?.logs ?? [];
const metrics = service.config?.metrics ?? [];
return (
logs.some((log: AzureConfig) => log.enabled) ||
metrics.some((metric: AzureConfig) => metric.enabled)
);
}
function AzureServices(): JSX.Element {
const urlQuery = useUrlQuery();
const [selectedAccount, setSelectedAccount] = useState<CloudAccount | null>(
null,
);
const [selectedService, setSelectedService] = useState<AzureService | null>(
null,
);
const {
data: accounts = [],
isLoading: isLoadingAccounts,
isFetching: isFetchingAccounts,
refetch: refetchAccounts,
} = useGetCloudIntegrationAccounts(INTEGRATION_TYPES.AZURE);
const initialAccount = useMemo(
() =>
accounts?.length
? getAccountById(accounts, urlQuery.get('cloudAccountId') || '') ||
accounts[0]
: null,
[accounts, urlQuery],
);
// Sync selectedAccount with initialAccount when accounts load (enables Subscription ID display)
// Cast: hook returns AWS-typed CloudAccount[] but AZURE fetch returns Azure-shaped accounts
useEffect(() => {
setSelectedAccount(initialAccount as CloudAccount | null);
}, [initialAccount]);
const cloudAccountId = initialAccount?.cloud_account_id;
const {
data: azureServices = [],
isLoading: isLoadingAzureServices,
} = useGetAccountServices(INTEGRATION_TYPES.AZURE, cloudAccountId);
const enabledServices = useMemo(
() => azureServices?.filter(hasAnySubItemEnabled) ?? [],
[azureServices],
);
// Derive from enabled to guarantee each service is in exactly one list
const enabledIds = useMemo(() => new Set(enabledServices.map((s) => s.id)), [
enabledServices,
]);
const notEnabledServices = useMemo(
() => azureServices?.filter((s) => !enabledIds.has(s.id)) ?? [],
[azureServices, enabledIds],
);
useEffect(() => {
if (enabledServices.length > 0) {
setSelectedService(enabledServices[0]);
} else if (notEnabledServices.length > 0) {
setSelectedService(notEnabledServices[0]);
}
}, [enabledServices, notEnabledServices]);
return (
<div className="azure-services-container">
<CloudIntegrationsHeader
cloudServiceId={IntegrationType.AZURE_SERVICES}
selectedAccount={selectedAccount}
accounts={accounts}
isLoadingAccounts={isLoadingAccounts}
onSelectAccount={setSelectedAccount}
refetchAccounts={refetchAccounts}
/>
<div className="azure-services-content">
<div className="azure-services-list-section">
{(isLoadingAzureServices || isFetchingAccounts) && (
<div className="azure-services-list-section-loading-skeleton">
<div className="azure-services-list-section-loading-skeleton-sidebar">
<Skeleton active />
<Skeleton active />
</div>
<div className="azure-services-list-section-loading-skeleton-main">
<Skeleton active />
<Skeleton active />
<Skeleton active />
</div>
</div>
)}
{!isLoadingAzureServices && !isFetchingAccounts && (
<div className="azure-services-list-section-content">
<AzureServicesListView
selectedService={selectedService}
enabledServices={enabledServices}
notEnabledServices={notEnabledServices}
onSelectService={setSelectedService}
/>
<AzureServiceDetails
selectedService={selectedService}
cloudAccountId={selectedAccount?.cloud_account_id || ''}
/>
</div>
)}
</div>
</div>
</div>
);
}
export default AzureServices;

View File

@@ -0,0 +1,3 @@
.cloud-integration-container {
height: 100%;
}

View File

@@ -0,0 +1,24 @@
// import { RequestIntegrationBtn } from 'container/Integrations/RequestIntegrationBtn';
import { IntegrationType } from 'container/Integrations/types';
import AWSTabs from './AmazonWebServices/ServicesTabs';
import AzureServices from './AzureServices';
import Header from './Header/Header';
import './CloudIntegration.styles.scss';
const CloudIntegration = ({ type }: { type: IntegrationType }): JSX.Element => {
return (
<div className="cloud-integration-container">
<Header title={type} />
{/* <RequestIntegrationBtn
type={type}
message="Can't find the service you're looking for? Request more integrations"
/> */}
{type === IntegrationType.AWS_SERVICES && <AWSTabs />}
{type === IntegrationType.AZURE_SERVICES && <AzureServices />}
</div>
);
};
export default CloudIntegration;

View File

@@ -48,7 +48,7 @@
.lightMode {
.cloud-header {
border-bottom: 1px solid var(--bg-slate-300);
border-bottom: 1px solid var(--bg-vanilla-300);
&__breadcrumb-title {
color: var(--bg-ink-400);

View File

@@ -1,11 +1,13 @@
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(): JSX.Element {
function Header({ title }: { title: IntegrationType }): JSX.Element {
return (
<div className="cloud-header">
<div className="cloud-header__navigation">
@@ -23,25 +25,26 @@ function Header(): JSX.Element {
),
},
{
title: (
<div className="cloud-header__breadcrumb-title">
Amazon Web Services
</div>
),
title: <div className="cloud-header__breadcrumb-title">{title}</div>,
},
]}
/>
</div>
<div className="cloud-header__actions">
<a
href="https://signoz.io/blog/native-aws-integrations-with-autodiscovery/"
target="_blank"
rel="noopener noreferrer"
className="cloud-header__help"
<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} />}
>
<LifeBuoy size={12} />
Get Help
</a>
</Button>
</div>
</div>
);

View File

@@ -1,14 +1,12 @@
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('Request AWS integration', () => {
describe.skip('Request AWS integration', () => {
it('should render the request integration button', async () => {
let capturedPayload: any;
server.use(

View File

@@ -0,0 +1,5 @@
export const getAccountById = <T extends { cloud_account_id: string }>(
accounts: T[],
accountId: string,
): T | null =>
accounts.find((account) => account.cloud_account_id === accountId) || null;

View File

@@ -3,7 +3,7 @@ import { Button, Typography } from 'antd';
import logEvent from 'api/common/logEvent';
import cx from 'classnames';
import { MarkdownRenderer } from 'components/MarkdownRenderer/MarkdownRenderer';
import { INTEGRATION_TELEMETRY_EVENTS } from 'pages/Integrations/utils';
import { INTEGRATION_TELEMETRY_EVENTS } from 'container/Integrations/constants';
import './IntegrationDetailContentTabs.styles.scss';

View File

@@ -1,7 +1,7 @@
/* eslint-disable no-nested-ternary */
import { useState } from 'react';
import { useMutation } from 'react-query';
import { Button, Modal, Tooltip, Typography } from 'antd';
import { Button, Modal, Skeleton, Tooltip, Typography } from 'antd';
import logEvent from 'api/common/logEvent';
import installIntegration from 'api/Integrations/installIntegration';
import ConfigureIcon from 'assets/Integrations/ConfigureIcon';
@@ -10,10 +10,10 @@ import { SOMETHING_WENT_WRONG } from 'constants/api';
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
import dayjs from 'dayjs';
import { useNotifications } from 'hooks/useNotifications';
import { ArrowLeftRight, Check } from 'lucide-react';
import { ArrowLeftRight, Cable, Check } from 'lucide-react';
import { IntegrationConnectionStatus } from 'types/api/integrations/types';
import { INTEGRATION_TELEMETRY_EVENTS } from '../utils';
import { INTEGRATION_TELEMETRY_EVENTS } from '../constants';
import TestConnection, { ConnectionStates } from './TestConnection';
import './IntegrationDetailPage.styles.scss';
@@ -27,6 +27,7 @@ interface IntegrationDetailHeaderProps {
connectionState: ConnectionStates;
connectionData: IntegrationConnectionStatus;
setActiveDetailTab: React.Dispatch<React.SetStateAction<string | null>>;
isLoading: boolean;
}
// eslint-disable-next-line sonarjs/cognitive-complexity
function IntegrationDetailHeader(
@@ -39,6 +40,7 @@ function IntegrationDetailHeader(
description,
connectionState,
connectionData,
isLoading,
onUnInstallSuccess,
setActiveDetailTab,
} = props;
@@ -115,16 +117,29 @@ function IntegrationDetailHeader(
const isConnectionStateNotInstalled =
connectionState === ConnectionStates.NotInstalled;
return (
<div className="integration-connection-header">
<div className="integration-detail-header" key={id}>
<div style={{ display: 'flex', gap: '10px' }}>
<div className="integration-detail-header-icon-title-container">
<div className="image-container">
<img src={icon} alt={title} className="image" />
{icon ? (
<img src={icon} alt={title} className="image" />
) : (
<div className="image-placeholder">
<Cable size={24} />
</div>
)}
</div>
<div className="details">
<Typography.Text className="heading">{title}</Typography.Text>
<Typography.Text className="description">{description}</Typography.Text>
{isLoading ? (
<Skeleton.Input active className="skeleton-item" />
) : (
<>
<Typography.Text className="heading">{title}</Typography.Text>
<Typography.Text className="description">{description}</Typography.Text>
</>
)}
</div>
</div>
<Button
@@ -133,7 +148,7 @@ function IntegrationDetailHeader(
!isConnectionStateNotInstalled && 'test-connection',
)}
icon={<ArrowLeftRight size={14} />}
disabled={isInstallLoading}
disabled={isInstallLoading || isLoading}
onClick={(): void => {
if (connectionState === ConnectionStates.NotInstalled) {
logEvent(INTEGRATION_TELEMETRY_EVENTS.INTEGRATIONS_DETAIL_CONNECT, {

View File

@@ -1,323 +1,329 @@
.integration-detail-content {
.integration-details-container {
display: flex;
flex-direction: column;
gap: 16px;
margin: 12px 0px 20px 0px;
padding: 16px;
.error-container {
display: flex;
border-radius: 6px;
border: 1px solid var(--bg-slate-500);
background: var(--bg-ink-400);
align-items: center;
justify-content: center;
flex-direction: column;
.error-content {
display: flex;
flex-direction: column;
justify-content: center;
height: 300px;
gap: 15px;
.error-btns {
display: flex;
flex-direction: row;
gap: 16px;
align-items: center;
.retry-btn {
display: flex;
align-items: center;
}
.contact-support {
display: flex;
align-items: center;
gap: 4px;
cursor: pointer;
.text {
color: var(--text-robin-400);
font-weight: 500;
}
}
}
.error-state-svg {
height: 40px;
width: 40px;
}
}
}
.loading-integration-details {
.integration-details-content-container {
display: flex;
flex-direction: column;
gap: 16px;
.skeleton-1 {
height: 125px;
width: 100%;
}
.skeleton-2 {
height: 250px;
width: 100%;
}
}
.all-integrations-btn {
width: fit-content;
display: flex;
justify-content: center;
align-items: center;
height: 24px;
padding-left: 0px;
color: #c0c1c3;
font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 18px; /* 128.571% */
}
.all-integrations-btn:hover {
&.ant-btn-text {
background-color: unset !important;
}
}
.integration-connection-header {
display: flex;
flex-direction: column;
padding: 16px;
gap: 12px;
border-radius: 6px;
border: 1px solid var(--bg-slate-500);
background: var(--bg-ink-400);
.integration-detail-header {
.error-container {
display: flex;
gap: 10px;
justify-content: space-between;
border-radius: 6px;
border: 1px solid var(--bg-slate-500);
background: var(--bg-ink-400);
align-items: center;
justify-content: center;
flex-direction: column;
.image-container {
height: 40px;
width: 40px;
flex-shrink: 0;
border-radius: 2px;
border: 1px solid var(--bg-ink-50);
background: var(--bg-ink-300);
.error-content {
display: flex;
flex-direction: column;
justify-content: center;
height: 300px;
gap: 15px;
.error-btns {
display: flex;
flex-direction: row;
gap: 16px;
align-items: center;
.retry-btn {
display: flex;
align-items: center;
}
.contact-support {
display: flex;
align-items: center;
gap: 4px;
cursor: pointer;
.text {
color: var(--text-robin-400);
font-weight: 500;
}
}
}
.error-state-svg {
height: 40px;
width: 40px;
}
}
}
.loading-integration-details {
display: flex;
flex-direction: column;
gap: 16px;
.skeleton-1 {
height: 125px;
width: 100%;
}
.skeleton-2 {
height: 250px;
width: 100%;
}
}
.integration-connection-header {
display: flex;
flex-direction: column;
padding: 16px;
gap: 12px;
border-radius: 6px;
border: 1px solid var(--bg-slate-500);
background: var(--bg-ink-400);
.integration-detail-header {
display: flex;
gap: 10px;
justify-content: space-between;
align-items: center;
.integration-detail-header-icon-title-container {
display: flex;
flex-direction: row;
align-items: center;
gap: 10px;
}
.image-container {
height: 40px;
width: 40px;
flex-shrink: 0;
border-radius: 2px;
border: 1px solid var(--bg-ink-50);
background: var(--bg-ink-300);
display: flex;
align-items: center;
justify-content: center;
.image {
height: 24px;
width: 24px;
}
}
.details {
display: flex;
flex-direction: column;
.heading {
color: var(--bg-vanilla-100);
font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 500;
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
}
.description {
color: var(--bg-vanilla-400);
font-family: Inter;
font-size: 12px;
font-style: normal;
font-weight: 400;
line-height: 18px; /* 150% */
}
}
.configure-btn {
display: flex;
justify-content: center;
align-items: center;
align-self: flex-start;
gap: 2px;
flex-shrink: 0;
min-width: 143px;
height: 30px;
padding: 6px;
border-radius: 2px;
border: 1px solid var(--bg-ink-50);
background: var(--bg-robin-500);
color: var(--bg-vanilla-100);
font-family: Inter;
font-size: 12px;
font-style: normal;
font-weight: 500;
line-height: 10px; /* 83.333% */
letter-spacing: 0.12px;
box-shadow: none;
&.test-connection {
border-radius: 2px;
border: 1px solid var(--bg-slate-400);
background: var(--bg-ink-300);
color: var(--bg-vanilla-400);
}
}
}
.connection-container {
padding: 0 18px;
height: 37px;
display: flex;
align-items: center;
.connection-text {
margin: 0px;
padding: 0px 0px 0px 10px;
font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 22px; /* 157.143% */
letter-spacing: -0.07px;
}
}
.testingConnection {
border-radius: 4px;
border: 1px solid rgba(255, 205, 86, 0.1);
background: rgba(255, 205, 86, 0.1);
color: var(--bg-amber-400);
}
.connected {
border-radius: 4px;
border: 1px solid rgba(37, 225, 146, 0.1);
background: rgba(37, 225, 146, 0.1);
color: var(--bg-forest-400);
}
.connectionFailed {
border-radius: 4px;
border: 1px solid rgba(218, 85, 101, 0.2);
background: rgba(218, 85, 101, 0.06);
color: var(--bg-cherry-500);
}
.noDataSinceLong {
border-radius: 4px;
border: 1px solid rgba(78, 116, 248, 0.1);
background: rgba(78, 116, 248, 0.1);
color: var(--bg-robin-400);
}
}
.integration-detail-container {
border-radius: 6px;
padding: 10px 16px;
border: 1px solid var(--bg-slate-500);
background: var(--bg-ink-400, #121317);
.integration-tab-btns {
display: flex;
align-items: center;
justify-content: center;
padding: 8px 8px 18px 8px !important;
.image {
height: 24px;
width: 24px;
}
}
.details {
display: flex;
flex-direction: column;
.heading {
.typography {
color: var(--bg-vanilla-100);
font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 500;
font-weight: 400;
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
}
}
.description {
color: var(--bg-vanilla-400);
font-family: Inter;
font-size: 12px;
font-style: normal;
font-weight: 400;
line-height: 18px; /* 150% */
.integration-tab-btns:hover {
&.ant-btn-text {
background-color: unset !important;
}
}
.configure-btn {
.ant-tabs-nav-list {
gap: 24px;
}
.ant-tabs-nav {
padding: 0px !important;
}
.ant-tabs-tab {
padding: 0 !important;
}
.ant-tabs-tab + .ant-tabs-tab {
margin: 0px !important;
}
}
.uninstall-integration-bar {
display: flex;
justify-content: space-between;
padding: 16px;
border-radius: 4px;
border: 1px solid rgba(218, 85, 101, 0.2);
background: rgba(218, 85, 101, 0.06);
gap: 32px;
.unintall-integration-bar-text {
display: flex;
justify-content: center;
align-items: center;
align-self: flex-start;
gap: 2px;
flex-shrink: 0;
min-width: 143px;
height: 30px;
padding: 6px;
flex-direction: column;
gap: 6px;
.heading {
color: var(--bg-cherry-500);
font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: normal;
letter-spacing: -0.07px;
}
.subtitle {
color: var(--bg-cherry-300);
font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 22px; /* 157.143% */
letter-spacing: -0.07px;
}
}
.uninstall-integration-btn {
border-radius: 2px;
border: 1px solid var(--bg-ink-50);
background: var(--bg-robin-500);
background: var(--bg-cherry-500);
border: none !important;
padding: 9px 13px;
display: flex;
align-items: center;
justify-content: center;
color: var(--bg-vanilla-100);
text-align: center;
font-family: Inter;
font-size: 12px;
font-style: normal;
font-weight: 500;
line-height: 10px; /* 83.333% */
letter-spacing: 0.12px;
box-shadow: none;
line-height: 13.3px; /* 110.833% */
}
&.test-connection {
border-radius: 2px;
border: 1px solid var(--bg-slate-400);
background: var(--bg-ink-300);
color: var(--bg-vanilla-400);
.uninstall-integration-btn:hover {
&.ant-btn-default {
color: var(--bg-vanilla-100) !important;
}
}
}
.connection-container {
padding: 0 18px;
height: 37px;
display: flex;
align-items: center;
.connection-text {
margin: 0px;
padding: 0px 0px 0px 10px;
font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 22px; /* 157.143% */
letter-spacing: -0.07px;
}
}
.testingConnection {
border-radius: 4px;
border: 1px solid rgba(255, 205, 86, 0.1);
background: rgba(255, 205, 86, 0.1);
color: var(--bg-amber-400);
}
.connected {
border-radius: 4px;
border: 1px solid rgba(37, 225, 146, 0.1);
background: rgba(37, 225, 146, 0.1);
color: var(--bg-forest-400);
}
.connectionFailed {
border-radius: 4px;
border: 1px solid rgba(218, 85, 101, 0.2);
background: rgba(218, 85, 101, 0.06);
color: var(--bg-cherry-500);
}
.noDataSinceLong {
border-radius: 4px;
border: 1px solid rgba(78, 116, 248, 0.1);
background: rgba(78, 116, 248, 0.1);
color: var(--bg-robin-400);
}
}
.integration-detail-container {
border-radius: 6px;
padding: 10px 16px;
border: 1px solid var(--bg-slate-500);
background: var(--bg-ink-400, #121317);
.integration-tab-btns {
display: flex;
align-items: center;
justify-content: center;
padding: 8px 8px 18px 8px !important;
.typography {
color: var(--bg-vanilla-100);
font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
}
}
.integration-tab-btns:hover {
&.ant-btn-text {
background-color: unset !important;
}
}
.ant-tabs-nav-list {
gap: 24px;
}
.ant-tabs-nav {
padding: 0px !important;
}
.ant-tabs-tab {
padding: 0 !important;
}
.ant-tabs-tab + .ant-tabs-tab {
margin: 0px !important;
}
}
.uninstall-integration-bar {
display: flex;
padding: 16px;
border-radius: 4px;
border: 1px solid rgba(218, 85, 101, 0.2);
background: rgba(218, 85, 101, 0.06);
gap: 32px;
.unintall-integration-bar-text {
.loading-container {
display: flex;
flex-direction: column;
gap: 6px;
gap: 8px;
.heading {
color: var(--bg-cherry-500);
font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: normal;
letter-spacing: -0.07px;
}
height: 160px;
.subtitle {
color: var(--bg-cherry-300);
font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 22px; /* 157.143% */
letter-spacing: -0.07px;
}
}
.uninstall-integration-btn {
border-radius: 2px;
background: var(--bg-cherry-500);
border: none !important;
padding: 9px 13px;
display: flex;
align-items: center;
justify-content: center;
color: var(--bg-vanilla-100);
text-align: center;
font-family: Inter;
font-size: 12px;
font-style: normal;
font-weight: 500;
line-height: 13.3px; /* 110.833% */
}
.uninstall-integration-btn:hover {
&.ant-btn-default {
color: var(--bg-vanilla-100) !important;
.skeleton-item {
padding: 16px;
}
}
}
@@ -538,115 +544,107 @@
}
.lightMode {
.integration-detail-content {
.error-container {
border: 1px solid var(--bg-slate-500);
background: var(--bg-ink-400);
.integration-details-container {
.integration-details-content-container {
.error-container {
border: 1px solid var(--bg-slate-500);
background: var(--bg-ink-400);
.error-content {
.error-btns {
.contact-support {
.text {
color: var(--text-robin-400);
font-weight: 500;
.error-content {
.error-btns {
.contact-support {
.text {
color: var(--text-robin-400);
font-weight: 500;
}
}
}
}
}
}
.all-integrations-btn {
color: var(--bg-slate-300);
}
.integration-connection-header {
border: 1px solid rgba(53, 59, 76, 0.2);
background: var(--bg-vanilla-100);
.all-integrations-btn:hover {
&.ant-btn-text {
background-color: unset !important;
}
}
.integration-detail-header {
.image-container {
border: 1.111px solid var(--bg-vanilla-300);
background: var(--bg-vanilla-100);
}
.details {
.heading {
color: var(--bg-ink-500);
}
.integration-connection-header {
border: 1px solid rgba(53, 59, 76, 0.2);
background: var(--bg-vanilla-100);
.integration-detail-header {
.image-container {
border: 1.111px solid var(--bg-vanilla-300);
background: var(--bg-vanilla-100);
.description {
color: var(--bg-slate-200);
}
}
.configure-btn {
&.test-connection {
border: 1px solid rgba(53, 59, 76, 0.2);
background: var(--bg-vanilla-200);
color: var(--bg-slate-200);
}
}
}
.details {
.testingConnection {
border: 1px solid rgba(255, 205, 86, 0.4);
background: rgba(255, 205, 86, 0.2);
color: var(--bg-amber-600);
}
.connected {
border: 1px solid rgba(37, 225, 146, 0.1);
background: rgba(37, 225, 146, 0.1);
color: var(--bg-forest-600);
}
.noDataSinceLong {
border: 1px solid rgba(78, 116, 248, 0.1);
background: rgba(78, 116, 248, 0.1);
color: var(--bg-robin-400);
}
}
.integration-detail-container {
border: 1px solid rgba(53, 59, 76, 0.2);
background: var(--bg-vanilla-100);
.integration-tab-btns {
.typography {
color: var(--bg-ink-500);
}
}
}
.uninstall-integration-bar {
border: 1px solid rgba(53, 59, 76, 0.2);
background: var(--bg-vanilla-100);
.unintall-integration-bar-text {
.heading {
color: var(--bg-ink-500);
}
.description {
color: var(--bg-slate-200);
.subtitle {
color: var(--bg-slate-100);
}
}
.configure-btn {
&.test-connection {
border: 1px solid rgba(53, 59, 76, 0.2);
background: var(--bg-vanilla-200);
color: var(--bg-slate-200);
.uninstall-integration-btn {
background: var(--bg-cherry-500, #e5484d);
border-color: none !important;
color: var(--bg-vanilla-100);
}
.uninstall-integration-btn:hover {
&.ant-btn-default {
color: var(--bg-vanilla-300) !important;
}
}
}
.testingConnection {
border: 1px solid rgba(255, 205, 86, 0.4);
background: rgba(255, 205, 86, 0.2);
color: var(--bg-amber-600);
}
.connected {
border: 1px solid rgba(37, 225, 146, 0.1);
background: rgba(37, 225, 146, 0.1);
color: var(--bg-forest-600);
}
.noDataSinceLong {
border: 1px solid rgba(78, 116, 248, 0.1);
background: rgba(78, 116, 248, 0.1);
color: var(--bg-robin-400);
}
}
.integration-detail-container {
border: 1px solid rgba(53, 59, 76, 0.2);
background: var(--bg-vanilla-100);
.integration-tab-btns {
.typography {
color: var(--bg-ink-500);
}
}
}
.uninstall-integration-bar {
border: 1px solid rgba(53, 59, 76, 0.2);
background: var(--bg-vanilla-100);
.unintall-integration-bar-text {
.heading {
color: var(--bg-ink-500);
}
.subtitle {
color: var(--bg-slate-100);
}
}
.uninstall-integration-btn {
background: var(--bg-cherry-500, #e5484d);
border-color: none !important;
color: var(--bg-vanilla-100);
}
.uninstall-integration-btn:hover {
&.ant-btn-default {
color: var(--bg-vanilla-300) !important;
}
}
}
}

View File

@@ -1,14 +1,18 @@
/* eslint-disable jsx-a11y/no-static-element-interactions */
/* eslint-disable jsx-a11y/click-events-have-key-events */
/* eslint-disable no-nested-ternary */
import { useState } from 'react';
import { useHistory, useParams } from 'react-router-dom';
import { Button } from '@signozhq/button';
import { Color } from '@signozhq/design-tokens';
import { Button, Flex, Skeleton, Typography } from 'antd';
import { Flex, Skeleton, Typography } from 'antd';
import ROUTES from 'constants/routes';
import { useGetIntegration } from 'hooks/Integrations/useGetIntegration';
import { useGetIntegrationStatus } from 'hooks/Integrations/useGetIntegrationStatus';
import { useGetTenantLicense } from 'hooks/useGetTenantLicense';
import { defaultTo } from 'lodash-es';
import { ArrowLeft, MoveUpRight, RotateCw } from 'lucide-react';
import CloudIntegration from '../CloudIntegration/CloudIntegration';
import { INTEGRATION_TYPES } from '../constants';
import { IntegrationType } from '../types';
import { handleContactSupport } from '../utils';
import IntegrationDetailContent from './IntegrationDetailContent';
import IntegrationDetailHeader from './IntegrationDetailHeader';
@@ -18,20 +22,13 @@ import { getConnectionStatesFromConnectionStatus } from './utils';
import './IntegrationDetailPage.styles.scss';
interface IntegrationDetailPageProps {
selectedIntegration: string;
setSelectedIntegration: (id: string | null) => void;
activeDetailTab: string;
setActiveDetailTab: React.Dispatch<React.SetStateAction<string | null>>;
}
function IntegrationDetailPage(props: IntegrationDetailPageProps): JSX.Element {
const {
selectedIntegration,
setSelectedIntegration,
activeDetailTab,
setActiveDetailTab,
} = props;
// eslint-disable-next-line sonarjs/cognitive-complexity
function IntegrationDetailPage(): JSX.Element {
const history = useHistory();
const { integrationId } = useParams<{ integrationId?: string }>();
const [activeDetailTab, setActiveDetailTab] = useState<string | null>(
'overview',
);
const {
data,
@@ -41,7 +38,7 @@ function IntegrationDetailPage(props: IntegrationDetailPageProps): JSX.Element {
isRefetching,
isError,
} = useGetIntegration({
integrationId: selectedIntegration,
integrationId: integrationId || '',
});
const { isCloudUser: isCloudUserVal } = useGetTenantLicense();
@@ -50,7 +47,7 @@ function IntegrationDetailPage(props: IntegrationDetailPageProps): JSX.Element {
data: integrationStatus,
isLoading: isStatusLoading,
} = useGetIntegrationStatus({
integrationId: selectedIntegration,
integrationId: integrationId || '',
});
const loading = isLoading || isFetching || isRefetching || isStatusLoading;
@@ -64,27 +61,38 @@ function IntegrationDetailPage(props: IntegrationDetailPageProps): JSX.Element {
),
);
if (
integrationId === INTEGRATION_TYPES.AWS ||
integrationId === INTEGRATION_TYPES.AZURE
) {
return (
<CloudIntegration
type={
integrationId === INTEGRATION_TYPES.AWS
? IntegrationType.AWS_SERVICES
: IntegrationType.AZURE_SERVICES
}
/>
);
}
return (
<div className="integration-detail-content">
<div className="integration-details-container">
<Flex justify="space-between" align="center">
<Button
type="text"
icon={<ArrowLeft size={14} />}
variant="link"
color="secondary"
prefixIcon={<ArrowLeft size={14} />}
className="all-integrations-btn"
onClick={(): void => {
setSelectedIntegration(null);
history.push(ROUTES.INTEGRATIONS);
}}
>
All Integrations
</Button>
</Flex>
{loading ? (
<div className="loading-integration-details">
<Skeleton.Input active size="large" className="skeleton-1" />
<Skeleton.Input active size="large" className="skeleton-2" />
</div>
) : isError ? (
{isError && (
<div className="error-container">
<div className="error-content">
<img
@@ -97,10 +105,10 @@ function IntegrationDetailPage(props: IntegrationDetailPageProps): JSX.Element {
</Typography.Text>
<div className="error-btns">
<Button
type="primary"
className="retry-btn"
variant="solid"
color="primary"
onClick={(): Promise<any> => refetch()}
icon={<RotateCw size={14} />}
prefixIcon={<RotateCw size={14} />}
>
Retry
</Button>
@@ -115,39 +123,51 @@ function IntegrationDetailPage(props: IntegrationDetailPageProps): JSX.Element {
</div>
</div>
</div>
) : (
integrationData && (
<>
<IntegrationDetailHeader
id={selectedIntegration}
title={defaultTo(integrationData?.title, '')}
description={defaultTo(integrationData?.description, '')}
icon={defaultTo(integrationData?.icon, '')}
connectionState={connectionStatus}
connectionData={defaultTo(integrationStatus?.data.data, {
logs: null,
metrics: null,
})}
onUnInstallSuccess={refetch}
setActiveDetailTab={setActiveDetailTab}
/>
<IntegrationDetailContent
activeDetailTab={activeDetailTab}
integrationData={integrationData}
integrationId={selectedIntegration}
setActiveDetailTab={setActiveDetailTab}
/>
)}
{connectionStatus !== ConnectionStates.NotInstalled && (
{!isError && (
<div className="integration-details-content-container">
<IntegrationDetailHeader
id={integrationId || ''}
title={defaultTo(integrationData?.title, '')}
description={defaultTo(integrationData?.description, '')}
icon={defaultTo(integrationData?.icon, '')}
connectionState={connectionStatus}
connectionData={defaultTo(integrationStatus?.data.data, {
logs: null,
metrics: null,
})}
onUnInstallSuccess={refetch}
setActiveDetailTab={setActiveDetailTab}
isLoading={loading}
/>
{loading && (
<div className="loading-container">
<Skeleton active className="skeleton-item" />
</div>
)}
{!isError && !loading && integrationData && (
<IntegrationDetailContent
activeDetailTab={activeDetailTab || 'overview'}
integrationData={integrationData}
integrationId={integrationId || ''}
setActiveDetailTab={setActiveDetailTab}
/>
)}
{!isError &&
!loading &&
connectionStatus !== ConnectionStates.NotInstalled && (
<IntergrationsUninstallBar
integrationTitle={defaultTo(integrationData?.title, '')}
integrationId={selectedIntegration}
integrationId={integrationId || ''}
onUnInstallSuccess={refetch}
connectionStatus={connectionStatus}
/>
)}
</>
)
</div>
)}
</div>
);

View File

@@ -7,7 +7,7 @@ import { SOMETHING_WENT_WRONG } from 'constants/api';
import { useNotifications } from 'hooks/useNotifications';
import { X } from 'lucide-react';
import { INTEGRATION_TELEMETRY_EVENTS } from '../utils';
import { INTEGRATION_TELEMETRY_EVENTS } from '../constants';
import { ConnectionStates } from './TestConnection';
import './IntegrationDetailPage.styles.scss';

View File

@@ -0,0 +1,66 @@
.integrations-page {
padding: 16px;
display: flex;
justify-content: center;
width: 100%;
.integrations-content {
width: 100%;
.integrations-listing-container {
display: flex;
flex-direction: column;
gap: 36px;
}
}
}
.integrations-not-found-container {
display: flex;
align-items: center;
justify-content: center;
min-height: 120px;
padding: 24px;
border-radius: 6px;
border: 1px solid var(--bg-slate-500);
background: var(--bg-ink-400);
width: 100%;
.integrations-not-found-content {
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
gap: 8px;
}
.integrations-not-found-text {
color: var(--bg-vanilla-400);
font-size: var(--font-size-sm);
text-align: center;
}
}
.lightMode {
.integrations-not-found-container {
border-color: var(--bg-vanilla-300);
background: var(--bg-vanilla-100);
.integrations-not-found-text {
color: var(--bg-slate-200);
}
}
}
.request-entity-container {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
border-radius: 4px;
border: 0.5px solid rgba(78, 116, 248, 0.2);
background: rgba(69, 104, 220, 0.1);
padding: 12px;
margin: 12px;
}

View File

@@ -0,0 +1,59 @@
import { useCallback, useEffect, useState } from 'react';
import { useHistory } from 'react-router-dom';
import logEvent from 'api/common/logEvent';
import ROUTES from 'constants/routes';
import { IntegrationsProps } from 'types/api/integrations/types';
import { INTEGRATION_TELEMETRY_EVENTS } from './constants';
import IntegrationsHeader from './IntegrationsHeader/IntegrationsHeader';
import IntegrationsList from './IntegrationsList/IntegrationsList';
import OneClickIntegrations from './OneClickIntegrations/OneClickIntegrations';
import './Integrations.styles.scss';
function Integrations(): JSX.Element {
const history = useHistory();
const [searchQuery, setSearchQuery] = useState('');
const setSelectedIntegration = useCallback(
(integration: IntegrationsProps | null) => {
if (integration) {
logEvent(INTEGRATION_TELEMETRY_EVENTS.INTEGRATIONS_ITEM_LIST_CLICKED, {
integration,
});
history.push(`${ROUTES.INTEGRATIONS}/${integration.id}`);
} else {
history.push(ROUTES.INTEGRATIONS);
}
},
[history],
);
useEffect(() => {
logEvent(INTEGRATION_TELEMETRY_EVENTS.INTEGRATIONS_LIST_VISITED, {});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return (
<div className="integrations-page">
<div className="integrations-content">
<div className="integrations-listing-container">
<IntegrationsHeader
searchQuery={searchQuery}
onSearchChange={setSearchQuery}
/>
<OneClickIntegrations
searchQuery={searchQuery}
setSelectedIntegration={setSelectedIntegration}
/>
<IntegrationsList
searchQuery={searchQuery}
setSelectedIntegration={setSelectedIntegration}
/>
</div>
</div>
</div>
);
}
export default Integrations;

View File

@@ -0,0 +1,57 @@
.integrations-header {
.integrations-header__subrow {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
flex-wrap: wrap;
margin-top: 4px;
}
.title {
color: var(--bg-vanilla-100);
font-size: var(--font-size-lg);
font-style: normal;
line-height: 28px; /* 155.556% */
letter-spacing: -0.09px;
font-weight: 500;
margin: 0;
}
.subtitle {
color: var(--bg-vanilla-400);
font-size: var(--font-size-sm);
font-style: normal;
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
font-weight: 400;
display: block;
}
.view-data-sources-btn {
gap: 8px;
padding: 6px 14px;
height: 32px;
font-size: 12px;
font-weight: 500;
white-space: nowrap;
}
.integrations-search-request-container {
display: flex;
align-items: center;
gap: 8px;
margin-top: 16px;
}
}
.lightMode {
.integrations-header {
.title {
color: var(--bg-ink-500);
}
.subtitle {
color: var(--bg-slate-200);
}
}
}

View File

@@ -0,0 +1,71 @@
import { useHistory } from 'react-router-dom';
import { Button } from '@signozhq/button';
import { Input } from '@signozhq/input';
import { Flex, Typography } from 'antd';
import ROUTES from 'constants/routes';
import { ArrowRight, Cable } from 'lucide-react';
import { useAppContext } from 'providers/App/App';
import { routePermission } from 'utils/permission';
import './IntegrationsHeader.styles.scss';
interface IntegrationsHeaderProps {
searchQuery: string;
onSearchChange: (value: string) => void;
}
function IntegrationsHeader(props: IntegrationsHeaderProps): JSX.Element {
const { searchQuery, onSearchChange } = props;
const history = useHistory();
const { user } = useAppContext();
const isGetStartedWithCloudAllowed = routePermission.GET_STARTED_WITH_CLOUD.includes(
user.role,
);
return (
<div className="integrations-header">
<Typography.Title className="title">Integrations</Typography.Title>
<Flex
justify="space-between"
align="center"
className="integrations-header__subrow"
>
<Typography.Text className="subtitle">
Manage integrations for this workspace.
</Typography.Text>
</Flex>
<div className="integrations-search-request-container">
<Input
placeholder="Search for an integration..."
value={searchQuery}
onChange={(e): void => onSearchChange(e.target.value)}
/>
<Button
variant="solid"
color="secondary"
className="request-integration-btn"
prefixIcon={<Cable size={14} />}
size="sm"
>
Request Integration
</Button>
{isGetStartedWithCloudAllowed && (
<Button
variant="solid"
color="secondary"
className="view-data-sources-btn"
onClick={(): void => history.push(ROUTES.GET_STARTED_WITH_CLOUD)}
>
<span>View 150+ Data Sources</span>
<ArrowRight size={14} />
</Button>
)}
</div>
</div>
);
}
export default IntegrationsHeader;

View File

@@ -0,0 +1,282 @@
.integrations-list-container {
display: flex;
flex-direction: column;
gap: 16px;
.integrations-list-title {
color: var(--Vanilla-400, #c0c1c3);
font-family: Inter;
font-size: 12px;
font-style: normal;
font-weight: 500;
line-height: 20px; /* 166.667% */
letter-spacing: 0.48px;
text-transform: uppercase;
}
.error-container {
display: flex;
border-radius: 6px;
border: 1px solid var(--bg-slate-500);
background: var(--bg-ink-400);
align-items: center;
justify-content: center;
flex-direction: column;
.error-content {
display: flex;
flex-direction: column;
justify-content: center;
height: 300px;
gap: 15px;
.error-btns {
display: flex;
flex-direction: row;
gap: 16px;
align-items: center;
.retry-btn {
display: flex;
align-items: center;
}
.contact-support {
display: flex;
align-items: center;
gap: 4px;
cursor: pointer;
.text {
color: var(--text-robin-400);
font-weight: 500;
}
}
}
.error-state-svg {
height: 40px;
width: 40px;
}
}
}
.integrations-list-title-header {
display: flex;
flex-direction: row;
gap: 32px;
align-items: center;
.integrations-list-header-title {
color: var(--Vanilla-400, #c0c1c3);
font-family: Inter;
font-size: 12px;
font-style: normal;
font-weight: 500;
line-height: 20px; /* 166.667% */
letter-spacing: 0.48px;
text-transform: uppercase;
word-wrap: normal;
white-space: nowrap;
}
.integrations-list-header-dotted-double-line {
width: 100%;
height: 100%;
}
}
.integrations-list {
display: flex;
flex-direction: column;
margin-left: -16px;
margin-right: -16px;
.integrations-list-header {
display: flex;
flex-direction: row;
gap: 10px;
align-items: center;
padding: 8px 16px;
.integrations-list-header-column {
flex: 1;
color: var(--Vanilla-400, #c0c1c3);
font-family: Inter;
font-size: 11px;
font-style: normal;
font-weight: 600;
line-height: 18px; /* 163.636% */
letter-spacing: 0.44px;
text-transform: uppercase;
&.title-column {
flex: 2;
}
&.published-by-column {
flex: 1;
}
&.installation-status-column {
flex: 1;
}
}
}
.integrations-list-item {
display: flex;
flex-direction: row;
gap: 10px;
padding: 8px 16px;
border-radius: 3px;
cursor: pointer;
.integrations-list-item-name-image-container {
display: flex;
flex-direction: row;
gap: 10px;
align-items: center;
.integrations-list-item-name-image-container-image {
height: 16px;
width: 16px;
border-radius: 2px;
border: 1px solid var(--bg-ink-50);
background: var(--bg-ink-300);
}
}
&:hover {
background: var(--bg-ink-400);
}
&:nth-child(even) {
background: rgba(171, 189, 255, 0.02);
}
}
.integrations-list-item-column {
display: flex;
flex-direction: row;
gap: 10px;
align-items: center;
flex: 1;
&.title-column {
display: flex;
flex-direction: row;
gap: 10px;
align-items: center;
flex: 2;
}
&.installation-status-column {
display: flex;
flex-direction: row;
gap: 10px;
align-items: center;
}
&.published-by-column {
display: flex;
flex-direction: row;
gap: 10px;
align-items: center;
}
}
}
.loading-container {
display: flex;
flex-direction: column;
gap: 8px;
margin-top: 16px;
.skeleton-item {
height: 32px;
width: 100%;
}
}
}
.integrations-list {
.error-container {
border: 1px solid rgba(53, 59, 76, 0.2);
background: var(--bg-vanilla-100);
}
.integrations-list-item {
.list-item-image-container {
border: 1.111px solid var(--bg-vanilla-300);
background: var(--bg-vanilla-100);
}
.list-item-details {
.heading {
color: var(--bg-ink-500);
}
.description {
color: var(--bg-slate-200);
}
}
.configure-btn {
border: 1px solid rgba(53, 59, 76, 0.2);
background: var(--bg-vanilla-200);
color: var(--bg-ink-500);
}
}
}
.lightMode {
.integrations-list-container {
.integrations-list-title {
color: var(--bg-ink-400);
}
.error-container {
border: 1px solid var(--bg-slate-500);
background: var(--bg-ink-400);
}
.integrations-list-item {
.list-item-image-container {
border: 1.111px solid var(--bg-vanilla-300);
background: var(--bg-vanilla-100);
}
.list-item-details {
.heading {
color: var(--bg-ink-500);
}
.description {
color: var(--bg-slate-200);
}
}
&:hover {
background: var(--bg-vanilla-100);
}
.configure-btn {
border: 1px solid rgba(53, 59, 76, 0.2);
background: var(--bg-vanilla-200);
color: var(--bg-ink-500);
}
}
.loading-container {
.skeleton-item {
background: var(--bg-vanilla-300);
}
}
}
}

View File

@@ -0,0 +1,183 @@
import { useMemo } from 'react';
import { Badge } from '@signozhq/badge';
import { Color } from '@signozhq/design-tokens';
import { Button, Skeleton, Typography } from 'antd';
import { useGetAllIntegrations } from 'hooks/Integrations/useGetAllIntegrations';
import { useGetTenantLicense } from 'hooks/useGetTenantLicense';
import { MoveUpRight, RotateCw } from 'lucide-react';
import { IntegrationsProps } from 'types/api/integrations/types';
import { handleContactSupport } from '../utils';
import './IntegrationsList.styles.scss';
interface IntegrationsListProps {
searchQuery: string;
setSelectedIntegration: (integration: IntegrationsProps) => void;
}
function IntegrationsList(props: IntegrationsListProps): JSX.Element {
const { searchQuery, setSelectedIntegration } = props;
const {
data,
isFetching,
isLoading,
isRefetching,
isError,
refetch,
} = useGetAllIntegrations();
const { isCloudUser: isCloudUserVal } = useGetTenantLicense();
const integrationsList = useMemo(() => {
if (!data?.data.data.integrations) {
return [];
}
const integrations = data.data.data.integrations;
const query = searchQuery.trim().toLowerCase();
if (!query) {
return integrations;
}
return integrations.filter(
(integration) =>
integration.title.toLowerCase().includes(query) ||
integration.description.toLowerCase().includes(query),
);
}, [data?.data.data.integrations, searchQuery]);
const loading = isLoading || isFetching || isRefetching;
const handleSelectedIntegration = (integration: IntegrationsProps): void => {
setSelectedIntegration(integration);
};
const renderError = (): JSX.Element => {
return (
<div className="error-container">
<div className="error-content">
<img
src="/Icons/awwSnap.svg"
alt="error-emoji"
className="error-state-svg"
/>
<Typography.Text>
Something went wrong :/ Please retry or contact support.
</Typography.Text>
<div className="error-btns">
<Button
type="primary"
className="retry-btn"
onClick={(): Promise<any> => refetch()}
icon={<RotateCw size={14} />}
>
Retry
</Button>
<div
className="contact-support"
onClick={(): void => handleContactSupport(isCloudUserVal)}
>
<Typography.Link className="text">Contact Support </Typography.Link>
<MoveUpRight size={14} color={Color.BG_ROBIN_400} />
</div>
</div>
</div>
</div>
);
};
return (
<div className="integrations-list-container">
<div className="integrations-list-title-header">
<div className="integrations-list-header-title">All Integrations</div>
<div className="integrations-list-header-dotted-double-line">
<img
src="/svgs/dotted-double-line.svg"
alt="dotted-double-line"
width="100%"
height="100%"
/>
</div>
</div>
{!loading && isError && renderError()}
{loading && (
<div className="loading-container">
<Skeleton.Input active size="large" className="skeleton-item" />
<Skeleton.Input active size="large" className="skeleton-item" />
<Skeleton.Input active size="large" className="skeleton-item" />
<Skeleton.Input active size="large" className="skeleton-item" />
<Skeleton.Input active size="large" className="skeleton-item" />
</div>
)}
{!loading && integrationsList.length === 0 && searchQuery.trim() && (
<div className="integrations-not-found-container">
<div className="integrations-not-found-content">
<img
src="/Icons/awwSnap.svg"
alt="no-integrations"
className="integrations-not-found-image"
/>
<div className="integrations-not-found-text">
No integrations found for &ldquo;{searchQuery.trim()}&rdquo;
</div>
</div>
</div>
)}
{!loading && integrationsList.length > 0 && (
<div className="integrations-list">
<div className="integrations-list-header">
<div className="integrations-list-header-column title-column">Name</div>
<div className="integrations-list-header-column published-by-column">
Published By
</div>
<div className="integrations-list-header-column installation-status-column">
Status
</div>
</div>
{integrationsList.map((integration) => (
<div
className="integrations-list-item"
key={integration.id}
onClick={(): void => handleSelectedIntegration(integration)}
>
<div className="integrations-list-item-column title-column">
<div className="integrations-list-item-name-image-container">
<img
src={integration.icon}
alt={integration.title}
className="integrations-list-item-name-image-container-image"
/>
<div className="integrations-list-item-name-text">
{integration.title}
</div>
</div>
</div>
<div className="integrations-list-item-column">
<div className="integrations-list-item-published-by">SigNoz</div>
</div>
<div className="integrations-list-item-column">
<div className="integrations-list-item-installation-status">
<Badge
color={integration.is_installed ? 'forest' : 'amber'}
variant="outline"
capitalize
>
{integration.is_installed ? 'Installed' : 'Not Installed'}
</Badge>
</div>
</div>
</div>
))}
</div>
)}
</div>
);
}
export default IntegrationsList;

View File

@@ -0,0 +1,103 @@
.one-click-integrations {
display: flex;
flex-direction: column;
gap: 16px;
.one-click-integrations-header {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
gap: 32px;
.one-click-integrations-header-title {
color: var(--bg-vanilla-400);
font-size: 12px;
font-weight: 500;
line-height: 20px;
letter-spacing: 0.48px;
text-transform: uppercase;
display: inline-block;
word-wrap: normal;
white-space: nowrap;
}
.one-click-integrations-header-dotted-double-line {
width: 100%;
height: 100%;
}
}
.one-click-integrations-list {
display: flex;
flex-direction: row;
gap: 16px;
.one-click-integrations-list-item {
display: flex;
flex-direction: column;
padding: 8px 12px 12px 12px;
gap: 10px;
width: fit-content;
border-radius: 3px;
border: 1px solid var(--Slate-500, #161922);
background: var(--Ink-400, #121317);
cursor: pointer;
.one-click-integrations-list-item-title {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
.one-click-integrations-list-item-title-image-container {
display: flex;
flex-direction: row;
align-items: center;
gap: 10px;
.one-click-integrations-list-item-title-text {
color: var(--Vanilla-100, #fff);
font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 500;
line-height: 32px; /* 200% */
letter-spacing: -0.08px;
}
}
}
}
}
}
.lightMode {
.one-click-integrations {
.one-click-integrations-header {
.one-click-integrations-header-title {
color: var(--bg-ink-400);
}
}
.one-click-integrations-list-item {
border: 1px solid var(--bg-vanilla-300);
background: var(--bg-vanilla-100);
.one-click-integrations-list-item-title {
.one-click-integrations-list-item-title-image-container {
.one-click-integrations-list-item-title-text {
color: var(--bg-ink-500);
}
}
}
.one-click-integrations-list-item-description {
color: var(--bg-slate-200);
}
&:hover {
background: var(--bg-vanilla-300);
}
}
}
}

View File

@@ -0,0 +1,100 @@
import { useMemo } from 'react';
import { Badge } from '@signozhq/badge';
import { IntegrationsProps } from 'types/api/integrations/types';
import { ONE_CLICK_INTEGRATIONS } from '../constants';
import './OneClickIntegrations.styles.scss';
interface OneClickIntegrationsProps {
searchQuery: string;
setSelectedIntegration: (integration: IntegrationsProps) => void;
}
function OneClickIntegrations(props: OneClickIntegrationsProps): JSX.Element {
const { searchQuery, setSelectedIntegration } = props;
const filteredIntegrations = useMemo(() => {
const query = searchQuery.trim().toLowerCase();
if (!query) {
return ONE_CLICK_INTEGRATIONS;
}
return ONE_CLICK_INTEGRATIONS.filter(
(integration) =>
integration.title.toLowerCase().includes(query) ||
integration.description.toLowerCase().includes(query),
);
}, [searchQuery]);
const handleSelectedIntegration = (integration: IntegrationsProps): void => {
setSelectedIntegration(integration);
};
return (
<div className="one-click-integrations">
<div className="one-click-integrations-header">
<div className="one-click-integrations-header-title">
One Click Integrations
</div>
<div className="one-click-integrations-header-dotted-double-line">
<img
src="/svgs/dotted-double-line.svg"
alt="dotted-double-line"
width="100%"
height="100%"
/>
</div>
</div>
<div className="one-click-integrations-list">
{filteredIntegrations.length === 0 && searchQuery.trim() ? (
<div className="integrations-not-found-container">
<div className="integrations-not-found-content">
<img
src="/Icons/awwSnap.svg"
alt="no-integrations"
className="integrations-not-found-image"
/>
<div className="integrations-not-found-text">
No integrations found for &ldquo;{searchQuery.trim()}&rdquo;
</div>
</div>
</div>
) : (
<>
{filteredIntegrations.map((integration) => (
<div
className="one-click-integrations-list-item"
key={integration.id}
onClick={(): void => handleSelectedIntegration(integration)}
>
<div className="one-click-integrations-list-item-title">
<div className="one-click-integrations-list-item-title-image-container">
<img src={integration.icon} alt={integration.title} />
<div className="one-click-integrations-list-item-title-text">
{integration.title}
</div>
</div>
{integration.is_new && (
<div className="one-click-integrations-list-item-new-tag">
<Badge color="robin" variant="default">
NEW
</Badge>
</div>
)}
</div>
<div className="one-click-integrations-list-item-description">
{integration.description}
</div>
</div>
))}
</>
)}
</div>
</div>
);
}
export default OneClickIntegrations;

View File

@@ -3,16 +3,12 @@ import { useTranslation } from 'react-i18next';
import { LoadingOutlined } from '@ant-design/icons';
import { Button, Input, Space, Typography } from 'antd';
import logEvent from 'api/common/logEvent';
import { IntegrationType } from 'container/Integrations/types';
import { useNotifications } from 'hooks/useNotifications';
import { Check } from 'lucide-react';
import './Integrations.styles.scss';
export enum IntegrationType {
AWS_SERVICES = 'aws-services',
INTEGRATIONS_LIST = 'integrations-list',
}
interface RequestIntegrationBtnProps {
type?: IntegrationType;
message?: string;
@@ -113,8 +109,3 @@ export function RequestIntegrationBtn({
</div>
);
}
RequestIntegrationBtn.defaultProps = {
type: IntegrationType.INTEGRATIONS_LIST,
message: "Can't find what youre looking for? Request more integrations",
};

View File

@@ -0,0 +1,163 @@
import { AzureRegion } from './types';
export const INTEGRATION_TELEMETRY_EVENTS = {
INTEGRATIONS_LIST_VISITED: 'Integrations Page: Visited the list page',
INTEGRATIONS_ITEM_LIST_CLICKED: 'Integrations Page: Clicked an integration',
INTEGRATIONS_DETAIL_CONNECT:
'Integrations Detail Page: Clicked connect integration button',
INTEGRATIONS_DETAIL_TEST_CONNECTION:
'Integrations Detail Page: Clicked test Connection button for integration',
INTEGRATIONS_DETAIL_REMOVE_INTEGRATION:
'Integrations Detail Page: Clicked remove Integration button for integration',
INTEGRATIONS_DETAIL_CONFIGURE_INSTRUCTION:
'Integrations Detail Page: Navigated to configure an integration',
AWS_INTEGRATION_ACCOUNT_REMOVED:
'AWS Integration Detail page: Clicked remove Integration button for integration',
};
export const INTEGRATION_TYPES = {
AWS: 'aws',
AZURE: 'azure',
};
export const AWS_INTEGRATION = {
id: INTEGRATION_TYPES.AWS,
title: 'Amazon Web Services',
description: 'One click setup for AWS monitoring with SigNoz',
author: {
name: 'SigNoz',
email: 'integrations@signoz.io',
homepage: 'https://signoz.io',
},
icon: `/Logos/aws-dark.svg`,
icon_alt: 'aws-logo',
is_installed: false,
is_new: false,
};
export const AZURE_INTEGRATION = {
id: INTEGRATION_TYPES.AZURE,
title: 'Microsoft Azure',
description: 'One click setup for Azure monitoring with SigNoz',
author: {
name: 'SigNoz',
email: 'integrations@signoz.io',
homepage: 'https://signoz.io',
},
icon: `/Logos/azure-openai.svg`,
icon_alt: 'azure-logo',
is_installed: false,
is_new: true,
};
export const ONE_CLICK_INTEGRATIONS = [AZURE_INTEGRATION, AWS_INTEGRATION];
export const AZURE_REGIONS: AzureRegion[] = [
{
label: 'Australia Central',
value: 'australiacentral',
geography: 'Australia',
},
{
label: 'Australia Central 2',
value: 'australiacentral2',
geography: 'Australia',
},
{ label: 'Australia East', value: 'australiaeast', geography: 'Australia' },
{
label: 'Australia Southeast',
value: 'australiasoutheast',
geography: 'Australia',
},
{ label: 'Austria East', value: 'austriaeast', geography: 'Austria' },
{ label: 'Belgium Central', value: 'belgiumcentral', geography: 'Belgium' },
{ label: 'Brazil South', value: 'brazilsouth', geography: 'Brazil' },
{ label: 'Brazil Southeast', value: 'brazilsoutheast', geography: 'Brazil' },
{ label: 'Canada Central', value: 'canadacentral', geography: 'Canada' },
{ label: 'Canada East', value: 'canadaeast', geography: 'Canada' },
{ label: 'Central India', value: 'centralindia', geography: 'India' },
{ label: 'Central US', value: 'centralus', geography: 'United States' },
{ label: 'Chile Central', value: 'chilecentral', geography: 'Chile' },
{ label: 'East Asia', value: 'eastasia', geography: 'Asia Pacific' },
{ label: 'East US', value: 'eastus', geography: 'United States' },
{ label: 'East US 2', value: 'eastus2', geography: 'United States' },
{ label: 'France Central', value: 'francecentral', geography: 'France' },
{ label: 'France South', value: 'francesouth', geography: 'France' },
{ label: 'Germany North', value: 'germanynorth', geography: 'Germany' },
{
label: 'Germany West Central',
value: 'germanywestcentral',
geography: 'Germany',
},
{
label: 'Indonesia Central',
value: 'indonesiacentral',
geography: 'Indonesia',
},
{ label: 'Israel Central', value: 'israelcentral', geography: 'Israel' },
{ label: 'Italy North', value: 'italynorth', geography: 'Italy' },
{ label: 'Japan East', value: 'japaneast', geography: 'Japan' },
{ label: 'Japan West', value: 'japanwest', geography: 'Japan' },
{ label: 'Korea Central', value: 'koreacentral', geography: 'Korea' },
{ label: 'Korea South', value: 'koreasouth', geography: 'Korea' },
{ label: 'Malaysia West', value: 'malaysiawest', geography: 'Malaysia' },
{ label: 'Mexico Central', value: 'mexicocentral', geography: 'Mexico' },
{
label: 'New Zealand North',
value: 'newzealandnorth',
geography: 'New Zealand',
},
{
label: 'North Central US',
value: 'northcentralus',
geography: 'United States',
},
{ label: 'North Europe', value: 'northeurope', geography: 'Europe' },
{ label: 'Norway East', value: 'norwayeast', geography: 'Norway' },
{ label: 'Norway West', value: 'norwaywest', geography: 'Norway' },
{ label: 'Poland Central', value: 'polandcentral', geography: 'Poland' },
{ label: 'Qatar Central', value: 'qatarcentral', geography: 'Qatar' },
{
label: 'South Africa North',
value: 'southafricanorth',
geography: 'South Africa',
},
{
label: 'South Africa West',
value: 'southafricawest',
geography: 'South Africa',
},
{
label: 'South Central US',
value: 'southcentralus',
geography: 'United States',
},
{ label: 'South India', value: 'southindia', geography: 'India' },
{ label: 'Southeast Asia', value: 'southeastasia', geography: 'Asia Pacific' },
{ label: 'Spain Central', value: 'spaincentral', geography: 'Spain' },
{ label: 'Sweden Central', value: 'swedencentral', geography: 'Sweden' },
{
label: 'Switzerland North',
value: 'switzerlandnorth',
geography: 'Switzerland',
},
{
label: 'Switzerland West',
value: 'switzerlandwest',
geography: 'Switzerland',
},
{ label: 'UAE Central', value: 'uaecentral', geography: 'UAE' },
{ label: 'UAE North', value: 'uaenorth', geography: 'UAE' },
{ label: 'UK South', value: 'uksouth', geography: 'United Kingdom' },
{ label: 'UK West', value: 'ukwest', geography: 'United Kingdom' },
{
label: 'West Central US',
value: 'westcentralus',
geography: 'United States',
},
{ label: 'West Europe', value: 'westeurope', geography: 'Europe' },
{ label: 'West India', value: 'westindia', geography: 'India' },
{ label: 'West US', value: 'westus', geography: 'United States' },
{ label: 'West US 2', value: 'westus2', geography: 'United States' },
{ label: 'West US 3', value: 'westus3', geography: 'United States' },
];

View File

@@ -0,0 +1,122 @@
import {
AWSCloudAccountConfig,
AWSServiceConfig,
} from './CloudIntegration/AmazonWebServices/types';
export enum IntegrationType {
AWS_SERVICES = 'aws-services',
AZURE_SERVICES = 'azure-services',
}
interface LogField {
name: string;
path: string;
type: string;
}
interface Metric {
name: string;
type: string;
unit: string;
}
export interface AzureConfig {
name: string;
enabled: boolean;
}
interface DataStatus {
last_received_ts_ms: number;
last_received_from: string;
}
export interface IServiceStatus {
logs: DataStatus | null;
metrics: DataStatus | null;
}
export interface AzureServicesConfig {
logs: AzureConfig[];
metrics: AzureConfig[];
}
export interface AzureServiceConfigPayload {
cloud_account_id: string;
config: AzureServicesConfig;
}
interface Dashboard {
id: string;
url: string;
title: string;
description: string;
image: string;
}
export interface SupportedSignals {
metrics: boolean;
logs: boolean;
}
export interface AzureService {
id: string;
title: string;
icon: string;
config: AzureServicesConfig;
}
export interface ServiceData {
id: string;
title: string;
icon: string;
overview: string;
supported_signals: SupportedSignals;
assets: {
dashboards: Dashboard[];
};
data_collected: {
logs?: LogField[];
metrics: Metric[];
};
config?: AWSServiceConfig | AzureServicesConfig;
status?: IServiceStatus;
}
export interface CloudAccount {
id: string;
cloud_account_id: string;
config: AzureCloudAccountConfig | AWSCloudAccountConfig;
status: AccountStatus | IServiceStatus;
}
export interface AzureCloudAccountConfig {
deployment_region: string;
resource_groups: string[];
}
export interface AccountStatus {
integration: IntegrationStatus;
}
export interface IntegrationStatus {
last_heartbeat_ts_ms: number;
}
export interface AzureRegion {
label: string;
geography: string;
value: string;
}
export interface UpdateServiceConfigPayload {
cloud_account_id: string;
config: AzureServicesConfig;
}
export interface UpdateServiceConfigResponse {
status: string;
data: {
id: string;
config: AzureServicesConfig;
};
}

Some files were not shown because too many files have changed in this diff Show More