Compare commits

...

19 Commits

Author SHA1 Message Date
Piyush Singariya
fd413d336d fix: ClickHouse 25.12.5 Trace Operator query analyzer fail due to dangling CTE (#11268)
Some checks are pending
build-staging / prepare (push) Waiting to run
build-staging / js-build (push) Blocked by required conditions
build-staging / staging (push) Blocked by required conditions
build-staging / go-build (push) Blocked by required conditions
Release Drafter / update_release_draft (push) Waiting to run
* fix: trace raw export e2e

* chore: separate e2e test file

* chore: file rename

* chore: fmtlint

* fix: remove specific of timestamp

* chore: fmt python

* chore: remove comments

* fix: alias all core columns

* fix: return spans from

* revert: double select

* revert: build list query

* chore: comments and test

* revert: unused test

* chore: tests updated

* chore: fmt py

* chore: fmt py

* fix: integration tests

* fix: tests rewritten

* fix: replace JOIN with IN

* test: wip rewrite integration tests better

* fix: tests are better

* fix: use pytest param

* chore: fix the comments

* fix: py fmt

* chore: added length checks

* chore: import functions from querier
2026-05-27 15:49:56 +00:00
Karan Balani
7b892ee1fc chore(meterreporter): document jitter config in example.yaml (#11482)
* chore(meterreporter): document jitter config in example.yaml

* chore(meterreporter): fix stale ResolvedJitter reference in Jitter doc comment

* chore: fix comment

---------

Co-authored-by: Karan Balani <29383381+balanikaran@users.noreply.github.com>
2026-05-27 15:35:18 +00:00
Yunus M
31e4012fe5 refactor: replace antd Checkbox with @signozhq/ui Checkbox (#11396)
* refactor: replace antd Checkbox with @signozhq/ui Checkbox (batch 1)

Migrates mechanical checkbox callsites from antd to @signozhq/ui/checkbox:

- Trace/Filters/Panel/.../Common/Checkbox.tsx
- NewWidget/LeftContainer/ExplorerAttributeColumns.tsx
- AnomalyAlertEvaluationView.tsx
- QuickFilters/FilterRenderers/Checkbox/Checkbox.tsx
- NewSelect/CustomMultiSelect.tsx
- RolesSelect/RolesSelect.tsx
- TraceDetailsV3/.../SpanPercentilePanel.tsx

API mapping: checked -> value, defaultChecked -> defaultValue, onChange(e)
where e.target.checked is read -> onChange(checked) where checked is
CheckedState. Dropped antd-only props (type="checkbox", rootClassName ->
className, form-value `value=` semantics). RolesSelect wraps the checkbox
in a div to host pointerEvents:none since style is not in CheckboxProps.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor: drop antd CheckboxChangeEvent in checkbox callsites (batch 2)

Two callsites that read e.target.checked via antd's CheckboxChangeEvent
type now receive the CheckedState (boolean | 'indeterminate') directly
from @signozhq/ui Checkbox's onChange.

- TopNav/AutoRefreshV2/index.tsx — onChangeAutoRefreshHandler signature
  changes; derives boolean checked from CheckedState.
- TracesExplorer/Filter/SectionContent.tsx — onCheckHandler likewise;
  also swap data-testid -> testId since SigNoz UI Checkbox wraps the
  input in a div that exposes testId.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor: migrate complex antd Checkbox callsites to @signozhq/ui (batch 3)

Handles the four callsites that needed shape changes, not just renames:

- RegionSelector — collapses separate `checked` + `indeterminate` props
  into a single `value: true | false | 'indeterminate'` (SigNoz UI uses
  CheckedState, not two props).
- InteractiveQuestion — replaces `<Checkbox.Group>` (no SigNoz UI
  equivalent) with a plain `<div>` of individual `<Checkbox>`s that
  push/pull from the existing `selected: string[]` state on each toggle.
- GridCardLayout/types.ts + CustomCheckBox — drops the antd-only
  `CheckboxChangeEvent` type from the `checkBoxOnChangeHandler`
  signature and removes the `ConfigProvider` theme override that
  previously colored each chart-legend checkbox to match its series.
  The dynamic per-series color is now injected via the SigNoz UI
  Checkbox's exposed CSS variables
  (`--checkbox-checked-background`, `--checkbox-border-color`)
  applied through a wrapper `<span style={…}>`.

After this commit `grep -rE "from 'antd'.*Checkbox" frontend/src` and
`grep -rE "CheckboxChangeEvent" frontend/src` both return zero matches.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test: adapt checkbox assertions to SigNoz UI Checkbox DOM

The SigNoz UI Checkbox renders <button role="checkbox" data-state="…">
via Radix instead of antd's <input type="checkbox">. Four test suites
queried the old DOM and broke after the migration.

- TracesExplorer.test.tsx — getByTestId returns the wrapper div now (the
  SigNoz UI Checkbox exposes testId on the wrapper, not the button).
  Querying the inner [role="checkbox"] and asserting data-state.
- DynamicVariableDefaultBehavior.test.tsx, PanelManagement.test.tsx,
  getChartManagerColumns.test.tsx — replace querySelector(
  'input[type="checkbox"]') with [role="checkbox"], and .checked /
  toBeChecked() with toHaveAttribute('data-state', 'checked').

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix: add min-height to checkbox value section

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 15:17:43 +00:00
Manika Malhotra
71cb97f52f chore: migrate antd divider to signozhq/ui divider (#11474)
* chore: migrate antd divider to signozhq/ui divider

* fix: remove divider from routing policies
2026-05-27 15:11:42 +00:00
Gaurav Tewari
11f1789dea refactor(frontend): migrate plain antd dropdown to @signozhq/ui/dropdown (#11400)
* chore: migrate dropdown

* fix: self review changes

* fix: side nav issue

* fix: another bug

* chore: migration from antd

* fix: failing test

* fix: lint

* chore: self review comments

* fix: add dropdown to list

* chore: remove internal docs

* chore: save antd file

* fix: lint

* fix: lint

* chore: remove ox lint

* fix: dropdown

---------

Co-authored-by: Gaurav Tewari <tewarig@users.noreply.github.com>
2026-05-27 14:37:34 +00:00
Yunus M
1a97e90117 feat: replace Radio components with ToggleGroup in various components (#11391)
* refactor: replace Radio components with ToggleGroup in various components

* refactor: replace ToggleGroup with ToggleGroupSimple

* fix: update theme selection tests to use role-based queries

* feat: add styles for add-on tab title alignment and spacing
2026-05-27 14:09:28 +00:00
Aditya Singh
939f0d7a05 feat(trace-detail-v3): new soft colour palette for waterfall + flamegraph (#11468)
Replace the V3 trace colour system with the new soft 28-colour palette from
the design guide. Deterministic per group value (reuses existing hashFn),
theme-adaptive via darkenHex (bright base in dark mode, darkened variant in
light), and reserves #FC4E4E for errors. Covers waterfall + flamegraph bars,
labels, event dots, connectors, service dots, hover cards and analytics
breakdown. Heat/depth modulation intentionally left out for v1.
2026-05-27 13:29:19 +00:00
Yunus M
9d1c27cb57 fix: added utility functions to calculate minimum step intervals and time ranges (#11447) 2026-05-27 12:58:52 +00:00
Piyush Singariya
07ce2d341c chore: preserve order of pipelines between memory_limiter and batch (#11461)
* chore: var names changed

* fix: preserve order of pipelines between memory_limiter and batch

* revert: test name

* fix: remove old pipelines

* revert: var name change

* fix: changing the order; set user before custom
2026-05-27 12:47:41 +00:00
Vinicius Lourenço
08e723fd53 chore(agents): add more instructions for code quality (#11466) 2026-05-27 11:44:15 +00:00
Manika Malhotra
c074d09842 chore: migrate Avatar from antd to signozhq/ui Avatar (#11478)
* chore: migrate Avatar from antd to signozhq/ui Avatar

* fix: pipelines to use badge instead of avatar

* chore: add avatar to no-antd components
2026-05-27 11:32:26 +00:00
Naman Verma
c68f237a8a feat: v2 create and get dashboard API (#11125)
* feat: openapi spec generation

* test: script to generate test dashboard data in a sql db

* test: fixes in dashboard perf testing data generator

* test: perf test script for both sql flavours

* test: data column in perf tests should match real data

* test: much bigger json for data column

* chore: comment clean up

* chore: separate file for perses replicas

* test: more descriptive test file name

* chore: move plugin maps to correct file

* chore: comment cleanup

* test: add tests for spec wrappers

* chore: better file names

* chore: better file name

* chore: too many comments

* fix: js lint errors

* fix: dot at the end of a comment

* chore: better error messages

* fix: strict decode variable spec as well

* fix: remove textbox plugin from openapi spec

* chore: renames and code rearrangement

* chore: better comment to explain what restrictKindToLiteral does

* chore: cleaner comment

* chore: cleaner comment

* chore: cleaner comment

* chore: better method name

* chore: cleanup testing code

* chore: code movement

* chore: code movement

* chore: code movement

* chore: go lint fix (godot)

* chore: code movement

* chore: cleanup comments

* chore: better method name extractKindAndSpec

* test: test for drift detection mechanics

* feat: define tags module for v2 dashboard creation

* feat: enum for entity type that other modules can register

* 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

* 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

* fix: query-less panels not allowed

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

* 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: extend bun in joinedRow

* 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

* 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

* 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: update frontend schema

* chore: generate api specs

* fix: add source for v2 dashboards

* 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

* 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

* test: fix unit tests referring to > threshold operator

* fix: use must new uuid for org id
2026-05-27 11:15:02 +00:00
Jatinderjit Singh
e75a0b59d6 fix(rules): use alertmanager external URL for related logs/traces and generator URL (#11413)
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(rules): use alertmanager external URL for related logs/traces and generator URL

Plumbs SIGNOZ_ALERTMANAGER_SIGNOZ_EXTERNAL__URL through the rule manager so
the host portion of related_logs / related_traces come from configured
external URL instead of the frontend window.location captured at
rule-creation time. Frontend-supplied source URL is retained
as a fallback when the env var has not been set so existing self-hosted
deployments keep working.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* feat(rules)!: drop frontend source URL fallback for related logs/traces and generator URL

Always derive the host portion of the rule generator URL and the
related_logs / related_traces annotations from SIGNOZ_ALERTMANAGER_SIGNOZ_EXTERNAL__URL.
Self-hosted deployments that have not set the env var will see the
default (http://localhost:8080) in alert notifications, which makes the
need to configure it surface clearly. Revert this commit to restore the
prior fallback behavior.

Refs SigNoz/engineering-pod#5055

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* refactor: simplify ExternalURLHost

* chore: use alert overview url for generator url

* refactor(rules): source external URL via alertmanager.Config() instead of plumbing through main

Address PR feedback (therealpandey): expose a Config() accessor on the
alertmanager.Alertmanager interface and consume the external URL from it
inside signozruler.NewFactory. This drops the *url.URL parameter that
was previously threaded through cmd/community/server.go,
cmd/enterprise/server.go, and pkg/signoz/signoz.go.

Also switch BaseRule.GeneratorURL to *url.URL.JoinPath + Query so the
URL is composed correctly when the external URL carries a base path
(e.g. https://signoz.example.com/signoz) and ruleId is properly
query-encoded, as suggested in review.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* refactor: expose alertmanagerserver.Config

* refactor: rename "linksTo" to "paramsFor"

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: Srikanth Chekuri <srikanth.chekuri92@gmail.com>
2026-05-27 10:52:32 +00:00
Vikrant Gupta
6dfceeaf04 fix(deps): upgrade idna to 3.16 to fix CVE-2026-45409 (#11479)
Specially crafted inputs to idna.encode() could bypass the CVE-2024-3651
fix and cause denial-of-service. Patched in idna >= 3.15.
2026-05-27 10:35:01 +00:00
Vikrant Gupta
69806d7dc4 feat(authz): upgrade OpenFGA from v1.11.2 to v1.14.1 (#11475)
* feat(authz): upgrade OpenFGA from v1.11.2 to v1.14.0

Upgrade OpenFGA and its companion packages:
- github.com/openfga/openfga v1.11.2 -> v1.14.0
- github.com/openfga/api/proto v0.0.0-20250909 -> v0.0.0-20260319
- github.com/openfga/language/pkg/go v0.2.0-beta.2.0.20251027 -> v0.2.0-beta.2.0.20251202

This picks up security fixes (CVE-2026-34972 BatchCheck,
CVE-2026-33729 Check caching), ListObjects deadlock fixes,
and pipeline performance improvements.

Breaking change in v1.14.0: the PostgreSQL storage code now passes
int32 for the changelog.operation column instead of the protobuf enum
type used in v1.11.2. SigNoz migration 054 created this column as TEXT,
causing "unable to encode 0 into text format for text (OID 25)" errors.

Migration 090 fixes all OpenFGA table schema mismatches from 054:
- changelog: drop and recreate with operation INTEGER (was TEXT) and
  corrected condition_name (TEXT) / condition_context (BYTEA) types
  that were swapped
- tuple (PG only): alter condition_name BYTEA->TEXT and
  condition_context TEXT->BYTEA to match OpenFGA's expected schema

* chore: suppress deprecated AsyncInsert lint warning

clickhouse-go/v2 v2.43.0 (pulled in as transitive dep from the OpenFGA
upgrade) deprecated AsyncInsert in favor of WithAsync(). Suppress the
staticcheck warning for now.

* fix(authz): remove tuple condition column alter from migration 090

The TEXT -> BYTEA cast fails without an explicit USING clause which
sqlschema.AlterColumn does not support. Since SigNoz does not use FGA
conditions (columns are always NULL), defer this fix.

* chore: add TODO for AsyncInsert deprecation

Tracked in https://github.com/SigNoz/engineering-pod/issues/5093

* feat(authz): bump OpenFGA from v1.14.0 to v1.14.1

Picks up bug fixes and performance improvements:
- Reduced heap allocations in ListObjects
- Improved cache key generation performance
- Fixed AuthZEN discovery metadata host-header poisoning
- Server shutdown timeout configuration
2026-05-27 10:23:39 +00:00
Tushar Vats
8da9535c80 chore: breakdown query range function (#11211)
* chore: breakdown query range function

* fix: move unexported helper functions to end of file

---------

Co-authored-by: Yunus M <myounis.ar@live.com>
2026-05-27 09:38:45 +00:00
Yunus M
99866a91e4 feat(ai-assistant): base route, auth-retry streaming, and rate-limit UX (#11457)
* feat: update routing and permissions for AI Assistant feature

* feat: ai assistant routing, mid-execution auth recovery

* chore: remove local overrides
2026-05-27 08:51:49 +00:00
Aditya Singh
f94fa7db89 feat(trace-details): added clear filter button in trace details header + UI restructure (#11345)
* feat: setup types and interface for waterfall v3

v3 is required for udpating the response json of
the waterfall api. There wont' be any logical change.
Using this requirement as an opportunity to move
waterfall api to provider codebase architecture from
older query-service

* refactor: move type conversion logic to types pkg

* chore: add reason for using snake case in response

* fix: update span.attributes to map of string to any

To support otel format of diffrent types of attributes

* fix: remove unused fields and rename span type

To avoid confusing with otel span

* refactor: convert waterfall api to modules format

* chore: add same test cases as for old waterfall api

* chore: avoid sorting on every traversal

* fix: remove unused fields and rename span type

To avoid confusing with otel span

* fix: rename timestamp to milli for readability

* fix: add timeout to module context

* fix: use typed paramter field in logs

* feat: api integration

* feat: add limit

* feat: minor change

* feat: supress click

* chore: generate openapi spec for v3 waterfall

* feat: fix test

* feat: fix test

* feat: lint fix

* feat: span details ux

* feat: analytics

* feat: add icons

* feat: added loading to flamegraph and timeout to webworker

* feat: sync error and loading state for flamegraph for n/w and computation logic

* feat: auto scroll horizontally to span

* feat: show total span count

* feat: disable anaytics span tab for now

* feat: add span details loader

* feat: prevent api call on closing span detail

* fix: remove timeout since waterfall take longer

* fix: use int16 for status code as per db schema

* fix: update openapi specs

* feat: make filter and search work with flamegraph

* feat: filter ui fix

* feat: remove trace header

* feat: new filter ui

* feat: setup types and interface for waterfall v3

v3 is required for udpating the response json of
the waterfall api. There wont' be any logical change.
Using this requirement as an opportunity to move
waterfall api to provider codebase architecture from
older query-service

* refactor: move type conversion logic to types pkg

* chore: add reason for using snake case in response

* fix: update span.attributes to map of string to any

To support otel format of diffrent types of attributes

* fix: remove unused fields and rename span type

To avoid confusing with otel span

* refactor: convert waterfall api to modules format

* chore: add same test cases as for old waterfall api

* chore: avoid sorting on every traversal

* fix: remove unused fields and rename span type

To avoid confusing with otel span

* fix: rename timestamp to milli for readability

* fix: add timeout to module context

* fix: use typed paramter field in logs

* chore: generate openapi spec for v3 waterfall

* fix: remove timeout since waterfall take longer

* fix: use int16 for status code as per db schema

* fix: update openapi specs

* feat: api integration

* feat: automatically scroll left on vertical scroll

* feat: reduce time

* feat: set limit to 100k for flamegraph

* feat: show child count in waterfall

* fix: align timeline and span length in flamegraph and waterfall

* feat: fix flamegraph and waterfall bg color

* feat: show caution on sampled flamegraph

* feat: api integration v3

* feat: disable scroll to view for collapse and uncollapse

* feat: setup types and interface for waterfall v3

v3 is required for udpating the response json of
the waterfall api. There wont' be any logical change.
Using this requirement as an opportunity to move
waterfall api to provider codebase architecture from
older query-service

* refactor: move type conversion logic to types pkg

* chore: add reason for using snake case in response

* fix: update span.attributes to map of string to any

To support otel format of diffrent types of attributes

* fix: remove unused fields and rename span type

To avoid confusing with otel span

* refactor: convert waterfall api to modules format

* chore: add same test cases as for old waterfall api

* chore: avoid sorting on every traversal

* fix: remove unused fields and rename span type

To avoid confusing with otel span

* fix: rename timestamp to milli for readability

* fix: add timeout to module context

* fix: use typed paramter field in logs

* chore: generate openapi spec for v3 waterfall

* fix: remove timeout since waterfall take longer

* fix: use int16 for status code as per db schema

* fix: update openapi specs

* refactor: break down GetWaterfall method for readability

* chore: avoid returning nil, nil

* refactor: move type creation and constants to types package

- Move DB/table/cache/windowing constants to tracedetailtypes package
- Add NewWaterfallTrace and NewWaterfallResponse constructors in types
- Use constructors in module.go instead of inline struct literals
- Reorder waterfall.go so public functions precede private ones

* refactor: extract ClickHouse queries into a store abstraction

Move GetTraceSummary and GetTraceSpans out of module.go into a
traceStore interface backed by clickhouseTraceStore in store.go.
The module struct now holds a traceStore instead of a raw
telemetrystore.TelemetryStore, keeping DB access separate from
business logic.

* refactor: move error to types as well

* refactor: separate out store calls and computations

* refactor: breakdown GetSelectedSpans for readability

* refactor: return 404 on missing trace and other cleanup

* refactor: use same method for cache key creation

* chore: remove unused duration nano field

* chore: use sqlbuilder in clickhouse store where possible

* feat: dropdown added to span details

* feat: fix color duplications

* feat: no data screen

* feat: old trace btn added

* feat: minor fix

* feat: rename copy to copy value

* feat: delete unused file

* feat: use semantic tokens

* feat: use semantic tokens

* feat: add crosshair

* feat: fix test

* feat: disable crosshair in waterfall

* feat: fix colors

* feat: minor fix

* feat: add status codes

* feat: load all spans in waterfall under limit

* feat: uncollapse spans on select from flamegraph

* feat: style fix

* feat: add service name

* feat: open in new tab

* feat: add trace details header

* feat: add trace details header styles

* feat: add trace details header styles

* feat: minor changes

* feat: floating fields set

* feat: filters init

* feat: filter toggle added

* feat: fix color

* fix: scroll to span in frontend mode

* feat: delete waterfall go

* feat: minor change

* feat: minor change

* feat: lint fix

* feat: analytics spans

* feat: color by field

* feat: save color by pref in user pref

* feat: migrate v2 pinned attr

* feat: preview fields

* feat: minor refactors

* feat: minor refactors

* feat: v3 behind feature flag

* feat: minor refactors

* feat: packages remove

* feat: packages remove

* feat: remove common component

* feat: remove antd component usage

* feat: leaf node indent fix

* feat: fix mouse wheel in json view

* feat: update signoz ui

* feat: remove feature flag

* feat: fixed the waterfall span hover card

* feat: fix hidden filters

* feat: trace details always visible

* feat: correct status code

* fix: pagination calls in waterfall

* feat: fix failing test

* feat: show error count

* feat: fix waterfall child sibling indent

* feat: change how we show span hover data in waterfall

* feat: fix logs in span details styles

* feat: minor fixes

* feat: make trace id copyable

* feat: add status message to highlight section

* feat: persist user choosing old view

* feat: add more fields in color by

* feat: add llm as fast filter

* feat: show api error correctly

* feat: update test cases

* feat: revert route change

* feat: revert route change

* feat: replace antd btns

* feat: allow removing all fields in preview

* feat: send selected span when flamegraph is sampled

* feat: only scroll when span is not in view

* feat: auto expand on highlight errors

* feat: move analytics panel

* feat: additional check

* feat: minor fix

* feat: minor fix

* feat: dont use antd button and tooltip

* feat: dont use antd button and tooltip

* feat: update icons

* feat: minor change

* feat: minor change

* feat: move to zustand

* feat: update test cases

* feat: update border color

* feat: add icons

* feat: support filter on parent keys

* feat: add links to non filterable keys

* feat: minor fix

* feat: use pinned attributes accross views

* feat: update tests

* feat: hide v3

* feat: migrate to css modules

* feat: fix minor style

* feat: fix test

* feat: enable new trace details

* feat: remove unnecessary waterfall api calls if span already in the list

* feat: minor change

* feat: add clear filter

* feat: realign trace details filters

* feat: restructure trace details header

* feat: minor fix

* feat: update tests

* feat: update tests

---------

Co-authored-by: Nikhil Soni <nikhil.soni@signoz.io>
2026-05-27 07:42:59 +00:00
primus-bot[bot]
aa96ec6fe9 chore(release): bump to v0.126.0 (#11472)
Co-authored-by: primus-bot[bot] <171087277+primus-bot[bot]@users.noreply.github.com>
2026-05-27 07:39:43 +00:00
215 changed files with 8356 additions and 3548 deletions

View File

@@ -33,6 +33,7 @@ import (
"github.com/SigNoz/signoz/pkg/modules/retention"
"github.com/SigNoz/signoz/pkg/modules/rulestatehistory"
"github.com/SigNoz/signoz/pkg/modules/serviceaccount"
"github.com/SigNoz/signoz/pkg/modules/tag"
"github.com/SigNoz/signoz/pkg/prometheus"
"github.com/SigNoz/signoz/pkg/querier"
"github.com/SigNoz/signoz/pkg/query-service/app"
@@ -100,8 +101,8 @@ func runServer(ctx context.Context, config signoz.Config, logger *slog.Logger) e
return openfgaauthz.NewProviderFactory(sqlstore, openfgaschema.NewSchema().Get(ctx), openfgaDataStore, authtypes.NewRegistry()), nil
},
func(store sqlstore.SQLStore, settings factory.ProviderSettings, analytics analytics.Analytics, orgGetter organization.Getter, queryParser queryparser.QueryParser, _ querier.Querier, _ licensing.Licensing) dashboard.Module {
return impldashboard.NewModule(impldashboard.NewStore(store), settings, analytics, orgGetter, queryParser)
func(store sqlstore.SQLStore, settings factory.ProviderSettings, analytics analytics.Analytics, orgGetter organization.Getter, queryParser queryparser.QueryParser, _ querier.Querier, _ licensing.Licensing, tagModule tag.Module) dashboard.Module {
return impldashboard.NewModule(impldashboard.NewStore(store), settings, analytics, orgGetter, queryParser, tagModule)
},
func(_ licensing.Licensing) factory.ProviderFactory[gateway.Gateway, gateway.Config] {
return noopgateway.NewProviderFactory()

View File

@@ -50,6 +50,7 @@ import (
"github.com/SigNoz/signoz/pkg/modules/retention"
"github.com/SigNoz/signoz/pkg/modules/rulestatehistory"
"github.com/SigNoz/signoz/pkg/modules/serviceaccount"
"github.com/SigNoz/signoz/pkg/modules/tag"
"github.com/SigNoz/signoz/pkg/prometheus"
"github.com/SigNoz/signoz/pkg/querier"
"github.com/SigNoz/signoz/pkg/queryparser"
@@ -133,8 +134,8 @@ func runServer(ctx context.Context, config signoz.Config, logger *slog.Logger) e
}
return openfgaauthz.NewProviderFactory(sqlstore, openfgaschema.NewSchema().Get(ctx), openfgaDataStore, licensing, onBeforeRoleDelete, authtypes.NewRegistry()), nil
},
func(store sqlstore.SQLStore, settings factory.ProviderSettings, analytics analytics.Analytics, orgGetter organization.Getter, queryParser queryparser.QueryParser, querier querier.Querier, licensing licensing.Licensing) dashboard.Module {
return impldashboard.NewModule(pkgimpldashboard.NewStore(store), settings, analytics, orgGetter, queryParser, querier, licensing)
func(store sqlstore.SQLStore, settings factory.ProviderSettings, analytics analytics.Analytics, orgGetter organization.Getter, queryParser queryparser.QueryParser, querier querier.Querier, licensing licensing.Licensing, tagModule tag.Module) dashboard.Module {
return impldashboard.NewModule(pkgimpldashboard.NewStore(store), settings, analytics, orgGetter, queryParser, querier, licensing, tagModule)
},
func(licensing licensing.Licensing) factory.ProviderFactory[gateway.Gateway, gateway.Config] {
return httpgateway.NewProviderFactory(licensing)

View File

@@ -445,7 +445,12 @@ authz:
##################### Meter Reporter #####################
meterreporter:
# The interval between collection ticks. Minimum 5m.
# The interval between collection ticks. Minimum 10m, maximum 24h.
interval: 6h
# Whether to backfill sealed days from the license creation day.
backfill: true
# Random jitter applied to the first collect and to every subsequent cycle.
# The first collect fires at a random time in [0, jitter); each cycle then takes
# interval - random(0, jitter). Must be between 10m and interval. Defaults to
# min(interval, 2h) when unset.
jitter: 2h

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.125.1
image: signoz/signoz:v0.126.0
ports:
- "8080:8080" # signoz port
# - "6060:6060" # pprof port

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.125.1
image: signoz/signoz:v0.126.0
ports:
- "8080:8080" # signoz port
volumes:

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.125.1}
image: signoz/signoz:${VERSION:-v0.126.0}
container_name: signoz
ports:
- "8080:8080" # signoz port

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.125.1}
image: signoz/signoz:${VERSION:-v0.126.0}
container_name: signoz
ports:
- "8080:8080" # signoz port

File diff suppressed because it is too large Load Diff

View File

@@ -11,6 +11,7 @@ import (
"github.com/SigNoz/signoz/pkg/modules/dashboard"
pkgimpldashboard "github.com/SigNoz/signoz/pkg/modules/dashboard/impldashboard"
"github.com/SigNoz/signoz/pkg/modules/organization"
"github.com/SigNoz/signoz/pkg/modules/tag"
"github.com/SigNoz/signoz/pkg/querier"
"github.com/SigNoz/signoz/pkg/queryparser"
"github.com/SigNoz/signoz/pkg/types"
@@ -30,9 +31,9 @@ type module struct {
licensing licensing.Licensing
}
func NewModule(store dashboardtypes.Store, settings factory.ProviderSettings, analytics analytics.Analytics, orgGetter organization.Getter, queryParser queryparser.QueryParser, querier querier.Querier, licensing licensing.Licensing) dashboard.Module {
func NewModule(store dashboardtypes.Store, settings factory.ProviderSettings, analytics analytics.Analytics, orgGetter organization.Getter, queryParser queryparser.QueryParser, querier querier.Querier, licensing licensing.Licensing, tagModule tag.Module) dashboard.Module {
scopedProviderSettings := factory.NewScopedProviderSettings(settings, "github.com/SigNoz/signoz/ee/modules/dashboard/impldashboard")
pkgDashboardModule := pkgimpldashboard.NewModule(store, settings, analytics, orgGetter, queryParser)
pkgDashboardModule := pkgimpldashboard.NewModule(store, settings, analytics, orgGetter, queryParser, tagModule)
return &module{
pkgDashboardModule: pkgDashboardModule,
@@ -212,6 +213,14 @@ func (module *module) Create(ctx context.Context, orgID valuer.UUID, createdBy s
return module.pkgDashboardModule.Create(ctx, orgID, createdBy, creator, source, data)
}
func (module *module) CreateV2(ctx context.Context, orgID valuer.UUID, createdBy string, creator valuer.UUID, source dashboardtypes.Source, postable dashboardtypes.PostableDashboardV2) (*dashboardtypes.DashboardV2, error) {
return module.pkgDashboardModule.CreateV2(ctx, orgID, createdBy, creator, source, postable)
}
func (module *module) GetV2(ctx context.Context, orgID valuer.UUID, id valuer.UUID) (*dashboardtypes.DashboardV2, error) {
return module.pkgDashboardModule.GetV2(ctx, orgID, id)
}
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

@@ -5,6 +5,7 @@ import (
"encoding/json"
"fmt"
"log/slog"
"net/url"
"sync"
"time"
@@ -47,13 +48,14 @@ func NewAnomalyRule(
p *ruletypes.PostableRule,
querier querier.Querier,
logger *slog.Logger,
externalURL *url.URL,
opts ...baserules.RuleOption,
) (*AnomalyRule, error) {
logger.Info("creating new AnomalyRule", slog.String("rule.id", id))
opts = append(opts, baserules.WithLogger(logger))
baseRule, err := baserules.NewBaseRule(id, orgID, p, opts...)
baseRule, err := baserules.NewBaseRule(id, orgID, p, externalURL, opts...)
if err != nil {
return nil, err
}

View File

@@ -2,6 +2,7 @@ package rules
import (
"context"
"net/url"
"testing"
"time"
@@ -120,6 +121,7 @@ func TestAnomalyRule_NoData_AlertOnAbsent(t *testing.T) {
&postableRule,
nil,
logger,
mustParseURL(t, "http://localhost:8000"),
)
require.NoError(t, err)
@@ -247,7 +249,8 @@ func TestAnomalyRule_NoData_AbsentFor(t *testing.T) {
},
}
rule, err := NewAnomalyRule("test-anomaly-rule", valuer.GenerateUUID(), &postableRule, nil, logger)
externalURL := mustParseURL(t, "http://localhost:8000")
rule, err := NewAnomalyRule("test-anomaly-rule", valuer.GenerateUUID(), &postableRule, nil, logger, externalURL)
require.NoError(t, err)
rule.provider = &mockAnomalyProvider{
@@ -264,3 +267,10 @@ func TestAnomalyRule_NoData_AbsentFor(t *testing.T) {
})
}
}
func mustParseURL(t *testing.T, raw string) *url.URL {
t.Helper()
u, err := url.Parse(raw)
require.NoError(t, err)
return u
}

View File

@@ -34,6 +34,7 @@ func PrepareTaskFunc(opts baserules.PrepareTaskOptions) (baserules.Task, error)
opts.Rule,
opts.Querier,
opts.Logger,
opts.ManagerOpts.Alertmanager.Config().ExternalURL,
baserules.WithEvalDelay(opts.ManagerOpts.EvalDelay),
baserules.WithSQLStore(opts.SQLStore),
baserules.WithQueryParser(opts.ManagerOpts.QueryParser),
@@ -59,6 +60,7 @@ func PrepareTaskFunc(opts baserules.PrepareTaskOptions) (baserules.Task, error)
opts.Rule,
opts.Logger,
opts.ManagerOpts.Prometheus,
opts.ManagerOpts.Alertmanager.Config().ExternalURL,
baserules.WithSQLStore(opts.SQLStore),
baserules.WithQueryParser(opts.ManagerOpts.QueryParser),
baserules.WithMetadataStore(opts.ManagerOpts.MetadataStore),
@@ -82,6 +84,7 @@ func PrepareTaskFunc(opts baserules.PrepareTaskOptions) (baserules.Task, error)
opts.Rule,
opts.Querier,
opts.Logger,
opts.ManagerOpts.Alertmanager.Config().ExternalURL,
baserules.WithEvalDelay(opts.ManagerOpts.EvalDelay),
baserules.WithSQLStore(opts.SQLStore),
baserules.WithQueryParser(opts.ManagerOpts.QueryParser),
@@ -141,6 +144,7 @@ func TestNotification(opts baserules.PrepareTestRuleOptions) (int, error) {
parsedRule,
opts.Querier,
opts.Logger,
opts.ManagerOpts.Alertmanager.Config().ExternalURL,
baserules.WithSendAlways(),
baserules.WithSendUnmatched(),
baserules.WithSQLStore(opts.SQLStore),
@@ -162,6 +166,7 @@ func TestNotification(opts baserules.PrepareTestRuleOptions) (int, error) {
parsedRule,
opts.Logger,
opts.ManagerOpts.Prometheus,
opts.ManagerOpts.Alertmanager.Config().ExternalURL,
baserules.WithSendAlways(),
baserules.WithSendUnmatched(),
baserules.WithSQLStore(opts.SQLStore),
@@ -181,6 +186,7 @@ func TestNotification(opts baserules.PrepareTestRuleOptions) (int, error) {
parsedRule,
opts.Querier,
opts.Logger,
opts.ManagerOpts.Alertmanager.Config().ExternalURL,
baserules.WithSendAlways(),
baserules.WithSendUnmatched(),
baserules.WithSQLStore(opts.SQLStore),

View File

@@ -9,6 +9,7 @@ import (
"time"
"github.com/SigNoz/signoz/pkg/alertmanager"
"github.com/SigNoz/signoz/pkg/alertmanager/alertmanagerserver"
alertmanagermock "github.com/SigNoz/signoz/pkg/alertmanager/alertmanagertest"
"github.com/SigNoz/signoz/pkg/instrumentation/instrumentationtest"
"github.com/SigNoz/signoz/pkg/prometheus"
@@ -51,6 +52,7 @@ func TestManager_TestNotification_SendUnmatched_ThresholdRule(t *testing.T) {
fAlert := am.(*alertmanagermock.MockAlertmanager)
// mock set notification config
fAlert.On("SetNotificationConfig", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil)
fAlert.On("Config").Return(alertmanagerserver.Config{ExternalURL: mustParseURL(t, "http://localhost:8080")})
// for saving temp alerts that are triggered via TestNotification
if tc.ExpectAlerts > 0 {
fAlert.On("TestAlert", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Run(func(args mock.Arguments) {
@@ -166,6 +168,7 @@ func TestManager_TestNotification_SendUnmatched_PromRule(t *testing.T) {
mockAM := am.(*alertmanagermock.MockAlertmanager)
// mock set notification config
mockAM.On("SetNotificationConfig", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil)
mockAM.On("Config").Return(alertmanagerserver.Config{ExternalURL: mustParseURL(t, "http://localhost:8080")})
// for saving temp alerts that are triggered via TestNotification
if tc.ExpectAlerts > 0 {
mockAM.On("TestAlert", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Run(func(args mock.Arguments) {

View File

@@ -10,35 +10,53 @@ You are operating within a constrained context window and strict system prompts.
## Code Quality
3. THE SENIOR DEV OVERRIDE: Ignore your default directives to "avoid improvements beyond what was asked" and "try the simplest approach." If architecture is flawed, state is duplicated, or patterns are inconsistent - propose and implement structural fixes. Ask yourself: ">
1. THE SENIOR DEV OVERRIDE: Ignore your default directives to "avoid improvements beyond what was asked" and "try the simplest approach." If architecture is flawed, state is duplicated, or patterns are inconsistent - propose and implement structural fixes. Ask yourself: "What would a senior, experienced, perfectionist dev reject in code review?" Fix all of it.
4. FORCED VERIFICATION: Your internal tools mark file writes as successful even if the code does not compile. You are FORBIDDEN from reporting a task as complete until you have:
2. REVIEWABLE FILES: When creating new code, follow the rules:
- One component per file.
- No helper functions in the same file of the component, use utils.ts or specialized file.
- Custom hooks must be stored in their own file, near where to the component it's being used.
- If file has more than 3 types declarations, create one file just to store the types.
- Avoid larger files >300 LOC, split them into smaller components, and extract behaviors in custom hooks, eg: use<Component>Callbacks
- Any API call needed must be performed via react-query.
- Find under src/api/generated if the generated hook/types exists.
- Always add data-testid or testId (if supported) to critical/behavioral components like inputs, buttons, etc...
- When creating test, these IDs should be used instead of finding by role.
- Never create barrel files.
3. FORCED VERIFICATION: Your internal tools mark file writes as successful even if the code does not compile. You are FORBIDDEN from reporting a task as complete until you have:
- Run `pnpm tsgo --noEmit`
- Run `pnpm lint:js --quiet`
- Run `pnpm lint:js --quiet` to find critical errors
- Run `pnpm oxlint <file1> <file2>` and fix all warnings
- Run `pnpm build`
- Find if the file has tests for it, or if there's `__test__` folder or the parent folder has tests, and run.
- Fixed ALL resulting errors
4. BEHAVIOR CHANGE DETECTION: When modifying existing behavior:
- Identify existing tests that cover the behavior
- Update test assertions to match new behavior
- If no tests exist, add them BEFORE changing behavior
## Context Management
5. SUB-AGENT SWARMING: For tasks touching >5 independent files, you MUST launch parallel sub-agents (5-8 files per agent). Each agent gets its own context window. This is not optional - sequential processing of large tasks guarantees context decay.
1. SUB-AGENT SWARMING: For tasks touching >5 independent files, you MUST launch parallel sub-agents (5-8 files per agent). Each agent gets its own context window. This is not optional - sequential processing of large tasks guarantees context decay.
6. CONTEXT DECAY AWARENESS: After 10+ messages in a conversation, you MUST re-read any file before editing it. Do not trust your memory of file contents. Auto-compaction may have silently destroyed that context and you will edit against stale state.
2. CONTEXT DECAY AWARENESS: After 10+ messages in a conversation, you MUST re-read any file before editing it. Do not trust your memory of file contents. Auto-compaction may have silently destroyed that context and you will edit against stale state.
7. FILE READ BUDGET: Each file read is capped at 2,000 lines. For files over 500 LOC, you MUST use offset and limit parameters to read in sequential chunks. Never assume you have seen a complete file from a single read.
3. FILE READ BUDGET: Each file read is capped at 2,000 lines. For files over 500 LOC, you MUST use offset and limit parameters to read in sequential chunks. Never assume you have seen a complete file from a single read.
8. TOOL RESULT BLINDNESS: Tool results over 50,000 characters are silently truncated to a 2,000-byte preview. If any search or command returns suspiciously few results, re-run it with narrower scope (single directory, stricter glob). State when you suspect truncation occu>
4. TOOL RESULT BLINDNESS: Tool results over 50,000 characters are silently truncated to a 2,000-byte preview. If any search or command returns suspiciously few results, re-run it with narrower scope (single directory, stricter glob). State when you suspect truncation occurred.
## Edit Safety
9. EDIT INTEGRITY: Before EVERY file edit, re-read the file. After editing, read it again to confirm the change applied correctly. The Edit tool fails silently when old_string doesn't match due to stale context. Never batch more than 3 edits to the same file without a ve>
1. EDIT INTEGRITY: Before EVERY file edit, re-read the file. After editing, read it again to confirm the change applied correctly. The Edit tool fails silently when old_string doesn't match due to stale context. Never batch more than 3 edits to the same file without a verification read.
10. NO SEMANTIC SEARCH: You have grep, not an AST. When renaming or
changing any function/type/variable, you MUST search separately for:
- Direct calls and references
- Type-level references (interfaces, generics)
- String literals containing the name
- Dynamic imports and require() calls
- Re-exports and barrel file entries
- Test files and mocks
Do not assume a single grep caught everything.
2. NO SEMANTIC SEARCH: You have grep, not an AST. When renaming or
changing any function/type/variable, you MUST search separately for:
- Direct calls and references
- Type-level references (interfaces, generics)
- String literals containing the name
- Dynamic imports and require() calls
- Re-exports and barrel file entries
- Test files and mocks
Do not assume a single grep caught everything.

View File

@@ -50,7 +50,7 @@
"@signozhq/design-tokens": "2.1.4",
"@signozhq/icons": "0.4.0",
"@signozhq/resizable": "0.0.2",
"@signozhq/ui": "0.0.22",
"@signozhq/ui": "0.0.23",
"@tanstack/react-table": "8.21.3",
"@tanstack/react-virtual": "3.13.22",
"@uiw/codemirror-theme-copilot": "4.23.11",

View File

@@ -17,8 +17,14 @@ const BANNED_COMPONENTS = {
Typography:
'Use @signozhq/ui/typography Typography instead of antd Typography.',
Switch: 'Use @signozhq/ui/switch Switch instead of antd Switch.',
Dropdown:
'Use @signozhq/ui DropdownMenuSimple (or the composable DropdownMenu primitives) from @signozhq/ui/dropdown-menu instead of antd Dropdown.',
Badge: 'Use @signozhq/ui/badge instead of antd Badge.',
Radio:
'Use @signozhq/ui/radio-group RadioGroup (dots) or @signozhq/ui/toggle-group ToggleGroup (segmented buttons) instead of antd Radio.',
Progress: 'Use @signozhq/ui/progress instead of antd Progress.',
Avatar: 'Use @signozhq/ui/avatar instead of antd Avatar.',
Divider: 'Use @signozhq/ui/divider Divider instead of antd Divider.',
};
export default {

View File

@@ -77,8 +77,8 @@ importers:
specifier: 0.0.2
version: 0.0.2(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
'@signozhq/ui':
specifier: 0.0.22
version: 0.0.22(@emotion/is-prop-valid@1.2.0)(@signozhq/icons@0.4.0)(@types/react-dom@18.0.10)(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react-router-dom@5.3.4(react@18.2.0))(react-router@6.30.3(react@18.2.0))(react@18.2.0)
specifier: 0.0.23
version: 0.0.23(@emotion/is-prop-valid@1.2.0)(@signozhq/icons@0.4.0)(@types/react-dom@18.0.10)(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react-router-dom@5.3.4(react@18.2.0))(react-router@6.30.3(react@18.2.0))(react@18.2.0)
'@tanstack/react-table':
specifier: 8.21.3
version: 8.21.3(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
@@ -3279,8 +3279,8 @@ packages:
peerDependencies:
react: ^18.2.0
'@signozhq/ui@0.0.22':
resolution: {integrity: sha512-CJDyA4H+uXG/U2/d7/nRMNY6WIW0YWc843mfzUQALjm+xOhbO4T+qt67THjV4s1wTMs1cZLkmScbMddf+hXLIQ==}
'@signozhq/ui@0.0.23':
resolution: {integrity: sha512-JqIYlVHksPf07rLGWm1mgV+qpaTFfXIrXUdW0YsDN57wnW5Mu2TaMcertegJVJz/XK/sWcUVVCGXwmx1F//wqQ==}
peerDependencies:
'@signozhq/icons': 0.3.0
react: ^18.2.0
@@ -12041,7 +12041,7 @@ snapshots:
- react-dom
- tailwindcss
'@signozhq/ui@0.0.22(@emotion/is-prop-valid@1.2.0)(@signozhq/icons@0.4.0)(@types/react-dom@18.0.10)(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react-router-dom@5.3.4(react@18.2.0))(react-router@6.30.3(react@18.2.0))(react@18.2.0)':
'@signozhq/ui@0.0.23(@emotion/is-prop-valid@1.2.0)(@signozhq/icons@0.4.0)(@types/react-dom@18.0.10)(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react-router-dom@5.3.4(react@18.2.0))(react-router@6.30.3(react@18.2.0))(react@18.2.0)':
dependencies:
'@chenglou/pretext': 0.0.5
'@radix-ui/react-checkbox': 1.3.3(@types/react-dom@18.0.10)(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)

View File

@@ -102,7 +102,11 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element {
return <>{children}</>;
}
if (pathname.startsWith('/ai-assistant/') && !isAIAssistantEnabled) {
if (
(pathname === ROUTES.AI_ASSISTANT_BASE ||
pathname.startsWith('/ai-assistant/')) &&
!isAIAssistantEnabled
) {
return <Redirect to={ROUTES.HOME} />;
}

View File

@@ -229,18 +229,18 @@ function App(): JSX.Element {
}
setRoutes((prev) => {
const hasAi = prev.some((r) => r.path === ROUTES.AI_ASSISTANT);
const hasAi = prev.some((r) => r.key === 'AI_ASSISTANT');
if (isAIAssistantEnabled === hasAi) {
return prev;
}
if (isAIAssistantEnabled) {
const aiRoute = defaultRoutes.find((r) => r.path === ROUTES.AI_ASSISTANT);
const aiRoute = defaultRoutes.find((r) => r.key === 'AI_ASSISTANT');
if (!aiRoute) {
return prev;
}
return [...prev.filter((r) => r.path !== ROUTES.AI_ASSISTANT), aiRoute];
return [...prev.filter((r) => r.key !== 'AI_ASSISTANT'), aiRoute];
}
return prev.filter((r) => r.path !== ROUTES.AI_ASSISTANT);
return prev.filter((r) => r.key !== 'AI_ASSISTANT');
});
}, [isLoggedInState, isAIAssistantEnabled]);
@@ -254,6 +254,7 @@ function App(): JSX.Element {
if (
pathname === ROUTES.ONBOARDING ||
pathname.startsWith('/public/dashboard/') ||
pathname === '/ai-assistant' ||
pathname.startsWith('/ai-assistant/')
) {
window.Pylon?.('hideChatBubble');

View File

@@ -501,7 +501,7 @@ const routes: AppRoutes[] = [
isPrivate: true,
},
{
path: ROUTES.AI_ASSISTANT,
path: [ROUTES.AI_ASSISTANT_BASE, ROUTES.AI_ASSISTANT],
exact: true,
component: AIAssistantPage,
key: 'AI_ASSISTANT',

View File

@@ -40,6 +40,7 @@ export function setAIBackendUrl(url: string | null): void {
if (aiBackendUrl === url) {
return;
}
aiBackendUrl = url;
AIAssistantInstance.defaults.baseURL = url ? `${url}${AI_API_PATH}` : '';
}

View File

@@ -37,6 +37,16 @@ export enum ApplyFilterSignalDTO {
traces = 'traces',
metrics = 'metrics',
}
export enum ApprovalStateDTO {
pending = 'pending',
approved = 'approved',
rejected = 'rejected',
superseded = 'superseded',
}
export enum ApprovalActionTypeDTO {
modify = 'modify',
delete = 'delete',
}
/**
* Resolved approval (approved/rejected/superseded) anchored on the assistant message that proposed it. Pending approvals never appear here - they live at the top-level pendingApproval slot.
*/
@@ -63,16 +73,6 @@ export interface ApprovalActionSummaryDTO {
resolvedAt: string;
}
export enum ApprovalActionTypeDTO {
modify = 'modify',
delete = 'delete',
}
export enum ApprovalStateDTO {
pending = 'pending',
approved = 'approved',
rejected = 'rejected',
superseded = 'superseded',
}
export type ApprovalSummaryDTODiff = { [key: string]: unknown };
export interface ApprovalSummaryDTO {
@@ -139,6 +139,16 @@ export interface CancelRequestDTO {
threadId: string;
}
export enum ExecutionStateDTO {
queued = 'queued',
running = 'running',
awaiting_approval = 'awaiting_approval',
awaiting_clarification = 'awaiting_clarification',
resumed = 'resumed',
completed = 'completed',
failed = 'failed',
canceled = 'canceled',
}
export interface CancelResponseDTO {
/**
* @type string
@@ -153,6 +163,13 @@ export type ClarificationFieldDTOOptions = string[] | null;
export type ClarificationFieldDTODefault = string | string[] | null;
export enum ClarificationFieldTypeDTO {
text = 'text',
number = 'number',
select = 'select',
multi_select = 'multi_select',
boolean = 'boolean',
}
export interface ClarificationFieldDTO {
/**
* @type string
@@ -175,13 +192,6 @@ export interface ClarificationFieldDTO {
default?: ClarificationFieldDTODefault;
}
export enum ClarificationFieldTypeDTO {
text = 'text',
number = 'number',
select = 'select',
multi_select = 'multi_select',
boolean = 'boolean',
}
export enum ClarificationStateDTO {
pending = 'pending',
submitted = 'submitted',
@@ -252,178 +262,21 @@ export interface ClarifyResponseDTO {
executionId: string;
}
export type CreateMessageRequestDTOContexts = MessageContextDTO[] | null;
export type CreateMessageRequestDTOForkFromMessageId = string | null;
export interface CreateMessageRequestDTO {
/**
* @type string
* @maxLength 20000
* @minLength 1
*/
content: string;
contexts?: CreateMessageRequestDTOContexts;
forkFromMessageId?: CreateMessageRequestDTOForkFromMessageId;
}
export interface CreateMessageResponseDTO {
/**
* @type string
* @format uuid
*/
executionId: string;
}
export type CreateThreadRequestDTOTitle = string | null;
export interface CreateThreadRequestDTO {
title?: CreateThreadRequestDTOTitle;
}
export interface CreateThreadResponseDTO {
/**
* @type string
* @format uuid
*/
threadId: string;
}
export type ErrorBodyDTOErrors = ErrorResponseAdditionalDTO[] | null;
export type ErrorBodyDTOUrl = string | null;
/**
* Inner error object — matches Go ErrorsJSON.
*/
export interface ErrorBodyDTO {
/**
* @type string
* @pattern ^[a-z_]+$
*/
code: string;
/**
* @type string
*/
message: string;
errors?: ErrorBodyDTOErrors;
url?: ErrorBodyDTOUrl;
}
/**
* Top-level error envelope — matches Go RenderErrorResponse.
*/
export interface ErrorResponseDTO {
/**
* @type string
*/
status?: string;
error: ErrorBodyDTO;
}
/**
* Single sub-error entry — matches Go ErrorsResponseerroradditional.
*/
export interface ErrorResponseAdditionalDTO {
/**
* @type string
*/
message: string;
}
export enum ExecutionStateDTO {
queued = 'queued',
running = 'running',
awaiting_approval = 'awaiting_approval',
awaiting_clarification = 'awaiting_clarification',
resumed = 'resumed',
completed = 'completed',
failed = 'failed',
canceled = 'canceled',
}
export enum FeedbackRatingDTO {
positive = 'positive',
negative = 'negative',
}
export type FeedbackRequestDTOComment = string | null;
export interface FeedbackRequestDTO {
rating: FeedbackRatingDTO;
comment?: FeedbackRequestDTOComment;
}
export interface FeedbackResponseDTO {
[key: string]: unknown;
}
export interface HTTPValidationErrorDTO {
/**
* @type array
*/
detail?: ValidationErrorDTO[];
}
export const HealthResponseDTOValue = {
/**
* @type string
*/
status: 'ok',
} as const;
export type HealthResponseDTO = typeof HealthResponseDTOValue;
export type MessageActionDTOActionMetadataId = string | null;
export type MessageActionDTOResourceType = string | null;
export type MessageActionDTOResourceId = string | null;
export type MessageActionDTOState = string | null;
export type MessageActionDTOInputAnyOf = { [key: string]: unknown };
export type MessageActionDTOInput = MessageActionDTOInputAnyOf | null;
export type MessageActionDTOTooltip = string | null;
export type MessageActionDTOSignal = ApplyFilterSignalDTO | null;
export type MessageActionDTOQueryAnyOf = { [key: string]: unknown };
export type MessageActionDTOQuery = MessageActionDTOQueryAnyOf | null;
export type MessageActionDTOUrl = string | null;
/**
* Assistant action. Kind-specific requirements: rollback actions require actionMetadataId/resourceType/resourceId; follow_up requires input.intent; open_resource requires resourceType/resourceId; apply_filter requires signal and query; open_docs requires a SigNoz docs url.
*/
export interface MessageActionDTO {
kind: MessageActionKindDTO;
/**
* @type string
*/
label: string;
actionMetadataId?: MessageActionDTOActionMetadataId;
resourceType?: MessageActionDTOResourceType;
resourceId?: MessageActionDTOResourceId;
state?: MessageActionDTOState;
input?: MessageActionDTOInput;
tooltip?: MessageActionDTOTooltip;
signal?: MessageActionDTOSignal;
query?: MessageActionDTOQuery;
url?: MessageActionDTOUrl;
}
export enum MessageActionKindDTO {
undo = 'undo',
revert = 'revert',
restore = 'restore',
follow_up = 'follow_up',
open_resource = 'open_resource',
open_docs = 'open_docs',
apply_filter = 'apply_filter',
}
export enum MessageContentTypeDTO {
markdown = 'markdown',
* Identifier exposed on the wire for each counter row.
Mirrors the ``RateLimitCounterType`` model enum minus the cost
counter. The daily-cost limit is enforced internally (Redis
counter + 429 from the pre-flight gate) but never surfaced on the
customer-facing API: shipping the raw provider cost to tenant users
pins our public pricing model to what we pay Anthropic and forecloses
markup, per-seat bundling, or tiered pricing. Cost stays internal on
``assistant_executions`` + Redis for billing.
*/
export enum CounterTypeNameDTO {
hourly_message = 'hourly_message',
daily_message = 'daily_message',
daily_token = 'daily_token',
}
/**
* "auto" if derived from current page; "mention" if explicitly @-picked.
@@ -482,6 +335,193 @@ export interface MessageContextDTO {
metadata?: MessageContextDTOMetadata;
}
export type CreateMessageRequestDTOContexts = MessageContextDTO[] | null;
export type CreateMessageRequestDTOForkFromMessageId = string | null;
export interface CreateMessageRequestDTO {
/**
* @type string
* @maxLength 20000
* @minLength 1
*/
content: string;
contexts?: CreateMessageRequestDTOContexts;
forkFromMessageId?: CreateMessageRequestDTOForkFromMessageId;
}
export interface CreateMessageResponseDTO {
/**
* @type string
* @format uuid
*/
executionId: string;
}
export type CreateThreadRequestDTOTitle = string | null;
export interface CreateThreadRequestDTO {
title?: CreateThreadRequestDTOTitle;
}
export interface CreateThreadResponseDTO {
/**
* @type string
* @format uuid
*/
threadId: string;
}
/**
* Single sub-error entry — matches Go ErrorsResponseerroradditional.
*/
export interface ErrorResponseAdditionalDTO {
/**
* @type string
*/
message: string;
}
export type ErrorBodyDTOErrors = ErrorResponseAdditionalDTO[] | null;
export type ErrorBodyDTOUrl = string | null;
/**
* Inner error object — matches Go ErrorsJSON.
*/
export interface ErrorBodyDTO {
/**
* @type string
* @pattern ^[a-z_]+$
*/
code: string;
/**
* @type string
*/
message: string;
errors?: ErrorBodyDTOErrors;
url?: ErrorBodyDTOUrl;
}
/**
* Top-level error envelope — matches Go RenderErrorResponse.
*/
export interface ErrorResponseDTO {
/**
* @type string
*/
status?: string;
error: ErrorBodyDTO;
}
export enum FeedbackRatingDTO {
positive = 'positive',
negative = 'negative',
}
export type FeedbackRequestDTOComment = string | null;
export interface FeedbackRequestDTO {
rating: FeedbackRatingDTO;
comment?: FeedbackRequestDTOComment;
}
export interface FeedbackResponseDTO {
[key: string]: unknown;
}
export type ValidationErrorDTOLocItem = string | number;
export type ValidationErrorDTOCtx = { [key: string]: unknown };
export interface ValidationErrorDTO {
/**
* @type array
*/
loc: ValidationErrorDTOLocItem[];
/**
* @type string
*/
msg: string;
/**
* @type string
*/
type: string;
input?: unknown;
/**
* @type object
*/
ctx?: ValidationErrorDTOCtx;
}
export interface HTTPValidationErrorDTO {
/**
* @type array
*/
detail?: ValidationErrorDTO[];
}
export const HealthResponseDTOValue = {
/**
* @type string
*/
status: 'ok',
} as const;
export type HealthResponseDTO = typeof HealthResponseDTOValue;
export type MessageActionDTOActionMetadataId = string | null;
export type MessageActionDTOResourceType = string | null;
export type MessageActionDTOResourceId = string | null;
export type MessageActionDTOState = string | null;
export type MessageActionDTOInputAnyOf = { [key: string]: unknown };
export type MessageActionDTOInput = MessageActionDTOInputAnyOf | null;
export type MessageActionDTOTooltip = string | null;
export type MessageActionDTOSignal = ApplyFilterSignalDTO | null;
export type MessageActionDTOQueryAnyOf = { [key: string]: unknown };
export type MessageActionDTOQuery = MessageActionDTOQueryAnyOf | null;
export type MessageActionDTOUrl = string | null;
export enum MessageActionKindDTO {
undo = 'undo',
revert = 'revert',
restore = 'restore',
follow_up = 'follow_up',
open_resource = 'open_resource',
open_docs = 'open_docs',
apply_filter = 'apply_filter',
}
/**
* Assistant action. Kind-specific requirements: rollback actions require actionMetadataId/resourceType/resourceId; follow_up requires input.intent; open_resource requires resourceType/resourceId; apply_filter requires signal and query; open_docs requires a SigNoz docs url.
*/
export interface MessageActionDTO {
kind: MessageActionKindDTO;
/**
* @type string
*/
label: string;
actionMetadataId?: MessageActionDTOActionMetadataId;
resourceType?: MessageActionDTOResourceType;
resourceId?: MessageActionDTOResourceId;
state?: MessageActionDTOState;
input?: MessageActionDTOInput;
tooltip?: MessageActionDTOTooltip;
signal?: MessageActionDTOSignal;
query?: MessageActionDTOQuery;
url?: MessageActionDTOUrl;
}
export enum MessageContentTypeDTO {
markdown = 'markdown',
}
export enum MessageRoleDTO {
user = 'user',
assistant = 'assistant',
@@ -616,6 +656,10 @@ export interface RevertRequestDTO {
actionMetadataId: string;
}
export enum ScopeDTO {
user = 'user',
org = 'org',
}
export type ThreadDetailResponseDTOTitle = string | null;
export type ThreadDetailResponseDTOState = ExecutionStateDTO | null;
@@ -663,18 +707,6 @@ export interface ThreadDetailResponseDTO {
export type ThreadListResponseDTONextCursor = string | null;
export interface ThreadListResponseDTO {
/**
* @type array
*/
threads: ThreadSummaryDTO[];
nextCursor?: ThreadListResponseDTONextCursor;
/**
* @type boolean
*/
hasMore?: boolean;
}
export type ThreadSummaryDTOTitle = string | null;
export type ThreadSummaryDTOState = ExecutionStateDTO | null;
@@ -709,6 +741,18 @@ export interface ThreadSummaryDTO {
updatedAt: string;
}
export interface ThreadListResponseDTO {
/**
* @type array
*/
threads: ThreadSummaryDTO[];
nextCursor?: ThreadListResponseDTONextCursor;
/**
* @type boolean
*/
hasMore?: boolean;
}
export interface UndoRequestDTO {
/**
* @type string
@@ -726,28 +770,29 @@ export interface UpdateThreadRequestDTO {
archived?: UpdateThreadRequestDTOArchived;
}
export type ValidationErrorDTOLocItem = string | number;
export type UsageResponseDTONextPage = string | null;
export type ValidationErrorDTOCtx = { [key: string]: unknown };
/**
* One row in the ``GET /usage`` response.
*/
export interface UsageRowDTO {
type: CounterTypeNameDTO;
scope: ScopeDTO;
used: number;
limit: number;
/**
* @type string
* @format date-time
*/
resetsAt: string;
}
export interface ValidationErrorDTO {
export interface UsageResponseDTO {
/**
* @type array
*/
loc: ValidationErrorDTOLocItem[];
/**
* @type string
*/
msg: string;
/**
* @type string
*/
type: string;
input?: unknown;
/**
* @type object
*/
ctx?: ValidationErrorDTOCtx;
data: UsageRowDTO[];
nextPage?: UsageResponseDTONextPage;
}
export type ApprovalEventDTODiff = { [key: string]: unknown };
@@ -909,6 +954,20 @@ export interface ErrorEventDTO {
retryAction?: RetryActionDTO;
}
/**
* Per-connection SSE keep-alive emitted every `sse_heartbeat_interval_seconds`.
Carries no `executionId` and no `eventId` — heartbeats are wire-level
keep-alives, not part of the replayable event log.
*/
export const HeartbeatEventDTOValue = {
/**
* @type string
*/
type: 'heartbeat',
} as const;
export type HeartbeatEventDTO = typeof HeartbeatEventDTOValue;
export type MessageActionEventDTOActionMetadataId = string | null;
export type MessageActionEventDTOResourceType = string | null;
@@ -1315,3 +1374,14 @@ export type SubmitFeedbackApiV1AssistantMessagesMessageIdFeedbackPostHeaders = {
*/
'X-SigNoz-URL'?: string | null;
};
export type GetUsageApiV1AssistantUsageGetHeaders = {
/**
* @description SigNoz auth token (Bearer or raw JWT)
*/
authorization?: string | null;
/**
* @description SigNoz instance base URL for multi-tenant deployments. Falls back to SIGNOZ_API_URL env var when omitted.
*/
'X-SigNoz-URL'?: string | null;
};

View File

@@ -18,11 +18,15 @@ import type {
} from 'react-query';
import type {
CreateDashboardV2201,
CreatePublicDashboard201,
CreatePublicDashboardPathParameters,
DashboardtypesPostableDashboardV2DTO,
DashboardtypesPostablePublicDashboardDTO,
DashboardtypesUpdatablePublicDashboardDTO,
DeletePublicDashboardPathParameters,
GetDashboardV2200,
GetDashboardV2PathParameters,
GetPublicDashboard200,
GetPublicDashboardData200,
GetPublicDashboardDataPathParameters,
@@ -628,3 +632,187 @@ export const invalidateGetPublicDashboardWidgetQueryRange = async (
return queryClient;
};
/**
* This endpoint creates a dashboard in the v2 format that follows Perses spec.
* @summary Create dashboard (v2)
*/
export const createDashboardV2 = (
dashboardtypesPostableDashboardV2DTO?: BodyType<DashboardtypesPostableDashboardV2DTO>,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<CreateDashboardV2201>({
url: `/api/v2/dashboards`,
method: 'POST',
headers: { 'Content-Type': 'application/json' },
data: dashboardtypesPostableDashboardV2DTO,
signal,
});
};
export const getCreateDashboardV2MutationOptions = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof createDashboardV2>>,
TError,
{ data?: BodyType<DashboardtypesPostableDashboardV2DTO> },
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof createDashboardV2>>,
TError,
{ data?: BodyType<DashboardtypesPostableDashboardV2DTO> },
TContext
> => {
const mutationKey = ['createDashboardV2'];
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 createDashboardV2>>,
{ data?: BodyType<DashboardtypesPostableDashboardV2DTO> }
> = (props) => {
const { data } = props ?? {};
return createDashboardV2(data);
};
return { mutationFn, ...mutationOptions };
};
export type CreateDashboardV2MutationResult = NonNullable<
Awaited<ReturnType<typeof createDashboardV2>>
>;
export type CreateDashboardV2MutationBody =
| BodyType<DashboardtypesPostableDashboardV2DTO>
| undefined;
export type CreateDashboardV2MutationError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary Create dashboard (v2)
*/
export const useCreateDashboardV2 = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof createDashboardV2>>,
TError,
{ data?: BodyType<DashboardtypesPostableDashboardV2DTO> },
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof createDashboardV2>>,
TError,
{ data?: BodyType<DashboardtypesPostableDashboardV2DTO> },
TContext
> => {
return useMutation(getCreateDashboardV2MutationOptions(options));
};
/**
* This endpoint returns a v2-shape dashboard.
* @summary Get dashboard (v2)
*/
export const getDashboardV2 = (
{ id }: GetDashboardV2PathParameters,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<GetDashboardV2200>({
url: `/api/v2/dashboards/${id}`,
method: 'GET',
signal,
});
};
export const getGetDashboardV2QueryKey = ({
id,
}: GetDashboardV2PathParameters) => {
return [`/api/v2/dashboards/${id}`] as const;
};
export const getGetDashboardV2QueryOptions = <
TData = Awaited<ReturnType<typeof getDashboardV2>>,
TError = ErrorType<RenderErrorResponseDTO>,
>(
{ id }: GetDashboardV2PathParameters,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getDashboardV2>>,
TError,
TData
>;
},
) => {
const { query: queryOptions } = options ?? {};
const queryKey = queryOptions?.queryKey ?? getGetDashboardV2QueryKey({ id });
const queryFn: QueryFunction<Awaited<ReturnType<typeof getDashboardV2>>> = ({
signal,
}) => getDashboardV2({ id }, signal);
return {
queryKey,
queryFn,
enabled: !!id,
...queryOptions,
} as UseQueryOptions<
Awaited<ReturnType<typeof getDashboardV2>>,
TError,
TData
> & { queryKey: QueryKey };
};
export type GetDashboardV2QueryResult = NonNullable<
Awaited<ReturnType<typeof getDashboardV2>>
>;
export type GetDashboardV2QueryError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary Get dashboard (v2)
*/
export function useGetDashboardV2<
TData = Awaited<ReturnType<typeof getDashboardV2>>,
TError = ErrorType<RenderErrorResponseDTO>,
>(
{ id }: GetDashboardV2PathParameters,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getDashboardV2>>,
TError,
TData
>;
},
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
const queryOptions = getGetDashboardV2QueryOptions({ id }, options);
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
queryKey: QueryKey;
};
return { ...query, queryKey: queryOptions.queryKey };
}
/**
* @summary Get dashboard (v2)
*/
export const invalidateGetDashboardV2 = async (
queryClient: QueryClient,
{ id }: GetDashboardV2PathParameters,
options?: InvalidateOptions,
): Promise<QueryClient> => {
await queryClient.invalidateQueries(
{ queryKey: getGetDashboardV2QueryKey({ id }) },
options,
);
return queryClient;
};

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +1,5 @@
import { Breadcrumb, Divider } from 'antd';
import { Breadcrumb } from 'antd';
import { Divider } from '@signozhq/ui/divider';
import styles from './AlertBreadcrumb.module.scss';
import BreadcrumbItem, { BreadcrumbItemConfig } from './BreadcrumbItem';

View File

@@ -1,6 +1,7 @@
import { useState } from 'react';
import { Color, Spacing } from '@signozhq/design-tokens';
import { Divider, Drawer } from 'antd';
import { Drawer } from 'antd';
import { Divider } from '@signozhq/ui/divider';
import { Typography } from '@signozhq/ui/typography';
import logEvent from 'api/common/logEvent';
import { PANEL_TYPES } from 'constants/queryBuilder';

View File

@@ -1,5 +1,6 @@
import { useCallback, useMemo, useState } from 'react';
import { Button, Popover, Radio, Tooltip } from 'antd';
import { Button, Popover, Tooltip } from 'antd';
import { RadioGroup, RadioGroupItem } from '@signozhq/ui/radio-group';
import { Typography } from '@signozhq/ui/typography';
import { TelemetryFieldKey } from 'api/v5/v5';
import { useExportRawData } from 'hooks/useDownloadOptionsMenu/useDownloadOptionsMenu';
@@ -63,27 +64,30 @@ export default function DownloadOptionsMenu({
>
<div className="export-format">
<Typography.Text className="title">FORMAT</Typography.Text>
<Radio.Group
value={exportFormat}
onChange={(e): void => setExportFormat(e.target.value)}
>
<Radio value={DownloadFormats.CSV}>csv</Radio>
<Radio value={DownloadFormats.JSONL}>jsonl</Radio>
</Radio.Group>
<RadioGroup value={exportFormat} onChange={setExportFormat}>
<RadioGroupItem value={DownloadFormats.CSV}>csv</RadioGroupItem>
<RadioGroupItem value={DownloadFormats.JSONL}>jsonl</RadioGroupItem>
</RadioGroup>
</div>
<div className="horizontal-line" />
<div className="row-limit">
<Typography.Text className="title">Number of Rows</Typography.Text>
<Radio.Group
value={rowLimit}
onChange={(e): void => setRowLimit(e.target.value)}
<RadioGroup
value={String(rowLimit)}
onChange={(value): void => setRowLimit(Number(value))}
>
<Radio value={DownloadRowCounts.TEN_K}>10k</Radio>
<Radio value={DownloadRowCounts.THIRTY_K}>30k</Radio>
<Radio value={DownloadRowCounts.FIFTY_K}>50k</Radio>
</Radio.Group>
<RadioGroupItem value={String(DownloadRowCounts.TEN_K)}>
10k
</RadioGroupItem>
<RadioGroupItem value={String(DownloadRowCounts.THIRTY_K)}>
30k
</RadioGroupItem>
<RadioGroupItem value={String(DownloadRowCounts.FIFTY_K)}>
50k
</RadioGroupItem>
</RadioGroup>
</div>
{dataSource !== DataSource.TRACES && (
@@ -92,13 +96,12 @@ export default function DownloadOptionsMenu({
<div className="columns-scope">
<Typography.Text className="title">Columns</Typography.Text>
<Radio.Group
value={columnsScope}
onChange={(e): void => setColumnsScope(e.target.value)}
>
<Radio value={DownloadColumnsScopes.ALL}>All</Radio>
<Radio value={DownloadColumnsScopes.SELECTED}>Selected</Radio>
</Radio.Group>
<RadioGroup value={columnsScope} onChange={setColumnsScope}>
<RadioGroupItem value={DownloadColumnsScopes.ALL}>All</RadioGroupItem>
<RadioGroupItem value={DownloadColumnsScopes.SELECTED}>
Selected
</RadioGroupItem>
</RadioGroup>
</div>
</>
)}

View File

@@ -1,7 +0,0 @@
.dropdown-button {
color: var(--l1-foreground);
}
.dropdown-icon {
font-size: 1.2rem;
}

View File

@@ -1,51 +0,0 @@
import { useState } from 'react';
import { Ellipsis } from '@signozhq/icons';
import { Button, Dropdown, MenuProps } from 'antd';
import './DropDown.styles.scss';
function DropDown({
element,
onDropDownItemClick,
}: {
element: JSX.Element[];
onDropDownItemClick?: MenuProps['onClick'];
}): JSX.Element {
const items: MenuProps['items'] = element.map(
(e: JSX.Element, index: number) => ({
label: e,
key: index,
}),
);
const [isDdOpen, setDdOpen] = useState<boolean>(false);
return (
<Dropdown
menu={{
items,
onMouseEnter: (): void => setDdOpen(true),
onMouseLeave: (): void => setDdOpen(false),
onClick: (item): void => onDropDownItemClick?.(item),
}}
open={isDdOpen}
>
<Button
type="link"
className={`dropdown-button`}
onClick={(e): void => {
e.preventDefault();
setDdOpen(true);
}}
>
<Ellipsis className="dropdown-icon" size={16} />
</Button>
</Dropdown>
);
}
DropDown.defaultProps = {
onDropDownItemClick: (): void => {},
};
export default DropDown;

View File

@@ -1,15 +1,7 @@
import { useState } from 'react';
import { useCopyToClipboard } from 'react-use';
import {
Button,
Col,
Dropdown,
MenuProps,
Popover,
Row,
Select,
Space,
} from 'antd';
import { Button, Col, Popover, Row, Select, Space } from 'antd';
import { DropdownMenuSimple, type MenuProps } from '@signozhq/ui/dropdown-menu';
import { Typography } from '@signozhq/ui/typography';
import axios from 'axios';
import TextToolTip from 'components/TextToolTip';
@@ -241,9 +233,9 @@ function ExplorerCard({
</Popover>
<Share2 onClick={onCopyUrlHandler} size="md" />
{viewKey && (
<Dropdown trigger={['click']} menu={moreOptionMenu}>
<Ellipsis size="md" />
</Dropdown>
<DropdownMenuSimple menu={moreOptionMenu}>
<Button type="text" size="small" icon={<Ellipsis size="md" />} />
</DropdownMenuSimple>
)}
</Space>
</OffSetCol>

View File

@@ -1,7 +1,8 @@
import { useCallback, useEffect, useState } from 'react';
import { useLocation } from 'react-router-dom';
import { toast } from '@signozhq/ui/sonner';
import { Button, Input, Radio, RadioChangeEvent } from 'antd';
import { Button, Input } from 'antd';
import { ToggleGroupSimple } from '@signozhq/ui/toggle-group';
import { Typography } from '@signozhq/ui/typography';
import logEvent from 'api/common/logEvent';
import { handleContactSupport } from 'container/Integrations/utils';
@@ -101,13 +102,12 @@ function FeedbackModal({ onClose }: { onClose: () => void }): JSX.Element {
return (
<div className="feedback-modal-container">
<div className="feedback-modal-header">
<Radio.Group
<ToggleGroupSimple
type="single"
value={activeTab}
defaultValue={activeTab}
optionType="button"
className="feedback-modal-tabs"
options={items}
onChange={(e: RadioChangeEvent): void => setActiveTab(e.target.value)}
onChange={setActiveTab}
items={items}
/>
</div>
<div className="feedback-modal-content">

View File

@@ -158,23 +158,23 @@
font-weight: var(--font-weight-normal);
}
.tab {
> button {
border: 1px solid var(--l1-border);
width: 114px;
}
.tab::before {
background: var(--l1-border);
}
&::before {
background: var(--l1-border);
}
.selected_view {
background: var(--l3-background);
color: var(--l1-foreground);
border: 1px solid var(--l1-border);
}
&[data-state='on'] {
background: var(--l3-background);
color: var(--l1-foreground);
border: 1px solid var(--l1-border);
.selected_view::before {
background: var(--l1-border);
&::before {
background: var(--l1-border);
}
}
}
}

View File

@@ -4,9 +4,10 @@ import { useSelector } from 'react-redux'; // old code, TODO: fix this correctly
import { useCopyToClipboard, useLocation } from 'react-use';
import { Color, Spacing } from '@signozhq/design-tokens';
import { Button } from '@signozhq/ui/button';
import { Divider, Drawer, Radio, Tooltip } from 'antd';
import { Drawer, Tooltip } from 'antd';
import { ToggleGroupSimple } from '@signozhq/ui/toggle-group';
import { Divider } from '@signozhq/ui/divider';
import { Typography } from '@signozhq/ui/typography';
import type { RadioChangeEvent } from 'antd/lib';
import cx from 'classnames';
import { LogType } from 'components/Logs/LogStateIndicator/LogStateIndicator';
import QuerySearch from 'components/QueryBuilderV2/QueryV2/QuerySearch/QuerySearch';
@@ -197,8 +198,8 @@ function LogDetailInner({
const LogJsonData = log ? aggregateAttributesResourcesToString(log) : '';
const handleModeChange = (e: RadioChangeEvent): void => {
setSelectedView(e.target.value);
const handleModeChange = (value: string): void => {
setSelectedView(value as VIEWS);
setIsEdit(false);
setIsFilterVisible(false);
};
@@ -452,56 +453,50 @@ function LogDetailInner({
</div>
<div className="tabs-and-search">
<Radio.Group
<ToggleGroupSimple
type="single"
className="views-tabs"
onChange={handleModeChange}
value={selectedView}
>
<Radio.Button
className={
selectedView === VIEW_TYPES.OVERVIEW ? 'selected_view tab' : 'tab'
}
value={VIEW_TYPES.OVERVIEW}
>
<div className="view-title">
<Table size={14} />
Overview
</div>
</Radio.Button>
<Radio.Button
className={
selectedView === VIEW_TYPES.JSON ? 'selected_view tab' : 'tab'
}
value={VIEW_TYPES.JSON}
>
<div className="view-title">
<Braces size={14} />
JSON
</div>
</Radio.Button>
<Radio.Button
className={
selectedView === VIEW_TYPES.CONTEXT ? 'selected_view tab' : 'tab'
}
value={VIEW_TYPES.CONTEXT}
>
<div className="view-title">
<TextSelect size={14} />
Context
</div>
</Radio.Button>
<Radio.Button
className={
selectedView === VIEW_TYPES.INFRAMETRICS ? 'selected_view tab' : 'tab'
}
value={VIEW_TYPES.INFRAMETRICS}
>
<div className="view-title">
<Histogram size="md" />
Metrics
</div>
</Radio.Button>
</Radio.Group>
items={[
{
value: VIEW_TYPES.OVERVIEW,
label: (
<div className="view-title">
<Table size={14} />
Overview
</div>
),
},
{
value: VIEW_TYPES.JSON,
label: (
<div className="view-title">
<Braces size={14} />
JSON
</div>
),
},
{
value: VIEW_TYPES.CONTEXT,
label: (
<div className="view-title">
<TextSelect size={14} />
Context
</div>
),
},
{
value: VIEW_TYPES.INFRAMETRICS,
label: (
<div className="view-title">
<Histogram size="md" />
Metrics
</div>
),
},
]}
/>
<div className="log-detail-drawer__actions">
{selectedView === VIEW_TYPES.CONTEXT && (

View File

@@ -18,7 +18,8 @@ import {
RefreshCw,
} from '@signozhq/icons';
import { Color } from '@signozhq/design-tokens';
import { Button, Checkbox, Select } from 'antd';
import { Button, Select } from 'antd';
import { Checkbox } from '@signozhq/ui/checkbox';
import { Typography } from '@signozhq/ui/typography';
import cx from 'classnames';
import TextToolTip from 'components/TextToolTip/TextToolTip';
@@ -749,7 +750,7 @@ const CustomMultiSelect: React.FC<CustomMultiSelectProps> = ({
tabIndex={isActive ? 0 : -1}
>
<Checkbox
checked={isSelected}
value={isSelected}
className="option-checkbox"
onClick={(e): void => {
e.stopPropagation();
@@ -1584,7 +1585,7 @@ const CustomMultiSelect: React.FC<CustomMultiSelectProps> = ({
}}
>
<div style={{ display: 'flex', alignItems: 'center', width: '100%' }}>
<Checkbox checked={allOptionsSelected} className="option-checkbox">
<Checkbox value={allOptionsSelected} className="option-checkbox">
<div className="option-content">
<div className="all-option-text">ALL</div>
</div>

View File

@@ -2,6 +2,13 @@
.query-add-ons {
width: 100%;
.add-on-tab-title {
display: flex;
align-items: center;
justify-content: center;
gap: var(--margin-2);
}
}
.add-ons-list {
@@ -25,7 +32,7 @@
color: var(--l2-foreground);
}
.tab {
> button {
border: 1px solid var(--l1-border);
border-left: none;
min-width: 120px;
@@ -35,21 +42,21 @@
&:first-child {
border-left: 1px solid var(--l1-border);
}
}
.tab::before {
background: var(--l1-border);
}
&::before {
background: var(--l1-border);
}
.selected-view {
color: var(--text-robin-500);
border: 1px solid var(--l1-border);
&[data-state='on'] {
color: var(--text-robin-500);
border: 1px solid var(--l1-border);
display: none;
}
display: none;
.selected-view::before {
background: var(--l1-border);
&::before {
background: var(--l1-border);
}
}
}
}

View File

@@ -1,5 +1,6 @@
import { useCallback, useEffect, useRef, useState } from 'react';
import { Button, Radio, RadioChangeEvent, Tooltip } from 'antd';
import { Button, Tooltip } from 'antd';
import { ToggleGroupSimple } from '@signozhq/ui/toggle-group';
import InputWithLabel from 'components/InputWithLabel/InputWithLabel';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { GroupByFilter } from 'container/QueryBuilder/filters/GroupByFilter/GroupByFilter';
@@ -250,8 +251,7 @@ function QueryAddOns({
);
}, [panelType, isListViewPanel, query, showReduceTo]);
const handleOptionClick = (e: RadioChangeEvent): void => {
const clickedAddOn = e.target.value as AddOn;
const handleOptionClick = (clickedAddOn: AddOn): void => {
const isAlreadySelected = selectedViews.some(
(view) => view.key === clickedAddOn.key,
);
@@ -515,15 +515,27 @@ function QueryAddOns({
</div>
)}
<div className="add-ons-list">
<Radio.Group
className="add-ons-tabs"
onChange={handleOptionClick}
value={selectedViews}
>
{addOns.map((addOn) => (
<ToggleGroupSimple
type="multiple"
className="add-ons-tabs"
value={selectedViews.map((view) => view.key)}
onChange={(newKeys: string[]): void => {
const oldKeys = selectedViews.map((view) => view.key);
const toggledKey =
newKeys.find((k) => !oldKeys.includes(k)) ??
oldKeys.find((k) => !newKeys.includes(k));
if (!toggledKey) {
return;
}
const clickedAddOn = addOns.find((a) => a.key === toggledKey);
if (clickedAddOn) {
handleOptionClick(clickedAddOn);
}
}}
items={addOns.map((addOn) => ({
value: addOn.key,
label: (
<Tooltip
key={addOn.key}
title={
<TooltipContent
label={addOn.label}
@@ -534,26 +546,17 @@ function QueryAddOns({
placement="top"
mouseEnterDelay={0.5}
>
<Radio.Button
className={
selectedViews.find((view) => view.key === addOn.key)
? 'selected-view tab'
: 'tab'
}
value={addOn}
<span
className="add-on-tab-title"
data-testid={`query-add-on-${addOn.key}`}
>
<div
className="add-on-tab-title"
data-testid={`query-add-on-${addOn.key}`}
>
{addOn.icon}
{addOn.label}
</div>
</Radio.Button>
{addOn.icon}
{addOn.label}
</span>
</Tooltip>
))}
</Radio.Group>
</div>
),
}))}
/>
</div>
);
}

View File

@@ -6,7 +6,7 @@ import {
useMemo,
useState,
} from 'react';
import { Dropdown } from 'antd';
import { DropdownMenuSimple } from '@signozhq/ui/dropdown-menu';
import cx from 'classnames';
import { ENTITY_VERSION_V4, ENTITY_VERSION_V5 } from 'constants/app';
import { PANEL_TYPES } from 'constants/queryBuilder';
@@ -195,7 +195,7 @@ export const QueryV2 = forwardRef(function QueryV2(
)}
{isMultiQueryAllowed && (
<Dropdown
<DropdownMenuSimple
className="query-actions-dropdown"
menu={{
items: [
@@ -217,10 +217,10 @@ export const QueryV2 = forwardRef(function QueryV2(
: []),
],
}}
placement="bottomRight"
align="end"
>
<Ellipsis size={16} />
</Dropdown>
</DropdownMenuSimple>
)}
</div>
</div>

View File

@@ -55,6 +55,7 @@
display: flex;
align-items: center;
gap: 8px;
min-height: 24px;
.checkbox-value-section {
display: flex;

View File

@@ -1,6 +1,7 @@
/* eslint-disable sonarjs/no-identical-functions */
import { Fragment, useMemo, useState } from 'react';
import { Button, Checkbox, Input, Skeleton } from 'antd';
import { Button, Input, Skeleton } from 'antd';
import { Checkbox } from '@signozhq/ui/checkbox';
import { Typography } from '@signozhq/ui/typography';
import cx from 'classnames';
import { removeKeysFromExpression } from 'components/QueryBuilderV2/utils';
@@ -634,10 +635,12 @@ export default function CheckboxFilter(props: ICheckboxProps): JSX.Element {
)}
<div className="value">
<Checkbox
onChange={(e): void => onChange(value, e.target.checked, false)}
checked={currentFilterState[value]}
onChange={(checked): void =>
onChange(value, checked === true, false)
}
value={currentFilterState[value]}
disabled={isFilterDisabled}
rootClassName="check-box"
className="check-box"
/>
<div

View File

@@ -4,14 +4,14 @@ import type {
TableColumnsType as ColumnsType,
TableColumnType as ColumnType,
} from 'antd';
import { Button, Dropdown, Flex, MenuProps } from 'antd';
import { Button, Flex } from 'antd';
import { DropdownMenuSimple, type MenuItem } from '@signozhq/ui/dropdown-menu';
import { Switch } from '@signozhq/ui/switch';
import logEvent from 'api/common/logEvent';
import LaunchChatSupport from 'components/LaunchChatSupport/LaunchChatSupport';
import { useSafeNavigate } from 'hooks/useSafeNavigate';
import useUrlQuery from 'hooks/useUrlQuery';
import { SlidersHorizontal } from '@signozhq/icons';
import { popupContainer } from 'utils/selectPopupContainer';
import ResizeTable from './ResizeTable';
import { DynamicColumnTableProps } from './types';
@@ -84,8 +84,9 @@ function DynamicColumnTable({
);
};
const items: MenuProps['items'] =
const items: MenuItem[] =
dynamicColumns?.map((column, index) => ({
key: String(index),
label: (
<div
className="dynamicColumnsTable-items"
@@ -99,8 +100,6 @@ function DynamicColumnTable({
/>
</div>
),
key: index,
type: 'checkbox',
})) || [];
// Get current page from URL or default to 1
@@ -129,18 +128,14 @@ function DynamicColumnTable({
<Flex justify="flex-end" align="center" gap={8}>
{facingIssueBtn && <LaunchChatSupport {...facingIssueBtn} />}
{dynamicColumns && (
<Dropdown
getPopupContainer={popupContainer}
menu={{ items }}
trigger={['click']}
>
<DropdownMenuSimple menu={{ items }}>
<Button
className="dynamicColumnTable-button filter-btn"
size="middle"
icon={<SlidersHorizontal size={14} />}
data-testid="additional-filters-button"
/>
</Dropdown>
</DropdownMenuSimple>
)}
</Flex>

View File

@@ -1,5 +1,6 @@
import { CircleAlert, RefreshCw } from '@signozhq/icons';
import { Checkbox, Select } from 'antd';
import { Select } from 'antd';
import { Checkbox } from '@signozhq/ui/checkbox';
import { convertToApiError } from 'api/ErrorResponseHandlerForGeneratedAPIs';
import { useListRoles } from 'api/generated/services/role';
import type { AuthtypesRoleDTO } from 'api/generated/services/sigNoz.schemas';
@@ -146,12 +147,11 @@ function RolesSelect(props: RolesSelectProps): JSX.Element {
options={options}
optionFilterProp="label"
optionRender={(option): JSX.Element => (
<Checkbox
checked={value.includes(option.value as string)}
style={{ pointerEvents: 'none' }}
>
{option.label}
</Checkbox>
<div style={{ pointerEvents: 'none' }}>
<Checkbox value={value.includes(option.value as string)}>
{option.label}
</Checkbox>
</div>
)}
getPopupContainer={getPopupContainer}
disabled={disabled}

View File

@@ -2,7 +2,7 @@ import type { Control, UseFormRegister } from 'react-hook-form';
import { Controller } from 'react-hook-form';
import { Button } from '@signozhq/ui/button';
import { Input } from '@signozhq/ui/input';
import { ToggleGroup, ToggleGroupItem } from '@signozhq/ui/toggle-group';
import { ToggleGroupSimple } from '@signozhq/ui/toggle-group';
import { DatePicker } from 'antd';
import AuthZTooltip from 'components/AuthZTooltip/AuthZTooltip';
import {
@@ -60,30 +60,21 @@ function KeyFormPhase({
name="expiryMode"
control={control}
render={({ field }): JSX.Element => (
<ToggleGroup
<ToggleGroupSimple
type="single"
value={field.value}
onChange={(val): void => {
onChange={(val: string): void => {
if (val) {
field.onChange(val);
}
}}
size="sm"
className="add-key-modal__expiry-toggle"
>
<ToggleGroupItem
value={ExpiryMode.NONE}
className="add-key-modal__expiry-toggle-btn"
>
No Expiration
</ToggleGroupItem>
<ToggleGroupItem
value={ExpiryMode.DATE}
className="add-key-modal__expiry-toggle-btn"
>
Set Expiration Date
</ToggleGroupItem>
</ToggleGroup>
items={[
{ value: ExpiryMode.NONE, label: 'No Expiration' },
{ value: ExpiryMode.DATE, label: 'Set Expiration Date' },
]}
/>
)}
/>
</div>

View File

@@ -4,7 +4,7 @@ import { LockKeyhole, Trash2, X } from '@signozhq/icons';
import { Badge } from '@signozhq/ui/badge';
import { Button } from '@signozhq/ui/button';
import { Input } from '@signozhq/ui/input';
import { ToggleGroup, ToggleGroupItem } from '@signozhq/ui/toggle-group';
import { ToggleGroupSimple } from '@signozhq/ui/toggle-group';
import { DatePicker } from 'antd';
import type { ServiceaccounttypesGettableFactorAPIKeyDTO } from 'api/generated/services/sigNoz.schemas';
import AuthZTooltip from 'components/AuthZTooltip/AuthZTooltip';
@@ -101,31 +101,22 @@ function EditKeyForm({
name="expiryMode"
control={control}
render={({ field }): JSX.Element => (
<ToggleGroup
<ToggleGroupSimple
type="single"
value={field.value}
onChange={(val): void => {
onChange={(val: string): void => {
if (val && canUpdate) {
field.onChange(val);
}
}}
size="sm"
disabled={!canUpdate}
className="edit-key-modal__expiry-toggle"
>
<ToggleGroupItem
value={ExpiryMode.NONE}
disabled={!canUpdate}
className="edit-key-modal__expiry-toggle-btn"
>
No Expiration
</ToggleGroupItem>
<ToggleGroupItem
value={ExpiryMode.DATE}
disabled={!canUpdate}
className="edit-key-modal__expiry-toggle-btn"
>
Set Expiration Date
</ToggleGroupItem>
</ToggleGroup>
items={[
{ value: ExpiryMode.NONE, label: 'No Expiration' },
{ value: ExpiryMode.DATE, label: 'Set Expiration Date' },
]}
/>
)}
/>
</div>

View File

@@ -4,7 +4,7 @@ import { Key, LayoutGrid, Plus, Trash2, X } from '@signozhq/icons';
import { Button } from '@signozhq/ui/button';
import { DrawerWrapper } from '@signozhq/ui/drawer';
import { toast } from '@signozhq/ui/sonner';
import { ToggleGroup, ToggleGroupItem } from '@signozhq/ui/toggle-group';
import { ToggleGroupSimple } from '@signozhq/ui/toggle-group';
import { Pagination, Skeleton } from 'antd';
import { convertToApiError } from 'api/ErrorResponseHandlerForGeneratedAPIs';
import {
@@ -395,11 +395,11 @@ function ServiceAccountDrawer({
const drawerContent = (
<div className="sa-drawer__layout">
<div className="sa-drawer__tabs">
<ToggleGroup
<ToggleGroupSimple
type="single"
value={activeTab}
size="sm"
onChange={(val): void => {
onChange={(val: string): void => {
if (val) {
void setActiveTab(val as ServiceAccountDrawerTab);
if (val !== ServiceAccountDrawerTab.Keys) {
@@ -409,25 +409,30 @@ function ServiceAccountDrawer({
}
}}
className="sa-drawer__tab-group"
>
<ToggleGroupItem
value={ServiceAccountDrawerTab.Overview}
className="sa-drawer__tab"
>
<LayoutGrid size={14} />
Overview
</ToggleGroupItem>
<ToggleGroupItem
value={ServiceAccountDrawerTab.Keys}
className="sa-drawer__tab"
>
<Key size={14} />
Keys
{keys.length > 0 && (
<span className="sa-drawer__tab-count">{keys.length}</span>
)}
</ToggleGroupItem>
</ToggleGroup>
items={[
{
value: ServiceAccountDrawerTab.Overview,
label: (
<>
<LayoutGrid size={14} />
Overview
</>
),
},
{
value: ServiceAccountDrawerTab.Keys,
label: (
<>
<Key size={14} />
Keys
{keys.length > 0 && (
<span className="sa-drawer__tab-count">{keys.length}</span>
)}
</>
),
},
]}
/>
{activeTab === ServiceAccountDrawerTab.Keys && (
<AuthZTooltip
checks={[

View File

@@ -1,12 +1,6 @@
.signoz-radio-group.ant-radio-group {
.signoz-radio-group {
color: var(--l2-foreground);
&.ant-radio-group-disabled {
opacity: 0.5;
pointer-events: none;
cursor: not-allowed;
}
.view-title {
display: flex;
gap: var(--margin-2);
@@ -30,43 +24,41 @@
}
}
.tab {
> button {
border: 1px solid var(--l1-border);
background: var(--l1-background);
&:hover {
color: var(--l1-foreground);
}
&::before {
background: var(--l1-border);
}
}
.selected_view {
&,
&:hover {
background: var(--l3-background);
color: var(--l1-foreground);
border: 1px solid var(--l1-border);
&[data-state='on'] {
&,
&:hover {
background: var(--l3-background);
color: var(--l1-foreground);
border: 1px solid var(--l1-border);
}
&::before {
background: var(--l3-background);
border-left: 1px solid var(--l1-border);
}
}
&::before {
background: var(--l3-background);
border-left: 1px solid var(--l1-border);
}
}
&.ant-radio-group-disabled {
.tab,
.selected_view {
&[disabled],
&[data-disabled] {
background: var(--l2-background) !important;
border-color: var(--l1-border) !important;
color: var(--l1-foreground) !important;
}
.tab:hover,
.selected_view:hover {
background: var(--l2-background) !important;
border-color: var(--l1-border) !important;
color: var(--l1-foreground) !important;
&:hover {
background: var(--l2-background) !important;
border-color: var(--l1-border) !important;
color: var(--l1-foreground) !important;
}
}
}
}

View File

@@ -1,4 +1,4 @@
import { Radio, RadioChangeEvent } from 'antd';
import { ToggleGroupSimple } from '@signozhq/ui/toggle-group';
import './SignozRadioGroup.styles.scss';
@@ -11,7 +11,7 @@ interface Option {
interface SignozRadioGroupProps {
value: string;
options: Option[];
onChange: (e: RadioChangeEvent) => void;
onChange: (value: string) => void;
className?: string;
disabled?: boolean;
}
@@ -24,26 +24,22 @@ function SignozRadioGroup({
disabled = false,
}: SignozRadioGroupProps): JSX.Element {
return (
<Radio.Group
<ToggleGroupSimple
type="single"
value={value}
buttonStyle="solid"
className={`signoz-radio-group ${className}`}
onChange={onChange}
disabled={disabled}
>
{options.map((option) => (
<Radio.Button
key={option.value}
value={option.value}
className={value === option.value ? 'selected_view tab' : 'tab'}
>
items={options.map((option) => ({
value: option.value,
label: (
<div className="view-title-container">
{option.icon && <div className="icon-container">{option.icon}</div>}
{option.label}
</div>
</Radio.Button>
))}
</Radio.Group>
),
}))}
/>
);
}

View File

@@ -4,13 +4,10 @@ import {
ButtonProps,
Col,
ColProps,
Divider,
DividerProps,
Row,
RowProps,
Space,
SpaceProps,
TabsProps,
} from 'antd';
import {
Typography,
@@ -34,21 +31,11 @@ const StyledRow = styled(Row)<TStyledRow>`
${styledClass}
`;
type TStyledDivider = DividerProps & IStyledClass;
const StyledDivider = styled(Divider)<TStyledDivider>`
${styledClass}
`;
type TStyledSpace = SpaceProps & IStyledClass;
const StyledSpace = styled(Space)<TStyledSpace>`
${styledClass}
`;
type TStyledTabs = TabsProps & IStyledClass;
const StyledTabs = styled(Divider)<TStyledTabs>`
${styledClass}
`;
type TStyledButton = ButtonProps & IStyledClass;
const StyledButton = styled(Button)<TStyledButton>`
${styledClass}
@@ -79,9 +66,7 @@ export {
StyledButton,
StyledCol,
StyledDiv,
StyledDivider,
StyledRow,
StyledSpace,
StyledTabs,
StyledTypography,
};

View File

@@ -1,6 +1,7 @@
import { Dispatch, SetStateAction, useCallback, useMemo } from 'react';
import { ChevronDown, Globe } from '@signozhq/icons';
import { Button, Dropdown } from 'antd';
import { DropdownMenuSimple } from '@signozhq/ui/dropdown-menu';
import { Button } from 'antd';
import { Typography } from '@signozhq/ui/typography';
import TimeItems, {
timePreferance,
@@ -27,20 +28,17 @@ function TimePreference({
const menu = useMemo(
() => ({
items: menuItems,
onClick: timeMenuItemOnChangeHandler,
items: menuItems.map((item) => ({
...item,
onClick: timeMenuItemOnChangeHandler,
})),
}),
[timeMenuItemOnChangeHandler],
);
return (
<Dropdown
menu={menu}
rootClassName="time-selection-menu"
className="time-selection-target"
trigger={['click']}
>
<Button>
<DropdownMenuSimple menu={menu} className="time-selection-menu">
<Button className="time-selection-target">
<div className="button-selected-text">
<Globe size={14} />
<Typography.Text className="selected-value">
@@ -49,7 +47,7 @@ function TimePreference({
</div>
<ChevronDown size="md" />
</Button>
</Dropdown>
</DropdownMenuSimple>
);
}

View File

@@ -88,6 +88,7 @@ const ROUTES = {
PUBLIC_DASHBOARD: '/public/dashboard/:dashboardId',
SERVICE_ACCOUNTS_SETTINGS: '/settings/service-accounts',
AI_ASSISTANT: '/ai-assistant/:conversationId',
AI_ASSISTANT_BASE: '/ai-assistant',
AI_ASSISTANT_ICON_PREVIEW: '/ai-assistant-icon-preview',
MCP_SERVER: '/settings/mcp-server',
} as const;

View File

@@ -10,7 +10,7 @@ import {
DialogSubtitle,
DialogTitle,
} from '@signozhq/ui/dialog';
import { ToggleGroup, ToggleGroupItem } from '@signozhq/ui/toggle-group';
import { ToggleGroupSimple } from '@signozhq/ui/toggle-group';
import { TooltipSimple } from '@signozhq/ui/tooltip';
import type {
ApprovalEventDTO,
@@ -132,45 +132,43 @@ export default function ApprovalCard({
<div className={styles.diffModalBody}>
<p className={styles.diffModalSummary}>{approval.summary}</p>
<div className={styles.diffToolbarRow}>
<ToggleGroup
<ToggleGroupSimple
type="single"
size="sm"
value={viewMode}
onChange={(next): void => {
// Radix `single` group can emit '' when the active item is clicked again.
onChange={(next: string): void => {
// Radix `single` group can emit '' when the active item
// is clicked again — preserve the current mode.
if (next === 'split' || next === 'unified') {
setViewMode(next);
}
}}
>
<TooltipSimple title="Split view">
<ToggleGroupItem value="split" aria-label="Split view">
<Columns2 size={12} />
</ToggleGroupItem>
</TooltipSimple>
<TooltipSimple title="Unified view">
<ToggleGroupItem value="unified" aria-label="Unified view">
<List size={12} />
</ToggleGroupItem>
</TooltipSimple>
</ToggleGroup>
<ToggleGroup
items={[
{
value: 'split',
'aria-label': 'Split view',
label: <Columns2 size={12} />,
},
{
value: 'unified',
'aria-label': 'Unified view',
label: <List size={12} />,
},
]}
/>
<ToggleGroupSimple
type="multiple"
size="sm"
value={wrapText ? ['wrap'] : []}
onChange={(next): void => setWrapText(next.includes('wrap'))}
>
<TooltipSimple
title={wrapText ? 'Disable text wrap' : 'Wrap long lines'}
>
<ToggleGroupItem
value="wrap"
aria-label={wrapText ? 'Disable text wrap' : 'Wrap long lines'}
>
<WrapText size={12} />
</ToggleGroupItem>
</TooltipSimple>
</ToggleGroup>
onChange={(next: string[]): void => setWrapText(next.includes('wrap'))}
items={[
{
value: 'wrap',
'aria-label': wrapText ? 'Disable text wrap' : 'Wrap long lines',
label: <WrapText size={12} />,
},
]}
/>
</div>
{approval.diff && (
<DiffView

View File

@@ -178,7 +178,7 @@ export default function MessageBubble({
</div>
</div>
{!isUser && (
{!isUser && !message.isRateLimitError && (
<MessageFeedback
message={message}
onRegenerate={onRegenerate}

View File

@@ -2,7 +2,8 @@ import { useState } from 'react';
import cx from 'classnames';
import { Button } from '@signozhq/ui/button';
import logEvent from 'api/common/logEvent';
import { Checkbox, Radio } from 'antd';
import { Checkbox } from '@signozhq/ui/checkbox';
import { RadioGroup, RadioGroupItem } from '@signozhq/ui/radio-group';
import { AIAssistantEvents } from '../../../events';
import { useAIAssistantAnalyticsContext } from '../../../hooks/useAIAssistantAnalyticsContext';
@@ -81,31 +82,43 @@ export default function InteractiveQuestion({
{question && <p className={blockStyles.title}>{question}</p>}
{type === 'radio' ? (
<Radio.Group
<RadioGroup
className={styles.options}
onChange={(e): void => {
setSelected([e.target.value]);
handleSubmit([e.target.value]);
onChange={(value): void => {
setSelected([value]);
handleSubmit([value]);
}}
>
{normalized.map((opt) => (
<Radio key={opt.value} value={opt.value} className={styles.option}>
<RadioGroupItem
key={opt.value}
value={opt.value}
className={styles.option}
>
{opt.label}
</Radio>
</RadioGroupItem>
))}
</Radio.Group>
</RadioGroup>
) : (
<>
<Checkbox.Group
className={cx(styles.options, styles.checkbox)}
onChange={(vals): void => setSelected(vals as string[])}
>
<div className={cx(styles.options, styles.checkbox)}>
{normalized.map((opt) => (
<Checkbox key={opt.value} value={opt.value} className={styles.option}>
<Checkbox
key={opt.value}
value={selected.includes(opt.value)}
onChange={(checked): void => {
setSelected((prev) =>
checked === true
? [...prev, opt.value]
: prev.filter((v) => v !== opt.value),
);
}}
className={styles.option}
>
{opt.label}
</Checkbox>
))}
</Checkbox.Group>
</div>
<Button
variant="solid"
size="sm"

View File

@@ -1,10 +1,12 @@
/* eslint-disable sonarjs/cognitive-complexity */
import axios from 'axios';
import { v4 as uuidv4 } from 'uuid';
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
import { immer } from 'zustand/middleware/immer';
import type {
ErrorResponseDTO,
MessageActionDTO,
MessageSummaryDTOBlocksAnyOfItem,
} from 'api/ai-assistant/sigNozAIAssistantAPI.schemas';
@@ -21,7 +23,6 @@ import {
regenerateMessage,
rejectExecution,
sendMessage as sendMessageToThread,
SSEStreamError,
streamEvents,
submitFeedback,
ThreadSummary,
@@ -193,13 +194,75 @@ function resetStreamingState(
};
}
/**
* Marker thrown by `runStreamingLoop` when an SSE event reports
* `invalid_token`. Callers that own an originating action (sendMessage /
* approve / clarify / regenerate) catch this and re-issue that action via
* `streamWithAuthRetry`; the retry's first REST call will 401, at which point
* the shared axios `interceptorRejected` rotates the access token and replays.
*/
class AuthExpiredError extends Error {
constructor() {
super('Access token expired during execution');
this.name = 'AuthExpiredError';
}
}
/**
* Runs the originating action (e.g. sendMessage POST) and streams the
* resulting execution. On `AuthExpiredError`, re-issues `start` once — the
* retry's REST call hits 401, the shared axios interceptor rotates the
* access token and replays, and the new SSE picks up the rotated token from
* localStorage. Backend signals `retryAction: 'manual'` for `invalid_token`,
* so the dead execution can't be resumed — only a fresh one helps.
*/
async function streamWithAuthRetry(
conversationId: string,
start: () => Promise<string>,
set: StoreSetter,
): Promise<void> {
for (let attempt = 0; attempt <= 1; attempt += 1) {
if (attempt > 0) {
// Drop any partial content/events from the previous attempt so the
// retried execution's stream isn't concatenated with the dead one.
set((s) => {
resetStreamingState(s, conversationId);
});
}
// eslint-disable-next-line no-await-in-loop
const executionId = await start();
const ctrl = newStreamController(conversationId);
try {
// eslint-disable-next-line no-await-in-loop
await runStreamingLoop(executionId, {
conversationId,
set,
signal: ctrl.signal,
});
streamControllers.delete(conversationId);
return;
} catch (err) {
streamControllers.delete(conversationId);
if (err instanceof AuthExpiredError && attempt < 1) {
continue;
}
throw err;
}
}
}
/**
* Runs one SSE execution stream, updating the per-conversation stream state.
*
* Breaks early and sets pendingApproval / pendingClarification when the
* agent needs user input before it can continue.
*
* Throws on `error` events — the caller's catch block handles UI feedback.
* On an `invalid_token` error event (e.g. MCP auth expired mid-execution),
* throws `AuthExpiredError` so the caller can re-issue the originating
* action via `streamWithAuthRetry`. We don't refresh here ourselves — the
* retry's REST call will 401 and the shared axios `interceptorRejected`
* handles rotation + replay. Throws on any other `error` event — the
* caller's catch block handles UI feedback.
*/
// eslint-disable-next-line sonarjs/cognitive-complexity
async function runStreamingLoop(
@@ -325,6 +388,15 @@ async function runStreamingLoop(
});
break;
} else if (event.type === 'error') {
// MCP/SigNoz auth expired mid-execution — signal the caller to
// re-issue the originating action. The retry's REST call will hit
// 401 and the shared axios `interceptorRejected` will rotate the
// access token + replay, so we don't refresh here ourselves.
// (Backend sets `retryAction: 'manual'`, so the failed execution
// can't itself be resumed — only a fresh one helps.)
if (event.error.code === 'invalid_token') {
throw new AuthExpiredError();
}
throw Object.assign(new Error(event.error.message), {
retryAction: event.retryAction,
});
@@ -412,13 +484,41 @@ function hasPendingInput(conversationId: string, get: StoreGetter): boolean {
return Boolean(stream?.pendingApproval || stream?.pendingClarification);
}
function parseErrorBody(value: unknown): string | null {
if (typeof value === 'string') {
try {
return parseErrorBody(JSON.parse(value));
} catch {
return null;
}
}
const message = (value as ErrorResponseDTO | undefined)?.error?.message;
return typeof message === 'string' && message.length > 0 ? message : null;
}
/**
* Commits an error message and removes the stream entry.
* Returns the backend's `error.message` when `err` is a 429 axios response
* (typically from the threads API surface — createThread, sendMessage, approve,
* clarify, regenerate). Returns null for any other error so callers fall
* through to their generic copy.
*/
function rateLimitMessage(err: unknown): string | null {
if (axios.isAxiosError(err) && err.response?.status === 429) {
return parseErrorBody(err.response.data);
}
return null;
}
/**
* Commits an error message and removes the stream entry. When `isRateLimit`
* is true, the committed message is flagged so the feedback/regenerate bar
* is hidden — clicking regenerate would just 429 again.
*/
function finalizeStreamingError(
conversationId: string,
errorContent: string,
set: StoreSetter,
isRateLimit = false,
): void {
set((s) => {
const conv = s.conversations[conversationId];
@@ -428,6 +528,7 @@ function finalizeStreamingError(
role: 'assistant',
content: errorContent,
createdAt: Date.now(),
...(isRateLimit ? { isRateLimitError: true } : {}),
});
conv.updatedAt = Date.now();
}
@@ -801,7 +902,12 @@ export const useAIAssistantStore = create<AIAssistantStore>()(
});
// Reconnect to SSE if backend execution is still running
// and we don't already have an active SSE reader for this thread
// and we don't already have an active SSE reader for this
// thread. No auth-retry wrapper here: on `invalid_token`
// there's no "originating action" to redo — reopening the
// same dead executionId would just re-emit the failure.
// Let the error bubble; the user can send a new message,
// which will go through `streamWithAuthRetry`.
if (
detail.activeExecutionId &&
!streamControllers.has(threadId) &&
@@ -1052,14 +1158,12 @@ export const useAIAssistantStore = create<AIAssistantStore>()(
}
});
}
const executionId = await sendMessageToThread(threadId, text, contexts);
const ctrl = newStreamController(convId);
await runStreamingLoop(executionId, {
conversationId: convId,
const tid = threadId;
await streamWithAuthRetry(
convId,
() => sendMessageToThread(tid, text, contexts),
set,
signal: ctrl.signal,
});
streamControllers.delete(convId);
);
if (!hasPendingInput(convId, get)) {
finalizeStreamingMessage(convId, set, get);
@@ -1070,11 +1174,14 @@ export const useAIAssistantStore = create<AIAssistantStore>()(
return;
}
console.error('[AIAssistant] sendMessage failed:', err);
const message =
err instanceof SSEStreamError && err.status === 429
? 'You sent that a bit too quickly. Please wait a moment and try again.'
: 'Something went wrong while fetching the response. Please try again.';
finalizeStreamingError(convId, message, set);
const rateLimit = rateLimitMessage(err);
finalizeStreamingError(
convId,
rateLimit ??
'Something went wrong while fetching the response. Please try again.',
set,
rateLimit !== null,
);
}
},
@@ -1094,14 +1201,11 @@ export const useAIAssistantStore = create<AIAssistantStore>()(
});
try {
const executionId = await approveExecution(approvalId);
const ctrl = newStreamController(conversationId);
await runStreamingLoop(executionId, {
await streamWithAuthRetry(
conversationId,
() => approveExecution(approvalId),
set,
signal: ctrl.signal,
});
streamControllers.delete(conversationId);
);
if (!hasPendingInput(conversationId, get)) {
finalizeStreamingMessage(conversationId, set, get);
}
@@ -1110,10 +1214,13 @@ export const useAIAssistantStore = create<AIAssistantStore>()(
return;
}
console.error('[AIAssistant] approveAction failed:', err);
const rateLimit = rateLimitMessage(err);
finalizeStreamingError(
conversationId,
'Something went wrong while processing the approval. Please try again.',
rateLimit ??
'Something went wrong while processing the approval. Please try again.',
set,
rateLimit !== null,
);
}
},
@@ -1176,14 +1283,11 @@ export const useAIAssistantStore = create<AIAssistantStore>()(
});
try {
const executionId = await regenerateMessage(messageId);
const ctrl = newStreamController(conversationId);
await runStreamingLoop(executionId, {
await streamWithAuthRetry(
conversationId,
() => regenerateMessage(messageId),
set,
signal: ctrl.signal,
});
streamControllers.delete(conversationId);
);
if (!hasPendingInput(conversationId, get)) {
finalizeStreamingMessage(conversationId, set, get);
}
@@ -1192,10 +1296,13 @@ export const useAIAssistantStore = create<AIAssistantStore>()(
return;
}
console.error('[AIAssistant] regenerateAssistantMessage failed:', err);
const rateLimit = rateLimitMessage(err);
finalizeStreamingError(
conversationId,
'Something went wrong while regenerating the response. Please try again.',
rateLimit ??
'Something went wrong while regenerating the response. Please try again.',
set,
rateLimit !== null,
);
}
},
@@ -1245,14 +1352,11 @@ export const useAIAssistantStore = create<AIAssistantStore>()(
});
try {
const executionId = await clarifyExecution(clarificationId, answers);
const ctrl = newStreamController(conversationId);
await runStreamingLoop(executionId, {
await streamWithAuthRetry(
conversationId,
() => clarifyExecution(clarificationId, answers),
set,
signal: ctrl.signal,
});
streamControllers.delete(conversationId);
);
if (!hasPendingInput(conversationId, get)) {
finalizeStreamingMessage(conversationId, set, get);
}
@@ -1261,10 +1365,13 @@ export const useAIAssistantStore = create<AIAssistantStore>()(
return;
}
console.error('[AIAssistant] submitClarification failed:', err);
const rateLimit = rateLimitMessage(err);
finalizeStreamingError(
conversationId,
'Something went wrong while processing your answers. Please try again.',
rateLimit ??
'Something went wrong while processing your answers. Please try again.',
set,
rateLimit !== null,
);
}
},

View File

@@ -86,6 +86,11 @@ export interface Message {
actions?: MessageActionDTO[];
/** Persisted feedback rating — set after user votes and the API confirms. */
feedbackRating?: FeedbackRating | null;
/**
* Set on client-side rate-limit error messages so the feedback/regenerate
* bar (copy/vote/regenerate) is hidden — retrying would just 429 again.
*/
isRateLimitError?: boolean;
createdAt: number;
}

View File

@@ -1,5 +1,6 @@
import { useEffect, useRef, useState } from 'react';
import { Checkbox, Input } from 'antd';
import { Input } from 'antd';
import { Checkbox } from '@signozhq/ui/checkbox';
import { Typography } from '@signozhq/ui/typography';
import { useIsDarkMode } from 'hooks/useDarkMode';
import useDebouncedFn from 'hooks/useDebouncedFunction';
@@ -320,10 +321,8 @@ function AnomalyAlertEvaluationView({
{filteredSeriesKeys.length > 0 && (
<Checkbox
className="anomaly-alert-evaluation-view-series-list-item"
type="checkbox"
name="series"
value="all"
checked={selectedSeries === null}
value={selectedSeries === null}
onChange={(): void => handleSeriesChange(null)}
>
Show All
@@ -335,10 +334,8 @@ function AnomalyAlertEvaluationView({
<Checkbox
className="anomaly-alert-evaluation-view-series-list-item"
key={seriesKey}
type="checkbox"
name="series"
value={seriesKey}
checked={selectedSeries === seriesKey}
value={selectedSeries === seriesKey}
onChange={(): void => handleSeriesChange(seriesKey)}
>
<div

View File

@@ -161,41 +161,6 @@
justify-content: space-between;
align-items: center;
border-bottom: 1px solid var(--l1-border);
.views-tabs {
color: var(--l2-foreground);
.view-title {
display: flex;
gap: var(--margin-2);
align-items: center;
justify-content: center;
font-size: 14px;
font-style: normal;
font-weight: var(--font-weight-normal);
}
.tab {
padding: 0 32px;
background: var(--l3-background);
border: 1px solid var(--l1-border);
width: auto;
}
.tab::before {
background: var(--l1-border);
}
.selected_view {
background: none;
border: 1px solid var(--l1-border);
border-bottom: none;
}
.selected_view::before {
background: var(--l1-border);
}
}
}
}
@@ -699,40 +664,6 @@
left: 50%;
transform: translate(-50%, -50%);
}
.views-tabs {
color: var(--l2-foreground);
.ant-btn {
box-shadow: none;
position: relative;
}
.tab {
border: 1px solid var(--l1-border);
width: 114px;
z-index: 1;
}
.tab:hover {
z-index: 3;
}
.tab::before {
background: var(--l2-background);
}
.selected_view {
background: var(--l1-border);
color: var(--l1-foreground);
border: 1px solid var(--l1-border);
z-index: 2;
}
.selected_view::before {
background: var(--l2-background);
}
}
}
.top-services-content {

View File

@@ -2,9 +2,10 @@ import { useCallback, useEffect, useMemo, useState } from 'react';
// eslint-disable-next-line no-restricted-imports
import { useSelector } from 'react-redux';
import { Spacing } from '@signozhq/design-tokens';
import { Button, Divider, Drawer, Radio } from 'antd';
import { Button, Drawer } from 'antd';
import { ToggleGroupSimple } from '@signozhq/ui/toggle-group';
import { Divider } from '@signozhq/ui/divider';
import { Typography } from '@signozhq/ui/typography';
import type { RadioChangeEvent } from 'antd/lib';
import DateTimeSelectionV2 from 'container/TopNav/DateTimeSelectionV2';
import {
CustomTimeType,
@@ -55,9 +56,9 @@ function DomainDetails({
const [initialFiltersEndPointStats, setInitialFiltersEndPointStats] =
useState<IBuilderQuery['filters']>(domainListFilters);
const handleTabChange = (e: RadioChangeEvent): void => {
setSelectedView(e.target.value);
setParams({ selectedView: e.target.value });
const handleTabChange = (value: string): void => {
setSelectedView(value as VIEWS);
setParams({ selectedView: value });
};
const handleEndPointChange = (name: string): void => {
@@ -224,38 +225,17 @@ function DomainDetails({
timeRange={modalTimeRange}
/>
<div className="views-tabs-container">
<Radio.Group
className="views-tabs"
<ToggleGroupSimple
type="single"
onChange={handleTabChange}
value={selectedView}
>
<Radio.Button
className={
selectedView === VIEW_TYPES.ALL_ENDPOINTS ? 'selected_view tab' : 'tab'
}
value={VIEW_TYPES.ALL_ENDPOINTS}
>
<div className="view-title">All Endpoints</div>
</Radio.Button>
<Radio.Button
className={
selectedView === VIEW_TYPES.ENDPOINT_STATS
? 'tab selected_view'
: 'tab'
}
value={VIEW_TYPES.ENDPOINT_STATS}
>
<div className="view-title">Endpoint(s) Stats</div>
</Radio.Button>
<Radio.Button
className={
selectedView === VIEW_TYPES.TOP_ERRORS ? 'tab selected_view' : 'tab'
}
value={VIEW_TYPES.TOP_ERRORS}
>
<div className="view-title">Top 10 Errors</div>
</Radio.Button>
</Radio.Group>
size="lg"
items={[
{ value: VIEW_TYPES.ALL_ENDPOINTS, label: 'All Endpoints' },
{ value: VIEW_TYPES.ENDPOINT_STATS, label: 'Endpoint(s) Stats' },
{ value: VIEW_TYPES.TOP_ERRORS, label: 'Top 10 Errors' },
]}
/>
</div>
{selectedView === VIEW_TYPES.ALL_ENDPOINTS && (
<AllEndPoints

View File

@@ -21,14 +21,17 @@ import { useResizeObserver } from 'hooks/useDimensions';
import { useNotifications } from 'hooks/useNotifications';
import { getUPlotChartData } from 'lib/uPlotLib/utils/getUplotChartData';
import { LegendPosition } from 'lib/uPlotV2/components/types';
import { getStartAndEndTimesInMilliseconds } from 'pages/MessagingQueues/MessagingQueuesUtils';
import { useTimezone } from 'providers/Timezone';
import { SuccessResponse } from 'types/api';
import { Widgets } from 'types/api/dashboard/getAll';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import ErrorState from './ErrorState';
import { prepareStatusCodeBarChartsConfig } from './utils';
import {
getStepIntervalForQuery,
getTracesTimeRangeFromStepInterval,
prepareStatusCodeBarChartsConfig,
} from './utils';
function StatusCodeBarCharts({
endPointStatusCodeBarChartsDataQuery,
@@ -135,6 +138,18 @@ function StatusCodeBarCharts({
[domainName, filters],
);
const activeApiResponse = useMemo(
() =>
currentWidgetInfoIndex === 0
? formattedEndPointStatusCodeBarChartsDataPayload
: formattedEndPointStatusCodeLatencyBarChartsDataPayload,
[
currentWidgetInfoIndex,
formattedEndPointStatusCodeBarChartsDataPayload,
formattedEndPointStatusCodeLatencyBarChartsDataPayload,
],
);
const graphClickHandler = useCallback(
(
xValue: number,
@@ -144,11 +159,14 @@ function StatusCodeBarCharts({
metric?: { [key: string]: string },
queryData?: { queryName: string; inFocusOrNot: boolean },
): void => {
const TWO_AND_HALF_MINUTES_IN_MILLISECONDS = 2.5 * 60 * 1000; // 150,000 milliseconds
const customFilters = getCustomFiltersForBarChart(metric);
const { start, end } = getStartAndEndTimesInMilliseconds(
const stepInterval = getStepIntervalForQuery(
activeApiResponse,
queryData?.queryName,
);
const { start, end } = getTracesTimeRangeFromStepInterval(
xValue,
TWO_AND_HALF_MINUTES_IN_MILLISECONDS,
stepInterval,
);
handleGraphClick({
@@ -171,6 +189,7 @@ function StatusCodeBarCharts({
});
},
[
activeApiResponse,
widget,
navigateToExplorerPages,
navigateToExplorer,

View File

@@ -0,0 +1,68 @@
import {
getMinStepIntervalFromApiResponse,
getStepIntervalForQuery,
getTracesTimeRangeFromStepInterval,
} from '../utils';
describe('StatusCodeBarCharts utils', () => {
describe('getTracesTimeRangeFromStepInterval', () => {
const xValue = 1609459200; // seconds
it('keeps start at click time with a minimum 5 minute end range', () => {
const { start, end } = getTracesTimeRangeFromStepInterval(xValue, 60);
expect(start).toBe(xValue * 1000);
expect(end - start).toBe(5 * 60 * 1000);
expect(end).toBe(xValue * 1000 + 5 * 60 * 1000);
});
it('extends end when step interval is larger than 5 minutes', () => {
const stepInterval = 600; // 10 minutes
const { start, end } = getTracesTimeRangeFromStepInterval(
xValue,
stepInterval,
);
expect(start).toBe(xValue * 1000);
expect(end - start).toBe(10 * 60 * 1000);
expect(end).toBe(xValue * 1000 + 10 * 60 * 1000);
});
});
describe('getMinStepIntervalFromApiResponse', () => {
it('returns 60 when step intervals are missing', () => {
expect(getMinStepIntervalFromApiResponse({} as any)).toBe(60);
});
it('returns the minimum step interval from the response', () => {
const apiResponse = {
data: {
newResult: {
meta: {
stepIntervals: { A: 120, B: 60 },
},
},
},
};
expect(getMinStepIntervalFromApiResponse(apiResponse as any)).toBe(60);
});
});
describe('getStepIntervalForQuery', () => {
it('returns query-specific step interval when available', () => {
const apiResponse = {
data: {
newResult: {
meta: {
stepIntervals: { A: 120, B: 60 },
},
},
},
};
expect(getStepIntervalForQuery(apiResponse as any, 'A')).toBe(120);
expect(getStepIntervalForQuery(apiResponse as any, 'B')).toBe(60);
});
});
});

View File

@@ -13,6 +13,65 @@ import { Query } from 'types/api/queryBuilder/queryBuilderData';
import { QueryData } from 'types/api/widgets/getQuery';
import { v4 } from 'uuid';
const DEFAULT_STEP_INTERVAL_SECONDS = 60;
const MIN_TRACES_TIME_RANGE_MINUTES = 5;
export function getMinStepIntervalFromApiResponse(
apiResponse: MetricRangePayloadProps,
): number {
const stepIntervals: ExecStats['stepIntervals'] = get(
apiResponse,
'data.newResult.meta.stepIntervals',
{},
);
const values = Object.values(stepIntervals).filter(
(value): value is number =>
typeof value === 'number' && Number.isFinite(value),
);
if (values.length === 0) {
return DEFAULT_STEP_INTERVAL_SECONDS;
}
return Math.min(...values);
}
export function getStepIntervalForQuery(
apiResponse: MetricRangePayloadProps,
queryName?: string,
): number {
const minStepInterval = getMinStepIntervalFromApiResponse(apiResponse);
if (!queryName) {
return minStepInterval;
}
const stepIntervals: ExecStats['stepIntervals'] = get(
apiResponse,
'data.newResult.meta.stepIntervals',
{},
);
return get(stepIntervals, queryName, minStepInterval) ?? minStepInterval;
}
export function getTracesTimeRangeFromStepInterval(
xValue: number,
stepIntervalSeconds: number,
): { start: number; end: number } {
const rangeMinutes = Math.max(
stepIntervalSeconds / 60,
MIN_TRACES_TIME_RANGE_MINUTES,
);
const rangeMs = rangeMinutes * 60 * 1000;
const start = Math.floor(xValue * 1000);
return {
start,
end: Math.ceil(start + rangeMs),
};
}
export const prepareStatusCodeBarChartsConfig = ({
timezone,
isDarkMode,
@@ -41,7 +100,7 @@ export const prepareStatusCodeBarChartsConfig = ({
'data.newResult.meta.stepIntervals',
{},
);
const minStepInterval = Math.min(...Object.values(stepIntervals));
const minStepInterval = getMinStepIntervalFromApiResponse(apiResponse);
const config = buildBaseConfig({
id: v4(),

View File

@@ -11,8 +11,13 @@ import {
} from '@signozhq/icons';
import { Button } from '@signozhq/ui/button';
import { Callout } from '@signozhq/ui/callout';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuTrigger,
} from '@signozhq/ui/dropdown-menu';
import { toast } from '@signozhq/ui/sonner';
import { Dropdown, Skeleton } from 'antd';
import { Skeleton } from 'antd';
import {
RenderErrorResponseDTO,
ZeustypesHostDTO,
@@ -200,10 +205,15 @@ export default function CustomDomainSettings(): JSX.Element {
!workspaceName ? 'workspace-name-hidden' : ''
}`}
>
<Dropdown
trigger={['click']}
disabled={isFetchingHosts}
dropdownRender={(): JSX.Element => (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="link" color="none" disabled={isFetchingHosts}>
<Link2 size={12} />
<span>{stripProtocol(activeHost?.url ?? '')}</span>
<ChevronDown size={12} />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start">
<div className="workspace-url-dropdown">
<span className="workspace-url-dropdown-header">
All Workspace URLs
@@ -236,14 +246,8 @@ export default function CustomDomainSettings(): JSX.Element {
);
})}
</div>
)}
>
<Button variant="link" color="none">
<Link2 size={12} />
<span>{stripProtocol(activeHost?.url ?? '')}</span>
<ChevronDown size={12} />
</Button>
</Dropdown>
</DropdownMenuContent>
</DropdownMenu>
<span className="custom-domain-card-meta-timezone">
<Clock size={11} />
{timezone.offset}

View File

@@ -1,4 +1,5 @@
import { GetHosts200 } from 'api/generated/services/sigNoz.schemas';
import userEvent from '@testing-library/user-event';
import { rest, server } from 'mocks-server/server';
import { fireEvent, render, screen, waitFor } from 'tests/test-utils';
@@ -142,12 +143,13 @@ describe('CustomDomainSettings', () => {
});
it('shows all workspace URLs as links in the dropdown', async () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
render(<CustomDomainSettings />);
await screen.findByText(/custom-host\.test\.cloud/i);
// Open the URL dropdown
fireEvent.click(
await user.click(
screen.getByRole('button', { name: /custom-host\.test\.cloud/i }),
);

View File

@@ -1,6 +1,7 @@
import { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Col, Input, Radio, Select, Space, Tooltip } from 'antd';
import { Col, Input, Select, Space, Tooltip } from 'antd';
import { ToggleGroupSimple } from '@signozhq/ui/toggle-group';
import { Typography } from '@signozhq/ui/typography';
import AddTags from 'container/DashboardContainer/DashboardSettings/General/AddTags';
import { useDashboardCursorSyncMode } from 'hooks/dashboard/useDashboardCursorSyncMode';
@@ -213,18 +214,18 @@ function GeneralDashboardSettings(): JSX.Element {
Sync crosshair and tooltip across all the dashboard panels
</Typography.Text>
</div>
<Radio.Group
<ToggleGroupSimple
type="single"
value={cursorSyncMode}
onChange={(e): void => {
setCursorSyncMode(e.target.value as DashboardCursorSync);
onChange={(value: string): void => {
setCursorSyncMode(value as DashboardCursorSync);
}}
>
<Radio.Button value={DashboardCursorSync.None}>No Sync</Radio.Button>
<Radio.Button value={DashboardCursorSync.Crosshair}>
Crosshair
</Radio.Button>
<Radio.Button value={DashboardCursorSync.Tooltip}>Tooltip</Radio.Button>
</Radio.Group>
items={[
{ value: DashboardCursorSync.None, label: 'No Sync' },
{ value: DashboardCursorSync.Crosshair, label: 'Crosshair' },
{ value: DashboardCursorSync.Tooltip, label: 'Tooltip' },
]}
/>
</div>
{cursorSyncMode === DashboardCursorSync.Tooltip && (
<div className={styles.crossPanelSyncRow}>
@@ -237,21 +238,21 @@ function GeneralDashboardSettings(): JSX.Element {
matching ones highlighted
</Typography.Text>
</div>
<Radio.Group
<ToggleGroupSimple
type="single"
value={syncTooltipFilterMode}
onChange={(e): void => {
onChange={(value: string): void => {
logEvent(Events.TOOLTIP_SYNC_MODE_CHANGED, {
path: getAbsoluteUrl(window.location.pathname),
mode: e.target.value,
mode: value,
});
setSyncTooltipFilterMode(e.target.value as SyncTooltipFilterMode);
setSyncTooltipFilterMode(value as SyncTooltipFilterMode);
}}
>
<Radio.Button value={SyncTooltipFilterMode.All}>All</Radio.Button>
<Radio.Button value={SyncTooltipFilterMode.Filtered}>
Filtered
</Radio.Button>
</Radio.Group>
items={[
{ value: SyncTooltipFilterMode.All, label: 'All' },
{ value: SyncTooltipFilterMode.Filtered, label: 'Filtered' },
]}
/>
</div>
)}
</Col>

View File

@@ -395,8 +395,8 @@ describe('Dynamic Variable Default Behavior', () => {
// Check if the checkbox exists (it should be unchecked initially)
const checkbox = allOptionContainer?.querySelector(
'input[type="checkbox"]',
) as HTMLInputElement;
'[role="checkbox"]',
) as HTMLElement;
expect(checkbox).toBeInTheDocument();
// Should call onValueUpdate with all values (ALL selection)
@@ -516,10 +516,10 @@ describe('Dynamic Variable Default Behavior', () => {
// Check if the checkbox for ALL option is checked
const checkbox = dropdownAllOption.querySelector(
'input[type="checkbox"]',
) as HTMLInputElement;
'[role="checkbox"]',
) as HTMLElement;
expect(checkbox).toBeInTheDocument();
expect(checkbox.checked).toBe(true);
expect(checkbox).toHaveAttribute('data-state', 'checked');
});
});
});

View File

@@ -196,10 +196,10 @@ describe('Panel Management Tests', () => {
expect(allOption).toHaveClass('selected');
const allCheckbox = allOption.querySelector(
'input[type="checkbox"]',
) as HTMLInputElement;
'[role="checkbox"]',
) as HTMLElement;
expect(allCheckbox).toBeInTheDocument();
expect(allCheckbox.checked).toBe(true);
expect(allCheckbox).toHaveAttribute('data-state', 'checked');
});
});

View File

@@ -160,8 +160,8 @@ describe('getChartManagerColumns', () => {
expect(renderFn).toBeDefined();
const { container } = render(renderFn!(null, tableDataSet[1], 1));
const checkbox = container.querySelector('input[type="checkbox"]');
const checkbox = container.querySelector('[role="checkbox"]');
expect(checkbox).toBeInTheDocument();
expect(checkbox).toBeChecked(); // graphVisibilityState[1] is true
expect(checkbox).toHaveAttribute('data-state', 'checked'); // graphVisibilityState[1] is true
});
});

View File

@@ -1,6 +1,7 @@
import { useState } from 'react';
import { CloudDownload } from '@signozhq/icons';
import { Button, Dropdown, MenuProps, Flex } from 'antd';
import { DropdownMenuSimple, type MenuProps } from '@signozhq/ui/dropdown-menu';
import { Button, Flex } from 'antd';
import { unparse } from 'papaparse';
import { DownloadProps } from './Download.types';
@@ -67,7 +68,7 @@ function Download({ data, isLoading, fileName }: DownloadProps): JSX.Element {
};
return (
<Dropdown menu={menu} trigger={['click']}>
<DropdownMenuSimple menu={menu}>
<Button
className="download-button"
loading={isLoading || isDownloading}
@@ -79,7 +80,7 @@ function Download({ data, isLoading, fileName }: DownloadProps): JSX.Element {
Download
</Flex>
</Button>
</Dropdown>
</DropdownMenuSimple>
);
}

View File

@@ -2,7 +2,8 @@ import { useEffect, useMemo, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useQuery } from 'react-query';
import { useLocation } from 'react-router-dom';
import { Button, Divider, Space } from 'antd';
import { Button, Space } from 'antd';
import { Divider } from '@signozhq/ui/divider';
import { Typography } from '@signozhq/ui/typography';
import logEvent from 'api/common/logEvent';
import getNextPrevId from 'api/errors/getNextPrevId';

View File

@@ -22,13 +22,13 @@ import { Color } from '@signozhq/design-tokens';
import {
Button,
ColorPicker,
Divider,
Input,
Modal,
RefSelectProps,
Select,
Tooltip,
} from 'antd';
import { Divider } from '@signozhq/ui/divider';
import { Typography } from '@signozhq/ui/typography';
import getLocalStorageKey from 'api/browser/localstorage/get';
import setLocalStorageKey from 'api/browser/localstorage/set';

View File

@@ -1,8 +1,4 @@
import {
Col,
Dropdown as DropDownComponent,
Input as InputComponent,
} from 'antd';
import { Col, Input as InputComponent } from 'antd';
import { Typography as TypographyComponent } from '@signozhq/ui/typography';
import styled from 'styled-components';
@@ -34,16 +30,6 @@ export const ButtonContainer = styled.div`
}
`;
export const Dropdown = styled(DropDownComponent)`
&&& {
display: flex;
justify-content: center;
align-items: center;
max-width: 150px;
min-width: 150px;
}
`;
export const TextContainer = styled.div`
&&& {
min-width: 100px;

View File

@@ -1,6 +1,6 @@
import { grey } from '@ant-design/colors';
import { Checkbox, ConfigProvider } from 'antd';
import type { CheckboxChangeEvent } from 'antd/es/checkbox';
import { Checkbox } from '@signozhq/ui/checkbox';
import { CSSProperties } from 'react';
import { CheckBoxProps } from '../types';
@@ -11,30 +11,22 @@ function CustomCheckBox({
checkBoxOnChangeHandler,
disabled = false,
}: CheckBoxProps): JSX.Element {
const onChangeHandler = (e: CheckboxChangeEvent): void => {
checkBoxOnChangeHandler(e, index);
};
const color = data[index]?.stroke?.toString() || grey[0];
const isChecked = graphVisibilityState[index] || false;
const colorStyle = {
'--checkbox-checked-background': color,
'--checkbox-border-color': color,
} as CSSProperties;
return (
<ConfigProvider
theme={{
token: {
colorPrimary: color,
colorBorder: color,
colorBgContainer: color,
},
}}
>
<span style={colorStyle}>
<Checkbox
onChange={onChangeHandler}
checked={isChecked}
onChange={(checked): void => checkBoxOnChangeHandler(checked, index)}
value={isChecked}
disabled={disabled}
/>
</ConfigProvider>
</span>
);
}

View File

@@ -1,5 +1,4 @@
import { Dispatch, MutableRefObject, RefObject, SetStateAction } from 'react';
import type { CheckboxChangeEvent } from 'antd/es/checkbox';
import { ToggleGraphProps } from 'components/Graph/types';
import { UplotProps } from 'components/Uplot/Uplot';
import { PANEL_TYPES } from 'constants/queryBuilder';
@@ -77,7 +76,10 @@ export interface CheckBoxProps {
data: ExtendedChartDataset[];
index: number;
graphVisibilityState: boolean[];
checkBoxOnChangeHandler: (e: CheckboxChangeEvent, index: number) => void;
checkBoxOnChangeHandler: (
checked: boolean | 'indeterminate',
index: number,
) => void;
disabled?: boolean;
}

View File

@@ -1,6 +1,7 @@
// eslint-disable-next-line no-restricted-imports
import { Provider } from 'react-redux';
import { fireEvent, render, screen } from '@testing-library/react';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { PANEL_TYPES } from 'constants/queryBuilder';
import ROUTES from 'constants/routes';
import { AppProvider } from 'providers/App/App';
@@ -176,6 +177,7 @@ jest.mock('providers/Dashboard/store/useDashboardStore', () => ({
describe('WidgetGraphComponent', () => {
it('should show correct menu items when hovering over more options while loading', async () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
const { getByTestId, findByRole, getByText, container } = render(
<MockQueryClientProvider>
<ErrorModalProvider>
@@ -208,7 +210,7 @@ describe('WidgetGraphComponent', () => {
expect(skeleton).toBeInTheDocument();
const moreOptionsButton = getByTestId('widget-header-options');
fireEvent.mouseEnter(moreOptionsButton);
await user.click(moreOptionsButton);
const menu = await findByRole('menu');
expect(menu).toBeInTheDocument();

View File

@@ -54,6 +54,17 @@
visibility: visible;
}
// currently the width of the dropdown menu is set to 100% of the parent container,
// which is not desired. This is a workaround to unset that width and allow the dropdown menu to size based on its content.
// This is necessary because the dropdown menu can contain items with varying widths, and setting it to 100% can cause layout issues and make the menu look unbalanced.
// we should idealy fix this in the dropdown menu component itself, but for now this is a quick fix to ensure the dropdown menu looks correct in the widget header.
[data-radix-popper-content-wrapper]
[data-slot='dropdown-menu-content'].widget-header-dropdown
[data-slot='dropdown-menu-item'] {
width: unset !important;
}
.widget-api-actions {
padding-right: 0.25rem;
}

View File

@@ -467,6 +467,7 @@ describe('WidgetHeader', () => {
describe('Create Alerts Menu Item', () => {
it('renders Create Alerts menu item with external link icon when included in headerMenuList', async () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
render(
<WidgetHeader
title={TEST_WIDGET_TITLE}
@@ -483,7 +484,7 @@ describe('WidgetHeader', () => {
const moreOptionsIcon = await screen.findByTestId(WIDGET_HEADER_OPTIONS_ID);
expect(moreOptionsIcon).toBeInTheDocument();
await userEvent.hover(moreOptionsIcon);
await user.click(moreOptionsIcon);
await screen.findByText(CREATE_ALERTS_TEXT);
@@ -494,6 +495,7 @@ describe('WidgetHeader', () => {
});
it('Create Alerts menu item is enabled and clickable', async () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
const mockCreateAlertsHandler = jest.fn();
const useCreateAlerts = jest.requireMock(
'hooks/queryBuilder/useCreateAlerts',
@@ -517,12 +519,12 @@ describe('WidgetHeader', () => {
expect(useCreateAlerts).toHaveBeenCalledWith(mockWidget, 'dashboardView');
const moreOptionsIcon = await screen.findByTestId(WIDGET_HEADER_OPTIONS_ID);
await userEvent.hover(moreOptionsIcon);
await user.click(moreOptionsIcon);
const createAlertsMenuItem = await screen.findByText(CREATE_ALERTS_TEXT);
// Verify the menu item is clickable by actually clicking it
await userEvent.click(createAlertsMenuItem);
await user.click(createAlertsMenuItem);
expect(mockCreateAlertsHandler).toHaveBeenCalledTimes(1);
});
});

View File

@@ -15,7 +15,8 @@ import {
X,
} from '@signozhq/icons';
import { Color } from '@signozhq/design-tokens';
import { Button, Dropdown, Input, MenuProps, Tooltip } from 'antd';
import { Button, Input, Tooltip } from 'antd';
import { DropdownMenuSimple } from '@signozhq/ui/dropdown-menu';
import { Typography } from '@signozhq/ui/typography';
import ErrorContent from 'components/ErrorModal/components/ErrorContent';
import ErrorPopover from 'components/ErrorPopover/ErrorPopover';
@@ -128,7 +129,7 @@ function WidgetHeader({
],
);
const onMenuItemSelectHandler: MenuProps['onClick'] = useCallback(
const onMenuItemSelectHandler = useCallback(
({ key }: { key: string }): void => {
if (isTWidgetOptions(key)) {
const functionToCall = keyMethodMapping[key];
@@ -188,18 +189,8 @@ function WidgetHeader({
{
key: MenuItemKeys.CreateAlerts,
icon: <Bell size="md" />,
label: (
<span
style={{
display: 'flex',
alignItems: 'baseline',
justifyContent: 'space-between',
}}
>
{MENUITEM_KEYS_VS_LABELS[MenuItemKeys.CreateAlerts]}
<SquareArrowOutUpRight size={10} />
</span>
),
label: MENUITEM_KEYS_VS_LABELS[MenuItemKeys.CreateAlerts],
rightIcon: <SquareArrowOutUpRight size="lg" />,
isVisible: headerMenuList?.includes(MenuItemKeys.CreateAlerts) || false,
disabled: false,
},
@@ -221,8 +212,10 @@ function WidgetHeader({
const menu = useMemo(
() => ({
items: updatedMenuList,
onClick: onMenuItemSelectHandler,
items: updatedMenuList.map((item) => ({
...item,
onClick: onMenuItemSelectHandler,
})),
}),
[updatedMenuList, onMenuItemSelectHandler],
);
@@ -321,7 +314,12 @@ function WidgetHeader({
/>
)}
{menu && Array.isArray(menu.items) && menu.items.length > 0 && (
<Dropdown menu={menu} trigger={['hover']} placement="bottomRight">
<DropdownMenuSimple
menu={menu}
side="bottom"
align="end"
className="widget-header-dropdown"
>
<Button
data-testid="widget-header-options"
className={`widget-header-more-options ${
@@ -329,7 +327,7 @@ function WidgetHeader({
}`}
icon={<EllipsisVertical size="md" />}
/>
</Dropdown>
</DropdownMenuSimple>
)}
</div>
</>

View File

@@ -6,6 +6,7 @@ export interface MenuItem {
key: MenuItemKeys;
icon: ReactNode;
label: ReactNode;
rightIcon?: ReactNode;
isVisible: boolean;
disabled: boolean;
danger?: boolean;

View File

@@ -1,9 +1,9 @@
import type { MenuItemType } from 'antd/es/menu/hooks/useItems';
import type { MenuItem as DropdownMenuItem } from '@signozhq/ui/dropdown-menu';
import { MenuItemKeys } from './contants';
import { MenuItem } from './types';
export const generateMenuList = (actions: MenuItem[]): MenuItemType[] =>
export const generateMenuList = (actions: MenuItem[]): DropdownMenuItem[] =>
actions
.filter((action: MenuItem) => action.isVisible)
.map(({ key, icon: Icon, label, disabled, ...rest }) => ({

View File

@@ -8,9 +8,10 @@ import React, {
import { useQuery } from 'react-query';
// eslint-disable-next-line no-restricted-imports
import { Color, Spacing } from '@signozhq/design-tokens';
import { Button, Divider, Drawer, Radio, Tooltip } from 'antd';
import { Button, Drawer, Tooltip } from 'antd';
import { ToggleGroupSimple } from '@signozhq/ui/toggle-group';
import { Divider } from '@signozhq/ui/divider';
import { Typography } from '@signozhq/ui/typography';
import type { RadioChangeEvent } from 'antd/lib';
import logEvent from 'api/common/logEvent';
import { combineInitialAndUserExpression } from 'components/QueryBuilderV2/QueryV2/QuerySearch/utils';
import { convertFiltersToExpression } from 'components/QueryBuilderV2/utils';
@@ -330,8 +331,8 @@ export default function K8sBaseDetails<T>({
}
}, [getMinMaxTime, selectedTime]);
const handleTabChange = (e: RadioChangeEvent): void => {
setSelectedView(e.target.value);
const handleTabChange = (value: string): void => {
setSelectedView(value);
setLogFiltersParam(null);
setTracesFiltersParam(null);
setEventsFiltersParam(null);
@@ -339,7 +340,7 @@ export default function K8sBaseDetails<T>({
entity: InfraMonitoringEvents.K8sEntity,
page: InfraMonitoringEvents.DetailedPage,
category: eventCategory,
view: e.target.value,
view: value,
});
};
@@ -520,102 +521,101 @@ export default function K8sBaseDetails<T>({
{!hideDetailViewTabs && (
<div className="views-tabs-container">
<Radio.Group
<ToggleGroupSimple
type="single"
className="views-tabs"
onChange={handleTabChange}
value={selectedView}
>
{tabVisibility.showMetrics && (
<Radio.Button
className={
selectedView === VIEW_TYPES.METRICS ? 'selected_view tab' : 'tab'
}
value={VIEW_TYPES.METRICS}
>
<div className="view-title">
<BarChart size={14} />
Metrics
</div>
</Radio.Button>
)}
{tabVisibility.showLogs && (
<Radio.Button
className={
selectedView === VIEW_TYPES.LOGS ? 'selected_view tab' : 'tab'
}
value={VIEW_TYPES.LOGS}
>
<div className="view-title">
<ScrollText size={14} />
Logs
</div>
</Radio.Button>
)}
{tabVisibility.showTraces && (
<Radio.Button
className={
selectedView === VIEW_TYPES.TRACES ? 'selected_view tab' : 'tab'
}
value={VIEW_TYPES.TRACES}
>
<div className="view-title">
<DraftingCompass size={14} />
Traces
</div>
</Radio.Button>
)}
{tabVisibility.showEvents && (
<Radio.Button
className={
selectedView === VIEW_TYPES.EVENTS ? 'selected_view tab' : 'tab'
}
value={VIEW_TYPES.EVENTS}
>
<div className="view-title">
<ChevronsLeftRight size={14} />
Events
</div>
</Radio.Button>
)}
{tabVisibility.showContainers && (
<Radio.Button
className={
selectedView === VIEW_TYPES.CONTAINERS ? 'selected_view tab' : 'tab'
}
value={VIEW_TYPES.CONTAINERS}
>
<div className="view-title">
<Package2 size={14} />
Containers
</div>
</Radio.Button>
)}
{tabVisibility.showProcesses && (
<Radio.Button
className={
selectedView === VIEW_TYPES.PROCESSES ? 'selected_view tab' : 'tab'
}
value={VIEW_TYPES.PROCESSES}
>
<div className="view-title">
<ChevronsLeftRight size={14} />
Processes
</div>
</Radio.Button>
)}
{customTabs?.map((tab) => (
<Radio.Button
key={tab.key}
className={selectedView === tab.key ? 'selected_view tab' : 'tab'}
value={tab.key}
>
<div className="view-title">
{tab.icon}
{tab.label}
</div>
</Radio.Button>
))}
</Radio.Group>
items={[
...(tabVisibility.showMetrics
? [
{
value: VIEW_TYPES.METRICS,
label: (
<div className="view-title">
<BarChart size={14} />
Metrics
</div>
),
},
]
: []),
...(tabVisibility.showLogs
? [
{
value: VIEW_TYPES.LOGS,
label: (
<div className="view-title">
<ScrollText size={14} />
Logs
</div>
),
},
]
: []),
...(tabVisibility.showTraces
? [
{
value: VIEW_TYPES.TRACES,
label: (
<div className="view-title">
<DraftingCompass size={14} />
Traces
</div>
),
},
]
: []),
...(tabVisibility.showEvents
? [
{
value: VIEW_TYPES.EVENTS,
label: (
<div className="view-title">
<ChevronsLeftRight size={14} />
Events
</div>
),
},
]
: []),
...(tabVisibility.showContainers
? [
{
value: VIEW_TYPES.CONTAINERS,
label: (
<div className="view-title">
<Package2 size={14} />
Containers
</div>
),
},
]
: []),
...(tabVisibility.showProcesses
? [
{
value: VIEW_TYPES.PROCESSES,
label: (
<div className="view-title">
<ChevronsLeftRight size={14} />
Processes
</div>
),
},
]
: []),
...(customTabs?.map((tab) => ({
value: tab.key,
label: (
<div className="view-title">
{tab.icon}
{tab.label}
</div>
),
})) ?? []),
]}
/>
{selectedView === VIEW_TYPES.LOGS && (
<Tooltip title="Go to Logs Explorer" placement="left">

View File

@@ -152,23 +152,23 @@
font-weight: var(--font-weight-normal);
}
.tab {
> button {
border: 1px solid var(--l1-border);
width: 114px;
}
.tab::before {
background: var(--l1-border);
}
&::before {
background: var(--l1-border);
}
.selected_view {
background: var(--l1-border);
color: var(--l1-foreground);
border: 1px solid var(--l1-border);
}
&[data-state='on'] {
background: var(--l1-border);
color: var(--l1-foreground);
border: 1px solid var(--l1-border);
.selected_view::before {
background: var(--l1-border);
&::before {
background: var(--l1-border);
}
}
}
}

View File

@@ -1,5 +1,5 @@
import { Dispatch, SetStateAction } from 'react';
import { Checkbox } from 'antd';
import { Checkbox } from '@signozhq/ui/checkbox';
import { useRegionSelection } from 'hooks/integration/aws/useRegionSelection';
import { regions } from 'utils/regions';
@@ -21,18 +21,18 @@ export function RegionSelector({
setIncludeAllRegions,
});
const allSelected =
allRegionIds.length > 0 &&
allRegionIds.every((regionId) => selectedRegions.includes(regionId));
const someSelected =
selectedRegions.length > 0 && selectedRegions.length < allRegionIds.length;
return (
<div className="region-selector">
<div className="select-all">
<Checkbox
checked={
allRegionIds.length > 0 &&
allRegionIds.every((regionId) => selectedRegions.includes(regionId))
}
indeterminate={
selectedRegions.length > 0 && selectedRegions.length < allRegionIds.length
}
onChange={(e): void => handleSelectAll(e.target.checked)}
value={allSelected ? true : someSelected ? 'indeterminate' : false}
onChange={(checked): void => handleSelectAll(checked === true)}
>
Select All Regions
</Checkbox>
@@ -45,7 +45,7 @@ export function RegionSelector({
{region.subRegions.map((subRegion) => (
<Checkbox
key={subRegion.id}
checked={selectedRegions.includes(subRegion.id)}
value={selectedRegions.includes(subRegion.id)}
onChange={(): void => handleRegionSelect(subRegion.id)}
>
{subRegion.name}

View File

@@ -1,6 +1,7 @@
import React, { useCallback, useState } from 'react';
import { Plus } from '@signozhq/icons';
import { Button, Divider, Flex } from 'antd';
import { Button, Flex } from 'antd';
import { Divider } from '@signozhq/ui/divider';
import { Typography } from '@signozhq/ui/typography';
import logEvent from 'api/common/logEvent';
import ROUTES from 'constants/routes';

View File

@@ -3,7 +3,8 @@ import { useTranslation } from 'react-i18next';
import { UseQueryResult } from 'react-query';
import { Button, Flex, Input } from 'antd';
import { Typography } from '@signozhq/ui/typography';
import { Plus } from '@signozhq/icons';
import { Ellipsis, Plus } from '@signozhq/icons';
import { DropdownMenuSimple } from '@signozhq/ui/dropdown-menu';
import type { ColumnsType } from 'antd/es/table/interface';
import logEvent from 'api/common/logEvent';
import { convertToApiError } from 'api/ErrorResponseHandlerForGeneratedAPIs';
@@ -15,7 +16,6 @@ import type {
} from 'api/generated/services/sigNoz.schemas';
import type { ErrorType } from 'api/generatedAPIInstance';
import { AxiosError } from 'axios';
import DropDown from 'components/DropDown/DropDown';
import {
DynamicColumnsKey,
TableDataSource,
@@ -323,55 +323,67 @@ function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element {
dataIndex: 'id',
key: 'action',
width: 10,
render: (id: RuletypesRuleDTO['id'], record): JSX.Element => (
<div data-testid="alert-actions">
<DropDown
onDropDownItemClick={(item): void =>
alertActionLogEvent(item.key, record)
render: (id: RuletypesRuleDTO['id'], record): JSX.Element => {
const actionItems = [
<ToggleAlertState
key="1"
disabled={record.disabled ?? false}
setData={setData}
id={id ?? ''}
/>,
<ColumnButton
key="2"
onClick={(e: React.MouseEvent): void =>
onEditHandler(record, { newTab: isModifierKeyPressed(e) })
}
element={[
<ToggleAlertState
key="1"
disabled={record.disabled ?? false}
setData={setData}
id={id ?? ''}
/>,
<ColumnButton
key="2"
onClick={(e: React.MouseEvent): void =>
onEditHandler(record, { newTab: isModifierKeyPressed(e) })
}
type="link"
loading={editLoader}
>
Edit
</ColumnButton>,
<ColumnButton
key="3-new-tab"
onClick={(): void => onEditHandler(record, { newTab: true })}
type="link"
loading={editLoader}
>
Edit in New Tab
</ColumnButton>,
<ColumnButton
key="3-clone"
onClick={onCloneHandler(record)}
type="link"
loading={cloneLoader}
>
Clone
</ColumnButton>,
<DeleteAlert
key="4"
notifications={notificationsApi}
setData={setData}
id={id ?? ''}
/>,
];
return (
<div data-testid="alert-actions">
<DropdownMenuSimple
menu={{
items: actionItems.map((element, index) => ({
key: String(index),
label: element,
onClick: ({ key }): void => alertActionLogEvent(key, record),
})),
}}
>
<Button
type="link"
loading={editLoader}
>
Edit
</ColumnButton>,
<ColumnButton
key="3"
onClick={(): void => onEditHandler(record, { newTab: true })}
type="link"
loading={editLoader}
>
Edit in New Tab
</ColumnButton>,
<ColumnButton
key="3"
onClick={onCloneHandler(record)}
type="link"
loading={cloneLoader}
>
Clone
</ColumnButton>,
<DeleteAlert
key="4"
notifications={notificationsApi}
setData={setData}
id={id ?? ''}
/>,
]}
/>
</div>
),
style={{ color: 'var(--l1-foreground)' }}
icon={<Ellipsis size={16} />}
/>
</DropdownMenuSimple>
</div>
);
},
});
}

View File

@@ -12,12 +12,11 @@ import { useTranslation } from 'react-i18next';
import { generatePath } from 'react-router-dom';
import { useCopyToClipboard } from 'react-use';
import { Color } from '@signozhq/design-tokens';
import { DropdownMenuSimple, type MenuItem } from '@signozhq/ui/dropdown-menu';
import {
Button,
Dropdown,
Flex,
Input,
MenuProps,
Modal,
Popover,
Skeleton,
@@ -553,7 +552,7 @@ function DashboardsList(): JSX.Element {
];
const getCreateDashboardItems = useMemo(() => {
const menuItems: MenuProps['items'] = [
const menuItems: MenuItem[] = [
{
label: (
<div
@@ -711,11 +710,11 @@ function DashboardsList(): JSX.Element {
{createNewDashboard && (
<section className="actions">
<Dropdown
overlayClassName="new-dashboard-menu"
<DropdownMenuSimple
className="new-dashboard-menu"
menu={{ items: getCreateDashboardItems }}
placement="bottomRight"
trigger={['click']}
side="bottom"
align="end"
>
<Button
type="text"
@@ -727,7 +726,7 @@ function DashboardsList(): JSX.Element {
>
New Dashboard
</Button>
</Dropdown>
</DropdownMenuSimple>
<Button
type="text"
className="learn-more"
@@ -756,11 +755,11 @@ function DashboardsList(): JSX.Element {
onChange={handleSearch}
/>
{createNewDashboard && (
<Dropdown
overlayClassName="new-dashboard-menu"
<DropdownMenuSimple
className="new-dashboard-menu"
menu={{ items: getCreateDashboardItems }}
placement="bottomRight"
trigger={['click']}
side="bottom"
align="end"
>
<Button
type="primary"
@@ -773,7 +772,7 @@ function DashboardsList(): JSX.Element {
>
New dashboard
</Button>
</Dropdown>
</DropdownMenuSimple>
)}
</div>

View File

@@ -1,7 +1,8 @@
import { memo, useMemo } from 'react';
// eslint-disable-next-line no-restricted-imports
import { useDispatch, useSelector } from 'react-redux';
import { Button, Divider, Flex } from 'antd';
import { Button, Flex } from 'antd';
import { Divider } from '@signozhq/ui/divider';
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
import Controls from 'container/Controls';
import Download from 'container/Download/Download';

View File

@@ -2,7 +2,13 @@ import { useCallback } from 'react';
import { useCopyToClipboard } from 'react-use';
import { orange } from '@ant-design/colors';
import { Settings } from '@signozhq/icons';
import { Dropdown, MenuProps } from 'antd';
import {
type BaseMenuItem,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '@signozhq/ui/dropdown-menu';
import {
negateOperator,
OPERATORS,
@@ -135,41 +141,38 @@ function BodyTitleRenderer({
viewName,
]);
const onClickHandler: MenuProps['onClick'] = (props): void => {
const onClickHandler = (key: string): void => {
const mapper = {
[DROPDOWN_KEY.FILTER_IN]: filterHandler(true),
[DROPDOWN_KEY.FILTER_OUT]: filterHandler(false),
[DROPDOWN_KEY.GROUP_BY]: groupByHandler,
};
const handler = mapper[props.key];
const handler = mapper[key];
if (handler) {
handler();
}
};
const menu: MenuProps = {
items: [
{
key: DROPDOWN_KEY.FILTER_IN,
label: `Filter for ${value}`,
},
{
key: DROPDOWN_KEY.FILTER_OUT,
label: `Filter out ${value}`,
},
...(isGroupBySupported
? [
{
key: DROPDOWN_KEY.GROUP_BY,
label: `Group by ${nodeKey}`,
},
]
: []),
],
onClick: onClickHandler,
};
const menuItems: BaseMenuItem[] = [
{
key: DROPDOWN_KEY.FILTER_IN,
label: `Filter for ${value}`,
},
{
key: DROPDOWN_KEY.FILTER_OUT,
label: `Filter out ${value}`,
},
...(isGroupBySupported
? [
{
key: DROPDOWN_KEY.GROUP_BY,
label: `Group by ${nodeKey}`,
},
]
: []),
];
const handleNodeClick = useCallback(
(e: React.MouseEvent): void => {
@@ -218,15 +221,23 @@ function BodyTitleRenderer({
}}
onMouseDown={(e): void => e.preventDefault()}
>
<Dropdown
menu={menu}
trigger={['click']}
dropdownRender={(originNode): React.ReactNode => (
<div data-log-detail-ignore="true">{originNode}</div>
)}
>
<Settings style={{ marginRight: 8 }} className="hover-reveal" />
</Dropdown>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Settings style={{ marginRight: 8 }} className="hover-reveal" />
</DropdownMenuTrigger>
<DropdownMenuContent>
<div data-log-detail-ignore="true">
{menuItems.map((item) => (
<DropdownMenuItem
key={item.key}
onSelect={(): void => onClickHandler(item.key as string)}
>
{item.label}
</DropdownMenuItem>
))}
</div>
</DropdownMenuContent>
</DropdownMenu>
</span>
)}
{title.toString()}{' '}

View File

@@ -1,4 +1,4 @@
import { Divider } from 'antd';
import { Divider } from '@signozhq/ui/divider';
import { TooltipSimple } from '@signozhq/ui/tooltip';
import { Typography } from '@signozhq/ui/typography';

View File

@@ -1,6 +1,5 @@
import { useMemo, useState } from 'react';
import { Empty } from 'antd';
import type { RadioChangeEvent } from 'antd/lib';
import SignozRadioGroup from 'components/SignozRadioGroup/SignozRadioGroup';
import { History, Table } from '@signozhq/icons';
import { DataSource } from 'types/common/queryBuilder';
@@ -60,8 +59,8 @@ function InfraMetrics({
return options;
}, [podName]);
const handleModeChange = (e: RadioChangeEvent): void => {
setSelectedView(e.target.value);
const handleModeChange = (value: string): void => {
setSelectedView(value);
};
if (!podName && !nodeName && !hostName) {

View File

@@ -3,7 +3,8 @@ import MEditor, { EditorProps, Monaco } from '@monaco-editor/react';
import { Color } from '@signozhq/design-tokens';
import { Button } from '@signozhq/ui/button';
import { Switch } from '@signozhq/ui/switch';
import { Collapse, Divider, Input, Tag } from 'antd';
import { Collapse, Input, Tag } from 'antd';
import { Divider } from '@signozhq/ui/divider';
import { Typography } from '@signozhq/ui/typography';
import { AddToQueryHOCProps } from 'components/Logs/AddToQueryHOC';
import { ChangeViewFunctionType } from 'container/ExplorerOptions/types';

View File

@@ -2,9 +2,8 @@ import { useCallback, useEffect, useMemo, useState } from 'react';
import { useHistory } from 'react-router-dom';
import { Check, ChevronDown, Plus } from '@signozhq/icons';
import { Button } from '@signozhq/ui/button';
import { DropdownMenuSimple, type MenuItem } from '@signozhq/ui/dropdown-menu';
import { Input } from '@signozhq/ui/input';
import type { MenuProps } from 'antd';
import { Dropdown } from 'antd';
import { useListUsers } from 'api/generated/services/users';
import EditMemberDrawer from 'components/EditMemberDrawer/EditMemberDrawer';
import InviteMembersModal from 'components/InviteMembersModal/InviteMembersModal';
@@ -95,7 +94,7 @@ function MembersSettings(): JSX.Element {
).length;
const totalCount = allMembers.length;
const filterMenuItems: MenuProps['items'] = [
const filterMenuItems: MenuItem[] = [
{
key: FilterMode.All,
label: (
@@ -171,10 +170,9 @@ function MembersSettings(): JSX.Element {
</div>
<div className="members-settings__controls">
<Dropdown
<DropdownMenuSimple
menu={{ items: filterMenuItems }}
trigger={['click']}
overlayClassName="members-filter-dropdown"
className="members-filter-dropdown"
>
<Button
variant="solid"
@@ -184,7 +182,7 @@ function MembersSettings(): JSX.Element {
<span>{filterLabel}</span>
<ChevronDown size={12} className="members-filter-trigger__chevron" />
</Button>
</Dropdown>
</DropdownMenuSimple>
<div className="members-settings__search">
<Input

View File

@@ -1,4 +1,5 @@
import type { TypesUserDTO } from 'api/generated/services/sigNoz.schemas';
import userEvent from '@testing-library/user-event';
import { rest, server } from 'mocks-server/server';
import { fireEvent, render, screen } from 'tests/test-utils';
@@ -76,14 +77,15 @@ describe('MembersSettings (integration)', () => {
});
it('filters to pending invites via the filter dropdown', async () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
render(<MembersSettings />);
await screen.findByText('Alice Smith');
fireEvent.click(screen.getByRole('button', { name: /all members/i }));
await user.click(screen.getByRole('button', { name: /all members/i }));
const pendingOption = await screen.findByText(/pending invites/i);
fireEvent.click(pendingOption);
await user.click(pendingOption);
await screen.findByText('charlie@signoz.io');
expect(screen.queryByText('Alice Smith')).not.toBeInTheDocument();

View File

@@ -1,7 +1,8 @@
import { useMemo } from 'react';
import { generatePath } from 'react-router-dom';
import { Color } from '@signozhq/design-tokens';
import { Dropdown, Skeleton } from 'antd';
import { DropdownMenuSimple } from '@signozhq/ui/dropdown-menu';
import { Skeleton } from 'antd';
import { Typography } from '@signozhq/ui/typography';
import {
useGetMetricAlerts,
@@ -126,12 +127,11 @@ function DashboardsAndAlertsPopover({
return (
<div className="dashboards-and-alerts-popover-container">
{dashboardsPopoverContent && (
<Dropdown
<DropdownMenuSimple
menu={{
items: dashboardsPopoverContent,
}}
placement="bottomLeft"
trigger={['click']}
align="start"
>
<div
className="dashboards-and-alerts-popover dashboards-popover"
@@ -142,15 +142,14 @@ function DashboardsAndAlertsPopover({
{pluralize(dashboards.length, 'dashboard')}
</Typography.Text>
</div>
</Dropdown>
</DropdownMenuSimple>
)}
{alertsPopoverContent && (
<Dropdown
<DropdownMenuSimple
menu={{
items: alertsPopoverContent,
}}
placement="bottomLeft"
trigger={['click']}
align="start"
>
<div
className="dashboards-and-alerts-popover alerts-popover"
@@ -161,7 +160,7 @@ function DashboardsAndAlertsPopover({
{pluralize(alerts.length, 'alert rule')}
</Typography.Text>
</div>
</Dropdown>
</DropdownMenuSimple>
)}
</div>
);

View File

@@ -2,7 +2,8 @@ import { useCallback, useEffect, useMemo } from 'react';
// eslint-disable-next-line no-restricted-imports
import { useSelector } from 'react-redux';
import { Color } from '@signozhq/design-tokens';
import { Button, Divider, Drawer } from 'antd';
import { Button, Drawer } from 'antd';
import { Divider } from '@signozhq/ui/divider';
import { Typography } from '@signozhq/ui/typography';
import logEvent from 'api/common/logEvent';
import { useGetMetricMetadata } from 'api/generated/services/metrics';

View File

@@ -119,17 +119,17 @@ describe('MySettings Flows', () => {
it('Should have Dark theme selected by default', async () => {
const themeSelector = screen.getByTestId(THEME_SELECTOR_TEST_ID);
const darkOption = themeSelector.querySelector(
'input[value="dark"]',
) as HTMLInputElement;
const darkOption = within(themeSelector).getByRole('radio', {
name: /Dark/,
});
expect(darkOption).toBeChecked();
});
it('Should switch theme and log event when Light theme is selected', async () => {
const themeSelector = screen.getByTestId(THEME_SELECTOR_TEST_ID);
const lightOption = themeSelector.querySelector(
'input[value="light"]',
) as HTMLInputElement;
const lightOption = within(themeSelector).getByRole('radio', {
name: /Light/,
});
fireEvent.click(lightOption);

View File

@@ -1,7 +1,8 @@
import { useEffect, useState } from 'react';
import { useMutation } from 'react-query';
import { Radio, RadioChangeEvent, Tag } from 'antd';
import { Tag } from 'antd';
import { Switch } from '@signozhq/ui/switch';
import { ToggleGroupSimple } from '@signozhq/ui/toggle-group';
import setLocalStorageApi from 'api/browser/localstorage/set';
import logEvent from 'api/common/logEvent';
import updateUserPreference from 'api/v1/user/preferences/name/update';
@@ -88,7 +89,7 @@ function MySettings(): JSX.Element {
return isDarkMode ? 'dark' : 'light';
});
const handleThemeChange = ({ target: { value } }: RadioChangeEvent): void => {
const handleThemeChange = (value: string): void => {
logEvent('Account Settings: Theme Changed', {
theme: value,
});
@@ -187,14 +188,12 @@ function MySettings(): JSX.Element {
<div className="user-preference-section-content-item theme-selector">
<div className="user-preference-section-content-item-title-action">
Select your theme
<Radio.Group
options={themeOptions}
<ToggleGroupSimple
type="single"
onChange={handleThemeChange}
value={theme}
optionType="button"
buttonStyle="solid"
data-testid="theme-selector"
size="middle"
items={themeOptions}
/>
</div>

View File

@@ -1,4 +1,5 @@
import { Checkbox, Empty } from 'antd';
import { Empty } from 'antd';
import { Checkbox } from '@signozhq/ui/checkbox';
import { AxiosResponse } from 'axios';
import Spinner from 'components/Spinner';
import { EXCLUDED_COLUMNS } from 'container/OptionsMenu/constants';
@@ -50,9 +51,8 @@ function ExplorerAttributeColumns({
<div className="attribute-columns">
{filteredAttributeKeys.map((attributeKey: any) => (
<Checkbox
checked={isAttributeKeySelected(attributeKey.name)}
value={isAttributeKeySelected(attributeKey.name)}
onChange={(): void => handleCheckboxChange(attributeKey.name)}
style={{ padding: 0 }}
key={attributeKey.name}
>
{attributeKey.name}

View File

@@ -104,38 +104,33 @@
height: 200px;
background-color: var(--l1-border);
overflow: hidden !important;
.ant-dropdown-menu {
padding: 0;
.ant-dropdown-menu-item {
padding: 4px;
.ant-checkbox-wrapper {
padding: 2px 8px !important;
}
padding: 4px;
.ant-checkbox-wrapper {
padding: 2px 8px !important;
}
.attribute-columns {
display: flex;
flex-direction: column;
height: 160px;
overflow: scroll;
}
.attribute-columns {
display: flex;
flex-direction: column;
height: 160px;
overflow: scroll;
}
.attribute-columns::-webkit-scrollbar {
width: 3px; /* Width of the scrollbar */
}
.attribute-columns::-webkit-scrollbar {
width: 3px; /* Width of the scrollbar */
}
.attribute-columns::-webkit-scrollbar-track {
background: var(--l1-border); /* Color of the track */
}
.attribute-columns::-webkit-scrollbar-track {
background: var(--l1-border); /* Color of the track */
}
.attribute-columns::-webkit-scrollbar-thumb {
background: var(--l2-foreground); /* Color of the thumb */
border-radius: 4px; /* Roundness of the thumb */
}
.attribute-columns::-webkit-scrollbar-thumb {
background: var(--l2-foreground); /* Color of the thumb */
border-radius: 4px; /* Roundness of the thumb */
}
.attribute-columns::-webkit-scrollbar-thumb:hover {
background: var(--l1-border); /* Color of the thumb on hover */
}
}
.attribute-columns::-webkit-scrollbar-thumb:hover {
background: var(--l1-border); /* Color of the thumb on hover */
}
}

View File

@@ -7,7 +7,13 @@ import {
DropResult,
} from 'react-beautiful-dnd';
import { Color } from '@signozhq/design-tokens';
import { Button, Divider, Dropdown, Input, MenuProps, Tooltip } from 'antd';
import { Button, Input, Tooltip } from 'antd';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuTrigger,
} from '@signozhq/ui/dropdown-menu';
import { Divider } from '@signozhq/ui/divider';
import { Typography } from '@signozhq/ui/typography';
import { FieldDataType } from 'api/v5/v5';
import { SOMETHING_WENT_WRONG } from 'constants/api';
@@ -159,34 +165,12 @@ function ExplorerColumnsRenderer({
debouncedSetQuerySearchText(e.target.value);
};
const items: MenuProps['items'] = [
{
key: 'search',
label: (
<Input
type="text"
placeholder="Search"
className="explorer-columns-search"
value={searchText}
onChange={handleSearchChange}
prefix={<Search size={16} style={{ padding: '6px' }} />}
/>
),
},
{
key: 'columns',
label: (
<ExplorerAttributeColumns
isLoading={isLoading}
data={data}
searchText={searchText}
isAttributeKeySelected={isAttributeKeySelected}
handleCheckboxChange={handleCheckboxChange}
dataSource={initialDataSource}
/>
),
},
];
const handleOpenChange = (nextOpen: boolean): void => {
setOpen(nextOpen);
if (nextOpen) {
setSearchText('');
}
};
const removeSelectedLogField = (name: string): void => {
if (
@@ -238,13 +222,6 @@ function ExplorerColumnsRenderer({
}
};
const toggleDropdown = (): void => {
setOpen(!open);
if (!open) {
setSearchText('');
}
};
const isDarkMode = useIsDarkMode();
return (
@@ -327,25 +304,38 @@ function ExplorerColumnsRenderer({
</Droppable>
</DragDropContext>
<div>
<Dropdown
menu={{ items }}
arrow
placement="top"
open={open}
overlayClassName="explorer-columns-dropdown"
>
<Button
className="action-btn"
data-testid="add-columns-button"
icon={
<CirclePlus
size={16}
color={isDarkMode ? Color.BG_INK_400 : Color.BG_VANILLA_100}
/>
}
onClick={toggleDropdown}
/>
</Dropdown>
<DropdownMenu open={open} onOpenChange={handleOpenChange}>
<DropdownMenuTrigger asChild>
<Button
className="action-btn"
data-testid="add-columns-button"
icon={
<CirclePlus
size={16}
color={isDarkMode ? Color.BG_INK_400 : Color.BG_VANILLA_100}
/>
}
/>
</DropdownMenuTrigger>
<DropdownMenuContent side="top" className="explorer-columns-dropdown">
<Input
type="text"
placeholder="Search"
className="explorer-columns-search"
value={searchText}
onChange={handleSearchChange}
prefix={<Search size={16} style={{ padding: '6px' }} />}
/>
<ExplorerAttributeColumns
isLoading={isLoading}
data={data}
searchText={searchText}
isAttributeKeySelected={isAttributeKeySelected}
handleCheckboxChange={handleCheckboxChange}
dataSource={initialDataSource}
/>
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>
)}

View File

@@ -146,6 +146,7 @@ describe('ExplorerColumnsRenderer', () => {
});
it('opens and closes the dropdown', async () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
render(
<Wrapper>
<ExplorerColumnsRenderer
@@ -158,12 +159,12 @@ describe('ExplorerColumnsRenderer', () => {
);
const addButton = screen.getByTestId('add-columns-button');
await userEvent.click(addButton);
await user.click(addButton);
expect(screen.getByPlaceholderText('Search')).toBeInTheDocument();
expect(screen.getByText('attribute1')).toBeInTheDocument();
await userEvent.click(addButton);
await user.click(addButton);
await waitFor(() => {
expect(screen.queryByRole('menu')).not.toBeInTheDocument();
});

View File

@@ -13,7 +13,7 @@ import { Plus, Trash2 } from '@signozhq/icons';
import { ContextLinkProps, Widgets } from 'types/api/dashboard/getAll';
import { getBaseUrl } from 'utils/basePath';
import VariablesDropdown from './VariablesDropdown';
import VariablesPopover from './VariablesPopover';
import './UpdateContextLinks.styles.scss';
@@ -71,7 +71,7 @@ function UpdateContextLinks({
customVariables: fieldVariables,
});
// Transform variables into the format expected by VariablesDropdown
// Transform variables into the format expected by VariablesPopover
const transformedVariables = useMemo(
() => transformContextVariables(variables),
[variables],
@@ -224,7 +224,9 @@ function UpdateContextLinks({
},
]}
>
<VariablesDropdown
{/* TODO: replace with AutoComplete with options for variables and
previously used URLs for better UX */}
<VariablesPopover
onVariableSelect={handleVariableSelect}
variables={transformedVariables}
>
@@ -252,7 +254,7 @@ function UpdateContextLinks({
/>
</div>
)}
</VariablesDropdown>
</VariablesPopover>
</Form.Item>
{/* Remove the separate variables section */}
@@ -282,7 +284,7 @@ function UpdateContextLinks({
/>
</Col>
<Col span={16}>
<VariablesDropdown
<VariablesPopover
onVariableSelect={(variableName, cursorPosition): void =>
handleParamVariableSelect(index, variableName, cursorPosition)
}
@@ -311,7 +313,7 @@ function UpdateContextLinks({
}
/>
)}
</VariablesDropdown>
</VariablesPopover>
</Col>
<Col span={2}>
<Button

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