Compare commits

..

94 Commits

Author SHA1 Message Date
Vinícius Lourenço
50db520ae6 refactor(alert-history-alert-popover): migrate to css modules 2026-06-08 13:41:40 -03:00
Vinícius Lourenço
cc276fa849 refactor(alert-history-top-stats-card): migrate to css modules 2026-06-08 13:41:40 -03:00
Vinícius Lourenço
3e594fc95a refactor(alert-history-top-contributors-card): migrate to css modules 2026-06-08 13:41:40 -03:00
Vinícius Lourenço
75de3327d1 refactor(alert-history-statistics): migrate to css modules 2026-06-08 13:41:40 -03:00
Vinícius Lourenço
56100dbae5 refactor(alert-history-timeline-graph): migrate to css modules 2026-06-08 13:41:39 -03:00
Vinícius Lourenço
5717e21d25 refactor(alert-history-timeline-table): migrate to css modules 2026-06-08 13:41:39 -03:00
Vinícius Lourenço
186531f00c refactor(alert-history-timeline-tabs-and-filters): migrate to css modules 2026-06-08 13:41:39 -03:00
Vinícius Lourenço
96c0508385 refactor(alert-history-timeline): migrate to css modules 2026-06-08 13:41:39 -03:00
Vinícius Lourenço
4211f8564e refactor(alert-history): migrate to css modules 2026-06-08 13:41:38 -03:00
Vinícius Lourenço
acc90d6058 refactor(anomaly-alert-evaluation-view): migrate to css modules 2026-06-08 13:41:38 -03:00
Vinícius Lourenço
870ba55262 refactor(form-alert-rules-chart-preview): migrate to css modules 2026-06-08 13:41:38 -03:00
Vinícius Lourenço
c8e7456dc3 refactor(form-alert-rules): migrate to css modules 2026-06-08 13:41:38 -03:00
Vinícius Lourenço
2fadfb4461 refactor(form-alert-rules-query-section): migrate to css modules 2026-06-08 13:41:37 -03:00
Vinícius Lourenço
8b9bf09be0 refactor(form-alert-rules-options): migrate to css modules 2026-06-08 13:41:37 -03:00
Vinícius Lourenço
8b1793b6bd refactor(create-alert-rule): migrate to css modules 2026-06-08 13:41:37 -03:00
Vinícius Lourenço
61f9d1d206 refactor(create-alert-v2-condition): migrate to css modules 2026-06-08 13:41:37 -03:00
Vinícius Lourenço
e92e533760 refactor(create-alert-v2-header): migrate to css modules 2026-06-08 13:41:36 -03:00
Vinícius Lourenço
4ee35dd9c5 refactor(create-alert-v2-advanced-option-item): migrate to css modules 2026-06-08 13:41:36 -03:00
Vinícius Lourenço
08f5025ebe refactor(create-alert-v2-time-input): migrate to css modules 2026-06-08 13:41:36 -03:00
Vinícius Lourenço
784c56e11f refactor(create-alert-v2-evaluation-settings): migrate to css modules 2026-06-08 13:41:36 -03:00
Vinícius Lourenço
30c4bac035 refactor(create-alert-v2-evaluation-cadence): migrate to css modules 2026-06-08 13:41:35 -03:00
Vinícius Lourenço
ce80d4fe5c refactor(create-alert-v2-footer): migrate to css modules 2026-06-08 13:41:35 -03:00
Vinícius Lourenço
b46f02a0fb refactor(create-alert-v2-notifications-settings): migrate to css modules 2026-06-08 13:41:35 -03:00
Vinícius Lourenço
d57c34240b refactor(create-alert-v2-chart-preview): migrate to css modules 2026-06-08 13:41:35 -03:00
Vinícius Lourenço
485d394e9c refactor(create-alert-v2-query-section): migrate to css modules 2026-06-08 13:41:34 -03:00
Vinícius Lourenço
ccbd556731 refactor(create-alert-v2-stepper): migrate to css modules 2026-06-08 13:41:34 -03:00
Vinícius Lourenço
ba6dc30a55 refactor(create-alert-v2): migrate to css modules 2026-06-08 13:41:34 -03:00
Vinícius Lourenço
5d07766cf1 refactor(edit-alert-v2): migrate to css modules 2026-06-08 13:41:33 -03:00
Vinícius Lourenço
3d3a7e2add refactor(alert-header-rename-modal): migrate to css modules 2026-06-08 13:41:33 -03:00
Vinícius Lourenço
fd70bca614 refactor(alert-header-action-buttons): migrate to css modules 2026-06-08 13:41:33 -03:00
Vinícius Lourenço
15f48c82eb refactor(alert-header-labels): migrate to css modules 2026-06-08 13:41:33 -03:00
Vinícius Lourenço
0e5b883e23 refactor(alert-header-severity): migrate to css modules 2026-06-08 13:41:32 -03:00
Vinícius Lourenço
bffac7ccdc refactor(alert-header-state): migrate to css modules 2026-06-08 13:41:32 -03:00
Vinícius Lourenço
6b188255b9 refactor(alert-header): migrate to css modules 2026-06-08 13:41:32 -03:00
Vinícius Lourenço
fccfeb22ed refactor(alert-not-found): migrate to css modules 2026-06-08 13:41:31 -03:00
Vinícius Lourenço
5be0ffa0ef refactor(alert-details): migrate to css modules 2026-06-08 13:41:31 -03:00
Vinícius Lourenço
37972262ba refactor(alert-list): migrate to css modules 2026-06-08 13:41:31 -03:00
Vinícius Lourenço
6920fc99b7 refactor(edit-rules): migrate to css modules 2026-06-08 13:41:31 -03:00
Vikrant Gupta
7844fc1fe1 fix(authn): include base path in SSO callback and error-redirect URLs (#11588)
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
* fix(authn): include base path in SSO callback and error-redirect URLs

The SAML ACS URL and the OIDC/Google redirect URLs were built from the
site URL host plus a hardcoded path (e.g. /api/v1/complete/saml), dropping
the base path. When SigNoz is served under a sub-path (global.external_url
with a path, e.g. https://example.com/signoz), the API is served at
<prefix>/api/v1/complete/<provider>, so the identity provider was told to
call back to a path without the prefix and hit a 404.

Thread global.Config into the SAML/OIDC/Google callback providers and the
session handler, and prepend global.Config.ExternalPath() to the callback
paths and the SSO error redirect to /login. Root deployments are
unchanged since ExternalPath() returns "" without a configured sub-path.

* fix(authn): run callbackauthn suite with base path

* refactor(tests): self-contained base-path fixture for callbackauthn

Move the base-path setup out of the shared create_signoz factory and into
a package-scoped signoz fixture in the callbackauthn suite's own conftest
(same pattern as rootuser/conftest.py). When --base-path is set the fixture
appends SIGNOZ_GLOBAL_EXTERNAL__URL and the url-config prefix locally;
without it it behaves exactly like the global fixture. The shared factory
and docker config are left untouched.

* test(authn): add base-path SSO integration suite

Adds a dedicated `basepath` integration suite that serves SigNoz under a
hardcoded /signoz prefix (SIGNOZ_GLOBAL_EXTERNAL__URL) and exercises the SAML
and OIDC happy-path logins end-to-end. Every SigNoz API call is issued under
the prefix and the IdP callback (ACS / redirect URI) is registered with the
prefix, so the flow only passes when the backend builds prefixed callback URLs.

The shared TestContainerUrlConfig and create_signoz factory are left untouched.
The suite's conftest shadows the same-named auth fixtures (create_user_admin,
get_token, get_session_context, apply_license) with base-path-aware variants and
reuses the Keycloak/browser fixtures, which are not under the base path.

Google SSO is not covered: it requires the real accounts.google.com issuer and
a real Google login, so it cannot run against the local Keycloak IdP; it shares
the identical path.Join(ExternalPath, redirectPath) callback logic that SAML
and OIDC validate.

* revert: drop in-place base-path wiring from integration harness

Removes the --base-path flag, TestContainerUrlConfig.base_path, the idp.py and
02_saml.py .get() changes, and the callbackauthn base-path conftest fixture.
Base-path SSO is now covered by the dedicated `basepath` suite, so the shared
harness (TestContainerUrlConfig, create_signoz, callbackauthn) is back to its
original root-only form.

* refactor(test): remove apply_license fixture

* refactor(test): extract base-path-aware auth factories

Extract the session-context / token / token-pair / admin-registration logic
in fixtures/auth.py into reusable factory functions that take an optional
base_path (token_getter, session_context_getter, tokens_getter, register_admin),
with the fixtures delegating to them. Default base_path="" is byte-identical for
existing callers.

The basepath suite's conftest now reuses these factories with the /signoz prefix
as thin one-line fixture overrides instead of duplicating the request logic.

* refactor(test): give base-path admin registration a distinct cache key

register_admin takes an optional cache_key (default "create_user_admin"); the
basepath suite passes a distinct key so that under --reuse the admin marker
cached against the signoz-base-path container is not restored for (or from)
other suites' default signoz instance.
2026-06-08 14:15:04 +00:00
Vinicius Lourenço
e02da843f2 fix(infra-monitoring-charts): fixes for hosts/deployments/jobs/namespaces (#11599)
* fix(infra-monitoring-jobs): title of the chart misleading

Ref: https://github.com/SigNoz/engineering-pod/issues/5211#issuecomment-4619888389

* fix(infra-monitoring-namespaces): wrong limit & using wrong filter for statefulsets

Ref: https://github.com/SigNoz/engineering-pod/issues/5211#issuecomment-4619023361

* feat(infra-monitoring-hosts): add new chart based on operations time

Ref: https://github.com/SigNoz/engineering-pod/issues/5211#issue-4578950064

* feat(infra-monitoring-hosts): add group by on chart for system disk io

Ref: https://github.com/SigNoz/engineering-pod/issues/5211#issuecomment-4611797867

* fix(infra-monitoring-hosts): chart for disk operations using the wrong metric

Ref: https://github.com/SigNoz/engineering-pod/issues/5211#issue-4578950064

* fix(infra-monitoring-deployments): little typo in the chart name

* fix(volumes): ensure the name/type are standard based on the metric type
2026-06-08 11:45:27 +00:00
Ashwin Bhatkal
0948a983c3 feat(dashboards): V2 dashboard — settings, configure drawer & inline title (#11581)
* refactor(dashboard-v2): name props interfaces by component (Props → <Component>Props)

* feat(dashboard-v2): shared header chrome + confirm-delete dialog

* feat(dashboard-v2): dashboard settings drawer — general, variables/publish tabs

* feat(dashboard-v2): sections & panels — empty states, menus, theming, review fixes

* feat(dashboard-v2): dashboard header — inline-editable title, actions menu

* feat(dashboard-v2): container wiring + new-panel flow
2026-06-08 08:13:06 +00:00
primus-bot[bot]
4f7ebd1ff1 chore(release): bump to v0.127.1 (#11606)
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
Co-authored-by: primus-bot[bot] <171087277+primus-bot[bot]@users.noreply.github.com>
2026-06-07 16:25:54 +00:00
Aditya Singh
15fbe4f788 fix(frontend): dropdown menu rendering behind antd drawer/modal (z-index regression) (#11604)
* feat: update dropdown menu content z-index to match antd base z-index

* feat: align dropdown to start
2026-06-07 14:26:08 +00:00
Srikanth Chekuri
7da7fda283 chore(alertmanager): support custom receiver configs (#11473)
Some checks failed
build-staging / js-build (push) Has been cancelled
build-staging / prepare (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(alertmanager): support custom receiver configs

* chore: update

* chore: update types

* chore: regenerate

* chore: copy type and function
2026-06-06 17:34:35 +00:00
swapnil-signoz
9093b4c442 fix: service integration tests fix (#11598)
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-06-05 18:17:36 +00:00
Naman Verma
97191eb7a2 chore: replace perses/perses with perses/spec and introduce kind enum values (#11559)
* chore: replace perses/perses with perses/spec and introduce kind enum values

* fix: enforce query kind values

* chore: generate frontend api spec

* test: fix patch unit tests using correct query kind value

* feat: add validation to query builder request type
2026-06-05 17:00:53 +00:00
SagarRajput-7
9222845ce8 feat(billing): migrate BillingUsageGraph from uPlotLib to uPlotV2 and added stacking (#11579)
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(billing): migrate BillingUsageGraph from uPlotLib to uPlotV2 and added stacking

* feat(billing): revamp billing page UI to match Settings Revamp designs

* feat(billing): refactor and feedback fixes

* feat(billing): test case fix and css refactor

* feat(billing): css fix

* feat(billing): migrate BillingContainer and BillingUsageGraph to CSS modules

* feat(billing): feedback fixes
2026-06-05 13:22:26 +00:00
swapnil-signoz
b3b245ebc5 feat: adding get cloud integration service for account handler (#11497)
* feat: adding get cloud integration service for account handler

* feat: adding integration test for new API endpoint

* ci: fix py fmt lint

* refactor: cloudfront integration service (#11570)

Co-authored-by: Gaurav Tewari <tewarig@users.noreply.github.com>

* fix: update tests

---------

Co-authored-by: Gaurav Tewari <gauravtewari111@gmail.com>
Co-authored-by: Gaurav Tewari <tewarig@users.noreply.github.com>
2026-06-05 12:42:18 +00:00
Ashwin Bhatkal
9c9016d49e feat(dashboards): V2 dashboard — sections, drag-and-drop & panel management (#11544)
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(dashboard-v2): session store — edit-context + persisted collapse

* feat(dashboard-v2): patch-op builders (RFC-6902) for layouts & panels

* feat(dashboard-v2): editable dashboard shell + within-section panel geometry

* feat(dashboard-v2): section layout — reorder & collapse

* feat(dashboard-v2): section lifecycle — add, rename, delete, migrate

* feat(dashboard-v2): panel management — add, delete, move
2026-06-05 06:33:48 +00:00
Aditya Singh
81eadac3a8 feat(trace-details): lazy aggregations + waterfall v4 cutover (#11556)
Some checks failed
build-staging / js-build (push) Has been cancelled
build-staging / prepare (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: wire available colors on spans instead of aggregations from waterfall api

* feat: integrate aggregation api

* feat: integrate v4 waterfall

* feat: dropdown style fix

* feat: move to open api spec

* feat: move to open api spec

* feat: minor changes
2026-06-04 20:09:44 +00:00
Aditya Singh
eec1c45e3f fix(trace-flamegraph): prevent layout hang on very wide traces (#11578)
* feat: algo change

* feat: update test
2026-06-04 20:02:13 +00:00
swapnil-signoz
ce26458b9f feat(cloud-integrations): adding endpoint services metadata for account (#11563)
* feat: adding endpoint services metadata for account

* fix: adding missing response writer in error

* refactor(cloud-integrations): use account-scoped  ListAccountServices endpoint when an account is connected (#11569)

* fix: frontend changes for issue 4616

* fix: update frontend changes

---------

Co-authored-by: Gaurav Tewari <tewarig@users.noreply.github.com>

---------

Co-authored-by: Gaurav Tewari <gauravtewari111@gmail.com>
Co-authored-by: Gaurav Tewari <tewarig@users.noreply.github.com>
2026-06-04 18:07:01 +00:00
swapnil-signoz
7932917f5f refactor: service response changes and dashboardID handling (#11485)
* refactor: service response changes and dashboardID handling

* refactor: removing unused enrichDashboardIDs func

* revert: restore frontend files to main state

* refactor: updating response

* refactor: updating nullable tag

* chore: adding required tags

* chore: adding required tags for ServiceDashboard struct

* refactor(integrations): align service-details UI with new cloud-integration API + disabled-dashboard state (#11547)

* refactor: cloud integration service

* fix: update schemas

* fix: update schema

* fix: minor fixes

* refactor: review comments

---------

Co-authored-by: Gaurav Tewari <tewarig@users.noreply.github.com>

---------

Co-authored-by: Gaurav Tewari <gauravtewari111@gmail.com>
Co-authored-by: Gaurav Tewari <tewarig@users.noreply.github.com>
2026-06-04 17:32:56 +00:00
Vinicius Lourenço
4cf8125372 refactor(sentry-config): disable tracing & ensure correct environment (#11552)
* refactor(sentry-config): disable tracing & ensure correct environment

* refactor(sentry-config): derive environment from build-time env var

Replace the runtime hostname-based getCurrentEnvironment() with a
build-time process.env.ENVIRONMENT value. The environment is injected
once at build time via VITE_ENVIRONMENT, wired through the vite define
block, and consumed directly by Sentry.init.

Staging builds bake in 'staging' and enterprise builds bake in
'production'.

* refactor(sentry-config): keep browserTracingIntegration for transaction tag

Tracing stays disabled (tracesSampleRate: 0), but the integration is
retained so the transaction tag remains available for routing.
Ref: https://github.com/SigNoz/platform-pod/issues/2393#issuecomment-4603658055

* feat(sentry-config): set Sentry release from VITE_VERSION

Set release in Sentry.init from process.env.VERSION (VITE_VERSION, default 'dev') and pin the @sentry/vite-plugin upload release to the same value so sourcemaps resolve. Plumb VITE_VERSION through build-enterprise & build-staging (make info-version) and gor-signoz (tag).

* fix(sentry-config): align Sentry.init indentation and set VITE_ENVIRONMENT for gor-signoz

* fix(sentry-config): require VITE_VERSION for sourcemap upload

Refuse to register the Sentry vite plugin without an explicit VITE_VERSION, and drop the 'dev' fallback for both the plugin release name and process.env.VERSION so a sourcemap upload can never happen without a matching release.

---------

Co-authored-by: grandwizard28 <vibhupandey28@gmail.com>
2026-06-04 15:30:44 +00:00
Vinicius Lourenço
959e32b6f3 chore(codeowners): move openapi schema generation ownership to backend (#11585) 2026-06-04 15:00:10 +00:00
SagarRajput-7
d3304af0ed chore: removed workspace suspended page's text for data retention (#11501)
* feat: removed workspace locked and suspended page's text for data retention

* chore: reverted changes to workspacelocked file
2026-06-04 14:53:10 +00:00
Naman Verma
df7ef3cdb5 feat: v2 dashboard update, lock, unlock, and patch API (#11481)
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: follow proper unmarshal json method structure

* feat: v2 create dashboard API

* fix: only return name of a tag in dashboard response

* fix: use existing tag's casing if new tag is a prefix of an existing tag

* fix: go lint fix

* fix: more dashboard request validations

* chore: separate method for validation

* fix: module should also validate postable dashboard

* test: integration tests for create API

* test: integration test fixes

* chore: use existing mapper

* fix: remove extra spec from builder query marshalling

* fix: merge conflicts

* feat: v2 dashboard GET API

* feat: v2 dashboard update API

* fix: add allowed values in err messages

* fix: remove extra (un)marshal cycle

* fix: return 500 err if spec is nil for composite kind w/ code comment

* fix: no need for copying textboxvariablespec

* fix: wrap errors

* chore: no v2 subpackage

* fix: no v2 package and its consequences

* fix: no v2 package and its consequences

* Merge branch 'nv/v2-dashboard-create' into nv/v2-dashboard-get

* fix: merge fixes

* fix: query-less panels not allowed

* feat: consolidate tag module and tagtypes changes from downstream branches

* chore: update api specs

* chore: update api specs

* fix: allow only 1 query in a panel

* test: unit test fixes

* feat: method to fetch tags for multiple entries at once

* test: fix mock interface in test

* feat: move tags to key:value pairs model

* feat: entity type column in tags

* fix: pass entity type in create many

* feat: reserved DSL key validation for tags

* feat: new module for tags

* chore: merge conflicts error fixing pt 1

* fix: lint fix regarding nil, nil return in test file

* chore: change where tag module is instantiated

* fix: add back api endpoint

* chore: generate api spec

* fix: remove soft delete references

* chore: embed StorableDashboard into joinedRow in store method

* fix: extend bun in joinedRow

* fix: compile error fix

* fix: remove soft delete references

* feat: method to build postable tags from tags

* fix: diff error codes for invalid keys and values

* fix: correct pk in bun model for tag relations

* fix: created and updated by schema

* fix: use coretypes.Kind instead of defining entity type

* fix: singular table name

* chore: remove org ID from tag relation

* feat: foreign key on tag id

* feat: add SyncTags method that covers creation and linking

* fix: remove entity type definition

* fix: fix build errors in dashboard module

* chore: bump migration number

* chore: change entity id to resource id

* fix: add org id filter in all list and delete queries

* fix: remove user auditable

* fix: add ID in tag relation

* fix: fix build error

* fix: fix build error

* chore: bump migration number

* fix: add len check on tags keys and values

* fix: add regex for tags

* chore: remove methods that shouldn't be exposed

* fix: use sync tags in create api

* feat: functional unique index in sql schema

* fix: only ascii in regex

* fix: use sync tags method in update

* chore: rename create method to createOrGet

* chore: use tagtypestest package for mock store

* chore: combine functional unique index with unique index

* chore: move tag resolution to module

* test: add unit tests for new idx type

* chore: comment out tags unique index for now

* chore: add a todo comment

* chore: comment out unique index test

* feat: add created at to tag relations

* chore: comment out unique index test

* chore: bump migration number

* chore: remove uploaded grafana flag from metadata

* Merge branch 'main' into nv/v2-dashboard-create

* chore: revert idx generation to resolve conflicts

* fix: use store.RunInTx instead of taking in sqlstore

* fix: use binding package to get request

* chore: move NewDashboardV2 to NewDashboardV2WithoutTags

* chore: rename module to m

* fix: add ctx needed in sqlstore

* fix: remove sqlstore passage in ee pkg

* chore: change dashboardData to dashboardSpec

* feat: follow the metadata+spec key structure

* feat: follow the metadata+spec key structure in open api spec

* feat: v2 dashboard GET API (#11136)

* feat: v2 dashboard GET API

* Merge branch 'nv/v2-dashboard-create' into nv/v2-dashboard-get

* chore: update api specs

* fix: remove soft delete references

* chore: embed StorableDashboard into joinedRow in store method

* fix: fix build error

* chore: revert all frontend changes

* fix: remove public dashboard from get v2 call

* chore: revert all frontend changes

* fix: fix build errors post merge conflict resolution

* feat: lock, unlock, create public, update public v2 dashboard APIs (#11167)

* feat: lock, unlock, create public, update public v2 dashboard APIs

* chore: update api specs

* fix: use new pattern of checking for admin permission

* fix: remove soft delete reference

* chore: revert all frontend changes

* fix: fix build errors and remove v2 create/update public apis

* chore: use v1 methods wherever possible

* fix: use update v2 store method

* chore: update frontend schema

* chore: update frontend schema

* chore: generate api specs

* chore: generate api specs

* feat: patch dashboard api (#11182)

* feat: lock, unlock, create public, update public v2 dashboard APIs

* feat: delete dashboard v2 API and hard delete cron job

* feat: patch dashboard api

* chore: update api specs

* chore: update api specs

* chore: update api specs

* chore: remove delete related work

* fix: add examples of structs for value param in param description

* test: unit test fixes

* fix: use new pattern of checking for admin permission

* fix: remove soft delete reference

* test: key value tags in test

* fix: build error in patch module method

* fix: build error in Apply method

* fix: use sync tags method in update

* fix: fix build errors

* fix: fix all patch application tests

* chore: add more mapper methods

* fix: add source for v2 dashboards

* chore: incorporate source

* chore: incorporate source in api spec

* chore: incorporate source

* fix: add some required fields

* feat: add immutable name in dashboard v2

* feat: add immutable name in dashboard v2

* feat: add immutable name in dashboard v2 api specs

* fix: remove unused param in constructor

* fix: improve api descriptions

* fix: remove unneeded comment

* chore: increase MaxTagsPerDashboard to 10

* fix: set display name in unmarshal json

* chore: remove integration test for now (will add along with list api)

* feat: add validation on dashboard name

* test: fix build errors and tests based on name related changes

* fix: correct convertor method name

* test: add unit tests for type conversions

* chore: remove enum def of threshold comparison operator

* feat: add flag to generate unique name in backend

* chore: generate api specs

* chore: make tags required in postable

* fix: build error fix

* fix: fix build error in test after merge conflict

* fix: remove unused store method

* fix: remove unused module methods

* fix: use v1 store update method

* fix: change data to spec in api param description

* chore: add back accidentally removed tests

* fix: address review comments

* chore: generate frontend api spec

* chore: use same jsonpatch package as done in zeus

* chore: remove JSONPatchDocument and use patchable everywhere

* fix: make remove idempotent in patch

* chore: separate file for patch types

* chore: better error passage

* fix: remove extra decodePatch calls

* fix: use must new org id

* fix: proper error passage

* chore: rename updateable to updatable

---------

Co-authored-by: Srikanth Chekuri <srikanth.chekuri92@gmail.com>
2026-06-04 11:59:40 +00:00
Gaurav Tewari
f4ede36d3e fix: ui issues in Trace and logs (#11564)
* fix: ui migration issues

* fix: minor issue

---------

Co-authored-by: Gaurav Tewari <tewarig@users.noreply.github.com>
2026-06-04 07:31:39 +00:00
Gaurav Tewari
fa7d941266 fix: time not being updated issue in domainlist and traces (#11460)
* fix: time not being updated issue in domainlist and traces

* refactor: make a hook

* chore: more refactor

* fix: test cases

* fix: test cases

* refactor: code

* fix: add test cases

* fix: test cases

---------

Co-authored-by: Gaurav Tewari <tewarig@users.noreply.github.com>
2026-06-04 07:31:14 +00:00
Vinicius Lourenço
fdb22e6669 test(alerts): add tests for list alerts & triggered alerts (#11554)
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
* test(alerts): add tests for triggered and list alerts page

* test(alerts): add tests for list alerts and triggered alerts

* test(fireEvent): replace by userEvent
2026-06-03 19:53:35 +00:00
Ashwin Bhatkal
95adbc31cc chore(dashboard): remove obsolete Sentry query-range timeout warning (#11576)
Removes the useEffect that captured a Sentry warning when a widget's
query range was not called within 120s, along with the now-unused
queryRangeCalledRef and the @sentry/react import in GridCard.

Closes SigNoz/engineering-pod#5217
2026-06-03 19:50:16 +00:00
Vinicius Lourenço
c5288fc1ea fix(dashboards): variables can be undefined when create new dashboard (#11155) 2026-06-03 16:46:33 +00:00
Rinky Devi
86e71151d7 fix: remove widget filter references when a dashboard variable is deleted (#11270)
* fix: the query being updated after deleting the variables

* fix: use exact variable string match when removing clauses on delete

Passing `true` to removeKeysFromExpression removed the first clause whose
value contained any `$`, which corrupted expressions when two variables
shared the same filter attribute (e.g. $env and $env_region both backed
by deployment.environment). Switching to the exact variable string
(`$${variableName}`) ensures only the deleted variable's clause is removed.

Also adds 9 targeted edge-case tests covering shared-key variables,
variable-name boundary ($env vs $environment), mixed literal/variable
clauses, multi-value array filter items, clickhouse_sql, idempotency,
empty widgets, and unrelated-variable no-ops.

* fix: refined the deletiong process

* fix: adding toast

* fix: resolved comments

* fix: updated the tests and moved func to utils
2026-06-03 16:46:04 +00:00
Nikhil Soni
4fce33e2b3 chore: add metric for waterfall monitoring (#11557)
Some checks failed
build-staging / js-build (push) Has been cancelled
build-staging / prepare (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: add metric for waterfall monitoring

Gauge for config limit and counter to count large traces

* chore: use traces instead of tracedetail in metric name

* chore: try removing high cardinality attributes

* chore: use typed argument in logs

* chore: add another metric to get idea of trace sizes

* chore: change config namespace to trace instead of tracedetail

To make it consistant with metric namespaces

* chore: add unit to the windowed response count metric

* chore: use metrics names as per otel conventions

* chore: use constants for metric attributes

* refactor: return error on metric creation failure

* Revert "refactor: return error on metric creation failure"

This reverts commit 091c93e80b.

* chore: panic on metric initiliazation error
2026-06-03 13:30:25 +00:00
Ashwin Bhatkal
a0ae4dfd05 feat(dashboards): V2 dashboard details — base scaffolding (#11543)
* feat(dashboard-v2): route entry — DashboardPageV2 fetches dashboard, gates loading/error

* feat(dashboard-v2): presentational container — compose description & sections layout

* feat(dashboard-v2): breadcrumbs + header chrome

* feat(dashboard-v2): dashboard description — title, meta, actions, rename

* feat(dashboard-v2): read-only panels & sections layout
2026-06-03 12:55:14 +00:00
Naman Verma
a0b14e0835 fix: do not show errors for non-existent cost meter metrics (#10843)
* fix: show warning for non-existent cost meter metrics

* chore: lint fix by removing unused list

* chore: py fmt add new line

* chore: missing newline between tests

* fix: no warnings or errors for internal metrics

* fix: pylint fix by adding new line

* fix: lint fix in test
2026-06-03 10:09:08 +00:00
Abhi kumar
987844dbc8 refactor(types): tighten MetricQueryRangeSuccessResponse shape (#11562)
Replaces the `SuccessResponse<...> & { warning?: Warning; meta?: ExecStats }`
intersection with a single interface that extends SuccessResponse and declares
warning + meta on its own — no stitched intersection. Also types `params` as
`QueryRangeRequestV5` (was `unknown`), so callers can drop the redundant
`as QueryRangeRequestV5` casts.

Knock-on updates: 13 consumers switched from the raw
`SuccessResponse<MetricRangePayloadProps, unknown>` shape to the new
`MetricQueryRangeSuccessResponse`; 3 prod casts removed;
convertV5ResponseToLegacy and getTimeRange tightened their accept-params types.
2026-06-03 07:55:44 +00:00
primus-bot[bot]
e0ad7e487a chore(release): bump to v0.127.0 (#11558)
Co-authored-by: primus-bot[bot] <171087277+primus-bot[bot]@users.noreply.github.com>
2026-06-03 07:22:32 +00:00
Ashwin Bhatkal
af72a4118b fix: resolve UX regressions across dashboards, metrics and alerts pages after component migration (#11546)
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
* fix: resolve UX regressions across dashboards, metrics and alerts pages after component migration

- NewSelect: make the @signozhq checkbox label fill the row (min-width: 0)
  so long option text truncates with an ellipsis; size Only/Toggle buttons to
  the resting row height so hover no longer grows the row
- PlannedDowntime: reset field-label styles bleeding into the migrated
  RadioGroup option labels and add spacing between options
- Page-level contrast, spacing, alignment and icon-size fixes across
  dashboard settings, metrics explorer, alert rules, routing policies,
  auto-refresh and the time picker

* fix(multiselect): surface clear icon and remove dropdown double-scroll

- add .ant-select-clear color override so the multiselect clear (x) icon is
  visible on the dark selector (the single-select had it, multiselect didn't)
- raise the dropdown max-height so the react-virtuoso list (<=300px) plus the
  header/footer fits, leaving the list as the single scroller instead of the
  list and the container both scrolling
- StatsCard: drop the unnecessary !important on .count-label color

* fix: success colors
2026-06-03 05:16:01 +00:00
Nityananda Gohain
dc6a1d0a4e fix: skip resource filter if it exceeds a threshold (#11524)
* fix: skip resource filter if it exceeds a threshold

* fix: test cleanup

* fix: test cleanup

* fix: more cleanup

* fix: tests
2026-06-03 05:08:40 +00:00
Aditya Singh
2e60ab0f81 fix(logs): dedupe body/timestamp columns in explorer picker (#11553)
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
* fix: duplicate body issue

* fix: update test
2026-06-02 20:15:32 +00:00
SagarRajput-7
29ed44a8ce fix(sso): send empty object when clearing group/domain mappings on save (#11550) 2026-06-02 19:30:09 +00:00
Aditya Singh
76ee298605 feat(logs/traces): migrate column picker UI to FieldsSelector (#11516)
* feat: field selector migrated to telemetry field key

* feat: move floating panel to field selector

* feat: sync columns state in logs

* feat: sync columns state in traces

* feat: logs field migration integrate

* feat: traces field migration integrate

* feat: minor refactor

* feat: tests updated

* feat: move to key from name on fields for logs and traces

* feat: update tests

* feat: update tests
2026-06-02 17:37:27 +00:00
Tushar Vats
218f8269dd fix: donot close suggestion on typing period (#11490) 2026-06-02 17:35:12 +00:00
Nityananda Gohain
e8effa5b3f feat: [traces] time aware dynamic field mapper (#11234)
* feat: [traces] time aware dynamic field mapper

* fix: minor changes

* fix: get keys after modifying the selectkeys

* fix: more updated

* fix: lint

* fix: address comments

* fix: tests

* fix: tests

---------

Co-authored-by: Srikanth Chekuri <srikanth.chekuri92@gmail.com>
2026-06-02 16:17:42 +00:00
Vikrant Gupta
ca5ff7f617 fix(web): update web settings config (#11548)
* fix(web): update web settings config

* fix(web): add test case for config settings
2026-06-02 15:12:51 +00:00
Vinicius Lourenço
a487b311bc fix(metrics-explorer): handle in case .data is undefined (#11527)
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-06-02 13:46:53 +00:00
Vinicius Lourenço
6473066193 fix(metrics-explorer): handle in case .atributes is undefined (#11528) 2026-06-02 13:46:52 +00:00
SagarRajput-7
fb0d34ae35 feat(auth): validate reset password token on page load before showing form (#11522)
* feat(auth): validate reset password token on page load before showing form

* fix(auth): distinct error copy for expired vs invalid token; skip 401 rotation on verify endpoint

* fix(auth): use endsWith for orval-generated endpoint guards in interceptorRejected

* Revert "fix(auth): use endsWith for orval-generated endpoint guards in interceptorRejected"

This reverts commit 00aa23b8fc.
2026-06-02 13:38:18 +00:00
Vinicius Lourenço
ba684acba3 fix(create-alert-v2): tooltip not showing due to pointer-events none (#11489) 2026-06-02 12:05:02 +00:00
Gaurav Tewari
184724003a chore: remove query status (#11476)
* chore: remove confusing query status

* chore: remove extra things

---------

Co-authored-by: Gaurav Tewari <tewarig@users.noreply.github.com>
2026-06-02 11:48:40 +00:00
Vinicius Lourenço
a4d3f10da8 chore(codeowners): add pulse for alerts and infra monitoring pages (#11508) 2026-06-02 11:44:38 +00:00
Nityananda Gohain
a71ac2ada6 fix: add adjustkeys in trace operator cte builder (#11349)
* fix: add adjustkeys in trace operator cte builder

* fix: more fixes

* fix: cleanup

* fix: move tests to trace operator file

* fix: address comments

* fix: lint issues

---------

Co-authored-by: Srikanth Chekuri <srikanth.chekuri92@gmail.com>
2026-06-02 10:59:15 +00:00
Vikrant Gupta
0963ff08cd feat(web): disable all integrations by default (#11539) 2026-06-02 09:32:20 +00:00
Nikhil Soni
e43aeb8e24 fix: add references in waterfall response (#11536)
* fix: add references in waterfall response

* chore: update openapi specs

* chore: mark reference a non nullable field
2026-06-02 08:42:26 +00:00
Aditya Singh
9074208b09 feat: added trace events (#11538) 2026-06-02 08:41:39 +00:00
Vishal Sharma
571e23910e feat(ai-assistant): show descriptive Noz hover tooltip on all entry points (#11526)
Noz launched under early access and its three entry points (header button,
floating trigger, sidebar nav item) previously showed a bare "Noz" tooltip,
which does not educate users who don't yet recognize the name. Replace it with
a shared descriptive tooltip ("Noz, your AI teammate") sourced from a single
constant so the surfaces never drift.

- Add NOZ_TOOLTIP_TITLE in components/Noz/Noz.constants.ts
- Header button and floating trigger read the constant
- Add optional tooltip field to SidebarItem; NavItem wraps the whole row in a
  Tooltip (non-pinnable items only, to avoid nesting with the pin tooltip)
- Move the trigger's Noz icon to the Button prefix slot to match the codebase
  convention for icon-only buttons
2026-06-02 06:53:57 +00:00
Nikhil Soni
5e94f7ac6e feat(trace-details): Add API endpoint & module for trace aggregations (#11452)
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: add endpoint & module for trace aggregations

* chore: use v1 for aggregations api

* chore: update openapi specs

* feat: add implementation for aggregation store

* chore: use query builder for count by field

* chore: remove support for aggregations on attributes

Only supporting resource fields for now, since this
is not user input but hard coded client experience

* chore: extract out inner query as cte

* chore: again extract out inner query

* chore: move end_ns computation to first cte for simplicity

* chore: use notEmpty function instead of having

* fix: type cast issue in sum function

* chore: use query builder for duration query as well

* chore: add tests for trace store sql

* chore: remove unnecessary column checks

* chore: format the expected sql queries

* fix: formating was breaking test

* fix: change import and remove formating from sql

* chore: remove formating from store impl as well

* fix: mark required fields as required

* fix: explicitly mark nullable false for required field

* fix: mark required fields in response as well
2026-06-01 15:32:09 +00:00
Gaurav Tewari
387ad06c2d fix: tag container styles in qb (#11503)
Co-authored-by: Gaurav Tewari <tewarig@users.noreply.github.com>
2026-06-01 14:09:06 +00:00
Aditya Singh
72ff433c20 feat(logs/traces): streamline column state — selectColumns becomes canonical source (#11426)
Some checks failed
Release Drafter / update_release_draft (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
build-staging / staging (push) Has been cancelled
* feat: field selector migrated to telemetry field key

* feat: move floating panel to field selector

* feat: sync columns state in logs

* feat: sync columns state in traces
2026-06-01 13:01:05 +00:00
Vinicius Lourenço
587f518599 refactor(infra-monitoring): removed primary filters deprecated prop (#11505) 2026-06-01 11:32:15 +00:00
Nityananda Gohain
bfc50ee9c3 chore: use distinct fingerprint in resource filter (#11521)
* chore: use distinct fingerprint in resource filter

* fix: use group by instead of distinct
2026-06-01 09:47:56 +00:00
Tushar Vats
4b08ba1330 fix: qb warnings (#11518) 2026-06-01 09:43:22 +00:00
Ashwin Bhatkal
557a7120df feat: v2 dashboards list page (#11404)
* feat: add useIsDashboardV2 hook

* feat(dashboards-list-v2): page shell

* feat(dashboards-list-v2): loading / error / empty / no-results state components

* feat(dashboards-list-v2): SearchBar input component

* feat(dashboards-list-v2): CreateDashboardDropdown (new / import JSON entry)

* feat(dashboards-list-v2): DashboardRow + per-row ActionsPopover with v2 delete

* feat(dashboards-list-v2): ListHeader with sort/order + Configure Metadata trigger

* feat(dashboards-list-v2): JSON import modal (Monaco editor + sample)

* feat(dashboards-list-v2): Configure Metadata modal + persisted visible-columns store

* feat(dashboards-list-v2): wire list to v2 API with pagination, URL state, debounced search

* chore: park v2 dashboard pages until backend lands on main

* refactor(dashboards-list-v2): extract lastUpdatedLabel into utils

Dedupe the relative-time formatter that was copied into both DashboardRow
and ConfigureMetadataModal. Single source in DashboardsListPageV2/utils.ts.

* refactor(dashboards-list-v2): group state components under states/

Move EmptyState, ErrorState, LoadingState, and NoResultsState under
components/states/. They're a coherent family (interchangeable view
branches in the list orchestrator) and grouping them sets up shared
styling extraction next.

Pure relocation — git mv preserves history; only DashboardsList.tsx's
imports change.

* refactor(dashboards-list-v2): extract shared state wrapper styles

Pull the dashed-border card layout, body text, and learn-more CTA into
states/states.module.scss. EmptyState, ErrorState, and NoResultsState
now compose from the shared base and only declare what differs
(padding, gap, color, font-weight). LoadingState is a different shape
(skeleton stack) and stays untouched.

Cascaded properties are byte-equivalent — pure de-duplication.

* refactor(dashboards-list-v2): port to @signozhq/ui primitives

Replace antd Button/Input usages and bespoke <button> rows with the
@signozhq/ui equivalents across the four review-flagged surfaces:

  - EmptyState "Learn more" → Button variant="link" color="primary"
  - ErrorState "Retry" → Button variant="outlined" color="secondary"
    with prefix icon; drops the sub-pixel .retryButton overrides in
    favour of the variant's tokenized layout.
  - ErrorState "Contact Support" → Button variant="link" color="primary"
  - SearchBar Input → signoz Input (data-testid → testId)
  - ActionsPopover rows → Button variant="ghost" color="secondary"
    (color="destructive" for Delete) with prefix icons
  - ActionsPopover trigger → Button size="icon" variant="ghost"

The signoz button variant handles the icon-slot alignment that the
removed .actionItem styling reimplemented manually, so the bulk of
ActionsPopover SCSS is deleted. A minimal .menuItem class still
left-aligns + fills the popover row, and .deleteDivider keeps the
hairline separator above the destructive action.

Popover itself stays on antd — migrating to @signozhq/ui/dropdown-menu
is deferred (same TODO as CreateDashboardDropdown).

* style(dashboards-list-v2): use font tokens across new SCSS

Replace literal font-size/weight values across the V2 page SCSS with
the shared design-token custom properties already used elsewhere:

  - 14px → var(--font-size-sm)
  - 12px → var(--font-size-xs)
  - 400  → var(--font-weight-normal)
  - 500  → var(--font-weight-medium)
  - 600  → var(--font-weight-semibold)

ErrorState.module.scss was tokenized alongside its .retryButton
removal in the previous commit. Three literals remain because they
have no clean token equivalent: 11px (sub-token), 8px (avatar
initial), and 12.805px (sub-pixel artifact from a Figma export,
a separate concern).

* style(dashboards-list-v2): replace sub-pixel padding in CreateDashboardDropdown

Round 5.937px / 11.875px / 1.484px to clean integer pixel values
(6px / 12px / 2px). The sub-pixel values were the "trial-and-error"
artifact reviewer flagged — likely a Figma scaling round-trip.

Difference is sub-pixel and not visible on any standard DPR.

* feat(dashboards-list-v2): treat search input as DSL with explicit submission

What the user types is what gets sent to the API as the `query`
param — no more wrapping the input in a synthesized
`name CONTAINS '…' or description CONTAINS '…'` clause.

This gives users the full DSL surface (name, description, created_by,
created_at, updated_at, locked, public, source, tag-key equality)
instead of the name+description CONTAINS approximation.

Submission is explicit (Enter, blur, or a return-key icon button
in the input's suffix slot) so an in-progress DSL string never
hits the API mid-typing. On error, the toolbar (search + create)
stays visible above the ErrorState so users can edit and retry
without losing their place.

  - Drop buildSearchDSL helper and the 300ms debounce.
  - Pass searchInput.trim() to the API; empty input clears filter.
  - SearchBar gains onSubmit (Enter / blur / suffix click).
  - Move ErrorState below the toolbar in DashboardsList layout.
  - Sync local input from searchString on URL changes (back/fwd).
  - Placeholder updated to hint at DSL syntax.

Trade-off: a bare "foo" no longer matches anything; users now need
to type "name CONTAINS 'foo'". This is the new UX.

* fix(dashboards-list-v2): tighten ActionsPopover button styling

  - Drop variant="ghost" on the three non-destructive row buttons so
    they pick up the signoz Button default.
  - Use <Divider /> from @signozhq/ui above the destructive Delete
    button instead of a bespoke .deleteDivider class.
  - Drop the surrounding .content padding and the now-unused
    .deleteDivider rule. Keep .menuItem for left-alignment + width.

* fix(dashboards-list-v2): surface DSL parse errors on 400 responses

When the BE rejects the search query with a 4xx, show the server-
provided detail (e.g. `invalid filter query: unsupported expression
"asdas" — every term must be of the form \`key OP value\``) instead
of the generic "Something went wrong" panel.

  - ErrorState gains optional httpStatus + errorMessage props.
    For 4xx it renders a two-line layout (title + cleaned detail)
    and drops the Retry button (retrying the same bad query won't
    help). Contact Support stays.
  - Add formatQueryErrorMessage util: strips the "invalid filter
    query:" prefix that the FE title already implies, and rewrites
    backtick-quoted format hints to use double quotes.
  - DashboardsList extracts status + message via toAPIError and
    passes them through. Search toolbar remains visible above the
    error, so the user can fix and resubmit.

* chore: fix lint error due to rebase
2026-06-01 08:48:50 +00:00
641 changed files with 27862 additions and 10728 deletions

23
.github/CODEOWNERS vendored
View File

@@ -169,3 +169,26 @@ go.mod @therealpandey
## Dashboard V2
/frontend/src/pages/DashboardPageV2/ @SigNoz/pulse-frontend
/frontend/src/pages/DashboardsListPageV2/ @SigNoz/pulse-frontend
## Infrastructure Monitoring
/frontend/src/pages/InfrastructureMonitoring/ @SigNoz/pulse-frontend
/frontend/src/container/InfraMonitoringHosts/ @SigNoz/pulse-frontend
/frontend/src/container/InfraMonitoringK8s/ @SigNoz/pulse-frontend
## Alerts
/frontend/src/pages/AlertList/ @SigNoz/pulse-frontend
/frontend/src/pages/AlertDetails/ @SigNoz/pulse-frontend
/frontend/src/pages/CreateAlert/ @SigNoz/pulse-frontend
/frontend/src/pages/EditRules/ @SigNoz/pulse-frontend
/frontend/src/container/AlertHistory/ @SigNoz/pulse-frontend
/frontend/src/container/CreateAlertRule/ @SigNoz/pulse-frontend
/frontend/src/container/CreateAlertV2/ @SigNoz/pulse-frontend
/frontend/src/container/EditAlertV2/ @SigNoz/pulse-frontend
/frontend/src/container/FormAlertRules/ @SigNoz/pulse-frontend
/frontend/src/container/ListAlertRules/ @SigNoz/pulse-frontend
/frontend/src/container/TriggeredAlerts/ @SigNoz/pulse-frontend
/frontend/src/container/AnomalyAlertEvaluationView/ @SigNoz/pulse-frontend
## OpenAPI Schema - Generated
/frontend/src/api/generated/services/ @therealpandey @vikrantgupta25 @srikanthccv
/docs/api/openapi.yml @therealpandey @vikrantgupta25 @srikanthccv

View File

@@ -69,6 +69,8 @@ jobs:
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
echo 'VITE_ENVIRONMENT="production"' >> frontend/.env
echo 'VITE_VERSION="${{ steps.build-info.outputs.version }}"' >> frontend/.env
- name: cache-dotenv
uses: actions/cache@v4
with:

View File

@@ -70,6 +70,8 @@ jobs:
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
echo 'VITE_ENVIRONMENT="staging"' >> frontend/.env
echo 'VITE_VERSION="${{ steps.build-info.outputs.version }}"' >> frontend/.env
- name: cache-dotenv
uses: actions/cache@v4
with:

View File

@@ -35,6 +35,8 @@ jobs:
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
echo 'VITE_ENVIRONMENT="production"' >> .env
echo 'VITE_VERSION="${{ github.ref_name }}"' >> .env
- name: node-setup
uses: actions/setup-node@v5
with:

View File

@@ -39,6 +39,7 @@ jobs:
matrix:
suite:
- alerts
- basepath
- callbackauthn
- cloudintegrations
- dashboard
@@ -52,6 +53,7 @@ jobs:
- rootuser
- serviceaccount
- querier_json_body
- querier_skip_resource_fingerprint
- ttl
sqlstore-provider:
- postgres
@@ -82,7 +84,7 @@ jobs:
run: |
cd tests && uv sync
- name: webdriver
if: matrix.suite == 'callbackauthn'
if: matrix.suite == 'callbackauthn' || matrix.suite == 'basepath'
run: |
wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | sudo apt-key add -
echo "deb http://dl.google.com/linux/chrome/deb/ stable main" | sudo tee -a /etc/apt/sources.list.d/google-chrome.list

View File

@@ -91,7 +91,7 @@ func runServer(ctx context.Context, config signoz.Config, logger *slog.Logger) e
sqlstoreProviderFactories(),
signoz.NewTelemetryStoreProviderFactories(),
func(ctx context.Context, providerSettings factory.ProviderSettings, store authtypes.AuthNStore, licensing licensing.Licensing) (map[authtypes.AuthNProvider]authn.AuthN, error) {
return signoz.NewAuthNs(ctx, providerSettings, store, licensing)
return signoz.NewAuthNs(ctx, providerSettings, store, licensing, config.Global)
},
func(ctx context.Context, sqlstore sqlstore.SQLStore, config authz.Config, _ licensing.Licensing, _ []authz.OnBeforeRoleDelete) (factory.ProviderFactory[authz.AuthZ, authz.Config], error) {
openfgaDataStore, err := openfgaserver.NewSQLStore(sqlstore, config)

View File

@@ -107,17 +107,17 @@ func runServer(ctx context.Context, config signoz.Config, logger *slog.Logger) e
sqlstoreProviderFactories(),
signoz.NewTelemetryStoreProviderFactories(),
func(ctx context.Context, providerSettings factory.ProviderSettings, store authtypes.AuthNStore, licensing licensing.Licensing) (map[authtypes.AuthNProvider]authn.AuthN, error) {
samlCallbackAuthN, err := samlcallbackauthn.New(ctx, store, licensing)
samlCallbackAuthN, err := samlcallbackauthn.New(ctx, store, licensing, config.Global)
if err != nil {
return nil, err
}
oidcCallbackAuthN, err := oidccallbackauthn.New(store, licensing, providerSettings)
oidcCallbackAuthN, err := oidccallbackauthn.New(store, licensing, providerSettings, config.Global)
if err != nil {
return nil, err
}
authNs, err := signoz.NewAuthNs(ctx, providerSettings, store, licensing)
authNs, err := signoz.NewAuthNs(ctx, providerSettings, store, licensing, config.Global)
if err != nil {
return nil, err
}

View File

@@ -64,16 +64,16 @@ web:
settings:
posthog:
# Whether to enable PostHog in web.
enabled: true
enabled: false
appcues:
# Whether to enable Appcues in web.
enabled: true
enabled: false
sentry:
# Whether to enable Sentry in web.
enabled: true
enabled: false
pylon:
# Whether to enable Pylon in web.
enabled: true
enabled: false
##################### Cache #####################
cache:
@@ -432,7 +432,7 @@ cloudintegration:
version: v0.0.8
##################### Trace Detail #####################
tracedetail:
traces:
waterfall:
# Number of spans returned per request when the trace is too large to show all at once.
span_page_size: 500

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.126.1
image: signoz/signoz:v0.127.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.4
image: signoz/signoz-otel-collector:v0.144.5
entrypoint:
- /bin/sh
command:
@@ -241,7 +241,7 @@ services:
replicas: 3
signoz-telemetrystore-migrator:
!!merge <<: *db-depend
image: signoz/signoz-otel-collector:v0.144.4
image: signoz/signoz-otel-collector:v0.144.5
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.126.1
image: signoz/signoz:v0.127.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.4
image: signoz/signoz-otel-collector:v0.144.5
entrypoint:
- /bin/sh
command:
@@ -167,7 +167,7 @@ services:
replicas: 3
signoz-telemetrystore-migrator:
!!merge <<: *db-depend
image: signoz/signoz-otel-collector:v0.144.4
image: signoz/signoz-otel-collector:v0.144.5
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.126.1}
image: signoz/signoz:${VERSION:-v0.127.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.4}
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-v0.144.5}
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.4}
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-v0.144.5}
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.126.1}
image: signoz/signoz:${VERSION:-v0.127.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.4}
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-v0.144.5}
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.4}
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-v0.144.5}
container_name: signoz-telemetrystore-migrator
environment:
- SIGNOZ_OTEL_COLLECTOR_CLICKHOUSE_DSN=tcp://clickhouse:9000

File diff suppressed because it is too large Load Diff

View File

@@ -5,10 +5,12 @@ import (
"fmt"
"log/slog"
"net/url"
"path"
"github.com/SigNoz/signoz/pkg/authn"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/factory"
"github.com/SigNoz/signoz/pkg/global"
"github.com/SigNoz/signoz/pkg/http/client"
"github.com/SigNoz/signoz/pkg/licensing"
"github.com/SigNoz/signoz/pkg/types/authtypes"
@@ -26,13 +28,14 @@ var defaultScopes []string = []string{"email", "profile", oidc.ScopeOpenID}
var _ authn.CallbackAuthN = (*AuthN)(nil)
type AuthN struct {
settings factory.ScopedProviderSettings
store authtypes.AuthNStore
licensing licensing.Licensing
httpClient *client.Client
settings factory.ScopedProviderSettings
store authtypes.AuthNStore
licensing licensing.Licensing
httpClient *client.Client
globalConfig global.Config
}
func New(store authtypes.AuthNStore, licensing licensing.Licensing, providerSettings factory.ProviderSettings) (*AuthN, error) {
func New(store authtypes.AuthNStore, licensing licensing.Licensing, providerSettings factory.ProviderSettings, globalConfig global.Config) (*AuthN, error) {
settings := factory.NewScopedProviderSettings(providerSettings, "github.com/SigNoz/signoz/ee/authn/callbackauthn/oidccallbackauthn")
httpClient, err := client.New(providerSettings.Logger, providerSettings.TracerProvider, providerSettings.MeterProvider)
@@ -41,10 +44,11 @@ func New(store authtypes.AuthNStore, licensing licensing.Licensing, providerSett
}
return &AuthN{
settings: settings,
store: store,
licensing: licensing,
httpClient: httpClient,
settings: settings,
store: store,
licensing: licensing,
httpClient: httpClient,
globalConfig: globalConfig,
}, nil
}
@@ -197,7 +201,7 @@ func (a *AuthN) oidcProviderAndoauth2Config(ctx context.Context, siteURL *url.UR
RedirectURL: (&url.URL{
Scheme: siteURL.Scheme,
Host: siteURL.Host,
Path: redirectPath,
Path: path.Join(a.globalConfig.ExternalPath(), redirectPath),
}).String(),
}, nil
}

View File

@@ -6,10 +6,12 @@ import (
"encoding/base64"
"encoding/pem"
"net/url"
"path"
"strings"
"github.com/SigNoz/signoz/pkg/authn"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/global"
"github.com/SigNoz/signoz/pkg/licensing"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/valuer"
@@ -24,14 +26,16 @@ const (
var _ authn.CallbackAuthN = (*AuthN)(nil)
type AuthN struct {
store authtypes.AuthNStore
licensing licensing.Licensing
store authtypes.AuthNStore
licensing licensing.Licensing
globalConfig global.Config
}
func New(ctx context.Context, store authtypes.AuthNStore, licensing licensing.Licensing) (*AuthN, error) {
func New(ctx context.Context, store authtypes.AuthNStore, licensing licensing.Licensing, globalConfig global.Config) (*AuthN, error) {
return &AuthN{
store: store,
licensing: licensing,
store: store,
licensing: licensing,
globalConfig: globalConfig,
}, nil
}
@@ -132,7 +136,7 @@ func (a *AuthN) serviceProvider(siteURL *url.URL, authDomain *authtypes.AuthDoma
return nil, err
}
acsURL := &url.URL{Scheme: siteURL.Scheme, Host: siteURL.Host, Path: redirectPath}
acsURL := &url.URL{Scheme: siteURL.Scheme, Host: siteURL.Host, Path: path.Join(a.globalConfig.ExternalPath(), redirectPath)}
// Note:
// The ServiceProviderIssuer is the client id in case of keycloak. Since we set it to the host here, we need to set the client id == host in keycloak.

View File

@@ -355,26 +355,32 @@ func (module *module) GetService(ctx context.Context, orgID valuer.UUID, service
var integrationService *cloudintegrationtypes.CloudIntegrationService
if !cloudIntegrationID.IsZero() {
storedService, err := module.store.GetServiceByServiceID(ctx, cloudIntegrationID, serviceID)
if err != nil && !errors.Ast(err, errors.TypeNotFound) {
return nil, err
}
if storedService != nil {
serviceConfig, err := cloudintegrationtypes.NewServiceConfigFromJSON(provider, storedService.Config)
if err != nil {
return nil, err
}
integrationService = cloudintegrationtypes.NewCloudIntegrationServiceFromStorable(storedService, serviceConfig)
}
if err := module.enrichDashboardIDs(ctx, orgID, provider, serviceID, serviceDefinition); err != nil {
return nil, err
}
if cloudIntegrationID.IsZero() {
return cloudintegrationtypes.NewService(provider, serviceDefinition, nil, nil), nil
}
return cloudintegrationtypes.NewService(*serviceDefinition, integrationService), nil
storedService, err := module.store.GetServiceByServiceID(ctx, cloudIntegrationID, serviceID)
if err != nil && !errors.Ast(err, errors.TypeNotFound) {
return nil, err
}
if storedService == nil {
return cloudintegrationtypes.NewService(provider, serviceDefinition, nil, nil), nil
}
serviceConfig, err := cloudintegrationtypes.NewServiceConfigFromJSON(provider, storedService.Config)
if err != nil {
return nil, err
}
integrationService = cloudintegrationtypes.NewCloudIntegrationServiceFromStorable(storedService, serviceConfig)
slugPrefix := cloudintegrationtypes.CloudIntegrationDashboardSlugPrefix(provider, serviceID)
integrationDashboards, err := module.store.ListIntegrationDashboardsBySlugPrefix(ctx, orgID, cloudintegrationtypes.IntegrationDashboardProviderCloudIntegration, slugPrefix)
if err != nil {
return nil, err
}
return cloudintegrationtypes.NewService(provider, serviceDefinition, integrationService, integrationDashboards), nil
}
func (module *module) CreateService(ctx context.Context, orgID valuer.UUID, createdBy string, creator valuer.UUID, service *cloudintegrationtypes.CloudIntegrationService, provider cloudintegrationtypes.CloudProviderType) error {
@@ -583,20 +589,3 @@ func (module *module) deprovisionDashboards(ctx context.Context, orgID valuer.UU
}
return nil
}
// enrichDashboardIDs replaces the raw dashboard name in each Dashboard.ID with the provisioned UUID.
// TODO: remove this hack and send idiomatic response to client.
func (module *module) enrichDashboardIDs(ctx context.Context, orgID valuer.UUID, provider cloudintegrationtypes.CloudProviderType, serviceID cloudintegrationtypes.ServiceID, serviceDefinition *cloudintegrationtypes.ServiceDefinition) error {
for i, d := range serviceDefinition.Assets.Dashboards {
slug := cloudintegrationtypes.CloudIntegrationDashboardSlug(provider, serviceID, d.ID)
row, err := module.store.GetIntegrationDashboardBySlug(ctx, orgID, cloudintegrationtypes.IntegrationDashboardProviderCloudIntegration, slug)
if err != nil {
if errors.Ast(err, errors.TypeNotFound) {
continue
}
return err
}
serviceDefinition.Assets.Dashboards[i].ID = row.DashboardID
}
return nil
}

View File

@@ -221,6 +221,18 @@ func (module *module) GetV2(ctx context.Context, orgID valuer.UUID, id valuer.UU
return module.pkgDashboardModule.GetV2(ctx, orgID, id)
}
func (module *module) UpdateV2(ctx context.Context, orgID valuer.UUID, id valuer.UUID, updatedBy string, updatable dashboardtypes.UpdatableDashboardV2) (*dashboardtypes.DashboardV2, error) {
return module.pkgDashboardModule.UpdateV2(ctx, orgID, id, updatedBy, updatable)
}
func (module *module) PatchV2(ctx context.Context, orgID valuer.UUID, id valuer.UUID, updatedBy string, patch dashboardtypes.PatchableDashboardV2) (*dashboardtypes.DashboardV2, error) {
return module.pkgDashboardModule.PatchV2(ctx, orgID, id, updatedBy, patch)
}
func (module *module) LockUnlockV2(ctx context.Context, orgID valuer.UUID, id valuer.UUID, updatedBy string, isAdmin bool, lock bool) error {
return module.pkgDashboardModule.LockUnlockV2(ctx, orgID, id, updatedBy, isAdmin, lock)
}
func (module *module) Get(ctx context.Context, orgID valuer.UUID, id valuer.UUID) (*dashboardtypes.Dashboard, error) {
return module.pkgDashboardModule.Get(ctx, orgID, id)
}

View File

@@ -1,28 +0,0 @@
// Mock for useSafeNavigate hook to avoid React Router version conflicts in tests
interface SafeNavigateOptions {
replace?: boolean;
state?: unknown;
newTab?: boolean;
}
interface SafeNavigateTo {
pathname?: string;
search?: string;
hash?: string;
}
type SafeNavigateToType = string | SafeNavigateTo;
interface UseSafeNavigateReturn {
safeNavigate: jest.MockedFunction<
(to: SafeNavigateToType, options?: SafeNavigateOptions) => void
>;
}
export const useSafeNavigate = (): UseSafeNavigateReturn => ({
safeNavigate: jest.fn(
(_to: SafeNavigateToType, _options?: SafeNavigateOptions) => {},
) as jest.MockedFunction<
(to: SafeNavigateToType, options?: SafeNavigateOptions) => void
>,
});

View File

@@ -1,6 +1,8 @@
import type { Config } from '@jest/types';
const USE_SAFE_NAVIGATE_MOCK_PATH = '<rootDir>/__mocks__/useSafeNavigate.ts';
const USE_SAFE_NAVIGATE_MOCK_PATH =
'<rootDir>/src/__tests__/safeNavigateMock.ts';
const LOG_EVENT_MOCK_PATH = '<rootDir>/src/__tests__/logEventMock.ts';
const config: Config.InitialOptions = {
silent: true,
@@ -22,6 +24,8 @@ 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,
'^api/common/logEvent$': LOG_EVENT_MOCK_PATH,
'^src/api/common/logEvent$': LOG_EVENT_MOCK_PATH,
'^constants/env$': '<rootDir>/__mocks__/env.ts',
'^src/constants/env$': '<rootDir>/__mocks__/env.ts',
'^@signozhq/icons$': '<rootDir>/__mocks__/signozhqIconsMock.tsx',

View File

@@ -351,19 +351,18 @@ function App(): JSX.Element {
Sentry.init({
dsn: process.env.SENTRY_DSN,
tunnel: process.env.TUNNEL_URL,
environment: 'production',
environment: process.env.ENVIRONMENT,
release: process.env.VERSION,
integrations: [
// Kept for the `transaction` tag used in routing, even though
// tracing is disabled. Ref: https://github.com/SigNoz/platform-pod/issues/2393#issuecomment-4603658055
Sentry.browserTracingIntegration(),
Sentry.replayIntegration({
maskAllText: false,
blockAllMedia: false,
}),
],
// Performance Monitoring
tracesSampleRate: 1.0, // Capture 100% of the transactions
// Set 'tracePropagationTargets' to control for which URLs distributed tracing should be enabled
tracePropagationTargets: [],
// Session Replay
tracesSampleRate: 0, // Ref: https://github.com/SigNoz/platform-pod/issues/2393#issuecomment-4603658055
replaysSessionSampleRate: 0.1, // This sets the sample rate at 10%. You may want to change it to 100% while in development and then sample at a lower rate in production.
replaysOnErrorSampleRate: 1.0, // If you're not already sampling the entire session, change the sample rate to 100% when sampling sessions where errors occur.
beforeSend(event) {

View File

@@ -0,0 +1,11 @@
// Shared mock for `api/common/logEvent`.
// Wired into jest.config.ts moduleNameMapper, so any import of
// `api/common/logEvent` in test code resolves to this file.
// Tests can import `logEventMock` to assert analytics calls — Jest's
// `clearMocks: true` resets call history between tests.
export const logEventMock: jest.MockedFunction<
(eventName: string, attributes?: Record<string, unknown>) => void
> = jest.fn();
export default logEventMock;

View File

@@ -0,0 +1,29 @@
// Shared mock for `hooks/useSafeNavigate`.
// Wired into jest.config.ts moduleNameMapper, so any import of
// `hooks/useSafeNavigate` in test code resolves to this file.
// Tests can import `safeNavigateMock` to assert navigation calls — Jest's
// `clearMocks: true` resets call history between tests.
interface SafeNavigateOptions {
replace?: boolean;
state?: unknown;
newTab?: boolean;
}
interface SafeNavigateTo {
pathname?: string;
search?: string;
hash?: string;
}
type SafeNavigateToType = string | SafeNavigateTo;
export const safeNavigateMock: jest.MockedFunction<
(to: SafeNavigateToType, options?: SafeNavigateOptions) => void
> = jest.fn();
export const useSafeNavigate = (): {
safeNavigate: typeof safeNavigateMock;
} => ({
safeNavigate: safeNavigateMock,
});

View File

@@ -3,13 +3,36 @@ import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import { ErrorResponse, SuccessResponse } from 'types/api';
export interface DayBreakdownEntry {
timestamp: number;
total: number;
quantity: number;
count: number;
size: number;
}
export interface TierEntry {
quantity: number;
unitPrice: number;
tierCost: number;
}
export interface BreakdownEntry {
type: string;
unit: string;
dayWiseBreakdown: {
breakdown: DayBreakdownEntry[];
};
tiers?: TierEntry[];
}
export interface UsageResponsePayloadProps {
billingPeriodStart: Date;
billingPeriodEnd: Date;
billingPeriodStart: number;
billingPeriodEnd: number;
details: {
total: number;
baseFee: number;
breakdown: [];
breakdown: BreakdownEntry[];
billTotal: number;
};
discount: number;

View File

@@ -19,7 +19,7 @@ import type {
import type {
AlertmanagertypesPostableChannelDTO,
ConfigReceiverDTO,
AlertmanagertypesReceiverDTO,
CreateChannel201,
DeleteChannelByIDPathParameters,
GetChannelByID200,
@@ -385,14 +385,14 @@ export const invalidateGetChannelByID = async (
*/
export const updateChannelByID = (
{ id }: UpdateChannelByIDPathParameters,
configReceiverDTO?: BodyType<ConfigReceiverDTO>,
alertmanagertypesReceiverDTO?: BodyType<AlertmanagertypesReceiverDTO>,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<void>({
url: `/api/v1/channels/${id}`,
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
data: configReceiverDTO,
data: alertmanagertypesReceiverDTO,
signal,
});
};
@@ -406,7 +406,7 @@ export const getUpdateChannelByIDMutationOptions = <
TError,
{
pathParams: UpdateChannelByIDPathParameters;
data?: BodyType<ConfigReceiverDTO>;
data?: BodyType<AlertmanagertypesReceiverDTO>;
},
TContext
>;
@@ -415,7 +415,7 @@ export const getUpdateChannelByIDMutationOptions = <
TError,
{
pathParams: UpdateChannelByIDPathParameters;
data?: BodyType<ConfigReceiverDTO>;
data?: BodyType<AlertmanagertypesReceiverDTO>;
},
TContext
> => {
@@ -432,7 +432,7 @@ export const getUpdateChannelByIDMutationOptions = <
Awaited<ReturnType<typeof updateChannelByID>>,
{
pathParams: UpdateChannelByIDPathParameters;
data?: BodyType<ConfigReceiverDTO>;
data?: BodyType<AlertmanagertypesReceiverDTO>;
}
> = (props) => {
const { pathParams, data } = props ?? {};
@@ -447,7 +447,7 @@ export type UpdateChannelByIDMutationResult = NonNullable<
Awaited<ReturnType<typeof updateChannelByID>>
>;
export type UpdateChannelByIDMutationBody =
| BodyType<ConfigReceiverDTO>
| BodyType<AlertmanagertypesReceiverDTO>
| undefined;
export type UpdateChannelByIDMutationError = ErrorType<RenderErrorResponseDTO>;
@@ -463,7 +463,7 @@ export const useUpdateChannelByID = <
TError,
{
pathParams: UpdateChannelByIDPathParameters;
data?: BodyType<ConfigReceiverDTO>;
data?: BodyType<AlertmanagertypesReceiverDTO>;
},
TContext
>;
@@ -472,7 +472,7 @@ export const useUpdateChannelByID = <
TError,
{
pathParams: UpdateChannelByIDPathParameters;
data?: BodyType<ConfigReceiverDTO>;
data?: BodyType<AlertmanagertypesReceiverDTO>;
},
TContext
> => {
@@ -483,14 +483,14 @@ export const useUpdateChannelByID = <
* @summary Test notification channel
*/
export const testChannel = (
configReceiverDTO?: BodyType<ConfigReceiverDTO>,
alertmanagertypesReceiverDTO?: BodyType<AlertmanagertypesReceiverDTO>,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<void>({
url: `/api/v1/channels/test`,
method: 'POST',
headers: { 'Content-Type': 'application/json' },
data: configReceiverDTO,
data: alertmanagertypesReceiverDTO,
signal,
});
};
@@ -502,13 +502,13 @@ export const getTestChannelMutationOptions = <
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof testChannel>>,
TError,
{ data?: BodyType<ConfigReceiverDTO> },
{ data?: BodyType<AlertmanagertypesReceiverDTO> },
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof testChannel>>,
TError,
{ data?: BodyType<ConfigReceiverDTO> },
{ data?: BodyType<AlertmanagertypesReceiverDTO> },
TContext
> => {
const mutationKey = ['testChannel'];
@@ -522,7 +522,7 @@ export const getTestChannelMutationOptions = <
const mutationFn: MutationFunction<
Awaited<ReturnType<typeof testChannel>>,
{ data?: BodyType<ConfigReceiverDTO> }
{ data?: BodyType<AlertmanagertypesReceiverDTO> }
> = (props) => {
const { data } = props ?? {};
@@ -535,7 +535,9 @@ export const getTestChannelMutationOptions = <
export type TestChannelMutationResult = NonNullable<
Awaited<ReturnType<typeof testChannel>>
>;
export type TestChannelMutationBody = BodyType<ConfigReceiverDTO> | undefined;
export type TestChannelMutationBody =
| BodyType<AlertmanagertypesReceiverDTO>
| undefined;
export type TestChannelMutationError = ErrorType<RenderErrorResponseDTO>;
/**
@@ -548,13 +550,13 @@ export const useTestChannel = <
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof testChannel>>,
TError,
{ data?: BodyType<ConfigReceiverDTO> },
{ data?: BodyType<AlertmanagertypesReceiverDTO> },
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof testChannel>>,
TError,
{ data?: BodyType<ConfigReceiverDTO> },
{ data?: BodyType<AlertmanagertypesReceiverDTO> },
TContext
> => {
return useMutation(getTestChannelMutationOptions(options));
@@ -565,14 +567,14 @@ export const useTestChannel = <
* @summary Test notification channel (deprecated)
*/
export const testChannelDeprecated = (
configReceiverDTO?: BodyType<ConfigReceiverDTO>,
alertmanagertypesReceiverDTO?: BodyType<AlertmanagertypesReceiverDTO>,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<void>({
url: `/api/v1/testChannel`,
method: 'POST',
headers: { 'Content-Type': 'application/json' },
data: configReceiverDTO,
data: alertmanagertypesReceiverDTO,
signal,
});
};
@@ -584,13 +586,13 @@ export const getTestChannelDeprecatedMutationOptions = <
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof testChannelDeprecated>>,
TError,
{ data?: BodyType<ConfigReceiverDTO> },
{ data?: BodyType<AlertmanagertypesReceiverDTO> },
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof testChannelDeprecated>>,
TError,
{ data?: BodyType<ConfigReceiverDTO> },
{ data?: BodyType<AlertmanagertypesReceiverDTO> },
TContext
> => {
const mutationKey = ['testChannelDeprecated'];
@@ -604,7 +606,7 @@ export const getTestChannelDeprecatedMutationOptions = <
const mutationFn: MutationFunction<
Awaited<ReturnType<typeof testChannelDeprecated>>,
{ data?: BodyType<ConfigReceiverDTO> }
{ data?: BodyType<AlertmanagertypesReceiverDTO> }
> = (props) => {
const { data } = props ?? {};
@@ -618,7 +620,7 @@ export type TestChannelDeprecatedMutationResult = NonNullable<
Awaited<ReturnType<typeof testChannelDeprecated>>
>;
export type TestChannelDeprecatedMutationBody =
| BodyType<ConfigReceiverDTO>
| BodyType<AlertmanagertypesReceiverDTO>
| undefined;
export type TestChannelDeprecatedMutationError =
ErrorType<RenderErrorResponseDTO>;
@@ -634,13 +636,13 @@ export const useTestChannelDeprecated = <
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof testChannelDeprecated>>,
TError,
{ data?: BodyType<ConfigReceiverDTO> },
{ data?: BodyType<AlertmanagertypesReceiverDTO> },
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof testChannelDeprecated>>,
TError,
{ data?: BodyType<ConfigReceiverDTO> },
{ data?: BodyType<AlertmanagertypesReceiverDTO> },
TContext
> => {
return useMutation(getTestChannelDeprecatedMutationOptions(options));

View File

@@ -31,11 +31,15 @@ import type {
DisconnectAccountPathParameters,
GetAccount200,
GetAccountPathParameters,
GetAccountService200,
GetAccountServicePathParameters,
GetConnectionCredentials200,
GetConnectionCredentialsPathParameters,
GetService200,
GetServiceParams,
GetServicePathParameters,
ListAccountServicesMetadata200,
ListAccountServicesMetadataPathParameters,
ListAccounts200,
ListAccountsPathParameters,
ListServicesMetadata200,
@@ -631,6 +635,227 @@ export const useUpdateAccount = <
> => {
return useMutation(getUpdateAccountMutationOptions(options));
};
/**
* This endpoint lists the services metadata for the specified account and cloud provider
* @summary List account services metadata
*/
export const listAccountServicesMetadata = (
{ cloudProvider, id }: ListAccountServicesMetadataPathParameters,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<ListAccountServicesMetadata200>({
url: `/api/v1/cloud_integrations/${cloudProvider}/accounts/${id}/services`,
method: 'GET',
signal,
});
};
export const getListAccountServicesMetadataQueryKey = ({
cloudProvider,
id,
}: ListAccountServicesMetadataPathParameters) => {
return [
`/api/v1/cloud_integrations/${cloudProvider}/accounts/${id}/services`,
] as const;
};
export const getListAccountServicesMetadataQueryOptions = <
TData = Awaited<ReturnType<typeof listAccountServicesMetadata>>,
TError = ErrorType<RenderErrorResponseDTO>,
>(
{ cloudProvider, id }: ListAccountServicesMetadataPathParameters,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof listAccountServicesMetadata>>,
TError,
TData
>;
},
) => {
const { query: queryOptions } = options ?? {};
const queryKey =
queryOptions?.queryKey ??
getListAccountServicesMetadataQueryKey({ cloudProvider, id });
const queryFn: QueryFunction<
Awaited<ReturnType<typeof listAccountServicesMetadata>>
> = ({ signal }) => listAccountServicesMetadata({ cloudProvider, id }, signal);
return {
queryKey,
queryFn,
enabled: !!(cloudProvider && id),
...queryOptions,
} as UseQueryOptions<
Awaited<ReturnType<typeof listAccountServicesMetadata>>,
TError,
TData
> & { queryKey: QueryKey };
};
export type ListAccountServicesMetadataQueryResult = NonNullable<
Awaited<ReturnType<typeof listAccountServicesMetadata>>
>;
export type ListAccountServicesMetadataQueryError =
ErrorType<RenderErrorResponseDTO>;
/**
* @summary List account services metadata
*/
export function useListAccountServicesMetadata<
TData = Awaited<ReturnType<typeof listAccountServicesMetadata>>,
TError = ErrorType<RenderErrorResponseDTO>,
>(
{ cloudProvider, id }: ListAccountServicesMetadataPathParameters,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof listAccountServicesMetadata>>,
TError,
TData
>;
},
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
const queryOptions = getListAccountServicesMetadataQueryOptions(
{ cloudProvider, id },
options,
);
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
queryKey: QueryKey;
};
return { ...query, queryKey: queryOptions.queryKey };
}
/**
* @summary List account services metadata
*/
export const invalidateListAccountServicesMetadata = async (
queryClient: QueryClient,
{ cloudProvider, id }: ListAccountServicesMetadataPathParameters,
options?: InvalidateOptions,
): Promise<QueryClient> => {
await queryClient.invalidateQueries(
{ queryKey: getListAccountServicesMetadataQueryKey({ cloudProvider, id }) },
options,
);
return queryClient;
};
/**
* This endpoint gets a service and its configuration for the specified cloud integration account
* @summary Get service for account
*/
export const getAccountService = (
{ cloudProvider, id, serviceId }: GetAccountServicePathParameters,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<GetAccountService200>({
url: `/api/v1/cloud_integrations/${cloudProvider}/accounts/${id}/services/${serviceId}`,
method: 'GET',
signal,
});
};
export const getGetAccountServiceQueryKey = ({
cloudProvider,
id,
serviceId,
}: GetAccountServicePathParameters) => {
return [
`/api/v1/cloud_integrations/${cloudProvider}/accounts/${id}/services/${serviceId}`,
] as const;
};
export const getGetAccountServiceQueryOptions = <
TData = Awaited<ReturnType<typeof getAccountService>>,
TError = ErrorType<RenderErrorResponseDTO>,
>(
{ cloudProvider, id, serviceId }: GetAccountServicePathParameters,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getAccountService>>,
TError,
TData
>;
},
) => {
const { query: queryOptions } = options ?? {};
const queryKey =
queryOptions?.queryKey ??
getGetAccountServiceQueryKey({ cloudProvider, id, serviceId });
const queryFn: QueryFunction<
Awaited<ReturnType<typeof getAccountService>>
> = ({ signal }) =>
getAccountService({ cloudProvider, id, serviceId }, signal);
return {
queryKey,
queryFn,
enabled: !!(cloudProvider && id && serviceId),
...queryOptions,
} as UseQueryOptions<
Awaited<ReturnType<typeof getAccountService>>,
TError,
TData
> & { queryKey: QueryKey };
};
export type GetAccountServiceQueryResult = NonNullable<
Awaited<ReturnType<typeof getAccountService>>
>;
export type GetAccountServiceQueryError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary Get service for account
*/
export function useGetAccountService<
TData = Awaited<ReturnType<typeof getAccountService>>,
TError = ErrorType<RenderErrorResponseDTO>,
>(
{ cloudProvider, id, serviceId }: GetAccountServicePathParameters,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getAccountService>>,
TError,
TData
>;
},
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
const queryOptions = getGetAccountServiceQueryOptions(
{ cloudProvider, id, serviceId },
options,
);
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
queryKey: QueryKey;
};
return { ...query, queryKey: queryOptions.queryKey };
}
/**
* @summary Get service for account
*/
export const invalidateGetAccountService = async (
queryClient: QueryClient,
{ cloudProvider, id, serviceId }: GetAccountServicePathParameters,
options?: InvalidateOptions,
): Promise<QueryClient> => {
await queryClient.invalidateQueries(
{ queryKey: getGetAccountServiceQueryKey({ cloudProvider, id, serviceId }) },
options,
);
return queryClient;
};
/**
* This endpoint updates a service for the specified cloud provider
* @summary Update service

View File

@@ -21,8 +21,10 @@ import type {
CreateDashboardV2201,
CreatePublicDashboard201,
CreatePublicDashboardPathParameters,
DashboardtypesPatchableDashboardV2DTO,
DashboardtypesPostableDashboardV2DTO,
DashboardtypesPostablePublicDashboardDTO,
DashboardtypesUpdatableDashboardV2DTO,
DashboardtypesUpdatablePublicDashboardDTO,
DeletePublicDashboardPathParameters,
GetDashboardV2200,
@@ -33,7 +35,13 @@ import type {
GetPublicDashboardPathParameters,
GetPublicDashboardWidgetQueryRange200,
GetPublicDashboardWidgetQueryRangePathParameters,
LockDashboardV2PathParameters,
PatchDashboardV2200,
PatchDashboardV2PathParameters,
RenderErrorResponseDTO,
UnlockDashboardV2PathParameters,
UpdateDashboardV2200,
UpdateDashboardV2PathParameters,
UpdatePublicDashboardPathParameters,
} from '../sigNoz.schemas';
@@ -816,3 +824,360 @@ export const invalidateGetDashboardV2 = async (
return queryClient;
};
/**
* This endpoint applies an RFC 6902 JSON Patch to a v2-shape dashboard. The patch is applied against the postable view of the dashboard (metadata, data, tags), so individual panels, queries, variables, layouts, or tags can be updated without re-sending the rest of the dashboard. Apply is lenient: `remove` on a missing path is a no-op (idempotent) and `add` creates any missing parent objects, rather than failing as strict RFC 6902 would. The resulting dashboard is still validated. Locked dashboards are rejected.
* @summary Patch dashboard (v2)
*/
export const patchDashboardV2 = (
{ id }: PatchDashboardV2PathParameters,
dashboardtypesPatchableDashboardV2DTONull?: BodyType<DashboardtypesPatchableDashboardV2DTO | null> | null,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<PatchDashboardV2200>({
url: `/api/v2/dashboards/${id}`,
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
data: dashboardtypesPatchableDashboardV2DTONull,
signal,
});
};
export const getPatchDashboardV2MutationOptions = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof patchDashboardV2>>,
TError,
{
pathParams: PatchDashboardV2PathParameters;
data?: BodyType<DashboardtypesPatchableDashboardV2DTO | null>;
},
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof patchDashboardV2>>,
TError,
{
pathParams: PatchDashboardV2PathParameters;
data?: BodyType<DashboardtypesPatchableDashboardV2DTO | null>;
},
TContext
> => {
const mutationKey = ['patchDashboardV2'];
const { mutation: mutationOptions } = options
? options.mutation &&
'mutationKey' in options.mutation &&
options.mutation.mutationKey
? options
: { ...options, mutation: { ...options.mutation, mutationKey } }
: { mutation: { mutationKey } };
const mutationFn: MutationFunction<
Awaited<ReturnType<typeof patchDashboardV2>>,
{
pathParams: PatchDashboardV2PathParameters;
data?: BodyType<DashboardtypesPatchableDashboardV2DTO | null>;
}
> = (props) => {
const { pathParams, data } = props ?? {};
return patchDashboardV2(pathParams, data);
};
return { mutationFn, ...mutationOptions };
};
export type PatchDashboardV2MutationResult = NonNullable<
Awaited<ReturnType<typeof patchDashboardV2>>
>;
export type PatchDashboardV2MutationBody =
| BodyType<DashboardtypesPatchableDashboardV2DTO | null>
| undefined;
export type PatchDashboardV2MutationError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary Patch dashboard (v2)
*/
export const usePatchDashboardV2 = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof patchDashboardV2>>,
TError,
{
pathParams: PatchDashboardV2PathParameters;
data?: BodyType<DashboardtypesPatchableDashboardV2DTO | null>;
},
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof patchDashboardV2>>,
TError,
{
pathParams: PatchDashboardV2PathParameters;
data?: BodyType<DashboardtypesPatchableDashboardV2DTO | null>;
},
TContext
> => {
return useMutation(getPatchDashboardV2MutationOptions(options));
};
/**
* This endpoint updates a v2-shape dashboard's metadata, data, and tag set. Locked dashboards are rejected.
* @summary Update dashboard (v2)
*/
export const updateDashboardV2 = (
{ id }: UpdateDashboardV2PathParameters,
dashboardtypesUpdatableDashboardV2DTO?: BodyType<DashboardtypesUpdatableDashboardV2DTO>,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<UpdateDashboardV2200>({
url: `/api/v2/dashboards/${id}`,
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
data: dashboardtypesUpdatableDashboardV2DTO,
signal,
});
};
export const getUpdateDashboardV2MutationOptions = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof updateDashboardV2>>,
TError,
{
pathParams: UpdateDashboardV2PathParameters;
data?: BodyType<DashboardtypesUpdatableDashboardV2DTO>;
},
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof updateDashboardV2>>,
TError,
{
pathParams: UpdateDashboardV2PathParameters;
data?: BodyType<DashboardtypesUpdatableDashboardV2DTO>;
},
TContext
> => {
const mutationKey = ['updateDashboardV2'];
const { mutation: mutationOptions } = options
? options.mutation &&
'mutationKey' in options.mutation &&
options.mutation.mutationKey
? options
: { ...options, mutation: { ...options.mutation, mutationKey } }
: { mutation: { mutationKey } };
const mutationFn: MutationFunction<
Awaited<ReturnType<typeof updateDashboardV2>>,
{
pathParams: UpdateDashboardV2PathParameters;
data?: BodyType<DashboardtypesUpdatableDashboardV2DTO>;
}
> = (props) => {
const { pathParams, data } = props ?? {};
return updateDashboardV2(pathParams, data);
};
return { mutationFn, ...mutationOptions };
};
export type UpdateDashboardV2MutationResult = NonNullable<
Awaited<ReturnType<typeof updateDashboardV2>>
>;
export type UpdateDashboardV2MutationBody =
| BodyType<DashboardtypesUpdatableDashboardV2DTO>
| undefined;
export type UpdateDashboardV2MutationError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary Update dashboard (v2)
*/
export const useUpdateDashboardV2 = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof updateDashboardV2>>,
TError,
{
pathParams: UpdateDashboardV2PathParameters;
data?: BodyType<DashboardtypesUpdatableDashboardV2DTO>;
},
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof updateDashboardV2>>,
TError,
{
pathParams: UpdateDashboardV2PathParameters;
data?: BodyType<DashboardtypesUpdatableDashboardV2DTO>;
},
TContext
> => {
return useMutation(getUpdateDashboardV2MutationOptions(options));
};
/**
* This endpoint unlocks a v2-shape dashboard. Only the dashboard's creator or an org admin may lock or unlock.
* @summary Unlock dashboard (v2)
*/
export const unlockDashboardV2 = (
{ id }: UnlockDashboardV2PathParameters,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<string>({
url: `/api/v2/dashboards/${id}/lock`,
method: 'DELETE',
signal,
});
};
export const getUnlockDashboardV2MutationOptions = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof unlockDashboardV2>>,
TError,
{ pathParams: UnlockDashboardV2PathParameters },
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof unlockDashboardV2>>,
TError,
{ pathParams: UnlockDashboardV2PathParameters },
TContext
> => {
const mutationKey = ['unlockDashboardV2'];
const { mutation: mutationOptions } = options
? options.mutation &&
'mutationKey' in options.mutation &&
options.mutation.mutationKey
? options
: { ...options, mutation: { ...options.mutation, mutationKey } }
: { mutation: { mutationKey } };
const mutationFn: MutationFunction<
Awaited<ReturnType<typeof unlockDashboardV2>>,
{ pathParams: UnlockDashboardV2PathParameters }
> = (props) => {
const { pathParams } = props ?? {};
return unlockDashboardV2(pathParams);
};
return { mutationFn, ...mutationOptions };
};
export type UnlockDashboardV2MutationResult = NonNullable<
Awaited<ReturnType<typeof unlockDashboardV2>>
>;
export type UnlockDashboardV2MutationError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary Unlock dashboard (v2)
*/
export const useUnlockDashboardV2 = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof unlockDashboardV2>>,
TError,
{ pathParams: UnlockDashboardV2PathParameters },
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof unlockDashboardV2>>,
TError,
{ pathParams: UnlockDashboardV2PathParameters },
TContext
> => {
return useMutation(getUnlockDashboardV2MutationOptions(options));
};
/**
* This endpoint locks a v2-shape dashboard. Only the dashboard's creator or an org admin may lock or unlock.
* @summary Lock dashboard (v2)
*/
export const lockDashboardV2 = (
{ id }: LockDashboardV2PathParameters,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<string>({
url: `/api/v2/dashboards/${id}/lock`,
method: 'PUT',
signal,
});
};
export const getLockDashboardV2MutationOptions = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof lockDashboardV2>>,
TError,
{ pathParams: LockDashboardV2PathParameters },
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof lockDashboardV2>>,
TError,
{ pathParams: LockDashboardV2PathParameters },
TContext
> => {
const mutationKey = ['lockDashboardV2'];
const { mutation: mutationOptions } = options
? options.mutation &&
'mutationKey' in options.mutation &&
options.mutation.mutationKey
? options
: { ...options, mutation: { ...options.mutation, mutationKey } }
: { mutation: { mutationKey } };
const mutationFn: MutationFunction<
Awaited<ReturnType<typeof lockDashboardV2>>,
{ pathParams: LockDashboardV2PathParameters }
> = (props) => {
const { pathParams } = props ?? {};
return lockDashboardV2(pathParams);
};
return { mutationFn, ...mutationOptions };
};
export type LockDashboardV2MutationResult = NonNullable<
Awaited<ReturnType<typeof lockDashboardV2>>
>;
export type LockDashboardV2MutationError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary Lock dashboard (v2)
*/
export const useLockDashboardV2 = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof lockDashboardV2>>,
TError,
{ pathParams: LockDashboardV2PathParameters },
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof lockDashboardV2>>,
TError,
{ pathParams: LockDashboardV2PathParameters },
TContext
> => {
return useMutation(getLockDashboardV2MutationOptions(options));
};

View File

@@ -134,113 +134,6 @@ export interface AlertmanagertypesGettableRoutePolicyDTO {
updatedBy?: string | null;
}
export enum AlertmanagertypesMaintenanceKindDTO {
fixed = 'fixed',
recurring = 'recurring',
}
export enum AlertmanagertypesMaintenanceStatusDTO {
active = 'active',
upcoming = 'upcoming',
expired = 'expired',
}
export enum AlertmanagertypesRepeatOnDTO {
sunday = 'sunday',
monday = 'monday',
tuesday = 'tuesday',
wednesday = 'wednesday',
thursday = 'thursday',
friday = 'friday',
saturday = 'saturday',
}
export enum AlertmanagertypesRepeatTypeDTO {
daily = 'daily',
weekly = 'weekly',
monthly = 'monthly',
}
export interface AlertmanagertypesRecurrenceDTO {
/**
* @type string
*/
duration: string;
/**
* @type string,null
* @format date-time
*/
endTime?: string | null;
/**
* @type array,null
*/
repeatOn?: AlertmanagertypesRepeatOnDTO[] | null;
repeatType: AlertmanagertypesRepeatTypeDTO;
/**
* @type string
* @format date-time
*/
startTime: string;
}
export interface AlertmanagertypesScheduleDTO {
/**
* @type string
* @format date-time
*/
endTime?: string;
recurrence?: AlertmanagertypesRecurrenceDTO;
/**
* @type string
* @format date-time
*/
startTime?: string;
/**
* @type string
*/
timezone: string;
}
export interface AlertmanagertypesPlannedMaintenanceDTO {
/**
* @type array,null
*/
alertIds?: string[] | null;
/**
* @type string
* @format date-time
*/
createdAt?: string;
/**
* @type string
*/
createdBy?: string;
/**
* @type string
*/
description?: string;
/**
* @type string
*/
id: string;
kind: AlertmanagertypesMaintenanceKindDTO;
/**
* @type string
*/
name: string;
schedule: AlertmanagertypesScheduleDTO;
/**
* @type string
*/
scope?: string;
status: AlertmanagertypesMaintenanceStatusDTO;
/**
* @type string
* @format date-time
*/
updatedAt?: string;
/**
* @type string
*/
updatedBy?: string;
}
export interface ConfigAuthorizationDTO {
/**
* @type string
@@ -475,6 +368,130 @@ export interface ConfigSecretURLDTO {
[key: string]: unknown;
}
export interface AlertmanagertypesGoogleChatReceiverConfigDTO {
http_config?: ConfigHTTPClientConfigDTO;
/**
* @type boolean
*/
send_resolved?: boolean;
/**
* @type string
*/
text?: string;
/**
* @type string
*/
title?: string;
webhook_url?: ConfigSecretURLDTO;
}
export enum AlertmanagertypesMaintenanceKindDTO {
fixed = 'fixed',
recurring = 'recurring',
}
export enum AlertmanagertypesMaintenanceStatusDTO {
active = 'active',
upcoming = 'upcoming',
expired = 'expired',
}
export enum AlertmanagertypesRepeatOnDTO {
sunday = 'sunday',
monday = 'monday',
tuesday = 'tuesday',
wednesday = 'wednesday',
thursday = 'thursday',
friday = 'friday',
saturday = 'saturday',
}
export enum AlertmanagertypesRepeatTypeDTO {
daily = 'daily',
weekly = 'weekly',
monthly = 'monthly',
}
export interface AlertmanagertypesRecurrenceDTO {
/**
* @type string
*/
duration: string;
/**
* @type string,null
* @format date-time
*/
endTime?: string | null;
/**
* @type array,null
*/
repeatOn?: AlertmanagertypesRepeatOnDTO[] | null;
repeatType: AlertmanagertypesRepeatTypeDTO;
/**
* @type string
* @format date-time
*/
startTime: string;
}
export interface AlertmanagertypesScheduleDTO {
/**
* @type string
* @format date-time
*/
endTime?: string;
recurrence?: AlertmanagertypesRecurrenceDTO;
/**
* @type string
* @format date-time
*/
startTime?: string;
/**
* @type string
*/
timezone: string;
}
export interface AlertmanagertypesPlannedMaintenanceDTO {
/**
* @type array,null
*/
alertIds?: string[] | null;
/**
* @type string
* @format date-time
*/
createdAt?: string;
/**
* @type string
*/
createdBy?: string;
/**
* @type string
*/
description?: string;
/**
* @type string
*/
id: string;
kind: AlertmanagertypesMaintenanceKindDTO;
/**
* @type string
*/
name: string;
schedule: AlertmanagertypesScheduleDTO;
/**
* @type string
*/
scope?: string;
status: AlertmanagertypesMaintenanceStatusDTO;
/**
* @type string
* @format date-time
*/
updatedAt?: string;
/**
* @type string
*/
updatedBy?: string;
}
export interface ConfigDiscordConfigDTO {
/**
* @type string
@@ -1634,6 +1651,10 @@ export type AlertmanagertypesPostableChannelDTO = unknown & {
* @type array
*/
email_configs?: ConfigEmailConfigDTO[];
/**
* @type array
*/
googlechat_configs?: AlertmanagertypesGoogleChatReceiverConfigDTO[];
/**
* @type array
*/
@@ -1748,6 +1769,89 @@ export interface AlertmanagertypesPostableRoutePolicyDTO {
tags?: string[] | null;
}
export interface AlertmanagertypesReceiverDTO {
/**
* @type array
*/
discord_configs?: ConfigDiscordConfigDTO[];
/**
* @type array
*/
email_configs?: ConfigEmailConfigDTO[];
/**
* @type array
*/
googlechat_configs?: AlertmanagertypesGoogleChatReceiverConfigDTO[];
/**
* @type array
*/
incidentio_configs?: ConfigIncidentioConfigDTO[];
/**
* @type array
*/
jira_configs?: ConfigJiraConfigDTO[];
/**
* @type array
*/
mattermost_configs?: ConfigMattermostConfigDTO[];
/**
* @type array
*/
msteams_configs?: ConfigMSTeamsConfigDTO[];
/**
* @type array
*/
msteamsv2_configs?: ConfigMSTeamsV2ConfigDTO[];
/**
* @type string
*/
name?: string;
/**
* @type array
*/
opsgenie_configs?: ConfigOpsGenieConfigDTO[];
/**
* @type array
*/
pagerduty_configs?: ConfigPagerdutyConfigDTO[];
/**
* @type array
*/
pushover_configs?: ConfigPushoverConfigDTO[];
/**
* @type array
*/
rocketchat_configs?: ConfigRocketchatConfigDTO[];
/**
* @type array
*/
slack_configs?: ConfigSlackConfigDTO[];
/**
* @type array
*/
sns_configs?: ConfigSNSConfigDTO[];
/**
* @type array
*/
telegram_configs?: ConfigTelegramConfigDTO[];
/**
* @type array
*/
victorops_configs?: ConfigVictorOpsConfigDTO[];
/**
* @type array
*/
webex_configs?: ConfigWebexConfigDTO[];
/**
* @type array
*/
webhook_configs?: ConfigWebhookConfigDTO[];
/**
* @type array
*/
wechat_configs?: ConfigWechatConfigDTO[];
}
export interface AuthtypesAttributeMappingDTO {
/**
* @type string
@@ -2457,33 +2561,6 @@ export interface CloudintegrationtypesAccountDTO {
updatedAt?: string;
}
export interface DashboardtypesStorableDashboardDataDTO {
[key: string]: unknown;
}
export interface CloudintegrationtypesDashboardDTO {
definition?: DashboardtypesStorableDashboardDataDTO;
/**
* @type string
*/
description?: string;
/**
* @type string
*/
id?: string;
/**
* @type string
*/
title?: string;
}
export interface CloudintegrationtypesAssetsDTO {
/**
* @type array,null
*/
dashboards?: CloudintegrationtypesDashboardDTO[] | null;
}
export interface CloudintegrationtypesAzureConnectionArtifactDTO {
/**
* @type string
@@ -2866,6 +2943,54 @@ export interface CloudintegrationtypesPostableAgentCheckInDTO {
providerAccountId?: string;
}
export interface CloudintegrationtypesStorableIntegrationDashboardDTO {
/**
* @type string
* @format date-time
*/
createdAt: string;
/**
* @type string
*/
dashboardId: string;
/**
* @type string
*/
id: string;
/**
* @type string
*/
provider: string;
/**
* @type string
*/
slug: string;
/**
* @type string
* @format date-time
*/
updatedAt: string;
}
export interface CloudintegrationtypesServiceDashboardDTO {
/**
* @type string
*/
description: string;
integrationDashboard?: CloudintegrationtypesStorableIntegrationDashboardDTO;
/**
* @type string
*/
title: string;
}
export interface CloudintegrationtypesServiceAssetsDTO {
/**
* @type array
*/
dashboards: CloudintegrationtypesServiceDashboardDTO[];
}
export interface CloudintegrationtypesSupportedSignalsDTO {
/**
* @type boolean
@@ -2877,13 +3002,8 @@ export interface CloudintegrationtypesSupportedSignalsDTO {
metrics?: boolean;
}
export interface CloudintegrationtypesTelemetryCollectionStrategyDTO {
aws?: CloudintegrationtypesAWSTelemetryCollectionStrategyDTO;
azure?: CloudintegrationtypesAzureTelemetryCollectionStrategyDTO;
}
export interface CloudintegrationtypesServiceDTO {
assets: CloudintegrationtypesAssetsDTO;
assets: CloudintegrationtypesServiceAssetsDTO;
cloudIntegrationService: CloudintegrationtypesCloudIntegrationServiceDTO | null;
dataCollected: CloudintegrationtypesDataCollectedDTO;
/**
@@ -2899,7 +3019,6 @@ export interface CloudintegrationtypesServiceDTO {
*/
overview: string;
supportedSignals: CloudintegrationtypesSupportedSignalsDTO;
telemetryCollectionStrategy: CloudintegrationtypesTelemetryCollectionStrategyDTO;
/**
* @type string
*/
@@ -2944,85 +3063,6 @@ export interface CommonJSONRefDTO {
$ref?: string;
}
export interface ConfigReceiverDTO {
/**
* @type array
*/
discord_configs?: ConfigDiscordConfigDTO[];
/**
* @type array
*/
email_configs?: ConfigEmailConfigDTO[];
/**
* @type array
*/
incidentio_configs?: ConfigIncidentioConfigDTO[];
/**
* @type array
*/
jira_configs?: ConfigJiraConfigDTO[];
/**
* @type array
*/
mattermost_configs?: ConfigMattermostConfigDTO[];
/**
* @type array
*/
msteams_configs?: ConfigMSTeamsConfigDTO[];
/**
* @type array
*/
msteamsv2_configs?: ConfigMSTeamsV2ConfigDTO[];
/**
* @type string
*/
name?: string;
/**
* @type array
*/
opsgenie_configs?: ConfigOpsGenieConfigDTO[];
/**
* @type array
*/
pagerduty_configs?: ConfigPagerdutyConfigDTO[];
/**
* @type array
*/
pushover_configs?: ConfigPushoverConfigDTO[];
/**
* @type array
*/
rocketchat_configs?: ConfigRocketchatConfigDTO[];
/**
* @type array
*/
slack_configs?: ConfigSlackConfigDTO[];
/**
* @type array
*/
sns_configs?: ConfigSNSConfigDTO[];
/**
* @type array
*/
telegram_configs?: ConfigTelegramConfigDTO[];
/**
* @type array
*/
victorops_configs?: ConfigVictorOpsConfigDTO[];
/**
* @type array
*/
webex_configs?: ConfigWebexConfigDTO[];
/**
* @type array
*/
webhook_configs?: ConfigWebhookConfigDTO[];
/**
* @type array
*/
wechat_configs?: ConfigWechatConfigDTO[];
}
export interface CoretypesObjectGroupDTO {
resource: CoretypesResourceRefDTO;
/**
@@ -3089,6 +3129,40 @@ export interface DashboardGridLayoutSpecDTO {
repeatVariable?: string;
}
export interface DashboardLinkDTO {
/**
* @type string
*/
name?: string;
/**
* @type boolean
*/
renderVariables?: boolean;
/**
* @type boolean
*/
targetBlank?: boolean;
/**
* @type string
*/
tooltip?: string;
/**
* @type string
*/
url?: string;
}
export interface DashboardPanelDisplayDTO {
/**
* @type string
*/
description?: string;
/**
* @type string
*/
name?: string;
}
export interface VariableDisplayDTO {
/**
* @type string
@@ -3705,6 +3779,10 @@ export interface DashboardtypesCustomVariableSpecDTO {
customValue: string;
}
export interface DashboardtypesStorableDashboardDataDTO {
[key: string]: unknown;
}
export enum DashboardtypesSourceDTO {
user = 'user',
system = 'system',
@@ -3786,40 +3864,9 @@ export type DashboardtypesDashboardSpecDTODatasources = {
[key: string]: DashboardtypesDatasourceSpecDTO;
};
export interface V1PanelDisplayDTO {
/**
* @type string
*/
description?: string;
/**
* @type string
*/
name?: string;
export enum DashboardtypesPanelKindDTO {
Panel = 'Panel',
}
export interface V1LinkDTO {
/**
* @type string
*/
name?: string;
/**
* @type boolean
*/
renderVariables?: boolean;
/**
* @type boolean
*/
targetBlank?: boolean;
/**
* @type string
*/
tooltip?: string;
/**
* @type string
*/
url?: string;
}
export enum DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesTimeSeriesPanelSpecDTOKind {
'signoz/TimeSeriesPanel' = 'signoz/TimeSeriesPanel',
}
@@ -4061,6 +4108,13 @@ export type DashboardtypesPanelPluginDTO =
| DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesHistogramPanelSpecDTO
| DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesListPanelSpecDTO;
export enum Querybuildertypesv5RequestTypeDTO {
scalar = 'scalar',
time_series = 'time_series',
raw = 'raw',
raw_stream = 'raw_stream',
trace = 'trace',
}
export enum DashboardtypesQueryPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesBuilderQuerySpecDTOKind {
'signoz/BuilderQuery' = 'signoz/BuilderQuery',
}
@@ -4365,19 +4419,16 @@ export interface DashboardtypesQuerySpecDTO {
}
export interface DashboardtypesQueryDTO {
/**
* @type string
*/
kind?: string;
kind?: Querybuildertypesv5RequestTypeDTO;
spec?: DashboardtypesQuerySpecDTO;
}
export interface DashboardtypesPanelSpecDTO {
display?: V1PanelDisplayDTO;
display?: DashboardPanelDisplayDTO;
/**
* @type array
*/
links?: V1LinkDTO[];
links?: DashboardLinkDTO[];
plugin?: DashboardtypesPanelPluginDTO;
/**
* @type array
@@ -4386,10 +4437,7 @@ export interface DashboardtypesPanelSpecDTO {
}
export interface DashboardtypesPanelDTO {
/**
* @type string
*/
kind?: string;
kind?: DashboardtypesPanelKindDTO;
spec?: DashboardtypesPanelSpecDTO;
}
@@ -4403,20 +4451,20 @@ export type DashboardtypesDashboardSpecDTOPanelsAnyOf = {
export type DashboardtypesDashboardSpecDTOPanels =
DashboardtypesDashboardSpecDTOPanelsAnyOf | null;
export enum DashboardtypesLayoutEnvelopeGithubComPersesPersesPkgModelApiV1DashboardGridLayoutSpecDTOKind {
export enum DashboardtypesLayoutEnvelopeGithubComPersesSpecGoDashboardGridLayoutSpecDTOKind {
Grid = 'Grid',
}
export interface DashboardtypesLayoutEnvelopeGithubComPersesPersesPkgModelApiV1DashboardGridLayoutSpecDTO {
export interface DashboardtypesLayoutEnvelopeGithubComPersesSpecGoDashboardGridLayoutSpecDTO {
/**
* @enum Grid
* @type string
*/
kind: DashboardtypesLayoutEnvelopeGithubComPersesPersesPkgModelApiV1DashboardGridLayoutSpecDTOKind;
kind: DashboardtypesLayoutEnvelopeGithubComPersesSpecGoDashboardGridLayoutSpecDTOKind;
spec: DashboardGridLayoutSpecDTO;
}
export type DashboardtypesLayoutDTO =
DashboardtypesLayoutEnvelopeGithubComPersesPersesPkgModelApiV1DashboardGridLayoutSpecDTO;
DashboardtypesLayoutEnvelopeGithubComPersesSpecGoDashboardGridLayoutSpecDTO;
export enum DashboardtypesVariableEnvelopeGithubComSigNozSignozPkgTypesDashboardtypesListVariableSpecDTOKind {
ListVariable = 'ListVariable',
@@ -4520,21 +4568,21 @@ export interface DashboardtypesVariableEnvelopeGithubComSigNozSignozPkgTypesDash
spec: DashboardtypesListVariableSpecDTO;
}
export enum DashboardtypesVariableEnvelopeGithubComPersesPersesPkgModelApiV1DashboardTextVariableSpecDTOKind {
export enum DashboardtypesVariableEnvelopeGithubComPersesSpecGoDashboardTextVariableSpecDTOKind {
TextVariable = 'TextVariable',
}
export interface DashboardtypesVariableEnvelopeGithubComPersesPersesPkgModelApiV1DashboardTextVariableSpecDTO {
export interface DashboardtypesVariableEnvelopeGithubComPersesSpecGoDashboardTextVariableSpecDTO {
/**
* @enum TextVariable
* @type string
*/
kind: DashboardtypesVariableEnvelopeGithubComPersesPersesPkgModelApiV1DashboardTextVariableSpecDTOKind;
kind: DashboardtypesVariableEnvelopeGithubComPersesSpecGoDashboardTextVariableSpecDTOKind;
spec: DashboardTextVariableSpecDTO;
}
export type DashboardtypesVariableDTO =
| DashboardtypesVariableEnvelopeGithubComSigNozSignozPkgTypesDashboardtypesListVariableSpecDTO
| DashboardtypesVariableEnvelopeGithubComPersesPersesPkgModelApiV1DashboardTextVariableSpecDTO;
| DashboardtypesVariableEnvelopeGithubComPersesSpecGoDashboardTextVariableSpecDTO;
export interface DashboardtypesDashboardSpecDTO {
/**
@@ -4553,7 +4601,7 @@ export interface DashboardtypesDashboardSpecDTO {
/**
* @type array
*/
links?: V1LinkDTO[];
links?: DashboardLinkDTO[];
/**
* @type object,null
*/
@@ -4653,6 +4701,32 @@ export interface DashboardtypesGettablePublicDashboardDataDTO {
publicDashboard?: DashboardtypesGettablePublicDasbhboardDTO;
}
export enum DashboardtypesPatchOpDTO {
add = 'add',
remove = 'remove',
replace = 'replace',
move = 'move',
copy = 'copy',
test = 'test',
}
export interface DashboardtypesJSONPatchOperationDTO {
/**
* @type string
* @description Source JSON Pointer for move/copy ops; ignored for other ops.
*/
from?: string;
op: DashboardtypesPatchOpDTO;
/**
* @type string
* @description JSON Pointer (RFC 6901) into the dashboard's postable shape — e.g. /spec/display/name, /spec/panels/<id>, /spec/panels/<id>/spec/queries/0, /tags/-.
*/
path: string;
/**
* @description Value to add/replace/test against. The expected type depends on the path. Common shapes (see referenced schemas for the exact field set): /spec/panels/<id> takes a DashboardtypesPanel; /spec/panels/<id>/spec/queries/N (or /-) takes a DashboardtypesQuery; /spec/variables/N takes a DashboardtypesVariable; /spec/layouts/N takes a DashboardtypesLayout; /tags/N (or /-) takes a TagtypesPostableTag; /spec/display/name and other leaf string fields take a string. Required for add/replace/test; ignored for remove/move/copy.
*/
value?: unknown;
}
export enum DashboardtypesPanelPluginKindDTO {
'signoz/TimeSeriesPanel' = 'signoz/TimeSeriesPanel',
'signoz/BarChartPanel' = 'signoz/BarChartPanel',
@@ -4662,6 +4736,13 @@ export enum DashboardtypesPanelPluginKindDTO {
'signoz/HistogramPanel' = 'signoz/HistogramPanel',
'signoz/ListPanel' = 'signoz/ListPanel',
}
/**
* @nullable
*/
export type DashboardtypesPatchableDashboardV2DTO =
| DashboardtypesJSONPatchOperationDTO[]
| null;
export interface DashboardtypesPostableDashboardV2DTO {
/**
* @type boolean
@@ -4705,6 +4786,26 @@ export enum DashboardtypesQueryPluginKindDTO {
'signoz/ClickHouseSQL' = 'signoz/ClickHouseSQL',
'signoz/TraceOperator' = 'signoz/TraceOperator',
}
export interface DashboardtypesUpdatableDashboardV2DTO {
/**
* @type string
*/
image?: string;
/**
* @type string
*/
name: string;
/**
* @type string
*/
schemaVersion: string;
spec: DashboardtypesDashboardSpecDTO;
/**
* @type array,null
*/
tags: TagtypesPostableTagDTO[] | null;
}
export interface DashboardtypesUpdatablePublicDashboardDTO {
/**
* @type string
@@ -6882,13 +6983,6 @@ export type Querybuildertypesv5QueryRangeRequestDTOVariables = {
[key: string]: Querybuildertypesv5VariableItemDTO;
};
export enum Querybuildertypesv5RequestTypeDTO {
scalar = 'scalar',
time_series = 'time_series',
raw = 'raw',
raw_stream = 'raw_stream',
trace = 'trace',
}
/**
* Request body for the v5 query range endpoint. Supports builder queries (traces, logs, metrics), formulas, joins, trace operators, PromQL, and ClickHouse SQL queries.
*/
@@ -7753,12 +7847,34 @@ export type SpantypesSpanAggregationResultDTOValue =
SpantypesSpanAggregationResultDTOValueAnyOf | null;
export interface SpantypesSpanAggregationResultDTO {
aggregation?: SpantypesSpanAggregationTypeDTO;
field?: TelemetrytypesTelemetryFieldKeyDTO;
aggregation: SpantypesSpanAggregationTypeDTO;
field: TelemetrytypesTelemetryFieldKeyDTO;
/**
* @type object,null
*/
value?: SpantypesSpanAggregationResultDTOValue;
value: SpantypesSpanAggregationResultDTOValue;
}
export interface SpantypesGettableTraceAggregationsDTO {
/**
* @type array
*/
aggregations: SpantypesSpanAggregationResultDTO[];
}
export interface SpantypesOtelSpanRefDTO {
/**
* @type string
*/
refType?: string;
/**
* @type string
*/
spanId?: string;
/**
* @type string
*/
traceId?: string;
}
export type SpantypesWaterfallSpanDTOAttributesAnyOf = {
@@ -7855,6 +7971,10 @@ export interface SpantypesWaterfallSpanDTO {
* @type string
*/
parent_span_id?: string;
/**
* @type array
*/
references: SpantypesOtelSpanRefDTO[];
/**
* @type object,null
*/
@@ -8000,8 +8120,15 @@ export interface SpantypesPostableSpanMapperGroupDTO {
}
export interface SpantypesSpanAggregationDTO {
aggregation?: SpantypesSpanAggregationTypeDTO;
field?: TelemetrytypesTelemetryFieldKeyDTO;
aggregation: SpantypesSpanAggregationTypeDTO;
field: TelemetrytypesTelemetryFieldKeyDTO;
}
export interface SpantypesPostableTraceAggregationsDTO {
/**
* @type array
*/
aggregations: SpantypesSpanAggregationDTO[];
}
export interface SpantypesPostableWaterfallDTO {
@@ -8590,6 +8717,31 @@ export type UpdateAccountPathParameters = {
cloudProvider: string;
id: string;
};
export type ListAccountServicesMetadataPathParameters = {
cloudProvider: string;
id: string;
};
export type ListAccountServicesMetadata200 = {
data: CloudintegrationtypesGettableServicesMetadataDTO;
/**
* @type string
*/
status: string;
};
export type GetAccountServicePathParameters = {
cloudProvider: string;
id: string;
serviceId: string;
};
export type GetAccountService200 = {
data: CloudintegrationtypesServiceDTO;
/**
* @type string
*/
status: string;
};
export type UpdateServicePathParameters = {
cloudProvider: string;
id: string;
@@ -9344,6 +9496,17 @@ export type UpdateSpanMapperPathParameters = {
groupId: string;
mapperId: string;
};
export type GetTraceAggregationsPathParameters = {
traceID: string;
};
export type GetTraceAggregations200 = {
data: SpantypesGettableTraceAggregationsDTO;
/**
* @type string
*/
status: string;
};
export type ListUsersDeprecated200 = {
/**
* @type array
@@ -9432,6 +9595,34 @@ export type GetDashboardV2200 = {
status: string;
};
export type PatchDashboardV2PathParameters = {
id: string;
};
export type PatchDashboardV2200 = {
data: DashboardtypesGettableDashboardV2DTO;
/**
* @type string
*/
status: string;
};
export type UpdateDashboardV2PathParameters = {
id: string;
};
export type UpdateDashboardV2200 = {
data: DashboardtypesGettableDashboardV2DTO;
/**
* @type string
*/
status: string;
};
export type UnlockDashboardV2PathParameters = {
id: string;
};
export type LockDashboardV2PathParameters = {
id: string;
};
export type GetFeatures200 = {
/**
* @type array

View File

@@ -12,17 +12,120 @@ import type {
} from 'react-query';
import type {
GetTraceAggregations200,
GetTraceAggregationsPathParameters,
GetWaterfall200,
GetWaterfallPathParameters,
GetWaterfallV4200,
GetWaterfallV4PathParameters,
RenderErrorResponseDTO,
SpantypesPostableTraceAggregationsDTO,
SpantypesPostableWaterfallDTO,
} from '../sigNoz.schemas';
import { GeneratedAPIInstance } from '../../../generatedAPIInstance';
import type { ErrorType, BodyType } from '../../../generatedAPIInstance';
/**
* Computes span aggregations grouped by requested field.
* @summary Get aggregations for a trace
*/
export const getTraceAggregations = (
{ traceID }: GetTraceAggregationsPathParameters,
spantypesPostableTraceAggregationsDTO?: BodyType<SpantypesPostableTraceAggregationsDTO>,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<GetTraceAggregations200>({
url: `/api/v1/traces/${traceID}/aggregations`,
method: 'POST',
headers: { 'Content-Type': 'application/json' },
data: spantypesPostableTraceAggregationsDTO,
signal,
});
};
export const getGetTraceAggregationsMutationOptions = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof getTraceAggregations>>,
TError,
{
pathParams: GetTraceAggregationsPathParameters;
data?: BodyType<SpantypesPostableTraceAggregationsDTO>;
},
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof getTraceAggregations>>,
TError,
{
pathParams: GetTraceAggregationsPathParameters;
data?: BodyType<SpantypesPostableTraceAggregationsDTO>;
},
TContext
> => {
const mutationKey = ['getTraceAggregations'];
const { mutation: mutationOptions } = options
? options.mutation &&
'mutationKey' in options.mutation &&
options.mutation.mutationKey
? options
: { ...options, mutation: { ...options.mutation, mutationKey } }
: { mutation: { mutationKey } };
const mutationFn: MutationFunction<
Awaited<ReturnType<typeof getTraceAggregations>>,
{
pathParams: GetTraceAggregationsPathParameters;
data?: BodyType<SpantypesPostableTraceAggregationsDTO>;
}
> = (props) => {
const { pathParams, data } = props ?? {};
return getTraceAggregations(pathParams, data);
};
return { mutationFn, ...mutationOptions };
};
export type GetTraceAggregationsMutationResult = NonNullable<
Awaited<ReturnType<typeof getTraceAggregations>>
>;
export type GetTraceAggregationsMutationBody =
| BodyType<SpantypesPostableTraceAggregationsDTO>
| undefined;
export type GetTraceAggregationsMutationError =
ErrorType<RenderErrorResponseDTO>;
/**
* @summary Get aggregations for a trace
*/
export const useGetTraceAggregations = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof getTraceAggregations>>,
TError,
{
pathParams: GetTraceAggregationsPathParameters;
data?: BodyType<SpantypesPostableTraceAggregationsDTO>;
},
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof getTraceAggregations>>,
TError,
{
pathParams: GetTraceAggregationsPathParameters;
data?: BodyType<SpantypesPostableTraceAggregationsDTO>;
},
TContext
> => {
return useMutation(getGetTraceAggregationsMutationOptions(options));
};
/**
* Returns the waterfall view of spans for a given trace ID with tree structure, metadata, and windowed pagination
* @summary Get waterfall view for a trace

View File

@@ -120,7 +120,8 @@ export const interceptorRejected = async (
!(
response.config.url === '/sessions' && response.config.method === 'delete'
) &&
response.config.url !== '/authz/check'
response.config.url !== '/authz/check' &&
response.config.url !== '/api/v2/reset_password_tokens/verify'
) {
try {
const accessToken = getLocalStorageApi(LOCALSTORAGE.AUTH_TOKEN);

View File

@@ -1,15 +1,14 @@
import { ApiV3Instance as axios } from 'api';
import { omit } from 'lodash-es';
import { getWaterfallV4 } from 'api/generated/services/tracedetail';
import { ErrorResponse, SuccessResponse } from 'types/api';
import {
GetTraceV3PayloadProps,
GetTraceV3SuccessResponse,
GetTraceV4PayloadProps,
GetTraceV4SuccessResponse,
SpanV3,
} from 'types/api/trace/getTraceV3';
const getTraceV3 = async (
props: GetTraceV3PayloadProps,
): Promise<SuccessResponse<GetTraceV3SuccessResponse> | ErrorResponse> => {
const getTraceV4 = async (
props: GetTraceV4PayloadProps,
): Promise<SuccessResponse<GetTraceV4SuccessResponse> | ErrorResponse> => {
let uncollapsedSpans = [...props.uncollapsedSpans];
if (!props.isSelectedSpanIDUnCollapsed) {
uncollapsedSpans = uncollapsedSpans.filter(
@@ -19,31 +18,37 @@ const getTraceV3 = async (
props.selectedSpanId &&
!uncollapsedSpans.includes(props.selectedSpanId)
) {
// V3 backend only uses uncollapsedSpans list (unlike V2 which also interprets
// Backend only uses the uncollapsedSpans list (unlike V2 which also interprets
// isSelectedSpanIDUnCollapsed server-side), so explicitly add the selected span
uncollapsedSpans.push(props.selectedSpanId);
}
const postData: GetTraceV3PayloadProps = {
...props,
uncollapsedSpans,
limit: 10000,
};
const response = await axios.post<GetTraceV3SuccessResponse>(
`/traces/${props.traceId}/waterfall`,
omit(postData, 'traceId'),
const response = await getWaterfallV4(
{ traceID: props.traceId },
{
selectedSpanId: props.selectedSpanId,
uncollapsedSpans,
limit: 10000,
},
);
// V3 API wraps response in { status, data }
const rawPayload = (response.data as any).data || response.data;
// Generated client unwraps the axios response; .data is the waterfall payload.
// Wire spans carry time_unix; SpanV3's timestamp + 'service.name' are derived below.
type WireSpan = Omit<SpanV3, 'timestamp' | 'service.name'> & {
time_unix: number;
};
const rawPayload = response.data as unknown as Omit<
GetTraceV4SuccessResponse,
'spans'
> & { spans: WireSpan[] | null };
// Derive 'service.name' from resource for convenience — only derived field
const spans: SpanV3[] = (rawPayload.spans || []).map((span: any) => ({
const spans: SpanV3[] = (rawPayload.spans || []).map((span) => ({
...span,
'service.name': span.resource?.['service.name'] || '',
timestamp: span.time_unix,
}));
// V3 API returns startTimestampMillis/endTimestampMillis as relative durations (ms from epoch offset),
// API returns startTimestampMillis/endTimestampMillis as relative durations (ms from epoch offset),
// not absolute unix millis like V2. The span timestamps are absolute unix millis.
// Convert by using the first span's timestamp as the base if there's a mismatch.
let { startTimestampMillis, endTimestampMillis } = rawPayload;
@@ -70,4 +75,4 @@ const getTraceV3 = async (
};
};
export default getTraceV3;
export default getTraceV4;

View File

@@ -349,7 +349,7 @@ function convertV5DataByType(
*/
// eslint-disable-next-line sonarjs/cognitive-complexity
export function convertV5ResponseToLegacy(
v5Response: SuccessResponse<MetricRangePayloadV5>,
v5Response: SuccessResponse<MetricRangePayloadV5, QueryRangeRequestV5>,
legendMap: Record<string, string>,
formatForWeb?: boolean,
): SuccessResponse<MetricRangePayloadV3> & { warning?: Warning } {
@@ -357,7 +357,7 @@ export function convertV5ResponseToLegacy(
const v5Data = payload?.data;
const aggregationPerQuery =
(params as QueryRangeRequestV5)?.compositeQuery?.queries
params?.compositeQuery?.queries
?.filter((query) => query.type === 'builder_query')
.reduce(
(acc, query) => {

View File

@@ -1 +0,0 @@
<svg width="14" height="14" fill="none" xmlns="http://www.w3.org/2000/svg"><g clip-path="url(#prefix__clip0_4062_7291)" stroke-width="1.167" stroke-linecap="round" stroke-linejoin="round"><path d="M7 12.833A5.833 5.833 0 107 1.167a5.833 5.833 0 000 11.666z" fill="#E5484D" stroke="#E5484D"/><path d="M8.75 5.25l-3.5 3.5M5.25 5.25l3.5 3.5" stroke="#121317"/></g><defs><clipPath id="prefix__clip0_4062_7291"><path fill="#fff" d="M0 0h14v14H0z"/></clipPath></defs></svg>

Before

Width:  |  Height:  |  Size: 467 B

View File

@@ -359,8 +359,7 @@ function CustomTimePickerPopoverContent({
<Clock
color={Color.BG_ROBIN_400}
className="timezone-container__clock-icon"
height={12}
width={12}
size={14}
/>
<span className="timezone__name">{timezone.name}</span>

View File

@@ -27,13 +27,15 @@ function SortableField({
field,
onRemove,
allowDrag,
isRequired,
}: {
field: TelemetryFieldKey;
onRemove: (field: TelemetryFieldKey) => void;
allowDrag: boolean;
isRequired: boolean;
}): JSX.Element {
const { attributes, listeners, setNodeRef, transform, transition } =
useSortable({ id: field.name });
useSortable({ id: field.key as string });
const style = {
transform: CSS.Transform.toString(transform),
@@ -53,15 +55,17 @@ function SortableField({
{allowDrag && <GripVertical size={14} />}
<span className={styles.fieldKey}>{field.name}</span>
</div>
<Button
className={cx(styles.removeBtn, 'periscope-btn')}
variant="outlined"
color="destructive"
size="sm"
onClick={(): void => onRemove(field)}
>
Remove
</Button>
{!isRequired && (
<Button
className={cx(styles.removeBtn, 'periscope-btn')}
variant="outlined"
color="destructive"
size="sm"
onClick={(): void => onRemove(field)}
>
Remove
</Button>
)}
</div>
);
}
@@ -71,6 +75,7 @@ interface AddedFieldsProps {
fields: TelemetryFieldKey[];
onFieldsChange: (fields: TelemetryFieldKey[]) => void;
maxFields?: number;
requiredFields?: readonly string[];
}
function AddedFields({
@@ -78,14 +83,18 @@ function AddedFields({
fields,
onFieldsChange,
maxFields,
requiredFields = [],
}: AddedFieldsProps): JSX.Element {
const sensors = useSensors(useSensor(PointerSensor));
// Contract: caller (FieldsSelector) normalizes `fields` so every entry has
// `.key` populated. AddedFields reads it directly.
const handleDragEnd = (event: DragEndEvent): void => {
const { active, over } = event;
if (over && active.id !== over.id) {
const oldIndex = fields.findIndex((f) => f.name === active.id);
const newIndex = fields.findIndex((f) => f.name === over.id);
const oldIndex = fields.findIndex((f) => f.key === active.id);
const newIndex = fields.findIndex((f) => f.key === over.id);
onFieldsChange(arrayMove(fields, oldIndex, newIndex));
}
};
@@ -99,7 +108,7 @@ function AddedFields({
);
const handleRemove = (field: TelemetryFieldKey): void => {
onFieldsChange(fields.filter((f) => f.name !== field.name));
onFieldsChange(fields.filter((f) => f.key !== field.key));
};
const allowDrag = inputValue.length === 0;
@@ -125,16 +134,17 @@ function AddedFields({
<div className={styles.noValues}>No values found</div>
) : (
<SortableContext
items={fields.map((f) => f.name)}
items={fields.map((f) => f.key as string)}
strategy={verticalListSortingStrategy}
disabled={!allowDrag}
>
{filteredFields.map((field) => (
<SortableField
key={field.name}
key={field.key}
field={field}
onRemove={handleRemove}
allowDrag={allowDrag}
isRequired={requiredFields.includes(field.key as string)}
/>
))}
</SortableContext>

View File

@@ -76,11 +76,8 @@
min-height: 0;
overflow: hidden;
// Ant Skeleton.Input rendered inside the loading state — override its
// hard-coded width.
:global(.ant-skeleton-input) {
width: 300px;
margin: 8px 12px;
width: 50% !important;
}
}
@@ -95,7 +92,8 @@
display: flex;
align-items: center;
justify-content: space-between;
padding: 6px 12px;
height: 36px;
padding: 0 12px;
border-radius: 4px;
user-select: none;
font-size: 13px;
@@ -132,7 +130,7 @@
}
.isDragDisabled {
padding: 6px 12px;
cursor: default;
}
.otherFieldItem {

View File

@@ -5,6 +5,7 @@ import { Input } from '@signozhq/ui/input';
import useDebouncedFn from 'hooks/useDebouncedFunction';
import { Check, TableColumnsSplit, X } from '@signozhq/icons';
import { FloatingPanel } from 'periscope/components/FloatingPanel';
import { buildCompositeKey } from 'container/OptionsMenu/utils';
import { TelemetryFieldKey } from 'types/api/v5/queryRange';
import { DataSource } from 'types/common/queryBuilder';
@@ -26,33 +27,36 @@ interface FieldsSelectorProps {
onClose: () => void;
signal: DataSource;
maxFields?: number;
requiredFields?: readonly string[];
width?: number;
height?: number;
defaultPosition?: { x: number; y: number };
}
function FieldsSelector({
isOpen,
type FieldsSelectorContentProps = Omit<FieldsSelectorProps, 'isOpen'>;
// Inner component: holds all hooks + UI. Gets mounted/unmounted via the
// outer gate so opening always seeds a fresh draft from `fields`.
// Assumes `fields` arrives normalized (key populated) — see outer gate.
function FieldsSelectorContent({
title,
fields,
onFieldsChange,
onClose,
signal,
maxFields,
requiredFields,
width = DEFAULT_PANEL_WIDTH,
height,
defaultPosition,
}: FieldsSelectorProps): JSX.Element | null {
if (!isOpen) {
return null;
}
}: FieldsSelectorContentProps): JSX.Element {
const resolvedHeight =
height ?? window.innerHeight - DEFAULT_PANEL_HEIGHT_OFFSET;
const resolvedPosition = defaultPosition ?? {
x: window.innerWidth - width - DEFAULT_PANEL_RIGHT_INSET,
y: DEFAULT_PANEL_TOP_INSET,
};
const [draftFields, setDraftFields] = useState<TelemetryFieldKey[]>(fields);
const [inputValue, setInputValue] = useState('');
const [debouncedInputValue, setDebouncedInputValue] = useState('');
@@ -72,15 +76,17 @@ function FieldsSelector({
const handleAdd = useCallback(
(field: TelemetryFieldKey): void => {
if (maxFields !== undefined && draftFields.length >= maxFields) {
return;
}
if (draftFields.some((f) => f.name === field.name)) {
return;
}
setDraftFields((prev) => [...prev, field]);
setDraftFields((prev) => {
if (maxFields !== undefined && prev.length >= maxFields) {
return prev;
}
if (prev.some((f) => f.key === field.key)) {
return prev;
}
return [...prev, field];
});
},
[draftFields, maxFields],
[maxFields],
);
const handleSave = useCallback((): void => {
@@ -99,7 +105,7 @@ function FieldsSelector({
() =>
!(
draftFields.length === fields.length &&
draftFields.every((f, i) => f.name === fields[i]?.name)
draftFields.every((f, i) => f.key === fields[i]?.key)
),
[draftFields, fields],
);
@@ -138,6 +144,7 @@ function FieldsSelector({
fields={draftFields}
onFieldsChange={setDraftFields}
maxFields={maxFields}
requiredFields={requiredFields}
/>
<OtherFields
@@ -173,4 +180,27 @@ function FieldsSelector({
);
}
// Outer gate: normalizes `fields` once (populates `key` so downstream code
// can read it directly) and decides whether the inner component renders.
// When isOpen flips false→true, the inner remounts → draft state seeds fresh.
function FieldsSelector({
isOpen,
fields,
...rest
}: FieldsSelectorProps): JSX.Element | null {
const normalizedFields = useMemo<TelemetryFieldKey[]>(
() =>
fields.map((f) => ({
...f,
key: f.key ?? buildCompositeKey(f.name, f.fieldContext),
})),
[fields],
);
if (!isOpen) {
return null;
}
return <FieldsSelectorContent {...rest} fields={normalizedFields} />;
}
export default FieldsSelector;

View File

@@ -4,6 +4,7 @@ import { Skeleton } from 'antd';
import cx from 'classnames';
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import { buildCompositeKey } from 'container/OptionsMenu/utils';
import { useGetQueryKeySuggestions } from 'hooks/querySuggestions/useGetQueryKeySuggestions';
import {
FieldContext,
@@ -47,15 +48,22 @@ function OtherFields({
const otherFields: TelemetryFieldKey[] = useMemo(() => {
const suggestions = Object.values(data?.data.data.keys || {}).flat();
const addedNames = new Set(addedFields.map((f) => f.name));
return suggestions
.filter((attr) => !addedNames.has(attr.name))
.map((attr) => ({
// Normalize: synthesize `key` once so downstream reads can trust it.
const normalizedSuggestions: TelemetryFieldKey[] = suggestions.map(
(attr) => ({
...attr,
key: buildCompositeKey(attr.name, attr.fieldContext as string),
signal: attr.signal as SignalType,
fieldContext: attr.fieldContext as FieldContext,
fieldDataType: attr.fieldDataType as FieldDataType,
}));
}),
);
const addedIds = new Set(
addedFields.map((f) => f.key ?? buildCompositeKey(f.name, f.fieldContext)),
);
return normalizedSuggestions.filter(
(attr) => !addedIds.has(attr.key as string),
);
}, [data, addedFields]);
if (isFetching) {
@@ -64,8 +72,13 @@ function OtherFields({
<div className={styles.sectionHeader}>OTHER FIELDS</div>
<div className={styles.otherList}>
{Array.from({ length: 5 }).map((_, i) => (
// eslint-disable-next-line react/no-array-index-key
<Skeleton.Input active size="small" key={i} />
<div
// eslint-disable-next-line react/no-array-index-key
key={i}
className={cx(styles.fieldItem, styles.otherFieldItem)}
>
<Skeleton.Input active size="small" block />
</div>
))}
</div>
</div>
@@ -83,7 +96,7 @@ function OtherFields({
) : (
otherFields.map((attr) => (
<div
key={attr.name}
key={attr.key}
className={cx(styles.fieldItem, styles.otherFieldItem)}
>
<span className={styles.fieldKey}>{attr.name}</span>

View File

@@ -0,0 +1,111 @@
import { render, screen } from 'tests/test-utils';
import { TelemetryFieldKey } from 'types/api/v5/queryRange';
import AddedFields from '../AddedFields';
// AddedFields assumes the caller has populated `key` (the parent
// FieldsSelector does this via its normalization useMemo). Tests pre-populate
// it directly.
const makeField = (name: string, fieldContext = 'log'): TelemetryFieldKey => ({
name,
signal: 'logs',
fieldContext: fieldContext as TelemetryFieldKey['fieldContext'],
fieldDataType: 'string',
key: `${fieldContext}.${name}`,
});
describe('AddedFields — requiredFields', () => {
it('renders a Remove button for every field when no requiredFields are passed', () => {
const fields = [makeField('a'), makeField('b'), makeField('c')];
render(
<AddedFields inputValue="" fields={fields} onFieldsChange={jest.fn()} />,
);
expect(screen.getAllByRole('button', { name: /remove/i })).toHaveLength(3);
});
it('hides the Remove button for fields whose composite key is in requiredFields', () => {
const fields = [makeField('a'), makeField('b'), makeField('c')];
render(
<AddedFields
inputValue=""
fields={fields}
onFieldsChange={jest.fn()}
requiredFields={['log.a', 'log.c']}
/>,
);
// Only 'b' is removable.
const removeButtons = screen.getAllByRole('button', { name: /remove/i });
expect(removeButtons).toHaveLength(1);
});
it('still renders the field name for required fields', () => {
const fields = [makeField('a'), makeField('b')];
render(
<AddedFields
inputValue=""
fields={fields}
onFieldsChange={jest.fn()}
requiredFields={['log.a']}
/>,
);
expect(screen.getByText('a')).toBeInTheDocument();
expect(screen.getByText('b')).toBeInTheDocument();
});
it('locks ONLY the canonical variant — a same-name field from another context stays removable', () => {
// Two `body` fields with different contexts. requiredFields holds the
// canonical composite key only, so the attribute variant is deletable.
const fields = [makeField('body', 'log'), makeField('body', 'attribute')];
render(
<AddedFields
inputValue=""
fields={fields}
onFieldsChange={jest.fn()}
requiredFields={['log.body']}
/>,
);
// One Remove button: the attribute variant. log variant is locked.
expect(screen.getAllByRole('button', { name: /remove/i })).toHaveLength(1);
});
it('does not lock anything when a bare name is passed (composite key required)', () => {
// Bare `body` no longer matches — matching is composite-key only now.
const fields = [makeField('body', 'log'), makeField('body', 'attribute')];
render(
<AddedFields
inputValue=""
fields={fields}
onFieldsChange={jest.fn()}
requiredFields={['body']}
/>,
);
// Neither variant locked → both removable.
expect(screen.getAllByRole('button', { name: /remove/i })).toHaveLength(2);
});
it('treats requiredFields as exact composite-key match (substring does not lock)', () => {
const fields = [makeField('body'), makeField('body_extra')];
render(
<AddedFields
inputValue=""
fields={fields}
onFieldsChange={jest.fn()}
requiredFields={['log.body']}
/>,
);
// 'log.body' locked, 'log.body_extra' removable.
expect(screen.getAllByRole('button', { name: /remove/i })).toHaveLength(1);
});
});

View File

@@ -4,6 +4,7 @@ import { Dot } from '@signozhq/icons';
import { Button } from '@signozhq/ui/button';
import { TooltipSimple } from '@signozhq/ui/tooltip';
import Noz from 'components/Noz/Noz';
import { NOZ_TOOLTIP_TITLE } from 'components/Noz/Noz.constants';
import { Popover } from 'antd';
import logEvent from 'api/common/logEvent';
import { AIAssistantEvents } from 'container/AIAssistant/events';
@@ -109,7 +110,7 @@ function HeaderRightSection({
</span>
) : null}
<TooltipSimple title="Noz">
<TooltipSimple title={NOZ_TOOLTIP_TITLE}>
<Button
variant="solid"
color="secondary"

View File

@@ -3,17 +3,12 @@ import { useLocation } from 'react-router-dom';
import { toast } from '@signozhq/ui/sonner';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import logEvent from 'api/common/logEvent';
import { logEventMock } from '__tests__/logEventMock';
import { handleContactSupport } from 'container/Integrations/utils';
import { useGetTenantLicense } from 'hooks/useGetTenantLicense';
import FeedbackModal from '../FeedbackModal';
jest.mock('api/common/logEvent', () => ({
__esModule: true,
default: jest.fn(() => Promise.resolve()),
}));
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useLocation: jest.fn(),
@@ -35,7 +30,6 @@ jest.mock('container/Integrations/utils', () => ({
handleContactSupport: jest.fn(),
}));
const mockLogEvent = logEvent as jest.MockedFunction<typeof logEvent>;
const mockUseLocation = useLocation as jest.Mock;
const mockUseGetTenantLicense = useGetTenantLicense as jest.Mock;
const mockHandleContactSupport = handleContactSupport as jest.Mock;
@@ -50,6 +44,7 @@ const mockLocation = {
describe('FeedbackModal', () => {
beforeEach(() => {
jest.clearAllMocks();
logEventMock.mockReturnValue(Promise.resolve() as never);
mockUseLocation.mockReturnValue(mockLocation);
mockUseGetTenantLicense.mockReturnValue({
isCloudUser: false,
@@ -116,7 +111,7 @@ describe('FeedbackModal', () => {
await user.type(textarea, testFeedback);
await user.click(submitButton);
expect(mockLogEvent).toHaveBeenCalledWith('Feedback: Submitted', {
expect(logEventMock).toHaveBeenCalledWith('Feedback: Submitted', {
data: testFeedback,
type: 'feedback',
page: mockLocation.pathname,
@@ -149,7 +144,7 @@ describe('FeedbackModal', () => {
await user.type(textarea, testFeedback);
await user.click(submitButton);
expect(mockLogEvent).toHaveBeenCalledWith('Feedback: Submitted', {
expect(logEventMock).toHaveBeenCalledWith('Feedback: Submitted', {
data: testFeedback,
type: 'reportBug',
page: mockLocation.pathname,
@@ -182,7 +177,7 @@ describe('FeedbackModal', () => {
await user.type(textarea, testFeedback);
await user.click(submitButton);
expect(mockLogEvent).toHaveBeenCalledWith('Feedback: Submitted', {
expect(logEventMock).toHaveBeenCalledWith('Feedback: Submitted', {
data: testFeedback,
type: 'featureRequest',
page: mockLocation.pathname,

View File

@@ -2,16 +2,11 @@
import { useLocation } from 'react-router-dom';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import logEvent from 'api/common/logEvent';
import { logEventMock } from '__tests__/logEventMock';
import { useGetTenantLicense } from 'hooks/useGetTenantLicense';
import HeaderRightSection from '../HeaderRightSection';
jest.mock('api/common/logEvent', () => ({
__esModule: true,
default: jest.fn(),
}));
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useLocation: jest.fn(),
@@ -50,7 +45,6 @@ jest.mock('hooks/useIsAIAssistantEnabled', () => ({
useIsAIAssistantEnabled: (): boolean => false,
}));
const mockLogEvent = logEvent as jest.Mock;
const mockUseLocation = useLocation as jest.Mock;
const mockUseGetTenantLicense = useGetTenantLicense as jest.Mock;
@@ -120,7 +114,7 @@ describe('HeaderRightSection', () => {
await user.click(feedbackButton!);
expect(mockLogEvent).toHaveBeenCalledWith('Feedback: Clicked', {
expect(logEventMock).toHaveBeenCalledWith('Feedback: Clicked', {
page: mockLocation.pathname,
});
expect(screen.getByTestId('feedback-modal')).toBeInTheDocument();
@@ -133,7 +127,7 @@ describe('HeaderRightSection', () => {
const shareButton = screen.getByRole('button', { name: /share/i });
await user.click(shareButton);
expect(mockLogEvent).toHaveBeenCalledWith('Share: Clicked', {
expect(logEventMock).toHaveBeenCalledWith('Share: Clicked', {
page: mockLocation.pathname,
});
expect(screen.getByTestId('share-modal')).toBeInTheDocument();
@@ -150,7 +144,7 @@ describe('HeaderRightSection', () => {
await user.click(announcementsButton!);
expect(mockLogEvent).toHaveBeenCalledWith('Announcements: Clicked', {
expect(logEventMock).toHaveBeenCalledWith('Announcements: Clicked', {
page: mockLocation.pathname,
});
});

View File

@@ -5,18 +5,13 @@ import { matchPath, useLocation } from 'react-router-dom';
import { useCopyToClipboard } from 'react-use';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import logEvent from 'api/common/logEvent';
import { logEventMock } from '__tests__/logEventMock';
import ROUTES from 'constants/routes';
import useUrlQuery from 'hooks/useUrlQuery';
import GetMinMax from 'lib/getMinMax';
import ShareURLModal from '../ShareURLModal';
jest.mock('api/common/logEvent', () => ({
__esModule: true,
default: jest.fn(),
}));
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useLocation: jest.fn(),
@@ -53,7 +48,6 @@ Object.defineProperty(window, 'location', {
writable: true,
});
const mockLogEvent = logEvent as jest.Mock;
const mockUseLocation = useLocation as jest.Mock;
const mockUseUrlQuery = useUrlQuery as jest.Mock;
const mockUseSelector = useSelector as jest.Mock;
@@ -125,7 +119,7 @@ describe('ShareURLModal', () => {
await user.click(copyButton);
expect(mockHandleCopyToClipboard).toHaveBeenCalled();
expect(mockLogEvent).toHaveBeenCalledWith('Share: Copy link clicked', {
expect(logEventMock).toHaveBeenCalledWith('Share: Copy link clicked', {
page: TEST_PATH,
URL: expect.any(String),
});

View File

@@ -0,0 +1,145 @@
import { renderHook } from '@testing-library/react';
import { FontSize } from 'container/OptionsMenu/types';
import { IField } from 'types/api/logs/fields';
import { useLogsTableColumns } from '../useLogsTableColumns';
jest.mock('providers/Timezone', () => ({
useTimezone: (): { formatTimezoneAdjustedTimestamp: jest.Mock } => ({
formatTimezoneAdjustedTimestamp: jest.fn(() => 'TS'),
}),
}));
const field = (name: string, type = ''): IField => ({
name,
type,
dataType: 'string',
});
describe('useLogsTableColumns — selectColumns-order respected', () => {
it('prepends stateIndicator and renders user fields in array order', () => {
const { result } = renderHook(() =>
useLogsTableColumns({
fields: [field('c'), field('a'), field('b')],
fontSize: FontSize.SMALL,
}),
);
expect(result.current.map((c) => c.id)).toStrictEqual([
'state-indicator',
'c',
'a',
'b',
]);
});
it('slots body and timestamp at their position in the fields array', () => {
const { result } = renderHook(() =>
useLogsTableColumns({
fields: [
field('service.name'),
field('body', 'log'),
field('request.id'),
field('timestamp', 'log'),
],
fontSize: FontSize.SMALL,
}),
);
// body/timestamp appear where the caller placed them, keyed by their
// composite IDs ('log.*'); contextless user fields collapse to bare name.
expect(result.current.map((c) => c.id)).toStrictEqual([
'state-indicator',
'service.name',
'log.body',
'request.id',
'log.timestamp',
]);
});
it('renders a same-name field from another context as a DISTINCT column (no collision)', () => {
const { result } = renderHook(() =>
useLogsTableColumns({
fields: [field('body', 'log'), field('body', 'attribute')],
fontSize: FontSize.SMALL,
}),
);
const byId = new Map(result.current.map((c) => [c.id, c]));
// Attribute variant is its own column, not a duplicate 'log.body'.
expect(result.current.map((c) => c.id)).toStrictEqual([
'state-indicator',
'log.body',
'attribute.body',
]);
expect(byId.get('log.body')?.enableRemove).toBe(false);
expect(byId.get('attribute.body')?.enableRemove).toBe(true);
});
it('applies the same distinct-column treatment to timestamp variants', () => {
const { result } = renderHook(() =>
useLogsTableColumns({
fields: [field('timestamp', 'log'), field('timestamp', 'attribute')],
fontSize: FontSize.SMALL,
}),
);
const byId = new Map(result.current.map((c) => [c.id, c]));
expect(result.current.map((c) => c.id)).toStrictEqual([
'state-indicator',
'log.timestamp',
'attribute.timestamp',
]);
expect(byId.get('log.timestamp')?.enableRemove).toBe(false);
expect(byId.get('attribute.timestamp')?.enableRemove).toBe(true);
});
it('skips the synthetic "id" field name', () => {
const { result } = renderHook(() =>
useLogsTableColumns({
fields: [field('id'), field('a'), field('b')],
fontSize: FontSize.SMALL,
}),
);
expect(result.current.map((c) => c.id)).toStrictEqual([
'state-indicator',
'a',
'b',
]);
});
it('uses the special body/timestamp coldefs (canBeHidden=false), not the generic user field def', () => {
const { result } = renderHook(() =>
useLogsTableColumns({
fields: [
field('body', 'log'),
field('timestamp', 'log'),
field('user_field'),
],
fontSize: FontSize.SMALL,
}),
);
const byId = new Map(result.current.map((c) => [c.id, c]));
// body + timestamp are locked from the table-X removal pathway.
expect(byId.get('log.body')?.canBeHidden).toBe(false);
expect(byId.get('log.body')?.enableRemove).toBe(false);
expect(byId.get('log.timestamp')?.canBeHidden).toBe(false);
expect(byId.get('log.timestamp')?.enableRemove).toBe(false);
// User-added fields stay removable. User field has type='' so composite
// collapses to bare name.
expect(byId.get('user_field')?.enableRemove).toBe(true);
});
it('renders only the stateIndicator when fields is empty', () => {
const { result } = renderHook(() =>
useLogsTableColumns({
fields: [],
fontSize: FontSize.SMALL,
}),
);
expect(result.current.map((c) => c.id)).toStrictEqual(['state-indicator']);
});
});

View File

@@ -7,6 +7,7 @@ import {
getSanitizedLogBody,
} from 'container/LogDetailedView/utils';
import { FontSize } from 'container/OptionsMenu/types';
import { buildCompositeKey } from 'container/OptionsMenu/utils';
import { FlatLogData } from 'lib/logs/flatLogData';
import { useTimezone } from 'providers/Timezone';
import { IField } from 'types/api/logs/fields';
@@ -18,13 +19,11 @@ import LogStateIndicator from '../LogStateIndicator/LogStateIndicator';
type UseLogsTableColumnsProps = {
fields: IField[];
fontSize: FontSize;
appendTo?: 'center' | 'end';
};
export function useLogsTableColumns({
fields,
fontSize,
appendTo = 'center',
}: UseLogsTableColumnsProps): TableColumnDef<ILog>[] {
const { formatTimezoneAdjustedTimestamp } = useTimezone();
@@ -47,71 +46,74 @@ export function useLogsTableColumns({
),
};
const fieldColumns: TableColumnDef<ILog>[] = fields
.filter((f): boolean => !['id', 'body', 'timestamp'].includes(f.name))
.map(
(f): TableColumnDef<ILog> => ({
id: f.name,
header: f.name,
accessorFn: (log): unknown => FlatLogData(log)[f.name],
enableRemove: true,
width: { min: 192 },
cell: ({ value }): ReactElement => (
<TanStackTable.Text>{String(value ?? '')}</TanStackTable.Text>
),
}),
);
const timestampCol: TableColumnDef<ILog> = {
id: buildCompositeKey('timestamp', 'log'),
header: 'Timestamp',
accessorFn: (log): unknown => log.timestamp,
canBeHidden: false,
enableRemove: false,
width: { default: 170, min: 170 },
cell: ({ value }): ReactElement => {
const ts = value as string | number;
const formatted =
typeof ts === 'string'
? formatTimezoneAdjustedTimestamp(ts, DATE_TIME_FORMATS.ISO_DATETIME_MS)
: formatTimezoneAdjustedTimestamp(
ts / 1e6,
DATE_TIME_FORMATS.ISO_DATETIME_MS,
);
return <TanStackTable.Text>{formatted}</TanStackTable.Text>;
},
};
const timestampCol: TableColumnDef<ILog> | null = fields.some(
(f) => f.name === 'timestamp',
)
? {
id: 'timestamp',
header: 'Timestamp',
accessorFn: (log): unknown => log.timestamp,
width: { default: 170, min: 170 },
cell: ({ value }): ReactElement => {
const ts = value as string | number;
const formatted =
typeof ts === 'string'
? formatTimezoneAdjustedTimestamp(ts, DATE_TIME_FORMATS.ISO_DATETIME_MS)
: formatTimezoneAdjustedTimestamp(
ts / 1e6,
DATE_TIME_FORMATS.ISO_DATETIME_MS,
);
return <TanStackTable.Text>{formatted}</TanStackTable.Text>;
},
const bodyCol: TableColumnDef<ILog> = {
id: buildCompositeKey('body', 'log'),
header: 'Body',
accessorFn: (log): string => getBodyDisplayString(log.body),
canBeHidden: false,
enableRemove: false,
width: { default: '100%', min: 300 },
cell: ({ value, isActive }): ReactElement => (
<TanStackTable.Text
dangerouslySetInnerHTML={{
__html: getSanitizedLogBody(value as string, {
shouldEscapeHtml: true,
}),
}}
data-active={isActive}
/>
),
};
const makeUserFieldCol = (f: IField): TableColumnDef<ILog> => ({
id: buildCompositeKey(f.name, f.type),
header: f.name,
accessorFn: (log): unknown => FlatLogData(log)[f.name],
enableRemove: true,
width: { min: 192 },
cell: ({ value }): ReactElement => (
<TanStackTable.Text>{String(value ?? '')}</TanStackTable.Text>
),
});
// Match body/timestamp by composite key, not bare name — else a variant
// like `attribute.body` collapses onto `log.body`, duplicating the column.
const fieldCols = fields
.map((f): TableColumnDef<ILog> | null => {
if (f.name === 'id') {
return null;
}
: null;
const bodyCol: TableColumnDef<ILog> | null = fields.some(
(f) => f.name === 'body',
)
? {
id: 'body',
header: 'Body',
accessorFn: (log): string => getBodyDisplayString(log.body),
canBeHidden: false,
width: { default: '100%', min: 300 },
cell: ({ value, isActive }): ReactElement => (
<TanStackTable.Text
dangerouslySetInnerHTML={{
__html: getSanitizedLogBody(value as string, {
shouldEscapeHtml: true,
}),
}}
data-active={isActive}
/>
),
const compositeKey = buildCompositeKey(f.name, f.type);
if (compositeKey === timestampCol.id) {
return timestampCol;
}
: null;
if (compositeKey === bodyCol.id) {
return bodyCol;
}
return makeUserFieldCol(f);
})
.filter((c): c is TableColumnDef<ILog> => c !== null);
return [
stateIndicatorCol,
...(timestampCol ? [timestampCol] : []),
...(appendTo === 'center' ? fieldColumns : []),
...(bodyCol ? [bodyCol] : []),
...(appendTo === 'end' ? fieldColumns : []),
];
}, [fields, appendTo, fontSize, formatTimezoneAdjustedTimestamp]);
return [stateIndicatorCol, ...fieldCols];
}, [fields, fontSize, formatTimezoneAdjustedTimestamp]);
}

View File

@@ -256,6 +256,34 @@
}
}
.edit-columns-container {
padding: 12px;
.edit-columns-btn {
display: flex;
width: 100%;
height: 20px;
padding: 4px 0px;
justify-content: space-between;
align-items: center;
border: none !important;
.edit-columns-text {
color: var(--l2-foreground);
font-family: Inter;
font-size: 13px;
font-style: normal;
font-weight: 400;
line-height: normal;
letter-spacing: 0.14px;
}
}
.edit-columns-btn:hover {
background-color: unset !important;
}
}
.add-new-column-container {
display: flex;
flex-direction: column;

View File

@@ -1,12 +1,9 @@
import { useCallback, useEffect, useRef, useState } from 'react';
import { Input } from '@signozhq/ui/input';
import { useCallback, useEffect, useState } from 'react';
import { Button, InputNumber, Popover, Tooltip } from 'antd';
import { Typography } from '@signozhq/ui/typography';
import type { DefaultOptionType } from 'antd/es/select';
import cx from 'classnames';
import { LogViewMode } from 'container/LogsTable';
import { FontSize, OptionsMenuConfig } from 'container/OptionsMenu/types';
import useDebouncedFn from 'hooks/useDebouncedFunction';
import {
Check,
ChevronLeft,
@@ -14,7 +11,6 @@ import {
Minus,
Plus,
SlidersVertical,
X,
} from '@signozhq/icons';
import './LogsFormatOptionsMenu.styles.scss';
@@ -23,14 +19,21 @@ interface LogsFormatOptionsMenuProps {
items: any;
selectedOptionFormat: any;
config: OptionsMenuConfig;
onOpenColumns?: () => void;
}
interface OptionsMenuContentProps extends LogsFormatOptionsMenuProps {
closePopover: () => void;
}
function OptionsMenu({
items,
selectedOptionFormat,
config,
}: LogsFormatOptionsMenuProps): JSX.Element {
const { maxLines, format, addColumn, fontSize } = config;
onOpenColumns,
closePopover,
}: OptionsMenuContentProps): JSX.Element {
const { maxLines, format, fontSize } = config;
const [selectedItem, setSelectedItem] = useState(selectedOptionFormat);
const maxLinesNumber = (maxLines?.value as number) || 1;
const [maxLinesPerRow, setMaxLinesPerRow] = useState<number>(maxLinesNumber);
@@ -40,13 +43,6 @@ function OptionsMenu({
const [isFontSizeOptionsOpen, setIsFontSizeOptionsOpen] =
useState<boolean>(false);
const [showAddNewColumnContainer, setShowAddNewColumnContainer] =
useState(false);
const [selectedValue, setSelectedValue] = useState<string | null>(null);
const listRef = useRef<HTMLDivElement>(null);
const initialMouseEnterRef = useRef<boolean>(false);
const onChange = useCallback(
(key: LogViewMode) => {
if (!format) {
@@ -61,7 +57,6 @@ function OptionsMenu({
const handleMenuItemClick = (key: LogViewMode): void => {
setSelectedItem(key);
onChange(key);
setShowAddNewColumnContainer(false);
};
const incrementMaxLinesPerRow = (): void => {
@@ -76,20 +71,6 @@ function OptionsMenu({
}
};
const handleSearchValueChange = useDebouncedFn((event): void => {
// @ts-expect-error
const value = event?.target?.value || '';
if (addColumn && addColumn?.onSearch) {
addColumn?.onSearch(value);
}
}, 300);
const handleToggleAddNewColumn = (): void => {
addColumn?.onSearch?.('');
setShowAddNewColumnContainer(!showAddNewColumnContainer);
};
const handleLinesPerRowChange = (maxLinesPerRow: number | null): void => {
if (
maxLinesPerRow &&
@@ -100,6 +81,11 @@ function OptionsMenu({
}
};
const handleEditColumns = (): void => {
onOpenColumns?.();
closePopover();
};
useEffect(() => {
if (maxLinesPerRow && config && config.maxLines?.onChange) {
config.maxLines.onChange(maxLinesPerRow);
@@ -112,110 +98,10 @@ function OptionsMenu({
}
}, [fontSizeValue]);
function handleColumnSelection(
currentIndex: number,
optionsData: DefaultOptionType[],
): void {
const currentItem = optionsData[currentIndex];
const itemLength = optionsData.length;
if (addColumn && addColumn?.onSelect) {
addColumn?.onSelect(selectedValue, {
label: currentItem.label,
disabled: false,
});
// if the last element is selected then select the previous one
if (currentIndex === itemLength - 1) {
// there should be more than 1 element in the list
if (currentIndex - 1 >= 0) {
const prevValue = optionsData[currentIndex - 1]?.value || null;
setSelectedValue(prevValue as string | null);
} else {
// if there is only one element then just select and do nothing
setSelectedValue(null);
}
} else {
// selecting any random element from the list except the last one
const nextIndex = currentIndex + 1;
const nextValue = optionsData[nextIndex]?.value || null;
setSelectedValue(nextValue as string | null);
}
}
}
const handleKeyDown = (e: KeyboardEvent): void => {
if (!selectedValue) {
return;
}
const optionsData = addColumn?.options || [];
const currentIndex = optionsData.findIndex(
(item) => item?.value === selectedValue,
);
const itemLength = optionsData.length;
switch (e.key) {
case 'ArrowUp': {
const newValue = optionsData[Math.max(0, currentIndex - 1)]?.value;
setSelectedValue(newValue as string | null);
e.preventDefault();
break;
}
case 'ArrowDown': {
const newValue =
optionsData[Math.min(itemLength - 1, currentIndex + 1)]?.value;
setSelectedValue(newValue as string | null);
e.preventDefault();
break;
}
case 'Enter':
e.preventDefault();
handleColumnSelection(currentIndex, optionsData);
break;
default:
break;
}
};
useEffect(() => {
// Scroll the selected item into view
const listNode = listRef.current;
if (listNode && selectedValue) {
const optionsData = addColumn?.options || [];
const currentIndex = optionsData.findIndex(
(item) => item?.value === selectedValue,
);
const itemNode = listNode.children[currentIndex] as HTMLElement;
if (itemNode) {
itemNode.scrollIntoView({
behavior: 'smooth',
block: 'nearest',
});
}
}
}, [selectedValue]);
useEffect(() => {
window.addEventListener('keydown', handleKeyDown);
return (): void => {
window.removeEventListener('keydown', handleKeyDown);
};
}, [selectedValue]);
return (
<div
className={cx(
'nested-menu-container',
showAddNewColumnContainer ? 'active' : '',
)}
className="nested-menu-container"
onClick={(event): void => {
// this is to restrict click events to propogate to parent
event.stopPropagation();
}}
>
@@ -269,71 +155,7 @@ function OptionsMenu({
</Button>
</div>
</div>
) : null}
{showAddNewColumnContainer && (
<div className="add-new-column-container">
<div className="add-new-column-header">
<div className="title">
<div className="periscope-btn ghost" onClick={handleToggleAddNewColumn}>
<ChevronLeft
size={14}
className="back-icon"
onClick={handleToggleAddNewColumn}
/>
</div>
Add New Column
</div>
<Input
tabIndex={0}
type="text"
autoFocus
onFocus={addColumn?.onFocus}
onChange={handleSearchValueChange}
placeholder="Search..."
/>
</div>
<div className="add-new-column-content">
{addColumn?.isFetching && (
<div className="loading-container"> Loading ... </div>
)}
<div className="column-format-new-options" ref={listRef}>
{addColumn?.options?.map(({ label, value }, index) => (
<div
className={cx('column-name', value === selectedValue && 'selected')}
key={value}
onMouseEnter={(): void => {
if (!initialMouseEnterRef.current) {
setSelectedValue(value as string | null);
}
initialMouseEnterRef.current = true;
}}
onMouseMove={(): void => {
// this is added to handle the mouse move explicit event and not the re-rendered on mouse enter event
setSelectedValue(value as string | null);
}}
onClick={(eve): void => {
eve.stopPropagation();
handleColumnSelection(index, addColumn?.options || []);
}}
>
<div className="name">
<Tooltip placement="left" title={label}>
{label}
</Tooltip>
</div>
</div>
))}
</div>
</div>
</div>
)}
{!isFontSizeOptionsOpen && !showAddNewColumnContainer && (
) : (
<div>
<div className="font-size-container">
<div className="title">Font Size</div>
@@ -373,72 +195,52 @@ function OptionsMenu({
{selectedItem && (
<>
<>
<div className="horizontal-line" />
<div className="max-lines-per-row">
<div className="title"> max lines per row </div>
<div className="raw-format max-lines-per-row-input">
<button
type="button"
className="periscope-btn"
onClick={decrementMaxLinesPerRow}
>
{' '}
<Minus size={12} />{' '}
</button>
<InputNumber
min={1}
max={10}
value={maxLinesPerRow}
onChange={handleLinesPerRowChange}
/>
<button
type="button"
className="periscope-btn"
onClick={incrementMaxLinesPerRow}
>
{' '}
<Plus size={12} />{' '}
</button>
</div>
<div className="horizontal-line" />
<div className="max-lines-per-row">
<div className="title"> max lines per row </div>
<div className="raw-format max-lines-per-row-input">
<button
type="button"
className="periscope-btn"
onClick={decrementMaxLinesPerRow}
>
{' '}
<Minus size={12} />{' '}
</button>
<InputNumber
min={1}
max={10}
value={maxLinesPerRow}
onChange={handleLinesPerRowChange}
/>
<button
type="button"
className="periscope-btn"
onClick={incrementMaxLinesPerRow}
>
{' '}
<Plus size={12} />{' '}
</button>
</div>
</>
</div>
</>
)}
<div className="selected-item-content-container active">
{!showAddNewColumnContainer && <div className="horizontal-line" />}
<div className="item-content">
{!showAddNewColumnContainer && (
<div className="title">
columns
<Plus size={14} onClick={handleToggleAddNewColumn} />{' '}
</div>
)}
<div className="column-format">
{addColumn?.value?.map(({ name }) => (
<div className="column-name" key={name}>
<div className="name">
<Tooltip placement="left" title={name}>
{name}
</Tooltip>
</div>
{addColumn?.value?.length > 1 && (
<X
className="delete-btn"
size={14}
onClick={(): void => addColumn.onRemove(name)}
/>
)}
</div>
))}
{addColumn && addColumn?.value?.length === 0 && (
<div className="column-name no-columns-selected">
No columns selected
</div>
)}
</div>
</div>
{selectedItem === 'table' && onOpenColumns && (
<>
<div className="horizontal-line" />
<div className="edit-columns-container">
<Button
className="edit-columns-btn"
type="text"
onClick={handleEditColumns}
data-testid="periscope-btn-edit-columns"
>
<Typography.Text className="edit-columns-text">
Edit columns
</Typography.Text>
<ChevronRight size={14} className="icon" />
</Button>
</div>
</>
)}
@@ -452,6 +254,7 @@ function LogsFormatOptionsMenu({
items,
selectedOptionFormat,
config,
onOpenColumns,
}: LogsFormatOptionsMenuProps): JSX.Element {
const [isPopoverOpen, setIsPopoverOpen] = useState<boolean>(false);
return (
@@ -461,6 +264,8 @@ function LogsFormatOptionsMenu({
items={items}
selectedOptionFormat={selectedOptionFormat}
config={config}
onOpenColumns={onOpenColumns}
closePopover={(): void => setIsPopoverOpen(false)}
/>
}
trigger="click"

View File

@@ -62,6 +62,7 @@ describe('LogsFormatOptionsMenu (unit)', () => {
onSearch: jest.fn(),
onSelect: jest.fn(),
onRemove: jest.fn(),
onReorder: jest.fn(),
},
}}
/>,
@@ -154,4 +155,66 @@ describe('LogsFormatOptionsMenu (unit)', () => {
expect(fontSizeOnChange).toHaveBeenCalledWith(FontSize.MEDIUM);
});
});
function renderWithOnOpen(
onOpenColumns?: jest.Mock,
selectedOptionFormat: 'table' | 'raw' | 'list' = 'table',
): { getByTestId: ReturnType<typeof render>['getByTestId'] } {
const items = [
{ key: 'raw', label: 'Raw', data: { title: 'max lines per row' } },
{ key: 'list', label: 'Default' },
{ key: 'table', label: 'Column', data: { title: 'columns' } },
];
const { getByTestId } = render(
<LogsFormatOptionsMenu
items={items}
selectedOptionFormat={selectedOptionFormat}
config={{
format: { value: selectedOptionFormat, onChange: jest.fn() },
maxLines: { value: 1, onChange: jest.fn() },
fontSize: { value: FontSize.SMALL, onChange: jest.fn() },
}}
onOpenColumns={onOpenColumns}
/>,
);
fireEvent.click(getByTestId('periscope-btn-format-options'));
return { getByTestId };
}
it('renders "Edit columns" row when format=table and onOpenColumns provided', () => {
const onOpenColumns = jest.fn();
const { getByTestId } = renderWithOnOpen(onOpenColumns, 'table');
expect(getByTestId('periscope-btn-edit-columns')).toBeInTheDocument();
});
it('does not render "Edit columns" row when onOpenColumns is not provided', () => {
renderWithOnOpen(undefined, 'table');
expect(
document.querySelector('[data-testid="periscope-btn-edit-columns"]'),
).toBeNull();
});
it('does not render "Edit columns" row when format is not table', () => {
renderWithOnOpen(jest.fn(), 'raw');
expect(
document.querySelector('[data-testid="periscope-btn-edit-columns"]'),
).toBeNull();
});
it('fires onOpenColumns and closes the popover when "Edit columns" is clicked', async () => {
const onOpenColumns = jest.fn();
const { getByTestId } = renderWithOnOpen(onOpenColumns, 'table');
fireEvent.click(getByTestId('periscope-btn-edit-columns'));
expect(onOpenColumns).toHaveBeenCalledTimes(1);
await waitFor(() => {
// Popover content unmounts on close.
expect(document.querySelector('.menu-container')).toBeNull();
});
});
});

View File

@@ -109,6 +109,16 @@ $custom-border-color: #2c3044;
color: color-mix(in srgb, var(--l2-foreground) 45%, transparent);
}
.ant-select-clear {
background-color: var(--l2-background);
color: var(--l2-foreground);
font-size: 12px;
&:hover {
color: var(--l1-foreground);
}
}
// Customize tags in multiselect (dark mode by default)
.ant-select-selection-item {
background-color: var(--l1-border);
@@ -392,7 +402,9 @@ $custom-border-color: #2c3044;
// Custom dropdown styles for multi-select
.custom-multiselect-dropdown {
padding: 8px 0 0 0;
max-height: 350px;
// Tall enough to hold the react-virtuoso list (<=300px) + header/footer
// so only the list scrolls (avoids a second scrollbar on this container).
max-height: 410px;
overflow-y: auto;
overflow-x: hidden;
scrollbar-width: thin;
@@ -483,8 +495,12 @@ $custom-border-color: #2c3044;
.option-checkbox {
width: 100%;
> span:not(.ant-checkbox) {
width: 100%;
// @signozhq/ui Checkbox renders children inside a <label> that is
// content-sized by default. Make it fill the row (min-width: 0 lets it
// shrink) so the option text below can truncate instead of overflowing.
> label {
flex: 1 1 auto;
min-width: 0;
}
.all-option-text {
@@ -501,7 +517,12 @@ $custom-border-color: #2c3044;
width: 100%;
.option-label-text {
flex: 1 1 auto;
min-width: 0;
margin-bottom: 0;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.option-badge {
@@ -514,26 +535,30 @@ $custom-border-color: #2c3044;
}
}
.only-btn {
display: none;
}
// Size the buttons to the row's resting content height (20px) and fully
// override antd's default 32px Button box, so revealing them on hover
// never changes the row height.
.only-btn,
.toggle-btn {
display: none;
}
align-items: center;
justify-content: center;
height: 18px;
min-height: 0;
padding: 0 6px;
font-size: 12px;
line-height: 1;
border: none;
box-shadow: none;
.only-btn:hover {
background-color: unset;
}
.toggle-btn:hover {
background-color: unset;
&:hover {
background-color: unset;
}
}
.option-content:hover {
.only-btn {
display: flex;
align-items: center;
justify-content: center;
height: 21px;
}
.toggle-btn {
display: none;
@@ -548,9 +573,6 @@ $custom-border-color: #2c3044;
.option-checkbox:hover {
.toggle-btn {
display: flex;
align-items: center;
justify-content: center;
height: 21px;
}
.option-badge {
display: none;

View File

@@ -0,0 +1,2 @@
/** Shared hover copy for every Noz entry point (header, floating trigger, sidebar). */
export const NOZ_TOOLTIP_TITLE = 'Noz, your AI teammate';

View File

@@ -721,6 +721,53 @@ export const removeKeysFromExpression = (
return result?.text ?? '';
};
const escapeRegExp = (value: string): string =>
value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
export const createVariablePlaceholderRegExp = (
variableName: string,
): RegExp => {
const escapedName = escapeRegExp(variableName);
// (?![\w.]) prevents $env from matching inside $environment or $env.attr
return new RegExp(
`(\\$${escapedName}(?![\\w.])|\\{\\{\\s*\\.?${escapedName}\\s*\\}\\}|\\[\\[\\s*${escapedName}\\s*\\]\\])`,
'g',
);
};
const matchesVariablePlaceholder = (
text: string,
variableName: string,
): boolean => createVariablePlaceholderRegExp(variableName).test(text);
export const removeVariableFromExpression = (
expression: string | undefined,
variableName: string,
): string => {
if (!expression) {
return '';
}
const queryPairs = extractQueryPairs(expression);
const keysToRemove = queryPairs
.filter((pair) => {
const singleValue = pair.value?.toString() ?? '';
const listValues = (pair.valueList ?? []).join(' ');
return (
matchesVariablePlaceholder(singleValue, variableName) ||
matchesVariablePlaceholder(listValues, variableName)
);
})
.map((pair) => pair.key);
if (keysToRemove.length === 0) {
return expression;
}
return removeKeysFromExpression(expression, keysToRemove, `$${variableName}`);
};
/**
* Convert old having format to new having format
* @param having - Array of old having objects with columnName, op, and value

View File

@@ -1,17 +1,17 @@
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Button, Tooltip } from 'antd';
import refreshPaymentStatus from 'api/v3/licenses/put';
import cx from 'classnames';
import { Button } from '@signozhq/ui/button';
import { TooltipSimple } from '@signozhq/ui/tooltip';
import { RefreshCcw } from '@signozhq/icons';
import { useAppContext } from 'providers/App/App';
function RefreshPaymentStatus({
btnShape,
type,
className,
}: {
btnShape?: 'default' | 'round' | 'circle';
type?: 'button' | 'text' | 'tooltip';
className?: string;
}): JSX.Element {
const { t } = useTranslation(['failedPayment']);
const { activeLicenseRefetch } = useAppContext();
@@ -31,26 +31,33 @@ function RefreshPaymentStatus({
setIsLoading(false);
};
const button = (
<Button
variant="link"
color={type === 'text' ? 'none' : 'secondary'}
size="md"
className={className}
onClick={handleRefreshPaymentStatus}
prefix={<RefreshCcw size={14} />}
loading={isLoading}
>
{type !== 'tooltip' ? t('refreshPaymentStatus') : ''}
</Button>
);
return (
<span className="refresh-payment-status-btn-wrapper">
<Tooltip title={type === 'tooltip' ? t('refreshPaymentStatus') : ''}>
<Button
type={type === 'text' ? 'text' : 'default'}
shape={btnShape}
className={cx('periscope-btn', { text: type === 'text' })}
onClick={handleRefreshPaymentStatus}
icon={<RefreshCcw size={14} />}
loading={isLoading}
>
{type !== 'tooltip' ? t('refreshPaymentStatus') : ''}
</Button>
</Tooltip>
{type === 'tooltip' ? (
<TooltipSimple title={t('refreshPaymentStatus')}>{button}</TooltipSimple>
) : (
button
)}
</span>
);
}
RefreshPaymentStatus.defaultProps = {
btnShape: 'default',
type: 'button',
className: undefined,
};
export default RefreshPaymentStatus;

View File

@@ -72,6 +72,7 @@ function TanStackTableInner<TData>(
data,
columns,
columnStorageKey,
respectColumnOrder = true,
columnSizing: columnSizingProp,
onColumnSizingChange,
onColumnOrderChange,
@@ -175,6 +176,7 @@ function TanStackTableInner<TData>(
storageKey: columnStorageKey,
columns,
isGrouped,
respectColumnOrder,
});
// Use store values when columnStorageKey is provided, otherwise fall back to props/defaults
@@ -206,6 +208,7 @@ function TanStackTableInner<TData>(
handleRemoveColumn,
} = useColumnHandlers({
columnStorageKey,
respectColumnOrder,
effectiveSizing,
storeSetSizing,
storeSetOrder,
@@ -322,9 +325,7 @@ function TanStackTableInner<TData>(
});
const hasSingleColumn = useMemo(
() =>
effectiveColumns.filter((c) => !c.pin && c.enableRemove !== false).length <=
1,
() => effectiveColumns.filter((c) => !c.pin).length <= 1,
[effectiveColumns],
);

View File

@@ -24,6 +24,8 @@ jest.mock('../TanStackTable.module.scss', () => ({
}));
beforeAll(() => {
// jsdom doesn't include ResizeObserver — must direct-assign rather than
// spyOn (spyOn requires the property to already exist).
window.ResizeObserver = jest.fn().mockImplementation(() => ({
disconnect: jest.fn(),
observe: jest.fn(),
@@ -867,4 +869,110 @@ describe('TanStackTableView Integration', () => {
expect(screen.queryByRole('navigation')).not.toBeInTheDocument();
});
});
describe('hasSingleColumn — gates the Remove popover per-column', () => {
const hasSingleColumnFlagPresent = (): boolean =>
Boolean(document.querySelector('th[data-single-column="true"]'));
it('is true when only one non-pinned column exists', async () => {
renderTanStackTable({
props: {
data: [{ id: '1', name: 'Item 1', value: 100 }],
columns: [
{
id: 'name',
header: 'Name',
accessorKey: 'name',
cell: ({ value }): string => String(value),
},
],
},
});
await waitFor(() => {
expect(screen.getByText('Item 1')).toBeInTheDocument();
});
expect(hasSingleColumnFlagPresent()).toBe(true);
});
it('is false when multiple non-pinned columns exist (all removable)', async () => {
renderTanStackTable({});
await waitFor(() => {
expect(screen.getByText('Item 1')).toBeInTheDocument();
});
// 3 default columns (id/name/value), none pinned, none non-removable
// → table is not single-column.
expect(hasSingleColumnFlagPresent()).toBe(false);
});
it('is false when removable + non-removable mix exists (the body/timestamp case)', async () => {
renderTanStackTable({
props: {
data: [{ id: '1', name: 'Item 1', value: 100 }],
columns: [
{
id: 'name',
header: 'Timestamp',
accessorKey: 'name',
enableRemove: false,
cell: ({ value }): string => String(value),
},
{
id: 'value',
header: 'Body',
accessorKey: 'value',
enableRemove: false,
cell: ({ value }): string => String(value),
},
{
id: 'id',
header: 'User',
accessorKey: 'id',
enableRemove: true,
cell: ({ value }): string => String(value),
},
],
},
});
await waitFor(() => {
expect(screen.getByText('Item 1')).toBeInTheDocument();
});
expect(hasSingleColumnFlagPresent()).toBe(false);
});
it('does not count pinned columns toward the total', async () => {
renderTanStackTable({
props: {
data: [{ id: '1', name: 'Item 1', value: 100 }],
columns: [
{
id: 'stateIndicator',
header: '',
pin: 'left',
accessorKey: 'id',
cell: ({ value }): string => String(value),
},
{
id: 'name',
header: 'Name',
accessorKey: 'name',
cell: ({ value }): string => String(value),
},
],
},
});
await waitFor(() => {
expect(screen.getByText('Item 1')).toBeInTheDocument();
});
// 1 pinned + 1 non-pinned → only the non-pinned counts → single-column.
expect(hasSingleColumnFlagPresent()).toBe(true);
});
});
});

View File

@@ -168,6 +168,53 @@ describe('useColumnState', () => {
'a',
]);
});
it('ignores stored columnOrder when respectColumnOrder=false', () => {
const columns = [col('a'), col('b'), col('c')];
act(() => {
useColumnStore.getState().initializeFromDefaults(TEST_KEY, columns);
useColumnStore.getState().setColumnOrder(TEST_KEY, ['c', 'a', 'b']);
});
const { result } = renderHook(() =>
useColumnState({
storageKey: TEST_KEY,
columns,
respectColumnOrder: false,
}),
);
// Falls through to natural columns-array order; stored order is ignored.
expect(result.current.sortedColumns.map((c) => c.id)).toStrictEqual([
'a',
'b',
'c',
]);
});
it('honors stored columnOrder when respectColumnOrder=true (default)', () => {
const columns = [col('a'), col('b'), col('c')];
act(() => {
useColumnStore.getState().initializeFromDefaults(TEST_KEY, columns);
useColumnStore.getState().setColumnOrder(TEST_KEY, ['c', 'a', 'b']);
});
const { result } = renderHook(() =>
useColumnState({
storageKey: TEST_KEY,
columns,
respectColumnOrder: true,
}),
);
expect(result.current.sortedColumns.map((c) => c.id)).toStrictEqual([
'c',
'a',
'b',
]);
});
});
describe('actions', () => {

View File

@@ -152,6 +152,8 @@ export type TanStackTableProps<TData> = {
columns: TableColumnDef<TData>[];
/** Storage key for column state persistence (visibility, sizing, ordering). When set, enables unified column management. */
columnStorageKey?: string;
/** When false, the table renders columns in their natural array order and the persisted columnOrder slot is ignored on read and skipped on write. Use when an external source (e.g. preferences.selectColumns) is the canonical order. Default true. */
respectColumnOrder?: boolean;
columnSizing?: ColumnSizingState;
onColumnSizingChange?: Dispatch<SetStateAction<ColumnSizingState>>;
onColumnOrderChange?: (cols: TableColumnDef<TData>[]) => void;

View File

@@ -7,6 +7,8 @@ import { TableColumnDef } from './types';
export interface UseColumnHandlersOptions<TData> {
/** Storage key for persisting column state (enables store mode) */
columnStorageKey?: string;
/** When false, drag-reorder skips the persisted columnOrder write — order flows back via onColumnOrderChange only. */
respectColumnOrder?: boolean;
effectiveSizing: ColumnSizingState;
storeSetSizing: (sizing: ColumnSizingState) => void;
storeSetOrder: (columns: TableColumnDef<TData>[]) => void;
@@ -28,6 +30,7 @@ export interface UseColumnHandlersResult<TData> {
*/
export function useColumnHandlers<TData>({
columnStorageKey,
respectColumnOrder = true,
effectiveSizing,
storeSetSizing,
storeSetOrder,
@@ -50,12 +53,12 @@ export function useColumnHandlers<TData>({
const handleColumnOrderChange = useCallback(
(cols: TableColumnDef<TData>[]) => {
if (columnStorageKey) {
if (columnStorageKey && respectColumnOrder) {
storeSetOrder(cols);
}
onColumnOrderChange?.(cols);
},
[columnStorageKey, storeSetOrder, onColumnOrderChange],
[columnStorageKey, respectColumnOrder, storeSetOrder, onColumnOrderChange],
);
const handleRemoveColumn = useCallback(

View File

@@ -20,6 +20,7 @@ type UseColumnStateOptions<TData> = {
storageKey?: string;
columns: TableColumnDef<TData>[];
isGrouped?: boolean;
respectColumnOrder?: boolean;
};
type UseColumnStateResult<TData> = {
@@ -40,6 +41,7 @@ export function useColumnState<TData>({
storageKey,
columns,
isGrouped = false,
respectColumnOrder = true,
}: UseColumnStateOptions<TData>): UseColumnStateResult<TData> {
useEffect(() => {
if (storageKey) {
@@ -130,7 +132,7 @@ export function useColumnState<TData>({
}, [hiddenColumnIds, columns, isGrouped]);
const sortedColumns = useMemo((): TableColumnDef<TData>[] => {
if (columnOrder.length === 0) {
if (!respectColumnOrder || columnOrder.length === 0) {
return columns;
}
@@ -144,7 +146,7 @@ export function useColumnState<TData>({
});
return [...pinned, ...sortedRest];
}, [columns, columnOrder]);
}, [columns, columnOrder, respectColumnOrder]);
const hideColumn = useCallback(
(columnId: string) => {

View File

@@ -93,7 +93,6 @@ function ValueGraph({
<div
ref={containerRef}
className="value-graph-container"
data-testid="value-graph-container"
style={{
backgroundColor:
threshold.thresholdFormat === 'Background'

View File

@@ -139,7 +139,6 @@ jest.mock('react-query', (): unknown => {
});
// mock other side-effecty modules
jest.mock('api/common/logEvent', () => jest.fn());
jest.mock('api/browser/localstorage/set', () => jest.fn());
jest.mock('utils/error', () => ({ showErrorNotification: jest.fn() }));

View File

@@ -42,4 +42,5 @@ export enum LOCALSTORAGE {
LICENSE_KEY_CALLOUT_DISMISSED = 'LICENSE_KEY_CALLOUT_DISMISSED',
DASHBOARD_PREFERENCES = 'DASHBOARD_PREFERENCES',
ACTIVE_SIGNOZ_INSTANCE_URL = 'ACTIVE_SIGNOZ_INSTANCE_URL',
DASHBOARDS_LIST_VISIBLE_COLUMNS = 'DASHBOARDS_LIST_VISIBLE_COLUMNS',
}

View File

@@ -33,7 +33,8 @@ export const REACT_QUERY_KEY = {
UPDATE_ALERT_RULE: 'UPDATE_ALERT_RULE',
GET_ACTIVE_LICENSE_V3: 'GET_ACTIVE_LICENSE_V3',
GET_TRACE_V2_WATERFALL: 'GET_TRACE_V2_WATERFALL',
GET_TRACE_V3_WATERFALL: 'GET_TRACE_V3_WATERFALL',
GET_TRACE_V4_WATERFALL: 'GET_TRACE_V4_WATERFALL',
GET_TRACE_AGGREGATIONS: 'GET_TRACE_AGGREGATIONS',
GET_TRACE_V2_FLAMEGRAPH: 'GET_TRACE_V2_FLAMEGRAPH',
GET_POD_LIST: 'GET_POD_LIST',
GET_NODE_LIST: 'GET_NODE_LIST',

View File

@@ -5,6 +5,7 @@ import { TooltipSimple } from '@signozhq/ui/tooltip';
import logEvent from 'api/common/logEvent';
import ROUTES from 'constants/routes';
import Noz from 'components/Noz/Noz';
import { NOZ_TOOLTIP_TITLE } from 'components/Noz/Noz.constants';
import { AIAssistantEvents, AIAssistantOpenSource } from '../events';
import { normalizePage } from '../hooks/useAIAssistantAnalyticsContext';
@@ -42,16 +43,15 @@ export default function AIAssistantTrigger(): JSX.Element | null {
}
return (
<TooltipSimple title="Noz">
<TooltipSimple title={NOZ_TOOLTIP_TITLE}>
<Button
variant="solid"
color="primary"
className={`${styles.trigger} noz-wave`}
onClick={handleOpen}
aria-label="Open Noz"
>
<Noz size={24} />
</Button>
prefix={<Noz size={24} />}
/>
</TooltipSimple>
);
}

View File

@@ -0,0 +1,5 @@
.alertHistory {
display: flex;
flex-direction: column;
gap: var(--spacing-10);
}

View File

@@ -3,13 +3,13 @@ import { useState } from 'react';
import Statistics from './Statistics/Statistics';
import Timeline from './Timeline/Timeline';
import './AlertHistory.styles.scss';
import styles from './AlertHistory.module.scss';
function AlertHistory(): JSX.Element {
const [totalCurrentTriggers, setTotalCurrentTriggers] = useState(0);
return (
<div className="alert-history">
<div className={styles.alertHistory}>
<Statistics
totalCurrentTriggers={totalCurrentTriggers}
setTotalCurrentTriggers={setTotalCurrentTriggers}

View File

@@ -0,0 +1,40 @@
.alertPopoverTriggerAction {
cursor: pointer;
}
.alertHistoryPopover {
:global(.ant-popover-inner) {
border: 1px solid var(--l1-border);
background: var(--l1-background) !important;
padding: 0 !important;
}
:global(.ant-popover-arrow) {
&::before {
background: var(--l1-background);
}
}
}
.contributorRowPopoverButtons {
display: flex;
flex-direction: column;
}
.contributorRowPopoverButtonsButton {
display: flex;
align-items: center;
gap: var(--spacing-3);
padding: var(--spacing-6) var(--spacing-7);
color: var(--l2-foreground);
font-size: var(--periscope-font-size-base);
letter-spacing: 0.14px;
width: 160px;
cursor: pointer;
background: var(--l1-background);
border-color: var(--l1-border);
&:hover {
background: var(--l2-background);
}
}

View File

@@ -1,15 +0,0 @@
.alert-popover-trigger-action {
cursor: pointer;
}
.alert-history-popover {
.ant-popover-inner {
border: 1px solid var(--l1-border);
background: var(--l1-background) !important;
}
.ant-popover-arrow {
&::before {
background: var(--l1-background);
}
}
}

View File

@@ -7,7 +7,7 @@ import ROUTES from 'constants/routes';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { DraftingCompass } from '@signozhq/icons';
import './AlertPopover.styles.scss';
import styles from './AlertPopover.module.scss';
type Props = {
children: React.ReactNode;
@@ -24,30 +24,30 @@ function PopoverContent({
}): JSX.Element {
const isDarkMode = useIsDarkMode();
return (
<div className="contributor-row-popover-buttons">
<div className={styles.contributorRowPopoverButtons}>
{!!relatedLogsLink && (
<Link
to={`${ROUTES.LOGS_EXPLORER}?${relatedLogsLink}`}
className="contributor-row-popover-buttons__button"
className={styles.contributorRowPopoverButtonsButton}
>
<div className="icon">
<div>
<LogsIcon />
</div>
<div className="text">View Logs</div>
<div>View Logs</div>
</Link>
)}
{!!relatedTracesLink && (
<Link
to={`${ROUTES.TRACES_EXPLORER}?${relatedTracesLink}`}
className="contributor-row-popover-buttons__button"
className={styles.contributorRowPopoverButtonsButton}
>
<div className="icon">
<div>
<DraftingCompass
size={14}
color={isDarkMode ? Color.BG_VANILLA_400 : Color.TEXT_INK_400}
/>
</div>
<div className="text">View Traces</div>
<div>View Traces</div>
</Link>
)}
</div>
@@ -64,13 +64,13 @@ function AlertPopover({
relatedLogsLink,
}: Props): JSX.Element {
return (
<div className="alert-popover-trigger-action">
<div className={styles.alertPopoverTriggerAction}>
<Popover
showArrow={false}
placement="bottom"
color="linear-gradient(139deg, rgba(18, 19, 23, 1) 0%, rgba(18, 19, 23, 1) 98.68%)"
destroyTooltipOnHide
rootClassName="alert-history-popover"
rootClassName={styles.alertHistoryPopover}
content={
<PopoverContent
relatedTracesLink={relatedTracesLink}
@@ -112,4 +112,3 @@ export function ConditionalAlertPopover({
}
return <div>{children}</div>;
}
export default AlertPopover;

View File

@@ -4,5 +4,5 @@
height: 280px;
border: 1px solid var(--l1-border);
border-radius: 4px;
margin: 0 16px;
margin: 0 var(--spacing-8);
}

View File

@@ -3,7 +3,7 @@ import { AlertRuleStats } from 'types/api/alerts/def';
import StatsCardsRenderer from './StatsCardsRenderer/StatsCardsRenderer';
import TopContributorsRenderer from './TopContributorsRenderer/TopContributorsRenderer';
import './Statistics.styles.scss';
import styles from './Statistics.module.scss';
function Statistics({
setTotalCurrentTriggers,
@@ -13,7 +13,7 @@ function Statistics({
totalCurrentTriggers: AlertRuleStats['totalCurrentTriggers'];
}): JSX.Element {
return (
<div className="statistics">
<div className={styles.statistics}>
<StatsCardsRenderer setTotalCurrentTriggers={setTotalCurrentTriggers} />
<TopContributorsRenderer totalCurrentTriggers={totalCurrentTriggers} />
</div>

View File

@@ -0,0 +1,102 @@
.statsCard {
width: 21.7%;
border-right: 1px solid var(--l1-border);
padding: var(--spacing-4) var(--spacing-6) var(--spacing-6);
}
.statsCardEmpty {
justify-content: normal;
}
.statsCardTitleWrapper {
display: flex;
justify-content: space-between;
align-items: center;
}
.title {
text-transform: uppercase;
font-size: var(--periscope-font-size-base);
line-height: 22px;
color: var(--l2-foreground);
font-weight: var(--font-weight-medium);
}
.durationIndicator {
display: flex;
align-items: center;
gap: var(--spacing-2);
}
.icon {
display: flex;
align-self: center;
}
.text {
text-transform: uppercase;
color: var(--l3-foreground);
font-size: var(--periscope-font-size-small);
font-weight: var(--font-weight-semibold);
letter-spacing: 0.48px;
}
.statsCardStats {
margin-top: var(--spacing-10);
display: flex;
flex-direction: column;
gap: var(--spacing-2);
}
.countLabel {
color: var(--l1-foreground);
font-family: var(--periscope-font-family-mono);
font-size: var(--font-size-2xl);
line-height: 36px;
}
.statsCardAlertHistoryGraph {
margin-top: var(--spacing-16);
}
.alertHistoryGraph {
width: 100%;
height: 72px;
}
.changePercentage {
width: max-content;
display: flex;
padding: var(--spacing-2) var(--spacing-4);
border-radius: 20px;
align-items: center;
gap: var(--spacing-2);
}
// TODO(@signozhq/design-tokens): replace --text-forest-* with --success-foreground after release
.changePercentageSuccess {
background: color-mix(in srgb, var(--text-forest-500) 10%, transparent);
color: var(--text-forest-400);
}
.changePercentageError {
background: color-mix(in srgb, var(--danger-background) 10%, transparent);
color: var(--danger-foreground);
}
.changePercentageNoPreviousData {
color: var(--primary-foreground);
background: color-mix(in srgb, var(--primary-background) 10%, transparent);
padding: var(--spacing-2) var(--spacing-8);
}
.changePercentageIcon {
display: flex;
align-self: center;
}
.changePercentageLabel {
font-size: var(--periscope-font-size-small);
font-weight: var(--font-weight-medium);
line-height: 16px;
}

View File

@@ -1,94 +0,0 @@
.stats-card {
width: 21.7%;
border-right: 1px solid var(--l1-border);
padding: 9px 12px 13px;
&--empty {
justify-content: normal;
}
&__title-wrapper {
display: flex;
justify-content: space-between;
align-items: center;
.title {
text-transform: uppercase;
font-size: 13px;
line-height: 22px;
color: var(--l2-foreground);
font-weight: 500;
}
.duration-indicator {
display: flex;
align-items: center;
gap: 4px;
.icon {
display: flex;
align-self: center;
}
.text {
text-transform: uppercase;
color: var(--l3-foreground);
font-size: 12px;
font-weight: 600;
letter-spacing: 0.48px;
}
}
}
&__stats {
margin-top: 20px;
display: flex;
flex-direction: column;
gap: 4px;
.count-label {
color: var(--l1-foreground);
font-family: 'Geist Mono';
font-size: 24px;
line-height: 36px;
}
}
&__alert-history-graph {
margin-top: 80px;
.alert-history-graph {
width: 100%;
height: 72px;
}
}
}
.change-percentage {
width: max-content;
display: flex;
padding: 4px 8px;
border-radius: 20px;
align-items: center;
gap: 4px;
&--success {
background: color-mix(in srgb, var(--success-background) 10%, transparent);
color: var(--success-foreground);
}
&--error {
background: color-mix(in srgb, var(--error-background) 10%, transparent);
color: var(--error-foreground);
}
&--no-previous-data {
color: var(--primary-foreground);
background: color-mix(in srgb, var(--primary-background) 10%, transparent);
padding: 4px 16px;
}
&__icon {
display: flex;
align-self: center;
}
&__label {
font-size: 12px;
font-weight: 500;
line-height: 16px;
}
}

View File

@@ -1,3 +1,4 @@
import cx from 'classnames';
import { Color } from '@signozhq/design-tokens';
import { Tooltip } from 'antd';
import { QueryParams } from 'constants/query';
@@ -12,7 +13,7 @@ import {
extractDayFromTimestamp,
} from './utils';
import './StatsCard.styles.scss';
import styles from './StatsCard.module.scss';
type ChangePercentageProps = {
percentage: number;
@@ -26,11 +27,11 @@ function ChangePercentage({
}: ChangePercentageProps): JSX.Element {
if (direction > 0) {
return (
<div className="change-percentage change-percentage--success">
<div className="change-percentage__icon">
<div className={cx(styles.changePercentage, styles.changePercentageSuccess)}>
<div className={styles.changePercentageIcon}>
<ArrowDownLeft size={14} color={Color.BG_FOREST_500} />
</div>
<div className="change-percentage__label">
<div className={styles.changePercentageLabel}>
{percentage}% vs Last {duration}
</div>
</div>
@@ -38,11 +39,11 @@ function ChangePercentage({
}
if (direction < 0) {
return (
<div className="change-percentage change-percentage--error">
<div className="change-percentage__icon">
<div className={cx(styles.changePercentage, styles.changePercentageError)}>
<div className={styles.changePercentageIcon}>
<ArrowUpRight size={14} color={Color.BG_CHERRY_500} />
</div>
<div className="change-percentage__label">
<div className={styles.changePercentageLabel}>
{percentage}% vs Last {duration}
</div>
</div>
@@ -50,8 +51,13 @@ function ChangePercentage({
}
return (
<div className="change-percentage change-percentage--no-previous-data">
<div className="change-percentage__label">no previous data</div>
<div
className={cx(
styles.changePercentage,
styles.changePercentageNoPreviousData,
)}
>
<div className={styles.changePercentageLabel}>no previous data</div>
</div>
);
}
@@ -103,27 +109,27 @@ function StatsCard({
const formattedEndTimeForTooltip = convertTimestampToLocaleDateString(endTime);
return (
<div className={`stats-card ${isEmpty ? 'stats-card--empty' : ''}`}>
<div className="stats-card__title-wrapper">
<div className="title">{title}</div>
<div className="duration-indicator">
<div className="icon">
<div className={cx(styles.statsCard, { [styles.statsCardEmpty]: isEmpty })}>
<div className={styles.statsCardTitleWrapper}>
<div className={styles.title}>{title}</div>
<div className={styles.durationIndicator}>
<div className={styles.icon}>
<Calendar size={14} color={Color.BG_SLATE_200} />
</div>
{relativeTime ? (
<div className="text">{displayTime}</div>
<div className={styles.text}>{displayTime}</div>
) : (
<Tooltip
title={`From ${formattedStartTimeForTooltip} to ${formattedEndTimeForTooltip}`}
>
<div className="text">{displayTime}</div>
<div className={styles.text}>{displayTime}</div>
</Tooltip>
)}
</div>
</div>
<div className="stats-card__stats">
<div className="count-label">
<div className={styles.statsCardStats}>
<div className={styles.countLabel}>
{isEmpty ? emptyMessage : displayValue || totalCurrentCount}
</div>
@@ -134,8 +140,8 @@ function StatsCard({
/>
</div>
<div className="stats-card__alert-history-graph">
<div className="alert-history-graph">
<div className={styles.statsCardAlertHistoryGraph}>
<div className={styles.alertHistoryGraph}>
{!isEmpty && timeSeries.length > 1 && (
<StatsGraph timeSeries={timeSeries} changeDirection={changeDirection} />
)}

View File

@@ -0,0 +1,45 @@
.topContributorsCard {
width: 56.6%;
overflow: hidden;
}
.topContributorsCardHeader {
display: flex;
align-items: center;
justify-content: space-between;
padding: var(--spacing-4) var(--spacing-6);
border-bottom: 1px solid var(--l1-border);
}
.title {
color: var(--l2-foreground);
font-size: var(--periscope-font-size-base);
font-weight: var(--font-weight-medium);
line-height: 22px;
letter-spacing: 0.52px;
text-transform: uppercase;
}
.viewAll {
display: flex;
align-items: center;
gap: var(--spacing-2);
cursor: pointer;
padding: 0;
height: var(--line-height-20);
&:hover {
background-color: transparent !important;
}
}
.label {
color: var(--l2-foreground);
font-size: var(--periscope-font-size-base);
line-height: var(--line-height-20);
letter-spacing: -0.07px;
}
.icon {
display: flex;
}

View File

@@ -1,163 +0,0 @@
.top-contributors-card {
width: 56.6%;
overflow: hidden;
&--view-all {
width: auto;
}
&__header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 8px 12px;
border-bottom: 1px solid var(--l1-border);
.title {
color: var(--l2-foreground);
font-size: 13px;
font-weight: 500;
line-height: 22px;
letter-spacing: 0.52px;
text-transform: uppercase;
}
.view-all {
display: flex;
align-items: center;
gap: 4px;
cursor: pointer;
padding: 0;
height: 20px;
&:hover {
background-color: transparent !important;
}
.label {
color: var(--l2-foreground);
font-size: 14px;
line-height: 20px;
letter-spacing: -0.07px;
}
.icon {
display: flex;
}
}
}
.contributors-row {
height: 80px;
}
.top-contributors-progress {
--progress-background: transparent;
}
&__content {
.ant-table {
&-cell {
padding: 12px !important;
}
}
.contributors-row {
background: var(--l1-background);
td {
border: none !important;
}
&:not(:last-of-type) td {
border-bottom: 1px solid var(--l1-border) !important;
}
}
.total-contribution {
color: var(--primary-foreground);
font-family: 'Geist Mono';
font-size: 12px;
font-weight: 500;
letter-spacing: -0.06px;
padding: 4px 8px;
background: color-mix(in srgb, var(--primary-background) 10%, transparent);
border-radius: 50px;
width: max-content;
}
}
.empty-content {
margin: 16px 12px;
padding: 40px 45px;
display: flex;
flex-direction: column;
gap: 12px;
border: 1px dashed var(--l1-border);
border-radius: 6px;
&__icon {
font-family: Inter;
font-size: 20px;
line-height: 26px;
letter-spacing: -0.103px;
}
&__text {
color: var(--l2-foreground);
line-height: 18px;
.bold-text {
color: var(--l1-foreground);
font-weight: 500;
}
}
&__button-wrapper {
margin-top: 12px;
.configure-alert-rule-button {
padding: 8px 16px;
border-radius: 2px;
background: var(--l3-background);
border-width: 0;
color: var(--l1-foreground);
line-height: 24px;
font-size: 12px;
font-weight: 500;
display: flex;
align-items: center;
}
}
}
}
.ant-popover-inner:has(.contributor-row-popover-buttons) {
padding: 0 !important;
}
.contributor-row-popover-buttons {
display: flex;
flex-direction: column;
&__button {
display: flex;
align-items: center;
gap: 6px;
padding: 12px 15px;
color: var(--l2-foreground);
font-size: 14px;
letter-spacing: 0.14px;
width: 160px;
cursor: pointer;
background: var(--l1-background);
border-color: var(--l1-border);
.text,
.icon {
color: var(--l1-foreground);
}
&:hover {
background: var(--l2-background);
.text,
.icon {
color: var(--l1-foreground);
}
}
.icon {
display: flex;
}
}
}
.view-all-drawer {
border-radius: 4px;
}

View File

@@ -10,7 +10,7 @@ import TopContributorsContent from './TopContributorsContent';
import { TopContributorsCardProps } from './types';
import ViewAllDrawer from './ViewAllDrawer';
import './TopContributorsCard.styles.scss';
import styles from './TopContributorsCard.module.scss';
function TopContributorsCard({
topContributorsData,
@@ -48,13 +48,17 @@ function TopContributorsCard({
return (
<>
<div className="top-contributors-card">
<div className="top-contributors-card__header">
<div className="title">top contributors</div>
<div className={styles.topContributorsCard}>
<div className={styles.topContributorsCardHeader}>
<div className={styles.title}>top contributors</div>
{topContributorsData.length > 3 && (
<Button type="text" className="view-all" onClick={toggleViewAllDrawer}>
<div className="label">View all</div>
<div className="icon">
<Button
type="text"
className={styles.viewAll}
onClick={toggleViewAllDrawer}
>
<div className={styles.label}>View all</div>
<div className={styles.icon}>
<ArrowRight
size={14}
color={isDarkMode ? Color.BG_VANILLA_400 : Color.BG_INK_400}

View File

@@ -0,0 +1,27 @@
.emptyContent {
margin: var(--spacing-8) var(--spacing-6);
padding: var(--spacing-20) 45px;
display: flex;
flex-direction: column;
gap: var(--spacing-6);
border: 1px dashed var(--l1-border);
border-radius: 6px;
}
.emptyContentIcon {
font-family: var(--font-family-inter);
font-size: var(--font-size-xl);
line-height: 26px;
letter-spacing: -0.103px;
}
.emptyContentText {
color: var(--l2-foreground);
line-height: var(--line-height-18);
}
.topContributorsCardContent {
:global(.ant-table-cell) {
padding: var(--spacing-6) !important;
}
}

View File

@@ -1,3 +1,4 @@
import styles from './TopContributorsContent.module.scss';
import TopContributorsRows from './TopContributorsRows';
import { TopContributorsCardProps } from './types';
@@ -9,9 +10,9 @@ function TopContributorsContent({
if (isEmpty) {
return (
<div className="empty-content">
<div className="empty-content__icon"></div>
<div className="empty-content__text">
<div className={styles.emptyContent}>
<div className={styles.emptyContentIcon}></div>
<div className={styles.emptyContentText}>
Top contributors highlight the most frequently triggering group-by
attributes in multi-dimensional alerts
</div>
@@ -20,7 +21,7 @@ function TopContributorsContent({
}
return (
<div className="top-contributors-card__content">
<div className={styles.topContributorsCardContent}>
<TopContributorsRows
topContributors={topContributorsData.slice(0, 3)}
totalCurrentTriggers={totalCurrentTriggers}

View File

@@ -0,0 +1,28 @@
.contributorsRow {
height: 80px;
background: var(--l1-background);
td {
border: none !important;
}
&:not(:last-of-type) td {
border-bottom: 1px solid var(--l1-border) !important;
}
}
.topContributorsProgress {
--progress-background: transparent;
}
.totalContribution {
color: var(--primary-foreground);
font-family: var(--periscope-font-family-mono);
font-size: var(--periscope-font-size-small);
font-weight: var(--font-weight-medium);
letter-spacing: -0.06px;
padding: var(--spacing-2) var(--spacing-4);
background: color-mix(in srgb, var(--primary-background) 10%, transparent);
border-radius: 50px;
width: max-content;
}

View File

@@ -12,6 +12,8 @@ import {
AlertRuleTopContributors,
} from 'types/api/alerts/def';
import styles from './TopContributorsRows.module.scss';
function TopContributorsRows({
topContributors,
totalCurrentTriggers,
@@ -53,7 +55,7 @@ function TopContributorsRows({
percent={(count / totalCurrentTriggers) * 100}
showInfo={false}
strokeColor={Color.BG_ROBIN_500}
className="top-contributors-progress"
className={styles.topContributorsProgress}
/>
</ConditionalAlertPopover>
),
@@ -68,7 +70,7 @@ function TopContributorsRows({
relatedTracesLink={record.relatedTracesLink}
relatedLogsLink={record.relatedLogsLink}
>
<div className="total-contribution">
<div className={styles.totalContribution}>
{count}/{totalCurrentTriggers}
</div>
</ConditionalAlertPopover>
@@ -88,7 +90,7 @@ function TopContributorsRows({
return (
<Table
rowClassName="contributors-row"
rowClassName={styles.contributorsRow}
rowKey={(row): string => `top-contributor-${row.fingerprint}`}
onRow={handleRowClick}
columns={columns}

View File

@@ -0,0 +1,13 @@
.topContributorsCardViewAll {
width: auto;
}
.topContributorsCardContent {
:global(.ant-table-cell) {
padding: var(--spacing-6) !important;
}
}
.viewAllDrawer {
border-radius: 4px;
}

View File

@@ -3,6 +3,7 @@ import { Drawer } from 'antd';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { AlertRuleStats, AlertRuleTopContributors } from 'types/api/alerts/def';
import styles from './ViewAllDrawer.module.scss';
import TopContributorsRows from './TopContributorsRows';
function ViewAllDrawer({
@@ -24,15 +25,15 @@ function ViewAllDrawer({
onClose={toggleViewAllDrawer}
placement="right"
width="50%"
className="view-all-drawer"
className={styles.viewAllDrawer}
style={{
overscrollBehavior: 'contain',
background: isDarkMode ? Color.BG_INK_400 : Color.BG_VANILLA_100,
}}
title="Viewing All Contributors"
>
<div className="top-contributors-card--view-all">
<div className="top-contributors-card__content">
<div className={styles.topContributorsCardViewAll}>
<div className={styles.topContributorsCardContent}>
<TopContributorsRows
topContributors={topContributorsData}
totalCurrentTriggers={totalCurrentTriggers}

View File

@@ -1,35 +0,0 @@
.timeline-graph {
display: flex;
flex-direction: column;
gap: 24px;
background: var(--l2-background);
padding: 12px;
border-radius: 4px;
border: 1px solid var(--l1-border);
height: 150px;
&__title {
width: max-content;
padding: 2px 8px;
border-radius: 4px;
border: 1px solid var(--l1-border);
background: var(--l1-background);
color: var(--l1-foreground);
font-size: 12px;
line-height: 18px;
letter-spacing: -0.06px;
}
&__chart {
.chart-placeholder {
width: 100%;
height: 52px;
background: color-mix(in srgb, var(--l1-foreground) 12%, transparent);
display: flex;
align-items: center;
justify-content: center;
.chart-icon {
font-size: 2rem;
}
}
}
}

View File

@@ -0,0 +1,22 @@
.timelineGraph {
display: flex;
flex-direction: column;
gap: var(--spacing-10);
background: var(--l2-background);
padding: var(--spacing-6);
border-radius: 4px;
border: 1px solid var(--l1-border);
height: 150px;
}
.timelineGraphTitle {
width: max-content;
padding: var(--spacing-1) var(--spacing-4);
border-radius: 4px;
border: 1px solid var(--l1-border);
background: var(--l1-background);
color: var(--l1-foreground);
font-size: var(--periscope-font-size-small);
line-height: var(--line-height-18);
letter-spacing: -0.06px;
}

View File

@@ -4,7 +4,7 @@ import DataStateRenderer from 'periscope/components/DataStateRenderer/DataStateR
import Graph from '../Graph/Graph';
import '../Graph/Graph.styles.scss';
import styles from './GraphWrapper.module.scss';
function GraphWrapper({
totalCurrentTriggers,
@@ -40,11 +40,11 @@ function GraphWrapper({
// }, [startTime]);
return (
<div className="timeline-graph">
<div className="timeline-graph__title">
<div className={styles.timelineGraph}>
<div className={styles.timelineGraphTitle}>
{totalCurrentTriggers} triggers in {relativeTime}
</div>
<div className="timeline-graph__chart">
<div>
<DataStateRenderer
isLoading={isLoading}
isError={isError || !isValidRuleId || !ruleId}

View File

@@ -0,0 +1,70 @@
.timelineTable {
border-top: 1px solid var(--l1-border);
border-radius: 6px;
overflow: hidden;
margin-top: var(--spacing-2);
min-height: 600px;
:global(.ant-table) {
background: var(--l1-background);
}
:global(.ant-table-cell) {
padding: var(--spacing-6) var(--spacing-8) !important;
vertical-align: baseline;
&::before {
display: none;
}
}
:global(.ant-table-thead) > tr > th {
border-color: var(--l1-border);
background: var(--l1-background);
font-size: var(--periscope-font-size-small);
font-weight: var(--font-weight-medium);
padding: var(--spacing-6) var(--spacing-8) var(--spacing-4) !important;
}
:global(.ant-table-tbody) > tr > td {
border: none;
}
:global(.ant-table.ant-table-middle) {
border-bottom: 1px solid var(--l1-border);
border-left: 1px solid var(--l1-border);
border-right: 1px solid var(--l1-border);
border-radius: 6px;
}
:global(.ant-pagination-item-active) {
display: flex;
width: var(--spacing-10);
height: var(--spacing-10);
align-items: center;
justify-content: center;
padding: var(--spacing-0) var(--spacing-4);
border-radius: 2px;
background: var(--primary-background);
& > a {
color: var(--primary-foreground);
line-height: var(--line-height-20);
font-weight: var(--font-weight-medium);
}
}
}
.alertRuleCreatedAt {
font-size: var(--periscope-font-size-base);
color: var(--l2-foreground);
line-height: var(--line-height-18);
letter-spacing: -0.07px;
}
.alertHistoryLabelSearch {
:global(.ant-select-selector) {
border: none;
background: var(--l2-background);
}
}

View File

@@ -1,89 +0,0 @@
.timeline-table {
border-top: 1px solid var(--l1-border);
border-radius: 6px;
overflow: hidden;
margin-top: 4px;
min-height: 600px;
.ant-table {
background: var(--l1-background);
&-cell {
padding: 12px 16px !important;
vertical-align: baseline;
&::before {
display: none;
}
}
&-thead > tr > th {
border-color: var(--l1-border);
background: var(--l1-background);
font-size: 12px;
font-weight: 500;
padding: 12px 16px 8px !important;
}
&-tbody > tr > td {
border: none;
}
}
.label-filter {
padding: 6px 8px;
border-radius: 4px;
background: var(--l1-foreground);
border-width: 0;
line-height: 18px;
& ::placeholder {
color: var(--l2-foreground);
font-size: 12px;
letter-spacing: 0.6px;
text-transform: uppercase;
font-weight: 500;
}
}
.alert-rule {
&-value,
&__created-at {
font-size: 14px;
color: var(--l2-foreground);
}
&-value {
font-weight: 500;
line-height: 20px;
}
&__created-at {
line-height: 18px;
letter-spacing: -0.07px;
}
}
.ant-table.ant-table-middle {
border-bottom: 1px solid var(--l1-border);
border-left: 1px solid var(--l1-border);
border-right: 1px solid var(--l1-border);
border-radius: 6px;
}
.ant-pagination-item {
&-active {
display: flex;
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
padding: 1px 8px;
border-radius: 2px;
background: var(--primary-background);
& > a {
color: var(--primary-foreground);
line-height: 20px;
font-weight: 500;
}
}
}
.alert-history-label-search {
.ant-select-selector {
border: none;
background: var(--l2-background);
}
}
}

View File

@@ -13,7 +13,7 @@ import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
import { timelineTableColumns } from './useTimelineTable';
import './Table.styles.scss';
import styles from './Table.module.scss';
function TimelineTable(): JSX.Element {
const [filters, setFilters] = useState<TagFilter>(initialFilters);
@@ -54,7 +54,7 @@ function TimelineTable(): JSX.Element {
});
return (
<div className="timeline-table">
<div className={styles.timelineTable}>
<Table
rowKey={(row): string => `${row.fingerprint}-${row.value}-${row.unixMilli}`}
columns={timelineTableColumns({

View File

@@ -17,6 +17,8 @@ import AlertState from 'pages/AlertDetails/AlertHeader/AlertState/AlertState';
import { AlertRuleTimelineTableResponse } from 'types/api/alerts/def';
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
import styles from './Table.module.scss';
const transformLabelsToQbKeys = (
labels: AlertRuleTimelineTableResponse['labels'],
): AttributeKey[] => Object.keys(labels).flatMap((key) => [{ key }]);
@@ -56,7 +58,7 @@ function LabelFilter({
<ClientSideQBSearch
onChange={handleSearch}
filters={filters}
className="alert-history-label-search"
className={styles.alertHistoryLabelSearch}
attributeKeys={transformedKeys}
attributeValuesMap={attributesMap}
suffixIcon={
@@ -88,29 +90,21 @@ export const timelineTableColumns = ({
dataIndex: 'state',
sorter: true,
width: 140,
render: (value): JSX.Element => (
<div className="alert-rule-state">
<AlertState state={value} showLabel />
</div>
),
render: (value): JSX.Element => <AlertState state={value} showLabel />,
},
{
title: (
<LabelFilter setFilters={setFilters} filters={filters} labels={labels} />
),
dataIndex: 'labels',
render: (labels): JSX.Element => (
<div className="alert-rule-labels">
<AlertLabels labels={labels} />
</div>
),
render: (labels): JSX.Element => <AlertLabels labels={labels} />,
},
{
title: 'CREATED AT',
dataIndex: 'unixMilli',
width: 200,
render: (value): JSX.Element => (
<div className="alert-rule__created-at">
<div className={styles.alertRuleCreatedAt}>
{formatTimezoneAdjustedTimestamp(value, DATE_TIME_FORMATS.DASH_DATETIME)}
</div>
),
@@ -125,7 +119,7 @@ export const timelineTableColumns = ({
relatedLogsLink={record.relatedLogsLink}
>
<Button type="text" ghost>
<Ellipsis className="dropdown-icon" size="md" />
<Ellipsis size="md" />
</Button>
</ConditionalAlertPopover>
),

View File

@@ -0,0 +1,35 @@
.timelineTabsAndFilters {
display: flex;
justify-content: space-between;
align-items: center;
}
.resetButton,
.top5Contributors {
display: flex;
align-items: center;
gap: var(--spacing-5);
}
.comingSoon {
display: inline-flex;
padding: var(--spacing-2) var(--spacing-4);
border-radius: 20px;
border: 1px solid color-mix(in srgb, var(--bg-sienna-500) 20%, transparent);
background: color-mix(in srgb, var(--bg-sienna-500) 10%, transparent);
justify-content: center;
align-items: center;
gap: var(--spacing-2);
}
.comingSoonText {
color: var(--text-sienna-400);
font-size: var(--periscope-font-size-small);
font-weight: var(--font-weight-medium);
letter-spacing: -0.05px;
line-height: normal;
}
.comingSoonIcon {
display: flex;
}

View File

@@ -1,32 +0,0 @@
.timeline-tabs-and-filters {
display: flex;
justify-content: space-between;
align-items: center;
.reset-button,
.top-5-contributors {
display: flex;
align-items: center;
gap: 10px;
}
.coming-soon {
display: inline-flex;
padding: 4px 8px;
border-radius: 20px;
border: 1px solid color-mix(in srgb, var(--bg-sienna-500) 20%, transparent);
background: color-mix(in srgb, var(--bg-sienna-500) 10%, transparent);
justify-content: center;
align-items: center;
gap: 5px;
&__text {
color: var(--text-sienna-400);
font-size: 10px;
font-weight: 500;
letter-spacing: -0.05px;
line-height: normal;
}
&__icon {
display: flex;
}
}
}

View File

@@ -6,13 +6,13 @@ import history from 'lib/history';
import { Info } from '@signozhq/icons';
import Tabs2 from 'periscope/components/Tabs2';
import './TabsAndFilters.styles.scss';
import styles from './TabsAndFilters.module.scss';
function ComingSoon(): JSX.Element {
return (
<div className="coming-soon">
<div className="coming-soon__text">Coming Soon</div>
<div className="coming-soon__icon">
<div className={styles.comingSoon}>
<div className={styles.comingSoonText}>Coming Soon</div>
<div className={styles.comingSoonIcon}>
<Info size={10} color={Color.BG_SIENNA_400} />
</div>
</div>
@@ -27,7 +27,7 @@ function TimelineTabs(): JSX.Element {
{
value: TimelineTab.TOP_5_CONTRIBUTORS,
label: (
<div className="top-5-contributors">
<div className={styles.top5Contributors}>
Top 5 Contributors
<ComingSoon />
</div>
@@ -80,7 +80,7 @@ function TimelineFilters(): JSX.Element {
function TabsAndFilters(): JSX.Element {
return (
<div className="timeline-tabs-and-filters">
<div className={styles.timelineTabsAndFilters}>
<TimelineTabs />
<TimelineFilters />
</div>

View File

@@ -0,0 +1,14 @@
.timeline {
display: flex;
flex-direction: column;
gap: var(--spacing-4);
margin: 0 var(--spacing-8);
}
.timelineTitle {
color: var(--l1-foreground);
font-size: var(--periscope-font-size-base);
font-weight: var(--font-weight-medium);
line-height: var(--line-height-20);
letter-spacing: -0.07px;
}

View File

@@ -1,14 +0,0 @@
.timeline {
display: flex;
flex-direction: column;
gap: 8px;
margin: 0 16px;
&__title {
color: var(--l1-foreground);
font-size: 14px;
font-weight: 500;
line-height: 20px;
letter-spacing: -0.07px;
}
}

View File

@@ -2,7 +2,7 @@ import GraphWrapper from './GraphWrapper/GraphWrapper';
import TimelineTable from './Table/Table';
import TabsAndFilters from './TabsAndFilters/TabsAndFilters';
import './Timeline.styles.scss';
import styles from './Timeline.module.scss';
function TimelineTableRenderer(): JSX.Element {
return <TimelineTable />;
@@ -14,15 +14,15 @@ function Timeline({
totalCurrentTriggers: number;
}): JSX.Element {
return (
<div className="timeline">
<div className="timeline__title">Timeline</div>
<div className="timeline__tabs-and-filters">
<div className={styles.timeline}>
<div className={styles.timelineTitle}>Timeline</div>
<div>
<TabsAndFilters />
</div>
<div className="timeline__graph">
<div>
<GraphWrapper totalCurrentTriggers={totalCurrentTriggers} />
</div>
<div className="timeline__table">
<div>
<TimelineTableRenderer />
</div>
</div>

View File

@@ -0,0 +1,183 @@
.anomalyAlertEvaluationView {
display: flex;
flex-direction: row;
justify-content: space-between;
gap: var(--spacing-4);
width: 100%;
height: 100%;
:global(.uplot-tooltip) {
background-color: rgb(0 0 0 / 90%);
box-shadow: 0 2px 4px rgb(0 0 0 / 10%);
color: #ddd;
font-size: var(--periscope-font-size-base);
line-height: 1.4;
padding: var(--spacing-4) var(--spacing-6);
pointer-events: none;
position: absolute;
z-index: 100;
max-height: 500px;
width: 280px;
overflow-y: auto;
display: none;
&::-webkit-scrollbar {
width: 0.3rem;
}
&::-webkit-scrollbar-corner {
background: transparent;
}
&::-webkit-scrollbar-thumb {
background: rgb(136 136 136);
border-radius: 0.625rem;
}
&::-webkit-scrollbar-track {
background: transparent;
}
}
:global(.uplot-tooltip-title) {
font-weight: var(--font-weight-semibold);
margin-bottom: var(--spacing-2);
}
:global(.uplot-tooltip-series) {
display: flex;
gap: var(--spacing-2);
padding: var(--spacing-2) 0;
align-items: center;
}
:global(.uplot-tooltip-series-name) {
margin-right: var(--spacing-2);
}
:global(.uplot-tooltip-band) {
font-style: italic;
color: #666;
}
:global(.uplot-tooltip-marker) {
display: inline-block;
width: 6px;
height: 6px;
border-radius: 50%;
margin-right: var(--spacing-4);
vertical-align: middle;
}
:global(.uplot) {
:global(.u-title) {
text-align: center;
font-size: var(--periscope-font-size-base);
font-weight: var(--font-weight-normal);
display: flex;
height: 40px;
align-items: center;
}
:global(.u-legend) {
display: flex;
margin-top: var(--spacing-8);
tbody {
width: 100%;
:global(.u-series) {
display: inline-flex;
}
}
}
}
}
.chartSection {
height: 100%;
width: 100%;
display: flex;
justify-content: center;
align-items: center;
}
.chartSectionMultiSeries {
composes: chartSection;
width: calc(100% - 240px);
}
.noDataContainer {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
gap: var(--spacing-4);
}
.seriesSelection {
display: flex;
flex-direction: column;
gap: var(--spacing-4);
width: 240px;
padding: 0 var(--spacing-4);
height: 100%;
}
.seriesList {
display: flex;
flex-direction: column;
gap: var(--spacing-4);
height: 100%;
}
.seriesListSearch {
margin-bottom: var(--spacing-8);
}
.seriesListTitle {
margin-top: var(--spacing-6);
font-size: var(--periscope-font-size-base);
font-weight: var(--font-weight-normal);
}
.seriesListItems {
display: flex;
flex-direction: column;
gap: var(--spacing-4);
height: 100%;
overflow-y: auto;
&::-webkit-scrollbar {
width: 0.1rem;
}
&::-webkit-scrollbar-corner {
background: transparent;
}
&::-webkit-scrollbar-thumb {
background: rgb(136 136 136);
border-radius: 0.625rem;
}
&::-webkit-scrollbar-track {
background: transparent;
}
}
.seriesListItem {
display: flex;
flex-direction: row;
gap: var(--spacing-4);
cursor: pointer;
}
.seriesListItemColor {
width: 6px;
height: 6px;
border-radius: 50%;
display: inline-flex;
margin-right: var(--spacing-4);
vertical-align: middle;
}

View File

@@ -1,180 +0,0 @@
.anomaly-alert-evaluation-view {
display: flex;
flex-direction: row;
justify-content: space-between;
gap: 8px;
width: 100%;
height: 100%;
.anomaly-alert-evaluation-view-chart-section {
height: 100%;
width: 100%;
display: flex;
justify-content: center;
align-items: center;
&.has-multi-series-data {
width: calc(100% - 240px);
}
.anomaly-alert-evaluation-view-no-data-container {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
gap: 8px;
}
}
.anomaly-alert-evaluation-view-series-selection {
display: flex;
flex-direction: column;
gap: 8px;
width: 240px;
padding: 0px 8px;
height: 100%;
.anomaly-alert-evaluation-view-series-list {
display: flex;
flex-direction: column;
gap: 8px;
height: 100%;
.anomaly-alert-evaluation-view-series-list-search {
margin-bottom: 16px;
}
.anomaly-alert-evaluation-view-series-list-title {
margin-top: 12px;
font-size: 13px !important;
font-weight: 400;
}
.anomaly-alert-evaluation-view-series-list-items {
display: flex;
flex-direction: column;
gap: 8px;
height: 100%;
overflow-y: auto;
.anomaly-alert-evaluation-view-series-list-item {
display: flex;
flex-direction: row;
gap: 8px;
.anomaly-alert-evaluation-view-series-list-item-color {
width: 6px;
height: 6px;
border-radius: 50%;
display: inline-flex;
margin-right: 8px;
vertical-align: middle;
}
cursor: pointer;
}
&::-webkit-scrollbar {
width: 0.1rem;
}
&::-webkit-scrollbar-corner {
background: transparent;
}
&::-webkit-scrollbar-thumb {
background: rgb(136, 136, 136);
border-radius: 0.625rem;
}
&::-webkit-scrollbar-track {
background: transparent;
}
}
}
}
.uplot {
.u-title {
text-align: center;
font-size: 18px;
font-weight: 400;
display: flex;
height: 40px;
font-size: 13px;
align-items: center;
}
.u-legend {
display: flex;
margin-top: 16px;
tbody {
width: 100%;
.u-series {
display: inline-flex;
}
}
}
}
}
.uplot-tooltip {
background-color: rgba(0, 0, 0, 0.9);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
color: #ddd;
font-size: 13px;
line-height: 1.4;
padding: 8px 12px;
pointer-events: none;
position: absolute;
z-index: 100;
max-height: 500px;
width: 280px;
overflow-y: auto;
display: none; /* Hide tooltip by default */
&::-webkit-scrollbar {
width: 0.3rem;
}
&::-webkit-scrollbar-corner {
background: transparent;
}
&::-webkit-scrollbar-thumb {
background: rgb(136, 136, 136);
border-radius: 0.625rem;
}
&::-webkit-scrollbar-track {
background: transparent;
}
}
.uplot-tooltip-title {
font-weight: bold;
margin-bottom: 4px;
}
.uplot-tooltip-series {
display: flex;
gap: 4px;
padding: 4px 0px;
align-items: center;
}
.uplot-tooltip-series-name {
margin-right: 4px;
}
.uplot-tooltip-band {
font-style: italic;
color: #666;
}
.uplot-tooltip-marker {
display: inline-block;
width: 6px;
height: 6px;
border-radius: 50%;
margin-right: 8px;
vertical-align: middle;
}

View File

@@ -15,7 +15,7 @@ import uPlot from 'uplot';
import tooltipPlugin from './tooltipPlugin';
import 'uplot/dist/uPlot.min.css';
import './AnomalyAlertEvaluationView.styles.scss';
import styles from './AnomalyAlertEvaluationView.module.scss';
const { Search } = Input;
@@ -284,11 +284,11 @@ function AnomalyAlertEvaluationView({
}, 300);
return (
<div className="anomaly-alert-evaluation-view">
<div className={styles.anomalyAlertEvaluationView}>
<div
className={`anomaly-alert-evaluation-view-chart-section ${
allSeries.length > 1 ? 'has-multi-series-data' : ''
}`}
className={
allSeries.length > 1 ? styles.chartSectionMultiSeries : styles.chartSection
}
ref={graphRef}
>
{allSeries.length > 0 ? (
@@ -298,7 +298,7 @@ function AnomalyAlertEvaluationView({
chartRef={chartRef}
/>
) : (
<div className="anomaly-alert-evaluation-view-no-data-container">
<div className={styles.noDataContainer}>
<ChartLine size={48} strokeWidth={0.5} />
<Typography>No Data</Typography>
@@ -307,20 +307,20 @@ function AnomalyAlertEvaluationView({
</div>
{allSeries.length > 1 && (
<div className="anomaly-alert-evaluation-view-series-selection">
<div className={styles.seriesSelection}>
{allSeries.length > 1 && (
<div className="anomaly-alert-evaluation-view-series-list">
<div className={styles.seriesList}>
<Search
className="anomaly-alert-evaluation-view-series-list-search"
className={styles.seriesListSearch}
placeholder="Search a series"
allowClear
onChange={handleSearchValueChange}
/>
<div className="anomaly-alert-evaluation-view-series-list-items">
<div className={styles.seriesListItems}>
{filteredSeriesKeys.length > 0 && (
<Checkbox
className="anomaly-alert-evaluation-view-series-list-item"
className={styles.seriesListItem}
name="series"
value={selectedSeries === null}
onChange={(): void => handleSeriesChange(null)}
@@ -332,14 +332,14 @@ function AnomalyAlertEvaluationView({
{filteredSeriesKeys.map((seriesKey) => (
<div key={seriesKey}>
<Checkbox
className="anomaly-alert-evaluation-view-series-list-item"
className={styles.seriesListItem}
key={seriesKey}
name="series"
value={selectedSeries === seriesKey}
onChange={(): void => handleSeriesChange(seriesKey)}
>
<div
className="anomaly-alert-evaluation-view-series-list-item-color"
className={styles.seriesListItemColor}
style={{ backgroundColor: seriesData[seriesKey].color }}
/>

View File

@@ -1,7 +1,7 @@
import { QueryClient, QueryClientProvider } from 'react-query';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import logEvent from 'api/common/logEvent';
import { logEventMock } from '__tests__/logEventMock';
import { GlobalShortcuts } from 'constants/shortcuts/globalShortcuts';
import { USER_PREFERENCES } from 'constants/userPreferences';
import {
@@ -24,8 +24,6 @@ jest.mock('providers/cmdKProvider', () => ({
}),
}));
jest.mock('api/common/logEvent', () => jest.fn());
// Mock the AppContext
const mockUpdateUserPreferenceInContext = jest.fn();
@@ -139,7 +137,7 @@ describe('Sidebar Toggle Shortcut', () => {
it('should log the toggle event with correct parameters', async () => {
const user = userEvent.setup();
const mockHandleShortcut = jest.fn(() => {
logEvent('Global Shortcut: Sidebar Toggle', {
logEventMock('Global Shortcut: Sidebar Toggle', {
previousState: false,
newState: true,
});
@@ -155,10 +153,13 @@ describe('Sidebar Toggle Shortcut', () => {
await user.keyboard(SHIFT_B_KEYBOARD_SHORTCUT);
expect(logEvent).toHaveBeenCalledWith('Global Shortcut: Sidebar Toggle', {
previousState: false,
newState: true,
});
expect(logEventMock).toHaveBeenCalledWith(
'Global Shortcut: Sidebar Toggle',
{
previousState: false,
newState: true,
},
);
});
it('should update user preference in context', async () => {

View File

@@ -0,0 +1,199 @@
.billingContainer {
margin-bottom: var(--spacing-20);
padding-top: 36px;
width: 90%;
margin: 0 auto;
.pageHeader {
margin-bottom: var(--spacing-8);
.pageHeaderTitle {
font-weight: var(--label-medium-500-font-weight);
font-size: var(--label-medium-500-font-size);
line-height: 32px;
letter-spacing: -0.08px;
color: var(--l1-foreground);
}
.pageHeaderSubtitle {
font-size: var(--font-size-sm);
line-height: var(--line-height-20);
letter-spacing: -0.07px;
color: var(--l2-foreground);
}
}
.pageInfoTitle {
margin: 0;
font-size: var(--font-size-sm);
font-weight: var(--font-weight-medium);
line-height: var(--line-height-20);
letter-spacing: -0.07px;
color: var(--l1-foreground);
}
.pageInfoSubtitle {
margin: 0;
font-size: var(--font-size-sm);
font-weight: var(--font-weight-normal);
line-height: var(--line-height-18);
letter-spacing: -0.07px;
color: var(--l2-foreground);
}
.pageInfo {
:global(.ant-card) {
padding: var(--padding-3);
}
.billingManageBtn {
background: var(--l3-background);
&:hover {
background: var(--l3-background-hover);
}
}
}
.billingSummary {
margin: var(--spacing-12) var(--spacing-4);
}
.billingDetails {
margin: var(--spacing-12) 0;
border: 1px solid var(--l1-border);
border-radius: 2px;
overflow: hidden;
:global {
.ant-table {
background: var(--l2-background);
}
.ant-table-thead > tr > th {
height: 52px;
padding: 0 var(--padding-4);
color: var(--l3-foreground);
font-size: var(--font-size-xs);
font-weight: var(--font-weight-medium);
letter-spacing: 0.48px;
text-transform: uppercase;
}
.ant-table-tbody > tr > td {
height: 52px;
padding: 0 var(--padding-4);
background: var(--l2-background);
border-bottom: 1px solid var(--l1-border);
color: var(--l2-foreground);
font-size: var(--font-size-sm);
&:first-child {
color: var(--l1-foreground);
}
&:not(:first-child) {
font-feature-settings:
'zero' 1,
'lnum' 1,
'tnum' 1;
}
}
.ant-table-tbody > tr:last-child > td {
border-bottom: none;
}
.ant-table-tbody > tr:hover > td {
background: var(--l2-background) !important;
}
}
.billingDetailsHeaderCell {
position: relative;
background: var(--l2-background) !important;
border: none !important;
border-bottom: 1px solid var(--l1-border) !important;
box-shadow: none !important;
&::after {
content: '';
position: absolute;
inset-block: 0;
inset-inline-end: 0;
width: 2px;
background: var(--l2-background);
z-index: 1;
}
}
}
.upgradePlanBenefits {
margin: 0 var(--spacing-4);
border: 1px solid var(--l1-border);
border-radius: 5px;
padding: 0 var(--padding-12);
.planBenefits {
.planBenefit {
display: flex;
align-items: center;
gap: var(--spacing-8);
margin: var(--spacing-8) 0;
}
}
}
.billingGraphSection {
border: 1px solid var(--l1-border);
border-radius: 4px;
overflow: hidden;
margin-bottom: var(--spacing-4);
.billingGraphFooter {
display: flex;
gap: var(--spacing-4);
padding: var(--padding-3) var(--padding-4);
border-top: 1px solid var(--l1-border);
background: var(--l2-background);
.billingFooterBtn {
background: var(--l3-background);
&:hover {
background: var(--l3-background-hover);
}
}
}
}
.emptyGraphCard {
:global(.ant-card-body) {
height: 40vh;
display: flex;
justify-content: center;
align-items: center;
}
}
.billingUpdateNote {
margin-top: var(--spacing-8);
font-family: var(--font-family-inter);
font-size: var(--font-size-sm);
font-style: normal;
font-weight: var(--font-weight-normal);
line-height: 22px;
letter-spacing: -0.07px;
}
:global {
.ant-skeleton.ant-skeleton-element.ant-skeleton-active {
width: 100%;
min-width: 100%;
}
.ant-skeleton.ant-skeleton-element .ant-skeleton-input {
min-width: 100% !important;
}
}
}

View File

@@ -1,70 +0,0 @@
.billing-container {
margin-bottom: 40px;
padding-top: 36px;
width: 90%;
margin: 0 auto;
.billing-summary {
margin: 24px 8px;
}
.billing-details {
margin: 24px 0px;
.ant-table-title {
color: var(--l2-foreground);
background-color: var(--l3-background);
}
.ant-table-cell {
background-color: var(--l1-background);
border-color: var(--l1-border);
}
.ant-table-tbody {
td {
border-color: var(--l1-border);
}
}
}
.upgrade-plan-benefits {
margin: 0px 8px;
border: 1px solid var(--l1-border);
border-radius: 5px;
padding: 0 48px;
.plan-benefits {
.plan-benefit {
display: flex;
align-items: center;
gap: 16px;
margin: 16px 0;
}
}
}
.empty-graph-card {
.ant-card-body {
height: 40vh;
display: flex;
justify-content: center;
align-items: center;
}
}
.billing-update-note {
text-align: left;
font-size: 13px;
color: var(--l2-foreground);
margin-top: 16px;
}
}
.ant-skeleton.ant-skeleton-element.ant-skeleton-active {
width: 100%;
min-width: 100%;
}
.ant-skeleton.ant-skeleton-element .ant-skeleton-input {
min-width: 100% !important;
}

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