Compare commits

..

19 Commits

Author SHA1 Message Date
grandwizard28
36d766d3d9 refactor(fixtures/seeder): align status codes with HTTP semantics
- POST /telemetry/{traces,logs,metrics}: return 201 Created (kept the
  {inserted: N} body so callers can verify the count landed).
- DELETE /telemetry/{traces,logs,metrics}: return 204 No Content with
  an empty body.
2026-04-21 17:49:59 +05:30
grandwizard28
96188a38b4 feat(fixtures/seeder): add logs and metrics endpoints
Extend the seeder with POST/DELETE endpoints for logs and metrics,
following the same shape as the existing traces endpoints:

- POST /telemetry/logs accepts a JSON list matching Logs.from_dict;
  tags each row's resources with seeder=true.
- POST /telemetry/metrics accepts a JSON list matching Metrics.from_dict;
  tags resource_attrs with seeder=true (Metrics.from_dict unpacks
  resource_attrs rather than a resources dict).
- DELETE /telemetry/logs, DELETE /telemetry/metrics truncate via the
  shared truncate_*_tables helpers.

Requirements gain svix-ksuid because fixtures/logs.py imports KsuidMs
for log id generation.

Verified end-to-end against the warm backend: POST inserted=1 on each
signal, DELETE truncated=true on each.
2026-04-21 17:00:02 +05:30
grandwizard28
8cfa3bbe94 refactor(fixtures/logs,metrics): extract insert + truncate helpers
Mirror the traces refactor: pull the ClickHouse insert path out of the
insert_logs / insert_metrics pytest fixtures into plain module-level
functions (insert_logs_to_clickhouse, insert_metrics_to_clickhouse) and
move the per-table TRUNCATE loops into truncate_logs_tables /
truncate_metrics_tables. The fixtures become thin wrappers — zero
behavioural change.

Sets up the seeder container to expose POST/DELETE endpoints for logs
and metrics using the exact same code paths as the pytest fixtures.
2026-04-21 16:59:13 +05:30
grandwizard28
0d97f543df fix(alerts-downtime): capture load-time GETs before navigation
Flow 1 registered cap.mark() AFTER page.goto() and then called
page.waitForResponse(/api/v2/rules) — but against a fast local backend
the GET /api/v2/rules response arrived during page.goto, before the
waiter could register, and the test timed out at 30s.

installCapture's page.on('response') listener runs from before the
navigation, so moving mark() above page.goto() and relying on
dumpSince's 500ms drain is enough. No lost precision.

One site only; the same pattern exists in later flows (via per-action
waitForResponse) and may surface similar races — those are left for a
follow-up once the backend-side 2095 migration lands on main (current
frontend still calls PATCH /api/v1/rules/:id which the spec's assertion
doesn't match anyway).
2026-04-21 00:48:14 +05:30
grandwizard28
be7099b2b4 feat(tests/e2e): surface seeder_url to Playwright via globalSetup
- bootstrap/setup.py: test_setup now depends on the seeder fixture and
  writes seeder_url into .signoz-backend.json alongside base_url.
- bootstrap/run.py: test_e2e exports SIGNOZ_E2E_SEEDER_URL to the
  subprocessed yarn test so Playwright specs can reach the seeder
  directly in the one-command path.
- global.setup.ts: if .signoz-backend.json carries seeder_url, populate
  process.env.SIGNOZ_E2E_SEEDER_URL. Remains optional — staging mode
  leaves it unset.

Playwright specs that want per-test telemetry can:
  await fetch(process.env.SIGNOZ_E2E_SEEDER_URL + '/telemetry/traces', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify([...])
  });
and await a truncate via DELETE on teardown.
2026-04-21 00:47:58 +05:30
grandwizard28
ab6e8291fe feat(fixtures/seeder): HTTP seeder container for fine-grained telemetry seeding
Adds a sibling container alongside signoz/clickhouse/postgres that exposes
HTTP endpoints for direct-ClickHouse telemetry seeding, so Playwright
tests can shape per-test data without going through OTel or the SigNoz
ingestion path.

tests/fixtures/seeder/:
- Dockerfile: python:3.13-slim + the shared fixtures/ tree so the
  container can import fixtures.traces and reuse the exact insert path
  used by pytest.
- server.py: FastAPI app with GET /healthz, POST /telemetry/traces
  (accepts a JSON list matching Traces.from_dict input; auto-tags each
  inserted row with resource seeder=true), DELETE /telemetry/traces
  (truncates all traces tables).
- requirements.txt: fastapi, uvicorn, clickhouse-connect, numpy plus
  sqlalchemy/pytest/testcontainers because fixtures/{__init__,types,
  traces}.py import them at module load.

tests/fixtures/seeder/__init__.py: pytest fixture (`seeder`, package-
scoped) that builds the image via docker-py (testcontainers DockerImage
had multi-segment dockerfile issues), starts the container on the
shared network wired to ClickHouse via env vars, and waits for
/healthz. Cache key + restore follow the dev.wrap pattern other
fixtures use for --reuse.

tests/.dockerignore: exclude .venv, caches, e2e node_modules, and test
outputs so the build context is small and deterministic.

tests/conftest.py: register fixtures.seeder as a pytest plugin.

Currently traces-only — logs + metrics follow the same pattern.
2026-04-21 00:47:43 +05:30
grandwizard28
0839c532bc refactor(fixtures/traces): extract insert + truncate helpers
Pull the ClickHouse insert path out of the insert_traces pytest fixture
into a plain module-level function insert_traces_to_clickhouse(conn,
traces), and move the per-table TRUNCATE loop into truncate_traces_tables
(conn, cluster). The fixture becomes a thin wrapper over both — zero
behavioural change.

Lets the HTTP seeder container (tests/fixtures/seeder/) reuse the exact
same insert + truncate code the pytest fixture uses, so the two stay in
sync as the trace schema evolves.
2026-04-21 00:47:23 +05:30
grandwizard28
5ef206a666 feat(tests/e2e): alerts-downtime regression suite (platform-pod/issues/2095)
Import the 34-step regression suite originally developed on
platform-pod/issues/2095-frontend. Targets the alerts and planned-downtime
frontend flows after their migration to generated OpenAPI clients and
generated react-query hooks.

- specs/alerts-downtime/: SUITE.md (the stable spec), README.md (scope +
  open observations from the original runs), results-schema.md (legacy
  per-run artifact shape, retained for context).
- tests/alerts-downtime/alerts-downtime.spec.ts: 881-line Playwright spec
  covering 6 flows — alert CRUD/toggle, alert detail 404, planned
  downtime CRUD, notification channel routing, anomaly alerts.

Integration with the shared suite:
- Uses baseURL + storageState from tests/e2e/playwright.config.ts (no
  separate config). page.goto calls use relative paths; SIGNOZ_E2E_*
  env vars from the pytest bootstrap drive auth.
- test.describe.configure({ mode: 'serial' }) at the top of the describe:
  the flows mutate shared tenant state, so parallel runs cause cross-
  flow interference (documented in the original 2095 config).
- Per-run artifacts (network captures + screenshots) land in
  tests/e2e/tests/alerts-downtime/run-spec-<ts>/ by default — gitignored.

Historical per-run artifacts (~7.5MB of screenshots across run-1 through
run-7) are not imported; they lived at e2e/2095/run-*/ on the original
branch and remain there if needed.
2026-04-20 23:34:12 +05:30
grandwizard28
fce92115a9 fix(tests/fixtures/signoz.py): anchor Docker build context to repo root
Previously used path="../../" which resolved to the repo root only when
pytest's cwd was tests/integration/. After hoisting the pytest project
to tests/, that same relative path pointed one level above the repo
root and the build failed with:

  Cannot locate specified Dockerfile: cmd/enterprise/Dockerfile.with-web.integration

Anchor the build context to an absolute path computed from __file__ so
the fixture works regardless of pytest cwd.
2026-04-20 21:45:27 +05:30
grandwizard28
9743002edf docs(tests): describe pytest-master workflow and shared fixture layout
- tests/README.md (new): top-level map of the shared pytest project,
  fixture-ownership rule (shared vs per-tree), and common commands.
- tests/e2e/README.md: lead with the one-command pytest run and the
  warm-backend dev loop; keep the staging fallback as option 2.
- tests/e2e/CLAUDE.md: updated commands so agent contexts reflect the
  pytest-driven lifecycle.
- tests/e2e/.env.example: drop unused SIGNOZ_E2E_ENV_TYPE; note the file
  is only needed for staging mode.
2026-04-20 21:06:32 +05:30
grandwizard28
0efde7b5ce feat(tests/e2e): pytest-driven backend bring-up, seeding, and playwright runner
Wire the Playwright suite into the shared pytest fixture graph so the
backend + its seeded state are provisioned locally instead of pointing
at remote staging.

Python side (owns lifecycle):
- tests/fixtures/dashboards.py — generic create/list/upsert_dashboard
  helpers (shared infra; testdata stays per-tree).
