Compare commits

...

39 Commits

Author SHA1 Message Date
SagarRajput-7
90a2702c77 Merge branch 'members-page' into ssopage-cleanup 2026-03-07 01:53:47 +05:30
SagarRajput-7
af406f806f feat: code refactor and added todos 2026-03-07 01:51:26 +05:30
SagarRajput-7
9a6976e50e Merge branch 'members-page' into ssopage-cleanup 2026-03-07 01:27:19 +05:30
SagarRajput-7
717c44dc26 feat: updated test case 2026-03-07 01:23:35 +05:30
SagarRajput-7
8a4c2c4be4 Merge branch 'main' into members-page 2026-03-07 01:19:58 +05:30
SagarRajput-7
ae315c600a feat: refactored and addressed the comments 2026-03-07 01:18:33 +05:30
Ashwin Bhatkal
08713cbf7d chore: support for merge queues (#10513)
Some checks are pending
build-staging / prepare (push) Waiting to run
build-staging / js-build (push) Blocked by required conditions
build-staging / go-build (push) Blocked by required conditions
build-staging / staging (push) Blocked by required conditions
Release Drafter / update_release_draft (push) Waiting to run
2026-03-06 14:22:17 +00:00
Ashwin Bhatkal
235dacf4a3 chore(frontend): separate out columnWidths from ResizeTable (#10510)
Some checks failed
build-staging / staging (push) Has been cancelled
build-staging / prepare (push) Has been cancelled
build-staging / js-build (push) Has been cancelled
build-staging / go-build (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
* chore(frontend): separate out columnWidths from ResizeTable

* chore: fix tests
2026-03-06 19:20:17 +05:30
Nityananda Gohain
3a82085eb4 chore: enrich clickhouse log_comment (#10446)
* chore: enchance clickhouse log_comment

* chore: enrich all clickhouse queries

* fix: remove is_meatadata

* fix: use the new func

* fix: use semconv keys

* fix: add more details

* fix: minor changes

* fix: address comments

* fix: address cursor comments

* fix: minor changes

* fix: addressed comments

* fix: address comments and move to module functions where possible

---------

Co-authored-by: Srikanth Chekuri <srikanth.chekuri92@gmail.com>
2026-03-06 12:57:15 +00:00
SagarRajput-7
0f5cab9d68 feat: removed members and invited user tables from sso page 2026-03-06 16:25:18 +05:30
SagarRajput-7
bc311660ba feat: added updatedAt as the current give that in the response 2026-03-06 16:06:54 +05:30
SagarRajput-7
6d1e9bb1bf feat: code refactor 2026-03-06 16:06:54 +05:30
SagarRajput-7
3c6535a33a feat: added test cases for the members feature 2026-03-06 16:06:54 +05:30
SagarRajput-7
a0c516ccda feat: refactored and used semantic token for edit member drawer 2026-03-06 16:06:54 +05:30
SagarRajput-7
61f83707cd feat: edit member drawer refactor 2026-03-06 16:06:54 +05:30
SagarRajput-7
f1de57d0d3 feat: code refactoring and use of semantic tokens 2026-03-06 16:06:54 +05:30
SagarRajput-7
c9f1b4cd0f feat: rebased with settings nav changes 2026-03-06 16:06:54 +05:30
SagarRajput-7
9a69f86a9e feat: added password modal and other functionality 2026-03-06 16:06:54 +05:30
SagarRajput-7
c6d9c83790 feat: added components and styles 2026-03-06 16:06:54 +05:30
SagarRajput-7
9c530d17fc feat: added members page and listing and edit view 2026-03-06 16:06:54 +05:30
SagarRajput-7
7691f3451a feat: revamped the settings nav and setting dropdown (#10494)
* feat: revamped the settings nav and setting dropdown

* feat: settings nav categorisation

* feat: added test cases

* feat: refactored code

* feat: updated test case

* feat: added keyboard shortcuts back into the settings nav at the bottom section
2026-03-06 10:32:25 +00:00
primus-bot[bot]
ded51c4379 chore(release): bump to v0.114.1 (#10515)
Co-authored-by: primus-bot[bot] <171087277+primus-bot[bot]@users.noreply.github.com>
2026-03-06 12:23:42 +05:30
Yunus M
ef8790b708 chore: remove search nav item from sidenav (#10512) 2026-03-06 06:13:30 +00:00
Yunus M
57fe84046d Revert "Sig 8931 : Migrate quick filters to use /fields/keys and /fields/valu…" (#10508)
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
This reverts commit 9a046ab89d.
2026-03-06 09:53:29 +05:30
Abhi kumar
129d18a1b7 fix: added fix for tooltip not rendering in fullscreen mode (#10504)
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
* fix: added fix for tooltip not rendering in fullscreen mode

* chore: added test for tooltip parent behaviour
2026-03-05 20:30:05 +05:30
Abhi kumar
48ccdfcf64 enh: updated tooltip pinning structure to be used across different charts (#10459)
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
* chore: made baseconfigbuilder generic to be used across different charts

* chore: updated baseconfigbuilder test

* chore: updated timezone types

* chore: fixed tsc + test

* chore: fixed tsc + test

* chore: fixed tsc + test

* feat: added the base changes for pinnedtooltip element

* chore: fix tsc + test

* chore: fixed tooltipplugin test

* chore: restructured the code

* chore: restrucuted tooltip schema and resolved pr comments

* chore: fixed cursor comments

* chore: fixed cursor comments

* chore: fixed cursor comments
2026-03-05 16:12:34 +05:30
Yunus M
8c7dc942d0 fix: handling of input changes in CustomTimePicker to ensure value is applied on popover close (#10484)
* fix: enhance input handling to prevent popover closure on input click

* fix: handling of input changes in CustomTimePicker to ensure value is applied on popover close

* chore: add test cases
2026-03-05 14:19:14 +05:30
Naman Verma
0e1bb5fd91 fix: exclude internal attributes from promQL results (#10465)
* fix: exclude internal attributes from promQL results

* fix: __name__ can stay
2026-03-05 13:07:06 +05:30
primus-bot[bot]
d7a743cea9 chore(release): bump to v0.114.0 (#10496)
Co-authored-by: primus-bot[bot] <171087277+primus-bot[bot]@users.noreply.github.com>
2026-03-05 12:43:16 +05:30
Vinicius Lourenço
850dc10983 fix(antd): wrong usage of imports (#10491)
Some checks failed
build-staging / prepare (push) Has been cancelled
build-staging / js-build (push) Has been cancelled
build-staging / go-build (push) Has been cancelled
build-staging / staging (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
2026-03-05 00:49:26 +05:30
SagarRajput-7
e8e85a3790 feat: added roles crud and details page (#10384)
* feat: added roles crud and details page

* feat: added details page content

* feat: added empty state in members tab

* feat: added permission detail side panel

* feat: made managed roles read only

* feat: added authzresources call in the app context

* feat: refactored files, made constants and utils

* feat: updated test mocks and util

* feat: added redirect to details page upon creation and other refactoring

* feat: used enum and refactoring

* feat: removed sidepanel toggle and changed the buildpayload logic

* feat: moved authz resource to roles page from app context

* feat: semantic token usage

* feat: cleanup, comment address and refactor

* feat: cleanup, comment address and refactor

* feat: temporarily hide the crud and role details page

* feat: added test cases for the role details flow and utilities

* feat: added test config in jest and refactored code

* feat: changed the page availability condition based on the tenant license type

* feat: resolved merge conflict changes in settings util
2026-03-04 19:03:40 +05:30
SagarRajput-7
20d9d5ae38 feat: redesign the custom domain and moved it to general settings (#10432)
* feat: redesign the custom domain and moved it to general settings

* feat: cleanup and refactor

* feat: updated and added test cases

* feat: updated home page data source info section

* feat: cleanup and refactor

* feat: comment addressed

* feat: comment addressed

* feat: scss refactoring with semantic tokens

* feat: update icons to use from signozhq

* feat: added test config in jest and refactored code

---------

Co-authored-by: Vikrant Gupta <vikrant@signoz.io>
2026-03-04 18:41:24 +05:30
Ishan
9a046ab89d Sig 8931 : Migrate quick filters to use /fields/keys and /fields/values (#10110)
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
* feat: merge conflicts

* feat: testcases updated

* feat: checkbox jitter updated

* feat: css updated

* feat: pr  comments signal source

* feat: relatedValues and all values together with unchecked filter handling

* feat: unchecked show more and tooltip for adaptive

* feat: test case

* feat: testcase

* fix: testcase
2026-03-04 10:09:47 +05:30
Vinicius Lourenço
ccb88ab03f feat(vite): migrate from webpack (#10392)
Some checks failed
build-staging / prepare (push) Has been cancelled
build-staging / js-build (push) Has been cancelled
build-staging / go-build (push) Has been cancelled
build-staging / staging (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
2026-03-03 19:50:13 +05:30
Ashwin Bhatkal
4f0e245e3d chore(frontend): dynamic vars load with empty textbox variables (#10480)
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
* fix(frontend): dynamic vars load with empty textbox variables
2026-03-03 16:11:57 +05:30
Nikhil Mantri
a5549f6d5b chore: add status info icon and include additional metrics for status (#10231) 2026-03-03 10:13:52 +00:00
Srikanth Chekuri
1d967fadac chore(metrics-explorer): handle errors properly (#10474) 2026-03-03 09:27:09 +00:00
Abhi kumar
5af6ed6148 chore: updated config builder types (#10477)
* chore: made baseconfigbuilder generic to be used across different charts

* chore: updated baseconfigbuilder test

* chore: updated timezone types

* chore: fixed tsc + test

* chore: fixed tsc + test

* chore: fixed tsc + test

* chore: updated config builder types

* chore: updated comment
2026-03-03 14:17:57 +05:30
Abhi kumar
d094e9cb45 chore: made baseconfigbuilder generic to be used across different charts (#10451)
* chore: made baseconfigbuilder generic to be used across different charts

* chore: updated baseconfigbuilder test

* chore: updated timezone types

* chore: fixed tsc + test

* chore: fixed tsc + test

* chore: fixed tsc + test
2026-03-03 14:05:54 +05:30
441 changed files with 16167 additions and 8250 deletions

View File

@@ -58,19 +58,19 @@ jobs:
run: |
mkdir -p frontend
echo 'CI=1' > frontend/.env
echo 'INTERCOM_APP_ID="${{ secrets.INTERCOM_APP_ID }}"' >> frontend/.env
echo 'SEGMENT_ID="${{ secrets.SEGMENT_ID }}"' >> frontend/.env
echo 'SENTRY_AUTH_TOKEN="${{ secrets.SENTRY_AUTH_TOKEN }}"' >> frontend/.env
echo 'SENTRY_ORG="${{ secrets.SENTRY_ORG }}"' >> frontend/.env
echo 'SENTRY_PROJECT_ID="${{ secrets.SENTRY_PROJECT_ID }}"' >> frontend/.env
echo 'SENTRY_DSN="${{ secrets.SENTRY_DSN }}"' >> frontend/.env
echo 'TUNNEL_URL="${{ secrets.TUNNEL_URL }}"' >> frontend/.env
echo 'TUNNEL_DOMAIN="${{ secrets.TUNNEL_DOMAIN }}"' >> frontend/.env
echo 'POSTHOG_KEY="${{ secrets.POSTHOG_KEY }}"' >> frontend/.env
echo 'PYLON_APP_ID="${{ secrets.PYLON_APP_ID }}"' >> frontend/.env
echo 'APPCUES_APP_ID="${{ secrets.APPCUES_APP_ID }}"' >> frontend/.env
echo 'PYLON_IDENTITY_SECRET="${{ secrets.PYLON_IDENTITY_SECRET }}"' >> frontend/.env
echo 'DOCS_BASE_URL="https://signoz.io"' >> frontend/.env
echo 'VITE_INTERCOM_APP_ID="${{ secrets.INTERCOM_APP_ID }}"' >> frontend/.env
echo 'VITE_SEGMENT_ID="${{ secrets.SEGMENT_ID }}"' >> frontend/.env
echo 'VITE_SENTRY_AUTH_TOKEN="${{ secrets.SENTRY_AUTH_TOKEN }}"' >> frontend/.env
echo 'VITE_SENTRY_ORG="${{ secrets.SENTRY_ORG }}"' >> frontend/.env
echo 'VITE_SENTRY_PROJECT_ID="${{ secrets.SENTRY_PROJECT_ID }}"' >> frontend/.env
echo 'VITE_SENTRY_DSN="${{ secrets.SENTRY_DSN }}"' >> frontend/.env
echo 'VITE_TUNNEL_URL="${{ secrets.TUNNEL_URL }}"' >> frontend/.env
echo 'VITE_TUNNEL_DOMAIN="${{ secrets.TUNNEL_DOMAIN }}"' >> frontend/.env
echo 'VITE_POSTHOG_KEY="${{ secrets.POSTHOG_KEY }}"' >> frontend/.env
echo 'VITE_PYLON_APP_ID="${{ secrets.PYLON_APP_ID }}"' >> frontend/.env
echo 'VITE_APPCUES_APP_ID="${{ secrets.APPCUES_APP_ID }}"' >> frontend/.env
echo 'VITE_PYLON_IDENTITY_SECRET="${{ secrets.PYLON_IDENTITY_SECRET }}"' >> frontend/.env
echo 'VITE_DOCS_BASE_URL="https://signoz.io"' >> frontend/.env
- name: cache-dotenv
uses: actions/cache@v4
with:

View File

@@ -64,12 +64,12 @@ jobs:
run: |
mkdir -p frontend
echo 'CI=1' > frontend/.env
echo 'TUNNEL_URL="${{ secrets.NP_TUNNEL_URL }}"' >> frontend/.env
echo 'TUNNEL_DOMAIN="${{ secrets.NP_TUNNEL_DOMAIN }}"' >> frontend/.env
echo 'PYLON_APP_ID="${{ secrets.NP_PYLON_APP_ID }}"' >> frontend/.env
echo 'APPCUES_APP_ID="${{ secrets.NP_APPCUES_APP_ID }}"' >> frontend/.env
echo 'PYLON_IDENTITY_SECRET="${{ secrets.NP_PYLON_IDENTITY_SECRET }}"' >> frontend/.env
echo 'DOCS_BASE_URL="https://staging.signoz.io"' >> frontend/.env
echo 'VITE_TUNNEL_URL="${{ secrets.NP_TUNNEL_URL }}"' >> frontend/.env
echo 'VITE_TUNNEL_DOMAIN="${{ secrets.NP_TUNNEL_DOMAIN }}"' >> frontend/.env
echo 'VITE_PYLON_APP_ID="${{ secrets.NP_PYLON_APP_ID }}"' >> frontend/.env
echo 'VITE_APPCUES_APP_ID="${{ secrets.NP_APPCUES_APP_ID }}"' >> frontend/.env
echo 'VITE_PYLON_IDENTITY_SECRET="${{ secrets.NP_PYLON_IDENTITY_SECRET }}"' >> frontend/.env
echo 'VITE_DOCS_BASE_URL="https://staging.signoz.io"' >> frontend/.env
- name: cache-dotenv
uses: actions/cache@v4
with:

View File

@@ -7,10 +7,14 @@ on:
pull_request_target:
types:
- labeled
merge_group:
types:
- checks_requested
jobs:
refcheck:
if: |
github.event_name == 'merge_group' ||
(github.event_name == 'pull_request' && ! github.event.pull_request.head.repo.fork && github.event.pull_request.user.login != 'dependabot[bot]' && ! contains(github.event.pull_request.labels.*.name, 'safe-to-test')) ||
(github.event_name == 'pull_request_target' && contains(github.event.pull_request.labels.*.name, 'safe-to-test'))
runs-on: ubuntu-latest

View File

@@ -7,10 +7,14 @@ on:
pull_request_target:
types:
- labeled
merge_group:
types:
- checks_requested
jobs:
test:
if: |
github.event_name == 'merge_group' ||
(github.event_name == 'pull_request' && ! github.event.pull_request.head.repo.fork && github.event.pull_request.user.login != 'dependabot[bot]' && ! contains(github.event.pull_request.labels.*.name, 'safe-to-test')) ||
(github.event_name == 'pull_request_target' && contains(github.event.pull_request.labels.*.name, 'safe-to-test'))
uses: signoz/primus.workflows/.github/workflows/go-test.yaml@main
@@ -21,6 +25,7 @@ jobs:
GO_VERSION: 1.24
fmt:
if: |
github.event_name == 'merge_group' ||
(github.event_name == 'pull_request' && ! github.event.pull_request.head.repo.fork && github.event.pull_request.user.login != 'dependabot[bot]' && ! contains(github.event.pull_request.labels.*.name, 'safe-to-test')) ||
(github.event_name == 'pull_request_target' && contains(github.event.pull_request.labels.*.name, 'safe-to-test'))
uses: signoz/primus.workflows/.github/workflows/go-fmt.yaml@main
@@ -30,6 +35,7 @@ jobs:
GO_VERSION: 1.24
lint:
if: |
github.event_name == 'merge_group' ||
(github.event_name == 'pull_request' && ! github.event.pull_request.head.repo.fork && github.event.pull_request.user.login != 'dependabot[bot]' && ! contains(github.event.pull_request.labels.*.name, 'safe-to-test')) ||
(github.event_name == 'pull_request_target' && contains(github.event.pull_request.labels.*.name, 'safe-to-test'))
uses: signoz/primus.workflows/.github/workflows/go-lint.yaml@main
@@ -39,6 +45,7 @@ jobs:
GO_VERSION: 1.24
deps:
if: |
github.event_name == 'merge_group' ||
(github.event_name == 'pull_request' && ! github.event.pull_request.head.repo.fork && github.event.pull_request.user.login != 'dependabot[bot]' && ! contains(github.event.pull_request.labels.*.name, 'safe-to-test')) ||
(github.event_name == 'pull_request_target' && contains(github.event.pull_request.labels.*.name, 'safe-to-test'))
uses: signoz/primus.workflows/.github/workflows/go-deps.yaml@main
@@ -48,6 +55,7 @@ jobs:
GO_VERSION: 1.24
build:
if: |
github.event_name == 'merge_group' ||
(github.event_name == 'pull_request' && ! github.event.pull_request.head.repo.fork && github.event.pull_request.user.login != 'dependabot[bot]' && ! contains(github.event.pull_request.labels.*.name, 'safe-to-test')) ||
(github.event_name == 'pull_request_target' && contains(github.event.pull_request.labels.*.name, 'safe-to-test'))
runs-on: ubuntu-latest
@@ -79,6 +87,7 @@ jobs:
make docker-build-enterprise
openapi:
if: |
github.event_name == 'merge_group' ||
(github.event_name == 'pull_request' && ! github.event.pull_request.head.repo.fork && github.event.pull_request.user.login != 'dependabot[bot]' && ! contains(github.event.pull_request.labels.*.name, 'safe-to-test')) ||
(github.event_name == 'pull_request_target' && contains(github.event.pull_request.labels.*.name, 'safe-to-test'))
runs-on: ubuntu-latest

View File

@@ -24,19 +24,19 @@ jobs:
- name: dotenv-frontend
working-directory: frontend
run: |
echo 'INTERCOM_APP_ID="${{ secrets.INTERCOM_APP_ID }}"' > .env
echo 'SEGMENT_ID="${{ secrets.SEGMENT_ID }}"' >> .env
echo 'SENTRY_AUTH_TOKEN="${{ secrets.SENTRY_AUTH_TOKEN }}"' >> .env
echo 'SENTRY_ORG="${{ secrets.SENTRY_ORG }}"' >> .env
echo 'SENTRY_PROJECT_ID="${{ secrets.SENTRY_PROJECT_ID }}"' >> .env
echo 'SENTRY_DSN="${{ secrets.SENTRY_DSN }}"' >> .env
echo 'TUNNEL_URL="${{ secrets.TUNNEL_URL }}"' >> .env
echo 'TUNNEL_DOMAIN="${{ secrets.TUNNEL_DOMAIN }}"' >> .env
echo 'POSTHOG_KEY="${{ secrets.POSTHOG_KEY }}"' >> .env
echo 'PYLON_APP_ID="${{ secrets.PYLON_APP_ID }}"' >> .env
echo 'APPCUES_APP_ID="${{ secrets.APPCUES_APP_ID }}"' >> .env
echo 'PYLON_IDENTITY_SECRET="${{ secrets.PYLON_IDENTITY_SECRET }}"' >> .env
echo 'DOCS_BASE_URL="https://signoz.io"' >> .env
echo 'VITE_INTERCOM_APP_ID="${{ secrets.INTERCOM_APP_ID }}"' > .env
echo 'VITE_SEGMENT_ID="${{ secrets.SEGMENT_ID }}"' >> .env
echo 'VITE_SENTRY_AUTH_TOKEN="${{ secrets.SENTRY_AUTH_TOKEN }}"' >> .env
echo 'VITE_SENTRY_ORG="${{ secrets.SENTRY_ORG }}"' >> .env
echo 'VITE_SENTRY_PROJECT_ID="${{ secrets.SENTRY_PROJECT_ID }}"' >> .env
echo 'VITE_SENTRY_DSN="${{ secrets.SENTRY_DSN }}"' >> .env
echo 'VITE_TUNNEL_URL="${{ secrets.TUNNEL_URL }}"' >> .env
echo 'VITE_TUNNEL_DOMAIN="${{ secrets.TUNNEL_DOMAIN }}"' >> .env
echo 'VITE_POSTHOG_KEY="${{ secrets.POSTHOG_KEY }}"' >> .env
echo 'VITE_PYLON_APP_ID="${{ secrets.PYLON_APP_ID }}"' >> .env
echo 'VITE_APPCUES_APP_ID="${{ secrets.APPCUES_APP_ID }}"' >> .env
echo 'VITE_PYLON_IDENTITY_SECRET="${{ secrets.PYLON_IDENTITY_SECRET }}"' >> .env
echo 'VITE_DOCS_BASE_URL="https://signoz.io"' >> .env
- name: node-setup
uses: actions/setup-node@v5
with:

View File

@@ -7,10 +7,14 @@ on:
pull_request_target:
types:
- labeled
merge_group:
types:
- checks_requested
jobs:
tsc:
if: |
github.event_name == 'merge_group' ||
(github.event_name == 'pull_request' && ! github.event.pull_request.head.repo.fork && github.event.pull_request.user.login != 'dependabot[bot]' && ! contains(github.event.pull_request.labels.*.name, 'safe-to-test')) ||
(github.event_name == 'pull_request_target' && contains(github.event.pull_request.labels.*.name, 'safe-to-test'))
runs-on: ubuntu-latest
@@ -27,6 +31,7 @@ jobs:
run: cd frontend && yarn tsc
tsc2:
if: |
github.event_name == 'merge_group' ||
(github.event_name == 'pull_request' && ! github.event.pull_request.head.repo.fork && github.event.pull_request.user.login != 'dependabot[bot]' && ! contains(github.event.pull_request.labels.*.name, 'safe-to-test')) ||
(github.event_name == 'pull_request_target' && contains(github.event.pull_request.labels.*.name, 'safe-to-test'))
uses: signoz/primus.workflows/.github/workflows/js-tsc.yaml@main
@@ -36,6 +41,7 @@ jobs:
JS_SRC: frontend
test:
if: |
github.event_name == 'merge_group' ||
(github.event_name == 'pull_request' && ! github.event.pull_request.head.repo.fork && github.event.pull_request.user.login != 'dependabot[bot]' && ! contains(github.event.pull_request.labels.*.name, 'safe-to-test')) ||
(github.event_name == 'pull_request_target' && contains(github.event.pull_request.labels.*.name, 'safe-to-test'))
uses: signoz/primus.workflows/.github/workflows/js-test.yaml@main
@@ -45,6 +51,7 @@ jobs:
JS_SRC: frontend
fmt:
if: |
github.event_name == 'merge_group' ||
(github.event_name == 'pull_request' && ! github.event.pull_request.head.repo.fork && github.event.pull_request.user.login != 'dependabot[bot]' && ! contains(github.event.pull_request.labels.*.name, 'safe-to-test')) ||
(github.event_name == 'pull_request_target' && contains(github.event.pull_request.labels.*.name, 'safe-to-test'))
uses: signoz/primus.workflows/.github/workflows/js-fmt.yaml@main
@@ -54,6 +61,7 @@ jobs:
JS_SRC: frontend
lint:
if: |
github.event_name == 'merge_group' ||
(github.event_name == 'pull_request' && ! github.event.pull_request.head.repo.fork && github.event.pull_request.user.login != 'dependabot[bot]' && ! contains(github.event.pull_request.labels.*.name, 'safe-to-test')) ||
(github.event_name == 'pull_request_target' && contains(github.event.pull_request.labels.*.name, 'safe-to-test'))
uses: signoz/primus.workflows/.github/workflows/js-lint.yaml@main
@@ -63,6 +71,7 @@ jobs:
JS_SRC: frontend
md-languages:
if: |
github.event_name == 'merge_group' ||
(github.event_name == 'pull_request' && ! github.event.pull_request.head.repo.fork && github.event.pull_request.user.login != 'dependabot[bot]' && ! contains(github.event.pull_request.labels.*.name, 'safe-to-test')) ||
(github.event_name == 'pull_request_target' && contains(github.event.pull_request.labels.*.name, 'safe-to-test'))
runs-on: ubuntu-latest
@@ -73,6 +82,7 @@ jobs:
run: bash frontend/scripts/validate-md-languages.sh
authz:
if: |
github.event_name == 'merge_group' ||
(github.event_name == 'pull_request' && ! github.event.pull_request.head.repo.fork && github.event.pull_request.user.login != 'dependabot[bot]' && ! contains(github.event.pull_request.labels.*.name, 'safe-to-test')) ||
(github.event_name == 'pull_request_target' && contains(github.event.pull_request.labels.*.name, 'safe-to-test'))
runs-on: ubuntu-latest
@@ -103,8 +113,9 @@ jobs:
make py-test-setup
- name: Generate permissions.type.ts
working-directory: ./frontend
run: |
node frontend/scripts/generate-permissions-type.js
yarn generate:permissions-type
- name: Teardown test environment
if: always()

View File

@@ -238,4 +238,4 @@ py-clean: ## Clear all pycache and pytest cache from tests directory recursively
.PHONY: gen-mocks
gen-mocks:
@echo ">> Generating mocks"
@mockery --config .mockery.yml
@mockery --config .mockery.yml

View File

@@ -190,7 +190,7 @@ services:
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
signoz:
!!merge <<: *db-depend
image: signoz/signoz:v0.113.0
image: signoz/signoz:v0.114.1
ports:
- "8080:8080" # signoz port
# - "6060:6060" # pprof port
@@ -213,7 +213,7 @@ services:
retries: 3
otel-collector:
!!merge <<: *db-depend
image: signoz/signoz-otel-collector:v0.144.1
image: signoz/signoz-otel-collector:v0.144.2
entrypoint:
- /bin/sh
command:
@@ -241,7 +241,7 @@ services:
replicas: 3
signoz-telemetrystore-migrator:
!!merge <<: *db-depend
image: signoz/signoz-otel-collector:v0.144.1
image: signoz/signoz-otel-collector:v0.144.2
environment:
- SIGNOZ_OTEL_COLLECTOR_CLICKHOUSE_DSN=tcp://clickhouse:9000
- SIGNOZ_OTEL_COLLECTOR_CLICKHOUSE_CLUSTER=cluster

View File

@@ -117,7 +117,7 @@ services:
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
signoz:
!!merge <<: *db-depend
image: signoz/signoz:v0.113.0
image: signoz/signoz:v0.114.1
ports:
- "8080:8080" # signoz port
volumes:
@@ -139,7 +139,7 @@ services:
retries: 3
otel-collector:
!!merge <<: *db-depend
image: signoz/signoz-otel-collector:v0.144.1
image: signoz/signoz-otel-collector:v0.144.2
entrypoint:
- /bin/sh
command:
@@ -167,7 +167,7 @@ services:
replicas: 3
signoz-telemetrystore-migrator:
!!merge <<: *db-depend
image: signoz/signoz-otel-collector:v0.144.1
image: signoz/signoz-otel-collector:v0.144.2
environment:
- SIGNOZ_OTEL_COLLECTOR_CLICKHOUSE_DSN=tcp://clickhouse:9000
- SIGNOZ_OTEL_COLLECTOR_CLICKHOUSE_CLUSTER=cluster

View File

@@ -181,7 +181,7 @@ services:
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
signoz:
!!merge <<: *db-depend
image: signoz/signoz:${VERSION:-v0.113.0}
image: signoz/signoz:${VERSION:-v0.114.1}
container_name: signoz
ports:
- "8080:8080" # signoz port
@@ -204,7 +204,7 @@ services:
retries: 3
otel-collector:
!!merge <<: *db-depend
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-v0.144.1}
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-v0.144.2}
container_name: signoz-otel-collector
entrypoint:
- /bin/sh
@@ -229,7 +229,7 @@ services:
- "4318:4318" # OTLP HTTP receiver
signoz-telemetrystore-migrator:
!!merge <<: *db-depend
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-v0.144.1}
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-v0.144.2}
container_name: signoz-telemetrystore-migrator
environment:
- SIGNOZ_OTEL_COLLECTOR_CLICKHOUSE_DSN=tcp://clickhouse:9000

View File

@@ -109,7 +109,7 @@ services:
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
signoz:
!!merge <<: *db-depend
image: signoz/signoz:${VERSION:-v0.113.0}
image: signoz/signoz:${VERSION:-v0.114.1}
container_name: signoz
ports:
- "8080:8080" # signoz port
@@ -132,7 +132,7 @@ services:
retries: 3
otel-collector:
!!merge <<: *db-depend
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-v0.144.1}
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-v0.144.2}
container_name: signoz-otel-collector
entrypoint:
- /bin/sh
@@ -157,7 +157,7 @@ services:
- "4318:4318" # OTLP HTTP receiver
signoz-telemetrystore-migrator:
!!merge <<: *db-depend
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-v0.144.1}
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-v0.144.2}
container_name: signoz-telemetrystore-migrator
environment:
- SIGNOZ_OTEL_COLLECTOR_CLICKHOUSE_DSN=tcp://clickhouse:9000

View File

@@ -8,6 +8,8 @@ import (
"github.com/SigNoz/signoz/pkg/querier"
"github.com/SigNoz/signoz/pkg/valuer"
"github.com/SigNoz/signoz/pkg/types/ctxtypes"
"github.com/SigNoz/signoz/pkg/types/instrumentationtypes"
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
)
@@ -67,6 +69,10 @@ func (p *BaseSeasonalProvider) toTSResults(ctx context.Context, resp *qbtypes.Qu
}
func (p *BaseSeasonalProvider) getResults(ctx context.Context, orgID valuer.UUID, params *anomalyQueryParams) (*anomalyQueryResults, error) {
ctx = ctxtypes.NewContextWithCommentVals(ctx, map[string]string{
instrumentationtypes.CodeNamespace: "anomaly",
instrumentationtypes.CodeFunctionName: "getResults",
})
// TODO(srikanthccv): parallelize this?
p.logger.InfoContext(ctx, "fetching results for current period", "anomaly_current_period_query", params.CurrentPeriodQuery)
currentPeriodResults, err := p.querier.QueryRange(ctx, orgID, &params.CurrentPeriodQuery)

View File

@@ -15,7 +15,9 @@ import (
"github.com/SigNoz/signoz/pkg/queryparser"
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/types/ctxtypes"
"github.com/SigNoz/signoz/pkg/types/dashboardtypes"
"github.com/SigNoz/signoz/pkg/types/instrumentationtypes"
"github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
"github.com/SigNoz/signoz/pkg/types/roletypes"
"github.com/SigNoz/signoz/pkg/valuer"
@@ -105,6 +107,10 @@ func (module *module) GetPublicDashboardSelectorsAndOrg(ctx context.Context, id
}
func (module *module) GetPublicWidgetQueryRange(ctx context.Context, id valuer.UUID, widgetIdx, startTime, endTime uint64) (*querybuildertypesv5.QueryRangeResponse, error) {
ctx = ctxtypes.NewContextWithCommentVals(ctx, map[string]string{
instrumentationtypes.CodeNamespace: "dashboard",
instrumentationtypes.CodeFunctionName: "GetPublicWidgetQueryRange",
})
dashboard, err := module.GetDashboardByPublicID(ctx, id)
if err != nil {
return nil, err

View File

@@ -10,6 +10,8 @@ import (
v3 "github.com/SigNoz/signoz/pkg/query-service/model/v3"
"github.com/SigNoz/signoz/pkg/query-service/postprocess"
"github.com/SigNoz/signoz/pkg/query-service/utils/labels"
"github.com/SigNoz/signoz/pkg/types/ctxtypes"
"github.com/SigNoz/signoz/pkg/types/instrumentationtypes"
"github.com/SigNoz/signoz/pkg/valuer"
"go.uber.org/zap"
)
@@ -61,6 +63,10 @@ func (p *BaseSeasonalProvider) getQueryParams(req *GetAnomaliesRequest) *anomaly
}
func (p *BaseSeasonalProvider) getResults(ctx context.Context, orgID valuer.UUID, params *anomalyQueryParams) (*anomalyQueryResults, error) {
ctx = ctxtypes.NewContextWithCommentVals(ctx, map[string]string{
instrumentationtypes.CodeNamespace: "anomaly",
instrumentationtypes.CodeFunctionName: "getResults",
})
zap.L().Info("fetching results for current period", zap.Any("currentPeriodQuery", params.CurrentPeriodQuery))
currentPeriodResults, _, err := p.querierV2.QueryRange(ctx, orgID, params.CurrentPeriodQuery)
if err != nil {

View File

@@ -1,16 +0,0 @@
{
"presets": [
"@babel/preset-env",
["@babel/preset-react", { "runtime": "automatic" }],
"@babel/preset-typescript"
],
"plugins": [
"react-hot-loader/babel",
"@babel/plugin-proposal-class-properties"
],
"env": {
"production": {
"presets": ["minify"]
}
}
}

View File

@@ -11,7 +11,6 @@ module.exports = {
browser: true,
es2021: true,
node: true,
'jest/globals': true,
},
extends: [
'eslint:recommended',
@@ -25,6 +24,7 @@ module.exports = {
parser: '@typescript-eslint/parser',
parserOptions: {
project: './tsconfig.json',
tsconfigRootDir: __dirname,
ecmaFeatures: {
jsx: true,
},
@@ -37,7 +37,7 @@ module.exports = {
'simple-import-sort', // Auto-sort imports
'react-hooks', // React Hooks rules
'prettier', // Code formatting
'jest', // Jest test rules
// 'jest', // TODO: Wait support on Biome to enable again
'jsx-a11y', // Accessibility rules
'import', // Import/export linting
'sonarjs', // Code quality/complexity

View File

@@ -1 +1 @@
16.15.0
22

View File

@@ -0,0 +1,4 @@
export const ENVIRONMENT = {
baseURL: process.env.VITE_FRONTEND_API_ENDPOINT || '',
wsURL: process.env.VITE_WEBSOCKET_API_ENDPOINT || '',
};

20
frontend/babel.config.cjs Normal file
View File

@@ -0,0 +1,20 @@
module.exports = {
presets: [
['@babel/preset-env', { modules: 'auto' }],
['@babel/preset-react', { runtime: 'automatic' }],
['@babel/preset-typescript'],
],
plugins: ['@babel/plugin-proposal-class-properties'],
env: {
test: {
presets: [
[
'@babel/preset-env',
{ modules: 'commonjs', targets: { node: 'current' } },
],
['@babel/preset-react', { runtime: 'automatic' }],
['@babel/preset-typescript'],
],
},
},
};

View File

@@ -1,6 +0,0 @@
module.exports = {
presets: [
['@babel/preset-env', { targets: { node: 'current' } }],
'@babel/preset-typescript',
],
};

View File

@@ -1,8 +0,0 @@
{
"files": [
{
"path": "./build/**.js",
"maxSize": "1.2MB"
}
]
}

View File

@@ -1,8 +1,8 @@
NODE_ENV="development"
BUNDLE_ANALYSER="true"
FRONTEND_API_ENDPOINT="http://localhost:8080/"
PYLON_APP_ID="pylon-app-id"
APPCUES_APP_ID="appcess-app-id"
PYLON_IDENTITY_SECRET="pylon-identity-secret"
VITE_FRONTEND_API_ENDPOINT="http://localhost:8080/"
VITE_PYLON_APP_ID="pylon-app-id"
VITE_APPCUES_APP_ID="appcess-app-id"
VITE_PYLON_IDENTITY_SECRET="pylon-identity-secret"
CI="1"
CI="1"

View File

@@ -1,127 +1,119 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta
http-equiv="Cache-Control"
content="no-cache, no-store, must-revalidate, max-age: 0"
/>
<meta http-equiv="Pragma" content="no-cache" />
<meta http-equiv="Expires" content="0" />
<!-- Preconnect to CDNs -->
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link rel="preconnect" href="https://cdn.vercel.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap"
rel="stylesheet"
/>
<title data-react-helmet="true">
Open source Observability platform | SigNoz
</title>
<meta
data-react-helmet="true"
property="og:title"
content="Open source Observability platform | SigNoz"
/>
<meta
data-react-helmet="true"
name="description"
content="SigNoz is an open source observability platform to help you find issues in your deployed applications & solve them quickly. It provides a single pane of glass for metrics, traces and logs with deep filtering and aggregation to pin down specific issues very quickly."
/>
<meta
data-react-helmet="true"
property="og:description"
content="SigNoz is an open source observability platform to help you find issues in your deployed applications & solve them quickly. It provides a single pane of glass for metrics, traces and logs with deep filtering and aggregation to pin down specific issues very quickly."
/>
<meta
data-react-helmet="true"
property="og:image"
content="/images/signoz-hero-image.webp"
/>
<meta
data-react-helmet="true"
name="twitter:image"
content="/images/signoz-hero-image.webp"
/>
<meta
data-react-helmet="true"
name="twitter:image:alt"
content="Image for Open source Observability platform | SigNoz"
/>
<meta
data-react-helmet="true"
name="twitter:card"
content="summary_large_image"
/>
<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" />
<% if (htmlWebpackPlugin.options.templateParameters.preloadFonts) { %> <%
htmlWebpackPlugin.options.templateParameters.preloadFonts.forEach(function(font)
{ %>
<link
rel="preload"
href="<%= font.href %>"
as="<%= font.as %>"
type="<%= font.type %>"
crossorigin="<%= font.crossorigin %>"
/>
<% }); %> <% } %>
</head>
<body data-theme="default">
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<script>
const PYLON_APP_ID = '<%= htmlWebpackPlugin.options.PYLON_APP_ID %>';
(function () {
var e = window;
var t = document;
var n = function () {
n.e(arguments);
};
n.q = [];
n.e = function (e) {
n.q.push(e);
};
e.Pylon = n;
var r = function () {
var e = t.createElement('script');
e.setAttribute('type', 'text/javascript');
e.setAttribute('async', 'true');
e.setAttribute(
'src',
'https://widget.usepylon.com/widget/' + PYLON_APP_ID,
);
var n = t.getElementsByTagName('script')[0];
n.parentNode.insertBefore(e, n);
};
if (t.readyState === 'complete') {
r();
} else if (e.addEventListener) {
e.addEventListener('load', r, false);
}
})();
</script>
<script type="text/javascript">
window.AppcuesSettings = { enableURLDetection: true };
</script>
<script>
const APPCUES_APP_ID = '<%= htmlWebpackPlugin.options.APPCUES_APP_ID %>';
(function (d, t) {
var a = d.createElement(t);
a.async = 1;
a.src = '//fast.appcues.com/' + APPCUES_APP_ID + '.js';
var s = d.getElementsByTagName(t)[0];
s.parentNode.insertBefore(a, s);
})(document, 'script');
</script>
<link rel="stylesheet" href="/css/uPlot.min.css" />
</body>
</html>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta
http-equiv="Cache-Control"
content="no-cache, no-store, must-revalidate, max-age: 0"
/>
<meta http-equiv="Pragma" content="no-cache" />
<meta http-equiv="Expires" content="0" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link rel="preconnect" href="https://cdn.vercel.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap"
rel="stylesheet"
/>
<title data-react-helmet="true">
Open source Observability platform | SigNoz
</title>
<meta
data-react-helmet="true"
property="og:title"
content="Open source Observability platform | SigNoz"
/>
<meta
data-react-helmet="true"
name="description"
content="SigNoz is an open source observability platform to help you find issues in your deployed applications & solve them quickly. It provides a single pane of glass for metrics, traces and logs with deep filtering and aggregation to pin down specific issues very quickly."
/>
<meta
data-react-helmet="true"
property="og:description"
content="SigNoz is an open source observability platform to help you find issues in your deployed applications & solve them quickly. It provides a single pane of glass for metrics, traces and logs with deep filtering and aggregation to pin down specific issues very quickly."
/>
<meta
data-react-helmet="true"
property="og:image"
content="/images/signoz-hero-image.webp"
/>
<meta
data-react-helmet="true"
name="twitter:image"
content="/images/signoz-hero-image.webp"
/>
<meta
data-react-helmet="true"
name="twitter:image:alt"
content="Image for Open source Observability platform | SigNoz"
/>
<meta
data-react-helmet="true"
name="twitter:card"
content="summary_large_image"
/>
<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" />
</head>
<body data-theme="default">
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<script>
var PYLON_APP_ID = '<%- PYLON_APP_ID %>';
if (PYLON_APP_ID) {
(function () {
var e = window;
var t = document;
var n = function () {
n.e(arguments);
};
n.q = [];
n.e = function (e) {
n.q.push(e);
};
e.Pylon = n;
var r = function () {
var e = t.createElement('script');
e.setAttribute('type', 'text/javascript');
e.setAttribute('async', 'true');
e.setAttribute(
'src',
'https://widget.usepylon.com/widget/' + PYLON_APP_ID,
);
var n = t.getElementsByTagName('script')[0];
n.parentNode.insertBefore(e, n);
};
if (t.readyState === 'complete') {
r();
} else if (e.addEventListener) {
e.addEventListener('load', r, false);
}
})();
}
</script>
<script type="text/javascript">
window.AppcuesSettings = { enableURLDetection: true };
</script>
<script>
var APPCUES_APP_ID = '<%- APPCUES_APP_ID %>';
if (APPCUES_APP_ID) {
(function (d, t) {
var a = d.createElement(t);
a.async = 1;
a.src = '//fast.appcues.com/' + APPCUES_APP_ID + '.js';
var s = d.getElementsByTagName(t)[0];
s.parentNode.insertBefore(a, s);
})(document, 'script');
}
</script>
<link rel="stylesheet" href="/css/uPlot.min.css" />
<script type="module" src="./src/index.tsx"></script>
</body>
</html>

View File

@@ -17,29 +17,31 @@ const config: Config.InitialOptions = {
'^hooks/useSafeNavigate$': USE_SAFE_NAVIGATE_MOCK_PATH,
'^src/hooks/useSafeNavigate$': USE_SAFE_NAVIGATE_MOCK_PATH,
'^.*/useSafeNavigate$': USE_SAFE_NAVIGATE_MOCK_PATH,
'^constants/env$': '<rootDir>/__mocks__/env.ts',
'^src/constants/env$': '<rootDir>/__mocks__/env.ts',
'^@signozhq/icons$':
'<rootDir>/node_modules/@signozhq/icons/dist/index.esm.js',
'^react-syntax-highlighter/dist/esm/(.*)$':
'<rootDir>/node_modules/react-syntax-highlighter/dist/cjs/$1',
'^@signozhq/([^/]+)$': '<rootDir>/node_modules/@signozhq/$1/dist/$1.js',
},
globals: {
extensionsToTreatAsEsm: ['.ts'],
'ts-jest': {
useESM: true,
isolatedModules: true,
tsconfig: '<rootDir>/tsconfig.jest.json',
},
},
extensionsToTreatAsEsm: ['.ts'],
testMatch: ['<rootDir>/src/**/*?(*.)(test).(ts|js)?(x)'],
preset: 'ts-jest/presets/js-with-ts-esm',
transform: {
'^.+\\.(ts|tsx)?$': 'ts-jest',
'^.+\\.(ts|tsx)?$': [
'ts-jest',
{
useESM: true,
tsconfig: '<rootDir>/tsconfig.jest.json',
},
],
'^.+\\.(js|jsx)$': 'babel-jest',
},
transformIgnorePatterns: [
'node_modules/(?!(lodash-es|react-dnd|core-dnd|@react-dnd|dnd-core|react-dnd-html5-backend|axios|@signozhq/design-tokens|@signozhq/table|@signozhq/calendar|@signozhq/input|@signozhq/popover|@signozhq/button|@signozhq/sonner|@signozhq/*|date-fns|d3-interpolate|d3-color|api|@codemirror|@lezer|@marijn)/)',
'node_modules/(?!(lodash-es|react-dnd|core-dnd|@react-dnd|dnd-core|react-dnd-html5-backend|axios|@signozhq/design-tokens|@signozhq/table|@signozhq/calendar|@signozhq/input|@signozhq/popover|@signozhq/button|@signozhq/sonner|@signozhq/*|date-fns|d3-interpolate|d3-color|api|@codemirror|@lezer|@marijn|@grafana)/)',
],
setupFilesAfterEnv: ['<rootDir>jest.setup.ts'],
setupFilesAfterEnv: ['<rootDir>/jest.setup.ts'],
testPathIgnorePatterns: ['/node_modules/', '/public/'],
moduleDirectories: ['node_modules', 'src'],
testEnvironment: 'jest-environment-jsdom',

View File

@@ -7,9 +7,10 @@
*/
import '@testing-library/jest-dom';
import 'jest-styled-components';
import './src/styles.scss';
import { server } from './src/mocks-server/server';
import './src/styles.scss';
// Establish API mocking before all tests.
// Mock window.matchMedia

View File

@@ -1,293 +1,286 @@
{
"name": "frontend",
"version": "1.0.0",
"description": "",
"main": "webpack.config.js",
"scripts": {
"i18n:generate-hash": "node ./i18-generate-hash.js",
"dev": "cross-env NODE_ENV=development webpack serve --progress",
"build": "webpack --config=webpack.config.prod.js --progress",
"prettify": "prettier --write .",
"fmt": "prettier --check .",
"lint": "eslint ./src",
"lint:fix": "eslint ./src --fix",
"jest": "jest",
"jest:coverage": "jest --coverage",
"jest:watch": "jest --watch",
"postinstall": "yarn i18n:generate-hash && (is-ci || yarn husky:configure) && node scripts/update-registry.js",
"husky:configure": "cd .. && husky install frontend/.husky && cd frontend && chmod ug+x .husky/*",
"commitlint": "commitlint --edit $1",
"test": "jest",
"test:changedsince": "jest --changedSince=main --coverage --silent",
"generate:api": "orval --config ./orval.config.ts && sh scripts/post-types-generation.sh",
"generate:permissions-type": "node scripts/generate-permissions-type.js"
},
"engines": {
"node": ">=16.15.0"
},
"author": "",
"license": "ISC",
"dependencies": {
"@ant-design/colors": "6.0.0",
"@ant-design/icons": "4.8.0",
"@codemirror/autocomplete": "6.18.6",
"@codemirror/lang-javascript": "6.2.3",
"@dnd-kit/core": "6.1.0",
"@dnd-kit/modifiers": "7.0.0",
"@dnd-kit/sortable": "8.0.0",
"@dnd-kit/utilities": "3.2.2",
"@grafana/data": "^11.2.3",
"@mdx-js/loader": "2.3.0",
"@mdx-js/react": "2.3.0",
"@monaco-editor/react": "^4.3.1",
"@playwright/test": "1.55.1",
"@radix-ui/react-tabs": "1.0.4",
"@radix-ui/react-tooltip": "1.0.7",
"@sentry/react": "8.41.0",
"@sentry/webpack-plugin": "2.22.6",
"@signozhq/badge": "0.0.2",
"@signozhq/button": "0.0.2",
"@signozhq/calendar": "0.0.0",
"@signozhq/callout": "0.0.2",
"@signozhq/checkbox": "0.0.2",
"@signozhq/combobox": "0.0.2",
"@signozhq/command": "0.0.0",
"@signozhq/design-tokens": "2.1.1",
"@signozhq/icons": "0.1.0",
"@signozhq/input": "0.0.2",
"@signozhq/popover": "0.0.0",
"@signozhq/radio-group": "0.0.2",
"@signozhq/resizable": "0.0.0",
"@signozhq/sonner": "0.1.0",
"@signozhq/switch": "0.0.2",
"@signozhq/table": "0.3.7",
"@signozhq/tooltip": "0.0.2",
"@tanstack/react-table": "8.20.6",
"@tanstack/react-virtual": "3.11.2",
"@uiw/codemirror-theme-copilot": "4.23.11",
"@uiw/codemirror-theme-github": "4.24.1",
"@uiw/react-codemirror": "4.23.10",
"@uiw/react-md-editor": "3.23.5",
"@visx/group": "3.3.0",
"@visx/hierarchy": "3.12.0",
"@visx/shape": "3.5.0",
"@visx/tooltip": "3.3.0",
"@xstate/react": "^3.0.0",
"ansi-to-html": "0.7.2",
"antd": "5.11.0",
"antd-table-saveas-excel": "2.2.1",
"antlr4": "4.13.2",
"axios": "1.12.0",
"babel-eslint": "^10.1.0",
"babel-jest": "^29.6.4",
"babel-loader": "9.1.3",
"babel-plugin-named-asset-import": "^0.3.7",
"babel-preset-minify": "^0.5.1",
"babel-preset-react-app": "^10.0.1",
"chart.js": "3.9.1",
"chartjs-adapter-date-fns": "^2.0.0",
"chartjs-plugin-annotation": "^1.4.0",
"classnames": "2.3.2",
"color": "^4.2.1",
"color-alpha": "1.1.3",
"cross-env": "^7.0.3",
"crypto-js": "4.2.0",
"css-loader": "5.0.0",
"css-minimizer-webpack-plugin": "5.0.1",
"d3-hierarchy": "3.1.2",
"dayjs": "^1.10.7",
"dompurify": "3.2.4",
"dotenv": "8.2.0",
"event-source-polyfill": "1.0.31",
"eventemitter3": "5.0.1",
"file-loader": "6.1.1",
"fontfaceobserver": "2.3.0",
"history": "4.10.1",
"html-webpack-plugin": "5.5.0",
"http-proxy-middleware": "3.0.5",
"http-status-codes": "2.3.0",
"i18next": "^21.6.12",
"i18next-browser-languagedetector": "^6.1.3",
"i18next-http-backend": "^1.3.2",
"immer": "11.1.3",
"jest": "^27.5.1",
"js-base64": "^3.7.2",
"less": "^4.1.2",
"less-loader": "^10.2.0",
"lodash-es": "^4.17.21",
"lucide-react": "0.498.0",
"mini-css-extract-plugin": "2.4.5",
"motion": "12.4.13",
"nuqs": "2.8.8",
"overlayscrollbars": "^2.8.1",
"overlayscrollbars-react": "^0.5.6",
"papaparse": "5.4.1",
"posthog-js": "1.298.0",
"rc-tween-one": "3.0.6",
"react": "18.2.0",
"react-addons-update": "15.6.3",
"react-beautiful-dnd": "13.1.1",
"react-dnd": "16.0.1",
"react-dnd-html5-backend": "16.0.1",
"react-dom": "18.2.0",
"react-drag-listview": "2.0.0",
"react-error-boundary": "4.0.11",
"react-force-graph-2d": "^1.29.1",
"react-full-screen": "1.1.1",
"react-grid-layout": "^1.3.4",
"react-helmet-async": "1.3.0",
"react-i18next": "^11.16.1",
"react-lottie": "1.2.10",
"react-markdown": "8.0.7",
"react-query": "3.39.3",
"react-redux": "^7.2.2",
"react-router-dom": "^5.2.0",
"react-router-dom-v5-compat": "6.27.0",
"react-syntax-highlighter": "15.5.0",
"react-use": "^17.3.2",
"react-virtuoso": "4.0.3",
"redux": "^4.0.5",
"redux-thunk": "^2.3.0",
"rehype-raw": "7.0.0",
"rrule": "2.8.1",
"stream": "^0.0.2",
"style-loader": "1.3.0",
"styled-components": "^5.3.11",
"terser-webpack-plugin": "^5.2.5",
"timestamp-nano": "^1.0.0",
"ts-node": "^10.2.1",
"tsconfig-paths-webpack-plugin": "^3.5.1",
"typescript": "^4.0.5",
"uplot": "1.6.31",
"uuid": "^8.3.2",
"web-vitals": "^0.2.4",
"webpack": "5.94.0",
"webpack-dev-server": "^5.2.1",
"webpack-retry-chunk-load-plugin": "3.1.1",
"xstate": "^4.31.0",
"zustand": "5.0.11"
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
},
"devDependencies": {
"@babel/core": "^7.22.11",
"@babel/plugin-proposal-class-properties": "^7.18.6",
"@babel/plugin-syntax-jsx": "^7.12.13",
"@babel/preset-env": "^7.22.14",
"@babel/preset-react": "^7.12.13",
"@babel/preset-typescript": "^7.21.4",
"@commitlint/cli": "^16.3.0",
"@commitlint/config-conventional": "^16.2.4",
"@faker-js/faker": "9.3.0",
"@jest/globals": "^27.5.1",
"@testing-library/jest-dom": "5.16.5",
"@testing-library/react": "13.4.0",
"@testing-library/user-event": "14.4.3",
"@types/color": "^3.0.3",
"@types/compression-webpack-plugin": "^9.0.0",
"@types/copy-webpack-plugin": "^8.0.1",
"@types/crypto-js": "4.2.2",
"@types/dompurify": "^2.4.0",
"@types/event-source-polyfill": "^1.0.0",
"@types/fontfaceobserver": "2.1.0",
"@types/jest": "^27.5.1",
"@types/lodash-es": "^4.17.4",
"@types/mini-css-extract-plugin": "^2.5.1",
"@types/node": "^16.10.3",
"@types/papaparse": "5.3.7",
"@types/react": "18.0.26",
"@types/react-addons-update": "0.14.21",
"@types/react-beautiful-dnd": "13.1.8",
"@types/react-dom": "18.0.10",
"@types/react-grid-layout": "^1.1.2",
"@types/react-helmet-async": "1.0.3",
"@types/react-lottie": "1.2.10",
"@types/react-redux": "^7.1.11",
"@types/react-resizable": "3.0.3",
"@types/react-router-dom": "^5.1.6",
"@types/react-syntax-highlighter": "15.5.13",
"@types/redux-mock-store": "1.0.4",
"@types/styled-components": "^5.1.4",
"@types/uuid": "^8.3.1",
"@types/webpack": "^5.28.0",
"@types/webpack-dev-server": "^4.7.2",
"@typescript-eslint/eslint-plugin": "^4.33.0",
"@typescript-eslint/parser": "^4.33.0",
"autoprefixer": "10.4.19",
"babel-plugin-styled-components": "^1.12.0",
"compression-webpack-plugin": "9.0.0",
"copy-webpack-plugin": "^11.0.0",
"eslint": "^7.32.0",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-import": "^2.28.1",
"eslint-plugin-jest": "^26.9.0",
"eslint-plugin-jsx-a11y": "^6.5.1",
"eslint-plugin-prettier": "^4.0.0",
"eslint-plugin-react": "^7.24.0",
"eslint-plugin-react-hooks": "^4.3.0",
"eslint-plugin-simple-import-sort": "^7.0.0",
"eslint-plugin-sonarjs": "^0.12.0",
"husky": "^7.0.4",
"image-minimizer-webpack-plugin": "^4.0.0",
"imagemin": "^8.0.1",
"imagemin-svgo": "^10.0.1",
"is-ci": "^3.0.1",
"jest-styled-components": "^7.0.8",
"lint-staged": "^12.5.0",
"msw": "1.3.2",
"npm-run-all": "latest",
"orval": "7.18.0",
"portfinder-sync": "^0.0.2",
"postcss": "8.4.38",
"prettier": "2.2.1",
"prop-types": "15.8.1",
"raw-loader": "4.0.2",
"react-hooks-testing-library": "0.6.0",
"react-hot-loader": "^4.13.0",
"react-resizable": "3.0.4",
"redux-mock-store": "1.5.4",
"sass": "1.66.1",
"sass-loader": "13.3.2",
"sharp": "^0.33.4",
"ts-jest": "^27.1.5",
"ts-node": "^10.2.1",
"typescript-plugin-css-modules": "5.2.0",
"webpack-bundle-analyzer": "^4.5.0",
"webpack-cli": "^5.1.4"
},
"lint-staged": {
"*.(js|jsx|ts|tsx)": [
"eslint --fix",
"sh scripts/typecheck-staged.sh"
]
},
"resolutions": {
"@types/react": "18.0.26",
"@types/react-dom": "18.0.10",
"debug": "4.3.4",
"semver": "7.5.4",
"xml2js": "0.5.0",
"phin": "^3.7.1",
"body-parser": "1.20.3",
"http-proxy-middleware": "3.0.5",
"cross-spawn": "7.0.5",
"cookie": "^0.7.1",
"serialize-javascript": "6.0.2",
"prismjs": "1.30.0",
"got": "11.8.5",
"form-data": "4.0.4",
"brace-expansion": "^2.0.2",
"on-headers": "^1.1.0",
"tmp": "0.2.4"
}
}
"name": "frontend",
"version": "1.0.0",
"description": "",
"type": "module",
"scripts": {
"i18n:generate-hash": "node ./i18-generate-hash.cjs",
"dev": "vite",
"build": "vite build",
"preview": "vite preview",
"prettify": "prettier --write .",
"fmt": "prettier --check .",
"lint": "eslint ./src",
"lint:fix": "eslint ./src --fix",
"jest": "jest",
"jest:coverage": "jest --coverage",
"jest:watch": "jest --watch",
"postinstall": "yarn i18n:generate-hash && (is-ci || yarn husky:configure) && node scripts/update-registry.cjs",
"husky:configure": "cd .. && husky install frontend/.husky && cd frontend && chmod ug+x .husky/*",
"commitlint": "commitlint --edit $1",
"test": "jest",
"test:changedsince": "jest --changedSince=main --coverage --silent",
"generate:api": "orval --config ./orval.config.ts && sh scripts/post-types-generation.sh",
"generate:permissions-type": "node scripts/generate-permissions-type.cjs"
},
"engines": {
"node": ">=22.0.0"
},
"author": "",
"license": "ISC",
"dependencies": {
"@ant-design/colors": "6.0.0",
"@ant-design/icons": "4.8.0",
"@codemirror/autocomplete": "6.18.6",
"@codemirror/lang-javascript": "6.2.3",
"@dnd-kit/core": "6.1.0",
"@dnd-kit/modifiers": "7.0.0",
"@dnd-kit/sortable": "8.0.0",
"@dnd-kit/utilities": "3.2.2",
"@grafana/data": "^11.2.3",
"@mdx-js/loader": "2.3.0",
"@mdx-js/react": "2.3.0",
"@monaco-editor/react": "^4.3.1",
"@playwright/test": "1.55.1",
"@radix-ui/react-tabs": "1.0.4",
"@radix-ui/react-tooltip": "1.0.7",
"@sentry/react": "8.41.0",
"@sentry/vite-plugin": "2.22.6",
"@signozhq/badge": "0.0.2",
"@signozhq/button": "0.0.2",
"@signozhq/calendar": "0.0.0",
"@signozhq/callout": "0.0.2",
"@signozhq/checkbox": "0.0.2",
"@signozhq/combobox": "0.0.2",
"@signozhq/command": "0.0.0",
"@signozhq/design-tokens": "2.1.1",
"@signozhq/dialog": "^0.0.2",
"@signozhq/drawer": "0.0.4",
"@signozhq/icons": "0.1.0",
"@signozhq/input": "0.0.2",
"@signozhq/popover": "0.0.0",
"@signozhq/radio-group": "0.0.2",
"@signozhq/resizable": "0.0.0",
"@signozhq/sonner": "0.1.0",
"@signozhq/switch": "0.0.2",
"@signozhq/table": "0.3.7",
"@signozhq/toggle-group": "^0.0.1",
"@signozhq/tooltip": "0.0.2",
"@tanstack/react-table": "8.20.6",
"@tanstack/react-virtual": "3.11.2",
"@uiw/codemirror-theme-copilot": "4.23.11",
"@uiw/codemirror-theme-github": "4.24.1",
"@uiw/react-codemirror": "4.23.10",
"@uiw/react-md-editor": "3.23.5",
"@visx/group": "3.3.0",
"@visx/hierarchy": "3.12.0",
"@visx/shape": "3.5.0",
"@visx/tooltip": "3.3.0",
"@vitejs/plugin-react": "5.1.4",
"@xstate/react": "^3.0.0",
"ansi-to-html": "0.7.2",
"antd": "5.11.0",
"antd-table-saveas-excel": "2.2.1",
"antlr4": "4.13.2",
"axios": "1.12.0",
"babel-eslint": "^10.1.0",
"babel-jest": "^29.6.4",
"babel-loader": "9.1.3",
"babel-plugin-named-asset-import": "^0.3.7",
"babel-preset-minify": "^0.5.1",
"babel-preset-react-app": "^10.0.1",
"chart.js": "3.9.1",
"chartjs-adapter-date-fns": "^2.0.0",
"chartjs-plugin-annotation": "^1.4.0",
"classnames": "2.3.2",
"color": "^4.2.1",
"color-alpha": "2.0.0",
"cross-env": "^7.0.3",
"crypto-js": "4.2.0",
"d3-hierarchy": "3.1.2",
"dayjs": "^1.10.7",
"dompurify": "3.2.4",
"dotenv": "8.2.0",
"event-source-polyfill": "1.0.31",
"eventemitter3": "5.0.1",
"fontfaceobserver": "2.3.0",
"history": "4.10.1",
"http-proxy-middleware": "3.0.5",
"http-status-codes": "2.3.0",
"i18next": "^21.6.12",
"i18next-browser-languagedetector": "^6.1.3",
"i18next-http-backend": "^1.3.2",
"immer": "11.1.3",
"jest": "30.2.0",
"js-base64": "^3.7.2",
"lodash-es": "^4.17.21",
"lucide-react": "0.498.0",
"mini-css-extract-plugin": "2.4.5",
"motion": "12.4.13",
"nuqs": "2.8.8",
"overlayscrollbars": "^2.8.1",
"overlayscrollbars-react": "^0.5.6",
"papaparse": "5.4.1",
"posthog-js": "1.298.0",
"rc-tween-one": "3.0.6",
"react": "18.2.0",
"react-addons-update": "15.6.3",
"react-beautiful-dnd": "13.1.1",
"react-dnd": "16.0.1",
"react-dnd-html5-backend": "16.0.1",
"react-dom": "18.2.0",
"react-drag-listview": "2.0.0",
"react-error-boundary": "4.0.11",
"react-force-graph-2d": "^1.29.1",
"react-full-screen": "1.1.1",
"react-grid-layout": "^1.3.4",
"react-helmet-async": "1.3.0",
"react-i18next": "^11.16.1",
"react-lottie": "1.2.10",
"react-markdown": "8.0.7",
"react-query": "3.39.3",
"react-redux": "^7.2.2",
"react-router-dom": "^5.2.0",
"react-router-dom-v5-compat": "6.27.0",
"react-syntax-highlighter": "15.5.0",
"react-use": "^17.3.2",
"react-virtuoso": "4.0.3",
"redux": "^4.0.5",
"redux-thunk": "^2.3.0",
"rehype-raw": "7.0.0",
"rollup-plugin-visualizer": "7.0.0",
"rrule": "2.8.1",
"stream": "^0.0.2",
"styled-components": "^5.3.11",
"timestamp-nano": "^1.0.0",
"ts-node": "^10.2.1",
"typescript": "5.9.3",
"uplot": "1.6.31",
"uuid": "^8.3.2",
"vite": "npm:rolldown-vite@7.3.1",
"vite-plugin-html": "3.2.2",
"web-vitals": "^0.2.4",
"xstate": "^4.31.0",
"zustand": "5.0.11"
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
},
"devDependencies": {
"@babel/core": "^7.22.11",
"@babel/plugin-proposal-class-properties": "^7.18.6",
"@babel/plugin-syntax-jsx": "^7.12.13",
"@babel/preset-env": "^7.22.14",
"@babel/preset-react": "^7.12.13",
"@babel/preset-typescript": "^7.21.4",
"@commitlint/cli": "^20.4.2",
"@commitlint/config-conventional": "^20.4.2",
"@faker-js/faker": "9.3.0",
"@jest/globals": "30.2.0",
"@testing-library/jest-dom": "5.16.5",
"@testing-library/react": "13.4.0",
"@testing-library/user-event": "14.4.3",
"@types/color": "^3.0.3",
"@types/crypto-js": "4.2.2",
"@types/dompurify": "^2.4.0",
"@types/event-source-polyfill": "^1.0.0",
"@types/fontfaceobserver": "2.1.0",
"@types/jest": "30.0.0",
"@types/lodash-es": "^4.17.4",
"@types/mini-css-extract-plugin": "^2.5.1",
"@types/node": "^16.10.3",
"@types/papaparse": "5.3.7",
"@types/react": "18.0.26",
"@types/react-addons-update": "0.14.21",
"@types/react-beautiful-dnd": "13.1.8",
"@types/react-dom": "18.0.10",
"@types/react-grid-layout": "^1.1.2",
"@types/react-helmet-async": "1.0.3",
"@types/react-lottie": "1.2.10",
"@types/react-redux": "^7.1.11",
"@types/react-resizable": "3.0.3",
"@types/react-router-dom": "^5.1.6",
"@types/react-syntax-highlighter": "15.5.13",
"@types/redux-mock-store": "1.0.4",
"@types/styled-components": "^5.1.4",
"@types/uuid": "^8.3.1",
"@typescript-eslint/eslint-plugin": "^4.33.0",
"@typescript-eslint/parser": "^4.33.0",
"autoprefixer": "10.4.19",
"babel-plugin-styled-components": "^1.12.0",
"eslint": "^7.32.0",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-import": "^2.28.1",
"eslint-plugin-jest": "^29.15.0",
"eslint-plugin-jsx-a11y": "^6.5.1",
"eslint-plugin-prettier": "^4.0.0",
"eslint-plugin-react": "^7.24.0",
"eslint-plugin-react-hooks": "^4.3.0",
"eslint-plugin-simple-import-sort": "^7.0.0",
"eslint-plugin-sonarjs": "^0.12.0",
"husky": "^7.0.4",
"imagemin": "^8.0.1",
"imagemin-svgo": "^10.0.1",
"is-ci": "^3.0.1",
"jest-environment-jsdom": "29.7.0",
"jest-environment-node": "29.7.0",
"jest-styled-components": "^7.2.0",
"lint-staged": "^12.5.0",
"msw": "1.3.2",
"npm-run-all": "latest",
"orval": "7.18.0",
"portfinder-sync": "^0.0.2",
"postcss": "8.5.6",
"prettier": "2.2.1",
"prop-types": "15.8.1",
"react-hooks-testing-library": "0.6.0",
"react-resizable": "3.0.4",
"redux-mock-store": "1.5.4",
"sass": "1.97.3",
"sharp": "0.34.5",
"svgo": "4.0.0",
"ts-api-utils": "2.4.0",
"ts-jest": "29.4.6",
"ts-node": "^10.2.1",
"typescript-plugin-css-modules": "5.2.0",
"vite-plugin-checker": "0.12.0",
"vite-plugin-compression": "0.5.1",
"vite-plugin-image-optimizer": "2.0.3",
"vite-tsconfig-paths": "6.1.1"
},
"lint-staged": {
"*.(js|jsx|ts|tsx)": [
"eslint --fix",
"sh scripts/typecheck-staged.sh"
]
},
"resolutions": {
"@types/react": "18.0.26",
"@types/react-dom": "18.0.10",
"debug": "4.3.4",
"semver": "7.5.4",
"xml2js": "0.5.0",
"phin": "^3.7.1",
"body-parser": "1.20.3",
"http-proxy-middleware": "3.0.5",
"cross-spawn": "7.0.5",
"cookie": "^0.7.1",
"serialize-javascript": "6.0.2",
"prismjs": "1.30.0",
"got": "11.8.5",
"form-data": "4.0.4",
"brace-expansion": "^2.0.2",
"on-headers": "^1.1.0",
"tmp": "0.2.4",
"vite": "npm:rolldown-vite@7.3.1"
}
}

View File

@@ -13,5 +13,7 @@
"pipelines": "Pipelines",
"archives": "Archives",
"logs_to_metrics": "Logs To Metrics",
"roles": "Roles"
"roles": "Roles",
"role_details": "Role Details",
"members": "Members"
}

View File

@@ -13,5 +13,7 @@
"pipelines": "Pipelines",
"archives": "Archives",
"logs_to_metrics": "Logs To Metrics",
"roles": "Roles"
"roles": "Roles",
"role_details": "Role Details",
"members": "Members"
}

View File

@@ -74,5 +74,6 @@
"METER_EXPLORER": "SigNoz | Meter Explorer",
"METER_EXPLORER_VIEWS": "SigNoz | Meter Explorer Views",
"METER": "SigNoz | Meter",
"ROLES_SETTINGS": "SigNoz | Roles"
"ROLES_SETTINGS": "SigNoz | Roles",
"MEMBERS_SETTINGS": "SigNoz | Members"
}

View File

@@ -27,7 +27,7 @@ const signozPackages = Object.keys(allDeps).filter((dep) =>
const fileContent = `// -------------------------------------------------------------------------
// AUTO-GENERATED FILE
// -------------------------------------------------------------------------
// This file is generated by scripts/update-registry.js automatically
// This file is generated by scripts/update-registry.cjs automatically
// whenever you run 'yarn install' or 'npm install'.
//
// It forces VS Code to index these specific packages to fix auto-import

View File

@@ -218,9 +218,9 @@ function App(): JSX.Element {
pathname === ROUTES.ONBOARDING ||
pathname.startsWith('/public/dashboard/')
) {
window.Pylon('hideChatBubble');
window.Pylon?.('hideChatBubble');
} else {
window.Pylon('showChatBubble');
window.Pylon?.('showChatBubble');
}
}, [pathname]);

View File

@@ -165,11 +165,6 @@ export const MySettings = Loadable(
() => import(/* webpackChunkName: "All MySettings" */ 'pages/Settings'),
);
export const CustomDomainSettings = Loadable(
() =>
import(/* webpackChunkName: "Custom Domain Settings" */ 'pages/Settings'),
);
export const Logs = Loadable(
() => import(/* webpackChunkName: "Logs" */ 'pages/LogsModulePage'),
);

View File

@@ -2,7 +2,7 @@ import { RenderErrorResponseDTO } from 'api/generated/services/sigNoz.schemas';
import { AxiosError } from 'axios';
import APIError from 'types/api/error';
// Handles errors from generated API hooks (which use RenderErrorResponseDTO)
// @deprecated Use convertToApiError instead
export function ErrorResponseHandlerForGeneratedAPIs(
error: AxiosError<RenderErrorResponseDTO>,
): never {
@@ -46,3 +46,34 @@ export function ErrorResponseHandlerForGeneratedAPIs(
},
});
}
// convertToApiError converts an AxiosError from generated API
// hooks into an APIError.
export function convertToApiError(
error: AxiosError<RenderErrorResponseDTO> | null,
): APIError | undefined {
if (!error) {
return undefined;
}
const response = error.response;
const errorData = response?.data?.error;
return new APIError({
httpStatusCode: response?.status || error.status || 500,
error: {
code:
errorData?.code ||
String(response?.status || error.code || 'unknown_error'),
message:
errorData?.message ||
response?.statusText ||
error.message ||
'Something went wrong',
url: errorData?.url ?? '',
errors: (errorData?.errors ?? []).map((e) => ({
message: e.message ?? '',
})),
},
});
}

View File

@@ -29,10 +29,6 @@ import type {
UpdateAuthDomainPathParameters,
} from '../sigNoz.schemas';
type AwaitedInput<T> = PromiseLike<T> | T;
type Awaited<O> = O extends AwaitedInput<infer T> ? T : never;
/**
* This endpoint lists all auth domains
* @summary List all auth domains

View File

@@ -26,10 +26,6 @@ import type {
RenderErrorResponseDTO,
} from '../sigNoz.schemas';
type AwaitedInput<T> = PromiseLike<T> | T;
type Awaited<O> = O extends AwaitedInput<infer T> ? T : never;
/**
* Checks if the authenticated user has permissions for given transactions
* @summary Check permissions

View File

@@ -35,10 +35,6 @@ import type {
UpdatePublicDashboardPathParameters,
} from '../sigNoz.schemas';
type AwaitedInput<T> = PromiseLike<T> | T;
type Awaited<O> = O extends AwaitedInput<infer T> ? T : never;
/**
* This endpoint deletes the public sharing config and disables the public sharing of a dashboard
* @summary Delete public dashboard

View File

@@ -18,10 +18,6 @@ import type { ErrorType } from '../../../generatedAPIInstance';
import { GeneratedAPIInstance } from '../../../generatedAPIInstance';
import type { GetFeatures200, RenderErrorResponseDTO } from '../sigNoz.schemas';
type AwaitedInput<T> = PromiseLike<T> | T;
type Awaited<O> = O extends AwaitedInput<infer T> ? T : never;
/**
* This endpoint returns the supported features and their details
* @summary Get features

View File

@@ -24,10 +24,6 @@ import type {
RenderErrorResponseDTO,
} from '../sigNoz.schemas';
type AwaitedInput<T> = PromiseLike<T> | T;
type Awaited<O> = O extends AwaitedInput<infer T> ? T : never;
/**
* This endpoint returns field keys
* @summary Get field keys

View File

@@ -37,10 +37,6 @@ import type {
UpdateIngestionKeyPathParameters,
} from '../sigNoz.schemas';
type AwaitedInput<T> = PromiseLike<T> | T;
type Awaited<O> = O extends AwaitedInput<infer T> ? T : never;
/**
* This endpoint returns the ingestion keys for a workspace
* @summary Get ingestion keys for workspace

View File

@@ -21,10 +21,6 @@ import type {
RenderErrorResponseDTO,
} from '../sigNoz.schemas';
type AwaitedInput<T> = PromiseLike<T> | T;
type Awaited<O> = O extends AwaitedInput<infer T> ? T : never;
/**
* This endpoint returns global config
* @summary Get global config

View File

@@ -25,10 +25,6 @@ import type {
RenderErrorResponseDTO,
} from '../sigNoz.schemas';
type AwaitedInput<T> = PromiseLike<T> | T;
type Awaited<O> = O extends AwaitedInput<infer T> ? T : never;
/**
* This endpoints promotes and indexes paths
* @summary Promote and index paths

View File

@@ -42,10 +42,6 @@ import type {
UpdateMetricMetadataPathParameters,
} from '../sigNoz.schemas';
type AwaitedInput<T> = PromiseLike<T> | T;
type Awaited<O> = O extends AwaitedInput<infer T> ? T : never;
/**
* This endpoint returns a list of distinct metric names within the specified time range
* @summary List metric names

View File

@@ -25,10 +25,6 @@ import type {
TypesOrganizationDTO,
} from '../sigNoz.schemas';
type AwaitedInput<T> = PromiseLike<T> | T;
type Awaited<O> = O extends AwaitedInput<infer T> ? T : never;
/**
* This endpoint returns the organization I belong to
* @summary Get my organization

View File

@@ -32,10 +32,6 @@ import type {
UpdateUserPreferencePathParameters,
} from '../sigNoz.schemas';
type AwaitedInput<T> = PromiseLike<T> | T;
type Awaited<O> = O extends AwaitedInput<infer T> ? T : never;
/**
* This endpoint lists all org preferences
* @summary List org preferences

View File

@@ -20,10 +20,6 @@ import type {
ReplaceVariables200,
} from '../sigNoz.schemas';
type AwaitedInput<T> = PromiseLike<T> | T;
type Awaited<O> = O extends AwaitedInput<infer T> ? T : never;
/**
* Execute a composite query over a time range. Supports builder queries (traces, logs, metrics), formulas, trace operators, PromQL, and ClickHouse SQL.
* @summary Query range

View File

@@ -35,10 +35,6 @@ import type {
RoletypesPostableRoleDTO,
} from '../sigNoz.schemas';
type AwaitedInput<T> = PromiseLike<T> | T;
type Awaited<O> = O extends AwaitedInput<infer T> ? T : never;
/**
* This endpoint lists all roles
* @summary List roles

View File

@@ -41,10 +41,6 @@ import type {
UpdateServiceAccountStatusPathParameters,
} from '../sigNoz.schemas';
type AwaitedInput<T> = PromiseLike<T> | T;
type Awaited<O> = O extends AwaitedInput<infer T> ? T : never;
/**
* This endpoint lists the service accounts for an organisation
* @summary List service accounts

View File

@@ -33,10 +33,6 @@ import type {
RotateSession200,
} from '../sigNoz.schemas';
type AwaitedInput<T> = PromiseLike<T> | T;
type Awaited<O> = O extends AwaitedInput<infer T> ? T : never;
/**
* This endpoint creates a session for a user using google callback
* @summary Create session by google callback

View File

@@ -51,10 +51,6 @@ import type {
UpdateUserPathParameters,
} from '../sigNoz.schemas';
type AwaitedInput<T> = PromiseLike<T> | T;
type Awaited<O> = O extends AwaitedInput<infer T> ? T : never;
/**
* This endpoint changes the password by id
* @summary Change password

View File

@@ -26,10 +26,6 @@ import type {
ZeustypesPostableProfileDTO,
} from '../sigNoz.schemas';
type AwaitedInput<T> = PromiseLike<T> | T;
type Awaited<O> = O extends AwaitedInput<infer T> ? T : never;
/**
* This endpoint gets the host info from zeus.
* @summary Get host info from Zeus.

View File

@@ -1,54 +0,0 @@
import axios from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import { TreemapViewType } from 'container/MetricsExplorer/Summary/types';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
export interface MetricsTreeMapPayload {
filters: TagFilter;
limit?: number;
treemap?: TreemapViewType;
}
export interface MetricsTreeMapResponse {
status: string;
data: {
[TreemapViewType.TIMESERIES]: TimeseriesData[];
[TreemapViewType.SAMPLES]: SamplesData[];
};
}
export interface TimeseriesData {
percentage: number;
total_value: number;
metric_name: string;
}
export interface SamplesData {
percentage: number;
metric_name: string;
}
export const getMetricsTreeMap = async (
props: MetricsTreeMapPayload,
signal?: AbortSignal,
headers?: Record<string, string>,
): Promise<SuccessResponse<MetricsTreeMapResponse> | ErrorResponse> => {
try {
const response = await axios.post('/metrics/treemap', props, {
signal,
headers,
});
return {
statusCode: 200,
error: null,
message: response.data.status,
payload: response.data,
params: props,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);
}
};

View File

@@ -1,36 +0,0 @@
import axios from 'api';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { Temporality } from './getMetricDetails';
import { MetricType } from './getMetricsList';
export interface UpdateMetricMetadataProps {
description: string;
metricType: MetricType;
temporality?: Temporality;
isMonotonic?: boolean;
unit?: string;
}
export interface UpdateMetricMetadataResponse {
success: boolean;
message: string;
}
const updateMetricMetadata = async (
metricName: string,
props: UpdateMetricMetadataProps,
): Promise<SuccessResponse<UpdateMetricMetadataResponse> | ErrorResponse> => {
const response = await axios.post(`/metrics/${metricName}/metadata`, {
...props,
});
return {
statusCode: 200,
error: null,
message: response.data.status,
payload: response.data.data,
};
};
export default updateMetricMetadata;

View File

@@ -1,7 +1,7 @@
// -------------------------------------------------------------------------
// AUTO-GENERATED FILE
// -------------------------------------------------------------------------
// This file is generated by scripts/update-registry.js automatically
// This file is generated by scripts/update-registry.cjs automatically
// whenever you run 'yarn install' or 'npm install'.
//
// It forces VS Code to index these specific packages to fix auto-import
@@ -18,6 +18,8 @@ import '@signozhq/checkbox';
import '@signozhq/combobox';
import '@signozhq/command';
import '@signozhq/design-tokens';
import '@signozhq/dialog';
import '@signozhq/drawer';
import '@signozhq/icons';
import '@signozhq/input';
import '@signozhq/popover';
@@ -26,4 +28,5 @@ import '@signozhq/resizable';
import '@signozhq/sonner';
import '@signozhq/switch';
import '@signozhq/table';
import '@signozhq/toggle-group';
import '@signozhq/tooltip';

View File

@@ -16,7 +16,7 @@ import {
Tooltip,
Typography,
} from 'antd';
import { FilterDropdownProps } from 'antd/lib/table/interface';
import type { FilterDropdownProps } from 'antd/lib/table/interface';
import logEvent from 'api/common/logEvent';
import {
getQueueOverview,

View File

@@ -1,7 +1,7 @@
import { useQuery } from 'react-query';
// eslint-disable-next-line no-restricted-imports
import { useSelector } from 'react-redux';
import { DefaultOptionType } from 'antd/es/select';
import type { DefaultOptionType } from 'antd/es/select';
import { getAttributesValues } from 'api/queryBuilder/getAttributesValues';
import { DATA_TYPE_VS_ATTRIBUTE_VALUES_KEY } from 'constants/queryBuilder';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';

View File

@@ -1,5 +1,5 @@
import { useState } from 'react';
import { DefaultOptionType } from 'antd/es/select';
import type { DefaultOptionType } from 'antd/es/select';
import useDebouncedFn from 'hooks/useDebouncedFunction';
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { DataSource } from 'types/common/queryBuilder';

View File

@@ -0,0 +1,279 @@
import { useState } from 'react';
import { fireEvent, render, screen } from '@testing-library/react';
import dayjs from 'dayjs';
import * as timeUtils from 'utils/timeUtils';
import CustomTimePicker from './CustomTimePicker';
jest.mock('react-router-dom', () => {
const actual = jest.requireActual('react-router-dom');
return {
...actual,
useLocation: jest.fn().mockReturnValue({
pathname: '/test-path',
}),
};
});
jest.mock('providers/Timezone', () => {
const actual = jest.requireActual('providers/Timezone');
return {
...actual,
useTimezone: jest.fn().mockReturnValue({
timezone: {
value: 'UTC',
offset: '+00:00',
name: 'UTC',
},
browserTimezone: {
value: 'UTC',
offset: '+00:00',
name: 'UTC',
},
}),
};
});
interface WrapperProps {
initialValue?: string;
showLiveLogs?: boolean;
onValidCustomDateChange?: () => void;
onError?: () => void;
onSelect?: (value: string) => void;
onCustomDateHandler?: () => void;
onCustomTimeStatusUpdate?: () => void;
}
function Wrapper({
initialValue = '2024-01-01 00:00:00 - 2024-01-01 01:00:00',
showLiveLogs = false,
onValidCustomDateChange = (): void => {},
onError = (): void => {},
onSelect = (): void => {},
onCustomDateHandler = (): void => {},
onCustomTimeStatusUpdate = (): void => {},
}: WrapperProps): JSX.Element {
const [open, setOpen] = useState(false);
const [selectedTime, setSelectedTime] = useState('custom');
const [selectedValue, setSelectedValue] = useState(initialValue);
const handleSelect = (value: string): void => {
setSelectedTime(value);
onSelect(value);
};
return (
<CustomTimePicker
open={open}
setOpen={setOpen}
onSelect={handleSelect}
onError={onError}
selectedTime={selectedTime}
selectedValue={selectedValue}
onValidCustomDateChange={({ timeStr }): void => {
setSelectedValue(timeStr);
onValidCustomDateChange();
}}
onCustomDateHandler={(): void => {
onCustomDateHandler();
}}
onCustomTimeStatusUpdate={(): void => {
onCustomTimeStatusUpdate();
}}
items={[
{ label: 'Last 5 minutes', value: '5m' },
{ label: 'Custom', value: 'custom' },
]}
minTime={dayjs('2024-01-01 00:00:00').valueOf() * 1000_000}
maxTime={dayjs('2024-01-01 01:00:00').valueOf() * 1000_000}
showLiveLogs={showLiveLogs}
/>
);
}
describe('CustomTimePicker', () => {
it('does not close or reset when clicking input while open', () => {
render(<Wrapper />);
const input = screen.getByRole('textbox');
// Open popover
fireEvent.focus(input);
// Type some text
fireEvent.change(input, { target: { value: '5m' } });
// Click the input again while open
fireEvent.mouseDown(input);
fireEvent.click(input);
// Value should remain as typed
expect((input as HTMLInputElement).value).toBe('5m');
});
it('applies valid shorthand on Enter', () => {
const onValid = jest.fn();
const onError = jest.fn();
render(<Wrapper onValidCustomDateChange={onValid} onError={onError} />);
const input = screen.getByRole('textbox');
fireEvent.focus(input);
fireEvent.change(input, { target: { value: '5m' } });
fireEvent.keyDown(input, { key: 'Enter', code: 'Enter' });
expect(onValid).toHaveBeenCalledTimes(1);
// onError(false) may be called by internal reset logic; we only assert that
// it was never called with a truthy error state
expect(onError).not.toHaveBeenCalledWith(true);
});
it('sets error and updates custom time status for invalid shorthand exceeding max allowed window', () => {
const onValid = jest.fn();
const onError = jest.fn();
const onCustomTimeStatusUpdate = jest.fn();
render(
<Wrapper
onValidCustomDateChange={onValid}
onError={onError}
onCustomTimeStatusUpdate={onCustomTimeStatusUpdate}
/>,
);
const input = screen.getByRole('textbox');
fireEvent.focus(input);
// large number of days to ensure it exceeds the 15 months allowed window
fireEvent.change(input, { target: { value: '9999d' } });
fireEvent.keyDown(input, { key: 'Enter', code: 'Enter' });
expect(onError).toHaveBeenCalledWith(true);
expect(onCustomTimeStatusUpdate).toHaveBeenCalledWith();
expect(onValid).not.toHaveBeenCalled();
});
it('treats close after change like pressing Enter (blur + chevron)', () => {
const onValid = jest.fn();
const onError = jest.fn();
render(<Wrapper onValidCustomDateChange={onValid} onError={onError} />);
const input = screen.getByRole('textbox');
// Open and change value so "changed since open" is true
fireEvent.focus(input);
fireEvent.change(input, { target: { value: '5m' } });
fireEvent.blur(input);
// Click the chevron (which triggers handleClose)
const chevron = document.querySelector(
'.time-input-suffix-icon-badge',
) as HTMLElement;
fireEvent.click(chevron);
// Should have applied the value (same as Enter)
expect(onValid).toHaveBeenCalledTimes(1);
expect(onError).not.toHaveBeenCalledWith(true);
});
it('applies epoch start/end range on Enter via onCustomDateHandler', () => {
const onCustomDateHandler = jest.fn();
const onError = jest.fn();
render(
<Wrapper onCustomDateHandler={onCustomDateHandler} onError={onError} />,
);
const now = dayjs().valueOf();
const later = dayjs().add(1, 'hour').valueOf();
const input = screen.getByRole('textbox');
fireEvent.focus(input);
fireEvent.change(input, {
target: { value: `${now} - ${later}` },
});
fireEvent.keyDown(input, { key: 'Enter', code: 'Enter' });
expect(onCustomDateHandler).toHaveBeenCalledTimes(1);
expect(onError).not.toHaveBeenCalledWith(true);
});
it('uses validateTimeRange result for generic formatted ranges (valid case)', () => {
const validateTimeRangeSpy = jest.spyOn(timeUtils, 'validateTimeRange');
const onCustomDateHandler = jest.fn();
const onError = jest.fn();
validateTimeRangeSpy.mockReturnValue({
isValid: true,
errorDetails: undefined,
startTimeMs: dayjs('2024-01-01 00:00:00').valueOf(),
endTimeMs: dayjs('2024-01-01 01:00:00').valueOf(),
});
render(
<Wrapper onCustomDateHandler={onCustomDateHandler} onError={onError} />,
);
const input = screen.getByRole('textbox');
fireEvent.focus(input);
fireEvent.change(input, {
target: { value: 'foo - bar' },
});
fireEvent.keyDown(input, { key: 'Enter', code: 'Enter' });
expect(validateTimeRangeSpy).toHaveBeenCalled();
expect(onCustomDateHandler).toHaveBeenCalledTimes(1);
expect(onError).not.toHaveBeenCalledWith(true);
validateTimeRangeSpy.mockRestore();
});
it('uses validateTimeRange result for generic formatted ranges (invalid case)', () => {
const validateTimeRangeSpy = jest.spyOn(timeUtils, 'validateTimeRange');
const onValid = jest.fn();
const onError = jest.fn();
validateTimeRangeSpy.mockReturnValue({
isValid: false,
errorDetails: {
message: 'Invalid range',
code: 'INVALID_RANGE',
description: 'Start must be before end',
},
startTimeMs: 0,
endTimeMs: 0,
});
render(<Wrapper onValidCustomDateChange={onValid} onError={onError} />);
const input = screen.getByRole('textbox');
fireEvent.focus(input);
fireEvent.change(input, {
target: { value: 'foo - bar' },
});
fireEvent.keyDown(input, { key: 'Enter', code: 'Enter' });
expect(validateTimeRangeSpy).toHaveBeenCalled();
expect(onError).toHaveBeenCalledWith(true);
expect(onValid).not.toHaveBeenCalled();
validateTimeRangeSpy.mockRestore();
});
it('opens live mode with correct label', () => {
render(<Wrapper showLiveLogs />);
const input = screen.getByRole('textbox');
fireEvent.focus(input);
expect((input as HTMLInputElement).value).toBe('Live');
});
});

View File

@@ -104,6 +104,10 @@ function CustomTimePicker({
const location = useLocation();
const inputRef = useRef<InputRef>(null);
const initialInputValueOnOpenRef = useRef<string>('');
const hasChangedSinceOpenRef = useRef<boolean>(false);
// Tracks if the last pointer down was on the input so we don't close the popover when user clicks the input again
const isClickFromInputRef = useRef(false);
const [activeView, setActiveView] = useState<ViewType>(DEFAULT_VIEW);
@@ -238,6 +242,21 @@ function CustomTimePicker({
};
const handleOpenChange = (newOpen: boolean): void => {
// Don't close when the user clicked the input (trigger); Ant Design treats trigger as "outside" overlay
if (!newOpen && isClickFromInputRef.current) {
isClickFromInputRef.current = false;
return;
}
isClickFromInputRef.current = false;
// If the popover is trying to close and the value changed since opening,
// treat it as if the user pressed Enter (attempt to apply the value)
if (!newOpen && hasChangedSinceOpenRef.current) {
hasChangedSinceOpenRef.current = false;
handleInputPressEnter();
return;
}
setOpen(newOpen);
if (!newOpen) {
@@ -406,10 +425,18 @@ function CustomTimePicker({
const handleOpen = (e?: React.SyntheticEvent): void => {
e?.stopPropagation?.();
// If the popover is already open, avoid resetting the input value
// so that any in-progress edits are preserved.
if (open) {
return;
}
if (showLiveLogs) {
setOpen(true);
setSelectedTimePlaceholderValue('Live');
setInputValue('Live');
initialInputValueOnOpenRef.current = 'Live';
hasChangedSinceOpenRef.current = false;
return;
}
@@ -424,11 +451,21 @@ function CustomTimePicker({
.tz(timezone.value)
.format(DATE_TIME_FORMATS.UK_DATETIME_SECONDS);
setInputValue(`${startTime} - ${endTime}`);
const nextValue = `${startTime} - ${endTime}`;
setInputValue(nextValue);
initialInputValueOnOpenRef.current = nextValue;
hasChangedSinceOpenRef.current = false;
};
const handleClose = (e: React.MouseEvent): void => {
e.stopPropagation();
// If the value changed since opening, treat this like pressing Enter
if (hasChangedSinceOpenRef.current) {
hasChangedSinceOpenRef.current = false;
handleInputPressEnter();
return;
}
setOpen(false);
setCustomDTPickerVisible?.(false);
@@ -450,6 +487,9 @@ function CustomTimePicker({
}, [location.pathname]);
const handleInputBlur = (): void => {
// Track whether the value was changed since the input was opened for editing
hasChangedSinceOpenRef.current =
inputValue !== initialInputValueOnOpenRef.current;
resetErrorStatus();
};
@@ -552,6 +592,12 @@ function CustomTimePicker({
readOnly={!open || showLiveLogs}
placeholder={selectedTimePlaceholderValue}
value={inputValue}
onMouseDown={(e): void => {
// Only treat as "click from input" when the actual input element is clicked (not suffix/chevron)
if (e.target === inputRef.current?.input) {
isClickFromInputRef.current = true;
}
}}
onFocus={handleOpen}
onClick={handleOpen}
onChange={handleInputChange}

View File

@@ -1,4 +1,4 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`DraggableTableRow Snapshot test should render DraggableTableRow 1`] = `
<DocumentFragment>

View File

@@ -0,0 +1,304 @@
.edit-member-drawer {
&__layout {
display: flex;
flex-direction: column;
height: calc(100vh - 48px);
}
&__body {
flex: 1;
overflow-y: auto;
display: flex;
flex-direction: column;
gap: var(--spacing-8);
padding: var(--padding-5) var(--padding-4);
}
&__field {
display: flex;
flex-direction: column;
gap: var(--spacing-4);
}
&__label {
font-size: var(--font-size-sm);
font-weight: var(--font-weight-normal);
color: var(--foreground);
line-height: var(--line-height-20);
letter-spacing: -0.07px;
cursor: default;
}
&__input {
height: 32px;
background: var(--l2-background);
border-color: var(--border);
color: var(--l1-foreground);
box-shadow: none;
&::placeholder {
color: var(--l3-foreground);
}
}
&__input-wrapper {
display: flex;
align-items: center;
justify-content: space-between;
height: 32px;
padding: 0 var(--padding-2);
border-radius: 2px;
background: var(--l2-background);
border: 1px solid var(--border);
&--disabled {
cursor: not-allowed;
opacity: 0.8;
}
}
&__email-text {
font-size: var(--font-size-sm);
font-weight: var(--font-weight-normal);
color: var(--foreground);
line-height: var(--line-height-18);
letter-spacing: -0.07px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
flex: 1;
}
&__lock-icon {
color: var(--foreground);
flex-shrink: 0;
margin-left: 6px;
opacity: 0.6;
}
&__role-select {
width: 100%;
height: 32px;
.ant-select-selector {
background-color: var(--l2-background) !important;
border-color: var(--border) !important;
border-radius: 2px;
padding: 0 var(--padding-2) !important;
display: flex;
align-items: center;
}
.ant-select-selection-item {
font-size: var(--font-size-sm);
color: var(--l1-foreground);
line-height: 32px;
letter-spacing: -0.07px;
}
.ant-select-arrow {
color: var(--foreground);
}
&:not(.ant-select-disabled):hover .ant-select-selector {
border-color: var(--foreground);
}
}
&__meta {
display: flex;
flex-direction: column;
gap: var(--spacing-8);
margin-top: var(--margin-1);
}
&__meta-item {
display: flex;
flex-direction: column;
gap: var(--spacing-2);
[data-slot='badge'] {
padding: var(--padding-1) var(--padding-2);
align-items: center;
font-size: var(--uppercase-small-500-font-size);
font-weight: var(--uppercase-small-500-font-weight);
line-height: 100%;
letter-spacing: 0.44px;
text-transform: uppercase;
}
}
&__meta-label {
font-size: var(--font-size-xs);
font-weight: var(--font-weight-medium);
color: var(--foreground);
line-height: var(--line-height-20);
letter-spacing: 0.48px;
text-transform: uppercase;
}
&__footer {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
height: 56px;
padding: 0 var(--padding-4);
border-top: 1px solid var(--border);
flex-shrink: 0;
background: var(--card);
}
&__footer-left {
display: flex;
align-items: center;
gap: var(--spacing-8);
}
&__footer-right {
display: flex;
align-items: center;
gap: var(--spacing-6);
}
&__footer-divider {
width: 1px;
height: 21px;
background: var(--border);
flex-shrink: 0;
}
&__footer-btn {
display: inline-flex;
align-items: center;
gap: var(--spacing-3);
padding: 0;
background: transparent;
border: none;
cursor: pointer;
font-family: Inter, sans-serif;
font-size: var(--label-small-400-font-size);
font-weight: var(--label-small-400-font-weight);
line-height: var(--label-small-400-line-height);
letter-spacing: var(--label-small-400-letter-spacing);
transition: opacity 0.15s ease;
&:disabled {
opacity: 0.5;
cursor: not-allowed;
}
&:not(:disabled):hover {
opacity: 0.8;
}
&--danger {
color: var(--destructive);
}
&--warning {
color: var(--accent-amber);
}
}
}
.delete-dialog {
background: var(--l2-background);
border: 1px solid var(--l2-border);
[data-slot='dialog-title'] {
color: var(--l1-foreground);
}
&__body {
font-size: var(--paragraph-base-400-font-size);
font-weight: var(--paragraph-base-400-font-weight);
color: var(--l2-foreground);
line-height: var(--paragraph-base-400-line-height);
letter-spacing: -0.065px;
margin: 0;
strong {
font-weight: var(--font-weight-medium);
color: var(--l1-foreground);
}
}
&__footer {
display: flex;
justify-content: flex-end;
gap: var(--spacing-4);
margin-top: var(--margin-6);
}
}
.reset-link-dialog {
background: var(--l2-background);
border: 1px solid var(--l2-border);
[data-slot='dialog-header'] {
border-color: var(--l2-border);
color: var(--l1-foreground);
}
[data-slot='dialog-description'] {
width: 510px;
}
&__content {
display: flex;
flex-direction: column;
gap: var(--spacing-8);
}
&__description {
font-size: var(--paragraph-base-400-font-size);
font-weight: var(--paragraph-base-400-font-weight);
color: var(--l2-foreground);
line-height: var(--paragraph-base-400-line-height);
letter-spacing: -0.065px;
margin: 0;
white-space: normal;
word-break: break-word;
}
&__link-row {
display: flex;
align-items: center;
height: 32px;
overflow: hidden;
background: var(--l2-background);
border: 1px solid var(--border);
border-radius: 2px;
}
&__link-text-wrap {
flex: 1;
min-width: 0;
overflow: hidden;
}
&__link-text {
display: block;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
padding: 0 var(--padding-2);
font-size: var(--paragraph-base-400-font-size);
font-weight: var(--paragraph-base-400-font-weight);
color: var(--l2-foreground);
line-height: var(--line-height-18);
letter-spacing: -0.07px;
}
&__copy-btn {
flex-shrink: 0;
height: 32px;
border-radius: 0 2px 2px 0;
border-top: none;
border-right: none;
border-bottom: none;
border-left: 1px solid var(--border);
min-width: 64px;
}
}

View File

@@ -0,0 +1,510 @@
import { useCallback, useEffect, useState } from 'react';
import { Badge } from '@signozhq/badge';
import { Button } from '@signozhq/button';
import { DialogFooter, DialogWrapper } from '@signozhq/dialog';
import { DrawerWrapper } from '@signozhq/drawer';
import {
Check,
ChevronDown,
Copy,
Link,
LockKeyhole,
RefreshCw,
Trash2,
X,
} from '@signozhq/icons';
import { Input } from '@signozhq/input';
import { toast } from '@signozhq/sonner';
import { Select } from 'antd';
import getResetPasswordToken from 'api/v1/factor_password/getResetPasswordToken';
import sendInvite from 'api/v1/invite/create';
import cancelInvite from 'api/v1/invite/id/delete';
import deleteUser from 'api/v1/user/id/delete';
import update from 'api/v1/user/id/update';
import { MemberRow } from 'components/MembersTable/MembersTable';
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
import ROUTES from 'constants/routes';
import { INVITE_PREFIX, MemberStatus } from 'container/MembersSettings/utils';
import { capitalize } from 'lodash-es';
import { useTimezone } from 'providers/Timezone';
import { ROLES } from 'types/roles';
import './EditMemberDrawer.styles.scss';
export interface EditMemberDrawerProps {
member: MemberRow | null;
open: boolean;
onClose: () => void;
onComplete: () => void;
onRefetch?: () => void;
}
// eslint-disable-next-line sonarjs/cognitive-complexity
function EditMemberDrawer({
member,
open,
onClose,
onComplete,
onRefetch,
}: EditMemberDrawerProps): JSX.Element {
const { formatTimezoneAdjustedTimestamp } = useTimezone();
const [displayName, setDisplayName] = useState('');
const [selectedRole, setSelectedRole] = useState<ROLES>('VIEWER');
const [isSaving, setIsSaving] = useState(false);
const [isDeleting, setIsDeleting] = useState(false);
const [isGeneratingLink, setIsGeneratingLink] = useState(false);
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
const [resetLink, setResetLink] = useState<string | null>(null);
const [showResetLinkDialog, setShowResetLinkDialog] = useState(false);
const [hasCopiedResetLink, setHasCopiedResetLink] = useState(false);
const isInvited = member?.status === MemberStatus.Invited;
// Invited member IDs are prefixed with 'invite-'; strip it to get the real invite ID
const inviteId =
isInvited && member ? member.id.slice(INVITE_PREFIX.length) : null;
useEffect(() => {
if (member) {
setDisplayName(member.name ?? '');
setSelectedRole(member.role);
}
}, [member]);
const isDirty =
member !== null &&
(displayName !== member.name || selectedRole !== member.role);
const formatTimestamp = useCallback(
(ts: string | null | undefined): string => {
if (!ts) {
return '—';
}
const d = new Date(ts);
if (Number.isNaN(d.getTime())) {
return '—';
}
return formatTimezoneAdjustedTimestamp(ts, DATE_TIME_FORMATS.DASH_DATETIME);
},
[formatTimezoneAdjustedTimestamp],
);
const saveInvitedMember = useCallback(async (): Promise<void> => {
if (!member || !inviteId) {
return;
}
await cancelInvite({ id: inviteId });
try {
await sendInvite({
email: member.email,
name: displayName,
role: selectedRole,
frontendBaseUrl: window.location.origin,
});
toast.success('Invite updated successfully', { richColors: true });
onComplete();
onClose();
} catch {
onRefetch?.();
onClose();
toast.error(
'Failed to send the updated invite. Please re-invite this member.',
{ richColors: true },
);
}
}, [
member,
inviteId,
displayName,
selectedRole,
onComplete,
onClose,
onRefetch,
]);
const saveActiveMember = useCallback(async (): Promise<void> => {
if (!member) {
return;
}
await update({
userId: member.id,
displayName,
role: selectedRole,
});
toast.success('Member details updated successfully', { richColors: true });
onComplete();
onClose();
}, [member, displayName, selectedRole, onComplete, onClose]);
const handleSave = useCallback(async (): Promise<void> => {
if (!member || !isDirty) {
return;
}
setIsSaving(true);
try {
if (isInvited && inviteId) {
await saveInvitedMember();
} else {
await saveActiveMember();
}
} catch {
toast.error(
isInvited ? 'Failed to update invite' : 'Failed to update member details',
{ richColors: true },
);
} finally {
setIsSaving(false);
}
}, [
member,
isDirty,
isInvited,
inviteId,
saveInvitedMember,
saveActiveMember,
]);
const handleDelete = useCallback(async (): Promise<void> => {
if (!member) {
return;
}
setIsDeleting(true);
try {
if (isInvited && inviteId) {
await cancelInvite({ id: inviteId });
toast.success('Invitation cancelled successfully', { richColors: true });
} else {
await deleteUser({ userId: member.id });
toast.success('Member deleted successfully', { richColors: true });
}
setShowDeleteConfirm(false);
onComplete();
onClose();
} catch {
toast.error(
isInvited ? 'Failed to cancel invitation' : 'Failed to delete member',
{ richColors: true },
);
} finally {
setIsDeleting(false);
}
}, [member, isInvited, inviteId, onComplete, onClose]);
const handleGenerateResetLink = useCallback(async (): Promise<void> => {
if (!member) {
return;
}
setIsGeneratingLink(true);
try {
const response = await getResetPasswordToken({ userId: member.id });
if (response?.data?.token) {
const link = `${window.location.origin}/password-reset?token=${response.data.token}`;
setResetLink(link);
setHasCopiedResetLink(false);
setShowResetLinkDialog(true);
onClose();
} else {
toast.error('Failed to generate password reset link', {
richColors: true,
position: 'top-right',
});
}
} catch {
toast.error('Failed to generate password reset link', {
richColors: true,
position: 'top-right',
});
} finally {
setIsGeneratingLink(false);
}
}, [member, onClose]);
const handleCopyResetLink = useCallback(async (): Promise<void> => {
if (!resetLink) {
return;
}
try {
await navigator.clipboard.writeText(resetLink);
setHasCopiedResetLink(true);
setTimeout(() => setHasCopiedResetLink(false), 2000);
toast.success('Reset link copied to clipboard', { richColors: true });
} catch {
toast.error('Failed to copy link', {
richColors: true,
});
}
}, [resetLink]);
const handleCopyInviteLink = useCallback(async (): Promise<void> => {
if (!member?.token) {
toast.error('Invite link is not available', {
richColors: true,
position: 'top-right',
});
return;
}
const inviteLink = `${window.location.origin}${ROUTES.SIGN_UP}?token=${member.token}`;
try {
await navigator.clipboard.writeText(inviteLink);
toast.success('Invite link copied to clipboard', {
richColors: true,
position: 'top-right',
});
} catch {
toast.error('Failed to copy invite link', {
richColors: true,
position: 'top-right',
});
}
}, [member]);
const handleClose = useCallback((): void => {
setShowDeleteConfirm(false);
onClose();
}, [onClose]);
const joinedOnLabel = isInvited ? 'Invited On' : 'Joined On';
const drawerContent = (
<div className="edit-member-drawer__layout">
<div className="edit-member-drawer__body">
<div className="edit-member-drawer__field">
<label className="edit-member-drawer__label" htmlFor="member-name">
Name
</label>
<Input
id="member-name"
value={displayName}
onChange={(e): void => setDisplayName(e.target.value)}
className="edit-member-drawer__input"
placeholder="Enter name"
/>
</div>
<div className="edit-member-drawer__field">
<label className="edit-member-drawer__label" htmlFor="member-email">
Email Address
</label>
<div className="edit-member-drawer__input-wrapper edit-member-drawer__input-wrapper--disabled">
<span className="edit-member-drawer__email-text">
{member?.email || '—'}
</span>
<LockKeyhole size={16} className="edit-member-drawer__lock-icon" />
</div>
</div>
<div className="edit-member-drawer__field">
<label className="edit-member-drawer__label" htmlFor="member-role">
Roles
</label>
<Select
id="member-role"
value={selectedRole}
onChange={(role): void => setSelectedRole(role as ROLES)}
className="edit-member-drawer__role-select"
suffixIcon={<ChevronDown size={14} />}
getPopupContainer={(triggerNode): HTMLElement =>
(triggerNode?.closest('.edit-member-drawer') as HTMLElement) ||
document.body
}
>
<Select.Option value="ADMIN">{capitalize('ADMIN')}</Select.Option>
<Select.Option value="EDITOR">{capitalize('EDITOR')}</Select.Option>
<Select.Option value="VIEWER">{capitalize('VIEWER')}</Select.Option>
</Select>
</div>
<div className="edit-member-drawer__meta">
<div className="edit-member-drawer__meta-item">
<span className="edit-member-drawer__meta-label">Status</span>
{member?.status === MemberStatus.Active ? (
<Badge color="forest" variant="outline">
ACTIVE
</Badge>
) : (
<Badge color="amber" variant="outline">
INVITED
</Badge>
)}
</div>
<div className="edit-member-drawer__meta-item">
<span className="edit-member-drawer__meta-label">{joinedOnLabel}</span>
<Badge color="vanilla">{formatTimestamp(member?.joinedOn)}</Badge>
</div>
{!isInvited && (
<div className="edit-member-drawer__meta-item">
<span className="edit-member-drawer__meta-label">Last Modified</span>
<Badge color="vanilla">{formatTimestamp(member?.updatedAt)}</Badge>
</div>
)}
</div>
</div>
<div className="edit-member-drawer__footer">
<div className="edit-member-drawer__footer-left">
<Button
className="edit-member-drawer__footer-btn edit-member-drawer__footer-btn--danger"
onClick={(): void => setShowDeleteConfirm(true)}
>
<Trash2 size={12} />
{isInvited ? 'Cancel Invite' : 'Delete Member'}
</Button>
<div className="edit-member-drawer__footer-divider" />
{isInvited ? (
<Button
className="edit-member-drawer__footer-btn edit-member-drawer__footer-btn--warning"
onClick={handleCopyInviteLink}
disabled={!member?.token}
>
<Link size={12} />
Copy Invite Link
</Button>
) : (
<Button
className="edit-member-drawer__footer-btn edit-member-drawer__footer-btn--warning"
onClick={handleGenerateResetLink}
disabled={isGeneratingLink}
>
<RefreshCw size={12} />
{isGeneratingLink ? 'Generating...' : 'Generate Password Reset Link'}
</Button>
)}
</div>
<div className="edit-member-drawer__footer-right">
<Button variant="solid" color="secondary" size="sm" onClick={handleClose}>
<X size={14} />
Cancel
</Button>
<Button
variant="solid"
color="primary"
size="sm"
disabled={!isDirty || isSaving}
onClick={handleSave}
>
{isSaving ? 'Saving...' : 'Save Member Details'}
</Button>
</div>
</div>
</div>
);
const deleteDialogTitle = isInvited ? 'Cancel Invitation' : 'Delete Member';
const deleteDialogBody = isInvited ? (
<>
Are you sure you want to cancel the invitation for{' '}
<strong>{member?.email}</strong>? They will no longer be able to join the
workspace using this invite.
</>
) : (
<>
Are you sure you want to delete{' '}
<strong>{member?.name || member?.email}</strong>? This will permanently
remove their access to the workspace.
</>
);
const deleteConfirmLabel = isInvited ? 'Cancel Invite' : 'Delete Member';
return (
<>
<DrawerWrapper
open={open}
onOpenChange={(isOpen): void => {
if (!isOpen) {
handleClose();
}
}}
direction="right"
type="panel"
showCloseButton
showOverlay={false}
allowOutsideClick
header={{ title: 'Member Details' }}
content={drawerContent}
className="edit-member-drawer"
/>
<DialogWrapper
open={showResetLinkDialog}
onOpenChange={(isOpen): void => {
if (!isOpen) {
setShowResetLinkDialog(false);
}
}}
title="Password Reset Link"
showCloseButton
width="base"
className="reset-link-dialog"
>
<div className="reset-link-dialog__content">
<p className="reset-link-dialog__description">
This creates a one-time link the team member can use to set a new password
for their SigNoz account.
</p>
<div className="reset-link-dialog__link-row">
<div className="reset-link-dialog__link-text-wrap">
<span className="reset-link-dialog__link-text">{resetLink}</span>
</div>
<Button
variant="outlined"
color="secondary"
size="sm"
onClick={handleCopyResetLink}
prefixIcon={
hasCopiedResetLink ? <Check size={12} /> : <Copy size={12} />
}
className="reset-link-dialog__copy-btn"
>
{hasCopiedResetLink ? 'Copied!' : 'Copy'}
</Button>
</div>
</div>
</DialogWrapper>
<DialogWrapper
open={showDeleteConfirm}
onOpenChange={(isOpen): void => {
if (!isOpen) {
setShowDeleteConfirm(false);
}
}}
title={deleteDialogTitle}
width="narrow"
className="alert-dialog delete-dialog"
showCloseButton={false}
disableOutsideClick={false}
>
<p className="delete-dialog__body">{deleteDialogBody}</p>
<DialogFooter className="delete-dialog__footer">
<Button
variant="solid"
color="secondary"
size="sm"
onClick={(): void => setShowDeleteConfirm(false)}
>
<X size={12} />
Cancel
</Button>
<Button
variant="solid"
color="destructive"
size="sm"
disabled={isDeleting}
onClick={handleDelete}
>
<Trash2 size={12} />
{isDeleting ? 'Processing...' : deleteConfirmLabel}
</Button>
</DialogFooter>
</DialogWrapper>
</>
);
}
export default EditMemberDrawer;

View File

@@ -0,0 +1,277 @@
import type { ReactNode } from 'react';
import { toast } from '@signozhq/sonner';
import getResetPasswordToken from 'api/v1/factor_password/getResetPasswordToken';
import cancelInvite from 'api/v1/invite/id/delete';
import deleteUser from 'api/v1/user/id/delete';
import update from 'api/v1/user/id/update';
import { MemberStatus } from 'container/MembersSettings/utils';
import {
fireEvent,
render,
screen,
userEvent,
waitFor,
} from 'tests/test-utils';
import { ROLES } from 'types/roles';
import EditMemberDrawer, { EditMemberDrawerProps } from '../EditMemberDrawer';
jest.mock('@signozhq/drawer', () => ({
DrawerWrapper: ({
content,
open,
}: {
content?: ReactNode;
open: boolean;
}): JSX.Element | null => (open ? <div>{content}</div> : null),
}));
jest.mock('@signozhq/dialog', () => ({
DialogWrapper: ({
children,
open,
title,
}: {
children?: ReactNode;
open: boolean;
title?: string;
}): JSX.Element | null =>
open ? (
<div role="dialog" aria-label={title}>
{children}
</div>
) : null,
DialogFooter: ({ children }: { children?: ReactNode }): JSX.Element => (
<div>{children}</div>
),
}));
jest.mock('api/v1/user/id/update');
jest.mock('api/v1/user/id/delete');
jest.mock('api/v1/invite/id/delete');
jest.mock('api/v1/invite/create');
jest.mock('api/v1/factor_password/getResetPasswordToken');
jest.mock('@signozhq/sonner', () => ({
toast: {
success: jest.fn(),
error: jest.fn(),
},
}));
const mockUpdate = jest.mocked(update);
const mockDeleteUser = jest.mocked(deleteUser);
const mockCancelInvite = jest.mocked(cancelInvite);
const mockGetResetPasswordToken = jest.mocked(getResetPasswordToken);
const activeMember = {
id: 'user-1',
name: 'Alice Smith',
email: 'alice@signoz.io',
role: 'ADMIN' as ROLES,
status: MemberStatus.Active,
joinedOn: '1700000000000',
updatedAt: '1710000000000',
};
const invitedMember = {
id: 'invite-abc123',
name: '',
email: 'bob@signoz.io',
role: 'VIEWER' as ROLES,
status: MemberStatus.Invited,
joinedOn: '1700000000000',
token: 'tok-xyz',
};
function renderDrawer(
props: Partial<EditMemberDrawerProps> = {},
): ReturnType<typeof render> {
return render(
<EditMemberDrawer
member={activeMember}
open
onClose={jest.fn()}
onComplete={jest.fn()}
{...props}
/>,
);
}
describe('EditMemberDrawer', () => {
beforeEach(() => {
jest.clearAllMocks();
mockUpdate.mockResolvedValue({ httpStatusCode: 200, data: null });
mockDeleteUser.mockResolvedValue({ httpStatusCode: 200, data: null });
mockCancelInvite.mockResolvedValue({ httpStatusCode: 200, data: null });
});
it('renders active member details and disables Save when form is not dirty', () => {
renderDrawer();
expect(screen.getByDisplayValue('Alice Smith')).toBeInTheDocument();
expect(screen.getByText('alice@signoz.io')).toBeInTheDocument();
expect(screen.getByText('ACTIVE')).toBeInTheDocument();
expect(
screen.getByRole('button', { name: /save member details/i }),
).toBeDisabled();
});
it('enables Save after editing name and calls update API on confirm', async () => {
const onComplete = jest.fn();
const user = userEvent.setup({ pointerEventsCheck: 0 });
renderDrawer({ onComplete });
const nameInput = screen.getByDisplayValue('Alice Smith');
await user.clear(nameInput);
await user.type(nameInput, 'Alice Updated');
const saveBtn = screen.getByRole('button', { name: /save member details/i });
await waitFor(() => expect(saveBtn).not.toBeDisabled());
await user.click(saveBtn);
await waitFor(() => {
expect(mockUpdate).toHaveBeenCalledWith(
expect.objectContaining({
userId: 'user-1',
displayName: 'Alice Updated',
}),
);
expect(onComplete).toHaveBeenCalled();
});
});
it('shows delete confirm dialog and calls deleteUser for active members', async () => {
const onComplete = jest.fn();
const user = userEvent.setup({ pointerEventsCheck: 0 });
renderDrawer({ onComplete });
await user.click(screen.getByRole('button', { name: /delete member/i }));
expect(
await screen.findByText(/are you sure you want to delete/i),
).toBeInTheDocument();
const confirmBtns = screen.getAllByRole('button', { name: /delete member/i });
await user.click(confirmBtns[confirmBtns.length - 1]);
await waitFor(() => {
expect(mockDeleteUser).toHaveBeenCalledWith({ userId: 'user-1' });
expect(onComplete).toHaveBeenCalled();
});
});
it('shows Cancel Invite and Copy Invite Link for invited members; hides Last Modified', () => {
renderDrawer({ member: invitedMember });
expect(
screen.getByRole('button', { name: /cancel invite/i }),
).toBeInTheDocument();
expect(
screen.getByRole('button', { name: /copy invite link/i }),
).toBeInTheDocument();
expect(screen.getByText('Invited On')).toBeInTheDocument();
expect(screen.queryByText('Last Modified')).not.toBeInTheDocument();
});
it('calls cancelInvite after confirming Cancel Invite for invited members', async () => {
const onComplete = jest.fn();
const user = userEvent.setup({ pointerEventsCheck: 0 });
renderDrawer({ member: invitedMember, onComplete });
await user.click(screen.getByRole('button', { name: /cancel invite/i }));
expect(
await screen.findByText(/are you sure you want to cancel the invitation/i),
).toBeInTheDocument();
const confirmBtns = screen.getAllByRole('button', { name: /cancel invite/i });
await user.click(confirmBtns[confirmBtns.length - 1]);
await waitFor(() => {
expect(mockCancelInvite).toHaveBeenCalledWith({ id: 'abc123' });
expect(onComplete).toHaveBeenCalled();
});
});
describe('Generate Password Reset Link', () => {
const mockWriteText = jest.fn().mockResolvedValue(undefined);
let clipboardSpy: jest.SpyInstance | undefined;
beforeAll(() => {
Object.defineProperty(navigator, 'clipboard', {
value: { writeText: (): Promise<void> => Promise.resolve() },
configurable: true,
writable: true,
});
});
beforeEach(() => {
mockWriteText.mockClear();
clipboardSpy = jest
.spyOn(navigator.clipboard, 'writeText')
.mockImplementation(mockWriteText);
mockGetResetPasswordToken.mockResolvedValue({
httpStatusCode: 200,
data: { token: 'reset-tok-abc', userId: 'user-1' },
});
});
afterEach(() => {
clipboardSpy?.mockRestore();
});
it('calls getResetPasswordToken and opens the reset link dialog with the generated link', async () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
renderDrawer();
await user.click(
screen.getByRole('button', { name: /generate password reset link/i }),
);
const dialog = await screen.findByRole('dialog', {
name: /password reset link/i,
});
expect(mockGetResetPasswordToken).toHaveBeenCalledWith({
userId: 'user-1',
});
expect(dialog).toBeInTheDocument();
expect(dialog).toHaveTextContent('reset-tok-abc');
});
it('copies the link to clipboard and shows "Copied!" on the button', async () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
const mockToast = jest.mocked(toast);
renderDrawer();
await user.click(
screen.getByRole('button', { name: /generate password reset link/i }),
);
const dialog = await screen.findByRole('dialog', {
name: /password reset link/i,
});
expect(dialog).toHaveTextContent('reset-tok-abc');
fireEvent.click(screen.getByRole('button', { name: /^copy$/i }));
// Verify success path: writeText called with the correct link
await waitFor(() => {
expect(mockToast.success).toHaveBeenCalledWith(
'Reset link copied to clipboard',
expect.anything(),
);
});
expect(mockWriteText).toHaveBeenCalledWith(
expect.stringContaining('reset-tok-abc'),
);
expect(screen.getByRole('button', { name: /copied!/i })).toBeInTheDocument();
});
});
});

View File

@@ -1,4 +1,4 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`Editor renders correctly with custom props 1`] = `
<div>

View File

@@ -13,7 +13,8 @@ jest.mock('react-router-dom', () => ({
}),
}));
jest.mock('antd/es/form/Form', () => ({
jest.mock('antd', () => ({
...jest.requireActual('antd'),
useForm: jest.fn().mockReturnValue({
onFinish: jest.fn(),
}),

View File

@@ -1,6 +1,6 @@
import { UseMutateAsyncFunction } from 'react-query';
import { FormInstance } from 'antd';
import { NotificationInstance } from 'antd/es/notification/interface';
import type { NotificationInstance } from 'antd/es/notification/interface';
import { AxiosResponse } from 'axios';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { OptionsQuery } from 'container/OptionsMenu/types';

View File

@@ -1,4 +1,4 @@
import { NotificationInstance } from 'antd/es/notification/interface';
import type { NotificationInstance } from 'antd/es/notification/interface';
import axios from 'axios';
import { SOMETHING_WENT_WRONG } from 'constants/api';
import { QueryParams } from 'constants/query';

View File

@@ -1,5 +1,4 @@
import { Tag, Typography } from 'antd';
import { ColumnsType } from 'antd/es/table';
import { TableColumnsType as ColumnsType, Tag, Typography } from 'antd';
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
import { getMs } from 'container/Trace/Filters/Panel/PanelBody/Duration/util';
import {

View File

@@ -12,7 +12,7 @@ import {
Tag,
Typography,
} from 'antd';
import { RadioChangeEvent } from 'antd/lib';
import type { RadioChangeEvent } from 'antd/lib';
import logEvent from 'api/common/logEvent';
import { InfraMonitoringEvents } from 'constants/events';
import { QueryParams } from 'constants/query';

View File

@@ -0,0 +1,264 @@
.invite-members-modal {
max-width: 700px;
background: var(--popover);
border: 1px solid var(--secondary);
border-radius: 4px;
box-shadow: 0 4px 9px 0 rgba(0, 0, 0, 0.04);
[data-slot='dialog-header'] {
padding: var(--padding-4);
border-bottom: 1px solid var(--secondary);
flex-shrink: 0;
background: transparent;
margin: 0;
}
[data-slot='dialog-title'] {
font-family: Inter, sans-serif;
font-size: var(--label-base-400-font-size);
font-weight: var(--label-base-400-font-weight);
line-height: var(--label-base-400-line-height);
letter-spacing: -0.065px;
color: var(--bg-base-white);
margin: 0;
}
[data-slot='dialog-description'] {
padding: 0;
.invite-members-modal__content {
display: flex;
flex-direction: column;
gap: var(--spacing-8);
padding: var(--padding-4);
}
}
}
.invite-members-modal__table {
display: flex;
flex-direction: column;
gap: var(--spacing-4);
width: 100%;
}
.invite-members-modal__table-header {
display: flex;
align-items: center;
gap: var(--spacing-8);
width: 100%;
.email-header {
flex: 0 0 240px;
}
.role-header {
flex: 1 0 0;
min-width: 0;
}
.action-header {
flex: 0 0 32px;
}
.table-header-cell {
font-family: Inter, sans-serif;
font-size: var(--paragraph-base-400-font-size);
font-weight: var(--paragraph-base-400-font-weight);
line-height: var(--paragraph-base-400-line-height);
letter-spacing: -0.07px;
color: var(--foreground);
}
}
.invite-members-modal__container {
display: flex;
flex-direction: column;
gap: var(--spacing-8);
width: 100%;
}
.team-member-row {
display: flex;
align-items: flex-start;
gap: var(--spacing-8);
width: 100%;
> .email-cell {
flex: 0 0 240px;
}
> .role-cell {
flex: 1 0 0;
min-width: 0;
}
> .action-cell {
flex: 0 0 32px;
}
}
.team-member-cell {
display: flex;
flex-direction: column;
gap: var(--spacing-2);
&.action-cell {
display: flex;
align-items: center;
justify-content: center;
height: 32px;
}
}
.team-member-email-input {
width: 100%;
height: 32px;
color: var(--l1-foreground);
background-color: var(--l2-background);
border-color: var(--border);
font-size: var(--paragraph-base-400-font-size);
&::placeholder {
color: var(--l3-foreground);
}
&:focus {
border-color: var(--primary);
box-shadow: none;
}
}
.team-member-role-select {
width: 100%;
.ant-select-selector {
height: 32px;
border-radius: 2px;
background-color: var(--l2-background) !important;
border: 1px solid var(--border) !important;
padding: 0 var(--padding-2) !important;
.ant-select-selection-placeholder {
color: var(--l3-foreground);
opacity: 0.4;
font-size: var(--paragraph-base-400-font-size);
letter-spacing: -0.07px;
line-height: 32px;
}
.ant-select-selection-item {
font-size: var(--paragraph-base-400-font-size);
letter-spacing: -0.07px;
color: var(--bg-base-white);
line-height: 32px;
}
}
.ant-select-arrow {
color: var(--foreground);
}
&.ant-select-focused .ant-select-selector,
&:not(.ant-select-disabled):hover .ant-select-selector {
border-color: var(--primary);
}
}
.remove-team-member-button {
display: flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
min-width: 32px;
border: none;
border-radius: 2px;
background: transparent;
color: var(--destructive);
opacity: 0.6;
padding: 0;
transition: background-color 0.2s, opacity 0.2s;
box-shadow: none;
&:hover {
background: rgba(229, 72, 77, 0.1);
opacity: 0.9;
}
}
.email-error-message {
display: block;
font-family: Inter, sans-serif;
font-size: var(--font-size-xs);
font-weight: var(--font-weight-normal);
line-height: var(--line-height-18);
color: var(--destructive);
}
.invite-team-members-error-callout {
background: rgba(229, 72, 77, 0.1);
border: 1px solid rgba(229, 72, 77, 0.2);
border-radius: 4px;
animation: horizontal-shaking 300ms ease-out;
}
@keyframes horizontal-shaking {
0% {
transform: translateX(0);
}
25% {
transform: translateX(5px);
}
50% {
transform: translateX(-5px);
}
75% {
transform: translateX(5px);
}
100% {
transform: translateX(0);
}
}
.invite-members-modal__footer {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
padding: 0 var(--padding-4);
height: 56px;
min-height: 56px;
border-top: 1px solid var(--secondary);
gap: 0;
flex-shrink: 0;
}
.invite-members-modal__footer-right {
display: flex;
align-items: center;
gap: var(--spacing-6);
}
.add-another-member-button {
&:hover {
border-color: var(--primary);
border-style: dashed;
color: var(--l1-foreground);
}
}
.lightMode {
.invite-members-modal {
[data-slot='dialog-title'] {
color: var(--bg-base-black);
}
}
.team-member-role-select {
.ant-select-selector {
.ant-select-selection-item {
color: var(--bg-base-black);
}
}
}
}

View File

@@ -0,0 +1,349 @@
import { useCallback, useEffect, useMemo, useState } from 'react';
import { Button } from '@signozhq/button';
import { Callout } from '@signozhq/callout';
import { Style } from '@signozhq/design-tokens';
import { DialogFooter, DialogWrapper } from '@signozhq/dialog';
import { ChevronDown, CircleAlert, Plus, Trash2, X } from '@signozhq/icons';
import { Input } from '@signozhq/input';
import { toast } from '@signozhq/sonner';
import { Select } from 'antd';
import inviteUsers from 'api/v1/invite/bulk/create';
import sendInvite from 'api/v1/invite/create';
import { cloneDeep, debounce } from 'lodash-es';
import APIError from 'types/api/error';
import { ROLES } from 'types/roles';
import { EMAIL_REGEX } from 'utils/app';
import { v4 as uuid } from 'uuid';
import './InviteMembersModal.styles.scss';
interface InviteRow {
id: string;
email: string;
role: ROLES | '';
}
export interface InviteMembersModalProps {
open: boolean;
onClose: () => void;
onComplete?: () => void;
}
const EMPTY_ROW = (): InviteRow => ({ id: uuid(), email: '', role: '' });
const isRowTouched = (row: InviteRow): boolean =>
row.email.trim() !== '' || Boolean(row.role && row.role.trim() !== '');
function InviteMembersModal({
open,
onClose,
onComplete,
}: InviteMembersModalProps): JSX.Element {
const [rows, setRows] = useState<InviteRow[]>(() => [
EMPTY_ROW(),
EMPTY_ROW(),
EMPTY_ROW(),
]);
const [isSubmitting, setIsSubmitting] = useState(false);
const [emailValidity, setEmailValidity] = useState<Record<string, boolean>>(
{},
);
const [hasInvalidEmails, setHasInvalidEmails] = useState<boolean>(false);
const [hasInvalidRoles, setHasInvalidRoles] = useState<boolean>(false);
const resetAndClose = useCallback((): void => {
setRows([EMPTY_ROW(), EMPTY_ROW(), EMPTY_ROW()]);
setEmailValidity({});
setHasInvalidEmails(false);
setHasInvalidRoles(false);
onClose();
}, [onClose]);
useEffect(() => {
if (open) {
setRows([EMPTY_ROW(), EMPTY_ROW(), EMPTY_ROW()]);
}
}, [open]);
const getValidationErrorMessage = (): string => {
if (hasInvalidEmails && hasInvalidRoles) {
return 'Please enter valid emails and select roles for team members';
}
if (hasInvalidEmails) {
return 'Please enter valid emails for team members';
}
return 'Please select roles for team members';
};
const validateAllUsers = useCallback((): boolean => {
let isValid = true;
let hasEmailErrors = false;
let hasRoleErrors = false;
const updatedEmailValidity: Record<string, boolean> = {};
const touchedRows = rows.filter(isRowTouched);
touchedRows.forEach((row) => {
const emailValid = EMAIL_REGEX.test(row.email);
const roleValid = Boolean(row.role && row.role.trim() !== '');
if (!emailValid || !row.email) {
isValid = false;
hasEmailErrors = true;
}
if (!roleValid) {
isValid = false;
hasRoleErrors = true;
}
if (row.id) {
updatedEmailValidity[row.id] = emailValid;
}
});
setEmailValidity(updatedEmailValidity);
setHasInvalidEmails(hasEmailErrors);
setHasInvalidRoles(hasRoleErrors);
return isValid;
}, [rows]);
const debouncedValidateEmail = useMemo(
() =>
debounce((email: string, rowId: string) => {
const isValid = EMAIL_REGEX.test(email);
setEmailValidity((prev) => ({ ...prev, [rowId]: isValid }));
}, 500),
[],
);
useEffect(() => {
if (!open) {
debouncedValidateEmail.cancel();
}
return (): void => {
debouncedValidateEmail.cancel();
};
}, [open, debouncedValidateEmail]);
const updateEmail = (id: string, email: string): void => {
const updatedRows = cloneDeep(rows);
const rowToUpdate = updatedRows.find((r) => r.id === id);
if (rowToUpdate) {
rowToUpdate.email = email;
setRows(updatedRows);
if (hasInvalidEmails) {
setHasInvalidEmails(false);
}
if (emailValidity[id] === false) {
setEmailValidity((prev) => ({ ...prev, [id]: true }));
}
debouncedValidateEmail(email, id);
}
};
const updateRole = (id: string, role: ROLES): void => {
const updatedRows = cloneDeep(rows);
const rowToUpdate = updatedRows.find((r) => r.id === id);
if (rowToUpdate) {
rowToUpdate.role = role;
setRows(updatedRows);
if (hasInvalidRoles) {
setHasInvalidRoles(false);
}
}
};
const addRow = (): void => {
setRows((prev) => [...prev, EMPTY_ROW()]);
};
const removeRow = (id: string): void => {
setRows((prev) => prev.filter((r) => r.id !== id));
};
const handleSubmit = useCallback(async (): Promise<void> => {
if (!validateAllUsers()) {
return;
}
const touchedRows = rows.filter(isRowTouched);
if (touchedRows.length === 0) {
return;
}
setIsSubmitting(true);
try {
if (touchedRows.length === 1) {
const row = touchedRows[0];
await sendInvite({
email: row.email.trim(),
name: '',
role: row.role as ROLES,
frontendBaseUrl: window.location.origin,
});
} else {
await inviteUsers({
invites: touchedRows.map((row) => ({
email: row.email.trim(),
name: '',
role: row.role,
frontendBaseUrl: window.location.origin,
})),
});
}
toast.success('Invites sent successfully', { richColors: true });
resetAndClose();
onComplete?.();
} catch (err) {
const apiErr = err as APIError;
if (apiErr?.getHttpStatusCode() === 409) {
toast.error(
touchedRows.length === 1
? `${touchedRows[0].email} is already a member`
: 'Invite for one or more users already exists',
{ richColors: true },
);
} else {
const errorMessage = apiErr?.getErrorMessage?.() ?? 'An error occurred';
toast.error(`Failed to send invites: ${errorMessage}`, {
richColors: true,
});
}
} finally {
setIsSubmitting(false);
}
}, [rows, onComplete, resetAndClose, validateAllUsers]);
const touchedRows = rows.filter(isRowTouched);
const isSubmitDisabled = isSubmitting || touchedRows.length === 0;
return (
<DialogWrapper
title="Invite Team Members"
open={open}
onOpenChange={(isOpen): void => {
if (!isOpen) {
resetAndClose();
}
}}
showCloseButton
width="wide"
className="invite-members-modal"
disableOutsideClick={false}
>
<div className="invite-members-modal__content">
<div className="invite-members-modal__table">
<div className="invite-members-modal__table-header">
<div className="table-header-cell email-header">Email address</div>
<div className="table-header-cell role-header">Roles</div>
<div className="table-header-cell action-header" />
</div>
<div className="invite-members-modal__container">
{rows.map(
(row): JSX.Element => (
<div key={row.id} className="team-member-row">
<div className="team-member-cell email-cell">
<Input
type="email"
placeholder="john@signoz.io"
value={row.email}
onChange={(e): void => updateEmail(row.id, e.target.value)}
className="team-member-email-input"
/>
{emailValidity[row.id] === false && row.email.trim() !== '' && (
<span className="email-error-message">Invalid email address</span>
)}
</div>
<div className="team-member-cell role-cell">
<Select
value={row.role || undefined}
onChange={(role): void => updateRole(row.id, role as ROLES)}
className="team-member-role-select"
placeholder="Select roles"
suffixIcon={<ChevronDown size={14} />}
getPopupContainer={(triggerNode): HTMLElement =>
(triggerNode?.closest('.invite-members-modal') as HTMLElement) ||
document.body
}
>
<Select.Option value="VIEWER">Viewer</Select.Option>
<Select.Option value="EDITOR">Editor</Select.Option>
<Select.Option value="ADMIN">Admin</Select.Option>
</Select>
</div>
<div className="team-member-cell action-cell">
{rows.length > 1 && (
<Button
variant="ghost"
color="destructive"
className="remove-team-member-button"
onClick={(): void => removeRow(row.id)}
aria-label="Remove row"
>
<Trash2 size={12} />
</Button>
)}
</div>
</div>
),
)}
</div>
</div>
{(hasInvalidEmails || hasInvalidRoles) && (
<Callout
type="error"
size="small"
showIcon
icon={<CircleAlert size={12} />}
className="invite-team-members-error-callout"
description={getValidationErrorMessage()}
/>
)}
</div>
<DialogFooter className="invite-members-modal__footer">
<Button
variant="dashed"
color="secondary"
size="sm"
className="add-another-member-button"
prefixIcon={<Plus size={12} color={Style.L1_FOREGROUND} />}
onClick={addRow}
>
Add another
</Button>
<div className="invite-members-modal__footer-right">
<Button
type="button"
variant="solid"
color="secondary"
size="sm"
onClick={resetAndClose}
>
<X size={12} />
Cancel
</Button>
<Button
variant="solid"
color="primary"
size="sm"
onClick={handleSubmit}
disabled={isSubmitDisabled}
>
{isSubmitting ? 'Inviting...' : 'Invite Team Members'}
</Button>
</div>
</DialogFooter>
</DialogWrapper>
);
}
export default InviteMembersModal;

View File

@@ -0,0 +1,177 @@
import inviteUsers from 'api/v1/invite/bulk/create';
import sendInvite from 'api/v1/invite/create';
import { render, screen, userEvent, waitFor } from 'tests/test-utils';
import InviteMembersModal from '../InviteMembersModal';
jest.mock('api/v1/invite/create');
jest.mock('api/v1/invite/bulk/create');
jest.mock('@signozhq/sonner', () => ({
toast: {
success: jest.fn(),
error: jest.fn(),
},
}));
const mockSendInvite = jest.mocked(sendInvite);
const mockInviteUsers = jest.mocked(inviteUsers);
const defaultProps = {
open: true,
onClose: jest.fn(),
onComplete: jest.fn(),
};
describe('InviteMembersModal', () => {
beforeEach(() => {
jest.clearAllMocks();
mockSendInvite.mockResolvedValue({
httpStatusCode: 200,
data: { data: 'test', status: 'success' },
});
mockInviteUsers.mockResolvedValue({ httpStatusCode: 200, data: null });
});
it('renders 3 initial empty rows and disables the submit button', () => {
render(<InviteMembersModal {...defaultProps} />);
const emailInputs = screen.getAllByPlaceholderText('john@signoz.io');
expect(emailInputs).toHaveLength(3);
expect(
screen.getByRole('button', { name: /invite team members/i }),
).toBeDisabled();
});
it('adds a row when "Add another" is clicked and removes a row via trash button', async () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
render(<InviteMembersModal {...defaultProps} />);
await user.click(screen.getByRole('button', { name: /add another/i }));
expect(screen.getAllByPlaceholderText('john@signoz.io')).toHaveLength(4);
const removeButtons = screen.getAllByRole('button', { name: /remove row/i });
await user.click(removeButtons[0]);
expect(screen.getAllByPlaceholderText('john@signoz.io')).toHaveLength(3);
});
describe('validation callout messages', () => {
it('shows combined message when email is invalid and role is missing', async () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
render(<InviteMembersModal {...defaultProps} />);
await user.type(
screen.getAllByPlaceholderText('john@signoz.io')[0],
'not-an-email',
);
await user.click(
screen.getByRole('button', { name: /invite team members/i }),
);
expect(
await screen.findByText(
'Please enter valid emails and select roles for team members',
),
).toBeInTheDocument();
});
it('shows email-only message when email is invalid but role is selected', async () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
render(<InviteMembersModal {...defaultProps} />);
const emailInputs = screen.getAllByPlaceholderText('john@signoz.io');
await user.type(emailInputs[0], 'not-an-email');
await user.click(screen.getAllByText('Select roles')[0]);
await user.click(await screen.findByText('Viewer'));
await user.click(
screen.getByRole('button', { name: /invite team members/i }),
);
expect(
await screen.findByText('Please enter valid emails for team members'),
).toBeInTheDocument();
});
it('shows role-only message when email is valid but role is missing', async () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
render(<InviteMembersModal {...defaultProps} />);
await user.type(
screen.getAllByPlaceholderText('john@signoz.io')[0],
'valid@signoz.io',
);
await user.click(
screen.getByRole('button', { name: /invite team members/i }),
);
expect(
await screen.findByText('Please select roles for team members'),
).toBeInTheDocument();
});
});
it('uses sendInvite (single) when only one row is filled', async () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
const onComplete = jest.fn();
render(<InviteMembersModal {...defaultProps} onComplete={onComplete} />);
const emailInputs = screen.getAllByPlaceholderText('john@signoz.io');
await user.type(emailInputs[0], 'single@signoz.io');
const roleSelects = screen.getAllByText('Select roles');
await user.click(roleSelects[0]);
await user.click(await screen.findByText('Viewer'));
await user.click(
screen.getByRole('button', { name: /invite team members/i }),
);
await waitFor(() => {
expect(mockSendInvite).toHaveBeenCalledWith(
expect.objectContaining({ email: 'single@signoz.io', role: 'VIEWER' }),
);
expect(mockInviteUsers).not.toHaveBeenCalled();
expect(onComplete).toHaveBeenCalled();
});
});
it('uses inviteUsers (bulk) when multiple rows are filled', async () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
const onComplete = jest.fn();
render(<InviteMembersModal {...defaultProps} onComplete={onComplete} />);
const emailInputs = screen.getAllByPlaceholderText('john@signoz.io');
await user.type(emailInputs[0], 'alice@signoz.io');
await user.click(screen.getAllByText('Select roles')[0]);
await user.click(await screen.findByText('Viewer'));
await user.type(emailInputs[1], 'bob@signoz.io');
await user.click(screen.getAllByText('Select roles')[0]);
const editorOptions = await screen.findAllByText('Editor');
await user.click(editorOptions[editorOptions.length - 1]);
await user.click(
screen.getByRole('button', { name: /invite team members/i }),
);
await waitFor(() => {
expect(mockInviteUsers).toHaveBeenCalledWith({
invites: expect.arrayContaining([
expect.objectContaining({ email: 'alice@signoz.io', role: 'VIEWER' }),
expect.objectContaining({ email: 'bob@signoz.io', role: 'EDITOR' }),
]),
});
expect(mockSendInvite).not.toHaveBeenCalled();
expect(onComplete).toHaveBeenCalled();
});
});
});

View File

@@ -4,7 +4,7 @@ import { useSelector } from 'react-redux';
import { useCopyToClipboard, useLocation } from 'react-use';
import { Color, Spacing } from '@signozhq/design-tokens';
import { Button, Divider, Drawer, Radio, Tooltip, Typography } from 'antd';
import { RadioChangeEvent } from 'antd/lib';
import type { RadioChangeEvent } from 'antd/lib';
import cx from 'classnames';
import { LogType } from 'components/Logs/LogStateIndicator/LogStateIndicator';
import QuerySearch from 'components/QueryBuilderV2/QueryV2/QuerySearch/QuerySearch';

View File

@@ -1,4 +1,7 @@
import { ColumnsType, ColumnType } from 'antd/es/table';
import {
TableColumnsType as ColumnsType,
TableColumnType as ColumnType,
} from 'antd';
import { FontSize } from 'container/OptionsMenu/types';
import { IField } from 'types/api/logs/fields';
import { ILog } from 'types/api/logs/log';

View File

@@ -1,6 +1,5 @@
import { useMemo } from 'react';
import { Typography } from 'antd';
import { ColumnsType } from 'antd/es/table';
import { TableColumnsType as ColumnsType, Typography } from 'antd';
import cx from 'classnames';
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
import { getSanitizedLogBody } from 'container/LogDetailedView/utils';

View File

@@ -1,6 +1,6 @@
import { useCallback, useEffect, useRef, useState } from 'react';
import { Button, Input, InputNumber, Popover, Tooltip, Typography } from 'antd';
import { DefaultOptionType } from 'antd/es/select';
import type { DefaultOptionType } from 'antd/es/select';
import cx from 'classnames';
import { LogViewMode } from 'container/LogsTable';
import { FontSize, OptionsMenuConfig } from 'container/OptionsMenu/types';

View File

@@ -0,0 +1,216 @@
.members-table-wrapper {
display: flex;
flex-direction: column;
flex: 1;
min-height: 0;
overflow: hidden;
border-radius: 4px;
}
.members-table {
.ant-table {
background: transparent;
font-size: 13px;
}
.ant-table-container {
border-radius: 0 !important;
border: none !important;
}
.ant-table-thead {
> tr > th,
> tr > td {
background: var(--background);
font-size: var(--paragraph-small-600-font-size);
font-weight: var(--paragraph-small-600-font-weight);
line-height: var(--paragraph-small-600-line-height);
letter-spacing: 0.44px;
text-transform: uppercase;
color: var(--foreground);
padding: var(--padding-2) var(--padding-4);
border-bottom: none !important;
border-top: none !important;
&::before {
display: none !important;
}
.ant-table-column-sorters {
display: inline-flex;
align-items: center;
gap: var(--spacing-1);
width: auto;
}
.ant-table-column-title {
flex: unset;
}
.ant-table-column-sorter {
color: var(--foreground);
opacity: 0.6;
}
.ant-table-column-sorter-up.active,
.ant-table-column-sorter-down.active {
color: var(--bg-base-white);
opacity: 1;
}
}
}
.ant-table-tbody {
> tr > td {
border-bottom: none !important;
padding: var(--padding-2) var(--padding-4);
background: transparent;
transition: none;
}
> tr.members-table-row--tinted > td {
background: rgba(171, 189, 255, 0.02);
}
> tr:hover > td {
background: rgba(171, 189, 255, 0.04) !important;
}
}
.ant-table-wrapper,
.ant-table-container,
.ant-spin-nested-loading,
.ant-spin-container {
border: none !important;
box-shadow: none !important;
}
.member-status-cell {
[data-slot='badge'] {
padding: var(--padding-1) var(--padding-2);
align-items: center;
font-size: var(--uppercase-small-500-font-size);
font-weight: var(--uppercase-small-500-font-weight);
line-height: 100%;
letter-spacing: 0.44px;
text-transform: uppercase;
}
}
}
.member-name-email-cell {
display: flex;
align-items: center;
gap: var(--spacing-2);
height: 22px;
overflow: hidden;
.member-name {
font-size: var(--paragraph-base-500-font-size);
font-weight: var(--paragraph-base-500-font-weight);
color: var(--foreground);
line-height: var(--paragraph-base-500-line-height);
letter-spacing: -0.07px;
white-space: nowrap;
flex-shrink: 0;
}
.member-email {
font-size: var(--paragraph-base-400-font-size);
font-weight: var(--paragraph-base-400-font-weight);
color: var(--l3-foreground-hover);
line-height: var(--paragraph-base-400-line-height);
letter-spacing: -0.07px;
flex: 1 0 0;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
.member-joined-date {
font-size: var(--paragraph-base-400-font-size);
font-weight: var(--paragraph-base-400-font-weight);
color: var(--foreground);
line-height: var(--line-height-18);
letter-spacing: -0.07px;
white-space: nowrap;
}
.member-joined-dash {
font-size: var(--paragraph-base-400-font-size);
color: var(--l3-foreground-hover);
line-height: var(--line-height-18);
letter-spacing: -0.07px;
}
.members-empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: var(--padding-12) var(--padding-4);
gap: var(--spacing-4);
color: var(--foreground);
&__emoji {
font-size: var(--font-size-2xl);
line-height: 1;
}
&__text {
font-size: var(--paragraph-base-400-font-size);
font-weight: var(--paragraph-base-400-font-weight);
color: var(--foreground);
margin: 0;
line-height: var(--paragraph-base-400-font-height);
strong {
font-weight: var(--font-weight-medium);
color: var(--bg-base-white);
}
}
}
.members-table-pagination {
display: flex;
align-items: center;
justify-content: flex-end;
padding: var(--padding-2) var(--padding-4);
.ant-pagination-total-text {
margin-right: auto;
}
.members-pagination-range {
font-size: var(--font-size-xs);
color: var(--foreground);
}
.members-pagination-total {
font-size: var(--font-size-xs);
color: var(--foreground);
opacity: 0.5;
}
}
.lightMode {
.members-table {
.ant-table-tbody {
> tr.members-table-row--tinted > td {
background: rgba(0, 0, 0, 0.015);
}
> tr:hover > td {
background: rgba(0, 0, 0, 0.03) !important;
}
}
}
.members-empty-state {
&__text {
strong {
color: var(--bg-base-black);
}
}
}
}

View File

@@ -0,0 +1,238 @@
import type React from 'react';
import { Badge } from '@signozhq/badge';
import { Pagination, Table, Tooltip } from 'antd';
import type { ColumnsType, SorterResult } from 'antd/es/table/interface';
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
import { MemberStatus } from 'container/MembersSettings/utils';
import { capitalize } from 'lodash-es';
import { useTimezone } from 'providers/Timezone';
import { ROLES } from 'types/roles';
import './MembersTable.styles.scss';
export interface MemberRow {
id: string;
name?: string;
email: string;
role: ROLES;
status: MemberStatus;
joinedOn: string | null;
updatedAt?: string | null;
token?: string | null;
}
interface MembersTableProps {
data: MemberRow[];
loading: boolean;
total: number;
currentPage: number;
pageSize: number;
searchQuery: string;
onPageChange: (page: number) => void;
onRowClick?: (member: MemberRow) => void;
onSortChange?: (
sorter: SorterResult<MemberRow> | SorterResult<MemberRow>[],
) => void;
}
function NameEmailCell({
name,
email,
}: {
name?: string;
email: string;
}): JSX.Element {
return (
<div className="member-name-email-cell">
{name && (
<span className="member-name" title={name}>
{name}
</span>
)}
<Tooltip title={email} overlayClassName="member-tooltip">
<span className="member-email">{email}</span>
</Tooltip>
</div>
);
}
function StatusBadge({ status }: { status: MemberRow['status'] }): JSX.Element {
if (status === MemberStatus.Active) {
return (
<Badge color="forest" variant="outline">
ACTIVE
</Badge>
);
}
return (
<Badge color="amber" variant="outline">
INVITED
</Badge>
);
}
function MembersEmptyState({
searchQuery,
}: {
searchQuery: string;
}): JSX.Element {
return (
<div className="members-empty-state">
<span
className="members-empty-state__emoji"
role="img"
aria-label="monocle face"
>
🧐
</span>
{searchQuery ? (
<p className="members-empty-state__text">
No results for <strong>{searchQuery}</strong>
</p>
) : (
<p className="members-empty-state__text">No members found</p>
)}
</div>
);
}
function MembersTable({
data,
loading,
total,
currentPage,
pageSize,
searchQuery,
onPageChange,
onRowClick,
onSortChange,
}: MembersTableProps): JSX.Element {
const { formatTimezoneAdjustedTimestamp } = useTimezone();
const formatJoinedOn = (date: string | null): string => {
if (!date) {
return '—';
}
const d = new Date(date);
if (Number.isNaN(d.getTime())) {
return '—';
}
return formatTimezoneAdjustedTimestamp(date, DATE_TIME_FORMATS.DASH_DATETIME);
};
const columns: ColumnsType<MemberRow> = [
{
title: 'Name / Email',
dataIndex: 'name',
key: 'name',
sorter: (a, b): number => a.email.localeCompare(b.email),
render: (_, record): JSX.Element => (
<NameEmailCell name={record.name} email={record.email} />
),
},
{
title: 'Roles',
dataIndex: 'role',
key: 'role',
width: 180,
sorter: (a, b): number => a.role.localeCompare(b.role),
render: (role: ROLES): JSX.Element => (
<Badge color="vanilla">{capitalize(role)}</Badge>
),
},
{
title: 'Status',
dataIndex: 'status',
key: 'status',
width: 100,
align: 'right' as const,
className: 'member-status-cell',
sorter: (a, b): number => a.status.localeCompare(b.status),
render: (status: MemberRow['status']): JSX.Element => (
<StatusBadge status={status} />
),
},
{
title: 'Joined On',
dataIndex: 'joinedOn',
key: 'joinedOn',
width: 250,
align: 'right' as const,
sorter: (a, b): number => {
if (!a.joinedOn && !b.joinedOn) {
return 0;
}
if (!a.joinedOn) {
return 1;
}
if (!b.joinedOn) {
return -1;
}
return new Date(a.joinedOn).getTime() - new Date(b.joinedOn).getTime();
},
render: (joinedOn: string | null): JSX.Element => {
const formatted = formatJoinedOn(joinedOn);
const isDash = formatted === '—';
return (
<span className={isDash ? 'member-joined-dash' : 'member-joined-date'}>
{formatted}
</span>
);
},
},
];
const showPaginationTotal = (_total: number, range: number[]): JSX.Element => (
<>
<span className="members-pagination-range">
{range[0]} &#8212; {range[1]}
</span>
<span className="members-pagination-total"> of {_total}</span>
</>
);
return (
<div className="members-table-wrapper">
<Table<MemberRow>
columns={columns}
dataSource={data}
rowKey="id"
loading={loading}
pagination={false}
rowClassName={(_, index): string =>
index % 2 === 0 ? 'members-table-row--tinted' : ''
}
onRow={(record): React.HTMLAttributes<HTMLElement> => ({
onClick: (): void => onRowClick?.(record),
style: onRowClick ? { cursor: 'pointer' } : undefined,
})}
onChange={(_, __, sorter): void => {
if (onSortChange) {
onSortChange(
sorter as SorterResult<MemberRow> | SorterResult<MemberRow>[],
);
}
}}
showSorterTooltip={false}
locale={{
emptyText: <MembersEmptyState searchQuery={searchQuery} />,
}}
className="members-table"
/>
{total > pageSize && (
<Pagination
current={currentPage}
pageSize={pageSize}
total={total}
showTotal={showPaginationTotal}
showSizeChanger={false}
onChange={onPageChange}
className="members-table-pagination"
/>
)}
</div>
);
}
export default MembersTable;

View File

@@ -0,0 +1,143 @@
import { MemberStatus } from 'container/MembersSettings/utils';
import { render, screen, userEvent } from 'tests/test-utils';
import { ROLES } from 'types/roles';
import MembersTable, { MemberRow } from '../MembersTable';
const mockActiveMembers: MemberRow[] = [
{
id: 'user-1',
name: 'Alice Smith',
email: 'alice@signoz.io',
role: 'ADMIN' as ROLES,
status: MemberStatus.Active,
joinedOn: '1700000000000',
},
{
id: 'user-2',
name: 'Bob Jones',
email: 'bob@signoz.io',
role: 'VIEWER' as ROLES,
status: MemberStatus.Active,
joinedOn: null,
},
];
const mockInvitedMember: MemberRow = {
id: 'invite-abc',
name: '',
email: 'charlie@signoz.io',
role: 'EDITOR' as ROLES,
status: MemberStatus.Invited,
joinedOn: null,
token: 'tok-123',
};
const defaultProps = {
loading: false,
total: 2,
currentPage: 1,
pageSize: 20,
searchQuery: '',
onPageChange: jest.fn(),
onRowClick: jest.fn(),
};
describe('MembersTable', () => {
beforeEach(() => {
jest.clearAllMocks();
});
it('renders member rows with name, email, role badge, and ACTIVE status', () => {
render(<MembersTable {...defaultProps} data={mockActiveMembers} />);
expect(screen.getByText('Alice Smith')).toBeInTheDocument();
expect(screen.getByText('alice@signoz.io')).toBeInTheDocument();
expect(screen.getByText('Admin')).toBeInTheDocument();
expect(screen.getAllByText('ACTIVE')).toHaveLength(2);
});
it('renders INVITED badge for pending invite members', () => {
render(
<MembersTable
{...defaultProps}
data={[...mockActiveMembers, mockInvitedMember]}
total={3}
/>,
);
expect(screen.getByText('INVITED')).toBeInTheDocument();
expect(screen.getByText('charlie@signoz.io')).toBeInTheDocument();
expect(screen.getByText('Editor')).toBeInTheDocument();
});
it('calls onRowClick with the member data when a row is clicked', async () => {
const onRowClick = jest.fn() as jest.MockedFunction<
(member: MemberRow) => void
>;
const user = userEvent.setup({ pointerEventsCheck: 0 });
render(
<MembersTable
{...defaultProps}
data={mockActiveMembers}
onRowClick={onRowClick}
/>,
);
await user.click(screen.getByText('Alice Smith'));
expect(onRowClick).toHaveBeenCalledTimes(1);
expect(onRowClick).toHaveBeenCalledWith(
expect.objectContaining({ id: 'user-1', email: 'alice@signoz.io' }),
);
});
it('shows "No members found" empty state when no data and no search query', () => {
render(<MembersTable {...defaultProps} data={[]} total={0} searchQuery="" />);
expect(screen.getByText('No members found')).toBeInTheDocument();
});
it('shows "No results for X" when no data and a search query is set', () => {
render(
<MembersTable {...defaultProps} data={[]} total={0} searchQuery="unknown" />,
);
expect(screen.getByText(/No results for/i)).toBeInTheDocument();
expect(screen.getByText('unknown')).toBeInTheDocument();
});
it('hides pagination when total does not exceed pageSize', () => {
const { container } = render(
<MembersTable
{...defaultProps}
data={mockActiveMembers}
total={2}
pageSize={20}
/>,
);
expect(
container.querySelector('.members-table-pagination'),
).not.toBeInTheDocument();
});
it('shows pagination when total exceeds pageSize', () => {
const { container } = render(
<MembersTable
{...defaultProps}
data={mockActiveMembers}
total={25}
pageSize={20}
/>,
);
expect(
container.querySelector('.members-table-pagination'),
).toBeInTheDocument();
expect(
container.querySelector('.members-pagination-total'),
).toBeInTheDocument();
});
});

View File

@@ -1,4 +1,4 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`MessageTip custom action 1`] = `
.c0 {

View File

@@ -1,6 +1,6 @@
import { Color } from '@signozhq/design-tokens';
import { Tooltip } from 'antd';
import { DefaultOptionType } from 'antd/es/select';
import type { DefaultOptionType } from 'antd/es/select';
import { Info } from 'lucide-react';
import './MQCommon.styles.scss';

View File

@@ -1,4 +1,4 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`Not Found page test should render Not Found page without errors 1`] = `
<DocumentFragment>

View File

@@ -1,7 +1,10 @@
import { memo, useEffect, useState } from 'react';
import type {
TableColumnGroupType as ColumnGroupType,
TableColumnsType as ColumnsType,
TableColumnType as ColumnType,
} from 'antd';
import { Button, Dropdown, Flex, MenuProps, Switch } from 'antd';
import { ColumnGroupType, ColumnType } from 'antd/es/table';
import { ColumnsType } from 'antd/lib/table';
import logEvent from 'api/common/logEvent';
import LaunchChatSupport from 'components/LaunchChatSupport/LaunchChatSupport';
import { useSafeNavigate } from 'hooks/useSafeNavigate';

View File

@@ -9,13 +9,11 @@ import {
import ReactDragListView from 'react-drag-listview';
import { ResizeCallbackData } from 'react-resizable';
import { Table } from 'antd';
import { ColumnsType } from 'antd/lib/table';
import type { ColumnsType } from 'antd/lib/table';
import cx from 'classnames';
import { dragColumnParams } from 'hooks/useDragColumns/configs';
import { getColumnWidth, RowData } from 'lib/query/createTableColumnsFromQuery';
import { debounce, set } from 'lodash-es';
import { useDashboard } from 'providers/Dashboard/Dashboard';
import { Widgets } from 'types/api/dashboard/getAll';
import ResizableHeader from './ResizableHeader';
import { DragSpanStyle } from './styles';
@@ -26,31 +24,26 @@ function ResizeTable({
columns,
onDragColumn,
pagination,
widgetId,
shouldPersistColumnWidths = false,
columnWidths,
onColumnWidthsChange,
...restProps
}: ResizeTableProps): JSX.Element {
const [columnsData, setColumns] = useState<ColumnsType>([]);
const { setColumnWidths, selectedDashboard } = useDashboard();
const columnWidths = shouldPersistColumnWidths
? (selectedDashboard?.data?.widgets?.find(
(widget) => widget.id === widgetId,
) as Widgets)?.columnWidths
: undefined;
const onColumnWidthsChangeRef = useRef(onColumnWidthsChange);
const updateAllColumnWidths = useRef(
debounce((widthsConfig: Record<string, number>) => {
if (!widgetId || !shouldPersistColumnWidths) {
if (!onColumnWidthsChangeRef.current) {
return;
}
setColumnWidths?.((prev) => ({
...prev,
[widgetId]: widthsConfig,
}));
onColumnWidthsChangeRef.current(widthsConfig);
}, 1000),
).current;
useEffect(() => {
onColumnWidthsChangeRef.current = onColumnWidthsChange;
}, [onColumnWidthsChange]);
const handleResize = useCallback(
(index: number) => (
e: SyntheticEvent<Element>,
@@ -75,7 +68,7 @@ function ResizeTable({
...col,
...(onDragColumn && {
title: (
<DragSpanStyle className="dragHandler">
<DragSpanStyle className="dragHandler" data-testid="drag-column-title">
{col?.title?.toString() || ''}
</DragSpanStyle>
),
@@ -106,31 +99,31 @@ function ResizeTable({
}, [mergedColumns, pagination, restProps]);
useEffect(() => {
if (columns) {
// Apply stored column widths from widget configuration
const columnsWithStoredWidths = columns.map((col) => {
const dataIndex = (col as RowData).dataIndex as string;
if (dataIndex && columnWidths) {
const width = getColumnWidth(dataIndex, columnWidths);
if (width) {
return {
...col,
width, // Apply stored width
};
}
}
return col;
});
setColumns(columnsWithStoredWidths);
}
}, [columns, columnWidths]);
useEffect(() => {
if (!shouldPersistColumnWidths) {
if (!columns) {
return;
}
// Collect all column widths in a single object
const columnsWithStoredWidths = columns.map((col) => {
const dataIndex = (col as RowData).dataIndex as string;
if (dataIndex && columnWidths) {
const width = getColumnWidth(dataIndex, columnWidths);
if (width) {
return { ...col, width };
}
}
return col;
});
setColumns(columnsWithStoredWidths);
}, [columns, columnWidths]);
const lastReportedWidthsRef = useRef<Record<string, number>>({});
useEffect(() => {
if (!onColumnWidthsChange) {
return;
}
const newColumnWidths: Record<string, number> = {};
mergedColumns.forEach((col) => {
@@ -140,11 +133,20 @@ function ResizeTable({
}
});
// Only update if there are actual widths to set
if (Object.keys(newColumnWidths).length > 0) {
if (Object.keys(newColumnWidths).length === 0) {
return;
}
const last = lastReportedWidthsRef.current;
const hasChange =
Object.keys(newColumnWidths).length !== Object.keys(last).length ||
Object.keys(newColumnWidths).some((k) => newColumnWidths[k] !== last[k]);
if (hasChange) {
lastReportedWidthsRef.current = newColumnWidths;
updateAllColumnWidths(newColumnWidths);
}
}, [mergedColumns, updateAllColumnWidths, shouldPersistColumnWidths]);
}, [mergedColumns, updateAllColumnWidths, onColumnWidthsChange]);
return onDragColumn ? (
<ReactDragListView.DragColumn {...dragColumnParams} onDragEnd={onDragColumn}>

View File

@@ -0,0 +1,244 @@
import { act } from '@testing-library/react';
import { render, screen, userEvent } from 'tests/test-utils';
import ResizeTable from '../ResizeTable';
jest.mock('react-resizable', () => ({
Resizable: ({
children,
onResize,
width,
}: {
children: React.ReactNode;
onResize: (
e: React.SyntheticEvent,
data: { size: { width: number } },
) => void;
width: number;
}): JSX.Element => (
<div>
{children}
<button
data-testid="resize-trigger"
type="button"
onClick={(e): void => onResize(e, { size: { width: width + 50 } })}
/>
</div>
),
}));
// Make debounce synchronous so onColumnWidthsChange fires immediately
jest.mock('lodash-es', () => ({
...jest.requireActual('lodash-es'),
debounce: (fn: (...args: any[]) => any): ((...args: any[]) => any) => fn,
}));
const baseColumns = [
{ dataIndex: 'name', title: 'Name', width: 100 },
{ dataIndex: 'value', title: 'Value', width: 100 },
];
const baseDataSource = [
{ key: '1', name: 'Alice', value: 42 },
{ key: '2', name: 'Bob', value: 99 },
];
describe('ResizeTable', () => {
it('renders column headers and data rows', () => {
render(
<ResizeTable
columns={baseColumns}
dataSource={baseDataSource}
rowKey="key"
/>,
);
expect(screen.getByText('Name')).toBeInTheDocument();
expect(screen.getByText('Value')).toBeInTheDocument();
expect(screen.getByText('Alice')).toBeInTheDocument();
expect(screen.getByText('Bob')).toBeInTheDocument();
});
it('overrides column widths from columnWidths prop and reports them via onColumnWidthsChange', () => {
const onColumnWidthsChange = jest.fn();
act(() => {
render(
<ResizeTable
columns={baseColumns}
dataSource={baseDataSource}
rowKey="key"
columnWidths={{ name: 250, value: 180 }}
onColumnWidthsChange={onColumnWidthsChange}
/>,
);
});
expect(onColumnWidthsChange).toHaveBeenCalledWith(
expect.objectContaining({ name: 250, value: 180 }),
);
});
it('reports original column widths via onColumnWidthsChange when columnWidths prop is not provided', () => {
const onColumnWidthsChange = jest.fn();
act(() => {
render(
<ResizeTable
columns={baseColumns}
dataSource={baseDataSource}
rowKey="key"
onColumnWidthsChange={onColumnWidthsChange}
/>,
);
});
expect(onColumnWidthsChange).toHaveBeenCalledWith(
expect.objectContaining({ name: 100, value: 100 }),
);
});
it('does not call onColumnWidthsChange when it is not provided', () => {
// Should render without errors and without attempting to call an undefined callback
expect(() => {
render(
<ResizeTable
columns={baseColumns}
dataSource={baseDataSource}
rowKey="key"
/>,
);
}).not.toThrow();
});
it('only overrides the column that has a stored width, leaving others at their original width', () => {
const onColumnWidthsChange = jest.fn();
act(() => {
render(
<ResizeTable
columns={baseColumns}
dataSource={baseDataSource}
rowKey="key"
columnWidths={{ name: 250 }}
onColumnWidthsChange={onColumnWidthsChange}
/>,
);
});
expect(onColumnWidthsChange).toHaveBeenCalledWith(
expect.objectContaining({ name: 250, value: 100 }),
);
});
it('does not call onColumnWidthsChange on re-render when widths have not changed', () => {
const onColumnWidthsChange = jest.fn();
const { rerender } = render(
<ResizeTable
columns={baseColumns}
dataSource={baseDataSource}
rowKey="key"
onColumnWidthsChange={onColumnWidthsChange}
/>,
);
expect(onColumnWidthsChange).toHaveBeenCalledTimes(1);
onColumnWidthsChange.mockClear();
rerender(
<ResizeTable
columns={baseColumns}
dataSource={baseDataSource}
rowKey="key"
onColumnWidthsChange={onColumnWidthsChange}
/>,
);
expect(onColumnWidthsChange).not.toHaveBeenCalled();
});
it('does not call onColumnWidthsChange when no column has a defined width', () => {
const onColumnWidthsChange = jest.fn();
render(
<ResizeTable
columns={[
{ dataIndex: 'name', title: 'Name' },
{ dataIndex: 'value', title: 'Value' },
]}
dataSource={baseDataSource}
rowKey="key"
onColumnWidthsChange={onColumnWidthsChange}
/>,
);
expect(onColumnWidthsChange).not.toHaveBeenCalled();
});
it('calls onColumnWidthsChange with the new width after a column is resized', async () => {
const user = userEvent.setup();
const onColumnWidthsChange = jest.fn();
render(
<ResizeTable
columns={baseColumns}
dataSource={baseDataSource}
rowKey="key"
onColumnWidthsChange={onColumnWidthsChange}
/>,
);
onColumnWidthsChange.mockClear();
// Click the first column's resize trigger — mock adds 50px to the current width (100 → 150)
const [firstResizeTrigger] = screen.getAllByTestId('resize-trigger');
await user.click(firstResizeTrigger);
expect(onColumnWidthsChange).toHaveBeenCalledWith(
expect.objectContaining({ name: 150, value: 100 }),
);
});
it('does not affect other columns when only one column is resized', async () => {
const user = userEvent.setup();
const onColumnWidthsChange = jest.fn();
render(
<ResizeTable
columns={baseColumns}
dataSource={baseDataSource}
rowKey="key"
onColumnWidthsChange={onColumnWidthsChange}
/>,
);
onColumnWidthsChange.mockClear();
// Resize only the second column (value: 100 → 150), name should stay at 100
const resizeTriggers = screen.getAllByTestId('resize-trigger');
await user.click(resizeTriggers[1]);
expect(onColumnWidthsChange).toHaveBeenCalledWith(
expect.objectContaining({ name: 100, value: 150 }),
);
});
it('wraps column titles in drag handler spans when onDragColumn is provided', () => {
const onDragColumn = jest.fn();
render(
<ResizeTable
columns={baseColumns}
dataSource={baseDataSource}
rowKey="key"
onDragColumn={onDragColumn}
/>,
);
const dragTitles = screen.getAllByTestId('drag-column-title');
expect(dragTitles).toHaveLength(baseColumns.length);
expect(dragTitles[0]).toHaveTextContent('Name');
expect(dragTitles[1]).toHaveTextContent('Value');
});
});

View File

@@ -1,15 +1,30 @@
import { TableProps } from 'antd';
import { ColumnsType } from 'antd/es/table';
import { PaginationProps } from 'antd/lib';
import { ColumnGroupType, ColumnType } from 'antd/lib/table';
import type { ColumnsType } from 'antd/es/table';
import type { PaginationProps } from 'antd/lib';
import type { ColumnGroupType, ColumnType } from 'antd/lib/table';
import { LaunchChatSupportProps } from 'components/LaunchChatSupport/LaunchChatSupport';
import { TableDataSource } from './contants';
type ColumnWidths = Record<string, number>;
export interface ResizeTableProps extends TableProps<any> {
onDragColumn?: (fromIndex: number, toIndex: number) => void;
widgetId?: string;
shouldPersistColumnWidths?: boolean;
/**
* Pre-resolved column widths for this table, keyed by column dataIndex.
* Use this to apply persisted widths on mount (e.g. from widget.columnWidths).
* Do NOT pass a value that updates reactively on every resize — that creates a
* feedback loop. Pass only stable / persisted values.
*/
columnWidths?: ColumnWidths;
/**
* Called (debounced) whenever the user finishes resizing a column.
* The widths object contains all current column widths keyed by dataIndex.
* Intended for persisting widths to an external store (e.g. dashboard context
* staging buffer). The caller owns the storage; ResizeTable does not read back
* whatever is written here.
*/
onColumnWidthsChange?: (widths: ColumnWidths) => void;
}
export interface DynamicColumnTableProps extends TableProps<any> {
tablesource: typeof TableDataSource[keyof typeof TableDataSource];

View File

@@ -1,5 +1,4 @@
import { Radio } from 'antd';
import { RadioChangeEvent } from 'antd/es/radio';
import { Radio, RadioChangeEvent } from 'antd';
import './SignozRadioGroup.styles.scss';

View File

@@ -13,8 +13,8 @@ import {
TabsProps,
Typography,
} from 'antd';
import { TextProps } from 'antd/lib/typography/Text';
import { TitleProps } from 'antd/lib/typography/Title';
import type { TextProps } from 'antd/lib/typography/Text';
import type { TitleProps } from 'antd/lib/typography/Title';
import styled, { FlattenSimpleInterpolation } from 'styled-components';
import { IStyledClass } from './types';

View File

@@ -1,5 +1,7 @@
import { ColumnType } from 'antd/es/table';
import { ColumnsType } from 'antd/lib/table';
import type {
TableColumnsType as ColumnsType,
TableColumnType as ColumnType,
} from 'antd';
export const generatorResizeTableColumns = <T>({
baseColumnOptions,

View File

@@ -1,6 +1,6 @@
import { useState } from 'react';
import { Radio } from 'antd';
import { RadioChangeEvent } from 'antd/lib';
import type { RadioChangeEvent } from 'antd/lib';
import { History, Table } from 'lucide-react';
import { ALERT_TABS } from '../constants';

View File

@@ -1,7 +1,6 @@
import React, { Dispatch, SetStateAction, useState } from 'react';
import { PlusOutlined } from '@ant-design/icons';
import { Button } from 'antd';
import { Tag } from 'antd/lib';
import { Button, Tag } from 'antd';
import Input from 'components/Input';
import { Check, X } from 'lucide-react';

View File

@@ -1,7 +1,7 @@
import { useMemo } from 'react';
import { WarningFilled } from '@ant-design/icons';
import { Select, Tooltip } from 'antd';
import { DefaultOptionType } from 'antd/es/select';
import type { DefaultOptionType } from 'antd/es/select';
import classNames from 'classnames';
import { UniversalYAxisUnitMappings } from './constants';

View File

@@ -49,7 +49,6 @@ export const REACT_QUERY_KEY = {
// Metrics Explorer Query Keys
GET_METRICS_LIST: 'GET_METRICS_LIST',
GET_METRICS_TREE_MAP: 'GET_METRICS_TREE_MAP',
GET_METRICS_LIST_FILTER_KEYS: 'GET_METRICS_LIST_FILTER_KEYS',
GET_METRICS_LIST_FILTER_VALUES: 'GET_METRICS_LIST_FILTER_VALUES',
GET_METRIC_DETAILS: 'GET_METRIC_DETAILS',

View File

@@ -38,7 +38,6 @@ const ROUTES = {
SETTINGS: '/settings',
MY_SETTINGS: '/settings/my-settings',
ORG_SETTINGS: '/settings/org-settings',
CUSTOM_DOMAIN_SETTINGS: '/settings/custom-domain-settings',
API_KEYS: '/settings/api-keys',
INGESTION_SETTINGS: '/settings/ingestion-settings',
SOMETHING_WENT_WRONG: '/something-went-wrong',
@@ -56,6 +55,8 @@ const ROUTES = {
TRACE_EXPLORER: '/trace-explorer',
BILLING: '/settings/billing',
ROLES_SETTINGS: '/settings/roles',
ROLE_DETAILS: '/settings/roles/:roleId',
MEMBERS_SETTINGS: '/settings/members',
SUPPORT: '/support',
LOGS_SAVE_VIEWS: '/logs/saved-views',
TRACES_SAVE_VIEWS: '/traces/saved-views',

View File

@@ -8,6 +8,7 @@ import {
Button,
Col,
Collapse,
CollapseProps,
Flex,
Form,
Input,
@@ -20,8 +21,7 @@ import {
Tooltip,
Typography,
} from 'antd';
import { NotificationInstance } from 'antd/es/notification/interface';
import { CollapseProps } from 'antd/lib';
import type { NotificationInstance } from 'antd/es/notification/interface';
import createAPIKeyApi from 'api/v1/pats/create';
import deleteAPIKeyApi from 'api/v1/pats/delete';
import updateAPIKeyApi from 'api/v1/pats/update';

View File

@@ -1,7 +1,6 @@
import { HTMLAttributes } from 'react';
import { Color } from '@signozhq/design-tokens';
import { Progress, Table } from 'antd';
import { ColumnsType } from 'antd/es/table';
import { Progress, Table, TableColumnsType as ColumnsType } from 'antd';
import logEvent from 'api/common/logEvent';
import { ConditionalAlertPopover } from 'container/AlertHistory/AlertPopover/AlertPopover';
import AlertLabels from 'pages/AlertDetails/AlertHeader/AlertLabels/AlertLabels';

View File

@@ -1,8 +1,7 @@
import { useMemo } from 'react';
import { EllipsisOutlined } from '@ant-design/icons';
import { Color } from '@signozhq/design-tokens';
import { Button } from 'antd';
import { ColumnsType } from 'antd/es/table';
import { Button, TableColumnsType as ColumnsType } from 'antd';
import ClientSideQBSearch, {
AttributeKey,
} from 'components/ClientSideQBSearch/ClientSideQBSearch';

View File

@@ -2,7 +2,7 @@ import { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { generatePath } from 'react-router-dom';
import { Button } from 'antd';
import { ColumnsType } from 'antd/lib/table';
import type { ColumnsType } from 'antd/lib/table';
import { ResizeTable } from 'components/ResizeTable';
import ROUTES from 'constants/routes';
import useComponentPermission from 'hooks/useComponentPermission';

View File

@@ -2,7 +2,7 @@ import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useQueryClient } from 'react-query';
import { Button } from 'antd';
import { NotificationInstance } from 'antd/es/notification/interface';
import type { NotificationInstance } from 'antd/es/notification/interface';
import deleteChannel from 'api/channels/delete';
import APIError from 'types/api/error';

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