mirror of
https://github.com/SigNoz/signoz.git
synced 2026-02-28 19:22:26 +00:00
Compare commits
3 Commits
feat/azure
...
refactor/c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
40288776e8 | ||
|
|
df30852296 | ||
|
|
b9beabb425 |
126
docs/contributing/go/packages.md
Normal file
126
docs/contributing/go/packages.md
Normal file
@@ -0,0 +1,126 @@
|
||||
# Packages
|
||||
|
||||
All shared Go code in SigNoz lives under `pkg/`. Each package represents a distinct domain concept and exposes a clear public interface. This guide covers the conventions for creating, naming, and organising packages so the codebase stays consistent as it grows.
|
||||
|
||||
## How should I name a package?
|
||||
|
||||
Use short, lowercase, single-word names. No underscores or camelCase (`querier`, `cache`, `authz`, not `query_builder` or `dataStore`).
|
||||
|
||||
Names must be **domain-specific**. A package name should tell you what problem domain it deals with, not what data structure it wraps. Prefer `alertmanager` over `manager`, `licensing` over `checker`.
|
||||
|
||||
Avoid generic names like `util`, `helpers`, `common`, `misc`, or `base`. If you can't name it, the code probably belongs in an existing package.
|
||||
|
||||
## When should I create a new package?
|
||||
|
||||
Create a new package when:
|
||||
|
||||
- The functionality represents a **distinct domain concept** (e.g., `authz`, `licensing`, `cache`).
|
||||
- Two or more other packages would import it; it serves as shared infrastructure.
|
||||
- The code has a clear public interface that can stand on its own.
|
||||
|
||||
Do **not** create a new package when:
|
||||
|
||||
- There is already a package that covers the same domain. Extend the existing package instead.
|
||||
- The code is only used in one place. Keep it local to the caller.
|
||||
- You are splitting purely for file size. Use multiple files within the same package instead.
|
||||
|
||||
## How should I lay out a package?
|
||||
|
||||
A typical package looks like:
|
||||
|
||||
```
|
||||
pkg/cache/
|
||||
├── cache.go # Public interface + exported types
|
||||
├── config.go # Configuration types if needed
|
||||
├── memorycache/ # Implementation sub-package
|
||||
├── rediscache/ # Another implementation
|
||||
└── cachetest/ # Test helpers for consumers
|
||||
```
|
||||
|
||||
Follow these rules:
|
||||
|
||||
1. **Interface-first file**: The file matching the package name (e.g., `cache.go` in `pkg/cache/`) should define the public interface and core exported types. Keep implementation details out of this file.
|
||||
|
||||
2. **One responsibility per file**: Name files after what they contain (`config.go`, `handler.go`, `service.go`), not after the package name. If a package merges two concerns, prefix files to group them (e.g., `memory_store.go`, `redis_store.go` in a storage package).
|
||||
|
||||
3. **Sub-packages for implementations**: When a package defines an interface with multiple implementations, put each implementation in its own sub-package (`memorycache/`, `rediscache/`). This keeps the parent package import-free of implementation dependencies.
|
||||
|
||||
4. **Test helpers in `{pkg}test/`**: If consumers need test mocks or builders, put them in a `{pkg}test/` sub-package (e.g., `cachetest/`, `sqlstoretest/`). This avoids polluting the main package with test-only code.
|
||||
|
||||
5. **Test files stay alongside source**: Unit tests go in `_test.go` files next to the code they test, in the same package.
|
||||
|
||||
## How should I name symbols?
|
||||
|
||||
### Exported symbols
|
||||
|
||||
- **Interfaces**: For single-method interfaces, follow the standard `-er` suffix convention (`Reader`, `Writer`, `Closer`). For multi-method interfaces, use clear nouns (`Cache`, `Store`, `Provider`).
|
||||
- **Constructors**: `New<Type>(...)` (e.g., `NewMemoryCache()`).
|
||||
- **Avoid stutter**: Since callers qualify with the package name, don't repeat it. Write `cache.Cache`, not `cache.CacheInterface`. Write `authz.FromRole`, not `authz.AuthzFromRole`.
|
||||
|
||||
### Unexported symbols
|
||||
|
||||
- Struct receivers: one or two characters (`c`, `f`, `br`).
|
||||
- Helper functions: descriptive lowercase names (`parseToken`, `buildQuery`).
|
||||
|
||||
### Constants
|
||||
|
||||
- Use `PascalCase` for exported constants.
|
||||
- When merging files from different origins into one package, watch out for **name collisions** across files. Prefix to disambiguate when two types share a natural name.
|
||||
|
||||
## How should I organise imports?
|
||||
|
||||
Group imports in three blocks separated by blank lines:
|
||||
|
||||
```go
|
||||
import (
|
||||
// 1. Standard library
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
// 2. External dependencies
|
||||
"github.com/gorilla/mux"
|
||||
|
||||
// 3. Internal
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/types"
|
||||
)
|
||||
```
|
||||
|
||||
Never introduce circular imports. If package A needs package B and B needs A, extract the shared types into a third package (often under `pkg/types/`).
|
||||
|
||||
## Where do shared types go?
|
||||
|
||||
Most types belong in `pkg/types/` under a domain-specific sub-package (e.g., `pkg/types/ruletypes`, `pkg/types/authtypes`).
|
||||
|
||||
Do not put domain logic in `pkg/types/`. Only data structures, constants, and simple methods.
|
||||
|
||||
## How do I merge or move packages?
|
||||
|
||||
When two packages are tightly coupled (one imports the other's constants, they cover the same domain), merge them:
|
||||
|
||||
1. Pick a domain-specific name for the combined package.
|
||||
2. Prefix files to preserve origin (e.g., `memory_store.go`, `redis_store.go`).
|
||||
3. Resolve symbol conflicts explicitly; rename with a prefix rather than silently shadowing.
|
||||
4. Update all consumers in a single change.
|
||||
5. Delete the old packages. Do not leave behind re-export shims.
|
||||
6. Verify with `go build ./...`, `go test ./<new-pkg>/...`, and `go vet ./...`.
|
||||
|
||||
## When should I add documentation?
|
||||
|
||||
Add a `doc.go` with a package-level comment for any package that is non-trivial or has multiple consumers. Keep it to 1–3 sentences:
|
||||
|
||||
```go
|
||||
// Package cache provides a caching interface with pluggable backends
|
||||
// for in-memory and Redis-based storage.
|
||||
package cache
|
||||
```
|
||||
|
||||
## What should I remember?
|
||||
|
||||
- Package names are domain-specific and lowercase. Never generic names like `util` or `common`.
|
||||
- The file matching the package name (e.g., `cache.go`) defines the public interface. Implementation details go elsewhere.
|
||||
- Never introduce circular imports. Extract shared types into `pkg/types/` when needed.
|
||||
- Watch for symbol name collisions when merging packages, prefix to disambiguate.
|
||||
- Put test helpers in a `{pkg}test/` sub-package, not in the main package.
|
||||
- Before submitting, verify with `go build ./...`, `go test ./<your-pkg>/...`, and `go vet ./...`.
|
||||
- Update all consumers when you rename or move symbols.
|
||||
@@ -8,4 +8,13 @@ We adhere to three primary style guides as our foundation:
|
||||
- [Code Review Comments](https://go.dev/wiki/CodeReviewComments) - For understanding common comments in code reviews
|
||||
- [Google Style Guide](https://google.github.io/styleguide/go/) - Additional practices from Google
|
||||
|
||||
We **recommend** (almost enforce) reviewing these guides before contributing to the codebase. They provide valuable insights into writing idiomatic Go code and will help you understand our approach to backend development. In addition, we have a few additional rules that make certain areas stricter than the above which can be found in area-specific files in this package.
|
||||
We **recommend** (almost enforce) reviewing these guides before contributing to the codebase. They provide valuable insights into writing idiomatic Go code and will help you understand our approach to backend development. In addition, we have a few additional rules that make certain areas stricter than the above which can be found in area-specific files in this package:
|
||||
|
||||
- [Packages](packages.md) — Naming, layout, and conventions for `pkg/` packages
|
||||
- [Errors](errors.md) — Structured error handling
|
||||
- [Handler](handler.md) — Writing HTTP handlers and OpenAPI integration
|
||||
- [Endpoint](endpoint.md) — Endpoint conventions
|
||||
- [SQL](sql.md) — Database query patterns
|
||||
- [Provider](provider.md) — Provider pattern
|
||||
- [Integration](integration.md) — Integration conventions
|
||||
- [Flagger](flagger.md) — Feature flag conventions
|
||||
|
||||
@@ -1,77 +0,0 @@
|
||||
---
|
||||
description: Global vs local mock strategy for Jest tests
|
||||
globs:
|
||||
- "**/*.test.ts"
|
||||
- "**/*.test.tsx"
|
||||
- "**/__tests__/**/*.ts"
|
||||
- "**/__tests__/**/*.tsx"
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
# Mock Decision Strategy
|
||||
|
||||
## Global Mocks (20+ test files)
|
||||
|
||||
- Core infrastructure: react-router-dom, react-query, antd
|
||||
- Browser APIs: ResizeObserver, matchMedia, localStorage
|
||||
- Utility libraries: date-fns, lodash
|
||||
- Available: `uplot` → `__mocks__/uplotMock.ts`
|
||||
|
||||
## Local Mocks (5–15 test files)
|
||||
|
||||
- Business logic dependencies
|
||||
- API endpoints with specific responses
|
||||
- Domain-specific components
|
||||
- Error scenarios and edge cases
|
||||
|
||||
## Decision Tree
|
||||
|
||||
```
|
||||
Is it used in 20+ test files?
|
||||
├─ YES → Use Global Mock
|
||||
│ ├─ react-router-dom
|
||||
│ ├─ react-query
|
||||
│ ├─ antd components
|
||||
│ └─ browser APIs
|
||||
│
|
||||
└─ NO → Is it business logic?
|
||||
├─ YES → Use Local Mock
|
||||
│ ├─ API endpoints
|
||||
│ ├─ Custom hooks
|
||||
│ └─ Domain components
|
||||
│
|
||||
└─ NO → Is it test-specific?
|
||||
├─ YES → Use Local Mock
|
||||
│ ├─ Error scenarios
|
||||
│ ├─ Loading states
|
||||
│ └─ Specific data
|
||||
│
|
||||
└─ NO → Consider Global Mock
|
||||
└─ If it becomes frequently used
|
||||
```
|
||||
|
||||
## Anti-patterns
|
||||
|
||||
❌ Don't mock global dependencies locally:
|
||||
```ts
|
||||
jest.mock('react-router-dom', () => ({ ... })); // Already globally mocked
|
||||
```
|
||||
|
||||
❌ Don't create global mocks for test-specific data:
|
||||
```ts
|
||||
jest.mock('../api/tracesService', () => ({
|
||||
getTraces: jest.fn(() => specificTestData) // BAD - should be local
|
||||
}));
|
||||
```
|
||||
|
||||
✅ Do use global mocks for infrastructure:
|
||||
```ts
|
||||
import { useLocation } from 'react-router-dom';
|
||||
```
|
||||
|
||||
✅ Do create local mocks for business logic:
|
||||
```ts
|
||||
jest.mock('../api/tracesService', () => ({
|
||||
getTraces: jest.fn(() => mockTracesData)
|
||||
}));
|
||||
```
|
||||
@@ -1,124 +0,0 @@
|
||||
---
|
||||
description: Core Jest/React Testing Library conventions - harness, MSW, interactions, timers
|
||||
globs:
|
||||
- "**/*.test.ts"
|
||||
- "**/*.test.tsx"
|
||||
- "**/__tests__/**/*.ts"
|
||||
- "**/__tests__/**/*.tsx"
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
# Jest Test Conventions
|
||||
|
||||
Expert developer with Jest, React Testing Library, MSW, and TypeScript. Focus on critical functionality, mock dependencies before imports, test multiple scenarios, write maintainable tests.
|
||||
|
||||
**Auto-detect TypeScript**: Check for TypeScript in the project through tsconfig.json or package.json dependencies. Adjust syntax based on this detection.
|
||||
|
||||
## Imports
|
||||
|
||||
Always import from our harness:
|
||||
```ts
|
||||
import { render, screen, userEvent, waitFor } from 'tests/test-utils';
|
||||
```
|
||||
For API mocks:
|
||||
```ts
|
||||
import { server, rest } from 'mocks-server/server';
|
||||
```
|
||||
❌ Do not import directly from `@testing-library/react`.
|
||||
|
||||
## Router
|
||||
|
||||
Use the router built into render:
|
||||
```ts
|
||||
render(<Page />, undefined, { initialRoute: '/traces-explorer' });
|
||||
```
|
||||
Only mock `useLocation` / `useParams` if the test depends on them.
|
||||
|
||||
## Hook Mocks
|
||||
|
||||
```ts
|
||||
import useFoo from 'hooks/useFoo';
|
||||
jest.mock('hooks/useFoo');
|
||||
const mockUseFoo = jest.mocked(useFoo);
|
||||
mockUseFoo.mockReturnValue(/* minimal shape */ as any);
|
||||
```
|
||||
Prefer helpers (`rqSuccess`, `rqLoading`, `rqError`) for React Query results.
|
||||
|
||||
## MSW
|
||||
|
||||
Global MSW server runs automatically. Override per-test:
|
||||
```ts
|
||||
server.use(
|
||||
rest.get('*/api/v1/foo', (_req, res, ctx) => res(ctx.status(200), ctx.json({ ok: true })))
|
||||
);
|
||||
```
|
||||
Keep large responses in `mocks-server/__mockdata__/`.
|
||||
|
||||
## Interactions
|
||||
|
||||
- Prefer `userEvent` for real user interactions (click, type, select, tab).
|
||||
- Use `fireEvent` only for low-level/programmatic events not covered by `userEvent` (e.g., scroll, resize, setting `element.scrollTop` for virtualization). Wrap in `act(...)` if needed.
|
||||
- Always await interactions:
|
||||
```ts
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
await user.click(screen.getByRole('button', { name: /save/i }));
|
||||
```
|
||||
|
||||
```ts
|
||||
// Example: virtualized list scroll (no userEvent helper)
|
||||
const scroller = container.querySelector('[data-test-id="virtuoso-scroller"]') as HTMLElement;
|
||||
scroller.scrollTop = targetScrollTop;
|
||||
act(() => { fireEvent.scroll(scroller); });
|
||||
```
|
||||
|
||||
## Timers
|
||||
|
||||
❌ No global fake timers. ✅ Per-test only:
|
||||
```ts
|
||||
jest.useFakeTimers();
|
||||
const user = userEvent.setup({ advanceTimers: (ms) => jest.advanceTimersByTime(ms) });
|
||||
await user.type(screen.getByRole('textbox'), 'query');
|
||||
jest.advanceTimersByTime(400);
|
||||
jest.useRealTimers();
|
||||
```
|
||||
|
||||
## Queries
|
||||
|
||||
Prefer accessible queries (`getByRole`, `findByRole`, `getByLabelText`). Fallback: visible text. Last resort: `data-testid`.
|
||||
|
||||
## Best Practices
|
||||
|
||||
- **Critical Functionality**: Prioritize testing business logic and utilities
|
||||
- **Dependency Mocking**: Global mocks for infra, local mocks for business logic
|
||||
- **Data Scenarios**: Always test valid, invalid, and edge cases
|
||||
- **Descriptive Names**: Make test intent clear
|
||||
- **Organization**: Group related tests in describe
|
||||
- **Consistency**: Match repo conventions
|
||||
- **Edge Cases**: Test null, undefined, unexpected values
|
||||
- **Limit Scope**: 3–5 focused tests per file
|
||||
- **Use Helpers**: `rqSuccess`, `makeUser`, etc.
|
||||
- **No Any**: Enforce type safety
|
||||
|
||||
## Anti-patterns
|
||||
|
||||
❌ Importing RTL directly | ❌ Global fake timers | ❌ Wrapping render in `act(...)` | ❌ Mocking infra locally
|
||||
✅ Use harness | ✅ MSW for API | ✅ userEvent + await | ✅ Pin time only for relative-date tests
|
||||
|
||||
## Example
|
||||
|
||||
```ts
|
||||
import { render, screen, userEvent, waitFor } from 'tests/test-utils';
|
||||
import { server, rest } from 'mocks-server/server';
|
||||
import MyComponent from '../MyComponent';
|
||||
|
||||
describe('MyComponent', () => {
|
||||
it('renders and interacts', async () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
server.use(rest.get('*/api/v1/example', (_req, res, ctx) => res(ctx.status(200), ctx.json({ value: 42 }))));
|
||||
render(<MyComponent />, undefined, { initialRoute: '/foo' });
|
||||
expect(await screen.findByText(/value: 42/i)).toBeInTheDocument();
|
||||
await user.click(screen.getByRole('button', { name: /refresh/i }));
|
||||
await waitFor(() => expect(screen.getByText(/loading/i)).toBeInTheDocument());
|
||||
});
|
||||
});
|
||||
```
|
||||
@@ -1,168 +0,0 @@
|
||||
---
|
||||
description: TypeScript type safety for Jest tests - mocks, interfaces, no any
|
||||
globs:
|
||||
- "**/*.test.ts"
|
||||
- "**/*.test.tsx"
|
||||
- "**/__tests__/**/*.ts"
|
||||
- "**/__tests__/**/*.tsx"
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
# TypeScript Type Safety for Jest Tests
|
||||
|
||||
**CRITICAL**: All Jest tests MUST be fully type-safe.
|
||||
|
||||
## Requirements
|
||||
|
||||
- Use proper TypeScript interfaces for all mock data
|
||||
- Type all Jest mock functions with `jest.MockedFunction<T>`
|
||||
- Use generic types for React components and hooks
|
||||
- Define proper return types for mock functions
|
||||
- Use `as const` for literal types when needed
|
||||
- Avoid `any` type – use proper typing instead
|
||||
|
||||
## Mock Function Typing
|
||||
|
||||
```ts
|
||||
// ✅ GOOD
|
||||
const mockFetchUser = jest.fn() as jest.MockedFunction<(id: number) => Promise<ApiResponse<User>>>;
|
||||
const mockEventHandler = jest.fn() as jest.MockedFunction<(event: Event) => void>;
|
||||
|
||||
// ❌ BAD
|
||||
const mockFetchUser = jest.fn() as any;
|
||||
```
|
||||
|
||||
## Mock Data with Interfaces
|
||||
|
||||
```ts
|
||||
interface User { id: number; name: string; email: string; }
|
||||
interface ApiResponse<T> { data: T; status: number; message: string; }
|
||||
|
||||
const mockUser: User = { id: 1, name: 'John Doe', email: 'john@example.com' };
|
||||
mockFetchUser.mockResolvedValue({ data: mockUser, status: 200, message: 'Success' });
|
||||
```
|
||||
|
||||
## Component Props Typing
|
||||
|
||||
```ts
|
||||
interface ComponentProps { title: string; data: User[]; onUserSelect: (user: User) => void; }
|
||||
|
||||
const mockProps: ComponentProps = {
|
||||
title: 'Test',
|
||||
data: [{ id: 1, name: 'John', email: 'john@example.com' }],
|
||||
onUserSelect: jest.fn() as jest.MockedFunction<(user: User) => void>,
|
||||
};
|
||||
render(<TestComponent {...mockProps} />);
|
||||
```
|
||||
|
||||
## Hook Testing with Types
|
||||
|
||||
```ts
|
||||
interface UseUserDataReturn {
|
||||
user: User | null;
|
||||
loading: boolean;
|
||||
error: string | null;
|
||||
refetch: () => void;
|
||||
}
|
||||
|
||||
describe('useUserData', () => {
|
||||
it('should return user data with proper typing', () => {
|
||||
const mockUser: User = { id: 1, name: 'John', email: 'john@example.com' };
|
||||
mockFetchUser.mockResolvedValue({ data: mockUser, status: 200, message: 'Success' });
|
||||
const { result } = renderHook(() => useUserData(1));
|
||||
expect(result.current.user).toEqual(mockUser);
|
||||
expect(result.current.loading).toBe(false);
|
||||
expect(result.current.error).toBeNull();
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## Generic Mock Typing
|
||||
|
||||
```ts
|
||||
interface MockApiResponse<T> { data: T; status: number; }
|
||||
|
||||
const mockFetchData = jest.fn() as jest.MockedFunction<
|
||||
<T>(endpoint: string) => Promise<MockApiResponse<T>>
|
||||
>;
|
||||
mockFetchData<User>('/users').mockResolvedValue({ data: { id: 1, name: 'John' }, status: 200 });
|
||||
```
|
||||
|
||||
## React Testing Library with Types
|
||||
|
||||
```ts
|
||||
type TestComponentProps = ComponentProps<typeof TestComponent>;
|
||||
|
||||
const renderTestComponent = (props: Partial<TestComponentProps> = {}): RenderResult => {
|
||||
const defaultProps: TestComponentProps = { title: 'Test', data: [], onSelect: jest.fn(), ...props };
|
||||
return render(<TestComponent {...defaultProps} />);
|
||||
};
|
||||
```
|
||||
|
||||
## Error Handling with Types
|
||||
|
||||
```ts
|
||||
interface ApiError { message: string; code: number; details?: Record<string, unknown>; }
|
||||
const mockApiError: ApiError = { message: 'API Error', code: 500, details: { endpoint: '/users' } };
|
||||
mockFetchUser.mockRejectedValue(new Error(JSON.stringify(mockApiError)));
|
||||
```
|
||||
|
||||
## Global Mock Type Safety
|
||||
|
||||
```ts
|
||||
// In __mocks__/routerMock.ts
|
||||
export const mockUseLocation = (overrides: Partial<Location> = {}): Location => ({
|
||||
pathname: '/traces',
|
||||
search: '',
|
||||
hash: '',
|
||||
state: null,
|
||||
key: 'test-key',
|
||||
...overrides,
|
||||
});
|
||||
// In test files: const location = useLocation(); // Properly typed from global mock
|
||||
```
|
||||
|
||||
## TypeScript Configuration for Jest
|
||||
|
||||
```json
|
||||
// jest.config.ts
|
||||
{
|
||||
"preset": "ts-jest/presets/js-with-ts-esm",
|
||||
"globals": {
|
||||
"ts-jest": {
|
||||
"useESM": true,
|
||||
"isolatedModules": true,
|
||||
"tsconfig": "<rootDir>/tsconfig.jest.json"
|
||||
}
|
||||
},
|
||||
"extensionsToTreatAsEsm": [".ts", ".tsx"],
|
||||
"moduleFileExtensions": ["ts", "tsx", "js", "json"]
|
||||
}
|
||||
```
|
||||
|
||||
```json
|
||||
// tsconfig.jest.json
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"types": ["jest", "@testing-library/jest-dom"],
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"moduleResolution": "node"
|
||||
},
|
||||
"include": ["src/**/*", "**/*.test.ts", "**/*.test.tsx", "__mocks__/**/*"]
|
||||
}
|
||||
```
|
||||
|
||||
## Type Safety Checklist
|
||||
|
||||
- [ ] All mock functions use `jest.MockedFunction<T>`
|
||||
- [ ] All mock data has proper interfaces
|
||||
- [ ] No `any` types in test files
|
||||
- [ ] Generic types are used where appropriate
|
||||
- [ ] Error types are properly defined
|
||||
- [ ] Component props are typed
|
||||
- [ ] Hook return types are defined
|
||||
- [ ] API response types are defined
|
||||
- [ ] Global mocks are type-safe
|
||||
- [ ] Test utilities are properly typed
|
||||
@@ -52,8 +52,6 @@
|
||||
"@signozhq/combobox": "0.0.2",
|
||||
"@signozhq/command": "0.0.0",
|
||||
"@signozhq/design-tokens": "2.1.1",
|
||||
"@signozhq/dialog": "0.0.2",
|
||||
"@signozhq/drawer": "0.0.4",
|
||||
"@signozhq/icons": "0.1.0",
|
||||
"@signozhq/input": "0.0.2",
|
||||
"@signozhq/popover": "0.0.0",
|
||||
@@ -62,12 +60,10 @@
|
||||
"@signozhq/sonner": "0.1.0",
|
||||
"@signozhq/switch": "0.0.2",
|
||||
"@signozhq/table": "0.3.7",
|
||||
"@signozhq/tabs": "0.0.11",
|
||||
"@signozhq/tooltip": "0.0.2",
|
||||
"@tanstack/react-table": "8.20.6",
|
||||
"@tanstack/react-virtual": "3.11.2",
|
||||
"@uiw/codemirror-theme-copilot": "4.23.11",
|
||||
"@uiw/codemirror-theme-dracula": "4.25.4",
|
||||
"@uiw/codemirror-theme-github": "4.24.1",
|
||||
"@uiw/react-codemirror": "4.23.10",
|
||||
"@uiw/react-md-editor": "3.23.5",
|
||||
@@ -139,7 +135,6 @@
|
||||
"react-full-screen": "1.1.1",
|
||||
"react-grid-layout": "^1.3.4",
|
||||
"react-helmet-async": "1.3.0",
|
||||
"react-hook-form": "7.40.0",
|
||||
"react-i18next": "^11.16.1",
|
||||
"react-lottie": "1.2.10",
|
||||
"react-markdown": "8.0.7",
|
||||
@@ -294,4 +289,4 @@
|
||||
"on-headers": "^1.1.0",
|
||||
"tmp": "0.2.4"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,312 +0,0 @@
|
||||
<svg width="929" height="8" viewBox="0 0 929 8" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="12" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="18" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="24" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="30" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="36" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="42" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="48" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="54" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="60" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="66" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="72" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="78" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="84" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="90" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="96" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="102" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="108" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="114" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="120" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="126" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="132" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="138" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="144" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="150" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="156" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="162" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="168" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="174" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="180" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="186" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="192" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="198" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="204" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="210" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="216" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="222" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="228" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="234" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="240" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="246" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="252" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="258" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="264" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="270" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="276" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="282" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="288" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="294" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="300" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="306" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="312" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="318" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="324" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="330" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="336" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="342" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="348" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="354" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="360" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="366" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="372" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="378" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="384" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="390" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="396" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="402" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="408" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="414" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="420" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="426" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="432" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="438" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="444" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="450" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="456" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="462" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="468" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="474" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="480" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="486" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="492" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="498" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="504" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="510" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="516" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="522" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="528" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="534" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="540" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="546" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="552" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="558" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="564" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="570" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="576" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="582" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="588" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="594" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="600" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="606" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="612" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="618" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="624" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="630" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="636" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="642" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="648" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="654" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="660" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="666" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="672" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="678" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="684" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="690" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="696" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="702" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="708" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="714" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="720" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="726" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="732" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="738" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="744" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="750" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="756" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="762" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="768" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="774" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="780" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="786" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="792" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="798" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="804" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="810" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="816" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="822" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="828" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="834" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="840" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="846" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="852" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="858" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="864" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="870" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="876" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="882" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="888" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="894" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="900" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="906" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="912" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="918" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="924" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="6" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="12" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="18" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="24" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="30" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="36" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="42" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="48" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="54" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="60" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="66" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="72" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="78" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="84" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="90" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="96" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="102" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="108" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="114" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="120" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="126" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="132" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="138" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="144" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="150" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="156" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="162" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="168" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="174" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="180" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="186" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="192" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="198" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="204" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="210" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="216" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="222" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="228" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="234" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="240" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="246" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="252" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="258" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="264" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="270" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="276" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="282" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="288" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="294" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="300" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="306" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="312" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="318" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="324" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="330" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="336" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="342" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="348" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="354" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="360" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="366" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="372" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="378" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="384" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="390" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="396" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="402" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="408" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="414" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="420" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="426" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="432" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="438" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="444" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="450" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="456" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="462" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="468" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="474" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="480" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="486" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="492" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="498" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="504" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="510" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="516" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="522" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="528" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="534" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="540" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="546" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="552" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="558" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="564" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="570" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="576" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="582" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="588" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="594" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="600" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="606" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="612" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="618" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="624" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="630" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="636" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="642" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="648" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="654" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="660" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="666" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="672" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="678" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="684" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="690" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="696" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="702" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="708" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="714" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="720" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="726" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="732" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="738" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="744" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="750" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="756" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="762" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="768" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="774" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="780" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="786" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="792" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="798" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="804" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="810" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="816" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="822" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="828" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="834" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="840" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="846" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="852" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="858" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="864" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="870" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="876" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="882" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="888" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="894" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="900" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="906" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="912" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="918" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
<rect x="924" y="6" width="2" height="2" rx="1" fill="#242834"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 19 KiB |
@@ -253,18 +253,12 @@ export const ShortcutsPage = Loadable(
|
||||
() => import(/* webpackChunkName: "ShortcutsPage" */ 'pages/Settings'),
|
||||
);
|
||||
|
||||
export const Integrations = Loadable(
|
||||
export const InstalledIntegrations = Loadable(
|
||||
() =>
|
||||
import(
|
||||
/* webpackChunkName: "InstalledIntegrations" */ 'pages/IntegrationsModulePage'
|
||||
),
|
||||
);
|
||||
export const IntegrationsDetailsPage = Loadable(
|
||||
() =>
|
||||
import(
|
||||
/* webpackChunkName: "IntegrationsDetailsPage" */ 'pages/IntegrationsDetailsPage'
|
||||
),
|
||||
);
|
||||
|
||||
export const MessagingQueuesMainPage = Loadable(
|
||||
() =>
|
||||
|
||||
@@ -18,8 +18,7 @@ import {
|
||||
ForgotPassword,
|
||||
Home,
|
||||
InfrastructureMonitoring,
|
||||
Integrations,
|
||||
IntegrationsDetailsPage,
|
||||
InstalledIntegrations,
|
||||
LicensePage,
|
||||
ListAllALertsPage,
|
||||
LiveLogs,
|
||||
@@ -390,17 +389,10 @@ const routes: AppRoutes[] = [
|
||||
isPrivate: true,
|
||||
key: 'WORKSPACE_ACCESS_RESTRICTED',
|
||||
},
|
||||
{
|
||||
path: ROUTES.INTEGRATIONS_DETAIL,
|
||||
exact: true,
|
||||
component: IntegrationsDetailsPage,
|
||||
isPrivate: true,
|
||||
key: 'INTEGRATIONS_DETAIL',
|
||||
},
|
||||
{
|
||||
path: ROUTES.INTEGRATIONS,
|
||||
exact: true,
|
||||
component: Integrations,
|
||||
component: InstalledIntegrations,
|
||||
isPrivate: true,
|
||||
key: 'INTEGRATIONS',
|
||||
},
|
||||
|
||||
@@ -5,13 +5,13 @@ import {
|
||||
ServiceData,
|
||||
UpdateServiceConfigPayload,
|
||||
UpdateServiceConfigResponse,
|
||||
} from 'container/Integrations/CloudIntegration/AmazonWebServices/types';
|
||||
} from 'container/CloudIntegrationPage/ServicesSection/types';
|
||||
import {
|
||||
AccountConfigPayload,
|
||||
AccountConfigResponse,
|
||||
AWSAccountConfigPayload,
|
||||
ConnectionParams,
|
||||
ConnectionUrlResponse,
|
||||
} from 'types/api/integrations/aws';
|
||||
import { ConnectionParams } from 'types/api/integrations/types';
|
||||
|
||||
export const getAwsAccounts = async (): Promise<CloudAccount[]> => {
|
||||
const response = await axios.get('/cloud-integrations/aws/accounts');
|
||||
@@ -60,7 +60,7 @@ export const generateConnectionUrl = async (params: {
|
||||
|
||||
export const updateAccountConfig = async (
|
||||
accountId: string,
|
||||
payload: AWSAccountConfigPayload,
|
||||
payload: AccountConfigPayload,
|
||||
): Promise<AccountConfigResponse> => {
|
||||
const response = await axios.post<AccountConfigResponse>(
|
||||
`/cloud-integrations/aws/accounts/${accountId}/config`,
|
||||
|
||||
@@ -1,122 +0,0 @@
|
||||
import axios from 'api';
|
||||
import {
|
||||
CloudAccount,
|
||||
ServiceData,
|
||||
} from 'container/Integrations/CloudIntegration/AmazonWebServices/types';
|
||||
import {
|
||||
AzureCloudAccountConfig,
|
||||
AzureService,
|
||||
AzureServiceConfigPayload,
|
||||
} from 'container/Integrations/types';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import {
|
||||
AccountConfigResponse,
|
||||
AWSAccountConfigPayload,
|
||||
} from 'types/api/integrations/aws';
|
||||
import {
|
||||
AzureAccountConfig,
|
||||
ConnectionParams,
|
||||
IAzureDeploymentCommands,
|
||||
} from 'types/api/integrations/types';
|
||||
|
||||
export const getCloudIntegrationAccounts = async (
|
||||
cloudServiceId: string,
|
||||
): Promise<CloudAccount[]> => {
|
||||
const response = await axios.get(
|
||||
`/cloud-integrations/${cloudServiceId}/accounts`,
|
||||
);
|
||||
|
||||
return response.data.data.accounts;
|
||||
};
|
||||
|
||||
export const getCloudIntegrationServices = async (
|
||||
cloudServiceId: string,
|
||||
cloudAccountId?: string,
|
||||
): Promise<AzureService[]> => {
|
||||
const params = cloudAccountId
|
||||
? { cloud_account_id: cloudAccountId }
|
||||
: undefined;
|
||||
|
||||
const response = await axios.get(
|
||||
`/cloud-integrations/${cloudServiceId}/services`,
|
||||
{
|
||||
params,
|
||||
},
|
||||
);
|
||||
|
||||
return response.data.data.services;
|
||||
};
|
||||
|
||||
export const getCloudIntegrationServiceDetails = async (
|
||||
cloudServiceId: string,
|
||||
serviceId: string,
|
||||
cloudAccountId?: string,
|
||||
): Promise<ServiceData> => {
|
||||
const params = cloudAccountId
|
||||
? { cloud_account_id: cloudAccountId }
|
||||
: undefined;
|
||||
const response = await axios.get(
|
||||
`/cloud-integrations/${cloudServiceId}/services/${serviceId}`,
|
||||
{ params },
|
||||
);
|
||||
return response.data.data;
|
||||
};
|
||||
|
||||
export const updateAccountConfig = async (
|
||||
cloudServiceId: string,
|
||||
accountId: string,
|
||||
payload: AWSAccountConfigPayload | AzureAccountConfig,
|
||||
): Promise<AccountConfigResponse> => {
|
||||
const response = await axios.post<AccountConfigResponse>(
|
||||
`/cloud-integrations/${cloudServiceId}/accounts/${accountId}/config`,
|
||||
payload,
|
||||
);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export const updateServiceConfig = async (
|
||||
cloudServiceId: string,
|
||||
serviceId: string,
|
||||
payload: AzureServiceConfigPayload,
|
||||
): Promise<AzureServiceConfigPayload> => {
|
||||
const response = await axios.post<AzureServiceConfigPayload>(
|
||||
`/cloud-integrations/${cloudServiceId}/services/${serviceId}/config`,
|
||||
payload,
|
||||
);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export const getConnectionParams = async (
|
||||
cloudServiceId: string,
|
||||
): Promise<ConnectionParams> => {
|
||||
const response = await axios.get(
|
||||
`/cloud-integrations/${cloudServiceId}/accounts/generate-connection-params`,
|
||||
);
|
||||
return response.data.data;
|
||||
};
|
||||
|
||||
export const getAzureDeploymentCommands = async (params: {
|
||||
agent_config: ConnectionParams;
|
||||
account_config: AzureCloudAccountConfig;
|
||||
}): Promise<IAzureDeploymentCommands> => {
|
||||
const response = await axios.post(
|
||||
`/cloud-integrations/azure/accounts/generate-connection-url`,
|
||||
params,
|
||||
);
|
||||
|
||||
return response.data.data;
|
||||
};
|
||||
|
||||
export const removeIntegrationAccount = async ({
|
||||
cloudServiceId,
|
||||
accountId,
|
||||
}: {
|
||||
cloudServiceId: string;
|
||||
accountId: string;
|
||||
}): Promise<SuccessResponse<Record<string, never>> | ErrorResponse> => {
|
||||
const response = await axios.post(
|
||||
`/cloud-integrations/${cloudServiceId}/accounts/${accountId}/disconnect`,
|
||||
);
|
||||
|
||||
return response.data;
|
||||
};
|
||||
3
frontend/src/auto-import-registry.d.ts
vendored
3
frontend/src/auto-import-registry.d.ts
vendored
@@ -18,8 +18,6 @@ import '@signozhq/checkbox';
|
||||
import '@signozhq/combobox';
|
||||
import '@signozhq/command';
|
||||
import '@signozhq/design-tokens';
|
||||
import '@signozhq/dialog';
|
||||
import '@signozhq/drawer';
|
||||
import '@signozhq/icons';
|
||||
import '@signozhq/input';
|
||||
import '@signozhq/popover';
|
||||
@@ -28,5 +26,4 @@ import '@signozhq/resizable';
|
||||
import '@signozhq/sonner';
|
||||
import '@signozhq/switch';
|
||||
import '@signozhq/table';
|
||||
import '@signozhq/tabs';
|
||||
import '@signozhq/tooltip';
|
||||
|
||||
@@ -1,109 +0,0 @@
|
||||
.cloud-integration-accounts {
|
||||
padding: 0px 16px;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
|
||||
.selected-cloud-integration-account-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
|
||||
.selected-cloud-integration-account-section-header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--l3-background);
|
||||
background: var(--l1-background);
|
||||
|
||||
.selected-cloud-integration-account-section-header-title {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
.selected-cloud-integration-account-status {
|
||||
display: flex;
|
||||
border-right: 1px solid var(--l3-background);
|
||||
border-radius: none;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 4px;
|
||||
line-height: 32px;
|
||||
}
|
||||
|
||||
.selected-cloud-integration-account-section-header-title-text {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
|
||||
.azure-cloud-account-selector {
|
||||
.ant-select {
|
||||
background-color: transparent !important;
|
||||
border: none !important;
|
||||
box-shadow: none !important;
|
||||
|
||||
.ant-select-selector {
|
||||
background: transparent !important;
|
||||
border: none !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.selected-cloud-integration-account-settings {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 16px;
|
||||
line-height: 32px;
|
||||
margin-right: 8px;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.account-settings-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.add-new-cloud-integration-account-button {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 16px;
|
||||
line-height: 32px;
|
||||
margin-right: 8px;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.cloud-integration-accounts-drawer-content {
|
||||
height: 100%;
|
||||
max-height: calc(100vh - 120px); // Account for drawer header and padding
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 0.2rem;
|
||||
}
|
||||
|
||||
.edit-account-content,
|
||||
.add-new-account-content {
|
||||
flex: 1;
|
||||
min-height: 0; // Allows flex children to shrink below content size
|
||||
}
|
||||
}
|
||||
@@ -1,165 +0,0 @@
|
||||
import { useState } from 'react';
|
||||
import { Button } from '@signozhq/button';
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { DrawerWrapper } from '@signozhq/drawer';
|
||||
import { Select } from 'antd';
|
||||
import ConnectNewAzureAccount from 'container/Integrations/CloudIntegration/AzureServices/AzureAccount/ConnectNewAzureAccount';
|
||||
import EditAzureAccount from 'container/Integrations/CloudIntegration/AzureServices/AzureAccount/EditAzureAccount';
|
||||
import { INTEGRATION_TYPES } from 'container/Integrations/constants';
|
||||
import { CloudAccount } from 'container/Integrations/types';
|
||||
import { useGetConnectionParams } from 'hooks/integration/useGetConnectionParams';
|
||||
import useAxiosError from 'hooks/useAxiosError';
|
||||
import { Dot, PencilLine, Plus } from 'lucide-react';
|
||||
|
||||
import './CloudIntegrationAccounts.styles.scss';
|
||||
|
||||
export type DrawerMode = 'edit' | 'add';
|
||||
|
||||
export default function CloudIntegrationAccounts({
|
||||
selectedAccount,
|
||||
accounts,
|
||||
isLoadingAccounts,
|
||||
onSelectAccount,
|
||||
refetchAccounts,
|
||||
}: {
|
||||
selectedAccount: CloudAccount | null;
|
||||
accounts: CloudAccount[];
|
||||
isLoadingAccounts: boolean;
|
||||
onSelectAccount: (account: CloudAccount) => void;
|
||||
refetchAccounts: () => void;
|
||||
}): JSX.Element {
|
||||
const [isDrawerOpen, setIsDrawerOpen] = useState(false);
|
||||
const [mode, setMode] = useState<DrawerMode>('add');
|
||||
|
||||
const handleDrawerOpenChange = (open: boolean): void => {
|
||||
setIsDrawerOpen(open);
|
||||
};
|
||||
|
||||
const handleEditAccount = (): void => {
|
||||
setMode('edit');
|
||||
setIsDrawerOpen(true);
|
||||
};
|
||||
|
||||
const handleAddNewAccount = (): void => {
|
||||
setMode('add');
|
||||
setIsDrawerOpen(true);
|
||||
};
|
||||
|
||||
const handleError = useAxiosError();
|
||||
|
||||
const {
|
||||
data: connectionParams,
|
||||
isLoading: isConnectionParamsLoading,
|
||||
} = useGetConnectionParams({
|
||||
cloudServiceId: INTEGRATION_TYPES.AZURE,
|
||||
options: { onError: handleError },
|
||||
});
|
||||
|
||||
const handleSelectAccount = (value: string): void => {
|
||||
const account = accounts.find(
|
||||
(account) => account.cloud_account_id === value,
|
||||
);
|
||||
|
||||
if (account) {
|
||||
onSelectAccount(account);
|
||||
}
|
||||
};
|
||||
|
||||
const handleAccountConnected = (): void => {
|
||||
refetchAccounts();
|
||||
};
|
||||
|
||||
const handleAccountUpdated = (): void => {
|
||||
refetchAccounts();
|
||||
};
|
||||
|
||||
const renderDrawerContent = (): JSX.Element => {
|
||||
return (
|
||||
<div className="cloud-integration-accounts-drawer-content">
|
||||
{mode === 'edit' ? (
|
||||
<div className="edit-account-content">
|
||||
<EditAzureAccount
|
||||
selectedAccount={selectedAccount as CloudAccount}
|
||||
connectionParams={connectionParams || {}}
|
||||
isConnectionParamsLoading={isConnectionParamsLoading}
|
||||
onAccountUpdated={handleAccountUpdated}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="add-new-account-content">
|
||||
<ConnectNewAzureAccount
|
||||
connectionParams={connectionParams || {}}
|
||||
isConnectionParamsLoading={isConnectionParamsLoading}
|
||||
onAccountConnected={handleAccountConnected}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="cloud-integration-accounts">
|
||||
{selectedAccount && (
|
||||
<div className="selected-cloud-integration-account-section">
|
||||
<div className="selected-cloud-integration-account-section-header">
|
||||
<div className="selected-cloud-integration-account-section-header-title">
|
||||
<div className="selected-cloud-integration-account-status">
|
||||
<Dot size={24} color={Color.BG_FOREST_500} />
|
||||
</div>
|
||||
<div className="selected-cloud-integration-account-section-header-title-text">
|
||||
Subscription ID :
|
||||
<span className="azure-cloud-account-selector">
|
||||
<Select
|
||||
value={selectedAccount?.cloud_account_id}
|
||||
options={accounts.map((account) => ({
|
||||
label: account.cloud_account_id,
|
||||
value: account.cloud_account_id,
|
||||
}))}
|
||||
onChange={handleSelectAccount}
|
||||
loading={isLoadingAccounts}
|
||||
placeholder="Select Account"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="selected-cloud-integration-account-settings">
|
||||
<Button
|
||||
variant="link"
|
||||
color="secondary"
|
||||
prefixIcon={<PencilLine size={14} />}
|
||||
onClick={handleEditAccount}
|
||||
>
|
||||
Edit Account
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="link"
|
||||
color="secondary"
|
||||
prefixIcon={<Plus size={14} />}
|
||||
onClick={handleAddNewAccount}
|
||||
>
|
||||
Add New Account
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="account-settings-container">
|
||||
<DrawerWrapper
|
||||
open={isDrawerOpen}
|
||||
onOpenChange={handleDrawerOpenChange}
|
||||
type="panel"
|
||||
header={{
|
||||
title: mode === 'add' ? 'Connect with Azure' : 'Edit Azure Account',
|
||||
}}
|
||||
content={renderDrawerContent()}
|
||||
showCloseButton
|
||||
allowOutsideClick={mode === 'edit'}
|
||||
direction="right"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
import CloudIntegrationAccounts from './CloudIntegrationAccounts';
|
||||
|
||||
export default CloudIntegrationAccounts;
|
||||
@@ -1,50 +0,0 @@
|
||||
.cloud-integrations-header-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
border-bottom: 1px solid var(--l3-background);
|
||||
|
||||
.cloud-integrations-header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
padding: 16px;
|
||||
padding-bottom: 0px;
|
||||
gap: 16px;
|
||||
|
||||
.cloud-integrations-title-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
|
||||
.cloud-integrations-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
|
||||
.cloud-integrations-icon {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
color: var(--l1-foreground);
|
||||
font-family: Inter;
|
||||
font-size: 16px;
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
line-height: 32px; /* 200% */
|
||||
letter-spacing: -0.08px;
|
||||
}
|
||||
|
||||
.cloud-integrations-description {
|
||||
color: var(--l2-foreground);
|
||||
font-family: Inter;
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 20px; /* 142.857% */
|
||||
letter-spacing: -0.07px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
import {
|
||||
AWS_INTEGRATION,
|
||||
AZURE_INTEGRATION,
|
||||
} from 'container/Integrations/constants';
|
||||
import { CloudAccount, IntegrationType } from 'container/Integrations/types';
|
||||
|
||||
import CloudIntegrationAccounts from '../CloudIntegrationAccounts';
|
||||
|
||||
import './CloudIntegrationsHeader.styles.scss';
|
||||
|
||||
export default function CloudIntegrationsHeader({
|
||||
cloudServiceId,
|
||||
selectedAccount,
|
||||
accounts,
|
||||
isLoadingAccounts,
|
||||
onSelectAccount,
|
||||
refetchAccounts,
|
||||
}: {
|
||||
selectedAccount: CloudAccount | null;
|
||||
accounts: CloudAccount[] | [];
|
||||
isLoadingAccounts: boolean;
|
||||
onSelectAccount: (account: CloudAccount) => void;
|
||||
cloudServiceId: IntegrationType;
|
||||
refetchAccounts: () => void;
|
||||
}): JSX.Element {
|
||||
const INTEGRATION_DATA =
|
||||
cloudServiceId === IntegrationType.AWS_SERVICES
|
||||
? AWS_INTEGRATION
|
||||
: AZURE_INTEGRATION;
|
||||
|
||||
return (
|
||||
<div className="cloud-integrations-header-section">
|
||||
<div className="cloud-integrations-header">
|
||||
<div className="cloud-integrations-title-section">
|
||||
<div className="cloud-integrations-title">
|
||||
<img
|
||||
className="cloud-integrations-icon"
|
||||
src={INTEGRATION_DATA.icon}
|
||||
alt={INTEGRATION_DATA.icon_alt}
|
||||
/>
|
||||
|
||||
{INTEGRATION_DATA.title}
|
||||
</div>
|
||||
<div className="cloud-integrations-description">
|
||||
{INTEGRATION_DATA.description}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="cloud-integrations-accounts-list">
|
||||
<CloudIntegrationAccounts
|
||||
selectedAccount={selectedAccount}
|
||||
accounts={accounts}
|
||||
isLoadingAccounts={isLoadingAccounts}
|
||||
onSelectAccount={onSelectAccount}
|
||||
refetchAccounts={refetchAccounts}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
import CloudIntegrationsHeader from './CloudIntegrationsHeader';
|
||||
|
||||
export default CloudIntegrationsHeader;
|
||||
@@ -1,34 +0,0 @@
|
||||
.cloud-service-data-collected {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
|
||||
.cloud-service-data-collected-table {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
|
||||
.cloud-service-data-collected-table-heading {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
color: var(--l2-foreground);
|
||||
|
||||
/* Bifrost (Ancient)/Content/sm */
|
||||
font-family: Inter;
|
||||
font-size: 13px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 20px; /* 142.857% */
|
||||
letter-spacing: -0.07px;
|
||||
}
|
||||
|
||||
.cloud-service-data-collected-table-logs {
|
||||
border-radius: 6px;
|
||||
border: 1px solid var(--l3-background);
|
||||
background: var(--l1-background);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
.code-block-container {
|
||||
position: relative;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
|
||||
.code-block-copy-btn {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
right: 8px;
|
||||
transform: translateY(-50%);
|
||||
z-index: 10;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 6px 8px;
|
||||
color: var(--bg-vanilla-100);
|
||||
transition: color 0.15s ease;
|
||||
|
||||
&.copied {
|
||||
background-color: var(--bg-robin-500);
|
||||
}
|
||||
}
|
||||
|
||||
// CodeMirror wrapper
|
||||
.code-block-editor {
|
||||
border-radius: 4px;
|
||||
|
||||
.cm-editor {
|
||||
border-radius: 4px;
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
font-family: 'Space Mono', monospace;
|
||||
}
|
||||
|
||||
.cm-scroller {
|
||||
font-family: 'Space Mono', monospace;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,145 +0,0 @@
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import { CheckOutlined, CopyOutlined } from '@ant-design/icons';
|
||||
import { javascript } from '@codemirror/lang-javascript';
|
||||
import { Button } from '@signozhq/button';
|
||||
import { dracula } from '@uiw/codemirror-theme-dracula';
|
||||
import { githubLight } from '@uiw/codemirror-theme-github';
|
||||
import CodeMirror, {
|
||||
EditorState,
|
||||
EditorView,
|
||||
Extension,
|
||||
} from '@uiw/react-codemirror';
|
||||
import cx from 'classnames';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
|
||||
import './CodeBlock.styles.scss';
|
||||
|
||||
export type CodeBlockLanguage =
|
||||
| 'javascript'
|
||||
| 'typescript'
|
||||
| 'js'
|
||||
| 'ts'
|
||||
| 'json'
|
||||
| 'bash'
|
||||
| 'shell'
|
||||
| 'text';
|
||||
|
||||
export type CodeBlockTheme = 'light' | 'dark' | 'auto';
|
||||
|
||||
interface CodeBlockProps {
|
||||
/** The code content to display */
|
||||
value: string;
|
||||
/** Language for syntax highlighting */
|
||||
language?: CodeBlockLanguage;
|
||||
/** Theme: 'light' | 'dark' | 'auto' (follows app dark mode when 'auto') */
|
||||
theme?: CodeBlockTheme;
|
||||
/** Show line numbers */
|
||||
lineNumbers?: boolean;
|
||||
/** Show copy button */
|
||||
showCopyButton?: boolean;
|
||||
/** Custom class name for the container */
|
||||
className?: string;
|
||||
/** Max height in pixels - enables scrolling when content exceeds */
|
||||
maxHeight?: number | string;
|
||||
/** Callback when copy is clicked */
|
||||
onCopy?: (copiedText: string) => void;
|
||||
}
|
||||
|
||||
const LANGUAGE_EXTENSION_MAP: Record<
|
||||
CodeBlockLanguage,
|
||||
ReturnType<typeof javascript> | undefined
|
||||
> = {
|
||||
javascript: javascript({ jsx: true }),
|
||||
typescript: javascript({ jsx: true }),
|
||||
js: javascript({ jsx: true }),
|
||||
ts: javascript({ jsx: true }),
|
||||
json: javascript(), // JSON is valid JS; proper json() would require @codemirror/lang-json
|
||||
bash: undefined,
|
||||
shell: undefined,
|
||||
text: undefined,
|
||||
};
|
||||
|
||||
function CodeBlock({
|
||||
value,
|
||||
language = 'text',
|
||||
theme: themeProp = 'auto',
|
||||
lineNumbers = true,
|
||||
showCopyButton = true,
|
||||
className,
|
||||
maxHeight,
|
||||
onCopy,
|
||||
}: CodeBlockProps): JSX.Element {
|
||||
const isDarkMode = useIsDarkMode();
|
||||
const [isCopied, setIsCopied] = useState(false);
|
||||
|
||||
const resolvedDark = themeProp === 'auto' ? isDarkMode : themeProp === 'dark';
|
||||
const theme = resolvedDark ? dracula : githubLight;
|
||||
|
||||
const extensions = useMemo((): Extension[] => {
|
||||
const langExtension = LANGUAGE_EXTENSION_MAP[language];
|
||||
return [
|
||||
EditorState.readOnly.of(true),
|
||||
EditorView.editable.of(false),
|
||||
EditorView.lineWrapping,
|
||||
...(langExtension ? [langExtension] : []),
|
||||
];
|
||||
}, [language]);
|
||||
|
||||
const handleCopy = useCallback((): void => {
|
||||
navigator.clipboard.writeText(value).then(() => {
|
||||
setIsCopied(true);
|
||||
onCopy?.(value);
|
||||
setTimeout(() => setIsCopied(false), 2000);
|
||||
});
|
||||
}, [value, onCopy]);
|
||||
|
||||
return (
|
||||
<div className={cx('code-block-container', className)}>
|
||||
{showCopyButton && (
|
||||
<Button
|
||||
variant="solid"
|
||||
size="xs"
|
||||
color="secondary"
|
||||
className={cx('code-block-copy-btn', { copied: isCopied })}
|
||||
onClick={handleCopy}
|
||||
aria-label={isCopied ? 'Copied' : 'Copy code'}
|
||||
title={isCopied ? 'Copied' : 'Copy code'}
|
||||
>
|
||||
{isCopied ? <CheckOutlined /> : <CopyOutlined />}
|
||||
</Button>
|
||||
)}
|
||||
<CodeMirror
|
||||
className="code-block-editor"
|
||||
value={value}
|
||||
theme={theme}
|
||||
readOnly
|
||||
editable={false}
|
||||
extensions={extensions}
|
||||
basicSetup={{
|
||||
lineNumbers,
|
||||
highlightActiveLineGutter: false,
|
||||
highlightActiveLine: false,
|
||||
highlightSelectionMatches: true,
|
||||
drawSelection: true,
|
||||
syntaxHighlighting: true,
|
||||
bracketMatching: true,
|
||||
history: false,
|
||||
foldGutter: false,
|
||||
autocompletion: false,
|
||||
defaultKeymap: false,
|
||||
searchKeymap: true,
|
||||
historyKeymap: false,
|
||||
foldKeymap: false,
|
||||
completionKeymap: false,
|
||||
closeBrackets: false,
|
||||
indentOnInput: false,
|
||||
}}
|
||||
style={{
|
||||
maxHeight: maxHeight ?? 'auto',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default CodeBlock;
|
||||
@@ -1,2 +0,0 @@
|
||||
export type { CodeBlockLanguage, CodeBlockTheme } from './CodeBlock';
|
||||
export { default as CodeBlock } from './CodeBlock';
|
||||
@@ -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');
|
||||
|
||||
@@ -4,8 +4,8 @@ import { toast } from '@signozhq/sonner';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { handleContactSupport } from 'container/Integrations/utils';
|
||||
import { useGetTenantLicense } from 'hooks/useGetTenantLicense';
|
||||
import { handleContactSupport } from 'pages/Integrations/utils';
|
||||
|
||||
import FeedbackModal from '../FeedbackModal';
|
||||
|
||||
@@ -30,7 +30,7 @@ jest.mock('hooks/useGetTenantLicense', () => ({
|
||||
useGetTenantLicense: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('container/Integrations/utils', () => ({
|
||||
jest.mock('pages/Integrations/utils', () => ({
|
||||
handleContactSupport: jest.fn(),
|
||||
}));
|
||||
|
||||
|
||||
@@ -1,19 +1,4 @@
|
||||
export const REACT_QUERY_KEY = {
|
||||
// Cloud Integration Query Keys
|
||||
CLOUD_INTEGRATION_ACCOUNTS: 'CLOUD_INTEGRATION_ACCOUNTS',
|
||||
CLOUD_INTEGRATION_SERVICES: 'CLOUD_INTEGRATION_SERVICES',
|
||||
CLOUD_INTEGRATION_SERVICE_DETAILS: 'CLOUD_INTEGRATION_SERVICE_DETAILS',
|
||||
CLOUD_INTEGRATION_ACCOUNT_STATUS: 'CLOUD_INTEGRATION_ACCOUNT_STATUS',
|
||||
CLOUD_INTEGRATION_UPDATE_ACCOUNT_CONFIG:
|
||||
'CLOUD_INTEGRATION_UPDATE_ACCOUNT_CONFIG',
|
||||
CLOUD_INTEGRATION_UPDATE_SERVICE_CONFIG:
|
||||
'CLOUD_INTEGRATION_UPDATE_SERVICE_CONFIG',
|
||||
CLOUD_INTEGRATION_GENERATE_CONNECTION_URL:
|
||||
'CLOUD_INTEGRATION_GENERATE_CONNECTION_URL',
|
||||
CLOUD_INTEGRATION_GET_CONNECTION_PARAMS:
|
||||
'CLOUD_INTEGRATION_GET_CONNECTION_PARAMS',
|
||||
CLOUD_INTEGRATION_GET_DEPLOYMENT_COMMANDS:
|
||||
'CLOUD_INTEGRATION_GET_DEPLOYMENT_COMMANDS',
|
||||
GET_PUBLIC_DASHBOARD: 'GET_PUBLIC_DASHBOARD',
|
||||
GET_PUBLIC_DASHBOARD_META: 'GET_PUBLIC_DASHBOARD_META',
|
||||
GET_PUBLIC_DASHBOARD_WIDGET_DATA: 'GET_PUBLIC_DASHBOARD_WIDGET_DATA',
|
||||
|
||||
@@ -65,7 +65,6 @@ const ROUTES = {
|
||||
WORKSPACE_SUSPENDED: '/workspace-suspended',
|
||||
SHORTCUTS: '/settings/shortcuts',
|
||||
INTEGRATIONS: '/integrations',
|
||||
INTEGRATIONS_DETAIL: '/integrations/:integrationId',
|
||||
MESSAGING_QUEUES_BASE: '/messaging-queues',
|
||||
MESSAGING_QUEUES_KAFKA: '/messaging-queues/kafka',
|
||||
MESSAGING_QUEUES_KAFKA_DETAIL: '/messaging-queues/kafka/detail',
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import {
|
||||
IntegrationType,
|
||||
RequestIntegrationBtn,
|
||||
} from 'pages/Integrations/RequestIntegrationBtn';
|
||||
|
||||
import Header from './Header/Header';
|
||||
import HeroSection from './HeroSection/HeroSection';
|
||||
import ServicesTabs from './ServicesSection/ServicesTabs';
|
||||
|
||||
function CloudIntegrationPage(): JSX.Element {
|
||||
return (
|
||||
<div>
|
||||
<Header />
|
||||
<HeroSection />
|
||||
<RequestIntegrationBtn
|
||||
type={IntegrationType.AWS_SERVICES}
|
||||
message="Can't find the AWS service you're looking for? Request more integrations"
|
||||
/>
|
||||
<ServicesTabs />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default CloudIntegrationPage;
|
||||
@@ -48,7 +48,7 @@
|
||||
|
||||
.lightMode {
|
||||
.cloud-header {
|
||||
border-bottom: 1px solid var(--bg-vanilla-300);
|
||||
border-bottom: 1px solid var(--bg-slate-300);
|
||||
|
||||
&__breadcrumb-title {
|
||||
color: var(--bg-ink-400);
|
||||
@@ -1,13 +1,11 @@
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Button } from '@signozhq/button';
|
||||
import Breadcrumb from 'antd/es/breadcrumb';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { IntegrationType } from 'container/Integrations/types';
|
||||
import { Blocks, LifeBuoy } from 'lucide-react';
|
||||
|
||||
import './Header.styles.scss';
|
||||
|
||||
function Header({ title }: { title: IntegrationType }): JSX.Element {
|
||||
function Header(): JSX.Element {
|
||||
return (
|
||||
<div className="cloud-header">
|
||||
<div className="cloud-header__navigation">
|
||||
@@ -25,26 +23,25 @@ function Header({ title }: { title: IntegrationType }): JSX.Element {
|
||||
),
|
||||
},
|
||||
{
|
||||
title: <div className="cloud-header__breadcrumb-title">{title}</div>,
|
||||
title: (
|
||||
<div className="cloud-header__breadcrumb-title">
|
||||
Amazon Web Services
|
||||
</div>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
<div className="cloud-header__actions">
|
||||
<Button
|
||||
variant="solid"
|
||||
size="sm"
|
||||
color="secondary"
|
||||
onClick={(): void => {
|
||||
window.open(
|
||||
'https://signoz.io/blog/native-aws-integrations-with-autodiscovery/',
|
||||
'_blank',
|
||||
);
|
||||
}}
|
||||
prefixIcon={<LifeBuoy size={12} />}
|
||||
<a
|
||||
href="https://signoz.io/blog/native-aws-integrations-with-autodiscovery/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="cloud-header__help"
|
||||
>
|
||||
<LifeBuoy size={12} />
|
||||
Get Help
|
||||
</Button>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -1,10 +1,8 @@
|
||||
.hero-section {
|
||||
padding: 16px;
|
||||
|
||||
height: 308px;
|
||||
padding: 26px 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
|
||||
gap: 24px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
background-position: right;
|
||||
@@ -12,37 +10,35 @@
|
||||
background-repeat: no-repeat;
|
||||
border-bottom: 1px solid var(--bg-slate-500);
|
||||
|
||||
&__icon {
|
||||
height: fit-content;
|
||||
background-color: var(--bg-ink-400);
|
||||
padding: 12px;
|
||||
border: 1px solid var(--bg-ink-300);
|
||||
border-radius: 6px;
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
img {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
&__details {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
|
||||
&-header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
|
||||
&__icon {
|
||||
height: fit-content;
|
||||
background-color: var(--bg-ink-400);
|
||||
padding: 12px;
|
||||
border: 1px solid var(--bg-ink-300);
|
||||
border-radius: 6px;
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
}
|
||||
|
||||
&__title {
|
||||
color: var(--bg-vanilla-100);
|
||||
font-size: 24px;
|
||||
font-weight: 500;
|
||||
line-height: 20px;
|
||||
letter-spacing: -0.12px;
|
||||
}
|
||||
.title {
|
||||
color: var(--bg-vanilla-100);
|
||||
font-size: 24px;
|
||||
font-weight: 500;
|
||||
line-height: 20px;
|
||||
letter-spacing: -0.12px;
|
||||
}
|
||||
|
||||
&__description {
|
||||
.description {
|
||||
color: var(--bg-vanilla-400);
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
@@ -0,0 +1,34 @@
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
|
||||
import AccountActions from './components/AccountActions';
|
||||
|
||||
import './HeroSection.style.scss';
|
||||
|
||||
function HeroSection(): JSX.Element {
|
||||
const isDarkMode = useIsDarkMode();
|
||||
return (
|
||||
<div
|
||||
className="hero-section"
|
||||
style={
|
||||
isDarkMode
|
||||
? {
|
||||
backgroundImage: `url('/Images/integrations-hero-bg.png')`,
|
||||
}
|
||||
: {}
|
||||
}
|
||||
>
|
||||
<div className="hero-section__icon">
|
||||
<img src="/Logos/aws-dark.svg" alt="aws-logo" />
|
||||
</div>
|
||||
<div className="hero-section__details">
|
||||
<div className="title">Amazon Web Services</div>
|
||||
<div className="description">
|
||||
One-click setup for AWS monitoring with SigNoz
|
||||
</div>
|
||||
<AccountActions />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default HeroSection;
|
||||
@@ -4,56 +4,14 @@
|
||||
|
||||
&-with-account {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--l3-background);
|
||||
background: var(--l1-background);
|
||||
|
||||
.selected-cloud-integration-account-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-right: 1px solid var(--l3-background);
|
||||
border-radius: none;
|
||||
width: 32px;
|
||||
}
|
||||
|
||||
&-selector-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
|
||||
.account-selector-label {
|
||||
color: var(--bg-vanilla-400);
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
line-height: 16px;
|
||||
padding: 8px 16px;
|
||||
}
|
||||
|
||||
.account-selector {
|
||||
.ant-select {
|
||||
background-color: transparent !important;
|
||||
border: none !important;
|
||||
box-shadow: none !important;
|
||||
|
||||
.ant-select-selector {
|
||||
background: transparent !important;
|
||||
border: none !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
&__input-skeleton {
|
||||
width: 300px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
&__new-account-button-skeleton {
|
||||
@@ -64,13 +22,11 @@
|
||||
&__account-settings-button-skeleton {
|
||||
width: 140px;
|
||||
}
|
||||
|
||||
&__action-buttons {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
&__action-button {
|
||||
font-family: 'Inter';
|
||||
border-radius: 2px;
|
||||
@@ -98,6 +54,44 @@
|
||||
}
|
||||
}
|
||||
|
||||
.cloud-account-selector {
|
||||
border-radius: 2px;
|
||||
border: 1px solid var(--bg-ink-300);
|
||||
background: linear-gradient(
|
||||
139deg,
|
||||
rgba(18, 19, 23, 0.8) 0%,
|
||||
rgba(18, 19, 23, 0.9) 98.68%
|
||||
);
|
||||
box-shadow: 4px 10px 16px 2px rgba(0, 0, 0, 0.2);
|
||||
-webkit-backdrop-filter: blur(20px);
|
||||
backdrop-filter: blur(20px);
|
||||
.ant-select-selector {
|
||||
border-color: var(--bg-slate-400) !important;
|
||||
background: var(--bg-ink-300) !important;
|
||||
padding: 6px 8px !important;
|
||||
}
|
||||
.ant-select-selection-item {
|
||||
color: var(--bg-vanilla-400);
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
line-height: 16px;
|
||||
}
|
||||
|
||||
.account-option-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
&__selected {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 14px;
|
||||
width: 14px;
|
||||
background-color: rgba(192, 193, 195, 0.2); /* #C0C1C3 with 0.2 opacity */
|
||||
border-radius: 2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.lightMode {
|
||||
.hero-section__action-button {
|
||||
&.primary {
|
||||
@@ -1,21 +1,58 @@
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom-v5-compat';
|
||||
import { Button } from '@signozhq/button';
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { Select, Skeleton } from 'antd';
|
||||
import { Button, Select, Skeleton } from 'antd';
|
||||
import { SelectProps } from 'antd/lib';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { getAccountById } from 'container/Integrations/CloudIntegration/utils';
|
||||
import { useAwsAccounts } from 'hooks/integration/aws/useAwsAccounts';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
import { ChevronDown, Dot, PencilLine, Plug, Plus } from 'lucide-react';
|
||||
import { Check, ChevronDown } from 'lucide-react';
|
||||
|
||||
import { CloudAccount } from '../../types';
|
||||
import { CloudAccount } from '../../ServicesSection/types';
|
||||
import AccountSettingsModal from './AccountSettingsModal';
|
||||
import CloudAccountSetupModal from './CloudAccountSetupModal';
|
||||
|
||||
import './AccountActions.style.scss';
|
||||
|
||||
interface AccountOptionItemProps {
|
||||
label: React.ReactNode;
|
||||
isSelected: boolean;
|
||||
}
|
||||
|
||||
function AccountOptionItem({
|
||||
label,
|
||||
isSelected,
|
||||
}: AccountOptionItemProps): JSX.Element {
|
||||
return (
|
||||
<div className="account-option-item">
|
||||
{label}
|
||||
{isSelected && (
|
||||
<div className="account-option-item__selected">
|
||||
<Check size={12} color={Color.BG_VANILLA_100} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function renderOption(
|
||||
option: any,
|
||||
activeAccountId: string | undefined,
|
||||
): JSX.Element {
|
||||
return (
|
||||
<AccountOptionItem
|
||||
label={option.label}
|
||||
isSelected={option.value === activeAccountId}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const getAccountById = (
|
||||
accounts: CloudAccount[],
|
||||
accountId: string,
|
||||
): CloudAccount | null =>
|
||||
accounts.find((account) => account.cloud_account_id === accountId) || null;
|
||||
|
||||
function AccountActionsRenderer({
|
||||
accounts,
|
||||
isLoading,
|
||||
@@ -36,51 +73,55 @@ function AccountActionsRenderer({
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="hero-section__actions-with-account">
|
||||
<Skeleton.Input active block className="hero-section__input-skeleton" />
|
||||
<Skeleton.Input
|
||||
active
|
||||
size="large"
|
||||
block
|
||||
className="hero-section__input-skeleton"
|
||||
/>
|
||||
<div className="hero-section__action-buttons">
|
||||
<Skeleton.Button
|
||||
active
|
||||
size="large"
|
||||
className="hero-section__new-account-button-skeleton"
|
||||
/>
|
||||
<Skeleton.Button
|
||||
active
|
||||
size="large"
|
||||
className="hero-section__account-settings-button-skeleton"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (accounts?.length) {
|
||||
return (
|
||||
<div className="hero-section__actions-with-account">
|
||||
<div className="hero-section__actions-with-account-selector-container">
|
||||
<div className="selected-cloud-integration-account-status">
|
||||
<Dot size={24} color={Color.BG_FOREST_500} />
|
||||
</div>
|
||||
|
||||
<div className="account-selector-label">Account:</div>
|
||||
|
||||
<span className="account-selector">
|
||||
<Select
|
||||
value={activeAccount?.cloud_account_id}
|
||||
options={selectOptions}
|
||||
rootClassName="cloud-account-selector"
|
||||
placeholder="Select AWS Account"
|
||||
suffixIcon={<ChevronDown size={16} color={Color.BG_VANILLA_400} />}
|
||||
onChange={onAccountChange}
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
<Select
|
||||
value={`Account: ${activeAccount?.cloud_account_id}`}
|
||||
options={selectOptions}
|
||||
rootClassName="cloud-account-selector"
|
||||
placeholder="Select AWS Account"
|
||||
suffixIcon={<ChevronDown size={16} color={Color.BG_VANILLA_400} />}
|
||||
optionRender={(option): JSX.Element =>
|
||||
renderOption(option, activeAccount?.cloud_account_id)
|
||||
}
|
||||
onChange={onAccountChange}
|
||||
/>
|
||||
<div className="hero-section__action-buttons">
|
||||
<Button
|
||||
variant="link"
|
||||
size="sm"
|
||||
color="secondary"
|
||||
prefixIcon={<PencilLine size={14} />}
|
||||
type="primary"
|
||||
className="hero-section__action-button primary"
|
||||
onClick={onIntegrationModalOpen}
|
||||
>
|
||||
Add New AWS Account
|
||||
</Button>
|
||||
<Button
|
||||
type="default"
|
||||
className="hero-section__action-button secondary"
|
||||
onClick={onAccountSettingsModalOpen}
|
||||
>
|
||||
Edit Account
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="link"
|
||||
size="sm"
|
||||
color="secondary"
|
||||
onClick={onIntegrationModalOpen}
|
||||
prefixIcon={<Plus size={14} />}
|
||||
>
|
||||
Add New Account
|
||||
Account Settings
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -88,11 +129,8 @@ function AccountActionsRenderer({
|
||||
}
|
||||
return (
|
||||
<Button
|
||||
variant="solid"
|
||||
color="primary"
|
||||
prefixIcon={<Plug size={14} />}
|
||||
className="hero-section__action-button primary"
|
||||
onClick={onIntegrationModalOpen}
|
||||
size="sm"
|
||||
>
|
||||
Integrate Now
|
||||
</Button>
|
||||
@@ -14,13 +14,8 @@
|
||||
border-radius: 3px;
|
||||
border: 1px solid var(--bg-slate-500);
|
||||
padding: 14px;
|
||||
|
||||
&-account-info {
|
||||
&-connected-account-details {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
|
||||
&-title {
|
||||
color: var(--bg-vanilla-100);
|
||||
font-size: 14px;
|
||||
@@ -43,36 +38,40 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-region-selector {
|
||||
&-regions-switch {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
|
||||
gap: 10px;
|
||||
&-title {
|
||||
color: var(--l1-foreground);
|
||||
color: var(--bg-vanilla-100);
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
line-height: 20px;
|
||||
letter-spacing: -0.07px;
|
||||
}
|
||||
|
||||
&-description {
|
||||
color: var(--l2-foreground);
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
letter-spacing: -0.06px;
|
||||
&-switch {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
&-label {
|
||||
color: var(--bg-vanilla-400);
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
font-family: Inter;
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
line-height: 18px;
|
||||
letter-spacing: -0.005em;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
&-regions-select {
|
||||
margin-top: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
&__footer {
|
||||
padding: 16px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 10px;
|
||||
justify-content: space-between;
|
||||
|
||||
&-close-button,
|
||||
&-save-button {
|
||||
color: var(--bg-vanilla-100);
|
||||
@@ -0,0 +1,213 @@
|
||||
import { Dispatch, SetStateAction, useCallback } from 'react';
|
||||
import { useQueryClient } from 'react-query';
|
||||
import { Form, Select, Switch } from 'antd';
|
||||
import SignozModal from 'components/SignozModal/SignozModal';
|
||||
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||
import {
|
||||
getRegionPreviewText,
|
||||
useAccountSettingsModal,
|
||||
} from 'hooks/integration/aws/useAccountSettingsModal';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
import history from 'lib/history';
|
||||
|
||||
import logEvent from '../../../../api/common/logEvent';
|
||||
import { CloudAccount } from '../../ServicesSection/types';
|
||||
import { RegionSelector } from './RegionSelector';
|
||||
import RemoveIntegrationAccount from './RemoveIntegrationAccount';
|
||||
|
||||
import './AccountSettingsModal.style.scss';
|
||||
|
||||
interface AccountSettingsModalProps {
|
||||
onClose: () => void;
|
||||
account: CloudAccount;
|
||||
setActiveAccount: Dispatch<SetStateAction<CloudAccount | null>>;
|
||||
}
|
||||
|
||||
function AccountSettingsModal({
|
||||
onClose,
|
||||
account,
|
||||
setActiveAccount,
|
||||
}: AccountSettingsModalProps): JSX.Element {
|
||||
const {
|
||||
form,
|
||||
isLoading,
|
||||
selectedRegions,
|
||||
includeAllRegions,
|
||||
isRegionSelectOpen,
|
||||
isSaveDisabled,
|
||||
setSelectedRegions,
|
||||
setIncludeAllRegions,
|
||||
setIsRegionSelectOpen,
|
||||
handleIncludeAllRegionsChange,
|
||||
handleSubmit,
|
||||
handleClose,
|
||||
} = useAccountSettingsModal({ onClose, account, setActiveAccount });
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
const urlQuery = useUrlQuery();
|
||||
|
||||
const handleRemoveIntegrationAccountSuccess = (): void => {
|
||||
queryClient.invalidateQueries([REACT_QUERY_KEY.AWS_ACCOUNTS]);
|
||||
urlQuery.delete('cloudAccountId');
|
||||
handleClose();
|
||||
history.replace({ search: urlQuery.toString() });
|
||||
|
||||
logEvent('AWS Integration: Account removed', {
|
||||
id: account?.id,
|
||||
cloudAccountId: account?.cloud_account_id,
|
||||
});
|
||||
};
|
||||
|
||||
const handleRegionDeselect = useCallback(
|
||||
(item: string): void => {
|
||||
if (selectedRegions.includes(item)) {
|
||||
setSelectedRegions(selectedRegions.filter((region) => region !== item));
|
||||
if (includeAllRegions) {
|
||||
setIncludeAllRegions(false);
|
||||
}
|
||||
}
|
||||
},
|
||||
[
|
||||
selectedRegions,
|
||||
includeAllRegions,
|
||||
setSelectedRegions,
|
||||
setIncludeAllRegions,
|
||||
],
|
||||
);
|
||||
|
||||
const renderRegionSelector = useCallback(() => {
|
||||
if (isRegionSelectOpen) {
|
||||
return (
|
||||
<RegionSelector
|
||||
selectedRegions={selectedRegions}
|
||||
setSelectedRegions={setSelectedRegions}
|
||||
setIncludeAllRegions={setIncludeAllRegions}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="account-settings-modal__body-regions-switch-switch ">
|
||||
<Switch
|
||||
checked={includeAllRegions}
|
||||
onChange={handleIncludeAllRegionsChange}
|
||||
/>
|
||||
<button
|
||||
className="account-settings-modal__body-regions-switch-switch-label"
|
||||
type="button"
|
||||
onClick={(): void => handleIncludeAllRegionsChange(!includeAllRegions)}
|
||||
>
|
||||
Include all regions
|
||||
</button>
|
||||
</div>
|
||||
<Select
|
||||
suffixIcon={null}
|
||||
placeholder="Select Region(s)"
|
||||
className="cloud-account-setup-form__select account-settings-modal__body-regions-select integrations-select"
|
||||
onClick={(): void => setIsRegionSelectOpen(true)}
|
||||
mode="multiple"
|
||||
maxTagCount={3}
|
||||
value={getRegionPreviewText(selectedRegions)}
|
||||
open={false}
|
||||
onDeselect={handleRegionDeselect}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}, [
|
||||
isRegionSelectOpen,
|
||||
includeAllRegions,
|
||||
handleIncludeAllRegionsChange,
|
||||
selectedRegions,
|
||||
handleRegionDeselect,
|
||||
setSelectedRegions,
|
||||
setIncludeAllRegions,
|
||||
setIsRegionSelectOpen,
|
||||
]);
|
||||
|
||||
const renderAccountDetails = useCallback(
|
||||
() => (
|
||||
<div className="account-settings-modal__body-account-info">
|
||||
<div className="account-settings-modal__body-account-info-connected-account-details">
|
||||
<div className="account-settings-modal__body-account-info-connected-account-details-title">
|
||||
Connected Account details
|
||||
</div>
|
||||
<div className="account-settings-modal__body-account-info-connected-account-details-account-id">
|
||||
AWS Account:{' '}
|
||||
<span className="account-settings-modal__body-account-info-connected-account-details-account-id-account-id">
|
||||
{account?.id}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
[account?.id],
|
||||
);
|
||||
|
||||
const modalTitle = (
|
||||
<div className="account-settings-modal__title">
|
||||
Account settings for{' '}
|
||||
<span className="account-settings-modal__title-account-id">
|
||||
{account?.id}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<SignozModal
|
||||
open
|
||||
title={modalTitle}
|
||||
onCancel={handleClose}
|
||||
onOk={handleSubmit}
|
||||
okText="Save"
|
||||
okButtonProps={{
|
||||
disabled: isSaveDisabled,
|
||||
className: 'account-settings-modal__footer-save-button',
|
||||
loading: isLoading,
|
||||
}}
|
||||
cancelButtonProps={{
|
||||
className: 'account-settings-modal__footer-close-button',
|
||||
}}
|
||||
width={672}
|
||||
rootClassName="account-settings-modal"
|
||||
>
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
initialValues={{
|
||||
selectedRegions,
|
||||
includeAllRegions,
|
||||
}}
|
||||
>
|
||||
<div className="account-settings-modal__body">
|
||||
{renderAccountDetails()}
|
||||
|
||||
<Form.Item
|
||||
name="selectedRegions"
|
||||
rules={[
|
||||
{
|
||||
validator: async (): Promise<void> => {
|
||||
if (selectedRegions.length === 0) {
|
||||
throw new Error('Please select at least one region to monitor');
|
||||
}
|
||||
},
|
||||
message: 'Please select at least one region to monitor',
|
||||
},
|
||||
]}
|
||||
>
|
||||
{renderRegionSelector()}
|
||||
</Form.Item>
|
||||
|
||||
<div className="integration-detail-content">
|
||||
<RemoveIntegrationAccount
|
||||
accountId={account?.id}
|
||||
onRemoveIntegrationAccountSuccess={handleRemoveIntegrationAccountSuccess}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Form>
|
||||
</SignozModal>
|
||||
);
|
||||
}
|
||||
|
||||
export default AccountSettingsModal;
|
||||
@@ -1,26 +1,4 @@
|
||||
.cloud-account-setup-modal {
|
||||
> div {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
&__content {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
min-height: 0;
|
||||
scrollbar-width: thin;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
&__footer {
|
||||
padding: 16px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.account-setup-modal-footer {
|
||||
&__confirm-button {
|
||||
background: var(--bg-robin-500);
|
||||
@@ -42,8 +20,6 @@
|
||||
}
|
||||
|
||||
.cloud-account-setup-form {
|
||||
padding: 16px;
|
||||
|
||||
.disabled {
|
||||
opacity: 0.4;
|
||||
}
|
||||
@@ -1,8 +1,7 @@
|
||||
import { useCallback } from 'react';
|
||||
import { useQueryClient } from 'react-query';
|
||||
import { Button } from '@signozhq/button';
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { DrawerWrapper } from '@signozhq/drawer';
|
||||
import SignozModal from 'components/SignozModal/SignozModal';
|
||||
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||
import { useIntegrationModal } from 'hooks/integration/aws/useIntegrationModal';
|
||||
import { SquareArrowOutUpRight } from 'lucide-react';
|
||||
@@ -13,6 +12,7 @@ import {
|
||||
ModalStateEnum,
|
||||
} from '../types';
|
||||
import { RegionForm } from './RegionForm';
|
||||
import { RegionSelector } from './RegionSelector';
|
||||
import { SuccessView } from './SuccessView';
|
||||
|
||||
import './CloudAccountSetupModal.style.scss';
|
||||
@@ -32,12 +32,14 @@ function CloudAccountSetupModal({
|
||||
isGeneratingUrl,
|
||||
setSelectedRegions,
|
||||
setIncludeAllRegions,
|
||||
handleIncludeAllRegionsChange,
|
||||
handleRegionSelect,
|
||||
handleSubmit,
|
||||
handleClose,
|
||||
setActiveView,
|
||||
allRegions,
|
||||
accountId,
|
||||
selectedDeploymentRegion,
|
||||
handleRegionChange,
|
||||
connectionParams,
|
||||
isConnectionParamsLoading,
|
||||
@@ -48,47 +50,45 @@ function CloudAccountSetupModal({
|
||||
return <SuccessView />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="cloud-account-setup-modal__content">
|
||||
<RegionForm
|
||||
form={form}
|
||||
modalState={modalState}
|
||||
setModalState={setModalState}
|
||||
if (activeView === ActiveViewEnum.SELECT_REGIONS) {
|
||||
return (
|
||||
<RegionSelector
|
||||
selectedRegions={selectedRegions}
|
||||
includeAllRegions={includeAllRegions}
|
||||
onRegionSelect={handleRegionSelect}
|
||||
onSubmit={handleSubmit}
|
||||
accountId={accountId}
|
||||
handleRegionChange={handleRegionChange}
|
||||
connectionParams={connectionParams}
|
||||
isConnectionParamsLoading={isConnectionParamsLoading}
|
||||
setSelectedRegions={setSelectedRegions}
|
||||
setIncludeAllRegions={setIncludeAllRegions}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
<div className="cloud-account-setup-modal__footer">
|
||||
<Button
|
||||
variant="solid"
|
||||
color="primary"
|
||||
prefixIcon={
|
||||
<SquareArrowOutUpRight size={17} color={Color.BG_VANILLA_100} />
|
||||
}
|
||||
onClick={handleSubmit}
|
||||
>
|
||||
Launch Cloud Formation Template
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
return (
|
||||
<RegionForm
|
||||
form={form}
|
||||
modalState={modalState}
|
||||
setModalState={setModalState}
|
||||
selectedRegions={selectedRegions}
|
||||
includeAllRegions={includeAllRegions}
|
||||
onIncludeAllRegionsChange={handleIncludeAllRegionsChange}
|
||||
onRegionSelect={handleRegionSelect}
|
||||
onSubmit={handleSubmit}
|
||||
accountId={accountId}
|
||||
selectedDeploymentRegion={selectedDeploymentRegion}
|
||||
handleRegionChange={handleRegionChange}
|
||||
connectionParams={connectionParams}
|
||||
isConnectionParamsLoading={isConnectionParamsLoading}
|
||||
/>
|
||||
);
|
||||
}, [
|
||||
modalState,
|
||||
activeView,
|
||||
form,
|
||||
setModalState,
|
||||
selectedRegions,
|
||||
includeAllRegions,
|
||||
handleIncludeAllRegionsChange,
|
||||
handleRegionSelect,
|
||||
handleSubmit,
|
||||
accountId,
|
||||
selectedDeploymentRegion,
|
||||
handleRegionChange,
|
||||
connectionParams,
|
||||
isConnectionParamsLoading,
|
||||
@@ -162,39 +162,28 @@ function CloudAccountSetupModal({
|
||||
|
||||
const modalConfig = getModalConfig();
|
||||
|
||||
const handleDrawerOpenChange = (open: boolean): void => {
|
||||
if (!open) {
|
||||
handleClose();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<DrawerWrapper
|
||||
open={true}
|
||||
type="panel"
|
||||
<SignozModal
|
||||
open
|
||||
className="cloud-account-setup-modal"
|
||||
// allowOutsideClick={false}
|
||||
content={renderContent()}
|
||||
onOpenChange={handleDrawerOpenChange}
|
||||
direction="right"
|
||||
showCloseButton
|
||||
header={{
|
||||
title: modalConfig.title,
|
||||
title={modalConfig.title}
|
||||
onCancel={handleClose}
|
||||
onOk={modalConfig.onOk}
|
||||
okText={modalConfig.okText}
|
||||
okButtonProps={{
|
||||
loading: isLoading,
|
||||
disabled: selectedRegions.length === 0 || modalConfig.disabled,
|
||||
className:
|
||||
activeView === ActiveViewEnum.FORM
|
||||
? 'cloud-account-setup-form__submit-button'
|
||||
: 'account-setup-modal-footer__confirm-button',
|
||||
block: activeView === ActiveViewEnum.FORM,
|
||||
}}
|
||||
// onCancel={handleClose}
|
||||
// onOk={modalConfig.onOk}
|
||||
// okText={modalConfig.okText}
|
||||
// okButtonProps={{
|
||||
// loading: isLoading,
|
||||
// disabled: selectedRegions.length === 0 || modalConfig.disabled,
|
||||
// className:
|
||||
// activeView === ActiveViewEnum.FORM
|
||||
// ? 'cloud-account-setup-form__submit-button'
|
||||
// : 'account-setup-modal-footer__confirm-button',
|
||||
// block: activeView === ActiveViewEnum.FORM,
|
||||
// }}
|
||||
// cancelButtonProps={modalConfig.cancelButtonProps}
|
||||
/>
|
||||
cancelButtonProps={modalConfig.cancelButtonProps}
|
||||
width={672}
|
||||
>
|
||||
{renderContent()}
|
||||
</SignozModal>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,19 +1,17 @@
|
||||
import { Dispatch, SetStateAction } from 'react';
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { Form, Select } from 'antd';
|
||||
import { Form, Select, Switch } from 'antd';
|
||||
import { ChevronDown } from 'lucide-react';
|
||||
import { Region } from 'utils/regions';
|
||||
import { popupContainer } from 'utils/selectPopupContainer';
|
||||
|
||||
import { RegionSelector } from './RegionSelector';
|
||||
|
||||
// Form section components
|
||||
function RegionDeploymentSection({
|
||||
regions,
|
||||
selectedDeploymentRegion,
|
||||
handleRegionChange,
|
||||
isFormDisabled,
|
||||
}: {
|
||||
regions: Region[];
|
||||
selectedDeploymentRegion: string | undefined;
|
||||
handleRegionChange: (value: string) => void;
|
||||
isFormDisabled: boolean;
|
||||
}): JSX.Element {
|
||||
@@ -35,8 +33,8 @@ function RegionDeploymentSection({
|
||||
suffixIcon={<ChevronDown size={16} color={Color.BG_VANILLA_400} />}
|
||||
className="cloud-account-setup-form__select integrations-select"
|
||||
onChange={handleRegionChange}
|
||||
value={selectedDeploymentRegion}
|
||||
disabled={isFormDisabled}
|
||||
getPopupContainer={popupContainer}
|
||||
>
|
||||
{regions.flatMap((region) =>
|
||||
region.subRegions.map((subRegion) => (
|
||||
@@ -52,13 +50,19 @@ function RegionDeploymentSection({
|
||||
}
|
||||
|
||||
function MonitoringRegionsSection({
|
||||
includeAllRegions,
|
||||
selectedRegions,
|
||||
setSelectedRegions,
|
||||
setIncludeAllRegions,
|
||||
onIncludeAllRegionsChange,
|
||||
getRegionPreviewText,
|
||||
onRegionSelect,
|
||||
isFormDisabled,
|
||||
}: {
|
||||
includeAllRegions: boolean;
|
||||
selectedRegions: string[];
|
||||
setSelectedRegions: Dispatch<SetStateAction<string[]>>;
|
||||
setIncludeAllRegions: Dispatch<SetStateAction<boolean>>;
|
||||
onIncludeAllRegionsChange: (checked: boolean) => void;
|
||||
getRegionPreviewText: (regions: string[]) => string[];
|
||||
onRegionSelect: () => void;
|
||||
isFormDisabled: boolean;
|
||||
}): JSX.Element {
|
||||
return (
|
||||
<div className="cloud-account-setup-form__form-group">
|
||||
@@ -69,12 +73,51 @@ function MonitoringRegionsSection({
|
||||
Choose only the regions you want SigNoz to monitor. You can enable all at
|
||||
once, or pick specific ones:
|
||||
</div>
|
||||
|
||||
<RegionSelector
|
||||
selectedRegions={selectedRegions}
|
||||
setSelectedRegions={setSelectedRegions}
|
||||
setIncludeAllRegions={setIncludeAllRegions}
|
||||
/>
|
||||
<Form.Item
|
||||
name="monitorRegions"
|
||||
rules={[
|
||||
{
|
||||
validator: async (): Promise<void> => {
|
||||
if (selectedRegions.length === 0) {
|
||||
return Promise.reject();
|
||||
}
|
||||
return Promise.resolve();
|
||||
},
|
||||
message: 'Please select at least one region to monitor',
|
||||
},
|
||||
]}
|
||||
className="cloud-account-setup-form__form-item"
|
||||
>
|
||||
<div className="cloud-account-setup-form__include-all-regions-switch">
|
||||
<Switch
|
||||
size="small"
|
||||
checked={includeAllRegions}
|
||||
onChange={onIncludeAllRegionsChange}
|
||||
disabled={isFormDisabled}
|
||||
/>
|
||||
<button
|
||||
className="cloud-account-setup-form__include-all-regions-switch-label"
|
||||
type="button"
|
||||
onClick={(): void =>
|
||||
!isFormDisabled
|
||||
? onIncludeAllRegionsChange(!includeAllRegions)
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
Include all regions
|
||||
</button>
|
||||
</div>
|
||||
<Select
|
||||
suffixIcon={null}
|
||||
placeholder="Select Region(s)"
|
||||
className="cloud-account-setup-form__select integrations-select"
|
||||
onClick={!isFormDisabled ? onRegionSelect : undefined}
|
||||
mode="multiple"
|
||||
maxTagCount={3}
|
||||
value={getRegionPreviewText(selectedRegions)}
|
||||
open={false}
|
||||
/>
|
||||
</Form.Item>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,12 +1,11 @@
|
||||
import { useRef } from 'react';
|
||||
import { Form } from 'antd';
|
||||
import cx from 'classnames';
|
||||
import { INTEGRATION_TYPES } from 'container/Integrations/constants';
|
||||
import { useGetAccountStatus } from 'hooks/integration/useGetAccountStatus';
|
||||
import { AccountStatusResponse } from 'types/api/integrations/types';
|
||||
import { useAccountStatus } from 'hooks/integration/aws/useAccountStatus';
|
||||
import { AccountStatusResponse } from 'types/api/integrations/aws';
|
||||
import { regions } from 'utils/regions';
|
||||
|
||||
import logEvent from '../../../../../../api/common/logEvent';
|
||||
import logEvent from '../../../../api/common/logEvent';
|
||||
import { ModalStateEnum, RegionFormProps } from '../types';
|
||||
import AlertMessage from './AlertMessage';
|
||||
import {
|
||||
@@ -16,48 +15,56 @@ import {
|
||||
} from './IntegrateNowFormSections';
|
||||
import RenderConnectionFields from './RenderConnectionParams';
|
||||
|
||||
const allRegions = (): string[] =>
|
||||
regions.flatMap((r) => r.subRegions.map((sr) => sr.name));
|
||||
|
||||
const getRegionPreviewText = (regions: string[]): string[] => {
|
||||
if (regions.includes('all')) {
|
||||
return allRegions();
|
||||
}
|
||||
return regions;
|
||||
};
|
||||
|
||||
export function RegionForm({
|
||||
form,
|
||||
modalState,
|
||||
setModalState,
|
||||
selectedRegions,
|
||||
includeAllRegions,
|
||||
onIncludeAllRegionsChange,
|
||||
onRegionSelect,
|
||||
onSubmit,
|
||||
accountId,
|
||||
selectedDeploymentRegion,
|
||||
handleRegionChange,
|
||||
connectionParams,
|
||||
isConnectionParamsLoading,
|
||||
setSelectedRegions,
|
||||
setIncludeAllRegions,
|
||||
}: RegionFormProps): JSX.Element {
|
||||
const startTimeRef = useRef(Date.now());
|
||||
const refetchInterval = 10 * 1000;
|
||||
const errorTimeout = 10 * 60 * 1000;
|
||||
|
||||
const { isLoading: isAccountStatusLoading } = useGetAccountStatus(
|
||||
INTEGRATION_TYPES.AWS,
|
||||
accountId,
|
||||
{
|
||||
refetchInterval,
|
||||
enabled: !!accountId && modalState === ModalStateEnum.WAITING,
|
||||
onSuccess: (data: AccountStatusResponse) => {
|
||||
if (data.data.status.integration.last_heartbeat_ts_ms !== null) {
|
||||
setModalState(ModalStateEnum.SUCCESS);
|
||||
logEvent('AWS Integration: Account connected', {
|
||||
cloudAccountId: data?.data?.cloud_account_id,
|
||||
status: data?.data?.status,
|
||||
});
|
||||
} else if (Date.now() - startTimeRef.current >= errorTimeout) {
|
||||
setModalState(ModalStateEnum.ERROR);
|
||||
logEvent('AWS Integration: Account connection attempt timed out', {
|
||||
id: accountId,
|
||||
});
|
||||
}
|
||||
},
|
||||
onError: () => {
|
||||
const { isLoading: isAccountStatusLoading } = useAccountStatus(accountId, {
|
||||
refetchInterval,
|
||||
enabled: !!accountId && modalState === ModalStateEnum.WAITING,
|
||||
onSuccess: (data: AccountStatusResponse) => {
|
||||
if (data.data.status.integration.last_heartbeat_ts_ms !== null) {
|
||||
setModalState(ModalStateEnum.SUCCESS);
|
||||
logEvent('AWS Integration: Account connected', {
|
||||
cloudAccountId: data?.data?.cloud_account_id,
|
||||
status: data?.data?.status,
|
||||
});
|
||||
} else if (Date.now() - startTimeRef.current >= errorTimeout) {
|
||||
setModalState(ModalStateEnum.ERROR);
|
||||
},
|
||||
logEvent('AWS Integration: Account connection attempt timed out', {
|
||||
id: accountId,
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
onError: () => {
|
||||
setModalState(ModalStateEnum.ERROR);
|
||||
},
|
||||
});
|
||||
|
||||
const isFormDisabled =
|
||||
modalState === ModalStateEnum.WAITING || isAccountStatusLoading;
|
||||
@@ -80,11 +87,15 @@ export function RegionForm({
|
||||
regions={regions}
|
||||
handleRegionChange={handleRegionChange}
|
||||
isFormDisabled={isFormDisabled}
|
||||
selectedDeploymentRegion={selectedDeploymentRegion}
|
||||
/>
|
||||
<MonitoringRegionsSection
|
||||
includeAllRegions={includeAllRegions}
|
||||
selectedRegions={selectedRegions}
|
||||
setSelectedRegions={setSelectedRegions}
|
||||
setIncludeAllRegions={setIncludeAllRegions}
|
||||
onIncludeAllRegionsChange={onIncludeAllRegionsChange}
|
||||
getRegionPreviewText={getRegionPreviewText}
|
||||
onRegionSelect={onRegionSelect}
|
||||
isFormDisabled={isFormDisabled}
|
||||
/>
|
||||
<ComplianceNote />
|
||||
<RenderConnectionFields
|
||||
@@ -1,6 +1,5 @@
|
||||
.select-all {
|
||||
margin-top: 16px;
|
||||
margin-bottom: 16px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.regions-grid {
|
||||
@@ -20,11 +19,3 @@
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.region-selector-footer {
|
||||
margin-top: 36px;
|
||||
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
.remove-integration-account {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 16px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid rgba(218, 85, 101, 0.2);
|
||||
background: rgba(218, 85, 101, 0.06);
|
||||
&__header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
&__title {
|
||||
color: var(--bg-cherry-500);
|
||||
font-size: 14px;
|
||||
letter-spacing: -0.07px;
|
||||
}
|
||||
|
||||
&__subtitle {
|
||||
color: var(--bg-cherry-300);
|
||||
font-size: 14px;
|
||||
line-height: 22px;
|
||||
letter-spacing: -0.07px;
|
||||
}
|
||||
|
||||
&__button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: var(--bg-cherry-500);
|
||||
border: none;
|
||||
color: var(--bg-vanilla-100);
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
line-height: 13.3px; /* 110.833% */
|
||||
padding: 9px 13px;
|
||||
.ant-btn-icon {
|
||||
margin-inline-end: 4px !important;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
&.ant-btn-default {
|
||||
color: var(--bg-vanilla-300) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,12 @@
|
||||
import { useState } from 'react';
|
||||
import { useMutation } from 'react-query';
|
||||
import { Button } from '@signozhq/button';
|
||||
import { Modal } from 'antd/lib';
|
||||
import { Button, Modal } from 'antd';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import removeAwsIntegrationAccount from 'api/integration/aws/removeAwsIntegrationAccount';
|
||||
import removeAwsIntegrationAccount from 'api/Integrations/removeAwsIntegrationAccount';
|
||||
import { SOMETHING_WENT_WRONG } from 'constants/api';
|
||||
import { INTEGRATION_TELEMETRY_EVENTS } from 'container/Integrations/constants';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import { Unlink } from 'lucide-react';
|
||||
import { X } from 'lucide-react';
|
||||
import { INTEGRATION_TELEMETRY_EVENTS } from 'pages/Integrations/utils';
|
||||
|
||||
import './RemoveIntegrationAccount.scss';
|
||||
|
||||
@@ -21,7 +20,7 @@ function RemoveIntegrationAccount({
|
||||
const { notifications } = useNotifications();
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
|
||||
const handleDisconnect = (): void => {
|
||||
const showModal = (): void => {
|
||||
setIsModalOpen(true);
|
||||
};
|
||||
|
||||
@@ -51,37 +50,42 @@ function RemoveIntegrationAccount({
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="remove-integration-account-container">
|
||||
<div className="remove-integration-account">
|
||||
<div className="remove-integration-account__header">
|
||||
<div className="remove-integration-account__title">Remove Integration</div>
|
||||
<div className="remove-integration-account__subtitle">
|
||||
Removing this integration won't delete any existing data but will stop
|
||||
collecting new data from AWS.
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
variant="solid"
|
||||
color="destructive"
|
||||
prefixIcon={<Unlink size={14} />}
|
||||
size="sm"
|
||||
onClick={handleDisconnect}
|
||||
disabled={isRemoveIntegrationLoading}
|
||||
className="remove-integration-account__button"
|
||||
icon={<X size={14} />}
|
||||
onClick={(): void => showModal()}
|
||||
>
|
||||
Disconnect
|
||||
Remove
|
||||
</Button>
|
||||
|
||||
<Modal
|
||||
className="remove-integration-account-modal"
|
||||
className="remove-integration-modal"
|
||||
open={isModalOpen}
|
||||
title="Remove integration"
|
||||
onOk={handleOk}
|
||||
onCancel={handleCancel}
|
||||
okText="Remove Account"
|
||||
okText="Remove Integration"
|
||||
okButtonProps={{
|
||||
danger: true,
|
||||
loading: isRemoveIntegrationLoading,
|
||||
disabled: isRemoveIntegrationLoading,
|
||||
}}
|
||||
>
|
||||
Removing this account will remove all components created for sending
|
||||
telemetry to SigNoz in your AWS account within the next ~15 minutes
|
||||
(cloudformation stacks named signoz-integration-telemetry-collection in
|
||||
enabled regions). <br />
|
||||
<br />
|
||||
After that, you can delete the cloudformation stack that was created
|
||||
manually when connecting this account.
|
||||
<div className="remove-integration-modal__text">
|
||||
Removing this account will remove all components created for sending
|
||||
telemetry to SigNoz in your AWS account within the next ~15 minutes
|
||||
(cloudformation stacks named signoz-integration-telemetry-collection in
|
||||
enabled regions). <br />
|
||||
<br />
|
||||
After that, you can delete the cloudformation stack that was created
|
||||
manually when connecting this account.
|
||||
</div>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Form, Input } from 'antd';
|
||||
import { ConnectionParams } from 'types/api/integrations/types';
|
||||
import { ConnectionParams } from 'types/api/integrations/aws';
|
||||
|
||||
function RenderConnectionFields({
|
||||
isConnectionParamsLoading,
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Dispatch, SetStateAction } from 'react';
|
||||
import { FormInstance } from 'antd';
|
||||
import { ConnectionParams } from 'types/api/integrations/types';
|
||||
import { ConnectionParams } from 'types/api/integrations/aws';
|
||||
|
||||
export enum ActiveViewEnum {
|
||||
SELECT_REGIONS = 'select-regions',
|
||||
@@ -20,14 +20,14 @@ export interface RegionFormProps {
|
||||
setModalState: Dispatch<SetStateAction<ModalStateEnum>>;
|
||||
selectedRegions: string[];
|
||||
includeAllRegions: boolean;
|
||||
onIncludeAllRegionsChange: (checked: boolean) => void;
|
||||
onRegionSelect: () => void;
|
||||
onSubmit: () => Promise<void>;
|
||||
accountId?: string;
|
||||
selectedDeploymentRegion: string | undefined;
|
||||
handleRegionChange: (value: string) => void;
|
||||
connectionParams?: ConnectionParams;
|
||||
isConnectionParamsLoading?: boolean;
|
||||
setSelectedRegions: Dispatch<SetStateAction<string[]>>;
|
||||
setIncludeAllRegions: Dispatch<SetStateAction<boolean>>;
|
||||
}
|
||||
|
||||
export interface IntegrationModalProps {
|
||||
@@ -0,0 +1,50 @@
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import { ServiceData } from './types';
|
||||
|
||||
function DashboardItem({
|
||||
dashboard,
|
||||
}: {
|
||||
dashboard: ServiceData['assets']['dashboards'][number];
|
||||
}): JSX.Element {
|
||||
const content = (
|
||||
<>
|
||||
<div className="cloud-service-dashboard-item__title">{dashboard.title}</div>
|
||||
<div className="cloud-service-dashboard-item__preview">
|
||||
<img
|
||||
src={dashboard.image}
|
||||
alt={dashboard.title}
|
||||
className="cloud-service-dashboard-item__preview-image"
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="cloud-service-dashboard-item">
|
||||
{dashboard.url ? (
|
||||
<Link to={dashboard.url} className="cloud-service-dashboard-item__link">
|
||||
{content}
|
||||
</Link>
|
||||
) : (
|
||||
content
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function CloudServiceDashboards({
|
||||
service,
|
||||
}: {
|
||||
service: ServiceData;
|
||||
}): JSX.Element {
|
||||
return (
|
||||
<>
|
||||
{service.assets.dashboards.map((dashboard) => (
|
||||
<DashboardItem key={dashboard.id} dashboard={dashboard} />
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default CloudServiceDashboards;
|
||||
@@ -1,8 +1,6 @@
|
||||
import { Table } from 'antd';
|
||||
import { ServiceData } from 'container/Integrations/CloudIntegration/AmazonWebServices/types';
|
||||
import { BarChart2, ScrollText } from 'lucide-react';
|
||||
|
||||
import './CloudServiceDataCollected.styles.scss';
|
||||
import { ServiceData } from './types';
|
||||
|
||||
function CloudServiceDataCollected({
|
||||
logsData,
|
||||
@@ -63,30 +61,24 @@ function CloudServiceDataCollected({
|
||||
return (
|
||||
<div className="cloud-service-data-collected">
|
||||
{logsData && logsData.length > 0 && (
|
||||
<div className="cloud-service-data-collected-table">
|
||||
<div className="cloud-service-data-collected-table-heading">
|
||||
<ScrollText size={14} />
|
||||
Logs
|
||||
</div>
|
||||
<div className="cloud-service-data-collected__table">
|
||||
<div className="cloud-service-data-collected__table-heading">Logs</div>
|
||||
<Table
|
||||
columns={logsColumns}
|
||||
dataSource={logsData}
|
||||
{...tableProps}
|
||||
className="cloud-service-data-collected-table-logs"
|
||||
className="cloud-service-data-collected__table-logs"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{metricsData && metricsData.length > 0 && (
|
||||
<div className="cloud-service-data-collected-table">
|
||||
<div className="cloud-service-data-collected-table-heading">
|
||||
<BarChart2 size={14} />
|
||||
Metrics
|
||||
</div>
|
||||
<div className="cloud-service-data-collected__table">
|
||||
<div className="cloud-service-data-collected__table-heading">Metrics</div>
|
||||
<Table
|
||||
columns={metricsColumns}
|
||||
dataSource={metricsData}
|
||||
{...tableProps}
|
||||
className="cloud-service-data-collected-table-metrics"
|
||||
className="cloud-service-data-collected__table-metrics"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
@@ -0,0 +1,89 @@
|
||||
.configure-service-modal {
|
||||
&__body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-radius: 3px;
|
||||
border: 1px solid var(--bg-slate-500);
|
||||
padding: 14px;
|
||||
|
||||
&-regions-switch-switch {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
|
||||
&-label {
|
||||
color: var(--bg-vanilla-100);
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
line-height: 20px;
|
||||
letter-spacing: -0.07px;
|
||||
}
|
||||
}
|
||||
|
||||
&-switch-description {
|
||||
margin-top: 4px;
|
||||
color: var(--bg-vanilla-400);
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
line-height: 18px;
|
||||
letter-spacing: -0.06px;
|
||||
}
|
||||
&-form-item {
|
||||
&:last-child {
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.ant-modal-body {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
.ant-modal-footer {
|
||||
margin: 0;
|
||||
padding-bottom: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.lightMode {
|
||||
.configure-service-modal {
|
||||
&__body {
|
||||
border-color: var(--bg-vanilla-300);
|
||||
|
||||
&-regions-switch-switch {
|
||||
&-label {
|
||||
color: var(--bg-ink-500);
|
||||
}
|
||||
}
|
||||
|
||||
&-switch-description {
|
||||
color: var(--bg-ink-400);
|
||||
}
|
||||
}
|
||||
|
||||
.ant-btn {
|
||||
&.ant-btn-default {
|
||||
background: var(--bg-vanilla-100);
|
||||
border: 1px solid var(--bg-vanilla-300);
|
||||
color: var(--bg-ink-400);
|
||||
|
||||
&:hover {
|
||||
border-color: var(--bg-vanilla-400);
|
||||
color: var(--bg-ink-500);
|
||||
}
|
||||
}
|
||||
|
||||
&.ant-btn-primary {
|
||||
// Keep primary button same as dark mode
|
||||
background: var(--bg-robin-500);
|
||||
color: var(--bg-vanilla-100);
|
||||
|
||||
&:hover {
|
||||
background: var(--bg-robin-400);
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.6;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,243 @@
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import { useQueryClient } from 'react-query';
|
||||
import { Form, Switch } from 'antd';
|
||||
import SignozModal from 'components/SignozModal/SignozModal';
|
||||
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||
import {
|
||||
ServiceConfig,
|
||||
SupportedSignals,
|
||||
} from 'container/CloudIntegrationPage/ServicesSection/types';
|
||||
import { useUpdateServiceConfig } from 'hooks/integration/aws/useUpdateServiceConfig';
|
||||
import { isEqual } from 'lodash-es';
|
||||
|
||||
import logEvent from '../../../api/common/logEvent';
|
||||
import S3BucketsSelector from './S3BucketsSelector';
|
||||
|
||||
import './ConfigureServiceModal.styles.scss';
|
||||
|
||||
export interface IConfigureServiceModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
serviceName: string;
|
||||
serviceId: string;
|
||||
cloudAccountId: string;
|
||||
supportedSignals: SupportedSignals;
|
||||
initialConfig?: ServiceConfig;
|
||||
}
|
||||
|
||||
function ConfigureServiceModal({
|
||||
isOpen,
|
||||
onClose,
|
||||
serviceName,
|
||||
serviceId,
|
||||
cloudAccountId,
|
||||
initialConfig,
|
||||
supportedSignals,
|
||||
}: IConfigureServiceModalProps): JSX.Element {
|
||||
const [form] = Form.useForm();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
// Track current form values
|
||||
const initialValues = useMemo(
|
||||
() => ({
|
||||
metrics: initialConfig?.metrics?.enabled || false,
|
||||
logs: initialConfig?.logs?.enabled || false,
|
||||
s3Buckets: initialConfig?.logs?.s3_buckets || {},
|
||||
}),
|
||||
[initialConfig],
|
||||
);
|
||||
const [currentValues, setCurrentValues] = useState(initialValues);
|
||||
|
||||
const isSaveDisabled = useMemo(
|
||||
() =>
|
||||
// disable only if current values are same as the initial config
|
||||
currentValues.metrics === initialValues.metrics &&
|
||||
currentValues.logs === initialValues.logs &&
|
||||
isEqual(currentValues.s3Buckets, initialValues.s3Buckets),
|
||||
[currentValues, initialValues],
|
||||
);
|
||||
|
||||
const handleS3BucketsChange = useCallback(
|
||||
(bucketsByRegion: Record<string, string[]>) => {
|
||||
setCurrentValues((prev) => ({
|
||||
...prev,
|
||||
s3Buckets: bucketsByRegion,
|
||||
}));
|
||||
form.setFieldsValue({ s3Buckets: bucketsByRegion });
|
||||
},
|
||||
[form],
|
||||
);
|
||||
|
||||
const {
|
||||
mutate: updateServiceConfig,
|
||||
isLoading: isUpdating,
|
||||
} = useUpdateServiceConfig();
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const handleSubmit = useCallback(async (): Promise<void> => {
|
||||
try {
|
||||
const values = await form.validateFields();
|
||||
setIsLoading(true);
|
||||
|
||||
updateServiceConfig(
|
||||
{
|
||||
serviceId,
|
||||
payload: {
|
||||
cloud_account_id: cloudAccountId,
|
||||
config: {
|
||||
logs: {
|
||||
enabled: values.logs,
|
||||
s3_buckets: values.s3Buckets,
|
||||
},
|
||||
metrics: {
|
||||
enabled: values.metrics,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries([
|
||||
REACT_QUERY_KEY.AWS_SERVICE_DETAILS,
|
||||
serviceId,
|
||||
]);
|
||||
onClose();
|
||||
|
||||
logEvent('AWS Integration: Service settings saved', {
|
||||
cloudAccountId,
|
||||
serviceId,
|
||||
logsEnabled: values?.logs,
|
||||
metricsEnabled: values?.metrics,
|
||||
});
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error('Failed to update service config:', error);
|
||||
},
|
||||
},
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('Form submission failed:', error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [
|
||||
form,
|
||||
updateServiceConfig,
|
||||
serviceId,
|
||||
cloudAccountId,
|
||||
queryClient,
|
||||
onClose,
|
||||
]);
|
||||
|
||||
const handleClose = useCallback(() => {
|
||||
form.resetFields();
|
||||
onClose();
|
||||
}, [form, onClose]);
|
||||
|
||||
return (
|
||||
<SignozModal
|
||||
title={
|
||||
<div className="account-settings-modal__title">Configure {serviceName}</div>
|
||||
}
|
||||
centered
|
||||
open={isOpen}
|
||||
okText="Save"
|
||||
okButtonProps={{
|
||||
disabled: isSaveDisabled,
|
||||
className: 'account-settings-modal__footer-save-button',
|
||||
loading: isLoading || isUpdating,
|
||||
}}
|
||||
onCancel={handleClose}
|
||||
onOk={handleSubmit}
|
||||
cancelText="Close"
|
||||
cancelButtonProps={{
|
||||
className: 'account-settings-modal__footer-close-button',
|
||||
}}
|
||||
width={672}
|
||||
rootClassName=" configure-service-modal"
|
||||
>
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
initialValues={{
|
||||
metrics: initialConfig?.metrics?.enabled || false,
|
||||
logs: initialConfig?.logs?.enabled || false,
|
||||
s3Buckets: initialConfig?.logs?.s3_buckets || {},
|
||||
}}
|
||||
>
|
||||
<div className=" configure-service-modal__body">
|
||||
{supportedSignals.metrics && (
|
||||
<Form.Item
|
||||
name="metrics"
|
||||
valuePropName="checked"
|
||||
className="configure-service-modal__body-form-item"
|
||||
>
|
||||
<div className="configure-service-modal__body-regions-switch-switch">
|
||||
<Switch
|
||||
checked={currentValues.metrics}
|
||||
onChange={(checked): void => {
|
||||
setCurrentValues((prev) => ({ ...prev, metrics: checked }));
|
||||
form.setFieldsValue({ metrics: checked });
|
||||
}}
|
||||
/>
|
||||
<span className="configure-service-modal__body-regions-switch-switch-label">
|
||||
Metric Collection
|
||||
</span>
|
||||
</div>
|
||||
<div className="configure-service-modal__body-switch-description">
|
||||
Metric Collection is enabled for this AWS account. We recommend keeping
|
||||
this enabled, but you can disable metric collection if you do not want
|
||||
to monitor your AWS infrastructure.
|
||||
</div>
|
||||
</Form.Item>
|
||||
)}
|
||||
|
||||
{supportedSignals.logs && (
|
||||
<>
|
||||
<Form.Item
|
||||
name="logs"
|
||||
valuePropName="checked"
|
||||
className="configure-service-modal__body-form-item"
|
||||
>
|
||||
<div className="configure-service-modal__body-regions-switch-switch">
|
||||
<Switch
|
||||
checked={currentValues.logs}
|
||||
onChange={(checked): void => {
|
||||
setCurrentValues((prev) => ({ ...prev, logs: checked }));
|
||||
form.setFieldsValue({ logs: checked });
|
||||
}}
|
||||
/>
|
||||
<span className="configure-service-modal__body-regions-switch-switch-label">
|
||||
Log Collection
|
||||
</span>
|
||||
</div>
|
||||
<div className="configure-service-modal__body-switch-description">
|
||||
To ingest logs from your AWS services, you must complete several steps
|
||||
</div>
|
||||
</Form.Item>
|
||||
|
||||
{currentValues.logs && serviceId === 's3sync' && (
|
||||
<Form.Item name="s3Buckets" noStyle>
|
||||
<S3BucketsSelector
|
||||
initialBucketsByRegion={currentValues.s3Buckets}
|
||||
onChange={handleS3BucketsChange}
|
||||
/>
|
||||
</Form.Item>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</Form>
|
||||
</SignozModal>
|
||||
);
|
||||
}
|
||||
|
||||
ConfigureServiceModal.defaultProps = {
|
||||
initialConfig: {
|
||||
metrics: { enabled: false },
|
||||
logs: { enabled: false },
|
||||
},
|
||||
};
|
||||
|
||||
export default ConfigureServiceModal;
|
||||
@@ -1,9 +1,9 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { Select, Skeleton } from 'antd';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import { Form, Select, Skeleton, Typography } from 'antd';
|
||||
import { useAwsAccounts } from 'hooks/integration/aws/useAwsAccounts';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
|
||||
import './S3BucketsSelector.styles.scss';
|
||||
const { Title } = Typography;
|
||||
|
||||
interface S3BucketsSelectorProps {
|
||||
onChange?: (bucketsByRegion: Record<string, string[]>) => void;
|
||||
@@ -24,10 +24,6 @@ function S3BucketsSelector({
|
||||
Record<string, string[]>
|
||||
>(initialBucketsByRegion);
|
||||
|
||||
useEffect(() => {
|
||||
setBucketsByRegion(initialBucketsByRegion);
|
||||
}, [initialBucketsByRegion]);
|
||||
|
||||
// Find the active AWS account based on the URL query parameter
|
||||
const activeAccount = useMemo(
|
||||
() =>
|
||||
@@ -85,41 +81,37 @@ function S3BucketsSelector({
|
||||
|
||||
return (
|
||||
<div className="s3-buckets-selector">
|
||||
<div className="s3-buckets-selector-title">Select S3 Buckets by Region</div>
|
||||
<div className="s3-buckets-selector-content">
|
||||
{allRegions.map((region) => {
|
||||
const disabled = isRegionDisabled(region);
|
||||
<Title level={5}>Select S3 Buckets by Region</Title>
|
||||
|
||||
return (
|
||||
<div key={region} className="s3-buckets-selector-region">
|
||||
<div className="s3-buckets-selector-region-header">
|
||||
<div className="s3-buckets-selector-region-label">{region}</div>
|
||||
{disabled && (
|
||||
<div className="s3-buckets-selector-region-help">
|
||||
Region disabled in account settings; S3 buckets here will not be
|
||||
synced.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="s3-buckets-selector-region-select">
|
||||
<Select
|
||||
mode="tags"
|
||||
placeholder={`Enter S3 bucket names for ${region}`}
|
||||
value={bucketsByRegion[region] || []}
|
||||
onChange={(value): void => handleRegionBucketsChange(region, value)}
|
||||
tokenSeparators={[',']}
|
||||
allowClear
|
||||
disabled={disabled}
|
||||
suffixIcon={null}
|
||||
notFoundContent={null}
|
||||
filterOption={false}
|
||||
showSearch
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
{allRegions.map((region) => {
|
||||
const disabled = isRegionDisabled(region);
|
||||
|
||||
return (
|
||||
<Form.Item
|
||||
key={region}
|
||||
label={region}
|
||||
{...(disabled && {
|
||||
help:
|
||||
'Region disabled in account settings; S3 buckets here will not be synced.',
|
||||
validateStatus: 'warning',
|
||||
})}
|
||||
>
|
||||
<Select
|
||||
mode="tags"
|
||||
placeholder={`Enter S3 bucket names for ${region}`}
|
||||
value={bucketsByRegion[region] || []}
|
||||
onChange={(value): void => handleRegionBucketsChange(region, value)}
|
||||
tokenSeparators={[',']}
|
||||
allowClear
|
||||
disabled={disabled}
|
||||
suffixIcon={null}
|
||||
notFoundContent={null}
|
||||
filterOption={false}
|
||||
showSearch
|
||||
/>
|
||||
</Form.Item>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,189 @@
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { Button, Tabs, TabsProps } from 'antd';
|
||||
import { MarkdownRenderer } from 'components/MarkdownRenderer/MarkdownRenderer';
|
||||
import Spinner from 'components/Spinner';
|
||||
import CloudServiceDashboards from 'container/CloudIntegrationPage/ServicesSection/CloudServiceDashboards';
|
||||
import CloudServiceDataCollected from 'container/CloudIntegrationPage/ServicesSection/CloudServiceDataCollected';
|
||||
import { IServiceStatus } from 'container/CloudIntegrationPage/ServicesSection/types';
|
||||
import dayjs from 'dayjs';
|
||||
import { useServiceDetails } from 'hooks/integration/aws/useServiceDetails';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
|
||||
import logEvent from '../../../api/common/logEvent';
|
||||
import ConfigureServiceModal from './ConfigureServiceModal';
|
||||
|
||||
const getStatus = (
|
||||
logsLastReceivedTimestamp: number | undefined,
|
||||
metricsLastReceivedTimestamp: number | undefined,
|
||||
): { text: string; className: string } => {
|
||||
if (!logsLastReceivedTimestamp && !metricsLastReceivedTimestamp) {
|
||||
return { text: 'No Data Yet', className: 'service-status--no-data' };
|
||||
}
|
||||
|
||||
const latestTimestamp = Math.max(
|
||||
logsLastReceivedTimestamp || 0,
|
||||
metricsLastReceivedTimestamp || 0,
|
||||
);
|
||||
|
||||
const isStale = dayjs().diff(dayjs(latestTimestamp), 'minute') > 30;
|
||||
|
||||
if (isStale) {
|
||||
return { text: 'Stale Data', className: 'service-status--stale-data' };
|
||||
}
|
||||
|
||||
return { text: 'Connected', className: 'service-status--connected' };
|
||||
};
|
||||
|
||||
function ServiceStatus({
|
||||
serviceStatus,
|
||||
}: {
|
||||
serviceStatus: IServiceStatus | undefined;
|
||||
}): JSX.Element {
|
||||
const logsLastReceivedTimestamp = serviceStatus?.logs?.last_received_ts_ms;
|
||||
const metricsLastReceivedTimestamp =
|
||||
serviceStatus?.metrics?.last_received_ts_ms;
|
||||
|
||||
const { text, className } = getStatus(
|
||||
logsLastReceivedTimestamp,
|
||||
metricsLastReceivedTimestamp,
|
||||
);
|
||||
|
||||
return <div className={`service-status ${className}`}>{text}</div>;
|
||||
}
|
||||
|
||||
function getTabItems(serviceDetailsData: any): TabsProps['items'] {
|
||||
const dashboards = serviceDetailsData?.assets.dashboards || [];
|
||||
const dataCollected = serviceDetailsData?.data_collected || {};
|
||||
const items: TabsProps['items'] = [];
|
||||
|
||||
if (dashboards.length) {
|
||||
items.push({
|
||||
key: 'dashboards',
|
||||
label: `Dashboards (${dashboards.length})`,
|
||||
children: <CloudServiceDashboards service={serviceDetailsData} />,
|
||||
});
|
||||
}
|
||||
|
||||
items.push({
|
||||
key: 'data-collected',
|
||||
label: 'Data Collected',
|
||||
children: (
|
||||
<CloudServiceDataCollected
|
||||
logsData={dataCollected.logs || []}
|
||||
metricsData={dataCollected.metrics || []}
|
||||
/>
|
||||
),
|
||||
});
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
function ServiceDetails(): JSX.Element | null {
|
||||
const urlQuery = useUrlQuery();
|
||||
const cloudAccountId = urlQuery.get('cloudAccountId');
|
||||
const serviceId = urlQuery.get('service');
|
||||
const [isConfigureServiceModalOpen, setIsConfigureServiceModalOpen] = useState(
|
||||
false,
|
||||
);
|
||||
const openServiceConfigModal = (): void => {
|
||||
setIsConfigureServiceModalOpen(true);
|
||||
logEvent('AWS Integration: Service settings viewed', {
|
||||
cloudAccountId,
|
||||
serviceId,
|
||||
});
|
||||
};
|
||||
|
||||
const { data: serviceDetailsData, isLoading } = useServiceDetails(
|
||||
serviceId || '',
|
||||
cloudAccountId || undefined,
|
||||
);
|
||||
|
||||
const { config, supported_signals } = serviceDetailsData ?? {};
|
||||
|
||||
const totalSupportedSignals = Object.entries(supported_signals || {}).filter(
|
||||
([, value]) => !!value,
|
||||
).length;
|
||||
const enabledSignals = useMemo(
|
||||
() =>
|
||||
Object.values(config || {}).filter((item) => item && item.enabled).length,
|
||||
[config],
|
||||
);
|
||||
|
||||
const isAnySignalConfigured = useMemo(
|
||||
() => !!config?.logs?.enabled || !!config?.metrics?.enabled,
|
||||
[config],
|
||||
);
|
||||
|
||||
// log telemetry event on visiting details of a service.
|
||||
useEffect(() => {
|
||||
if (serviceId) {
|
||||
logEvent('AWS Integration: Service viewed', {
|
||||
cloudAccountId,
|
||||
serviceId,
|
||||
});
|
||||
}
|
||||
}, [cloudAccountId, serviceId]);
|
||||
|
||||
if (isLoading) {
|
||||
return <Spinner size="large" height="50vh" />;
|
||||
}
|
||||
|
||||
if (!serviceDetailsData) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const tabItems = getTabItems(serviceDetailsData);
|
||||
|
||||
return (
|
||||
<div className="service-details">
|
||||
<div className="service-details__title-bar">
|
||||
<div className="service-details__details-title">Details</div>
|
||||
<div className="service-details__right-actions">
|
||||
{isAnySignalConfigured && (
|
||||
<ServiceStatus serviceStatus={serviceDetailsData.status} />
|
||||
)}
|
||||
|
||||
{!!cloudAccountId &&
|
||||
(isAnySignalConfigured ? (
|
||||
<Button
|
||||
className="configure-button configure-button--default"
|
||||
onClick={openServiceConfigModal}
|
||||
>
|
||||
Configure ({enabledSignals}/{totalSupportedSignals})
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
type="primary"
|
||||
className="configure-button configure-button--primary"
|
||||
onClick={openServiceConfigModal}
|
||||
>
|
||||
Enable Service
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="service-details__overview">
|
||||
<MarkdownRenderer
|
||||
variables={{}}
|
||||
markdownContent={serviceDetailsData?.overview}
|
||||
/>
|
||||
</div>
|
||||
<div className="service-details__tabs">
|
||||
<Tabs items={tabItems} />
|
||||
</div>
|
||||
{isConfigureServiceModalOpen && (
|
||||
<ConfigureServiceModal
|
||||
isOpen
|
||||
onClose={(): void => setIsConfigureServiceModalOpen(false)}
|
||||
serviceName={serviceDetailsData.title}
|
||||
serviceId={serviceId || ''}
|
||||
cloudAccountId={cloudAccountId || ''}
|
||||
initialConfig={serviceDetailsData.config}
|
||||
supportedSignals={serviceDetailsData.supported_signals || {}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ServiceDetails;
|
||||
@@ -0,0 +1,75 @@
|
||||
import { useCallback, useEffect, useMemo } from 'react';
|
||||
import { useNavigate } from 'react-router-dom-v5-compat';
|
||||
import Spinner from 'components/Spinner';
|
||||
import { useGetAccountServices } from 'hooks/integration/aws/useGetAccountServices';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
|
||||
import ServiceItem from './ServiceItem';
|
||||
|
||||
interface ServicesListProps {
|
||||
cloudAccountId: string;
|
||||
filter: 'all_services' | 'enabled' | 'available';
|
||||
}
|
||||
|
||||
function ServicesList({
|
||||
cloudAccountId,
|
||||
filter,
|
||||
}: ServicesListProps): JSX.Element {
|
||||
const urlQuery = useUrlQuery();
|
||||
const navigate = useNavigate();
|
||||
const { data: services = [], isLoading } = useGetAccountServices(
|
||||
cloudAccountId,
|
||||
);
|
||||
const activeService = urlQuery.get('service');
|
||||
|
||||
const handleActiveService = useCallback(
|
||||
(serviceId: string): void => {
|
||||
const latestUrlQuery = new URLSearchParams(window.location.search);
|
||||
latestUrlQuery.set('service', serviceId);
|
||||
navigate({ search: latestUrlQuery.toString() });
|
||||
},
|
||||
[navigate],
|
||||
);
|
||||
|
||||
const filteredServices = useMemo(() => {
|
||||
if (filter === 'all_services') {
|
||||
return services;
|
||||
}
|
||||
|
||||
return services.filter((service) => {
|
||||
const isEnabled =
|
||||
service?.config?.logs?.enabled || service?.config?.metrics?.enabled;
|
||||
return filter === 'enabled' ? isEnabled : !isEnabled;
|
||||
});
|
||||
}, [services, filter]);
|
||||
|
||||
useEffect(() => {
|
||||
if (activeService || !services?.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
handleActiveService(services[0].id);
|
||||
}, [services, activeService, handleActiveService]);
|
||||
|
||||
if (isLoading) {
|
||||
return <Spinner size="large" height="25vh" />;
|
||||
}
|
||||
if (!services) {
|
||||
return <div>No services found</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="services-list">
|
||||
{filteredServices.map((service) => (
|
||||
<ServiceItem
|
||||
key={service.id}
|
||||
service={service}
|
||||
onClick={handleActiveService}
|
||||
isActive={service.id === activeService}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ServicesList;
|
||||
@@ -1,8 +1,4 @@
|
||||
.services-tabs {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: calc(100% - 54px); /* 54px is the height of the header */
|
||||
|
||||
.ant-tabs-tab {
|
||||
font-family: 'Inter';
|
||||
padding: 16px 4px 14px;
|
||||
@@ -22,42 +18,21 @@
|
||||
background: var(--bg-robin-500);
|
||||
}
|
||||
}
|
||||
|
||||
.services-section {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
|
||||
gap: 10px;
|
||||
&__sidebar {
|
||||
width: 240px;
|
||||
border-right: 1px solid var(--bg-slate-400);
|
||||
height: 100%;
|
||||
width: 16%;
|
||||
padding: 0 16px;
|
||||
}
|
||||
|
||||
&__content {
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
width: 84%;
|
||||
padding: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.service-details-loading,
|
||||
.services-list-loading {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
|
||||
.service-details-loading-item {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: var(--bg-slate-400);
|
||||
}
|
||||
}
|
||||
|
||||
.services-filter {
|
||||
padding: 12px;
|
||||
|
||||
padding: 16px 0;
|
||||
.ant-select-selector {
|
||||
background-color: var(--bg-ink-300) !important;
|
||||
border: 1px solid var(--bg-slate-400) !important;
|
||||
@@ -71,99 +46,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
.aws-services-list-view {
|
||||
height: 100%;
|
||||
|
||||
.aws-services-list-view-sidebar {
|
||||
width: 240px;
|
||||
height: 100%;
|
||||
border-right: 1px solid var(--l3-background);
|
||||
padding: 12px;
|
||||
|
||||
.aws-services-list-view-sidebar-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
.aws-services-enabled {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.aws-services-not-enabled {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.aws-services-list-view-sidebar-content-header {
|
||||
color: var(--l2-foreground);
|
||||
font-family: Inter;
|
||||
font-size: 11px;
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
line-height: 18px; /* 163.636% */
|
||||
letter-spacing: 0.44px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.aws-services-list-view-sidebar-content-item {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 4px 12px;
|
||||
border-radius: 4px;
|
||||
|
||||
cursor: pointer;
|
||||
|
||||
.aws-services-list-view-sidebar-content-item-icon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.aws-services-list-view-sidebar-content-item-title {
|
||||
color: var(--l1-foreground);
|
||||
font-family: Inter;
|
||||
font-size: 13px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 18px; /* 128.571% */
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: var(--l1-foreground);
|
||||
font-family: Inter;
|
||||
font-size: 13px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 18px; /* 128.571% */
|
||||
}
|
||||
|
||||
&.active {
|
||||
color: var(--l1-foreground);
|
||||
font-family: Inter;
|
||||
font-size: 13px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 18px; /* 128.571% */
|
||||
|
||||
background-color: var(--l3-background);
|
||||
|
||||
.aws-services-list-view-sidebar-content-item-title {
|
||||
color: var(--l1-foreground);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.aws-services-list-view-main {
|
||||
flex: 1;
|
||||
padding: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.service-item {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
@@ -181,19 +63,17 @@
|
||||
background-color: var(--bg-ink-100);
|
||||
}
|
||||
&__icon-wrapper {
|
||||
height: 32px;
|
||||
width: 32px;
|
||||
height: 40px;
|
||||
width: 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: var(--bg-ink-300);
|
||||
border: 1px solid var(--bg-slate-400);
|
||||
border-radius: 4px;
|
||||
|
||||
.service-item__icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
object-fit: contain;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
}
|
||||
&__title {
|
||||
@@ -210,13 +90,11 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
|
||||
&__title-bar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
height: 48px;
|
||||
padding: 8px 12px;
|
||||
border-bottom: 1px solid var(--bg-slate-400);
|
||||
|
||||
.service-details__details-title {
|
||||
@@ -227,7 +105,6 @@
|
||||
letter-spacing: -0.07px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.service-details__right-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -280,28 +157,21 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__overview {
|
||||
color: var(--bg-vanilla-400);
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
line-height: 20px; /* 142.857% */
|
||||
letter-spacing: -0.07px;
|
||||
width: 100%;
|
||||
|
||||
padding: 8px 12px;
|
||||
width: 800px;
|
||||
}
|
||||
|
||||
&__tabs {
|
||||
padding: 0px 12px 12px 8px;
|
||||
|
||||
.ant-tabs {
|
||||
&-ink-bar {
|
||||
background-color: transparent;
|
||||
}
|
||||
&-nav {
|
||||
padding: 0;
|
||||
|
||||
padding: 8px 0 18px;
|
||||
&-wrap {
|
||||
padding: 0;
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
import { useMemo, useState } from 'react';
|
||||
import { useQuery } from 'react-query';
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import type { SelectProps, TabsProps } from 'antd';
|
||||
import { Select, Tabs } from 'antd';
|
||||
import { getAwsServices } from 'api/integration/aws';
|
||||
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
import { ChevronDown } from 'lucide-react';
|
||||
|
||||
import ServiceDetails from './ServiceDetails';
|
||||
import ServicesList from './ServicesList';
|
||||
|
||||
import './ServicesTabs.style.scss';
|
||||
|
||||
export enum ServiceFilterType {
|
||||
ALL_SERVICES = 'all_services',
|
||||
ENABLED = 'enabled',
|
||||
AVAILABLE = 'available',
|
||||
}
|
||||
|
||||
interface ServicesFilterProps {
|
||||
cloudAccountId: string;
|
||||
onFilterChange: (value: ServiceFilterType) => void;
|
||||
}
|
||||
|
||||
function ServicesFilter({
|
||||
cloudAccountId,
|
||||
onFilterChange,
|
||||
}: ServicesFilterProps): JSX.Element | null {
|
||||
const { data: services, isLoading } = useQuery(
|
||||
[REACT_QUERY_KEY.AWS_SERVICES, cloudAccountId],
|
||||
() => getAwsServices(cloudAccountId),
|
||||
);
|
||||
|
||||
const { enabledCount, availableCount } = useMemo(() => {
|
||||
if (!services) {
|
||||
return { enabledCount: 0, availableCount: 0 };
|
||||
}
|
||||
|
||||
return services.reduce(
|
||||
(acc, service) => {
|
||||
const isEnabled =
|
||||
service?.config?.logs?.enabled || service?.config?.metrics?.enabled;
|
||||
return {
|
||||
enabledCount: acc.enabledCount + (isEnabled ? 1 : 0),
|
||||
availableCount: acc.availableCount + (isEnabled ? 0 : 1),
|
||||
};
|
||||
},
|
||||
{ enabledCount: 0, availableCount: 0 },
|
||||
);
|
||||
}, [services]);
|
||||
|
||||
const selectOptions: SelectProps['options'] = useMemo(
|
||||
() => [
|
||||
{ value: 'all_services', label: `All Services (${services?.length || 0})` },
|
||||
{ value: 'enabled', label: `Enabled (${enabledCount})` },
|
||||
{ value: 'available', label: `Available (${availableCount})` },
|
||||
],
|
||||
[services, enabledCount, availableCount],
|
||||
);
|
||||
|
||||
if (isLoading) {
|
||||
return null;
|
||||
}
|
||||
if (!services?.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="services-filter">
|
||||
<Select
|
||||
style={{ width: '100%' }}
|
||||
defaultValue={ServiceFilterType.ALL_SERVICES}
|
||||
options={selectOptions}
|
||||
className="services-sidebar__select"
|
||||
suffixIcon={<ChevronDown size={16} color={Color.BG_VANILLA_400} />}
|
||||
onChange={onFilterChange}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ServicesSection(): JSX.Element {
|
||||
const urlQuery = useUrlQuery();
|
||||
const cloudAccountId = urlQuery.get('cloudAccountId') || '';
|
||||
|
||||
const [activeFilter, setActiveFilter] = useState<
|
||||
'all_services' | 'enabled' | 'available'
|
||||
>('all_services');
|
||||
|
||||
return (
|
||||
<div className="services-section">
|
||||
<div className="services-section__sidebar">
|
||||
<ServicesFilter
|
||||
cloudAccountId={cloudAccountId}
|
||||
onFilterChange={setActiveFilter}
|
||||
/>
|
||||
<ServicesList cloudAccountId={cloudAccountId} filter={activeFilter} />
|
||||
</div>
|
||||
<div className="services-section__content">
|
||||
<ServiceDetails />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ServicesTabs(): JSX.Element {
|
||||
const tabItems: TabsProps['items'] = [
|
||||
{
|
||||
key: 'services',
|
||||
label: 'Services For Integration',
|
||||
children: <ServicesSection />,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="services-tabs">
|
||||
<Tabs defaultActiveKey="services" items={tabItems} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ServicesTabs;
|
||||
@@ -0,0 +1,161 @@
|
||||
import { act, fireEvent, screen, waitFor } from '@testing-library/react';
|
||||
import { server } from 'mocks-server/server';
|
||||
import { rest, RestRequest } from 'msw'; // Import RestRequest for req.json() typing
|
||||
|
||||
import { UpdateServiceConfigPayload } from '../types';
|
||||
import { accountsResponse, CLOUD_ACCOUNT_ID, initialBuckets } from './mockData';
|
||||
import {
|
||||
assertGenericModalElements,
|
||||
assertS3SyncSpecificElements,
|
||||
renderModal,
|
||||
} from './utils';
|
||||
|
||||
// --- MOCKS ---
|
||||
jest.mock('hooks/useUrlQuery', () => ({
|
||||
__esModule: true,
|
||||
default: jest.fn(() => ({
|
||||
get: jest.fn((paramName: string) => {
|
||||
if (paramName === 'cloudAccountId') {
|
||||
return CLOUD_ACCOUNT_ID;
|
||||
}
|
||||
return null;
|
||||
}),
|
||||
})),
|
||||
}));
|
||||
|
||||
// --- TEST SUITE ---
|
||||
describe('ConfigureServiceModal for S3 Sync service', () => {
|
||||
jest.setTimeout(10000);
|
||||
beforeEach(() => {
|
||||
server.use(
|
||||
rest.get(
|
||||
'http://localhost/api/v1/cloud-integrations/aws/accounts',
|
||||
(req, res, ctx) => res(ctx.json(accountsResponse)),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
it('should render with logs collection switch and bucket selectors (no buckets initially selected)', async () => {
|
||||
act(() => {
|
||||
renderModal({}); // No initial S3 buckets, defaults to 's3sync' serviceId
|
||||
});
|
||||
await assertGenericModalElements(); // Use new generic assertion
|
||||
await assertS3SyncSpecificElements({}); // Use new S3-specific assertion
|
||||
});
|
||||
|
||||
it('should render with logs collection switch and bucket selectors (some buckets initially selected)', async () => {
|
||||
act(() => {
|
||||
renderModal(initialBuckets); // Defaults to 's3sync' serviceId
|
||||
});
|
||||
await assertGenericModalElements(); // Use new generic assertion
|
||||
await assertS3SyncSpecificElements(initialBuckets); // Use new S3-specific assertion
|
||||
});
|
||||
|
||||
it('should enable save button after adding a new bucket via combobox', async () => {
|
||||
act(() => {
|
||||
renderModal(initialBuckets); // Defaults to 's3sync' serviceId
|
||||
});
|
||||
await assertGenericModalElements();
|
||||
await assertS3SyncSpecificElements(initialBuckets);
|
||||
|
||||
expect(screen.getByRole('button', { name: /save/i })).toBeDisabled();
|
||||
|
||||
const targetCombobox = screen.getAllByRole('combobox')[0];
|
||||
const newBucketName = 'a-newly-added-bucket';
|
||||
|
||||
act(() => {
|
||||
fireEvent.change(targetCombobox, { target: { value: newBucketName } });
|
||||
fireEvent.keyDown(targetCombobox, {
|
||||
key: 'Enter',
|
||||
code: 'Enter',
|
||||
keyCode: 13,
|
||||
});
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByLabelText(newBucketName)).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: /save/i })).toBeEnabled();
|
||||
});
|
||||
});
|
||||
|
||||
it('should send updated bucket configuration on save', async () => {
|
||||
let capturedPayload: UpdateServiceConfigPayload | null = null;
|
||||
const mockUpdateConfigUrl =
|
||||
'http://localhost/api/v1/cloud-integrations/aws/services/s3sync/config';
|
||||
|
||||
// Override POST handler specifically for this test to capture payload
|
||||
server.use(
|
||||
rest.post(mockUpdateConfigUrl, async (req: RestRequest, res, ctx) => {
|
||||
capturedPayload = await req.json();
|
||||
return res(ctx.status(200), ctx.json({ message: 'Config updated' }));
|
||||
}),
|
||||
);
|
||||
act(() => {
|
||||
renderModal(initialBuckets); // Defaults to 's3sync' serviceId
|
||||
});
|
||||
await assertGenericModalElements();
|
||||
await assertS3SyncSpecificElements(initialBuckets);
|
||||
|
||||
expect(screen.getByRole('button', { name: /save/i })).toBeDisabled();
|
||||
|
||||
const newBucketName = 'another-new-bucket';
|
||||
// As before, targeting the first combobox, assumed to be for 'ap-south-1'.
|
||||
const targetCombobox = screen.getAllByRole('combobox')[0];
|
||||
|
||||
act(() => {
|
||||
fireEvent.change(targetCombobox, { target: { value: newBucketName } });
|
||||
fireEvent.keyDown(targetCombobox, {
|
||||
key: 'Enter',
|
||||
code: 'Enter',
|
||||
keyCode: 13,
|
||||
});
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByLabelText(newBucketName)).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: /save/i })).toBeEnabled();
|
||||
act(() => {
|
||||
fireEvent.click(screen.getByRole('button', { name: /save/i }));
|
||||
});
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(capturedPayload).not.toBeNull();
|
||||
});
|
||||
|
||||
expect(capturedPayload).toEqual({
|
||||
cloud_account_id: CLOUD_ACCOUNT_ID,
|
||||
config: {
|
||||
logs: {
|
||||
enabled: true,
|
||||
s3_buckets: {
|
||||
'us-east-2': ['first-bucket', 'second-bucket'], // Existing buckets
|
||||
'ap-south-1': [newBucketName], // Newly added bucket for the first region
|
||||
},
|
||||
},
|
||||
metrics: {},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should not render S3 bucket region selector UI for services other than s3sync', async () => {
|
||||
const otherServiceId = 'cloudwatch';
|
||||
act(() => {
|
||||
renderModal({}, otherServiceId);
|
||||
});
|
||||
await assertGenericModalElements();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.queryByRole('heading', { name: /select s3 buckets by region/i }),
|
||||
).not.toBeInTheDocument();
|
||||
|
||||
const regions = accountsResponse.data.accounts[0]?.config?.regions || [];
|
||||
regions.forEach((region) => {
|
||||
expect(
|
||||
screen.queryByText(`Enter S3 bucket names for ${region}`),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,44 @@
|
||||
import { IConfigureServiceModalProps } from '../ConfigureServiceModal';
|
||||
|
||||
const CLOUD_ACCOUNT_ID = '123456789012';
|
||||
|
||||
const initialBuckets = { 'us-east-2': ['first-bucket', 'second-bucket'] };
|
||||
|
||||
const accountsResponse = {
|
||||
status: 'success',
|
||||
data: {
|
||||
accounts: [
|
||||
{
|
||||
id: 'a1b2c3d4-e5f6-7890-1234-567890abcdef',
|
||||
cloud_account_id: CLOUD_ACCOUNT_ID,
|
||||
config: {
|
||||
regions: ['ap-south-1', 'ap-south-2', 'us-east-1', 'us-east-2'],
|
||||
},
|
||||
status: {
|
||||
integration: {
|
||||
last_heartbeat_ts_ms: 1747114366214,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const defaultModalProps: Omit<IConfigureServiceModalProps, 'initialConfig'> = {
|
||||
isOpen: true,
|
||||
onClose: jest.fn(),
|
||||
serviceName: 'S3 Sync',
|
||||
serviceId: 's3sync',
|
||||
cloudAccountId: CLOUD_ACCOUNT_ID,
|
||||
supportedSignals: {
|
||||
logs: true,
|
||||
metrics: false,
|
||||
},
|
||||
};
|
||||
|
||||
export {
|
||||
accountsResponse,
|
||||
CLOUD_ACCOUNT_ID,
|
||||
defaultModalProps,
|
||||
initialBuckets,
|
||||
};
|
||||
@@ -0,0 +1,78 @@
|
||||
import { render, RenderResult, screen, waitFor } from '@testing-library/react';
|
||||
import MockQueryClientProvider from 'providers/test/MockQueryClientProvider';
|
||||
|
||||
import ConfigureServiceModal from '../ConfigureServiceModal';
|
||||
import { accountsResponse, defaultModalProps } from './mockData';
|
||||
|
||||
/**
|
||||
* Renders the ConfigureServiceModal with specified S3 bucket initial configurations.
|
||||
*/
|
||||
const renderModal = (
|
||||
initialConfigLogsS3Buckets: Record<string, string[]> = {},
|
||||
serviceId = 's3sync',
|
||||
): RenderResult => {
|
||||
const initialConfig = {
|
||||
logs: { enabled: true, s3_buckets: initialConfigLogsS3Buckets },
|
||||
metrics: { enabled: false },
|
||||
};
|
||||
|
||||
return render(
|
||||
<MockQueryClientProvider>
|
||||
<ConfigureServiceModal
|
||||
{...defaultModalProps}
|
||||
serviceId={serviceId}
|
||||
initialConfig={initialConfig}
|
||||
/>
|
||||
</MockQueryClientProvider>,
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Asserts that generic UI elements of the modal are present.
|
||||
*/
|
||||
const assertGenericModalElements = async (): Promise<void> => {
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole('switch')).toBeInTheDocument();
|
||||
expect(screen.getByText(/log collection/i)).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText(
|
||||
/to ingest logs from your aws services, you must complete several steps/i,
|
||||
),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Asserts the state of S3 bucket selectors for each region, specific to S3 Sync.
|
||||
*/
|
||||
const assertS3SyncSpecificElements = async (
|
||||
expectedBucketsByRegion: Record<string, string[]> = {},
|
||||
): Promise<void> => {
|
||||
const regions = accountsResponse.data.accounts[0]?.config?.regions || [];
|
||||
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.getByRole('heading', { name: /select s3 buckets by region/i }),
|
||||
).toBeInTheDocument();
|
||||
|
||||
regions.forEach((region) => {
|
||||
expect(screen.getByText(region)).toBeInTheDocument();
|
||||
const bucketsForRegion = expectedBucketsByRegion[region] || [];
|
||||
if (bucketsForRegion.length > 0) {
|
||||
bucketsForRegion.forEach((bucket) => {
|
||||
expect(screen.getByText(bucket)).toBeInTheDocument();
|
||||
});
|
||||
} else {
|
||||
expect(
|
||||
screen.getByText(`Enter S3 bucket names for ${region}`),
|
||||
).toBeInTheDocument();
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
export {
|
||||
assertGenericModalElements,
|
||||
assertS3SyncSpecificElements,
|
||||
renderModal,
|
||||
};
|
||||
@@ -1,40 +1,90 @@
|
||||
import { ServiceData } from 'container/Integrations/types';
|
||||
|
||||
interface Service {
|
||||
id: string;
|
||||
title: string;
|
||||
icon: string;
|
||||
config: AWSServiceConfig;
|
||||
config: ServiceConfig;
|
||||
}
|
||||
|
||||
interface S3BucketsByRegion {
|
||||
[region: string]: string[];
|
||||
interface Dashboard {
|
||||
id: string;
|
||||
url: string;
|
||||
title: string;
|
||||
description: string;
|
||||
image: string;
|
||||
}
|
||||
|
||||
interface LogField {
|
||||
name: string;
|
||||
path: string;
|
||||
type: string;
|
||||
}
|
||||
|
||||
interface Metric {
|
||||
name: string;
|
||||
type: string;
|
||||
unit: string;
|
||||
}
|
||||
|
||||
interface ConfigStatus {
|
||||
enabled: boolean;
|
||||
}
|
||||
|
||||
interface DataStatus {
|
||||
last_received_ts_ms: number;
|
||||
last_received_from: string;
|
||||
}
|
||||
|
||||
interface S3BucketsByRegion {
|
||||
[region: string]: string[];
|
||||
}
|
||||
|
||||
interface LogsConfig extends ConfigStatus {
|
||||
s3_buckets?: S3BucketsByRegion;
|
||||
}
|
||||
|
||||
interface AWSServiceConfig {
|
||||
interface ServiceConfig {
|
||||
logs: LogsConfig;
|
||||
metrics: ConfigStatus;
|
||||
s3_sync?: LogsConfig;
|
||||
}
|
||||
|
||||
interface IServiceStatus {
|
||||
logs: DataStatus | null;
|
||||
metrics: DataStatus | null;
|
||||
}
|
||||
|
||||
interface SupportedSignals {
|
||||
metrics: boolean;
|
||||
logs: boolean;
|
||||
}
|
||||
|
||||
interface ServiceData {
|
||||
id: string;
|
||||
title: string;
|
||||
icon: string;
|
||||
overview: string;
|
||||
supported_signals: SupportedSignals;
|
||||
assets: {
|
||||
dashboards: Dashboard[];
|
||||
};
|
||||
data_collected: {
|
||||
logs?: LogField[];
|
||||
metrics: Metric[];
|
||||
};
|
||||
config?: ServiceConfig;
|
||||
status?: IServiceStatus;
|
||||
}
|
||||
|
||||
interface ServiceDetailsResponse {
|
||||
status: 'success';
|
||||
data: ServiceData;
|
||||
}
|
||||
|
||||
export interface AWSCloudAccountConfig {
|
||||
interface CloudAccountConfig {
|
||||
regions: string[];
|
||||
}
|
||||
|
||||
export interface IntegrationStatus {
|
||||
interface IntegrationStatus {
|
||||
last_heartbeat_ts_ms: number;
|
||||
}
|
||||
|
||||
@@ -45,7 +95,7 @@ interface AccountStatus {
|
||||
interface CloudAccount {
|
||||
id: string;
|
||||
cloud_account_id: string;
|
||||
config: AWSCloudAccountConfig;
|
||||
config: CloudAccountConfig;
|
||||
status: AccountStatus;
|
||||
}
|
||||
|
||||
@@ -83,13 +133,15 @@ interface UpdateServiceConfigResponse {
|
||||
}
|
||||
|
||||
export type {
|
||||
AWSServiceConfig,
|
||||
CloudAccount,
|
||||
CloudAccountsData,
|
||||
IServiceStatus,
|
||||
S3BucketsByRegion,
|
||||
Service,
|
||||
ServiceConfig,
|
||||
ServiceData,
|
||||
ServiceDetailsResponse,
|
||||
SupportedSignals,
|
||||
UpdateServiceConfigPayload,
|
||||
UpdateServiceConfigResponse,
|
||||
};
|
||||
@@ -1,12 +1,14 @@
|
||||
import { I18nextProvider } from 'react-i18next';
|
||||
import { act, fireEvent, render, screen } from '@testing-library/react';
|
||||
import { RequestIntegrationBtn } from 'container/Integrations/RequestIntegrationBtn';
|
||||
import { IntegrationType } from 'container/Integrations/types';
|
||||
import { server } from 'mocks-server/server';
|
||||
import { rest } from 'msw';
|
||||
import {
|
||||
IntegrationType,
|
||||
RequestIntegrationBtn,
|
||||
} from 'pages/Integrations/RequestIntegrationBtn';
|
||||
import i18n from 'ReactI18';
|
||||
|
||||
describe.skip('Request AWS integration', () => {
|
||||
describe('Request AWS integration', () => {
|
||||
it('should render the request integration button', async () => {
|
||||
let capturedPayload: any;
|
||||
server.use(
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useState } from 'react';
|
||||
import { CloudDownloadOutlined } from '@ant-design/icons';
|
||||
import { Button, Dropdown, MenuProps } from 'antd';
|
||||
import { Excel } from 'antd-table-saveas-excel';
|
||||
import { unparse } from 'papaparse';
|
||||
|
||||
import { DownloadProps } from './Download.types';
|
||||
@@ -8,25 +8,36 @@ import { DownloadProps } from './Download.types';
|
||||
import './Download.styles.scss';
|
||||
|
||||
function Download({ data, isLoading, fileName }: DownloadProps): JSX.Element {
|
||||
const downloadExcelFile = (): void => {
|
||||
const headers = Object.keys(Object.assign({}, ...data)).map((item) => {
|
||||
const updatedTitle = item
|
||||
.split('_')
|
||||
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
||||
.join(' ');
|
||||
return {
|
||||
title: updatedTitle,
|
||||
dataIndex: item,
|
||||
};
|
||||
});
|
||||
const excel = new Excel();
|
||||
excel
|
||||
.addSheet(fileName)
|
||||
.addColumns(headers)
|
||||
.addDataSource(data, {
|
||||
str2Percent: true,
|
||||
})
|
||||
.saveAs(`${fileName}.xlsx`);
|
||||
const [isDownloading, setIsDownloading] = useState(false);
|
||||
|
||||
const downloadExcelFile = async (): Promise<void> => {
|
||||
setIsDownloading(true);
|
||||
|
||||
try {
|
||||
const headers = Object.keys(Object.assign({}, ...data)).map((item) => {
|
||||
const updatedTitle = item
|
||||
.split('_')
|
||||
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
||||
.join(' ');
|
||||
return {
|
||||
title: updatedTitle,
|
||||
dataIndex: item,
|
||||
};
|
||||
});
|
||||
|
||||
const excelLib = await import('antd-table-saveas-excel');
|
||||
|
||||
const excel = new excelLib.Excel();
|
||||
excel
|
||||
.addSheet(fileName)
|
||||
.addColumns(headers)
|
||||
.addDataSource(data, {
|
||||
str2Percent: true,
|
||||
})
|
||||
.saveAs(`${fileName}.xlsx`);
|
||||
} finally {
|
||||
setIsDownloading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const downloadCsvFile = (): void => {
|
||||
@@ -59,7 +70,7 @@ function Download({ data, isLoading, fileName }: DownloadProps): JSX.Element {
|
||||
<Dropdown menu={menu} trigger={['click']}>
|
||||
<Button
|
||||
className="download-button"
|
||||
loading={isLoading}
|
||||
loading={isLoading || isDownloading}
|
||||
size="small"
|
||||
type="link"
|
||||
>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useState } from 'react';
|
||||
import { Button, Popover, Typography } from 'antd';
|
||||
import { Excel } from 'antd-table-saveas-excel';
|
||||
import { FileDigit, FileDown, Sheet } from 'lucide-react';
|
||||
import { unparse } from 'papaparse';
|
||||
|
||||
@@ -8,25 +8,34 @@ import { DownloadProps } from './DownloadV2.types';
|
||||
import './DownloadV2.styles.scss';
|
||||
|
||||
function Download({ data, isLoading, fileName }: DownloadProps): JSX.Element {
|
||||
const downloadExcelFile = (): void => {
|
||||
const headers = Object.keys(Object.assign({}, ...data)).map((item) => {
|
||||
const updatedTitle = item
|
||||
.split('_')
|
||||
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
||||
.join(' ');
|
||||
return {
|
||||
title: updatedTitle,
|
||||
dataIndex: item,
|
||||
};
|
||||
});
|
||||
const excel = new Excel();
|
||||
excel
|
||||
.addSheet(fileName)
|
||||
.addColumns(headers)
|
||||
.addDataSource(data, {
|
||||
str2Percent: true,
|
||||
})
|
||||
.saveAs(`${fileName}.xlsx`);
|
||||
const [isDownloading, setIsDownloading] = useState(false);
|
||||
|
||||
const downloadExcelFile = async (): Promise<void> => {
|
||||
setIsDownloading(true);
|
||||
|
||||
try {
|
||||
const headers = Object.keys(Object.assign({}, ...data)).map((item) => {
|
||||
const updatedTitle = item
|
||||
.split('_')
|
||||
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
||||
.join(' ');
|
||||
return {
|
||||
title: updatedTitle,
|
||||
dataIndex: item,
|
||||
};
|
||||
});
|
||||
const excelLib = await import('antd-table-saveas-excel');
|
||||
const excel = new excelLib.Excel();
|
||||
excel
|
||||
.addSheet(fileName)
|
||||
.addColumns(headers)
|
||||
.addDataSource(data, {
|
||||
str2Percent: true,
|
||||
})
|
||||
.saveAs(`${fileName}.xlsx`);
|
||||
} finally {
|
||||
setIsDownloading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const downloadCsvFile = (): void => {
|
||||
@@ -54,6 +63,7 @@ function Download({ data, isLoading, fileName }: DownloadProps): JSX.Element {
|
||||
type="text"
|
||||
onClick={downloadExcelFile}
|
||||
className="action-btns"
|
||||
loading={isDownloading}
|
||||
>
|
||||
Excel (.xlsx)
|
||||
</Button>
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
import AccountActions from './components/AccountActions';
|
||||
|
||||
import './HeroSection.style.scss';
|
||||
|
||||
function HeroSection(): JSX.Element {
|
||||
return (
|
||||
<div className="hero-section">
|
||||
<div className="hero-section__details">
|
||||
<div className="hero-section__details-header">
|
||||
<div className="hero-section__icon">
|
||||
<img src="/Logos/aws-dark.svg" alt="AWS" />
|
||||
</div>
|
||||
|
||||
<div className="hero-section__details-title">AWS</div>
|
||||
</div>
|
||||
<div className="hero-section__details-description">
|
||||
AWS is a cloud computing platform that provides a range of services for
|
||||
building and running applications.
|
||||
</div>
|
||||
</div>
|
||||
<AccountActions />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default HeroSection;
|
||||
@@ -1,172 +0,0 @@
|
||||
import { Dispatch, SetStateAction, useCallback } from 'react';
|
||||
import { useQueryClient } from 'react-query';
|
||||
import { Button } from '@signozhq/button';
|
||||
import { DrawerWrapper } from '@signozhq/drawer';
|
||||
import { Form } from 'antd';
|
||||
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||
import { useAccountSettingsModal } from 'hooks/integration/aws/useAccountSettingsModal';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
import history from 'lib/history';
|
||||
import { Save } from 'lucide-react';
|
||||
|
||||
import logEvent from '../../../../../../api/common/logEvent';
|
||||
import { CloudAccount } from '../../types';
|
||||
import { RegionSelector } from './RegionSelector';
|
||||
import RemoveIntegrationAccount from './RemoveIntegrationAccount';
|
||||
|
||||
import './AccountSettingsModal.style.scss';
|
||||
|
||||
interface AccountSettingsModalProps {
|
||||
onClose: () => void;
|
||||
account: CloudAccount;
|
||||
setActiveAccount: Dispatch<SetStateAction<CloudAccount | null>>;
|
||||
}
|
||||
|
||||
function AccountSettingsModal({
|
||||
onClose,
|
||||
account,
|
||||
setActiveAccount,
|
||||
}: AccountSettingsModalProps): JSX.Element {
|
||||
const {
|
||||
form,
|
||||
isLoading,
|
||||
selectedRegions,
|
||||
includeAllRegions,
|
||||
isSaveDisabled,
|
||||
setSelectedRegions,
|
||||
setIncludeAllRegions,
|
||||
handleSubmit,
|
||||
handleClose,
|
||||
} = useAccountSettingsModal({ onClose, account, setActiveAccount });
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
const urlQuery = useUrlQuery();
|
||||
|
||||
const handleRemoveIntegrationAccountSuccess = useCallback((): void => {
|
||||
queryClient.invalidateQueries([REACT_QUERY_KEY.AWS_ACCOUNTS]);
|
||||
urlQuery.delete('cloudAccountId');
|
||||
handleClose();
|
||||
history.replace({ search: urlQuery.toString() });
|
||||
|
||||
logEvent('AWS Integration: Account removed', {
|
||||
id: account?.id,
|
||||
cloudAccountId: account?.cloud_account_id,
|
||||
});
|
||||
}, [
|
||||
queryClient,
|
||||
urlQuery,
|
||||
handleClose,
|
||||
account?.id,
|
||||
account?.cloud_account_id,
|
||||
]);
|
||||
|
||||
const renderAccountDetails = useCallback(() => {
|
||||
return (
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
initialValues={{
|
||||
selectedRegions,
|
||||
includeAllRegions,
|
||||
}}
|
||||
>
|
||||
<div className="account-settings-modal__body">
|
||||
<div className="account-settings-modal__body-account-info">
|
||||
<div className="account-settings-modal__body-account-info-connected-account-details">
|
||||
<div className="account-settings-modal__body-account-info-connected-account-details-title">
|
||||
Connected Account details
|
||||
</div>
|
||||
<div className="account-settings-modal__body-account-info-connected-account-details-account-id">
|
||||
AWS Account:{' '}
|
||||
<span className="account-settings-modal__body-account-info-connected-account-details-account-id-account-id">
|
||||
{account?.id}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="account-settings-modal__body-region-selector">
|
||||
<div className="account-settings-modal__body-region-selector-title">
|
||||
Which regions do you want to monitor?
|
||||
</div>
|
||||
<div className="account-settings-modal__body-region-selector-description">
|
||||
Choose only the regions you want SigNoz to monitor.
|
||||
</div>
|
||||
|
||||
<RegionSelector
|
||||
selectedRegions={selectedRegions}
|
||||
setSelectedRegions={setSelectedRegions}
|
||||
setIncludeAllRegions={setIncludeAllRegions}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="account-settings-modal__footer">
|
||||
<RemoveIntegrationAccount
|
||||
accountId={account?.id}
|
||||
onRemoveIntegrationAccountSuccess={handleRemoveIntegrationAccountSuccess}
|
||||
/>
|
||||
|
||||
<Button
|
||||
variant="solid"
|
||||
color="secondary"
|
||||
disabled={isSaveDisabled}
|
||||
onClick={handleSubmit}
|
||||
loading={isLoading}
|
||||
prefixIcon={<Save size={14} />}
|
||||
>
|
||||
Update Changes
|
||||
</Button>
|
||||
</div>
|
||||
</Form>
|
||||
);
|
||||
}, [
|
||||
form,
|
||||
selectedRegions,
|
||||
includeAllRegions,
|
||||
account?.id,
|
||||
handleRemoveIntegrationAccountSuccess,
|
||||
isSaveDisabled,
|
||||
handleSubmit,
|
||||
isLoading,
|
||||
setSelectedRegions,
|
||||
setIncludeAllRegions,
|
||||
]);
|
||||
|
||||
const handleDrawerOpenChange = useCallback(
|
||||
(open: boolean): void => {
|
||||
if (!open) {
|
||||
handleClose();
|
||||
}
|
||||
},
|
||||
[handleClose],
|
||||
);
|
||||
|
||||
return (
|
||||
<DrawerWrapper
|
||||
open={true}
|
||||
type="panel"
|
||||
className="account-settings-modal"
|
||||
header={{
|
||||
title: 'Account Settings',
|
||||
}}
|
||||
// onCancel={handleClose}
|
||||
// onOk={handleSubmit}
|
||||
// okText="Save"
|
||||
// okButtonProps={{
|
||||
// disabled: isSaveDisabled,
|
||||
// className: 'account-settings-modal__footer-save-button',
|
||||
// loading: isLoading,
|
||||
// }}
|
||||
// cancelButtonProps={{
|
||||
// className: 'account-settings-modal__footer-close-button',
|
||||
// }}
|
||||
direction="right"
|
||||
showCloseButton
|
||||
content={renderAccountDetails()}
|
||||
onOpenChange={handleDrawerOpenChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default AccountSettingsModal;
|
||||
@@ -1,32 +0,0 @@
|
||||
.remove-integration-account-modal {
|
||||
.ant-modal-content {
|
||||
background-color: var(--l1-background);
|
||||
border: 1px solid var(--l3-background);
|
||||
border-radius: 4px;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.ant-modal-close {
|
||||
color: var(--l1-foreground);
|
||||
}
|
||||
|
||||
.ant-modal-header {
|
||||
background-color: var(--l1-background);
|
||||
color: var(--l1-foreground);
|
||||
|
||||
.ant-modal-title {
|
||||
color: var(--l1-foreground);
|
||||
}
|
||||
}
|
||||
|
||||
.ant-modal-body {
|
||||
margin-top: 16px;
|
||||
color: var(--l1-foreground);
|
||||
background-color: var(--l1-background);
|
||||
}
|
||||
|
||||
.ant-modal-footer {
|
||||
margin-top: 16px;
|
||||
background-color: var(--l1-background);
|
||||
}
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
.s3-buckets-selector {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
padding: 12px;
|
||||
background: var(--l2-background);
|
||||
border-radius: 4px;
|
||||
|
||||
.s3-buckets-selector-title {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
font-size: 12px;
|
||||
color: var(--l2-foreground);
|
||||
}
|
||||
|
||||
.s3-buckets-selector-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
|
||||
.s3-buckets-selector-region {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
|
||||
.s3-buckets-selector-region-header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.s3-buckets-selector-region-help {
|
||||
color: var(--l2-foreground);
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
letter-spacing: -0.06px;
|
||||
}
|
||||
|
||||
.s3-buckets-selector-region-select {
|
||||
flex: 1;
|
||||
|
||||
.ant-select {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
.aws-service-dashboards {
|
||||
border-radius: 6px;
|
||||
border: 1px solid var(--l3-background);
|
||||
background: var(--l1-background);
|
||||
|
||||
.aws-service-dashboards-title {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
color: var(--l2-foreground);
|
||||
font-family: Inter;
|
||||
font-size: 13px;
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
line-height: 20px; /* 142.857% */
|
||||
letter-spacing: -0.07px;
|
||||
|
||||
padding: 8px 16px;
|
||||
border-bottom: 1px solid var(--l3-background);
|
||||
}
|
||||
|
||||
.aws-service-dashboards-items {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.aws-service-dashboard-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
padding: 12px 16px 12px 16px;
|
||||
border-bottom: 1px solid var(--l3-background);
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
&.aws-service-dashboard-item-clickable {
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
background-color: var(--l1-background);
|
||||
}
|
||||
}
|
||||
|
||||
.aws-service-dashboard-item-title {
|
||||
color: var(--l1-foreground);
|
||||
font-family: Inter;
|
||||
font-size: 13px;
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
line-height: 20px; /* 142.857% */
|
||||
letter-spacing: -0.07px;
|
||||
}
|
||||
|
||||
.aws-service-dashboard-item-description {
|
||||
color: var(--l2-foreground);
|
||||
font-family: Inter;
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 18px; /* 150% */
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
import { useSafeNavigate } from 'hooks/useSafeNavigate';
|
||||
|
||||
import { ServiceData } from '../types';
|
||||
|
||||
import './ServiceDashboards.styles.scss';
|
||||
|
||||
function ServiceDashboards({ service }: { service: ServiceData }): JSX.Element {
|
||||
const dashboards = service?.assets?.dashboards || [];
|
||||
const { safeNavigate } = useSafeNavigate();
|
||||
|
||||
return (
|
||||
<div className="aws-service-dashboards">
|
||||
<div className="aws-service-dashboards-title">Dashboards</div>
|
||||
<div className="aws-service-dashboards-items">
|
||||
{dashboards.map((dashboard) => (
|
||||
<div
|
||||
key={dashboard.id}
|
||||
className={`aws-service-dashboard-item ${
|
||||
dashboard.url ? 'aws-service-dashboard-item-clickable' : ''
|
||||
}`}
|
||||
onClick={(): void => {
|
||||
if (dashboard.url) {
|
||||
safeNavigate(dashboard.url);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className="aws-service-dashboard-item-title">{dashboard.title}</div>
|
||||
<div className="aws-service-dashboard-item-description">
|
||||
{dashboard.description}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ServiceDashboards;
|
||||
@@ -1,209 +0,0 @@
|
||||
.aws-service-details-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
|
||||
.aws-service-details-tabs {
|
||||
margin-top: 8px;
|
||||
|
||||
// remove the padding left from the first div of the tabs component
|
||||
// this needs to be handled in the tabs component
|
||||
> div:first-child {
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
.aws-service-details-data-collected-content-logs,
|
||||
.aws-service-details-data-collected-content-metrics {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 8px;
|
||||
|
||||
.aws-service-details-data-collected-content-title {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
color: var(--l2-foreground);
|
||||
|
||||
/* Bifrost (Ancient)/Content/sm */
|
||||
font-family: Inter;
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 20px; /* 142.857% */
|
||||
letter-spacing: -0.07px;
|
||||
}
|
||||
}
|
||||
|
||||
.aws-service-details-overview {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
|
||||
.aws-service-details-overview-configuration {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--l3-background);
|
||||
background: var(--l1-background);
|
||||
|
||||
.aws-service-details-overview-configuration-title {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
|
||||
border-radius: 4px 4px 0 0;
|
||||
|
||||
padding: 8px 12px;
|
||||
|
||||
.aws-service-details-overview-configuration-title-text {
|
||||
color: var(--l1-foreground);
|
||||
font-family: Inter;
|
||||
font-size: 13px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 22px; /* 157.143% */
|
||||
letter-spacing: -0.07px;
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.configuration-action {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.aws-service-details-overview-configuration-s3-buckets {
|
||||
padding: 12px;
|
||||
background: var(--l1-background);
|
||||
}
|
||||
|
||||
.aws-service-details-overview-configuration-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
|
||||
padding: 12px;
|
||||
background: var(--l1-background);
|
||||
|
||||
.aws-service-details-overview-configuration-content-item {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.aws-service-details-overview-configuration-actions {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
padding: 8px 12px;
|
||||
|
||||
border-top: 1px solid var(--l3-background);
|
||||
background: var(--l1-background);
|
||||
|
||||
.discard-btn {
|
||||
width: 100px;
|
||||
}
|
||||
|
||||
.save-btn {
|
||||
width: 100px;
|
||||
}
|
||||
}
|
||||
|
||||
.aws-service-details-overview-configuration-title-text-select-all {
|
||||
color: var(--l2-foreground);
|
||||
font-family: Inter;
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 18px; /* 138.462% */
|
||||
}
|
||||
}
|
||||
|
||||
.aws-service-details-actions {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 8px;
|
||||
padding: 12px 0;
|
||||
}
|
||||
|
||||
.aws-service-dashboards {
|
||||
border-radius: 6px;
|
||||
border: 1px solid var(--l3-background);
|
||||
background: var(--l1-background);
|
||||
|
||||
.aws-service-dashboards-title {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
color: var(--l2-foreground);
|
||||
font-family: Inter;
|
||||
font-size: 13px;
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
line-height: 20px; /* 142.857% */
|
||||
letter-spacing: -0.07px;
|
||||
|
||||
padding: 8px 16px;
|
||||
border-bottom: 1px solid var(--l3-background);
|
||||
}
|
||||
|
||||
.aws-service-dashboards-items {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
|
||||
.aws-service-dashboard-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
padding: 12px 16px 12px 16px;
|
||||
|
||||
&.aws-service-dashboard-item-clickable {
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
background-color: var(--l1-background);
|
||||
}
|
||||
}
|
||||
|
||||
.aws-service-dashboard-item-title {
|
||||
color: var(--l1-foreground);
|
||||
font-family: Inter;
|
||||
font-size: 13px;
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
line-height: 20px; /* 142.857% */
|
||||
letter-spacing: -0.07px;
|
||||
}
|
||||
|
||||
.aws-service-dashboard-item-description {
|
||||
color: var(--l2-foreground);
|
||||
font-family: Inter;
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 18px; /* 150% */
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,372 +0,0 @@
|
||||
import { useCallback, useEffect } from 'react';
|
||||
import { Controller, useForm } from 'react-hook-form';
|
||||
import { useQueryClient } from 'react-query';
|
||||
import { Button } from '@signozhq/button';
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { toast } from '@signozhq/sonner';
|
||||
import { Switch } from '@signozhq/switch';
|
||||
import Tabs from '@signozhq/tabs';
|
||||
import { Popover, Skeleton } from 'antd';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import CloudServiceDataCollected from 'components/CloudIntegrations/CloudServiceDataCollected/CloudServiceDataCollected';
|
||||
import { MarkdownRenderer } from 'components/MarkdownRenderer/MarkdownRenderer';
|
||||
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||
import ServiceDashboards from 'container/Integrations/CloudIntegration/AmazonWebServices/ServiceDashboards/ServiceDashboards';
|
||||
import { AWSServiceConfig } from 'container/Integrations/CloudIntegration/AmazonWebServices/types';
|
||||
import { useServiceDetails } from 'hooks/integration/aws/useServiceDetails';
|
||||
import { useUpdateServiceConfig } from 'hooks/integration/aws/useUpdateServiceConfig';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
import { CheckCircle, Save, TriangleAlert, X } from 'lucide-react';
|
||||
|
||||
import { ConfigConnectionStatus } from '../../ConfigConnectionStatus/ConfigConnectionStatus';
|
||||
import S3BucketsSelector from '../S3BucketsSelector/S3BucketsSelector';
|
||||
|
||||
import './ServiceDetails.styles.scss';
|
||||
|
||||
type ServiceConfigFormValues = {
|
||||
logsEnabled: boolean;
|
||||
metricsEnabled: boolean;
|
||||
s3BucketsByRegion: Record<string, string[]>;
|
||||
};
|
||||
|
||||
function ServiceDetails(): JSX.Element | null {
|
||||
const urlQuery = useUrlQuery();
|
||||
const cloudAccountId = urlQuery.get('cloudAccountId');
|
||||
const serviceId = urlQuery.get('service');
|
||||
|
||||
const {
|
||||
data: serviceDetailsData,
|
||||
isLoading: isServiceDetailsLoading,
|
||||
} = useServiceDetails(serviceId || '', cloudAccountId || undefined);
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
const { config } = serviceDetailsData ?? {};
|
||||
|
||||
const awsConfig = config as AWSServiceConfig | undefined;
|
||||
|
||||
const {
|
||||
control,
|
||||
handleSubmit: handleFormSubmit,
|
||||
reset,
|
||||
watch,
|
||||
formState: { isDirty },
|
||||
} = useForm<ServiceConfigFormValues>({
|
||||
defaultValues: {
|
||||
logsEnabled: awsConfig?.logs?.enabled || false,
|
||||
metricsEnabled: awsConfig?.metrics?.enabled || false,
|
||||
s3BucketsByRegion: awsConfig?.logs?.s3_buckets || {},
|
||||
},
|
||||
});
|
||||
|
||||
const resetToAwsConfig = useCallback((): void => {
|
||||
reset({
|
||||
logsEnabled: awsConfig?.logs?.enabled || false,
|
||||
metricsEnabled: awsConfig?.metrics?.enabled || false,
|
||||
s3BucketsByRegion: awsConfig?.logs?.s3_buckets || {},
|
||||
});
|
||||
}, [awsConfig, reset]);
|
||||
|
||||
useEffect(() => {
|
||||
resetToAwsConfig();
|
||||
}, [resetToAwsConfig]);
|
||||
|
||||
// log telemetry event on visiting details of a service.
|
||||
useEffect(() => {
|
||||
if (serviceId) {
|
||||
logEvent('AWS Integration: Service viewed', {
|
||||
cloudAccountId,
|
||||
serviceId,
|
||||
});
|
||||
}
|
||||
}, [cloudAccountId, serviceId]);
|
||||
|
||||
const {
|
||||
mutate: updateServiceConfig,
|
||||
isLoading: isUpdatingServiceConfig,
|
||||
} = useUpdateServiceConfig();
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const handleDiscard = useCallback((): void => {
|
||||
resetToAwsConfig();
|
||||
}, [resetToAwsConfig]);
|
||||
|
||||
const onSubmit = useCallback(
|
||||
async (values: ServiceConfigFormValues): Promise<void> => {
|
||||
const { logsEnabled, metricsEnabled, s3BucketsByRegion } = values;
|
||||
|
||||
try {
|
||||
if (!serviceId || !cloudAccountId) {
|
||||
return;
|
||||
}
|
||||
|
||||
updateServiceConfig(
|
||||
{
|
||||
serviceId,
|
||||
payload: {
|
||||
cloud_account_id: cloudAccountId,
|
||||
config: {
|
||||
logs: {
|
||||
enabled: logsEnabled,
|
||||
s3_buckets: s3BucketsByRegion,
|
||||
},
|
||||
metrics: {
|
||||
enabled: metricsEnabled,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
// Immediately sync form state to remove dirty flag and hide actions,
|
||||
// instead of waiting for the refetch to complete.
|
||||
reset(values);
|
||||
|
||||
queryClient.invalidateQueries([
|
||||
REACT_QUERY_KEY.AWS_SERVICE_DETAILS,
|
||||
serviceId,
|
||||
]);
|
||||
|
||||
logEvent('AWS Integration: Service settings saved', {
|
||||
cloudAccountId,
|
||||
serviceId,
|
||||
logsEnabled,
|
||||
metricsEnabled,
|
||||
});
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error('Failed to update service config:', error);
|
||||
|
||||
toast.error('Failed to update service config', {
|
||||
description: error?.message,
|
||||
});
|
||||
},
|
||||
},
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('Form submission failed:', error);
|
||||
}
|
||||
},
|
||||
[serviceId, cloudAccountId, updateServiceConfig, queryClient, reset],
|
||||
);
|
||||
|
||||
if (isServiceDetailsLoading) {
|
||||
return (
|
||||
<div className="service-details-loading">
|
||||
<Skeleton active />
|
||||
<Skeleton active />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!serviceDetailsData) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||
const renderOverview = (): JSX.Element => {
|
||||
const logsEnabled = watch('logsEnabled');
|
||||
const metricsEnabled = watch('metricsEnabled');
|
||||
const s3BucketsByRegion = watch('s3BucketsByRegion');
|
||||
|
||||
const isLogsSupported = serviceDetailsData?.supported_signals?.logs || false;
|
||||
const isMetricsSupported =
|
||||
serviceDetailsData?.supported_signals?.metrics || false;
|
||||
|
||||
const logsStatus = serviceDetailsData?.status?.logs || null;
|
||||
const metricsStatus = serviceDetailsData?.status?.metrics || null;
|
||||
|
||||
const logsConnectionStatus = logsStatus?.find(
|
||||
(log) => log.last_received_ts_ms > 0,
|
||||
);
|
||||
|
||||
const metricsConnectionStatus = metricsStatus?.find(
|
||||
(metric) => metric.last_received_ts_ms > 0,
|
||||
);
|
||||
|
||||
const hasUnsavedChanges = isDirty;
|
||||
|
||||
const isS3SyncBucketsMissing =
|
||||
serviceId === 's3sync' &&
|
||||
logsEnabled &&
|
||||
(!s3BucketsByRegion || Object.keys(s3BucketsByRegion).length === 0);
|
||||
|
||||
return (
|
||||
<div className="aws-service-details-overview ">
|
||||
{!isServiceDetailsLoading && (
|
||||
<form
|
||||
className="aws-service-details-overview-configuration"
|
||||
onSubmit={handleFormSubmit(onSubmit)}
|
||||
>
|
||||
{isLogsSupported && (
|
||||
<div className="aws-service-details-overview-configuration-logs">
|
||||
<div className="aws-service-details-overview-configuration-title">
|
||||
<div className="aws-service-details-overview-configuration-title-text">
|
||||
{logsEnabled && (
|
||||
<Popover
|
||||
content={<ConfigConnectionStatus status={logsStatus} />}
|
||||
trigger="hover"
|
||||
placement="right"
|
||||
overlayClassName="config-connection-status-popover"
|
||||
>
|
||||
<div className="aws-service-details-overview-configuration-title-text-icon">
|
||||
{logsConnectionStatus ? (
|
||||
<CheckCircle size={16} color={Color.BG_FOREST_500} />
|
||||
) : (
|
||||
<TriangleAlert size={16} color={Color.BG_AMBER_500} />
|
||||
)}
|
||||
</div>
|
||||
</Popover>
|
||||
)}
|
||||
|
||||
<span>Log Collection</span>
|
||||
</div>
|
||||
<div className="configuration-action">
|
||||
<Controller<ServiceConfigFormValues, 'logsEnabled'>
|
||||
control={control}
|
||||
name="logsEnabled"
|
||||
render={({ field }): JSX.Element => (
|
||||
<Switch
|
||||
checked={field.value}
|
||||
disabled={isUpdatingServiceConfig}
|
||||
onCheckedChange={(checked): void => {
|
||||
field.onChange(checked);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{logsEnabled && serviceId === 's3sync' && (
|
||||
<div className="aws-service-details-overview-configuration-s3-buckets">
|
||||
<Controller<ServiceConfigFormValues, 's3BucketsByRegion'>
|
||||
control={control}
|
||||
name="s3BucketsByRegion"
|
||||
render={({ field }): JSX.Element => (
|
||||
<S3BucketsSelector
|
||||
initialBucketsByRegion={field.value}
|
||||
onChange={field.onChange}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isMetricsSupported && (
|
||||
<div className="aws-service-details-overview-configuration-metrics">
|
||||
<div className="aws-service-details-overview-configuration-title">
|
||||
<div className="aws-service-details-overview-configuration-title-text">
|
||||
{metricsEnabled && (
|
||||
<Popover
|
||||
content={<ConfigConnectionStatus status={metricsStatus} />}
|
||||
trigger="hover"
|
||||
placement="right"
|
||||
overlayClassName="config-connection-status-popover"
|
||||
>
|
||||
<div className="aws-service-details-overview-configuration-title-text-icon">
|
||||
{metricsConnectionStatus ? (
|
||||
<CheckCircle size={16} color={Color.BG_FOREST_500} />
|
||||
) : (
|
||||
<TriangleAlert size={16} color={Color.BG_AMBER_500} />
|
||||
)}
|
||||
</div>
|
||||
</Popover>
|
||||
)}
|
||||
|
||||
<span>Metric Collection</span>
|
||||
</div>
|
||||
<div className="configuration-action">
|
||||
<Controller<ServiceConfigFormValues, 'metricsEnabled'>
|
||||
control={control}
|
||||
name="metricsEnabled"
|
||||
render={({ field }): JSX.Element => (
|
||||
<Switch
|
||||
checked={field.value}
|
||||
disabled={isUpdatingServiceConfig}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{hasUnsavedChanges && (
|
||||
<div className="aws-service-details-overview-configuration-actions">
|
||||
<Button
|
||||
variant="solid"
|
||||
color="secondary"
|
||||
onClick={handleDiscard}
|
||||
disabled={isUpdatingServiceConfig}
|
||||
size="xs"
|
||||
prefixIcon={<X size={14} />}
|
||||
className="discard-btn"
|
||||
type="button"
|
||||
>
|
||||
Discard
|
||||
</Button>
|
||||
<Button
|
||||
variant="solid"
|
||||
color="primary"
|
||||
size="xs"
|
||||
className="save-btn"
|
||||
prefixIcon={<Save size={14} />}
|
||||
type="submit"
|
||||
loading={isUpdatingServiceConfig}
|
||||
disabled={isS3SyncBucketsMissing || isUpdatingServiceConfig}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</form>
|
||||
)}
|
||||
|
||||
<MarkdownRenderer
|
||||
variables={{}}
|
||||
markdownContent={serviceDetailsData?.overview}
|
||||
/>
|
||||
<ServiceDashboards service={serviceDetailsData} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const renderDataCollected = (): JSX.Element => {
|
||||
return (
|
||||
<div className="aws-service-details-data-collected-table">
|
||||
<CloudServiceDataCollected
|
||||
logsData={serviceDetailsData?.data_collected?.logs || []}
|
||||
metricsData={serviceDetailsData?.data_collected?.metrics || []}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="aws-service-details-container">
|
||||
<Tabs
|
||||
defaultValue="overview"
|
||||
className="aws-service-details-tabs"
|
||||
items={[
|
||||
{
|
||||
children: renderOverview(),
|
||||
key: 'overview',
|
||||
label: 'Overview',
|
||||
},
|
||||
{
|
||||
children: renderDataCollected(),
|
||||
key: 'data-collected',
|
||||
label: 'Data Collected',
|
||||
},
|
||||
]}
|
||||
variant="secondary"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ServiceDetails;
|
||||
@@ -1,150 +0,0 @@
|
||||
import { useCallback, useEffect, useMemo } from 'react';
|
||||
import { useNavigate } from 'react-router-dom-v5-compat';
|
||||
import { Skeleton } from 'antd';
|
||||
import cx from 'classnames';
|
||||
import { useGetAccountServices } from 'hooks/integration/aws/useGetAccountServices';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
|
||||
import { Service } from './types';
|
||||
|
||||
interface ServicesListProps {
|
||||
cloudAccountId: string;
|
||||
}
|
||||
|
||||
/** Service is enabled if even one sub item (log or metric) is enabled */
|
||||
function hasAnySubItemEnabled(service: Service): boolean {
|
||||
const logs = service.config?.logs ?? {};
|
||||
const metrics = service.config?.metrics ?? {};
|
||||
return logs.enabled || metrics.enabled;
|
||||
}
|
||||
|
||||
function ServicesList({ cloudAccountId }: ServicesListProps): JSX.Element {
|
||||
const urlQuery = useUrlQuery();
|
||||
const navigate = useNavigate();
|
||||
const { data: awsServices = [], isLoading } = useGetAccountServices(
|
||||
cloudAccountId,
|
||||
);
|
||||
const activeService = urlQuery.get('service');
|
||||
|
||||
const handleActiveService = useCallback(
|
||||
(serviceId: string): void => {
|
||||
const latestUrlQuery = new URLSearchParams(window.location.search);
|
||||
latestUrlQuery.set('service', serviceId);
|
||||
navigate({ search: latestUrlQuery.toString() });
|
||||
},
|
||||
[navigate],
|
||||
);
|
||||
|
||||
const enabledServices = useMemo(
|
||||
() => awsServices?.filter(hasAnySubItemEnabled) ?? [],
|
||||
[awsServices],
|
||||
);
|
||||
|
||||
// Derive from enabled to guarantee each service is in exactly one list
|
||||
const enabledIds = useMemo(() => new Set(enabledServices.map((s) => s.id)), [
|
||||
enabledServices,
|
||||
]);
|
||||
const notEnabledServices = useMemo(
|
||||
() => awsServices?.filter((s) => !enabledIds.has(s.id)) ?? [],
|
||||
[awsServices, enabledIds],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const allServices = [...enabledServices, ...notEnabledServices];
|
||||
|
||||
// If a service is already selected and still exists in the refreshed list, keep it
|
||||
if (activeService && allServices.some((s) => s.id === activeService)) {
|
||||
// Update the selected service reference to the fresh object from the new list
|
||||
const freshService = allServices.find((s) => s.id === activeService);
|
||||
if (freshService) {
|
||||
handleActiveService(freshService.id);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// No valid selection — pick a default
|
||||
if (enabledServices.length > 0) {
|
||||
handleActiveService(enabledServices[0].id);
|
||||
} else if (notEnabledServices.length > 0) {
|
||||
handleActiveService(notEnabledServices[0].id);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [enabledServices, notEnabledServices]);
|
||||
|
||||
useEffect(() => {
|
||||
if (activeService || !awsServices?.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
handleActiveService(awsServices[0].id);
|
||||
}, [awsServices, activeService, handleActiveService]);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="services-list-loading">
|
||||
<Skeleton active />
|
||||
<Skeleton active />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!awsServices?.length) {
|
||||
return <div>No services found</div>;
|
||||
}
|
||||
|
||||
const isEnabledServicesEmpty = enabledServices.length === 0;
|
||||
const isNotEnabledServicesEmpty = notEnabledServices.length === 0;
|
||||
|
||||
const renderServiceItem = (service: Service): JSX.Element => {
|
||||
return (
|
||||
<div
|
||||
className={cx('aws-services-list-view-sidebar-content-item', {
|
||||
active: service.id === activeService,
|
||||
})}
|
||||
key={service.id}
|
||||
onClick={(): void => handleActiveService(service.id)}
|
||||
>
|
||||
<img
|
||||
src={service.icon}
|
||||
alt={service.title}
|
||||
className="aws-services-list-view-sidebar-content-item-icon"
|
||||
/>
|
||||
<div className="aws-services-list-view-sidebar-content-item-title">
|
||||
{service.title}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="aws-services-list-view">
|
||||
<div className="aws-services-list-view-sidebar">
|
||||
<div className="aws-services-list-view-sidebar-content">
|
||||
<div className="aws-services-enabled">
|
||||
<div className="aws-services-list-view-sidebar-content-header">
|
||||
Enabled
|
||||
</div>
|
||||
{enabledServices.map((service) => renderServiceItem(service))}
|
||||
|
||||
{isEnabledServicesEmpty && (
|
||||
<div className="aws-services-list-view-sidebar-content-item-empty-message">
|
||||
No enabled services
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{!isNotEnabledServicesEmpty && (
|
||||
<div className="aws-services-not-enabled">
|
||||
<div className="aws-services-list-view-sidebar-content-header">
|
||||
Not Enabled
|
||||
</div>
|
||||
{notEnabledServices.map((service) => renderServiceItem(service))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ServicesList;
|
||||
@@ -1,35 +0,0 @@
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
|
||||
import HeroSection from './HeroSection/HeroSection';
|
||||
import ServiceDetails from './ServiceDetails/ServiceDetails';
|
||||
import ServicesList from './ServicesList';
|
||||
|
||||
import './ServicesTabs.style.scss';
|
||||
|
||||
export enum ServiceFilterType {
|
||||
ALL_SERVICES = 'all_services',
|
||||
ENABLED = 'enabled',
|
||||
AVAILABLE = 'available',
|
||||
}
|
||||
|
||||
function ServicesTabs(): JSX.Element {
|
||||
const urlQuery = useUrlQuery();
|
||||
const cloudAccountId = urlQuery.get('cloudAccountId') || '';
|
||||
|
||||
return (
|
||||
<div className="services-tabs">
|
||||
<HeroSection />
|
||||
|
||||
<div className="services-section">
|
||||
<div className="services-section__sidebar">
|
||||
<ServicesList cloudAccountId={cloudAccountId} />
|
||||
</div>
|
||||
<div className="services-section__content">
|
||||
<ServiceDetails />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ServicesTabs;
|
||||
@@ -1,179 +0,0 @@
|
||||
import { fireEvent, screen, waitFor } from '@testing-library/react';
|
||||
import { server } from 'mocks-server/server';
|
||||
import { rest, RestRequest } from 'msw';
|
||||
|
||||
import { UpdateServiceConfigPayload } from '../types';
|
||||
import {
|
||||
accountsResponse,
|
||||
buildServiceDetailsResponse,
|
||||
CLOUD_ACCOUNT_ID,
|
||||
initialBuckets,
|
||||
} from './mockData';
|
||||
import {
|
||||
assertGenericModalElements,
|
||||
assertS3SyncSpecificElements,
|
||||
renderServiceDetails,
|
||||
} from './utils';
|
||||
|
||||
// --- RESIZE OBSERVER (required by @radix-ui in Tabs/Switch) ---
|
||||
class ResizeObserverMock {
|
||||
observe(): void {}
|
||||
|
||||
unobserve(): void {}
|
||||
|
||||
disconnect(): void {}
|
||||
}
|
||||
global.ResizeObserver = (ResizeObserverMock as unknown) as typeof ResizeObserver;
|
||||
|
||||
// --- MOCKS ---
|
||||
jest.mock('components/MarkdownRenderer/MarkdownRenderer', () => ({
|
||||
MarkdownRenderer: (): JSX.Element => <div data-testid="markdown-renderer" />,
|
||||
}));
|
||||
jest.mock(
|
||||
'container/Integrations/CloudIntegration/AmazonWebServices/ServiceDashboards/ServiceDashboards',
|
||||
() => ({
|
||||
__esModule: true,
|
||||
default: (): JSX.Element => <div data-testid="service-dashboards" />,
|
||||
}),
|
||||
);
|
||||
|
||||
let testServiceId = 's3sync';
|
||||
let testInitialBuckets: Record<string, string[]> = {};
|
||||
const mockGet = jest.fn((param: string) => {
|
||||
if (param === 'cloudAccountId') {
|
||||
return CLOUD_ACCOUNT_ID;
|
||||
}
|
||||
if (param === 'service') {
|
||||
return testServiceId;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
jest.mock('hooks/useUrlQuery', () => ({
|
||||
__esModule: true,
|
||||
default: (): { get: (param: string) => string | null } => ({ get: mockGet }),
|
||||
}));
|
||||
|
||||
// --- TEST SUITE ---
|
||||
describe('ServiceDetails for S3 Sync service', () => {
|
||||
jest.setTimeout(10000);
|
||||
beforeEach(() => {
|
||||
testServiceId = 's3sync';
|
||||
testInitialBuckets = {};
|
||||
server.use(
|
||||
rest.get(
|
||||
'http://localhost/api/v1/cloud-integrations/aws/accounts',
|
||||
(_req, res, ctx) => res(ctx.json(accountsResponse)),
|
||||
),
|
||||
rest.get(
|
||||
'http://localhost/api/v1/cloud-integrations/aws/services/:serviceId',
|
||||
(req, res, ctx) =>
|
||||
res(
|
||||
ctx.json(
|
||||
buildServiceDetailsResponse(
|
||||
req.params.serviceId as string,
|
||||
testInitialBuckets,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
it('should render with logs collection switch and bucket selectors (no buckets initially selected)', async () => {
|
||||
renderServiceDetails({}); // No initial S3 buckets, defaults to 's3sync' serviceId
|
||||
await assertGenericModalElements();
|
||||
await assertS3SyncSpecificElements({});
|
||||
});
|
||||
|
||||
it('should render with logs collection switch and bucket selectors (some buckets initially selected)', async () => {
|
||||
testInitialBuckets = initialBuckets;
|
||||
renderServiceDetails(initialBuckets);
|
||||
await assertGenericModalElements();
|
||||
await assertS3SyncSpecificElements(initialBuckets);
|
||||
});
|
||||
|
||||
it('should enable save button after adding a new bucket via combobox', async () => {
|
||||
testInitialBuckets = initialBuckets;
|
||||
renderServiceDetails(initialBuckets);
|
||||
await assertGenericModalElements();
|
||||
await assertS3SyncSpecificElements(initialBuckets);
|
||||
|
||||
const targetCombobox = screen.getAllByRole('combobox')[0];
|
||||
const newBucketName = 'a-newly-added-bucket';
|
||||
|
||||
fireEvent.change(targetCombobox, { target: { value: newBucketName } });
|
||||
fireEvent.keyDown(targetCombobox, {
|
||||
key: 'Enter',
|
||||
code: 'Enter',
|
||||
keyCode: 13,
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByLabelText(newBucketName)).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: /save/i })).toBeEnabled();
|
||||
});
|
||||
});
|
||||
|
||||
it('should send updated bucket configuration on save', async () => {
|
||||
let capturedPayload: UpdateServiceConfigPayload | null = null;
|
||||
const mockUpdateConfigUrl =
|
||||
'http://localhost/api/v1/cloud-integrations/aws/services/s3sync/config';
|
||||
|
||||
// Override POST handler specifically for this test to capture payload
|
||||
server.use(
|
||||
rest.post(mockUpdateConfigUrl, async (req: RestRequest, res, ctx) => {
|
||||
capturedPayload = await req.json();
|
||||
return res(ctx.status(200), ctx.json({ message: 'Config updated' }));
|
||||
}),
|
||||
);
|
||||
testInitialBuckets = initialBuckets;
|
||||
renderServiceDetails(initialBuckets);
|
||||
await assertGenericModalElements();
|
||||
await assertS3SyncSpecificElements(initialBuckets);
|
||||
|
||||
const newBucketName = 'another-new-bucket';
|
||||
const targetCombobox = screen.getAllByRole('combobox')[0];
|
||||
|
||||
fireEvent.change(targetCombobox, { target: { value: newBucketName } });
|
||||
fireEvent.keyDown(targetCombobox, {
|
||||
key: 'Enter',
|
||||
code: 'Enter',
|
||||
keyCode: 13,
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByLabelText(newBucketName)).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: /save/i })).toBeEnabled();
|
||||
});
|
||||
fireEvent.click(screen.getByRole('button', { name: /save/i }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(capturedPayload).not.toBeNull();
|
||||
});
|
||||
|
||||
expect(capturedPayload).toEqual({
|
||||
cloud_account_id: CLOUD_ACCOUNT_ID,
|
||||
config: {
|
||||
logs: {
|
||||
enabled: true,
|
||||
s3_buckets: {
|
||||
'us-east-2': ['first-bucket', 'second-bucket'],
|
||||
'ap-south-1': [newBucketName],
|
||||
},
|
||||
},
|
||||
metrics: { enabled: false },
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should not render S3 bucket region selector UI for services other than s3sync', async () => {
|
||||
testServiceId = 'cloudwatch';
|
||||
testInitialBuckets = {};
|
||||
renderServiceDetails({}, 'cloudwatch');
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.queryByText(/select s3 buckets by region/i),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,54 +0,0 @@
|
||||
import { ServiceDetailsResponse } from '../types';
|
||||
|
||||
const CLOUD_ACCOUNT_ID = '123456789012';
|
||||
|
||||
const initialBuckets = { 'us-east-2': ['first-bucket', 'second-bucket'] };
|
||||
|
||||
const accountsResponse = {
|
||||
status: 'success',
|
||||
data: {
|
||||
accounts: [
|
||||
{
|
||||
id: 'a1b2c3d4-e5f6-7890-1234-567890abcdef',
|
||||
cloud_account_id: CLOUD_ACCOUNT_ID,
|
||||
config: {
|
||||
regions: ['ap-south-1', 'ap-south-2', 'us-east-1', 'us-east-2'],
|
||||
},
|
||||
status: {
|
||||
integration: {
|
||||
last_heartbeat_ts_ms: 1747114366214,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
/** Response shape for GET /cloud-integrations/aws/services/:serviceId (used by ServiceDetails). */
|
||||
const buildServiceDetailsResponse = (
|
||||
serviceId: string,
|
||||
initialConfigLogsS3Buckets: Record<string, string[]> = {},
|
||||
): ServiceDetailsResponse => ({
|
||||
status: 'success',
|
||||
data: {
|
||||
id: serviceId,
|
||||
title: serviceId === 's3sync' ? 'S3 Sync' : serviceId,
|
||||
icon: '',
|
||||
overview: '',
|
||||
supported_signals: { logs: serviceId === 's3sync', metrics: false },
|
||||
assets: { dashboards: [] },
|
||||
data_collected: { logs: [], metrics: [] },
|
||||
config: {
|
||||
logs: { enabled: true, s3_buckets: initialConfigLogsS3Buckets },
|
||||
metrics: { enabled: false },
|
||||
},
|
||||
status: { logs: null, metrics: null },
|
||||
},
|
||||
});
|
||||
|
||||
export {
|
||||
accountsResponse,
|
||||
buildServiceDetailsResponse,
|
||||
CLOUD_ACCOUNT_ID,
|
||||
initialBuckets,
|
||||
};
|
||||
@@ -1,56 +0,0 @@
|
||||
import { render, RenderResult, screen, waitFor } from '@testing-library/react';
|
||||
import MockQueryClientProvider from 'providers/test/MockQueryClientProvider';
|
||||
|
||||
import ServiceDetails from '../ServiceDetails/ServiceDetails';
|
||||
import { accountsResponse } from './mockData';
|
||||
|
||||
/**
|
||||
* Renders ServiceDetails (inline config form). Tests must register MSW handlers
|
||||
* for GET accounts and GET service details, and mock useUrlQuery (cloudAccountId, service).
|
||||
*/
|
||||
const renderServiceDetails = (
|
||||
_initialConfigLogsS3Buckets: Record<string, string[]> = {},
|
||||
_serviceId = 's3sync',
|
||||
): RenderResult =>
|
||||
render(
|
||||
<MockQueryClientProvider>
|
||||
<ServiceDetails />
|
||||
</MockQueryClientProvider>,
|
||||
);
|
||||
|
||||
/**
|
||||
* Asserts generic UI elements of the ServiceDetails config form (Overview tab).
|
||||
*/
|
||||
const assertGenericModalElements = async (): Promise<void> => {
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole('switch')).toBeInTheDocument();
|
||||
expect(screen.getByText(/log collection/i)).toBeInTheDocument();
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Asserts S3 bucket selector section: title, region labels, and one combobox per region.
|
||||
* Does not assert placeholder text (antd Select may not expose it as placeholder attribute).
|
||||
*/
|
||||
const assertS3SyncSpecificElements = async (
|
||||
_expectedBucketsByRegion: Record<string, string[]> = {},
|
||||
): Promise<void> => {
|
||||
const regions = accountsResponse.data.accounts[0]?.config?.regions || [];
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/select s3 buckets by region/i)).toBeInTheDocument();
|
||||
|
||||
regions.forEach((region) => {
|
||||
expect(screen.getByText(region)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
const comboboxes = screen.getAllByRole('combobox');
|
||||
expect(comboboxes.length).toBeGreaterThanOrEqual(regions.length);
|
||||
});
|
||||
};
|
||||
|
||||
export {
|
||||
assertGenericModalElements,
|
||||
assertS3SyncSpecificElements,
|
||||
renderServiceDetails,
|
||||
};
|
||||
@@ -1,345 +0,0 @@
|
||||
.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 {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
|
||||
.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-form-regions-selector {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
|
||||
.ant-select {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.azure-account-form-error {
|
||||
color: var(--destructive);
|
||||
font-family: Inter;
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.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-form-resource-groups-selector {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
|
||||
.ant-select {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.azure-account-form-error {
|
||||
color: var(--destructive);
|
||||
font-family: Inter;
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
@@ -1,245 +0,0 @@
|
||||
import { useCallback, useState } from 'react';
|
||||
import { Controller, useForm } from 'react-hook-form';
|
||||
import { useMutation, useQueryClient } from 'react-query';
|
||||
import { Button } from '@signozhq/button';
|
||||
import { toast } from '@signozhq/sonner';
|
||||
import { 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;
|
||||
}
|
||||
|
||||
type AzureAccountFormValues = {
|
||||
primaryRegion: string;
|
||||
resourceGroups: string[];
|
||||
};
|
||||
|
||||
export const AzureAccountForm = ({
|
||||
mode = 'add',
|
||||
selectedAccount,
|
||||
connectionParams,
|
||||
isConnectionParamsLoading,
|
||||
isLoading,
|
||||
onSubmit,
|
||||
submitButtonText = 'Fetch Deployment Command',
|
||||
showDisconnectAccountButton = false,
|
||||
}: AzureAccountFormProps): JSX.Element => {
|
||||
const queryClient = useQueryClient();
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
|
||||
const {
|
||||
control,
|
||||
handleSubmit: handleFormSubmit,
|
||||
} = useForm<AzureAccountFormValues>({
|
||||
defaultValues: {
|
||||
primaryRegion:
|
||||
(selectedAccount?.config as AzureCloudAccountConfig)?.deployment_region ||
|
||||
'',
|
||||
resourceGroups:
|
||||
(selectedAccount?.config as AzureCloudAccountConfig)?.resource_groups || [],
|
||||
},
|
||||
});
|
||||
|
||||
const onFormSubmit = useCallback(
|
||||
(values: AzureAccountFormValues): void => {
|
||||
onSubmit({
|
||||
primaryRegion: values.primaryRegion,
|
||||
resourceGroups: values.resourceGroups,
|
||||
});
|
||||
},
|
||||
[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 className="azure-account-form">
|
||||
<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">
|
||||
<Controller
|
||||
control={control}
|
||||
name="primaryRegion"
|
||||
rules={{
|
||||
required: 'Please select a primary region',
|
||||
}}
|
||||
render={({ field, fieldState }): JSX.Element => (
|
||||
<div className="azure-account-form-regions-selector">
|
||||
<Select
|
||||
{...field}
|
||||
disabled={mode === 'edit'}
|
||||
placeholder="Select primary region"
|
||||
options={AZURE_REGIONS}
|
||||
showSearch
|
||||
filterOption={(input, option): boolean => {
|
||||
const label = String(option?.label ?? '');
|
||||
const value = String(option?.value ?? '');
|
||||
|
||||
return (
|
||||
label.toLowerCase().includes(input.toLowerCase()) ||
|
||||
value.toLowerCase().includes(input.toLowerCase())
|
||||
);
|
||||
}}
|
||||
notFoundContent={null}
|
||||
/>
|
||||
{fieldState.error && (
|
||||
<div className="azure-account-form-error">
|
||||
{fieldState.error.message}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
</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">
|
||||
<Controller
|
||||
control={control}
|
||||
name="resourceGroups"
|
||||
rules={{
|
||||
required: 'Please enter resource groups you want to monitor',
|
||||
validate: (value: string[] | undefined): boolean | string =>
|
||||
Array.isArray(value) && value.length > 0
|
||||
? true
|
||||
: 'Please enter resource groups you want to monitor',
|
||||
}}
|
||||
render={({ field, fieldState }): JSX.Element => (
|
||||
<div className="azure-account-form-resource-groups-selector">
|
||||
<Select
|
||||
{...field}
|
||||
placeholder="Enter resource groups you want to monitor"
|
||||
options={[]}
|
||||
mode="tags"
|
||||
notFoundContent={null}
|
||||
filterOption={false}
|
||||
showSearch={false}
|
||||
/>
|
||||
{fieldState.error && (
|
||||
<div className="azure-account-form-error">
|
||||
{fieldState.error.message}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
</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={handleFormSubmit(onFormSubmit)}
|
||||
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>
|
||||
);
|
||||
};
|
||||
@@ -1,379 +0,0 @@
|
||||
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 you’re 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>
|
||||
);
|
||||
}
|
||||
@@ -1,90 +0,0 @@
|
||||
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;
|
||||
@@ -1,201 +0,0 @@
|
||||
.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-metrics {
|
||||
border-top: 1px solid var(--l3-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 {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
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% */
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,461 +0,0 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useQueryClient } from 'react-query';
|
||||
import { Button } from '@signozhq/button';
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import Tabs from '@signozhq/tabs';
|
||||
import { Checkbox, Popover, 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 { CheckCircle, Save, TriangleAlert, X } from 'lucide-react';
|
||||
|
||||
import { ConfigConnectionStatus } from '../../ConfigConnectionStatus/ConfigConnectionStatus';
|
||||
|
||||
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]);
|
||||
|
||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||
const renderOverview = (): JSX.Element => {
|
||||
const dashboards = serviceDetailsData?.assets?.dashboards || [];
|
||||
|
||||
const logsStatus = serviceDetailsData?.status?.logs || null;
|
||||
const metricsStatus = serviceDetailsData?.status?.metrics || null;
|
||||
|
||||
// connected if all config items are connected
|
||||
const logsConnectionStatus = logsStatus?.every(
|
||||
(log) => log.last_received_ts_ms > 0,
|
||||
);
|
||||
const metricsConnectionStatus = metricsStatus?.every(
|
||||
(metric) => metric.last_received_ts_ms > 0,
|
||||
);
|
||||
|
||||
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">
|
||||
{logsStatus && (
|
||||
<Popover
|
||||
content={<ConfigConnectionStatus status={logsStatus} />}
|
||||
trigger="hover"
|
||||
placement="right"
|
||||
overlayClassName="config-connection-status-popover"
|
||||
>
|
||||
<div className="aws-service-details-overview-configuration-title-text-icon">
|
||||
{logsConnectionStatus ? (
|
||||
<CheckCircle size={16} color={Color.BG_FOREST_500} />
|
||||
) : (
|
||||
<TriangleAlert size={16} color={Color.BG_AMBER_500} />
|
||||
)}
|
||||
</div>
|
||||
</Popover>
|
||||
)}
|
||||
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">
|
||||
{metricsStatus && (
|
||||
<Popover
|
||||
content={<ConfigConnectionStatus status={metricsStatus} />}
|
||||
trigger="hover"
|
||||
placement="right"
|
||||
overlayClassName="config-connection-status-popover"
|
||||
>
|
||||
<div className="aws-service-details-overview-configuration-title-text-icon">
|
||||
{metricsConnectionStatus ? (
|
||||
<CheckCircle size={16} color={Color.BG_FOREST_500} />
|
||||
) : (
|
||||
<TriangleAlert size={16} color={Color.BG_AMBER_500} />
|
||||
)}
|
||||
</div>
|
||||
</Popover>
|
||||
)}
|
||||
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>
|
||||
);
|
||||
}
|
||||
@@ -1,182 +0,0 @@
|
||||
.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
@@ -1,152 +0,0 @@
|
||||
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;
|
||||
@@ -1,3 +0,0 @@
|
||||
.cloud-integration-container {
|
||||
height: 100%;
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
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;
|
||||
@@ -1,42 +0,0 @@
|
||||
.config-connection-status-popover {
|
||||
.ant-popover-inner {
|
||||
padding: 0;
|
||||
background-color: var(--l2-background);
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--l3-background);
|
||||
padding: 8px;
|
||||
|
||||
width: 240px;
|
||||
|
||||
.ant-popover-content {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.config-connection-status-item {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.config-connection-status-icon {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.config-connection-status-category-display-name {
|
||||
color: var(--l2-foreground);
|
||||
font-family: Inter;
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 18px; /* 138.462% */
|
||||
}
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { IConfigConnectionStatus } from 'container/Integrations/types';
|
||||
import { CheckCircle, TriangleAlert } from 'lucide-react';
|
||||
|
||||
import './ConfigConnectionStatus.styles.scss';
|
||||
|
||||
export function ConfigConnectionStatus({
|
||||
status,
|
||||
}: {
|
||||
status: IConfigConnectionStatus[] | null;
|
||||
}): JSX.Element {
|
||||
return (
|
||||
<div className="config-connection-status-container">
|
||||
{status?.map((status) => (
|
||||
<div key={status.category} className="config-connection-status-item">
|
||||
<div className="config-connection-status-icon">
|
||||
{status.last_received_ts_ms && status.last_received_ts_ms > 0 ? (
|
||||
<CheckCircle size={16} color={Color.BG_FOREST_500} />
|
||||
) : (
|
||||
<TriangleAlert size={16} color={Color.BG_AMBER_500} />
|
||||
)}
|
||||
</div>
|
||||
<div className="config-connection-status-category-display-name">
|
||||
{status.category_display_name}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
export const getAccountById = <T extends { cloud_account_id: string }>(
|
||||
accounts: T[],
|
||||
accountId: string,
|
||||
): T | null =>
|
||||
accounts.find((account) => account.cloud_account_id === accountId) || null;
|
||||
@@ -1,66 +0,0 @@
|
||||
.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;
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
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;
|
||||
@@ -1,81 +0,0 @@
|
||||
.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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,165 +0,0 @@
|
||||
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;
|
||||
@@ -1,235 +0,0 @@
|
||||
.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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,183 +0,0 @@
|
||||
import { useMemo } from 'react';
|
||||
import { Badge } from '@signozhq/badge';
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { Button, Skeleton, Typography } from 'antd';
|
||||
import { useGetAllIntegrations } from 'hooks/Integrations/useGetAllIntegrations';
|
||||
import { useGetTenantLicense } from 'hooks/useGetTenantLicense';
|
||||
import { MoveUpRight, RotateCw } from 'lucide-react';
|
||||
import { IntegrationsProps } from 'types/api/integrations/types';
|
||||
|
||||
import { handleContactSupport } from '../utils';
|
||||
|
||||
import './IntegrationsList.styles.scss';
|
||||
|
||||
interface IntegrationsListProps {
|
||||
searchQuery: string;
|
||||
setSelectedIntegration: (integration: IntegrationsProps) => void;
|
||||
}
|
||||
|
||||
function IntegrationsList(props: IntegrationsListProps): JSX.Element {
|
||||
const { searchQuery, setSelectedIntegration } = props;
|
||||
|
||||
const {
|
||||
data,
|
||||
isFetching,
|
||||
isLoading,
|
||||
isRefetching,
|
||||
isError,
|
||||
refetch,
|
||||
} = useGetAllIntegrations();
|
||||
|
||||
const { isCloudUser: isCloudUserVal } = useGetTenantLicense();
|
||||
|
||||
const integrationsList = useMemo(() => {
|
||||
if (!data?.data.data.integrations) {
|
||||
return [];
|
||||
}
|
||||
const integrations = data.data.data.integrations;
|
||||
const query = searchQuery.trim().toLowerCase();
|
||||
if (!query) {
|
||||
return integrations;
|
||||
}
|
||||
return integrations.filter(
|
||||
(integration) =>
|
||||
integration.title.toLowerCase().includes(query) ||
|
||||
integration.description.toLowerCase().includes(query),
|
||||
);
|
||||
}, [data?.data.data.integrations, searchQuery]);
|
||||
|
||||
const loading = isLoading || isFetching || isRefetching;
|
||||
|
||||
const handleSelectedIntegration = (integration: IntegrationsProps): void => {
|
||||
setSelectedIntegration(integration);
|
||||
};
|
||||
|
||||
const renderError = (): JSX.Element => {
|
||||
return (
|
||||
<div className="error-container">
|
||||
<div className="error-content">
|
||||
<img
|
||||
src="/Icons/awwSnap.svg"
|
||||
alt="error-emoji"
|
||||
className="error-state-svg"
|
||||
/>
|
||||
<Typography.Text>
|
||||
Something went wrong :/ Please retry or contact support.
|
||||
</Typography.Text>
|
||||
<div className="error-btns">
|
||||
<Button
|
||||
type="primary"
|
||||
className="retry-btn"
|
||||
onClick={(): Promise<any> => refetch()}
|
||||
icon={<RotateCw size={14} />}
|
||||
>
|
||||
Retry
|
||||
</Button>
|
||||
<div
|
||||
className="contact-support"
|
||||
onClick={(): void => handleContactSupport(isCloudUserVal)}
|
||||
>
|
||||
<Typography.Link className="text">Contact Support </Typography.Link>
|
||||
|
||||
<MoveUpRight size={14} color={Color.BG_ROBIN_400} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="integrations-list-container">
|
||||
<div className="integrations-list-title-header">
|
||||
<div className="integrations-list-header-title">All Integrations</div>
|
||||
<div className="integrations-list-header-dotted-double-line">
|
||||
<img
|
||||
src="/svgs/dotted-double-line.svg"
|
||||
alt="dotted-double-line"
|
||||
width="100%"
|
||||
height="100%"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{!loading && isError && renderError()}
|
||||
|
||||
{loading && (
|
||||
<div className="loading-container">
|
||||
<Skeleton.Input active size="large" className="skeleton-item" />
|
||||
<Skeleton.Input active size="large" className="skeleton-item" />
|
||||
<Skeleton.Input active size="large" className="skeleton-item" />
|
||||
<Skeleton.Input active size="large" className="skeleton-item" />
|
||||
<Skeleton.Input active size="large" className="skeleton-item" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!loading && integrationsList.length === 0 && searchQuery.trim() && (
|
||||
<div className="integrations-not-found-container">
|
||||
<div className="integrations-not-found-content">
|
||||
<img
|
||||
src="/Icons/awwSnap.svg"
|
||||
alt="no-integrations"
|
||||
className="integrations-not-found-image"
|
||||
/>
|
||||
<div className="integrations-not-found-text">
|
||||
No integrations found for “{searchQuery.trim()}”
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!loading && integrationsList.length > 0 && (
|
||||
<div className="integrations-list">
|
||||
<div className="integrations-list-header">
|
||||
<div className="integrations-list-header-column title-column">Name</div>
|
||||
<div className="integrations-list-header-column published-by-column">
|
||||
Published By
|
||||
</div>
|
||||
<div className="integrations-list-header-column installation-status-column">
|
||||
Status
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{integrationsList.map((integration) => (
|
||||
<div
|
||||
className="integrations-list-item"
|
||||
key={integration.id}
|
||||
onClick={(): void => handleSelectedIntegration(integration)}
|
||||
>
|
||||
<div className="integrations-list-item-column title-column">
|
||||
<div className="integrations-list-item-name-image-container">
|
||||
<img
|
||||
src={integration.icon}
|
||||
alt={integration.title}
|
||||
className="integrations-list-item-name-image-container-image"
|
||||
/>
|
||||
<div className="integrations-list-item-name-text">
|
||||
{integration.title}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="integrations-list-item-column">
|
||||
<div className="integrations-list-item-published-by">SigNoz</div>
|
||||
</div>
|
||||
<div className="integrations-list-item-column">
|
||||
<div className="integrations-list-item-installation-status">
|
||||
<Badge
|
||||
color={integration.is_installed ? 'forest' : 'amber'}
|
||||
variant="outline"
|
||||
capitalize
|
||||
>
|
||||
{integration.is_installed ? 'Installed' : 'Not Installed'}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default IntegrationsList;
|
||||
@@ -1,77 +0,0 @@
|
||||
.one-click-integrations {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
|
||||
.one-click-integrations-header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 32px;
|
||||
|
||||
.one-click-integrations-header-title {
|
||||
color: var(--l1-foreground);
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
line-height: 20px;
|
||||
letter-spacing: 0.48px;
|
||||
text-transform: uppercase;
|
||||
|
||||
display: inline-block;
|
||||
word-wrap: normal;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.one-click-integrations-header-dotted-double-line {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.one-click-integrations-list {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 16px;
|
||||
.one-click-integrations-list-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 8px 12px 12px 12px;
|
||||
gap: 10px;
|
||||
width: fit-content;
|
||||
|
||||
border-radius: 3px;
|
||||
border: 1px solid var(--l3-background);
|
||||
background: var(--l1-background);
|
||||
cursor: pointer;
|
||||
transition: background 0.2s ease-in-out;
|
||||
|
||||
.one-click-integrations-list-item-title {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
.one-click-integrations-list-item-title-image-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
|
||||
.one-click-integrations-list-item-title-text {
|
||||
color: var(--l1-foreground);
|
||||
font-family: Inter;
|
||||
font-size: 13px;
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
line-height: 32px; /* 200% */
|
||||
letter-spacing: -0.08px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: var(--l3-background);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user