mirror of
https://github.com/SigNoz/signoz.git
synced 2026-04-20 18:50:29 +01:00
Compare commits
15 Commits
platform-p
...
tests/unif
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fce92115a9 | ||
|
|
9743002edf | ||
|
|
0efde7b5ce | ||
|
|
8bdaecbe25 | ||
|
|
deb90abd9c | ||
|
|
52992c0e80 | ||
|
|
5d6ada7a5b | ||
|
|
dbe55d4ae0 | ||
|
|
837da705b3 | ||
|
|
a9b458f1f6 | ||
|
|
ac26299c3d | ||
|
|
96d816bd1a | ||
|
|
f52d89c338 | ||
|
|
691e919a41 | ||
|
|
61ae49d4ab |
64
.github/CODEOWNERS
vendored
64
.github/CODEOWNERS
vendored
@@ -16,38 +16,38 @@ go.mod @therealpandey
|
||||
|
||||
# Scaffold Owners
|
||||
|
||||
/pkg/config/ @vikrantgupta25
|
||||
/pkg/errors/ @vikrantgupta25
|
||||
/pkg/factory/ @vikrantgupta25
|
||||
/pkg/types/ @vikrantgupta25
|
||||
/pkg/valuer/ @vikrantgupta25
|
||||
/cmd/ @vikrantgupta25
|
||||
.golangci.yml @vikrantgupta25
|
||||
/pkg/config/ @therealpandey
|
||||
/pkg/errors/ @therealpandey
|
||||
/pkg/factory/ @therealpandey
|
||||
/pkg/types/ @therealpandey
|
||||
/pkg/valuer/ @therealpandey
|
||||
/cmd/ @therealpandey
|
||||
.golangci.yml @therealpandey
|
||||
|
||||
# Zeus Owners
|
||||
|
||||
/pkg/zeus/ @vikrantgupta25
|
||||
/ee/zeus/ @vikrantgupta25
|
||||
/pkg/licensing/ @vikrantgupta25
|
||||
/ee/licensing/ @vikrantgupta25
|
||||
/pkg/zeus/ @therealpandey
|
||||
/ee/zeus/ @therealpandey
|
||||
/pkg/licensing/ @therealpandey
|
||||
/ee/licensing/ @therealpandey
|
||||
|
||||
# SQL Owners
|
||||
|
||||
/pkg/sqlmigration/ @vikrantgupta25
|
||||
/ee/sqlmigration/ @vikrantgupta25
|
||||
/pkg/sqlschema/ @vikrantgupta25
|
||||
/ee/sqlschema/ @vikrantgupta25
|
||||
/pkg/sqlmigration/ @therealpandey
|
||||
/ee/sqlmigration/ @therealpandey
|
||||
/pkg/sqlschema/ @therealpandey
|
||||
/ee/sqlschema/ @therealpandey
|
||||
|
||||
# Analytics Owners
|
||||
|
||||
/pkg/analytics/ @vikrantgupta25
|
||||
/pkg/statsreporter/ @vikrantgupta25
|
||||
/pkg/analytics/ @therealpandey
|
||||
/pkg/statsreporter/ @therealpandey
|
||||
|
||||
# Emailing Owners
|
||||
|
||||
/pkg/emailing/ @vikrantgupta25
|
||||
/pkg/types/emailtypes/ @vikrantgupta25
|
||||
/templates/email/ @vikrantgupta25
|
||||
/pkg/emailing/ @therealpandey
|
||||
/pkg/types/emailtypes/ @therealpandey
|
||||
/templates/email/ @therealpandey
|
||||
|
||||
# Querier Owners
|
||||
|
||||
@@ -97,23 +97,23 @@ go.mod @therealpandey
|
||||
|
||||
# AuthN / AuthZ Owners
|
||||
|
||||
/pkg/authz/ @vikrantgupta25
|
||||
/ee/authz/ @vikrantgupta25
|
||||
/pkg/authn/ @vikrantgupta25
|
||||
/ee/authn/ @vikrantgupta25
|
||||
/pkg/modules/user/ @vikrantgupta25
|
||||
/pkg/modules/session/ @vikrantgupta25
|
||||
/pkg/modules/organization/ @vikrantgupta25
|
||||
/pkg/modules/authdomain/ @vikrantgupta25
|
||||
/pkg/modules/role/ @vikrantgupta25
|
||||
/pkg/authz/ @therealpandey
|
||||
/ee/authz/ @therealpandey
|
||||
/pkg/authn/ @therealpandey
|
||||
/ee/authn/ @therealpandey
|
||||
/pkg/modules/user/ @therealpandey
|
||||
/pkg/modules/session/ @therealpandey
|
||||
/pkg/modules/organization/ @therealpandey
|
||||
/pkg/modules/authdomain/ @therealpandey
|
||||
/pkg/modules/role/ @therealpandey
|
||||
|
||||
# IdentN Owners
|
||||
/pkg/identn/ @vikrantgupta25
|
||||
/pkg/http/middleware/identn.go @vikrantgupta25
|
||||
/pkg/identn/ @therealpandey
|
||||
/pkg/http/middleware/identn.go @therealpandey
|
||||
|
||||
# Integration tests
|
||||
|
||||
/tests/integration/ @vikrantgupta25
|
||||
/tests/integration/ @therealpandey
|
||||
|
||||
# OpenAPI types generator
|
||||
|
||||
|
||||
10
.github/workflows/integrationci.yaml
vendored
10
.github/workflows/integrationci.yaml
vendored
@@ -25,11 +25,11 @@ jobs:
|
||||
uses: astral-sh/setup-uv@v4
|
||||
- name: install
|
||||
run: |
|
||||
cd tests/integration && uv sync
|
||||
cd tests && uv sync
|
||||
- name: fmt
|
||||
run: |
|
||||
make py-fmt
|
||||
git diff --exit-code -- tests/integration/
|
||||
git diff --exit-code -- tests/
|
||||
- name: lint
|
||||
run: |
|
||||
make py-lint
|
||||
@@ -79,7 +79,7 @@ jobs:
|
||||
uses: astral-sh/setup-uv@v4
|
||||
- name: install
|
||||
run: |
|
||||
cd tests/integration && uv sync
|
||||
cd tests && uv sync
|
||||
- name: webdriver
|
||||
run: |
|
||||
wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | sudo apt-key add -
|
||||
@@ -99,10 +99,10 @@ jobs:
|
||||
google-chrome-stable --version
|
||||
- name: run
|
||||
run: |
|
||||
cd tests/integration && \
|
||||
cd tests && \
|
||||
uv run pytest \
|
||||
--basetemp=./tmp/ \
|
||||
src/${{matrix.src}} \
|
||||
integration/src/${{matrix.src}} \
|
||||
--sqlstore-provider ${{matrix.sqlstore-provider}} \
|
||||
--sqlite-mode ${{matrix.sqlite-mode}} \
|
||||
--postgres-version ${{matrix.postgres-version}} \
|
||||
|
||||
22
Makefile
22
Makefile
@@ -201,26 +201,26 @@ docker-buildx-enterprise: go-build-enterprise js-build
|
||||
# python commands
|
||||
##############################################################
|
||||
.PHONY: py-fmt
|
||||
py-fmt: ## Run black for integration tests
|
||||
@cd tests/integration && uv run black .
|
||||
py-fmt: ## Run black across the shared tests project
|
||||
@cd tests && uv run black .
|
||||
|
||||
.PHONY: py-lint
|
||||
py-lint: ## Run lint for integration tests
|
||||
@cd tests/integration && uv run isort .
|
||||
@cd tests/integration && uv run autoflake .
|
||||
@cd tests/integration && uv run pylint .
|
||||
py-lint: ## Run lint across the shared tests project
|
||||
@cd tests && uv run isort .
|
||||
@cd tests && uv run autoflake .
|
||||
@cd tests && uv run pylint .
|
||||
|
||||
.PHONY: py-test-setup
|
||||
py-test-setup: ## Runs integration tests
|
||||
@cd tests/integration && uv run pytest --basetemp=./tmp/ -vv --reuse --capture=no src/bootstrap/setup.py::test_setup
|
||||
py-test-setup: ## Bring up the shared SigNoz backend used by integration and e2e tests
|
||||
@cd tests && uv run pytest --basetemp=./tmp/ -vv --reuse --capture=no integration/src/bootstrap/setup.py::test_setup
|
||||
|
||||
.PHONY: py-test-teardown
|
||||
py-test-teardown: ## Runs integration tests with teardown
|
||||
@cd tests/integration && uv run pytest --basetemp=./tmp/ -vv --teardown --capture=no src/bootstrap/setup.py::test_teardown
|
||||
py-test-teardown: ## Tear down the shared SigNoz backend
|
||||
@cd tests && uv run pytest --basetemp=./tmp/ -vv --teardown --capture=no integration/src/bootstrap/setup.py::test_teardown
|
||||
|
||||
.PHONY: py-test
|
||||
py-test: ## Runs integration tests
|
||||
@cd tests/integration && uv run pytest --basetemp=./tmp/ -vv --capture=no src/
|
||||
@cd tests && uv run pytest --basetemp=./tmp/ -vv --capture=no integration/src/
|
||||
|
||||
.PHONY: py-clean
|
||||
py-clean: ## Clear all pycache and pytest cache from tests directory recursively
|
||||
|
||||
@@ -3755,71 +3755,6 @@ components:
|
||||
spec:
|
||||
$ref: '#/components/schemas/RuletypesRollingWindow'
|
||||
type: object
|
||||
RuletypesGettableRule:
|
||||
properties:
|
||||
alert:
|
||||
type: string
|
||||
alertType:
|
||||
$ref: '#/components/schemas/RuletypesAlertType'
|
||||
annotations:
|
||||
additionalProperties:
|
||||
type: string
|
||||
type: object
|
||||
condition:
|
||||
$ref: '#/components/schemas/RuletypesRuleCondition'
|
||||
createAt:
|
||||
format: date-time
|
||||
type: string
|
||||
createBy:
|
||||
nullable: true
|
||||
type: string
|
||||
description:
|
||||
type: string
|
||||
disabled:
|
||||
type: boolean
|
||||
evalWindow:
|
||||
type: string
|
||||
evaluation:
|
||||
$ref: '#/components/schemas/RuletypesEvaluationEnvelope'
|
||||
frequency:
|
||||
type: string
|
||||
id:
|
||||
type: string
|
||||
labels:
|
||||
additionalProperties:
|
||||
type: string
|
||||
type: object
|
||||
notificationSettings:
|
||||
$ref: '#/components/schemas/RuletypesNotificationSettings'
|
||||
preferredChannels:
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
ruleType:
|
||||
$ref: '#/components/schemas/RuletypesRuleType'
|
||||
schemaVersion:
|
||||
type: string
|
||||
source:
|
||||
type: string
|
||||
state:
|
||||
$ref: '#/components/schemas/RuletypesAlertState'
|
||||
updateAt:
|
||||
format: date-time
|
||||
type: string
|
||||
updateBy:
|
||||
nullable: true
|
||||
type: string
|
||||
version:
|
||||
type: string
|
||||
required:
|
||||
- id
|
||||
- state
|
||||
- alert
|
||||
- ruleType
|
||||
- condition
|
||||
- createAt
|
||||
- updateAt
|
||||
type: object
|
||||
RuletypesGettableTestRule:
|
||||
properties:
|
||||
alertCount:
|
||||
@@ -4029,6 +3964,67 @@ components:
|
||||
- evalWindow
|
||||
- frequency
|
||||
type: object
|
||||
RuletypesRule:
|
||||
properties:
|
||||
alert:
|
||||
type: string
|
||||
alertType:
|
||||
$ref: '#/components/schemas/RuletypesAlertType'
|
||||
annotations:
|
||||
additionalProperties:
|
||||
type: string
|
||||
type: object
|
||||
condition:
|
||||
$ref: '#/components/schemas/RuletypesRuleCondition'
|
||||
createdAt:
|
||||
format: date-time
|
||||
type: string
|
||||
createdBy:
|
||||
type: string
|
||||
description:
|
||||
type: string
|
||||
disabled:
|
||||
type: boolean
|
||||
evalWindow:
|
||||
type: string
|
||||
evaluation:
|
||||
$ref: '#/components/schemas/RuletypesEvaluationEnvelope'
|
||||
frequency:
|
||||
type: string
|
||||
id:
|
||||
type: string
|
||||
labels:
|
||||
additionalProperties:
|
||||
type: string
|
||||
type: object
|
||||
notificationSettings:
|
||||
$ref: '#/components/schemas/RuletypesNotificationSettings'
|
||||
preferredChannels:
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
ruleType:
|
||||
$ref: '#/components/schemas/RuletypesRuleType'
|
||||
schemaVersion:
|
||||
type: string
|
||||
source:
|
||||
type: string
|
||||
state:
|
||||
$ref: '#/components/schemas/RuletypesAlertState'
|
||||
updatedAt:
|
||||
format: date-time
|
||||
type: string
|
||||
updatedBy:
|
||||
type: string
|
||||
version:
|
||||
type: string
|
||||
required:
|
||||
- id
|
||||
- state
|
||||
- alert
|
||||
- ruleType
|
||||
- condition
|
||||
type: object
|
||||
RuletypesRuleCondition:
|
||||
properties:
|
||||
absentFor:
|
||||
@@ -10776,7 +10772,7 @@ paths:
|
||||
properties:
|
||||
data:
|
||||
items:
|
||||
$ref: '#/components/schemas/RuletypesGettableRule'
|
||||
$ref: '#/components/schemas/RuletypesRule'
|
||||
type: array
|
||||
status:
|
||||
type: string
|
||||
@@ -10827,7 +10823,7 @@ paths:
|
||||
schema:
|
||||
properties:
|
||||
data:
|
||||
$ref: '#/components/schemas/RuletypesGettableRule'
|
||||
$ref: '#/components/schemas/RuletypesRule'
|
||||
status:
|
||||
type: string
|
||||
required:
|
||||
@@ -10930,7 +10926,7 @@ paths:
|
||||
schema:
|
||||
properties:
|
||||
data:
|
||||
$ref: '#/components/schemas/RuletypesGettableRule'
|
||||
$ref: '#/components/schemas/RuletypesRule'
|
||||
status:
|
||||
type: string
|
||||
required:
|
||||
@@ -10992,7 +10988,7 @@ paths:
|
||||
schema:
|
||||
properties:
|
||||
data:
|
||||
$ref: '#/components/schemas/RuletypesGettableRule'
|
||||
$ref: '#/components/schemas/RuletypesRule'
|
||||
status:
|
||||
type: string
|
||||
required:
|
||||
|
||||
@@ -20,3 +20,4 @@ We **recommend** (almost enforce) reviewing these guides before contributing to
|
||||
- [Packages](packages.md) - Naming, layout, and conventions for `pkg/` packages
|
||||
- [Service](service.md) - Managed service lifecycle with `factory.Service`
|
||||
- [SQL](sql.md) - Database and SQL patterns
|
||||
- [Types](types.md) - Domain types, request/response bodies, and storage rows in `pkg/types/`
|
||||
|
||||
152
docs/contributing/go/types.md
Normal file
152
docs/contributing/go/types.md
Normal file
@@ -0,0 +1,152 @@
|
||||
# Types
|
||||
|
||||
Domain types in `pkg/types/<domain>/` live on three serialization boundaries — inbound HTTP, outbound HTTP, and SQL — on top of an in-memory domain representation. SigNoz's convention is **core-type-first**: every domain defines a single canonical type `X`, and specialized flavors (`PostableX`, `GettableX`, `UpdatableX`, `StorableX`) are introduced **only when they actually differ from `X`**. This guide spells out when each flavor is warranted and how they relate to each other.
|
||||
|
||||
Before reading, make sure you have read [abstractions.md](abstractions.md) — the rules here build on its guidance that every new type must earn its place.
|
||||
|
||||
## The core type is required
|
||||
|
||||
Every domain package in `pkg/types/<domain>/` defines exactly one core type `X`: `AuthDomain`, `Channel`, `Rule`, `Dashboard`, `Role`, `PlannedMaintenance`. This is the canonical in-memory representation of the domain object. Domain methods, validation invariants, and business logic hang off `X` — not off the flavor types.
|
||||
|
||||
Two rules shape how the core type behaves:
|
||||
|
||||
- **Conversions can be either `New<Output>From<Input>` or a receiver-style `(x *X) ToY()` method.** Either form is fine; pick whichever reads best at the call site:
|
||||
|
||||
```go
|
||||
// Constructor form
|
||||
func NewGettableAuthDomainFromAuthDomain(d *AuthDomain, info *AuthNProviderInfo) *GettableAuthDomain
|
||||
|
||||
// Receiver form
|
||||
func (m *PlannedMaintenanceWithRules) ToPlannedMaintenance() *PlannedMaintenance
|
||||
```
|
||||
- **`X` can double as the storage row** when the DB shape would be identical. `Channel` embeds `bun.BaseModel` directly, and there is no `StorableChannel`. This is the preferred shape when it works.
|
||||
|
||||
Domain packages under `pkg/types/` must not import from other `pkg/` packages. Keep the core type's methods lightweight and push orchestration out to the module layer.
|
||||
|
||||
## Add a flavor only when it differs
|
||||
|
||||
For each of the four flavors, create it only if its shape diverges from `X`. If a flavor would have the same fields and tags as `X`, reuse `X` directly, or declare a type alias. Every flavor must earn its place per [abstractions.md](abstractions.md) rule 6 ("Wrappers must add semantics, not just rename").
|
||||
|
||||
| Flavor | Create it when it differs in… |
|
||||
| ------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `PostableX` | JSON shape differs from `X` — typically no `Id`, no audit fields, no server-computed fields. Often owns input validation via `Validate()` or a custom `UnmarshalJSON`. |
|
||||
| `GettableX` | Response shape adds server-computed fields that are not persisted — e.g., `GettableAuthDomain` adds `AuthNProviderInfo`, which is resolved at read time. |
|
||||
| `UpdatableX` | Only a strict subset of `PostableX` is replaceable on PUT. If the updatable shape equals `PostableX`, reuse `PostableX`. |
|
||||
| `StorableX` | DB row shape differs from `X` — usually `X` carries nested typed config while `StorableX` carries a flat `Data string` JSON column, plus bun tags, audit mixins, and an `OrgID`. If `X` already has those, skip the flavor. |
|
||||
|
||||
The failure mode this rule exists to prevent: minting all four flavors on reflex for every new resource, even when two or three are structurally identical. Each unnecessary flavor is another type contributors must understand and another conversion that can drift.
|
||||
|
||||
## Worked examples
|
||||
|
||||
### Channel — core type only
|
||||
|
||||
```go
|
||||
type Channels = []*Channel
|
||||
type GettableChannels = []*Channel
|
||||
|
||||
type Channel struct {
|
||||
bun.BaseModel `bun:"table:notification_channel"`
|
||||
types.Identifiable
|
||||
types.TimeAuditable
|
||||
Name string `json:"name" required:"true" bun:"name"`
|
||||
Type string `json:"type" required:"true" bun:"type"`
|
||||
Data string `json:"data" required:"true" bun:"data"`
|
||||
OrgID string `json:"orgId" required:"true" bun:"org_id"`
|
||||
}
|
||||
```
|
||||
|
||||
`Channel` is both the domain type and the bun row. `GettableChannels` is a **type alias** because `*Channel` already serializes correctly as a response. There is no `StorableChannel`, `PostableChannel`, or `UpdatableChannel` — those would be identical to `Channel` and so do not exist. Prefer this shape when it works.
|
||||
|
||||
### AuthDomain — all four flavors
|
||||
|
||||
```go
|
||||
type AuthDomain struct {
|
||||
storableAuthDomain *StorableAuthDomain
|
||||
authDomainConfig *AuthDomainConfig
|
||||
}
|
||||
|
||||
type StorableAuthDomain struct {
|
||||
bun.BaseModel `bun:"table:auth_domain"`
|
||||
types.Identifiable
|
||||
Name string `bun:"name"`
|
||||
Data string `bun:"data"` // AuthDomainConfig serialized as JSON
|
||||
OrgID valuer.UUID `bun:"org_id"`
|
||||
types.TimeAuditable
|
||||
}
|
||||
|
||||
type PostableAuthDomain struct {
|
||||
Config AuthDomainConfig `json:"config"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
type UpdateableAuthDomain struct {
|
||||
Config AuthDomainConfig `json:"config"` // Name intentionally absent
|
||||
}
|
||||
|
||||
type GettableAuthDomain struct {
|
||||
*StorableAuthDomain
|
||||
*AuthDomainConfig
|
||||
AuthNProviderInfo *AuthNProviderInfo `json:"authNProviderInfo"`
|
||||
}
|
||||
```
|
||||
|
||||
Each flavor exists for a concrete reason:
|
||||
|
||||
- `StorableAuthDomain` stores the typed config as an opaque `Data string` column, so the schema does not need to migrate every time a config field is added.
|
||||
- `PostableAuthDomain` carries the config as a structured object (not a string) for the request.
|
||||
- `UpdateableAuthDomain` excludes `Name` because a domain's name cannot change after creation.
|
||||
- `GettableAuthDomain` adds `AuthNProviderInfo`, which is derived at read time and never persisted.
|
||||
|
||||
The core `AuthDomain` holds the two live halves — `storableAuthDomain` and `authDomainConfig` — and owns business methods such as `Update(config)`. Conversions use the `New<Output>From<Input>` form: `NewAuthDomainFromConfig`, `NewAuthDomainFromStorableAuthDomain`, `NewGettableAuthDomainFromAuthDomain`.
|
||||
|
||||
## Conventions that tie the flavors together
|
||||
|
||||
- **Conversions** use either a `New<Output>From<Input>` constructor — e.g. `NewChannelFromReceiver`, `NewGettableAuthDomainFromAuthDomain` — or a receiver-style `ToY()` method. Both forms coexist in the codebase; use whichever fits the call site.
|
||||
- **Validation belongs on the core type `X`.** Putting it on `X` means every write path — HTTP create, HTTP update, in-process migration, replay — runs the same checks. `Validate()` on `PostableX` is reserved for checks that are specific to the request shape and do not apply to `X`. `UnmarshalJSON` on `PostableX` is a separate tool that lives there because decoding only happens at the HTTP boundary — `PostableAuthDomain.UnmarshalJSON` rejecting a malformed domain name at decode time is the canonical example.
|
||||
|
||||
```go
|
||||
// Domain invariants: every write path re-runs these.
|
||||
func (x *X) Validate() error { ... }
|
||||
|
||||
// Request-shape-only: checks that do not apply once the value is persisted.
|
||||
func (p *PostableX) Validate() error { ... }
|
||||
```
|
||||
- **Type aliases, not wrappers**, when two shapes are identical. `type GettableChannels = []*Channel` is correct because it adds no semantics beyond the underlying type.
|
||||
- **Serialization tags** follow [handler.md](handler.md): `required:"true"` means the JSON key must be present, `nullable:"true"` is required on any slice or map that may serialize as `null`, and types with a fixed value set must implement `Enum() []any`.
|
||||
|
||||
## A note on `UpdatableX` and `PatchableX`
|
||||
|
||||
- `UpdatableX` — the body for PUT (full replace) when the shape is a strict subset of `PostableX`. If the updatable shape equals `PostableX`, reuse `PostableX`.
|
||||
- `PatchableX` — the body for PATCH (partial update); only the fields a client is allowed to patch. For example, `PatchableRole` carries a single `Description` field even though `Role` has many — clients may patch the description but not anything else.
|
||||
|
||||
```go
|
||||
type PatchableRole struct {
|
||||
Description string `json:"description"`
|
||||
}
|
||||
```
|
||||
|
||||
Both are optional. Do not introduce them if `PostableX` already covers the case.
|
||||
|
||||
## What to avoid
|
||||
|
||||
- **Do not mint a flavor that mirrors the core type.** If `StorableX` would have the same fields as `X`, use `X` directly with `bun.BaseModel` embedded. `Channel` is the canonical example.
|
||||
- **Do not bolt domain methods onto `StorableX`.** Storage types are data carriers. Domain methods live on `X`.
|
||||
- **Do not invent new suffixes** (`Creatable`, `Fetchable`, `Savable`). The core type plus `Postable` / `Gettable` / `Updatable` / `Patchable` / `Storable` covers every case that exists today.
|
||||
- **Spelling — `Updatable`, not `Updateable`.** `Updateable` is a common typo. Prefer the shorter form when introducing new types, and rename any stragglers you come across.
|
||||
- **Spelling — `Storable`, not `Storeable`.** `Storeable` is a common typo. Prefer the shorter form when introducing new types, and rename any stragglers you come across.
|
||||
|
||||
## What should I remember?
|
||||
|
||||
- Every domain package defines the core type `X`. Only `X` is mandatory.
|
||||
- Add `PostableX` / `GettableX` / `UpdatableX` / `StorableX` one at a time, only when the shape actually diverges from `X`.
|
||||
- Domain logic lives on `X`, not on the flavor types.
|
||||
- Conversions can be a `New<Output>From<Input>` constructor or a receiver-style `ToY()` method — pick whichever reads best at the call site.
|
||||
- Use a type alias when two shapes are truly identical.
|
||||
- `pkg/types/<domain>/` must not import from other `pkg/` packages.
|
||||
|
||||
## Further reading
|
||||
|
||||
- [abstractions.md](abstractions.md) — when to introduce a new type at all.
|
||||
- [handler.md](handler.md) — struct tag rules at the HTTP boundary.
|
||||
- [packages.md](packages.md) — where types live under `pkg/types/`.
|
||||
- [sql.md](sql.md) — star-schema requirements for `StorableX`.
|
||||
@@ -19,11 +19,11 @@ func NewAWSCloudProvider(defStore cloudintegrationtypes.ServiceDefinitionStore)
|
||||
}
|
||||
|
||||
func (provider *awscloudprovider) GetConnectionArtifact(ctx context.Context, account *cloudintegrationtypes.Account, req *cloudintegrationtypes.GetConnectionArtifactRequest) (*cloudintegrationtypes.ConnectionArtifact, error) {
|
||||
baseURL := fmt.Sprintf(cloudintegrationtypes.CloudFormationQuickCreateBaseURL.StringValue(), req.Config.Aws.DeploymentRegion)
|
||||
baseURL := fmt.Sprintf(cloudintegrationtypes.CloudFormationQuickCreateBaseURL.StringValue(), req.Config.AWS.DeploymentRegion)
|
||||
u, _ := url.Parse(baseURL)
|
||||
|
||||
q := u.Query()
|
||||
q.Set("region", req.Config.Aws.DeploymentRegion)
|
||||
q.Set("region", req.Config.AWS.DeploymentRegion)
|
||||
u.Fragment = "/stacks/quickcreate"
|
||||
|
||||
u.RawQuery = q.Encode()
|
||||
@@ -39,9 +39,7 @@ func (provider *awscloudprovider) GetConnectionArtifact(ctx context.Context, acc
|
||||
q.Set("param_IngestionKey", req.Credentials.IngestionKey)
|
||||
|
||||
return &cloudintegrationtypes.ConnectionArtifact{
|
||||
Aws: &cloudintegrationtypes.AWSConnectionArtifact{
|
||||
ConnectionURL: u.String() + "?&" + q.Encode(), // this format is required by AWS
|
||||
},
|
||||
AWS: cloudintegrationtypes.NewAWSConnectionArtifact(u.String() + "?&" + q.Encode()), // this format is required by AWS
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -124,9 +122,6 @@ func (provider *awscloudprovider) BuildIntegrationConfig(
|
||||
}
|
||||
|
||||
return &cloudintegrationtypes.ProviderIntegrationConfig{
|
||||
AWS: &cloudintegrationtypes.AWSIntegrationConfig{
|
||||
EnabledRegions: account.Config.AWS.Regions,
|
||||
TelemetryCollectionStrategy: collectionStrategy,
|
||||
},
|
||||
AWS: cloudintegrationtypes.NewAWSIntegrationConfig(account.Config.AWS.Regions, collectionStrategy),
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -120,7 +120,7 @@ func NewServer(config signoz.Config, signoz *signoz.SigNoz) (*Server, error) {
|
||||
}
|
||||
|
||||
// start the usagemanager
|
||||
usageManager, err := usage.New(signoz.Licensing, signoz.TelemetryStore.ClickhouseDB(), signoz.Zeus, signoz.Modules.OrgGetter)
|
||||
usageManager, err := usage.New(signoz.Licensing, signoz.TelemetryStore.ClickhouseDB(), signoz.Zeus, signoz.Modules.OrgGetter, signoz.Flagger)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -16,9 +16,11 @@ import (
|
||||
|
||||
"github.com/SigNoz/signoz/ee/query-service/model"
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/flagger"
|
||||
"github.com/SigNoz/signoz/pkg/licensing"
|
||||
"github.com/SigNoz/signoz/pkg/modules/organization"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/utils/encryption"
|
||||
"github.com/SigNoz/signoz/pkg/types/featuretypes"
|
||||
"github.com/SigNoz/signoz/pkg/zeus"
|
||||
)
|
||||
|
||||
@@ -43,15 +45,18 @@ type Manager struct {
|
||||
zeus zeus.Zeus
|
||||
|
||||
orgGetter organization.Getter
|
||||
|
||||
flagger flagger.Flagger
|
||||
}
|
||||
|
||||
func New(licenseService licensing.Licensing, clickhouseConn clickhouse.Conn, zeus zeus.Zeus, orgGetter organization.Getter) (*Manager, error) {
|
||||
func New(licenseService licensing.Licensing, clickhouseConn clickhouse.Conn, zeus zeus.Zeus, orgGetter organization.Getter, flagger flagger.Flagger) (*Manager, error) {
|
||||
m := &Manager{
|
||||
clickhouseConn: clickhouseConn,
|
||||
licenseService: licenseService,
|
||||
scheduler: gocron.NewScheduler(time.UTC).Every(1).Day().At("00:00"), // send usage every at 00:00 UTC
|
||||
zeus: zeus,
|
||||
orgGetter: orgGetter,
|
||||
flagger: flagger,
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
@@ -168,7 +173,14 @@ func (lm *Manager) UploadUsage(ctx context.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
errv2 = lm.zeus.PutMeters(ctx, payload.LicenseKey.String(), body)
|
||||
evalCtx := featuretypes.NewFlaggerEvaluationContext(organization.ID)
|
||||
useZeus := lm.flagger.BooleanOrEmpty(ctx, flagger.FeaturePutMetersInZeus, evalCtx)
|
||||
|
||||
if useZeus {
|
||||
errv2 = lm.zeus.PutMetersV2(ctx, payload.LicenseKey.String(), body)
|
||||
} else {
|
||||
errv2 = lm.zeus.PutMeters(ctx, payload.LicenseKey.String(), body)
|
||||
}
|
||||
if errv2 != nil {
|
||||
slog.ErrorContext(ctx, "failed to upload usage", errors.Attr(errv2))
|
||||
// not returning error here since it is captured in the failed count
|
||||
|
||||
@@ -136,6 +136,18 @@ func (provider *Provider) PutMeters(ctx context.Context, key string, data []byte
|
||||
return err
|
||||
}
|
||||
|
||||
func (provider *Provider) PutMetersV2(ctx context.Context, key string, data []byte) error {
|
||||
_, err := provider.do(
|
||||
ctx,
|
||||
provider.config.URL.JoinPath("/v1/meters"),
|
||||
http.MethodPost,
|
||||
key,
|
||||
data,
|
||||
)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (provider *Provider) PutProfile(ctx context.Context, key string, profile *zeustypes.PostableProfile) error {
|
||||
body, err := json.Marshal(profile)
|
||||
if err != nil {
|
||||
|
||||
@@ -62,7 +62,8 @@
|
||||
"@signozhq/popover": "0.1.2",
|
||||
"@signozhq/radio-group": "0.0.4",
|
||||
"@signozhq/resizable": "0.0.2",
|
||||
"@signozhq/table": "0.3.7",
|
||||
"@signozhq/tabs": "0.0.11",
|
||||
"@signozhq/table": "0.3.8",
|
||||
"@signozhq/toggle-group": "0.0.3",
|
||||
"@signozhq/ui": "0.0.5",
|
||||
"@tanstack/react-table": "8.21.3",
|
||||
|
||||
@@ -244,12 +244,18 @@ export const ShortcutsPage = Loadable(
|
||||
() => import(/* webpackChunkName: "ShortcutsPage" */ 'pages/Settings'),
|
||||
);
|
||||
|
||||
export const InstalledIntegrations = Loadable(
|
||||
export const Integrations = Loadable(
|
||||
() =>
|
||||
import(
|
||||
/* webpackChunkName: "InstalledIntegrations" */ 'pages/IntegrationsModulePage'
|
||||
),
|
||||
);
|
||||
export const IntegrationsDetailsPage = Loadable(
|
||||
() =>
|
||||
import(
|
||||
/* webpackChunkName: "IntegrationsDetailsPage" */ 'pages/IntegrationsDetailsPage'
|
||||
),
|
||||
);
|
||||
|
||||
export const MessagingQueuesMainPage = Loadable(
|
||||
() =>
|
||||
|
||||
@@ -18,7 +18,8 @@ import {
|
||||
ForgotPassword,
|
||||
Home,
|
||||
InfrastructureMonitoring,
|
||||
InstalledIntegrations,
|
||||
Integrations,
|
||||
IntegrationsDetailsPage,
|
||||
LicensePage,
|
||||
ListAllALertsPage,
|
||||
LiveLogs,
|
||||
@@ -389,10 +390,17 @@ const routes: AppRoutes[] = [
|
||||
isPrivate: true,
|
||||
key: 'WORKSPACE_ACCESS_RESTRICTED',
|
||||
},
|
||||
{
|
||||
path: ROUTES.INTEGRATIONS_DETAIL,
|
||||
exact: true,
|
||||
component: IntegrationsDetailsPage,
|
||||
isPrivate: true,
|
||||
key: 'INTEGRATIONS_DETAIL',
|
||||
},
|
||||
{
|
||||
path: ROUTES.INTEGRATIONS,
|
||||
exact: true,
|
||||
component: InstalledIntegrations,
|
||||
component: Integrations,
|
||||
isPrivate: true,
|
||||
key: 'INTEGRATIONS',
|
||||
},
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
import axios from 'api';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
|
||||
const removeAwsIntegrationAccount = async (
|
||||
accountId: string,
|
||||
): Promise<SuccessResponse<Record<string, never>> | ErrorResponse> => {
|
||||
const response = await axios.post(
|
||||
`/cloud-integrations/aws/accounts/${accountId}/disconnect`,
|
||||
);
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
error: null,
|
||||
message: response.data.status,
|
||||
payload: response.data.data,
|
||||
};
|
||||
};
|
||||
|
||||
export default removeAwsIntegrationAccount;
|
||||
@@ -4659,87 +4659,6 @@ export interface RuletypesEvaluationRollingDTO {
|
||||
spec?: RuletypesRollingWindowDTO;
|
||||
}
|
||||
|
||||
export type RuletypesGettableRuleDTOAnnotations = { [key: string]: string };
|
||||
|
||||
export type RuletypesGettableRuleDTOLabels = { [key: string]: string };
|
||||
|
||||
export interface RuletypesGettableRuleDTO {
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
alert: string;
|
||||
alertType?: RuletypesAlertTypeDTO;
|
||||
/**
|
||||
* @type object
|
||||
*/
|
||||
annotations?: RuletypesGettableRuleDTOAnnotations;
|
||||
condition: RuletypesRuleConditionDTO;
|
||||
/**
|
||||
* @type string
|
||||
* @format date-time
|
||||
*/
|
||||
createAt: Date;
|
||||
/**
|
||||
* @type string
|
||||
* @nullable true
|
||||
*/
|
||||
createBy?: string | null;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
description?: string;
|
||||
/**
|
||||
* @type boolean
|
||||
*/
|
||||
disabled?: boolean;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
evalWindow?: string;
|
||||
evaluation?: RuletypesEvaluationEnvelopeDTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
frequency?: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
id: string;
|
||||
/**
|
||||
* @type object
|
||||
*/
|
||||
labels?: RuletypesGettableRuleDTOLabels;
|
||||
notificationSettings?: RuletypesNotificationSettingsDTO;
|
||||
/**
|
||||
* @type array
|
||||
*/
|
||||
preferredChannels?: string[];
|
||||
ruleType: RuletypesRuleTypeDTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
schemaVersion?: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
source?: string;
|
||||
state: RuletypesAlertStateDTO;
|
||||
/**
|
||||
* @type string
|
||||
* @format date-time
|
||||
*/
|
||||
updateAt: Date;
|
||||
/**
|
||||
* @type string
|
||||
* @nullable true
|
||||
*/
|
||||
updateBy?: string | null;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
version?: string;
|
||||
}
|
||||
|
||||
export interface RuletypesGettableTestRuleDTO {
|
||||
/**
|
||||
* @type integer
|
||||
@@ -4971,6 +4890,85 @@ export interface RuletypesRollingWindowDTO {
|
||||
frequency: string;
|
||||
}
|
||||
|
||||
export type RuletypesRuleDTOAnnotations = { [key: string]: string };
|
||||
|
||||
export type RuletypesRuleDTOLabels = { [key: string]: string };
|
||||
|
||||
export interface RuletypesRuleDTO {
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
alert: string;
|
||||
alertType?: RuletypesAlertTypeDTO;
|
||||
/**
|
||||
* @type object
|
||||
*/
|
||||
annotations?: RuletypesRuleDTOAnnotations;
|
||||
condition: RuletypesRuleConditionDTO;
|
||||
/**
|
||||
* @type string
|
||||
* @format date-time
|
||||
*/
|
||||
createdAt?: Date;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
createdBy?: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
description?: string;
|
||||
/**
|
||||
* @type boolean
|
||||
*/
|
||||
disabled?: boolean;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
evalWindow?: string;
|
||||
evaluation?: RuletypesEvaluationEnvelopeDTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
frequency?: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
id: string;
|
||||
/**
|
||||
* @type object
|
||||
*/
|
||||
labels?: RuletypesRuleDTOLabels;
|
||||
notificationSettings?: RuletypesNotificationSettingsDTO;
|
||||
/**
|
||||
* @type array
|
||||
*/
|
||||
preferredChannels?: string[];
|
||||
ruleType: RuletypesRuleTypeDTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
schemaVersion?: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
source?: string;
|
||||
state: RuletypesAlertStateDTO;
|
||||
/**
|
||||
* @type string
|
||||
* @format date-time
|
||||
*/
|
||||
updatedAt?: Date;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
updatedBy?: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
version?: string;
|
||||
}
|
||||
|
||||
export interface RuletypesRuleConditionDTO {
|
||||
/**
|
||||
* @type integer
|
||||
@@ -6831,7 +6829,7 @@ export type ListRules200 = {
|
||||
/**
|
||||
* @type array
|
||||
*/
|
||||
data: RuletypesGettableRuleDTO[];
|
||||
data: RuletypesRuleDTO[];
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
@@ -6839,7 +6837,7 @@ export type ListRules200 = {
|
||||
};
|
||||
|
||||
export type CreateRule201 = {
|
||||
data: RuletypesGettableRuleDTO;
|
||||
data: RuletypesRuleDTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
@@ -6853,7 +6851,7 @@ export type GetRuleByIDPathParameters = {
|
||||
id: string;
|
||||
};
|
||||
export type GetRuleByID200 = {
|
||||
data: RuletypesGettableRuleDTO;
|
||||
data: RuletypesRuleDTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
@@ -6864,7 +6862,7 @@ export type PatchRuleByIDPathParameters = {
|
||||
id: string;
|
||||
};
|
||||
export type PatchRuleByID200 = {
|
||||
data: RuletypesGettableRuleDTO;
|
||||
data: RuletypesRuleDTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
|
||||
@@ -1,88 +0,0 @@
|
||||
import axios from 'api';
|
||||
import {
|
||||
CloudAccount,
|
||||
Service,
|
||||
ServiceData,
|
||||
UpdateServiceConfigPayload,
|
||||
UpdateServiceConfigResponse,
|
||||
} from 'container/CloudIntegrationPage/ServicesSection/types';
|
||||
import {
|
||||
AccountConfigPayload,
|
||||
AccountConfigResponse,
|
||||
ConnectionParams,
|
||||
ConnectionUrlResponse,
|
||||
} from 'types/api/integrations/aws';
|
||||
|
||||
export const getAwsAccounts = async (): Promise<CloudAccount[]> => {
|
||||
const response = await axios.get('/cloud-integrations/aws/accounts');
|
||||
|
||||
return response.data.data.accounts;
|
||||
};
|
||||
|
||||
export const getAwsServices = async (
|
||||
cloudAccountId?: string,
|
||||
): Promise<Service[]> => {
|
||||
const params = cloudAccountId
|
||||
? { cloud_account_id: cloudAccountId }
|
||||
: undefined;
|
||||
const response = await axios.get('/cloud-integrations/aws/services', {
|
||||
params,
|
||||
});
|
||||
|
||||
return response.data.data.services;
|
||||
};
|
||||
|
||||
export const getServiceDetails = async (
|
||||
serviceId: string,
|
||||
cloudAccountId?: string,
|
||||
): Promise<ServiceData> => {
|
||||
const params = cloudAccountId
|
||||
? { cloud_account_id: cloudAccountId }
|
||||
: undefined;
|
||||
const response = await axios.get(
|
||||
`/cloud-integrations/aws/services/${serviceId}`,
|
||||
{ params },
|
||||
);
|
||||
return response.data.data;
|
||||
};
|
||||
|
||||
export const generateConnectionUrl = async (params: {
|
||||
agent_config: { region: string };
|
||||
account_config: { regions: string[] };
|
||||
account_id?: string;
|
||||
}): Promise<ConnectionUrlResponse> => {
|
||||
const response = await axios.post(
|
||||
'/cloud-integrations/aws/accounts/generate-connection-url',
|
||||
params,
|
||||
);
|
||||
return response.data.data;
|
||||
};
|
||||
|
||||
export const updateAccountConfig = async (
|
||||
accountId: string,
|
||||
payload: AccountConfigPayload,
|
||||
): Promise<AccountConfigResponse> => {
|
||||
const response = await axios.post<AccountConfigResponse>(
|
||||
`/cloud-integrations/aws/accounts/${accountId}/config`,
|
||||
payload,
|
||||
);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export const updateServiceConfig = async (
|
||||
serviceId: string,
|
||||
payload: UpdateServiceConfigPayload,
|
||||
): Promise<UpdateServiceConfigResponse> => {
|
||||
const response = await axios.post<UpdateServiceConfigResponse>(
|
||||
`/cloud-integrations/aws/services/${serviceId}/config`,
|
||||
payload,
|
||||
);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export const getConnectionParams = async (): Promise<ConnectionParams> => {
|
||||
const response = await axios.get(
|
||||
'/cloud-integrations/aws/accounts/generate-connection-params',
|
||||
);
|
||||
return response.data.data;
|
||||
};
|
||||
9
frontend/src/assets/svgs/dotted-double-line.svg
Normal file
9
frontend/src/assets/svgs/dotted-double-line.svg
Normal file
@@ -0,0 +1,9 @@
|
||||
<svg width="929" height="8" viewBox="0 0 929 8" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<pattern id="dotted-double-line-pattern" x="0" y="0" width="6" height="8" patternUnits="userSpaceOnUse">
|
||||
<rect width="2" height="2" rx="1" fill="#242834" />
|
||||
<rect y="6" width="2" height="2" rx="1" fill="#242834" />
|
||||
</pattern>
|
||||
</defs>
|
||||
<rect width="929" height="8" fill="url(#dotted-double-line-pattern)" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 442 B |
1
frontend/src/auto-import-registry.d.ts
vendored
1
frontend/src/auto-import-registry.d.ts
vendored
@@ -24,6 +24,7 @@ import '@signozhq/input';
|
||||
import '@signozhq/popover';
|
||||
import '@signozhq/radio-group';
|
||||
import '@signozhq/resizable';
|
||||
import '@signozhq/tabs';
|
||||
import '@signozhq/table';
|
||||
import '@signozhq/toggle-group';
|
||||
import '@signozhq/ui';
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
.cloud-service-data-collected {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
|
||||
.cloud-service-data-collected-table {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
|
||||
.cloud-service-data-collected-table-heading {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
color: var(--l2-foreground);
|
||||
|
||||
/* Bifrost (Ancient)/Content/sm */
|
||||
font-family: Inter;
|
||||
font-size: 13px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 20px; /* 142.857% */
|
||||
letter-spacing: -0.07px;
|
||||
}
|
||||
|
||||
.cloud-service-data-collected-table-logs {
|
||||
border-radius: 6px;
|
||||
border: 1px solid var(--l3-background);
|
||||
background: var(--l1-background);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,18 @@
|
||||
import { Table } from 'antd';
|
||||
import {
|
||||
CloudintegrationtypesCollectedLogAttributeDTO,
|
||||
CloudintegrationtypesCollectedMetricDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
import { BarChart2, ScrollText } from 'lucide-react';
|
||||
|
||||
import { ServiceData } from './types';
|
||||
import './CloudServiceDataCollected.styles.scss';
|
||||
|
||||
function CloudServiceDataCollected({
|
||||
logsData,
|
||||
metricsData,
|
||||
}: {
|
||||
logsData: ServiceData['data_collected']['logs'];
|
||||
metricsData: ServiceData['data_collected']['metrics'];
|
||||
logsData: CloudintegrationtypesCollectedLogAttributeDTO[] | null | undefined;
|
||||
metricsData: CloudintegrationtypesCollectedMetricDTO[] | null | undefined;
|
||||
}): JSX.Element {
|
||||
const logsColumns = [
|
||||
{
|
||||
@@ -61,24 +66,30 @@ function CloudServiceDataCollected({
|
||||
return (
|
||||
<div className="cloud-service-data-collected">
|
||||
{logsData && logsData.length > 0 && (
|
||||
<div className="cloud-service-data-collected__table">
|
||||
<div className="cloud-service-data-collected__table-heading">Logs</div>
|
||||
<div className="cloud-service-data-collected-table">
|
||||
<div className="cloud-service-data-collected-table-heading">
|
||||
<ScrollText size={14} />
|
||||
Logs
|
||||
</div>
|
||||
<Table
|
||||
columns={logsColumns}
|
||||
dataSource={logsData}
|
||||
{...tableProps}
|
||||
className="cloud-service-data-collected__table-logs"
|
||||
className="cloud-service-data-collected-table-logs"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{metricsData && metricsData.length > 0 && (
|
||||
<div className="cloud-service-data-collected__table">
|
||||
<div className="cloud-service-data-collected__table-heading">Metrics</div>
|
||||
<div className="cloud-service-data-collected-table">
|
||||
<div className="cloud-service-data-collected-table-heading">
|
||||
<BarChart2 size={14} />
|
||||
Metrics
|
||||
</div>
|
||||
<Table
|
||||
columns={metricsColumns}
|
||||
dataSource={metricsData}
|
||||
{...tableProps}
|
||||
className="cloud-service-data-collected__table-metrics"
|
||||
className="cloud-service-data-collected-table-metrics"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
@@ -3,8 +3,8 @@ import { useLocation } from 'react-router-dom';
|
||||
import { toast } from '@signozhq/ui';
|
||||
import { Button, Input, Radio, RadioChangeEvent, Typography } from 'antd';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { handleContactSupport } from 'container/Integrations/utils';
|
||||
import { useGetTenantLicense } from 'hooks/useGetTenantLicense';
|
||||
import { handleContactSupport } from 'pages/Integrations/utils';
|
||||
|
||||
function FeedbackModal({ onClose }: { onClose: () => void }): JSX.Element {
|
||||
const [activeTab, setActiveTab] = useState('feedback');
|
||||
|
||||
@@ -4,8 +4,8 @@ import { toast } from '@signozhq/ui';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { handleContactSupport } from 'container/Integrations/utils';
|
||||
import { useGetTenantLicense } from 'hooks/useGetTenantLicense';
|
||||
import { handleContactSupport } from 'pages/Integrations/utils';
|
||||
|
||||
import FeedbackModal from '../FeedbackModal';
|
||||
|
||||
@@ -31,7 +31,7 @@ jest.mock('hooks/useGetTenantLicense', () => ({
|
||||
useGetTenantLicense: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('pages/Integrations/utils', () => ({
|
||||
jest.mock('container/Integrations/utils', () => ({
|
||||
handleContactSupport: jest.fn(),
|
||||
}));
|
||||
|
||||
|
||||
@@ -100,16 +100,19 @@ function MarkdownRenderer({
|
||||
variables,
|
||||
trackCopyAction,
|
||||
elementDetails,
|
||||
className,
|
||||
}: {
|
||||
markdownContent: any;
|
||||
variables: any;
|
||||
trackCopyAction?: boolean;
|
||||
elementDetails?: Record<string, unknown>;
|
||||
className?: string;
|
||||
}): JSX.Element {
|
||||
const interpolatedMarkdown = interpolateMarkdown(markdownContent, variables);
|
||||
|
||||
return (
|
||||
<ReactMarkdown
|
||||
className={className}
|
||||
rehypePlugins={[rehypeRaw as any]}
|
||||
components={{
|
||||
// @ts-ignore
|
||||
|
||||
@@ -7,6 +7,14 @@
|
||||
[data-slot='dialog-content'] {
|
||||
position: fixed;
|
||||
z-index: 60;
|
||||
background: var(--l1-background);
|
||||
color: var(--l1-foreground);
|
||||
|
||||
/* Override the background and color of the dialog content from the theme */
|
||||
> div {
|
||||
background: var(--l1-background);
|
||||
color: var(--l1-foreground);
|
||||
}
|
||||
}
|
||||
|
||||
.cmdk-section-heading [cmdk-group-heading] {
|
||||
@@ -43,6 +51,22 @@
|
||||
|
||||
.cmdk-item {
|
||||
cursor: pointer;
|
||||
color: var(--l1-foreground);
|
||||
|
||||
font-family: Inter;
|
||||
font-size: 13px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 18px;
|
||||
|
||||
&:hover {
|
||||
background: var(--l1-background-hover);
|
||||
}
|
||||
|
||||
&[data-selected='true'] {
|
||||
background: var(--l3-background);
|
||||
color: var(--l1-foreground);
|
||||
}
|
||||
}
|
||||
|
||||
[cmdk-item] svg {
|
||||
|
||||
@@ -65,6 +65,7 @@ const ROUTES = {
|
||||
WORKSPACE_SUSPENDED: '/workspace-suspended',
|
||||
SHORTCUTS: '/settings/shortcuts',
|
||||
INTEGRATIONS: '/integrations',
|
||||
INTEGRATIONS_DETAIL: '/integrations/:integrationId',
|
||||
MESSAGING_QUEUES_BASE: '/messaging-queues',
|
||||
MESSAGING_QUEUES_KAFKA: '/messaging-queues/kafka',
|
||||
MESSAGING_QUEUES_KAFKA_DETAIL: '/messaging-queues/kafka/detail',
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
import {
|
||||
IntegrationType,
|
||||
RequestIntegrationBtn,
|
||||
} from 'pages/Integrations/RequestIntegrationBtn';
|
||||
|
||||
import Header from './Header/Header';
|
||||
import HeroSection from './HeroSection/HeroSection';
|
||||
import ServicesTabs from './ServicesSection/ServicesTabs';
|
||||
|
||||
function CloudIntegrationPage(): JSX.Element {
|
||||
return (
|
||||
<div>
|
||||
<Header />
|
||||
<HeroSection />
|
||||
<RequestIntegrationBtn
|
||||
type={IntegrationType.AWS_SERVICES}
|
||||
message="Can't find the AWS service you're looking for? Request more integrations"
|
||||
/>
|
||||
<ServicesTabs />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default CloudIntegrationPage;
|
||||
@@ -1,37 +0,0 @@
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
|
||||
import integrationsHeroBgUrl from '@/assets/Images/integrations-hero-bg.png';
|
||||
import awsDarkUrl from '@/assets/Logos/aws-dark.svg';
|
||||
|
||||
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('${integrationsHeroBgUrl}')`,
|
||||
}
|
||||
: {}
|
||||
}
|
||||
>
|
||||
<div className="hero-section__icon">
|
||||
<img src={awsDarkUrl} 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;
|
||||
@@ -1,213 +0,0 @@
|
||||
import { Dispatch, SetStateAction, useCallback } from 'react';
|
||||
import { useQueryClient } from 'react-query';
|
||||
import { Form, Select, Switch } from 'antd';
|
||||
import SignozModal from 'components/SignozModal/SignozModal';
|
||||
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||
import {
|
||||
getRegionPreviewText,
|
||||
useAccountSettingsModal,
|
||||
} from 'hooks/integration/aws/useAccountSettingsModal';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
import history from 'lib/history';
|
||||
|
||||
import logEvent from '../../../../api/common/logEvent';
|
||||
import { CloudAccount } from '../../ServicesSection/types';
|
||||
import { RegionSelector } from './RegionSelector';
|
||||
import RemoveIntegrationAccount from './RemoveIntegrationAccount';
|
||||
|
||||
import './AccountSettingsModal.style.scss';
|
||||
|
||||
interface AccountSettingsModalProps {
|
||||
onClose: () => void;
|
||||
account: CloudAccount;
|
||||
setActiveAccount: Dispatch<SetStateAction<CloudAccount | null>>;
|
||||
}
|
||||
|
||||
function AccountSettingsModal({
|
||||
onClose,
|
||||
account,
|
||||
setActiveAccount,
|
||||
}: AccountSettingsModalProps): JSX.Element {
|
||||
const {
|
||||
form,
|
||||
isLoading,
|
||||
selectedRegions,
|
||||
includeAllRegions,
|
||||
isRegionSelectOpen,
|
||||
isSaveDisabled,
|
||||
setSelectedRegions,
|
||||
setIncludeAllRegions,
|
||||
setIsRegionSelectOpen,
|
||||
handleIncludeAllRegionsChange,
|
||||
handleSubmit,
|
||||
handleClose,
|
||||
} = useAccountSettingsModal({ onClose, account, setActiveAccount });
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
const urlQuery = useUrlQuery();
|
||||
|
||||
const handleRemoveIntegrationAccountSuccess = (): void => {
|
||||
queryClient.invalidateQueries([REACT_QUERY_KEY.AWS_ACCOUNTS]);
|
||||
urlQuery.delete('cloudAccountId');
|
||||
handleClose();
|
||||
history.replace({ search: urlQuery.toString() });
|
||||
|
||||
logEvent('AWS Integration: Account removed', {
|
||||
id: account?.id,
|
||||
cloudAccountId: account?.cloud_account_id,
|
||||
});
|
||||
};
|
||||
|
||||
const handleRegionDeselect = useCallback(
|
||||
(item: string): void => {
|
||||
if (selectedRegions.includes(item)) {
|
||||
setSelectedRegions(selectedRegions.filter((region) => region !== item));
|
||||
if (includeAllRegions) {
|
||||
setIncludeAllRegions(false);
|
||||
}
|
||||
}
|
||||
},
|
||||
[
|
||||
selectedRegions,
|
||||
includeAllRegions,
|
||||
setSelectedRegions,
|
||||
setIncludeAllRegions,
|
||||
],
|
||||
);
|
||||
|
||||
const renderRegionSelector = useCallback(() => {
|
||||
if (isRegionSelectOpen) {
|
||||
return (
|
||||
<RegionSelector
|
||||
selectedRegions={selectedRegions}
|
||||
setSelectedRegions={setSelectedRegions}
|
||||
setIncludeAllRegions={setIncludeAllRegions}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="account-settings-modal__body-regions-switch-switch ">
|
||||
<Switch
|
||||
checked={includeAllRegions}
|
||||
onChange={handleIncludeAllRegionsChange}
|
||||
/>
|
||||
<button
|
||||
className="account-settings-modal__body-regions-switch-switch-label"
|
||||
type="button"
|
||||
onClick={(): void => handleIncludeAllRegionsChange(!includeAllRegions)}
|
||||
>
|
||||
Include all regions
|
||||
</button>
|
||||
</div>
|
||||
<Select
|
||||
suffixIcon={null}
|
||||
placeholder="Select Region(s)"
|
||||
className="cloud-account-setup-form__select account-settings-modal__body-regions-select integrations-select"
|
||||
onClick={(): void => setIsRegionSelectOpen(true)}
|
||||
mode="multiple"
|
||||
maxTagCount={3}
|
||||
value={getRegionPreviewText(selectedRegions)}
|
||||
open={false}
|
||||
onDeselect={handleRegionDeselect}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}, [
|
||||
isRegionSelectOpen,
|
||||
includeAllRegions,
|
||||
handleIncludeAllRegionsChange,
|
||||
selectedRegions,
|
||||
handleRegionDeselect,
|
||||
setSelectedRegions,
|
||||
setIncludeAllRegions,
|
||||
setIsRegionSelectOpen,
|
||||
]);
|
||||
|
||||
const renderAccountDetails = useCallback(
|
||||
() => (
|
||||
<div className="account-settings-modal__body-account-info">
|
||||
<div className="account-settings-modal__body-account-info-connected-account-details">
|
||||
<div className="account-settings-modal__body-account-info-connected-account-details-title">
|
||||
Connected Account details
|
||||
</div>
|
||||
<div className="account-settings-modal__body-account-info-connected-account-details-account-id">
|
||||
AWS Account:{' '}
|
||||
<span className="account-settings-modal__body-account-info-connected-account-details-account-id-account-id">
|
||||
{account?.id}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
[account?.id],
|
||||
);
|
||||
|
||||
const modalTitle = (
|
||||
<div className="account-settings-modal__title">
|
||||
Account settings for{' '}
|
||||
<span className="account-settings-modal__title-account-id">
|
||||
{account?.id}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<SignozModal
|
||||
open
|
||||
title={modalTitle}
|
||||
onCancel={handleClose}
|
||||
onOk={handleSubmit}
|
||||
okText="Save"
|
||||
okButtonProps={{
|
||||
disabled: isSaveDisabled,
|
||||
className: 'account-settings-modal__footer-save-button',
|
||||
loading: isLoading,
|
||||
}}
|
||||
cancelButtonProps={{
|
||||
className: 'account-settings-modal__footer-close-button',
|
||||
}}
|
||||
width={672}
|
||||
rootClassName="account-settings-modal"
|
||||
>
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
initialValues={{
|
||||
selectedRegions,
|
||||
includeAllRegions,
|
||||
}}
|
||||
>
|
||||
<div className="account-settings-modal__body">
|
||||
{renderAccountDetails()}
|
||||
|
||||
<Form.Item
|
||||
name="selectedRegions"
|
||||
rules={[
|
||||
{
|
||||
validator: async (): Promise<void> => {
|
||||
if (selectedRegions.length === 0) {
|
||||
throw new Error('Please select at least one region to monitor');
|
||||
}
|
||||
},
|
||||
message: 'Please select at least one region to monitor',
|
||||
},
|
||||
]}
|
||||
>
|
||||
{renderRegionSelector()}
|
||||
</Form.Item>
|
||||
|
||||
<div className="integration-detail-content">
|
||||
<RemoveIntegrationAccount
|
||||
accountId={account?.id}
|
||||
onRemoveIntegrationAccountSuccess={handleRemoveIntegrationAccountSuccess}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Form>
|
||||
</SignozModal>
|
||||
);
|
||||
}
|
||||
|
||||
export default AccountSettingsModal;
|
||||
@@ -1,109 +0,0 @@
|
||||
import { useRef } from 'react';
|
||||
import { Form } from 'antd';
|
||||
import cx from 'classnames';
|
||||
import { useAccountStatus } from 'hooks/integration/aws/useAccountStatus';
|
||||
import { AccountStatusResponse } from 'types/api/integrations/aws';
|
||||
import { regions } from 'utils/regions';
|
||||
|
||||
import logEvent from '../../../../api/common/logEvent';
|
||||
import { ModalStateEnum, RegionFormProps } from '../types';
|
||||
import AlertMessage from './AlertMessage';
|
||||
import {
|
||||
ComplianceNote,
|
||||
MonitoringRegionsSection,
|
||||
RegionDeploymentSection,
|
||||
} from './IntegrateNowFormSections';
|
||||
import RenderConnectionFields from './RenderConnectionParams';
|
||||
|
||||
const allRegions = (): string[] =>
|
||||
regions.flatMap((r) => r.subRegions.map((sr) => sr.name));
|
||||
|
||||
const getRegionPreviewText = (regions: string[]): string[] => {
|
||||
if (regions.includes('all')) {
|
||||
return allRegions();
|
||||
}
|
||||
return regions;
|
||||
};
|
||||
|
||||
export function RegionForm({
|
||||
form,
|
||||
modalState,
|
||||
setModalState,
|
||||
selectedRegions,
|
||||
includeAllRegions,
|
||||
onIncludeAllRegionsChange,
|
||||
onRegionSelect,
|
||||
onSubmit,
|
||||
accountId,
|
||||
selectedDeploymentRegion,
|
||||
handleRegionChange,
|
||||
connectionParams,
|
||||
isConnectionParamsLoading,
|
||||
}: RegionFormProps): JSX.Element {
|
||||
const startTimeRef = useRef(Date.now());
|
||||
const refetchInterval = 10 * 1000;
|
||||
const errorTimeout = 10 * 60 * 1000;
|
||||
|
||||
const { isLoading: isAccountStatusLoading } = useAccountStatus(accountId, {
|
||||
refetchInterval,
|
||||
enabled: !!accountId && modalState === ModalStateEnum.WAITING,
|
||||
onSuccess: (data: AccountStatusResponse) => {
|
||||
if (data.data.status.integration.last_heartbeat_ts_ms !== null) {
|
||||
setModalState(ModalStateEnum.SUCCESS);
|
||||
logEvent('AWS Integration: Account connected', {
|
||||
cloudAccountId: data?.data?.cloud_account_id,
|
||||
status: data?.data?.status,
|
||||
});
|
||||
} else if (Date.now() - startTimeRef.current >= errorTimeout) {
|
||||
setModalState(ModalStateEnum.ERROR);
|
||||
logEvent('AWS Integration: Account connection attempt timed out', {
|
||||
id: accountId,
|
||||
});
|
||||
}
|
||||
},
|
||||
onError: () => {
|
||||
setModalState(ModalStateEnum.ERROR);
|
||||
},
|
||||
});
|
||||
|
||||
const isFormDisabled =
|
||||
modalState === ModalStateEnum.WAITING || isAccountStatusLoading;
|
||||
|
||||
return (
|
||||
<Form
|
||||
form={form}
|
||||
className="cloud-account-setup-form"
|
||||
layout="vertical"
|
||||
onFinish={onSubmit}
|
||||
>
|
||||
<AlertMessage modalState={modalState} />
|
||||
|
||||
<div
|
||||
className={cx(`cloud-account-setup-form__content`, {
|
||||
disabled: isFormDisabled,
|
||||
})}
|
||||
>
|
||||
<RegionDeploymentSection
|
||||
regions={regions}
|
||||
handleRegionChange={handleRegionChange}
|
||||
isFormDisabled={isFormDisabled}
|
||||
selectedDeploymentRegion={selectedDeploymentRegion}
|
||||
/>
|
||||
<MonitoringRegionsSection
|
||||
includeAllRegions={includeAllRegions}
|
||||
selectedRegions={selectedRegions}
|
||||
onIncludeAllRegionsChange={onIncludeAllRegionsChange}
|
||||
getRegionPreviewText={getRegionPreviewText}
|
||||
onRegionSelect={onRegionSelect}
|
||||
isFormDisabled={isFormDisabled}
|
||||
/>
|
||||
<ComplianceNote />
|
||||
<RenderConnectionFields
|
||||
isConnectionParamsLoading={isConnectionParamsLoading}
|
||||
connectionParams={connectionParams}
|
||||
isFormDisabled={isFormDisabled}
|
||||
/>
|
||||
</div>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
.remove-integration-account {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 16px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid color-mix(in srgb, var(--bg-sakura-500) 20%, transparent);
|
||||
background: color-mix(in srgb, var(--bg-sakura-500) 6%, transparent);
|
||||
&__header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
&__title {
|
||||
color: var(--bg-cherry-500);
|
||||
font-size: 14px;
|
||||
letter-spacing: -0.07px;
|
||||
}
|
||||
|
||||
&__subtitle {
|
||||
color: var(--bg-cherry-300);
|
||||
font-size: 14px;
|
||||
line-height: 22px;
|
||||
letter-spacing: -0.07px;
|
||||
}
|
||||
|
||||
&__button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: var(--bg-cherry-500);
|
||||
border: none;
|
||||
color: var(--l1-foreground);
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
line-height: 13.3px; /* 110.833% */
|
||||
padding: 9px 13px;
|
||||
.ant-btn-icon {
|
||||
margin-inline-end: 4px !important;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
&.ant-btn-default {
|
||||
color: var(--l2-foreground) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,94 +0,0 @@
|
||||
import { useState } from 'react';
|
||||
import { useMutation } from 'react-query';
|
||||
import { Button, Modal } from 'antd';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import removeAwsIntegrationAccount from 'api/Integrations/removeAwsIntegrationAccount';
|
||||
import { SOMETHING_WENT_WRONG } from 'constants/api';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import { X } from 'lucide-react';
|
||||
import { INTEGRATION_TELEMETRY_EVENTS } from 'pages/Integrations/utils';
|
||||
|
||||
import './RemoveIntegrationAccount.scss';
|
||||
|
||||
function RemoveIntegrationAccount({
|
||||
accountId,
|
||||
onRemoveIntegrationAccountSuccess,
|
||||
}: {
|
||||
accountId: string;
|
||||
onRemoveIntegrationAccountSuccess: () => void;
|
||||
}): JSX.Element {
|
||||
const { notifications } = useNotifications();
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
|
||||
const showModal = (): void => {
|
||||
setIsModalOpen(true);
|
||||
};
|
||||
|
||||
const {
|
||||
mutate: removeIntegration,
|
||||
isLoading: isRemoveIntegrationLoading,
|
||||
} = useMutation(removeAwsIntegrationAccount, {
|
||||
onSuccess: () => {
|
||||
onRemoveIntegrationAccountSuccess?.();
|
||||
setIsModalOpen(false);
|
||||
},
|
||||
onError: () => {
|
||||
notifications.error({
|
||||
message: SOMETHING_WENT_WRONG,
|
||||
});
|
||||
},
|
||||
});
|
||||
const handleOk = (): void => {
|
||||
logEvent(INTEGRATION_TELEMETRY_EVENTS.AWS_INTEGRATION_ACCOUNT_REMOVED, {
|
||||
accountId,
|
||||
});
|
||||
removeIntegration(accountId);
|
||||
};
|
||||
|
||||
const handleCancel = (): void => {
|
||||
setIsModalOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="remove-integration-account">
|
||||
<div className="remove-integration-account__header">
|
||||
<div className="remove-integration-account__title">Remove Integration</div>
|
||||
<div className="remove-integration-account__subtitle">
|
||||
Removing this integration won't delete any existing data but will stop
|
||||
collecting new data from AWS.
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
className="remove-integration-account__button"
|
||||
icon={<X size={14} />}
|
||||
onClick={(): void => showModal()}
|
||||
>
|
||||
Remove
|
||||
</Button>
|
||||
<Modal
|
||||
className="remove-integration-modal"
|
||||
open={isModalOpen}
|
||||
title="Remove integration"
|
||||
onOk={handleOk}
|
||||
onCancel={handleCancel}
|
||||
okText="Remove Integration"
|
||||
okButtonProps={{
|
||||
danger: true,
|
||||
disabled: isRemoveIntegrationLoading,
|
||||
}}
|
||||
>
|
||||
<div className="remove-integration-modal__text">
|
||||
Removing this account will remove all components created for sending
|
||||
telemetry to SigNoz in your AWS account within the next ~15 minutes
|
||||
(cloudformation stacks named signoz-integration-telemetry-collection in
|
||||
enabled regions). <br />
|
||||
<br />
|
||||
After that, you can delete the cloudformation stack that was created
|
||||
manually when connecting this account.
|
||||
</div>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default RemoveIntegrationAccount;
|
||||
@@ -1,162 +0,0 @@
|
||||
.cloud-account-setup-success-view {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 40px;
|
||||
text-align: center;
|
||||
padding-top: 34px;
|
||||
p,
|
||||
h3,
|
||||
h4 {
|
||||
margin: 0;
|
||||
}
|
||||
&__content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 14px;
|
||||
.cloud-account-setup-success-view {
|
||||
&__title {
|
||||
h3 {
|
||||
color: var(--l1-foreground);
|
||||
font-size: 20px;
|
||||
font-weight: 500;
|
||||
line-height: 32px;
|
||||
}
|
||||
}
|
||||
&__description {
|
||||
color: var(--l2-foreground);
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
line-height: 20px;
|
||||
letter-spacing: -0.07px;
|
||||
}
|
||||
}
|
||||
}
|
||||
&__what-next {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 18px;
|
||||
text-align: left;
|
||||
&-title {
|
||||
color: var(--muted-foreground);
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
line-height: 18px;
|
||||
letter-spacing: 0.88px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.what-next-items-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
&__item {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: baseline;
|
||||
|
||||
&.ant-alert {
|
||||
padding: 14px;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
line-height: 20px; /* 142.857% */
|
||||
letter-spacing: -0.21px;
|
||||
}
|
||||
|
||||
&.ant-alert-info {
|
||||
border: 1px solid color-mix(in srgb, var(--bg-robin-600) 50%, transparent);
|
||||
background: color-mix(in srgb, var(--primary-background) 20%, transparent);
|
||||
color: var(--primary-foreground);
|
||||
}
|
||||
|
||||
.what-next-item {
|
||||
color: var(--bg-robin-400);
|
||||
&-bullet-icon {
|
||||
font-size: 20px;
|
||||
line-height: 20px;
|
||||
}
|
||||
&-text {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
line-height: 20px;
|
||||
letter-spacing: -0.21px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
&__footer {
|
||||
padding-top: 18px;
|
||||
.ant-btn {
|
||||
background: var(--primary-background);
|
||||
color: var(--primary-foreground);
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
line-height: 20px;
|
||||
height: 36px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.lottie-container {
|
||||
position: absolute;
|
||||
width: 743.5px;
|
||||
height: 990.342px;
|
||||
top: -100px;
|
||||
left: -36px;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.lightMode {
|
||||
.cloud-account-setup-success-view {
|
||||
&__content {
|
||||
.cloud-account-setup-success-view {
|
||||
&__title {
|
||||
h3 {
|
||||
color: var(--l1-foreground);
|
||||
}
|
||||
}
|
||||
|
||||
&__description {
|
||||
color: var(--l1-foreground);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__what-next {
|
||||
&-title {
|
||||
color: var(--l1-foreground);
|
||||
}
|
||||
|
||||
.what-next-items-wrapper {
|
||||
&__item {
|
||||
&.ant-alert-info {
|
||||
border: 1px solid color-mix(in srgb, var(--bg-robin-600) 20%, transparent);
|
||||
background: color-mix(
|
||||
in srgb,
|
||||
var(--primary-background) 10%,
|
||||
transparent
|
||||
);
|
||||
color: var(--primary-foreground);
|
||||
}
|
||||
|
||||
.what-next-item {
|
||||
color: var(--primary-foreground);
|
||||
|
||||
&-text {
|
||||
color: var(--primary-foreground);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__footer {
|
||||
.ant-btn {
|
||||
background: var(--primary-background);
|
||||
color: var(--primary-foreground);
|
||||
|
||||
&:hover {
|
||||
background: var(--primary-background-hover);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,75 +0,0 @@
|
||||
import { useState } from 'react';
|
||||
import Lottie from 'react-lottie';
|
||||
import { Alert } from 'antd';
|
||||
import integrationsSuccess from 'assets/Lotties/integrations-success.json';
|
||||
|
||||
import solidCheckCircleUrl from '@/assets/Icons/solid-check-circle.svg';
|
||||
|
||||
import './SuccessView.style.scss';
|
||||
|
||||
export function SuccessView(): JSX.Element {
|
||||
const [isAnimationComplete, setIsAnimationComplete] = useState(false);
|
||||
|
||||
const defaultOptions = {
|
||||
loop: false,
|
||||
autoplay: true,
|
||||
animationData: integrationsSuccess,
|
||||
rendererSettings: {
|
||||
preserveAspectRatio: 'xMidYMid slice',
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{!isAnimationComplete && (
|
||||
<div className="lottie-container">
|
||||
<Lottie
|
||||
options={defaultOptions}
|
||||
height={990.342}
|
||||
width={743.5}
|
||||
eventListeners={[
|
||||
{
|
||||
eventName: 'complete',
|
||||
callback: (): void => setIsAnimationComplete(true),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="cloud-account-setup-success-view">
|
||||
<div className="cloud-account-setup-success-view__icon">
|
||||
<img src={solidCheckCircleUrl} alt="Success" />
|
||||
</div>
|
||||
<div className="cloud-account-setup-success-view__content">
|
||||
<div className="cloud-account-setup-success-view__title">
|
||||
<h3>🎉 Success! </h3>
|
||||
<h3>Your AWS Web Service integration is all set.</h3>
|
||||
</div>
|
||||
<div className="cloud-account-setup-success-view__description">
|
||||
<p>Your observability journey is off to a great start. </p>
|
||||
<p>Now that your data is flowing, here’s what you can do next:</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="cloud-account-setup-success-view__what-next">
|
||||
<h4 className="cloud-account-setup-success-view__what-next-title">
|
||||
WHAT NEXT
|
||||
</h4>
|
||||
<div className="what-next-items-wrapper">
|
||||
<Alert
|
||||
message={
|
||||
<div className="what-next-items-wrapper__item">
|
||||
<div className="what-next-item-bullet-icon">•</div>
|
||||
<div className="what-next-item-text">
|
||||
Set up your AWS services effortlessly under your enabled account.
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
type="info"
|
||||
className="what-next-items-wrapper__item"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import { ServiceData } from './types';
|
||||
|
||||
function DashboardItem({
|
||||
dashboard,
|
||||
}: {
|
||||
dashboard: ServiceData['assets']['dashboards'][number];
|
||||
}): JSX.Element {
|
||||
const content = (
|
||||
<>
|
||||
<div className="cloud-service-dashboard-item__title">{dashboard.title}</div>
|
||||
<div className="cloud-service-dashboard-item__preview">
|
||||
<img
|
||||
src={dashboard.image}
|
||||
alt={dashboard.title}
|
||||
className="cloud-service-dashboard-item__preview-image"
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="cloud-service-dashboard-item">
|
||||
{dashboard.url ? (
|
||||
<Link to={dashboard.url} className="cloud-service-dashboard-item__link">
|
||||
{content}
|
||||
</Link>
|
||||
) : (
|
||||
content
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function CloudServiceDashboards({
|
||||
service,
|
||||
}: {
|
||||
service: ServiceData;
|
||||
}): JSX.Element {
|
||||
return (
|
||||
<>
|
||||
{service.assets.dashboards.map((dashboard) => (
|
||||
<DashboardItem key={dashboard.id} dashboard={dashboard} />
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default CloudServiceDashboards;
|
||||
@@ -1,89 +0,0 @@
|
||||
.configure-service-modal {
|
||||
&__body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-radius: 3px;
|
||||
border: 1px solid var(--l1-border);
|
||||
padding: 14px;
|
||||
|
||||
&-regions-switch-switch {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
|
||||
&-label {
|
||||
color: var(--l1-foreground);
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
line-height: 20px;
|
||||
letter-spacing: -0.07px;
|
||||
}
|
||||
}
|
||||
|
||||
&-switch-description {
|
||||
margin-top: 4px;
|
||||
color: var(--l2-foreground);
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
line-height: 18px;
|
||||
letter-spacing: -0.06px;
|
||||
}
|
||||
&-form-item {
|
||||
&:last-child {
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.ant-modal-body {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
.ant-modal-footer {
|
||||
margin: 0;
|
||||
padding-bottom: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.lightMode {
|
||||
.configure-service-modal {
|
||||
&__body {
|
||||
border-color: var(--l1-border);
|
||||
|
||||
&-regions-switch-switch {
|
||||
&-label {
|
||||
color: var(--l1-foreground);
|
||||
}
|
||||
}
|
||||
|
||||
&-switch-description {
|
||||
color: var(--l1-foreground);
|
||||
}
|
||||
}
|
||||
|
||||
.ant-btn {
|
||||
&.ant-btn-default {
|
||||
background: var(--l1-background);
|
||||
border: 1px solid var(--l1-border);
|
||||
color: var(--l1-foreground);
|
||||
|
||||
&:hover {
|
||||
border-color: var(--l1-border);
|
||||
color: var(--l1-foreground);
|
||||
}
|
||||
}
|
||||
|
||||
&.ant-btn-primary {
|
||||
// Keep primary button same as dark mode
|
||||
background: var(--primary-background);
|
||||
color: var(--l1-background);
|
||||
|
||||
&:hover {
|
||||
background: var(--bg-robin-400);
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.6;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,243 +0,0 @@
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import { useQueryClient } from 'react-query';
|
||||
import { Form, Switch } from 'antd';
|
||||
import SignozModal from 'components/SignozModal/SignozModal';
|
||||
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||
import {
|
||||
ServiceConfig,
|
||||
SupportedSignals,
|
||||
} from 'container/CloudIntegrationPage/ServicesSection/types';
|
||||
import { useUpdateServiceConfig } from 'hooks/integration/aws/useUpdateServiceConfig';
|
||||
import { isEqual } from 'lodash-es';
|
||||
|
||||
import logEvent from '../../../api/common/logEvent';
|
||||
import S3BucketsSelector from './S3BucketsSelector';
|
||||
|
||||
import './ConfigureServiceModal.styles.scss';
|
||||
|
||||
export interface IConfigureServiceModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
serviceName: string;
|
||||
serviceId: string;
|
||||
cloudAccountId: string;
|
||||
supportedSignals: SupportedSignals;
|
||||
initialConfig?: ServiceConfig;
|
||||
}
|
||||
|
||||
function ConfigureServiceModal({
|
||||
isOpen,
|
||||
onClose,
|
||||
serviceName,
|
||||
serviceId,
|
||||
cloudAccountId,
|
||||
initialConfig,
|
||||
supportedSignals,
|
||||
}: IConfigureServiceModalProps): JSX.Element {
|
||||
const [form] = Form.useForm();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
// Track current form values
|
||||
const initialValues = useMemo(
|
||||
() => ({
|
||||
metrics: initialConfig?.metrics?.enabled || false,
|
||||
logs: initialConfig?.logs?.enabled || false,
|
||||
s3Buckets: initialConfig?.logs?.s3_buckets || {},
|
||||
}),
|
||||
[initialConfig],
|
||||
);
|
||||
const [currentValues, setCurrentValues] = useState(initialValues);
|
||||
|
||||
const isSaveDisabled = useMemo(
|
||||
() =>
|
||||
// disable only if current values are same as the initial config
|
||||
currentValues.metrics === initialValues.metrics &&
|
||||
currentValues.logs === initialValues.logs &&
|
||||
isEqual(currentValues.s3Buckets, initialValues.s3Buckets),
|
||||
[currentValues, initialValues],
|
||||
);
|
||||
|
||||
const handleS3BucketsChange = useCallback(
|
||||
(bucketsByRegion: Record<string, string[]>) => {
|
||||
setCurrentValues((prev) => ({
|
||||
...prev,
|
||||
s3Buckets: bucketsByRegion,
|
||||
}));
|
||||
form.setFieldsValue({ s3Buckets: bucketsByRegion });
|
||||
},
|
||||
[form],
|
||||
);
|
||||
|
||||
const {
|
||||
mutate: updateServiceConfig,
|
||||
isLoading: isUpdating,
|
||||
} = useUpdateServiceConfig();
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const handleSubmit = useCallback(async (): Promise<void> => {
|
||||
try {
|
||||
const values = await form.validateFields();
|
||||
setIsLoading(true);
|
||||
|
||||
updateServiceConfig(
|
||||
{
|
||||
serviceId,
|
||||
payload: {
|
||||
cloud_account_id: cloudAccountId,
|
||||
config: {
|
||||
logs: {
|
||||
enabled: values.logs,
|
||||
s3_buckets: values.s3Buckets,
|
||||
},
|
||||
metrics: {
|
||||
enabled: values.metrics,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries([
|
||||
REACT_QUERY_KEY.AWS_SERVICE_DETAILS,
|
||||
serviceId,
|
||||
]);
|
||||
onClose();
|
||||
|
||||
logEvent('AWS Integration: Service settings saved', {
|
||||
cloudAccountId,
|
||||
serviceId,
|
||||
logsEnabled: values?.logs,
|
||||
metricsEnabled: values?.metrics,
|
||||
});
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error('Failed to update service config:', error);
|
||||
},
|
||||
},
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('Form submission failed:', error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [
|
||||
form,
|
||||
updateServiceConfig,
|
||||
serviceId,
|
||||
cloudAccountId,
|
||||
queryClient,
|
||||
onClose,
|
||||
]);
|
||||
|
||||
const handleClose = useCallback(() => {
|
||||
form.resetFields();
|
||||
onClose();
|
||||
}, [form, onClose]);
|
||||
|
||||
return (
|
||||
<SignozModal
|
||||
title={
|
||||
<div className="account-settings-modal__title">Configure {serviceName}</div>
|
||||
}
|
||||
centered
|
||||
open={isOpen}
|
||||
okText="Save"
|
||||
okButtonProps={{
|
||||
disabled: isSaveDisabled,
|
||||
className: 'account-settings-modal__footer-save-button',
|
||||
loading: isLoading || isUpdating,
|
||||
}}
|
||||
onCancel={handleClose}
|
||||
onOk={handleSubmit}
|
||||
cancelText="Close"
|
||||
cancelButtonProps={{
|
||||
className: 'account-settings-modal__footer-close-button',
|
||||
}}
|
||||
width={672}
|
||||
rootClassName=" configure-service-modal"
|
||||
>
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
initialValues={{
|
||||
metrics: initialConfig?.metrics?.enabled || false,
|
||||
logs: initialConfig?.logs?.enabled || false,
|
||||
s3Buckets: initialConfig?.logs?.s3_buckets || {},
|
||||
}}
|
||||
>
|
||||
<div className=" configure-service-modal__body">
|
||||
{supportedSignals.metrics && (
|
||||
<Form.Item
|
||||
name="metrics"
|
||||
valuePropName="checked"
|
||||
className="configure-service-modal__body-form-item"
|
||||
>
|
||||
<div className="configure-service-modal__body-regions-switch-switch">
|
||||
<Switch
|
||||
checked={currentValues.metrics}
|
||||
onChange={(checked): void => {
|
||||
setCurrentValues((prev) => ({ ...prev, metrics: checked }));
|
||||
form.setFieldsValue({ metrics: checked });
|
||||
}}
|
||||
/>
|
||||
<span className="configure-service-modal__body-regions-switch-switch-label">
|
||||
Metric Collection
|
||||
</span>
|
||||
</div>
|
||||
<div className="configure-service-modal__body-switch-description">
|
||||
Metric Collection is enabled for this AWS account. We recommend keeping
|
||||
this enabled, but you can disable metric collection if you do not want
|
||||
to monitor your AWS infrastructure.
|
||||
</div>
|
||||
</Form.Item>
|
||||
)}
|
||||
|
||||
{supportedSignals.logs && (
|
||||
<>
|
||||
<Form.Item
|
||||
name="logs"
|
||||
valuePropName="checked"
|
||||
className="configure-service-modal__body-form-item"
|
||||
>
|
||||
<div className="configure-service-modal__body-regions-switch-switch">
|
||||
<Switch
|
||||
checked={currentValues.logs}
|
||||
onChange={(checked): void => {
|
||||
setCurrentValues((prev) => ({ ...prev, logs: checked }));
|
||||
form.setFieldsValue({ logs: checked });
|
||||
}}
|
||||
/>
|
||||
<span className="configure-service-modal__body-regions-switch-switch-label">
|
||||
Log Collection
|
||||
</span>
|
||||
</div>
|
||||
<div className="configure-service-modal__body-switch-description">
|
||||
To ingest logs from your AWS services, you must complete several steps
|
||||
</div>
|
||||
</Form.Item>
|
||||
|
||||
{currentValues.logs && serviceId === 's3sync' && (
|
||||
<Form.Item name="s3Buckets" noStyle>
|
||||
<S3BucketsSelector
|
||||
initialBucketsByRegion={currentValues.s3Buckets}
|
||||
onChange={handleS3BucketsChange}
|
||||
/>
|
||||
</Form.Item>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</Form>
|
||||
</SignozModal>
|
||||
);
|
||||
}
|
||||
|
||||
ConfigureServiceModal.defaultProps = {
|
||||
initialConfig: {
|
||||
metrics: { enabled: false },
|
||||
logs: { enabled: false },
|
||||
},
|
||||
};
|
||||
|
||||
export default ConfigureServiceModal;
|
||||
@@ -1,189 +0,0 @@
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { Button, Tabs, TabsProps } from 'antd';
|
||||
import { MarkdownRenderer } from 'components/MarkdownRenderer/MarkdownRenderer';
|
||||
import Spinner from 'components/Spinner';
|
||||
import CloudServiceDashboards from 'container/CloudIntegrationPage/ServicesSection/CloudServiceDashboards';
|
||||
import CloudServiceDataCollected from 'container/CloudIntegrationPage/ServicesSection/CloudServiceDataCollected';
|
||||
import { IServiceStatus } from 'container/CloudIntegrationPage/ServicesSection/types';
|
||||
import dayjs from 'dayjs';
|
||||
import { useServiceDetails } from 'hooks/integration/aws/useServiceDetails';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
|
||||
import logEvent from '../../../api/common/logEvent';
|
||||
import ConfigureServiceModal from './ConfigureServiceModal';
|
||||
|
||||
const getStatus = (
|
||||
logsLastReceivedTimestamp: number | undefined,
|
||||
metricsLastReceivedTimestamp: number | undefined,
|
||||
): { text: string; className: string } => {
|
||||
if (!logsLastReceivedTimestamp && !metricsLastReceivedTimestamp) {
|
||||
return { text: 'No Data Yet', className: 'service-status--no-data' };
|
||||
}
|
||||
|
||||
const latestTimestamp = Math.max(
|
||||
logsLastReceivedTimestamp || 0,
|
||||
metricsLastReceivedTimestamp || 0,
|
||||
);
|
||||
|
||||
const isStale = dayjs().diff(dayjs(latestTimestamp), 'minute') > 30;
|
||||
|
||||
if (isStale) {
|
||||
return { text: 'Stale Data', className: 'service-status--stale-data' };
|
||||
}
|
||||
|
||||
return { text: 'Connected', className: 'service-status--connected' };
|
||||
};
|
||||
|
||||
function ServiceStatus({
|
||||
serviceStatus,
|
||||
}: {
|
||||
serviceStatus: IServiceStatus | undefined;
|
||||
}): JSX.Element {
|
||||
const logsLastReceivedTimestamp = serviceStatus?.logs?.last_received_ts_ms;
|
||||
const metricsLastReceivedTimestamp =
|
||||
serviceStatus?.metrics?.last_received_ts_ms;
|
||||
|
||||
const { text, className } = getStatus(
|
||||
logsLastReceivedTimestamp,
|
||||
metricsLastReceivedTimestamp,
|
||||
);
|
||||
|
||||
return <div className={`service-status ${className}`}>{text}</div>;
|
||||
}
|
||||
|
||||
function getTabItems(serviceDetailsData: any): TabsProps['items'] {
|
||||
const dashboards = serviceDetailsData?.assets.dashboards || [];
|
||||
const dataCollected = serviceDetailsData?.data_collected || {};
|
||||
const items: TabsProps['items'] = [];
|
||||
|
||||
if (dashboards.length) {
|
||||
items.push({
|
||||
key: 'dashboards',
|
||||
label: `Dashboards (${dashboards.length})`,
|
||||
children: <CloudServiceDashboards service={serviceDetailsData} />,
|
||||
});
|
||||
}
|
||||
|
||||
items.push({
|
||||
key: 'data-collected',
|
||||
label: 'Data Collected',
|
||||
children: (
|
||||
<CloudServiceDataCollected
|
||||
logsData={dataCollected.logs || []}
|
||||
metricsData={dataCollected.metrics || []}
|
||||
/>
|
||||
),
|
||||
});
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
function ServiceDetails(): JSX.Element | null {
|
||||
const urlQuery = useUrlQuery();
|
||||
const cloudAccountId = urlQuery.get('cloudAccountId');
|
||||
const serviceId = urlQuery.get('service');
|
||||
const [isConfigureServiceModalOpen, setIsConfigureServiceModalOpen] = useState(
|
||||
false,
|
||||
);
|
||||
const openServiceConfigModal = (): void => {
|
||||
setIsConfigureServiceModalOpen(true);
|
||||
logEvent('AWS Integration: Service settings viewed', {
|
||||
cloudAccountId,
|
||||
serviceId,
|
||||
});
|
||||
};
|
||||
|
||||
const { data: serviceDetailsData, isLoading } = useServiceDetails(
|
||||
serviceId || '',
|
||||
cloudAccountId || undefined,
|
||||
);
|
||||
|
||||
const { config, supported_signals } = serviceDetailsData ?? {};
|
||||
|
||||
const totalSupportedSignals = Object.entries(supported_signals || {}).filter(
|
||||
([, value]) => !!value,
|
||||
).length;
|
||||
const enabledSignals = useMemo(
|
||||
() =>
|
||||
Object.values(config || {}).filter((item) => item && item.enabled).length,
|
||||
[config],
|
||||
);
|
||||
|
||||
const isAnySignalConfigured = useMemo(
|
||||
() => !!config?.logs?.enabled || !!config?.metrics?.enabled,
|
||||
[config],
|
||||
);
|
||||
|
||||
// log telemetry event on visiting details of a service.
|
||||
useEffect(() => {
|
||||
if (serviceId) {
|
||||
logEvent('AWS Integration: Service viewed', {
|
||||
cloudAccountId,
|
||||
serviceId,
|
||||
});
|
||||
}
|
||||
}, [cloudAccountId, serviceId]);
|
||||
|
||||
if (isLoading) {
|
||||
return <Spinner size="large" height="50vh" />;
|
||||
}
|
||||
|
||||
if (!serviceDetailsData) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const tabItems = getTabItems(serviceDetailsData);
|
||||
|
||||
return (
|
||||
<div className="service-details">
|
||||
<div className="service-details__title-bar">
|
||||
<div className="service-details__details-title">Details</div>
|
||||
<div className="service-details__right-actions">
|
||||
{isAnySignalConfigured && (
|
||||
<ServiceStatus serviceStatus={serviceDetailsData.status} />
|
||||
)}
|
||||
|
||||
{!!cloudAccountId &&
|
||||
(isAnySignalConfigured ? (
|
||||
<Button
|
||||
className="configure-button configure-button--default"
|
||||
onClick={openServiceConfigModal}
|
||||
>
|
||||
Configure ({enabledSignals}/{totalSupportedSignals})
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
type="primary"
|
||||
className="configure-button configure-button--primary"
|
||||
onClick={openServiceConfigModal}
|
||||
>
|
||||
Enable Service
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="service-details__overview">
|
||||
<MarkdownRenderer
|
||||
variables={{}}
|
||||
markdownContent={serviceDetailsData?.overview}
|
||||
/>
|
||||
</div>
|
||||
<div className="service-details__tabs">
|
||||
<Tabs items={tabItems} />
|
||||
</div>
|
||||
{isConfigureServiceModalOpen && (
|
||||
<ConfigureServiceModal
|
||||
isOpen
|
||||
onClose={(): void => setIsConfigureServiceModalOpen(false)}
|
||||
serviceName={serviceDetailsData.title}
|
||||
serviceId={serviceId || ''}
|
||||
cloudAccountId={cloudAccountId || ''}
|
||||
initialConfig={serviceDetailsData.config}
|
||||
supportedSignals={serviceDetailsData.supported_signals || {}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ServiceDetails;
|
||||
@@ -1,75 +0,0 @@
|
||||
import { useCallback, useEffect, useMemo } from 'react';
|
||||
import { useNavigate } from 'react-router-dom-v5-compat';
|
||||
import Spinner from 'components/Spinner';
|
||||
import { useGetAccountServices } from 'hooks/integration/aws/useGetAccountServices';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
|
||||
import ServiceItem from './ServiceItem';
|
||||
|
||||
interface ServicesListProps {
|
||||
cloudAccountId: string;
|
||||
filter: 'all_services' | 'enabled' | 'available';
|
||||
}
|
||||
|
||||
function ServicesList({
|
||||
cloudAccountId,
|
||||
filter,
|
||||
}: ServicesListProps): JSX.Element {
|
||||
const urlQuery = useUrlQuery();
|
||||
const navigate = useNavigate();
|
||||
const { data: services = [], isLoading } = useGetAccountServices(
|
||||
cloudAccountId,
|
||||
);
|
||||
const activeService = urlQuery.get('service');
|
||||
|
||||
const handleActiveService = useCallback(
|
||||
(serviceId: string): void => {
|
||||
const latestUrlQuery = new URLSearchParams(window.location.search);
|
||||
latestUrlQuery.set('service', serviceId);
|
||||
navigate({ search: latestUrlQuery.toString() });
|
||||
},
|
||||
[navigate],
|
||||
);
|
||||
|
||||
const filteredServices = useMemo(() => {
|
||||
if (filter === 'all_services') {
|
||||
return services;
|
||||
}
|
||||
|
||||
return services.filter((service) => {
|
||||
const isEnabled =
|
||||
service?.config?.logs?.enabled || service?.config?.metrics?.enabled;
|
||||
return filter === 'enabled' ? isEnabled : !isEnabled;
|
||||
});
|
||||
}, [services, filter]);
|
||||
|
||||
useEffect(() => {
|
||||
if (activeService || !services?.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
handleActiveService(services[0].id);
|
||||
}, [services, activeService, handleActiveService]);
|
||||
|
||||
if (isLoading) {
|
||||
return <Spinner size="large" height="25vh" />;
|
||||
}
|
||||
if (!services) {
|
||||
return <div>No services found</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="services-list">
|
||||
{filteredServices.map((service) => (
|
||||
<ServiceItem
|
||||
key={service.id}
|
||||
service={service}
|
||||
onClick={handleActiveService}
|
||||
isActive={service.id === activeService}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ServicesList;
|
||||
@@ -1,124 +0,0 @@
|
||||
import { useMemo, useState } from 'react';
|
||||
import { useQuery } from 'react-query';
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import type { SelectProps, TabsProps } from 'antd';
|
||||
import { Select, Tabs } from 'antd';
|
||||
import { getAwsServices } from 'api/integration/aws';
|
||||
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
import { ChevronDown } from 'lucide-react';
|
||||
|
||||
import ServiceDetails from './ServiceDetails';
|
||||
import ServicesList from './ServicesList';
|
||||
|
||||
import './ServicesTabs.style.scss';
|
||||
|
||||
export enum ServiceFilterType {
|
||||
ALL_SERVICES = 'all_services',
|
||||
ENABLED = 'enabled',
|
||||
AVAILABLE = 'available',
|
||||
}
|
||||
|
||||
interface ServicesFilterProps {
|
||||
cloudAccountId: string;
|
||||
onFilterChange: (value: ServiceFilterType) => void;
|
||||
}
|
||||
|
||||
function ServicesFilter({
|
||||
cloudAccountId,
|
||||
onFilterChange,
|
||||
}: ServicesFilterProps): JSX.Element | null {
|
||||
const { data: services, isLoading } = useQuery(
|
||||
[REACT_QUERY_KEY.AWS_SERVICES, cloudAccountId],
|
||||
() => getAwsServices(cloudAccountId),
|
||||
);
|
||||
|
||||
const { enabledCount, availableCount } = useMemo(() => {
|
||||
if (!services) {
|
||||
return { enabledCount: 0, availableCount: 0 };
|
||||
}
|
||||
|
||||
return services.reduce(
|
||||
(acc, service) => {
|
||||
const isEnabled =
|
||||
service?.config?.logs?.enabled || service?.config?.metrics?.enabled;
|
||||
return {
|
||||
enabledCount: acc.enabledCount + (isEnabled ? 1 : 0),
|
||||
availableCount: acc.availableCount + (isEnabled ? 0 : 1),
|
||||
};
|
||||
},
|
||||
{ enabledCount: 0, availableCount: 0 },
|
||||
);
|
||||
}, [services]);
|
||||
|
||||
const selectOptions: SelectProps['options'] = useMemo(
|
||||
() => [
|
||||
{ value: 'all_services', label: `All Services (${services?.length || 0})` },
|
||||
{ value: 'enabled', label: `Enabled (${enabledCount})` },
|
||||
{ value: 'available', label: `Available (${availableCount})` },
|
||||
],
|
||||
[services, enabledCount, availableCount],
|
||||
);
|
||||
|
||||
if (isLoading) {
|
||||
return null;
|
||||
}
|
||||
if (!services?.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="services-filter">
|
||||
<Select
|
||||
style={{ width: '100%' }}
|
||||
defaultValue={ServiceFilterType.ALL_SERVICES}
|
||||
options={selectOptions}
|
||||
className="services-sidebar__select"
|
||||
suffixIcon={<ChevronDown size={16} color={Color.BG_VANILLA_400} />}
|
||||
onChange={onFilterChange}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ServicesSection(): JSX.Element {
|
||||
const urlQuery = useUrlQuery();
|
||||
const cloudAccountId = urlQuery.get('cloudAccountId') || '';
|
||||
|
||||
const [activeFilter, setActiveFilter] = useState<
|
||||
'all_services' | 'enabled' | 'available'
|
||||
>('all_services');
|
||||
|
||||
return (
|
||||
<div className="services-section">
|
||||
<div className="services-section__sidebar">
|
||||
<ServicesFilter
|
||||
cloudAccountId={cloudAccountId}
|
||||
onFilterChange={setActiveFilter}
|
||||
/>
|
||||
<ServicesList cloudAccountId={cloudAccountId} filter={activeFilter} />
|
||||
</div>
|
||||
<div className="services-section__content">
|
||||
<ServiceDetails />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ServicesTabs(): JSX.Element {
|
||||
const tabItems: TabsProps['items'] = [
|
||||
{
|
||||
key: 'services',
|
||||
label: 'Services For Integration',
|
||||
children: <ServicesSection />,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="services-tabs">
|
||||
<Tabs defaultActiveKey="services" items={tabItems} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ServicesTabs;
|
||||
@@ -1,161 +0,0 @@
|
||||
import { act, fireEvent, screen, waitFor } from '@testing-library/react';
|
||||
import { server } from 'mocks-server/server';
|
||||
import { rest, RestRequest } from 'msw'; // Import RestRequest for req.json() typing
|
||||
|
||||
import { UpdateServiceConfigPayload } from '../types';
|
||||
import { accountsResponse, CLOUD_ACCOUNT_ID, initialBuckets } from './mockData';
|
||||
import {
|
||||
assertGenericModalElements,
|
||||
assertS3SyncSpecificElements,
|
||||
renderModal,
|
||||
} from './utils';
|
||||
|
||||
// --- MOCKS ---
|
||||
jest.mock('hooks/useUrlQuery', () => ({
|
||||
__esModule: true,
|
||||
default: jest.fn(() => ({
|
||||
get: jest.fn((paramName: string) => {
|
||||
if (paramName === 'cloudAccountId') {
|
||||
return CLOUD_ACCOUNT_ID;
|
||||
}
|
||||
return null;
|
||||
}),
|
||||
})),
|
||||
}));
|
||||
|
||||
// --- TEST SUITE ---
|
||||
describe('ConfigureServiceModal for S3 Sync service', () => {
|
||||
jest.setTimeout(10000);
|
||||
beforeEach(() => {
|
||||
server.use(
|
||||
rest.get(
|
||||
'http://localhost/api/v1/cloud-integrations/aws/accounts',
|
||||
(req, res, ctx) => res(ctx.json(accountsResponse)),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
it('should render with logs collection switch and bucket selectors (no buckets initially selected)', async () => {
|
||||
act(() => {
|
||||
renderModal({}); // No initial S3 buckets, defaults to 's3sync' serviceId
|
||||
});
|
||||
await assertGenericModalElements(); // Use new generic assertion
|
||||
await assertS3SyncSpecificElements({}); // Use new S3-specific assertion
|
||||
});
|
||||
|
||||
it('should render with logs collection switch and bucket selectors (some buckets initially selected)', async () => {
|
||||
act(() => {
|
||||
renderModal(initialBuckets); // Defaults to 's3sync' serviceId
|
||||
});
|
||||
await assertGenericModalElements(); // Use new generic assertion
|
||||
await assertS3SyncSpecificElements(initialBuckets); // Use new S3-specific assertion
|
||||
});
|
||||
|
||||
it('should enable save button after adding a new bucket via combobox', async () => {
|
||||
act(() => {
|
||||
renderModal(initialBuckets); // Defaults to 's3sync' serviceId
|
||||
});
|
||||
await assertGenericModalElements();
|
||||
await assertS3SyncSpecificElements(initialBuckets);
|
||||
|
||||
expect(screen.getByRole('button', { name: /save/i })).toBeDisabled();
|
||||
|
||||
const targetCombobox = screen.getAllByRole('combobox')[0];
|
||||
const newBucketName = 'a-newly-added-bucket';
|
||||
|
||||
act(() => {
|
||||
fireEvent.change(targetCombobox, { target: { value: newBucketName } });
|
||||
fireEvent.keyDown(targetCombobox, {
|
||||
key: 'Enter',
|
||||
code: 'Enter',
|
||||
keyCode: 13,
|
||||
});
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByLabelText(newBucketName)).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: /save/i })).toBeEnabled();
|
||||
});
|
||||
});
|
||||
|
||||
it('should send updated bucket configuration on save', async () => {
|
||||
let capturedPayload: UpdateServiceConfigPayload | null = null;
|
||||
const mockUpdateConfigUrl =
|
||||
'http://localhost/api/v1/cloud-integrations/aws/services/s3sync/config';
|
||||
|
||||
// Override POST handler specifically for this test to capture payload
|
||||
server.use(
|
||||
rest.post(mockUpdateConfigUrl, async (req: RestRequest, res, ctx) => {
|
||||
capturedPayload = await req.json();
|
||||
return res(ctx.status(200), ctx.json({ message: 'Config updated' }));
|
||||
}),
|
||||
);
|
||||
act(() => {
|
||||
renderModal(initialBuckets); // Defaults to 's3sync' serviceId
|
||||
});
|
||||
await assertGenericModalElements();
|
||||
await assertS3SyncSpecificElements(initialBuckets);
|
||||
|
||||
expect(screen.getByRole('button', { name: /save/i })).toBeDisabled();
|
||||
|
||||
const newBucketName = 'another-new-bucket';
|
||||
// As before, targeting the first combobox, assumed to be for 'ap-south-1'.
|
||||
const targetCombobox = screen.getAllByRole('combobox')[0];
|
||||
|
||||
act(() => {
|
||||
fireEvent.change(targetCombobox, { target: { value: newBucketName } });
|
||||
fireEvent.keyDown(targetCombobox, {
|
||||
key: 'Enter',
|
||||
code: 'Enter',
|
||||
keyCode: 13,
|
||||
});
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByLabelText(newBucketName)).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: /save/i })).toBeEnabled();
|
||||
act(() => {
|
||||
fireEvent.click(screen.getByRole('button', { name: /save/i }));
|
||||
});
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(capturedPayload).not.toBeNull();
|
||||
});
|
||||
|
||||
expect(capturedPayload).toEqual({
|
||||
cloud_account_id: CLOUD_ACCOUNT_ID,
|
||||
config: {
|
||||
logs: {
|
||||
enabled: true,
|
||||
s3_buckets: {
|
||||
'us-east-2': ['first-bucket', 'second-bucket'], // Existing buckets
|
||||
'ap-south-1': [newBucketName], // Newly added bucket for the first region
|
||||
},
|
||||
},
|
||||
metrics: {},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should not render S3 bucket region selector UI for services other than s3sync', async () => {
|
||||
const otherServiceId = 'cloudwatch';
|
||||
act(() => {
|
||||
renderModal({}, otherServiceId);
|
||||
});
|
||||
await assertGenericModalElements();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.queryByRole('heading', { name: /select s3 buckets by region/i }),
|
||||
).not.toBeInTheDocument();
|
||||
|
||||
const regions = accountsResponse.data.accounts[0]?.config?.regions || [];
|
||||
regions.forEach((region) => {
|
||||
expect(
|
||||
screen.queryByText(`Enter S3 bucket names for ${region}`),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,44 +0,0 @@
|
||||
import { IConfigureServiceModalProps } from '../ConfigureServiceModal';
|
||||
|
||||
const CLOUD_ACCOUNT_ID = '123456789012';
|
||||
|
||||
const initialBuckets = { 'us-east-2': ['first-bucket', 'second-bucket'] };
|
||||
|
||||
const accountsResponse = {
|
||||
status: 'success',
|
||||
data: {
|
||||
accounts: [
|
||||
{
|
||||
id: 'a1b2c3d4-e5f6-7890-1234-567890abcdef',
|
||||
cloud_account_id: CLOUD_ACCOUNT_ID,
|
||||
config: {
|
||||
regions: ['ap-south-1', 'ap-south-2', 'us-east-1', 'us-east-2'],
|
||||
},
|
||||
status: {
|
||||
integration: {
|
||||
last_heartbeat_ts_ms: 1747114366214,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const defaultModalProps: Omit<IConfigureServiceModalProps, 'initialConfig'> = {
|
||||
isOpen: true,
|
||||
onClose: jest.fn(),
|
||||
serviceName: 'S3 Sync',
|
||||
serviceId: 's3sync',
|
||||
cloudAccountId: CLOUD_ACCOUNT_ID,
|
||||
supportedSignals: {
|
||||
logs: true,
|
||||
metrics: false,
|
||||
},
|
||||
};
|
||||
|
||||
export {
|
||||
accountsResponse,
|
||||
CLOUD_ACCOUNT_ID,
|
||||
defaultModalProps,
|
||||
initialBuckets,
|
||||
};
|
||||
@@ -1,78 +0,0 @@
|
||||
import { render, RenderResult, screen, waitFor } from '@testing-library/react';
|
||||
import MockQueryClientProvider from 'providers/test/MockQueryClientProvider';
|
||||
|
||||
import ConfigureServiceModal from '../ConfigureServiceModal';
|
||||
import { accountsResponse, defaultModalProps } from './mockData';
|
||||
|
||||
/**
|
||||
* Renders the ConfigureServiceModal with specified S3 bucket initial configurations.
|
||||
*/
|
||||
const renderModal = (
|
||||
initialConfigLogsS3Buckets: Record<string, string[]> = {},
|
||||
serviceId = 's3sync',
|
||||
): RenderResult => {
|
||||
const initialConfig = {
|
||||
logs: { enabled: true, s3_buckets: initialConfigLogsS3Buckets },
|
||||
metrics: { enabled: false },
|
||||
};
|
||||
|
||||
return render(
|
||||
<MockQueryClientProvider>
|
||||
<ConfigureServiceModal
|
||||
{...defaultModalProps}
|
||||
serviceId={serviceId}
|
||||
initialConfig={initialConfig}
|
||||
/>
|
||||
</MockQueryClientProvider>,
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Asserts that generic UI elements of the modal are present.
|
||||
*/
|
||||
const assertGenericModalElements = async (): Promise<void> => {
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole('switch')).toBeInTheDocument();
|
||||
expect(screen.getByText(/log collection/i)).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText(
|
||||
/to ingest logs from your aws services, you must complete several steps/i,
|
||||
),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Asserts the state of S3 bucket selectors for each region, specific to S3 Sync.
|
||||
*/
|
||||
const assertS3SyncSpecificElements = async (
|
||||
expectedBucketsByRegion: Record<string, string[]> = {},
|
||||
): Promise<void> => {
|
||||
const regions = accountsResponse.data.accounts[0]?.config?.regions || [];
|
||||
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.getByRole('heading', { name: /select s3 buckets by region/i }),
|
||||
).toBeInTheDocument();
|
||||
|
||||
regions.forEach((region) => {
|
||||
expect(screen.getByText(region)).toBeInTheDocument();
|
||||
const bucketsForRegion = expectedBucketsByRegion[region] || [];
|
||||
if (bucketsForRegion.length > 0) {
|
||||
bucketsForRegion.forEach((bucket) => {
|
||||
expect(screen.getByText(bucket)).toBeInTheDocument();
|
||||
});
|
||||
} else {
|
||||
expect(
|
||||
screen.getByText(`Enter S3 bucket names for ${region}`),
|
||||
).toBeInTheDocument();
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
export {
|
||||
assertGenericModalElements,
|
||||
assertS3SyncSpecificElements,
|
||||
renderModal,
|
||||
};
|
||||
@@ -1,64 +0,0 @@
|
||||
import { I18nextProvider } from 'react-i18next';
|
||||
import { act, fireEvent, render, screen } from '@testing-library/react';
|
||||
import { server } from 'mocks-server/server';
|
||||
import { rest } from 'msw';
|
||||
import {
|
||||
IntegrationType,
|
||||
RequestIntegrationBtn,
|
||||
} from 'pages/Integrations/RequestIntegrationBtn';
|
||||
import i18n from 'ReactI18';
|
||||
|
||||
describe('Request AWS integration', () => {
|
||||
it('should render the request integration button', async () => {
|
||||
let capturedPayload: any;
|
||||
server.use(
|
||||
rest.post('http://localhost/api/v1/event', async (req, res, ctx) => {
|
||||
capturedPayload = await req.json();
|
||||
return res(
|
||||
ctx.status(200),
|
||||
ctx.json({
|
||||
statusCode: 200,
|
||||
error: null,
|
||||
payload: 'Event Processed Successfully',
|
||||
}),
|
||||
);
|
||||
}),
|
||||
);
|
||||
act(() => {
|
||||
render(
|
||||
<I18nextProvider i18n={i18n}>
|
||||
<RequestIntegrationBtn type={IntegrationType.AWS_SERVICES} />{' '}
|
||||
</I18nextProvider>,
|
||||
);
|
||||
});
|
||||
|
||||
expect(
|
||||
screen.getByText(
|
||||
/can't find what you’re looking for\? request more integrations/i,
|
||||
),
|
||||
).toBeInTheDocument();
|
||||
|
||||
await act(() => {
|
||||
fireEvent.change(screen.getByPlaceholderText(/Enter integration name/i), {
|
||||
target: { value: 's3 sync' },
|
||||
});
|
||||
|
||||
const submitButton = screen.getByRole('button', { name: /submit/i });
|
||||
|
||||
expect(submitButton).toBeEnabled();
|
||||
|
||||
fireEvent.click(submitButton);
|
||||
});
|
||||
|
||||
expect(capturedPayload.eventName).toBeDefined();
|
||||
expect(capturedPayload.attributes).toBeDefined();
|
||||
|
||||
expect(capturedPayload.eventName).toBe('AWS service integration requested');
|
||||
expect(capturedPayload.attributes).toEqual({
|
||||
screen: 'AWS integration details',
|
||||
integration: 's3 sync',
|
||||
deployment_url: 'localhost',
|
||||
user_email: null,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,8 +1,10 @@
|
||||
.hero-section {
|
||||
height: 308px;
|
||||
padding: 26px 16px;
|
||||
padding: 16px;
|
||||
|
||||
display: flex;
|
||||
gap: 24px;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
background-position: right;
|
||||
@@ -30,7 +32,36 @@
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
|
||||
.title {
|
||||
&-header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
|
||||
&__icon {
|
||||
height: fit-content;
|
||||
background-color: var(--l1-background);
|
||||
padding: 12px;
|
||||
border: 1px solid var(--l2-background);
|
||||
border-radius: 6px;
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
}
|
||||
|
||||
&__title {
|
||||
color: var(--l1-foreground);
|
||||
font-size: 24px;
|
||||
font-weight: 500;
|
||||
line-height: 20px;
|
||||
letter-spacing: -0.12px;
|
||||
}
|
||||
}
|
||||
|
||||
&__description {
|
||||
color: var(--l2-foreground);
|
||||
}
|
||||
|
||||
&-title {
|
||||
color: var(--l1-foreground);
|
||||
font-size: 24px;
|
||||
font-weight: 500;
|
||||
@@ -38,7 +69,7 @@
|
||||
letter-spacing: -0.12px;
|
||||
}
|
||||
|
||||
.description {
|
||||
&-description {
|
||||
color: var(--l2-foreground);
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
@@ -0,0 +1,28 @@
|
||||
import awsDarkLogoUrl from '@/assets/Logos/aws-dark.svg';
|
||||
|
||||
import AccountActions from './components/AccountActions';
|
||||
|
||||
import './HeroSection.style.scss';
|
||||
|
||||
function HeroSection(): JSX.Element {
|
||||
return (
|
||||
<div className="hero-section">
|
||||
<div className="hero-section__details">
|
||||
<div className="hero-section__details-header">
|
||||
<div className="hero-section__icon">
|
||||
<img src={awsDarkLogoUrl} alt="AWS" />
|
||||
</div>
|
||||
|
||||
<div className="hero-section__details-title">AWS</div>
|
||||
</div>
|
||||
<div className="hero-section__details-description">
|
||||
AWS is a cloud computing platform that provides a range of services for
|
||||
building and running applications.
|
||||
</div>
|
||||
</div>
|
||||
<AccountActions />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default HeroSection;
|
||||
@@ -4,14 +4,57 @@
|
||||
|
||||
&-with-account {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
gap: 16px;
|
||||
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--l3-background);
|
||||
background: var(--l1-background);
|
||||
|
||||
.selected-cloud-integration-account-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-right: 1px solid var(--l3-background);
|
||||
border-radius: none;
|
||||
width: 32px;
|
||||
}
|
||||
|
||||
&-selector-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
|
||||
.account-selector-label {
|
||||
color: var(--l2-foreground);
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
line-height: 16px;
|
||||
padding: 8px 16px;
|
||||
}
|
||||
|
||||
.account-selector {
|
||||
.ant-select {
|
||||
background-color: transparent !important;
|
||||
border: none !important;
|
||||
box-shadow: none !important;
|
||||
|
||||
.ant-select-selector {
|
||||
background: transparent !important;
|
||||
border: none !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__input-skeleton {
|
||||
width: 300px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
&__new-account-button-skeleton {
|
||||
@@ -22,11 +65,13 @@
|
||||
&__account-settings-button-skeleton {
|
||||
width: 140px;
|
||||
}
|
||||
|
||||
&__action-buttons {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
&__action-button {
|
||||
font-family: 'Inter';
|
||||
border-radius: 2px;
|
||||
@@ -45,11 +90,16 @@
|
||||
&.secondary {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border: 1px solid var(--l3-background);
|
||||
color: var(--l1-foreground);
|
||||
border: 1px solid var(--l1-border);
|
||||
border-radius: 2px;
|
||||
background: var(--l1-border);
|
||||
background: var(--l1-background);
|
||||
box-shadow: none;
|
||||
color: var(--l1-foreground);
|
||||
|
||||
&:hover {
|
||||
border-color: var(--l1-border);
|
||||
color: var(--l1-foreground);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -57,25 +107,30 @@
|
||||
.cloud-account-selector {
|
||||
border-radius: 2px;
|
||||
border: 1px solid var(--l3-background);
|
||||
background: linear-gradient(
|
||||
139deg,
|
||||
color-mix(in srgb, var(--card) 80%, transparent) 0%,
|
||||
color-mix(in srgb, var(--card) 90%, transparent) 98.68%
|
||||
);
|
||||
background: var(--l1-background);
|
||||
box-shadow: 4px 10px 16px 2px rgba(0, 0, 0, 0.2);
|
||||
-webkit-backdrop-filter: blur(20px);
|
||||
backdrop-filter: blur(20px);
|
||||
|
||||
.ant-select-selector {
|
||||
border-color: var(--l1-border) !important;
|
||||
background: var(--l3-background) !important;
|
||||
background: var(--l1-background) !important;
|
||||
padding: 6px 8px !important;
|
||||
min-width: 140px !important;
|
||||
}
|
||||
|
||||
.ant-select-item-option-active {
|
||||
background: var(--l3-background) !important;
|
||||
}
|
||||
.ant-select-selection-item {
|
||||
color: var(--l2-foreground);
|
||||
color: var(--l1-foreground);
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
line-height: 16px;
|
||||
}
|
||||
&:hover {
|
||||
.ant-select-selector {
|
||||
border-color: var(--l1-border) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.account-option-item {
|
||||
display: flex;
|
||||
@@ -87,60 +142,8 @@
|
||||
justify-content: center;
|
||||
height: 14px;
|
||||
width: 14px;
|
||||
background-color: color-mix(
|
||||
in srgb,
|
||||
var(--border) 20%,
|
||||
transparent
|
||||
); /* #C0C1C3 with 0.2 opacity */
|
||||
background-color: color-mix(in srgb, var(--border) 20%, transparent);
|
||||
border-radius: 2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.lightMode {
|
||||
.hero-section__action-button {
|
||||
&.primary {
|
||||
background: var(--primary-background);
|
||||
color: var(--primary-foreground);
|
||||
}
|
||||
|
||||
&.secondary {
|
||||
border-color: var(--l1-border);
|
||||
color: var(--l1-foreground);
|
||||
background: var(--l1-background);
|
||||
|
||||
&:hover {
|
||||
border-color: var(--l1-border);
|
||||
color: var(--l1-foreground);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.cloud-account-selector {
|
||||
background: var(--l1-background);
|
||||
.ant-select-selector {
|
||||
background: var(--l1-background) !important;
|
||||
border-color: var(--l1-border) !important;
|
||||
}
|
||||
.ant-select-item-option-active {
|
||||
background: var(--l3-background) !important;
|
||||
}
|
||||
|
||||
.ant-select-selection-item {
|
||||
color: var(--l1-foreground);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.ant-select-selector {
|
||||
border-color: var(--l1-border) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.account-option-item {
|
||||
color: var(--l1-foreground);
|
||||
|
||||
&__selected {
|
||||
background: var(--primary-background);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,58 +1,23 @@
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom-v5-compat';
|
||||
import { Button } from '@signozhq/button';
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { Button, Select, Skeleton } from 'antd';
|
||||
import type { SelectProps } from 'antd/lib';
|
||||
import { Select, Skeleton } from 'antd';
|
||||
import { SelectProps } from 'antd/lib';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { useAwsAccounts } from 'hooks/integration/aws/useAwsAccounts';
|
||||
import { useListAccounts } from 'api/generated/services/cloudintegration';
|
||||
import { getAccountById } from 'container/Integrations/CloudIntegration/utils';
|
||||
import { INTEGRATION_TYPES } from 'container/Integrations/constants';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
import { Check, ChevronDown } from 'lucide-react';
|
||||
import { ChevronDown, Dot, PencilLine, Plug, Plus } from 'lucide-react';
|
||||
|
||||
import { CloudAccount } from '../../ServicesSection/types';
|
||||
import { mapAccountDtoToAwsCloudAccount } from '../../mapAwsCloudAccountFromDto';
|
||||
import { CloudAccount } from '../../types';
|
||||
import AccountSettingsModal from './AccountSettingsModal';
|
||||
import CloudAccountSetupModal from './CloudAccountSetupModal';
|
||||
|
||||
import './AccountActions.style.scss';
|
||||
|
||||
interface AccountOptionItemProps {
|
||||
label: React.ReactNode;
|
||||
isSelected: boolean;
|
||||
}
|
||||
|
||||
function AccountOptionItem({
|
||||
label,
|
||||
isSelected,
|
||||
}: AccountOptionItemProps): JSX.Element {
|
||||
return (
|
||||
<div className="account-option-item">
|
||||
{label}
|
||||
{isSelected && (
|
||||
<div className="account-option-item__selected">
|
||||
<Check size={12} color={Color.BG_VANILLA_100} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function renderOption(
|
||||
option: any,
|
||||
activeAccountId: string | undefined,
|
||||
): JSX.Element {
|
||||
return (
|
||||
<AccountOptionItem
|
||||
label={option.label}
|
||||
isSelected={option.value === activeAccountId}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const getAccountById = (
|
||||
accounts: CloudAccount[],
|
||||
accountId: string,
|
||||
): CloudAccount | null =>
|
||||
accounts.find((account) => account.cloud_account_id === accountId) || null;
|
||||
|
||||
function AccountActionsRenderer({
|
||||
accounts,
|
||||
isLoading,
|
||||
@@ -73,55 +38,52 @@ function AccountActionsRenderer({
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="hero-section__actions-with-account">
|
||||
<Skeleton.Input
|
||||
active
|
||||
size="large"
|
||||
block
|
||||
className="hero-section__input-skeleton"
|
||||
/>
|
||||
<div className="hero-section__action-buttons">
|
||||
<Skeleton.Button
|
||||
active
|
||||
size="large"
|
||||
className="hero-section__new-account-button-skeleton"
|
||||
/>
|
||||
<Skeleton.Button
|
||||
active
|
||||
size="large"
|
||||
className="hero-section__account-settings-button-skeleton"
|
||||
/>
|
||||
</div>
|
||||
<Skeleton.Input active block className="hero-section__input-skeleton" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (accounts?.length) {
|
||||
return (
|
||||
<div className="hero-section__actions-with-account">
|
||||
<Select
|
||||
value={`Account: ${activeAccount?.cloud_account_id}`}
|
||||
options={selectOptions}
|
||||
rootClassName="cloud-account-selector"
|
||||
placeholder="Select AWS Account"
|
||||
suffixIcon={<ChevronDown size={16} color={Color.BG_VANILLA_400} />}
|
||||
optionRender={(option): JSX.Element =>
|
||||
renderOption(option, activeAccount?.cloud_account_id)
|
||||
}
|
||||
onChange={onAccountChange}
|
||||
/>
|
||||
<div className="hero-section__actions-with-account-selector-container">
|
||||
<div className="selected-cloud-integration-account-status">
|
||||
<Dot size={24} color={Color.BG_FOREST_500} />
|
||||
</div>
|
||||
|
||||
<div className="account-selector-label">Account:</div>
|
||||
|
||||
<span className="account-selector">
|
||||
<Select
|
||||
value={activeAccount?.providerAccountId}
|
||||
options={selectOptions}
|
||||
rootClassName="cloud-account-selector"
|
||||
popupMatchSelectWidth={false}
|
||||
placeholder="Select AWS Account"
|
||||
suffixIcon={<ChevronDown size={16} color={Color.BG_VANILLA_400} />}
|
||||
onChange={onAccountChange}
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
<div className="hero-section__action-buttons">
|
||||
<Button
|
||||
type="primary"
|
||||
className="hero-section__action-button primary"
|
||||
onClick={onIntegrationModalOpen}
|
||||
>
|
||||
Add New AWS Account
|
||||
</Button>
|
||||
<Button
|
||||
type="default"
|
||||
className="hero-section__action-button secondary"
|
||||
variant="link"
|
||||
size="sm"
|
||||
color="secondary"
|
||||
prefixIcon={<PencilLine size={14} />}
|
||||
onClick={onAccountSettingsModalOpen}
|
||||
>
|
||||
Account Settings
|
||||
Edit Account
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="link"
|
||||
size="sm"
|
||||
color="secondary"
|
||||
onClick={onIntegrationModalOpen}
|
||||
prefixIcon={<Plus size={14} />}
|
||||
>
|
||||
Add New Account
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -129,8 +91,11 @@ function AccountActionsRenderer({
|
||||
}
|
||||
return (
|
||||
<Button
|
||||
className="hero-section__action-button primary"
|
||||
variant="solid"
|
||||
color="primary"
|
||||
prefixIcon={<Plug size={14} />}
|
||||
onClick={onIntegrationModalOpen}
|
||||
size="sm"
|
||||
>
|
||||
Integrate Now
|
||||
</Button>
|
||||
@@ -140,7 +105,18 @@ function AccountActionsRenderer({
|
||||
function AccountActions(): JSX.Element {
|
||||
const urlQuery = useUrlQuery();
|
||||
const navigate = useNavigate();
|
||||
const { data: accounts, isLoading } = useAwsAccounts();
|
||||
const { data: listAccountsResponse, isLoading } = useListAccounts({
|
||||
cloudProvider: INTEGRATION_TYPES.AWS,
|
||||
});
|
||||
const accounts = useMemo((): CloudAccount[] | undefined => {
|
||||
const raw = listAccountsResponse?.data?.accounts;
|
||||
if (!raw) {
|
||||
return undefined;
|
||||
}
|
||||
return raw
|
||||
.map(mapAccountDtoToAwsCloudAccount)
|
||||
.filter((account): account is CloudAccount => account !== null);
|
||||
}, [listAccountsResponse]);
|
||||
|
||||
const initialAccount = useMemo(
|
||||
() =>
|
||||
@@ -162,7 +138,13 @@ function AccountActions(): JSX.Element {
|
||||
const latestUrlQuery = new URLSearchParams(window.location.search);
|
||||
latestUrlQuery.set('cloudAccountId', initialAccount.cloud_account_id);
|
||||
navigate({ search: latestUrlQuery.toString() });
|
||||
return;
|
||||
}
|
||||
|
||||
setActiveAccount(null);
|
||||
const latestUrlQuery = new URLSearchParams(window.location.search);
|
||||
latestUrlQuery.delete('cloudAccountId');
|
||||
navigate({ search: latestUrlQuery.toString() });
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [initialAccount]);
|
||||
|
||||
@@ -198,7 +180,7 @@ function AccountActions(): JSX.Element {
|
||||
accounts?.length
|
||||
? accounts.map((account) => ({
|
||||
value: account.cloud_account_id,
|
||||
label: account.cloud_account_id,
|
||||
label: account.providerAccountId,
|
||||
}))
|
||||
: [],
|
||||
[accounts],
|
||||
@@ -228,10 +210,10 @@ function AccountActions(): JSX.Element {
|
||||
/>
|
||||
)}
|
||||
|
||||
{isAccountSettingsModalOpen && (
|
||||
{isAccountSettingsModalOpen && activeAccount && (
|
||||
<AccountSettingsModal
|
||||
onClose={(): void => setIsAccountSettingsModalOpen(false)}
|
||||
account={activeAccount as CloudAccount}
|
||||
account={activeAccount}
|
||||
setActiveAccount={setActiveAccount}
|
||||
/>
|
||||
)}
|
||||
@@ -14,8 +14,13 @@
|
||||
border-radius: 3px;
|
||||
border: 1px solid var(--l1-border);
|
||||
padding: 14px;
|
||||
|
||||
&-account-info {
|
||||
&-connected-account-details {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
|
||||
&-title {
|
||||
color: var(--l1-foreground);
|
||||
font-size: 14px;
|
||||
@@ -38,10 +43,12 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
&-regions-switch {
|
||||
|
||||
&-region-selector {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
gap: 4px;
|
||||
|
||||
&-title {
|
||||
color: var(--l1-foreground);
|
||||
font-size: 14px;
|
||||
@@ -49,6 +56,14 @@
|
||||
line-height: 20px;
|
||||
letter-spacing: -0.07px;
|
||||
}
|
||||
|
||||
&-description {
|
||||
color: var(--l2-foreground);
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
letter-spacing: -0.06px;
|
||||
}
|
||||
|
||||
&-switch {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -66,15 +81,17 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
&-regions-select {
|
||||
margin-top: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
&__footer {
|
||||
padding: 16px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 10px;
|
||||
justify-content: space-between;
|
||||
|
||||
&-close-button,
|
||||
&-save-button {
|
||||
color: var(--l1-foreground);
|
||||
font-family: Inter;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
@@ -83,18 +100,31 @@
|
||||
}
|
||||
&-close-button {
|
||||
border-radius: 2px;
|
||||
background: var(--l1-border);
|
||||
border: none;
|
||||
background: var(--l1-background);
|
||||
border: 1px solid var(--l1-border);
|
||||
color: var(--l1-foreground);
|
||||
|
||||
&:hover {
|
||||
border-color: var(--l1-border);
|
||||
color: var(--l1-foreground);
|
||||
}
|
||||
}
|
||||
&-save-button {
|
||||
background: var(--primary-background);
|
||||
color: var(--primary-foreground);
|
||||
border: none;
|
||||
border-radius: 2px;
|
||||
margin: 0 !important;
|
||||
|
||||
&:disabled {
|
||||
background: var(--primary-background);
|
||||
color: var(--primary-foreground);
|
||||
opacity: 0.6;
|
||||
border: none;
|
||||
}
|
||||
border-radius: 2px;
|
||||
margin: 0 !important;
|
||||
|
||||
&:not(:disabled):hover {
|
||||
background: var(--primary-background-hover);
|
||||
}
|
||||
}
|
||||
}
|
||||
.ant-modal-body {
|
||||
@@ -109,81 +139,3 @@
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.lightMode {
|
||||
.account-settings-modal {
|
||||
&__title-account-id {
|
||||
color: var(--l1-foreground);
|
||||
}
|
||||
|
||||
&__body {
|
||||
border-color: var(--l1-border);
|
||||
|
||||
&-account-info {
|
||||
&-connected-account-details {
|
||||
&-title {
|
||||
color: var(--l1-foreground);
|
||||
}
|
||||
|
||||
&-account-id {
|
||||
color: var(--l1-foreground);
|
||||
|
||||
&-account-id {
|
||||
color: var(--l1-foreground);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-regions-switch {
|
||||
&-title {
|
||||
color: var(--l1-foreground);
|
||||
}
|
||||
|
||||
&-switch {
|
||||
&-label {
|
||||
color: var(--l1-foreground);
|
||||
|
||||
&:hover {
|
||||
color: var(--l1-foreground);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__footer {
|
||||
&-close-button,
|
||||
&-save-button {
|
||||
color: var(--l1-background);
|
||||
}
|
||||
|
||||
&-close-button {
|
||||
background: var(--l1-background);
|
||||
border: 1px solid var(--l1-border);
|
||||
color: var(--l1-foreground);
|
||||
|
||||
&:hover {
|
||||
border-color: var(--l1-border);
|
||||
color: var(--l1-foreground);
|
||||
}
|
||||
}
|
||||
|
||||
&-save-button {
|
||||
// Keep primary button same as dark mode
|
||||
background: var(--primary-background);
|
||||
color: var(--primary-foreground);
|
||||
|
||||
&:disabled {
|
||||
background: var(--primary-background);
|
||||
color: var(--primary-foreground);
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
&:not(:disabled):hover {
|
||||
background: var(--bg-robin-400);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
import { Dispatch, SetStateAction, useCallback } from 'react';
|
||||
import { useQueryClient } from 'react-query';
|
||||
import { Button } from '@signozhq/button';
|
||||
import { DrawerWrapper } from '@signozhq/drawer';
|
||||
import { Form } from 'antd';
|
||||
import { invalidateListAccounts } from 'api/generated/services/cloudintegration';
|
||||
import { INTEGRATION_TYPES } from 'container/Integrations/constants';
|
||||
import { useAccountSettingsModal } from 'hooks/integration/aws/useAccountSettingsModal';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
import history from 'lib/history';
|
||||
import { Save } from 'lucide-react';
|
||||
|
||||
import logEvent from '../../../../../../api/common/logEvent';
|
||||
import { CloudAccount } from '../../types';
|
||||
import { RegionSelector } from './RegionSelector';
|
||||
import RemoveIntegrationAccount from './RemoveIntegrationAccount';
|
||||
|
||||
import './AccountSettingsModal.style.scss';
|
||||
|
||||
interface AccountSettingsModalProps {
|
||||
onClose: () => void;
|
||||
account: CloudAccount;
|
||||
setActiveAccount: Dispatch<SetStateAction<CloudAccount | null>>;
|
||||
}
|
||||
|
||||
function AccountSettingsModal({
|
||||
onClose,
|
||||
account,
|
||||
setActiveAccount,
|
||||
}: AccountSettingsModalProps): JSX.Element {
|
||||
const {
|
||||
form,
|
||||
isLoading,
|
||||
selectedRegions,
|
||||
includeAllRegions,
|
||||
isSaveDisabled,
|
||||
setSelectedRegions,
|
||||
setIncludeAllRegions,
|
||||
handleSubmit,
|
||||
handleClose,
|
||||
} = useAccountSettingsModal({ onClose, account, setActiveAccount });
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
const urlQuery = useUrlQuery();
|
||||
|
||||
const handleRemoveIntegrationAccountSuccess = useCallback((): void => {
|
||||
void invalidateListAccounts(queryClient, {
|
||||
cloudProvider: INTEGRATION_TYPES.AWS,
|
||||
});
|
||||
urlQuery.delete('cloudAccountId');
|
||||
setActiveAccount(null);
|
||||
handleClose();
|
||||
history.replace({ search: urlQuery.toString() });
|
||||
|
||||
logEvent('AWS Integration: Account removed', {
|
||||
id: account?.id,
|
||||
cloudAccountId: account?.cloud_account_id,
|
||||
});
|
||||
}, [
|
||||
queryClient,
|
||||
urlQuery,
|
||||
setActiveAccount,
|
||||
handleClose,
|
||||
account?.id,
|
||||
account?.cloud_account_id,
|
||||
]);
|
||||
|
||||
const renderAccountDetails = useCallback(() => {
|
||||
return (
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
initialValues={{
|
||||
selectedRegions,
|
||||
includeAllRegions,
|
||||
}}
|
||||
>
|
||||
<div className="account-settings-modal__body">
|
||||
<div className="account-settings-modal__body-account-info">
|
||||
<div className="account-settings-modal__body-account-info-connected-account-details">
|
||||
<div className="account-settings-modal__body-account-info-connected-account-details-title">
|
||||
Connected Account details
|
||||
</div>
|
||||
<div className="account-settings-modal__body-account-info-connected-account-details-account-id">
|
||||
AWS Account:{' '}
|
||||
<span className="account-settings-modal__body-account-info-connected-account-details-account-id-account-id">
|
||||
{account?.providerAccountId}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="account-settings-modal__body-region-selector">
|
||||
<div className="account-settings-modal__body-region-selector-title">
|
||||
Which regions do you want to monitor?
|
||||
</div>
|
||||
<div className="account-settings-modal__body-region-selector-description">
|
||||
Choose only the regions you want SigNoz to monitor.
|
||||
</div>
|
||||
|
||||
<RegionSelector
|
||||
selectedRegions={selectedRegions}
|
||||
setSelectedRegions={setSelectedRegions}
|
||||
setIncludeAllRegions={setIncludeAllRegions}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="account-settings-modal__footer">
|
||||
<RemoveIntegrationAccount
|
||||
accountId={account?.id}
|
||||
onRemoveIntegrationAccountSuccess={handleRemoveIntegrationAccountSuccess}
|
||||
/>
|
||||
|
||||
<Button
|
||||
variant="solid"
|
||||
color="secondary"
|
||||
disabled={isSaveDisabled}
|
||||
onClick={handleSubmit}
|
||||
loading={isLoading}
|
||||
prefixIcon={<Save size={14} />}
|
||||
>
|
||||
Update Changes
|
||||
</Button>
|
||||
</div>
|
||||
</Form>
|
||||
);
|
||||
}, [
|
||||
form,
|
||||
selectedRegions,
|
||||
includeAllRegions,
|
||||
account?.id,
|
||||
handleRemoveIntegrationAccountSuccess,
|
||||
isSaveDisabled,
|
||||
handleSubmit,
|
||||
isLoading,
|
||||
setSelectedRegions,
|
||||
setIncludeAllRegions,
|
||||
]);
|
||||
|
||||
const handleDrawerOpenChange = useCallback(
|
||||
(open: boolean): void => {
|
||||
if (!open) {
|
||||
handleClose();
|
||||
}
|
||||
},
|
||||
[handleClose],
|
||||
);
|
||||
|
||||
return (
|
||||
<DrawerWrapper
|
||||
open={true}
|
||||
type="panel"
|
||||
className="account-settings-modal"
|
||||
header={{
|
||||
title: 'Account Settings',
|
||||
}}
|
||||
direction="right"
|
||||
showCloseButton
|
||||
content={renderAccountDetails()}
|
||||
onOpenChange={handleDrawerOpenChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default AccountSettingsModal;
|
||||
@@ -1,4 +1,33 @@
|
||||
.cloud-account-setup-modal {
|
||||
background: var(--l1-background);
|
||||
color: var(--l1-foreground);
|
||||
|
||||
[data-slot='drawer-title'] {
|
||||
color: var(--l1-foreground);
|
||||
}
|
||||
|
||||
> div {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
&__content {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
min-height: 0;
|
||||
scrollbar-width: thin;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
&__footer {
|
||||
padding: 16px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.account-setup-modal-footer {
|
||||
&__confirm-button {
|
||||
background: var(--primary-background);
|
||||
@@ -10,16 +39,24 @@
|
||||
font-family: 'Geist Mono';
|
||||
}
|
||||
&__close-button {
|
||||
background: var(--l1-border);
|
||||
background: var(--l1-background);
|
||||
border: 1px solid var(--l1-border);
|
||||
border-radius: 2px;
|
||||
color: var(--l1-foreground);
|
||||
font-family: 'Inter';
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
|
||||
&:hover {
|
||||
border-color: var(--l1-border);
|
||||
color: var(--l1-foreground);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.cloud-account-setup-form {
|
||||
padding: 16px;
|
||||
|
||||
.disabled {
|
||||
opacity: 0.4;
|
||||
}
|
||||
@@ -56,6 +93,8 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
color: var(--l1-foreground);
|
||||
|
||||
.retry-time {
|
||||
font-family: 'Geist Mono';
|
||||
font-size: 14px;
|
||||
@@ -116,7 +155,7 @@
|
||||
}
|
||||
&__note {
|
||||
padding: 12px;
|
||||
color: var(--bg-robin-400);
|
||||
color: var(--callout-primary-description);
|
||||
font-size: 12px;
|
||||
line-height: 22px;
|
||||
letter-spacing: -0.06px;
|
||||
@@ -144,87 +183,3 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.lightMode {
|
||||
.cloud-account-setup-modal {
|
||||
.account-setup-modal-footer {
|
||||
&__confirm-button {
|
||||
background: var(--primary-background);
|
||||
color: var(--primary-foreground);
|
||||
}
|
||||
|
||||
&__close-button {
|
||||
background: var(--l1-background);
|
||||
border: 1px solid var(--l1-border);
|
||||
color: var(--l1-foreground);
|
||||
|
||||
&:hover {
|
||||
border-color: var(--l1-border);
|
||||
color: var(--l1-foreground);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.cloud-account-setup-form {
|
||||
&__title {
|
||||
color: var(--l1-foreground);
|
||||
}
|
||||
|
||||
&__description {
|
||||
color: var(--l1-foreground);
|
||||
}
|
||||
|
||||
&__select {
|
||||
.ant-select-selection-item {
|
||||
color: var(--l1-foreground);
|
||||
}
|
||||
}
|
||||
|
||||
&__include-all-regions-switch {
|
||||
color: var(--l1-foreground);
|
||||
|
||||
&-label {
|
||||
color: var(--l1-foreground);
|
||||
|
||||
&:hover {
|
||||
color: var(--l1-foreground);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__note {
|
||||
color: var(--primary-foreground);
|
||||
border: 1px solid
|
||||
color-mix(in srgb, var(--primary-background) 20%, transparent);
|
||||
background: color-mix(in srgb, var(--primary-background) 10%, transparent);
|
||||
}
|
||||
|
||||
&__submit-button {
|
||||
background: var(--primary-background);
|
||||
color: var(--primary-foreground);
|
||||
}
|
||||
|
||||
&__alert {
|
||||
&.ant-alert-error {
|
||||
color: var(--danger-foreground);
|
||||
border: 1px solid
|
||||
color-mix(in srgb, var(--danger-background) 20%, transparent);
|
||||
background: color-mix(in srgb, var(--danger-background) 10%, transparent);
|
||||
}
|
||||
|
||||
&.ant-alert-warning {
|
||||
color: var(--warning-foreground);
|
||||
border: 1px solid
|
||||
color-mix(in srgb, var(--warning-background) 20%, transparent);
|
||||
background: color-mix(in srgb, var(--warning-background) 10%, transparent);
|
||||
}
|
||||
|
||||
&-message {
|
||||
.retry-time {
|
||||
color: var(--l1-foreground);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,7 @@
|
||||
import { useCallback } from 'react';
|
||||
import { useQueryClient } from 'react-query';
|
||||
import { Button } from '@signozhq/button';
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import SignozModal from 'components/SignozModal/SignozModal';
|
||||
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||
import { DrawerWrapper } from '@signozhq/drawer';
|
||||
import { useIntegrationModal } from 'hooks/integration/aws/useIntegrationModal';
|
||||
import { SquareArrowOutUpRight } from 'lucide-react';
|
||||
|
||||
@@ -12,19 +11,15 @@ import {
|
||||
ModalStateEnum,
|
||||
} from '../types';
|
||||
import { RegionForm } from './RegionForm';
|
||||
import { RegionSelector } from './RegionSelector';
|
||||
import { SuccessView } from './SuccessView';
|
||||
|
||||
import './CloudAccountSetupModal.style.scss';
|
||||
|
||||
function CloudAccountSetupModal({
|
||||
onClose,
|
||||
}: IntegrationModalProps): JSX.Element {
|
||||
const queryClient = useQueryClient();
|
||||
const {
|
||||
form,
|
||||
modalState,
|
||||
setModalState,
|
||||
isLoading,
|
||||
activeView,
|
||||
selectedRegions,
|
||||
@@ -32,97 +27,86 @@ function CloudAccountSetupModal({
|
||||
isGeneratingUrl,
|
||||
setSelectedRegions,
|
||||
setIncludeAllRegions,
|
||||
handleIncludeAllRegionsChange,
|
||||
handleRegionSelect,
|
||||
handleSubmit,
|
||||
handleClose,
|
||||
setActiveView,
|
||||
allRegions,
|
||||
accountId,
|
||||
selectedDeploymentRegion,
|
||||
handleRegionChange,
|
||||
connectionParams,
|
||||
isConnectionParamsLoading,
|
||||
handleConnectionSuccess,
|
||||
handleConnectionTimeout,
|
||||
handleConnectionError,
|
||||
} = useIntegrationModal({ onClose });
|
||||
|
||||
const renderContent = useCallback(() => {
|
||||
if (modalState === ModalStateEnum.SUCCESS) {
|
||||
return <SuccessView />;
|
||||
}
|
||||
|
||||
if (activeView === ActiveViewEnum.SELECT_REGIONS) {
|
||||
return (
|
||||
<RegionSelector
|
||||
return (
|
||||
<div className="cloud-account-setup-modal__content">
|
||||
<RegionForm
|
||||
form={form}
|
||||
modalState={modalState}
|
||||
selectedRegions={selectedRegions}
|
||||
includeAllRegions={includeAllRegions}
|
||||
onRegionSelect={handleRegionSelect}
|
||||
onSubmit={handleSubmit}
|
||||
accountId={accountId}
|
||||
handleRegionChange={handleRegionChange}
|
||||
connectionParams={connectionParams}
|
||||
isConnectionParamsLoading={isConnectionParamsLoading}
|
||||
setSelectedRegions={setSelectedRegions}
|
||||
setIncludeAllRegions={setIncludeAllRegions}
|
||||
onConnectionSuccess={handleConnectionSuccess}
|
||||
onConnectionTimeout={handleConnectionTimeout}
|
||||
onConnectionError={handleConnectionError}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<RegionForm
|
||||
form={form}
|
||||
modalState={modalState}
|
||||
setModalState={setModalState}
|
||||
selectedRegions={selectedRegions}
|
||||
includeAllRegions={includeAllRegions}
|
||||
onIncludeAllRegionsChange={handleIncludeAllRegionsChange}
|
||||
onRegionSelect={handleRegionSelect}
|
||||
onSubmit={handleSubmit}
|
||||
accountId={accountId}
|
||||
selectedDeploymentRegion={selectedDeploymentRegion}
|
||||
handleRegionChange={handleRegionChange}
|
||||
connectionParams={connectionParams}
|
||||
isConnectionParamsLoading={isConnectionParamsLoading}
|
||||
/>
|
||||
<div className="cloud-account-setup-modal__footer">
|
||||
<Button
|
||||
variant="solid"
|
||||
color="primary"
|
||||
prefixIcon={
|
||||
<SquareArrowOutUpRight size={17} color={Color.BG_VANILLA_100} />
|
||||
}
|
||||
onClick={handleSubmit}
|
||||
disabled={
|
||||
selectedRegions.length === 0 ||
|
||||
isLoading ||
|
||||
isGeneratingUrl ||
|
||||
modalState === ModalStateEnum.WAITING
|
||||
}
|
||||
>
|
||||
Launch Cloud Formation Template
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}, [
|
||||
modalState,
|
||||
activeView,
|
||||
form,
|
||||
setModalState,
|
||||
selectedRegions,
|
||||
includeAllRegions,
|
||||
handleIncludeAllRegionsChange,
|
||||
handleRegionSelect,
|
||||
handleSubmit,
|
||||
accountId,
|
||||
selectedDeploymentRegion,
|
||||
handleRegionChange,
|
||||
connectionParams,
|
||||
isConnectionParamsLoading,
|
||||
setSelectedRegions,
|
||||
setIncludeAllRegions,
|
||||
isLoading,
|
||||
isGeneratingUrl,
|
||||
handleConnectionSuccess,
|
||||
handleConnectionTimeout,
|
||||
handleConnectionError,
|
||||
]);
|
||||
|
||||
const getSelectedRegionsCount = useCallback(
|
||||
(): number =>
|
||||
selectedRegions.includes('all') ? allRegions.length : selectedRegions.length,
|
||||
[selectedRegions, allRegions],
|
||||
(): number => selectedRegions.length,
|
||||
[selectedRegions],
|
||||
);
|
||||
|
||||
const getModalConfig = useCallback(() => {
|
||||
// Handle success state first
|
||||
if (modalState === ModalStateEnum.SUCCESS) {
|
||||
return {
|
||||
title: 'AWS Integration',
|
||||
okText: (
|
||||
<div className="cloud-account-setup-success-view__footer-button">
|
||||
Continue
|
||||
</div>
|
||||
),
|
||||
block: true,
|
||||
onOk: (): void => {
|
||||
queryClient.invalidateQueries([REACT_QUERY_KEY.AWS_ACCOUNTS]);
|
||||
handleClose();
|
||||
},
|
||||
cancelButtonProps: { style: { display: 'none' } },
|
||||
disabled: false,
|
||||
};
|
||||
}
|
||||
|
||||
// Handle other views
|
||||
const viewConfigs = {
|
||||
[ActiveViewEnum.FORM]: {
|
||||
title: 'Add AWS Account',
|
||||
@@ -155,35 +139,30 @@ function CloudAccountSetupModal({
|
||||
isLoading,
|
||||
isGeneratingUrl,
|
||||
activeView,
|
||||
handleClose,
|
||||
setActiveView,
|
||||
queryClient,
|
||||
]);
|
||||
|
||||
const modalConfig = getModalConfig();
|
||||
|
||||
const handleDrawerOpenChange = (open: boolean): void => {
|
||||
if (!open) {
|
||||
handleClose();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<SignozModal
|
||||
open
|
||||
<DrawerWrapper
|
||||
open={true}
|
||||
type="panel"
|
||||
className="cloud-account-setup-modal"
|
||||
title={modalConfig.title}
|
||||
onCancel={handleClose}
|
||||
onOk={modalConfig.onOk}
|
||||
okText={modalConfig.okText}
|
||||
okButtonProps={{
|
||||
loading: isLoading,
|
||||
disabled: selectedRegions.length === 0 || modalConfig.disabled,
|
||||
className:
|
||||
activeView === ActiveViewEnum.FORM
|
||||
? 'cloud-account-setup-form__submit-button'
|
||||
: 'account-setup-modal-footer__confirm-button',
|
||||
block: activeView === ActiveViewEnum.FORM,
|
||||
content={renderContent()}
|
||||
onOpenChange={handleDrawerOpenChange}
|
||||
direction="right"
|
||||
showCloseButton
|
||||
header={{
|
||||
title: modalConfig.title,
|
||||
}}
|
||||
cancelButtonProps={modalConfig.cancelButtonProps}
|
||||
width={672}
|
||||
>
|
||||
{renderContent()}
|
||||
</SignozModal>
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,17 +1,19 @@
|
||||
import { Dispatch, SetStateAction } from 'react';
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { Form, Select, Switch } from 'antd';
|
||||
import { Form, Select } from 'antd';
|
||||
import { ChevronDown } from 'lucide-react';
|
||||
import { Region } from 'utils/regions';
|
||||
import { popupContainer } from 'utils/selectPopupContainer';
|
||||
|
||||
import { RegionSelector } from './RegionSelector';
|
||||
|
||||
// Form section components
|
||||
function RegionDeploymentSection({
|
||||
regions,
|
||||
selectedDeploymentRegion,
|
||||
handleRegionChange,
|
||||
isFormDisabled,
|
||||
}: {
|
||||
regions: Region[];
|
||||
selectedDeploymentRegion: string | undefined;
|
||||
handleRegionChange: (value: string) => void;
|
||||
isFormDisabled: boolean;
|
||||
}): JSX.Element {
|
||||
@@ -33,8 +35,8 @@ function RegionDeploymentSection({
|
||||
suffixIcon={<ChevronDown size={16} color={Color.BG_VANILLA_400} />}
|
||||
className="cloud-account-setup-form__select integrations-select"
|
||||
onChange={handleRegionChange}
|
||||
value={selectedDeploymentRegion}
|
||||
disabled={isFormDisabled}
|
||||
getPopupContainer={popupContainer}
|
||||
>
|
||||
{regions.flatMap((region) =>
|
||||
region.subRegions.map((subRegion) => (
|
||||
@@ -50,19 +52,13 @@ function RegionDeploymentSection({
|
||||
}
|
||||
|
||||
function MonitoringRegionsSection({
|
||||
includeAllRegions,
|
||||
selectedRegions,
|
||||
onIncludeAllRegionsChange,
|
||||
getRegionPreviewText,
|
||||
onRegionSelect,
|
||||
isFormDisabled,
|
||||
setSelectedRegions,
|
||||
setIncludeAllRegions,
|
||||
}: {
|
||||
includeAllRegions: boolean;
|
||||
selectedRegions: string[];
|
||||
onIncludeAllRegionsChange: (checked: boolean) => void;
|
||||
getRegionPreviewText: (regions: string[]) => string[];
|
||||
onRegionSelect: () => void;
|
||||
isFormDisabled: boolean;
|
||||
setSelectedRegions: Dispatch<SetStateAction<string[]>>;
|
||||
setIncludeAllRegions: Dispatch<SetStateAction<boolean>>;
|
||||
}): JSX.Element {
|
||||
return (
|
||||
<div className="cloud-account-setup-form__form-group">
|
||||
@@ -73,51 +69,12 @@ function MonitoringRegionsSection({
|
||||
Choose only the regions you want SigNoz to monitor. You can enable all at
|
||||
once, or pick specific ones:
|
||||
</div>
|
||||
<Form.Item
|
||||
name="monitorRegions"
|
||||
rules={[
|
||||
{
|
||||
validator: async (): Promise<void> => {
|
||||
if (selectedRegions.length === 0) {
|
||||
return Promise.reject();
|
||||
}
|
||||
return Promise.resolve();
|
||||
},
|
||||
message: 'Please select at least one region to monitor',
|
||||
},
|
||||
]}
|
||||
className="cloud-account-setup-form__form-item"
|
||||
>
|
||||
<div className="cloud-account-setup-form__include-all-regions-switch">
|
||||
<Switch
|
||||
size="small"
|
||||
checked={includeAllRegions}
|
||||
onChange={onIncludeAllRegionsChange}
|
||||
disabled={isFormDisabled}
|
||||
/>
|
||||
<button
|
||||
className="cloud-account-setup-form__include-all-regions-switch-label"
|
||||
type="button"
|
||||
onClick={(): void =>
|
||||
!isFormDisabled
|
||||
? onIncludeAllRegionsChange(!includeAllRegions)
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
Include all regions
|
||||
</button>
|
||||
</div>
|
||||
<Select
|
||||
suffixIcon={null}
|
||||
placeholder="Select Region(s)"
|
||||
className="cloud-account-setup-form__select integrations-select"
|
||||
onClick={!isFormDisabled ? onRegionSelect : undefined}
|
||||
mode="multiple"
|
||||
maxTagCount={3}
|
||||
value={getRegionPreviewText(selectedRegions)}
|
||||
open={false}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<RegionSelector
|
||||
selectedRegions={selectedRegions}
|
||||
setSelectedRegions={setSelectedRegions}
|
||||
setIncludeAllRegions={setIncludeAllRegions}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
import { useRef } from 'react';
|
||||
import { Form } from 'antd';
|
||||
import { useGetAccount } from 'api/generated/services/cloudintegration';
|
||||
import cx from 'classnames';
|
||||
import { INTEGRATION_TYPES } from 'container/Integrations/constants';
|
||||
import { regions } from 'utils/regions';
|
||||
|
||||
import { ModalStateEnum, RegionFormProps } from '../types';
|
||||
import AlertMessage from './AlertMessage';
|
||||
import {
|
||||
ComplianceNote,
|
||||
MonitoringRegionsSection,
|
||||
RegionDeploymentSection,
|
||||
} from './IntegrateNowFormSections';
|
||||
import RenderConnectionFields from './RenderConnectionParams';
|
||||
|
||||
export function RegionForm({
|
||||
form,
|
||||
modalState,
|
||||
selectedRegions,
|
||||
onSubmit,
|
||||
accountId,
|
||||
handleRegionChange,
|
||||
connectionParams,
|
||||
isConnectionParamsLoading,
|
||||
setSelectedRegions,
|
||||
setIncludeAllRegions,
|
||||
onConnectionSuccess,
|
||||
onConnectionTimeout,
|
||||
onConnectionError,
|
||||
}: RegionFormProps): JSX.Element {
|
||||
const startTimeRef = useRef(Date.now());
|
||||
const refetchInterval = 10 * 1000;
|
||||
const errorTimeout = 10 * 60 * 1000;
|
||||
|
||||
const { isLoading: isAccountStatusLoading } = useGetAccount(
|
||||
{
|
||||
cloudProvider: INTEGRATION_TYPES.AWS,
|
||||
id: accountId ?? '',
|
||||
},
|
||||
{
|
||||
query: {
|
||||
refetchInterval,
|
||||
enabled: !!accountId && modalState === ModalStateEnum.WAITING,
|
||||
onSuccess: (response) => {
|
||||
const isConnected =
|
||||
Boolean(response.data.providerAccountId) &&
|
||||
response.data.removedAt === null;
|
||||
|
||||
if (isConnected) {
|
||||
const cloudAccountId =
|
||||
response.data.providerAccountId ?? response.data.id;
|
||||
|
||||
onConnectionSuccess({
|
||||
cloudAccountId,
|
||||
status: response.data.agentReport,
|
||||
});
|
||||
} else if (Date.now() - startTimeRef.current >= errorTimeout) {
|
||||
onConnectionTimeout({ id: accountId });
|
||||
}
|
||||
},
|
||||
onError: () => {
|
||||
onConnectionError();
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const isFormDisabled =
|
||||
modalState === ModalStateEnum.WAITING || isAccountStatusLoading;
|
||||
|
||||
return (
|
||||
<Form
|
||||
form={form}
|
||||
className="cloud-account-setup-form"
|
||||
layout="vertical"
|
||||
onFinish={onSubmit}
|
||||
>
|
||||
<AlertMessage modalState={modalState} />
|
||||
|
||||
<div
|
||||
className={cx(`cloud-account-setup-form__content`, {
|
||||
disabled: isFormDisabled,
|
||||
})}
|
||||
>
|
||||
<RegionDeploymentSection
|
||||
regions={regions}
|
||||
handleRegionChange={handleRegionChange}
|
||||
isFormDisabled={isFormDisabled}
|
||||
/>
|
||||
<MonitoringRegionsSection
|
||||
selectedRegions={selectedRegions}
|
||||
setSelectedRegions={setSelectedRegions}
|
||||
setIncludeAllRegions={setIncludeAllRegions}
|
||||
/>
|
||||
<ComplianceNote />
|
||||
<RenderConnectionFields
|
||||
isConnectionParamsLoading={isConnectionParamsLoading}
|
||||
connectionParams={connectionParams}
|
||||
isFormDisabled={isFormDisabled}
|
||||
/>
|
||||
</div>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
.select-all {
|
||||
margin-bottom: 20px;
|
||||
margin-top: 16px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.regions-grid {
|
||||
@@ -19,3 +20,11 @@
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.region-selector-footer {
|
||||
margin-top: 36px;
|
||||
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
}
|
||||
@@ -28,10 +28,12 @@ export function RegionSelector({
|
||||
<div className="region-selector">
|
||||
<div className="select-all">
|
||||
<Checkbox
|
||||
checked={selectedRegions.includes('all')}
|
||||
checked={
|
||||
allRegionIds.length > 0 &&
|
||||
allRegionIds.every((regionId) => selectedRegions.includes(regionId))
|
||||
}
|
||||
indeterminate={
|
||||
selectedRegions.length > 20 &&
|
||||
selectedRegions.length < allRegionIds.length
|
||||
selectedRegions.length > 0 && selectedRegions.length < allRegionIds.length
|
||||
}
|
||||
onChange={(e): void => handleSelectAll(e.target.checked)}
|
||||
>
|
||||
@@ -46,10 +48,7 @@ export function RegionSelector({
|
||||
{region.subRegions.map((subRegion) => (
|
||||
<Checkbox
|
||||
key={subRegion.id}
|
||||
checked={
|
||||
selectedRegions.includes('all') ||
|
||||
selectedRegions.includes(subRegion.id)
|
||||
}
|
||||
checked={selectedRegions.includes(subRegion.id)}
|
||||
onChange={(): void => handleRegionSelect(subRegion.id)}
|
||||
>
|
||||
{subRegion.name}
|
||||
@@ -0,0 +1,32 @@
|
||||
.remove-integration-account-modal {
|
||||
.ant-modal-content {
|
||||
background-color: var(--l1-background);
|
||||
border: 1px solid var(--l3-background);
|
||||
border-radius: 4px;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.ant-modal-close {
|
||||
color: var(--l1-foreground);
|
||||
}
|
||||
|
||||
.ant-modal-header {
|
||||
background-color: var(--l1-background);
|
||||
color: var(--l1-foreground);
|
||||
|
||||
.ant-modal-title {
|
||||
color: var(--l1-foreground);
|
||||
}
|
||||
}
|
||||
|
||||
.ant-modal-body {
|
||||
margin-top: 16px;
|
||||
color: var(--l1-foreground);
|
||||
background-color: var(--l1-background);
|
||||
}
|
||||
|
||||
.ant-modal-footer {
|
||||
margin-top: 16px;
|
||||
background-color: var(--l1-background);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
import { useState } from 'react';
|
||||
import { Button } from '@signozhq/button';
|
||||
import { Modal } from 'antd/lib';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { useDisconnectAccount } from 'api/generated/services/cloudintegration';
|
||||
import { SOMETHING_WENT_WRONG } from 'constants/api';
|
||||
import { INTEGRATION_TELEMETRY_EVENTS } from 'container/Integrations/constants';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import { Unlink } from 'lucide-react';
|
||||
|
||||
import './RemoveIntegrationAccount.scss';
|
||||
|
||||
function RemoveIntegrationAccount({
|
||||
accountId,
|
||||
onRemoveIntegrationAccountSuccess,
|
||||
}: {
|
||||
accountId: string;
|
||||
onRemoveIntegrationAccountSuccess: () => void;
|
||||
}): JSX.Element {
|
||||
const { notifications } = useNotifications();
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
|
||||
const handleDisconnect = (): void => {
|
||||
setIsModalOpen(true);
|
||||
};
|
||||
|
||||
const {
|
||||
mutate: disconnectAccount,
|
||||
isLoading: isRemoveIntegrationLoading,
|
||||
} = useDisconnectAccount({
|
||||
mutation: {
|
||||
onSuccess: () => {
|
||||
onRemoveIntegrationAccountSuccess?.();
|
||||
setIsModalOpen(false);
|
||||
},
|
||||
onError: () => {
|
||||
notifications.error({
|
||||
message: SOMETHING_WENT_WRONG,
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
const handleOk = (): void => {
|
||||
logEvent(INTEGRATION_TELEMETRY_EVENTS.AWS_INTEGRATION_ACCOUNT_REMOVED, {
|
||||
accountId,
|
||||
});
|
||||
disconnectAccount({
|
||||
pathParams: {
|
||||
cloudProvider: 'aws',
|
||||
id: accountId,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleCancel = (): void => {
|
||||
setIsModalOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="remove-integration-account-container">
|
||||
<Button
|
||||
variant="solid"
|
||||
color="destructive"
|
||||
prefixIcon={<Unlink size={14} />}
|
||||
size="sm"
|
||||
onClick={handleDisconnect}
|
||||
disabled={isRemoveIntegrationLoading}
|
||||
>
|
||||
Disconnect
|
||||
</Button>
|
||||
|
||||
<Modal
|
||||
className="remove-integration-account-modal"
|
||||
open={isModalOpen}
|
||||
title="Remove integration"
|
||||
onOk={handleOk}
|
||||
onCancel={handleCancel}
|
||||
okText="Remove Account"
|
||||
okButtonProps={{
|
||||
danger: true,
|
||||
loading: isRemoveIntegrationLoading,
|
||||
}}
|
||||
>
|
||||
Removing this account will remove all components created for sending
|
||||
telemetry to SigNoz in your AWS account within the next ~15 minutes
|
||||
(cloudformation stacks named signoz-integration-telemetry-collection in
|
||||
enabled regions). <br />
|
||||
<br />
|
||||
After that, you can delete the cloudformation stack that was created
|
||||
manually when connecting this account.
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default RemoveIntegrationAccount;
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Form, Input } from 'antd';
|
||||
import { ConnectionParams } from 'types/api/integrations/aws';
|
||||
import { CloudintegrationtypesCredentialsDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
|
||||
function RenderConnectionFields({
|
||||
isConnectionParamsLoading,
|
||||
@@ -7,51 +7,51 @@ function RenderConnectionFields({
|
||||
isFormDisabled,
|
||||
}: {
|
||||
isConnectionParamsLoading?: boolean;
|
||||
connectionParams?: ConnectionParams | null;
|
||||
connectionParams?: CloudintegrationtypesCredentialsDTO | null;
|
||||
isFormDisabled?: boolean;
|
||||
}): JSX.Element | null {
|
||||
if (
|
||||
isConnectionParamsLoading ||
|
||||
(!!connectionParams?.ingestion_url &&
|
||||
!!connectionParams?.ingestion_key &&
|
||||
!!connectionParams?.signoz_api_url &&
|
||||
!!connectionParams?.signoz_api_key)
|
||||
(!!connectionParams?.ingestionUrl &&
|
||||
!!connectionParams?.ingestionKey &&
|
||||
!!connectionParams?.sigNozApiUrl &&
|
||||
!!connectionParams?.sigNozApiKey)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Form.Item name="connection_params">
|
||||
{!connectionParams?.ingestion_url && (
|
||||
<Form.Item name="connectionParams">
|
||||
{!connectionParams?.ingestionUrl && (
|
||||
<Form.Item
|
||||
name="ingestion_url"
|
||||
name="ingestionUrl"
|
||||
label="Ingestion URL"
|
||||
rules={[{ required: true, message: 'Please enter ingestion URL' }]}
|
||||
>
|
||||
<Input placeholder="Enter ingestion URL" disabled={isFormDisabled} />
|
||||
</Form.Item>
|
||||
)}
|
||||
{!connectionParams?.ingestion_key && (
|
||||
{!connectionParams?.ingestionKey && (
|
||||
<Form.Item
|
||||
name="ingestion_key"
|
||||
name="ingestionKey"
|
||||
label="Ingestion Key"
|
||||
rules={[{ required: true, message: 'Please enter ingestion key' }]}
|
||||
>
|
||||
<Input placeholder="Enter ingestion key" disabled={isFormDisabled} />
|
||||
</Form.Item>
|
||||
)}
|
||||
{!connectionParams?.signoz_api_url && (
|
||||
{!connectionParams?.sigNozApiUrl && (
|
||||
<Form.Item
|
||||
name="signoz_api_url"
|
||||
name="sigNozApiUrl"
|
||||
label="SigNoz API URL"
|
||||
rules={[{ required: true, message: 'Please enter SigNoz API URL' }]}
|
||||
>
|
||||
<Input placeholder="Enter SigNoz API URL" disabled={isFormDisabled} />
|
||||
</Form.Item>
|
||||
)}
|
||||
{!connectionParams?.signoz_api_key && (
|
||||
{!connectionParams?.sigNozApiKey && (
|
||||
<Form.Item
|
||||
name="signoz_api_key"
|
||||
name="sigNozApiKey"
|
||||
label="SigNoz API KEY"
|
||||
rules={[{ required: true, message: 'Please enter SigNoz API Key' }]}
|
||||
>
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Dispatch, SetStateAction } from 'react';
|
||||
import { FormInstance } from 'antd';
|
||||
import { ConnectionParams } from 'types/api/integrations/aws';
|
||||
import { CloudintegrationtypesCredentialsDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
|
||||
export enum ActiveViewEnum {
|
||||
SELECT_REGIONS = 'select-regions',
|
||||
@@ -11,23 +11,27 @@ export enum ModalStateEnum {
|
||||
FORM = 'form',
|
||||
WAITING = 'waiting',
|
||||
ERROR = 'error',
|
||||
SUCCESS = 'success',
|
||||
}
|
||||
|
||||
export interface RegionFormProps {
|
||||
form: FormInstance;
|
||||
modalState: ModalStateEnum;
|
||||
setModalState: Dispatch<SetStateAction<ModalStateEnum>>;
|
||||
selectedRegions: string[];
|
||||
includeAllRegions: boolean;
|
||||
onIncludeAllRegionsChange: (checked: boolean) => void;
|
||||
onRegionSelect: () => void;
|
||||
onSubmit: () => Promise<void>;
|
||||
accountId?: string;
|
||||
selectedDeploymentRegion: string | undefined;
|
||||
handleRegionChange: (value: string) => void;
|
||||
connectionParams?: ConnectionParams;
|
||||
connectionParams?: CloudintegrationtypesCredentialsDTO;
|
||||
isConnectionParamsLoading?: boolean;
|
||||
setSelectedRegions: Dispatch<SetStateAction<string[]>>;
|
||||
setIncludeAllRegions: Dispatch<SetStateAction<boolean>>;
|
||||
onConnectionSuccess: (payload: {
|
||||
cloudAccountId: string;
|
||||
status?: unknown;
|
||||
}) => void;
|
||||
onConnectionTimeout: (payload: { id?: string }) => void;
|
||||
onConnectionError: () => void;
|
||||
}
|
||||
|
||||
export interface IntegrationModalProps {
|
||||
@@ -0,0 +1,53 @@
|
||||
.s3-buckets-selector {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
padding: 12px;
|
||||
background: var(--l2-background);
|
||||
border-radius: 4px;
|
||||
|
||||
.s3-buckets-selector-title {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
font-size: 12px;
|
||||
color: var(--l2-foreground);
|
||||
}
|
||||
|
||||
.s3-buckets-selector-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
|
||||
.s3-buckets-selector-region {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
|
||||
.s3-buckets-selector-region-header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.s3-buckets-selector-region-help {
|
||||
color: var(--l2-foreground);
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
letter-spacing: -0.06px;
|
||||
}
|
||||
|
||||
.s3-buckets-selector-region-select {
|
||||
flex: 1;
|
||||
|
||||
.ant-select {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,18 @@
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import { Form, Select, Skeleton, Typography } from 'antd';
|
||||
import { useAwsAccounts } from 'hooks/integration/aws/useAwsAccounts';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { Select, Skeleton } from 'antd';
|
||||
import { useListAccounts } from 'api/generated/services/cloudintegration';
|
||||
import { INTEGRATION_TYPES } from 'container/Integrations/constants';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
|
||||
const { Title } = Typography;
|
||||
import { mapAccountDtoToAwsCloudAccount } from '../mapAwsCloudAccountFromDto';
|
||||
import { CloudAccount } from '../types';
|
||||
|
||||
import './S3BucketsSelector.styles.scss';
|
||||
|
||||
interface S3BucketsSelectorProps {
|
||||
onChange?: (bucketsByRegion: Record<string, string[]>) => void;
|
||||
initialBucketsByRegion?: Record<string, string[]>;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -17,13 +22,29 @@ interface S3BucketsSelectorProps {
|
||||
function S3BucketsSelector({
|
||||
onChange,
|
||||
initialBucketsByRegion = {},
|
||||
disabled: isSelectorDisabled = false,
|
||||
}: S3BucketsSelectorProps): JSX.Element {
|
||||
const cloudAccountId = useUrlQuery().get('cloudAccountId');
|
||||
const { data: accounts, isLoading } = useAwsAccounts();
|
||||
const { data: listAccountsResponse, isLoading } = useListAccounts({
|
||||
cloudProvider: INTEGRATION_TYPES.AWS,
|
||||
});
|
||||
const accounts = useMemo((): CloudAccount[] | undefined => {
|
||||
const raw = listAccountsResponse?.data?.accounts;
|
||||
if (!raw) {
|
||||
return undefined;
|
||||
}
|
||||
return raw
|
||||
.map(mapAccountDtoToAwsCloudAccount)
|
||||
.filter((account): account is CloudAccount => account !== null);
|
||||
}, [listAccountsResponse]);
|
||||
const [bucketsByRegion, setBucketsByRegion] = useState<
|
||||
Record<string, string[]>
|
||||
>(initialBucketsByRegion);
|
||||
|
||||
useEffect(() => {
|
||||
setBucketsByRegion(initialBucketsByRegion);
|
||||
}, [initialBucketsByRegion]);
|
||||
|
||||
// Find the active AWS account based on the URL query parameter
|
||||
const activeAccount = useMemo(
|
||||
() =>
|
||||
@@ -81,37 +102,41 @@ function S3BucketsSelector({
|
||||
|
||||
return (
|
||||
<div className="s3-buckets-selector">
|
||||
<Title level={5}>Select S3 Buckets by Region</Title>
|
||||
<div className="s3-buckets-selector-title">Select S3 Buckets by Region</div>
|
||||
<div className="s3-buckets-selector-content">
|
||||
{allRegions.map((region) => {
|
||||
const isRegionUnavailable = isRegionDisabled(region);
|
||||
|
||||
{allRegions.map((region) => {
|
||||
const disabled = isRegionDisabled(region);
|
||||
|
||||
return (
|
||||
<Form.Item
|
||||
key={region}
|
||||
label={region}
|
||||
{...(disabled && {
|
||||
help:
|
||||
'Region disabled in account settings; S3 buckets here will not be synced.',
|
||||
validateStatus: 'warning',
|
||||
})}
|
||||
>
|
||||
<Select
|
||||
mode="tags"
|
||||
placeholder={`Enter S3 bucket names for ${region}`}
|
||||
value={bucketsByRegion[region] || []}
|
||||
onChange={(value): void => handleRegionBucketsChange(region, value)}
|
||||
tokenSeparators={[',']}
|
||||
allowClear
|
||||
disabled={disabled}
|
||||
suffixIcon={null}
|
||||
notFoundContent={null}
|
||||
filterOption={false}
|
||||
showSearch
|
||||
/>
|
||||
</Form.Item>
|
||||
);
|
||||
})}
|
||||
return (
|
||||
<div key={region} className="s3-buckets-selector-region">
|
||||
<div className="s3-buckets-selector-region-header">
|
||||
<div className="s3-buckets-selector-region-label">{region}</div>
|
||||
{isRegionUnavailable && (
|
||||
<div className="s3-buckets-selector-region-help">
|
||||
Region disabled in account settings; S3 buckets here will not be
|
||||
synced.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="s3-buckets-selector-region-select">
|
||||
<Select
|
||||
mode="tags"
|
||||
placeholder={`Enter S3 bucket names for ${region}`}
|
||||
value={bucketsByRegion[region] || []}
|
||||
onChange={(value): void => handleRegionBucketsChange(region, value)}
|
||||
tokenSeparators={[',']}
|
||||
allowClear
|
||||
disabled={isSelectorDisabled || isRegionUnavailable}
|
||||
suffixIcon={null}
|
||||
notFoundContent={null}
|
||||
filterOption={false}
|
||||
showSearch
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
.aws-service-dashboards {
|
||||
border-radius: 6px;
|
||||
border: 1px solid var(--l3-background);
|
||||
background: var(--l1-background);
|
||||
|
||||
.aws-service-dashboards-title {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
color: var(--l2-foreground);
|
||||
font-family: Inter;
|
||||
font-size: 13px;
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
line-height: 20px; /* 142.857% */
|
||||
letter-spacing: -0.07px;
|
||||
|
||||
padding: 8px 16px;
|
||||
border-bottom: 1px solid var(--l3-background);
|
||||
}
|
||||
|
||||
.aws-service-dashboards-items {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.aws-service-dashboard-item {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
padding: 12px 16px 12px 16px;
|
||||
border-bottom: 1px solid var(--l3-background);
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
&.aws-service-dashboard-item-clickable {
|
||||
cursor: pointer;
|
||||
transition: background-color 0.15s ease;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--muted);
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline: 1px solid var(--primary-background);
|
||||
outline-offset: -1px;
|
||||
}
|
||||
}
|
||||
|
||||
.aws-service-dashboard-item-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
width: 100%;
|
||||
|
||||
.aws-service-dashboard-item-title {
|
||||
color: var(--l1-foreground);
|
||||
font-family: Inter;
|
||||
font-size: 13px;
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
line-height: 20px; /* 142.857% */
|
||||
letter-spacing: -0.07px;
|
||||
text-align: left;
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.aws-service-dashboard-item-description {
|
||||
color: var(--l2-foreground);
|
||||
font-family: Inter;
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 18px; /* 150% */
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
|
||||
.aws-service-dashboard-item-open-new-tab {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
padding: 4px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
background: transparent;
|
||||
color: var(--l2-foreground);
|
||||
cursor: pointer;
|
||||
opacity: 0.8;
|
||||
margin-top: 1px;
|
||||
|
||||
&:hover {
|
||||
background: var(--secondary);
|
||||
color: var(--l1-foreground);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
/* eslint-disable sonarjs/cognitive-complexity */
|
||||
import {
|
||||
CloudintegrationtypesDashboardDTO,
|
||||
CloudintegrationtypesServiceDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
import { useSafeNavigate } from 'hooks/useSafeNavigate';
|
||||
|
||||
import './ServiceDashboards.styles.scss';
|
||||
|
||||
function ServiceDashboards({
|
||||
service,
|
||||
isInteractive = true,
|
||||
}: {
|
||||
service: Pick<CloudintegrationtypesServiceDTO, 'assets'>;
|
||||
isInteractive?: boolean;
|
||||
}): JSX.Element {
|
||||
const dashboards = service?.assets?.dashboards || [];
|
||||
const { safeNavigate } = useSafeNavigate();
|
||||
if (!dashboards.length) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="aws-service-dashboards">
|
||||
<div className="aws-service-dashboards-title">Dashboards</div>
|
||||
<div className="aws-service-dashboards-items">
|
||||
{dashboards.map((dashboard: CloudintegrationtypesDashboardDTO) => {
|
||||
if (!dashboard.id) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const dashboardUrl = `/dashboard/${dashboard.id}`;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={dashboard.id}
|
||||
className={`aws-service-dashboard-item ${
|
||||
isInteractive ? 'aws-service-dashboard-item-clickable' : ''
|
||||
}`}
|
||||
role={isInteractive ? 'button' : undefined}
|
||||
tabIndex={isInteractive ? 0 : -1}
|
||||
onClick={(event): void => {
|
||||
if (!isInteractive) {
|
||||
return;
|
||||
}
|
||||
if (event.metaKey || event.ctrlKey) {
|
||||
window.open(dashboardUrl, '_blank', 'noopener,noreferrer');
|
||||
return;
|
||||
}
|
||||
safeNavigate(dashboardUrl);
|
||||
}}
|
||||
onAuxClick={(event): void => {
|
||||
if (!isInteractive) {
|
||||
return;
|
||||
}
|
||||
if (event.button === 1) {
|
||||
window.open(dashboardUrl, '_blank', 'noopener,noreferrer');
|
||||
}
|
||||
}}
|
||||
onKeyDown={(event): void => {
|
||||
if (!isInteractive) {
|
||||
return;
|
||||
}
|
||||
if (event.key === 'Enter' || event.key === ' ') {
|
||||
event.preventDefault();
|
||||
safeNavigate(dashboardUrl);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className="aws-service-dashboard-item-content">
|
||||
<div className="aws-service-dashboard-item-title">
|
||||
{dashboard.title}
|
||||
</div>
|
||||
<div className="aws-service-dashboard-item-description">
|
||||
{dashboard.description}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ServiceDashboards;
|
||||
@@ -0,0 +1,215 @@
|
||||
.aws-service-details-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
|
||||
.aws-service-details-tabs {
|
||||
margin-top: 8px;
|
||||
|
||||
// remove the padding left from the first div of the tabs component
|
||||
// this needs to be handled in the tabs component
|
||||
> div:first-child {
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
.aws-service-details-data-collected-content-logs,
|
||||
.aws-service-details-data-collected-content-metrics {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 8px;
|
||||
|
||||
.aws-service-details-data-collected-content-title {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
color: var(--l2-foreground);
|
||||
|
||||
/* Bifrost (Ancient)/Content/sm */
|
||||
font-family: Inter;
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 20px; /* 142.857% */
|
||||
letter-spacing: -0.07px;
|
||||
}
|
||||
}
|
||||
|
||||
.aws-service-details-overview {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
|
||||
.aws-service-details-overview-configuration {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--l3-background);
|
||||
background: var(--l1-background);
|
||||
|
||||
.aws-service-details-overview-configuration-title {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
|
||||
border-radius: 4px 4px 0 0;
|
||||
|
||||
padding: 8px 12px;
|
||||
|
||||
.aws-service-details-overview-configuration-title-text {
|
||||
color: var(--l1-foreground);
|
||||
font-family: Inter;
|
||||
font-size: 13px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 22px; /* 157.143% */
|
||||
letter-spacing: -0.07px;
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.configuration-action {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.aws-service-details-overview-configuration-s3-buckets {
|
||||
padding: 12px;
|
||||
background: var(--l1-background);
|
||||
}
|
||||
|
||||
.aws-service-details-overview-configuration-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
|
||||
padding: 12px;
|
||||
background: var(--l1-background);
|
||||
|
||||
.aws-service-details-overview-configuration-content-item {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.aws-service-details-overview-configuration-actions {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
padding: 8px 12px;
|
||||
|
||||
border-top: 1px solid var(--l3-background);
|
||||
background: var(--l1-background);
|
||||
|
||||
.discard-btn {
|
||||
width: 100px;
|
||||
}
|
||||
|
||||
.save-btn {
|
||||
width: 100px;
|
||||
}
|
||||
}
|
||||
|
||||
.aws-service-details-overview-configuration-title-text-select-all {
|
||||
color: var(--l2-foreground);
|
||||
font-family: Inter;
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 18px; /* 138.462% */
|
||||
}
|
||||
}
|
||||
|
||||
.aws-service-details-overview-markdown {
|
||||
padding: 12px;
|
||||
background: var(--l1-background);
|
||||
color: var(--l1-foreground);
|
||||
}
|
||||
|
||||
.aws-service-details-actions {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 8px;
|
||||
padding: 12px 0;
|
||||
}
|
||||
|
||||
.aws-service-dashboards {
|
||||
border-radius: 6px;
|
||||
border: 1px solid var(--l3-background);
|
||||
background: var(--l1-background);
|
||||
|
||||
.aws-service-dashboards-title {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
color: var(--l2-foreground);
|
||||
font-family: Inter;
|
||||
font-size: 13px;
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
line-height: 20px; /* 142.857% */
|
||||
letter-spacing: -0.07px;
|
||||
|
||||
padding: 8px 16px;
|
||||
border-bottom: 1px solid var(--l3-background);
|
||||
}
|
||||
|
||||
.aws-service-dashboards-items {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.aws-service-dashboard-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
padding: 12px 16px 12px 16px;
|
||||
|
||||
&.aws-service-dashboard-item-clickable {
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--l2-background);
|
||||
}
|
||||
}
|
||||
|
||||
.aws-service-dashboard-item-title {
|
||||
color: var(--l1-foreground);
|
||||
font-family: Inter;
|
||||
font-size: 13px;
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
line-height: 20px; /* 142.857% */
|
||||
letter-spacing: -0.07px;
|
||||
}
|
||||
|
||||
.aws-service-dashboard-item-description {
|
||||
color: var(--l2-foreground);
|
||||
font-family: Inter;
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 18px; /* 150% */
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,430 @@
|
||||
import { useCallback, useEffect } from 'react';
|
||||
import { Controller, useForm } from 'react-hook-form';
|
||||
import { useQueryClient } from 'react-query';
|
||||
import { Button } from '@signozhq/button';
|
||||
import Tabs from '@signozhq/tabs';
|
||||
import { toast } from '@signozhq/ui';
|
||||
import { Switch } from '@signozhq/ui';
|
||||
import { Skeleton } from 'antd';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import {
|
||||
getListServicesMetadataQueryKey,
|
||||
invalidateGetService,
|
||||
invalidateListServicesMetadata,
|
||||
useGetService,
|
||||
useUpdateService,
|
||||
} from 'api/generated/services/cloudintegration';
|
||||
import {
|
||||
CloudintegrationtypesServiceDTO,
|
||||
ListServicesMetadata200,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
import CloudServiceDataCollected from 'components/CloudIntegrations/CloudServiceDataCollected/CloudServiceDataCollected';
|
||||
import { MarkdownRenderer } from 'components/MarkdownRenderer/MarkdownRenderer';
|
||||
import ServiceDashboards from 'container/Integrations/CloudIntegration/AmazonWebServices/ServiceDashboards/ServiceDashboards';
|
||||
import { INTEGRATION_TYPES } from 'container/Integrations/constants';
|
||||
import { IServiceStatus } from 'container/Integrations/types';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
import { Save, X } from 'lucide-react';
|
||||
|
||||
import S3BucketsSelector from '../S3BucketsSelector/S3BucketsSelector';
|
||||
|
||||
import './ServiceDetails.styles.scss';
|
||||
|
||||
type ServiceConfigFormValues = {
|
||||
logsEnabled: boolean;
|
||||
metricsEnabled: boolean;
|
||||
s3BucketsByRegion: Record<string, string[]>;
|
||||
};
|
||||
type ServiceDetailsData = CloudintegrationtypesServiceDTO & {
|
||||
status?: IServiceStatus;
|
||||
};
|
||||
|
||||
function ServiceDetails(): JSX.Element | null {
|
||||
const urlQuery = useUrlQuery();
|
||||
const cloudAccountId = urlQuery.get('cloudAccountId');
|
||||
const serviceId = urlQuery.get('service');
|
||||
const isReadOnly = !cloudAccountId;
|
||||
const serviceQueryParams = cloudAccountId
|
||||
? { cloud_integration_id: cloudAccountId }
|
||||
: undefined;
|
||||
|
||||
const {
|
||||
queryKey: _queryKey,
|
||||
data: serviceDetailsData,
|
||||
isLoading: isServiceDetailsLoading,
|
||||
} = useGetService(
|
||||
{
|
||||
cloudProvider: INTEGRATION_TYPES.AWS,
|
||||
serviceId: serviceId || '',
|
||||
},
|
||||
{
|
||||
...serviceQueryParams,
|
||||
},
|
||||
{
|
||||
query: {
|
||||
enabled: !!serviceId,
|
||||
select: (response): ServiceDetailsData => response.data,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const awsConfig = serviceDetailsData?.cloudIntegrationService?.config?.aws;
|
||||
const isServiceEnabledInPersistedConfig =
|
||||
Boolean(awsConfig?.logs?.enabled) || Boolean(awsConfig?.metrics?.enabled);
|
||||
const serviceDetailsId = serviceDetailsData?.id;
|
||||
|
||||
const {
|
||||
control,
|
||||
handleSubmit: handleFormSubmit,
|
||||
reset,
|
||||
watch,
|
||||
formState: { isDirty },
|
||||
} = useForm<ServiceConfigFormValues>({
|
||||
defaultValues: {
|
||||
logsEnabled: awsConfig?.logs?.enabled || false,
|
||||
metricsEnabled: awsConfig?.metrics?.enabled || false,
|
||||
s3BucketsByRegion: awsConfig?.logs?.s3Buckets || {},
|
||||
},
|
||||
});
|
||||
|
||||
const resetToAwsConfig = useCallback((): void => {
|
||||
reset({
|
||||
logsEnabled: awsConfig?.logs?.enabled || false,
|
||||
metricsEnabled: awsConfig?.metrics?.enabled || false,
|
||||
s3BucketsByRegion: awsConfig?.logs?.s3Buckets || {},
|
||||
});
|
||||
}, [awsConfig, reset]);
|
||||
|
||||
// Ensure form state does not leak across service switches while new details load.
|
||||
useEffect(() => {
|
||||
reset({
|
||||
logsEnabled: false,
|
||||
metricsEnabled: false,
|
||||
s3BucketsByRegion: {},
|
||||
});
|
||||
}, [reset, serviceId]);
|
||||
|
||||
useEffect(() => {
|
||||
resetToAwsConfig();
|
||||
}, [resetToAwsConfig, serviceDetailsId]);
|
||||
|
||||
// log telemetry event on visiting details of a service.
|
||||
useEffect(() => {
|
||||
if (serviceId) {
|
||||
logEvent('AWS Integration: Service viewed', {
|
||||
cloudAccountId,
|
||||
serviceId,
|
||||
});
|
||||
}
|
||||
}, [cloudAccountId, serviceId]);
|
||||
|
||||
const {
|
||||
mutate: updateService,
|
||||
isLoading: isUpdatingServiceConfig,
|
||||
} = useUpdateService();
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const handleDiscard = useCallback((): void => {
|
||||
resetToAwsConfig();
|
||||
}, [resetToAwsConfig]);
|
||||
|
||||
const onSubmit = useCallback(
|
||||
async (values: ServiceConfigFormValues): Promise<void> => {
|
||||
const { logsEnabled, metricsEnabled, s3BucketsByRegion } = values;
|
||||
const shouldClearS3Buckets = serviceId === 's3sync' && !logsEnabled;
|
||||
const normalizedS3BucketsByRegion = shouldClearS3Buckets
|
||||
? {}
|
||||
: s3BucketsByRegion;
|
||||
const nextFormValues: ServiceConfigFormValues = {
|
||||
...values,
|
||||
s3BucketsByRegion: normalizedS3BucketsByRegion,
|
||||
};
|
||||
|
||||
try {
|
||||
if (!serviceId || !cloudAccountId) {
|
||||
return;
|
||||
}
|
||||
|
||||
updateService(
|
||||
{
|
||||
pathParams: {
|
||||
cloudProvider: INTEGRATION_TYPES.AWS,
|
||||
id: cloudAccountId,
|
||||
serviceId,
|
||||
},
|
||||
data: {
|
||||
config: {
|
||||
aws: {
|
||||
logs: {
|
||||
enabled: logsEnabled,
|
||||
s3Buckets: normalizedS3BucketsByRegion,
|
||||
},
|
||||
metrics: {
|
||||
enabled: metricsEnabled,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
// Immediately sync form state to remove dirty flag and hide actions,
|
||||
// instead of waiting for the refetch to complete.
|
||||
reset(nextFormValues);
|
||||
|
||||
const servicesListQueryKey = getListServicesMetadataQueryKey(
|
||||
{
|
||||
cloudProvider: INTEGRATION_TYPES.AWS,
|
||||
},
|
||||
{
|
||||
cloud_integration_id: cloudAccountId,
|
||||
},
|
||||
);
|
||||
|
||||
queryClient.setQueryData<ListServicesMetadata200 | undefined>(
|
||||
servicesListQueryKey,
|
||||
(prev) => {
|
||||
if (!prev?.data?.services?.length) {
|
||||
return prev;
|
||||
}
|
||||
|
||||
const isServiceEnabled = logsEnabled || metricsEnabled;
|
||||
|
||||
return {
|
||||
...prev,
|
||||
data: {
|
||||
...prev.data,
|
||||
services: prev.data.services.map((service) =>
|
||||
service.id === serviceId
|
||||
? { ...service, enabled: isServiceEnabled }
|
||||
: service,
|
||||
),
|
||||
},
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
invalidateGetService(
|
||||
queryClient,
|
||||
{
|
||||
cloudProvider: INTEGRATION_TYPES.AWS,
|
||||
serviceId,
|
||||
},
|
||||
{
|
||||
cloud_integration_id: cloudAccountId,
|
||||
},
|
||||
);
|
||||
|
||||
invalidateListServicesMetadata(
|
||||
queryClient,
|
||||
{
|
||||
cloudProvider: INTEGRATION_TYPES.AWS,
|
||||
},
|
||||
{
|
||||
cloud_integration_id: cloudAccountId,
|
||||
},
|
||||
);
|
||||
|
||||
logEvent('AWS Integration: Service settings saved', {
|
||||
cloudAccountId,
|
||||
serviceId,
|
||||
logsEnabled,
|
||||
metricsEnabled,
|
||||
});
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error('Failed to update service config:', error);
|
||||
|
||||
toast.error('Failed to update service config', {
|
||||
description: error?.message,
|
||||
});
|
||||
},
|
||||
},
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('Form submission failed:', error);
|
||||
}
|
||||
},
|
||||
[serviceId, cloudAccountId, updateService, queryClient, reset],
|
||||
);
|
||||
|
||||
if (isServiceDetailsLoading) {
|
||||
return (
|
||||
<div className="service-details-loading">
|
||||
<Skeleton active />
|
||||
<Skeleton active />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!serviceDetailsData) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||
const renderOverview = (): JSX.Element => {
|
||||
const logsEnabled = watch('logsEnabled');
|
||||
const s3BucketsByRegion = watch('s3BucketsByRegion');
|
||||
|
||||
const isLogsSupported = serviceDetailsData?.supportedSignals?.logs || false;
|
||||
const isMetricsSupported =
|
||||
serviceDetailsData?.supportedSignals?.metrics || false;
|
||||
|
||||
const hasUnsavedChanges = isDirty;
|
||||
|
||||
const isS3SyncBucketsMissing =
|
||||
serviceId === 's3sync' &&
|
||||
logsEnabled &&
|
||||
(!s3BucketsByRegion || Object.keys(s3BucketsByRegion).length === 0);
|
||||
|
||||
return (
|
||||
<div className="aws-service-details-overview ">
|
||||
{!isServiceDetailsLoading && (
|
||||
<form
|
||||
className="aws-service-details-overview-configuration"
|
||||
onSubmit={handleFormSubmit(onSubmit)}
|
||||
>
|
||||
{isLogsSupported && (
|
||||
<div className="aws-service-details-overview-configuration-logs">
|
||||
<div className="aws-service-details-overview-configuration-title">
|
||||
<div className="aws-service-details-overview-configuration-title-text">
|
||||
<span>Log Collection</span>
|
||||
</div>
|
||||
<div className="configuration-action">
|
||||
<Controller<ServiceConfigFormValues, 'logsEnabled'>
|
||||
control={control}
|
||||
name="logsEnabled"
|
||||
render={({ field }): JSX.Element => (
|
||||
<Switch
|
||||
value={field.value}
|
||||
disabled={isUpdatingServiceConfig || isReadOnly}
|
||||
onChange={(checked): void => {
|
||||
field.onChange(checked);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{logsEnabled && serviceId === 's3sync' && (
|
||||
<div className="aws-service-details-overview-configuration-s3-buckets">
|
||||
<Controller<ServiceConfigFormValues, 's3BucketsByRegion'>
|
||||
control={control}
|
||||
name="s3BucketsByRegion"
|
||||
render={({ field }): JSX.Element => (
|
||||
<S3BucketsSelector
|
||||
initialBucketsByRegion={field.value}
|
||||
onChange={field.onChange}
|
||||
disabled={isReadOnly}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isMetricsSupported && (
|
||||
<div className="aws-service-details-overview-configuration-metrics">
|
||||
<div className="aws-service-details-overview-configuration-title">
|
||||
<div className="aws-service-details-overview-configuration-title-text">
|
||||
<span>Metric Collection</span>
|
||||
</div>
|
||||
<div className="configuration-action">
|
||||
<Controller<ServiceConfigFormValues, 'metricsEnabled'>
|
||||
control={control}
|
||||
name="metricsEnabled"
|
||||
render={({ field }): JSX.Element => (
|
||||
<Switch
|
||||
value={field.value}
|
||||
disabled={isUpdatingServiceConfig || isReadOnly}
|
||||
onChange={field.onChange}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{hasUnsavedChanges && !isReadOnly && (
|
||||
<div className="aws-service-details-overview-configuration-actions">
|
||||
<Button
|
||||
variant="solid"
|
||||
color="secondary"
|
||||
onClick={handleDiscard}
|
||||
disabled={isUpdatingServiceConfig}
|
||||
size="xs"
|
||||
prefixIcon={<X size={14} />}
|
||||
className="discard-btn"
|
||||
type="button"
|
||||
>
|
||||
Discard
|
||||
</Button>
|
||||
<Button
|
||||
variant="solid"
|
||||
color="primary"
|
||||
size="xs"
|
||||
className="save-btn"
|
||||
prefixIcon={<Save size={14} />}
|
||||
type="submit"
|
||||
loading={isUpdatingServiceConfig}
|
||||
disabled={isS3SyncBucketsMissing || isUpdatingServiceConfig}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</form>
|
||||
)}
|
||||
|
||||
<MarkdownRenderer
|
||||
variables={{}}
|
||||
markdownContent={serviceDetailsData?.overview}
|
||||
className="aws-service-details-overview-markdown"
|
||||
/>
|
||||
<ServiceDashboards
|
||||
service={serviceDetailsData}
|
||||
isInteractive={!isReadOnly && isServiceEnabledInPersistedConfig}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const renderDataCollected = (): JSX.Element => {
|
||||
return (
|
||||
<div className="aws-service-details-data-collected-table">
|
||||
<CloudServiceDataCollected
|
||||
logsData={serviceDetailsData?.dataCollected?.logs || []}
|
||||
metricsData={serviceDetailsData?.dataCollected?.metrics || []}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="aws-service-details-container">
|
||||
<Tabs
|
||||
defaultValue="overview"
|
||||
className="aws-service-details-tabs"
|
||||
items={[
|
||||
{
|
||||
children: renderOverview(),
|
||||
key: 'overview',
|
||||
label: 'Overview',
|
||||
},
|
||||
{
|
||||
children: renderDataCollected(),
|
||||
key: 'data-collected',
|
||||
label: 'Data Collected',
|
||||
},
|
||||
]}
|
||||
variant="secondary"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ServiceDetails;
|
||||
@@ -0,0 +1,155 @@
|
||||
import { useCallback, useEffect, useMemo } from 'react';
|
||||
import { useNavigate } from 'react-router-dom-v5-compat';
|
||||
import { Skeleton } from 'antd';
|
||||
import { useListServicesMetadata } from 'api/generated/services/cloudintegration';
|
||||
import type { CloudintegrationtypesServiceMetadataDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import cx from 'classnames';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
|
||||
import emptyStateIconUrl from '@/assets/Icons/emptyState.svg';
|
||||
|
||||
interface ServicesListProps {
|
||||
cloudAccountId: string;
|
||||
}
|
||||
|
||||
function ServicesList({ cloudAccountId }: ServicesListProps): JSX.Element {
|
||||
const urlQuery = useUrlQuery();
|
||||
const navigate = useNavigate();
|
||||
const hasValidCloudAccountId = Boolean(cloudAccountId);
|
||||
const serviceQueryParams = hasValidCloudAccountId
|
||||
? { cloud_integration_id: cloudAccountId }
|
||||
: undefined;
|
||||
|
||||
const { data: servicesMetadata, isLoading } = useListServicesMetadata(
|
||||
{
|
||||
cloudProvider: 'aws',
|
||||
},
|
||||
serviceQueryParams,
|
||||
);
|
||||
|
||||
const awsServices = useMemo(() => servicesMetadata?.data?.services ?? [], [
|
||||
servicesMetadata,
|
||||
]);
|
||||
|
||||
const activeService = urlQuery.get('service');
|
||||
|
||||
const handleActiveService = useCallback(
|
||||
(serviceId: string): void => {
|
||||
const latestUrlQuery = new URLSearchParams(window.location.search);
|
||||
latestUrlQuery.set('service', serviceId);
|
||||
navigate({ search: latestUrlQuery.toString() });
|
||||
},
|
||||
[navigate],
|
||||
);
|
||||
|
||||
const enabledServices = useMemo(
|
||||
() => awsServices.filter((service) => service.enabled),
|
||||
[awsServices],
|
||||
);
|
||||
|
||||
// Derive from enabled to guarantee each service is in exactly one list
|
||||
const enabledIds = useMemo(() => new Set(enabledServices.map((s) => s.id)), [
|
||||
enabledServices,
|
||||
]);
|
||||
const notEnabledServices = useMemo(
|
||||
() => awsServices?.filter((s) => !enabledIds.has(s.id)) ?? [],
|
||||
[awsServices, enabledIds],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const allServices = [...enabledServices, ...notEnabledServices];
|
||||
const defaultServiceId =
|
||||
enabledServices[0]?.id ?? notEnabledServices[0]?.id ?? null;
|
||||
|
||||
// If a service is already selected and still exists in the refreshed list, keep it
|
||||
if (activeService && allServices.some((s) => s.id === activeService)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// No valid selection — pick a default
|
||||
if (defaultServiceId) {
|
||||
handleActiveService(defaultServiceId);
|
||||
}
|
||||
}, [activeService, enabledServices, notEnabledServices, handleActiveService]);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="services-list-loading">
|
||||
<Skeleton active />
|
||||
<Skeleton active />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!awsServices?.length) {
|
||||
return (
|
||||
<div className="services-list-empty-message">
|
||||
{' '}
|
||||
<img
|
||||
src={emptyStateIconUrl}
|
||||
alt="no-services-found"
|
||||
className="empty-state-svg"
|
||||
/>{' '}
|
||||
No services found
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const isEnabledServicesEmpty = enabledServices.length === 0;
|
||||
const isNotEnabledServicesEmpty = notEnabledServices.length === 0;
|
||||
|
||||
const renderServiceItem = (
|
||||
service: CloudintegrationtypesServiceMetadataDTO,
|
||||
): JSX.Element => {
|
||||
return (
|
||||
<div
|
||||
className={cx('aws-services-list-view-sidebar-content-item', {
|
||||
active: service.id === activeService,
|
||||
})}
|
||||
key={service.id}
|
||||
onClick={(): void => handleActiveService(service.id)}
|
||||
>
|
||||
<img
|
||||
src={service.icon}
|
||||
alt={service.title}
|
||||
className="aws-services-list-view-sidebar-content-item-icon"
|
||||
/>
|
||||
<div className="aws-services-list-view-sidebar-content-item-title">
|
||||
{service.title}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="aws-services-list-view">
|
||||
<div className="aws-services-list-view-sidebar">
|
||||
<div className="aws-services-list-view-sidebar-content">
|
||||
<div className="aws-services-enabled">
|
||||
<div className="aws-services-list-view-sidebar-content-header">
|
||||
Enabled
|
||||
</div>
|
||||
{enabledServices.map((service) => renderServiceItem(service))}
|
||||
|
||||
{isEnabledServicesEmpty && (
|
||||
<div className="aws-services-list-view-sidebar-content-item-empty-message">
|
||||
No enabled services
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{!isNotEnabledServicesEmpty && (
|
||||
<div className="aws-services-not-enabled">
|
||||
<div className="aws-services-list-view-sidebar-content-header">
|
||||
Not Enabled
|
||||
</div>
|
||||
{notEnabledServices.map((service) => renderServiceItem(service))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ServicesList;
|
||||
@@ -1,4 +1,8 @@
|
||||
.services-tabs {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: calc(100% - 54px); /* 54px is the height of the header */
|
||||
|
||||
.ant-tabs-tab {
|
||||
font-family: 'Inter';
|
||||
padding: 16px 4px 14px;
|
||||
@@ -18,21 +22,60 @@
|
||||
background: var(--primary-background);
|
||||
}
|
||||
}
|
||||
|
||||
.services-section {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
|
||||
&__sidebar {
|
||||
width: 16%;
|
||||
padding: 0 16px;
|
||||
width: 240px;
|
||||
border-right: 1px solid var(--l2-border);
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
&__content {
|
||||
width: 84%;
|
||||
padding: 16px;
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.service-details-loading,
|
||||
.services-list-loading {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
|
||||
.service-details-loading-item {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: var(--muted);
|
||||
}
|
||||
}
|
||||
|
||||
.services-list-empty-message {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
padding: 12px;
|
||||
color: var(--l2-foreground);
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
line-height: 20px; /* 142.857% */
|
||||
letter-spacing: -0.07px;
|
||||
|
||||
.empty-state-svg {
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.services-filter {
|
||||
padding: 16px 0;
|
||||
padding: 12px;
|
||||
|
||||
.ant-select-selector {
|
||||
background-color: var(--l3-background) !important;
|
||||
border: 1px solid var(--l1-border) !important;
|
||||
@@ -46,6 +89,111 @@
|
||||
}
|
||||
}
|
||||
|
||||
.aws-services-list-view {
|
||||
height: 100%;
|
||||
|
||||
.aws-services-list-view-sidebar {
|
||||
width: 240px;
|
||||
height: 100%;
|
||||
border-right: 1px solid var(--l3-background);
|
||||
padding: 12px;
|
||||
|
||||
.aws-services-list-view-sidebar-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
|
||||
.aws-services-enabled {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.aws-services-not-enabled {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.aws-services-list-view-sidebar-content-header {
|
||||
color: var(--l2-foreground);
|
||||
font-family: Inter;
|
||||
font-size: 11px;
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
line-height: 18px; /* 163.636% */
|
||||
letter-spacing: 0.44px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.aws-services-list-view-sidebar-content-item-empty-message {
|
||||
color: var(--l2-foreground);
|
||||
font-family: Inter;
|
||||
font-size: 11px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 18px; /* 163.636% */
|
||||
letter-spacing: 0.44px;
|
||||
}
|
||||
|
||||
.aws-services-list-view-sidebar-content-item {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 4px 12px;
|
||||
border-radius: 4px;
|
||||
|
||||
cursor: pointer;
|
||||
|
||||
.aws-services-list-view-sidebar-content-item-icon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.aws-services-list-view-sidebar-content-item-title {
|
||||
color: var(--l1-foreground);
|
||||
font-family: Inter;
|
||||
font-size: 13px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 18px; /* 128.571% */
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: var(--l1-foreground);
|
||||
font-family: Inter;
|
||||
font-size: 13px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 18px; /* 128.571% */
|
||||
}
|
||||
|
||||
&.active {
|
||||
color: var(--l1-foreground);
|
||||
font-family: Inter;
|
||||
font-size: 13px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 18px; /* 128.571% */
|
||||
|
||||
background-color: var(--l3-background);
|
||||
|
||||
.aws-services-list-view-sidebar-content-item-title {
|
||||
color: var(--l1-foreground);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.aws-services-list-view-main {
|
||||
flex: 1;
|
||||
padding: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.service-item {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
@@ -60,20 +208,22 @@
|
||||
}
|
||||
|
||||
&.active {
|
||||
background-color: var(--bg-ink-100); /* keep: no semantic equivalent */
|
||||
background-color: var(--l3-background);
|
||||
}
|
||||
&__icon-wrapper {
|
||||
height: 40px;
|
||||
width: 40px;
|
||||
height: 32px;
|
||||
width: 32px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: var(--l3-background);
|
||||
border: 1px solid var(--l1-border);
|
||||
border-radius: 4px;
|
||||
|
||||
.service-item__icon {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
object-fit: contain;
|
||||
}
|
||||
}
|
||||
&__title {
|
||||
@@ -90,11 +240,13 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
|
||||
&__title-bar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
height: 48px;
|
||||
padding: 8px 12px;
|
||||
border-bottom: 1px solid var(--l1-border);
|
||||
|
||||
.service-details__details-title {
|
||||
@@ -105,6 +257,7 @@
|
||||
letter-spacing: -0.07px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.service-details__right-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -120,19 +273,30 @@
|
||||
border-radius: 2px;
|
||||
line-height: normal;
|
||||
&--connected {
|
||||
border: 1px solid color-mix(in srgb, var(--bg-forest-500) 10%, transparent);
|
||||
background: color-mix(in srgb, var(--bg-forest-500) 10%, transparent);
|
||||
color: var(--bg-forest-400);
|
||||
border: 1px solid
|
||||
color-mix(in srgb, var(--success-background) 10%, transparent);
|
||||
background: color-mix(in srgb, var(--success-background) 10%, transparent);
|
||||
color: var(--callout-success-title);
|
||||
}
|
||||
&--stale-data {
|
||||
background: color-mix(in srgb, var(--bg-amber-400) 10%, transparent);
|
||||
border: 1px solid color-mix(in srgb, var(--bg-amber-400) 10%, transparent);
|
||||
color: var(--bg-amber-400);
|
||||
background: color-mix(
|
||||
in srgb,
|
||||
var(--warning-background-hover) 10%,
|
||||
transparent
|
||||
);
|
||||
border: 1px solid
|
||||
color-mix(in srgb, var(--warning-background-hover) 10%, transparent);
|
||||
color: var(--callout-warning-title);
|
||||
}
|
||||
&--no-data {
|
||||
border: 1px solid color-mix(in srgb, var(--bg-cherry-400) 10%, transparent);
|
||||
background: color-mix(in srgb, var(--bg-cherry-400) 10%, transparent);
|
||||
color: var(--bg-cherry-400);
|
||||
border: 1px solid
|
||||
color-mix(in srgb, var(--danger-background-hover) 10%, transparent);
|
||||
background: color-mix(
|
||||
in srgb,
|
||||
var(--danger-background-hover) 10%,
|
||||
transparent
|
||||
);
|
||||
color: var(--callout-error-description);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -157,21 +321,28 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__overview {
|
||||
color: var(--l2-foreground);
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
line-height: 20px; /* 142.857% */
|
||||
letter-spacing: -0.07px;
|
||||
width: 800px;
|
||||
width: 100%;
|
||||
|
||||
padding: 8px 12px;
|
||||
}
|
||||
|
||||
&__tabs {
|
||||
padding: 0px 12px 12px 8px;
|
||||
|
||||
.ant-tabs {
|
||||
&-ink-bar {
|
||||
background-color: transparent;
|
||||
}
|
||||
&-nav {
|
||||
padding: 8px 0 18px;
|
||||
padding: 0;
|
||||
|
||||
&-wrap {
|
||||
padding: 0;
|
||||
}
|
||||
@@ -290,153 +461,3 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.lightMode {
|
||||
.services-tabs {
|
||||
.ant-tabs-tab {
|
||||
&.ant-tabs-tab-active {
|
||||
.ant-tabs-tab-btn {
|
||||
color: var(--l1-foreground);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.services-filter {
|
||||
.ant-select-selector {
|
||||
background-color: var(--l1-background) !important;
|
||||
border-color: var(--l1-border) !important;
|
||||
color: var(--l1-foreground) !important;
|
||||
}
|
||||
|
||||
.ant-select-arrow {
|
||||
color: var(--l1-foreground);
|
||||
}
|
||||
}
|
||||
|
||||
.service-item {
|
||||
&:not(:last-child) {
|
||||
border-bottom: 1px solid var(--l1-border);
|
||||
}
|
||||
|
||||
&.active {
|
||||
background-color: var(--l3-background);
|
||||
}
|
||||
|
||||
&__icon-wrapper {
|
||||
background-color: var(--l1-background);
|
||||
border-color: var(--l1-border);
|
||||
}
|
||||
|
||||
&__title {
|
||||
color: var(--l1-foreground);
|
||||
}
|
||||
}
|
||||
|
||||
.service-details {
|
||||
&__title-bar {
|
||||
border-bottom: 1px solid var(--l1-border);
|
||||
|
||||
.service-details__details-title {
|
||||
color: var(--l1-foreground);
|
||||
}
|
||||
|
||||
.configure-button {
|
||||
color: var(--l1-foreground);
|
||||
background: var(--l1-background);
|
||||
border-color: var(--l1-border);
|
||||
|
||||
&:hover {
|
||||
border-color: var(--l2-foreground);
|
||||
color: var(--l1-foreground);
|
||||
}
|
||||
}
|
||||
|
||||
.service-status {
|
||||
&--connected {
|
||||
border: 1px solid color-mix(in srgb, var(--bg-forest-500) 20%, transparent);
|
||||
background: color-mix(in srgb, var(--bg-forest-500) 10%, transparent);
|
||||
color: var(--bg-forest-500);
|
||||
}
|
||||
|
||||
&--stale-data {
|
||||
border: 1px solid color-mix(in srgb, var(--bg-amber-400) 20%, transparent);
|
||||
background: color-mix(in srgb, var(--bg-amber-400) 10%, transparent);
|
||||
color: var(--bg-amber-500);
|
||||
}
|
||||
|
||||
&--no-data {
|
||||
border: 1px solid color-mix(in srgb, var(--bg-cherry-400) 20%, transparent);
|
||||
background: color-mix(in srgb, var(--bg-cherry-400) 10%, transparent);
|
||||
color: var(--bg-cherry-500);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__overview {
|
||||
color: var(--l1-foreground);
|
||||
}
|
||||
|
||||
&__tabs {
|
||||
.ant-tabs {
|
||||
&-tab {
|
||||
&-btn {
|
||||
color: var(--l1-foreground) !important;
|
||||
|
||||
&[aria-selected='true'] {
|
||||
color: var(--l1-foreground) !important;
|
||||
}
|
||||
}
|
||||
|
||||
&-active {
|
||||
background: var(--l3-background);
|
||||
}
|
||||
}
|
||||
|
||||
&-nav-list {
|
||||
border-color: var(--l1-border);
|
||||
background: var(--l1-background);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.cloud-service {
|
||||
&-dashboard-item {
|
||||
&__title {
|
||||
color: var(--l1-foreground);
|
||||
}
|
||||
}
|
||||
|
||||
&-data-collected {
|
||||
&__table {
|
||||
.ant-table {
|
||||
border-color: var(--l1-border);
|
||||
|
||||
.ant-table-thead {
|
||||
> tr > th {
|
||||
color: var(--l1-foreground);
|
||||
}
|
||||
}
|
||||
|
||||
.ant-table-tbody {
|
||||
> tr {
|
||||
&:nth-child(odd),
|
||||
&:hover > td {
|
||||
background: var(--l1-background) !important;
|
||||
}
|
||||
|
||||
> td {
|
||||
color: var(--l1-foreground);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__table-heading {
|
||||
color: var(--l1-foreground);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
|
||||
import HeroSection from './HeroSection/HeroSection';
|
||||
import ServiceDetails from './ServiceDetails/ServiceDetails';
|
||||
import ServicesList from './ServicesList';
|
||||
|
||||
import './ServicesTabs.style.scss';
|
||||
|
||||
function ServicesTabs(): JSX.Element {
|
||||
const urlQuery = useUrlQuery();
|
||||
const cloudAccountId = urlQuery.get('cloudAccountId') || '';
|
||||
|
||||
return (
|
||||
<div className="services-tabs">
|
||||
<HeroSection />
|
||||
|
||||
<div className="services-section">
|
||||
<div className="services-section__sidebar">
|
||||
<ServicesList cloudAccountId={cloudAccountId} />
|
||||
</div>
|
||||
<div className="services-section__content">
|
||||
<ServiceDetails />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ServicesTabs;
|
||||
@@ -0,0 +1,178 @@
|
||||
import { fireEvent, screen, waitFor } from '@testing-library/react';
|
||||
import { server } from 'mocks-server/server';
|
||||
import { rest, RestRequest } from 'msw';
|
||||
|
||||
import {
|
||||
accountsResponse,
|
||||
buildServiceDetailsResponse,
|
||||
CLOUD_ACCOUNT_ID,
|
||||
initialBuckets,
|
||||
} from './mockData';
|
||||
import {
|
||||
assertGenericModalElements,
|
||||
assertS3SyncSpecificElements,
|
||||
renderServiceDetails,
|
||||
} from './utils';
|
||||
|
||||
// --- RESIZE OBSERVER (required by @radix-ui in Tabs/Switch) ---
|
||||
class ResizeObserverMock {
|
||||
observe(): void {}
|
||||
|
||||
unobserve(): void {}
|
||||
|
||||
disconnect(): void {}
|
||||
}
|
||||
global.ResizeObserver = (ResizeObserverMock as unknown) as typeof ResizeObserver;
|
||||
|
||||
// --- MOCKS ---
|
||||
jest.mock('components/MarkdownRenderer/MarkdownRenderer', () => ({
|
||||
MarkdownRenderer: (): JSX.Element => <div data-testid="markdown-renderer" />,
|
||||
}));
|
||||
jest.mock(
|
||||
'container/Integrations/CloudIntegration/AmazonWebServices/ServiceDashboards/ServiceDashboards',
|
||||
() => ({
|
||||
__esModule: true,
|
||||
default: (): JSX.Element => <div data-testid="service-dashboards" />,
|
||||
}),
|
||||
);
|
||||
|
||||
let testServiceId = 's3sync';
|
||||
let testInitialBuckets: Record<string, string[]> = {};
|
||||
const mockGet = jest.fn((param: string) => {
|
||||
if (param === 'cloudAccountId') {
|
||||
return CLOUD_ACCOUNT_ID;
|
||||
}
|
||||
if (param === 'service') {
|
||||
return testServiceId;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
jest.mock('hooks/useUrlQuery', () => ({
|
||||
__esModule: true,
|
||||
default: (): { get: (param: string) => string | null } => ({ get: mockGet }),
|
||||
}));
|
||||
|
||||
// --- TEST SUITE ---
|
||||
describe('ServiceDetails for S3 Sync service', () => {
|
||||
jest.setTimeout(10000);
|
||||
beforeEach(() => {
|
||||
testServiceId = 's3sync';
|
||||
testInitialBuckets = {};
|
||||
server.use(
|
||||
rest.get(
|
||||
'http://localhost/api/v1/cloud_integrations/aws/accounts',
|
||||
(_req, res, ctx) => res(ctx.json(accountsResponse)),
|
||||
),
|
||||
rest.get(
|
||||
'http://localhost/api/v1/cloud_integrations/aws/services/:serviceId',
|
||||
(req, res, ctx) =>
|
||||
res(
|
||||
ctx.json(
|
||||
buildServiceDetailsResponse(
|
||||
req.params.serviceId as string,
|
||||
testInitialBuckets,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
it('should render with logs collection switch and bucket selectors (no buckets initially selected)', async () => {
|
||||
renderServiceDetails({}); // No initial S3 buckets, defaults to 's3sync' serviceId
|
||||
await assertGenericModalElements();
|
||||
await assertS3SyncSpecificElements({});
|
||||
});
|
||||
|
||||
it('should render with logs collection switch and bucket selectors (some buckets initially selected)', async () => {
|
||||
testInitialBuckets = initialBuckets;
|
||||
renderServiceDetails(initialBuckets);
|
||||
await assertGenericModalElements();
|
||||
await assertS3SyncSpecificElements(initialBuckets);
|
||||
});
|
||||
|
||||
it('should enable save button after adding a new bucket via combobox', async () => {
|
||||
testInitialBuckets = initialBuckets;
|
||||
renderServiceDetails(initialBuckets);
|
||||
await assertGenericModalElements();
|
||||
await assertS3SyncSpecificElements(initialBuckets);
|
||||
|
||||
const targetCombobox = screen.getAllByRole('combobox')[0];
|
||||
const newBucketName = 'a-newly-added-bucket';
|
||||
|
||||
fireEvent.change(targetCombobox, { target: { value: newBucketName } });
|
||||
fireEvent.keyDown(targetCombobox, {
|
||||
key: 'Enter',
|
||||
code: 'Enter',
|
||||
keyCode: 13,
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByLabelText(newBucketName)).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: /save/i })).toBeEnabled();
|
||||
});
|
||||
});
|
||||
|
||||
it('should send updated bucket configuration on save', async () => {
|
||||
let capturedPayload: Record<string, unknown> | null = null;
|
||||
const mockUpdateConfigUrl = `http://localhost/api/v1/cloud_integrations/aws/accounts/${CLOUD_ACCOUNT_ID}/services/s3sync`;
|
||||
|
||||
// Override PUT handler specifically for this test to capture payload
|
||||
server.use(
|
||||
rest.put(mockUpdateConfigUrl, async (req: RestRequest, res, ctx) => {
|
||||
capturedPayload = await req.json();
|
||||
return res(ctx.status(200), ctx.json({ message: 'Config updated' }));
|
||||
}),
|
||||
);
|
||||
testInitialBuckets = initialBuckets;
|
||||
renderServiceDetails(initialBuckets);
|
||||
await assertGenericModalElements();
|
||||
await assertS3SyncSpecificElements(initialBuckets);
|
||||
|
||||
const newBucketName = 'another-new-bucket';
|
||||
const targetCombobox = screen.getAllByRole('combobox')[0];
|
||||
|
||||
fireEvent.change(targetCombobox, { target: { value: newBucketName } });
|
||||
fireEvent.keyDown(targetCombobox, {
|
||||
key: 'Enter',
|
||||
code: 'Enter',
|
||||
keyCode: 13,
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByLabelText(newBucketName)).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: /save/i })).toBeEnabled();
|
||||
});
|
||||
fireEvent.click(screen.getByRole('button', { name: /save/i }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(capturedPayload).not.toBeNull();
|
||||
});
|
||||
|
||||
expect(capturedPayload).toEqual({
|
||||
config: {
|
||||
aws: {
|
||||
logs: {
|
||||
enabled: true,
|
||||
s3Buckets: {
|
||||
'us-east-2': ['first-bucket', 'second-bucket'],
|
||||
'ap-south-1': [newBucketName],
|
||||
},
|
||||
},
|
||||
metrics: { enabled: false },
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should not render S3 bucket region selector UI for services other than s3sync', async () => {
|
||||
testServiceId = 'ec2';
|
||||
testInitialBuckets = {};
|
||||
renderServiceDetails({}, 'ec2');
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.queryByText(/select s3 buckets by region/i),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,68 @@
|
||||
import {
|
||||
GetService200,
|
||||
ListAccounts200,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
|
||||
const CLOUD_ACCOUNT_ID = 'a1b2c3d4-e5f6-7890-1234-567890abcdef';
|
||||
const PROVIDER_ACCOUNT_ID = '123456789012';
|
||||
|
||||
const initialBuckets = { 'us-east-2': ['first-bucket', 'second-bucket'] };
|
||||
|
||||
const accountsResponse: ListAccounts200 = {
|
||||
status: 'success',
|
||||
data: {
|
||||
accounts: [
|
||||
{
|
||||
id: CLOUD_ACCOUNT_ID,
|
||||
orgId: 'org-1',
|
||||
provider: 'aws',
|
||||
config: {
|
||||
aws: {
|
||||
regions: ['ap-south-1', 'ap-south-2', 'us-east-1', 'us-east-2'],
|
||||
},
|
||||
},
|
||||
agentReport: {
|
||||
timestampMillis: 1747114366214,
|
||||
data: null,
|
||||
},
|
||||
providerAccountId: PROVIDER_ACCOUNT_ID,
|
||||
removedAt: null,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
/** Response shape for GET /cloud_integrations/aws/services/:serviceId (used by ServiceDetails). */
|
||||
const buildServiceDetailsResponse = (
|
||||
serviceId: string,
|
||||
initialConfigLogsS3Buckets: Record<string, string[]> = {},
|
||||
): GetService200 => ({
|
||||
status: 'success',
|
||||
data: {
|
||||
id: serviceId,
|
||||
title: serviceId === 's3sync' ? 'S3 Sync' : serviceId,
|
||||
icon: '',
|
||||
overview: '',
|
||||
supportedSignals: { logs: serviceId === 's3sync', metrics: false },
|
||||
assets: { dashboards: [] },
|
||||
dataCollected: { logs: [], metrics: [] },
|
||||
cloudIntegrationService: {
|
||||
id: serviceId,
|
||||
config: {
|
||||
aws: {
|
||||
logs: { enabled: true, s3Buckets: initialConfigLogsS3Buckets },
|
||||
metrics: { enabled: false },
|
||||
},
|
||||
},
|
||||
},
|
||||
telemetryCollectionStrategy: { aws: {} },
|
||||
},
|
||||
});
|
||||
|
||||
export {
|
||||
accountsResponse,
|
||||
buildServiceDetailsResponse,
|
||||
CLOUD_ACCOUNT_ID,
|
||||
initialBuckets,
|
||||
PROVIDER_ACCOUNT_ID,
|
||||
};
|
||||
@@ -0,0 +1,56 @@
|
||||
import { render, RenderResult, screen, waitFor } from '@testing-library/react';
|
||||
import MockQueryClientProvider from 'providers/test/MockQueryClientProvider';
|
||||
|
||||
import ServiceDetails from '../ServiceDetails/ServiceDetails';
|
||||
import { accountsResponse } from './mockData';
|
||||
|
||||
/**
|
||||
* Renders ServiceDetails (inline config form). Tests must register MSW handlers
|
||||
* for GET accounts and GET service details, and mock useUrlQuery (cloudAccountId, service).
|
||||
*/
|
||||
const renderServiceDetails = (
|
||||
_initialConfigLogsS3Buckets: Record<string, string[]> = {},
|
||||
_serviceId = 's3sync',
|
||||
): RenderResult =>
|
||||
render(
|
||||
<MockQueryClientProvider>
|
||||
<ServiceDetails />
|
||||
</MockQueryClientProvider>,
|
||||
);
|
||||
|
||||
/**
|
||||
* Asserts generic UI elements of the ServiceDetails config form (Overview tab).
|
||||
*/
|
||||
const assertGenericModalElements = async (): Promise<void> => {
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole('switch')).toBeInTheDocument();
|
||||
expect(screen.getByText(/log collection/i)).toBeInTheDocument();
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Asserts S3 bucket selector section: title, region labels, and one combobox per region.
|
||||
* Does not assert placeholder text (antd Select may not expose it as placeholder attribute).
|
||||
*/
|
||||
const assertS3SyncSpecificElements = async (
|
||||
_expectedBucketsByRegion: Record<string, string[]> = {},
|
||||
): Promise<void> => {
|
||||
const regions = accountsResponse.data.accounts[0]?.config?.aws?.regions || [];
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/select s3 buckets by region/i)).toBeInTheDocument();
|
||||
|
||||
regions.forEach((region) => {
|
||||
expect(screen.getByText(region)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
const comboboxes = screen.getAllByRole('combobox');
|
||||
expect(comboboxes.length).toBeGreaterThanOrEqual(regions.length);
|
||||
});
|
||||
};
|
||||
|
||||
export {
|
||||
assertGenericModalElements,
|
||||
assertS3SyncSpecificElements,
|
||||
renderServiceDetails,
|
||||
};
|
||||
@@ -0,0 +1,25 @@
|
||||
import { CloudintegrationtypesAccountDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
|
||||
import { CloudAccount } from './types';
|
||||
|
||||
export function mapAccountDtoToAwsCloudAccount(
|
||||
account: CloudintegrationtypesAccountDTO,
|
||||
): CloudAccount | null {
|
||||
if (!account.providerAccountId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
id: account.id,
|
||||
cloud_account_id: account.id,
|
||||
config: {
|
||||
regions: account.config?.aws?.regions ?? [],
|
||||
},
|
||||
status: {
|
||||
integration: {
|
||||
last_heartbeat_ts_ms: account.agentReport?.timestampMillis ?? 0,
|
||||
},
|
||||
},
|
||||
providerAccountId: account.providerAccountId,
|
||||
};
|
||||
}
|
||||
@@ -1,90 +1,40 @@
|
||||
import { ServiceData } from 'container/Integrations/types';
|
||||
|
||||
interface Service {
|
||||
id: string;
|
||||
title: string;
|
||||
icon: string;
|
||||
config: ServiceConfig;
|
||||
}
|
||||
|
||||
interface Dashboard {
|
||||
id: string;
|
||||
url: string;
|
||||
title: string;
|
||||
description: string;
|
||||
image: string;
|
||||
}
|
||||
|
||||
interface LogField {
|
||||
name: string;
|
||||
path: string;
|
||||
type: string;
|
||||
}
|
||||
|
||||
interface Metric {
|
||||
name: string;
|
||||
type: string;
|
||||
unit: string;
|
||||
}
|
||||
|
||||
interface ConfigStatus {
|
||||
enabled: boolean;
|
||||
}
|
||||
|
||||
interface DataStatus {
|
||||
last_received_ts_ms: number;
|
||||
last_received_from: string;
|
||||
config: AWSServiceConfig;
|
||||
}
|
||||
|
||||
interface S3BucketsByRegion {
|
||||
[region: string]: string[];
|
||||
}
|
||||
|
||||
interface ConfigStatus {
|
||||
enabled: boolean;
|
||||
}
|
||||
|
||||
interface LogsConfig extends ConfigStatus {
|
||||
s3_buckets?: S3BucketsByRegion;
|
||||
}
|
||||
|
||||
interface ServiceConfig {
|
||||
interface AWSServiceConfig {
|
||||
logs: LogsConfig;
|
||||
metrics: ConfigStatus;
|
||||
s3_sync?: LogsConfig;
|
||||
}
|
||||
|
||||
interface IServiceStatus {
|
||||
logs: DataStatus | null;
|
||||
metrics: DataStatus | null;
|
||||
}
|
||||
|
||||
interface SupportedSignals {
|
||||
metrics: boolean;
|
||||
logs: boolean;
|
||||
}
|
||||
|
||||
interface ServiceData {
|
||||
id: string;
|
||||
title: string;
|
||||
icon: string;
|
||||
overview: string;
|
||||
supported_signals: SupportedSignals;
|
||||
assets: {
|
||||
dashboards: Dashboard[];
|
||||
};
|
||||
data_collected: {
|
||||
logs?: LogField[];
|
||||
metrics: Metric[];
|
||||
};
|
||||
config?: ServiceConfig;
|
||||
status?: IServiceStatus;
|
||||
}
|
||||
|
||||
interface ServiceDetailsResponse {
|
||||
status: 'success';
|
||||
data: ServiceData;
|
||||
}
|
||||
|
||||
interface CloudAccountConfig {
|
||||
export interface AWSCloudAccountConfig {
|
||||
regions: string[];
|
||||
}
|
||||
|
||||
interface IntegrationStatus {
|
||||
export interface IntegrationStatus {
|
||||
last_heartbeat_ts_ms: number;
|
||||
}
|
||||
|
||||
@@ -95,8 +45,9 @@ interface AccountStatus {
|
||||
interface CloudAccount {
|
||||
id: string;
|
||||
cloud_account_id: string;
|
||||
config: CloudAccountConfig;
|
||||
config: AWSCloudAccountConfig;
|
||||
status: AccountStatus;
|
||||
providerAccountId: string;
|
||||
}
|
||||
|
||||
interface CloudAccountsData {
|
||||
@@ -133,15 +84,13 @@ interface UpdateServiceConfigResponse {
|
||||
}
|
||||
|
||||
export type {
|
||||
AWSServiceConfig,
|
||||
CloudAccount,
|
||||
CloudAccountsData,
|
||||
IServiceStatus,
|
||||
S3BucketsByRegion,
|
||||
Service,
|
||||
ServiceConfig,
|
||||
ServiceData,
|
||||
ServiceDetailsResponse,
|
||||
SupportedSignals,
|
||||
UpdateServiceConfigPayload,
|
||||
UpdateServiceConfigResponse,
|
||||
};
|
||||
@@ -0,0 +1,3 @@
|
||||
.cloud-integration-container {
|
||||
height: 100%;
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import { IntegrationType } from 'container/Integrations/types';
|
||||
|
||||
import AWSTabs from './AmazonWebServices/ServicesTabs';
|
||||
import Header from './Header/Header';
|
||||
|
||||
import './CloudIntegration.styles.scss';
|
||||
|
||||
const CloudIntegration = ({ type }: { type: IntegrationType }): JSX.Element => {
|
||||
return (
|
||||
<div className="cloud-integration-container">
|
||||
<Header title={type} />
|
||||
|
||||
{type === IntegrationType.AWS_SERVICES && <AWSTabs />}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CloudIntegration;
|
||||
@@ -0,0 +1,42 @@
|
||||
.config-connection-status-popover {
|
||||
.ant-popover-inner {
|
||||
padding: 0;
|
||||
background-color: var(--l2-background);
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--l3-background);
|
||||
padding: 8px;
|
||||
|
||||
width: 240px;
|
||||
|
||||
.ant-popover-content {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.config-connection-status-item {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.config-connection-status-icon {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.config-connection-status-category-display-name {
|
||||
color: var(--l2-foreground);
|
||||
font-family: Inter;
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 18px; /* 138.462% */
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { IConfigConnectionStatus } from 'container/Integrations/types';
|
||||
import { CheckCircle, TriangleAlert } from 'lucide-react';
|
||||
|
||||
import './ConfigConnectionStatus.styles.scss';
|
||||
|
||||
export function ConfigConnectionStatus({
|
||||
status,
|
||||
}: {
|
||||
status: IConfigConnectionStatus[] | null;
|
||||
}): JSX.Element {
|
||||
return (
|
||||
<div className="config-connection-status-container">
|
||||
{status?.map((status) => (
|
||||
<div key={status.category} className="config-connection-status-item">
|
||||
<div className="config-connection-status-icon">
|
||||
{status.last_received_ts_ms && status.last_received_ts_ms > 0 ? (
|
||||
<CheckCircle size={16} color={Color.BG_FOREST_500} />
|
||||
) : (
|
||||
<TriangleAlert size={16} color={Color.BG_AMBER_500} />
|
||||
)}
|
||||
</div>
|
||||
<div className="config-connection-status-category-display-name">
|
||||
{status.category_display_name}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -3,7 +3,7 @@
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 8px 18px;
|
||||
border-bottom: 1px solid var(--l1-border);
|
||||
border-bottom: 1px solid var(--border);
|
||||
|
||||
&__navigation {
|
||||
display: flex;
|
||||
@@ -18,7 +18,7 @@
|
||||
}
|
||||
|
||||
&__breadcrumb-title {
|
||||
color: var(--l2-foreground);
|
||||
color: var(--l1-foreground);
|
||||
font-size: 14px;
|
||||
line-height: 20px; /* 142.857% */
|
||||
letter-spacing: -0.07px;
|
||||
@@ -30,8 +30,8 @@
|
||||
justify-content: center;
|
||||
padding: 6px;
|
||||
gap: 6px;
|
||||
border: 1px solid var(--l1-border);
|
||||
background: var(--l3-background);
|
||||
border: 1px solid var(--border);
|
||||
background: var(--card);
|
||||
border-radius: 2px;
|
||||
font-size: 12px;
|
||||
line-height: 10px;
|
||||
@@ -39,9 +39,11 @@
|
||||
width: 113px;
|
||||
height: 32px;
|
||||
cursor: pointer;
|
||||
&,
|
||||
color: var(--l1-foreground);
|
||||
|
||||
&:hover {
|
||||
color: var(--l2-foreground);
|
||||
border-color: var(--l2-border);
|
||||
color: var(--l1-foreground);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,13 @@
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Breadcrumb } from 'antd';
|
||||
import { Button } from '@signozhq/button';
|
||||
import Breadcrumb from 'antd/es/breadcrumb';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { IntegrationType } from 'container/Integrations/types';
|
||||
import { Blocks, LifeBuoy } from 'lucide-react';
|
||||
|
||||
import './Header.styles.scss';
|
||||
|
||||
function Header(): JSX.Element {
|
||||
function Header({ title }: { title: IntegrationType }): JSX.Element {
|
||||
return (
|
||||
<div className="cloud-header">
|
||||
<div className="cloud-header__navigation">
|
||||
@@ -16,32 +18,33 @@ function Header(): JSX.Element {
|
||||
title: (
|
||||
<Link to={ROUTES.INTEGRATIONS}>
|
||||
<span className="cloud-header__breadcrumb-link">
|
||||
<Blocks size={16} color="var(--bg-vanilla-400)" />
|
||||
<Blocks size={16} color="var(--l2-foreground)" />
|
||||
<span className="cloud-header__breadcrumb-title">Integrations</span>
|
||||
</span>
|
||||
</Link>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: (
|
||||
<div className="cloud-header__breadcrumb-title">
|
||||
Amazon Web Services
|
||||
</div>
|
||||
),
|
||||
title: <div className="cloud-header__breadcrumb-title">{title}</div>,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
<div className="cloud-header__actions">
|
||||
<a
|
||||
href="https://signoz.io/blog/native-aws-integrations-with-autodiscovery/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="cloud-header__help"
|
||||
<Button
|
||||
variant="solid"
|
||||
size="sm"
|
||||
color="secondary"
|
||||
onClick={(): void => {
|
||||
window.open(
|
||||
'https://signoz.io/blog/native-aws-integrations-with-autodiscovery/',
|
||||
'_blank',
|
||||
);
|
||||
}}
|
||||
prefixIcon={<LifeBuoy size={12} />}
|
||||
>
|
||||
<LifeBuoy size={12} />
|
||||
Get Help
|
||||
</a>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -0,0 +1,5 @@
|
||||
export const getAccountById = <T extends { cloud_account_id: string }>(
|
||||
accounts: T[],
|
||||
accountId: string,
|
||||
): T | null =>
|
||||
accounts.find((account) => account.cloud_account_id === accountId) || null;
|
||||
@@ -3,7 +3,7 @@ import { Button, Typography } from 'antd';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import cx from 'classnames';
|
||||
import { MarkdownRenderer } from 'components/MarkdownRenderer/MarkdownRenderer';
|
||||
import { INTEGRATION_TELEMETRY_EVENTS } from 'pages/Integrations/utils';
|
||||
import { INTEGRATION_TELEMETRY_EVENTS } from 'container/Integrations/constants';
|
||||
|
||||
import './IntegrationDetailContentTabs.styles.scss';
|
||||
|
||||
@@ -41,9 +41,9 @@
|
||||
.category-tab {
|
||||
padding: 2px 8px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid color-mix(in srgb, var(--bg-sienna-500) 20%, transparent);
|
||||
background: color-mix(in srgb, var(--bg-sienna-500) 10%, transparent);
|
||||
color: var(--bg-sienna-400);
|
||||
border: 1px solid color-mix(in srgb, var(--accent-sienna) 20%, transparent);
|
||||
background: color-mix(in srgb, var(--accent-sienna) 10%, transparent);
|
||||
color: var(--accent-sienna-hover);
|
||||
font-family: Inter;
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
@@ -223,56 +223,3 @@
|
||||
overflow-y: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.lightMode {
|
||||
.integration-detail-overview {
|
||||
.integration-detail-overview-left-container {
|
||||
.integration-detail-overview-category {
|
||||
.category-tabs {
|
||||
.category-tab {
|
||||
border: 1px solid var(--bg-sienna-600);
|
||||
background: color-mix(in srgb, var(--bg-sienna-500) 10%, transparent);
|
||||
color: var(--bg-sienna-500);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.integration-data-collected {
|
||||
.logs-section {
|
||||
.table-row-dark {
|
||||
background: var(--l3-background);
|
||||
}
|
||||
|
||||
.logs-section-table {
|
||||
border: 1px solid var(--l1-border);
|
||||
background: var(--l3-background);
|
||||
}
|
||||
}
|
||||
|
||||
.metrics-section {
|
||||
.table-row-dark {
|
||||
background: var(--l3-background);
|
||||
}
|
||||
|
||||
.metrics-section-table {
|
||||
border: 1px solid var(--l1-border);
|
||||
background: var(--l3-background);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.integration-detail-configure {
|
||||
.configure-menu {
|
||||
.configure-menu-item:hover {
|
||||
background-color: var(--l2-background);
|
||||
}
|
||||
|
||||
.active {
|
||||
color: var(--l1-foreground);
|
||||
background-color: var(--l2-background);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useState } from 'react';
|
||||
import { useMutation } from 'react-query';
|
||||
import { Button, Modal, Tooltip, Typography } from 'antd';
|
||||
import { Button, Modal, Skeleton, Tooltip, Typography } from 'antd';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import installIntegration from 'api/Integrations/installIntegration';
|
||||
import ConfigureIcon from 'assets/Integrations/ConfigureIcon';
|
||||
@@ -9,10 +9,10 @@ import { SOMETHING_WENT_WRONG } from 'constants/api';
|
||||
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
|
||||
import dayjs from 'dayjs';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import { ArrowLeftRight, Check } from 'lucide-react';
|
||||
import { ArrowLeftRight, Cable, Check } from 'lucide-react';
|
||||
import { IntegrationConnectionStatus } from 'types/api/integrations/types';
|
||||
|
||||
import { INTEGRATION_TELEMETRY_EVENTS } from '../utils';
|
||||
import { INTEGRATION_TELEMETRY_EVENTS } from '../constants';
|
||||
import TestConnection, { ConnectionStates } from './TestConnection';
|
||||
|
||||
import './IntegrationDetailPage.styles.scss';
|
||||
@@ -26,6 +26,7 @@ interface IntegrationDetailHeaderProps {
|
||||
connectionState: ConnectionStates;
|
||||
connectionData: IntegrationConnectionStatus;
|
||||
setActiveDetailTab: React.Dispatch<React.SetStateAction<string | null>>;
|
||||
isLoading: boolean;
|
||||
}
|
||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||
function IntegrationDetailHeader(
|
||||
@@ -38,6 +39,7 @@ function IntegrationDetailHeader(
|
||||
description,
|
||||
connectionState,
|
||||
connectionData,
|
||||
isLoading,
|
||||
onUnInstallSuccess,
|
||||
setActiveDetailTab,
|
||||
} = props;
|
||||
@@ -114,16 +116,29 @@ function IntegrationDetailHeader(
|
||||
|
||||
const isConnectionStateNotInstalled =
|
||||
connectionState === ConnectionStates.NotInstalled;
|
||||
|
||||
return (
|
||||
<div className="integration-connection-header">
|
||||
<div className="integration-detail-header" key={id}>
|
||||
<div style={{ display: 'flex', gap: '10px' }}>
|
||||
<div className="integration-detail-header-icon-title-container">
|
||||
<div className="image-container">
|
||||
<img src={icon} alt={title} className="image" />
|
||||
{icon ? (
|
||||
<img src={icon} alt={title} className="image" />
|
||||
) : (
|
||||
<div className="image-placeholder">
|
||||
<Cable size={24} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="details">
|
||||
<Typography.Text className="heading">{title}</Typography.Text>
|
||||
<Typography.Text className="description">{description}</Typography.Text>
|
||||
{isLoading ? (
|
||||
<Skeleton.Input active className="skeleton-item" />
|
||||
) : (
|
||||
<>
|
||||
<Typography.Text className="heading">{title}</Typography.Text>
|
||||
<Typography.Text className="description">{description}</Typography.Text>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
@@ -132,7 +147,7 @@ function IntegrationDetailHeader(
|
||||
!isConnectionStateNotInstalled && 'test-connection',
|
||||
)}
|
||||
icon={<ArrowLeftRight size={14} />}
|
||||
disabled={isInstallLoading}
|
||||
disabled={isInstallLoading || isLoading}
|
||||
onClick={(): void => {
|
||||
if (connectionState === ConnectionStates.NotInstalled) {
|
||||
logEvent(INTEGRATION_TELEMETRY_EVENTS.INTEGRATIONS_DETAIL_CONNECT, {
|
||||
@@ -0,0 +1,544 @@
|
||||
.integration-details-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
padding: 16px;
|
||||
|
||||
.integration-details-content-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
|
||||
.error-container {
|
||||
display: flex;
|
||||
border-radius: 6px;
|
||||
border: 1px solid var(--l1-border);
|
||||
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(--callout-primary-description);
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.error-state-svg {
|
||||
height: 40px;
|
||||
width: 40px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.loading-integration-details {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
|
||||
.skeleton-1 {
|
||||
height: 125px;
|
||||
width: 100%;
|
||||
}
|
||||
.skeleton-2 {
|
||||
height: 250px;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.integration-connection-header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 16px;
|
||||
gap: 12px;
|
||||
border-radius: 6px;
|
||||
border: 1px solid var(--l1-border);
|
||||
background: var(--l1-background);
|
||||
|
||||
.integration-detail-header {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
.integration-detail-header-icon-title-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.image-container {
|
||||
height: 40px;
|
||||
width: 40px;
|
||||
flex-shrink: 0;
|
||||
border-radius: 2px;
|
||||
border: 1px solid var(--l3-border);
|
||||
background: var(--l2-background);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
.image {
|
||||
height: 24px;
|
||||
width: 24px;
|
||||
}
|
||||
}
|
||||
.details {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
.heading {
|
||||
color: var(--l1-foreground);
|
||||
font-family: Inter;
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
line-height: 20px; /* 142.857% */
|
||||
letter-spacing: -0.07px;
|
||||
}
|
||||
|
||||
.description {
|
||||
color: var(--l2-foreground);
|
||||
font-family: Inter;
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 18px; /* 150% */
|
||||
}
|
||||
}
|
||||
|
||||
.configure-btn {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
align-self: flex-start;
|
||||
gap: 2px;
|
||||
flex-shrink: 0;
|
||||
min-width: 143px;
|
||||
height: 30px;
|
||||
padding: 6px;
|
||||
border-radius: 2px;
|
||||
border: 1px solid var(--l3-border);
|
||||
background: var(--primary-background);
|
||||
color: var(--l1-foreground);
|
||||
font-family: Inter;
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
line-height: 10px; /* 83.333% */
|
||||
letter-spacing: 0.12px;
|
||||
box-shadow: none;
|
||||
|
||||
&.test-connection {
|
||||
border-radius: 2px;
|
||||
border: 1px solid var(--l2-border);
|
||||
background: var(--l2-background);
|
||||
color: var(--l2-foreground);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.connection-container {
|
||||
padding: 0 18px;
|
||||
height: 37px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.connection-text {
|
||||
margin: 0px;
|
||||
padding: 0px 0px 0px 10px;
|
||||
font-family: Inter;
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 22px; /* 157.143% */
|
||||
letter-spacing: -0.07px;
|
||||
}
|
||||
}
|
||||
|
||||
.testingConnection {
|
||||
border-radius: 4px;
|
||||
border: 1px solid rgba(255, 205, 86, 0.1);
|
||||
background: rgba(255, 205, 86, 0.1);
|
||||
color: var(--callout-warning-title);
|
||||
}
|
||||
|
||||
.connected {
|
||||
border-radius: 4px;
|
||||
border: 1px solid rgba(37, 225, 146, 0.1);
|
||||
background: rgba(37, 225, 146, 0.1);
|
||||
color: var(--callout-success-title);
|
||||
}
|
||||
|
||||
.connectionFailed {
|
||||
border-radius: 4px;
|
||||
border: 1px solid rgba(218, 85, 101, 0.2);
|
||||
background: rgba(218, 85, 101, 0.06);
|
||||
color: var(--callout-error-title);
|
||||
}
|
||||
|
||||
.noDataSinceLong {
|
||||
border-radius: 4px;
|
||||
border: 1px solid rgba(78, 116, 248, 0.1);
|
||||
background: rgba(78, 116, 248, 0.1);
|
||||
color: var(--callout-primary-description);
|
||||
}
|
||||
}
|
||||
|
||||
.integration-detail-container {
|
||||
border-radius: 6px;
|
||||
padding: 10px 16px;
|
||||
border: 1px solid var(--l1-border);
|
||||
background: var(--l1-background);
|
||||
|
||||
.integration-tab-btns {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 8px 8px 18px 8px !important;
|
||||
|
||||
.typography {
|
||||
color: var(--l1-foreground);
|
||||
font-family: Inter;
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 20px; /* 142.857% */
|
||||
letter-spacing: -0.07px;
|
||||
}
|
||||
}
|
||||
|
||||
.integration-tab-btns:hover {
|
||||
&.ant-btn-text {
|
||||
background-color: unset !important;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-tabs-nav-list {
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.ant-tabs-nav {
|
||||
padding: 0px !important;
|
||||
}
|
||||
|
||||
.ant-tabs-tab {
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
.ant-tabs-tab + .ant-tabs-tab {
|
||||
margin: 0px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.uninstall-integration-bar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 16px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid rgba(218, 85, 101, 0.2);
|
||||
background: rgba(218, 85, 101, 0.06);
|
||||
gap: 32px;
|
||||
|
||||
.unintall-integration-bar-text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
|
||||
.heading {
|
||||
color: var(--callout-error-title);
|
||||
font-family: Inter;
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: normal;
|
||||
letter-spacing: -0.07px;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
color: var(--callout-error-description);
|
||||
font-family: Inter;
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 22px; /* 157.143% */
|
||||
letter-spacing: -0.07px;
|
||||
}
|
||||
}
|
||||
|
||||
.uninstall-integration-btn {
|
||||
border-radius: 2px;
|
||||
background: var(--danger-background);
|
||||
border: none !important;
|
||||
padding: 9px 13px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--l1-foreground);
|
||||
text-align: center;
|
||||
font-family: Inter;
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
line-height: 13.3px; /* 110.833% */
|
||||
}
|
||||
|
||||
.uninstall-integration-btn:hover {
|
||||
&.ant-btn-default {
|
||||
color: var(--l1-foreground) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.loading-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
|
||||
height: 160px;
|
||||
|
||||
.skeleton-item {
|
||||
padding: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.remove-integration-modal {
|
||||
.ant-modal-content {
|
||||
width: 400px;
|
||||
min-height: 200px;
|
||||
flex-shrink: 0;
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--l1-border);
|
||||
box-shadow: 0px -4px 16px 2px rgba(0, 0, 0, 0.2);
|
||||
background: var(--l1-background);
|
||||
}
|
||||
|
||||
.ant-modal-footer {
|
||||
margin-top: 28px;
|
||||
}
|
||||
|
||||
.ant-modal-header {
|
||||
background: unset;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.ant-modal-title {
|
||||
color: var(--l1-foreground);
|
||||
font-family: Inter;
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
line-height: 20px; /* 142.857% */
|
||||
letter-spacing: -0.07px;
|
||||
}
|
||||
|
||||
.remove-integration-text {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
.test-connection-modal {
|
||||
.ant-modal-content {
|
||||
width: 512px;
|
||||
min-height: 170px;
|
||||
flex-shrink: 0;
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--l1-border);
|
||||
background: var(--l1-background);
|
||||
box-shadow: 0px -4px 16px 2px rgba(0, 0, 0, 0.2);
|
||||
|
||||
.ant-modal-header {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.ant-modal-body {
|
||||
border-top: 1px solid var(--l1-border);
|
||||
padding-top: 16px;
|
||||
}
|
||||
|
||||
.ant-modal-footer {
|
||||
margin-top: 25px;
|
||||
display: flex;
|
||||
flex-direction: row-reverse;
|
||||
|
||||
.connection-footer {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
.understandBtn {
|
||||
width: 50%;
|
||||
border-radius: 2px;
|
||||
border: 1px solid var(--l2-border);
|
||||
background: var(--l2-background);
|
||||
box-shadow: none;
|
||||
color: var(--l2-foreground);
|
||||
font-family: Inter;
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 10px; /* 83.333% */
|
||||
letter-spacing: 0.12px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 34px;
|
||||
padding: 6px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.configureBtn {
|
||||
width: 50%;
|
||||
color: var(--l1-foreground);
|
||||
font-family: Inter;
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 10px; /* 83.333% */
|
||||
letter-spacing: 0.12px;
|
||||
border-radius: 2px;
|
||||
background: var(--primary-background);
|
||||
display: flex;
|
||||
height: 34px;
|
||||
padding: 6px;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
flex: 1 0 0;
|
||||
}
|
||||
|
||||
&.not-pending {
|
||||
flex-direction: row-reverse;
|
||||
|
||||
.understandBtn {
|
||||
width: 131px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ant-modal-header {
|
||||
background: unset;
|
||||
}
|
||||
|
||||
.connection-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
|
||||
.connection-container {
|
||||
padding: 0 10px;
|
||||
height: 37px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.connection-text {
|
||||
margin: 0px;
|
||||
padding: 0px 0px 0px 10px;
|
||||
font-family: Inter;
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 22px; /* 157.143% */
|
||||
letter-spacing: -0.07px;
|
||||
}
|
||||
}
|
||||
|
||||
.data-test-connection {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
.data-info {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
.connection-line {
|
||||
border: 1px dashed var(--l2-border);
|
||||
min-width: 20px;
|
||||
height: 0px;
|
||||
flex-grow: 1;
|
||||
margin: 0px 8px;
|
||||
}
|
||||
|
||||
.last-data {
|
||||
color: var(--l2-foreground);
|
||||
font-family: Inter;
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 22px; /* 157.143% */
|
||||
letter-spacing: -0.07px;
|
||||
}
|
||||
|
||||
.last-value {
|
||||
color: var(--l1-foreground);
|
||||
font-family: 'Space Mono';
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 18px; /* 150% */
|
||||
max-width: 320px;
|
||||
}
|
||||
}
|
||||
.testingConnection {
|
||||
border-radius: 4px;
|
||||
border: 1px solid rgba(255, 205, 86, 0.1);
|
||||
background: rgba(255, 205, 86, 0.1);
|
||||
color: var(--callout-warning-title);
|
||||
}
|
||||
|
||||
.connected {
|
||||
border-radius: 4px;
|
||||
border: 1px solid rgba(37, 225, 146, 0.1);
|
||||
background: rgba(37, 225, 146, 0.1);
|
||||
color: var(--callout-success-title);
|
||||
}
|
||||
|
||||
.connectionFailed {
|
||||
border-radius: 4px;
|
||||
border: 1px solid rgba(218, 85, 101, 0.2);
|
||||
background: rgba(218, 85, 101, 0.06);
|
||||
color: var(--callout-error-title);
|
||||
}
|
||||
|
||||
.noDataSinceLong {
|
||||
border-radius: 4px;
|
||||
border: 1px solid rgba(78, 116, 248, 0.1);
|
||||
background: rgba(78, 116, 248, 0.1);
|
||||
color: var(--callout-primary-description);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,9 @@
|
||||
import { useState } from 'react';
|
||||
import { useHistory, useParams } from 'react-router-dom';
|
||||
import { Button } from '@signozhq/button';
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { Button, Flex, Skeleton, Typography } from 'antd';
|
||||
import { Flex, Skeleton, Typography } from 'antd';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { useGetIntegration } from 'hooks/Integrations/useGetIntegration';
|
||||
import { useGetIntegrationStatus } from 'hooks/Integrations/useGetIntegrationStatus';
|
||||
import { useGetTenantLicense } from 'hooks/useGetTenantLicense';
|
||||
@@ -8,6 +12,9 @@ import { ArrowLeft, MoveUpRight, RotateCw } from 'lucide-react';
|
||||
|
||||
import awwSnapUrl from '@/assets/Icons/awwSnap.svg';
|
||||
|
||||
import CloudIntegration from '../CloudIntegration/CloudIntegration';
|
||||
import { INTEGRATION_TYPES } from '../constants';
|
||||
import { IntegrationType } from '../types';
|
||||
import { handleContactSupport } from '../utils';
|
||||
import IntegrationDetailContent from './IntegrationDetailContent';
|
||||
import IntegrationDetailHeader from './IntegrationDetailHeader';
|
||||
@@ -17,20 +24,13 @@ import { getConnectionStatesFromConnectionStatus } from './utils';
|
||||
|
||||
import './IntegrationDetailPage.styles.scss';
|
||||
|
||||
interface IntegrationDetailPageProps {
|
||||
selectedIntegration: string;
|
||||
setSelectedIntegration: (id: string | null) => void;
|
||||
activeDetailTab: string;
|
||||
setActiveDetailTab: React.Dispatch<React.SetStateAction<string | null>>;
|
||||
}
|
||||
|
||||
function IntegrationDetailPage(props: IntegrationDetailPageProps): JSX.Element {
|
||||
const {
|
||||
selectedIntegration,
|
||||
setSelectedIntegration,
|
||||
activeDetailTab,
|
||||
setActiveDetailTab,
|
||||
} = props;
|
||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||
function IntegrationDetailPage(): JSX.Element {
|
||||
const history = useHistory();
|
||||
const { integrationId } = useParams<{ integrationId?: string }>();
|
||||
const [activeDetailTab, setActiveDetailTab] = useState<string | null>(
|
||||
'overview',
|
||||
);
|
||||
|
||||
const {
|
||||
data,
|
||||
@@ -40,7 +40,7 @@ function IntegrationDetailPage(props: IntegrationDetailPageProps): JSX.Element {
|
||||
isRefetching,
|
||||
isError,
|
||||
} = useGetIntegration({
|
||||
integrationId: selectedIntegration,
|
||||
integrationId: integrationId || '',
|
||||
});
|
||||
|
||||
const { isCloudUser: isCloudUserVal } = useGetTenantLicense();
|
||||
@@ -49,7 +49,7 @@ function IntegrationDetailPage(props: IntegrationDetailPageProps): JSX.Element {
|
||||
data: integrationStatus,
|
||||
isLoading: isStatusLoading,
|
||||
} = useGetIntegrationStatus({
|
||||
integrationId: selectedIntegration,
|
||||
integrationId: integrationId || '',
|
||||
});
|
||||
|
||||
const loading = isLoading || isFetching || isRefetching || isStatusLoading;
|
||||
@@ -63,27 +63,27 @@ function IntegrationDetailPage(props: IntegrationDetailPageProps): JSX.Element {
|
||||
),
|
||||
);
|
||||
|
||||
if (integrationId === INTEGRATION_TYPES.AWS) {
|
||||
return <CloudIntegration type={IntegrationType.AWS_SERVICES} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="integration-detail-content">
|
||||
<div className="integration-details-container">
|
||||
<Flex justify="space-between" align="center">
|
||||
<Button
|
||||
type="text"
|
||||
icon={<ArrowLeft size={14} />}
|
||||
variant="link"
|
||||
color="secondary"
|
||||
prefixIcon={<ArrowLeft size={14} />}
|
||||
className="all-integrations-btn"
|
||||
onClick={(): void => {
|
||||
setSelectedIntegration(null);
|
||||
history.push(ROUTES.INTEGRATIONS);
|
||||
}}
|
||||
>
|
||||
All Integrations
|
||||
</Button>
|
||||
</Flex>
|
||||
|
||||
{loading ? (
|
||||
<div className="loading-integration-details">
|
||||
<Skeleton.Input active size="large" className="skeleton-1" />
|
||||
<Skeleton.Input active size="large" className="skeleton-2" />
|
||||
</div>
|
||||
) : isError ? (
|
||||
{isError && (
|
||||
<div className="error-container">
|
||||
<div className="error-content">
|
||||
<img src={awwSnapUrl} alt="error-emoji" className="error-state-svg" />
|
||||
@@ -92,10 +92,10 @@ function IntegrationDetailPage(props: IntegrationDetailPageProps): JSX.Element {
|
||||
</Typography.Text>
|
||||
<div className="error-btns">
|
||||
<Button
|
||||
type="primary"
|
||||
className="retry-btn"
|
||||
variant="solid"
|
||||
color="primary"
|
||||
onClick={(): Promise<any> => refetch()}
|
||||
icon={<RotateCw size={14} />}
|
||||
prefixIcon={<RotateCw size={14} />}
|
||||
>
|
||||
Retry
|
||||
</Button>
|
||||
@@ -110,39 +110,51 @@ function IntegrationDetailPage(props: IntegrationDetailPageProps): JSX.Element {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
integrationData && (
|
||||
<>
|
||||
<IntegrationDetailHeader
|
||||
id={selectedIntegration}
|
||||
title={defaultTo(integrationData?.title, '')}
|
||||
description={defaultTo(integrationData?.description, '')}
|
||||
icon={defaultTo(integrationData?.icon, '')}
|
||||
connectionState={connectionStatus}
|
||||
connectionData={defaultTo(integrationStatus?.data.data, {
|
||||
logs: null,
|
||||
metrics: null,
|
||||
})}
|
||||
onUnInstallSuccess={refetch}
|
||||
setActiveDetailTab={setActiveDetailTab}
|
||||
/>
|
||||
<IntegrationDetailContent
|
||||
activeDetailTab={activeDetailTab}
|
||||
integrationData={integrationData}
|
||||
integrationId={selectedIntegration}
|
||||
setActiveDetailTab={setActiveDetailTab}
|
||||
/>
|
||||
)}
|
||||
|
||||
{connectionStatus !== ConnectionStates.NotInstalled && (
|
||||
{!isError && (
|
||||
<div className="integration-details-content-container">
|
||||
<IntegrationDetailHeader
|
||||
id={integrationId || ''}
|
||||
title={defaultTo(integrationData?.title, '')}
|
||||
description={defaultTo(integrationData?.description, '')}
|
||||
icon={defaultTo(integrationData?.icon, '')}
|
||||
connectionState={connectionStatus}
|
||||
connectionData={defaultTo(integrationStatus?.data.data, {
|
||||
logs: null,
|
||||
metrics: null,
|
||||
})}
|
||||
onUnInstallSuccess={refetch}
|
||||
setActiveDetailTab={setActiveDetailTab}
|
||||
isLoading={loading}
|
||||
/>
|
||||
|
||||
{loading && (
|
||||
<div className="loading-container">
|
||||
<Skeleton active className="skeleton-item" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!isError && !loading && integrationData && (
|
||||
<IntegrationDetailContent
|
||||
activeDetailTab={activeDetailTab || 'overview'}
|
||||
integrationData={integrationData}
|
||||
integrationId={integrationId || ''}
|
||||
setActiveDetailTab={setActiveDetailTab}
|
||||
/>
|
||||
)}
|
||||
|
||||
{!isError &&
|
||||
!loading &&
|
||||
connectionStatus !== ConnectionStates.NotInstalled && (
|
||||
<IntergrationsUninstallBar
|
||||
integrationTitle={defaultTo(integrationData?.title, '')}
|
||||
integrationId={selectedIntegration}
|
||||
integrationId={integrationId || ''}
|
||||
onUnInstallSuccess={refetch}
|
||||
connectionStatus={connectionStatus}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
@@ -7,7 +7,7 @@ import { SOMETHING_WENT_WRONG } from 'constants/api';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import { X } from 'lucide-react';
|
||||
|
||||
import { INTEGRATION_TELEMETRY_EVENTS } from '../utils';
|
||||
import { INTEGRATION_TELEMETRY_EVENTS } from '../constants';
|
||||
import { ConnectionStates } from './TestConnection';
|
||||
|
||||
import './IntegrationDetailPage.styles.scss';
|
||||
55
frontend/src/container/Integrations/Integrations.styles.scss
Normal file
55
frontend/src/container/Integrations/Integrations.styles.scss
Normal file
@@ -0,0 +1,55 @@
|
||||
.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(--l1-border);
|
||||
background: var(--l1-background);
|
||||
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(--l2-foreground);
|
||||
font-size: var(--font-size-sm);
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
59
frontend/src/container/Integrations/Integrations.tsx
Normal file
59
frontend/src/container/Integrations/Integrations.tsx
Normal file
@@ -0,0 +1,59 @@
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { IntegrationsProps } from 'types/api/integrations/types';
|
||||
|
||||
import { INTEGRATION_TELEMETRY_EVENTS } from './constants';
|
||||
import IntegrationsHeader from './IntegrationsHeader/IntegrationsHeader';
|
||||
import IntegrationsList from './IntegrationsList/IntegrationsList';
|
||||
import OneClickIntegrations from './OneClickIntegrations/OneClickIntegrations';
|
||||
|
||||
import './Integrations.styles.scss';
|
||||
|
||||
function Integrations(): JSX.Element {
|
||||
const history = useHistory();
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
|
||||
const setSelectedIntegration = useCallback(
|
||||
(integration: IntegrationsProps | null) => {
|
||||
if (integration) {
|
||||
logEvent(INTEGRATION_TELEMETRY_EVENTS.INTEGRATIONS_ITEM_LIST_CLICKED, {
|
||||
integration,
|
||||
});
|
||||
history.push(`${ROUTES.INTEGRATIONS}/${integration.id}`);
|
||||
} else {
|
||||
history.push(ROUTES.INTEGRATIONS);
|
||||
}
|
||||
},
|
||||
[history],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
logEvent(INTEGRATION_TELEMETRY_EVENTS.INTEGRATIONS_LIST_VISITED, {});
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="integrations-page">
|
||||
<div className="integrations-content">
|
||||
<div className="integrations-listing-container">
|
||||
<IntegrationsHeader
|
||||
searchQuery={searchQuery}
|
||||
onSearchChange={setSearchQuery}
|
||||
/>
|
||||
<OneClickIntegrations
|
||||
searchQuery={searchQuery}
|
||||
setSelectedIntegration={setSelectedIntegration}
|
||||
/>
|
||||
<IntegrationsList
|
||||
searchQuery={searchQuery}
|
||||
setSelectedIntegration={setSelectedIntegration}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Integrations;
|
||||
@@ -0,0 +1,79 @@
|
||||
.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(--l1-foreground);
|
||||
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(--l2-foreground);
|
||||
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;
|
||||
|
||||
input {
|
||||
color: var(--l1-foreground);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.request-integration-dialog {
|
||||
.request-integration-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
|
||||
.request-integration-input {
|
||||
width: 100%;
|
||||
color: var(--l1-foreground);
|
||||
}
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
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/ui';
|
||||
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
|
||||
className="request-integration-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;
|
||||
@@ -0,0 +1,237 @@
|
||||
.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(--callout-primary-description);
|
||||
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;
|
||||
color: var(--l1-foreground);
|
||||
|
||||
cursor: pointer;
|
||||
|
||||
.integrations-list-item-name-image-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
padding: 4px;
|
||||
|
||||
border: 1px solid var(--l3-border);
|
||||
background: var(--l3-background);
|
||||
border-radius: 2px;
|
||||
|
||||
.integrations-list-item-name-image-container-image {
|
||||
height: 12px;
|
||||
width: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
&:nth-child(even) {
|
||||
background: var(--l2-background);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: var(--l3-background-hover);
|
||||
}
|
||||
}
|
||||
|
||||
.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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
import { useMemo } from 'react';
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { Badge } from '@signozhq/ui';
|
||||
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 awwSnapIconUrl from '@/assets/Icons/awwSnap.svg';
|
||||
import dottedDoubleLineUrl from '@/assets/svgs/dotted-double-line.svg';
|
||||
|
||||
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={awwSnapIconUrl} 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={dottedDoubleLineUrl}
|
||||
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={awwSnapIconUrl}
|
||||
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>
|
||||
|
||||
<div className="integrations-list-item-name-text">
|
||||
{integration.title}
|
||||
</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;
|
||||
@@ -0,0 +1,82 @@
|
||||
.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(--l2-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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.one-click-integrations-list-item-description {
|
||||
color: var(--l2-foreground);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: var(--l3-background);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user