Compare commits

...

48 Commits

Author SHA1 Message Date
Yunus M
dbad4defde feat: enhance AWS services list with empty state UI and improve connection handling 2026-04-15 11:56:30 +05:30
Yunus M
7fa89dc0f8 feat: add providerAccountId to AWS cloud account mapping and update related components 2026-04-15 11:23:45 +05:30
Yunus M
0c93eb17d5 refactor: remove Azure integrations files 2026-04-15 10:30:35 +05:30
Yunus M
1bd4b6970c refactor: simplify service selection logic and update dashboard URL path 2026-04-13 18:22:39 +05:30
Yunus M
8daaa0daba refactor: update cloud integration API types 2026-04-13 17:53:23 +05:30
Yunus M
c74d0ec4fb Merge branch 'main' into feat/azure-integration-ui 2026-04-13 12:32:18 +05:30
swapnil-signoz
7279c5f770 feat: adding query params in cloud integration APIs (#10900)
Some checks failed
Release Drafter / update_release_draft (push) Has been cancelled
build-staging / prepare (push) Has been cancelled
build-staging / js-build (push) Has been cancelled
build-staging / go-build (push) Has been cancelled
build-staging / staging (push) Has been cancelled
* feat: adding query params in cloud integration APIs

* refactor: create account HTTP status change from OK to CREATED
2026-04-10 09:20:35 +00:00
Yunus M
e5e915b54a Merge branch 'main' into feat/azure-integration-ui 2026-04-10 11:05:09 +05:30
Nikhil Soni
e543776efc chore: send obfuscate query in the clickhouse query panel update (#10848)
Some checks failed
build-staging / prepare (push) Has been cancelled
build-staging / js-build (push) Has been cancelled
build-staging / go-build (push) Has been cancelled
build-staging / staging (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
* chore: send query in the clickhouse query panel update

* chore: obfuscate query to avoid sending sensitive values
2026-04-09 14:15:10 +00:00
Yunus M
1eef6e7dc8 fix: use copyToClipboard instead of navigator clipboard 2026-04-04 00:32:18 +05:30
Yunus M
d9e40c3fa1 chore: add @uiw/codemirror-theme-dracula theme 2026-04-04 00:20:04 +05:30
Yunus M
3333cd8dbb Merge branch 'main' into feat/azure-integration-ui 2026-04-04 00:18:03 +05:30
Yunus M
1405a34d96 feat: refactor S3 Sync service tests to remove unnecessary act calls and add ResizeObserver mock 2026-02-28 12:26:08 +05:30
Yunus M
4ac4ffe34f Merge branch 'main' into feat/azure-integration-ui 2026-02-27 17:59:52 +05:30
Yunus M
28d880753e feat: enhance AzureAccountForm with react-hook-form integration and improve styling for form 2026-02-27 17:58:22 +05:30
Yunus M
505d231d68 chore: downgrade react-hook-form to version 7.40.0 in package.json and update yarn.lock 2026-02-27 15:39:56 +05:30
Yunus M
cf078e906a feat: add react-hook-form for form handling in ServiceDetails and enhance S3BucketsSelector styles 2026-02-27 15:38:19 +05:30
Yunus M
ac40cc4f5c feat: maintain width of save - discard buttons 2026-02-27 14:40:28 +05:30
Yunus M
49a90e79f9 Merge branch 'main' into feat/azure-integration-ui 2026-02-27 13:45:25 +05:30
Yunus M
9beff5a527 feat: implement ServiceDetails for S3 Sync with comprehensive tests and mock data 2026-02-27 02:13:38 +05:30
Yunus M
3bbde37f08 feat: add S3BucketsSelector component and integrate it into ServiceDetails 2026-02-27 01:52:25 +05:30
Yunus M
b4eb5f9df5 refactor: remove unused AWS service components and update connection status handling 2026-02-27 00:52:28 +05:30
Yunus M
3adbf92a8e refactor: enhance AWS service details and list UI with loading states and improved layout 2026-02-26 16:21:32 +05:30
Yunus M
ca006bc851 feat: update aws integrations as per new design 2026-02-26 15:51:47 +05:30
Yunus M
534ceac3d7 Merge branch 'main' into feat/azure-integration-ui 2026-02-25 15:07:56 +05:30
Yunus M
89e64fde70 feat: use semantic tokens 2026-02-18 13:11:32 +05:30
Yunus M
b52bfb16d8 fix: selected service getting reset on config update 2026-02-16 14:28:36 +05:30
Yunus M
1facf20561 fix: show scrollbar in drawer for overflowing content 2026-02-16 14:23:49 +05:30
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 5577 additions and 3585 deletions

View File

@@ -3309,7 +3309,7 @@ paths:
schema:
$ref: '#/components/schemas/CloudintegrationtypesPostableAccount'
responses:
"200":
"201":
content:
application/json:
schema:
@@ -3322,7 +3322,7 @@ paths:
- status
- data
type: object
description: OK
description: Created
"401":
content:
application/json:
@@ -3683,6 +3683,11 @@ paths:
provider
operationId: ListServicesMetadata
parameters:
- in: query
name: cloud_integration_id
required: false
schema:
type: string
- in: path
name: cloud_provider
required: true
@@ -3735,6 +3740,11 @@ paths:
description: This endpoint gets a service for the specified cloud provider
operationId: GetService
parameters:
- in: query
name: cloud_integration_id
required: false
schema:
type: string
- in: path
name: cloud_provider
required: true

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

@@ -65,12 +65,14 @@
"@signozhq/sonner": "0.1.0",
"@signozhq/switch": "0.0.2",
"@signozhq/table": "0.3.7",
"@signozhq/tabs": "0.0.11",
"@signozhq/toggle-group": "0.0.1",
"@signozhq/tooltip": "0.0.2",
"@signozhq/ui": "0.0.5",
"@tanstack/react-table": "8.21.3",
"@tanstack/react-virtual": "3.13.22",
"@uiw/codemirror-theme-copilot": "4.23.11",
"@uiw/codemirror-theme-dracula": "4.25.9",
"@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

@@ -244,12 +244,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

@@ -18,7 +18,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

@@ -28,7 +28,7 @@ import type {
CloudintegrationtypesPostableAgentCheckInDTO,
CloudintegrationtypesUpdatableAccountDTO,
CloudintegrationtypesUpdatableServiceDTO,
CreateAccount200,
CreateAccount201,
CreateAccountPathParameters,
DisconnectAccountPathParameters,
GetAccount200,
@@ -36,10 +36,12 @@ import type {
GetConnectionCredentials200,
GetConnectionCredentialsPathParameters,
GetService200,
GetServiceParams,
GetServicePathParameters,
ListAccounts200,
ListAccountsPathParameters,
ListServicesMetadata200,
ListServicesMetadataParams,
ListServicesMetadataPathParameters,
RenderErrorResponseDTO,
UpdateAccountPathParameters,
@@ -260,7 +262,7 @@ export const createAccount = (
cloudintegrationtypesPostableAccountDTO: BodyType<CloudintegrationtypesPostableAccountDTO>,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<CreateAccount200>({
return GeneratedAPIInstance<CreateAccount201>({
url: `/api/v1/cloud_integrations/${cloudProvider}/accounts`,
method: 'POST',
headers: { 'Content-Type': 'application/json' },
@@ -940,19 +942,25 @@ export const invalidateGetConnectionCredentials = async (
*/
export const listServicesMetadata = (
{ cloudProvider }: ListServicesMetadataPathParameters,
params?: ListServicesMetadataParams,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<ListServicesMetadata200>({
url: `/api/v1/cloud_integrations/${cloudProvider}/services`,
method: 'GET',
params,
signal,
});
};
export const getListServicesMetadataQueryKey = ({
cloudProvider,
}: ListServicesMetadataPathParameters) => {
return [`/api/v1/cloud_integrations/${cloudProvider}/services`] as const;
export const getListServicesMetadataQueryKey = (
{ cloudProvider }: ListServicesMetadataPathParameters,
params?: ListServicesMetadataParams,
) => {
return [
`/api/v1/cloud_integrations/${cloudProvider}/services`,
...(params ? [params] : []),
] as const;
};
export const getListServicesMetadataQueryOptions = <
@@ -960,6 +968,7 @@ export const getListServicesMetadataQueryOptions = <
TError = ErrorType<RenderErrorResponseDTO>
>(
{ cloudProvider }: ListServicesMetadataPathParameters,
params?: ListServicesMetadataParams,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof listServicesMetadata>>,
@@ -971,11 +980,12 @@ export const getListServicesMetadataQueryOptions = <
const { query: queryOptions } = options ?? {};
const queryKey =
queryOptions?.queryKey ?? getListServicesMetadataQueryKey({ cloudProvider });
queryOptions?.queryKey ??
getListServicesMetadataQueryKey({ cloudProvider }, params);
const queryFn: QueryFunction<
Awaited<ReturnType<typeof listServicesMetadata>>
> = ({ signal }) => listServicesMetadata({ cloudProvider }, signal);
> = ({ signal }) => listServicesMetadata({ cloudProvider }, params, signal);
return {
queryKey,
@@ -1003,6 +1013,7 @@ export function useListServicesMetadata<
TError = ErrorType<RenderErrorResponseDTO>
>(
{ cloudProvider }: ListServicesMetadataPathParameters,
params?: ListServicesMetadataParams,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof listServicesMetadata>>,
@@ -1013,6 +1024,7 @@ export function useListServicesMetadata<
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
const queryOptions = getListServicesMetadataQueryOptions(
{ cloudProvider },
params,
options,
);
@@ -1031,10 +1043,11 @@ export function useListServicesMetadata<
export const invalidateListServicesMetadata = async (
queryClient: QueryClient,
{ cloudProvider }: ListServicesMetadataPathParameters,
params?: ListServicesMetadataParams,
options?: InvalidateOptions,
): Promise<QueryClient> => {
await queryClient.invalidateQueries(
{ queryKey: getListServicesMetadataQueryKey({ cloudProvider }) },
{ queryKey: getListServicesMetadataQueryKey({ cloudProvider }, params) },
options,
);
@@ -1047,21 +1060,24 @@ export const invalidateListServicesMetadata = async (
*/
export const getService = (
{ cloudProvider, serviceId }: GetServicePathParameters,
params?: GetServiceParams,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<GetService200>({
url: `/api/v1/cloud_integrations/${cloudProvider}/services/${serviceId}`,
method: 'GET',
params,
signal,
});
};
export const getGetServiceQueryKey = ({
cloudProvider,
serviceId,
}: GetServicePathParameters) => {
export const getGetServiceQueryKey = (
{ cloudProvider, serviceId }: GetServicePathParameters,
params?: GetServiceParams,
) => {
return [
`/api/v1/cloud_integrations/${cloudProvider}/services/${serviceId}`,
...(params ? [params] : []),
] as const;
};
@@ -1070,6 +1086,7 @@ export const getGetServiceQueryOptions = <
TError = ErrorType<RenderErrorResponseDTO>
>(
{ cloudProvider, serviceId }: GetServicePathParameters,
params?: GetServiceParams,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getService>>,
@@ -1081,11 +1098,12 @@ export const getGetServiceQueryOptions = <
const { query: queryOptions } = options ?? {};
const queryKey =
queryOptions?.queryKey ?? getGetServiceQueryKey({ cloudProvider, serviceId });
queryOptions?.queryKey ??
getGetServiceQueryKey({ cloudProvider, serviceId }, params);
const queryFn: QueryFunction<Awaited<ReturnType<typeof getService>>> = ({
signal,
}) => getService({ cloudProvider, serviceId }, signal);
}) => getService({ cloudProvider, serviceId }, params, signal);
return {
queryKey,
@@ -1111,6 +1129,7 @@ export function useGetService<
TError = ErrorType<RenderErrorResponseDTO>
>(
{ cloudProvider, serviceId }: GetServicePathParameters,
params?: GetServiceParams,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getService>>,
@@ -1121,6 +1140,7 @@ export function useGetService<
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
const queryOptions = getGetServiceQueryOptions(
{ cloudProvider, serviceId },
params,
options,
);
@@ -1139,10 +1159,11 @@ export function useGetService<
export const invalidateGetService = async (
queryClient: QueryClient,
{ cloudProvider, serviceId }: GetServicePathParameters,
params?: GetServiceParams,
options?: InvalidateOptions,
): Promise<QueryClient> => {
await queryClient.invalidateQueries(
{ queryKey: getGetServiceQueryKey({ cloudProvider, serviceId }) },
{ queryKey: getGetServiceQueryKey({ cloudProvider, serviceId }, params) },
options,
);

View File

@@ -3589,7 +3589,7 @@ export type ListAccounts200 = {
export type CreateAccountPathParameters = {
cloudProvider: string;
};
export type CreateAccount200 = {
export type CreateAccount201 = {
data: CloudintegrationtypesGettableAccountWithConnectionArtifactDTO;
/**
* @type string
@@ -3647,6 +3647,14 @@ export type GetConnectionCredentials200 = {
export type ListServicesMetadataPathParameters = {
cloudProvider: string;
};
export type ListServicesMetadataParams = {
/**
* @type string
* @description undefined
*/
cloud_integration_id?: string;
};
export type ListServicesMetadata200 = {
data: CloudintegrationtypesGettableServicesMetadataDTO;
/**
@@ -3659,6 +3667,14 @@ export type GetServicePathParameters = {
cloudProvider: string;
serviceId: string;
};
export type GetServiceParams = {
/**
* @type string
* @description undefined
*/
cloud_integration_id?: string;
};
export type GetService200 = {
data: CloudintegrationtypesServiceDTO;
/**

View File

@@ -5,11 +5,10 @@ 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';
@@ -60,7 +59,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`,
@@ -79,10 +78,3 @@ export const updateServiceConfig = async (
);
return response.data;
};
export const getConnectionParams = async (): Promise<ConnectionParams> => {
const response = await axios.get(
'/cloud-integrations/aws/accounts/generate-connection-params',
);
return response.data.data;
};

View File

@@ -28,6 +28,7 @@ import '@signozhq/resizable';
import '@signozhq/sonner';
import '@signozhq/switch';
import '@signozhq/table';
import '@signozhq/tabs';
import '@signozhq/toggle-group';
import '@signozhq/tooltip';
import '@signozhq/ui';

View File

@@ -0,0 +1,34 @@
.cloud-service-data-collected {
display: flex;
flex-direction: column;
gap: 16px;
.cloud-service-data-collected-table {
display: flex;
flex-direction: column;
gap: 8px;
.cloud-service-data-collected-table-heading {
display: flex;
flex-direction: row;
align-items: center;
gap: 8px;
color: var(--l2-foreground);
/* Bifrost (Ancient)/Content/sm */
font-family: Inter;
font-size: 13px;
font-style: normal;
font-weight: 400;
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
}
.cloud-service-data-collected-table-logs {
border-radius: 6px;
border: 1px solid var(--l3-background);
background: var(--l1-background);
}
}
}

View File

@@ -1,13 +1,18 @@
import { Table } from 'antd';
import {
CloudintegrationtypesCollectedLogAttributeDTO,
CloudintegrationtypesCollectedMetricDTO,
} from 'api/generated/services/sigNoz.schemas';
import { BarChart2, ScrollText } from 'lucide-react';
import { ServiceData } from './types';
import './CloudServiceDataCollected.styles.scss';
function CloudServiceDataCollected({
logsData,
metricsData,
}: {
logsData: ServiceData['data_collected']['logs'];
metricsData: ServiceData['data_collected']['metrics'];
logsData: CloudintegrationtypesCollectedLogAttributeDTO[] | null | undefined;
metricsData: CloudintegrationtypesCollectedMetricDTO[] | null | undefined;
}): JSX.Element {
const logsColumns = [
{
@@ -61,24 +66,30 @@ 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}
{...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}
{...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,146 @@
import { useCallback, useMemo, useState } from 'react';
import { useCopyToClipboard } from 'react-use';
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 [, copyToClipboard] = useCopyToClipboard();
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 => {
copyToClipboard(value);
setIsCopied(true);
onCopy?.(value);
setTimeout(() => setIsCopied(false), 2000);
}, [value, onCopy, copyToClipboard]);
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

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

View File

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

@@ -1,213 +0,0 @@
import { Dispatch, SetStateAction, useCallback } from 'react';
import { useQueryClient } from 'react-query';
import { Form, Select, Switch } from 'antd';
import SignozModal from 'components/SignozModal/SignozModal';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import {
getRegionPreviewText,
useAccountSettingsModal,
} from 'hooks/integration/aws/useAccountSettingsModal';
import useUrlQuery from 'hooks/useUrlQuery';
import history from 'lib/history';
import logEvent from '../../../../api/common/logEvent';
import { CloudAccount } from '../../ServicesSection/types';
import { RegionSelector } from './RegionSelector';
import RemoveIntegrationAccount from './RemoveIntegrationAccount';
import './AccountSettingsModal.style.scss';
interface AccountSettingsModalProps {
onClose: () => void;
account: CloudAccount;
setActiveAccount: Dispatch<SetStateAction<CloudAccount | null>>;
}
function AccountSettingsModal({
onClose,
account,
setActiveAccount,
}: AccountSettingsModalProps): JSX.Element {
const {
form,
isLoading,
selectedRegions,
includeAllRegions,
isRegionSelectOpen,
isSaveDisabled,
setSelectedRegions,
setIncludeAllRegions,
setIsRegionSelectOpen,
handleIncludeAllRegionsChange,
handleSubmit,
handleClose,
} = useAccountSettingsModal({ onClose, account, setActiveAccount });
const queryClient = useQueryClient();
const urlQuery = useUrlQuery();
const handleRemoveIntegrationAccountSuccess = (): void => {
queryClient.invalidateQueries([REACT_QUERY_KEY.AWS_ACCOUNTS]);
urlQuery.delete('cloudAccountId');
handleClose();
history.replace({ search: urlQuery.toString() });
logEvent('AWS Integration: Account removed', {
id: account?.id,
cloudAccountId: account?.cloud_account_id,
});
};
const handleRegionDeselect = useCallback(
(item: string): void => {
if (selectedRegions.includes(item)) {
setSelectedRegions(selectedRegions.filter((region) => region !== item));
if (includeAllRegions) {
setIncludeAllRegions(false);
}
}
},
[
selectedRegions,
includeAllRegions,
setSelectedRegions,
setIncludeAllRegions,
],
);
const renderRegionSelector = useCallback(() => {
if (isRegionSelectOpen) {
return (
<RegionSelector
selectedRegions={selectedRegions}
setSelectedRegions={setSelectedRegions}
setIncludeAllRegions={setIncludeAllRegions}
/>
);
}
return (
<>
<div className="account-settings-modal__body-regions-switch-switch ">
<Switch
checked={includeAllRegions}
onChange={handleIncludeAllRegionsChange}
/>
<button
className="account-settings-modal__body-regions-switch-switch-label"
type="button"
onClick={(): void => handleIncludeAllRegionsChange(!includeAllRegions)}
>
Include all regions
</button>
</div>
<Select
suffixIcon={null}
placeholder="Select Region(s)"
className="cloud-account-setup-form__select account-settings-modal__body-regions-select integrations-select"
onClick={(): void => setIsRegionSelectOpen(true)}
mode="multiple"
maxTagCount={3}
value={getRegionPreviewText(selectedRegions)}
open={false}
onDeselect={handleRegionDeselect}
/>
</>
);
}, [
isRegionSelectOpen,
includeAllRegions,
handleIncludeAllRegionsChange,
selectedRegions,
handleRegionDeselect,
setSelectedRegions,
setIncludeAllRegions,
setIsRegionSelectOpen,
]);
const renderAccountDetails = useCallback(
() => (
<div className="account-settings-modal__body-account-info">
<div className="account-settings-modal__body-account-info-connected-account-details">
<div className="account-settings-modal__body-account-info-connected-account-details-title">
Connected Account details
</div>
<div className="account-settings-modal__body-account-info-connected-account-details-account-id">
AWS Account:{' '}
<span className="account-settings-modal__body-account-info-connected-account-details-account-id-account-id">
{account?.id}
</span>
</div>
</div>
</div>
),
[account?.id],
);
const modalTitle = (
<div className="account-settings-modal__title">
Account settings for{' '}
<span className="account-settings-modal__title-account-id">
{account?.id}
</span>
</div>
);
return (
<SignozModal
open
title={modalTitle}
onCancel={handleClose}
onOk={handleSubmit}
okText="Save"
okButtonProps={{
disabled: isSaveDisabled,
className: 'account-settings-modal__footer-save-button',
loading: isLoading,
}}
cancelButtonProps={{
className: 'account-settings-modal__footer-close-button',
}}
width={672}
rootClassName="account-settings-modal"
>
<Form
form={form}
layout="vertical"
initialValues={{
selectedRegions,
includeAllRegions,
}}
>
<div className="account-settings-modal__body">
{renderAccountDetails()}
<Form.Item
name="selectedRegions"
rules={[
{
validator: async (): Promise<void> => {
if (selectedRegions.length === 0) {
throw new Error('Please select at least one region to monitor');
}
},
message: 'Please select at least one region to monitor',
},
]}
>
{renderRegionSelector()}
</Form.Item>
<div className="integration-detail-content">
<RemoveIntegrationAccount
accountId={account?.id}
onRemoveIntegrationAccountSuccess={handleRemoveIntegrationAccountSuccess}
/>
</div>
</div>
</Form>
</SignozModal>
);
}
export default AccountSettingsModal;

View File

@@ -1,109 +0,0 @@
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 { regions } from 'utils/regions';
import logEvent from '../../../../api/common/logEvent';
import { ModalStateEnum, RegionFormProps } from '../types';
import AlertMessage from './AlertMessage';
import {
ComplianceNote,
MonitoringRegionsSection,
RegionDeploymentSection,
} from './IntegrateNowFormSections';
import RenderConnectionFields from './RenderConnectionParams';
const allRegions = (): string[] =>
regions.flatMap((r) => r.subRegions.map((sr) => sr.name));
const getRegionPreviewText = (regions: string[]): string[] => {
if (regions.includes('all')) {
return allRegions();
}
return regions;
};
export function RegionForm({
form,
modalState,
setModalState,
selectedRegions,
includeAllRegions,
onIncludeAllRegionsChange,
onRegionSelect,
onSubmit,
accountId,
selectedDeploymentRegion,
handleRegionChange,
connectionParams,
isConnectionParamsLoading,
}: RegionFormProps): JSX.Element {
const startTimeRef = useRef(Date.now());
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) {
setModalState(ModalStateEnum.ERROR);
logEvent('AWS Integration: Account connection attempt timed out', {
id: accountId,
});
}
},
onError: () => {
setModalState(ModalStateEnum.ERROR);
},
});
const isFormDisabled =
modalState === ModalStateEnum.WAITING || isAccountStatusLoading;
return (
<Form
form={form}
className="cloud-account-setup-form"
layout="vertical"
onFinish={onSubmit}
>
<AlertMessage modalState={modalState} />
<div
className={cx(`cloud-account-setup-form__content`, {
disabled: isFormDisabled,
})}
>
<RegionDeploymentSection
regions={regions}
handleRegionChange={handleRegionChange}
isFormDisabled={isFormDisabled}
selectedDeploymentRegion={selectedDeploymentRegion}
/>
<MonitoringRegionsSection
includeAllRegions={includeAllRegions}
selectedRegions={selectedRegions}
onIncludeAllRegionsChange={onIncludeAllRegionsChange}
getRegionPreviewText={getRegionPreviewText}
onRegionSelect={onRegionSelect}
isFormDisabled={isFormDisabled}
/>
<ComplianceNote />
<RenderConnectionFields
isConnectionParamsLoading={isConnectionParamsLoading}
connectionParams={connectionParams}
isFormDisabled={isFormDisabled}
/>
</div>
</Form>
);
}

View File

@@ -1,47 +0,0 @@
.remove-integration-account {
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);
&__header {
display: flex;
flex-direction: column;
gap: 6px;
}
&__title {
color: var(--bg-cherry-500);
font-size: 14px;
letter-spacing: -0.07px;
}
&__subtitle {
color: var(--bg-cherry-300);
font-size: 14px;
line-height: 22px;
letter-spacing: -0.07px;
}
&__button {
display: flex;
align-items: center;
background: var(--bg-cherry-500);
border: none;
color: var(--bg-vanilla-100);
font-size: 12px;
font-weight: 500;
line-height: 13.3px; /* 110.833% */
padding: 9px 13px;
.ant-btn-icon {
margin-inline-end: 4px !important;
}
&:hover {
&.ant-btn-default {
color: var(--bg-vanilla-300) !important;
}
}
}
}

View File

@@ -1,94 +0,0 @@
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 { SOMETHING_WENT_WRONG } from 'constants/api';
import { useNotifications } from 'hooks/useNotifications';
import { X } from 'lucide-react';
import { INTEGRATION_TELEMETRY_EVENTS } from 'pages/Integrations/utils';
import './RemoveIntegrationAccount.scss';
function RemoveIntegrationAccount({
accountId,
onRemoveIntegrationAccountSuccess,
}: {
accountId: string;
onRemoveIntegrationAccountSuccess: () => void;
}): JSX.Element {
const { notifications } = useNotifications();
const [isModalOpen, setIsModalOpen] = useState(false);
const showModal = (): void => {
setIsModalOpen(true);
};
const {
mutate: removeIntegration,
isLoading: isRemoveIntegrationLoading,
} = useMutation(removeAwsIntegrationAccount, {
onSuccess: () => {
onRemoveIntegrationAccountSuccess?.();
setIsModalOpen(false);
},
onError: () => {
notifications.error({
message: SOMETHING_WENT_WRONG,
});
},
});
const handleOk = (): void => {
logEvent(INTEGRATION_TELEMETRY_EVENTS.AWS_INTEGRATION_ACCOUNT_REMOVED, {
accountId,
});
removeIntegration(accountId);
};
const handleCancel = (): void => {
setIsModalOpen(false);
};
return (
<div className="remove-integration-account">
<div className="remove-integration-account__header">
<div className="remove-integration-account__title">Remove Integration</div>
<div className="remove-integration-account__subtitle">
Removing this integration won&apos;t delete any existing data but will stop
collecting new data from AWS.
</div>
</div>
<Button
className="remove-integration-account__button"
icon={<X size={14} />}
onClick={(): void => showModal()}
>
Remove
</Button>
<Modal
className="remove-integration-modal"
open={isModalOpen}
title="Remove integration"
onOk={handleOk}
onCancel={handleCancel}
okText="Remove Integration"
okButtonProps={{
danger: true,
disabled: isRemoveIntegrationLoading,
}}
>
<div className="remove-integration-modal__text">
Removing this account will remove all components created for sending
telemetry to SigNoz in your AWS account within the next ~15 minutes
(cloudformation stacks named signoz-integration-telemetry-collection in
enabled regions). <br />
<br />
After that, you can delete the cloudformation stack that was created
manually when connecting this account.
</div>
</Modal>
</div>
);
}
export default RemoveIntegrationAccount;

View File

@@ -1,156 +0,0 @@
.cloud-account-setup-success-view {
display: flex;
flex-direction: column;
gap: 40px;
text-align: center;
padding-top: 34px;
p,
h3,
h4 {
margin: 0;
}
&__content {
display: flex;
flex-direction: column;
gap: 14px;
.cloud-account-setup-success-view {
&__title {
h3 {
color: var(--bg-vanilla-100);
font-size: 20px;
font-weight: 500;
line-height: 32px;
}
}
&__description {
color: var(--bg-vanilla-400);
font-size: 14px;
font-weight: 400;
line-height: 20px;
letter-spacing: -0.07px;
}
}
}
&__what-next {
display: flex;
flex-direction: column;
gap: 18px;
text-align: left;
&-title {
color: var(--bg-slate-50);
font-size: 11px;
font-weight: 500;
line-height: 18px;
letter-spacing: 0.88px;
text-transform: uppercase;
}
.what-next-items-wrapper {
display: flex;
flex-direction: column;
gap: 12px;
&__item {
display: flex;
gap: 10px;
align-items: baseline;
&.ant-alert {
padding: 14px;
border-radius: 8px;
font-size: 14px;
line-height: 20px; /* 142.857% */
letter-spacing: -0.21px;
}
&.ant-alert-info {
border: 1px solid rgba(63, 94, 204, 0.5);
background: rgba(78, 116, 248, 0.2);
color: var(--bg-robin-400);
}
.what-next-item {
color: var(--bg-robin-400);
&-bullet-icon {
font-size: 20px;
line-height: 20px;
}
&-text {
font-size: 14px;
font-weight: 500;
line-height: 20px;
letter-spacing: -0.21px;
}
}
}
}
}
&__footer {
padding-top: 18px;
.ant-btn {
background: var(--bg-robin-500);
color: var(--bg-vanilla-100);
font-size: 14px;
font-weight: 500;
line-height: 20px;
height: 36px;
}
}
}
.lottie-container {
position: absolute;
width: 743.5px;
height: 990.342px;
top: -100px;
left: -36px;
z-index: 1;
}
.lightMode {
.cloud-account-setup-success-view {
&__content {
.cloud-account-setup-success-view {
&__title {
h3 {
color: var(--bg-ink-500);
}
}
&__description {
color: var(--bg-ink-400);
}
}
}
&__what-next {
&-title {
color: var(--bg-ink-500);
}
.what-next-items-wrapper {
&__item {
&.ant-alert-info {
border: 1px solid rgba(63, 94, 204, 0.2);
background: rgba(78, 116, 248, 0.1);
color: var(--bg-robin-500);
}
.what-next-item {
color: var(--bg-robin-500);
&-text {
color: var(--bg-robin-500);
}
}
}
}
}
&__footer {
.ant-btn {
background: var(--bg-robin-500);
color: var(--bg-vanilla-100);
&:hover {
background: var(--bg-robin-400);
}
}
}
}
}

View File

@@ -1,73 +0,0 @@
import { useState } from 'react';
import Lottie from 'react-lottie';
import { Alert } from 'antd';
import integrationsSuccess from 'assets/Lotties/integrations-success.json';
import './SuccessView.style.scss';
export function SuccessView(): JSX.Element {
const [isAnimationComplete, setIsAnimationComplete] = useState(false);
const defaultOptions = {
loop: false,
autoplay: true,
animationData: integrationsSuccess,
rendererSettings: {
preserveAspectRatio: 'xMidYMid slice',
},
};
return (
<>
{!isAnimationComplete && (
<div className="lottie-container">
<Lottie
options={defaultOptions}
height={990.342}
width={743.5}
eventListeners={[
{
eventName: 'complete',
callback: (): void => setIsAnimationComplete(true),
},
]}
/>
</div>
)}
<div className="cloud-account-setup-success-view">
<div className="cloud-account-setup-success-view__icon">
<img src="Icons/solid-check-circle.svg" alt="Success" />
</div>
<div className="cloud-account-setup-success-view__content">
<div className="cloud-account-setup-success-view__title">
<h3>🎉 Success! </h3>
<h3>Your AWS Web Service integration is all set.</h3>
</div>
<div className="cloud-account-setup-success-view__description">
<p>Your observability journey is off to a great start. </p>
<p>Now that your data is flowing, heres what you can do next:</p>
</div>
</div>
<div className="cloud-account-setup-success-view__what-next">
<h4 className="cloud-account-setup-success-view__what-next-title">
WHAT NEXT
</h4>
<div className="what-next-items-wrapper">
<Alert
message={
<div className="what-next-items-wrapper__item">
<div className="what-next-item-bullet-icon"></div>
<div className="what-next-item-text">
Set up your AWS services effortlessly under your enabled account.
</div>
</div>
}
type="info"
className="what-next-items-wrapper__item"
/>
</div>
</div>
</div>
</>
);
}

View File

@@ -1,50 +0,0 @@
import { Link } from 'react-router-dom';
import { ServiceData } from './types';
function DashboardItem({
dashboard,
}: {
dashboard: ServiceData['assets']['dashboards'][number];
}): JSX.Element {
const content = (
<>
<div className="cloud-service-dashboard-item__title">{dashboard.title}</div>
<div className="cloud-service-dashboard-item__preview">
<img
src={dashboard.image}
alt={dashboard.title}
className="cloud-service-dashboard-item__preview-image"
/>
</div>
</>
);
return (
<div className="cloud-service-dashboard-item">
{dashboard.url ? (
<Link to={dashboard.url} className="cloud-service-dashboard-item__link">
{content}
</Link>
) : (
content
)}
</div>
);
}
function CloudServiceDashboards({
service,
}: {
service: ServiceData;
}): JSX.Element {
return (
<>
{service.assets.dashboards.map((dashboard) => (
<DashboardItem key={dashboard.id} dashboard={dashboard} />
))}
</>
);
}
export default CloudServiceDashboards;

View File

@@ -1,89 +0,0 @@
.configure-service-modal {
&__body {
display: flex;
flex-direction: column;
border-radius: 3px;
border: 1px solid var(--bg-slate-500);
padding: 14px;
&-regions-switch-switch {
display: flex;
align-items: center;
gap: 6px;
&-label {
color: var(--bg-vanilla-100);
font-size: 14px;
font-weight: 500;
line-height: 20px;
letter-spacing: -0.07px;
}
}
&-switch-description {
margin-top: 4px;
color: var(--bg-vanilla-400);
font-size: 12px;
font-weight: 400;
line-height: 18px;
letter-spacing: -0.06px;
}
&-form-item {
&:last-child {
margin-bottom: 0px;
}
}
}
.ant-modal-body {
padding-bottom: 0;
}
.ant-modal-footer {
margin: 0;
padding-bottom: 12px;
}
}
.lightMode {
.configure-service-modal {
&__body {
border-color: var(--bg-vanilla-300);
&-regions-switch-switch {
&-label {
color: var(--bg-ink-500);
}
}
&-switch-description {
color: var(--bg-ink-400);
}
}
.ant-btn {
&.ant-btn-default {
background: var(--bg-vanilla-100);
border: 1px solid var(--bg-vanilla-300);
color: var(--bg-ink-400);
&:hover {
border-color: var(--bg-vanilla-400);
color: var(--bg-ink-500);
}
}
&.ant-btn-primary {
// Keep primary button same as dark mode
background: var(--bg-robin-500);
color: var(--bg-vanilla-100);
&:hover {
background: var(--bg-robin-400);
}
&:disabled {
opacity: 0.6;
}
}
}
}
}

View File

@@ -1,243 +0,0 @@
import { useCallback, useMemo, useState } from 'react';
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 { useUpdateServiceConfig } from 'hooks/integration/aws/useUpdateServiceConfig';
import { isEqual } from 'lodash-es';
import logEvent from '../../../api/common/logEvent';
import S3BucketsSelector from './S3BucketsSelector';
import './ConfigureServiceModal.styles.scss';
export interface IConfigureServiceModalProps {
isOpen: boolean;
onClose: () => void;
serviceName: string;
serviceId: string;
cloudAccountId: string;
supportedSignals: SupportedSignals;
initialConfig?: ServiceConfig;
}
function ConfigureServiceModal({
isOpen,
onClose,
serviceName,
serviceId,
cloudAccountId,
initialConfig,
supportedSignals,
}: IConfigureServiceModalProps): JSX.Element {
const [form] = Form.useForm();
const [isLoading, setIsLoading] = useState(false);
// Track current form values
const initialValues = useMemo(
() => ({
metrics: initialConfig?.metrics?.enabled || false,
logs: initialConfig?.logs?.enabled || false,
s3Buckets: initialConfig?.logs?.s3_buckets || {},
}),
[initialConfig],
);
const [currentValues, setCurrentValues] = useState(initialValues);
const isSaveDisabled = useMemo(
() =>
// disable only if current values are same as the initial config
currentValues.metrics === initialValues.metrics &&
currentValues.logs === initialValues.logs &&
isEqual(currentValues.s3Buckets, initialValues.s3Buckets),
[currentValues, initialValues],
);
const handleS3BucketsChange = useCallback(
(bucketsByRegion: Record<string, string[]>) => {
setCurrentValues((prev) => ({
...prev,
s3Buckets: bucketsByRegion,
}));
form.setFieldsValue({ s3Buckets: bucketsByRegion });
},
[form],
);
const {
mutate: updateServiceConfig,
isLoading: isUpdating,
} = useUpdateServiceConfig();
const queryClient = useQueryClient();
const handleSubmit = useCallback(async (): Promise<void> => {
try {
const values = await form.validateFields();
setIsLoading(true);
updateServiceConfig(
{
serviceId,
payload: {
cloud_account_id: cloudAccountId,
config: {
logs: {
enabled: values.logs,
s3_buckets: values.s3Buckets,
},
metrics: {
enabled: values.metrics,
},
},
},
},
{
onSuccess: () => {
queryClient.invalidateQueries([
REACT_QUERY_KEY.AWS_SERVICE_DETAILS,
serviceId,
]);
onClose();
logEvent('AWS Integration: Service settings saved', {
cloudAccountId,
serviceId,
logsEnabled: values?.logs,
metricsEnabled: values?.metrics,
});
},
onError: (error) => {
console.error('Failed to update service config:', error);
},
},
);
} catch (error) {
console.error('Form submission failed:', error);
} finally {
setIsLoading(false);
}
}, [
form,
updateServiceConfig,
serviceId,
cloudAccountId,
queryClient,
onClose,
]);
const handleClose = useCallback(() => {
form.resetFields();
onClose();
}, [form, onClose]);
return (
<SignozModal
title={
<div className="account-settings-modal__title">Configure {serviceName}</div>
}
centered
open={isOpen}
okText="Save"
okButtonProps={{
disabled: isSaveDisabled,
className: 'account-settings-modal__footer-save-button',
loading: isLoading || isUpdating,
}}
onCancel={handleClose}
onOk={handleSubmit}
cancelText="Close"
cancelButtonProps={{
className: 'account-settings-modal__footer-close-button',
}}
width={672}
rootClassName=" configure-service-modal"
>
<Form
form={form}
layout="vertical"
initialValues={{
metrics: initialConfig?.metrics?.enabled || false,
logs: initialConfig?.logs?.enabled || false,
s3Buckets: initialConfig?.logs?.s3_buckets || {},
}}
>
<div className=" configure-service-modal__body">
{supportedSignals.metrics && (
<Form.Item
name="metrics"
valuePropName="checked"
className="configure-service-modal__body-form-item"
>
<div className="configure-service-modal__body-regions-switch-switch">
<Switch
checked={currentValues.metrics}
onChange={(checked): void => {
setCurrentValues((prev) => ({ ...prev, metrics: checked }));
form.setFieldsValue({ metrics: checked });
}}
/>
<span className="configure-service-modal__body-regions-switch-switch-label">
Metric Collection
</span>
</div>
<div className="configure-service-modal__body-switch-description">
Metric Collection is enabled for this AWS account. We recommend keeping
this enabled, but you can disable metric collection if you do not want
to monitor your AWS infrastructure.
</div>
</Form.Item>
)}
{supportedSignals.logs && (
<>
<Form.Item
name="logs"
valuePropName="checked"
className="configure-service-modal__body-form-item"
>
<div className="configure-service-modal__body-regions-switch-switch">
<Switch
checked={currentValues.logs}
onChange={(checked): void => {
setCurrentValues((prev) => ({ ...prev, logs: checked }));
form.setFieldsValue({ logs: checked });
}}
/>
<span className="configure-service-modal__body-regions-switch-switch-label">
Log Collection
</span>
</div>
<div className="configure-service-modal__body-switch-description">
To ingest logs from your AWS services, you must complete several steps
</div>
</Form.Item>
{currentValues.logs && serviceId === 's3sync' && (
<Form.Item name="s3Buckets" noStyle>
<S3BucketsSelector
initialBucketsByRegion={currentValues.s3Buckets}
onChange={handleS3BucketsChange}
/>
</Form.Item>
)}
</>
)}
</div>
</Form>
</SignozModal>
);
}
ConfigureServiceModal.defaultProps = {
initialConfig: {
metrics: { enabled: false },
logs: { enabled: false },
},
};
export default ConfigureServiceModal;

View File

@@ -1,189 +0,0 @@
import { useEffect, useMemo, useState } from 'react';
import { Button, Tabs, TabsProps } from 'antd';
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 dayjs from 'dayjs';
import { useServiceDetails } from 'hooks/integration/aws/useServiceDetails';
import useUrlQuery from 'hooks/useUrlQuery';
import logEvent from '../../../api/common/logEvent';
import ConfigureServiceModal from './ConfigureServiceModal';
const getStatus = (
logsLastReceivedTimestamp: number | undefined,
metricsLastReceivedTimestamp: number | undefined,
): { text: string; className: string } => {
if (!logsLastReceivedTimestamp && !metricsLastReceivedTimestamp) {
return { text: 'No Data Yet', className: 'service-status--no-data' };
}
const latestTimestamp = Math.max(
logsLastReceivedTimestamp || 0,
metricsLastReceivedTimestamp || 0,
);
const isStale = dayjs().diff(dayjs(latestTimestamp), 'minute') > 30;
if (isStale) {
return { text: 'Stale Data', className: 'service-status--stale-data' };
}
return { text: 'Connected', className: 'service-status--connected' };
};
function ServiceStatus({
serviceStatus,
}: {
serviceStatus: IServiceStatus | undefined;
}): JSX.Element {
const logsLastReceivedTimestamp = serviceStatus?.logs?.last_received_ts_ms;
const metricsLastReceivedTimestamp =
serviceStatus?.metrics?.last_received_ts_ms;
const { text, className } = getStatus(
logsLastReceivedTimestamp,
metricsLastReceivedTimestamp,
);
return <div className={`service-status ${className}`}>{text}</div>;
}
function getTabItems(serviceDetailsData: any): TabsProps['items'] {
const dashboards = serviceDetailsData?.assets.dashboards || [];
const dataCollected = serviceDetailsData?.data_collected || {};
const items: TabsProps['items'] = [];
if (dashboards.length) {
items.push({
key: 'dashboards',
label: `Dashboards (${dashboards.length})`,
children: <CloudServiceDashboards service={serviceDetailsData} />,
});
}
items.push({
key: 'data-collected',
label: 'Data Collected',
children: (
<CloudServiceDataCollected
logsData={dataCollected.logs || []}
metricsData={dataCollected.metrics || []}
/>
),
});
return items;
}
function ServiceDetails(): JSX.Element | null {
const urlQuery = useUrlQuery();
const cloudAccountId = urlQuery.get('cloudAccountId');
const serviceId = urlQuery.get('service');
const [isConfigureServiceModalOpen, setIsConfigureServiceModalOpen] = useState(
false,
);
const openServiceConfigModal = (): void => {
setIsConfigureServiceModalOpen(true);
logEvent('AWS Integration: Service settings viewed', {
cloudAccountId,
serviceId,
});
};
const { data: serviceDetailsData, isLoading } = useServiceDetails(
serviceId || '',
cloudAccountId || undefined,
);
const { config, supported_signals } = serviceDetailsData ?? {};
const totalSupportedSignals = Object.entries(supported_signals || {}).filter(
([, value]) => !!value,
).length;
const enabledSignals = useMemo(
() =>
Object.values(config || {}).filter((item) => item && item.enabled).length,
[config],
);
const isAnySignalConfigured = useMemo(
() => !!config?.logs?.enabled || !!config?.metrics?.enabled,
[config],
);
// log telemetry event on visiting details of a service.
useEffect(() => {
if (serviceId) {
logEvent('AWS Integration: Service viewed', {
cloudAccountId,
serviceId,
});
}
}, [cloudAccountId, serviceId]);
if (isLoading) {
return <Spinner size="large" height="50vh" />;
}
if (!serviceDetailsData) {
return null;
}
const tabItems = getTabItems(serviceDetailsData);
return (
<div className="service-details">
<div className="service-details__title-bar">
<div className="service-details__details-title">Details</div>
<div className="service-details__right-actions">
{isAnySignalConfigured && (
<ServiceStatus serviceStatus={serviceDetailsData.status} />
)}
{!!cloudAccountId &&
(isAnySignalConfigured ? (
<Button
className="configure-button configure-button--default"
onClick={openServiceConfigModal}
>
Configure ({enabledSignals}/{totalSupportedSignals})
</Button>
) : (
<Button
type="primary"
className="configure-button configure-button--primary"
onClick={openServiceConfigModal}
>
Enable Service
</Button>
))}
</div>
</div>
<div className="service-details__overview">
<MarkdownRenderer
variables={{}}
markdownContent={serviceDetailsData?.overview}
/>
</div>
<div className="service-details__tabs">
<Tabs items={tabItems} />
</div>
{isConfigureServiceModalOpen && (
<ConfigureServiceModal
isOpen
onClose={(): void => setIsConfigureServiceModalOpen(false)}
serviceName={serviceDetailsData.title}
serviceId={serviceId || ''}
cloudAccountId={cloudAccountId || ''}
initialConfig={serviceDetailsData.config}
supportedSignals={serviceDetailsData.supported_signals || {}}
/>
)}
</div>
);
}
export default ServiceDetails;

View File

@@ -1,75 +0,0 @@
import { useCallback, useEffect, useMemo } from 'react';
import { useNavigate } from 'react-router-dom-v5-compat';
import Spinner from 'components/Spinner';
import { useGetAccountServices } from 'hooks/integration/aws/useGetAccountServices';
import useUrlQuery from 'hooks/useUrlQuery';
import ServiceItem from './ServiceItem';
interface ServicesListProps {
cloudAccountId: string;
filter: 'all_services' | 'enabled' | 'available';
}
function ServicesList({
cloudAccountId,
filter,
}: ServicesListProps): JSX.Element {
const urlQuery = useUrlQuery();
const navigate = useNavigate();
const { data: services = [], isLoading } = useGetAccountServices(
cloudAccountId,
);
const activeService = urlQuery.get('service');
const handleActiveService = useCallback(
(serviceId: string): void => {
const latestUrlQuery = new URLSearchParams(window.location.search);
latestUrlQuery.set('service', serviceId);
navigate({ search: latestUrlQuery.toString() });
},
[navigate],
);
const filteredServices = useMemo(() => {
if (filter === 'all_services') {
return services;
}
return services.filter((service) => {
const isEnabled =
service?.config?.logs?.enabled || service?.config?.metrics?.enabled;
return filter === 'enabled' ? isEnabled : !isEnabled;
});
}, [services, filter]);
useEffect(() => {
if (activeService || !services?.length) {
return;
}
handleActiveService(services[0].id);
}, [services, activeService, handleActiveService]);
if (isLoading) {
return <Spinner size="large" height="25vh" />;
}
if (!services) {
return <div>No services found</div>;
}
return (
<div className="services-list">
{filteredServices.map((service) => (
<ServiceItem
key={service.id}
service={service}
onClick={handleActiveService}
isActive={service.id === activeService}
/>
))}
</div>
);
}
export default ServicesList;

View File

@@ -1,124 +0,0 @@
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 { getAwsServices } from 'api/integration/aws';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import useUrlQuery from 'hooks/useUrlQuery';
import { ChevronDown } from 'lucide-react';
import ServiceDetails from './ServiceDetails';
import ServicesList from './ServicesList';
import './ServicesTabs.style.scss';
export enum ServiceFilterType {
ALL_SERVICES = 'all_services',
ENABLED = 'enabled',
AVAILABLE = 'available',
}
interface ServicesFilterProps {
cloudAccountId: string;
onFilterChange: (value: ServiceFilterType) => void;
}
function ServicesFilter({
cloudAccountId,
onFilterChange,
}: ServicesFilterProps): JSX.Element | null {
const { data: services, isLoading } = useQuery(
[REACT_QUERY_KEY.AWS_SERVICES, cloudAccountId],
() => getAwsServices(cloudAccountId),
);
const { enabledCount, availableCount } = useMemo(() => {
if (!services) {
return { enabledCount: 0, availableCount: 0 };
}
return services.reduce(
(acc, service) => {
const isEnabled =
service?.config?.logs?.enabled || service?.config?.metrics?.enabled;
return {
enabledCount: acc.enabledCount + (isEnabled ? 1 : 0),
availableCount: acc.availableCount + (isEnabled ? 0 : 1),
};
},
{ enabledCount: 0, availableCount: 0 },
);
}, [services]);
const selectOptions: SelectProps['options'] = useMemo(
() => [
{ value: 'all_services', label: `All Services (${services?.length || 0})` },
{ value: 'enabled', label: `Enabled (${enabledCount})` },
{ value: 'available', label: `Available (${availableCount})` },
],
[services, enabledCount, availableCount],
);
if (isLoading) {
return null;
}
if (!services?.length) {
return null;
}
return (
<div className="services-filter">
<Select
style={{ width: '100%' }}
defaultValue={ServiceFilterType.ALL_SERVICES}
options={selectOptions}
className="services-sidebar__select"
suffixIcon={<ChevronDown size={16} color={Color.BG_VANILLA_400} />}
onChange={onFilterChange}
/>
</div>
);
}
function ServicesSection(): JSX.Element {
const urlQuery = useUrlQuery();
const cloudAccountId = urlQuery.get('cloudAccountId') || '';
const [activeFilter, setActiveFilter] = useState<
'all_services' | 'enabled' | 'available'
>('all_services');
return (
<div className="services-section">
<div className="services-section__sidebar">
<ServicesFilter
cloudAccountId={cloudAccountId}
onFilterChange={setActiveFilter}
/>
<ServicesList cloudAccountId={cloudAccountId} filter={activeFilter} />
</div>
<div className="services-section__content">
<ServiceDetails />
</div>
</div>
);
}
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} />
</div>
);
}
export default ServicesTabs;

View File

@@ -1,161 +0,0 @@
import { act, fireEvent, screen, waitFor } from '@testing-library/react';
import { server } from 'mocks-server/server';
import { rest, RestRequest } from 'msw'; // Import RestRequest for req.json() typing
import { UpdateServiceConfigPayload } from '../types';
import { accountsResponse, CLOUD_ACCOUNT_ID, initialBuckets } from './mockData';
import {
assertGenericModalElements,
assertS3SyncSpecificElements,
renderModal,
} from './utils';
// --- MOCKS ---
jest.mock('hooks/useUrlQuery', () => ({
__esModule: true,
default: jest.fn(() => ({
get: jest.fn((paramName: string) => {
if (paramName === 'cloudAccountId') {
return CLOUD_ACCOUNT_ID;
}
return null;
}),
})),
}));
// --- TEST SUITE ---
describe('ConfigureServiceModal for S3 Sync service', () => {
jest.setTimeout(10000);
beforeEach(() => {
server.use(
rest.get(
'http://localhost/api/v1/cloud-integrations/aws/accounts',
(req, res, ctx) => res(ctx.json(accountsResponse)),
),
);
});
it('should render with logs collection switch and bucket selectors (no buckets initially selected)', async () => {
act(() => {
renderModal({}); // No initial S3 buckets, defaults to 's3sync' serviceId
});
await assertGenericModalElements(); // Use new generic assertion
await assertS3SyncSpecificElements({}); // Use new S3-specific assertion
});
it('should render with logs collection switch and bucket selectors (some buckets initially selected)', async () => {
act(() => {
renderModal(initialBuckets); // Defaults to 's3sync' serviceId
});
await assertGenericModalElements(); // Use new generic assertion
await assertS3SyncSpecificElements(initialBuckets); // Use new S3-specific assertion
});
it('should enable save button after adding a new bucket via combobox', async () => {
act(() => {
renderModal(initialBuckets); // Defaults to 's3sync' serviceId
});
await assertGenericModalElements();
await assertS3SyncSpecificElements(initialBuckets);
expect(screen.getByRole('button', { name: /save/i })).toBeDisabled();
const targetCombobox = screen.getAllByRole('combobox')[0];
const newBucketName = 'a-newly-added-bucket';
act(() => {
fireEvent.change(targetCombobox, { target: { value: newBucketName } });
fireEvent.keyDown(targetCombobox, {
key: 'Enter',
code: 'Enter',
keyCode: 13,
});
});
await waitFor(() => {
expect(screen.getByLabelText(newBucketName)).toBeInTheDocument();
expect(screen.getByRole('button', { name: /save/i })).toBeEnabled();
});
});
it('should send updated bucket configuration on save', async () => {
let capturedPayload: UpdateServiceConfigPayload | null = null;
const mockUpdateConfigUrl =
'http://localhost/api/v1/cloud-integrations/aws/services/s3sync/config';
// Override POST handler specifically for this test to capture payload
server.use(
rest.post(mockUpdateConfigUrl, async (req: RestRequest, res, ctx) => {
capturedPayload = await req.json();
return res(ctx.status(200), ctx.json({ message: 'Config updated' }));
}),
);
act(() => {
renderModal(initialBuckets); // Defaults to 's3sync' serviceId
});
await assertGenericModalElements();
await assertS3SyncSpecificElements(initialBuckets);
expect(screen.getByRole('button', { name: /save/i })).toBeDisabled();
const newBucketName = 'another-new-bucket';
// As before, targeting the first combobox, assumed to be for 'ap-south-1'.
const targetCombobox = screen.getAllByRole('combobox')[0];
act(() => {
fireEvent.change(targetCombobox, { target: { value: newBucketName } });
fireEvent.keyDown(targetCombobox, {
key: 'Enter',
code: 'Enter',
keyCode: 13,
});
});
await waitFor(() => {
expect(screen.getByLabelText(newBucketName)).toBeInTheDocument();
expect(screen.getByRole('button', { name: /save/i })).toBeEnabled();
act(() => {
fireEvent.click(screen.getByRole('button', { name: /save/i }));
});
});
await waitFor(() => {
expect(capturedPayload).not.toBeNull();
});
expect(capturedPayload).toEqual({
cloud_account_id: CLOUD_ACCOUNT_ID,
config: {
logs: {
enabled: true,
s3_buckets: {
'us-east-2': ['first-bucket', 'second-bucket'], // Existing buckets
'ap-south-1': [newBucketName], // Newly added bucket for the first region
},
},
metrics: {},
},
});
});
it('should not render S3 bucket region selector UI for services other than s3sync', async () => {
const otherServiceId = 'cloudwatch';
act(() => {
renderModal({}, otherServiceId);
});
await assertGenericModalElements();
await waitFor(() => {
expect(
screen.queryByRole('heading', { name: /select s3 buckets by region/i }),
).not.toBeInTheDocument();
const regions = accountsResponse.data.accounts[0]?.config?.regions || [];
regions.forEach((region) => {
expect(
screen.queryByText(`Enter S3 bucket names for ${region}`),
).not.toBeInTheDocument();
});
});
});
});

View File

@@ -1,44 +0,0 @@
import { IConfigureServiceModalProps } from '../ConfigureServiceModal';
const CLOUD_ACCOUNT_ID = '123456789012';
const initialBuckets = { 'us-east-2': ['first-bucket', 'second-bucket'] };
const accountsResponse = {
status: 'success',
data: {
accounts: [
{
id: 'a1b2c3d4-e5f6-7890-1234-567890abcdef',
cloud_account_id: CLOUD_ACCOUNT_ID,
config: {
regions: ['ap-south-1', 'ap-south-2', 'us-east-1', 'us-east-2'],
},
status: {
integration: {
last_heartbeat_ts_ms: 1747114366214,
},
},
},
],
},
};
const defaultModalProps: Omit<IConfigureServiceModalProps, 'initialConfig'> = {
isOpen: true,
onClose: jest.fn(),
serviceName: 'S3 Sync',
serviceId: 's3sync',
cloudAccountId: CLOUD_ACCOUNT_ID,
supportedSignals: {
logs: true,
metrics: false,
},
};
export {
accountsResponse,
CLOUD_ACCOUNT_ID,
defaultModalProps,
initialBuckets,
};

View File

@@ -1,78 +0,0 @@
import { render, RenderResult, screen, waitFor } from '@testing-library/react';
import MockQueryClientProvider from 'providers/test/MockQueryClientProvider';
import ConfigureServiceModal from '../ConfigureServiceModal';
import { accountsResponse, defaultModalProps } from './mockData';
/**
* Renders the ConfigureServiceModal with specified S3 bucket initial configurations.
*/
const renderModal = (
initialConfigLogsS3Buckets: Record<string, string[]> = {},
serviceId = 's3sync',
): RenderResult => {
const initialConfig = {
logs: { enabled: true, s3_buckets: initialConfigLogsS3Buckets },
metrics: { enabled: false },
};
return render(
<MockQueryClientProvider>
<ConfigureServiceModal
{...defaultModalProps}
serviceId={serviceId}
initialConfig={initialConfig}
/>
</MockQueryClientProvider>,
);
};
/**
* Asserts that generic UI elements of the modal are present.
*/
const assertGenericModalElements = async (): Promise<void> => {
await waitFor(() => {
expect(screen.getByRole('switch')).toBeInTheDocument();
expect(screen.getByText(/log collection/i)).toBeInTheDocument();
expect(
screen.getByText(
/to ingest logs from your aws services, you must complete several steps/i,
),
).toBeInTheDocument();
});
};
/**
* Asserts the state of S3 bucket selectors for each region, specific to S3 Sync.
*/
const assertS3SyncSpecificElements = async (
expectedBucketsByRegion: Record<string, string[]> = {},
): Promise<void> => {
const regions = accountsResponse.data.accounts[0]?.config?.regions || [];
await waitFor(() => {
expect(
screen.getByRole('heading', { name: /select s3 buckets by region/i }),
).toBeInTheDocument();
regions.forEach((region) => {
expect(screen.getByText(region)).toBeInTheDocument();
const bucketsForRegion = expectedBucketsByRegion[region] || [];
if (bucketsForRegion.length > 0) {
bucketsForRegion.forEach((bucket) => {
expect(screen.getByText(bucket)).toBeInTheDocument();
});
} else {
expect(
screen.getByText(`Enter S3 bucket names for ${region}`),
).toBeInTheDocument();
}
});
});
};
export {
assertGenericModalElements,
assertS3SyncSpecificElements,
renderModal,
};

View File

@@ -1,8 +1,10 @@
.hero-section {
height: 308px;
padding: 26px 16px;
padding: 16px;
display: flex;
gap: 24px;
flex-direction: column;
gap: 16px;
position: relative;
overflow: hidden;
background-position: right;
@@ -10,35 +12,37 @@
background-repeat: no-repeat;
border-bottom: 1px solid var(--bg-slate-500);
&__icon {
height: fit-content;
background-color: var(--bg-ink-400);
padding: 12px;
border: 1px solid var(--bg-ink-300);
border-radius: 6px;
width: 60px;
height: 60px;
display: flex;
align-items: center;
img {
width: 100%;
}
}
&__details {
display: flex;
flex-direction: column;
gap: 12px;
.title {
color: var(--bg-vanilla-100);
font-size: 24px;
font-weight: 500;
line-height: 20px;
letter-spacing: -0.12px;
&-header {
display: flex;
flex-direction: row;
align-items: center;
gap: 16px;
&__icon {
height: fit-content;
background-color: var(--bg-ink-400);
padding: 12px;
border: 1px solid var(--bg-ink-300);
border-radius: 6px;
width: 60px;
height: 60px;
}
&__title {
color: var(--bg-vanilla-100);
font-size: 24px;
font-weight: 500;
line-height: 20px;
letter-spacing: -0.12px;
}
}
.description {
&__description {
color: var(--bg-vanilla-400);
font-size: 12px;
font-weight: 400;

View File

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

View File

@@ -4,14 +4,57 @@
&-with-account {
display: flex;
flex-direction: column;
gap: 10px;
flex-direction: row;
align-items: center;
justify-content: space-between;
flex-wrap: wrap;
gap: 16px;
border-radius: 4px;
border: 1px solid var(--l3-background);
background: var(--l1-background);
.selected-cloud-integration-account-status {
display: flex;
align-items: center;
justify-content: center;
border-right: 1px solid var(--l3-background);
border-radius: none;
width: 32px;
}
&-selector-container {
display: flex;
flex-direction: row;
align-items: center;
.account-selector-label {
color: var(--bg-vanilla-400);
font-size: 12px;
font-weight: 400;
line-height: 16px;
padding: 8px 16px;
}
.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;
}
}
}
}
}
}
&__input-skeleton {
width: 300px;
margin-bottom: 16px;
}
&__new-account-button-skeleton {
@@ -22,11 +65,13 @@
&__account-settings-button-skeleton {
width: 140px;
}
&__action-buttons {
display: flex;
align-items: center;
gap: 8px;
}
&__action-button {
font-family: 'Inter';
border-radius: 2px;
@@ -54,44 +99,6 @@
}
}
.cloud-account-selector {
border-radius: 2px;
border: 1px solid var(--bg-ink-300);
background: linear-gradient(
139deg,
rgba(18, 19, 23, 0.8) 0%,
rgba(18, 19, 23, 0.9) 98.68%
);
box-shadow: 4px 10px 16px 2px rgba(0, 0, 0, 0.2);
-webkit-backdrop-filter: blur(20px);
backdrop-filter: blur(20px);
.ant-select-selector {
border-color: var(--bg-slate-400) !important;
background: var(--bg-ink-300) !important;
padding: 6px 8px !important;
}
.ant-select-selection-item {
color: var(--bg-vanilla-400);
font-size: 12px;
font-weight: 400;
line-height: 16px;
}
.account-option-item {
display: flex;
align-items: center;
justify-content: space-between;
&__selected {
display: flex;
align-items: center;
justify-content: center;
height: 14px;
width: 14px;
background-color: rgba(192, 193, 195, 0.2); /* #C0C1C3 with 0.2 opacity */
border-radius: 2px;
}
}
}
.lightMode {
.hero-section__action-button {
&.primary {

View File

@@ -1,58 +1,23 @@
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 type { SelectProps } from 'antd/lib';
import { Select, Skeleton } from 'antd';
import { SelectProps } from 'antd/lib';
import logEvent from 'api/common/logEvent';
import { useAwsAccounts } from 'hooks/integration/aws/useAwsAccounts';
import { useListAccounts } from 'api/generated/services/cloudintegration';
import { getAccountById } from 'container/Integrations/CloudIntegration/utils';
import { INTEGRATION_TYPES } from 'container/Integrations/constants';
import useUrlQuery from 'hooks/useUrlQuery';
import { Check, ChevronDown } from 'lucide-react';
import { ChevronDown, Dot, PencilLine, Plug, Plus } from 'lucide-react';
import { CloudAccount } from '../../ServicesSection/types';
import { mapAccountDtoToAwsCloudAccount } from '../../mapAwsCloudAccountFromDto';
import { CloudAccount } from '../../types';
import AccountSettingsModal from './AccountSettingsModal';
import CloudAccountSetupModal from './CloudAccountSetupModal';
import './AccountActions.style.scss';
interface AccountOptionItemProps {
label: React.ReactNode;
isSelected: boolean;
}
function AccountOptionItem({
label,
isSelected,
}: AccountOptionItemProps): JSX.Element {
return (
<div className="account-option-item">
{label}
{isSelected && (
<div className="account-option-item__selected">
<Check size={12} color={Color.BG_VANILLA_100} />
</div>
)}
</div>
);
}
function renderOption(
option: any,
activeAccountId: string | undefined,
): JSX.Element {
return (
<AccountOptionItem
label={option.label}
isSelected={option.value === activeAccountId}
/>
);
}
const getAccountById = (
accounts: CloudAccount[],
accountId: string,
): CloudAccount | null =>
accounts.find((account) => account.cloud_account_id === accountId) || null;
function AccountActionsRenderer({
accounts,
isLoading,
@@ -73,55 +38,51 @@ 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>
);
}
if (accounts?.length) {
return (
<div className="hero-section__actions-with-account">
<Select
value={`Account: ${activeAccount?.cloud_account_id}`}
options={selectOptions}
rootClassName="cloud-account-selector"
placeholder="Select AWS Account"
suffixIcon={<ChevronDown size={16} color={Color.BG_VANILLA_400} />}
optionRender={(option): JSX.Element =>
renderOption(option, activeAccount?.cloud_account_id)
}
onChange={onAccountChange}
/>
<div className="hero-section__actions-with-account-selector-container">
<div className="selected-cloud-integration-account-status">
<Dot size={24} color={Color.BG_FOREST_500} />
</div>
<div className="account-selector-label">Account:</div>
<span className="account-selector">
<Select
value={activeAccount?.providerAccountId}
options={selectOptions}
rootClassName="cloud-account-selector"
placeholder="Select AWS Account"
suffixIcon={<ChevronDown size={16} color={Color.BG_VANILLA_400} />}
onChange={onAccountChange}
/>
</span>
</div>
<div className="hero-section__action-buttons">
<Button
type="primary"
className="hero-section__action-button primary"
onClick={onIntegrationModalOpen}
>
Add New AWS Account
</Button>
<Button
type="default"
className="hero-section__action-button secondary"
variant="link"
size="sm"
color="secondary"
prefixIcon={<PencilLine size={14} />}
onClick={onAccountSettingsModalOpen}
>
Account Settings
Edit Account
</Button>
<Button
variant="link"
size="sm"
color="secondary"
onClick={onIntegrationModalOpen}
prefixIcon={<Plus size={14} />}
>
Add New Account
</Button>
</div>
</div>
@@ -129,8 +90,11 @@ function AccountActionsRenderer({
}
return (
<Button
className="hero-section__action-button primary"
variant="solid"
color="primary"
prefixIcon={<Plug size={14} />}
onClick={onIntegrationModalOpen}
size="sm"
>
Integrate Now
</Button>
@@ -140,7 +104,18 @@ function AccountActionsRenderer({
function AccountActions(): JSX.Element {
const urlQuery = useUrlQuery();
const navigate = useNavigate();
const { data: accounts, isLoading } = useAwsAccounts();
const { data: listAccountsResponse, isLoading } = useListAccounts({
cloudProvider: INTEGRATION_TYPES.AWS,
});
const accounts = useMemo((): CloudAccount[] | undefined => {
const raw = listAccountsResponse?.data?.accounts;
if (!raw) {
return undefined;
}
return raw
.map(mapAccountDtoToAwsCloudAccount)
.filter((account): account is CloudAccount => account !== null);
}, [listAccountsResponse]);
const initialAccount = useMemo(
() =>
@@ -162,7 +137,13 @@ function AccountActions(): JSX.Element {
const latestUrlQuery = new URLSearchParams(window.location.search);
latestUrlQuery.set('cloudAccountId', initialAccount.cloud_account_id);
navigate({ search: latestUrlQuery.toString() });
return;
}
setActiveAccount(null);
const latestUrlQuery = new URLSearchParams(window.location.search);
latestUrlQuery.delete('cloudAccountId');
navigate({ search: latestUrlQuery.toString() });
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [initialAccount]);
@@ -198,7 +179,7 @@ function AccountActions(): JSX.Element {
accounts?.length
? accounts.map((account) => ({
value: account.cloud_account_id,
label: account.cloud_account_id,
label: account.providerAccountId,
}))
: [],
[accounts],

View File

@@ -14,8 +14,13 @@
border-radius: 3px;
border: 1px solid var(--bg-slate-500);
padding: 14px;
&-account-info {
&-connected-account-details {
display: flex;
flex-direction: column;
gap: 8px;
&-title {
color: var(--bg-vanilla-100);
font-size: 14px;
@@ -38,40 +43,36 @@
}
}
}
&-regions-switch {
&-region-selector {
display: flex;
flex-direction: column;
gap: 10px;
gap: 4px;
&-title {
color: var(--bg-vanilla-100);
color: var(--l1-foreground);
font-size: 14px;
font-weight: 500;
line-height: 20px;
letter-spacing: -0.07px;
}
&-switch {
display: flex;
align-items: center;
gap: 10px;
&-label {
color: var(--bg-vanilla-400);
background-color: transparent;
border: none;
font-family: Inter;
font-size: 12px;
font-weight: 400;
line-height: 18px;
letter-spacing: -0.005em;
cursor: pointer;
}
&-description {
color: var(--l2-foreground);
font-size: 12px;
line-height: 18px;
letter-spacing: -0.06px;
}
}
&-regions-select {
margin-top: 8px;
}
}
&__footer {
padding: 16px;
display: flex;
flex-direction: row;
gap: 10px;
justify-content: space-between;
&-close-button,
&-save-button {
color: var(--bg-vanilla-100);

View File

@@ -0,0 +1,177 @@
import { Dispatch, SetStateAction, useCallback } from 'react';
import { useQueryClient } from 'react-query';
import { Button } from '@signozhq/button';
import { DrawerWrapper } from '@signozhq/drawer';
import { Form } from 'antd';
import { invalidateListAccounts } from 'api/generated/services/cloudintegration';
import { INTEGRATION_TYPES } from 'container/Integrations/constants';
import { useAccountSettingsModal } from 'hooks/integration/aws/useAccountSettingsModal';
import useUrlQuery from 'hooks/useUrlQuery';
import history from 'lib/history';
import { Save } from 'lucide-react';
import logEvent from '../../../../../../api/common/logEvent';
import { CloudAccount } from '../../types';
import { RegionSelector } from './RegionSelector';
import RemoveIntegrationAccount from './RemoveIntegrationAccount';
import './AccountSettingsModal.style.scss';
interface AccountSettingsModalProps {
onClose: () => void;
account: CloudAccount;
setActiveAccount: Dispatch<SetStateAction<CloudAccount | null>>;
}
function AccountSettingsModal({
onClose,
account,
setActiveAccount,
}: AccountSettingsModalProps): JSX.Element {
const {
form,
isLoading,
selectedRegions,
includeAllRegions,
isSaveDisabled,
setSelectedRegions,
setIncludeAllRegions,
handleSubmit,
handleClose,
} = useAccountSettingsModal({ onClose, account, setActiveAccount });
const queryClient = useQueryClient();
const urlQuery = useUrlQuery();
const handleRemoveIntegrationAccountSuccess = useCallback((): void => {
void invalidateListAccounts(queryClient, {
cloudProvider: INTEGRATION_TYPES.AWS,
});
urlQuery.delete('cloudAccountId');
setActiveAccount(null);
handleClose();
history.replace({ search: urlQuery.toString() });
logEvent('AWS Integration: Account removed', {
id: account?.id,
cloudAccountId: account?.cloud_account_id,
});
}, [
queryClient,
urlQuery,
setActiveAccount,
handleClose,
account?.id,
account?.cloud_account_id,
]);
const renderAccountDetails = useCallback(() => {
return (
<Form
form={form}
layout="vertical"
initialValues={{
selectedRegions,
includeAllRegions,
}}
>
<div className="account-settings-modal__body">
<div className="account-settings-modal__body-account-info">
<div className="account-settings-modal__body-account-info-connected-account-details">
<div className="account-settings-modal__body-account-info-connected-account-details-title">
Connected Account details
</div>
<div className="account-settings-modal__body-account-info-connected-account-details-account-id">
AWS Account:{' '}
<span className="account-settings-modal__body-account-info-connected-account-details-account-id-account-id">
{account?.id}
</span>
</div>
</div>
</div>
<div className="account-settings-modal__body-region-selector">
<div className="account-settings-modal__body-region-selector-title">
Which regions do you want to monitor?
</div>
<div className="account-settings-modal__body-region-selector-description">
Choose only the regions you want SigNoz to monitor.
</div>
<RegionSelector
selectedRegions={selectedRegions}
setSelectedRegions={setSelectedRegions}
setIncludeAllRegions={setIncludeAllRegions}
/>
</div>
</div>
<div className="account-settings-modal__footer">
<RemoveIntegrationAccount
accountId={account?.id}
onRemoveIntegrationAccountSuccess={handleRemoveIntegrationAccountSuccess}
/>
<Button
variant="solid"
color="secondary"
disabled={isSaveDisabled}
onClick={handleSubmit}
loading={isLoading}
prefixIcon={<Save size={14} />}
>
Update Changes
</Button>
</div>
</Form>
);
}, [
form,
selectedRegions,
includeAllRegions,
account?.id,
handleRemoveIntegrationAccountSuccess,
isSaveDisabled,
handleSubmit,
isLoading,
setSelectedRegions,
setIncludeAllRegions,
]);
const handleDrawerOpenChange = useCallback(
(open: boolean): void => {
if (!open) {
handleClose();
}
},
[handleClose],
);
return (
<DrawerWrapper
open={true}
type="panel"
className="account-settings-modal"
header={{
title: 'Account Settings',
}}
// onCancel={handleClose}
// onOk={handleSubmit}
// okText="Save"
// okButtonProps={{
// disabled: isSaveDisabled,
// className: 'account-settings-modal__footer-save-button',
// loading: isLoading,
// }}
// cancelButtonProps={{
// className: 'account-settings-modal__footer-close-button',
// }}
direction="right"
showCloseButton
content={renderAccountDetails()}
onOpenChange={handleDrawerOpenChange}
/>
);
}
export default AccountSettingsModal;

View File

@@ -1,4 +1,26 @@
.cloud-account-setup-modal {
> div {
display: flex;
flex-direction: column;
overflow: hidden;
}
&__content {
flex: 1;
overflow-y: auto;
min-height: 0;
scrollbar-width: thin;
&::-webkit-scrollbar {
width: 2px;
}
}
&__footer {
padding: 16px;
margin-bottom: 16px;
}
.account-setup-modal-footer {
&__confirm-button {
background: var(--bg-robin-500);
@@ -20,6 +42,8 @@
}
.cloud-account-setup-form {
padding: 16px;
.disabled {
opacity: 0.4;
}

View File

@@ -1,8 +1,7 @@
import { useCallback } from 'react';
import { useQueryClient } from 'react-query';
import { Button } from '@signozhq/button';
import { Color } from '@signozhq/design-tokens';
import SignozModal from 'components/SignozModal/SignozModal';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import { DrawerWrapper } from '@signozhq/drawer';
import { useIntegrationModal } from 'hooks/integration/aws/useIntegrationModal';
import { SquareArrowOutUpRight } from 'lucide-react';
@@ -12,19 +11,15 @@ import {
ModalStateEnum,
} from '../types';
import { RegionForm } from './RegionForm';
import { RegionSelector } from './RegionSelector';
import { SuccessView } from './SuccessView';
import './CloudAccountSetupModal.style.scss';
function CloudAccountSetupModal({
onClose,
}: IntegrationModalProps): JSX.Element {
const queryClient = useQueryClient();
const {
form,
modalState,
setModalState,
isLoading,
activeView,
selectedRegions,
@@ -32,97 +27,86 @@ function CloudAccountSetupModal({
isGeneratingUrl,
setSelectedRegions,
setIncludeAllRegions,
handleIncludeAllRegionsChange,
handleRegionSelect,
handleSubmit,
handleClose,
setActiveView,
allRegions,
accountId,
selectedDeploymentRegion,
handleRegionChange,
connectionParams,
isConnectionParamsLoading,
handleConnectionSuccess,
handleConnectionTimeout,
handleConnectionError,
} = useIntegrationModal({ onClose });
const renderContent = useCallback(() => {
if (modalState === ModalStateEnum.SUCCESS) {
return <SuccessView />;
}
if (activeView === ActiveViewEnum.SELECT_REGIONS) {
return (
<RegionSelector
return (
<div className="cloud-account-setup-modal__content">
<RegionForm
form={form}
modalState={modalState}
selectedRegions={selectedRegions}
includeAllRegions={includeAllRegions}
onRegionSelect={handleRegionSelect}
onSubmit={handleSubmit}
accountId={accountId}
handleRegionChange={handleRegionChange}
connectionParams={connectionParams}
isConnectionParamsLoading={isConnectionParamsLoading}
setSelectedRegions={setSelectedRegions}
setIncludeAllRegions={setIncludeAllRegions}
onConnectionSuccess={handleConnectionSuccess}
onConnectionTimeout={handleConnectionTimeout}
onConnectionError={handleConnectionError}
/>
);
}
return (
<RegionForm
form={form}
modalState={modalState}
setModalState={setModalState}
selectedRegions={selectedRegions}
includeAllRegions={includeAllRegions}
onIncludeAllRegionsChange={handleIncludeAllRegionsChange}
onRegionSelect={handleRegionSelect}
onSubmit={handleSubmit}
accountId={accountId}
selectedDeploymentRegion={selectedDeploymentRegion}
handleRegionChange={handleRegionChange}
connectionParams={connectionParams}
isConnectionParamsLoading={isConnectionParamsLoading}
/>
<div className="cloud-account-setup-modal__footer">
<Button
variant="solid"
color="primary"
prefixIcon={
<SquareArrowOutUpRight size={17} color={Color.BG_VANILLA_100} />
}
onClick={handleSubmit}
disabled={
selectedRegions.length === 0 ||
isLoading ||
isGeneratingUrl ||
modalState === ModalStateEnum.WAITING
}
>
Launch Cloud Formation Template
</Button>
</div>
</div>
);
}, [
modalState,
activeView,
form,
setModalState,
selectedRegions,
includeAllRegions,
handleIncludeAllRegionsChange,
handleRegionSelect,
handleSubmit,
accountId,
selectedDeploymentRegion,
handleRegionChange,
connectionParams,
isConnectionParamsLoading,
setSelectedRegions,
setIncludeAllRegions,
isLoading,
isGeneratingUrl,
handleConnectionSuccess,
handleConnectionTimeout,
handleConnectionError,
]);
const getSelectedRegionsCount = useCallback(
(): number =>
selectedRegions.includes('all') ? allRegions.length : selectedRegions.length,
[selectedRegions, allRegions],
(): number => selectedRegions.length,
[selectedRegions],
);
const getModalConfig = useCallback(() => {
// Handle success state first
if (modalState === ModalStateEnum.SUCCESS) {
return {
title: 'AWS Integration',
okText: (
<div className="cloud-account-setup-success-view__footer-button">
Continue
</div>
),
block: true,
onOk: (): void => {
queryClient.invalidateQueries([REACT_QUERY_KEY.AWS_ACCOUNTS]);
handleClose();
},
cancelButtonProps: { style: { display: 'none' } },
disabled: false,
};
}
// Handle other views
const viewConfigs = {
[ActiveViewEnum.FORM]: {
title: 'Add AWS Account',
@@ -155,35 +139,44 @@ function CloudAccountSetupModal({
isLoading,
isGeneratingUrl,
activeView,
handleClose,
setActiveView,
queryClient,
]);
const modalConfig = getModalConfig();
const handleDrawerOpenChange = (open: boolean): void => {
if (!open) {
handleClose();
}
};
return (
<SignozModal
open
<DrawerWrapper
open={true}
type="panel"
className="cloud-account-setup-modal"
title={modalConfig.title}
onCancel={handleClose}
onOk={modalConfig.onOk}
okText={modalConfig.okText}
okButtonProps={{
loading: isLoading,
disabled: selectedRegions.length === 0 || modalConfig.disabled,
className:
activeView === ActiveViewEnum.FORM
? 'cloud-account-setup-form__submit-button'
: 'account-setup-modal-footer__confirm-button',
block: activeView === ActiveViewEnum.FORM,
// allowOutsideClick={false}
content={renderContent()}
onOpenChange={handleDrawerOpenChange}
direction="right"
showCloseButton
header={{
title: modalConfig.title,
}}
cancelButtonProps={modalConfig.cancelButtonProps}
width={672}
>
{renderContent()}
</SignozModal>
// onCancel={handleClose}
// onOk={modalConfig.onOk}
// okText={modalConfig.okText}
// okButtonProps={{
// loading: isLoading,
// disabled: selectedRegions.length === 0 || modalConfig.disabled,
// className:
// activeView === ActiveViewEnum.FORM
// ? 'cloud-account-setup-form__submit-button'
// : 'account-setup-modal-footer__confirm-button',
// block: activeView === ActiveViewEnum.FORM,
// }}
// cancelButtonProps={modalConfig.cancelButtonProps}
/>
);
}

View File

@@ -1,17 +1,19 @@
import { Dispatch, SetStateAction } from 'react';
import { Color } from '@signozhq/design-tokens';
import { Form, Select, Switch } from 'antd';
import { Form, Select } from 'antd';
import { ChevronDown } from 'lucide-react';
import { Region } from 'utils/regions';
import { popupContainer } from 'utils/selectPopupContainer';
import { RegionSelector } from './RegionSelector';
// Form section components
function RegionDeploymentSection({
regions,
selectedDeploymentRegion,
handleRegionChange,
isFormDisabled,
}: {
regions: Region[];
selectedDeploymentRegion: string | undefined;
handleRegionChange: (value: string) => void;
isFormDisabled: boolean;
}): JSX.Element {
@@ -33,8 +35,8 @@ function RegionDeploymentSection({
suffixIcon={<ChevronDown size={16} color={Color.BG_VANILLA_400} />}
className="cloud-account-setup-form__select integrations-select"
onChange={handleRegionChange}
value={selectedDeploymentRegion}
disabled={isFormDisabled}
getPopupContainer={popupContainer}
>
{regions.flatMap((region) =>
region.subRegions.map((subRegion) => (
@@ -50,19 +52,13 @@ function RegionDeploymentSection({
}
function MonitoringRegionsSection({
includeAllRegions,
selectedRegions,
onIncludeAllRegionsChange,
getRegionPreviewText,
onRegionSelect,
isFormDisabled,
setSelectedRegions,
setIncludeAllRegions,
}: {
includeAllRegions: boolean;
selectedRegions: string[];
onIncludeAllRegionsChange: (checked: boolean) => void;
getRegionPreviewText: (regions: string[]) => string[];
onRegionSelect: () => void;
isFormDisabled: boolean;
setSelectedRegions: Dispatch<SetStateAction<string[]>>;
setIncludeAllRegions: Dispatch<SetStateAction<boolean>>;
}): JSX.Element {
return (
<div className="cloud-account-setup-form__form-group">
@@ -73,51 +69,12 @@ function MonitoringRegionsSection({
Choose only the regions you want SigNoz to monitor. You can enable all at
once, or pick specific ones:
</div>
<Form.Item
name="monitorRegions"
rules={[
{
validator: async (): Promise<void> => {
if (selectedRegions.length === 0) {
return Promise.reject();
}
return Promise.resolve();
},
message: 'Please select at least one region to monitor',
},
]}
className="cloud-account-setup-form__form-item"
>
<div className="cloud-account-setup-form__include-all-regions-switch">
<Switch
size="small"
checked={includeAllRegions}
onChange={onIncludeAllRegionsChange}
disabled={isFormDisabled}
/>
<button
className="cloud-account-setup-form__include-all-regions-switch-label"
type="button"
onClick={(): void =>
!isFormDisabled
? onIncludeAllRegionsChange(!includeAllRegions)
: undefined
}
>
Include all regions
</button>
</div>
<Select
suffixIcon={null}
placeholder="Select Region(s)"
className="cloud-account-setup-form__select integrations-select"
onClick={!isFormDisabled ? onRegionSelect : undefined}
mode="multiple"
maxTagCount={3}
value={getRegionPreviewText(selectedRegions)}
open={false}
/>
</Form.Item>
<RegionSelector
selectedRegions={selectedRegions}
setSelectedRegions={setSelectedRegions}
setIncludeAllRegions={setIncludeAllRegions}
/>
</div>
);
}

View File

@@ -0,0 +1,105 @@
import { useRef } from 'react';
import { Form } from 'antd';
import { useGetAccount } from 'api/generated/services/cloudintegration';
import cx from 'classnames';
import { INTEGRATION_TYPES } from 'container/Integrations/constants';
import { regions } from 'utils/regions';
import { ModalStateEnum, RegionFormProps } from '../types';
import AlertMessage from './AlertMessage';
import {
ComplianceNote,
MonitoringRegionsSection,
RegionDeploymentSection,
} from './IntegrateNowFormSections';
import RenderConnectionFields from './RenderConnectionParams';
export function RegionForm({
form,
modalState,
selectedRegions,
onSubmit,
accountId,
handleRegionChange,
connectionParams,
isConnectionParamsLoading,
setSelectedRegions,
setIncludeAllRegions,
onConnectionSuccess,
onConnectionTimeout,
onConnectionError,
}: RegionFormProps): JSX.Element {
const startTimeRef = useRef(Date.now());
const refetchInterval = 10 * 1000;
const errorTimeout = 10 * 60 * 1000;
const { isLoading: isAccountStatusLoading } = useGetAccount(
{
cloudProvider: INTEGRATION_TYPES.AWS,
id: accountId ?? '',
},
{
query: {
refetchInterval,
enabled: !!accountId && modalState === ModalStateEnum.WAITING,
onSuccess: (response) => {
const isConnected =
Boolean(response.data.providerAccountId) &&
response.data.removedAt === null;
if (isConnected) {
const cloudAccountId =
response.data.providerAccountId ?? response.data.id;
onConnectionSuccess({
cloudAccountId,
status: response.data.agentReport,
});
} else if (Date.now() - startTimeRef.current >= errorTimeout) {
onConnectionTimeout({ id: accountId });
}
},
onError: () => {
onConnectionError();
},
},
},
);
const isFormDisabled =
modalState === ModalStateEnum.WAITING || isAccountStatusLoading;
return (
<Form
form={form}
className="cloud-account-setup-form"
layout="vertical"
onFinish={onSubmit}
>
<AlertMessage modalState={modalState} />
<div
className={cx(`cloud-account-setup-form__content`, {
disabled: isFormDisabled,
})}
>
<RegionDeploymentSection
regions={regions}
handleRegionChange={handleRegionChange}
isFormDisabled={isFormDisabled}
/>
<MonitoringRegionsSection
selectedRegions={selectedRegions}
setSelectedRegions={setSelectedRegions}
setIncludeAllRegions={setIncludeAllRegions}
/>
<ComplianceNote />
<RenderConnectionFields
isConnectionParamsLoading={isConnectionParamsLoading}
connectionParams={connectionParams}
isFormDisabled={isFormDisabled}
/>
</div>
</Form>
);
}

View File

@@ -1,5 +1,6 @@
.select-all {
margin-bottom: 20px;
margin-top: 16px;
margin-bottom: 16px;
}
.regions-grid {
@@ -19,3 +20,11 @@
gap: 10px;
align-items: center;
}
.region-selector-footer {
margin-top: 36px;
display: flex;
justify-content: flex-end;
gap: 8px;
}

View File

@@ -28,10 +28,12 @@ export function RegionSelector({
<div className="region-selector">
<div className="select-all">
<Checkbox
checked={selectedRegions.includes('all')}
checked={
allRegionIds.length > 0 &&
allRegionIds.every((regionId) => selectedRegions.includes(regionId))
}
indeterminate={
selectedRegions.length > 20 &&
selectedRegions.length < allRegionIds.length
selectedRegions.length > 0 && selectedRegions.length < allRegionIds.length
}
onChange={(e): void => handleSelectAll(e.target.checked)}
>
@@ -46,10 +48,7 @@ export function RegionSelector({
{region.subRegions.map((subRegion) => (
<Checkbox
key={subRegion.id}
checked={
selectedRegions.includes('all') ||
selectedRegions.includes(subRegion.id)
}
checked={selectedRegions.includes(subRegion.id)}
onChange={(): void => handleRegionSelect(subRegion.id)}
>
{subRegion.name}

View File

@@ -0,0 +1,32 @@
.remove-integration-account-modal {
.ant-modal-content {
background-color: var(--l1-background);
border: 1px solid var(--l3-background);
border-radius: 4px;
padding: 12px;
}
.ant-modal-close {
color: var(--l1-foreground);
}
.ant-modal-header {
background-color: var(--l1-background);
color: var(--l1-foreground);
.ant-modal-title {
color: var(--l1-foreground);
}
}
.ant-modal-body {
margin-top: 16px;
color: var(--l1-foreground);
background-color: var(--l1-background);
}
.ant-modal-footer {
margin-top: 16px;
background-color: var(--l1-background);
}
}

View File

@@ -0,0 +1,96 @@
import { useState } from 'react';
import { Button } from '@signozhq/button';
import { Modal } from 'antd/lib';
import logEvent from 'api/common/logEvent';
import { useDisconnectAccount } from 'api/generated/services/cloudintegration';
import { SOMETHING_WENT_WRONG } from 'constants/api';
import { INTEGRATION_TELEMETRY_EVENTS } from 'container/Integrations/constants';
import { useNotifications } from 'hooks/useNotifications';
import { Unlink } from 'lucide-react';
import './RemoveIntegrationAccount.scss';
function RemoveIntegrationAccount({
accountId,
onRemoveIntegrationAccountSuccess,
}: {
accountId: string;
onRemoveIntegrationAccountSuccess: () => void;
}): JSX.Element {
const { notifications } = useNotifications();
const [isModalOpen, setIsModalOpen] = useState(false);
const handleDisconnect = (): void => {
setIsModalOpen(true);
};
const {
mutate: disconnectAccount,
isLoading: isRemoveIntegrationLoading,
} = useDisconnectAccount({
mutation: {
onSuccess: () => {
onRemoveIntegrationAccountSuccess?.();
setIsModalOpen(false);
},
onError: () => {
notifications.error({
message: SOMETHING_WENT_WRONG,
});
},
},
});
const handleOk = (): void => {
logEvent(INTEGRATION_TELEMETRY_EVENTS.AWS_INTEGRATION_ACCOUNT_REMOVED, {
accountId,
});
disconnectAccount({
pathParams: {
cloudProvider: 'aws',
id: accountId,
},
});
};
const handleCancel = (): void => {
setIsModalOpen(false);
};
return (
<div className="remove-integration-account-container">
<Button
variant="solid"
color="destructive"
prefixIcon={<Unlink size={14} />}
size="sm"
onClick={handleDisconnect}
disabled={isRemoveIntegrationLoading}
>
Disconnect
</Button>
<Modal
className="remove-integration-account-modal"
open={isModalOpen}
title="Remove integration"
onOk={handleOk}
onCancel={handleCancel}
okText="Remove Account"
okButtonProps={{
danger: true,
loading: isRemoveIntegrationLoading,
}}
>
Removing this account will remove all components created for sending
telemetry to SigNoz in your AWS account within the next ~15 minutes
(cloudformation stacks named signoz-integration-telemetry-collection in
enabled regions). <br />
<br />
After that, you can delete the cloudformation stack that was created
manually when connecting this account.
</Modal>
</div>
);
}
export default RemoveIntegrationAccount;

View File

@@ -1,5 +1,5 @@
import { Form, Input } from 'antd';
import { ConnectionParams } from 'types/api/integrations/aws';
import { CloudintegrationtypesCredentialsDTO } from 'api/generated/services/sigNoz.schemas';
function RenderConnectionFields({
isConnectionParamsLoading,
@@ -7,51 +7,51 @@ function RenderConnectionFields({
isFormDisabled,
}: {
isConnectionParamsLoading?: boolean;
connectionParams?: ConnectionParams | null;
connectionParams?: CloudintegrationtypesCredentialsDTO | null;
isFormDisabled?: boolean;
}): JSX.Element | null {
if (
isConnectionParamsLoading ||
(!!connectionParams?.ingestion_url &&
!!connectionParams?.ingestion_key &&
!!connectionParams?.signoz_api_url &&
!!connectionParams?.signoz_api_key)
(!!connectionParams?.ingestionUrl &&
!!connectionParams?.ingestionKey &&
!!connectionParams?.sigNozApiUrl &&
!!connectionParams?.sigNozApiKey)
) {
return null;
}
return (
<Form.Item name="connection_params">
{!connectionParams?.ingestion_url && (
<Form.Item name="connectionParams">
{!connectionParams?.ingestionUrl && (
<Form.Item
name="ingestion_url"
name="ingestionUrl"
label="Ingestion URL"
rules={[{ required: true, message: 'Please enter ingestion URL' }]}
>
<Input placeholder="Enter ingestion URL" disabled={isFormDisabled} />
</Form.Item>
)}
{!connectionParams?.ingestion_key && (
{!connectionParams?.ingestionKey && (
<Form.Item
name="ingestion_key"
name="ingestionKey"
label="Ingestion Key"
rules={[{ required: true, message: 'Please enter ingestion key' }]}
>
<Input placeholder="Enter ingestion key" disabled={isFormDisabled} />
</Form.Item>
)}
{!connectionParams?.signoz_api_url && (
{!connectionParams?.sigNozApiUrl && (
<Form.Item
name="signoz_api_url"
name="sigNozApiUrl"
label="SigNoz API URL"
rules={[{ required: true, message: 'Please enter SigNoz API URL' }]}
>
<Input placeholder="Enter SigNoz API URL" disabled={isFormDisabled} />
</Form.Item>
)}
{!connectionParams?.signoz_api_key && (
{!connectionParams?.sigNozApiKey && (
<Form.Item
name="signoz_api_key"
name="sigNozApiKey"
label="SigNoz API KEY"
rules={[{ required: true, message: 'Please enter SigNoz API Key' }]}
>

View File

@@ -1,6 +1,6 @@
import { Dispatch, SetStateAction } from 'react';
import { FormInstance } from 'antd';
import { ConnectionParams } from 'types/api/integrations/aws';
import { CloudintegrationtypesCredentialsDTO } from 'api/generated/services/sigNoz.schemas';
export enum ActiveViewEnum {
SELECT_REGIONS = 'select-regions',
@@ -11,23 +11,27 @@ export enum ModalStateEnum {
FORM = 'form',
WAITING = 'waiting',
ERROR = 'error',
SUCCESS = 'success',
}
export interface RegionFormProps {
form: FormInstance;
modalState: ModalStateEnum;
setModalState: Dispatch<SetStateAction<ModalStateEnum>>;
selectedRegions: string[];
includeAllRegions: boolean;
onIncludeAllRegionsChange: (checked: boolean) => void;
onRegionSelect: () => void;
onSubmit: () => Promise<void>;
accountId?: string;
selectedDeploymentRegion: string | undefined;
handleRegionChange: (value: string) => void;
connectionParams?: ConnectionParams;
connectionParams?: CloudintegrationtypesCredentialsDTO;
isConnectionParamsLoading?: boolean;
setSelectedRegions: Dispatch<SetStateAction<string[]>>;
setIncludeAllRegions: Dispatch<SetStateAction<boolean>>;
onConnectionSuccess: (payload: {
cloudAccountId: string;
status?: unknown;
}) => void;
onConnectionTimeout: (payload: { id?: string }) => void;
onConnectionError: () => void;
}
export interface IntegrationModalProps {

View File

@@ -0,0 +1,53 @@
.s3-buckets-selector {
display: flex;
flex-direction: column;
gap: 8px;
padding: 12px;
background: var(--l2-background);
border-radius: 4px;
.s3-buckets-selector-title {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
gap: 8px;
font-size: 12px;
color: var(--l2-foreground);
}
.s3-buckets-selector-content {
display: flex;
flex-direction: column;
gap: 12px;
.s3-buckets-selector-region {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
gap: 8px;
.s3-buckets-selector-region-header {
display: flex;
flex-direction: column;
gap: 4px;
}
.s3-buckets-selector-region-help {
color: var(--l2-foreground);
font-size: 12px;
line-height: 18px;
letter-spacing: -0.06px;
}
.s3-buckets-selector-region-select {
flex: 1;
.ant-select {
width: 100%;
}
}
}
}
}

View File

@@ -1,13 +1,18 @@
import { useCallback, useMemo, useState } from 'react';
import { Form, Select, Skeleton, Typography } from 'antd';
import { useAwsAccounts } from 'hooks/integration/aws/useAwsAccounts';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { Select, Skeleton } from 'antd';
import { useListAccounts } from 'api/generated/services/cloudintegration';
import { INTEGRATION_TYPES } from 'container/Integrations/constants';
import useUrlQuery from 'hooks/useUrlQuery';
const { Title } = Typography;
import { mapAccountDtoToAwsCloudAccount } from '../mapAwsCloudAccountFromDto';
import { CloudAccount } from '../types';
import './S3BucketsSelector.styles.scss';
interface S3BucketsSelectorProps {
onChange?: (bucketsByRegion: Record<string, string[]>) => void;
initialBucketsByRegion?: Record<string, string[]>;
disabled?: boolean;
}
/**
@@ -17,13 +22,29 @@ interface S3BucketsSelectorProps {
function S3BucketsSelector({
onChange,
initialBucketsByRegion = {},
disabled: isSelectorDisabled = false,
}: S3BucketsSelectorProps): JSX.Element {
const cloudAccountId = useUrlQuery().get('cloudAccountId');
const { data: accounts, isLoading } = useAwsAccounts();
const { data: listAccountsResponse, isLoading } = useListAccounts({
cloudProvider: INTEGRATION_TYPES.AWS,
});
const accounts = useMemo((): CloudAccount[] | undefined => {
const raw = listAccountsResponse?.data?.accounts;
if (!raw) {
return undefined;
}
return raw
.map(mapAccountDtoToAwsCloudAccount)
.filter((account): account is CloudAccount => account !== null);
}, [listAccountsResponse]);
const [bucketsByRegion, setBucketsByRegion] = useState<
Record<string, string[]>
>(initialBucketsByRegion);
useEffect(() => {
setBucketsByRegion(initialBucketsByRegion);
}, [initialBucketsByRegion]);
// Find the active AWS account based on the URL query parameter
const activeAccount = useMemo(
() =>
@@ -81,37 +102,41 @@ function S3BucketsSelector({
return (
<div className="s3-buckets-selector">
<Title level={5}>Select S3 Buckets by Region</Title>
<div className="s3-buckets-selector-title">Select S3 Buckets by Region</div>
<div className="s3-buckets-selector-content">
{allRegions.map((region) => {
const isRegionUnavailable = isRegionDisabled(region);
{allRegions.map((region) => {
const disabled = isRegionDisabled(region);
return (
<Form.Item
key={region}
label={region}
{...(disabled && {
help:
'Region disabled in account settings; S3 buckets here will not be synced.',
validateStatus: 'warning',
})}
>
<Select
mode="tags"
placeholder={`Enter S3 bucket names for ${region}`}
value={bucketsByRegion[region] || []}
onChange={(value): void => handleRegionBucketsChange(region, value)}
tokenSeparators={[',']}
allowClear
disabled={disabled}
suffixIcon={null}
notFoundContent={null}
filterOption={false}
showSearch
/>
</Form.Item>
);
})}
return (
<div key={region} className="s3-buckets-selector-region">
<div className="s3-buckets-selector-region-header">
<div className="s3-buckets-selector-region-label">{region}</div>
{isRegionUnavailable && (
<div className="s3-buckets-selector-region-help">
Region disabled in account settings; S3 buckets here will not be
synced.
</div>
)}
</div>
<div className="s3-buckets-selector-region-select">
<Select
mode="tags"
placeholder={`Enter S3 bucket names for ${region}`}
value={bucketsByRegion[region] || []}
onChange={(value): void => handleRegionBucketsChange(region, value)}
tokenSeparators={[',']}
allowClear
disabled={isSelectorDisabled || isRegionUnavailable}
suffixIcon={null}
notFoundContent={null}
filterOption={false}
showSearch
/>
</div>
</div>
);
})}
</div>
</div>
);
}

View File

@@ -0,0 +1,113 @@
.aws-service-dashboards {
border-radius: 6px;
border: 1px solid var(--l3-background);
background: var(--l1-background);
.aws-service-dashboards-title {
display: flex;
flex-direction: row;
align-items: center;
gap: 8px;
color: var(--l2-foreground);
font-family: Inter;
font-size: 13px;
font-style: normal;
font-weight: 500;
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
padding: 8px 16px;
border-bottom: 1px solid var(--l3-background);
}
.aws-service-dashboards-items {
display: flex;
flex-direction: column;
.aws-service-dashboard-item {
display: flex;
flex-direction: row;
align-items: flex-start;
justify-content: space-between;
gap: 12px;
padding: 12px 16px 12px 16px;
border-bottom: 1px solid var(--l3-background);
width: 100%;
text-align: left;
&:last-child {
border-bottom: none;
}
&.aws-service-dashboard-item-clickable {
cursor: pointer;
transition: background-color 0.15s ease;
&:hover {
background-color: var(--bg-vanilla-200);
}
&:focus-visible {
outline: 1px solid var(--bg-robin-500);
outline-offset: -1px;
}
}
.aws-service-dashboard-item-content {
display: flex;
flex-direction: column;
gap: 8px;
flex: 1;
min-width: 0;
width: 100%;
.aws-service-dashboard-item-title {
color: var(--l1-foreground);
font-family: Inter;
font-size: 13px;
font-style: normal;
font-weight: 500;
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
text-align: left;
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
gap: 4px;
}
.aws-service-dashboard-item-description {
color: var(--l2-foreground);
font-family: Inter;
font-size: 12px;
font-style: normal;
font-weight: 400;
line-height: 18px; /* 150% */
text-align: left;
}
}
.aws-service-dashboard-item-open-new-tab {
display: inline-flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
padding: 4px;
border: none;
border-radius: 4px;
background: transparent;
color: var(--l2-foreground);
cursor: pointer;
opacity: 0.8;
margin-top: 1px;
&:hover {
background: var(--bg-vanilla-300);
color: var(--l1-foreground);
}
}
}
}
}

View File

@@ -0,0 +1,86 @@
/* eslint-disable sonarjs/cognitive-complexity */
import {
CloudintegrationtypesDashboardDTO,
CloudintegrationtypesServiceDTO,
} from 'api/generated/services/sigNoz.schemas';
import { useSafeNavigate } from 'hooks/useSafeNavigate';
import './ServiceDashboards.styles.scss';
function ServiceDashboards({
service,
isInteractive = true,
}: {
service: Pick<CloudintegrationtypesServiceDTO, 'assets'>;
isInteractive?: boolean;
}): JSX.Element {
const dashboards = service?.assets?.dashboards || [];
const { safeNavigate } = useSafeNavigate();
if (!dashboards.length) {
return <></>;
}
return (
<div className="aws-service-dashboards">
<div className="aws-service-dashboards-title">Dashboards</div>
<div className="aws-service-dashboards-items">
{dashboards.map((dashboard: CloudintegrationtypesDashboardDTO) => {
if (!dashboard.id) {
return null;
}
const dashboardUrl = `/dashboard/${dashboard.id}`;
return (
<div
key={dashboard.id}
className={`aws-service-dashboard-item ${
isInteractive ? 'aws-service-dashboard-item-clickable' : ''
}`}
role={isInteractive ? 'button' : undefined}
tabIndex={isInteractive ? 0 : -1}
onClick={(event): void => {
if (!isInteractive) {
return;
}
if (event.metaKey || event.ctrlKey) {
window.open(dashboardUrl, '_blank', 'noopener,noreferrer');
return;
}
safeNavigate(dashboardUrl);
}}
onAuxClick={(event): void => {
if (!isInteractive) {
return;
}
if (event.button === 1) {
window.open(dashboardUrl, '_blank', 'noopener,noreferrer');
}
}}
onKeyDown={(event): void => {
if (!isInteractive) {
return;
}
if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault();
safeNavigate(dashboardUrl);
}
}}
>
<div className="aws-service-dashboard-item-content">
<div className="aws-service-dashboard-item-title">
{dashboard.title}
</div>
<div className="aws-service-dashboard-item-description">
{dashboard.description}
</div>
</div>
</div>
);
})}
</div>
</div>
);
}
export default ServiceDashboards;

View File

@@ -0,0 +1,209 @@
.aws-service-details-container {
display: flex;
flex-direction: column;
width: 100%;
.aws-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;
}
.aws-service-details-data-collected-content-logs,
.aws-service-details-data-collected-content-metrics {
display: flex;
flex-direction: row;
gap: 8px;
.aws-service-details-data-collected-content-title {
display: flex;
flex-direction: row;
align-items: center;
gap: 8px;
color: var(--l2-foreground);
/* 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;
}
}
.aws-service-details-overview {
display: flex;
flex-direction: column;
gap: 16px;
.aws-service-details-overview-configuration {
display: flex;
flex-direction: column;
border-radius: 4px;
border: 1px solid var(--l3-background);
background: var(--l1-background);
.aws-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;
padding: 8px 12px;
.aws-service-details-overview-configuration-title-text {
color: var(--l1-foreground);
font-family: Inter;
font-size: 13px;
font-style: normal;
font-weight: 400;
line-height: 22px; /* 157.143% */
letter-spacing: -0.07px;
display: flex;
flex-direction: row;
align-items: center;
gap: 8px;
}
.configuration-action {
display: flex;
flex-direction: row;
align-items: center;
gap: 8px;
}
}
.aws-service-details-overview-configuration-s3-buckets {
padding: 12px;
background: var(--l1-background);
}
.aws-service-details-overview-configuration-content {
display: flex;
flex-direction: column;
gap: 16px;
padding: 12px;
background: var(--l1-background);
.aws-service-details-overview-configuration-content-item {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
gap: 8px;
}
}
.aws-service-details-overview-configuration-actions {
display: flex;
flex-direction: row;
justify-content: flex-end;
gap: 8px;
padding: 8px 12px;
border-top: 1px solid var(--l3-background);
background: var(--l1-background);
.discard-btn {
width: 100px;
}
.save-btn {
width: 100px;
}
}
.aws-service-details-overview-configuration-title-text-select-all {
color: var(--l2-foreground);
font-family: Inter;
font-size: 12px;
font-style: normal;
font-weight: 400;
line-height: 18px; /* 138.462% */
}
}
.aws-service-details-actions {
display: flex;
flex-direction: row;
gap: 8px;
padding: 12px 0;
}
.aws-service-dashboards {
border-radius: 6px;
border: 1px solid var(--l3-background);
background: var(--l1-background);
.aws-service-dashboards-title {
display: flex;
flex-direction: row;
align-items: center;
gap: 8px;
color: var(--l2-foreground);
font-family: Inter;
font-size: 13px;
font-style: normal;
font-weight: 500;
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
padding: 8px 16px;
border-bottom: 1px solid var(--l3-background);
}
.aws-service-dashboards-items {
display: flex;
flex-direction: column;
.aws-service-dashboard-item {
display: flex;
flex-direction: column;
gap: 8px;
padding: 12px 16px 12px 16px;
&.aws-service-dashboard-item-clickable {
cursor: pointer;
&:hover {
background-color: var(--l2-background);
}
}
.aws-service-dashboard-item-title {
color: var(--l1-foreground);
font-family: Inter;
font-size: 13px;
font-style: normal;
font-weight: 500;
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
}
.aws-service-dashboard-item-description {
color: var(--l2-foreground);
font-family: Inter;
font-size: 12px;
font-style: normal;
font-weight: 400;
line-height: 18px; /* 150% */
}
}
}
}
}
}
}

View File

@@ -0,0 +1,421 @@
import { useCallback, useEffect } from 'react';
import { Controller, useForm } from 'react-hook-form';
import { useQueryClient } from 'react-query';
import { Button } from '@signozhq/button';
import { toast } from '@signozhq/sonner';
import { Switch } from '@signozhq/switch';
import Tabs from '@signozhq/tabs';
import { Skeleton } from 'antd';
import logEvent from 'api/common/logEvent';
import {
getListServicesMetadataQueryKey,
invalidateGetService,
invalidateListServicesMetadata,
useGetService,
useUpdateService,
} from 'api/generated/services/cloudintegration';
import {
CloudintegrationtypesServiceDTO,
ListServicesMetadata200,
} from 'api/generated/services/sigNoz.schemas';
import CloudServiceDataCollected from 'components/CloudIntegrations/CloudServiceDataCollected/CloudServiceDataCollected';
import { MarkdownRenderer } from 'components/MarkdownRenderer/MarkdownRenderer';
import ServiceDashboards from 'container/Integrations/CloudIntegration/AmazonWebServices/ServiceDashboards/ServiceDashboards';
import { INTEGRATION_TYPES } from 'container/Integrations/constants';
import { IServiceStatus } from 'container/Integrations/types';
import useUrlQuery from 'hooks/useUrlQuery';
import { Save, X } from 'lucide-react';
import S3BucketsSelector from '../S3BucketsSelector/S3BucketsSelector';
import './ServiceDetails.styles.scss';
type ServiceConfigFormValues = {
logsEnabled: boolean;
metricsEnabled: boolean;
s3BucketsByRegion: Record<string, string[]>;
};
type ServiceDetailsData = CloudintegrationtypesServiceDTO & {
status?: IServiceStatus;
};
function ServiceDetails(): JSX.Element | null {
const urlQuery = useUrlQuery();
const cloudAccountId = urlQuery.get('cloudAccountId');
const serviceId = urlQuery.get('service');
const isReadOnly = !cloudAccountId;
const serviceQueryParams = cloudAccountId
? { cloud_integration_id: cloudAccountId }
: undefined;
const {
queryKey: _queryKey,
data: serviceDetailsData,
isLoading: isServiceDetailsLoading,
} = useGetService(
{
cloudProvider: INTEGRATION_TYPES.AWS,
serviceId: serviceId || '',
},
{
...serviceQueryParams,
},
{
query: {
enabled: !!serviceId,
select: (response): ServiceDetailsData => response.data,
},
},
);
const awsConfig = serviceDetailsData?.cloudIntegrationService?.config?.aws;
const isServiceEnabledInPersistedConfig =
Boolean(awsConfig?.logs?.enabled) || Boolean(awsConfig?.metrics?.enabled);
const serviceDetailsId = serviceDetailsData?.id;
const {
control,
handleSubmit: handleFormSubmit,
reset,
watch,
formState: { isDirty },
} = useForm<ServiceConfigFormValues>({
defaultValues: {
logsEnabled: awsConfig?.logs?.enabled || false,
metricsEnabled: awsConfig?.metrics?.enabled || false,
s3BucketsByRegion: awsConfig?.logs?.s3Buckets || {},
},
});
const resetToAwsConfig = useCallback((): void => {
reset({
logsEnabled: awsConfig?.logs?.enabled || false,
metricsEnabled: awsConfig?.metrics?.enabled || false,
s3BucketsByRegion: awsConfig?.logs?.s3Buckets || {},
});
}, [awsConfig, reset]);
// Ensure form state does not leak across service switches while new details load.
useEffect(() => {
reset({
logsEnabled: false,
metricsEnabled: false,
s3BucketsByRegion: {},
});
}, [reset, serviceId]);
useEffect(() => {
resetToAwsConfig();
}, [resetToAwsConfig, serviceDetailsId]);
// log telemetry event on visiting details of a service.
useEffect(() => {
if (serviceId) {
logEvent('AWS Integration: Service viewed', {
cloudAccountId,
serviceId,
});
}
}, [cloudAccountId, serviceId]);
const {
mutate: updateService,
isLoading: isUpdatingServiceConfig,
} = useUpdateService();
const queryClient = useQueryClient();
const handleDiscard = useCallback((): void => {
resetToAwsConfig();
}, [resetToAwsConfig]);
const onSubmit = useCallback(
async (values: ServiceConfigFormValues): Promise<void> => {
const { logsEnabled, metricsEnabled, s3BucketsByRegion } = values;
try {
if (!serviceId || !cloudAccountId) {
return;
}
updateService(
{
pathParams: {
cloudProvider: INTEGRATION_TYPES.AWS,
id: cloudAccountId,
serviceId,
},
data: {
config: {
aws: {
logs: {
enabled: logsEnabled,
s3Buckets: s3BucketsByRegion,
},
metrics: {
enabled: metricsEnabled,
},
},
},
},
},
{
onSuccess: () => {
// Immediately sync form state to remove dirty flag and hide actions,
// instead of waiting for the refetch to complete.
reset(values);
const servicesListQueryKey = getListServicesMetadataQueryKey(
{
cloudProvider: INTEGRATION_TYPES.AWS,
},
{
cloud_integration_id: cloudAccountId,
},
);
queryClient.setQueryData<ListServicesMetadata200 | undefined>(
servicesListQueryKey,
(prev) => {
if (!prev?.data?.services?.length) {
return prev;
}
const isServiceEnabled = logsEnabled || metricsEnabled;
return {
...prev,
data: {
...prev.data,
services: prev.data.services.map((service) =>
service.id === serviceId
? { ...service, enabled: isServiceEnabled }
: service,
),
},
};
},
);
invalidateGetService(
queryClient,
{
cloudProvider: INTEGRATION_TYPES.AWS,
serviceId,
},
{
cloud_integration_id: cloudAccountId,
},
);
invalidateListServicesMetadata(
queryClient,
{
cloudProvider: INTEGRATION_TYPES.AWS,
},
{
cloud_integration_id: cloudAccountId,
},
);
logEvent('AWS Integration: Service settings saved', {
cloudAccountId,
serviceId,
logsEnabled,
metricsEnabled,
});
},
onError: (error) => {
console.error('Failed to update service config:', error);
toast.error('Failed to update service config', {
description: error?.message,
});
},
},
);
} catch (error) {
console.error('Form submission failed:', error);
}
},
[serviceId, cloudAccountId, updateService, queryClient, reset],
);
if (isServiceDetailsLoading) {
return (
<div className="service-details-loading">
<Skeleton active />
<Skeleton active />
</div>
);
}
if (!serviceDetailsData) {
return null;
}
// eslint-disable-next-line sonarjs/cognitive-complexity
const renderOverview = (): JSX.Element => {
const logsEnabled = watch('logsEnabled');
const s3BucketsByRegion = watch('s3BucketsByRegion');
const isLogsSupported = serviceDetailsData?.supportedSignals?.logs || false;
const isMetricsSupported =
serviceDetailsData?.supportedSignals?.metrics || false;
const hasUnsavedChanges = isDirty;
const isS3SyncBucketsMissing =
serviceId === 's3sync' &&
logsEnabled &&
(!s3BucketsByRegion || Object.keys(s3BucketsByRegion).length === 0);
return (
<div className="aws-service-details-overview ">
{!isServiceDetailsLoading && (
<form
className="aws-service-details-overview-configuration"
onSubmit={handleFormSubmit(onSubmit)}
>
{isLogsSupported && (
<div className="aws-service-details-overview-configuration-logs">
<div className="aws-service-details-overview-configuration-title">
<div className="aws-service-details-overview-configuration-title-text">
<span>Log Collection</span>
</div>
<div className="configuration-action">
<Controller<ServiceConfigFormValues, 'logsEnabled'>
control={control}
name="logsEnabled"
render={({ field }): JSX.Element => (
<Switch
checked={field.value}
disabled={isUpdatingServiceConfig || isReadOnly}
onCheckedChange={(checked): void => {
field.onChange(checked);
}}
/>
)}
/>
</div>
</div>
{logsEnabled && serviceId === 's3sync' && (
<div className="aws-service-details-overview-configuration-s3-buckets">
<Controller<ServiceConfigFormValues, 's3BucketsByRegion'>
control={control}
name="s3BucketsByRegion"
render={({ field }): JSX.Element => (
<S3BucketsSelector
initialBucketsByRegion={field.value}
onChange={field.onChange}
disabled={isReadOnly}
/>
)}
/>
</div>
)}
</div>
)}
{isMetricsSupported && (
<div className="aws-service-details-overview-configuration-metrics">
<div className="aws-service-details-overview-configuration-title">
<div className="aws-service-details-overview-configuration-title-text">
<span>Metric Collection</span>
</div>
<div className="configuration-action">
<Controller<ServiceConfigFormValues, 'metricsEnabled'>
control={control}
name="metricsEnabled"
render={({ field }): JSX.Element => (
<Switch
checked={field.value}
disabled={isUpdatingServiceConfig || isReadOnly}
onCheckedChange={field.onChange}
/>
)}
/>
</div>
</div>
</div>
)}
{hasUnsavedChanges && !isReadOnly && (
<div className="aws-service-details-overview-configuration-actions">
<Button
variant="solid"
color="secondary"
onClick={handleDiscard}
disabled={isUpdatingServiceConfig}
size="xs"
prefixIcon={<X size={14} />}
className="discard-btn"
type="button"
>
Discard
</Button>
<Button
variant="solid"
color="primary"
size="xs"
className="save-btn"
prefixIcon={<Save size={14} />}
type="submit"
loading={isUpdatingServiceConfig}
disabled={isS3SyncBucketsMissing || isUpdatingServiceConfig}
>
Save
</Button>
</div>
)}
</form>
)}
<MarkdownRenderer
variables={{}}
markdownContent={serviceDetailsData?.overview}
/>
<ServiceDashboards
service={serviceDetailsData}
isInteractive={!isReadOnly && isServiceEnabledInPersistedConfig}
/>
</div>
);
};
const renderDataCollected = (): JSX.Element => {
return (
<div className="aws-service-details-data-collected-table">
<CloudServiceDataCollected
logsData={serviceDetailsData?.dataCollected?.logs || []}
metricsData={serviceDetailsData?.dataCollected?.metrics || []}
/>
</div>
);
};
return (
<div className="aws-service-details-container">
<Tabs
defaultValue="overview"
className="aws-service-details-tabs"
items={[
{
children: renderOverview(),
key: 'overview',
label: 'Overview',
},
{
children: renderDataCollected(),
key: 'data-collected',
label: 'Data Collected',
},
]}
variant="secondary"
/>
</div>
);
}
export default ServiceDetails;

View File

@@ -0,0 +1,153 @@
import { useCallback, useEffect, useMemo } from 'react';
import { useNavigate } from 'react-router-dom-v5-compat';
import { Skeleton } from 'antd';
import { useListServicesMetadata } from 'api/generated/services/cloudintegration';
import type { CloudintegrationtypesServiceMetadataDTO } from 'api/generated/services/sigNoz.schemas';
import cx from 'classnames';
import useUrlQuery from 'hooks/useUrlQuery';
interface ServicesListProps {
cloudAccountId: string;
}
function ServicesList({ cloudAccountId }: ServicesListProps): JSX.Element {
const urlQuery = useUrlQuery();
const navigate = useNavigate();
const hasValidCloudAccountId = Boolean(cloudAccountId);
const serviceQueryParams = hasValidCloudAccountId
? { cloud_integration_id: cloudAccountId }
: undefined;
const { data: servicesMetadata, isLoading } = useListServicesMetadata(
{
cloudProvider: 'aws',
},
serviceQueryParams,
);
const awsServices = useMemo(() => servicesMetadata?.data?.services ?? [], [
servicesMetadata,
]);
const activeService = urlQuery.get('service');
const handleActiveService = useCallback(
(serviceId: string): void => {
const latestUrlQuery = new URLSearchParams(window.location.search);
latestUrlQuery.set('service', serviceId);
navigate({ search: latestUrlQuery.toString() });
},
[navigate],
);
const enabledServices = useMemo(
() => awsServices.filter((service) => service.enabled),
[awsServices],
);
// 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(
() => awsServices?.filter((s) => !enabledIds.has(s.id)) ?? [],
[awsServices, enabledIds],
);
useEffect(() => {
const allServices = [...enabledServices, ...notEnabledServices];
const defaultServiceId =
enabledServices[0]?.id ?? notEnabledServices[0]?.id ?? null;
// If a service is already selected and still exists in the refreshed list, keep it
if (activeService && allServices.some((s) => s.id === activeService)) {
return;
}
// No valid selection — pick a default
if (defaultServiceId) {
handleActiveService(defaultServiceId);
}
}, [activeService, enabledServices, notEnabledServices, handleActiveService]);
if (isLoading) {
return (
<div className="services-list-loading">
<Skeleton active />
<Skeleton active />
</div>
);
}
if (!awsServices?.length) {
return (
<div className="services-list-empty-message">
{' '}
<img
src="/Icons/emptyState.svg"
alt="no-services-found"
className="empty-state-svg"
/>{' '}
No services found
</div>
);
}
const isEnabledServicesEmpty = enabledServices.length === 0;
const isNotEnabledServicesEmpty = notEnabledServices.length === 0;
const renderServiceItem = (
service: CloudintegrationtypesServiceMetadataDTO,
): JSX.Element => {
return (
<div
className={cx('aws-services-list-view-sidebar-content-item', {
active: service.id === activeService,
})}
key={service.id}
onClick={(): void => handleActiveService(service.id)}
>
<img
src={service.icon}
alt={service.title}
className="aws-services-list-view-sidebar-content-item-icon"
/>
<div className="aws-services-list-view-sidebar-content-item-title">
{service.title}
</div>
</div>
);
};
return (
<div className="aws-services-list-view">
<div className="aws-services-list-view-sidebar">
<div className="aws-services-list-view-sidebar-content">
<div className="aws-services-enabled">
<div className="aws-services-list-view-sidebar-content-header">
Enabled
</div>
{enabledServices.map((service) => renderServiceItem(service))}
{isEnabledServicesEmpty && (
<div className="aws-services-list-view-sidebar-content-item-empty-message">
No enabled services
</div>
)}
</div>
{!isNotEnabledServicesEmpty && (
<div className="aws-services-not-enabled">
<div className="aws-services-list-view-sidebar-content-header">
Not Enabled
</div>
{notEnabledServices.map((service) => renderServiceItem(service))}
</div>
)}
</div>
</div>
</div>
);
}
export default ServicesList;

View File

@@ -1,4 +1,8 @@
.services-tabs {
display: flex;
flex-direction: column;
height: calc(100% - 54px); /* 54px is the height of the header */
.ant-tabs-tab {
font-family: 'Inter';
padding: 16px 4px 14px;
@@ -18,21 +22,60 @@
background: var(--bg-robin-500);
}
}
.services-section {
display: flex;
gap: 10px;
flex: 1;
min-height: 0;
&__sidebar {
width: 16%;
padding: 0 16px;
width: 240px;
border-right: 1px solid var(--bg-slate-400);
height: 100%;
}
&__content {
width: 84%;
padding: 16px;
flex: 1;
height: 100%;
}
}
.service-details-loading,
.services-list-loading {
display: flex;
flex-direction: column;
gap: 10px;
width: 100%;
padding: 12px;
.service-details-loading-item {
width: 100%;
height: 100%;
background-color: var(--bg-slate-400);
}
}
.services-list-empty-message {
display: flex;
align-items: center;
gap: 8px;
padding: 12px;
color: var(--bg-vanilla-400);
font-size: 14px;
font-weight: 400;
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
.empty-state-svg {
height: 20px;
width: 20px;
}
}
.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;
@@ -46,6 +89,101 @@
}
}
.aws-services-list-view {
height: 100%;
.aws-services-list-view-sidebar {
width: 240px;
height: 100%;
border-right: 1px solid var(--l3-background);
padding: 12px;
.aws-services-list-view-sidebar-content {
display: flex;
flex-direction: column;
gap: 8px;
.aws-services-enabled {
display: flex;
flex-direction: column;
gap: 8px;
margin-bottom: 12px;
}
.aws-services-not-enabled {
display: flex;
flex-direction: column;
gap: 8px;
}
.aws-services-list-view-sidebar-content-header {
color: var(--l2-foreground);
font-family: Inter;
font-size: 11px;
font-style: normal;
font-weight: 600;
line-height: 18px; /* 163.636% */
letter-spacing: 0.44px;
text-transform: uppercase;
}
.aws-services-list-view-sidebar-content-item {
display: flex;
flex-direction: row;
align-items: center;
gap: 10px;
padding: 4px 12px;
border-radius: 4px;
cursor: pointer;
.aws-services-list-view-sidebar-content-item-icon {
width: 20px;
height: 20px;
}
.aws-services-list-view-sidebar-content-item-title {
color: var(--l1-foreground);
font-family: Inter;
font-size: 13px;
font-style: normal;
font-weight: 400;
line-height: 18px; /* 128.571% */
}
&:hover {
color: var(--l1-foreground);
font-family: Inter;
font-size: 13px;
font-style: normal;
font-weight: 400;
line-height: 18px; /* 128.571% */
}
&.active {
color: var(--l1-foreground);
font-family: Inter;
font-size: 13px;
font-style: normal;
font-weight: 400;
line-height: 18px; /* 128.571% */
background-color: var(--l3-background);
.aws-services-list-view-sidebar-content-item-title {
color: var(--l1-foreground);
}
}
}
}
}
.aws-services-list-view-main {
flex: 1;
padding: 12px;
}
}
.service-item {
display: flex;
gap: 12px;
@@ -63,17 +201,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 +230,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 +247,7 @@
letter-spacing: -0.07px;
text-align: left;
}
.service-details__right-actions {
display: flex;
align-items: center;
@@ -157,21 +300,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

@@ -0,0 +1,35 @@
import useUrlQuery from 'hooks/useUrlQuery';
import HeroSection from './HeroSection/HeroSection';
import ServiceDetails from './ServiceDetails/ServiceDetails';
import ServicesList from './ServicesList';
import './ServicesTabs.style.scss';
export enum ServiceFilterType {
ALL_SERVICES = 'all_services',
ENABLED = 'enabled',
AVAILABLE = 'available',
}
function ServicesTabs(): JSX.Element {
const urlQuery = useUrlQuery();
const cloudAccountId = urlQuery.get('cloudAccountId') || '';
return (
<div className="services-tabs">
<HeroSection />
<div className="services-section">
<div className="services-section__sidebar">
<ServicesList cloudAccountId={cloudAccountId} />
</div>
<div className="services-section__content">
<ServiceDetails />
</div>
</div>
</div>
);
}
export default ServicesTabs;

View File

@@ -0,0 +1,179 @@
import { fireEvent, screen, waitFor } from '@testing-library/react';
import { server } from 'mocks-server/server';
import { rest, RestRequest } from 'msw';
import { UpdateServiceConfigPayload } from '../types';
import {
accountsResponse,
buildServiceDetailsResponse,
CLOUD_ACCOUNT_ID,
initialBuckets,
} from './mockData';
import {
assertGenericModalElements,
assertS3SyncSpecificElements,
renderServiceDetails,
} from './utils';
// --- RESIZE OBSERVER (required by @radix-ui in Tabs/Switch) ---
class ResizeObserverMock {
observe(): void {}
unobserve(): void {}
disconnect(): void {}
}
global.ResizeObserver = (ResizeObserverMock as unknown) as typeof ResizeObserver;
// --- MOCKS ---
jest.mock('components/MarkdownRenderer/MarkdownRenderer', () => ({
MarkdownRenderer: (): JSX.Element => <div data-testid="markdown-renderer" />,
}));
jest.mock(
'container/Integrations/CloudIntegration/AmazonWebServices/ServiceDashboards/ServiceDashboards',
() => ({
__esModule: true,
default: (): JSX.Element => <div data-testid="service-dashboards" />,
}),
);
let testServiceId = 's3sync';
let testInitialBuckets: Record<string, string[]> = {};
const mockGet = jest.fn((param: string) => {
if (param === 'cloudAccountId') {
return CLOUD_ACCOUNT_ID;
}
if (param === 'service') {
return testServiceId;
}
return null;
});
jest.mock('hooks/useUrlQuery', () => ({
__esModule: true,
default: (): { get: (param: string) => string | null } => ({ get: mockGet }),
}));
// --- TEST SUITE ---
describe('ServiceDetails for S3 Sync service', () => {
jest.setTimeout(10000);
beforeEach(() => {
testServiceId = 's3sync';
testInitialBuckets = {};
server.use(
rest.get(
'http://localhost/api/v1/cloud-integrations/aws/accounts',
(_req, res, ctx) => res(ctx.json(accountsResponse)),
),
rest.get(
'http://localhost/api/v1/cloud-integrations/aws/services/:serviceId',
(req, res, ctx) =>
res(
ctx.json(
buildServiceDetailsResponse(
req.params.serviceId as string,
testInitialBuckets,
),
),
),
),
);
});
it('should render with logs collection switch and bucket selectors (no buckets initially selected)', async () => {
renderServiceDetails({}); // No initial S3 buckets, defaults to 's3sync' serviceId
await assertGenericModalElements();
await assertS3SyncSpecificElements({});
});
it('should render with logs collection switch and bucket selectors (some buckets initially selected)', async () => {
testInitialBuckets = initialBuckets;
renderServiceDetails(initialBuckets);
await assertGenericModalElements();
await assertS3SyncSpecificElements(initialBuckets);
});
it('should enable save button after adding a new bucket via combobox', async () => {
testInitialBuckets = initialBuckets;
renderServiceDetails(initialBuckets);
await assertGenericModalElements();
await assertS3SyncSpecificElements(initialBuckets);
const targetCombobox = screen.getAllByRole('combobox')[0];
const newBucketName = 'a-newly-added-bucket';
fireEvent.change(targetCombobox, { target: { value: newBucketName } });
fireEvent.keyDown(targetCombobox, {
key: 'Enter',
code: 'Enter',
keyCode: 13,
});
await waitFor(() => {
expect(screen.getByLabelText(newBucketName)).toBeInTheDocument();
expect(screen.getByRole('button', { name: /save/i })).toBeEnabled();
});
});
it('should send updated bucket configuration on save', async () => {
let capturedPayload: UpdateServiceConfigPayload | null = null;
const mockUpdateConfigUrl =
'http://localhost/api/v1/cloud-integrations/aws/services/s3sync/config';
// Override POST handler specifically for this test to capture payload
server.use(
rest.post(mockUpdateConfigUrl, async (req: RestRequest, res, ctx) => {
capturedPayload = await req.json();
return res(ctx.status(200), ctx.json({ message: 'Config updated' }));
}),
);
testInitialBuckets = initialBuckets;
renderServiceDetails(initialBuckets);
await assertGenericModalElements();
await assertS3SyncSpecificElements(initialBuckets);
const newBucketName = 'another-new-bucket';
const targetCombobox = screen.getAllByRole('combobox')[0];
fireEvent.change(targetCombobox, { target: { value: newBucketName } });
fireEvent.keyDown(targetCombobox, {
key: 'Enter',
code: 'Enter',
keyCode: 13,
});
await waitFor(() => {
expect(screen.getByLabelText(newBucketName)).toBeInTheDocument();
expect(screen.getByRole('button', { name: /save/i })).toBeEnabled();
});
fireEvent.click(screen.getByRole('button', { name: /save/i }));
await waitFor(() => {
expect(capturedPayload).not.toBeNull();
});
expect(capturedPayload).toEqual({
cloud_account_id: CLOUD_ACCOUNT_ID,
config: {
logs: {
enabled: true,
s3_buckets: {
'us-east-2': ['first-bucket', 'second-bucket'],
'ap-south-1': [newBucketName],
},
},
metrics: { enabled: false },
},
});
});
it('should not render S3 bucket region selector UI for services other than s3sync', async () => {
testServiceId = 'cloudwatch';
testInitialBuckets = {};
renderServiceDetails({}, 'cloudwatch');
await waitFor(() => {
expect(
screen.queryByText(/select s3 buckets by region/i),
).not.toBeInTheDocument();
});
});
});

View File

@@ -0,0 +1,54 @@
import { ServiceDetailsResponse } from '../types';
const CLOUD_ACCOUNT_ID = '123456789012';
const initialBuckets = { 'us-east-2': ['first-bucket', 'second-bucket'] };
const accountsResponse = {
status: 'success',
data: {
accounts: [
{
id: 'a1b2c3d4-e5f6-7890-1234-567890abcdef',
cloud_account_id: CLOUD_ACCOUNT_ID,
config: {
regions: ['ap-south-1', 'ap-south-2', 'us-east-1', 'us-east-2'],
},
status: {
integration: {
last_heartbeat_ts_ms: 1747114366214,
},
},
},
],
},
};
/** Response shape for GET /cloud-integrations/aws/services/:serviceId (used by ServiceDetails). */
const buildServiceDetailsResponse = (
serviceId: string,
initialConfigLogsS3Buckets: Record<string, string[]> = {},
): ServiceDetailsResponse => ({
status: 'success',
data: {
id: serviceId,
title: serviceId === 's3sync' ? 'S3 Sync' : serviceId,
icon: '',
overview: '',
supported_signals: { logs: serviceId === 's3sync', metrics: false },
assets: { dashboards: [] },
data_collected: { logs: [], metrics: [] },
config: {
logs: { enabled: true, s3_buckets: initialConfigLogsS3Buckets },
metrics: { enabled: false },
},
status: { logs: null, metrics: null },
},
});
export {
accountsResponse,
buildServiceDetailsResponse,
CLOUD_ACCOUNT_ID,
initialBuckets,
};

View File

@@ -0,0 +1,56 @@
import { render, RenderResult, screen, waitFor } from '@testing-library/react';
import MockQueryClientProvider from 'providers/test/MockQueryClientProvider';
import ServiceDetails from '../ServiceDetails/ServiceDetails';
import { accountsResponse } from './mockData';
/**
* Renders ServiceDetails (inline config form). Tests must register MSW handlers
* for GET accounts and GET service details, and mock useUrlQuery (cloudAccountId, service).
*/
const renderServiceDetails = (
_initialConfigLogsS3Buckets: Record<string, string[]> = {},
_serviceId = 's3sync',
): RenderResult =>
render(
<MockQueryClientProvider>
<ServiceDetails />
</MockQueryClientProvider>,
);
/**
* Asserts generic UI elements of the ServiceDetails config form (Overview tab).
*/
const assertGenericModalElements = async (): Promise<void> => {
await waitFor(() => {
expect(screen.getByRole('switch')).toBeInTheDocument();
expect(screen.getByText(/log collection/i)).toBeInTheDocument();
});
};
/**
* Asserts S3 bucket selector section: title, region labels, and one combobox per region.
* Does not assert placeholder text (antd Select may not expose it as placeholder attribute).
*/
const assertS3SyncSpecificElements = async (
_expectedBucketsByRegion: Record<string, string[]> = {},
): Promise<void> => {
const regions = accountsResponse.data.accounts[0]?.config?.regions || [];
await waitFor(() => {
expect(screen.getByText(/select s3 buckets by region/i)).toBeInTheDocument();
regions.forEach((region) => {
expect(screen.getByText(region)).toBeInTheDocument();
});
const comboboxes = screen.getAllByRole('combobox');
expect(comboboxes.length).toBeGreaterThanOrEqual(regions.length);
});
};
export {
assertGenericModalElements,
assertS3SyncSpecificElements,
renderServiceDetails,
};

View File

@@ -0,0 +1,25 @@
import { CloudintegrationtypesAccountDTO } from 'api/generated/services/sigNoz.schemas';
import { CloudAccount } from './types';
export function mapAccountDtoToAwsCloudAccount(
account: CloudintegrationtypesAccountDTO,
): CloudAccount | null {
if (!account.providerAccountId) {
return null;
}
return {
id: account.id,
cloud_account_id: account.id,
config: {
regions: account.config.aws.regions,
},
status: {
integration: {
last_heartbeat_ts_ms: account.agentReport?.timestampMillis ?? 0,
},
},
providerAccountId: account.providerAccountId,
};
}

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,8 +45,9 @@ interface AccountStatus {
interface CloudAccount {
id: string;
cloud_account_id: string;
config: CloudAccountConfig;
config: AWSCloudAccountConfig;
status: AccountStatus;
providerAccountId: string;
}
interface CloudAccountsData {
@@ -133,15 +84,13 @@ interface UpdateServiceConfigResponse {
}
export type {
AWSServiceConfig,
CloudAccount,
CloudAccountsData,
IServiceStatus,
S3BucketsByRegion,
Service,
ServiceConfig,
ServiceData,
ServiceDetailsResponse,
SupportedSignals,
UpdateServiceConfigPayload,
UpdateServiceConfigResponse,
};

View File

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

View File

@@ -0,0 +1,18 @@
import { IntegrationType } from 'container/Integrations/types';
import AWSTabs from './AmazonWebServices/ServicesTabs';
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} />
{type === IntegrationType.AWS_SERVICES && <AWSTabs />}
</div>
);
};
export default CloudIntegration;

View File

@@ -0,0 +1,42 @@
.config-connection-status-popover {
.ant-popover-inner {
padding: 0;
background-color: var(--l2-background);
border-radius: 4px;
border: 1px solid var(--l3-background);
padding: 8px;
width: 240px;
.ant-popover-content {
padding: 0;
}
}
.config-connection-status-item {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
gap: 8px;
padding: 4px 8px;
border-radius: 4px;
}
.config-connection-status-icon {
display: flex;
flex-direction: row;
align-items: center;
gap: 8px;
}
.config-connection-status-category-display-name {
color: var(--l2-foreground);
font-family: Inter;
font-size: 12px;
font-style: normal;
font-weight: 400;
line-height: 18px; /* 138.462% */
}
}

View File

@@ -0,0 +1,30 @@
import { Color } from '@signozhq/design-tokens';
import { IConfigConnectionStatus } from 'container/Integrations/types';
import { CheckCircle, TriangleAlert } from 'lucide-react';
import './ConfigConnectionStatus.styles.scss';
export function ConfigConnectionStatus({
status,
}: {
status: IConfigConnectionStatus[] | null;
}): JSX.Element {
return (
<div className="config-connection-status-container">
{status?.map((status) => (
<div key={status.category} className="config-connection-status-item">
<div className="config-connection-status-icon">
{status.last_received_ts_ms && status.last_received_ts_ms > 0 ? (
<CheckCircle size={16} color={Color.BG_FOREST_500} />
) : (
<TriangleAlert size={16} color={Color.BG_AMBER_500} />
)}
</div>
<div className="config-connection-status-category-display-name">
{status.category_display_name}
</div>
</div>
))}
</div>
);
}

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 { Breadcrumb } from 'antd';
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,6 +1,6 @@
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';
@@ -9,10 +9,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';
@@ -26,6 +26,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(
@@ -38,6 +39,7 @@ function IntegrationDetailHeader(
description,
connectionState,
connectionData,
isLoading,
onUnInstallSuccess,
setActiveDetailTab,
} = props;
@@ -114,16 +116,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
@@ -132,7 +147,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,11 +1,18 @@
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';
@@ -15,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,
@@ -38,7 +38,7 @@ function IntegrationDetailPage(props: IntegrationDetailPageProps): JSX.Element {
isRefetching,
isError,
} = useGetIntegration({
integrationId: selectedIntegration,
integrationId: integrationId || '',
});
const { isCloudUser: isCloudUserVal } = useGetTenantLicense();
@@ -47,7 +47,7 @@ function IntegrationDetailPage(props: IntegrationDetailPageProps): JSX.Element {
data: integrationStatus,
isLoading: isStatusLoading,
} = useGetIntegrationStatus({
integrationId: selectedIntegration,
integrationId: integrationId || '',
});
const loading = isLoading || isFetching || isRefetching || isStatusLoading;
@@ -61,27 +61,27 @@ function IntegrationDetailPage(props: IntegrationDetailPageProps): JSX.Element {
),
);
if (integrationId === INTEGRATION_TYPES.AWS) {
return <CloudIntegration type={IntegrationType.AWS_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
@@ -94,10 +94,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>
@@ -112,39 +112,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,81 @@
.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;
}
}
.request-integration-dialog {
.request-integration-form {
display: flex;
flex-direction: column;
gap: 16px;
}
.request-integration-form-title {
color: var(--l1-foreground);
font-family: Inter;
font-size: 13px;
font-style: normal;
font-weight: 400;
line-height: 20px; /* 142.857% */
}
.request-integration-form-footer {
margin-top: 16px;
display: flex;
justify-content: flex-end;
gap: 8px;
}
}
.lightMode {
.integrations-header {
.title {
color: var(--bg-ink-500);
}
.subtitle {
color: var(--bg-slate-200);
}
}
}

View File

@@ -0,0 +1,165 @@
import { useState } from 'react';
import { useHistory } from 'react-router-dom';
import { Button } from '@signozhq/button';
import { DialogWrapper } from '@signozhq/dialog';
import { Input } from '@signozhq/input';
import { toast } from '@signozhq/sonner';
import { Flex, Typography } from 'antd';
import logEvent from 'api/common/logEvent';
import ROUTES from 'constants/routes';
import { ArrowRight, Cable, Check } 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 history = useHistory();
const { user } = useAppContext();
const { searchQuery, onSearchChange } = props;
const [
isRequestIntegrationDialogOpen,
setIsRequestIntegrationDialogOpen,
] = useState(false);
const [
isSubmittingRequestForIntegration,
setIsSubmittingRequestForIntegration,
] = useState(false);
const [requestedIntegrationName, setRequestedIntegrationName] = useState('');
const isGetStartedWithCloudAllowed = routePermission.GET_STARTED_WITH_CLOUD.includes(
user.role,
);
const handleRequestIntegrationSubmit = async (): Promise<void> => {
try {
setIsSubmittingRequestForIntegration(true);
const eventName = 'Integration requested';
const screenName = 'Integration list page';
const response = await logEvent(eventName, {
screen: screenName,
integration: requestedIntegrationName,
});
if (response.statusCode === 200) {
toast.success('Integration Request Submitted', {
position: 'top-right',
});
setRequestedIntegrationName('');
setIsRequestIntegrationDialogOpen(false);
setIsSubmittingRequestForIntegration(false);
} else {
toast.error(response.error || 'Something went wrong', {
position: 'top-right',
});
setIsSubmittingRequestForIntegration(false);
}
} catch (error) {
toast.error('Something went wrong', {
position: 'top-right',
});
setIsSubmittingRequestForIntegration(false);
}
};
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"
onClick={(): void => setIsRequestIntegrationDialogOpen(true)}
>
Request Integration
</Button>
<DialogWrapper
className="request-integration-dialog"
title="Request New Integration"
open={isRequestIntegrationDialogOpen}
onOpenChange={setIsRequestIntegrationDialogOpen}
>
<div className="request-integration-form">
<div className="request-integration-form-title">
Which integration are you looking for?
</div>
<Input
placeholder="Enter integration name..."
value={requestedIntegrationName}
onChange={(e): void => {
setRequestedIntegrationName(e.target.value);
}}
onKeyDown={(e): void => {
if (e.key === 'Enter' && requestedIntegrationName?.trim().length > 0) {
handleRequestIntegrationSubmit();
}
}}
disabled={isSubmittingRequestForIntegration}
/>
</div>
<div className="request-integration-form-footer">
<Button
variant="solid"
color="primary"
size="sm"
prefixIcon={<Check size={14} />}
onClick={handleRequestIntegrationSubmit}
loading={isSubmittingRequestForIntegration}
disabled={
isSubmittingRequestForIntegration ||
!requestedIntegrationName ||
requestedIntegrationName?.trim().length === 0
}
>
Submit
</Button>
</div>
</DialogWrapper>
{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,235 @@
.integrations-list-container {
display: flex;
flex-direction: column;
gap: 16px;
.integrations-list-title {
color: var(--l2-foreground);
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(--l3-background);
background: var(--l1-background);
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(--l1-foreground);
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(--l2-foreground);
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: var(--l2-background);
}
}
.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 {
background: var(--l1-background);
}
.integrations-list-item {
.list-item-image-container {
border: 1.111px solid var(--l3-background);
background: var(--l1-background);
}
.list-item-details {
.heading {
color: var(--l1-foreground);
}
.description {
color: var(--l2-foreground);
}
}
.configure-btn {
border: 1px solid var(--l3-background);
background: var(--l1-background);
color: var(--l1-foreground);
}
}
}

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,77 @@
.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(--l1-foreground);
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(--l3-background);
background: var(--l1-background);
cursor: pointer;
transition: background 0.2s ease-in-out;
.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(--l1-foreground);
font-family: Inter;
font-size: 13px;
font-style: normal;
font-weight: 500;
line-height: 32px; /* 200% */
letter-spacing: -0.08px;
}
}
}
&:hover {
background: var(--l3-background);
}
}
}
}

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 = [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' },
];

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