mirror of
https://github.com/SigNoz/signoz.git
synced 2026-02-25 09:42:25 +00:00
Compare commits
1 Commits
feat/azure
...
query-rang
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f05c574606 |
216
docs/contributing/query-range.md
Normal file
216
docs/contributing/query-range.md
Normal file
@@ -0,0 +1,216 @@
|
||||
# Query Range v5 — Design Principles & Architectural Contracts
|
||||
|
||||
## Purpose of This Document
|
||||
|
||||
This document defines the design principles, invariants, and architectural contracts of the Query Range v5 system. It is intended for the authors working on the querier and querier related parts codebase. Any change to the system must align with the principles described here. If a change would violate a principle, it must be flagged and discussed.
|
||||
|
||||
---
|
||||
|
||||
## Core Architectural Principle
|
||||
|
||||
**The user speaks OpenTelemetry. The storage speaks ClickHouse. The system translates between them. These two worlds must never leak into each other.**
|
||||
|
||||
Every design choice in Query Range flows from this separation. The user-facing API surface deals exclusively in `TelemetryFieldKey`: a representation of fields as they exist in the OpenTelemetry data model. The storage layer deals in ClickHouse column expressions, table names, and SQL fragments. The translation between them is mediated by a small set of composable abstractions with strict boundaries.
|
||||
|
||||
---
|
||||
|
||||
## The Central Type: `TelemetryFieldKey`
|
||||
|
||||
`TelemetryFieldKey` is the atomic unit of the entire query system. Every filter, aggregation, group-by, order-by, and select operation is expressed in terms of field keys. Understanding its design contracts is non-negotiable.
|
||||
|
||||
### Identity
|
||||
|
||||
A field key is identified by three dimensions:
|
||||
|
||||
- **Name** — the field name as the user knows it (`service.name`, `http.method`, `trace_id`)
|
||||
- **FieldContext** — where the field lives in the OTel model (`resource`, `attribute`, `span`, `log`, `body`, `scope`, `event`, `metric`)
|
||||
- **FieldDataType** — the data type (`string`, `bool`, `number`/`float64`/`int64`, array variants)
|
||||
|
||||
### Invariant: Same name does not mean same field
|
||||
|
||||
`status` as an attribute, `status` as a body JSON key, and `status` as a span-level field are **three different fields**. The context disambiguates. Code that resolves or compares field keys must always consider all three dimensions, never just the name.
|
||||
|
||||
### Invariant: Normalization happens once, at the boundary
|
||||
|
||||
`TelemetryFieldKey.Normalize()` is called during JSON unmarshaling. After normalization, the text representation `resource.service.name:string` and the programmatic construction `{Name: "service.name", FieldContext: Resource, FieldDataType: String}` are identical.
|
||||
|
||||
**Consequence:** Downstream code must never re-parse or re-normalize field keys. If you find yourself splitting on `.` or `:` deep in the pipeline, something is wrong — the normalization should have already happened.
|
||||
|
||||
### Invariant: The text format is `context.name:datatype`
|
||||
|
||||
Parsing rules (implemented in `GetFieldKeyFromKeyText` and `Normalize`):
|
||||
|
||||
1. Data type is extracted from the right, after the last `:`.
|
||||
2. Field context is extracted from the left, before the first `.`, if it matches a known context prefix.
|
||||
3. Everything remaining is the name.
|
||||
|
||||
Special case: `log.body.X` normalizes to `{FieldContext: body, Name: X}` — the `log.body.` prefix collapses because body fields under log are a nested context.
|
||||
|
||||
### Invariant: Historical aliases must be preserved
|
||||
|
||||
The `fieldContexts` map includes aliases (`tag` -> `attribute`, `spanfield` -> `span`, `logfield` -> `log`). These exist because older database entries use these names. Removing or changing these aliases will break existing saved queries and dashboard configurations.
|
||||
|
||||
---
|
||||
|
||||
## The Abstraction Stack
|
||||
|
||||
The query pipeline is built from four interfaces that compose vertically. Each layer has a single responsibility. Each layer depends only on the layers below it. This layering is intentional and must be preserved.
|
||||
|
||||
```
|
||||
StatementBuilder <- Orchestrates everything into executable SQL
|
||||
├── AggExprRewriter <- Rewrites aggregation expressions (maps field refs to columns)
|
||||
├── ConditionBuilder <- Builds WHERE predicates (field + operator + value -> SQL)
|
||||
└── FieldMapper <- Maps TelemetryFieldKey -> ClickHouse column expression
|
||||
```
|
||||
|
||||
### FieldMapper
|
||||
|
||||
**Contract:** Given a `TelemetryFieldKey`, return a ClickHouse column expression that yields the value for that field when used in a SELECT.
|
||||
|
||||
**Principle:** This is the *only* place where field-to-column translation happens. No other layer should contain knowledge of how fields map to storage. If you need a column expression, go through the FieldMapper.
|
||||
|
||||
**Why:** The user says `http.request.method`. ClickHouse might store it as `attributes_string['http.request.method']`, or as a materialized column `` `attribute_string_http$$request$$method` ``, or via a JSON access path in a body column. This variation is entirely contained within the FieldMapper. Everything above it is storage-agnostic.
|
||||
|
||||
### ConditionBuilder
|
||||
|
||||
**Contract:** Given a field key, an operator, and a value, produce a valid SQL predicate for a WHERE clause.
|
||||
|
||||
**Dependency:** Uses FieldMapper for the left-hand side of the condition.
|
||||
|
||||
**Principle:** The ConditionBuilder owns all the complexity of operator semantics, i.e type casting, array operators (`hasAny`/`hasAll` vs `=`), existence checks, and negative operator behavior. This complexity must not leak upward into the StatementBuilder.
|
||||
|
||||
### AggExprRewriter
|
||||
|
||||
**Contract:** Given a user-facing aggregation expression like `sum(duration_nano)`, resolve field references within it and produce valid ClickHouse SQL.
|
||||
|
||||
**Dependency:** Uses FieldMapper to resolve field names within expressions.
|
||||
|
||||
**Principle:** Aggregation expressions are user-authored strings that contain field references. The rewriter parses them, identifies field references, resolves each through the FieldMapper, and reassembles the expression.
|
||||
|
||||
### StatementBuilder
|
||||
|
||||
**Contract:** Given a complete `QueryBuilderQuery`, a time range, and a request type, produces an executable SQL statement.
|
||||
|
||||
**Dependency:** Uses all three abstractions above.
|
||||
|
||||
**Principle:** This is the composition layer. It does not contain field mapping logic, condition building logic, or expression rewriting logic. It orchestrates the other abstractions. If you find storage-specific logic creeping into the StatementBuilder, push it down into the appropriate abstraction.
|
||||
|
||||
### Invariant: No layer skipping
|
||||
|
||||
The StatementBuilder must not call FieldMapper directly to build conditions, it goes through the ConditionBuilder. The AggExprRewriter must not hardcode column names, it goes through the FieldMapper. Skipping layers creates hidden coupling and makes the system fragile to storage changes.
|
||||
|
||||
---
|
||||
|
||||
## Design Decisions as Constraints
|
||||
|
||||
### Constraint: Formula evaluation happens in Go, not in ClickHouse
|
||||
|
||||
Formulas (`A + B`, `A / B`, `sqrt(A*A + B*B)`) are evaluated application-side by `FormulaEvaluator`, not via ClickHouse JOINs.
|
||||
|
||||
**Why this is a constraint, not just an implementation choice:** The original JOIN-based approach was abandoned because ClickHouse evaluates joins right-to-left, serializing execution unnecessarily. Running queries independently allows parallelism and caching of intermediate results. Any future optimization must not reintroduce the JOIN pattern without solving the serialization problem.
|
||||
|
||||
**Consequence:** Individual query results must be independently cacheable. Formula evaluation must handle label matching, timestamp alignment, and missing values without requiring the queries to coordinate at the SQL level.
|
||||
|
||||
### Constraint: Zero-defaulting is aggregation-dependent
|
||||
|
||||
Only additive/counting aggregations (`count`, `count_distinct`, `sum`, `rate`) default missing values to zero. Statistical aggregations (`avg`, `min`, `max`, percentiles) must show gaps.
|
||||
|
||||
**Why:** Absence of data has different meanings. No error requests in a time bucket means error count = 0. No requests at all means average latency is *unknown*, not 0. Conflating these is a correctness bug, not a display preference.
|
||||
|
||||
**Enforcement:** `GetQueriesSupportingZeroDefault` determines which queries can default to zero. The `FormulaEvaluator` consumes this via `canDefaultZero`. Changes to aggregation handling must preserve this distinction.
|
||||
|
||||
### Constraint: Existence semantics differ for positive vs negative operators
|
||||
|
||||
- **Positive operators** (`=`, `>`, `LIKE`, `IN`, etc.) implicitly assert field existence. `http.method = GET` means "the field exists AND equals GET".
|
||||
- **Negative operators** (`!=`, `NOT IN`, `NOT LIKE`, etc.) do **not** add an existence check. `http.method != GET` includes records where the field doesn't exist at all.
|
||||
|
||||
**Why:** The user's intent with negative operators is ambiguous. Rather than guess, we take the broader interpretation. Users can add an explicit `EXISTS` filter if they want the narrower one. This is documented in `AddDefaultExistsFilter`.
|
||||
|
||||
**Consequence:** Any new operator must declare its existence behavior in `AddDefaultExistsFilter`. Do not add operators without considering this.
|
||||
|
||||
### Constraint: Post-processing functions operate on result sets, not in SQL
|
||||
|
||||
Functions like `cutOffMin`, `ewma`, `median`, `timeShift`, `fillZero`, `runningDiff`, and `cumulativeSum` are applied in Go on the returned time series, not pushed into ClickHouse SQL.
|
||||
|
||||
**Why:** These are sequential time-series transformations that require complete, ordered result sets. Pushing them into SQL would complicate query generation, prevent caching of raw results, and make the functions harder to test. They are applied via `ApplyFunctions` after query execution.
|
||||
|
||||
**Consequence:** New time-series transformation functions should follow this pattern i.e implement them as Go functions on `*TimeSeries`, not as SQL modifications.
|
||||
|
||||
### Constraint: The API surface rejects unknown fields with suggestions
|
||||
|
||||
All request types use custom `UnmarshalJSON` that calls `DisallowUnknownFields`. Unknown fields trigger error messages with Levenshtein-based suggestions ("did you mean: 'groupBy'?").
|
||||
|
||||
**Why:** Silent acceptance of unknown fields causes subtle bugs. A misspelled `groupBy` results in ungrouped data with no indication of what went wrong. Failing fast with suggestions turns errors into actionable feedback.
|
||||
|
||||
**Consequence:** Any new request type or query spec struct must implement custom unmarshaling with `UnmarshalJSONWithContext`. Do not use default `json.Unmarshal` for user-facing types.
|
||||
|
||||
### Constraint: Validation is context-sensitive to request type
|
||||
|
||||
What's valid depends on the `RequestType`. For aggregation requests (`time_series`, `scalar`, `distribution`), fields like `groupBy`, `aggregations`, `having`, and aggregation-referenced `orderBy` are validated. For non-aggregation requests (`raw`, `raw_stream`, `trace`), these fields are ignored.
|
||||
|
||||
**Why:** A raw log query doesn't have aggregations, so requiring `aggregations` would be wrong. But a time-series query without aggregations is meaningless. The validation rules are request-type-aware to avoid both false positives and false negatives.
|
||||
|
||||
**Consequence:** When adding new fields to query specs, consider which request types they apply to and gate validation accordingly.
|
||||
|
||||
---
|
||||
|
||||
## The Composite Query Model
|
||||
|
||||
### Structure
|
||||
|
||||
A `QueryRangeRequest` contains a `CompositeQuery` which holds `[]QueryEnvelope`. Each envelope is a discriminated union: a `Type` field determines how `Spec` is decoded.
|
||||
|
||||
### Invariant: Query names are unique within a composite query
|
||||
|
||||
Builder queries must have unique names. Formulas reference queries by name (`A`, `B`, `A.0`, `A.my_alias`). Duplicate names would make formula evaluation ambiguous.
|
||||
|
||||
### Invariant: Multi-aggregation uses indexed or aliased references
|
||||
|
||||
A single builder query can have multiple aggregations. They are accessed in formulas via:
|
||||
- Index: `A.0`, `A.1` (zero-based)
|
||||
- Alias: `A.total`, `A.error_count`
|
||||
|
||||
The default (just `A`) resolves to index 0. This is the formula evaluation contract and must be preserved.
|
||||
|
||||
### Invariant: Type-specific decoding through signal detection
|
||||
|
||||
Builder queries are decoded by first peeking at the `signal` field in the raw JSON, then unmarshaling into the appropriate generic type (`QueryBuilderQuery[TraceAggregation]`, `QueryBuilderQuery[LogAggregation]`, `QueryBuilderQuery[MetricAggregation]`). This two-pass decoding is intentional — it allows each signal to have its own aggregation schema while sharing the query structure.
|
||||
|
||||
---
|
||||
|
||||
## The Metadata Layer
|
||||
|
||||
### MetadataStore
|
||||
|
||||
The `MetadataStore` interface provides runtime field discovery and type resolution. It answers questions like "what fields exist for this signal?" and "what are the data types of field X?".
|
||||
|
||||
### Principle: Fields can be ambiguous until resolved
|
||||
|
||||
The same name can map to multiple `TelemetryFieldKey` variants (different contexts, different types). The metadata store returns *all* variants. Resolution to a single field happens during query building, using the query's signal and any explicit context/type hints from the user.
|
||||
|
||||
**Consequence:** Code that calls `GetKey` or `GetKeys` must handle multiple results. Do not assume a name maps to a single field.
|
||||
|
||||
### Principle: Materialized fields are a performance optimization, not a semantic distinction
|
||||
|
||||
A materialized field and its non-materialized equivalent represent the same logical field. The `Materialized` flag tells the FieldMapper to generate a simpler column expression. The user should never need to know whether a field is materialized.
|
||||
|
||||
### Principle: JSON body fields require access plans
|
||||
|
||||
Fields inside JSON body columns (`body.response.errors[].code`) need pre-computed `JSONAccessPlan` trees that encode the traversal path, including branching at array boundaries between `Array(JSON)` and `Array(Dynamic)` representations. These plans are computed during metadata resolution, not during query execution.
|
||||
|
||||
---
|
||||
|
||||
## Summary of Inviolable Rules
|
||||
|
||||
1. **User-facing types never contain ClickHouse column names or SQL fragments.**
|
||||
2. **Field-to-column translation only happens in FieldMapper.**
|
||||
3. **Normalization happens once at the API boundary, never deeper.**
|
||||
4. **Historical aliases in fieldContexts and fieldDataTypes must not be removed.**
|
||||
5. **Formula evaluation stays in Go — do not push it into ClickHouse JOINs.**
|
||||
6. **Zero-defaulting is aggregation-type-dependent — do not universally default to zero.**
|
||||
7. **Positive operators imply existence, negative operators do not.**
|
||||
8. **Post-processing functions operate on Go result sets, not in SQL.**
|
||||
9. **All user-facing types reject unknown JSON fields with suggestions.**
|
||||
10. **Validation rules are gated by request type.**
|
||||
11. **Query names must be unique within a composite query.**
|
||||
12. **The four-layer abstraction stack (FieldMapper -> ConditionBuilder -> AggExprRewriter -> StatementBuilder) must not be bypassed or flattened.**
|
||||
@@ -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",
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -5,8 +5,8 @@ import { toast } from '@signozhq/sonner';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { handleContactSupport } from 'container/Integrations/utils';
|
||||
import { useGetTenantLicense } from 'hooks/useGetTenantLicense';
|
||||
import { handleContactSupport } from 'pages/Integrations/utils';
|
||||
|
||||
import FeedbackModal from '../FeedbackModal';
|
||||
|
||||
@@ -31,7 +31,7 @@ jest.mock('hooks/useGetTenantLicense', () => ({
|
||||
useGetTenantLicense: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('container/Integrations/utils', () => ({
|
||||
jest.mock('pages/Integrations/utils', () => ({
|
||||
handleContactSupport: jest.fn(),
|
||||
}));
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ export function getDefaultCellStyle(isDarkMode?: boolean): CSSProperties {
|
||||
|
||||
export const defaultTableStyle: CSSProperties = {
|
||||
minWidth: '40rem',
|
||||
maxWidth: '90rem',
|
||||
maxWidth: '60rem',
|
||||
};
|
||||
|
||||
export const defaultListViewPanelStyle: CSSProperties = {
|
||||
|
||||
@@ -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,4 +1,5 @@
|
||||
.hero-section {
|
||||
height: 308px;
|
||||
padding: 26px 16px;
|
||||
display: flex;
|
||||
gap: 24px;
|
||||
@@ -0,0 +1,34 @@
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
|
||||
import AccountActions from './components/AccountActions';
|
||||
|
||||
import './HeroSection.style.scss';
|
||||
|
||||
function HeroSection(): JSX.Element {
|
||||
const isDarkMode = useIsDarkMode();
|
||||
return (
|
||||
<div
|
||||
className="hero-section"
|
||||
style={
|
||||
isDarkMode
|
||||
? {
|
||||
backgroundImage: `url('/Images/integrations-hero-bg.png')`,
|
||||
}
|
||||
: {}
|
||||
}
|
||||
>
|
||||
<div className="hero-section__icon">
|
||||
<img src="/Logos/aws-dark.svg" alt="aws-logo" />
|
||||
</div>
|
||||
<div className="hero-section__details">
|
||||
<div className="title">Amazon Web Services</div>
|
||||
<div className="description">
|
||||
One-click setup for AWS monitoring with SigNoz
|
||||
</div>
|
||||
<AccountActions />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default HeroSection;
|
||||
@@ -11,6 +11,7 @@
|
||||
|
||||
&__input-skeleton {
|
||||
width: 300px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
&__new-account-button-skeleton {
|
||||
@@ -1,16 +1,14 @@
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom-v5-compat';
|
||||
import { Button } from '@signozhq/button';
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { Select, Skeleton } from 'antd';
|
||||
import { Button, Select, Skeleton } from 'antd';
|
||||
import { SelectProps } from 'antd/lib';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { getAccountById } from 'container/Integrations/CloudIntegration/utils';
|
||||
import { useAwsAccounts } from 'hooks/integration/aws/useAwsAccounts';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
import { Check, ChevronDown } from 'lucide-react';
|
||||
|
||||
import { CloudAccount } from '../../types';
|
||||
import { CloudAccount } from '../../ServicesSection/types';
|
||||
import AccountSettingsModal from './AccountSettingsModal';
|
||||
import CloudAccountSetupModal from './CloudAccountSetupModal';
|
||||
|
||||
@@ -50,6 +48,12 @@ function renderOption(
|
||||
);
|
||||
}
|
||||
|
||||
const getAccountById = (
|
||||
accounts: CloudAccount[],
|
||||
accountId: string,
|
||||
): CloudAccount | null =>
|
||||
accounts.find((account) => account.cloud_account_id === accountId) || null;
|
||||
|
||||
function AccountActionsRenderer({
|
||||
accounts,
|
||||
isLoading,
|
||||
@@ -70,7 +74,24 @@ function AccountActionsRenderer({
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="hero-section__actions-with-account">
|
||||
<Skeleton.Input active block className="hero-section__input-skeleton" />
|
||||
<Skeleton.Input
|
||||
active
|
||||
size="large"
|
||||
block
|
||||
className="hero-section__input-skeleton"
|
||||
/>
|
||||
<div className="hero-section__action-buttons">
|
||||
<Skeleton.Button
|
||||
active
|
||||
size="large"
|
||||
className="hero-section__new-account-button-skeleton"
|
||||
/>
|
||||
<Skeleton.Button
|
||||
active
|
||||
size="large"
|
||||
className="hero-section__account-settings-button-skeleton"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -89,12 +110,16 @@ function AccountActionsRenderer({
|
||||
onChange={onAccountChange}
|
||||
/>
|
||||
<div className="hero-section__action-buttons">
|
||||
<Button variant="solid" color="primary" onClick={onIntegrationModalOpen}>
|
||||
<Button
|
||||
type="primary"
|
||||
className="hero-section__action-button primary"
|
||||
onClick={onIntegrationModalOpen}
|
||||
>
|
||||
Add New AWS Account
|
||||
</Button>
|
||||
<Button
|
||||
variant="solid"
|
||||
color="primary"
|
||||
type="default"
|
||||
className="hero-section__action-button secondary"
|
||||
onClick={onAccountSettingsModalOpen}
|
||||
>
|
||||
Account Settings
|
||||
@@ -104,7 +129,10 @@ function AccountActionsRenderer({
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Button variant="solid" color="primary" onClick={onIntegrationModalOpen}>
|
||||
<Button
|
||||
className="hero-section__action-button primary"
|
||||
onClick={onIntegrationModalOpen}
|
||||
>
|
||||
Integrate Now
|
||||
</Button>
|
||||
);
|
||||
@@ -10,8 +10,8 @@ import {
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
import history from 'lib/history';
|
||||
|
||||
import logEvent from '../../../../../../api/common/logEvent';
|
||||
import { CloudAccount } from '../../types';
|
||||
import logEvent from '../../../../api/common/logEvent';
|
||||
import { CloudAccount } from '../../ServicesSection/types';
|
||||
import { RegionSelector } from './RegionSelector';
|
||||
import RemoveIntegrationAccount from './RemoveIntegrationAccount';
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import { useRef } from 'react';
|
||||
import { Form } from 'antd';
|
||||
import cx from 'classnames';
|
||||
import { INTEGRATION_TYPES } from 'container/Integrations/constants';
|
||||
import { useGetAccountStatus } from 'hooks/integration/useGetAccountStatus';
|
||||
import { AccountStatusResponse } from 'types/api/integrations/types';
|
||||
import { useAccountStatus } from 'hooks/integration/aws/useAccountStatus';
|
||||
import { AccountStatusResponse } from 'types/api/integrations/aws';
|
||||
import { regions } from 'utils/regions';
|
||||
|
||||
import logEvent from '../../../../../../api/common/logEvent';
|
||||
import logEvent from '../../../../api/common/logEvent';
|
||||
import { ModalStateEnum, RegionFormProps } from '../types';
|
||||
import AlertMessage from './AlertMessage';
|
||||
import {
|
||||
@@ -45,31 +44,27 @@ export function RegionForm({
|
||||
const refetchInterval = 10 * 1000;
|
||||
const errorTimeout = 10 * 60 * 1000;
|
||||
|
||||
const { isLoading: isAccountStatusLoading } = useGetAccountStatus(
|
||||
INTEGRATION_TYPES.AWS,
|
||||
accountId,
|
||||
{
|
||||
refetchInterval,
|
||||
enabled: !!accountId && modalState === ModalStateEnum.WAITING,
|
||||
onSuccess: (data: AccountStatusResponse) => {
|
||||
if (data.data.status.integration.last_heartbeat_ts_ms !== null) {
|
||||
setModalState(ModalStateEnum.SUCCESS);
|
||||
logEvent('AWS Integration: Account connected', {
|
||||
cloudAccountId: data?.data?.cloud_account_id,
|
||||
status: data?.data?.status,
|
||||
});
|
||||
} else if (Date.now() - startTimeRef.current >= errorTimeout) {
|
||||
setModalState(ModalStateEnum.ERROR);
|
||||
logEvent('AWS Integration: Account connection attempt timed out', {
|
||||
id: accountId,
|
||||
});
|
||||
}
|
||||
},
|
||||
onError: () => {
|
||||
const { isLoading: isAccountStatusLoading } = useAccountStatus(accountId, {
|
||||
refetchInterval,
|
||||
enabled: !!accountId && modalState === ModalStateEnum.WAITING,
|
||||
onSuccess: (data: AccountStatusResponse) => {
|
||||
if (data.data.status.integration.last_heartbeat_ts_ms !== null) {
|
||||
setModalState(ModalStateEnum.SUCCESS);
|
||||
logEvent('AWS Integration: Account connected', {
|
||||
cloudAccountId: data?.data?.cloud_account_id,
|
||||
status: data?.data?.status,
|
||||
});
|
||||
} else if (Date.now() - startTimeRef.current >= errorTimeout) {
|
||||
setModalState(ModalStateEnum.ERROR);
|
||||
},
|
||||
logEvent('AWS Integration: Account connection attempt timed out', {
|
||||
id: accountId,
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
onError: () => {
|
||||
setModalState(ModalStateEnum.ERROR);
|
||||
},
|
||||
});
|
||||
|
||||
const isFormDisabled =
|
||||
modalState === ModalStateEnum.WAITING || isAccountStatusLoading;
|
||||
@@ -2,11 +2,11 @@ import { useState } from 'react';
|
||||
import { useMutation } from 'react-query';
|
||||
import { Button, Modal } from 'antd';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import removeAwsIntegrationAccount from 'api/integration/aws/removeAwsIntegrationAccount';
|
||||
import removeAwsIntegrationAccount from 'api/Integrations/removeAwsIntegrationAccount';
|
||||
import { SOMETHING_WENT_WRONG } from 'constants/api';
|
||||
import { INTEGRATION_TELEMETRY_EVENTS } from 'container/Integrations/constants';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import { X } from 'lucide-react';
|
||||
import { INTEGRATION_TELEMETRY_EVENTS } from 'pages/Integrations/utils';
|
||||
|
||||
import './RemoveIntegrationAccount.scss';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Form, Input } from 'antd';
|
||||
import { ConnectionParams } from 'types/api/integrations/types';
|
||||
import { ConnectionParams } from 'types/api/integrations/aws';
|
||||
|
||||
function RenderConnectionFields({
|
||||
isConnectionParamsLoading,
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Dispatch, SetStateAction } from 'react';
|
||||
import { FormInstance } from 'antd';
|
||||
import { ConnectionParams } from 'types/api/integrations/types';
|
||||
import { ConnectionParams } from 'types/api/integrations/aws';
|
||||
|
||||
export enum ActiveViewEnum {
|
||||
SELECT_REGIONS = 'select-regions',
|
||||
@@ -1,8 +1,6 @@
|
||||
import { Table } from 'antd';
|
||||
import { ServiceData } from 'container/Integrations/CloudIntegration/AmazonWebServices/types';
|
||||
import { BarChart2, ScrollText } from 'lucide-react';
|
||||
|
||||
import './CloudServiceDataCollected.styles.scss';
|
||||
import { ServiceData } from './types';
|
||||
|
||||
function CloudServiceDataCollected({
|
||||
logsData,
|
||||
@@ -63,32 +61,26 @@ function CloudServiceDataCollected({
|
||||
return (
|
||||
<div className="cloud-service-data-collected">
|
||||
{logsData && logsData.length > 0 && (
|
||||
<div className="cloud-service-data-collected-table">
|
||||
<div className="cloud-service-data-collected-table-heading">
|
||||
<ScrollText size={14} />
|
||||
Logs
|
||||
</div>
|
||||
<div className="cloud-service-data-collected__table">
|
||||
<div className="cloud-service-data-collected__table-heading">Logs</div>
|
||||
<Table
|
||||
columns={logsColumns}
|
||||
dataSource={logsData}
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
{...tableProps}
|
||||
className="cloud-service-data-collected-table-logs"
|
||||
className="cloud-service-data-collected__table-logs"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{metricsData && metricsData.length > 0 && (
|
||||
<div className="cloud-service-data-collected-table">
|
||||
<div className="cloud-service-data-collected-table-heading">
|
||||
<BarChart2 size={14} />
|
||||
Metrics
|
||||
</div>
|
||||
<div className="cloud-service-data-collected__table">
|
||||
<div className="cloud-service-data-collected__table-heading">Metrics</div>
|
||||
<Table
|
||||
columns={metricsColumns}
|
||||
dataSource={metricsData}
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
{...tableProps}
|
||||
className="cloud-service-data-collected-table-metrics"
|
||||
className="cloud-service-data-collected__table-metrics"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
@@ -3,12 +3,14 @@ import { useQueryClient } from 'react-query';
|
||||
import { Form, Switch } from 'antd';
|
||||
import SignozModal from 'components/SignozModal/SignozModal';
|
||||
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||
import { AWSServiceConfig } from 'container/Integrations/CloudIntegration/AmazonWebServices/types';
|
||||
import { SupportedSignals } from 'container/Integrations/types';
|
||||
import {
|
||||
ServiceConfig,
|
||||
SupportedSignals,
|
||||
} from 'container/CloudIntegrationPage/ServicesSection/types';
|
||||
import { useUpdateServiceConfig } from 'hooks/integration/aws/useUpdateServiceConfig';
|
||||
import { isEqual } from 'lodash-es';
|
||||
|
||||
import logEvent from '../../../../api/common/logEvent';
|
||||
import logEvent from '../../../api/common/logEvent';
|
||||
import S3BucketsSelector from './S3BucketsSelector';
|
||||
|
||||
import './ConfigureServiceModal.styles.scss';
|
||||
@@ -20,7 +22,7 @@ export interface IConfigureServiceModalProps {
|
||||
serviceId: string;
|
||||
cloudAccountId: string;
|
||||
supportedSignals: SupportedSignals;
|
||||
initialConfig?: AWSServiceConfig;
|
||||
initialConfig?: ServiceConfig;
|
||||
}
|
||||
|
||||
function ConfigureServiceModal({
|
||||
@@ -1,16 +1,15 @@
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { Button, Tabs, TabsProps } from 'antd';
|
||||
import CloudServiceDataCollected from 'components/CloudIntegrations/CloudServiceDataCollected/CloudServiceDataCollected';
|
||||
import { MarkdownRenderer } from 'components/MarkdownRenderer/MarkdownRenderer';
|
||||
import Spinner from 'components/Spinner';
|
||||
import CloudServiceDashboards from 'container/Integrations/CloudIntegration/AmazonWebServices/CloudServiceDashboards';
|
||||
import { AWSServiceConfig } from 'container/Integrations/CloudIntegration/AmazonWebServices/types';
|
||||
import { IServiceStatus } from 'container/Integrations/types';
|
||||
import CloudServiceDashboards from 'container/CloudIntegrationPage/ServicesSection/CloudServiceDashboards';
|
||||
import CloudServiceDataCollected from 'container/CloudIntegrationPage/ServicesSection/CloudServiceDataCollected';
|
||||
import { IServiceStatus } from 'container/CloudIntegrationPage/ServicesSection/types';
|
||||
import dayjs from 'dayjs';
|
||||
import { useServiceDetails } from 'hooks/integration/aws/useServiceDetails';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
|
||||
import logEvent from '../../../../api/common/logEvent';
|
||||
import logEvent from '../../../api/common/logEvent';
|
||||
import ConfigureServiceModal from './ConfigureServiceModal';
|
||||
|
||||
const getStatus = (
|
||||
@@ -111,10 +110,9 @@ function ServiceDetails(): JSX.Element | null {
|
||||
[config],
|
||||
);
|
||||
|
||||
const awsConfig = config as AWSServiceConfig | undefined;
|
||||
const isAnySignalConfigured = useMemo(
|
||||
() => !!awsConfig?.logs?.enabled || !!awsConfig?.metrics?.enabled,
|
||||
[awsConfig],
|
||||
() => !!config?.logs?.enabled || !!config?.metrics?.enabled,
|
||||
[config],
|
||||
);
|
||||
|
||||
// log telemetry event on visiting details of a service.
|
||||
@@ -181,7 +179,7 @@ function ServiceDetails(): JSX.Element | null {
|
||||
serviceName={serviceDetailsData.title}
|
||||
serviceId={serviceId || ''}
|
||||
cloudAccountId={cloudAccountId || ''}
|
||||
initialConfig={awsConfig}
|
||||
initialConfig={serviceDetailsData.config}
|
||||
supportedSignals={serviceDetailsData.supported_signals || {}}
|
||||
/>
|
||||
)}
|
||||
@@ -20,19 +20,19 @@
|
||||
}
|
||||
.services-section {
|
||||
display: flex;
|
||||
|
||||
gap: 10px;
|
||||
&__sidebar {
|
||||
width: 240px;
|
||||
border-right: 1px solid var(--bg-slate-400);
|
||||
width: 16%;
|
||||
padding: 0 16px;
|
||||
}
|
||||
|
||||
&__content {
|
||||
flex: 1;
|
||||
width: 84%;
|
||||
padding: 16px;
|
||||
}
|
||||
}
|
||||
.services-filter {
|
||||
padding: 12px;
|
||||
|
||||
padding: 16px 0;
|
||||
.ant-select-selector {
|
||||
background-color: var(--bg-ink-300) !important;
|
||||
border: 1px solid var(--bg-slate-400) !important;
|
||||
@@ -63,19 +63,17 @@
|
||||
background-color: var(--bg-ink-100);
|
||||
}
|
||||
&__icon-wrapper {
|
||||
height: 32px;
|
||||
width: 32px;
|
||||
height: 40px;
|
||||
width: 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: var(--bg-ink-300);
|
||||
border: 1px solid var(--bg-slate-400);
|
||||
border-radius: 4px;
|
||||
|
||||
.service-item__icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
object-fit: contain;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
}
|
||||
&__title {
|
||||
@@ -92,13 +90,11 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
|
||||
&__title-bar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
height: 48px;
|
||||
padding: 8px 12px;
|
||||
border-bottom: 1px solid var(--bg-slate-400);
|
||||
|
||||
.service-details__details-title {
|
||||
@@ -109,7 +105,6 @@
|
||||
letter-spacing: -0.07px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.service-details__right-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -162,28 +157,21 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__overview {
|
||||
color: var(--bg-vanilla-400);
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
line-height: 20px; /* 142.857% */
|
||||
letter-spacing: -0.07px;
|
||||
width: 100%;
|
||||
|
||||
padding: 8px 12px;
|
||||
width: 800px;
|
||||
}
|
||||
|
||||
&__tabs {
|
||||
padding: 0px 12px 12px 8px;
|
||||
|
||||
.ant-tabs {
|
||||
&-ink-bar {
|
||||
background-color: transparent;
|
||||
}
|
||||
&-nav {
|
||||
padding: 0;
|
||||
|
||||
padding: 8px 0 18px;
|
||||
&-wrap {
|
||||
padding: 0;
|
||||
}
|
||||
@@ -1,14 +1,13 @@
|
||||
import { useMemo, useState } from 'react';
|
||||
import { useQuery } from 'react-query';
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import type { SelectProps } from 'antd';
|
||||
import { Select } from 'antd';
|
||||
import type { SelectProps, TabsProps } from 'antd';
|
||||
import { Select, Tabs } from 'antd';
|
||||
import { getAwsServices } from 'api/integration/aws';
|
||||
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
import { ChevronDown } from 'lucide-react';
|
||||
|
||||
import HeroSection from './HeroSection/HeroSection';
|
||||
import ServiceDetails from './ServiceDetails';
|
||||
import ServicesList from './ServicesList';
|
||||
|
||||
@@ -107,10 +106,17 @@ function ServicesSection(): JSX.Element {
|
||||
}
|
||||
|
||||
function ServicesTabs(): JSX.Element {
|
||||
const tabItems: TabsProps['items'] = [
|
||||
{
|
||||
key: 'services',
|
||||
label: 'Services For Integration',
|
||||
children: <ServicesSection />,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="services-tabs">
|
||||
<HeroSection />
|
||||
<ServicesSection />
|
||||
<Tabs defaultActiveKey="services" items={tabItems} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,40 +1,90 @@
|
||||
import { ServiceData } from 'container/Integrations/types';
|
||||
|
||||
interface Service {
|
||||
id: string;
|
||||
title: string;
|
||||
icon: string;
|
||||
config: AWSServiceConfig;
|
||||
config: ServiceConfig;
|
||||
}
|
||||
|
||||
interface S3BucketsByRegion {
|
||||
[region: string]: string[];
|
||||
interface Dashboard {
|
||||
id: string;
|
||||
url: string;
|
||||
title: string;
|
||||
description: string;
|
||||
image: string;
|
||||
}
|
||||
|
||||
interface LogField {
|
||||
name: string;
|
||||
path: string;
|
||||
type: string;
|
||||
}
|
||||
|
||||
interface Metric {
|
||||
name: string;
|
||||
type: string;
|
||||
unit: string;
|
||||
}
|
||||
|
||||
interface ConfigStatus {
|
||||
enabled: boolean;
|
||||
}
|
||||
|
||||
interface DataStatus {
|
||||
last_received_ts_ms: number;
|
||||
last_received_from: string;
|
||||
}
|
||||
|
||||
interface S3BucketsByRegion {
|
||||
[region: string]: string[];
|
||||
}
|
||||
|
||||
interface LogsConfig extends ConfigStatus {
|
||||
s3_buckets?: S3BucketsByRegion;
|
||||
}
|
||||
|
||||
interface AWSServiceConfig {
|
||||
interface ServiceConfig {
|
||||
logs: LogsConfig;
|
||||
metrics: ConfigStatus;
|
||||
s3_sync?: LogsConfig;
|
||||
}
|
||||
|
||||
interface IServiceStatus {
|
||||
logs: DataStatus | null;
|
||||
metrics: DataStatus | null;
|
||||
}
|
||||
|
||||
interface SupportedSignals {
|
||||
metrics: boolean;
|
||||
logs: boolean;
|
||||
}
|
||||
|
||||
interface ServiceData {
|
||||
id: string;
|
||||
title: string;
|
||||
icon: string;
|
||||
overview: string;
|
||||
supported_signals: SupportedSignals;
|
||||
assets: {
|
||||
dashboards: Dashboard[];
|
||||
};
|
||||
data_collected: {
|
||||
logs?: LogField[];
|
||||
metrics: Metric[];
|
||||
};
|
||||
config?: ServiceConfig;
|
||||
status?: IServiceStatus;
|
||||
}
|
||||
|
||||
interface ServiceDetailsResponse {
|
||||
status: 'success';
|
||||
data: ServiceData;
|
||||
}
|
||||
|
||||
export interface AWSCloudAccountConfig {
|
||||
interface CloudAccountConfig {
|
||||
regions: string[];
|
||||
}
|
||||
|
||||
export interface IntegrationStatus {
|
||||
interface IntegrationStatus {
|
||||
last_heartbeat_ts_ms: number;
|
||||
}
|
||||
|
||||
@@ -45,7 +95,7 @@ interface AccountStatus {
|
||||
interface CloudAccount {
|
||||
id: string;
|
||||
cloud_account_id: string;
|
||||
config: AWSCloudAccountConfig;
|
||||
config: CloudAccountConfig;
|
||||
status: AccountStatus;
|
||||
}
|
||||
|
||||
@@ -83,13 +133,15 @@ interface UpdateServiceConfigResponse {
|
||||
}
|
||||
|
||||
export type {
|
||||
AWSServiceConfig,
|
||||
CloudAccount,
|
||||
CloudAccountsData,
|
||||
IServiceStatus,
|
||||
S3BucketsByRegion,
|
||||
Service,
|
||||
ServiceConfig,
|
||||
ServiceData,
|
||||
ServiceDetailsResponse,
|
||||
SupportedSignals,
|
||||
UpdateServiceConfigPayload,
|
||||
UpdateServiceConfigResponse,
|
||||
};
|
||||
@@ -1,12 +1,14 @@
|
||||
import { I18nextProvider } from 'react-i18next';
|
||||
import { act, fireEvent, render, screen } from '@testing-library/react';
|
||||
import { RequestIntegrationBtn } from 'container/Integrations/RequestIntegrationBtn';
|
||||
import { IntegrationType } from 'container/Integrations/types';
|
||||
import { server } from 'mocks-server/server';
|
||||
import { rest } from 'msw';
|
||||
import {
|
||||
IntegrationType,
|
||||
RequestIntegrationBtn,
|
||||
} from 'pages/Integrations/RequestIntegrationBtn';
|
||||
import i18n from 'ReactI18';
|
||||
|
||||
describe.skip('Request AWS integration', () => {
|
||||
describe('Request AWS integration', () => {
|
||||
it('should render the request integration button', async () => {
|
||||
let capturedPayload: any;
|
||||
server.use(
|
||||
@@ -9,7 +9,6 @@ import {
|
||||
import useVariablesFromUrl from 'hooks/dashboard/useVariablesFromUrl';
|
||||
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
||||
import { initializeDefaultVariables } from 'providers/Dashboard/initializeDefaultVariables';
|
||||
import { updateDashboardVariablesStore } from 'providers/Dashboard/store/dashboardVariables/dashboardVariablesStore';
|
||||
import {
|
||||
enqueueDescendantsOfVariable,
|
||||
enqueueFetchOfAllVariables,
|
||||
@@ -32,9 +31,6 @@ function DashboardVariableSelection(): JSX.Element | null {
|
||||
const { updateUrlVariable, getUrlVariables } = useVariablesFromUrl();
|
||||
|
||||
const { dashboardVariables } = useDashboardVariables();
|
||||
const dashboardId = useDashboardVariablesSelector(
|
||||
(state) => state.dashboardId,
|
||||
);
|
||||
const sortedVariablesArray = useDashboardVariablesSelector(
|
||||
(state) => state.sortedVariablesArray,
|
||||
);
|
||||
@@ -100,28 +96,6 @@ function DashboardVariableSelection(): JSX.Element | null {
|
||||
updateUrlVariable(name || id, value);
|
||||
}
|
||||
|
||||
// Synchronously update the external store with the new variable value so that
|
||||
// child variables see the updated parent value when they refetch, rather than
|
||||
// waiting for setSelectedDashboard → useEffect → updateDashboardVariablesStore.
|
||||
const updatedVariables = { ...dashboardVariables };
|
||||
if (updatedVariables[id]) {
|
||||
updatedVariables[id] = {
|
||||
...updatedVariables[id],
|
||||
selectedValue: value,
|
||||
allSelected,
|
||||
haveCustomValuesSelected,
|
||||
};
|
||||
}
|
||||
if (updatedVariables[name]) {
|
||||
updatedVariables[name] = {
|
||||
...updatedVariables[name],
|
||||
selectedValue: value,
|
||||
allSelected,
|
||||
haveCustomValuesSelected,
|
||||
};
|
||||
}
|
||||
updateDashboardVariablesStore({ dashboardId, variables: updatedVariables });
|
||||
|
||||
setSelectedDashboard((prev) => {
|
||||
if (prev) {
|
||||
const oldVariables = { ...prev?.data.variables };
|
||||
@@ -156,12 +130,10 @@ function DashboardVariableSelection(): JSX.Element | null {
|
||||
return prev;
|
||||
});
|
||||
|
||||
// Cascade: enqueue query-type descendants for refetching.
|
||||
// Safe to call synchronously now that the store already has the updated value.
|
||||
// Cascade: enqueue query-type descendants for refetching
|
||||
enqueueDescendantsOfVariable(name);
|
||||
},
|
||||
[
|
||||
dashboardId,
|
||||
dashboardVariables,
|
||||
updateLocalStorageDashboardVariables,
|
||||
updateUrlVariable,
|
||||
|
||||
@@ -5,7 +5,7 @@ import dashboardVariablesQuery from 'api/dashboard/variables/dashboardVariablesQ
|
||||
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||
import { useVariableFetchState } from 'hooks/dashboard/useVariableFetchState';
|
||||
import sortValues from 'lib/dashboardVariables/sortVariableValues';
|
||||
import { isArray, isEmpty } from 'lodash-es';
|
||||
import { isArray, isEmpty, isString } from 'lodash-es';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { VariableResponseProps } from 'types/api/dashboard/variables/query';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
@@ -54,7 +54,7 @@ function QueryVariableInput({
|
||||
onChange,
|
||||
onDropdownVisibleChange,
|
||||
handleClear,
|
||||
getDefaultValue,
|
||||
applyDefaultIfNeeded,
|
||||
} = useDashboardVariableSelectHelper({
|
||||
variableData,
|
||||
optionsData,
|
||||
@@ -68,93 +68,81 @@ function QueryVariableInput({
|
||||
try {
|
||||
setErrorMessage(null);
|
||||
|
||||
// This is just a check given the previously undefined typed name prop. Not significant
|
||||
// This will be changed when we change the schema
|
||||
// TODO: @AshwinBhatkal Perses
|
||||
if (!variableData.name) {
|
||||
return;
|
||||
}
|
||||
|
||||
// if the response is not an array, premature return
|
||||
if (
|
||||
!variablesRes?.variableValues ||
|
||||
!Array.isArray(variablesRes?.variableValues)
|
||||
variablesRes?.variableValues &&
|
||||
Array.isArray(variablesRes?.variableValues)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const sortedNewOptions = sortValues(
|
||||
variablesRes.variableValues,
|
||||
variableData.sort,
|
||||
);
|
||||
const sortedOldOptions = sortValues(optionsData, variableData.sort);
|
||||
|
||||
// if options are the same as before, no need to update state or check for selected value validity
|
||||
// ! selectedValue needs to be set in the first pass though, as options are initially empty array and we need to apply default if needed
|
||||
// Expecatation is that when oldOptions are not empty, then there is always some selectedValue
|
||||
if (areArraysEqual(sortedNewOptions, sortedOldOptions)) {
|
||||
return;
|
||||
}
|
||||
|
||||
setOptionsData(sortedNewOptions);
|
||||
|
||||
let isSelectedValueMissingInNewOptions = false;
|
||||
|
||||
// Check if currently selected value(s) are present in the new options list
|
||||
if (isArray(variableData.selectedValue)) {
|
||||
isSelectedValueMissingInNewOptions = variableData.selectedValue.some(
|
||||
(val) => !sortedNewOptions.includes(val),
|
||||
const newOptionsData = sortValues(
|
||||
variablesRes?.variableValues,
|
||||
variableData.sort,
|
||||
);
|
||||
} else if (
|
||||
variableData.selectedValue &&
|
||||
!sortedNewOptions.includes(variableData.selectedValue)
|
||||
) {
|
||||
isSelectedValueMissingInNewOptions = true;
|
||||
}
|
||||
|
||||
// If multi-select with ALL option enabled, and ALL is currently selected, we want to maintain that state and select all new options
|
||||
// This block does not depend on selected value because of ALL and also because we would only come here if options are different from the previous
|
||||
if (
|
||||
variableData.multiSelect &&
|
||||
variableData.showALLOption &&
|
||||
variableData.allSelected &&
|
||||
isSelectedValueMissingInNewOptions
|
||||
) {
|
||||
onValueUpdate(variableData.name, variableData.id, sortedNewOptions, true);
|
||||
const oldOptionsData = sortValues(optionsData, variableData.sort) as never;
|
||||
|
||||
// Update tempSelection to maintain ALL state when dropdown is open
|
||||
if (tempSelection !== undefined) {
|
||||
setTempSelection(sortedNewOptions.map((option) => option.toString()));
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (!areArraysEqual(newOptionsData, oldOptionsData)) {
|
||||
let valueNotInList = false;
|
||||
|
||||
const value = variableData.selectedValue;
|
||||
let allSelected = false;
|
||||
if (isArray(variableData.selectedValue)) {
|
||||
variableData.selectedValue.forEach((val) => {
|
||||
if (!newOptionsData.includes(val)) {
|
||||
valueNotInList = true;
|
||||
}
|
||||
});
|
||||
} else if (
|
||||
isString(variableData.selectedValue) &&
|
||||
!newOptionsData.includes(variableData.selectedValue)
|
||||
) {
|
||||
valueNotInList = true;
|
||||
}
|
||||
|
||||
if (variableData.multiSelect) {
|
||||
const { selectedValue } = variableData;
|
||||
allSelected =
|
||||
sortedNewOptions.length > 0 &&
|
||||
Array.isArray(selectedValue) &&
|
||||
sortedNewOptions.every((option) => selectedValue.includes(option));
|
||||
}
|
||||
if (variableData.name && (valueNotInList || variableData.allSelected)) {
|
||||
if (
|
||||
variableData.allSelected &&
|
||||
variableData.multiSelect &&
|
||||
variableData.showALLOption
|
||||
) {
|
||||
if (
|
||||
variableData.name &&
|
||||
variableData.id &&
|
||||
!isEmpty(variableData.selectedValue)
|
||||
) {
|
||||
onValueUpdate(
|
||||
variableData.name,
|
||||
variableData.id,
|
||||
newOptionsData,
|
||||
true,
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
variableData.name &&
|
||||
variableData.id &&
|
||||
!isEmpty(variableData.selectedValue)
|
||||
) {
|
||||
onValueUpdate(variableData.name, variableData.id, value, allSelected);
|
||||
} else {
|
||||
const defaultValue = getDefaultValue(sortedNewOptions);
|
||||
if (defaultValue !== undefined) {
|
||||
onValueUpdate(
|
||||
variableData.name,
|
||||
variableData.id,
|
||||
defaultValue,
|
||||
allSelected,
|
||||
);
|
||||
// Update tempSelection to maintain ALL state when dropdown is open
|
||||
if (tempSelection !== undefined) {
|
||||
setTempSelection(newOptionsData.map((option) => option.toString()));
|
||||
}
|
||||
} else {
|
||||
const value = variableData.selectedValue;
|
||||
let allSelected = false;
|
||||
|
||||
if (variableData.multiSelect) {
|
||||
const { selectedValue } = variableData;
|
||||
allSelected =
|
||||
newOptionsData.length > 0 &&
|
||||
Array.isArray(selectedValue) &&
|
||||
newOptionsData.every((option) => selectedValue.includes(option));
|
||||
}
|
||||
|
||||
if (
|
||||
variableData.name &&
|
||||
variableData.id &&
|
||||
!isEmpty(variableData.selectedValue)
|
||||
) {
|
||||
onValueUpdate(variableData.name, variableData.id, value, allSelected);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setOptionsData(newOptionsData);
|
||||
// Apply default if no value is selected (e.g., new variable, first load)
|
||||
applyDefaultIfNeeded(newOptionsData);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
@@ -167,7 +155,7 @@ function QueryVariableInput({
|
||||
onValueUpdate,
|
||||
tempSelection,
|
||||
setTempSelection,
|
||||
getDefaultValue,
|
||||
applyDefaultIfNeeded,
|
||||
],
|
||||
);
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
/* eslint-disable sonarjs/no-duplicate-string */
|
||||
import { act, render } from '@testing-library/react';
|
||||
import * as dashboardVariablesStoreModule from 'providers/Dashboard/store/dashboardVariables/dashboardVariablesStore';
|
||||
import {
|
||||
dashboardVariablesStore,
|
||||
setDashboardVariablesStore,
|
||||
@@ -11,7 +10,6 @@ import {
|
||||
IDashboardVariablesStoreState,
|
||||
} from 'providers/Dashboard/store/dashboardVariables/dashboardVariablesStoreTypes';
|
||||
import {
|
||||
enqueueDescendantsOfVariable,
|
||||
enqueueFetchOfAllVariables,
|
||||
initializeVariableFetchStore,
|
||||
} from 'providers/Dashboard/store/variableFetchStore';
|
||||
@@ -19,17 +17,6 @@ import { IDashboardVariable } from 'types/api/dashboard/getAll';
|
||||
|
||||
import DashboardVariableSelection from '../DashboardVariableSelection';
|
||||
|
||||
// Mutable container to capture the onValueUpdate callback from VariableItem
|
||||
const mockVariableItemCallbacks: {
|
||||
onValueUpdate?: (
|
||||
name: string,
|
||||
id: string,
|
||||
value: IDashboardVariable['selectedValue'],
|
||||
allSelected: boolean,
|
||||
haveCustomValuesSelected?: boolean,
|
||||
) => void;
|
||||
} = {};
|
||||
|
||||
// Mock providers/Dashboard/Dashboard
|
||||
const mockSetSelectedDashboard = jest.fn();
|
||||
const mockUpdateLocalStorageDashboardVariables = jest.fn();
|
||||
@@ -69,14 +56,10 @@ jest.mock('react-redux', () => ({
|
||||
useSelector: jest.fn().mockReturnValue({ minTime: 1000, maxTime: 2000 }),
|
||||
}));
|
||||
|
||||
// VariableItem mock captures the onValueUpdate prop for use in onValueUpdate tests
|
||||
// Mock VariableItem to avoid rendering complexity
|
||||
jest.mock('../VariableItem', () => ({
|
||||
__esModule: true,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
default: (props: any): JSX.Element => {
|
||||
mockVariableItemCallbacks.onValueUpdate = props.onValueUpdate;
|
||||
return <div data-testid="variable-item" />;
|
||||
},
|
||||
default: (): JSX.Element => <div data-testid="variable-item" />,
|
||||
}));
|
||||
|
||||
function createVariable(
|
||||
@@ -217,162 +200,4 @@ describe('DashboardVariableSelection', () => {
|
||||
expect(initializeVariableFetchStore).not.toHaveBeenCalled();
|
||||
expect(enqueueFetchOfAllVariables).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
describe('onValueUpdate', () => {
|
||||
let updateStoreSpy: jest.SpyInstance;
|
||||
|
||||
beforeEach(() => {
|
||||
resetStore();
|
||||
jest.clearAllMocks();
|
||||
// Real implementation pass-through — we just want to observe calls
|
||||
updateStoreSpy = jest.spyOn(
|
||||
dashboardVariablesStoreModule,
|
||||
'updateDashboardVariablesStore',
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
updateStoreSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('updates dashboardVariablesStore synchronously before enqueueDescendantsOfVariable', () => {
|
||||
setDashboardVariablesStore({
|
||||
dashboardId: 'dash-1',
|
||||
variables: {
|
||||
env: createVariable({ name: 'env', id: 'env-id', order: 0 }),
|
||||
},
|
||||
});
|
||||
|
||||
render(<DashboardVariableSelection />);
|
||||
|
||||
const callOrder: string[] = [];
|
||||
updateStoreSpy.mockImplementation(() => {
|
||||
callOrder.push('updateDashboardVariablesStore');
|
||||
});
|
||||
(enqueueDescendantsOfVariable as jest.Mock).mockImplementation(() => {
|
||||
callOrder.push('enqueueDescendantsOfVariable');
|
||||
});
|
||||
|
||||
act(() => {
|
||||
mockVariableItemCallbacks.onValueUpdate?.(
|
||||
'env',
|
||||
'env-id',
|
||||
'production',
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
expect(callOrder).toEqual([
|
||||
'updateDashboardVariablesStore',
|
||||
'enqueueDescendantsOfVariable',
|
||||
]);
|
||||
});
|
||||
|
||||
it('passes updated variable value to dashboardVariablesStore', () => {
|
||||
setDashboardVariablesStore({
|
||||
dashboardId: 'dash-1',
|
||||
variables: {
|
||||
env: createVariable({
|
||||
name: 'env',
|
||||
id: 'env-id',
|
||||
order: 0,
|
||||
selectedValue: 'staging',
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
render(<DashboardVariableSelection />);
|
||||
|
||||
// Clear spy calls that happened during setup/render
|
||||
updateStoreSpy.mockClear();
|
||||
|
||||
act(() => {
|
||||
mockVariableItemCallbacks.onValueUpdate?.(
|
||||
'env',
|
||||
'env-id',
|
||||
'production',
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
expect(updateStoreSpy).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
dashboardId: 'dash-1',
|
||||
variables: expect.objectContaining({
|
||||
env: expect.objectContaining({
|
||||
selectedValue: 'production',
|
||||
allSelected: false,
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('calls enqueueDescendantsOfVariable synchronously without a timer', () => {
|
||||
jest.useFakeTimers();
|
||||
|
||||
setDashboardVariablesStore({
|
||||
dashboardId: 'dash-1',
|
||||
variables: {
|
||||
env: createVariable({ name: 'env', id: 'env-id', order: 0 }),
|
||||
},
|
||||
});
|
||||
|
||||
render(<DashboardVariableSelection />);
|
||||
|
||||
act(() => {
|
||||
mockVariableItemCallbacks.onValueUpdate?.(
|
||||
'env',
|
||||
'env-id',
|
||||
'production',
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
// Must be called immediately — no timer advancement needed
|
||||
expect(enqueueDescendantsOfVariable).toHaveBeenCalledWith('env');
|
||||
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
it('propagates allSelected and haveCustomValuesSelected to the store', () => {
|
||||
setDashboardVariablesStore({
|
||||
dashboardId: 'dash-1',
|
||||
variables: {
|
||||
env: createVariable({
|
||||
name: 'env',
|
||||
id: 'env-id',
|
||||
order: 0,
|
||||
multiSelect: true,
|
||||
showALLOption: true,
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
render(<DashboardVariableSelection />);
|
||||
updateStoreSpy.mockClear();
|
||||
|
||||
act(() => {
|
||||
mockVariableItemCallbacks.onValueUpdate?.(
|
||||
'env',
|
||||
'env-id',
|
||||
['production', 'staging'],
|
||||
true,
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
expect(updateStoreSpy).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
variables: expect.objectContaining({
|
||||
env: expect.objectContaining({
|
||||
selectedValue: ['production', 'staging'],
|
||||
allSelected: true,
|
||||
haveCustomValuesSelected: false,
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,275 +0,0 @@
|
||||
/* eslint-disable sonarjs/no-duplicate-string */
|
||||
import { QueryClient, QueryClientProvider } from 'react-query';
|
||||
import { act, render, waitFor } from '@testing-library/react';
|
||||
import dashboardVariablesQuery from 'api/dashboard/variables/dashboardVariablesQuery';
|
||||
import { variableFetchStore } from 'providers/Dashboard/store/variableFetchStore';
|
||||
import { IDashboardVariable } from 'types/api/dashboard/getAll';
|
||||
|
||||
import QueryVariableInput from '../QueryVariableInput';
|
||||
|
||||
jest.mock('api/dashboard/variables/dashboardVariablesQuery');
|
||||
|
||||
jest.mock('react-redux', () => ({
|
||||
...jest.requireActual('react-redux'),
|
||||
useSelector: jest.fn().mockReturnValue({ minTime: 1000, maxTime: 2000 }),
|
||||
}));
|
||||
|
||||
function createTestQueryClient(): QueryClient {
|
||||
return new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: { retry: false, refetchOnWindowFocus: false },
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function Wrapper({
|
||||
children,
|
||||
queryClient,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
queryClient: QueryClient;
|
||||
}): JSX.Element {
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
||||
);
|
||||
}
|
||||
|
||||
function createVariable(
|
||||
overrides: Partial<IDashboardVariable> = {},
|
||||
): IDashboardVariable {
|
||||
return {
|
||||
id: 'env-id',
|
||||
name: 'env',
|
||||
description: '',
|
||||
type: 'QUERY',
|
||||
sort: 'DISABLED',
|
||||
showALLOption: false,
|
||||
multiSelect: false,
|
||||
order: 0,
|
||||
queryValue: 'SELECT env FROM table',
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
/** Put the named variable into 'loading' state so useQuery fires on mount */
|
||||
function setVariableLoading(name: string): void {
|
||||
variableFetchStore.update((draft) => {
|
||||
draft.states[name] = 'loading';
|
||||
draft.cycleIds[name] = (draft.cycleIds[name] || 0) + 1;
|
||||
});
|
||||
}
|
||||
|
||||
function resetFetchStore(): void {
|
||||
variableFetchStore.set(() => ({
|
||||
states: {},
|
||||
lastUpdated: {},
|
||||
cycleIds: {},
|
||||
}));
|
||||
}
|
||||
|
||||
describe('QueryVariableInput - getOptions logic', () => {
|
||||
const mockOnValueUpdate = jest.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
resetFetchStore();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
resetFetchStore();
|
||||
});
|
||||
|
||||
it('applies default value (first option) when selectedValue is empty on first load', async () => {
|
||||
(dashboardVariablesQuery as jest.Mock).mockResolvedValue({
|
||||
statusCode: 200,
|
||||
payload: { variableValues: ['production', 'staging', 'dev'] },
|
||||
});
|
||||
|
||||
const variable = createVariable({ selectedValue: undefined });
|
||||
setVariableLoading('env');
|
||||
|
||||
const queryClient = createTestQueryClient();
|
||||
render(
|
||||
<Wrapper queryClient={queryClient}>
|
||||
<QueryVariableInput
|
||||
variableData={variable}
|
||||
existingVariables={{ 'env-id': variable }}
|
||||
onValueUpdate={mockOnValueUpdate}
|
||||
/>
|
||||
</Wrapper>,
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockOnValueUpdate).toHaveBeenCalledWith(
|
||||
'env',
|
||||
'env-id',
|
||||
'production', // first option by default
|
||||
false,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('keeps existing selectedValue when it is present in new options', async () => {
|
||||
(dashboardVariablesQuery as jest.Mock).mockResolvedValue({
|
||||
statusCode: 200,
|
||||
payload: { variableValues: ['production', 'staging'] },
|
||||
});
|
||||
|
||||
const variable = createVariable({ selectedValue: 'staging' });
|
||||
setVariableLoading('env');
|
||||
|
||||
const queryClient = createTestQueryClient();
|
||||
render(
|
||||
<Wrapper queryClient={queryClient}>
|
||||
<QueryVariableInput
|
||||
variableData={variable}
|
||||
existingVariables={{ 'env-id': variable }}
|
||||
onValueUpdate={mockOnValueUpdate}
|
||||
/>
|
||||
</Wrapper>,
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockOnValueUpdate).toHaveBeenCalledWith(
|
||||
'env',
|
||||
'env-id',
|
||||
'staging',
|
||||
false,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('selects all new options when allSelected=true and value is missing from new options', async () => {
|
||||
(dashboardVariablesQuery as jest.Mock).mockResolvedValue({
|
||||
statusCode: 200,
|
||||
payload: { variableValues: ['production', 'staging'] },
|
||||
});
|
||||
|
||||
const variable = createVariable({
|
||||
selectedValue: ['old-env'],
|
||||
allSelected: true,
|
||||
multiSelect: true,
|
||||
showALLOption: true,
|
||||
});
|
||||
setVariableLoading('env');
|
||||
|
||||
const queryClient = createTestQueryClient();
|
||||
render(
|
||||
<Wrapper queryClient={queryClient}>
|
||||
<QueryVariableInput
|
||||
variableData={variable}
|
||||
existingVariables={{ 'env-id': variable }}
|
||||
onValueUpdate={mockOnValueUpdate}
|
||||
/>
|
||||
</Wrapper>,
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockOnValueUpdate).toHaveBeenCalledWith(
|
||||
'env',
|
||||
'env-id',
|
||||
['production', 'staging'],
|
||||
true,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('does not call onValueUpdate a second time when options have not changed', async () => {
|
||||
const mockQueryFn = jest.fn().mockResolvedValue({
|
||||
statusCode: 200,
|
||||
payload: { variableValues: ['production', 'staging'] },
|
||||
});
|
||||
(dashboardVariablesQuery as jest.Mock).mockImplementation(mockQueryFn);
|
||||
|
||||
const variable = createVariable({ selectedValue: 'production' });
|
||||
setVariableLoading('env');
|
||||
|
||||
const queryClient = createTestQueryClient();
|
||||
render(
|
||||
<Wrapper queryClient={queryClient}>
|
||||
<QueryVariableInput
|
||||
variableData={variable}
|
||||
existingVariables={{ 'env-id': variable }}
|
||||
onValueUpdate={mockOnValueUpdate}
|
||||
/>
|
||||
</Wrapper>,
|
||||
);
|
||||
|
||||
// Wait for first fetch and onValueUpdate call
|
||||
await waitFor(() => {
|
||||
expect(mockOnValueUpdate).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
mockOnValueUpdate.mockClear();
|
||||
|
||||
// Trigger a second fetch cycle with the same API response
|
||||
act(() => {
|
||||
variableFetchStore.update((draft) => {
|
||||
draft.states['env'] = 'revalidating';
|
||||
draft.cycleIds['env'] = (draft.cycleIds['env'] || 0) + 1;
|
||||
});
|
||||
});
|
||||
|
||||
// Wait for second query to fire
|
||||
await waitFor(() => {
|
||||
expect(mockQueryFn).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
// Options are unchanged, so onValueUpdate must not fire again
|
||||
expect(mockOnValueUpdate).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('does not call onValueUpdate when API returns a non-array response', async () => {
|
||||
(dashboardVariablesQuery as jest.Mock).mockResolvedValue({
|
||||
statusCode: 200,
|
||||
payload: { variableValues: null },
|
||||
});
|
||||
|
||||
const variable = createVariable({ selectedValue: 'production' });
|
||||
setVariableLoading('env');
|
||||
|
||||
const queryClient = createTestQueryClient();
|
||||
render(
|
||||
<Wrapper queryClient={queryClient}>
|
||||
<QueryVariableInput
|
||||
variableData={variable}
|
||||
existingVariables={{ 'env-id': variable }}
|
||||
onValueUpdate={mockOnValueUpdate}
|
||||
/>
|
||||
</Wrapper>,
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(dashboardVariablesQuery).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
expect(mockOnValueUpdate).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('does not fire the query when variableData.name is empty', () => {
|
||||
(dashboardVariablesQuery as jest.Mock).mockResolvedValue({
|
||||
statusCode: 200,
|
||||
payload: { variableValues: ['production'] },
|
||||
});
|
||||
|
||||
// Variable with no name — useVariableFetchState will be called with ''
|
||||
// and the query key will have an empty name, leaving it disabled
|
||||
const variable = createVariable({ name: '' });
|
||||
// Note: we do NOT put it in 'loading' state since name is empty
|
||||
// (no variableFetchStore entry for '' means isVariableFetching=false)
|
||||
|
||||
const queryClient = createTestQueryClient();
|
||||
render(
|
||||
<Wrapper queryClient={queryClient}>
|
||||
<QueryVariableInput
|
||||
variableData={variable}
|
||||
existingVariables={{ 'env-id': variable }}
|
||||
onValueUpdate={mockOnValueUpdate}
|
||||
/>
|
||||
</Wrapper>,
|
||||
);
|
||||
|
||||
expect(dashboardVariablesQuery).not.toHaveBeenCalled();
|
||||
expect(mockOnValueUpdate).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -46,9 +46,6 @@ interface UseDashboardVariableSelectHelperReturn {
|
||||
applyDefaultIfNeeded: (
|
||||
overrideOptions?: (string | number | boolean)[],
|
||||
) => void;
|
||||
getDefaultValue: (
|
||||
overrideOptions?: (string | number | boolean)[],
|
||||
) => string | string[] | undefined;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||
@@ -251,6 +248,5 @@ export function useDashboardVariableSelectHelper({
|
||||
defaultValue,
|
||||
onChange,
|
||||
applyDefaultIfNeeded,
|
||||
getDefaultValue,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
import AccountActions from './components/AccountActions';
|
||||
|
||||
import './HeroSection.style.scss';
|
||||
|
||||
function HeroSection(): JSX.Element {
|
||||
return (
|
||||
<div className="hero-section">
|
||||
<div className="hero-section__icon">
|
||||
<img src="/Logos/aws-dark.svg" alt="AWS" />
|
||||
</div>
|
||||
<div className="hero-section__details">
|
||||
<div className="title">AWS</div>
|
||||
<div className="description">
|
||||
AWS is a cloud computing platform that provides a range of services for
|
||||
building and running applications.
|
||||
</div>
|
||||
<AccountActions />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default HeroSection;
|
||||
@@ -1,311 +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 {
|
||||
.azure-account-configure-agent-step-primary-region {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
|
||||
.azure-account-configure-agent-step-primary-region-title {
|
||||
color: var(--l1-foreground);
|
||||
font-family: Inter;
|
||||
font-size: 13px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 20px; /* 150% */
|
||||
letter-spacing: -0.06px;
|
||||
}
|
||||
|
||||
.azure-account-configure-agent-step-primary-region-select {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.azure-account-configure-agent-step-resource-groups {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
|
||||
.azure-account-configure-agent-step-resource-groups-title {
|
||||
color: var(--l1-foreground);
|
||||
font-family: Inter;
|
||||
font-size: 13px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 20px; /* 150% */
|
||||
letter-spacing: -0.06px;
|
||||
}
|
||||
|
||||
.azure-account-configure-agent-step-resource-groups-select {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.azure-account-actions-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-select {
|
||||
.ant-select-selector {
|
||||
background: var(--l1-background);
|
||||
border: 1px solid var(--l3-background);
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.ant-select-selection-item {
|
||||
color: var(--l1-foreground);
|
||||
}
|
||||
|
||||
.ant-select-arrow {
|
||||
color: var(--l1-foreground);
|
||||
}
|
||||
|
||||
.ant-select-dropdown {
|
||||
background: var(--l1-background);
|
||||
border: 1px solid var(--l3-background);
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.ant-select-item {
|
||||
color: var(--l1-foreground);
|
||||
}
|
||||
|
||||
.ant-select-item-option-active {
|
||||
background: var(--l1-background);
|
||||
}
|
||||
}
|
||||
|
||||
.azure-account-disconnect-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
padding: 16px 0px;
|
||||
}
|
||||
}
|
||||
|
||||
.azure-account-disconnect-modal {
|
||||
.ant-modal-content {
|
||||
width: 480px;
|
||||
min-height: 200px;
|
||||
flex-shrink: 0;
|
||||
background: var(--l1-background);
|
||||
color: var(--l1-foreground);
|
||||
border: 1px solid var(--l3-background);
|
||||
border-radius: 4px;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.ant-modal-header {
|
||||
background: var(--l1-background);
|
||||
color: var(--l1-foreground);
|
||||
|
||||
.ant-modal-title {
|
||||
color: var(--l1-foreground);
|
||||
}
|
||||
}
|
||||
|
||||
.ant-modal-body {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.ant-modal-close {
|
||||
color: var(--l1-foreground);
|
||||
}
|
||||
|
||||
.ant-modal-footer {
|
||||
margin-top: 32px;
|
||||
}
|
||||
}
|
||||
@@ -1,218 +0,0 @@
|
||||
import { useCallback, useState } from 'react';
|
||||
import { useMutation, useQueryClient } from 'react-query';
|
||||
import { Button } from '@signozhq/button';
|
||||
import { toast } from '@signozhq/sonner';
|
||||
import { Form, Select } from 'antd';
|
||||
import { Modal } from 'antd/lib';
|
||||
import { removeIntegrationAccount } from 'api/integration';
|
||||
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||
import {
|
||||
AZURE_REGIONS,
|
||||
INTEGRATION_TYPES,
|
||||
} from 'container/Integrations/constants';
|
||||
import {
|
||||
AzureCloudAccountConfig,
|
||||
CloudAccount,
|
||||
} from 'container/Integrations/types';
|
||||
import { CornerDownRight, Unlink } from 'lucide-react';
|
||||
import { ConnectionParams } from 'types/api/integrations/types';
|
||||
|
||||
interface AzureAccountFormProps {
|
||||
mode?: 'add' | 'edit';
|
||||
selectedAccount: CloudAccount | null;
|
||||
connectionParams: ConnectionParams;
|
||||
isConnectionParamsLoading: boolean;
|
||||
isLoading: boolean;
|
||||
onSubmit: (values: {
|
||||
primaryRegion: string;
|
||||
resourceGroups: string[];
|
||||
}) => void;
|
||||
submitButtonText?: string;
|
||||
showDisconnectAccountButton?: boolean;
|
||||
}
|
||||
|
||||
export const AzureAccountForm = ({
|
||||
mode = 'add',
|
||||
selectedAccount,
|
||||
connectionParams,
|
||||
isConnectionParamsLoading,
|
||||
isLoading,
|
||||
onSubmit,
|
||||
submitButtonText = 'Fetch Deployment Command',
|
||||
showDisconnectAccountButton = false,
|
||||
}: AzureAccountFormProps): JSX.Element => {
|
||||
const [azureAccountForm] = Form.useForm();
|
||||
const queryClient = useQueryClient();
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
|
||||
const handleSubmit = useCallback((): void => {
|
||||
azureAccountForm
|
||||
.validateFields()
|
||||
.then((values) => {
|
||||
onSubmit({
|
||||
primaryRegion: values.primaryRegion,
|
||||
resourceGroups: values.resourceGroups,
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Form submission failed:', error);
|
||||
});
|
||||
}, [azureAccountForm, onSubmit]);
|
||||
|
||||
const handleDisconnect = (): void => {
|
||||
setIsModalOpen(true);
|
||||
};
|
||||
|
||||
const {
|
||||
mutate: removeIntegration,
|
||||
isLoading: isRemoveIntegrationLoading,
|
||||
} = useMutation(removeIntegrationAccount, {
|
||||
onSuccess: () => {
|
||||
toast.success('Azure account disconnected successfully', {
|
||||
description: 'Azure account disconnected successfully',
|
||||
position: 'top-right',
|
||||
duration: 3000,
|
||||
});
|
||||
|
||||
queryClient.invalidateQueries([REACT_QUERY_KEY.CLOUD_INTEGRATION_ACCOUNTS]);
|
||||
setIsModalOpen(false);
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error('Failed to remove integration:', error);
|
||||
},
|
||||
});
|
||||
|
||||
const handleOk = (): void => {
|
||||
removeIntegration({
|
||||
cloudServiceId: INTEGRATION_TYPES.AZURE,
|
||||
accountId: selectedAccount?.id as string,
|
||||
});
|
||||
};
|
||||
|
||||
const handleCancel = (): void => {
|
||||
setIsModalOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<Form
|
||||
name="azure-account-form"
|
||||
className="azure-account-form"
|
||||
form={azureAccountForm}
|
||||
layout="vertical"
|
||||
autoComplete="off"
|
||||
initialValues={{
|
||||
primaryRegion:
|
||||
(selectedAccount?.config as AzureCloudAccountConfig)?.deployment_region ||
|
||||
undefined,
|
||||
resourceGroups:
|
||||
(selectedAccount?.config as AzureCloudAccountConfig)?.resource_groups ||
|
||||
[],
|
||||
}}
|
||||
>
|
||||
<div className="azure-account-configure-agent-step-primary-region">
|
||||
<div className="azure-account-configure-agent-step-primary-region-title">
|
||||
Select primary region
|
||||
</div>
|
||||
<div className="azure-account-configure-agent-step-primary-region-select">
|
||||
<Form.Item
|
||||
name="primaryRegion"
|
||||
rules={[{ required: true, message: 'Please select a primary region' }]}
|
||||
>
|
||||
<Select
|
||||
disabled={mode === 'edit'}
|
||||
placeholder="Select primary region"
|
||||
options={AZURE_REGIONS}
|
||||
showSearch
|
||||
filterOption={(input, option): boolean =>
|
||||
option?.label?.toLowerCase().includes(input.toLowerCase()) ||
|
||||
option?.value?.toLowerCase().includes(input.toLowerCase()) ||
|
||||
false
|
||||
}
|
||||
notFoundContent={null}
|
||||
/>
|
||||
</Form.Item>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="azure-account-configure-agent-step-resource-groups">
|
||||
<div className="azure-account-configure-agent-step-resource-groups-title">
|
||||
Enter resource groups you want to monitor
|
||||
</div>
|
||||
|
||||
<div className="azure-account-configure-agent-step-resource-groups-select">
|
||||
<Form.Item
|
||||
name="resourceGroups"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: 'Please enter resource groups you want to monitor',
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Select
|
||||
placeholder="Enter resource groups you want to monitor"
|
||||
options={[]}
|
||||
mode="tags"
|
||||
notFoundContent={null}
|
||||
filterOption={false}
|
||||
showSearch={false}
|
||||
/>
|
||||
</Form.Item>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="azure-account-actions-container">
|
||||
{showDisconnectAccountButton && (
|
||||
<div className="azure-account-disconnect-container">
|
||||
<Button
|
||||
variant="solid"
|
||||
color="destructive"
|
||||
prefixIcon={<Unlink size={14} />}
|
||||
size="sm"
|
||||
onClick={handleDisconnect}
|
||||
disabled={isRemoveIntegrationLoading}
|
||||
>
|
||||
Disconnect
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Button
|
||||
variant="solid"
|
||||
color="primary"
|
||||
onClick={handleSubmit}
|
||||
size="sm"
|
||||
prefixIcon={<CornerDownRight size={12} />}
|
||||
loading={
|
||||
isConnectionParamsLoading || isLoading || isRemoveIntegrationLoading
|
||||
}
|
||||
disabled={
|
||||
isConnectionParamsLoading ||
|
||||
!connectionParams ||
|
||||
isLoading ||
|
||||
isRemoveIntegrationLoading
|
||||
}
|
||||
>
|
||||
{submitButtonText}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Modal
|
||||
className="azure-account-disconnect-modal"
|
||||
open={isModalOpen}
|
||||
title="Remove integration"
|
||||
onOk={handleOk}
|
||||
onCancel={handleCancel}
|
||||
okText="Remove Integration"
|
||||
okButtonProps={{
|
||||
danger: true,
|
||||
}}
|
||||
>
|
||||
<div className="remove-integration-modal-content">
|
||||
Removing this account will remove all components created for sending
|
||||
telemetry to SigNoz in your Azure account within the next ~15 minutes
|
||||
</div>
|
||||
</Modal>
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
@@ -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,192 +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-title {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
|
||||
border-radius: 4px 4px 0 0;
|
||||
border-bottom: 1px solid var(--l3-background);
|
||||
|
||||
padding: 8px 12px;
|
||||
|
||||
.azure-service-details-overview-configuration-title-text {
|
||||
color: var(--l1-foreground);
|
||||
font-family: Inter;
|
||||
font-size: 13px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 22px; /* 157.143% */
|
||||
letter-spacing: -0.07px;
|
||||
}
|
||||
|
||||
.configuration-action {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.azure-service-details-overview-configuration-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
|
||||
padding: 12px;
|
||||
background: var(--l1-background);
|
||||
|
||||
.azure-service-details-overview-configuration-content-item {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.azure-service-details-overview-configuration-actions {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
padding: 8px 12px;
|
||||
|
||||
border-top: 1px solid var(--l3-background);
|
||||
background: var(--l1-background);
|
||||
}
|
||||
|
||||
.azure-service-details-overview-configuration-title-text-select-all {
|
||||
color: var(--l2-foreground);
|
||||
font-family: Inter;
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 18px; /* 138.462% */
|
||||
}
|
||||
}
|
||||
|
||||
.azure-service-details-actions {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 8px;
|
||||
padding: 12px 0;
|
||||
}
|
||||
|
||||
.azure-service-dashboards {
|
||||
border-radius: 6px;
|
||||
border: 1px solid var(--l3-background);
|
||||
background: var(--l1-background);
|
||||
|
||||
.azure-service-dashboards-title {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
color: var(--l2-foreground);
|
||||
font-family: Inter;
|
||||
font-size: 13px;
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
line-height: 20px; /* 142.857% */
|
||||
letter-spacing: -0.07px;
|
||||
|
||||
padding: 8px 16px;
|
||||
border-bottom: 1px solid var(--l3-background);
|
||||
}
|
||||
|
||||
.azure-service-dashboards-items {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
|
||||
.azure-service-dashboard-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
padding: 12px 16px 12px 16px;
|
||||
|
||||
&.azure-service-dashboard-item-clickable {
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
background-color: var(--l1-background);
|
||||
}
|
||||
}
|
||||
|
||||
.azure-service-dashboard-item-title {
|
||||
color: var(--l1-foreground);
|
||||
font-family: Inter;
|
||||
font-size: 13px;
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
line-height: 20px; /* 142.857% */
|
||||
letter-spacing: -0.07px;
|
||||
}
|
||||
|
||||
.azure-service-dashboard-item-description {
|
||||
color: var(--l2-foreground);
|
||||
font-family: Inter;
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 18px; /* 150% */
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,414 +0,0 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useQueryClient } from 'react-query';
|
||||
import { Button } from '@signozhq/button';
|
||||
import Tabs from '@signozhq/tabs';
|
||||
import { Checkbox, Skeleton } from 'antd';
|
||||
import CloudServiceDataCollected from 'components/CloudIntegrations/CloudServiceDataCollected/CloudServiceDataCollected';
|
||||
import { MarkdownRenderer } from 'components/MarkdownRenderer/MarkdownRenderer';
|
||||
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||
import { INTEGRATION_TYPES } from 'container/Integrations/constants';
|
||||
import { AzureConfig, AzureService } from 'container/Integrations/types';
|
||||
import { useGetCloudIntegrationServiceDetails } from 'hooks/integration/useServiceDetails';
|
||||
import { useUpdateServiceConfig } from 'hooks/integration/useUpdateServiceConfig';
|
||||
import { useSafeNavigate } from 'hooks/useSafeNavigate';
|
||||
import { isEqual } from 'lodash-es';
|
||||
import { Save, X } from 'lucide-react';
|
||||
|
||||
import './AzureServiceDetails.styles.scss';
|
||||
|
||||
interface AzureServiceDetailsProps {
|
||||
selectedService: AzureService | null;
|
||||
cloudAccountId: string;
|
||||
}
|
||||
|
||||
function configToMap(
|
||||
config: AzureConfig[] | undefined,
|
||||
): { [key: string]: boolean } {
|
||||
return (config || []).reduce(
|
||||
(acc: { [key: string]: boolean }, item: AzureConfig) => {
|
||||
acc[item.name] = item.enabled;
|
||||
return acc;
|
||||
},
|
||||
{},
|
||||
);
|
||||
}
|
||||
|
||||
export default function AzureServiceDetails({
|
||||
selectedService,
|
||||
cloudAccountId,
|
||||
}: AzureServiceDetailsProps): JSX.Element {
|
||||
const queryClient = useQueryClient();
|
||||
const { safeNavigate } = useSafeNavigate();
|
||||
|
||||
const {
|
||||
data: serviceDetailsData,
|
||||
isLoading,
|
||||
refetch: refetchServiceDetails,
|
||||
} = useGetCloudIntegrationServiceDetails(
|
||||
INTEGRATION_TYPES.AZURE,
|
||||
selectedService?.id || '',
|
||||
cloudAccountId || undefined,
|
||||
);
|
||||
|
||||
const {
|
||||
mutate: updateAzureServiceConfig,
|
||||
isLoading: isUpdating,
|
||||
} = useUpdateServiceConfig();
|
||||
|
||||
// Last saved/committed config — updated when data loads and on save success.
|
||||
// Used for hasChanges and Discard so buttons hide immediately after save.
|
||||
const [lastSavedSnapshot, setLastSavedSnapshot] = useState<{
|
||||
logs: { [key: string]: boolean };
|
||||
metrics: { [key: string]: boolean };
|
||||
}>({ logs: {}, metrics: {} });
|
||||
|
||||
// Editable state
|
||||
const [azureLogsEnabledAll, setAzureLogsEnabledAll] = useState<boolean>(false);
|
||||
const [azureMetricsEnabledAll, setAzureMetricsEnabledAll] = useState<boolean>(
|
||||
false,
|
||||
);
|
||||
const [logsConfig, updateLogsConfig] = useState<{ [key: string]: boolean }>(
|
||||
{},
|
||||
);
|
||||
const [metricsConfigs, updateMetricsConfigs] = useState<{
|
||||
[key: string]: boolean;
|
||||
}>({});
|
||||
|
||||
// Sync state when serviceDetailsData loads
|
||||
useEffect(() => {
|
||||
if (!serviceDetailsData?.config) {
|
||||
return;
|
||||
}
|
||||
|
||||
const logs = configToMap(serviceDetailsData.config.logs as AzureConfig[]);
|
||||
const metrics = configToMap(
|
||||
serviceDetailsData.config.metrics as AzureConfig[],
|
||||
);
|
||||
|
||||
if (Object.keys(logs).length > 0) {
|
||||
updateLogsConfig(logs);
|
||||
setAzureLogsEnabledAll(
|
||||
!(serviceDetailsData.config.logs as AzureConfig[])?.some(
|
||||
(log: AzureConfig) => !log.enabled,
|
||||
),
|
||||
);
|
||||
}
|
||||
if (Object.keys(metrics).length > 0) {
|
||||
updateMetricsConfigs(metrics);
|
||||
setAzureMetricsEnabledAll(
|
||||
!(serviceDetailsData.config.metrics as AzureConfig[])?.some(
|
||||
(metric: AzureConfig) => !metric.enabled,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
setLastSavedSnapshot({ logs, metrics });
|
||||
}, [serviceDetailsData]);
|
||||
|
||||
const hasChanges =
|
||||
!isEqual(logsConfig, lastSavedSnapshot.logs) ||
|
||||
!isEqual(metricsConfigs, lastSavedSnapshot.metrics);
|
||||
|
||||
const handleSave = (): void => {
|
||||
if (!selectedService?.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
updateAzureServiceConfig(
|
||||
{
|
||||
cloudServiceId: INTEGRATION_TYPES.AZURE,
|
||||
serviceId: selectedService?.id,
|
||||
payload: {
|
||||
cloud_account_id: cloudAccountId,
|
||||
config: {
|
||||
logs: Object.entries(logsConfig).map(([name, enabled]) => ({
|
||||
name,
|
||||
enabled,
|
||||
})),
|
||||
metrics: Object.entries(metricsConfigs).map(([name, enabled]) => ({
|
||||
name,
|
||||
enabled,
|
||||
})),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
onSuccess: (_, variables) => {
|
||||
// Update snapshot immediately from what we saved (not current state)
|
||||
const saved = variables.payload.config;
|
||||
setLastSavedSnapshot({
|
||||
logs: configToMap(saved.logs),
|
||||
metrics: configToMap(saved.metrics),
|
||||
});
|
||||
queryClient.invalidateQueries([
|
||||
REACT_QUERY_KEY.AWS_SERVICE_DETAILS,
|
||||
selectedService?.id,
|
||||
cloudAccountId,
|
||||
]);
|
||||
// Invalidate services list so Enabled/Not Enabled stays in sync
|
||||
queryClient.invalidateQueries([INTEGRATION_TYPES.AZURE]);
|
||||
refetchServiceDetails();
|
||||
},
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
const handleDiscard = (): void => {
|
||||
updateLogsConfig(lastSavedSnapshot.logs);
|
||||
updateMetricsConfigs(lastSavedSnapshot.metrics);
|
||||
setAzureLogsEnabledAll(
|
||||
Object.values(lastSavedSnapshot.logs).every(Boolean) &&
|
||||
Object.keys(lastSavedSnapshot.logs).length > 0,
|
||||
);
|
||||
setAzureMetricsEnabledAll(
|
||||
Object.values(lastSavedSnapshot.metrics).every(Boolean) &&
|
||||
Object.keys(lastSavedSnapshot.metrics).length > 0,
|
||||
);
|
||||
};
|
||||
|
||||
const handleAzureLogsEnableAllChange = (checked: boolean): void => {
|
||||
setAzureLogsEnabledAll(checked);
|
||||
updateLogsConfig((prev) =>
|
||||
Object.fromEntries(Object.keys(prev).map((key) => [key, checked])),
|
||||
);
|
||||
};
|
||||
|
||||
const handleAzureMetricsEnableAllChange = (checked: boolean): void => {
|
||||
setAzureMetricsEnabledAll(checked);
|
||||
updateMetricsConfigs((prev) =>
|
||||
Object.fromEntries(Object.keys(prev).map((key) => [key, checked])),
|
||||
);
|
||||
};
|
||||
|
||||
const handleAzureLogsEnabledChange = (
|
||||
logName: string,
|
||||
checked: boolean,
|
||||
): void => {
|
||||
updateLogsConfig((prev) => ({ ...prev, [logName]: checked }));
|
||||
};
|
||||
|
||||
const handleAzureMetricsEnabledChange = (
|
||||
metricName: string,
|
||||
checked: boolean,
|
||||
): void => {
|
||||
updateMetricsConfigs((prev) => ({ ...prev, [metricName]: checked }));
|
||||
};
|
||||
|
||||
// Keep "enable all" in sync when individual items change
|
||||
useEffect(() => {
|
||||
if (Object.keys(logsConfig).length > 0) {
|
||||
const allEnabled = Object.values(logsConfig).every(Boolean);
|
||||
setAzureLogsEnabledAll(allEnabled);
|
||||
}
|
||||
}, [logsConfig]);
|
||||
useEffect(() => {
|
||||
if (Object.keys(metricsConfigs).length > 0) {
|
||||
const allEnabled = Object.values(metricsConfigs).every(Boolean);
|
||||
setAzureMetricsEnabledAll(allEnabled);
|
||||
}
|
||||
}, [metricsConfigs]);
|
||||
|
||||
const renderOverview = (): JSX.Element => {
|
||||
const dashboards = serviceDetailsData?.assets?.dashboards || [];
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="azure-service-details-overview-loading">
|
||||
<Skeleton active />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="azure-service-details-overview">
|
||||
{!isLoading && (
|
||||
<div className="azure-service-details-overview-configuration">
|
||||
<div className="azure-service-details-overview-configuration-logs">
|
||||
<div className="azure-service-details-overview-configuration-title">
|
||||
<div className="azure-service-details-overview-configuration-title-text">
|
||||
Azure Logs
|
||||
</div>
|
||||
<div className="configuration-action">
|
||||
<Checkbox
|
||||
checked={azureLogsEnabledAll}
|
||||
indeterminate={
|
||||
Object.values(logsConfig).some(Boolean) &&
|
||||
!Object.values(logsConfig).every(Boolean)
|
||||
}
|
||||
onChange={(e): void =>
|
||||
handleAzureLogsEnableAllChange(e.target.checked)
|
||||
}
|
||||
disabled={isUpdating}
|
||||
>
|
||||
<span className="azure-service-details-overview-configuration-title-text-select-all">
|
||||
Select All
|
||||
</span>
|
||||
</Checkbox>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="azure-service-details-overview-configuration-content">
|
||||
{logsConfig &&
|
||||
Object.keys(logsConfig).length > 0 &&
|
||||
Object.keys(logsConfig).map((logName: string) => (
|
||||
<div
|
||||
key={logName}
|
||||
className="azure-service-details-overview-configuration-content-item"
|
||||
>
|
||||
<div className="azure-service-details-overview-configuration-content-item-text">
|
||||
{logName}
|
||||
</div>
|
||||
<Checkbox
|
||||
checked={logsConfig[logName]}
|
||||
onChange={(e): void =>
|
||||
handleAzureLogsEnabledChange(logName, e.target.checked)
|
||||
}
|
||||
disabled={isUpdating}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="azure-service-details-overview-configuration-metrics">
|
||||
<div className="azure-service-details-overview-configuration-title">
|
||||
<div className="azure-service-details-overview-configuration-title-text">
|
||||
Azure Metrics
|
||||
</div>
|
||||
<div className="configuration-action">
|
||||
<Checkbox
|
||||
checked={azureMetricsEnabledAll}
|
||||
indeterminate={
|
||||
Object.values(metricsConfigs).some(Boolean) &&
|
||||
!Object.values(metricsConfigs).every(Boolean)
|
||||
}
|
||||
onChange={(e): void =>
|
||||
handleAzureMetricsEnableAllChange(e.target.checked)
|
||||
}
|
||||
disabled={isUpdating}
|
||||
>
|
||||
<span className="azure-service-details-overview-configuration-title-text-select-all">
|
||||
Select All
|
||||
</span>
|
||||
</Checkbox>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="azure-service-details-overview-configuration-content">
|
||||
{metricsConfigs &&
|
||||
Object.keys(metricsConfigs).length > 0 &&
|
||||
Object.keys(metricsConfigs).map((metricName: string) => (
|
||||
<div
|
||||
key={metricName}
|
||||
className="azure-service-details-overview-configuration-content-item"
|
||||
>
|
||||
<div className="azure-service-details-overview-configuration-content-item-text">
|
||||
{metricName}
|
||||
</div>
|
||||
<Checkbox
|
||||
checked={metricsConfigs[metricName]}
|
||||
onChange={(e): void =>
|
||||
handleAzureMetricsEnabledChange(metricName, e.target.checked)
|
||||
}
|
||||
disabled={isUpdating}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
{hasChanges && (
|
||||
<div className="azure-service-details-overview-configuration-actions">
|
||||
<Button
|
||||
variant="solid"
|
||||
color="secondary"
|
||||
onClick={handleDiscard}
|
||||
disabled={isUpdating}
|
||||
size="xs"
|
||||
prefixIcon={<X size={14} />}
|
||||
>
|
||||
Discard
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="solid"
|
||||
color="primary"
|
||||
onClick={handleSave}
|
||||
loading={isUpdating}
|
||||
disabled={isUpdating}
|
||||
size="xs"
|
||||
prefixIcon={<Save size={14} />}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<MarkdownRenderer
|
||||
variables={{}}
|
||||
markdownContent={serviceDetailsData?.overview}
|
||||
/>
|
||||
|
||||
<div className="azure-service-dashboards">
|
||||
<div className="azure-service-dashboards-title">Dashboards</div>
|
||||
<div className="azure-service-dashboards-items">
|
||||
{dashboards.map((dashboard) => (
|
||||
<div
|
||||
key={dashboard.id}
|
||||
className={`azure-service-dashboard-item ${
|
||||
dashboard.url ? 'azure-service-dashboard-item-clickable' : ''
|
||||
}`}
|
||||
onClick={(): void => {
|
||||
if (dashboard.url) {
|
||||
safeNavigate(dashboard.url);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className="azure-service-dashboard-item-title">
|
||||
{dashboard.title}
|
||||
</div>
|
||||
<div className="azure-service-dashboard-item-description">
|
||||
{dashboard.description}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const renderDataCollected = (): JSX.Element => {
|
||||
return (
|
||||
<div className="azure-service-details-data-collected-table">
|
||||
<CloudServiceDataCollected
|
||||
logsData={serviceDetailsData?.data_collected?.logs || []}
|
||||
metricsData={serviceDetailsData?.data_collected?.metrics || []}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="azure-service-details-container">
|
||||
<Tabs
|
||||
defaultValue="overview"
|
||||
className="azure-service-details-tabs"
|
||||
items={[
|
||||
{
|
||||
children: renderOverview(),
|
||||
key: 'overview',
|
||||
label: 'Overview',
|
||||
},
|
||||
{
|
||||
children: renderDataCollected(),
|
||||
key: 'data-collected',
|
||||
label: 'Data Collected',
|
||||
},
|
||||
]}
|
||||
variant="secondary"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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,151 +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,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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,100 +0,0 @@
|
||||
import { useMemo } from 'react';
|
||||
import { Badge } from '@signozhq/badge';
|
||||
import { IntegrationsProps } from 'types/api/integrations/types';
|
||||
|
||||
import { ONE_CLICK_INTEGRATIONS } from '../constants';
|
||||
|
||||
import './OneClickIntegrations.styles.scss';
|
||||
|
||||
interface OneClickIntegrationsProps {
|
||||
searchQuery: string;
|
||||
setSelectedIntegration: (integration: IntegrationsProps) => void;
|
||||
}
|
||||
|
||||
function OneClickIntegrations(props: OneClickIntegrationsProps): JSX.Element {
|
||||
const { searchQuery, setSelectedIntegration } = props;
|
||||
|
||||
const filteredIntegrations = useMemo(() => {
|
||||
const query = searchQuery.trim().toLowerCase();
|
||||
if (!query) {
|
||||
return ONE_CLICK_INTEGRATIONS;
|
||||
}
|
||||
return ONE_CLICK_INTEGRATIONS.filter(
|
||||
(integration) =>
|
||||
integration.title.toLowerCase().includes(query) ||
|
||||
integration.description.toLowerCase().includes(query),
|
||||
);
|
||||
}, [searchQuery]);
|
||||
|
||||
const handleSelectedIntegration = (integration: IntegrationsProps): void => {
|
||||
setSelectedIntegration(integration);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="one-click-integrations">
|
||||
<div className="one-click-integrations-header">
|
||||
<div className="one-click-integrations-header-title">
|
||||
One Click Integrations
|
||||
</div>
|
||||
|
||||
<div className="one-click-integrations-header-dotted-double-line">
|
||||
<img
|
||||
src="/svgs/dotted-double-line.svg"
|
||||
alt="dotted-double-line"
|
||||
width="100%"
|
||||
height="100%"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="one-click-integrations-list">
|
||||
{filteredIntegrations.length === 0 && searchQuery.trim() ? (
|
||||
<div className="integrations-not-found-container">
|
||||
<div className="integrations-not-found-content">
|
||||
<img
|
||||
src="/Icons/awwSnap.svg"
|
||||
alt="no-integrations"
|
||||
className="integrations-not-found-image"
|
||||
/>
|
||||
<div className="integrations-not-found-text">
|
||||
No integrations found for “{searchQuery.trim()}”
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{filteredIntegrations.map((integration) => (
|
||||
<div
|
||||
className="one-click-integrations-list-item"
|
||||
key={integration.id}
|
||||
onClick={(): void => handleSelectedIntegration(integration)}
|
||||
>
|
||||
<div className="one-click-integrations-list-item-title">
|
||||
<div className="one-click-integrations-list-item-title-image-container">
|
||||
<img src={integration.icon} alt={integration.title} />
|
||||
<div className="one-click-integrations-list-item-title-text">
|
||||
{integration.title}
|
||||
</div>
|
||||
</div>
|
||||
{integration.is_new && (
|
||||
<div className="one-click-integrations-list-item-new-tag">
|
||||
<Badge color="robin" variant="default">
|
||||
NEW
|
||||
</Badge>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="one-click-integrations-list-item-description">
|
||||
{integration.description}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default OneClickIntegrations;
|
||||
@@ -1,163 +0,0 @@
|
||||
import { AzureRegion } from './types';
|
||||
|
||||
export const INTEGRATION_TELEMETRY_EVENTS = {
|
||||
INTEGRATIONS_LIST_VISITED: 'Integrations Page: Visited the list page',
|
||||
INTEGRATIONS_ITEM_LIST_CLICKED: 'Integrations Page: Clicked an integration',
|
||||
INTEGRATIONS_DETAIL_CONNECT:
|
||||
'Integrations Detail Page: Clicked connect integration button',
|
||||
INTEGRATIONS_DETAIL_TEST_CONNECTION:
|
||||
'Integrations Detail Page: Clicked test Connection button for integration',
|
||||
INTEGRATIONS_DETAIL_REMOVE_INTEGRATION:
|
||||
'Integrations Detail Page: Clicked remove Integration button for integration',
|
||||
INTEGRATIONS_DETAIL_CONFIGURE_INSTRUCTION:
|
||||
'Integrations Detail Page: Navigated to configure an integration',
|
||||
AWS_INTEGRATION_ACCOUNT_REMOVED:
|
||||
'AWS Integration Detail page: Clicked remove Integration button for integration',
|
||||
};
|
||||
|
||||
export const INTEGRATION_TYPES = {
|
||||
AWS: 'aws',
|
||||
AZURE: 'azure',
|
||||
};
|
||||
|
||||
export const AWS_INTEGRATION = {
|
||||
id: INTEGRATION_TYPES.AWS,
|
||||
title: 'Amazon Web Services',
|
||||
description: 'One click setup for AWS monitoring with SigNoz',
|
||||
author: {
|
||||
name: 'SigNoz',
|
||||
email: 'integrations@signoz.io',
|
||||
homepage: 'https://signoz.io',
|
||||
},
|
||||
icon: `/Logos/aws-dark.svg`,
|
||||
icon_alt: 'aws-logo',
|
||||
is_installed: false,
|
||||
is_new: false,
|
||||
};
|
||||
|
||||
export const AZURE_INTEGRATION = {
|
||||
id: INTEGRATION_TYPES.AZURE,
|
||||
title: 'Microsoft Azure',
|
||||
description: 'One click setup for Azure monitoring with SigNoz',
|
||||
author: {
|
||||
name: 'SigNoz',
|
||||
email: 'integrations@signoz.io',
|
||||
homepage: 'https://signoz.io',
|
||||
},
|
||||
icon: `/Logos/azure-openai.svg`,
|
||||
icon_alt: 'azure-logo',
|
||||
is_installed: false,
|
||||
is_new: true,
|
||||
};
|
||||
|
||||
export const ONE_CLICK_INTEGRATIONS = [AZURE_INTEGRATION, AWS_INTEGRATION];
|
||||
|
||||
export const AZURE_REGIONS: AzureRegion[] = [
|
||||
{
|
||||
label: 'Australia Central',
|
||||
value: 'australiacentral',
|
||||
geography: 'Australia',
|
||||
},
|
||||
{
|
||||
label: 'Australia Central 2',
|
||||
value: 'australiacentral2',
|
||||
geography: 'Australia',
|
||||
},
|
||||
{ label: 'Australia East', value: 'australiaeast', geography: 'Australia' },
|
||||
{
|
||||
label: 'Australia Southeast',
|
||||
value: 'australiasoutheast',
|
||||
geography: 'Australia',
|
||||
},
|
||||
{ label: 'Austria East', value: 'austriaeast', geography: 'Austria' },
|
||||
{ label: 'Belgium Central', value: 'belgiumcentral', geography: 'Belgium' },
|
||||
{ label: 'Brazil South', value: 'brazilsouth', geography: 'Brazil' },
|
||||
{ label: 'Brazil Southeast', value: 'brazilsoutheast', geography: 'Brazil' },
|
||||
{ label: 'Canada Central', value: 'canadacentral', geography: 'Canada' },
|
||||
{ label: 'Canada East', value: 'canadaeast', geography: 'Canada' },
|
||||
{ label: 'Central India', value: 'centralindia', geography: 'India' },
|
||||
{ label: 'Central US', value: 'centralus', geography: 'United States' },
|
||||
{ label: 'Chile Central', value: 'chilecentral', geography: 'Chile' },
|
||||
{ label: 'East Asia', value: 'eastasia', geography: 'Asia Pacific' },
|
||||
{ label: 'East US', value: 'eastus', geography: 'United States' },
|
||||
{ label: 'East US 2', value: 'eastus2', geography: 'United States' },
|
||||
{ label: 'France Central', value: 'francecentral', geography: 'France' },
|
||||
{ label: 'France South', value: 'francesouth', geography: 'France' },
|
||||
{ label: 'Germany North', value: 'germanynorth', geography: 'Germany' },
|
||||
{
|
||||
label: 'Germany West Central',
|
||||
value: 'germanywestcentral',
|
||||
geography: 'Germany',
|
||||
},
|
||||
{
|
||||
label: 'Indonesia Central',
|
||||
value: 'indonesiacentral',
|
||||
geography: 'Indonesia',
|
||||
},
|
||||
{ label: 'Israel Central', value: 'israelcentral', geography: 'Israel' },
|
||||
{ label: 'Italy North', value: 'italynorth', geography: 'Italy' },
|
||||
{ label: 'Japan East', value: 'japaneast', geography: 'Japan' },
|
||||
{ label: 'Japan West', value: 'japanwest', geography: 'Japan' },
|
||||
{ label: 'Korea Central', value: 'koreacentral', geography: 'Korea' },
|
||||
{ label: 'Korea South', value: 'koreasouth', geography: 'Korea' },
|
||||
{ label: 'Malaysia West', value: 'malaysiawest', geography: 'Malaysia' },
|
||||
{ label: 'Mexico Central', value: 'mexicocentral', geography: 'Mexico' },
|
||||
{
|
||||
label: 'New Zealand North',
|
||||
value: 'newzealandnorth',
|
||||
geography: 'New Zealand',
|
||||
},
|
||||
{
|
||||
label: 'North Central US',
|
||||
value: 'northcentralus',
|
||||
geography: 'United States',
|
||||
},
|
||||
{ label: 'North Europe', value: 'northeurope', geography: 'Europe' },
|
||||
{ label: 'Norway East', value: 'norwayeast', geography: 'Norway' },
|
||||
{ label: 'Norway West', value: 'norwaywest', geography: 'Norway' },
|
||||
{ label: 'Poland Central', value: 'polandcentral', geography: 'Poland' },
|
||||
{ label: 'Qatar Central', value: 'qatarcentral', geography: 'Qatar' },
|
||||
{
|
||||
label: 'South Africa North',
|
||||
value: 'southafricanorth',
|
||||
geography: 'South Africa',
|
||||
},
|
||||
{
|
||||
label: 'South Africa West',
|
||||
value: 'southafricawest',
|
||||
geography: 'South Africa',
|
||||
},
|
||||
{
|
||||
label: 'South Central US',
|
||||
value: 'southcentralus',
|
||||
geography: 'United States',
|
||||
},
|
||||
{ label: 'South India', value: 'southindia', geography: 'India' },
|
||||
{ label: 'Southeast Asia', value: 'southeastasia', geography: 'Asia Pacific' },
|
||||
{ label: 'Spain Central', value: 'spaincentral', geography: 'Spain' },
|
||||
{ label: 'Sweden Central', value: 'swedencentral', geography: 'Sweden' },
|
||||
{
|
||||
label: 'Switzerland North',
|
||||
value: 'switzerlandnorth',
|
||||
geography: 'Switzerland',
|
||||
},
|
||||
{
|
||||
label: 'Switzerland West',
|
||||
value: 'switzerlandwest',
|
||||
geography: 'Switzerland',
|
||||
},
|
||||
{ label: 'UAE Central', value: 'uaecentral', geography: 'UAE' },
|
||||
{ label: 'UAE North', value: 'uaenorth', geography: 'UAE' },
|
||||
{ label: 'UK South', value: 'uksouth', geography: 'United Kingdom' },
|
||||
{ label: 'UK West', value: 'ukwest', geography: 'United Kingdom' },
|
||||
{
|
||||
label: 'West Central US',
|
||||
value: 'westcentralus',
|
||||
geography: 'United States',
|
||||
},
|
||||
{ label: 'West Europe', value: 'westeurope', geography: 'Europe' },
|
||||
{ label: 'West India', value: 'westindia', geography: 'India' },
|
||||
{ label: 'West US', value: 'westus', geography: 'United States' },
|
||||
{ label: 'West US 2', value: 'westus2', geography: 'United States' },
|
||||
{ label: 'West US 3', value: 'westus3', geography: 'United States' },
|
||||
];
|
||||
@@ -1,122 +0,0 @@
|
||||
import {
|
||||
AWSCloudAccountConfig,
|
||||
AWSServiceConfig,
|
||||
} from './CloudIntegration/AmazonWebServices/types';
|
||||
|
||||
export enum IntegrationType {
|
||||
AWS_SERVICES = 'aws-services',
|
||||
AZURE_SERVICES = 'azure-services',
|
||||
}
|
||||
|
||||
interface LogField {
|
||||
name: string;
|
||||
path: string;
|
||||
type: string;
|
||||
}
|
||||
|
||||
interface Metric {
|
||||
name: string;
|
||||
type: string;
|
||||
unit: string;
|
||||
}
|
||||
|
||||
export interface AzureConfig {
|
||||
name: string;
|
||||
enabled: boolean;
|
||||
}
|
||||
|
||||
interface DataStatus {
|
||||
last_received_ts_ms: number;
|
||||
last_received_from: string;
|
||||
}
|
||||
|
||||
export interface IServiceStatus {
|
||||
logs: DataStatus | null;
|
||||
metrics: DataStatus | null;
|
||||
}
|
||||
|
||||
export interface AzureServicesConfig {
|
||||
logs: AzureConfig[];
|
||||
metrics: AzureConfig[];
|
||||
}
|
||||
|
||||
export interface AzureServiceConfigPayload {
|
||||
cloud_account_id: string;
|
||||
config: AzureServicesConfig;
|
||||
}
|
||||
|
||||
interface Dashboard {
|
||||
id: string;
|
||||
url: string;
|
||||
title: string;
|
||||
description: string;
|
||||
image: string;
|
||||
}
|
||||
|
||||
export interface SupportedSignals {
|
||||
metrics: boolean;
|
||||
logs: boolean;
|
||||
}
|
||||
|
||||
export interface AzureService {
|
||||
id: string;
|
||||
title: string;
|
||||
icon: string;
|
||||
config: AzureServicesConfig;
|
||||
}
|
||||
|
||||
export interface ServiceData {
|
||||
id: string;
|
||||
title: string;
|
||||
icon: string;
|
||||
overview: string;
|
||||
supported_signals: SupportedSignals;
|
||||
assets: {
|
||||
dashboards: Dashboard[];
|
||||
};
|
||||
data_collected: {
|
||||
logs?: LogField[];
|
||||
metrics: Metric[];
|
||||
};
|
||||
config?: AWSServiceConfig | AzureServicesConfig;
|
||||
status?: IServiceStatus;
|
||||
}
|
||||
|
||||
export interface CloudAccount {
|
||||
id: string;
|
||||
cloud_account_id: string;
|
||||
config: AzureCloudAccountConfig | AWSCloudAccountConfig;
|
||||
status: AccountStatus | IServiceStatus;
|
||||
}
|
||||
|
||||
export interface AzureCloudAccountConfig {
|
||||
deployment_region: string;
|
||||
resource_groups: string[];
|
||||
}
|
||||
|
||||
export interface AccountStatus {
|
||||
integration: IntegrationStatus;
|
||||
}
|
||||
|
||||
export interface IntegrationStatus {
|
||||
last_heartbeat_ts_ms: number;
|
||||
}
|
||||
|
||||
export interface AzureRegion {
|
||||
label: string;
|
||||
geography: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
export interface UpdateServiceConfigPayload {
|
||||
cloud_account_id: string;
|
||||
config: AzureServicesConfig;
|
||||
}
|
||||
|
||||
export interface UpdateServiceConfigResponse {
|
||||
status: string;
|
||||
data: {
|
||||
id: string;
|
||||
config: AzureServicesConfig;
|
||||
};
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
import history from 'lib/history';
|
||||
|
||||
export const handleContactSupport = (isCloudUser: boolean): void => {
|
||||
if (isCloudUser) {
|
||||
history.push('/support');
|
||||
} else {
|
||||
window.open('https://signoz.io/slack', '_blank');
|
||||
}
|
||||
};
|
||||
@@ -44,9 +44,6 @@ import {
|
||||
sanitizeDashboardData,
|
||||
} from 'container/DashboardContainer/DashboardDescription/utils';
|
||||
import { Base64Icons } from 'container/DashboardContainer/DashboardSettings/General/utils';
|
||||
// #TODO: lucide will be removing brand icons like Github in future, in that case we can use simple icons
|
||||
// see more: https://github.com/lucide-icons/lucide/issues/94
|
||||
import { handleContactSupport } from 'container/Integrations/utils';
|
||||
import dayjs from 'dayjs';
|
||||
import { useGetAllDashboard } from 'hooks/dashboard/useGetAllDashboard';
|
||||
import useComponentPermission from 'hooks/useComponentPermission';
|
||||
@@ -75,6 +72,9 @@ import {
|
||||
Search,
|
||||
SquareArrowOutUpRight,
|
||||
} from 'lucide-react';
|
||||
// #TODO: lucide will be removing brand icons like Github in future, in that case we can use simple icons
|
||||
// see more: https://github.com/lucide-icons/lucide/issues/94
|
||||
import { handleContactSupport } from 'pages/Integrations/utils';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
||||
import { useErrorModal } from 'providers/ErrorModalProvider';
|
||||
|
||||
@@ -6,7 +6,7 @@ import logEvent from 'api/common/logEvent';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import { Check } from 'lucide-react';
|
||||
|
||||
import '../Integrations/Integrations.styles.scss';
|
||||
import '../../pages/Integrations/Integrations.styles.scss';
|
||||
|
||||
export function RequestDashboardBtn(): JSX.Element {
|
||||
const [
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { ReactNode, useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { ReactNode, useState } from 'react';
|
||||
import MEditor, { EditorProps, Monaco } from '@monaco-editor/react';
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import type { InputRef } from 'antd';
|
||||
import {
|
||||
Button,
|
||||
Collapse,
|
||||
@@ -47,23 +46,12 @@ function Overview({
|
||||
handleChangeSelectedView,
|
||||
}: Props): JSX.Element {
|
||||
const [isWrapWord, setIsWrapWord] = useState<boolean>(true);
|
||||
const [isSearchVisible, setIsSearchVisible] = useState<boolean>(true);
|
||||
const [isSearchVisible, setIsSearchVisible] = useState<boolean>(false);
|
||||
const [isAttributesExpanded, setIsAttributesExpanded] = useState<boolean>(
|
||||
true,
|
||||
);
|
||||
const [fieldSearchInput, setFieldSearchInput] = useState<string>('');
|
||||
|
||||
const focusTimerRef = useRef<ReturnType<typeof setTimeout>>();
|
||||
|
||||
const searchInputRef = useCallback((node: InputRef | null) => {
|
||||
clearTimeout(focusTimerRef.current);
|
||||
if (node) {
|
||||
focusTimerRef.current = setTimeout(() => node.focus(), 100);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => (): void => clearTimeout(focusTimerRef.current), []);
|
||||
|
||||
const isDarkMode = useIsDarkMode();
|
||||
|
||||
const options: EditorProps['options'] = {
|
||||
@@ -208,7 +196,7 @@ function Overview({
|
||||
<>
|
||||
{isSearchVisible && (
|
||||
<Input
|
||||
ref={searchInputRef}
|
||||
autoFocus
|
||||
placeholder="Search for a field..."
|
||||
className="search-input"
|
||||
value={fieldSearchInput}
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
|
||||
import { getColorsForSeverityLabels, isRedLike } from '../utils';
|
||||
|
||||
describe('getColorsForSeverityLabels', () => {
|
||||
it('should return slate for blank labels', () => {
|
||||
expect(getColorsForSeverityLabels('', 0)).toBe(Color.BG_SLATE_300);
|
||||
expect(getColorsForSeverityLabels(' ', 0)).toBe(Color.BG_SLATE_300);
|
||||
});
|
||||
|
||||
it('should return correct colors for known severity variants', () => {
|
||||
expect(getColorsForSeverityLabels('INFO', 0)).toBe(Color.BG_ROBIN_600);
|
||||
expect(getColorsForSeverityLabels('ERROR', 0)).toBe(Color.BG_CHERRY_600);
|
||||
expect(getColorsForSeverityLabels('WARN', 0)).toBe(Color.BG_AMBER_600);
|
||||
expect(getColorsForSeverityLabels('DEBUG', 0)).toBe(Color.BG_AQUA_600);
|
||||
expect(getColorsForSeverityLabels('TRACE', 0)).toBe(Color.BG_FOREST_600);
|
||||
expect(getColorsForSeverityLabels('FATAL', 0)).toBe(Color.BG_SAKURA_600);
|
||||
});
|
||||
|
||||
it('should return non-red colors for unrecognized labels at any index', () => {
|
||||
for (let i = 0; i < 30; i++) {
|
||||
const color = getColorsForSeverityLabels('4', i);
|
||||
expect(isRedLike(color)).toBe(false);
|
||||
}
|
||||
});
|
||||
|
||||
it('should return non-red colors for numeric severity text', () => {
|
||||
const numericLabels = ['1', '2', '4', '9', '13', '17', '21'];
|
||||
numericLabels.forEach((label) => {
|
||||
const color = getColorsForSeverityLabels(label, 0);
|
||||
expect(isRedLike(color)).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,16 +1,7 @@
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { themeColors } from 'constants/theme';
|
||||
import { colors } from 'lib/getRandomColor';
|
||||
|
||||
// Function to determine if a color is "red-like" based on its RGB values
|
||||
export function isRedLike(hex: string): boolean {
|
||||
const r = parseInt(hex.slice(1, 3), 16);
|
||||
const g = parseInt(hex.slice(3, 5), 16);
|
||||
const b = parseInt(hex.slice(5, 7), 16);
|
||||
return r > 180 && r > g * 1.4 && r > b * 1.4;
|
||||
}
|
||||
|
||||
const SAFE_FALLBACK_COLORS = colors.filter((c) => !isRedLike(c));
|
||||
|
||||
const SEVERITY_VARIANT_COLORS: Record<string, string> = {
|
||||
TRACE: Color.BG_FOREST_600,
|
||||
Trace: Color.BG_FOREST_500,
|
||||
@@ -76,13 +67,8 @@ export function getColorsForSeverityLabels(
|
||||
label: string,
|
||||
index: number,
|
||||
): string {
|
||||
const trimmed = label.trim();
|
||||
|
||||
if (!trimmed) {
|
||||
return Color.BG_SLATE_300;
|
||||
}
|
||||
|
||||
const variantColor = SEVERITY_VARIANT_COLORS[trimmed];
|
||||
// Check if we have a direct mapping for this severity variant
|
||||
const variantColor = SEVERITY_VARIANT_COLORS[label.trim()];
|
||||
if (variantColor) {
|
||||
return variantColor;
|
||||
}
|
||||
@@ -117,8 +103,5 @@ export function getColorsForSeverityLabels(
|
||||
return Color.BG_SAKURA_500;
|
||||
}
|
||||
|
||||
return (
|
||||
SAFE_FALLBACK_COLORS[index % SAFE_FALLBACK_COLORS.length] ||
|
||||
Color.BG_SLATE_400
|
||||
);
|
||||
return colors[index % colors.length] || themeColors.red;
|
||||
}
|
||||
|
||||
@@ -111,19 +111,23 @@ const InfinityTable = forwardRef<TableVirtuosoHandle, InfinityTableProps>(
|
||||
);
|
||||
|
||||
const itemContent = useCallback(
|
||||
(index: number, log: Record<string, unknown>): JSX.Element => (
|
||||
<TableRow
|
||||
tableColumns={tableColumns}
|
||||
index={index}
|
||||
log={log}
|
||||
logs={tableViewProps.logs}
|
||||
hasActions
|
||||
fontSize={tableViewProps.fontSize}
|
||||
onShowLogDetails={onSetActiveLog}
|
||||
isActiveLog={activeLog?.id === log.id}
|
||||
onClearActiveLog={onCloseActiveLog}
|
||||
/>
|
||||
),
|
||||
(index: number, log: Record<string, unknown>): JSX.Element => {
|
||||
return (
|
||||
<div key={log.id as string}>
|
||||
<TableRow
|
||||
tableColumns={tableColumns}
|
||||
index={index}
|
||||
log={log}
|
||||
logs={tableViewProps.logs}
|
||||
hasActions
|
||||
fontSize={tableViewProps.fontSize}
|
||||
onShowLogDetails={onSetActiveLog}
|
||||
isActiveLog={activeLog?.id === log.id}
|
||||
onClearActiveLog={onCloseActiveLog}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
[
|
||||
tableColumns,
|
||||
onSetActiveLog,
|
||||
@@ -139,8 +143,7 @@ const InfinityTable = forwardRef<TableVirtuosoHandle, InfinityTableProps>(
|
||||
{tableColumns
|
||||
.filter((column) => column.key)
|
||||
.map((column) => {
|
||||
const isDragColumn =
|
||||
column.key !== 'expand' && column.key !== 'state-indicator';
|
||||
const isDragColumn = column.key !== 'expand';
|
||||
|
||||
return (
|
||||
<TableHeaderCellStyled
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Form } from 'antd';
|
||||
import { initialQueryBuilderFormValuesMap } from 'constants/queryBuilder';
|
||||
import QueryBuilderSearchV2 from 'container/QueryBuilder/filters/QueryBuilderSearchV2/QueryBuilderSearchV2';
|
||||
import QueryBuilderSearch from 'container/QueryBuilder/filters/QueryBuilderSearch';
|
||||
import isEqual from 'lodash-es/isEqual';
|
||||
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
@@ -30,7 +30,7 @@ function TagFilterInput({
|
||||
};
|
||||
|
||||
return (
|
||||
<QueryBuilderSearchV2
|
||||
<QueryBuilderSearch
|
||||
query={query}
|
||||
onChange={onQueryChange}
|
||||
placeholder={placeholder}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user