- tests/e2e/conftest.py — e2e-scoped pytest fixtures: seed_dashboards
  (idempotent upsert from tests/e2e/testdata/dashboards/*.json),
  seed_alert_rules (from tests/e2e/testdata/alerts/*.json, via existing
  create_alert_rule), seed_e2e_telemetry (fresh traces/logs across a
  few synthetic services so /home and Services pages have data).
- tests/e2e/src/bootstrap/setup.py — test_setup depends on the fixture
  graph and persists backend coordinates to tests/e2e/.signoz-backend.json;
  test_teardown is the --teardown target.
- tests/e2e/src/bootstrap/run.py — test_e2e: one-command entrypoint that
  brings up the backend + seeds, then subprocesses yarn test and asserts
  Playwright exits 0.
- tests/conftest.py — register fixtures.dashboards plugin.

Playwright side (just reads):
- tests/e2e/global.setup.ts — loads .signoz-backend.json and injects
  SIGNOZ_E2E_BASE_URL/USERNAME/PASSWORD. No-op when env is already
  populated (staging mode, or pytest-driven runs where env is pre-set).
- playwright.config.ts registers globalSetup.
- package.json gains test:staging; existing scripts unchanged.

Testdata layout: tests/e2e/testdata/{dashboards,alerts,channels}/*.json
— per-tree (integration has its own tests/integration/testdata/).
2026-04-20 21:03:52 +05:30
grandwizard28
8bdaecbe25 feat(tests/e2e): import Playwright suite from signoz-e2e
Relocate the standalone signoz-e2e repository into tests/e2e/ as a
sibling of tests/integration/. The suite still points at remote
staging by default; subsequent commits wire it to the shared pytest
fixture graph so the backend can be provisioned locally.

Excluded from the import: .git, .github (CI migration deferred),
.auth, node_modules, test-results, playwright-report.
2026-04-20 20:40:02 +05:30
grandwizard28
deb90abd9c refactor(tests): hoist pytest project to tests/ root for shared fixtures
Lift pyproject.toml, uv.lock, conftest.py, and fixtures/ up from
tests/integration/ so the pytest project becomes shared infrastructure
rather than integration's private property. A sibling tests/e2e/ can
reuse the same fixture graph (containers, auth, seeding) without
duplicating plugins.

Also:
- Merge tests/integration/src/querier/util.py into tests/fixtures/querier.py
  (response assertions and corrupt-metadata generators belong with the
  other querier helpers).
- Use --import-mode=importlib + pythonpath=["."] in pyproject so
  same-basename tests across src/*/ do not collide at the now-wider
  rootdir.
- Broaden python_files to "*/src/**/**.py" so future test trees under
  tests/e2e/src/ get discovered.
- Update Makefile py-* targets and integrationci.yaml to cd into tests/
  and reference integration/src/... paths.
2026-04-20 20:39:16 +05:30
Pandey
52992c0e80 chore(switch): switch for some time to @therealpandey (#11017) 2026-04-20 13:29:03 +00:00
Yunus M
5d6ada7a5b fix: semantic token issues in aws refactor (#11014)
* fix: semantic token issues in aws refactor

* fix: semantic token issues in aws refactor

* chore: remove unnecessary light mode styles
2026-04-20 12:34:25 +00:00
Nikhil Soni
dbe55d4ae0 chore: remove setting of trace cache since it was not getting used (#10986)
* chore: remove caching spans since v2 was not using it

So we can directly introduce redis instead of relying
on in-memory cache

* chore: remove unnecessary logs
2026-04-20 11:11:12 +00:00
Yunus M
837da705b3 refactor: aws integrations (#10937)
* chore: clean up integrations code for better code organisation and extensibility

* feat: render integration in new route

* refactor: reorganize AWS integration components and update imports

- Moved AWS-related components to a new directory structure for better organization.
- Updated import paths to reflect the new structure.
- Removed unused components and styles related to the previous integration setup.
- Adjusted constants and integration logic to ensure compatibility with the new structure.

* feat: enhance IntegrationDetailHeader with loading state and styles

* feat: improve light mode styles

* feat: improve light mode styles

* feat: add new Azure integration components and update existing ones

* refactor: update integration types and improve imports

* refactor: update integration types and improve imports

* feat: integrate azure account connect / edit APIs

* feat: integrate service update api

* fix: sorting logic for enabled and not enabled services

* fix: aws integration - minor ui improvements

* feat: add search functionality and no results UI for integrations

* feat: integrate disconnect integration api

* fix: update integrations util path to fix test case

* chore: move cursor rules to folder to follow the current format

* chore: remove cursor rules from gitignore

* chore: skip request integration service test in aws

* fix: show scrollbar in drawer for overflowing content

* fix: selected service getting reset on config update

* feat: use semantic tokens

* feat: update aws integrations as per new design

* refactor: enhance AWS service details and list UI with loading states and improved layout

* refactor: remove unused AWS service components and update connection status handling

* feat: add S3BucketsSelector component and integrate it into ServiceDetails

* feat: implement ServiceDetails for S3 Sync with comprehensive tests and mock data

* feat: maintain width of save - discard buttons

* feat: add react-hook-form for form handling in ServiceDetails and enhance S3BucketsSelector styles

* chore: downgrade react-hook-form to version 7.40.0 in package.json and update yarn.lock

* feat: enhance AzureAccountForm with react-hook-form integration and improve styling for form

* feat: refactor S3 Sync service tests to remove unnecessary act calls and add ResizeObserver mock

* chore: add @uiw/codemirror-theme-dracula theme

* fix: use copyToClipboard instead of navigator clipboard

* refactor: update cloud integration API types

* refactor: simplify service selection logic and update dashboard URL path

* refactor: remove Azure integrations files

* feat: add providerAccountId to AWS cloud account mapping and update related components

* feat: enhance AWS services list with empty state UI and improve connection handling

* fix: use new account invalidation method and correct region selection logic

* refactor: update mock data and API response structures for AWS integration tests

* fix: do not call connection_status for aws

* fix: clear s3 buckets if log collection is false

* refactor: improve AWS cloud account mapping and clean up unused code in account settings modal

* refactor: remove unused account services and status hooks

* refactor: remove AWS API integration and related hooks

* refactor: remove unused api and components

* refactor: remove duplicate files

* refactor: remove unused codemirror theme

* refactor: remove unused Azure account configuration interfaces

* feat: update image imports in ServicesList and IntegrationsList components

* refactor: update image imports to use URL constructor and unify toast imports from @signozhq/ui

* refactor: update integration icons to use imported assets for AWS and Azure logos

* fix: use semantic tokens

* fix: use semantic tokens

* fix: format style files

* refactor: remove unused SVG and test files, update styles and structure in Integrations components
2026-04-20 11:09:13 +00:00
swapnil-signoz
a9b458f1f6 refactor: moving types to cloud provider specific namespace/pkg (#10976)
* refactor: moving types to cloud provider specific namespace/pkg

* refactor: separating cloud provider types

* refactor: using upper case key for AWS
2026-04-20 10:42:13 +00:00
Naman Verma
ac26299c3d docs: perses schema for dashboards (#10609)
Some checks failed
build-staging / prepare (push) Has been cancelled
build-staging / js-build (push) Has been cancelled
build-staging / go-build (push) Has been cancelled
build-staging / staging (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
* docs: perses schema for dashboards

* chore: no need for Signal type in commons, only used once

* chore: no need for PageSize type in commons, only used once

* chore: rm comment

* chore: remove stub for time series chart

* chore: remove manually written manifest and package

* chore: remove validate file

* chore: no config folder

* chore: no config folder

* chore: no commons (for now)

* feat: validation script

* fix: remove fields from variable specs that are there in ListVariable

* chore: test file with way more examples

* chore: test file with way more examples

* chore: checkpoint for half correct setup

* chore: rearrange specs in package.json

* chore: py script not needed

* chore: rename

* chore: folders in schemas for arranging

* chore: folders in schemas for arranging

* fix: proper composite query schema

* feat: custom time series schema

* chore: comment explaining when to use composite query and when not

* feat: promql example

* chore: remove upstream import

* fix: promql fix

* docs: time series panel schema without upstream ref

* chore: object for visualization section

* docs: bar chart panel schema without upstream ref

* docs: number panel schema without upstream ref

* docs: number panel schema without upstream ref

* docs: pie chart panel schema without upstream ref

* docs: table chart panel schema without upstream ref

* docs: histogram chart panel schema without upstream ref

* docs: list panel schema without upstream ref

* chore: a more complex example

* chore: examples for panel types

* chore: remaining fields file

* fix: no more online validation

* chore: replace yAxisUnit by unit

* chore: no need for threshold prefix inside threshold obj

* chore: remove unimplemented join query schema

* fix: no nesting in context links

* fix: less verbose field names in dynamic var

* chore: actually name every panel as a panel

* chore: common package for panels' repeated definitions

* chore: common package for queries' repeated definitions

* chore: common package for variables' repeated definitions

* fix: functions in formula

* fix: only allow one of metric or expr aggregation in builder query

* fix: datasource in perses.json

* fix: promql step duration schema

* fix: proper type for selectFields

* chore: single version for all schemas

* fix: normalise enum defs

* chore: change attr name to name

* chore: common threshold type

* chore: doc for how to add a panel spec

* feat: textbox variable

* feat: go struct based schema for dashboardv2 with validations and some tests

* fix: go mod fix

* chore: perses folder not needed anymore

* chore: use perses updated/createdat

* fix: builder query validation (might need to revisit, 3 types seems bad)

* chore: go lint fixes

* chore: define constants for enum values

* chore: nil factory case not needed

* chore: nil factory case not needed

* chore: slight rearrange for builder spec readability

* feat: add TimeSeriesChartAppearance

* chore: no omit empty

* chore: span gaps in schema

* chore: context link not needed in plugins

* chore: remove format from threshold with label, rearrange structs

* test: fix unit tests

* chore: refer to common struct

* feat: query type and panel type matching

* test: unit tests improvement first pass

* test: unit tests improvement second pass

* test: unit tests improvement third pass

* test: unit tests improvement fourth pass

* test: unit test for dashboard with sections

* test: unit test for dashboard with sections

* fix: add missing dashboard metadata fields

* chore: go lint fixes

* chore: go lint fixes

* chore: changes for create v2 api

* chore: more info in StorableDashboardDataV2

* chore: diff check in update method

* chore: add required true tag to required fields

* feat: update metadata methods

* chore: go mod tidy

* chore: put id in metadata.name, authtypes for v2

* revert: only the schema for now in this PR

* chore: comment for why v1.DashboardSpec is chosen

* chore: change source to signal in DynamicVariableSpec

* fix: string values for precision option

* feat: literal options for comparison operator

* fix: missing required tag in threshold fields

* chore: use valuer.string for plugin kind enums

* chore: use only TelemetryFieldKey in ListPanelSpec

* chore: simplify variable plugin validation

* fix: do not allow nil panels

* fix: do not allow nil plugin spec

* fix: signal should be an enum not a string

* chore: rearrange enums to separate those with default values

* test: unit tests for invalid enum values

* fix: all enums should have a default value

* refactor: extract UnmarshalBuilderQueryBySignal to deduplicate signal dispatch

* refactor: proper struct for span gaps

* chore: back to normal strings for kind enums

* chore: ticks in err messages

* chore: ticks in err messages

* chore: remove unused struct

* chore: snake case for non-kind enum values

* chore: proper error wrapping

* chore: accept int values in PrecisionOption as fallback

* fix: actually update the plugin from map to custom struct

* feat: disallow unknown fields in plugins

* chore: make enums valuer.string

* chore: proper enum types in constants

* chore: rename value to avoid overriding valuer.string method

* test: db cycle test

* fix: lint fix in some other file

* test: remove collapse info from sections

* test: use testify package

---------

Co-authored-by: Srikanth Chekuri <srikanth.chekuri92@gmail.com>
2026-04-20 09:29:03 +00:00
354 changed files with 23379 additions and 6327 deletions

64
.github/CODEOWNERS vendored
View File

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

View File

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

View File

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

View File

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

View File

@@ -66,8 +66,6 @@ module.exports = {
rules: {
// Asset migration — base-path safety
'rulesdir/no-unsupported-asset-pattern': 'error',
// Base-path safety — window.open and origin-concat patterns
'rulesdir/no-raw-absolute-path': 'error',
// Code quality rules
'prefer-const': 'error', // Enforces const for variables never reassigned
@@ -215,31 +213,6 @@ module.exports = {
message:
'Avoid calling .getState() directly. Export a standalone action from the store instead.',
},
{
selector:
"MemberExpression[object.name='window'][property.name='localStorage']",
message:
'Use getLocalStorageKey/setLocalStorageKey/removeLocalStorageKey from api/browser/localstorage instead.',
},
{
selector:
"MemberExpression[object.name='window'][property.name='sessionStorage']",
message:
'Use getSessionStorageApi/setSessionStorageApi/removeSessionStorageApi from api/browser/sessionstorage instead.',
},
],
'no-restricted-globals': [
'error',
{
name: 'localStorage',
message:
'Use getLocalStorageKey/setLocalStorageKey/removeLocalStorageKey from api/browser/localstorage instead.',
},
{
name: 'sessionStorage',
message:
'Use getSessionStorageApi/setSessionStorageApi/removeSessionStorageApi from api/browser/sessionstorage instead.',
},
],
},
overrides: [
@@ -273,11 +246,6 @@ module.exports = {
'sonarjs/cognitive-complexity': 'off', // Tests can be complex
'sonarjs/no-identical-functions': 'off', // Similar test patterns are OK
'sonarjs/no-small-switch': 'off', // Small switches are OK in tests
// Test assertions intentionally reference window.location.origin for expected-value checks
'rulesdir/no-raw-absolute-path': 'off',
// Tests may access storage directly for setup/assertion/spy purposes
'no-restricted-globals': 'off',
'no-restricted-syntax': 'off',
},
},
{

View File

@@ -1,153 +0,0 @@
'use strict';
/**
* ESLint rule: no-raw-absolute-path
*
* Catches patterns that break at runtime when the app is served from a
* sub-path (e.g. /signoz/):
*
* 1. window.open(path, '_blank')
* → use openInNewTab(path) which calls withBasePath internally
*
* 2. window.location.origin + path / `${window.location.origin}${path}`
* → use getAbsoluteUrl(path)
*
* 3. frontendBaseUrl: window.location.origin (bare origin usage)
* → use getBaseUrl() to include the base path
*
* 4. window.location.href = path
* → use withBasePath(path) or navigate() for internal navigation
*
* External URLs (first arg starts with "http") are explicitly allowed.
*/
function isOriginAccess(node) {
return (
node.type === 'MemberExpression' &&
!node.computed &&
node.property.name === 'origin' &&
node.object.type === 'MemberExpression' &&
!node.object.computed &&
node.object.property.name === 'location' &&
node.object.object.type === 'Identifier' &&
node.object.object.name === 'window'
);
}
function isHrefAccess(node) {
return (
node.type === 'MemberExpression' &&
!node.computed &&
node.property.name === 'href' &&
node.object.type === 'MemberExpression' &&
!node.object.computed &&
node.object.property.name === 'location' &&
node.object.object.type === 'Identifier' &&
node.object.object.name === 'window'
);
}
function isExternalUrl(node) {
if (node.type === 'Literal' && typeof node.value === 'string') {
return node.value.startsWith('http://') || node.value.startsWith('https://');
}
if (node.type === 'TemplateLiteral' && node.quasis.length > 0) {
const raw = node.quasis[0].value.raw;
return raw.startsWith('http://') || raw.startsWith('https://');
}
return false;
}
// window.open(withBasePath(x)) and window.open(getAbsoluteUrl(x)) are already safe.
function isSafeHelperCall(node) {
return (
node.type === 'CallExpression' &&
node.callee.type === 'Identifier' &&
(node.callee.name === 'withBasePath' || node.callee.name === 'getAbsoluteUrl')
);
}
module.exports = {
meta: {
type: 'suggestion',
docs: {
description:
'Disallow raw window.open and origin-concatenation patterns that miss the runtime base path',
category: 'Base Path Safety',
},
schema: [],
messages: {
windowOpen:
'Use openInNewTab(path) instead of window.open(path, "_blank") — openInNewTab prepends the base path automatically.',
originConcat:
'Use getAbsoluteUrl(path) instead of window.location.origin + path — getAbsoluteUrl prepends the base path automatically.',
originDirect:
'Use getBaseUrl() instead of window.location.origin — getBaseUrl includes the base path.',
hrefAssign:
'Use withBasePath(path) or navigate() instead of window.location.href = path — ensures the base path is included.',
},
},
create(context) {
return {
// window.open(path, ...) — allow only external first-arg URLs
CallExpression(node) {
const { callee, arguments: args } = node;
if (
callee.type !== 'MemberExpression' ||
callee.object.type !== 'Identifier' ||
callee.object.name !== 'window' ||
callee.property.name !== 'open'
)
return;
if (args.length < 1) return;
if (isExternalUrl(args[0])) return;
if (isSafeHelperCall(args[0])) return;
context.report({ node, messageId: 'windowOpen' });
},
// window.location.origin + path
BinaryExpression(node) {
if (node.operator !== '+') return;
if (isOriginAccess(node.left) || isOriginAccess(node.right)) {
context.report({ node, messageId: 'originConcat' });
}
},
// `${window.location.origin}${path}`
TemplateLiteral(node) {
if (node.expressions.some(isOriginAccess)) {
context.report({ node, messageId: 'originConcat' });
}
},
// window.location.origin used directly (not in concatenation)
// Catches: frontendBaseUrl: window.location.origin
MemberExpression(node) {
if (!isOriginAccess(node)) return;
const parent = node.parent;
// Skip if parent is BinaryExpression with + (handled by BinaryExpression visitor)
if (parent.type === 'BinaryExpression' && parent.operator === '+') return;
// Skip if inside TemplateLiteral (handled by TemplateLiteral visitor)
if (parent.type === 'TemplateLiteral') return;
context.report({ node, messageId: 'originDirect' });
},
// window.location.href = path
AssignmentExpression(node) {
if (node.operator !== '=') return;
if (!isHrefAccess(node.left)) return;
// Allow external URLs
if (isExternalUrl(node.right)) return;
// Allow safe helper calls
if (isSafeHelperCall(node.right)) return;
context.report({ node, messageId: 'hrefAssign' });
},
};
},
};

View File

@@ -2,7 +2,6 @@
<html lang="en">
<head>
<meta charset="utf-8" />
<base href="[[.BaseHref]]" />
<meta
http-equiv="Cache-Control"
content="no-cache, no-store, must-revalidate, max-age: 0"
@@ -40,12 +39,12 @@
<meta
data-react-helmet="true"
property="og:image"
content="[[.BaseHref]]images/signoz-hero-image.webp"
content="/images/signoz-hero-image.webp"
/>
<meta
data-react-helmet="true"
name="twitter:image"
content="[[.BaseHref]]images/signoz-hero-image.webp"
content="/images/signoz-hero-image.webp"
/>
<meta
data-react-helmet="true"
@@ -60,7 +59,7 @@
<meta data-react-helmet="true" name="docusaurus_locale" content="en" />
<meta data-react-helmet="true" name="docusaurus_tag" content="default" />
<meta name="robots" content="noindex" />
<link data-react-helmet="true" rel="shortcut icon" href="favicon.ico" />
<link data-react-helmet="true" rel="shortcut icon" href="/favicon.ico" />
</head>
<body data-theme="default">
<script>
@@ -137,7 +136,7 @@
})(document, 'script');
}
</script>
<link rel="stylesheet" href="css/uPlot.min.css" />
<link rel="stylesheet" href="/css/uPlot.min.css" />
<script type="module" src="./src/index.tsx"></script>
</body>
</html>

View File

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

View File

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

View File

@@ -18,7 +18,8 @@ import {
ForgotPassword,
Home,
InfrastructureMonitoring,
InstalledIntegrations,
Integrations,
IntegrationsDetailsPage,
LicensePage,
ListAllALertsPage,
LiveLogs,
@@ -389,10 +390,17 @@ const routes: AppRoutes[] = [
isPrivate: true,
key: 'WORKSPACE_ACCESS_RESTRICTED',
},
{
path: ROUTES.INTEGRATIONS_DETAIL,
exact: true,
component: IntegrationsDetailsPage,
isPrivate: true,
key: 'INTEGRATIONS_DETAIL',
},
{
path: ROUTES.INTEGRATIONS,
exact: true,
component: InstalledIntegrations,
component: Integrations,
isPrivate: true,
key: 'INTEGRATIONS',
},

View File

@@ -2,7 +2,6 @@ import { initReactI18next } from 'react-i18next';
import i18n from 'i18next';
import LanguageDetector from 'i18next-browser-languagedetector';
import Backend from 'i18next-http-backend';
import { getBasePath } from 'utils/basePath';
import cacheBursting from '../../i18n-translations-hash.json';
@@ -25,7 +24,7 @@ i18n
const ns = namespace[0];
const pathkey = `/${language}/${ns}`;
const hash = cacheBursting[pathkey as keyof typeof cacheBursting] || '';
return `${getBasePath()}locales/${language}/${namespace}.json?h=${hash}`;
return `/locales/${language}/${namespace}.json?h=${hash}`;
},
},
react: {

View File

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

View File

@@ -1,9 +1,6 @@
/* eslint-disable no-restricted-globals */
import { getScopedKey } from 'utils/storage';
const get = (key: string): string | null => {
try {
return localStorage.getItem(getScopedKey(key));
return localStorage.getItem(key);
} catch (e) {
return '';
}

View File

@@ -1,9 +1,6 @@
/* eslint-disable no-restricted-globals */
import { getScopedKey } from 'utils/storage';
const remove = (key: string): boolean => {
try {
localStorage.removeItem(getScopedKey(key));
window.localStorage.removeItem(key);
return true;
} catch (e) {
return false;

View File

@@ -1,9 +1,6 @@
/* eslint-disable no-restricted-globals */
import { getScopedKey } from 'utils/storage';
const set = (key: string, value: string): boolean => {
try {
localStorage.setItem(getScopedKey(key), value);
localStorage.setItem(key, value);
return true;
} catch (e) {
return false;

View File

@@ -1,12 +0,0 @@
/* eslint-disable no-restricted-globals */
import { getScopedKey } from 'utils/storage';
const get = (key: string): string | null => {
try {
return sessionStorage.getItem(getScopedKey(key));
} catch (e) {
return '';
}
};
export default get;

View File

@@ -1,13 +0,0 @@
/* eslint-disable no-restricted-globals */
import { getScopedKey } from 'utils/storage';
const remove = (key: string): boolean => {
try {
sessionStorage.removeItem(getScopedKey(key));
return true;
} catch (e) {
return false;
}
};
export default remove;

View File

@@ -1,13 +0,0 @@
/* eslint-disable no-restricted-globals */
import { getScopedKey } from 'utils/storage';
const set = (key: string, value: string): boolean => {
try {
sessionStorage.setItem(getScopedKey(key), value);
return true;
} catch (e) {
return false;
}
};
export default set;

View File

@@ -1,6 +1,5 @@
import {
interceptorRejected,
interceptorsRequestBasePath,
interceptorsRequestResponse,
interceptorsResponse,
} from 'api';
@@ -18,7 +17,6 @@ export const GeneratedAPIInstance = <T>(
return generatedAPIAxiosInstance({ ...config }).then(({ data }) => data);
};
generatedAPIAxiosInstance.interceptors.request.use(interceptorsRequestBasePath);
generatedAPIAxiosInstance.interceptors.request.use(interceptorsRequestResponse);
generatedAPIAxiosInstance.interceptors.response.use(
interceptorsResponse,

View File

@@ -11,7 +11,6 @@ import axios, {
import { ENVIRONMENT } from 'constants/env';
import { Events } from 'constants/events';
import { LOCALSTORAGE } from 'constants/localStorage';
import { getBasePath } from 'utils/basePath';
import { eventEmitter } from 'utils/getEventEmitter';
import apiV1, { apiAlertManager, apiV2, apiV3, apiV4, apiV5 } from './apiV1';
@@ -68,39 +67,6 @@ export const interceptorsRequestResponse = (
return value;
};
// Strips the leading '/' from path and joins with base — idempotent if already prefixed.
// e.g. prependBase('/signoz/', '/api/v1/') → '/signoz/api/v1/'
function prependBase(base: string, path: string): string {
return path.startsWith(base) ? path : base + path.slice(1);
}
// Prepends the runtime base path to outgoing requests so API calls work under
// a URL prefix (e.g. /signoz/api/v1/…). No-op for root deployments and dev
// (dev baseURL is a full http:// URL, not an absolute path).
export const interceptorsRequestBasePath = (
value: InternalAxiosRequestConfig,
): InternalAxiosRequestConfig => {
const basePath = getBasePath();
if (basePath === '/') {
return value;
}
if (value.baseURL?.startsWith('/')) {
// Production relative baseURL: '/api/v1/' → '/signoz/api/v1/'
value.baseURL = prependBase(basePath, value.baseURL);
} else if (value.baseURL?.startsWith('http')) {
// Dev absolute baseURL (VITE_FRONTEND_API_ENDPOINT): 'https://host/api/v1/' → 'https://host/signoz/api/v1/'
const url = new URL(value.baseURL);
url.pathname = prependBase(basePath, url.pathname);
value.baseURL = url.toString();
} else if (!value.baseURL && value.url?.startsWith('/')) {
// Orval-generated client (empty baseURL, path in url): '/api/signoz/v1/rules' → '/signoz/api/signoz/v1/rules'
value.url = prependBase(basePath, value.url);
}
return value;
};
export const interceptorRejected = async (
value: AxiosResponse<any>,
): Promise<AxiosResponse<any>> => {
@@ -167,7 +133,6 @@ const instance = axios.create({
});
instance.interceptors.request.use(interceptorsRequestResponse);
instance.interceptors.request.use(interceptorsRequestBasePath);
instance.interceptors.response.use(interceptorsResponse, interceptorRejected);
export const AxiosAlertManagerInstance = axios.create({
@@ -182,7 +147,6 @@ ApiV2Instance.interceptors.response.use(
interceptorRejected,
);
ApiV2Instance.interceptors.request.use(interceptorsRequestResponse);
ApiV2Instance.interceptors.request.use(interceptorsRequestBasePath);
// axios V3
export const ApiV3Instance = axios.create({
@@ -194,7 +158,6 @@ ApiV3Instance.interceptors.response.use(
interceptorRejected,
);
ApiV3Instance.interceptors.request.use(interceptorsRequestResponse);
ApiV3Instance.interceptors.request.use(interceptorsRequestBasePath);
//
// axios V4
@@ -207,7 +170,6 @@ ApiV4Instance.interceptors.response.use(
interceptorRejected,
);
ApiV4Instance.interceptors.request.use(interceptorsRequestResponse);
ApiV4Instance.interceptors.request.use(interceptorsRequestBasePath);
//
// axios V5
@@ -220,7 +182,6 @@ ApiV5Instance.interceptors.response.use(
interceptorRejected,
);
ApiV5Instance.interceptors.request.use(interceptorsRequestResponse);
ApiV5Instance.interceptors.request.use(interceptorsRequestBasePath);
//
// axios Base
@@ -233,7 +194,6 @@ LogEventAxiosInstance.interceptors.response.use(
interceptorRejectedBase,
);
LogEventAxiosInstance.interceptors.request.use(interceptorsRequestResponse);
LogEventAxiosInstance.interceptors.request.use(interceptorsRequestBasePath);
//
AxiosAlertManagerInstance.interceptors.response.use(
@@ -241,7 +201,6 @@ AxiosAlertManagerInstance.interceptors.response.use(
interceptorRejected,
);
AxiosAlertManagerInstance.interceptors.request.use(interceptorsRequestResponse);
AxiosAlertManagerInstance.interceptors.request.use(interceptorsRequestBasePath);
export { apiV1 };
export default instance;

View File

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

View File

@@ -3,16 +3,13 @@ import getLocalStorageKey from 'api/browser/localstorage/get';
import { ENVIRONMENT } from 'constants/env';
import { LOCALSTORAGE } from 'constants/localStorage';
import { EventSourcePolyfill } from 'event-source-polyfill';
import { withBasePath } from 'utils/basePath';
// 10 min in ms
const TIMEOUT_IN_MS = 10 * 60 * 1000;
export const LiveTail = (queryParams: string): EventSourcePolyfill =>
new EventSourcePolyfill(
ENVIRONMENT.baseURL
? `${ENVIRONMENT.baseURL}${apiV1}logs/tail?${queryParams}`
: withBasePath(`${apiV1}logs/tail?${queryParams}`),
`${ENVIRONMENT.baseURL}${apiV1}logs/tail?${queryParams}`,
{
headers: {
Authorization: `Bearer ${getLocalStorageKey(LOCALSTORAGE.AUTH_TOKEN)}`,

View 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

View File

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

View File

@@ -12,7 +12,6 @@ import { AppState } from 'store/reducers';
import { Query, TagFilterItem } from 'types/api/queryBuilder/queryBuilderData';
import { DataSource, MetricAggregateOperator } from 'types/common/queryBuilder';
import { GlobalReducer } from 'types/reducer/globalTime';
import { withBasePath } from 'utils/basePath';
export interface NavigateToExplorerProps {
filters: TagFilterItem[];
@@ -134,7 +133,7 @@ export function useNavigateToExplorer(): (
QueryParams.compositeQuery
}=${JSONCompositeQuery}`;
window.open(withBasePath(newExplorerPath), sameTab ? '_self' : '_blank');
window.open(newExplorerPath, sameTab ? '_self' : '_blank');
},
[
prepareQuery,

View File

@@ -9,7 +9,6 @@ import { CreditCard, MessageSquareText, X } from 'lucide-react';
import { SuccessResponseV2 } from 'types/api';
import { CheckoutSuccessPayloadProps } from 'types/api/billing/checkout';
import APIError from 'types/api/error';
import { getBaseUrl } from 'utils/basePath';
export default function ChatSupportGateway(): JSX.Element {
const { notifications } = useNotifications();
@@ -55,7 +54,7 @@ export default function ChatSupportGateway(): JSX.Element {
});
updateCreditCard({
url: getBaseUrl(),
url: window.location.origin,
});
};

View File

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

View File

@@ -1,13 +1,18 @@
import { Table } from 'antd';
import {
CloudintegrationtypesCollectedLogAttributeDTO,
CloudintegrationtypesCollectedMetricDTO,
} from 'api/generated/services/sigNoz.schemas';
import { BarChart2, ScrollText } from 'lucide-react';
import { ServiceData } from './types';
import './CloudServiceDataCollected.styles.scss';
function CloudServiceDataCollected({
logsData,
metricsData,
}: {
logsData: ServiceData['data_collected']['logs'];
metricsData: ServiceData['data_collected']['metrics'];
logsData: CloudintegrationtypesCollectedLogAttributeDTO[] | null | undefined;
metricsData: CloudintegrationtypesCollectedMetricDTO[] | null | undefined;
}): JSX.Element {
const logsColumns = [
{
@@ -61,24 +66,30 @@ function CloudServiceDataCollected({
return (
<div className="cloud-service-data-collected">
{logsData && logsData.length > 0 && (
<div className="cloud-service-data-collected__table">
<div className="cloud-service-data-collected__table-heading">Logs</div>
<div className="cloud-service-data-collected-table">
<div className="cloud-service-data-collected-table-heading">
<ScrollText size={14} />
Logs
</div>
<Table
columns={logsColumns}
dataSource={logsData}
{...tableProps}
className="cloud-service-data-collected__table-logs"
className="cloud-service-data-collected-table-logs"
/>
</div>
)}
{metricsData && metricsData.length > 0 && (
<div className="cloud-service-data-collected__table">
<div className="cloud-service-data-collected__table-heading">Metrics</div>
<div className="cloud-service-data-collected-table">
<div className="cloud-service-data-collected-table-heading">
<BarChart2 size={14} />
Metrics
</div>
<Table
columns={metricsColumns}
dataSource={metricsData}
{...tableProps}
className="cloud-service-data-collected__table-metrics"
className="cloud-service-data-collected-table-metrics"
/>
</div>
)}

View File

@@ -31,7 +31,6 @@ import { useAppContext } from 'providers/App/App';
import { useErrorModal } from 'providers/ErrorModalProvider';
import { useTimezone } from 'providers/Timezone';
import APIError from 'types/api/error';
import { getAbsoluteUrl } from 'utils/basePath';
import { toAPIError } from 'utils/errorUtils';
import DeleteMemberDialog from './DeleteMemberDialog';
@@ -388,7 +387,7 @@ function EditMemberDrawer({
pathParams: { id: member.id },
});
if (response?.data?.token) {
const link = getAbsoluteUrl(`/password-reset?token=${response.data.token}`);
const link = `${window.location.origin}/password-reset?token=${response.data.token}`;
setResetLink(link);
setResetLinkExpiresAt(
response.data.expiresAt

View File

@@ -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');

View File

@@ -13,7 +13,6 @@ import GetMinMax from 'lib/getMinMax';
import { Check, Info, Link2 } from 'lucide-react';
import { AppState } from 'store/reducers';
import { GlobalReducer } from 'types/reducer/globalTime';
import { getAbsoluteUrl } from 'utils/basePath';
const routesToBeSharedWithTime = [
ROUTES.LOGS_EXPLORER,
@@ -81,13 +80,17 @@ function ShareURLModal(): JSX.Element {
urlQuery.delete(QueryParams.relativeTime);
currentUrl = getAbsoluteUrl(`${location.pathname}?${urlQuery.toString()}`);
currentUrl = `${window.location.origin}${
location.pathname
}?${urlQuery.toString()}`;
} else {
urlQuery.delete(QueryParams.startTime);
urlQuery.delete(QueryParams.endTime);
urlQuery.set(QueryParams.relativeTime, selectedTime);
currentUrl = getAbsoluteUrl(`${location.pathname}?${urlQuery.toString()}`);
currentUrl = `${window.location.origin}${
location.pathname
}?${urlQuery.toString()}`;
}
}

View File

@@ -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(),
}));

View File

@@ -50,7 +50,6 @@ import {
TracesAggregatorOperator,
} from 'types/common/queryBuilder';
import { GlobalReducer } from 'types/reducer/globalTime';
import { openInNewTab } from 'utils/navigation';
import { v4 as uuidv4 } from 'uuid';
import { VIEW_TYPES, VIEWS } from './constants';
@@ -331,7 +330,10 @@ function HostMetricsDetails({
urlQuery.set('compositeQuery', JSON.stringify(compositeQuery));
openInNewTab(`${ROUTES.LOGS_EXPLORER}?${urlQuery.toString()}`);
window.open(
`${window.location.origin}${ROUTES.LOGS_EXPLORER}?${urlQuery.toString()}`,
'_blank',
);
} else if (selectedView === VIEW_TYPES.TRACES) {
const compositeQuery = {
...initialQueryState,
@@ -350,7 +352,10 @@ function HostMetricsDetails({
urlQuery.set('compositeQuery', JSON.stringify(compositeQuery));
openInNewTab(`${ROUTES.TRACES_EXPLORER}?${urlQuery.toString()}`);
window.open(
`${window.location.origin}${ROUTES.TRACES_EXPLORER}?${urlQuery.toString()}`,
'_blank',
);
}
};

View File

@@ -14,7 +14,6 @@ import { useErrorModal } from 'providers/ErrorModalProvider';
import APIError from 'types/api/error';
import { ROLES } from 'types/roles';
import { EMAIL_REGEX } from 'utils/app';
import { getBaseUrl } from 'utils/basePath';
import { popupContainer } from 'utils/selectPopupContainer';
import { v4 as uuid } from 'uuid';
@@ -189,7 +188,7 @@ function InviteMembersModal({
email: row.email.trim(),
name: '',
role: row.role as ROLES,
frontendBaseUrl: getBaseUrl(),
frontendBaseUrl: window.location.origin,
});
} else {
await inviteUsers({
@@ -197,7 +196,7 @@ function InviteMembersModal({
email: row.email.trim(),
name: '',
role: row.role,
frontendBaseUrl: getBaseUrl(),
frontendBaseUrl: window.location.origin,
})),
});
}

View File

@@ -14,7 +14,6 @@ import { useAppContext } from 'providers/App/App';
import { SuccessResponseV2 } from 'types/api';
import { CheckoutSuccessPayloadProps } from 'types/api/billing/checkout';
import APIError from 'types/api/error';
import { getBaseUrl } from 'utils/basePath';
import './LaunchChatSupport.styles.scss';
@@ -155,7 +154,7 @@ function LaunchChatSupport({
});
updateCreditCard({
url: getBaseUrl(),
url: window.location.origin,
});
};

View File

@@ -1,7 +1,6 @@
import { Color } from '@signozhq/design-tokens';
import { Button } from 'antd';
import { ArrowUpRight } from 'lucide-react';
import { openInNewTab } from 'utils/navigation';
import './LearnMore.styles.scss';
@@ -15,7 +14,7 @@ function LearnMore({ text, url, onClick }: LearnMoreProps): JSX.Element {
const handleClick = (): void => {
onClick?.();
if (url) {
openInNewTab(url);
window.open(url, '_blank');
}
};
return (

View File

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

View File

@@ -20,7 +20,6 @@ import {
KAFKA_SETUP_DOC_LINK,
MessagingQueueHealthCheckService,
} from 'pages/MessagingQueues/MessagingQueuesUtils';
import { openInNewTab } from 'utils/navigation';
import { v4 as uuid } from 'uuid';
import './MessagingQueueHealthCheck.styles.scss';
@@ -77,7 +76,7 @@ function ErrorTitleAndKey({
if (isCloudUserVal && !!link) {
history.push(link);
} else {
openInNewTab(KAFKA_SETUP_DOC_LINK);
window.open(KAFKA_SETUP_DOC_LINK, '_blank');
}
};
return {

View File

@@ -1,13 +1,10 @@
import getSessionStorageApi from 'api/browser/sessionstorage/get';
import removeSessionStorageApi from 'api/browser/sessionstorage/remove';
import setSessionStorageApi from 'api/browser/sessionstorage/set';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
export const PREVIOUS_QUERY_KEY = 'previousQuery';
function getPreviousQueryFromStore(): Record<string, IBuilderQuery> {
try {
const raw = getSessionStorageApi(PREVIOUS_QUERY_KEY);
const raw = sessionStorage.getItem(PREVIOUS_QUERY_KEY);
if (!raw) {
return {};
}
@@ -20,7 +17,7 @@ function getPreviousQueryFromStore(): Record<string, IBuilderQuery> {
function writePreviousQueryToStore(store: Record<string, IBuilderQuery>): void {
try {
setSessionStorageApi(PREVIOUS_QUERY_KEY, JSON.stringify(store));
sessionStorage.setItem(PREVIOUS_QUERY_KEY, JSON.stringify(store));
} catch {
// ignore quota or serialization errors
}
@@ -66,7 +63,7 @@ export const removeKeyFromPreviousQuery = (key: string): void => {
export const clearPreviousQuery = (): void => {
try {
removeSessionStorageApi(PREVIOUS_QUERY_KEY);
sessionStorage.removeItem(PREVIOUS_QUERY_KEY);
} catch {
// no-op
}

View File

@@ -1,6 +1,3 @@
import getLocalStorageKey from 'api/browser/localstorage/get';
import setLocalStorageKey from 'api/browser/localstorage/set';
import { DynamicColumnsKey } from './contants';
import {
GetNewColumnDataFunction,
@@ -15,7 +12,7 @@ export const getVisibleColumns: GetVisibleColumnsFunction = ({
}) => {
let columnVisibilityData: { [key: string]: boolean };
try {
const storedData = getLocalStorageKey(tablesource);
const storedData = localStorage.getItem(tablesource);
if (typeof storedData === 'string' && dynamicColumns) {
columnVisibilityData = JSON.parse(storedData);
return dynamicColumns.filter((column) => {
@@ -31,7 +28,7 @@ export const getVisibleColumns: GetVisibleColumnsFunction = ({
initialColumnVisibility[key] = false;
});
setLocalStorageKey(tablesource, JSON.stringify(initialColumnVisibility));
localStorage.setItem(tablesource, JSON.stringify(initialColumnVisibility));
} catch (error) {
console.error(error);
}
@@ -45,14 +42,14 @@ export const setVisibleColumns = ({
dynamicColumns,
}: SetVisibleColumnsProps): void => {
try {
const storedData = getLocalStorageKey(tablesource);
const storedData = localStorage.getItem(tablesource);
if (typeof storedData === 'string' && dynamicColumns) {
const columnVisibilityData = JSON.parse(storedData);
const { key } = dynamicColumns[index];
if (key) {
columnVisibilityData[key] = checked;
}
setLocalStorageKey(tablesource, JSON.stringify(columnVisibilityData));
localStorage.setItem(tablesource, JSON.stringify(columnVisibilityData));
}
} catch (error) {
console.error(error);

View File

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

View File

@@ -65,6 +65,7 @@ const ROUTES = {
WORKSPACE_SUSPENDED: '/workspace-suspended',
SHORTCUTS: '/settings/shortcuts',
INTEGRATIONS: '/integrations',
INTEGRATIONS_DETAIL: '/integrations/:integrationId',
MESSAGING_QUEUES_BASE: '/messaging-queues',
MESSAGING_QUEUES_KAFKA: '/messaging-queues/kafka',
MESSAGING_QUEUES_KAFKA_DETAIL: '/messaging-queues/kafka/detail',

View File

@@ -9,7 +9,6 @@ import {
} from 'container/ApiMonitoring/utils';
import { UnfoldVertical } from 'lucide-react';
import { SuccessResponse } from 'types/api';
import { openInNewTab } from 'utils/navigation';
import emptyStateUrl from '@/assets/Icons/emptyState.svg';
@@ -95,14 +94,20 @@ function DependentServices({
}}
onRow={(record): { onClick: () => void; className: string } => ({
onClick: (): void => {
const serviceName =
record.serviceData.serviceName && record.serviceData.serviceName !== '-'
? record.serviceData.serviceName
: '';
const url = new URL(
`/services/${
record.serviceData.serviceName &&
record.serviceData.serviceName !== '-'
? record.serviceData.serviceName
: ''
}`,
window.location.origin,
);
const urlQuery = new URLSearchParams();
urlQuery.set(QueryParams.startTime, timeRange.startTime.toString());
urlQuery.set(QueryParams.endTime, timeRange.endTime.toString());
openInNewTab(`/services/${serviceName}?${urlQuery.toString()}`);
url.search = urlQuery.toString();
window.open(url.toString(), '_blank');
},
className: 'clickable-row',
})}

View File

@@ -73,7 +73,6 @@ import {
import { UserPreference } from 'types/api/preferences/preference';
import AppReducer from 'types/reducer/app';
import { USER_ROLES } from 'types/roles';
import { getBaseUrl } from 'utils/basePath';
import { showErrorNotification } from 'utils/error';
import { eventEmitter } from 'utils/getEventEmitter';
import {
@@ -462,7 +461,7 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
const handleFailedPayment = useCallback((): void => {
manageCreditCard({
url: getBaseUrl(),
url: window.location.origin,
});
}, [manageCreditCard]);

View File

@@ -31,7 +31,6 @@ import { isEmpty, pick } from 'lodash-es';
import { useAppContext } from 'providers/App/App';
import { SuccessResponseV2 } from 'types/api';
import { CheckoutSuccessPayloadProps } from 'types/api/billing/checkout';
import { getBaseUrl } from 'utils/basePath';
import { getFormattedDate, getRemainingDays } from 'utils/timeUtils';
import { BillingUsageGraph } from './BillingUsageGraph/BillingUsageGraph';
@@ -325,7 +324,7 @@ export default function BillingContainer(): JSX.Element {
});
updateCreditCard({
url: getBaseUrl(),
url: window.location.origin,
});
} else {
logEvent('Billing : Manage Billing', {
@@ -334,7 +333,7 @@ export default function BillingContainer(): JSX.Element {
});
manageCreditCard({
url: getBaseUrl(),
url: window.location.origin,
});
}
// eslint-disable-next-line react-hooks/exhaustive-deps

View File

@@ -1,24 +0,0 @@
import {
IntegrationType,
RequestIntegrationBtn,
} from 'pages/Integrations/RequestIntegrationBtn';
import Header from './Header/Header';
import HeroSection from './HeroSection/HeroSection';
import ServicesTabs from './ServicesSection/ServicesTabs';
function CloudIntegrationPage(): JSX.Element {
return (
<div>
<Header />
<HeroSection />
<RequestIntegrationBtn
type={IntegrationType.AWS_SERVICES}
message="Can't find the AWS service you're looking for? Request more integrations"
/>
<ServicesTabs />
</div>
);
}
export default CloudIntegrationPage;

View File

@@ -1,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;

View File

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

View File

@@ -1,109 +0,0 @@
import { useRef } from 'react';
import { Form } from 'antd';
import cx from 'classnames';
import { useAccountStatus } from 'hooks/integration/aws/useAccountStatus';
import { AccountStatusResponse } from 'types/api/integrations/aws';
import { regions } from 'utils/regions';
import logEvent from '../../../../api/common/logEvent';
import { ModalStateEnum, RegionFormProps } from '../types';
import AlertMessage from './AlertMessage';
import {
ComplianceNote,
MonitoringRegionsSection,
RegionDeploymentSection,
} from './IntegrateNowFormSections';
import RenderConnectionFields from './RenderConnectionParams';
const allRegions = (): string[] =>
regions.flatMap((r) => r.subRegions.map((sr) => sr.name));
const getRegionPreviewText = (regions: string[]): string[] => {
if (regions.includes('all')) {
return allRegions();
}
return regions;
};
export function RegionForm({
form,
modalState,
setModalState,
selectedRegions,
includeAllRegions,
onIncludeAllRegionsChange,
onRegionSelect,
onSubmit,
accountId,
selectedDeploymentRegion,
handleRegionChange,
connectionParams,
isConnectionParamsLoading,
}: RegionFormProps): JSX.Element {
const startTimeRef = useRef(Date.now());
const refetchInterval = 10 * 1000;
const errorTimeout = 10 * 60 * 1000;
const { isLoading: isAccountStatusLoading } = useAccountStatus(accountId, {
refetchInterval,
enabled: !!accountId && modalState === ModalStateEnum.WAITING,
onSuccess: (data: AccountStatusResponse) => {
if (data.data.status.integration.last_heartbeat_ts_ms !== null) {
setModalState(ModalStateEnum.SUCCESS);
logEvent('AWS Integration: Account connected', {
cloudAccountId: data?.data?.cloud_account_id,
status: data?.data?.status,
});
} else if (Date.now() - startTimeRef.current >= errorTimeout) {
setModalState(ModalStateEnum.ERROR);
logEvent('AWS Integration: Account connection attempt timed out', {
id: accountId,
});
}
},
onError: () => {
setModalState(ModalStateEnum.ERROR);
},
});
const isFormDisabled =
modalState === ModalStateEnum.WAITING || isAccountStatusLoading;
return (
<Form
form={form}
className="cloud-account-setup-form"
layout="vertical"
onFinish={onSubmit}
>
<AlertMessage modalState={modalState} />
<div
className={cx(`cloud-account-setup-form__content`, {
disabled: isFormDisabled,
})}
>
<RegionDeploymentSection
regions={regions}
handleRegionChange={handleRegionChange}
isFormDisabled={isFormDisabled}
selectedDeploymentRegion={selectedDeploymentRegion}
/>
<MonitoringRegionsSection
includeAllRegions={includeAllRegions}
selectedRegions={selectedRegions}
onIncludeAllRegionsChange={onIncludeAllRegionsChange}
getRegionPreviewText={getRegionPreviewText}
onRegionSelect={onRegionSelect}
isFormDisabled={isFormDisabled}
/>
<ComplianceNote />
<RenderConnectionFields
isConnectionParamsLoading={isConnectionParamsLoading}
connectionParams={connectionParams}
isFormDisabled={isFormDisabled}
/>
</div>
</Form>
);
}

View File

@@ -1,47 +0,0 @@
.remove-integration-account {
display: flex;
justify-content: space-between;
padding: 16px;
border-radius: 4px;
border: 1px solid 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;
}
}
}
}

View File

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

View File

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

View File

@@ -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, heres what you can do next:</p>
</div>
</div>
<div className="cloud-account-setup-success-view__what-next">
<h4 className="cloud-account-setup-success-view__what-next-title">
WHAT NEXT
</h4>
<div className="what-next-items-wrapper">
<Alert
message={
<div className="what-next-items-wrapper__item">
<div className="what-next-item-bullet-icon"></div>
<div className="what-next-item-text">
Set up your AWS services effortlessly under your enabled account.
</div>
</div>
}
type="info"
className="what-next-items-wrapper__item"
/>
</div>
</div>
</div>
</>
);
}

View File

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

View File

@@ -1,89 +0,0 @@
.configure-service-modal {
&__body {
display: flex;
flex-direction: column;
border-radius: 3px;
border: 1px solid var(--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;
}
}
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -7,7 +7,6 @@ import { FeatureKeys } from 'constants/features';
import { useAppContext } from 'providers/App/App';
import { AlertTypes } from 'types/api/alerts/alertTypes';
import { isModifierKeyPressed } from 'utils/app';
import { openInNewTab } from 'utils/navigation';
import { getOptionList } from './config';
import { AlertTypeCard, SelectTypeContainer } from './styles';
@@ -56,7 +55,7 @@ function SelectAlertType({ onSelect }: SelectAlertTypeProps): JSX.Element {
page: 'New alert data source selection page',
});
openInNewTab(url);
window.open(url, '_blank');
}
const renderOptions = useMemo(
() => (

View File

@@ -14,7 +14,6 @@ import { IUser } from 'providers/App/types';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
import { EQueryType } from 'types/common/dashboard';
import { USER_ROLES } from 'types/roles';
import { openInNewTab } from 'utils/navigation';
import { ROUTING_POLICIES_ROUTE } from './constants';
import { RoutingPolicyBannerProps } from './types';
@@ -388,7 +387,7 @@ export function NotificationChannelsNotFoundContent({
style={{ padding: '0 4px' }}
type="link"
onClick={(): void => {
openInNewTab(ROUTES.CHANNELS_NEW);
window.open(ROUTES.CHANNELS_NEW, '_blank');
}}
>
here.

View File

@@ -48,7 +48,6 @@ function DomainUpdateToast({
className="custom-domain-toast-visit-btn"
suffixIcon={<ExternalLink size={12} />}
onClick={(): void => {
// eslint-disable-next-line rulesdir/no-raw-absolute-path
window.open(url, '_blank', 'noopener,noreferrer');
}}
>

View File

@@ -16,8 +16,6 @@ import { useDashboardStore } from 'providers/Dashboard/store/useDashboardStore';
import { PublicDashboardMetaProps } from 'types/api/dashboard/public/getMeta';
import APIError from 'types/api/error';
import { USER_ROLES } from 'types/roles';
import { getAbsoluteUrl } from 'utils/basePath';
import { openInNewTab } from 'utils/navigation';
import './PublicDashboard.styles.scss';
@@ -215,7 +213,7 @@ function PublicDashboardSetting(): JSX.Element {
try {
setCopyPublicDashboardURL(
getAbsoluteUrl(publicDashboardResponse?.data?.publicPath ?? ''),
`${window.location.origin}${publicDashboardResponse?.data?.publicPath}`,
);
toast.success('Copied Public Dashboard URL successfully');
} catch (error) {
@@ -224,7 +222,7 @@ function PublicDashboardSetting(): JSX.Element {
};
const publicDashboardURL = useMemo(
() => getAbsoluteUrl(publicDashboardResponse?.data?.publicPath ?? ''),
() => `${window.location.origin}${publicDashboardResponse?.data?.publicPath}`,
[publicDashboardResponse],
);
@@ -296,7 +294,7 @@ function PublicDashboardSetting(): JSX.Element {
icon={<ExternalLink size={12} />}
onClick={(): void => {
if (publicDashboardURL) {
openInNewTab(publicDashboardURL);
window.open(publicDashboardURL, '_blank');
}
}}
/>

View File

@@ -1,6 +1,5 @@
import { useCallback, useRef } from 'react';
import { Button } from 'antd';
import getSessionStorageApi from 'api/browser/sessionstorage/get';
import ROUTES from 'constants/routes';
import { DASHBOARDS_LIST_QUERY_PARAMS_STORAGE_KEY } from 'hooks/dashboard/useDashboardsListQueryParams';
import { useSafeNavigate } from 'hooks/useSafeNavigate';
@@ -27,7 +26,7 @@ function DashboardBreadcrumbs(): JSX.Element {
const { title = '', image = Base64Icons[0] } = selectedData || {};
const goToListPage = useCallback(() => {
const dashboardsListQueryParamsString = getSessionStorageApi(
const dashboardsListQueryParamsString = sessionStorage.getItem(
DASHBOARDS_LIST_QUERY_PARAMS_STORAGE_KEY,
);

View File

@@ -1,6 +1,3 @@
import getLocalStorageKey from 'api/browser/localstorage/get';
import removeLocalStorageKey from 'api/browser/localstorage/remove';
import setLocalStorageKey from 'api/browser/localstorage/set';
import { LOCALSTORAGE } from 'constants/localStorage';
import { GraphVisibilityState, SeriesVisibilityItem } from '../types';
@@ -15,7 +12,7 @@ export function getStoredSeriesVisibility(
widgetId: string,
): SeriesVisibilityItem[] | null {
try {
const storedData = getLocalStorageKey(LOCALSTORAGE.GRAPH_VISIBILITY_STATES);
const storedData = localStorage.getItem(LOCALSTORAGE.GRAPH_VISIBILITY_STATES);
if (!storedData) {
return null;
@@ -32,7 +29,7 @@ export function getStoredSeriesVisibility(
} catch (error) {
if (error instanceof SyntaxError) {
// If the stored data is malformed, remove it
removeLocalStorageKey(LOCALSTORAGE.GRAPH_VISIBILITY_STATES);
localStorage.removeItem(LOCALSTORAGE.GRAPH_VISIBILITY_STATES);
}
// Silently handle parsing errors - fall back to default visibility
return null;
@@ -45,7 +42,7 @@ export function updateSeriesVisibilityToLocalStorage(
): void {
let visibilityStates: GraphVisibilityState[] = [];
try {
const storedData = getLocalStorageKey(LOCALSTORAGE.GRAPH_VISIBILITY_STATES);
const storedData = localStorage.getItem(LOCALSTORAGE.GRAPH_VISIBILITY_STATES);
visibilityStates = JSON.parse(storedData || '[]');
} catch (error) {
if (error instanceof SyntaxError) {
@@ -66,7 +63,7 @@ export function updateSeriesVisibilityToLocalStorage(
];
}
setLocalStorageKey(
localStorage.setItem(
LOCALSTORAGE.GRAPH_VISIBILITY_STATES,
JSON.stringify(visibilityStates),
);

View File

@@ -22,8 +22,6 @@ import {
Tooltip,
Typography,
} from 'antd';
import getLocalStorageKey from 'api/browser/localstorage/get';
import setLocalStorageKey from 'api/browser/localstorage/set';
import logEvent from 'api/common/logEvent';
import { TelemetryFieldKey } from 'api/v5/v5';
import axios from 'axios';
@@ -474,7 +472,7 @@ function ExplorerOptions({
value: string;
}): void => {
// Retrieve stored views from local storage
const storedViews = getLocalStorageKey(PRESERVED_VIEW_LOCAL_STORAGE_KEY);
const storedViews = localStorage.getItem(PRESERVED_VIEW_LOCAL_STORAGE_KEY);
// Initialize or parse the stored views
const updatedViews: PreservedViewsInLocalStorage = storedViews
@@ -488,7 +486,7 @@ function ExplorerOptions({
};
// Save the updated views back to local storage
setLocalStorageKey(
localStorage.setItem(
PRESERVED_VIEW_LOCAL_STORAGE_KEY,
JSON.stringify(updatedViews),
);
@@ -539,7 +537,7 @@ function ExplorerOptions({
const removeCurrentViewFromLocalStorage = (): void => {
// Retrieve stored views from local storage
const storedViews = getLocalStorageKey(PRESERVED_VIEW_LOCAL_STORAGE_KEY);
const storedViews = localStorage.getItem(PRESERVED_VIEW_LOCAL_STORAGE_KEY);
if (storedViews) {
// Parse the stored views
@@ -549,7 +547,7 @@ function ExplorerOptions({
delete parsedViews[PRESERVED_VIEW_TYPE];
// Update local storage with the modified views
setLocalStorageKey(
localStorage.setItem(
PRESERVED_VIEW_LOCAL_STORAGE_KEY,
JSON.stringify(parsedViews),
);
@@ -674,7 +672,7 @@ function ExplorerOptions({
}
const parsedPreservedView = JSON.parse(
getLocalStorageKey(PRESERVED_VIEW_LOCAL_STORAGE_KEY) || '{}',
localStorage.getItem(PRESERVED_VIEW_LOCAL_STORAGE_KEY) || '{}',
);
const preservedView = parsedPreservedView[PRESERVED_VIEW_TYPE] || {};

View File

@@ -1,6 +1,4 @@
import { Color } from '@signozhq/design-tokens';
import getLocalStorageKey from 'api/browser/localstorage/get';
import setLocalStorageKey from 'api/browser/localstorage/set';
import { showErrorNotification } from 'components/ExplorerCard/utils';
import { LOCALSTORAGE } from 'constants/localStorage';
import { QueryParams } from 'constants/query';
@@ -73,7 +71,7 @@ export const generateRGBAFromHex = (hex: string, opacity: number): string =>
export const getExplorerToolBarVisibility = (dataSource: string): boolean => {
try {
const showExplorerToolbar = getLocalStorageKey(
const showExplorerToolbar = localStorage.getItem(
LOCALSTORAGE.SHOW_EXPLORER_TOOLBAR,
);
if (showExplorerToolbar === null) {
@@ -86,7 +84,7 @@ export const getExplorerToolBarVisibility = (dataSource: string): boolean => {
[DataSource.TRACES]: true,
[DataSource.LOGS]: true,
};
setLocalStorageKey(
localStorage.setItem(
LOCALSTORAGE.SHOW_EXPLORER_TOOLBAR,
JSON.stringify(parsedShowExplorerToolbar),
);
@@ -105,13 +103,13 @@ export const setExplorerToolBarVisibility = (
dataSource: string,
): void => {
try {
const showExplorerToolbar = getLocalStorageKey(
const showExplorerToolbar = localStorage.getItem(
LOCALSTORAGE.SHOW_EXPLORER_TOOLBAR,
);
if (showExplorerToolbar) {
const parsedShowExplorerToolbar = JSON.parse(showExplorerToolbar);
parsedShowExplorerToolbar[dataSource] = value;
setLocalStorageKey(
localStorage.setItem(
LOCALSTORAGE.SHOW_EXPLORER_TOOLBAR,
JSON.stringify(parsedShowExplorerToolbar),
);

View File

@@ -10,7 +10,6 @@ import ROUTES from 'constants/routes';
import history from 'lib/history';
import APIError from 'types/api/error';
import { OrgSessionContext } from 'types/api/v2/sessions/context/get';
import { getBaseUrl } from 'utils/basePath';
import tvUrl from '@/assets/svgs/tv.svg';
@@ -106,7 +105,7 @@ function ForgotPassword({
data: {
email: values.email,
orgId: currentOrgId,
frontendBaseURL: getBaseUrl(),
frontendBaseURL: window.location.origin,
},
});
}, [form, forgotPasswordMutate, initialOrgId, hasMultipleOrgs]);

View File

@@ -15,7 +15,6 @@ import { AlertDef, Labels } from 'types/api/alerts/def';
import { Channels } from 'types/api/channels/getAll';
import APIError from 'types/api/error';
import { requireErrorMessage } from 'utils/form/requireErrorMessage';
import { openInNewTab } from 'utils/navigation';
import { popupContainer } from 'utils/selectPopupContainer';
import ChannelSelect from './ChannelSelect';
@@ -88,7 +87,7 @@ function BasicInfo({
dataSource: ALERTS_DATA_SOURCE_MAP[alertDef?.alertType as AlertTypes],
ruleId: isNewRule ? 0 : alertDef?.id,
});
openInNewTab(ROUTES.CHANNELS_NEW);
window.open(ROUTES.CHANNELS_NEW, '_blank');
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const hasLoggedEvent = useRef(false);

View File

@@ -46,7 +46,6 @@ import { DataSource } from 'types/common/queryBuilder';
import { GlobalReducer } from 'types/reducer/globalTime';
import { isModifierKeyPressed } from 'utils/app';
import { compositeQueryToQueryEnvelope } from 'utils/compositeQueryToQueryEnvelope';
import { openInNewTab } from 'utils/navigation';
import BasicInfo from './BasicInfo';
import ChartPreview from './ChartPreview';
@@ -772,7 +771,7 @@ function FormAlertRules({
queryType: currentQuery.queryType,
link: url,
});
openInNewTab(url);
window.open(url, '_blank');
}
}

View File

@@ -1,5 +1,3 @@
import getLocalStorageKey from 'api/browser/localstorage/get';
import setLocalStorageKey from 'api/browser/localstorage/set';
import { LOCALSTORAGE } from 'constants/localStorage';
import getLabelName from 'lib/getLabelName';
import { QueryData } from 'types/api/widgets/getQuery';
@@ -102,7 +100,7 @@ export const saveLegendEntriesToLocalStorage = ({
try {
existingEntries = JSON.parse(
getLocalStorageKey(LOCALSTORAGE.GRAPH_VISIBILITY_STATES) || '[]',
localStorage.getItem(LOCALSTORAGE.GRAPH_VISIBILITY_STATES) || '[]',
);
} catch (error) {
console.error('Error parsing LEGEND_GRAPH from local storage', error);
@@ -117,7 +115,7 @@ export const saveLegendEntriesToLocalStorage = ({
}
try {
setLocalStorageKey(
localStorage.setItem(
LOCALSTORAGE.GRAPH_VISIBILITY_STATES,
JSON.stringify(existingEntries),
);

View File

@@ -1,6 +1,5 @@
/* eslint-disable sonarjs/cognitive-complexity */
import type { NotificationInstance } from 'antd/es/notification/interface';
import getLocalStorageKey from 'api/browser/localstorage/get';
import { NavigateToExplorerProps } from 'components/CeleryTask/useNavigateToExplorer';
import { LOCALSTORAGE } from 'constants/localStorage';
import { PANEL_TYPES } from 'constants/queryBuilder';
@@ -45,8 +44,8 @@ export const getLocalStorageGraphVisibilityState = ({
],
};
if (getLocalStorageKey(LOCALSTORAGE.GRAPH_VISIBILITY_STATES) !== null) {
const legendGraphFromLocalStore = getLocalStorageKey(
if (localStorage.getItem(LOCALSTORAGE.GRAPH_VISIBILITY_STATES) !== null) {
const legendGraphFromLocalStore = localStorage.getItem(
LOCALSTORAGE.GRAPH_VISIBILITY_STATES,
);
let legendFromLocalStore: {
@@ -95,8 +94,8 @@ export const getGraphVisibilityStateOnDataChange = ({
graphVisibilityStates: Array(options.series.length).fill(true),
legendEntry: showAllDataSet(options),
};
if (getLocalStorageKey(LOCALSTORAGE.GRAPH_VISIBILITY_STATES) !== null) {
const legendGraphFromLocalStore = getLocalStorageKey(
if (localStorage.getItem(LOCALSTORAGE.GRAPH_VISIBILITY_STATES) !== null) {
const legendGraphFromLocalStore = localStorage.getItem(
LOCALSTORAGE.GRAPH_VISIBILITY_STATES,
);
let legendFromLocalStore: {

View File

@@ -10,7 +10,6 @@ import Card from 'periscope/components/Card/Card';
import { useAppContext } from 'providers/App/App';
import { Dashboard } from 'types/api/dashboard/getAll';
import { USER_ROLES } from 'types/roles';
import { openInNewTab } from 'utils/navigation';
import dialsUrl from '@/assets/Icons/dials.svg';
@@ -115,7 +114,7 @@ export default function Dashboards({
dashboardName: dashboard.data.title,
});
if (event.metaKey || event.ctrlKey) {
openInNewTab(getLink());
window.open(getLink(), '_blank');
} else {
safeNavigate(getLink());
}

View File

@@ -9,7 +9,6 @@ import { Link2 } from 'lucide-react';
import Card from 'periscope/components/Card/Card';
import { useAppContext } from 'providers/App/App';
import { LicensePlatform } from 'types/api/licensesV3/getActive';
import { openInNewTab } from 'utils/navigation';
import containerPlusUrl from '@/assets/Icons/container-plus.svg';
import helloWaveUrl from '@/assets/Icons/hello-wave.svg';
@@ -52,7 +51,7 @@ function DataSourceInfo({
if (activeLicense && activeLicense.platform === LicensePlatform.CLOUD) {
history.push(ROUTES.GET_STARTED_WITH_CLOUD);
} else {
openInNewTab(DOCS_LINKS.ADD_DATA_SOURCE);
window?.open(DOCS_LINKS.ADD_DATA_SOURCE, '_blank', 'noopener noreferrer');
}
};

View File

@@ -8,7 +8,6 @@ import { ArrowRight, ArrowRightToLine, BookOpenText } from 'lucide-react';
import { useAppContext } from 'providers/App/App';
import { LicensePlatform } from 'types/api/licensesV3/getActive';
import { USER_ROLES } from 'types/roles';
import { openInNewTab } from 'utils/navigation';
import './HomeChecklist.styles.scss';
@@ -100,7 +99,11 @@ function HomeChecklist({
) {
history.push(item.toRoute || '');
} else {
openInNewTab(item.docsLink || '');
window?.open(
item.docsLink || '',
'_blank',
'noopener noreferrer',
);
}
}}
>
@@ -116,7 +119,7 @@ function HomeChecklist({
step: item.id,
});
openInNewTab(item.docsLink ?? '');
window?.open(item.docsLink, '_blank', 'noopener noreferrer');
}}
>
<BookOpenText size={16} />

View File

@@ -31,7 +31,6 @@ import { GlobalReducer } from 'types/reducer/globalTime';
import { Tags } from 'types/reducer/trace';
import { USER_ROLES } from 'types/roles';
import { isModifierKeyPressed } from 'utils/app';
import { openInNewTab } from 'utils/navigation';
import triangleRulerUrl from '@/assets/Icons/triangle-ruler.svg';
@@ -80,7 +79,11 @@ const EmptyState = memo(
) {
history.push(ROUTES.GET_STARTED_WITH_CLOUD);
} else {
openInNewTab(DOCS_LINKS.ADD_DATA_SOURCE);
window?.open(
DOCS_LINKS.ADD_DATA_SOURCE,
'_blank',
'noopener noreferrer',
);
}
}}
>

View File

@@ -17,7 +17,6 @@ import { ServicesList } from 'types/api/metrics/getService';
import { GlobalReducer } from 'types/reducer/globalTime';
import { USER_ROLES } from 'types/roles';
import { isModifierKeyPressed } from 'utils/app';
import { openInNewTab } from 'utils/navigation';
import triangleRulerUrl from '@/assets/Icons/triangle-ruler.svg';
@@ -134,7 +133,11 @@ export default function ServiceTraces({
) {
history.push(ROUTES.GET_STARTED_WITH_CLOUD);
} else {
openInNewTab(DOCS_LINKS.ADD_DATA_SOURCE);
window?.open(
DOCS_LINKS.ADD_DATA_SOURCE,
'_blank',
'noopener noreferrer',
);
}
}}
>

View File

@@ -49,7 +49,6 @@ import {
TracesAggregatorOperator,
} from 'types/common/queryBuilder';
import { GlobalReducer } from 'types/reducer/globalTime';
import { openInNewTab } from 'utils/navigation';
import { v4 as uuidv4 } from 'uuid';
import ClusterEvents from '../../EntityDetailsUtils/EntityEvents';
@@ -415,7 +414,10 @@ function ClusterDetails({
urlQuery.set('compositeQuery', JSON.stringify(compositeQuery));
openInNewTab(`${ROUTES.LOGS_EXPLORER}?${urlQuery.toString()}`);
window.open(
`${window.location.origin}${ROUTES.LOGS_EXPLORER}?${urlQuery.toString()}`,
'_blank',
);
} else if (selectedView === VIEW_TYPES.TRACES) {
const compositeQuery = {
...initialQueryState,
@@ -434,7 +436,10 @@ function ClusterDetails({
urlQuery.set('compositeQuery', JSON.stringify(compositeQuery));
openInNewTab(`${ROUTES.TRACES_EXPLORER}?${urlQuery.toString()}`);
window.open(
`${window.location.origin}${ROUTES.TRACES_EXPLORER}?${urlQuery.toString()}`,
'_blank',
);
}
};

View File

@@ -48,7 +48,6 @@ import {
TracesAggregatorOperator,
} from 'types/common/queryBuilder';
import { GlobalReducer } from 'types/reducer/globalTime';
import { openInNewTab } from 'utils/navigation';
import { v4 as uuidv4 } from 'uuid';
import DaemonSetEvents from '../../EntityDetailsUtils/EntityEvents';
@@ -430,7 +429,10 @@ function DaemonSetDetails({
urlQuery.set('compositeQuery', JSON.stringify(compositeQuery));
openInNewTab(`${ROUTES.LOGS_EXPLORER}?${urlQuery.toString()}`);
window.open(
`${window.location.origin}${ROUTES.LOGS_EXPLORER}?${urlQuery.toString()}`,
'_blank',
);
} else if (selectedView === VIEW_TYPES.TRACES) {
const compositeQuery = {
...initialQueryState,
@@ -449,7 +451,10 @@ function DaemonSetDetails({
urlQuery.set('compositeQuery', JSON.stringify(compositeQuery));
openInNewTab(`${ROUTES.TRACES_EXPLORER}?${urlQuery.toString()}`);
window.open(
`${window.location.origin}${ROUTES.TRACES_EXPLORER}?${urlQuery.toString()}`,
'_blank',
);
}
};

View File

@@ -50,7 +50,6 @@ import {
TracesAggregatorOperator,
} from 'types/common/queryBuilder';
import { GlobalReducer } from 'types/reducer/globalTime';
import { openInNewTab } from 'utils/navigation';
import { v4 as uuidv4 } from 'uuid';
import DeploymentEvents from '../../EntityDetailsUtils/EntityEvents';
@@ -434,7 +433,10 @@ function DeploymentDetails({
urlQuery.set('compositeQuery', JSON.stringify(compositeQuery));
openInNewTab(`${ROUTES.LOGS_EXPLORER}?${urlQuery.toString()}`);
window.open(
`${window.location.origin}${ROUTES.LOGS_EXPLORER}?${urlQuery.toString()}`,
'_blank',
);
} else if (selectedView === VIEW_TYPES.TRACES) {
const compositeQuery = {
...initialQueryState,
@@ -453,7 +455,10 @@ function DeploymentDetails({
urlQuery.set('compositeQuery', JSON.stringify(compositeQuery));
openInNewTab(`${ROUTES.TRACES_EXPLORER}?${urlQuery.toString()}`);
window.open(
`${window.location.origin}${ROUTES.TRACES_EXPLORER}?${urlQuery.toString()}`,
'_blank',
);
}
};

View File

@@ -48,7 +48,6 @@ import {
TracesAggregatorOperator,
} from 'types/common/queryBuilder';
import { GlobalReducer } from 'types/reducer/globalTime';
import { openInNewTab } from 'utils/navigation';
import { v4 as uuidv4 } from 'uuid';
import JobEvents from '../../EntityDetailsUtils/EntityEvents';
@@ -428,7 +427,10 @@ function JobDetails({
urlQuery.set('compositeQuery', JSON.stringify(compositeQuery));
openInNewTab(`${ROUTES.LOGS_EXPLORER}?${urlQuery.toString()}`);
window.open(
`${window.location.origin}${ROUTES.LOGS_EXPLORER}?${urlQuery.toString()}`,
'_blank',
);
} else if (selectedView === VIEW_TYPES.TRACES) {
const compositeQuery = {
...initialQueryState,
@@ -447,7 +449,10 @@ function JobDetails({
urlQuery.set('compositeQuery', JSON.stringify(compositeQuery));
openInNewTab(`${ROUTES.TRACES_EXPLORER}?${urlQuery.toString()}`);
window.open(
`${window.location.origin}${ROUTES.TRACES_EXPLORER}?${urlQuery.toString()}`,
'_blank',
);
}
};

View File

@@ -50,7 +50,6 @@ import {
TracesAggregatorOperator,
} from 'types/common/queryBuilder';
import { GlobalReducer } from 'types/reducer/globalTime';
import { openInNewTab } from 'utils/navigation';
import { v4 as uuidv4 } from 'uuid';
import NamespaceEvents from '../../EntityDetailsUtils/EntityEvents';
@@ -420,7 +419,10 @@ function NamespaceDetails({
urlQuery.set('compositeQuery', JSON.stringify(compositeQuery));
openInNewTab(`${ROUTES.LOGS_EXPLORER}?${urlQuery.toString()}`);
window.open(
`${window.location.origin}${ROUTES.LOGS_EXPLORER}?${urlQuery.toString()}`,
'_blank',
);
} else if (selectedView === VIEW_TYPES.TRACES) {
const compositeQuery = {
...initialQueryState,
@@ -439,7 +441,10 @@ function NamespaceDetails({
urlQuery.set('compositeQuery', JSON.stringify(compositeQuery));
openInNewTab(`${ROUTES.TRACES_EXPLORER}?${urlQuery.toString()}`);
window.open(
`${window.location.origin}${ROUTES.TRACES_EXPLORER}?${urlQuery.toString()}`,
'_blank',
);
}
};

View File

@@ -50,7 +50,6 @@ import {
TracesAggregatorOperator,
} from 'types/common/queryBuilder';
import { GlobalReducer } from 'types/reducer/globalTime';
import { openInNewTab } from 'utils/navigation';
import { v4 as uuidv4 } from 'uuid';
import NodeLogs from '../../EntityDetailsUtils/EntityLogs';
@@ -417,7 +416,10 @@ function NodeDetails({
urlQuery.set('compositeQuery', JSON.stringify(compositeQuery));
openInNewTab(`${ROUTES.LOGS_EXPLORER}?${urlQuery.toString()}`);
window.open(
`${window.location.origin}${ROUTES.LOGS_EXPLORER}?${urlQuery.toString()}`,
'_blank',
);
} else if (selectedView === VIEW_TYPES.TRACES) {
const compositeQuery = {
...initialQueryState,
@@ -436,7 +438,10 @@ function NodeDetails({
urlQuery.set('compositeQuery', JSON.stringify(compositeQuery));
openInNewTab(`${ROUTES.TRACES_EXPLORER}?${urlQuery.toString()}`);
window.open(
`${window.location.origin}${ROUTES.TRACES_EXPLORER}?${urlQuery.toString()}`,
'_blank',
);
}
};

View File

@@ -50,7 +50,6 @@ import {
TracesAggregatorOperator,
} from 'types/common/queryBuilder';
import { GlobalReducer } from 'types/reducer/globalTime';
import { openInNewTab } from 'utils/navigation';
import { v4 as uuidv4 } from 'uuid';
import PodEvents from '../../EntityDetailsUtils/EntityEvents';
@@ -436,7 +435,10 @@ function PodDetails({
urlQuery.set('compositeQuery', JSON.stringify(compositeQuery));
openInNewTab(`${ROUTES.LOGS_EXPLORER}?${urlQuery.toString()}`);
window.open(
`${window.location.origin}${ROUTES.LOGS_EXPLORER}?${urlQuery.toString()}`,
'_blank',
);
} else if (selectedView === VIEW_TYPES.TRACES) {
const compositeQuery = {
...initialQueryState,
@@ -455,7 +457,10 @@ function PodDetails({
urlQuery.set('compositeQuery', JSON.stringify(compositeQuery));
openInNewTab(`${ROUTES.TRACES_EXPLORER}?${urlQuery.toString()}`);
window.open(
`${window.location.origin}${ROUTES.TRACES_EXPLORER}?${urlQuery.toString()}`,
'_blank',
);
}
};

View File

@@ -53,7 +53,6 @@ import {
TracesAggregatorOperator,
} from 'types/common/queryBuilder';
import { GlobalReducer } from 'types/reducer/globalTime';
import { openInNewTab } from 'utils/navigation';
import { v4 as uuidv4 } from 'uuid';
import {
@@ -432,7 +431,10 @@ function StatefulSetDetails({
urlQuery.set('compositeQuery', JSON.stringify(compositeQuery));
openInNewTab(`${ROUTES.LOGS_EXPLORER}?${urlQuery.toString()}`);
window.open(
`${window.location.origin}${ROUTES.LOGS_EXPLORER}?${urlQuery.toString()}`,
'_blank',
);
} else if (selectedView === VIEW_TYPES.TRACES) {
const compositeQuery = {
...initialQueryState,
@@ -451,7 +453,10 @@ function StatefulSetDetails({
urlQuery.set('compositeQuery', JSON.stringify(compositeQuery));
openInNewTab(`${ROUTES.TRACES_EXPLORER}?${urlQuery.toString()}`);
window.open(
`${window.location.origin}${ROUTES.TRACES_EXPLORER}?${urlQuery.toString()}`,
'_blank',
);
}
};

View File

@@ -1,8 +1,10 @@
.hero-section {
height: 308px;
padding: 26px 16px;
padding: 16px;
display: flex;
gap: 24px;
flex-direction: column;
gap: 16px;
position: relative;
overflow: hidden;
background-position: right;
@@ -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;

View File

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

View File

@@ -4,14 +4,57 @@
&-with-account {
display: flex;
flex-direction: column;
gap: 10px;
flex-direction: row;
align-items: center;
justify-content: space-between;
flex-wrap: wrap;
gap: 16px;
border-radius: 4px;
border: 1px solid var(--l3-background);
background: var(--l1-background);
.selected-cloud-integration-account-status {
display: flex;
align-items: center;
justify-content: center;
border-right: 1px solid var(--l3-background);
border-radius: none;
width: 32px;
}
&-selector-container {
display: flex;
flex-direction: row;
align-items: center;
.account-selector-label {
color: var(--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);
}
}
}

View File

@@ -1,58 +1,23 @@
import { useEffect, useMemo, useState } from 'react';
import { useNavigate } from 'react-router-dom-v5-compat';
import { Button } from '@signozhq/button';
import { Color } from '@signozhq/design-tokens';
import { Button, Select, Skeleton } from 'antd';
import type { SelectProps } from 'antd/lib';
import { Select, Skeleton } from 'antd';
import { SelectProps } from 'antd/lib';
import logEvent from 'api/common/logEvent';
import { useAwsAccounts } from 'hooks/integration/aws/useAwsAccounts';
import { useListAccounts } from 'api/generated/services/cloudintegration';
import { getAccountById } from 'container/Integrations/CloudIntegration/utils';
import { INTEGRATION_TYPES } from 'container/Integrations/constants';
import useUrlQuery from 'hooks/useUrlQuery';
import { Check, ChevronDown } from 'lucide-react';
import { ChevronDown, Dot, PencilLine, Plug, Plus } from 'lucide-react';
import { CloudAccount } from '../../ServicesSection/types';
import { mapAccountDtoToAwsCloudAccount } from '../../mapAwsCloudAccountFromDto';
import { CloudAccount } from '../../types';
import AccountSettingsModal from './AccountSettingsModal';
import CloudAccountSetupModal from './CloudAccountSetupModal';
import './AccountActions.style.scss';
interface AccountOptionItemProps {
label: React.ReactNode;
isSelected: boolean;
}
function AccountOptionItem({
label,
isSelected,
}: AccountOptionItemProps): JSX.Element {
return (
<div className="account-option-item">
{label}
{isSelected && (
<div className="account-option-item__selected">
<Check size={12} color={Color.BG_VANILLA_100} />
</div>
)}
</div>
);
}
function renderOption(
option: any,
activeAccountId: string | undefined,
): JSX.Element {
return (
<AccountOptionItem
label={option.label}
isSelected={option.value === activeAccountId}
/>
);
}
const getAccountById = (
accounts: CloudAccount[],
accountId: string,
): CloudAccount | null =>
accounts.find((account) => account.cloud_account_id === accountId) || null;
function AccountActionsRenderer({
accounts,
isLoading,
@@ -73,55 +38,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}
/>
)}

View File

@@ -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);
}
}
}
}
}

View File

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

View File

@@ -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);
}
}
}
}
}
}

View File

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

View File

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

View File

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

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