Compare commits

..

30 Commits

Author SHA1 Message Date
Yunus M
534ceac3d7 Merge branch 'main' into feat/azure-integration-ui 2026-02-25 15:07:56 +05:30
Aditya Singh
016da679b9 Migrate QueryBuilderSearch in Logs Pipelines to v2 (#10387)
Some checks are pending
build-staging / prepare (push) Waiting to run
build-staging / js-build (push) Blocked by required conditions
build-staging / go-build (push) Blocked by required conditions
build-staging / staging (push) Blocked by required conditions
Release Drafter / update_release_draft (push) Waiting to run
* feat: function idon func added

* feat: test update

* fix: minor refactor

* feat: migrate to querybuilder search v2

* feat: fix failing test

* feat: minor change
2026-02-25 09:31:56 +00:00
Ashwin Bhatkal
ff028e366b fix: first query variable without 'ALL' selection (#10417)
* fix: first query variable without all

* chore: resolve self comments

* chore: added tests for getOptions logic
2026-02-25 08:36:39 +00:00
Ishan
c579614d56 feat: color fallback and red checks (#10389)
* feat: color fallback and red checks

* feat: testcase added
2026-02-25 11:54:22 +05:30
Ishan
78ba2ba356 feat: text selection block (#10373)
* feat: text selection block

* chore: added test file
2026-02-25 11:38:15 +05:30
Ishan
7fd4762e2a feat: ui bugs body width and table css (#10377)
* feat: ui bugs body width and table css

* feat: defualt open search overview

* feat: added timerRef to cleanup
2026-02-25 11:25:54 +05:30
Nageshbansal
4e4c9ce5af chore: enable metadataexporter in docker (#10409)
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
2026-02-25 03:13:27 +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
185 changed files with 6574 additions and 1515 deletions

View File

@@ -82,6 +82,12 @@ exporters:
timeout: 45s
sending_queue:
enabled: false
metadataexporter:
cache:
provider: in_memory
dsn: tcp://clickhouse:9000/signoz_metadata
enabled: true
timeout: 45s
service:
telemetry:
logs:
@@ -93,19 +99,19 @@ service:
traces:
receivers: [otlp]
processors: [signozspanmetrics/delta, batch]
exporters: [clickhousetraces, signozmeter]
exporters: [clickhousetraces, metadataexporter, signozmeter]
metrics:
receivers: [otlp]
processors: [batch]
exporters: [signozclickhousemetrics, signozmeter]
exporters: [signozclickhousemetrics, metadataexporter, signozmeter]
metrics/prometheus:
receivers: [prometheus]
processors: [batch]
exporters: [signozclickhousemetrics, signozmeter]
exporters: [signozclickhousemetrics, metadataexporter, signozmeter]
logs:
receivers: [otlp]
processors: [batch]
exporters: [clickhouselogsexporter, signozmeter]
exporters: [clickhouselogsexporter, metadataexporter, signozmeter]
metrics/meter:
receivers: [signozmeter]
processors: [batch/meter]

View File

@@ -82,6 +82,12 @@ exporters:
timeout: 45s
sending_queue:
enabled: false
metadataexporter:
cache:
provider: in_memory
dsn: tcp://clickhouse:9000/signoz_metadata
enabled: true
timeout: 45s
service:
telemetry:
logs:
@@ -93,19 +99,19 @@ service:
traces:
receivers: [otlp]
processors: [signozspanmetrics/delta, batch]
exporters: [clickhousetraces, signozmeter]
exporters: [clickhousetraces, metadataexporter, signozmeter]
metrics:
receivers: [otlp]
processors: [batch]
exporters: [signozclickhousemetrics, signozmeter]
exporters: [signozclickhousemetrics, metadataexporter, signozmeter]
metrics/prometheus:
receivers: [prometheus]
processors: [batch]
exporters: [signozclickhousemetrics, signozmeter]
exporters: [signozclickhousemetrics, metadataexporter, signozmeter]
logs:
receivers: [otlp]
processors: [batch]
exporters: [clickhouselogsexporter, signozmeter]
exporters: [clickhouselogsexporter, metadataexporter, signozmeter]
metrics/meter:
receivers: [signozmeter]
processors: [batch/meter]

View File

@@ -26,7 +26,7 @@ import (
"github.com/SigNoz/signoz/pkg/query-service/utils/times"
"github.com/SigNoz/signoz/pkg/query-service/utils/timestamp"
"github.com/SigNoz/signoz/pkg/formatter"
"github.com/SigNoz/signoz/pkg/query-service/formatter"
baserules "github.com/SigNoz/signoz/pkg/query-service/rules"

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

@@ -52,6 +52,8 @@
"@signozhq/combobox": "0.0.2",
"@signozhq/command": "0.0.0",
"@signozhq/design-tokens": "2.1.1",
"@signozhq/dialog": "0.0.2",
"@signozhq/drawer": "0.0.4",
"@signozhq/icons": "0.1.0",
"@signozhq/input": "0.0.2",
"@signozhq/popover": "0.0.0",
@@ -60,10 +62,12 @@
"@signozhq/sonner": "0.1.0",
"@signozhq/switch": "0.0.2",
"@signozhq/table": "0.3.7",
"@signozhq/tabs": "0.0.11",
"@signozhq/tooltip": "0.0.2",
"@tanstack/react-table": "8.20.6",
"@tanstack/react-virtual": "3.11.2",
"@uiw/codemirror-theme-copilot": "4.23.11",
"@uiw/codemirror-theme-dracula": "4.25.4",
"@uiw/codemirror-theme-github": "4.24.1",
"@uiw/react-codemirror": "4.23.10",
"@uiw/react-md-editor": "3.23.5",

View File

@@ -0,0 +1,312 @@
<svg width="929" height="8" viewBox="0 0 929 8" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="2" height="2" rx="1" fill="#242834"/>
<rect x="6" width="2" height="2" rx="1" fill="#242834"/>
<rect x="12" width="2" height="2" rx="1" fill="#242834"/>
<rect x="18" width="2" height="2" rx="1" fill="#242834"/>
<rect x="24" width="2" height="2" rx="1" fill="#242834"/>
<rect x="30" width="2" height="2" rx="1" fill="#242834"/>
<rect x="36" width="2" height="2" rx="1" fill="#242834"/>
<rect x="42" width="2" height="2" rx="1" fill="#242834"/>
<rect x="48" width="2" height="2" rx="1" fill="#242834"/>
<rect x="54" width="2" height="2" rx="1" fill="#242834"/>
<rect x="60" width="2" height="2" rx="1" fill="#242834"/>
<rect x="66" width="2" height="2" rx="1" fill="#242834"/>
<rect x="72" width="2" height="2" rx="1" fill="#242834"/>
<rect x="78" width="2" height="2" rx="1" fill="#242834"/>
<rect x="84" width="2" height="2" rx="1" fill="#242834"/>
<rect x="90" width="2" height="2" rx="1" fill="#242834"/>
<rect x="96" width="2" height="2" rx="1" fill="#242834"/>
<rect x="102" width="2" height="2" rx="1" fill="#242834"/>
<rect x="108" width="2" height="2" rx="1" fill="#242834"/>
<rect x="114" width="2" height="2" rx="1" fill="#242834"/>
<rect x="120" width="2" height="2" rx="1" fill="#242834"/>
<rect x="126" width="2" height="2" rx="1" fill="#242834"/>
<rect x="132" width="2" height="2" rx="1" fill="#242834"/>
<rect x="138" width="2" height="2" rx="1" fill="#242834"/>
<rect x="144" width="2" height="2" rx="1" fill="#242834"/>
<rect x="150" width="2" height="2" rx="1" fill="#242834"/>
<rect x="156" width="2" height="2" rx="1" fill="#242834"/>
<rect x="162" width="2" height="2" rx="1" fill="#242834"/>
<rect x="168" width="2" height="2" rx="1" fill="#242834"/>
<rect x="174" width="2" height="2" rx="1" fill="#242834"/>
<rect x="180" width="2" height="2" rx="1" fill="#242834"/>
<rect x="186" width="2" height="2" rx="1" fill="#242834"/>
<rect x="192" width="2" height="2" rx="1" fill="#242834"/>
<rect x="198" width="2" height="2" rx="1" fill="#242834"/>
<rect x="204" width="2" height="2" rx="1" fill="#242834"/>
<rect x="210" width="2" height="2" rx="1" fill="#242834"/>
<rect x="216" width="2" height="2" rx="1" fill="#242834"/>
<rect x="222" width="2" height="2" rx="1" fill="#242834"/>
<rect x="228" width="2" height="2" rx="1" fill="#242834"/>
<rect x="234" width="2" height="2" rx="1" fill="#242834"/>
<rect x="240" width="2" height="2" rx="1" fill="#242834"/>
<rect x="246" width="2" height="2" rx="1" fill="#242834"/>
<rect x="252" width="2" height="2" rx="1" fill="#242834"/>
<rect x="258" width="2" height="2" rx="1" fill="#242834"/>
<rect x="264" width="2" height="2" rx="1" fill="#242834"/>
<rect x="270" width="2" height="2" rx="1" fill="#242834"/>
<rect x="276" width="2" height="2" rx="1" fill="#242834"/>
<rect x="282" width="2" height="2" rx="1" fill="#242834"/>
<rect x="288" width="2" height="2" rx="1" fill="#242834"/>
<rect x="294" width="2" height="2" rx="1" fill="#242834"/>
<rect x="300" width="2" height="2" rx="1" fill="#242834"/>
<rect x="306" width="2" height="2" rx="1" fill="#242834"/>
<rect x="312" width="2" height="2" rx="1" fill="#242834"/>
<rect x="318" width="2" height="2" rx="1" fill="#242834"/>
<rect x="324" width="2" height="2" rx="1" fill="#242834"/>
<rect x="330" width="2" height="2" rx="1" fill="#242834"/>
<rect x="336" width="2" height="2" rx="1" fill="#242834"/>
<rect x="342" width="2" height="2" rx="1" fill="#242834"/>
<rect x="348" width="2" height="2" rx="1" fill="#242834"/>
<rect x="354" width="2" height="2" rx="1" fill="#242834"/>
<rect x="360" width="2" height="2" rx="1" fill="#242834"/>
<rect x="366" width="2" height="2" rx="1" fill="#242834"/>
<rect x="372" width="2" height="2" rx="1" fill="#242834"/>
<rect x="378" width="2" height="2" rx="1" fill="#242834"/>
<rect x="384" width="2" height="2" rx="1" fill="#242834"/>
<rect x="390" width="2" height="2" rx="1" fill="#242834"/>
<rect x="396" width="2" height="2" rx="1" fill="#242834"/>
<rect x="402" width="2" height="2" rx="1" fill="#242834"/>
<rect x="408" width="2" height="2" rx="1" fill="#242834"/>
<rect x="414" width="2" height="2" rx="1" fill="#242834"/>
<rect x="420" width="2" height="2" rx="1" fill="#242834"/>
<rect x="426" width="2" height="2" rx="1" fill="#242834"/>
<rect x="432" width="2" height="2" rx="1" fill="#242834"/>
<rect x="438" width="2" height="2" rx="1" fill="#242834"/>
<rect x="444" width="2" height="2" rx="1" fill="#242834"/>
<rect x="450" width="2" height="2" rx="1" fill="#242834"/>
<rect x="456" width="2" height="2" rx="1" fill="#242834"/>
<rect x="462" width="2" height="2" rx="1" fill="#242834"/>
<rect x="468" width="2" height="2" rx="1" fill="#242834"/>
<rect x="474" width="2" height="2" rx="1" fill="#242834"/>
<rect x="480" width="2" height="2" rx="1" fill="#242834"/>
<rect x="486" width="2" height="2" rx="1" fill="#242834"/>
<rect x="492" width="2" height="2" rx="1" fill="#242834"/>
<rect x="498" width="2" height="2" rx="1" fill="#242834"/>
<rect x="504" width="2" height="2" rx="1" fill="#242834"/>
<rect x="510" width="2" height="2" rx="1" fill="#242834"/>
<rect x="516" width="2" height="2" rx="1" fill="#242834"/>
<rect x="522" width="2" height="2" rx="1" fill="#242834"/>
<rect x="528" width="2" height="2" rx="1" fill="#242834"/>
<rect x="534" width="2" height="2" rx="1" fill="#242834"/>
<rect x="540" width="2" height="2" rx="1" fill="#242834"/>
<rect x="546" width="2" height="2" rx="1" fill="#242834"/>
<rect x="552" width="2" height="2" rx="1" fill="#242834"/>
<rect x="558" width="2" height="2" rx="1" fill="#242834"/>
<rect x="564" width="2" height="2" rx="1" fill="#242834"/>
<rect x="570" width="2" height="2" rx="1" fill="#242834"/>
<rect x="576" width="2" height="2" rx="1" fill="#242834"/>
<rect x="582" width="2" height="2" rx="1" fill="#242834"/>
<rect x="588" width="2" height="2" rx="1" fill="#242834"/>
<rect x="594" width="2" height="2" rx="1" fill="#242834"/>
<rect x="600" width="2" height="2" rx="1" fill="#242834"/>
<rect x="606" width="2" height="2" rx="1" fill="#242834"/>
<rect x="612" width="2" height="2" rx="1" fill="#242834"/>
<rect x="618" width="2" height="2" rx="1" fill="#242834"/>
<rect x="624" width="2" height="2" rx="1" fill="#242834"/>
<rect x="630" width="2" height="2" rx="1" fill="#242834"/>
<rect x="636" width="2" height="2" rx="1" fill="#242834"/>
<rect x="642" width="2" height="2" rx="1" fill="#242834"/>
<rect x="648" width="2" height="2" rx="1" fill="#242834"/>
<rect x="654" width="2" height="2" rx="1" fill="#242834"/>
<rect x="660" width="2" height="2" rx="1" fill="#242834"/>
<rect x="666" width="2" height="2" rx="1" fill="#242834"/>
<rect x="672" width="2" height="2" rx="1" fill="#242834"/>
<rect x="678" width="2" height="2" rx="1" fill="#242834"/>
<rect x="684" width="2" height="2" rx="1" fill="#242834"/>
<rect x="690" width="2" height="2" rx="1" fill="#242834"/>
<rect x="696" width="2" height="2" rx="1" fill="#242834"/>
<rect x="702" width="2" height="2" rx="1" fill="#242834"/>
<rect x="708" width="2" height="2" rx="1" fill="#242834"/>
<rect x="714" width="2" height="2" rx="1" fill="#242834"/>
<rect x="720" width="2" height="2" rx="1" fill="#242834"/>
<rect x="726" width="2" height="2" rx="1" fill="#242834"/>
<rect x="732" width="2" height="2" rx="1" fill="#242834"/>
<rect x="738" width="2" height="2" rx="1" fill="#242834"/>
<rect x="744" width="2" height="2" rx="1" fill="#242834"/>
<rect x="750" width="2" height="2" rx="1" fill="#242834"/>
<rect x="756" width="2" height="2" rx="1" fill="#242834"/>
<rect x="762" width="2" height="2" rx="1" fill="#242834"/>
<rect x="768" width="2" height="2" rx="1" fill="#242834"/>
<rect x="774" width="2" height="2" rx="1" fill="#242834"/>
<rect x="780" width="2" height="2" rx="1" fill="#242834"/>
<rect x="786" width="2" height="2" rx="1" fill="#242834"/>
<rect x="792" width="2" height="2" rx="1" fill="#242834"/>
<rect x="798" width="2" height="2" rx="1" fill="#242834"/>
<rect x="804" width="2" height="2" rx="1" fill="#242834"/>
<rect x="810" width="2" height="2" rx="1" fill="#242834"/>
<rect x="816" width="2" height="2" rx="1" fill="#242834"/>
<rect x="822" width="2" height="2" rx="1" fill="#242834"/>
<rect x="828" width="2" height="2" rx="1" fill="#242834"/>
<rect x="834" width="2" height="2" rx="1" fill="#242834"/>
<rect x="840" width="2" height="2" rx="1" fill="#242834"/>
<rect x="846" width="2" height="2" rx="1" fill="#242834"/>
<rect x="852" width="2" height="2" rx="1" fill="#242834"/>
<rect x="858" width="2" height="2" rx="1" fill="#242834"/>
<rect x="864" width="2" height="2" rx="1" fill="#242834"/>
<rect x="870" width="2" height="2" rx="1" fill="#242834"/>
<rect x="876" width="2" height="2" rx="1" fill="#242834"/>
<rect x="882" width="2" height="2" rx="1" fill="#242834"/>
<rect x="888" width="2" height="2" rx="1" fill="#242834"/>
<rect x="894" width="2" height="2" rx="1" fill="#242834"/>
<rect x="900" width="2" height="2" rx="1" fill="#242834"/>
<rect x="906" width="2" height="2" rx="1" fill="#242834"/>
<rect x="912" width="2" height="2" rx="1" fill="#242834"/>
<rect x="918" width="2" height="2" rx="1" fill="#242834"/>
<rect x="924" width="2" height="2" rx="1" fill="#242834"/>
<rect y="6" width="2" height="2" rx="1" fill="#242834"/>
<rect x="6" y="6" width="2" height="2" rx="1" fill="#242834"/>
<rect x="12" y="6" width="2" height="2" rx="1" fill="#242834"/>
<rect x="18" y="6" width="2" height="2" rx="1" fill="#242834"/>
<rect x="24" y="6" width="2" height="2" rx="1" fill="#242834"/>
<rect x="30" y="6" width="2" height="2" rx="1" fill="#242834"/>
<rect x="36" y="6" width="2" height="2" rx="1" fill="#242834"/>
<rect x="42" y="6" width="2" height="2" rx="1" fill="#242834"/>
<rect x="48" y="6" width="2" height="2" rx="1" fill="#242834"/>
<rect x="54" y="6" width="2" height="2" rx="1" fill="#242834"/>
<rect x="60" y="6" width="2" height="2" rx="1" fill="#242834"/>
<rect x="66" y="6" width="2" height="2" rx="1" fill="#242834"/>
<rect x="72" y="6" width="2" height="2" rx="1" fill="#242834"/>
<rect x="78" y="6" width="2" height="2" rx="1" fill="#242834"/>
<rect x="84" y="6" width="2" height="2" rx="1" fill="#242834"/>
<rect x="90" y="6" width="2" height="2" rx="1" fill="#242834"/>
<rect x="96" y="6" width="2" height="2" rx="1" fill="#242834"/>
<rect x="102" y="6" width="2" height="2" rx="1" fill="#242834"/>
<rect x="108" y="6" width="2" height="2" rx="1" fill="#242834"/>
<rect x="114" y="6" width="2" height="2" rx="1" fill="#242834"/>
<rect x="120" y="6" width="2" height="2" rx="1" fill="#242834"/>
<rect x="126" y="6" width="2" height="2" rx="1" fill="#242834"/>
<rect x="132" y="6" width="2" height="2" rx="1" fill="#242834"/>
<rect x="138" y="6" width="2" height="2" rx="1" fill="#242834"/>
<rect x="144" y="6" width="2" height="2" rx="1" fill="#242834"/>
<rect x="150" y="6" width="2" height="2" rx="1" fill="#242834"/>
<rect x="156" y="6" width="2" height="2" rx="1" fill="#242834"/>
<rect x="162" y="6" width="2" height="2" rx="1" fill="#242834"/>
<rect x="168" y="6" width="2" height="2" rx="1" fill="#242834"/>
<rect x="174" y="6" width="2" height="2" rx="1" fill="#242834"/>
<rect x="180" y="6" width="2" height="2" rx="1" fill="#242834"/>
<rect x="186" y="6" width="2" height="2" rx="1" fill="#242834"/>
<rect x="192" y="6" width="2" height="2" rx="1" fill="#242834"/>
<rect x="198" y="6" width="2" height="2" rx="1" fill="#242834"/>
<rect x="204" y="6" width="2" height="2" rx="1" fill="#242834"/>
<rect x="210" y="6" width="2" height="2" rx="1" fill="#242834"/>
<rect x="216" y="6" width="2" height="2" rx="1" fill="#242834"/>
<rect x="222" y="6" width="2" height="2" rx="1" fill="#242834"/>
<rect x="228" y="6" width="2" height="2" rx="1" fill="#242834"/>
<rect x="234" y="6" width="2" height="2" rx="1" fill="#242834"/>
<rect x="240" y="6" width="2" height="2" rx="1" fill="#242834"/>
<rect x="246" y="6" width="2" height="2" rx="1" fill="#242834"/>
<rect x="252" y="6" width="2" height="2" rx="1" fill="#242834"/>
<rect x="258" y="6" width="2" height="2" rx="1" fill="#242834"/>
<rect x="264" y="6" width="2" height="2" rx="1" fill="#242834"/>
<rect x="270" y="6" width="2" height="2" rx="1" fill="#242834"/>
<rect x="276" y="6" width="2" height="2" rx="1" fill="#242834"/>
<rect x="282" y="6" width="2" height="2" rx="1" fill="#242834"/>
<rect x="288" y="6" width="2" height="2" rx="1" fill="#242834"/>
<rect x="294" y="6" width="2" height="2" rx="1" fill="#242834"/>
<rect x="300" y="6" width="2" height="2" rx="1" fill="#242834"/>
<rect x="306" y="6" width="2" height="2" rx="1" fill="#242834"/>
<rect x="312" y="6" width="2" height="2" rx="1" fill="#242834"/>
<rect x="318" y="6" width="2" height="2" rx="1" fill="#242834"/>
<rect x="324" y="6" width="2" height="2" rx="1" fill="#242834"/>
<rect x="330" y="6" width="2" height="2" rx="1" fill="#242834"/>
<rect x="336" y="6" width="2" height="2" rx="1" fill="#242834"/>
<rect x="342" y="6" width="2" height="2" rx="1" fill="#242834"/>
<rect x="348" y="6" width="2" height="2" rx="1" fill="#242834"/>
<rect x="354" y="6" width="2" height="2" rx="1" fill="#242834"/>
<rect x="360" y="6" width="2" height="2" rx="1" fill="#242834"/>
<rect x="366" y="6" width="2" height="2" rx="1" fill="#242834"/>
<rect x="372" y="6" width="2" height="2" rx="1" fill="#242834"/>
<rect x="378" y="6" width="2" height="2" rx="1" fill="#242834"/>
<rect x="384" y="6" width="2" height="2" rx="1" fill="#242834"/>
<rect x="390" y="6" width="2" height="2" rx="1" fill="#242834"/>
<rect x="396" y="6" width="2" height="2" rx="1" fill="#242834"/>
<rect x="402" y="6" width="2" height="2" rx="1" fill="#242834"/>
<rect x="408" y="6" width="2" height="2" rx="1" fill="#242834"/>
<rect x="414" y="6" width="2" height="2" rx="1" fill="#242834"/>
<rect x="420" y="6" width="2" height="2" rx="1" fill="#242834"/>
<rect x="426" y="6" width="2" height="2" rx="1" fill="#242834"/>
<rect x="432" y="6" width="2" height="2" rx="1" fill="#242834"/>
<rect x="438" y="6" width="2" height="2" rx="1" fill="#242834"/>
<rect x="444" y="6" width="2" height="2" rx="1" fill="#242834"/>
<rect x="450" y="6" width="2" height="2" rx="1" fill="#242834"/>
<rect x="456" y="6" width="2" height="2" rx="1" fill="#242834"/>
<rect x="462" y="6" width="2" height="2" rx="1" fill="#242834"/>
<rect x="468" y="6" width="2" height="2" rx="1" fill="#242834"/>
<rect x="474" y="6" width="2" height="2" rx="1" fill="#242834"/>
<rect x="480" y="6" width="2" height="2" rx="1" fill="#242834"/>
<rect x="486" y="6" width="2" height="2" rx="1" fill="#242834"/>
<rect x="492" y="6" width="2" height="2" rx="1" fill="#242834"/>
<rect x="498" y="6" width="2" height="2" rx="1" fill="#242834"/>
<rect x="504" y="6" width="2" height="2" rx="1" fill="#242834"/>
<rect x="510" y="6" width="2" height="2" rx="1" fill="#242834"/>
<rect x="516" y="6" width="2" height="2" rx="1" fill="#242834"/>
<rect x="522" y="6" width="2" height="2" rx="1" fill="#242834"/>
<rect x="528" y="6" width="2" height="2" rx="1" fill="#242834"/>
<rect x="534" y="6" width="2" height="2" rx="1" fill="#242834"/>
<rect x="540" y="6" width="2" height="2" rx="1" fill="#242834"/>
<rect x="546" y="6" width="2" height="2" rx="1" fill="#242834"/>
<rect x="552" y="6" width="2" height="2" rx="1" fill="#242834"/>
<rect x="558" y="6" width="2" height="2" rx="1" fill="#242834"/>
<rect x="564" y="6" width="2" height="2" rx="1" fill="#242834"/>
<rect x="570" y="6" width="2" height="2" rx="1" fill="#242834"/>
<rect x="576" y="6" width="2" height="2" rx="1" fill="#242834"/>
<rect x="582" y="6" width="2" height="2" rx="1" fill="#242834"/>
<rect x="588" y="6" width="2" height="2" rx="1" fill="#242834"/>
<rect x="594" y="6" width="2" height="2" rx="1" fill="#242834"/>
<rect x="600" y="6" width="2" height="2" rx="1" fill="#242834"/>
<rect x="606" y="6" width="2" height="2" rx="1" fill="#242834"/>
<rect x="612" y="6" width="2" height="2" rx="1" fill="#242834"/>
<rect x="618" y="6" width="2" height="2" rx="1" fill="#242834"/>
<rect x="624" y="6" width="2" height="2" rx="1" fill="#242834"/>
<rect x="630" y="6" width="2" height="2" rx="1" fill="#242834"/>
<rect x="636" y="6" width="2" height="2" rx="1" fill="#242834"/>
<rect x="642" y="6" width="2" height="2" rx="1" fill="#242834"/>
<rect x="648" y="6" width="2" height="2" rx="1" fill="#242834"/>
<rect x="654" y="6" width="2" height="2" rx="1" fill="#242834"/>
<rect x="660" y="6" width="2" height="2" rx="1" fill="#242834"/>
<rect x="666" y="6" width="2" height="2" rx="1" fill="#242834"/>
<rect x="672" y="6" width="2" height="2" rx="1" fill="#242834"/>
<rect x="678" y="6" width="2" height="2" rx="1" fill="#242834"/>
<rect x="684" y="6" width="2" height="2" rx="1" fill="#242834"/>
<rect x="690" y="6" width="2" height="2" rx="1" fill="#242834"/>
<rect x="696" y="6" width="2" height="2" rx="1" fill="#242834"/>
<rect x="702" y="6" width="2" height="2" rx="1" fill="#242834"/>
<rect x="708" y="6" width="2" height="2" rx="1" fill="#242834"/>
<rect x="714" y="6" width="2" height="2" rx="1" fill="#242834"/>
<rect x="720" y="6" width="2" height="2" rx="1" fill="#242834"/>
<rect x="726" y="6" width="2" height="2" rx="1" fill="#242834"/>
<rect x="732" y="6" width="2" height="2" rx="1" fill="#242834"/>
<rect x="738" y="6" width="2" height="2" rx="1" fill="#242834"/>
<rect x="744" y="6" width="2" height="2" rx="1" fill="#242834"/>
<rect x="750" y="6" width="2" height="2" rx="1" fill="#242834"/>
<rect x="756" y="6" width="2" height="2" rx="1" fill="#242834"/>
<rect x="762" y="6" width="2" height="2" rx="1" fill="#242834"/>
<rect x="768" y="6" width="2" height="2" rx="1" fill="#242834"/>
<rect x="774" y="6" width="2" height="2" rx="1" fill="#242834"/>
<rect x="780" y="6" width="2" height="2" rx="1" fill="#242834"/>
<rect x="786" y="6" width="2" height="2" rx="1" fill="#242834"/>
<rect x="792" y="6" width="2" height="2" rx="1" fill="#242834"/>
<rect x="798" y="6" width="2" height="2" rx="1" fill="#242834"/>
<rect x="804" y="6" width="2" height="2" rx="1" fill="#242834"/>
<rect x="810" y="6" width="2" height="2" rx="1" fill="#242834"/>
<rect x="816" y="6" width="2" height="2" rx="1" fill="#242834"/>
<rect x="822" y="6" width="2" height="2" rx="1" fill="#242834"/>
<rect x="828" y="6" width="2" height="2" rx="1" fill="#242834"/>
<rect x="834" y="6" width="2" height="2" rx="1" fill="#242834"/>
<rect x="840" y="6" width="2" height="2" rx="1" fill="#242834"/>
<rect x="846" y="6" width="2" height="2" rx="1" fill="#242834"/>
<rect x="852" y="6" width="2" height="2" rx="1" fill="#242834"/>
<rect x="858" y="6" width="2" height="2" rx="1" fill="#242834"/>
<rect x="864" y="6" width="2" height="2" rx="1" fill="#242834"/>
<rect x="870" y="6" width="2" height="2" rx="1" fill="#242834"/>
<rect x="876" y="6" width="2" height="2" rx="1" fill="#242834"/>
<rect x="882" y="6" width="2" height="2" rx="1" fill="#242834"/>
<rect x="888" y="6" width="2" height="2" rx="1" fill="#242834"/>
<rect x="894" y="6" width="2" height="2" rx="1" fill="#242834"/>
<rect x="900" y="6" width="2" height="2" rx="1" fill="#242834"/>
<rect x="906" y="6" width="2" height="2" rx="1" fill="#242834"/>
<rect x="912" y="6" width="2" height="2" rx="1" fill="#242834"/>
<rect x="918" y="6" width="2" height="2" rx="1" fill="#242834"/>
<rect x="924" y="6" width="2" height="2" rx="1" fill="#242834"/>
</svg>

After

Width:  |  Height:  |  Size: 19 KiB

View File

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

View File

@@ -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

@@ -5,13 +5,13 @@ import {
ServiceData,
UpdateServiceConfigPayload,
UpdateServiceConfigResponse,
} from 'container/CloudIntegrationPage/ServicesSection/types';
} from 'container/Integrations/CloudIntegration/AmazonWebServices/types';
import {
AccountConfigPayload,
AccountConfigResponse,
ConnectionParams,
AWSAccountConfigPayload,
ConnectionUrlResponse,
} from 'types/api/integrations/aws';
import { ConnectionParams } from 'types/api/integrations/types';
export const getAwsAccounts = async (): Promise<CloudAccount[]> => {
const response = await axios.get('/cloud-integrations/aws/accounts');
@@ -60,7 +60,7 @@ export const generateConnectionUrl = async (params: {
export const updateAccountConfig = async (
accountId: string,
payload: AccountConfigPayload,
payload: AWSAccountConfigPayload,
): Promise<AccountConfigResponse> => {
const response = await axios.post<AccountConfigResponse>(
`/cloud-integrations/aws/accounts/${accountId}/config`,

View File

@@ -0,0 +1,122 @@
import axios from 'api';
import {
CloudAccount,
ServiceData,
} from 'container/Integrations/CloudIntegration/AmazonWebServices/types';
import {
AzureCloudAccountConfig,
AzureService,
AzureServiceConfigPayload,
} from 'container/Integrations/types';
import { ErrorResponse, SuccessResponse } from 'types/api';
import {
AccountConfigResponse,
AWSAccountConfigPayload,
} from 'types/api/integrations/aws';
import {
AzureAccountConfig,
ConnectionParams,
IAzureDeploymentCommands,
} from 'types/api/integrations/types';
export const getCloudIntegrationAccounts = async (
cloudServiceId: string,
): Promise<CloudAccount[]> => {
const response = await axios.get(
`/cloud-integrations/${cloudServiceId}/accounts`,
);
return response.data.data.accounts;
};
export const getCloudIntegrationServices = async (
cloudServiceId: string,
cloudAccountId?: string,
): Promise<AzureService[]> => {
const params = cloudAccountId
? { cloud_account_id: cloudAccountId }
: undefined;
const response = await axios.get(
`/cloud-integrations/${cloudServiceId}/services`,
{
params,
},
);
return response.data.data.services;
};
export const getCloudIntegrationServiceDetails = async (
cloudServiceId: string,
serviceId: string,
cloudAccountId?: string,
): Promise<ServiceData> => {
const params = cloudAccountId
? { cloud_account_id: cloudAccountId }
: undefined;
const response = await axios.get(
`/cloud-integrations/${cloudServiceId}/services/${serviceId}`,
{ params },
);
return response.data.data;
};
export const updateAccountConfig = async (
cloudServiceId: string,
accountId: string,
payload: AWSAccountConfigPayload | AzureAccountConfig,
): Promise<AccountConfigResponse> => {
const response = await axios.post<AccountConfigResponse>(
`/cloud-integrations/${cloudServiceId}/accounts/${accountId}/config`,
payload,
);
return response.data;
};
export const updateServiceConfig = async (
cloudServiceId: string,
serviceId: string,
payload: AzureServiceConfigPayload,
): Promise<AzureServiceConfigPayload> => {
const response = await axios.post<AzureServiceConfigPayload>(
`/cloud-integrations/${cloudServiceId}/services/${serviceId}/config`,
payload,
);
return response.data;
};
export const getConnectionParams = async (
cloudServiceId: string,
): Promise<ConnectionParams> => {
const response = await axios.get(
`/cloud-integrations/${cloudServiceId}/accounts/generate-connection-params`,
);
return response.data.data;
};
export const getAzureDeploymentCommands = async (params: {
agent_config: ConnectionParams;
account_config: AzureCloudAccountConfig;
}): Promise<IAzureDeploymentCommands> => {
const response = await axios.post(
`/cloud-integrations/azure/accounts/generate-connection-url`,
params,
);
return response.data.data;
};
export const removeIntegrationAccount = async ({
cloudServiceId,
accountId,
}: {
cloudServiceId: string;
accountId: string;
}): Promise<SuccessResponse<Record<string, never>> | ErrorResponse> => {
const response = await axios.post(
`/cloud-integrations/${cloudServiceId}/accounts/${accountId}/disconnect`,
);
return response.data;
};

View File

@@ -18,6 +18,8 @@ import '@signozhq/checkbox';
import '@signozhq/combobox';
import '@signozhq/command';
import '@signozhq/design-tokens';
import '@signozhq/dialog';
import '@signozhq/drawer';
import '@signozhq/icons';
import '@signozhq/input';
import '@signozhq/popover';
@@ -26,4 +28,5 @@ import '@signozhq/resizable';
import '@signozhq/sonner';
import '@signozhq/switch';
import '@signozhq/table';
import '@signozhq/tabs';
import '@signozhq/tooltip';

View File

@@ -0,0 +1,109 @@
.cloud-integration-accounts {
padding: 0px 16px;
display: flex;
flex-direction: column;
gap: 16px;
.selected-cloud-integration-account-section {
display: flex;
flex-direction: column;
gap: 16px;
.selected-cloud-integration-account-section-header {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
border-radius: 4px;
border: 1px solid var(--l3-background);
background: var(--l1-background);
.selected-cloud-integration-account-section-header-title {
display: flex;
flex-direction: row;
align-items: center;
gap: 8px;
.selected-cloud-integration-account-status {
display: flex;
border-right: 1px solid var(--l3-background);
border-radius: none;
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 4px;
line-height: 32px;
}
.selected-cloud-integration-account-section-header-title-text {
display: flex;
flex-direction: row;
align-items: center;
gap: 16px;
.azure-cloud-account-selector {
.ant-select {
background-color: transparent !important;
border: none !important;
box-shadow: none !important;
.ant-select-selector {
background: transparent !important;
border: none !important;
box-shadow: none !important;
}
}
}
}
}
.selected-cloud-integration-account-settings {
display: flex;
flex-direction: row;
gap: 16px;
line-height: 32px;
margin-right: 8px;
cursor: pointer;
}
}
.account-settings-container {
display: flex;
flex-direction: column;
gap: 16px;
}
.add-new-cloud-integration-account-button {
display: flex;
flex-direction: row;
gap: 16px;
line-height: 32px;
margin-right: 8px;
cursor: pointer;
}
}
}
.cloud-integration-accounts-drawer-content {
height: 100%;
max-height: calc(100vh - 120px); // Account for drawer header and padding
overflow-y: auto;
overflow-x: hidden;
display: flex;
flex-direction: column;
&::-webkit-scrollbar {
width: 0.2rem;
}
.edit-account-content,
.add-new-account-content {
flex: 1;
min-height: 0; // Allows flex children to shrink below content size
}
}

View File

@@ -0,0 +1,165 @@
import { useState } from 'react';
import { Button } from '@signozhq/button';
import { Color } from '@signozhq/design-tokens';
import { DrawerWrapper } from '@signozhq/drawer';
import { Select } from 'antd';
import ConnectNewAzureAccount from 'container/Integrations/CloudIntegration/AzureServices/AzureAccount/ConnectNewAzureAccount';
import EditAzureAccount from 'container/Integrations/CloudIntegration/AzureServices/AzureAccount/EditAzureAccount';
import { INTEGRATION_TYPES } from 'container/Integrations/constants';
import { CloudAccount } from 'container/Integrations/types';
import { useGetConnectionParams } from 'hooks/integration/useGetConnectionParams';
import useAxiosError from 'hooks/useAxiosError';
import { Dot, PencilLine, Plus } from 'lucide-react';
import './CloudIntegrationAccounts.styles.scss';
export type DrawerMode = 'edit' | 'add';
export default function CloudIntegrationAccounts({
selectedAccount,
accounts,
isLoadingAccounts,
onSelectAccount,
refetchAccounts,
}: {
selectedAccount: CloudAccount | null;
accounts: CloudAccount[];
isLoadingAccounts: boolean;
onSelectAccount: (account: CloudAccount) => void;
refetchAccounts: () => void;
}): JSX.Element {
const [isDrawerOpen, setIsDrawerOpen] = useState(false);
const [mode, setMode] = useState<DrawerMode>('add');
const handleDrawerOpenChange = (open: boolean): void => {
setIsDrawerOpen(open);
};
const handleEditAccount = (): void => {
setMode('edit');
setIsDrawerOpen(true);
};
const handleAddNewAccount = (): void => {
setMode('add');
setIsDrawerOpen(true);
};
const handleError = useAxiosError();
const {
data: connectionParams,
isLoading: isConnectionParamsLoading,
} = useGetConnectionParams({
cloudServiceId: INTEGRATION_TYPES.AZURE,
options: { onError: handleError },
});
const handleSelectAccount = (value: string): void => {
const account = accounts.find(
(account) => account.cloud_account_id === value,
);
if (account) {
onSelectAccount(account);
}
};
const handleAccountConnected = (): void => {
refetchAccounts();
};
const handleAccountUpdated = (): void => {
refetchAccounts();
};
const renderDrawerContent = (): JSX.Element => {
return (
<div className="cloud-integration-accounts-drawer-content">
{mode === 'edit' ? (
<div className="edit-account-content">
<EditAzureAccount
selectedAccount={selectedAccount as CloudAccount}
connectionParams={connectionParams || {}}
isConnectionParamsLoading={isConnectionParamsLoading}
onAccountUpdated={handleAccountUpdated}
/>
</div>
) : (
<div className="add-new-account-content">
<ConnectNewAzureAccount
connectionParams={connectionParams || {}}
isConnectionParamsLoading={isConnectionParamsLoading}
onAccountConnected={handleAccountConnected}
/>
</div>
)}
</div>
);
};
return (
<div className="cloud-integration-accounts">
{selectedAccount && (
<div className="selected-cloud-integration-account-section">
<div className="selected-cloud-integration-account-section-header">
<div className="selected-cloud-integration-account-section-header-title">
<div className="selected-cloud-integration-account-status">
<Dot size={24} color={Color.BG_FOREST_500} />
</div>
<div className="selected-cloud-integration-account-section-header-title-text">
Subscription ID :
<span className="azure-cloud-account-selector">
<Select
value={selectedAccount?.cloud_account_id}
options={accounts.map((account) => ({
label: account.cloud_account_id,
value: account.cloud_account_id,
}))}
onChange={handleSelectAccount}
loading={isLoadingAccounts}
placeholder="Select Account"
/>
</span>
</div>
</div>
<div className="selected-cloud-integration-account-settings">
<Button
variant="link"
color="secondary"
prefixIcon={<PencilLine size={14} />}
onClick={handleEditAccount}
>
Edit Account
</Button>
<Button
variant="link"
color="secondary"
prefixIcon={<Plus size={14} />}
onClick={handleAddNewAccount}
>
Add New Account
</Button>
</div>
</div>
</div>
)}
<div className="account-settings-container">
<DrawerWrapper
open={isDrawerOpen}
onOpenChange={handleDrawerOpenChange}
type="panel"
header={{
title: mode === 'add' ? 'Connect with Azure' : 'Edit Azure Account',
}}
content={renderDrawerContent()}
showCloseButton
allowOutsideClick={mode === 'edit'}
direction="right"
/>
</div>
</div>
);
}

View File

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

View File

@@ -0,0 +1,50 @@
.cloud-integrations-header-section {
display: flex;
flex-direction: column;
gap: 16px;
border-bottom: 1px solid var(--l3-background);
.cloud-integrations-header {
display: flex;
flex-direction: row;
align-items: center;
padding: 16px;
padding-bottom: 0px;
gap: 16px;
.cloud-integrations-title-section {
display: flex;
flex-direction: column;
gap: 4px;
.cloud-integrations-title {
display: flex;
align-items: center;
gap: 16px;
.cloud-integrations-icon {
width: 40px;
height: 40px;
}
color: var(--l1-foreground);
font-family: Inter;
font-size: 16px;
font-style: normal;
font-weight: 500;
line-height: 32px; /* 200% */
letter-spacing: -0.08px;
}
.cloud-integrations-description {
color: var(--l2-foreground);
font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
}
}
}
}

View File

@@ -0,0 +1,61 @@
import {
AWS_INTEGRATION,
AZURE_INTEGRATION,
} from 'container/Integrations/constants';
import { CloudAccount, IntegrationType } from 'container/Integrations/types';
import CloudIntegrationAccounts from '../CloudIntegrationAccounts';
import './CloudIntegrationsHeader.styles.scss';
export default function CloudIntegrationsHeader({
cloudServiceId,
selectedAccount,
accounts,
isLoadingAccounts,
onSelectAccount,
refetchAccounts,
}: {
selectedAccount: CloudAccount | null;
accounts: CloudAccount[] | [];
isLoadingAccounts: boolean;
onSelectAccount: (account: CloudAccount) => void;
cloudServiceId: IntegrationType;
refetchAccounts: () => void;
}): JSX.Element {
const INTEGRATION_DATA =
cloudServiceId === IntegrationType.AWS_SERVICES
? AWS_INTEGRATION
: AZURE_INTEGRATION;
return (
<div className="cloud-integrations-header-section">
<div className="cloud-integrations-header">
<div className="cloud-integrations-title-section">
<div className="cloud-integrations-title">
<img
className="cloud-integrations-icon"
src={INTEGRATION_DATA.icon}
alt={INTEGRATION_DATA.icon_alt}
/>
{INTEGRATION_DATA.title}
</div>
<div className="cloud-integrations-description">
{INTEGRATION_DATA.description}
</div>
</div>
</div>
<div className="cloud-integrations-accounts-list">
<CloudIntegrationAccounts
selectedAccount={selectedAccount}
accounts={accounts}
isLoadingAccounts={isLoadingAccounts}
onSelectAccount={onSelectAccount}
refetchAccounts={refetchAccounts}
/>
</div>
</div>
);
}

View File

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

View File

@@ -0,0 +1,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,6 +1,8 @@
import { Table } from 'antd';
import { ServiceData } from 'container/Integrations/CloudIntegration/AmazonWebServices/types';
import { BarChart2, ScrollText } from 'lucide-react';
import { ServiceData } from './types';
import './CloudServiceDataCollected.styles.scss';
function CloudServiceDataCollected({
logsData,
@@ -61,26 +63,32 @@ function CloudServiceDataCollected({
return (
<div className="cloud-service-data-collected">
{logsData && logsData.length > 0 && (
<div className="cloud-service-data-collected__table">
<div className="cloud-service-data-collected__table-heading">Logs</div>
<div className="cloud-service-data-collected-table">
<div className="cloud-service-data-collected-table-heading">
<ScrollText size={14} />
Logs
</div>
<Table
columns={logsColumns}
dataSource={logsData}
// eslint-disable-next-line react/jsx-props-no-spreading
{...tableProps}
className="cloud-service-data-collected__table-logs"
className="cloud-service-data-collected-table-logs"
/>
</div>
)}
{metricsData && metricsData.length > 0 && (
<div className="cloud-service-data-collected__table">
<div className="cloud-service-data-collected__table-heading">Metrics</div>
<div className="cloud-service-data-collected-table">
<div className="cloud-service-data-collected-table-heading">
<BarChart2 size={14} />
Metrics
</div>
<Table
columns={metricsColumns}
dataSource={metricsData}
// eslint-disable-next-line react/jsx-props-no-spreading
{...tableProps}
className="cloud-service-data-collected__table-metrics"
className="cloud-service-data-collected-table-metrics"
/>
</div>
)}

View File

@@ -0,0 +1,39 @@
.code-block-container {
position: relative;
border-radius: 4px;
overflow: hidden;
.code-block-copy-btn {
position: absolute;
top: 50%;
right: 8px;
transform: translateY(-50%);
z-index: 10;
display: flex;
align-items: center;
justify-content: center;
padding: 6px 8px;
color: var(--bg-vanilla-100);
transition: color 0.15s ease;
&.copied {
background-color: var(--bg-robin-500);
}
}
// CodeMirror wrapper
.code-block-editor {
border-radius: 4px;
.cm-editor {
border-radius: 4px;
font-size: 13px;
line-height: 1.5;
font-family: 'Space Mono', monospace;
}
.cm-scroller {
font-family: 'Space Mono', monospace;
}
}
}

View File

@@ -0,0 +1,145 @@
import { useCallback, useMemo, useState } from 'react';
import { CheckOutlined, CopyOutlined } from '@ant-design/icons';
import { javascript } from '@codemirror/lang-javascript';
import { Button } from '@signozhq/button';
import { dracula } from '@uiw/codemirror-theme-dracula';
import { githubLight } from '@uiw/codemirror-theme-github';
import CodeMirror, {
EditorState,
EditorView,
Extension,
} from '@uiw/react-codemirror';
import cx from 'classnames';
import { useIsDarkMode } from 'hooks/useDarkMode';
import './CodeBlock.styles.scss';
export type CodeBlockLanguage =
| 'javascript'
| 'typescript'
| 'js'
| 'ts'
| 'json'
| 'bash'
| 'shell'
| 'text';
export type CodeBlockTheme = 'light' | 'dark' | 'auto';
interface CodeBlockProps {
/** The code content to display */
value: string;
/** Language for syntax highlighting */
language?: CodeBlockLanguage;
/** Theme: 'light' | 'dark' | 'auto' (follows app dark mode when 'auto') */
theme?: CodeBlockTheme;
/** Show line numbers */
lineNumbers?: boolean;
/** Show copy button */
showCopyButton?: boolean;
/** Custom class name for the container */
className?: string;
/** Max height in pixels - enables scrolling when content exceeds */
maxHeight?: number | string;
/** Callback when copy is clicked */
onCopy?: (copiedText: string) => void;
}
const LANGUAGE_EXTENSION_MAP: Record<
CodeBlockLanguage,
ReturnType<typeof javascript> | undefined
> = {
javascript: javascript({ jsx: true }),
typescript: javascript({ jsx: true }),
js: javascript({ jsx: true }),
ts: javascript({ jsx: true }),
json: javascript(), // JSON is valid JS; proper json() would require @codemirror/lang-json
bash: undefined,
shell: undefined,
text: undefined,
};
function CodeBlock({
value,
language = 'text',
theme: themeProp = 'auto',
lineNumbers = true,
showCopyButton = true,
className,
maxHeight,
onCopy,
}: CodeBlockProps): JSX.Element {
const isDarkMode = useIsDarkMode();
const [isCopied, setIsCopied] = useState(false);
const resolvedDark = themeProp === 'auto' ? isDarkMode : themeProp === 'dark';
const theme = resolvedDark ? dracula : githubLight;
const extensions = useMemo((): Extension[] => {
const langExtension = LANGUAGE_EXTENSION_MAP[language];
return [
EditorState.readOnly.of(true),
EditorView.editable.of(false),
EditorView.lineWrapping,
...(langExtension ? [langExtension] : []),
];
}, [language]);
const handleCopy = useCallback((): void => {
navigator.clipboard.writeText(value).then(() => {
setIsCopied(true);
onCopy?.(value);
setTimeout(() => setIsCopied(false), 2000);
});
}, [value, onCopy]);
return (
<div className={cx('code-block-container', className)}>
{showCopyButton && (
<Button
variant="solid"
size="xs"
color="secondary"
className={cx('code-block-copy-btn', { copied: isCopied })}
onClick={handleCopy}
aria-label={isCopied ? 'Copied' : 'Copy code'}
title={isCopied ? 'Copied' : 'Copy code'}
>
{isCopied ? <CheckOutlined /> : <CopyOutlined />}
</Button>
)}
<CodeMirror
className="code-block-editor"
value={value}
theme={theme}
readOnly
editable={false}
extensions={extensions}
basicSetup={{
lineNumbers,
highlightActiveLineGutter: false,
highlightActiveLine: false,
highlightSelectionMatches: true,
drawSelection: true,
syntaxHighlighting: true,
bracketMatching: true,
history: false,
foldGutter: false,
autocompletion: false,
defaultKeymap: false,
searchKeymap: true,
historyKeymap: false,
foldKeymap: false,
completionKeymap: false,
closeBrackets: false,
indentOnInput: false,
}}
style={{
maxHeight: maxHeight ?? 'auto',
}}
/>
</div>
);
}
export default CodeBlock;

View File

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

View File

@@ -3,8 +3,8 @@ import { useLocation } from 'react-router-dom';
import { toast } from '@signozhq/sonner';
import { Button, Input, Radio, RadioChangeEvent, Typography } from 'antd';
import logEvent from 'api/common/logEvent';
import { handleContactSupport } from 'container/Integrations/utils';
import { useGetTenantLicense } from 'hooks/useGetTenantLicense';
import { handleContactSupport } from 'pages/Integrations/utils';
function FeedbackModal({ onClose }: { onClose: () => void }): JSX.Element {
const [activeTab, setActiveTab] = useState('feedback');

View File

@@ -5,8 +5,8 @@ import { toast } from '@signozhq/sonner';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import logEvent from 'api/common/logEvent';
import { handleContactSupport } from 'container/Integrations/utils';
import { useGetTenantLicense } from 'hooks/useGetTenantLicense';
import { handleContactSupport } from 'pages/Integrations/utils';
import FeedbackModal from '../FeedbackModal';
@@ -31,7 +31,7 @@ jest.mock('hooks/useGetTenantLicense', () => ({
useGetTenantLicense: jest.fn(),
}));
jest.mock('pages/Integrations/utils', () => ({
jest.mock('container/Integrations/utils', () => ({
handleContactSupport: jest.fn(),
}));

View File

@@ -21,7 +21,7 @@ export function getDefaultCellStyle(isDarkMode?: boolean): CSSProperties {
export const defaultTableStyle: CSSProperties = {
minWidth: '40rem',
maxWidth: '60rem',
maxWidth: '90rem',
};
export const defaultListViewPanelStyle: CSSProperties = {

View File

@@ -1,4 +1,19 @@
export const REACT_QUERY_KEY = {
// Cloud Integration Query Keys
CLOUD_INTEGRATION_ACCOUNTS: 'CLOUD_INTEGRATION_ACCOUNTS',
CLOUD_INTEGRATION_SERVICES: 'CLOUD_INTEGRATION_SERVICES',
CLOUD_INTEGRATION_SERVICE_DETAILS: 'CLOUD_INTEGRATION_SERVICE_DETAILS',
CLOUD_INTEGRATION_ACCOUNT_STATUS: 'CLOUD_INTEGRATION_ACCOUNT_STATUS',
CLOUD_INTEGRATION_UPDATE_ACCOUNT_CONFIG:
'CLOUD_INTEGRATION_UPDATE_ACCOUNT_CONFIG',
CLOUD_INTEGRATION_UPDATE_SERVICE_CONFIG:
'CLOUD_INTEGRATION_UPDATE_SERVICE_CONFIG',
CLOUD_INTEGRATION_GENERATE_CONNECTION_URL:
'CLOUD_INTEGRATION_GENERATE_CONNECTION_URL',
CLOUD_INTEGRATION_GET_CONNECTION_PARAMS:
'CLOUD_INTEGRATION_GET_CONNECTION_PARAMS',
CLOUD_INTEGRATION_GET_DEPLOYMENT_COMMANDS:
'CLOUD_INTEGRATION_GET_DEPLOYMENT_COMMANDS',
GET_PUBLIC_DASHBOARD: 'GET_PUBLIC_DASHBOARD',
GET_PUBLIC_DASHBOARD_META: 'GET_PUBLIC_DASHBOARD_META',
GET_PUBLIC_DASHBOARD_WIDGET_DATA: 'GET_PUBLIC_DASHBOARD_WIDGET_DATA',

View File

@@ -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

@@ -9,6 +9,7 @@ import {
import useVariablesFromUrl from 'hooks/dashboard/useVariablesFromUrl';
import { useDashboard } from 'providers/Dashboard/Dashboard';
import { initializeDefaultVariables } from 'providers/Dashboard/initializeDefaultVariables';
import { updateDashboardVariablesStore } from 'providers/Dashboard/store/dashboardVariables/dashboardVariablesStore';
import {
enqueueDescendantsOfVariable,
enqueueFetchOfAllVariables,
@@ -31,6 +32,9 @@ function DashboardVariableSelection(): JSX.Element | null {
const { updateUrlVariable, getUrlVariables } = useVariablesFromUrl();
const { dashboardVariables } = useDashboardVariables();
const dashboardId = useDashboardVariablesSelector(
(state) => state.dashboardId,
);
const sortedVariablesArray = useDashboardVariablesSelector(
(state) => state.sortedVariablesArray,
);
@@ -96,6 +100,28 @@ function DashboardVariableSelection(): JSX.Element | null {
updateUrlVariable(name || id, value);
}
// Synchronously update the external store with the new variable value so that
// child variables see the updated parent value when they refetch, rather than
// waiting for setSelectedDashboard → useEffect → updateDashboardVariablesStore.
const updatedVariables = { ...dashboardVariables };
if (updatedVariables[id]) {
updatedVariables[id] = {
...updatedVariables[id],
selectedValue: value,
allSelected,
haveCustomValuesSelected,
};
}
if (updatedVariables[name]) {
updatedVariables[name] = {
...updatedVariables[name],
selectedValue: value,
allSelected,
haveCustomValuesSelected,
};
}
updateDashboardVariablesStore({ dashboardId, variables: updatedVariables });
setSelectedDashboard((prev) => {
if (prev) {
const oldVariables = { ...prev?.data.variables };
@@ -130,10 +156,12 @@ function DashboardVariableSelection(): JSX.Element | null {
return prev;
});
// Cascade: enqueue query-type descendants for refetching
// Cascade: enqueue query-type descendants for refetching.
// Safe to call synchronously now that the store already has the updated value.
enqueueDescendantsOfVariable(name);
},
[
dashboardId,
dashboardVariables,
updateLocalStorageDashboardVariables,
updateUrlVariable,

View File

@@ -5,7 +5,7 @@ import dashboardVariablesQuery from 'api/dashboard/variables/dashboardVariablesQ
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import { useVariableFetchState } from 'hooks/dashboard/useVariableFetchState';
import sortValues from 'lib/dashboardVariables/sortVariableValues';
import { isArray, isEmpty, isString } from 'lodash-es';
import { isArray, isEmpty } from 'lodash-es';
import { AppState } from 'store/reducers';
import { VariableResponseProps } from 'types/api/dashboard/variables/query';
import { GlobalReducer } from 'types/reducer/globalTime';
@@ -54,7 +54,7 @@ function QueryVariableInput({
onChange,
onDropdownVisibleChange,
handleClear,
applyDefaultIfNeeded,
getDefaultValue,
} = useDashboardVariableSelectHelper({
variableData,
optionsData,
@@ -68,81 +68,93 @@ function QueryVariableInput({
try {
setErrorMessage(null);
// This is just a check given the previously undefined typed name prop. Not significant
// This will be changed when we change the schema
// TODO: @AshwinBhatkal Perses
if (!variableData.name) {
return;
}
// if the response is not an array, premature return
if (
variablesRes?.variableValues &&
Array.isArray(variablesRes?.variableValues)
!variablesRes?.variableValues ||
!Array.isArray(variablesRes?.variableValues)
) {
const newOptionsData = sortValues(
variablesRes?.variableValues,
variableData.sort,
return;
}
const sortedNewOptions = sortValues(
variablesRes.variableValues,
variableData.sort,
);
const sortedOldOptions = sortValues(optionsData, variableData.sort);
// if options are the same as before, no need to update state or check for selected value validity
// ! selectedValue needs to be set in the first pass though, as options are initially empty array and we need to apply default if needed
// Expecatation is that when oldOptions are not empty, then there is always some selectedValue
if (areArraysEqual(sortedNewOptions, sortedOldOptions)) {
return;
}
setOptionsData(sortedNewOptions);
let isSelectedValueMissingInNewOptions = false;
// Check if currently selected value(s) are present in the new options list
if (isArray(variableData.selectedValue)) {
isSelectedValueMissingInNewOptions = variableData.selectedValue.some(
(val) => !sortedNewOptions.includes(val),
);
} else if (
variableData.selectedValue &&
!sortedNewOptions.includes(variableData.selectedValue)
) {
isSelectedValueMissingInNewOptions = true;
}
const oldOptionsData = sortValues(optionsData, variableData.sort) as never;
// If multi-select with ALL option enabled, and ALL is currently selected, we want to maintain that state and select all new options
// This block does not depend on selected value because of ALL and also because we would only come here if options are different from the previous
if (
variableData.multiSelect &&
variableData.showALLOption &&
variableData.allSelected &&
isSelectedValueMissingInNewOptions
) {
onValueUpdate(variableData.name, variableData.id, sortedNewOptions, true);
if (!areArraysEqual(newOptionsData, oldOptionsData)) {
let valueNotInList = false;
// Update tempSelection to maintain ALL state when dropdown is open
if (tempSelection !== undefined) {
setTempSelection(sortedNewOptions.map((option) => option.toString()));
}
return;
}
if (isArray(variableData.selectedValue)) {
variableData.selectedValue.forEach((val) => {
if (!newOptionsData.includes(val)) {
valueNotInList = true;
}
});
} else if (
isString(variableData.selectedValue) &&
!newOptionsData.includes(variableData.selectedValue)
) {
valueNotInList = true;
}
const value = variableData.selectedValue;
let allSelected = false;
if (variableData.name && (valueNotInList || variableData.allSelected)) {
if (
variableData.allSelected &&
variableData.multiSelect &&
variableData.showALLOption
) {
if (
variableData.name &&
variableData.id &&
!isEmpty(variableData.selectedValue)
) {
onValueUpdate(
variableData.name,
variableData.id,
newOptionsData,
true,
);
}
if (variableData.multiSelect) {
const { selectedValue } = variableData;
allSelected =
sortedNewOptions.length > 0 &&
Array.isArray(selectedValue) &&
sortedNewOptions.every((option) => selectedValue.includes(option));
}
// Update tempSelection to maintain ALL state when dropdown is open
if (tempSelection !== undefined) {
setTempSelection(newOptionsData.map((option) => option.toString()));
}
} else {
const value = variableData.selectedValue;
let allSelected = false;
if (variableData.multiSelect) {
const { selectedValue } = variableData;
allSelected =
newOptionsData.length > 0 &&
Array.isArray(selectedValue) &&
newOptionsData.every((option) => selectedValue.includes(option));
}
if (
variableData.name &&
variableData.id &&
!isEmpty(variableData.selectedValue)
) {
onValueUpdate(variableData.name, variableData.id, value, allSelected);
}
}
}
setOptionsData(newOptionsData);
// Apply default if no value is selected (e.g., new variable, first load)
applyDefaultIfNeeded(newOptionsData);
if (
variableData.name &&
variableData.id &&
!isEmpty(variableData.selectedValue)
) {
onValueUpdate(variableData.name, variableData.id, value, allSelected);
} else {
const defaultValue = getDefaultValue(sortedNewOptions);
if (defaultValue !== undefined) {
onValueUpdate(
variableData.name,
variableData.id,
defaultValue,
allSelected,
);
}
}
} catch (e) {
@@ -155,7 +167,7 @@ function QueryVariableInput({
onValueUpdate,
tempSelection,
setTempSelection,
applyDefaultIfNeeded,
getDefaultValue,
],
);

View File

@@ -1,5 +1,6 @@
/* eslint-disable sonarjs/no-duplicate-string */
import { act, render } from '@testing-library/react';
import * as dashboardVariablesStoreModule from 'providers/Dashboard/store/dashboardVariables/dashboardVariablesStore';
import {
dashboardVariablesStore,
setDashboardVariablesStore,
@@ -10,6 +11,7 @@ import {
IDashboardVariablesStoreState,
} from 'providers/Dashboard/store/dashboardVariables/dashboardVariablesStoreTypes';
import {
enqueueDescendantsOfVariable,
enqueueFetchOfAllVariables,
initializeVariableFetchStore,
} from 'providers/Dashboard/store/variableFetchStore';
@@ -17,6 +19,17 @@ import { IDashboardVariable } from 'types/api/dashboard/getAll';
import DashboardVariableSelection from '../DashboardVariableSelection';
// Mutable container to capture the onValueUpdate callback from VariableItem
const mockVariableItemCallbacks: {
onValueUpdate?: (
name: string,
id: string,
value: IDashboardVariable['selectedValue'],
allSelected: boolean,
haveCustomValuesSelected?: boolean,
) => void;
} = {};
// Mock providers/Dashboard/Dashboard
const mockSetSelectedDashboard = jest.fn();
const mockUpdateLocalStorageDashboardVariables = jest.fn();
@@ -56,10 +69,14 @@ jest.mock('react-redux', () => ({
useSelector: jest.fn().mockReturnValue({ minTime: 1000, maxTime: 2000 }),
}));
// Mock VariableItem to avoid rendering complexity
// VariableItem mock captures the onValueUpdate prop for use in onValueUpdate tests
jest.mock('../VariableItem', () => ({
__esModule: true,
default: (): JSX.Element => <div data-testid="variable-item" />,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
default: (props: any): JSX.Element => {
mockVariableItemCallbacks.onValueUpdate = props.onValueUpdate;
return <div data-testid="variable-item" />;
},
}));
function createVariable(
@@ -200,4 +217,162 @@ describe('DashboardVariableSelection', () => {
expect(initializeVariableFetchStore).not.toHaveBeenCalled();
expect(enqueueFetchOfAllVariables).not.toHaveBeenCalled();
});
describe('onValueUpdate', () => {
let updateStoreSpy: jest.SpyInstance;
beforeEach(() => {
resetStore();
jest.clearAllMocks();
// Real implementation pass-through — we just want to observe calls
updateStoreSpy = jest.spyOn(
dashboardVariablesStoreModule,
'updateDashboardVariablesStore',
);
});
afterEach(() => {
updateStoreSpy.mockRestore();
});
it('updates dashboardVariablesStore synchronously before enqueueDescendantsOfVariable', () => {
setDashboardVariablesStore({
dashboardId: 'dash-1',
variables: {
env: createVariable({ name: 'env', id: 'env-id', order: 0 }),
},
});
render(<DashboardVariableSelection />);
const callOrder: string[] = [];
updateStoreSpy.mockImplementation(() => {
callOrder.push('updateDashboardVariablesStore');
});
(enqueueDescendantsOfVariable as jest.Mock).mockImplementation(() => {
callOrder.push('enqueueDescendantsOfVariable');
});
act(() => {
mockVariableItemCallbacks.onValueUpdate?.(
'env',
'env-id',
'production',
false,
);
});
expect(callOrder).toEqual([
'updateDashboardVariablesStore',
'enqueueDescendantsOfVariable',
]);
});
it('passes updated variable value to dashboardVariablesStore', () => {
setDashboardVariablesStore({
dashboardId: 'dash-1',
variables: {
env: createVariable({
name: 'env',
id: 'env-id',
order: 0,
selectedValue: 'staging',
}),
},
});
render(<DashboardVariableSelection />);
// Clear spy calls that happened during setup/render
updateStoreSpy.mockClear();
act(() => {
mockVariableItemCallbacks.onValueUpdate?.(
'env',
'env-id',
'production',
false,
);
});
expect(updateStoreSpy).toHaveBeenCalledWith(
expect.objectContaining({
dashboardId: 'dash-1',
variables: expect.objectContaining({
env: expect.objectContaining({
selectedValue: 'production',
allSelected: false,
}),
}),
}),
);
});
it('calls enqueueDescendantsOfVariable synchronously without a timer', () => {
jest.useFakeTimers();
setDashboardVariablesStore({
dashboardId: 'dash-1',
variables: {
env: createVariable({ name: 'env', id: 'env-id', order: 0 }),
},
});
render(<DashboardVariableSelection />);
act(() => {
mockVariableItemCallbacks.onValueUpdate?.(
'env',
'env-id',
'production',
false,
);
});
// Must be called immediately — no timer advancement needed
expect(enqueueDescendantsOfVariable).toHaveBeenCalledWith('env');
jest.useRealTimers();
});
it('propagates allSelected and haveCustomValuesSelected to the store', () => {
setDashboardVariablesStore({
dashboardId: 'dash-1',
variables: {
env: createVariable({
name: 'env',
id: 'env-id',
order: 0,
multiSelect: true,
showALLOption: true,
}),
},
});
render(<DashboardVariableSelection />);
updateStoreSpy.mockClear();
act(() => {
mockVariableItemCallbacks.onValueUpdate?.(
'env',
'env-id',
['production', 'staging'],
true,
false,
);
});
expect(updateStoreSpy).toHaveBeenCalledWith(
expect.objectContaining({
variables: expect.objectContaining({
env: expect.objectContaining({
selectedValue: ['production', 'staging'],
allSelected: true,
haveCustomValuesSelected: false,
}),
}),
}),
);
});
});
});

View File

@@ -0,0 +1,275 @@
/* eslint-disable sonarjs/no-duplicate-string */
import { QueryClient, QueryClientProvider } from 'react-query';
import { act, render, waitFor } from '@testing-library/react';
import dashboardVariablesQuery from 'api/dashboard/variables/dashboardVariablesQuery';
import { variableFetchStore } from 'providers/Dashboard/store/variableFetchStore';
import { IDashboardVariable } from 'types/api/dashboard/getAll';
import QueryVariableInput from '../QueryVariableInput';
jest.mock('api/dashboard/variables/dashboardVariablesQuery');
jest.mock('react-redux', () => ({
...jest.requireActual('react-redux'),
useSelector: jest.fn().mockReturnValue({ minTime: 1000, maxTime: 2000 }),
}));
function createTestQueryClient(): QueryClient {
return new QueryClient({
defaultOptions: {
queries: { retry: false, refetchOnWindowFocus: false },
},
});
}
function Wrapper({
children,
queryClient,
}: {
children: React.ReactNode;
queryClient: QueryClient;
}): JSX.Element {
return (
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
);
}
function createVariable(
overrides: Partial<IDashboardVariable> = {},
): IDashboardVariable {
return {
id: 'env-id',
name: 'env',
description: '',
type: 'QUERY',
sort: 'DISABLED',
showALLOption: false,
multiSelect: false,
order: 0,
queryValue: 'SELECT env FROM table',
...overrides,
};
}
/** Put the named variable into 'loading' state so useQuery fires on mount */
function setVariableLoading(name: string): void {
variableFetchStore.update((draft) => {
draft.states[name] = 'loading';
draft.cycleIds[name] = (draft.cycleIds[name] || 0) + 1;
});
}
function resetFetchStore(): void {
variableFetchStore.set(() => ({
states: {},
lastUpdated: {},
cycleIds: {},
}));
}
describe('QueryVariableInput - getOptions logic', () => {
const mockOnValueUpdate = jest.fn();
beforeEach(() => {
jest.clearAllMocks();
resetFetchStore();
});
afterEach(() => {
resetFetchStore();
});
it('applies default value (first option) when selectedValue is empty on first load', async () => {
(dashboardVariablesQuery as jest.Mock).mockResolvedValue({
statusCode: 200,
payload: { variableValues: ['production', 'staging', 'dev'] },
});
const variable = createVariable({ selectedValue: undefined });
setVariableLoading('env');
const queryClient = createTestQueryClient();
render(
<Wrapper queryClient={queryClient}>
<QueryVariableInput
variableData={variable}
existingVariables={{ 'env-id': variable }}
onValueUpdate={mockOnValueUpdate}
/>
</Wrapper>,
);
await waitFor(() => {
expect(mockOnValueUpdate).toHaveBeenCalledWith(
'env',
'env-id',
'production', // first option by default
false,
);
});
});
it('keeps existing selectedValue when it is present in new options', async () => {
(dashboardVariablesQuery as jest.Mock).mockResolvedValue({
statusCode: 200,
payload: { variableValues: ['production', 'staging'] },
});
const variable = createVariable({ selectedValue: 'staging' });
setVariableLoading('env');
const queryClient = createTestQueryClient();
render(
<Wrapper queryClient={queryClient}>
<QueryVariableInput
variableData={variable}
existingVariables={{ 'env-id': variable }}
onValueUpdate={mockOnValueUpdate}
/>
</Wrapper>,
);
await waitFor(() => {
expect(mockOnValueUpdate).toHaveBeenCalledWith(
'env',
'env-id',
'staging',
false,
);
});
});
it('selects all new options when allSelected=true and value is missing from new options', async () => {
(dashboardVariablesQuery as jest.Mock).mockResolvedValue({
statusCode: 200,
payload: { variableValues: ['production', 'staging'] },
});
const variable = createVariable({
selectedValue: ['old-env'],
allSelected: true,
multiSelect: true,
showALLOption: true,
});
setVariableLoading('env');
const queryClient = createTestQueryClient();
render(
<Wrapper queryClient={queryClient}>
<QueryVariableInput
variableData={variable}
existingVariables={{ 'env-id': variable }}
onValueUpdate={mockOnValueUpdate}
/>
</Wrapper>,
);
await waitFor(() => {
expect(mockOnValueUpdate).toHaveBeenCalledWith(
'env',
'env-id',
['production', 'staging'],
true,
);
});
});
it('does not call onValueUpdate a second time when options have not changed', async () => {
const mockQueryFn = jest.fn().mockResolvedValue({
statusCode: 200,
payload: { variableValues: ['production', 'staging'] },
});
(dashboardVariablesQuery as jest.Mock).mockImplementation(mockQueryFn);
const variable = createVariable({ selectedValue: 'production' });
setVariableLoading('env');
const queryClient = createTestQueryClient();
render(
<Wrapper queryClient={queryClient}>
<QueryVariableInput
variableData={variable}
existingVariables={{ 'env-id': variable }}
onValueUpdate={mockOnValueUpdate}
/>
</Wrapper>,
);
// Wait for first fetch and onValueUpdate call
await waitFor(() => {
expect(mockOnValueUpdate).toHaveBeenCalledTimes(1);
});
mockOnValueUpdate.mockClear();
// Trigger a second fetch cycle with the same API response
act(() => {
variableFetchStore.update((draft) => {
draft.states['env'] = 'revalidating';
draft.cycleIds['env'] = (draft.cycleIds['env'] || 0) + 1;
});
});
// Wait for second query to fire
await waitFor(() => {
expect(mockQueryFn).toHaveBeenCalledTimes(2);
});
// Options are unchanged, so onValueUpdate must not fire again
expect(mockOnValueUpdate).not.toHaveBeenCalled();
});
it('does not call onValueUpdate when API returns a non-array response', async () => {
(dashboardVariablesQuery as jest.Mock).mockResolvedValue({
statusCode: 200,
payload: { variableValues: null },
});
const variable = createVariable({ selectedValue: 'production' });
setVariableLoading('env');
const queryClient = createTestQueryClient();
render(
<Wrapper queryClient={queryClient}>
<QueryVariableInput
variableData={variable}
existingVariables={{ 'env-id': variable }}
onValueUpdate={mockOnValueUpdate}
/>
</Wrapper>,
);
await waitFor(() => {
expect(dashboardVariablesQuery).toHaveBeenCalled();
});
expect(mockOnValueUpdate).not.toHaveBeenCalled();
});
it('does not fire the query when variableData.name is empty', () => {
(dashboardVariablesQuery as jest.Mock).mockResolvedValue({
statusCode: 200,
payload: { variableValues: ['production'] },
});
// Variable with no name — useVariableFetchState will be called with ''
// and the query key will have an empty name, leaving it disabled
const variable = createVariable({ name: '' });
// Note: we do NOT put it in 'loading' state since name is empty
// (no variableFetchStore entry for '' means isVariableFetching=false)
const queryClient = createTestQueryClient();
render(
<Wrapper queryClient={queryClient}>
<QueryVariableInput
variableData={variable}
existingVariables={{ 'env-id': variable }}
onValueUpdate={mockOnValueUpdate}
/>
</Wrapper>,
);
expect(dashboardVariablesQuery).not.toHaveBeenCalled();
expect(mockOnValueUpdate).not.toHaveBeenCalled();
});
});

View File

@@ -46,6 +46,9 @@ interface UseDashboardVariableSelectHelperReturn {
applyDefaultIfNeeded: (
overrideOptions?: (string | number | boolean)[],
) => void;
getDefaultValue: (
overrideOptions?: (string | number | boolean)[],
) => string | string[] | undefined;
}
// eslint-disable-next-line sonarjs/cognitive-complexity
@@ -248,5 +251,6 @@ export function useDashboardVariableSelectHelper({
defaultValue,
onChange,
applyDefaultIfNeeded,
getDefaultValue,
};
}

View File

@@ -3,14 +3,12 @@ import { useQueryClient } from 'react-query';
import { Form, Switch } from 'antd';
import SignozModal from 'components/SignozModal/SignozModal';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import {
ServiceConfig,
SupportedSignals,
} from 'container/CloudIntegrationPage/ServicesSection/types';
import { AWSServiceConfig } from 'container/Integrations/CloudIntegration/AmazonWebServices/types';
import { SupportedSignals } from 'container/Integrations/types';
import { useUpdateServiceConfig } from 'hooks/integration/aws/useUpdateServiceConfig';
import { isEqual } from 'lodash-es';
import logEvent from '../../../api/common/logEvent';
import logEvent from '../../../../api/common/logEvent';
import S3BucketsSelector from './S3BucketsSelector';
import './ConfigureServiceModal.styles.scss';
@@ -22,7 +20,7 @@ export interface IConfigureServiceModalProps {
serviceId: string;
cloudAccountId: string;
supportedSignals: SupportedSignals;
initialConfig?: ServiceConfig;
initialConfig?: AWSServiceConfig;
}
function ConfigureServiceModal({

View File

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

View File

@@ -1,14 +1,16 @@
import { useEffect, useMemo, useState } from 'react';
import { useNavigate } from 'react-router-dom-v5-compat';
import { Button } from '@signozhq/button';
import { Color } from '@signozhq/design-tokens';
import { Button, Select, Skeleton } from 'antd';
import { Select, Skeleton } from 'antd';
import { SelectProps } from 'antd/lib';
import logEvent from 'api/common/logEvent';
import { getAccountById } from 'container/Integrations/CloudIntegration/utils';
import { useAwsAccounts } from 'hooks/integration/aws/useAwsAccounts';
import useUrlQuery from 'hooks/useUrlQuery';
import { Check, ChevronDown } from 'lucide-react';
import { CloudAccount } from '../../ServicesSection/types';
import { CloudAccount } from '../../types';
import AccountSettingsModal from './AccountSettingsModal';
import CloudAccountSetupModal from './CloudAccountSetupModal';
@@ -48,12 +50,6 @@ function renderOption(
);
}
const getAccountById = (
accounts: CloudAccount[],
accountId: string,
): CloudAccount | null =>
accounts.find((account) => account.cloud_account_id === accountId) || null;
function AccountActionsRenderer({
accounts,
isLoading,
@@ -74,24 +70,7 @@ function AccountActionsRenderer({
if (isLoading) {
return (
<div className="hero-section__actions-with-account">
<Skeleton.Input
active
size="large"
block
className="hero-section__input-skeleton"
/>
<div className="hero-section__action-buttons">
<Skeleton.Button
active
size="large"
className="hero-section__new-account-button-skeleton"
/>
<Skeleton.Button
active
size="large"
className="hero-section__account-settings-button-skeleton"
/>
</div>
<Skeleton.Input active block className="hero-section__input-skeleton" />
</div>
);
}
@@ -110,16 +89,12 @@ function AccountActionsRenderer({
onChange={onAccountChange}
/>
<div className="hero-section__action-buttons">
<Button
type="primary"
className="hero-section__action-button primary"
onClick={onIntegrationModalOpen}
>
<Button variant="solid" color="primary" onClick={onIntegrationModalOpen}>
Add New AWS Account
</Button>
<Button
type="default"
className="hero-section__action-button secondary"
variant="solid"
color="primary"
onClick={onAccountSettingsModalOpen}
>
Account Settings
@@ -129,10 +104,7 @@ function AccountActionsRenderer({
);
}
return (
<Button
className="hero-section__action-button primary"
onClick={onIntegrationModalOpen}
>
<Button variant="solid" color="primary" onClick={onIntegrationModalOpen}>
Integrate Now
</Button>
);

View File

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

View File

@@ -1,11 +1,12 @@
import { useRef } from 'react';
import { Form } from 'antd';
import cx from 'classnames';
import { useAccountStatus } from 'hooks/integration/aws/useAccountStatus';
import { AccountStatusResponse } from 'types/api/integrations/aws';
import { INTEGRATION_TYPES } from 'container/Integrations/constants';
import { useGetAccountStatus } from 'hooks/integration/useGetAccountStatus';
import { AccountStatusResponse } from 'types/api/integrations/types';
import { regions } from 'utils/regions';
import logEvent from '../../../../api/common/logEvent';
import logEvent from '../../../../../../api/common/logEvent';
import { ModalStateEnum, RegionFormProps } from '../types';
import AlertMessage from './AlertMessage';
import {
@@ -44,27 +45,31 @@ export function RegionForm({
const refetchInterval = 10 * 1000;
const errorTimeout = 10 * 60 * 1000;
const { isLoading: isAccountStatusLoading } = useAccountStatus(accountId, {
refetchInterval,
enabled: !!accountId && modalState === ModalStateEnum.WAITING,
onSuccess: (data: AccountStatusResponse) => {
if (data.data.status.integration.last_heartbeat_ts_ms !== null) {
setModalState(ModalStateEnum.SUCCESS);
logEvent('AWS Integration: Account connected', {
cloudAccountId: data?.data?.cloud_account_id,
status: data?.data?.status,
});
} else if (Date.now() - startTimeRef.current >= errorTimeout) {
const { isLoading: isAccountStatusLoading } = useGetAccountStatus(
INTEGRATION_TYPES.AWS,
accountId,
{
refetchInterval,
enabled: !!accountId && modalState === ModalStateEnum.WAITING,
onSuccess: (data: AccountStatusResponse) => {
if (data.data.status.integration.last_heartbeat_ts_ms !== null) {
setModalState(ModalStateEnum.SUCCESS);
logEvent('AWS Integration: Account connected', {
cloudAccountId: data?.data?.cloud_account_id,
status: data?.data?.status,
});
} else if (Date.now() - startTimeRef.current >= errorTimeout) {
setModalState(ModalStateEnum.ERROR);
logEvent('AWS Integration: Account connection attempt timed out', {
id: accountId,
});
}
},
onError: () => {
setModalState(ModalStateEnum.ERROR);
logEvent('AWS Integration: Account connection attempt timed out', {
id: accountId,
});
}
},
},
onError: () => {
setModalState(ModalStateEnum.ERROR);
},
});
);
const isFormDisabled =
modalState === ModalStateEnum.WAITING || isAccountStatusLoading;

View File

@@ -2,11 +2,11 @@ import { useState } from 'react';
import { useMutation } from 'react-query';
import { Button, Modal } from 'antd';
import logEvent from 'api/common/logEvent';
import removeAwsIntegrationAccount from 'api/Integrations/removeAwsIntegrationAccount';
import removeAwsIntegrationAccount from 'api/integration/aws/removeAwsIntegrationAccount';
import { SOMETHING_WENT_WRONG } from 'constants/api';
import { INTEGRATION_TELEMETRY_EVENTS } from 'container/Integrations/constants';
import { useNotifications } from 'hooks/useNotifications';
import { X } from 'lucide-react';
import { INTEGRATION_TELEMETRY_EVENTS } from 'pages/Integrations/utils';
import './RemoveIntegrationAccount.scss';

View File

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

View File

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

View File

@@ -1,15 +1,16 @@
import { useEffect, useMemo, useState } from 'react';
import { Button, Tabs, TabsProps } from 'antd';
import CloudServiceDataCollected from 'components/CloudIntegrations/CloudServiceDataCollected/CloudServiceDataCollected';
import { MarkdownRenderer } from 'components/MarkdownRenderer/MarkdownRenderer';
import Spinner from 'components/Spinner';
import CloudServiceDashboards from 'container/CloudIntegrationPage/ServicesSection/CloudServiceDashboards';
import CloudServiceDataCollected from 'container/CloudIntegrationPage/ServicesSection/CloudServiceDataCollected';
import { IServiceStatus } from 'container/CloudIntegrationPage/ServicesSection/types';
import CloudServiceDashboards from 'container/Integrations/CloudIntegration/AmazonWebServices/CloudServiceDashboards';
import { AWSServiceConfig } from 'container/Integrations/CloudIntegration/AmazonWebServices/types';
import { IServiceStatus } from 'container/Integrations/types';
import dayjs from 'dayjs';
import { useServiceDetails } from 'hooks/integration/aws/useServiceDetails';
import useUrlQuery from 'hooks/useUrlQuery';
import logEvent from '../../../api/common/logEvent';
import logEvent from '../../../../api/common/logEvent';
import ConfigureServiceModal from './ConfigureServiceModal';
const getStatus = (
@@ -110,9 +111,10 @@ function ServiceDetails(): JSX.Element | null {
[config],
);
const awsConfig = config as AWSServiceConfig | undefined;
const isAnySignalConfigured = useMemo(
() => !!config?.logs?.enabled || !!config?.metrics?.enabled,
[config],
() => !!awsConfig?.logs?.enabled || !!awsConfig?.metrics?.enabled,
[awsConfig],
);
// log telemetry event on visiting details of a service.
@@ -179,7 +181,7 @@ function ServiceDetails(): JSX.Element | null {
serviceName={serviceDetailsData.title}
serviceId={serviceId || ''}
cloudAccountId={cloudAccountId || ''}
initialConfig={serviceDetailsData.config}
initialConfig={awsConfig}
supportedSignals={serviceDetailsData.supported_signals || {}}
/>
)}

View File

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

View File

@@ -1,13 +1,14 @@
import { useMemo, useState } from 'react';
import { useQuery } from 'react-query';
import { Color } from '@signozhq/design-tokens';
import type { SelectProps, TabsProps } from 'antd';
import { Select, Tabs } from 'antd';
import type { SelectProps } from 'antd';
import { Select } from 'antd';
import { getAwsServices } from 'api/integration/aws';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import useUrlQuery from 'hooks/useUrlQuery';
import { ChevronDown } from 'lucide-react';
import HeroSection from './HeroSection/HeroSection';
import ServiceDetails from './ServiceDetails';
import ServicesList from './ServicesList';
@@ -106,17 +107,10 @@ function ServicesSection(): JSX.Element {
}
function ServicesTabs(): JSX.Element {
const tabItems: TabsProps['items'] = [
{
key: 'services',
label: 'Services For Integration',
children: <ServicesSection />,
},
];
return (
<div className="services-tabs">
<Tabs defaultActiveKey="services" items={tabItems} />
<HeroSection />
<ServicesSection />
</div>
);
}

View File

@@ -1,90 +1,40 @@
import { ServiceData } from 'container/Integrations/types';
interface Service {
id: string;
title: string;
icon: string;
config: ServiceConfig;
}
interface Dashboard {
id: string;
url: string;
title: string;
description: string;
image: string;
}
interface LogField {
name: string;
path: string;
type: string;
}
interface Metric {
name: string;
type: string;
unit: string;
}
interface ConfigStatus {
enabled: boolean;
}
interface DataStatus {
last_received_ts_ms: number;
last_received_from: string;
config: AWSServiceConfig;
}
interface S3BucketsByRegion {
[region: string]: string[];
}
interface ConfigStatus {
enabled: boolean;
}
interface LogsConfig extends ConfigStatus {
s3_buckets?: S3BucketsByRegion;
}
interface ServiceConfig {
interface AWSServiceConfig {
logs: LogsConfig;
metrics: ConfigStatus;
s3_sync?: LogsConfig;
}
interface IServiceStatus {
logs: DataStatus | null;
metrics: DataStatus | null;
}
interface SupportedSignals {
metrics: boolean;
logs: boolean;
}
interface ServiceData {
id: string;
title: string;
icon: string;
overview: string;
supported_signals: SupportedSignals;
assets: {
dashboards: Dashboard[];
};
data_collected: {
logs?: LogField[];
metrics: Metric[];
};
config?: ServiceConfig;
status?: IServiceStatus;
}
interface ServiceDetailsResponse {
status: 'success';
data: ServiceData;
}
interface CloudAccountConfig {
export interface AWSCloudAccountConfig {
regions: string[];
}
interface IntegrationStatus {
export interface IntegrationStatus {
last_heartbeat_ts_ms: number;
}
@@ -95,7 +45,7 @@ interface AccountStatus {
interface CloudAccount {
id: string;
cloud_account_id: string;
config: CloudAccountConfig;
config: AWSCloudAccountConfig;
status: AccountStatus;
}
@@ -133,15 +83,13 @@ interface UpdateServiceConfigResponse {
}
export type {
AWSServiceConfig,
CloudAccount,
CloudAccountsData,
IServiceStatus,
S3BucketsByRegion,
Service,
ServiceConfig,
ServiceData,
ServiceDetailsResponse,
SupportedSignals,
UpdateServiceConfigPayload,
UpdateServiceConfigResponse,
};

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,192 @@
.azure-service-details-container {
display: flex;
flex-direction: column;
width: 100%;
.azure-service-details-tabs {
margin-top: 8px;
// remove the padding left from the first div of the tabs component
// this needs to be handled in the tabs component
> div:first-child {
padding-left: 0;
}
.azure-service-details-data-collected-content-logs,
.azure-service-details-data-collected-content-metrics {
display: flex;
flex-direction: row;
gap: 8px;
.azure-service-details-data-collected-content-title {
display: flex;
flex-direction: row;
align-items: center;
gap: 8px;
color: var(--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;
}
}
.azure-service-details-overview {
display: flex;
flex-direction: column;
gap: 16px;
.azure-service-details-overview-configuration {
display: flex;
flex-direction: column;
border-radius: 4px;
border: 1px solid var(--l3-background);
background: var(--l1-background);
.azure-service-details-overview-configuration-title {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
gap: 8px;
width: 100%;
border-radius: 4px 4px 0 0;
border-bottom: 1px solid var(--l3-background);
padding: 8px 12px;
.azure-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;
}
.configuration-action {
display: flex;
flex-direction: row;
align-items: center;
gap: 8px;
}
}
.azure-service-details-overview-configuration-content {
display: flex;
flex-direction: column;
gap: 16px;
padding: 12px;
background: var(--l1-background);
.azure-service-details-overview-configuration-content-item {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
gap: 8px;
}
}
.azure-service-details-overview-configuration-actions {
display: flex;
flex-direction: row;
justify-content: flex-end;
gap: 8px;
padding: 8px 12px;
border-top: 1px solid var(--l3-background);
background: var(--l1-background);
}
.azure-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% */
}
}
.azure-service-details-actions {
display: flex;
flex-direction: row;
gap: 8px;
padding: 12px 0;
}
.azure-service-dashboards {
border-radius: 6px;
border: 1px solid var(--l3-background);
background: var(--l1-background);
.azure-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);
}
.azure-service-dashboards-items {
display: flex;
flex-direction: column;
gap: 16px;
.azure-service-dashboard-item {
display: flex;
flex-direction: column;
gap: 8px;
padding: 12px 16px 12px 16px;
&.azure-service-dashboard-item-clickable {
cursor: pointer;
&:hover {
background-color: var(--l1-background);
}
}
.azure-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;
}
.azure-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,414 @@
import { useEffect, useState } from 'react';
import { useQueryClient } from 'react-query';
import { Button } from '@signozhq/button';
import Tabs from '@signozhq/tabs';
import { Checkbox, Skeleton } from 'antd';
import CloudServiceDataCollected from 'components/CloudIntegrations/CloudServiceDataCollected/CloudServiceDataCollected';
import { MarkdownRenderer } from 'components/MarkdownRenderer/MarkdownRenderer';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import { INTEGRATION_TYPES } from 'container/Integrations/constants';
import { AzureConfig, AzureService } from 'container/Integrations/types';
import { useGetCloudIntegrationServiceDetails } from 'hooks/integration/useServiceDetails';
import { useUpdateServiceConfig } from 'hooks/integration/useUpdateServiceConfig';
import { useSafeNavigate } from 'hooks/useSafeNavigate';
import { isEqual } from 'lodash-es';
import { Save, X } from 'lucide-react';
import './AzureServiceDetails.styles.scss';
interface AzureServiceDetailsProps {
selectedService: AzureService | null;
cloudAccountId: string;
}
function configToMap(
config: AzureConfig[] | undefined,
): { [key: string]: boolean } {
return (config || []).reduce(
(acc: { [key: string]: boolean }, item: AzureConfig) => {
acc[item.name] = item.enabled;
return acc;
},
{},
);
}
export default function AzureServiceDetails({
selectedService,
cloudAccountId,
}: AzureServiceDetailsProps): JSX.Element {
const queryClient = useQueryClient();
const { safeNavigate } = useSafeNavigate();
const {
data: serviceDetailsData,
isLoading,
refetch: refetchServiceDetails,
} = useGetCloudIntegrationServiceDetails(
INTEGRATION_TYPES.AZURE,
selectedService?.id || '',
cloudAccountId || undefined,
);
const {
mutate: updateAzureServiceConfig,
isLoading: isUpdating,
} = useUpdateServiceConfig();
// Last saved/committed config — updated when data loads and on save success.
// Used for hasChanges and Discard so buttons hide immediately after save.
const [lastSavedSnapshot, setLastSavedSnapshot] = useState<{
logs: { [key: string]: boolean };
metrics: { [key: string]: boolean };
}>({ logs: {}, metrics: {} });
// Editable state
const [azureLogsEnabledAll, setAzureLogsEnabledAll] = useState<boolean>(false);
const [azureMetricsEnabledAll, setAzureMetricsEnabledAll] = useState<boolean>(
false,
);
const [logsConfig, updateLogsConfig] = useState<{ [key: string]: boolean }>(
{},
);
const [metricsConfigs, updateMetricsConfigs] = useState<{
[key: string]: boolean;
}>({});
// Sync state when serviceDetailsData loads
useEffect(() => {
if (!serviceDetailsData?.config) {
return;
}
const logs = configToMap(serviceDetailsData.config.logs as AzureConfig[]);
const metrics = configToMap(
serviceDetailsData.config.metrics as AzureConfig[],
);
if (Object.keys(logs).length > 0) {
updateLogsConfig(logs);
setAzureLogsEnabledAll(
!(serviceDetailsData.config.logs as AzureConfig[])?.some(
(log: AzureConfig) => !log.enabled,
),
);
}
if (Object.keys(metrics).length > 0) {
updateMetricsConfigs(metrics);
setAzureMetricsEnabledAll(
!(serviceDetailsData.config.metrics as AzureConfig[])?.some(
(metric: AzureConfig) => !metric.enabled,
),
);
}
setLastSavedSnapshot({ logs, metrics });
}, [serviceDetailsData]);
const hasChanges =
!isEqual(logsConfig, lastSavedSnapshot.logs) ||
!isEqual(metricsConfigs, lastSavedSnapshot.metrics);
const handleSave = (): void => {
if (!selectedService?.id) {
return;
}
updateAzureServiceConfig(
{
cloudServiceId: INTEGRATION_TYPES.AZURE,
serviceId: selectedService?.id,
payload: {
cloud_account_id: cloudAccountId,
config: {
logs: Object.entries(logsConfig).map(([name, enabled]) => ({
name,
enabled,
})),
metrics: Object.entries(metricsConfigs).map(([name, enabled]) => ({
name,
enabled,
})),
},
},
},
{
onSuccess: (_, variables) => {
// Update snapshot immediately from what we saved (not current state)
const saved = variables.payload.config;
setLastSavedSnapshot({
logs: configToMap(saved.logs),
metrics: configToMap(saved.metrics),
});
queryClient.invalidateQueries([
REACT_QUERY_KEY.AWS_SERVICE_DETAILS,
selectedService?.id,
cloudAccountId,
]);
// Invalidate services list so Enabled/Not Enabled stays in sync
queryClient.invalidateQueries([INTEGRATION_TYPES.AZURE]);
refetchServiceDetails();
},
},
);
};
const handleDiscard = (): void => {
updateLogsConfig(lastSavedSnapshot.logs);
updateMetricsConfigs(lastSavedSnapshot.metrics);
setAzureLogsEnabledAll(
Object.values(lastSavedSnapshot.logs).every(Boolean) &&
Object.keys(lastSavedSnapshot.logs).length > 0,
);
setAzureMetricsEnabledAll(
Object.values(lastSavedSnapshot.metrics).every(Boolean) &&
Object.keys(lastSavedSnapshot.metrics).length > 0,
);
};
const handleAzureLogsEnableAllChange = (checked: boolean): void => {
setAzureLogsEnabledAll(checked);
updateLogsConfig((prev) =>
Object.fromEntries(Object.keys(prev).map((key) => [key, checked])),
);
};
const handleAzureMetricsEnableAllChange = (checked: boolean): void => {
setAzureMetricsEnabledAll(checked);
updateMetricsConfigs((prev) =>
Object.fromEntries(Object.keys(prev).map((key) => [key, checked])),
);
};
const handleAzureLogsEnabledChange = (
logName: string,
checked: boolean,
): void => {
updateLogsConfig((prev) => ({ ...prev, [logName]: checked }));
};
const handleAzureMetricsEnabledChange = (
metricName: string,
checked: boolean,
): void => {
updateMetricsConfigs((prev) => ({ ...prev, [metricName]: checked }));
};
// Keep "enable all" in sync when individual items change
useEffect(() => {
if (Object.keys(logsConfig).length > 0) {
const allEnabled = Object.values(logsConfig).every(Boolean);
setAzureLogsEnabledAll(allEnabled);
}
}, [logsConfig]);
useEffect(() => {
if (Object.keys(metricsConfigs).length > 0) {
const allEnabled = Object.values(metricsConfigs).every(Boolean);
setAzureMetricsEnabledAll(allEnabled);
}
}, [metricsConfigs]);
const renderOverview = (): JSX.Element => {
const dashboards = serviceDetailsData?.assets?.dashboards || [];
if (isLoading) {
return (
<div className="azure-service-details-overview-loading">
<Skeleton active />
</div>
);
}
return (
<div className="azure-service-details-overview">
{!isLoading && (
<div className="azure-service-details-overview-configuration">
<div className="azure-service-details-overview-configuration-logs">
<div className="azure-service-details-overview-configuration-title">
<div className="azure-service-details-overview-configuration-title-text">
Azure Logs
</div>
<div className="configuration-action">
<Checkbox
checked={azureLogsEnabledAll}
indeterminate={
Object.values(logsConfig).some(Boolean) &&
!Object.values(logsConfig).every(Boolean)
}
onChange={(e): void =>
handleAzureLogsEnableAllChange(e.target.checked)
}
disabled={isUpdating}
>
<span className="azure-service-details-overview-configuration-title-text-select-all">
Select All
</span>
</Checkbox>
</div>
</div>
<div className="azure-service-details-overview-configuration-content">
{logsConfig &&
Object.keys(logsConfig).length > 0 &&
Object.keys(logsConfig).map((logName: string) => (
<div
key={logName}
className="azure-service-details-overview-configuration-content-item"
>
<div className="azure-service-details-overview-configuration-content-item-text">
{logName}
</div>
<Checkbox
checked={logsConfig[logName]}
onChange={(e): void =>
handleAzureLogsEnabledChange(logName, e.target.checked)
}
disabled={isUpdating}
/>
</div>
))}
</div>
</div>
<div className="azure-service-details-overview-configuration-metrics">
<div className="azure-service-details-overview-configuration-title">
<div className="azure-service-details-overview-configuration-title-text">
Azure Metrics
</div>
<div className="configuration-action">
<Checkbox
checked={azureMetricsEnabledAll}
indeterminate={
Object.values(metricsConfigs).some(Boolean) &&
!Object.values(metricsConfigs).every(Boolean)
}
onChange={(e): void =>
handleAzureMetricsEnableAllChange(e.target.checked)
}
disabled={isUpdating}
>
<span className="azure-service-details-overview-configuration-title-text-select-all">
Select All
</span>
</Checkbox>
</div>
</div>
<div className="azure-service-details-overview-configuration-content">
{metricsConfigs &&
Object.keys(metricsConfigs).length > 0 &&
Object.keys(metricsConfigs).map((metricName: string) => (
<div
key={metricName}
className="azure-service-details-overview-configuration-content-item"
>
<div className="azure-service-details-overview-configuration-content-item-text">
{metricName}
</div>
<Checkbox
checked={metricsConfigs[metricName]}
onChange={(e): void =>
handleAzureMetricsEnabledChange(metricName, e.target.checked)
}
disabled={isUpdating}
/>
</div>
))}
</div>
</div>
{hasChanges && (
<div className="azure-service-details-overview-configuration-actions">
<Button
variant="solid"
color="secondary"
onClick={handleDiscard}
disabled={isUpdating}
size="xs"
prefixIcon={<X size={14} />}
>
Discard
</Button>
<Button
variant="solid"
color="primary"
onClick={handleSave}
loading={isUpdating}
disabled={isUpdating}
size="xs"
prefixIcon={<Save size={14} />}
>
Save
</Button>
</div>
)}
</div>
)}
<MarkdownRenderer
variables={{}}
markdownContent={serviceDetailsData?.overview}
/>
<div className="azure-service-dashboards">
<div className="azure-service-dashboards-title">Dashboards</div>
<div className="azure-service-dashboards-items">
{dashboards.map((dashboard) => (
<div
key={dashboard.id}
className={`azure-service-dashboard-item ${
dashboard.url ? 'azure-service-dashboard-item-clickable' : ''
}`}
onClick={(): void => {
if (dashboard.url) {
safeNavigate(dashboard.url);
}
}}
>
<div className="azure-service-dashboard-item-title">
{dashboard.title}
</div>
<div className="azure-service-dashboard-item-description">
{dashboard.description}
</div>
</div>
))}
</div>
</div>
</div>
);
};
const renderDataCollected = (): JSX.Element => {
return (
<div className="azure-service-details-data-collected-table">
<CloudServiceDataCollected
logsData={serviceDetailsData?.data_collected?.logs || []}
metricsData={serviceDetailsData?.data_collected?.metrics || []}
/>
</div>
);
};
return (
<div className="azure-service-details-container">
<Tabs
defaultValue="overview"
className="azure-service-details-tabs"
items={[
{
children: renderOverview(),
key: 'overview',
label: 'Overview',
},
{
children: renderDataCollected(),
key: 'data-collected',
label: 'Data Collected',
},
]}
variant="secondary"
/>
</div>
);
}

View File

@@ -0,0 +1,182 @@
.azure-services-container {
display: flex;
flex-direction: column;
height: calc(100% - 52px);
position: relative;
.azure-services-content {
height: 100%;
position: relative;
.azure-services-list-header {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
padding: 6px 10px;
height: 40px;
border-bottom: 1px solid var(--bg-slate-400);
.azure-services-views-btn-group {
border: 1px solid var(--bg-slate-400);
border-radius: 4px;
display: flex;
align-items: center;
.azure-services-views-btn {
&:first-child {
border-radius: 4px 0px 0px 4px;
}
&:last-child {
border-radius: 0px 4px 4px 0px;
}
&:hover {
background-color: var(--bg-slate-400);
}
&.active {
background-color: var(--bg-slate-400);
}
}
}
}
.azure-services-list-section {
height: 100%;
position: relative;
.azure-services-list-section-content {
display: flex;
flex-direction: row;
width: 100%;
height: 100%;
}
.azure-services-list-section-loading-skeleton {
display: flex;
flex-direction: row;
width: 100%;
.azure-services-list-section-loading-skeleton-sidebar {
width: 240px;
padding: 12px;
display: flex;
flex-direction: column;
gap: 24px;
}
.azure-services-list-section-loading-skeleton-main {
flex: 1;
padding: 12px;
display: flex;
flex-direction: column;
gap: 24px;
}
}
.azure-services-list-view {
height: 100%;
.azure-services-list-view-sidebar {
width: 240px;
height: 100%;
border-right: 1px solid var(--l3-background);
padding: 12px;
.azure-services-list-view-sidebar-content {
display: flex;
flex-direction: column;
gap: 8px;
.azure-services-enabled {
display: flex;
flex-direction: column;
gap: 8px;
}
.azure-services-not-enabled {
display: flex;
flex-direction: column;
gap: 8px;
}
.azure-services-list-view-sidebar-content-header {
color: var(--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;
}
.azure-services-list-view-sidebar-content-item {
display: flex;
flex-direction: row;
align-items: center;
gap: 10px;
padding: 4px 12px;
border-radius: 4px;
cursor: pointer;
.azure-services-list-view-sidebar-content-item-icon {
width: 20px;
height: 20px;
}
.azure-services-list-view-sidebar-content-item-title {
color: var(--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);
.azure-services-list-view-sidebar-content-item-title {
color: var(--l1-foreground);
}
}
}
}
}
.azure-services-list-view-main {
flex: 1;
padding: 12px;
}
}
.azure-services-grid-view {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
}
}
}
}

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,20 @@
import { IntegrationType } from 'container/Integrations/types';
import AWSTabs from './AmazonWebServices/ServicesTabs';
import AzureServices from './AzureServices';
import Header from './Header/Header';
import './CloudIntegration.styles.scss';
const CloudIntegration = ({ type }: { type: IntegrationType }): JSX.Element => {
return (
<div className="cloud-integration-container">
<Header title={type} />
{type === IntegrationType.AWS_SERVICES && <AWSTabs />}
{type === IntegrationType.AZURE_SERVICES && <AzureServices />}
</div>
);
};
export default CloudIntegration;

View File

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

View File

@@ -1,11 +1,13 @@
import { Link } from 'react-router-dom';
import { Button } from '@signozhq/button';
import Breadcrumb from 'antd/es/breadcrumb';
import ROUTES from 'constants/routes';
import { IntegrationType } from 'container/Integrations/types';
import { Blocks, LifeBuoy } from 'lucide-react';
import './Header.styles.scss';
function Header(): JSX.Element {
function Header({ title }: { title: IntegrationType }): JSX.Element {
return (
<div className="cloud-header">
<div className="cloud-header__navigation">
@@ -23,25 +25,26 @@ function Header(): JSX.Element {
),
},
{
title: (
<div className="cloud-header__breadcrumb-title">
Amazon Web Services
</div>
),
title: <div className="cloud-header__breadcrumb-title">{title}</div>,
},
]}
/>
</div>
<div className="cloud-header__actions">
<a
href="https://signoz.io/blog/native-aws-integrations-with-autodiscovery/"
target="_blank"
rel="noopener noreferrer"
className="cloud-header__help"
<Button
variant="solid"
size="sm"
color="secondary"
onClick={(): void => {
window.open(
'https://signoz.io/blog/native-aws-integrations-with-autodiscovery/',
'_blank',
);
}}
prefixIcon={<LifeBuoy size={12} />}
>
<LifeBuoy size={12} />
Get Help
</a>
</Button>
</div>
</div>
);

View File

@@ -1,14 +1,12 @@
import { I18nextProvider } from 'react-i18next';
import { act, fireEvent, render, screen } from '@testing-library/react';
import { RequestIntegrationBtn } from 'container/Integrations/RequestIntegrationBtn';
import { IntegrationType } from 'container/Integrations/types';
import { server } from 'mocks-server/server';
import { rest } from 'msw';
import {
IntegrationType,
RequestIntegrationBtn,
} from 'pages/Integrations/RequestIntegrationBtn';
import i18n from 'ReactI18';
describe('Request AWS integration', () => {
describe.skip('Request AWS integration', () => {
it('should render the request integration button', async () => {
let capturedPayload: any;
server.use(

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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