Compare commits

..

25 Commits

Author SHA1 Message Date
nityanandagohain
1eb3ef40e1 feat: add endpoint for fetching unpriced models 2026-06-17 21:21:28 +05:30
Swapnil Nakade
03796f012f chore: bumping agent version to v0.0.13 (#11757)
Some checks are pending
build-staging / prepare (push) Waiting to run
build-staging / js-build (push) Blocked by required conditions
build-staging / go-build (push) Blocked by required conditions
build-staging / staging (push) Blocked by required conditions
Release Drafter / update_release_draft (push) Waiting to run
2026-06-17 12:05:28 +00:00
Abhi kumar
a06900bbff chore: added fix for infinite query call on services page (#11755) 2026-06-17 11:17:17 +00:00
Nikhil Soni
b50933d622 chore(trace-details): remove flamegraph v2 API (#11629)
* chore: remove flamegraph v2 since we've moved to v4 now

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* chore: remove orphaned helpers after v2 waterfall and flamegraph removal

Drop GetSpansForTrace, cacheForTraceDetail, fluxIntervalForTraceDetail from
clickhouseReader — all were only used by the deleted v2 handlers. Also remove
the orphaned model.Span and model.Event types, and clean up the
cacheForTraceDetail initialization from server.go.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: update ee/query-service NewReader call after v2 cleanup

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* chore: format whitespacing

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-17 10:24:24 +00:00
Abhi kumar
8bf649a3a5 feat(dashboards-v2): panel rendering system (pure-V5 data path, renderers, panel chrome) (#11639)
* feat(dashboards-v2): panel type system & shared chart utilities

* feat(dashboards-v2): usePanelQuery data-fetching hook

* feat(dashboards-v2): TimeSeriesPanel renderer

* feat(dashboards-v2): BarChartPanel renderer

* feat(dashboards-v2): HistogramPanel renderer

* feat(dashboards-v2): PieChartPanel renderer

* feat(dashboards-v2): panel plugin registry

* feat(dashboards-v2): panel status popover (error/warning surfacing)

* feat(dashboards-v2): panel chrome - header, body, interactions & grid wiring

* chore(dashboards-v2): dashboards-list-v2 query-param sort/order adjustments

* feat(dashboards-v2): number panel kind + registry entry

* fix(dashboards-v2): cap bottom legend height at 30% of the panel

The bottom-legend reservation was capped only at min(2 rows, 80px) with
no relation to the container height, despite the helper's doc promising a
%-of-container cap. On short grid panels the legend kept its full slice
and the chart — the pie donut especially — collapsed to a sliver.

- calculateChartDimensions now also caps the bottom legend at 30% of the
  container height (mirroring the RIGHT-legend width cap).
- Drop the legend grid row-gap so the capped height isn't overrun.
- Add the previously-missing tests for calculateChartDimensions,
  including the short-container cap.

echo "--- commit 1 done ---" && git log --oneline -1
echo "--- remaining ---" && git status --short

* fix(dashboards-v2): prevent pie donut and labels from clipping

The donut used a flat radius = 0.35 * min(w,h) with leader labels anchored
at 1.3 * radius, leaving only ~0.045 * size between the label anchor and
the SVG edge — so on small panels the top/bottom labels (and a stray
container padding under border-box) clipped.

- getDonutGeometry now solves the radius back from the box half-extent
  minus a fixed label allowance, so the label anchor always lands a
  constant 22px inside the edge regardless of panel size — extracted as a
  pure, tested helper with a shared label-ratio constant (no drift with
  getArcGeometry) and a DonutGeometry type.
- Align the pie legend wrapper padding with ChartLayout's.
- Tests for getDonutGeometry (anchor stays in-box, derived radii, smaller
  axis wins, no negative radius).

echo "--- commit 2 done ---" && git log --oneline -3
echo "--- remaining ---" && git status --short

* refactor(dashboards-v2): address PR review feedback

Tightening + structure changes from review of #11639:

- types: make rendererProps `panel`/`data` required and `syncMode`
  non-optional (None is the off-state); `Panel.panel` required with
  orphan layout items guarded in SectionGrid; key the panel interaction
  map by PanelKind; require `requestPayload` in useGetQueryRangeV5.
- panel chrome: split the unknown-kind path into UnsupportedPanelBody so
  PanelBody only runs with a resolved renderer; rename chartBody ->
  chartContainer; move PanelHeader/PanelBody into their own folders with
  dedicated scss (Panel.module.scss trimmed to .panel).
- panel status: normalize query failures via convertToApiError (the
  generated client rejects with a raw AxiosError, so the old isAPIError
  guard never matched and dropped the backend code/message); drop an
  unnecessary useMemo; docs link -> Typography.Link.
- registry: PascalCase aliases; Panels/index.ts -> registry.ts.
- utils: extract selectionPreferences out of baseConfigBuilder; split
  chartAppearanceMappings into chartAppearance/{enumMaps,resolvers};
  move each kind's buildConfig under utils/.
- renderers: render NoData on an empty result set.
- dashboards-list: drop redundant sort/order casts.

echo "=== result ==="; git log --oneline -3; echo "--- working tree ---"; git status --short

* refactor(dashboards-v2): use @signozhq/ui tooltip for panel status

Replace the antd Popover in the panel header status indicator with
@signozhq/ui TooltipSimple. Relies on the global TooltipProvider mounted
in AppLayout; the PanelHeader test wraps renders in TooltipProvider since
the radix tooltip needs that ancestor in isolation.

* refactor(dashboards-v2): derive PanelKind from generated contract

Type panel kinds off DashboardtypesPanelPluginKindDTO instead of a
hand-written union, keeping the generated API contract as the single
source of truth. getPanelDefinition and PANEL_KIND_TO_PANEL_TYPE now take
PanelKind directly, dropping the `as PanelKind` casts at their call sites.

Renderers drop the defensive optional chaining / `?? {}` fallbacks the
render boundary already guarantees, and panelDef is renamed to
panelDefinition for clarity.

* chore: pr reviews fixes
2026-06-17 09:54:04 +00:00
Naman Verma
9150c2ac87 feat: introduce dashboard views for list dashboards page (#11465)
* feat: functional unique index in sql schema

* fix: only ascii in regex

* fix: use sync tags method in update

* fix: use sync tags method in update

* fix: correct the method name being called

* 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

* feat: delete dashboard v2 API

* chore: comment out tags unique index for now

* chore: add a todo comment

* chore: comment out unique index test

* feat: add created at to tag relations

* chore: comment out unique index test

* chore: bump migration number

* chore: remove uploaded grafana flag from metadata

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

* chore: revert idx generation to resolve conflicts

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

* fix: use binding package to get request

* chore: move NewDashboardV2 to NewDashboardV2WithoutTags

* chore: rename module to m

* fix: add ctx needed in sqlstore

* fix: remove sqlstore passage in ee pkg

* chore: change dashboardData to dashboardSpec

* feat: follow the metadata+spec key structure

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

* feat: v2 dashboard GET API (#11136)

* feat: v2 dashboard GET API

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

* chore: update api specs

* fix: remove soft delete references

* chore: embed StorableDashboard into joinedRow in store method

* fix: fix build error

* chore: revert all frontend changes

* fix: remove public dashboard from get v2 call

* chore: revert all frontend changes

* fix: fix build errors post merge conflict resolution

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

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

* chore: update api specs

* fix: use new pattern of checking for admin permission

* fix: remove soft delete reference

* chore: revert all frontend changes

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

* chore: use v1 methods wherever possible

* fix: use update v2 store method

* chore: update frontend schema

* chore: update frontend schema

* chore: generate api specs

* chore: generate api specs

* feat: patch dashboard api (#11182)

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

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

* feat: patch dashboard api

* chore: update api specs

* chore: update api specs

* chore: update api specs

* chore: remove delete related work

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

* test: unit test fixes

* fix: use new pattern of checking for admin permission

* fix: remove soft delete reference

* test: key value tags in test

* fix: build error in patch module method

* fix: build error in Apply method

* fix: use sync tags method in update

* fix: fix build errors

* fix: fix all patch application tests

* chore: add more mapper methods

* fix: fix build errors

* chore: generate api specs

* fix: update migration numbering

* fix: add missing request struct in list api

* fix: remove hasMore from list response

* chore: bump migration number

* fix: send total count in response + bug fixes

* fix: add source for v2 dashboards

* chore: incorporate source

* chore: incorporate source in api spec

* chore: incorporate source

* fix: remove system dashboards from list v2 response

* fix: add some required fields

* feat: add immutable name in dashboard v2

* feat: add immutable name in dashboard v2

* feat: add immutable name in dashboard v2 api specs

* fix: remove unused param in constructor

* fix: improve api descriptions

* fix: remove unneeded comment

* chore: increase MaxTagsPerDashboard to 10

* fix: set display name in unmarshal json

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

* feat: add validation on dashboard name

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

* chore: bump migration number

* chore: generate api specs

* fix: fix tests based on name related changes

* fix: dont include full data in list response

* fix: add quotes around tag relation kind

* chore: bump migration number

* fix: fix post merge build and spec errors

* fix: correct convertor method name

* test: add unit tests for type conversions

* chore: remove enum def of threshold comparison operator

* feat: add flag to generate unique name in backend

* chore: generate api specs

* chore: make tags required in postable

* fix: build error fix

* chore: bump migration number

* feat: introduce dashboard views for list dashboards page

* fix: fix build error in test after merge conflict

* fix: remove unused store method

* fix: remove unused module methods

* fix: use v1 store update method

* fix: change data to spec in api param description

* chore: add back accidentally removed tests

* chore: update api spec

* chore: bump migration number

* feat: delete dashboard v2 API (#11299)

* feat: delete dashboard v2 API

* fix: fix post merge build and spec errors

* fix: address review comments

* chore: generate frontend api spec

* fix: add missing name fetch in listv2 store method

* fix: change title to name in api description

* fix: add all error codes for new apis

* test: change data to spec in unit tests

* fix: remove join to public dashboard table in list call

* fix: use valuer string for list order and sort

* test: integration test and fixes found through it

* chore: use same jsonpatch package as done in zeus

* chore: remove JSONPatchDocument and use patchable everywhere

* fix: make remove idempotent in patch

* chore: separate file for patch types

* chore: better error passage

* fix: remove extra decodePatch calls

* fix: fix post merge build and schema errors

* fix: remove omitempty

* chore: bump migration number

* fix: remove user auditable

* chore: dont pass email to create and update methods

* chore: dont pass email to create and update methods

* chore: regenerate api specs

* chore: rearrange

* fix: return better err

* chore: add all err codes in api spec

* chore: rearrange

* fix: use must new org id

* fix: proper error passage

* chore: rename updateable to updatable

* fix: use must new org id

* fix: use must new org id

* feat: include list of all dashboard tags in list api response

* fix: remove wrong api description msg

* fix: use must method for user id as well

* chore: add nolint comment

* fix: add missing image field in list response

* chore: regenerate api specs

* chore: regenerate api specs

* fix: make GettableTag a defined type instead of an alias

* fix: dont allow system dashboards to be deleted

* fix: remove public filter from visitor

* chore: use go sqlbuilder

* fix: use ESCAPE literal in contains and like operators

* fix: use correct perses package in list v2 file

* feat: change pinned dashboard table to user dashboard preference table

* fix: delete preferences on dashboard delete

* test: add integration test for pinning

* fix: wrap naked errors

* fix: integration dashboards should not be deletable either

* fix: remove org column in preferences and add foreign key to users table

* chore: add fk from prefs to dashbaord table

* chore: remove outer parenthesis removal function

* test: add unit test to ensure that all reserved keys have handlers

* fix: proper url for pin apis

* fix: delete preferences on user deletion

* test: address integration test comments

* test: change limit

* fix: revert the check in can delete

* fix: remove unit test from ee package

* fix: move list filter to impl to avoid db impl logic in types

* chore: code movement

* feat: add a pin free list dashboards api

* fix: update api specs

* fix: use request query in api defs for list apis

* chore: explicitly mark request as nil in list apis

* fix: remove extra noop assignment

* chore: remove separate gettable view typedef

* chore: rearrange structs

* fix: cover errors, fix column type, add tests

* test: add unit tests for dashboard view validations

* fix: move view create/edit/delete to edit access

* fix: move view create/edit/delete to edit access

* fix: change url and fix api descriptions

* chore: add query max length check for views

* fix: wrong spelling for UpdatableDashboardView

* fix: extract common validation logic between list api and saved views

* test: add integration tests for dashboard views

* test: add similar tests for dashboards as well

* fix: dont trim name in views

* fix: add integration test for trailing whitespace rejection

* fix: return name required err for name with only spaces

* test: fix python formatting

* fix: generate api specs

---------

Co-authored-by: Srikanth Chekuri <srikanth.chekuri92@gmail.com>
2026-06-17 08:33:07 +00:00
Naman Verma
78df13dabd perf: reuse label maps and index series by variable in formulas (#11529)
* perf: reuse label maps and index series by variable in formulas

* perf: switch to simple worker pool

* perf: use a pointer free backing array for result values

---------

Co-authored-by: Srikanth Chekuri <srikanth.chekuri92@gmail.com>
2026-06-17 05:34:46 +00:00
Naman Verma
eb4b11295b feat: add clone dashboard API for perses dashboards (#11742)
Some checks failed
build-staging / prepare (push) Has been cancelled
build-staging / js-build (push) Has been cancelled
build-staging / go-build (push) Has been cancelled
build-staging / staging (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
* feat: add clone dashboard API for perses dashboards

* fix: allow integration dashboards to be cloned

* test: add integration test for cloning of integration dashboards

* test: fix lint in test

* test: revert the integration dashboard test
2026-06-17 01:35:07 +00:00
Yunus M
bdb0091c87 feat: use empty suggestions api for empty ui state of ai assistant (#11744)
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: use empty suggestions api for empty ui state of ai assistant

* feat: add useResolvePageType hook

* feat: update resolver to use all from DTO

* feat: update getAutoContexts and resolvePageType to omit auto-context for homepage and infra
2026-06-16 18:58:37 +00:00
Gaurav Tewari
5198530056 chore(traces): remove legacy trace view toggle and redirect /trace-old to /trace (#11732)
* chore: remove legacy trace view redirect

* refactor: review  changes

---------

Co-authored-by: Gaurav Tewari <tewarig@users.noreply.github.com>
2026-06-16 16:46:16 +00:00
Yunus M
6a4e694a34 fix: AI Assistant improvements (#11722)
* fix: wire open view and open channel flows

* feat: show current alert name in context in alert details page

* feat: links should open in new tab

* fix: show Noz in dashboard edit panel view

* fix: reduce size of fixed footer when assistant side panel is open

* feat: streamline error handling with resolveAssistantErrorMessage utility

* feat: hide suggestions and share in edit dashboard panel
2026-06-16 16:42:33 +00:00
swapnil-signoz
bd9a6cc17d chore: bump cloud integration agent version to v0.0.12 (#11741) 2026-06-16 16:15:12 +00:00
Nikhil Mantri
51f180453e feat(infra-monitoring): add default group by namespace name, cluster name for workloads. (#11716)
* chore: deployments -> add default namespace group by

* chore: added integration tests for statefulsets

* chore: namespace group by for jobs

* chore: namespace group by for daemonsets

* chore: added group by clustername for all workloads and integration tests for the same

* chore: fix py fmt for integration tests
2026-06-16 15:51:38 +00:00
Vinicius Lourenço
2ad5cb19c3 chore(sentry): bump packages to v10 & v5 for vite-plugin (#11723) 2026-06-16 15:34:48 +00:00
swapnil-signoz
5d711377ec feat: adding support for Azure DB services (#11679)
* feat: adding support for Azure SQL DB

* refactor: adding missing metrics in data collected and panels in dash

* feat: adding support for Azure SQL DB Managed Instances

* feat: generating openapi spec

* feat: adding nosql and cache services in azure integration
2026-06-16 14:20:49 +00:00
Vinicius Lourenço
c5d0fd8966 chore(oxc): bump oxfmt and oxlint/tsgolint to 0.54 and 1.69/0.23 (#11724) 2026-06-16 12:27:05 +00:00
Vinicius Lourenço
ed04ff09ff chore(css-modules): add good practices for writing css module files (#11587)
* chore(css-modules): add good practices for writing css module files

* fix(guide): add more guidelines based on notion doc

* fix(pr): address pr comments
2026-06-16 12:26:27 +00:00
Srikanth Chekuri
e1e9d516ac chore: add firing alert count and system/k8s metric existance status (#11730)
Some checks failed
build-staging / prepare (push) Has been cancelled
build-staging / js-build (push) Has been cancelled
build-staging / go-build (push) Has been cancelled
build-staging / staging (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
2026-06-16 10:28:22 +00:00
Pandey
58b55c922d fix(openapi): omit content type for responses without a body (#11720)
Some checks failed
build-staging / staging (push) Has been cancelled
build-staging / prepare (push) Has been cancelled
build-staging / js-build (push) Has been cancelled
build-staging / go-build (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
ServeOpenAPI's nil-response branch still passed WithContentType, so any route
with Response == nil but a ResponseContentType set (notably 204 No Content)
emitted a content block in the generated spec. Clients then try to decode an
empty body and fail — e.g. "unexpected end of JSON input" on
DELETE /api/v1/service_accounts/{id}.

Omit the content type when Response is nil. Regenerate docs/api/openapi.yml (18
bodyless responses drop their content block) and the frontend orval client.

Signed-off-by: grandwizard28 <vibhupandey28@gmail.com>
2026-06-15 13:07:16 +00:00
Tushar Vats
629ea3b8be feat: extend error responses with new error struct (#11542)
Some checks failed
build-staging / js-build (push) Has been cancelled
build-staging / prepare (push) Has been cancelled
build-staging / go-build (push) Has been cancelled
build-staging / staging (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
* feat: extend error responses with new error struct

* fix: enriched error for dashboard api

* fix: merge issues

* fix: reverted dashboards changes and add for cloud integrations

* fix: delete file

* fix: add back file

* fix: added a helper

* fix: removed invlaid referencess

* fix: generate openapi

* fix: keeping additional along with suggestion

* Revert "fix: keeping additional along with suggestion"

This reverts commit be30e2ffd2.

* fix: added suggestions per additonal error

* fix: generate openapi

* fix: remove valid references

* fix: removeg valid references for select and group by and only did you mean is kept

* fix: unit test

* fix: use binding for deconding for both ee and community

* fix: trim down suggestions methods

* fix: added renamed methods and moved stuff around

* fix: typo

* fix: removed json decoder

* fix: added empty check

* fix: retain addtional

* fix: reverted re-structing of file
2026-06-15 11:58:12 +00:00
Pandey
287b60cbe6 feat(statsreporter): expose collected stats via GET /api/v1/stats (#11698)
* feat(statsreporter): expose collected stats via GET /api/v1/stats

Extract per-org stats collection out of the analytics reporter into an
always-on Aggregator (collector fan-out + telemetry-store counts) shared
by the reporter and a new HTTP handler. The GET /api/v1/stats endpoint
returns the caller's org stats regardless of whether scheduled reporting
is enabled.

* refactor(statsreporter): collect telemetry stats via the querier

Move the trace/log/metric row-count and last-observed queries out of the
stats aggregator and into the querier, which now implements
statsreporter.StatsCollector. The aggregator becomes a pure collector
fan-out and no longer depends on telemetrystore; the querier is wired in
as one of the stats collectors.

* chore: regenerate openapi spec and frontend client

Backend docs/api/openapi.yml gains the GET /api/v1/stats (GetStats)
operation; the Orval client gains a useGetStats hook and GetStats200
type.

* chore: remove comment from querier Collect

* fix(statsreporter): use MustNewUUID for org from claims

Claims come from validated auth context, so the org UUID is guaranteed
valid; drop the dead NewUUID error branch.

* fix(flagger): use MustNewUUID for org from claims

Claims come from validated auth context, so the org UUID is guaranteed
valid; drop the dead NewUUID error branch.

* docs(contributing): note MustNewUUID for IDs from claims

* perf(querier): combine count and last-observed into one query per signal

Each signal's COUNT(*) and max(timestamp) scan the same table, so fetch
both in a single query — 3 queries instead of 6. Same emitted keys and
empty-table guard.
2026-06-15 11:27:17 +00:00
Ashwin Bhatkal
e206625e5f feat(dashboard-v2): public dashboard settings section (#11696)
* feat(dashboard-v2): add public dashboard settings section

Add a self-contained Public Dashboard settings section for the V2 dashboard
drawer, reusing the existing /dashboards/{id}/public endpoint via the generated
client (useGetPublicDashboard / useCreatePublicDashboard /
useUpdatePublicDashboard / useDeletePublicDashboard).

Broken into small components: a usePublicDashboard hook owning the query, the
create/update/delete mutations and the time-range form state; plus presentational
status, settings-form, URL, callout and actions pieces. Publishing a dashboard
exposes a shareable URL and lets the viewer default time range be configured.

* feat(dashboard-v2): wire public dashboard tab in settings drawer

Render the Public Dashboard section in the Publish tab in place of the
placeholder, and drop the now-unused SettingsTabPlaceholder util (the Publish tab
was its last consumer).

* fix: format failure
2026-06-15 10:59:56 +00:00
Nikhil Mantri
4f3b7647d3 feat(infra-monitoring): v2 daemonsets integration tests (#11442)
* chore: updated logic and use centralized function in the module

* chore: filter metric groups

* chore: filter metric groups

* chore: formula correction

* chore: added step flooring note

* chore: comment correction

* chore: comment correction

* chore: removed function

* chore: renamed variables

* chore: added happy test

* chore: added test 2 for accuracy and test 3 for missing metrics check

* chore: added filter test 4

* chore: added 5th test for filterByStatus

* chore: added group by tests

* chore: pagination test added

* chore: added validation tests

* chore: added auth test

* chore: added all tests

* chore: fix for surfacing meta for pods custom group by

* chore: added nodes integration test suite

* chore: namespaces integration tests

* ci: register inframonitoring suite + ruff format 01_hosts

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

* chore: added integration tests for clusters

* chore: formatting changed

* chore: formatting changed

* chore: formatting changed

* chore: added volumes integration tests

* chore: added deployments

* chore: added tests

* chore: added integration tests for jobs api

* chore: added integration tests for v2 daemonsets api

* chore: added order by host.name test

* refactor(infra-monitoring): address review comments on hosts integration tests

- inline _post helper at call sites
- combine filter operator tests into parametrized test_hosts_filter
- combine bad attr/grammar tests into parametrized test_hosts_filter_invalid
- convert orderby total-invariant nested loop to stacked parametrize
- drop redundant test_hosts_auth (auth covered globally)

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

* refactor(infra-monitoring): align pods integration tests with review feedback

- inline _post helper at call sites
- combine filter operator tests into parametrized test_pods_filter
- combine bad attr/grammar tests into parametrized test_pods_filter_invalid
- convert orderby total-invariant nested loop to stacked parametrize
- drop redundant test_pods_auth (auth covered globally)

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

* refactor(infra-monitoring): align nodes integration tests with review feedback

- inline _post helper at call sites
- combine filter operator tests into parametrized test_nodes_filter
- combine bad attr/grammar tests into parametrized test_nodes_filter_invalid
- convert orderby total-invariant nested loop to stacked parametrize
- drop redundant test_nodes_auth (auth covered globally)

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

* refactor(infra-monitoring): align namespaces integration tests with review feedback

- inline _post helper at call sites
- combine filter operator tests into parametrized test_namespaces_filter
- combine bad attr/grammar tests into parametrized test_namespaces_filter_invalid
- convert orderby total-invariant nested loop to stacked parametrize
- drop redundant test_namespaces_auth (auth covered globally)

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

* refactor(infra-monitoring): align clusters integration tests with review feedback

- inline _post helper at call sites
- combine filter operator tests into parametrized test_clusters_filter
- combine bad attr/grammar tests into parametrized test_clusters_filter_invalid
- convert orderby total-invariant nested loop to stacked parametrize
- drop redundant test_clusters_auth (auth covered globally)

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

* refactor(infra-monitoring): align volumes integration tests with review feedback

- inline _post helper at call sites
- combine filter operator tests into parametrized test_volumes_filter
- combine bad attr/grammar tests into parametrized test_volumes_filter_invalid
- convert orderby total-invariant nested loop to stacked parametrize
- drop redundant test_volumes_auth (auth covered globally)

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

* refactor(infra-monitoring): align deployments integration tests with review feedback

- inline _post helper at call sites
- combine filter operator tests into parametrized test_deployments_filter
- combine bad attr/grammar tests into parametrized test_deployments_filter_invalid
- convert orderby total-invariant nested loop to stacked parametrize
- drop redundant test_deployments_auth (auth covered globally)

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

* refactor(infra-monitoring): align statefulsets integration tests with review feedback

- inline _post helper at call sites
- combine filter operator tests into parametrized test_statefulsets_filter
- combine bad attr/grammar tests into parametrized test_statefulsets_filter_invalid
- convert orderby total-invariant nested loop to stacked parametrize
- drop redundant test_statefulsets_auth (auth covered globally)

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

* refactor(infra-monitoring): align jobs integration tests with review feedback

- inline _post helper at call sites
- combine filter operator tests into parametrized test_jobs_filter
- combine bad attr/grammar tests into parametrized test_jobs_filter_invalid
- convert orderby total-invariant nested loop to stacked parametrize
- drop redundant test_jobs_auth (auth covered globally)

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

* refactor(infra-monitoring): align daemonsets integration tests with review feedback

- inline _post helper at call sites
- combine filter operator tests into parametrized test_daemonsets_filter
- combine bad attr/grammar tests into parametrized test_daemonsets_filter_invalid
- convert orderby total-invariant nested loop to stacked parametrize
- drop redundant test_daemonsets_auth (auth covered globally)

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

* refactor(infra-monitoring): combine hosts groupby tests into parametrized test

- merge test_hosts_groupby_hostname + test_hosts_groupby_os_type into
  test_hosts_groupby parametrized on (dataset, group key, expected counts/values)
- preserves all assertions incl hostName populated-vs-empty branch coverage

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

* refactor(infra-monitoring): combine hosts orderby tests into parametrized test

- merge total_invariant_across_orderby + orderby_correctness + orderby_by_host_name
  into test_hosts_orderby parametrized on (column, record_field) x direction
- each case asserts both the total/len invariant and sortedness; sortedness now
  covered for all metric columns, not just cpu
- single dataset (hosts_orderby.jsonl) + CONTAINS 'order-' guard on all cases

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

* refactor(infra-monitoring): combine hosts pagination tests

- fold offset-beyond-total case into the test_hosts_pagination page walk
  (offset K+5 expects 0 records via max(0, min(limit, K-offset)); total
  invariant covers the beyond-total page's total == K)
- single seed instead of two

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

* refactor(infra-monitoring): merge hosts happy_path into test_hosts_accuracy

- happy_path and value_accuracy datasets were structurally identical
  (same 4 metrics, same sample counts, 2 hosts); one test now asserts
  shape/contract + exact metric values in a single seed/request
- drop unused hosts_happy_path.jsonl

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

* refactor(infra-monitoring): combine pods integration tests logically

- merge happy_path into test_pods_accuracy (datasets structurally identical);
  drop unused pods_happy_path.jsonl
- merge groupby namespace/deployment into parametrized test_pods_groupby
- merge orderby invariant + correctness + by-pod-name into test_pods_orderby
  ((column, record_field) x direction; sortedness now covered for all columns)
- fold offset-beyond-total into test_pods_pagination page walk

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

* refactor(infra-monitoring): combine nodes integration tests logically

- merge happy_path into test_nodes_accuracy; drop unused nodes_happy_path.jsonl
- merge orderby invariant + correctness + by-node-name into test_nodes_orderby
  ((column, record_field) x direction; sortedness now covered for all columns)
- fold offset-beyond-total into test_nodes_pagination page walk

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

* refactor(infra-monitoring): combine namespaces integration tests logically

- merge happy_path into test_namespaces_accuracy; drop unused namespaces_happy_path.jsonl
- merge orderby invariant + correctness + by-namespace-name into test_namespaces_orderby
- fold offset-beyond-total into test_namespaces_pagination page walk

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

* refactor(infra-monitoring): combine clusters integration tests logically

- merge happy_path into test_clusters_accuracy; drop unused clusters_happy_path.jsonl
- merge orderby invariant + correctness + by-cluster-name into test_clusters_orderby
- fold offset-beyond-total into test_clusters_pagination page walk

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

* refactor(infra-monitoring): combine volumes integration tests logically

- merge happy_path into test_volumes_accuracy; drop unused volumes_happy_path.jsonl
- merge orderby invariant + correctness + by-pvc-name into test_volumes_orderby
- fold offset-beyond-total into test_volumes_pagination page walk

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

* refactor(infra-monitoring): combine deployments integration tests logically

- merge happy_path into test_deployments_accuracy; drop unused deployments_happy_path.jsonl
- merge orderby invariant + correctness + by-deployment-name into test_deployments_orderby
- fold offset-beyond-total into test_deployments_pagination page walk

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

* refactor(infra-monitoring): combine statefulsets integration tests logically

- merge happy_path into test_statefulsets_accuracy; drop unused statefulsets_happy_path.jsonl
- merge orderby invariant + correctness + by-statefulset-name into test_statefulsets_orderby
- fold offset-beyond-total into test_statefulsets_pagination page walk

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

* refactor(infra-monitoring): combine jobs integration tests logically

- merge happy_path into test_jobs_accuracy; drop unused jobs_happy_path.jsonl
- merge orderby invariant + correctness + by-job-name into test_jobs_orderby
- fold offset-beyond-total into test_jobs_pagination page walk

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

* refactor(infra-monitoring): combine daemonsets integration tests logically

- merge happy_path into test_daemonsets_accuracy; drop unused daemonsets_happy_path.jsonl
- merge orderby invariant + correctness + by-daemonset-name into test_daemonsets_orderby
- fold offset-beyond-total into test_daemonsets_pagination page walk

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

* feat(infra-monitoring): assert metric value accuracy in hosts filter tests

- regenerate hosts_filter_dataset.jsonl so every host mirrors the acc-h1
  sample pattern from hosts_value_accuracy.jsonl (CI-proven expected values)
- test_hosts_filter now asserts cpu/memory/wait/load15/diskUsage per filtered
  record against FILTER_DATASET_EXPECTED (1e-9), proving filters don't
  distort aggregation

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

* refactor(infra-monitoring): scope filter expected values inside test_hosts_filter

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

* feat(infra-monitoring): assert metric value accuracy in pods filter tests

- regenerate pods_filter_dataset.jsonl so every pod mirrors the acc-p1
  sample pattern from pods_value_accuracy.jsonl (CI-proven expected values)
- test_pods_filter now asserts the 6 CPU/memory fields per filtered record

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

* feat(infra-monitoring): assert metric value accuracy in nodes filter tests

- regenerate nodes_filter_dataset.jsonl so every node mirrors the acc-n1
  sample pattern from nodes_value_accuracy.jsonl (CI-proven expected values)
- test_nodes_filter now asserts the 4 CPU/memory fields per filtered record

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

* feat(infra-monitoring): assert metric value accuracy in namespaces filter tests

- regenerate namespaces_filter_dataset.jsonl so every namespace mirrors the
  acc-ns-1 sample pattern (2 pods) from namespaces_value_accuracy.jsonl
- test_namespaces_filter now asserts namespaceCPU/namespaceMemory per record

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

* feat(infra-monitoring): assert metric value accuracy in clusters filter tests

- regenerate clusters_filter_dataset.jsonl so every cluster mirrors the
  acc-cluster-1 sample pattern (2 nodes) from clusters_value_accuracy.jsonl
- test_clusters_filter now asserts the 4 CPU/memory fields per filtered record

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

* feat(infra-monitoring): assert metric value accuracy in volumes filter tests

- regenerate volumes_filter_dataset.jsonl so every PVC mirrors the acc-pvc-1
  sample pattern from volumes_value_accuracy.jsonl (CI-proven expected values)
- test_volumes_filter now asserts all 6 volume fields per filtered record

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

* feat(infra-monitoring): assert metric value accuracy in deployments filter tests

- regenerate deployments_filter_dataset.jsonl so every deployment mirrors the
  acc-dep-1 sample pattern (2 pods) from deployments_value_accuracy.jsonl
- test_deployments_filter now asserts the 6 CPU/memory fields per filtered record

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

* feat(infra-monitoring): assert metric value accuracy in statefulsets filter tests

- regenerate statefulsets_filter_dataset.jsonl so every statefulset mirrors
  the acc-ss-1 sample pattern (2 pods) from statefulsets_value_accuracy.jsonl
- test_statefulsets_filter now asserts the 6 CPU/memory fields per filtered record

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

* feat(infra-monitoring): assert metric value accuracy in jobs filter tests

- regenerate jobs_filter_dataset.jsonl so every job mirrors the acc-job-1
  sample pattern (2 pods) from jobs_value_accuracy.jsonl
- test_jobs_filter now asserts the 6 CPU/memory fields per filtered record

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

* feat(infra-monitoring): assert metric value accuracy in daemonsets filter tests

- regenerate daemonsets_filter_dataset.jsonl so every daemonset mirrors the
  acc-ds-1 sample pattern (2 pods) from daemonsets_value_accuracy.jsonl
- test_daemonsets_filter now asserts the 6 CPU/memory fields per filtered record

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

* refactor(infra-monitoring): align nodes groupby + pod-phase tests with structure

- merge pod_phase_counts_list_mode + _no_pods_on_node into parametrized
  test_nodes_pod_phase_counts ((dataset, node, filter, expected) cases)
- rename groupby_cluster -> test_nodes_groupby, parametrize over
  k8s.node.name + k8s.cluster.name; adds the node-name-in-groupBy branch
  (nodeName populated, condition derived) that hosts/pods both cover

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

* refactor(infra-monitoring): align namespaces groupby with structure

- rename groupby_cluster -> test_namespaces_groupby, parametrize over
  k8s.namespace.name + k8s.cluster.name; adds the namespace-name-in-groupBy
  branch (namespaceName populated) that hosts/pods/nodes all cover

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

* refactor(infra-monitoring): align clusters groupby with structure

- rename groupby_cloud_provider -> test_clusters_groupby, parametrize over
  k8s.cluster.name + cloud.provider; adds the cluster-name-in-groupBy branch
  (clusterName populated) that hosts/pods/nodes/namespaces all cover

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

* refactor(infra-monitoring): align volumes groupby with structure

- rename groupby_namespace -> test_volumes_groupby, parametrize over
  k8s.persistentvolumeclaim.name + k8s.namespace.name; adds the
  pvc-name-in-groupBy branch (persistentVolumeClaimName populated) that the
  other endpoints all cover

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

* refactor(infra-monitoring): align deployments groupby with structure

- rename groupby_namespace -> test_deployments_groupby, parametrize over
  k8s.deployment.name + k8s.namespace.name; adds the deployment-name-in-groupBy
  branch (deploymentName populated) that the other endpoints all cover

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

* refactor(infra-monitoring): align statefulsets groupby with structure

- rename groupby_namespace -> test_statefulsets_groupby, parametrize over
  k8s.statefulset.name + k8s.namespace.name; adds the statefulset-name-in-groupBy
  branch (statefulSetName populated) that the other endpoints all cover

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

* refactor(infra-monitoring): align jobs groupby with structure

- rename groupby_namespace -> test_jobs_groupby, parametrize over
  k8s.job.name + k8s.namespace.name; adds the job-name-in-groupBy branch
  (jobName populated) that the other endpoints all cover

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

* refactor(infra-monitoring): align daemonsets groupby with structure

- rename groupby_namespace -> test_daemonsets_groupby, parametrize over
  k8s.daemonset.name + k8s.namespace.name; adds the daemonset-name-in-groupBy
  branch (daemonSetName populated) that the other endpoints all cover

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

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-15 06:51:47 +00:00
Ashwin Bhatkal
76ab0f25c0 feat(dashboard-v2): consolidate dashboard detail header, toolbar & settings (#11713)
* feat(dashboard-v2): consolidate dashboard page header & breadcrumbs

Replace DashboardHeader/DashboardBreadcrumbs with a single DashboardPageHeader
(title + image + breadcrumbs) under components/DashboardPageHeader.

* feat(dashboard-v2): consolidate dashboard detail toolbar

Fold the old DashboardDescription (title/meta) into a DashboardPageToolbar with
DashboardInfo (inline-editable title, tags, public/lock badges), DashboardActions
and the settings drawer; drop the separate DashboardMeta/DashboardTitle pieces.

* feat(dashboard-v2): rename General settings to Overview with shared settings card

Rename the General settings tab to Overview, extract DashboardInfoForm and a
reusable SegmentedControl, and restructure the settings drawer tabs onto the
shared settings-card layout. Publish tab stays a placeholder.

* feat(dashboard-v2): wire consolidated header & toolbar into the container

Render DashboardPageHeader + DashboardPageToolbar from DashboardContainer and
drop the removed header/description wiring.
2026-06-15 05:34:53 +00:00
Nikhil Mantri
59501ce4a7 feat(infra-monitoring): v2 jobs integration tests (#11441)
Some checks failed
build-staging / prepare (push) Has been cancelled
build-staging / js-build (push) Has been cancelled
build-staging / go-build (push) Has been cancelled
build-staging / staging (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
* chore: updated logic and use centralized function in the module

* chore: filter metric groups

* chore: filter metric groups

* chore: formula correction

* chore: added step flooring note

* chore: comment correction

* chore: comment correction

* chore: removed function

* chore: renamed variables

* chore: added happy test

* chore: added test 2 for accuracy and test 3 for missing metrics check

* chore: added filter test 4

* chore: added 5th test for filterByStatus

* chore: added group by tests

* chore: pagination test added

* chore: added validation tests

* chore: added auth test

* chore: added all tests

* chore: fix for surfacing meta for pods custom group by

* chore: added nodes integration test suite

* chore: namespaces integration tests

* ci: register inframonitoring suite + ruff format 01_hosts

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

* chore: added integration tests for clusters

* chore: formatting changed

* chore: formatting changed

* chore: formatting changed

* chore: added volumes integration tests

* chore: added deployments

* chore: added tests

* chore: added integration tests for jobs api

* chore: added order by host.name test

* refactor(infra-monitoring): address review comments on hosts integration tests

- inline _post helper at call sites
- combine filter operator tests into parametrized test_hosts_filter
- combine bad attr/grammar tests into parametrized test_hosts_filter_invalid
- convert orderby total-invariant nested loop to stacked parametrize
- drop redundant test_hosts_auth (auth covered globally)

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

* refactor(infra-monitoring): align pods integration tests with review feedback

- inline _post helper at call sites
- combine filter operator tests into parametrized test_pods_filter
- combine bad attr/grammar tests into parametrized test_pods_filter_invalid
- convert orderby total-invariant nested loop to stacked parametrize
- drop redundant test_pods_auth (auth covered globally)

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

* refactor(infra-monitoring): align nodes integration tests with review feedback

- inline _post helper at call sites
- combine filter operator tests into parametrized test_nodes_filter
- combine bad attr/grammar tests into parametrized test_nodes_filter_invalid
- convert orderby total-invariant nested loop to stacked parametrize
- drop redundant test_nodes_auth (auth covered globally)

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

* refactor(infra-monitoring): align namespaces integration tests with review feedback

- inline _post helper at call sites
- combine filter operator tests into parametrized test_namespaces_filter
- combine bad attr/grammar tests into parametrized test_namespaces_filter_invalid
- convert orderby total-invariant nested loop to stacked parametrize
- drop redundant test_namespaces_auth (auth covered globally)

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

* refactor(infra-monitoring): align clusters integration tests with review feedback

- inline _post helper at call sites
- combine filter operator tests into parametrized test_clusters_filter
- combine bad attr/grammar tests into parametrized test_clusters_filter_invalid
- convert orderby total-invariant nested loop to stacked parametrize
- drop redundant test_clusters_auth (auth covered globally)

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

* refactor(infra-monitoring): align volumes integration tests with review feedback

- inline _post helper at call sites
- combine filter operator tests into parametrized test_volumes_filter
- combine bad attr/grammar tests into parametrized test_volumes_filter_invalid
- convert orderby total-invariant nested loop to stacked parametrize
- drop redundant test_volumes_auth (auth covered globally)

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

* refactor(infra-monitoring): align deployments integration tests with review feedback

- inline _post helper at call sites
- combine filter operator tests into parametrized test_deployments_filter
- combine bad attr/grammar tests into parametrized test_deployments_filter_invalid
- convert orderby total-invariant nested loop to stacked parametrize
- drop redundant test_deployments_auth (auth covered globally)

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

* refactor(infra-monitoring): align statefulsets integration tests with review feedback

- inline _post helper at call sites
- combine filter operator tests into parametrized test_statefulsets_filter
- combine bad attr/grammar tests into parametrized test_statefulsets_filter_invalid
- convert orderby total-invariant nested loop to stacked parametrize
- drop redundant test_statefulsets_auth (auth covered globally)

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

* refactor(infra-monitoring): align jobs integration tests with review feedback

- inline _post helper at call sites
- combine filter operator tests into parametrized test_jobs_filter
- combine bad attr/grammar tests into parametrized test_jobs_filter_invalid
- convert orderby total-invariant nested loop to stacked parametrize
- drop redundant test_jobs_auth (auth covered globally)

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

* refactor(infra-monitoring): combine hosts groupby tests into parametrized test

- merge test_hosts_groupby_hostname + test_hosts_groupby_os_type into
  test_hosts_groupby parametrized on (dataset, group key, expected counts/values)
- preserves all assertions incl hostName populated-vs-empty branch coverage

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

* refactor(infra-monitoring): combine hosts orderby tests into parametrized test

- merge total_invariant_across_orderby + orderby_correctness + orderby_by_host_name
  into test_hosts_orderby parametrized on (column, record_field) x direction
- each case asserts both the total/len invariant and sortedness; sortedness now
  covered for all metric columns, not just cpu
- single dataset (hosts_orderby.jsonl) + CONTAINS 'order-' guard on all cases

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

* refactor(infra-monitoring): combine hosts pagination tests

- fold offset-beyond-total case into the test_hosts_pagination page walk
  (offset K+5 expects 0 records via max(0, min(limit, K-offset)); total
  invariant covers the beyond-total page's total == K)
- single seed instead of two

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

* refactor(infra-monitoring): merge hosts happy_path into test_hosts_accuracy

- happy_path and value_accuracy datasets were structurally identical
  (same 4 metrics, same sample counts, 2 hosts); one test now asserts
  shape/contract + exact metric values in a single seed/request
- drop unused hosts_happy_path.jsonl

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

* refactor(infra-monitoring): combine pods integration tests logically

- merge happy_path into test_pods_accuracy (datasets structurally identical);
  drop unused pods_happy_path.jsonl
- merge groupby namespace/deployment into parametrized test_pods_groupby
- merge orderby invariant + correctness + by-pod-name into test_pods_orderby
  ((column, record_field) x direction; sortedness now covered for all columns)
- fold offset-beyond-total into test_pods_pagination page walk

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

* refactor(infra-monitoring): combine nodes integration tests logically

- merge happy_path into test_nodes_accuracy; drop unused nodes_happy_path.jsonl
- merge orderby invariant + correctness + by-node-name into test_nodes_orderby
  ((column, record_field) x direction; sortedness now covered for all columns)
- fold offset-beyond-total into test_nodes_pagination page walk

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

* refactor(infra-monitoring): combine namespaces integration tests logically

- merge happy_path into test_namespaces_accuracy; drop unused namespaces_happy_path.jsonl
- merge orderby invariant + correctness + by-namespace-name into test_namespaces_orderby
- fold offset-beyond-total into test_namespaces_pagination page walk

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

* refactor(infra-monitoring): combine clusters integration tests logically

- merge happy_path into test_clusters_accuracy; drop unused clusters_happy_path.jsonl
- merge orderby invariant + correctness + by-cluster-name into test_clusters_orderby
- fold offset-beyond-total into test_clusters_pagination page walk

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

* refactor(infra-monitoring): combine volumes integration tests logically

- merge happy_path into test_volumes_accuracy; drop unused volumes_happy_path.jsonl
- merge orderby invariant + correctness + by-pvc-name into test_volumes_orderby
- fold offset-beyond-total into test_volumes_pagination page walk

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

* refactor(infra-monitoring): combine deployments integration tests logically

- merge happy_path into test_deployments_accuracy; drop unused deployments_happy_path.jsonl
- merge orderby invariant + correctness + by-deployment-name into test_deployments_orderby
- fold offset-beyond-total into test_deployments_pagination page walk

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

* refactor(infra-monitoring): combine statefulsets integration tests logically

- merge happy_path into test_statefulsets_accuracy; drop unused statefulsets_happy_path.jsonl
- merge orderby invariant + correctness + by-statefulset-name into test_statefulsets_orderby
- fold offset-beyond-total into test_statefulsets_pagination page walk

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

* refactor(infra-monitoring): combine jobs integration tests logically

- merge happy_path into test_jobs_accuracy; drop unused jobs_happy_path.jsonl
- merge orderby invariant + correctness + by-job-name into test_jobs_orderby
- fold offset-beyond-total into test_jobs_pagination page walk

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

* feat(infra-monitoring): assert metric value accuracy in hosts filter tests

- regenerate hosts_filter_dataset.jsonl so every host mirrors the acc-h1
  sample pattern from hosts_value_accuracy.jsonl (CI-proven expected values)
- test_hosts_filter now asserts cpu/memory/wait/load15/diskUsage per filtered
  record against FILTER_DATASET_EXPECTED (1e-9), proving filters don't
  distort aggregation

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

* refactor(infra-monitoring): scope filter expected values inside test_hosts_filter

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

* feat(infra-monitoring): assert metric value accuracy in pods filter tests

- regenerate pods_filter_dataset.jsonl so every pod mirrors the acc-p1
  sample pattern from pods_value_accuracy.jsonl (CI-proven expected values)
- test_pods_filter now asserts the 6 CPU/memory fields per filtered record

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

* feat(infra-monitoring): assert metric value accuracy in nodes filter tests

- regenerate nodes_filter_dataset.jsonl so every node mirrors the acc-n1
  sample pattern from nodes_value_accuracy.jsonl (CI-proven expected values)
- test_nodes_filter now asserts the 4 CPU/memory fields per filtered record

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

* feat(infra-monitoring): assert metric value accuracy in namespaces filter tests

- regenerate namespaces_filter_dataset.jsonl so every namespace mirrors the
  acc-ns-1 sample pattern (2 pods) from namespaces_value_accuracy.jsonl
- test_namespaces_filter now asserts namespaceCPU/namespaceMemory per record

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

* feat(infra-monitoring): assert metric value accuracy in clusters filter tests

- regenerate clusters_filter_dataset.jsonl so every cluster mirrors the
  acc-cluster-1 sample pattern (2 nodes) from clusters_value_accuracy.jsonl
- test_clusters_filter now asserts the 4 CPU/memory fields per filtered record

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

* feat(infra-monitoring): assert metric value accuracy in volumes filter tests

- regenerate volumes_filter_dataset.jsonl so every PVC mirrors the acc-pvc-1
  sample pattern from volumes_value_accuracy.jsonl (CI-proven expected values)
- test_volumes_filter now asserts all 6 volume fields per filtered record

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

* feat(infra-monitoring): assert metric value accuracy in deployments filter tests

- regenerate deployments_filter_dataset.jsonl so every deployment mirrors the
  acc-dep-1 sample pattern (2 pods) from deployments_value_accuracy.jsonl
- test_deployments_filter now asserts the 6 CPU/memory fields per filtered record

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

* feat(infra-monitoring): assert metric value accuracy in statefulsets filter tests

- regenerate statefulsets_filter_dataset.jsonl so every statefulset mirrors
  the acc-ss-1 sample pattern (2 pods) from statefulsets_value_accuracy.jsonl
- test_statefulsets_filter now asserts the 6 CPU/memory fields per filtered record

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

* feat(infra-monitoring): assert metric value accuracy in jobs filter tests

- regenerate jobs_filter_dataset.jsonl so every job mirrors the acc-job-1
  sample pattern (2 pods) from jobs_value_accuracy.jsonl
- test_jobs_filter now asserts the 6 CPU/memory fields per filtered record

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

* refactor(infra-monitoring): align nodes groupby + pod-phase tests with structure

- merge pod_phase_counts_list_mode + _no_pods_on_node into parametrized
  test_nodes_pod_phase_counts ((dataset, node, filter, expected) cases)
- rename groupby_cluster -> test_nodes_groupby, parametrize over
  k8s.node.name + k8s.cluster.name; adds the node-name-in-groupBy branch
  (nodeName populated, condition derived) that hosts/pods both cover

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

* refactor(infra-monitoring): align namespaces groupby with structure

- rename groupby_cluster -> test_namespaces_groupby, parametrize over
  k8s.namespace.name + k8s.cluster.name; adds the namespace-name-in-groupBy
  branch (namespaceName populated) that hosts/pods/nodes all cover

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

* refactor(infra-monitoring): align clusters groupby with structure

- rename groupby_cloud_provider -> test_clusters_groupby, parametrize over
  k8s.cluster.name + cloud.provider; adds the cluster-name-in-groupBy branch
  (clusterName populated) that hosts/pods/nodes/namespaces all cover

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

* refactor(infra-monitoring): align volumes groupby with structure

- rename groupby_namespace -> test_volumes_groupby, parametrize over
  k8s.persistentvolumeclaim.name + k8s.namespace.name; adds the
  pvc-name-in-groupBy branch (persistentVolumeClaimName populated) that the
  other endpoints all cover

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

* refactor(infra-monitoring): align deployments groupby with structure

- rename groupby_namespace -> test_deployments_groupby, parametrize over
  k8s.deployment.name + k8s.namespace.name; adds the deployment-name-in-groupBy
  branch (deploymentName populated) that the other endpoints all cover

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

* refactor(infra-monitoring): align statefulsets groupby with structure

- rename groupby_namespace -> test_statefulsets_groupby, parametrize over
  k8s.statefulset.name + k8s.namespace.name; adds the statefulset-name-in-groupBy
  branch (statefulSetName populated) that the other endpoints all cover

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

* refactor(infra-monitoring): align jobs groupby with structure

- rename groupby_namespace -> test_jobs_groupby, parametrize over
  k8s.job.name + k8s.namespace.name; adds the job-name-in-groupBy branch
  (jobName populated) that the other endpoints all cover

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

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-14 09:27:20 +00:00
374 changed files with 47451 additions and 5193 deletions

View File

@@ -39,7 +39,6 @@ jobs:
matrix:
suite:
- alerts
- alertmanager
- basepath
- callbackauthn
- cloudintegrations

View File

@@ -19,5 +19,8 @@
"editor.defaultFormatter": "vscode.html-language-features"
},
"python-envs.defaultEnvManager": "ms-python.python:system",
"python-envs.pythonProjects": []
"python-envs.pythonProjects": [],
"[json]": {
"editor.defaultFormatter": "vscode.json-language-features"
}
}

View File

@@ -1357,6 +1357,14 @@ components:
- appservice
- containerapp
- aks
- sqldatabase
- sqldatabasemi
- mysqlflexibleserver
- postgresqlflexibleserver
- mongodb
- cosmosdb
- cassandradb
- redis
type: string
CloudintegrationtypesServiceMetadata:
properties:
@@ -2583,6 +2591,41 @@ components:
- panels
- layouts
type: object
DashboardtypesDashboardView:
properties:
createdAt:
format: date-time
type: string
data:
$ref: '#/components/schemas/DashboardtypesDashboardViewData'
id:
type: string
name:
type: string
orgId:
type: string
updatedAt:
format: date-time
type: string
required:
- id
- name
- data
- orgId
type: object
DashboardtypesDashboardViewData:
properties:
order:
$ref: '#/components/schemas/DashboardtypesListOrder'
query:
type: string
sort:
$ref: '#/components/schemas/DashboardtypesListSort'
version:
type: string
required:
- version
type: object
DashboardtypesDatasourcePlugin:
discriminator:
mapping:
@@ -2867,6 +2910,15 @@ components:
- total
- tags
type: object
DashboardtypesListableDashboardView:
properties:
views:
items:
$ref: '#/components/schemas/DashboardtypesDashboardView'
type: array
required:
- views
type: object
DashboardtypesListedDashboardForUserV2:
properties:
createdAt:
@@ -3171,6 +3223,16 @@ components:
- tags
- spec
type: object
DashboardtypesPostableDashboardView:
properties:
data:
$ref: '#/components/schemas/DashboardtypesDashboardViewData'
name:
type: string
required:
- name
- data
type: object
DashboardtypesPostablePublicDashboard:
properties:
defaultTimeRange:
@@ -3566,10 +3628,6 @@ components:
items:
$ref: '#/components/schemas/ErrorsResponseerroradditional'
type: array
invalidReferences:
items:
type: string
type: array
message:
type: string
retry:
@@ -3590,6 +3648,10 @@ components:
properties:
message:
type: string
suggestions:
items:
type: string
type: array
type: object
ErrorsResponseretryjson:
properties:
@@ -4827,6 +4889,19 @@ components:
- offset
- limit
type: object
LlmpricingruletypesGettableUnmappedModels:
properties:
items:
items:
$ref: '#/components/schemas/LlmpricingruletypesUnmappedModel'
nullable: true
type: array
total:
type: integer
required:
- items
- total
type: object
LlmpricingruletypesLLMPricingCacheCosts:
properties:
mode:
@@ -4916,6 +4991,19 @@ components:
type: string
nullable: true
type: array
LlmpricingruletypesUnmappedModel:
properties:
modelName:
type: string
provider:
type: string
spanCount:
minimum: 0
type: integer
required:
- modelName
- spanCount
type: object
LlmpricingruletypesUpdatableLLMPricingRule:
properties:
enabled:
@@ -9004,10 +9092,6 @@ paths:
type: string
responses:
"204":
content:
application/json:
schema:
type: string
description: No Content
"401":
content:
@@ -9160,10 +9244,6 @@ paths:
$ref: '#/components/schemas/DashboardtypesUpdatablePublicDashboard'
responses:
"204":
content:
application/json:
schema:
type: string
description: No Content
"401":
content:
@@ -9758,10 +9838,6 @@ paths:
$ref: '#/components/schemas/Querybuildertypesv5QueryRangeRequest'
responses:
"200":
content:
application/json:
schema:
type: string
description: OK
"400":
content:
@@ -10401,6 +10477,60 @@ paths:
summary: Get a pricing rule
tags:
- llmpricingrules
/api/v1/llm_pricing_rules/unmapped_models:
get:
deprecated: false
description: Returns models seen in the last hour of trace data (gen_ai.request.model)
that no pricing rule pattern matches, so the user can add them to an existing
rule or create a new one.
operationId: ListUnmappedLLMModels
responses:
"200":
content:
application/json:
schema:
properties:
data:
$ref: '#/components/schemas/LlmpricingruletypesGettableUnmappedModels'
status:
type: string
required:
- status
- data
type: object
description: OK
"400":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Bad Request
"401":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Unauthorized
"403":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Forbidden
"500":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Internal Server Error
security:
- api_key:
- VIEWER
- tokenizer:
- VIEWER
summary: List unmapped models
tags:
- llmpricingrules
/api/v1/logs/promote_paths:
get:
deprecated: false
@@ -10946,10 +11076,6 @@ paths:
type: string
responses:
"204":
content:
application/json:
schema:
type: string
description: No Content
"401":
content:
@@ -11063,10 +11189,6 @@ paths:
$ref: '#/components/schemas/AuthtypesPatchableRole'
responses:
"204":
content:
application/json:
schema:
type: string
description: No Content
"401":
content:
@@ -11213,10 +11335,6 @@ paths:
$ref: '#/components/schemas/CoretypesPatchableObjects'
responses:
"204":
content:
application/json:
schema:
type: string
description: No Content
"400":
content:
@@ -11666,10 +11784,6 @@ paths:
type: string
responses:
"204":
content:
application/json:
schema:
type: string
description: No Content
"401":
content:
@@ -11777,10 +11891,6 @@ paths:
$ref: '#/components/schemas/ServiceaccounttypesPostableServiceAccount'
responses:
"204":
content:
application/json:
schema:
type: string
description: No Content
"400":
content:
@@ -11962,10 +12072,6 @@ paths:
type: string
responses:
"204":
content:
application/json:
schema:
type: string
description: No Content
"401":
content:
@@ -12023,10 +12129,6 @@ paths:
$ref: '#/components/schemas/ServiceaccounttypesUpdatableFactorAPIKey'
responses:
"204":
content:
application/json:
schema:
type: string
description: No Content
"400":
content:
@@ -12209,10 +12311,6 @@ paths:
type: string
responses:
"204":
content:
application/json:
schema:
type: string
description: No Content
"401":
content:
@@ -12288,10 +12386,6 @@ paths:
$ref: '#/components/schemas/ServiceaccounttypesPostableServiceAccount'
responses:
"204":
content:
application/json:
schema:
type: string
description: No Content
"404":
content:
@@ -12783,6 +12877,53 @@ paths:
summary: Update a span mapper
tags:
- spanmapper
/api/v1/stats:
get:
deprecated: false
description: This endpoint returns the collected stats for the organization
operationId: GetStats
responses:
"200":
content:
application/json:
schema:
properties:
data:
additionalProperties: {}
type: object
status:
type: string
required:
- status
- data
type: object
description: OK
"401":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Unauthorized
"403":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Forbidden
"500":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Internal Server Error
security:
- api_key:
- VIEWER
- tokenizer:
- VIEWER
summary: Get stats
tags:
- stats
/api/v1/testChannel:
post:
deprecated: true
@@ -13321,6 +13462,231 @@ paths:
summary: Update user preference
tags:
- preferences
/api/v2/dashboard_views:
get:
deprecated: false
description: Returns every saved view in the calling user's org. Saved views
are shared org-wide.
operationId: ListDashboardViews
responses:
"200":
content:
application/json:
schema:
properties:
data:
$ref: '#/components/schemas/DashboardtypesListableDashboardView'
status:
type: string
required:
- status
- data
type: object
description: OK
"401":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Unauthorized
"403":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Forbidden
"500":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Internal Server Error
security:
- api_key:
- VIEWER
- tokenizer:
- VIEWER
summary: List dashboard saved views
tags:
- dashboard
post:
deprecated: false
description: Persists the calling user's dashboard listing state (query, sort,
order) as a named, reusable view shared across the org.
operationId: CreateDashboardView
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/DashboardtypesPostableDashboardView'
responses:
"201":
content:
application/json:
schema:
properties:
data:
$ref: '#/components/schemas/DashboardtypesDashboardView'
status:
type: string
required:
- status
- data
type: object
description: Created
"400":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Bad Request
"401":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Unauthorized
"403":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Forbidden
"500":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Internal Server Error
security:
- api_key:
- EDITOR
- tokenizer:
- EDITOR
summary: Create dashboard saved view
tags:
- dashboard
/api/v2/dashboard_views/{id}:
delete:
deprecated: false
description: Removes a saved view. Saved views are shared org-wide. Deleting
a non-existent view returns 404.
operationId: DeleteDashboardView
parameters:
- in: path
name: id
required: true
schema:
type: string
responses:
"204":
description: No Content
"400":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Bad Request
"401":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Unauthorized
"403":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Forbidden
"404":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Not Found
"500":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Internal Server Error
security:
- api_key:
- EDITOR
- tokenizer:
- EDITOR
summary: Delete dashboard saved view
tags:
- dashboard
put:
deprecated: false
description: Replaces a saved view's name and data. Saved views are shared org-wide.
operationId: UpdateDashboardView
parameters:
- in: path
name: id
required: true
schema:
type: string
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/DashboardtypesPostableDashboardView'
responses:
"200":
content:
application/json:
schema:
properties:
data:
$ref: '#/components/schemas/DashboardtypesDashboardView'
status:
type: string
required:
- status
- data
type: object
description: OK
"400":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Bad Request
"401":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Unauthorized
"403":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Forbidden
"404":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Not Found
"500":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Internal Server Error
security:
- api_key:
- EDITOR
- tokenizer:
- EDITOR
summary: Update dashboard saved view
tags:
- dashboard
/api/v2/dashboards:
get:
deprecated: false
@@ -13469,10 +13835,6 @@ paths:
type: string
responses:
"204":
content:
application/json:
schema:
type: string
description: No Content
"400":
content:
@@ -13718,6 +14080,74 @@ paths:
summary: Update dashboard (v2)
tags:
- dashboard
/api/v2/dashboards/{id}/clone:
post:
deprecated: false
description: This endpoint clones an existing v2-shape dashboard. User and integration
dashboards can be cloned; system dashboards are rejected. The clone keeps
the source's display name, panels, and tags, but gets a freshly generated
unique internal name and is always created as an unlocked user dashboard owned
by the caller.
operationId: CloneDashboardV2
parameters:
- in: path
name: id
required: true
schema:
type: string
responses:
"201":
content:
application/json:
schema:
properties:
data:
$ref: '#/components/schemas/DashboardtypesGettableDashboardV2'
status:
type: string
required:
- status
- data
type: object
description: Created
"400":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Bad Request
"401":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Unauthorized
"403":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Forbidden
"404":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Not Found
"500":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Internal Server Error
security:
- api_key:
- EDITOR
- tokenizer:
- EDITOR
summary: Clone dashboard (v2)
tags:
- dashboard
/api/v2/dashboards/{id}/lock:
delete:
deprecated: false
@@ -13732,10 +14162,6 @@ paths:
type: string
responses:
"204":
content:
application/json:
schema:
type: string
description: No Content
"400":
content:
@@ -13788,10 +14214,6 @@ paths:
type: string
responses:
"204":
content:
application/json:
schema:
type: string
description: No Content
"400":
content:
@@ -15522,10 +15944,6 @@ paths:
$ref: '#/components/schemas/MetricsexplorertypesUpdateMetricMetadataRequest'
responses:
"200":
content:
application/json:
schema:
type: string
description: OK
"400":
content:
@@ -20824,10 +21242,6 @@ paths:
type: string
responses:
"204":
content:
application/json:
schema:
type: string
description: No Content
"400":
content:
@@ -20875,10 +21289,6 @@ paths:
type: string
responses:
"204":
content:
application/json:
schema:
type: string
description: No Content
"400":
content:

View File

@@ -109,6 +109,20 @@ func (h *handler) CreateThing(rw http.ResponseWriter, req *http.Request) {
}
```
When you need an ID from `claims` as a `valuer.UUID` (for example to pass it to a module), derive it with the `Must*` constructor instead of `NewUUID` plus an error check. Claims are validated by the auth middleware, so the conversion cannot fail and the error branch would be dead code:
```go
// Good — claims are pre-validated, the conversion cannot fail.
orgID := valuer.MustNewUUID(claims.OrgID)
// Avoid — the error path is unreachable.
orgID, err := valuer.NewUUID(claims.OrgID)
if err != nil {
render.Error(rw, err)
return
}
```
### 3. Register the handler in `signozapiserver`
In `pkg/apiserver/signozapiserver`, add a route in the appropriate `add*Routes` function (`addUserRoutes`, `addSessionRoutes`, `addOrgRoutes`, etc.). The pattern is:
@@ -387,3 +401,4 @@ Note the discriminator property lives in the variants, not on the parent — the
- **Add `nullable:"true"`** on fields that can be `null`. Pay special attention to slices and maps -- in Go these default to `nil` which serializes to `null`. If the field should always be an array, initialize it and do not mark it nullable.
- **Implement `Enum()`** on every type that has a fixed set of acceptable values so the JSON schema generates proper `enum` constraints.
- **Add request examples** via `RequestExamples` in `OpenAPIDef` for any non-trivial endpoint. See `pkg/apiserver/signozapiserver/querier.go` for reference.
- **Derive IDs from `claims` with `valuer.MustNewUUID`** (e.g. `claims.OrgID`, `claims.UserID`). Claims are pre-validated by the auth middleware, so use the `Must*` constructor — don't write `NewUUID` followed by an `if err != nil { render.Error(...); return }` block.

View File

@@ -217,6 +217,10 @@ func (module *module) CreateV2(ctx context.Context, orgID valuer.UUID, createdBy
return module.pkgDashboardModule.CreateV2(ctx, orgID, createdBy, creator, source, postable)
}
func (module *module) CloneV2(ctx context.Context, orgID valuer.UUID, createdBy string, creator valuer.UUID, id valuer.UUID) (*dashboardtypes.DashboardV2, error) {
return module.pkgDashboardModule.CloneV2(ctx, orgID, createdBy, creator, id)
}
func (module *module) GetV2(ctx context.Context, orgID valuer.UUID, id valuer.UUID) (*dashboardtypes.DashboardV2, error) {
return module.pkgDashboardModule.GetV2(ctx, orgID, id)
}
@@ -262,6 +266,22 @@ func (module *module) DeletePreferencesForUser(ctx context.Context, orgID valuer
return module.pkgDashboardModule.DeletePreferencesForUser(ctx, orgID, userID)
}
func (module *module) CreateView(ctx context.Context, orgID valuer.UUID, postable dashboardtypes.PostableDashboardView) (*dashboardtypes.DashboardView, error) {
return module.pkgDashboardModule.CreateView(ctx, orgID, postable)
}
func (module *module) ListViews(ctx context.Context, orgID valuer.UUID) (*dashboardtypes.ListableDashboardView, error) {
return module.pkgDashboardModule.ListViews(ctx, orgID)
}
func (module *module) UpdateView(ctx context.Context, orgID valuer.UUID, id valuer.UUID, updateable dashboardtypes.UpdatableDashboardView) (*dashboardtypes.DashboardView, error) {
return module.pkgDashboardModule.UpdateView(ctx, orgID, id, updateable)
}
func (module *module) DeleteView(ctx context.Context, orgID valuer.UUID, id valuer.UUID) error {
return module.pkgDashboardModule.DeleteView(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

@@ -3,13 +3,13 @@ package querier
import (
"bytes"
"context"
"encoding/json"
"io"
"net/http"
anomalyV2 "github.com/SigNoz/signoz/ee/anomaly"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/factory"
"github.com/SigNoz/signoz/pkg/http/binding"
"github.com/SigNoz/signoz/pkg/http/render"
"github.com/SigNoz/signoz/pkg/querier"
"github.com/SigNoz/signoz/pkg/types/authtypes"
@@ -48,8 +48,8 @@ func (h *handler) QueryRange(rw http.ResponseWriter, req *http.Request) {
}
var queryRangeRequest qbtypes.QueryRangeRequest
if err := json.NewDecoder(req.Body).Decode(&queryRangeRequest); err != nil {
render.Error(rw, errors.NewInvalidInputf(errors.CodeInvalidInput, "failed to decode request body: %v", err))
if err := binding.JSON.BindBody(req.Body, &queryRangeRequest); err != nil {
render.Error(rw, err)
return
}

View File

@@ -10,7 +10,6 @@ import (
"go.opentelemetry.io/contrib/instrumentation/github.com/gorilla/mux/otelmux"
"go.opentelemetry.io/otel/propagation"
"github.com/SigNoz/signoz/pkg/cache/memorycache"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/gorilla/handlers"
@@ -20,7 +19,6 @@ import (
"github.com/SigNoz/signoz/ee/query-service/app/api"
"github.com/SigNoz/signoz/ee/query-service/usage"
"github.com/SigNoz/signoz/pkg/cache"
"github.com/SigNoz/signoz/pkg/http/middleware"
"github.com/SigNoz/signoz/pkg/signoz"
"github.com/SigNoz/signoz/pkg/web"
@@ -59,25 +57,12 @@ type Server struct {
// NewServer creates and initializes Server
func NewServer(config signoz.Config, signoz *signoz.SigNoz) (*Server, error) {
cacheForTraceDetail, err := memorycache.New(context.TODO(), signoz.Instrumentation.ToProviderSettings(), cache.Config{
Provider: "memory",
Memory: cache.Memory{
NumCounters: 10 * 10000,
MaxCost: 1 << 27, // 128 MB
},
})
if err != nil {
return nil, err
}
reader := clickhouseReader.NewReader(
signoz.Instrumentation.Logger(),
signoz.SQLStore,
signoz.TelemetryStore,
signoz.Prometheus,
signoz.TelemetryStore.Cluster(),
config.Querier.FluxInterval,
cacheForTraceDetail,
signoz.Cache,
nil,
)

View File

@@ -291,6 +291,8 @@
// Prevents the usage of specific antd components in favor of our lib
"signoz/no-signozhq-ui-barrel": "error",
// Forces subpath imports (@signozhq/ui/<component>) instead of the eagerly-loaded barrel
"signoz/no-css-module-bracket-access": "warn",
// Prevents bracket access on CSS modules (styles['kebab-case']) which fails with camelCaseOnly config
"no-restricted-globals": [
"error",
{

View File

@@ -2,9 +2,33 @@
const path = require('path');
module.exports = {
plugins: [path.join(__dirname, 'stylelint-rules/no-unsupported-asset-url.js')],
plugins: [
path.join(__dirname, 'stylelint-rules/no-unsupported-asset-url.js'),
path.join(__dirname, 'stylelint-rules/css-modules/no-deep-nesting.js'),
path.join(__dirname, 'stylelint-rules/css-modules/no-id-selectors.js'),
path.join(
__dirname,
'stylelint-rules/css-modules/no-bare-element-selectors.js',
),
path.join(__dirname, 'stylelint-rules/css-modules/prefer-css-variables.js'),
path.join(__dirname, 'stylelint-rules/css-modules/class-name-pattern.js'),
],
customSyntax: 'postcss-scss',
rules: {
// Applies to all SCSS files
'local/no-unsupported-asset-url': true,
},
overrides: [
{
// CSS module-specific rules
files: ['**/*.module.scss'],
rules: {
'local/no-deep-nesting': [true, { severity: 'warning' }],
'local/no-id-selectors': true,
'local/no-bare-element-selectors': true,
'local/prefer-css-variables': [true, { severity: 'warning' }],
'local/class-name-pattern': [true, { severity: 'warning' }],
},
},
],
};

View File

@@ -23,6 +23,8 @@ You are operating within a constrained context window and strict system prompts.
- 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.
- When writing new css, prefer CSS Modules
- Use ./docs/css-modules-guide.md as reference on how to write good CSS Modules.
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`

View File

@@ -0,0 +1,513 @@
# CSS Modules Guide
## Checklist Before Committing
- [ ] All class names use camelCase in CSS
- [ ] State classes use `is-`/`has-` prefix (e.g., `isActive`, `hasError`)
- [ ] No bracket access (`styles['...']`) in JS unless verified
- [ ] No dynamic class lookup - use explicit variant maps instead
- [ ] No deep class nesting (max 3 class levels; pseudo-classes/elements and parent-reference selectors like `&.active`, `&#bar` are not counted)
- [ ] No hardcoded colors - use `--l1/l2/l3-*` semantic tokens (not `--bg-*` primitives)
- [ ] No magic numbers - use `--spacing-*` tokens
- [ ] Typography uses `--periscope-font-size-*` or `--font-size-*` tokens
- [ ] @signozhq/ui overrides use CSS variables, not direct class overrides
- [ ] Global escapes only for third-party overrides
- [ ] No ID selectors
- [ ] No bare element selectors
- [ ] Keyframes use `:local(@keyframes name)` to avoid global collisions
## Config (vite.config.ts)
```ts
css: {
modules: {
localsConvention: 'camelCaseOnly',
},
}
```
**Critical:** `camelCaseOnly` exports ONLY camelCase keys. Original kebab-case NOT accessible.
## Quick Reference
| CSS Class | JS Access | Works? | Preferred? |
|-----------|-----------|--------|----------------------------|
| `.alertHistory` | `styles.alertHistory` | Yes | Yes |
| `.alert-history` | `styles.alertHistory` | Yes | No, use `.alertHistory` |
| `.alert-history` | `styles['alert-history']` | NO - undefined | Never, use `.alertHistory` |
## Bad Patterns
### Class Naming
```scss
// BAD: Bracket access won't work
.my-class { }
// Then in JS: styles['my-class'] -> undefined
// BAD: Collision - both become same key
.alertHistory { }
.alert-history { } // -> styles.alertHistory (conflicts)
// BAD: Underscore inconsistency
.my_class { } // -> styles.myClass (confusing)
// GOOD: Direct camelCase
.alertHistory { }
.statsCard { }
// GOOD: State classes with is-/has- prefix
.isDisabled { }
.isActive { }
.hasError { }
.isLoading { }
```
### Nesting
```scss
// BAD: Deep nesting - specificity wars, hard to override
.container {
.wrapper {
.inner {
.content { }
}
}
}
// BAD: Nesting creates separate classes you might not expect
.button {
.icon { } // -> styles.icon (separate class, not scoped under .button)
}
// GOOD: Flat structure
.container { }
.containerWrapper { }
.containerContent { }
// GOOD: Nesting only for pseudo/states
.button {
&:hover { }
&:disabled { }
&::before { }
}
```
### Global Escapes
```scss
// BAD: Overusing global
:global {
.everything { }
.in-here { }
.is-global { }
}
// BAD: Global without necessity
:global(.myComponent) { } // defeats purpose of modules
// GOOD: Targeted global for third-party overrides
.container {
:global(.ant-modal-content) {
padding: 0;
}
}
```
### Selectors
```scss
// BAD: ID selectors - not reusable
#myComponent { }
// BAD: Element selectors without scope
div { } // affects ALL divs in component
// BAD: Complex selectors
.container > div + span ~ p { }
// GOOD: Class-only selectors
.container { }
.title { }
```
### Variables & Values
```scss
// BAD: Hardcoded colors
.button {
background: #1890ff;
color: white;
}
// BAD: Magic numbers
.container {
padding: 17px;
margin-left: 43px;
}
// GOOD: Semantic tokens (theme-aware)
.button {
background: var(--primary-background);
color: var(--primary-foreground);
}
.card {
background: var(--l2-background);
color: var(--l2-foreground);
}
// GOOD: Spacing system
.container {
padding: var(--spacing-4);
margin-left: var(--spacing-5);
}
```
## Design Tokens (@signozhq/design-tokens)
Prefer semantic tokens over hardcoded values.
You can read the ./node_modules/@signozhq/design-tokens/dist/style.css to find complete list of available tokens.
### Spacing
```scss
// Spacing scale (index -> px):
// --spacing-0=0 --spacing-1=2 --spacing-2=4 --spacing-3=6 --spacing-4=8
// --spacing-5=10 --spacing-6=12 --spacing-7=14 --spacing-8=16 --spacing-10=20
// --spacing-12=24 --spacing-16=32 --spacing-20=40 --spacing-24=48 --spacing-32=64
// --spacing-40=80 --spacing-48=96 --spacing-56=112 --spacing-64=128
// (index != px; --spacing-2 is 4px, not 2px)
.container {
padding: var(--spacing-4); // 8px
gap: var(--spacing-6); // 12px
margin-bottom: var(--spacing-8); // 16px
}
// Also available: --padding-* and --margin-* (rem-based)
// --padding-1 = 0.25rem, --padding-4 = 1rem, etc.
```
### Typography
```scss
// Font sizes (preferred)
.title {
font-size: var(--periscope-font-size-large); // 18px
font-size: var(--periscope-font-size-medium); // 16px
font-size: var(--periscope-font-size-base); // 13px
font-size: var(--periscope-font-size-small); // 11px
}
// Alternative scale (rem-based)
.heading {
font-size: var(--font-size-xl); // 1.25rem
font-size: var(--font-size-lg); // 1.125rem
font-size: var(--font-size-base); // 1rem
font-size: var(--font-size-sm); // 0.875rem
}
// Font weights
.bold {
font-weight: var(--font-weight-semibold); // 600
font-weight: var(--font-weight-medium); // 500
font-weight: var(--font-weight-normal); // 400
}
// Line heights
.text {
line-height: var(--line-height-20); // 20px
line-height: var(--line-height-24); // 24px
}
```
### Colors (Prefer Semantic Tokens)
Use L1/L2/L3 semantic tokens - they handle light/dark theme automatically.
```scss
// BAD: Primitive tokens (fixed value across themes, won't swap on theme change)
.card {
background: var(--bg-ink-400);
color: var(--text-vanilla-100);
}
// GOOD: L1/L2/L3 tokens (theme-aware - swap automatically light/dark)
.card {
background: var(--l1-background); // base layer
color: var(--l1-foreground); // primary text
}
.panel {
background: var(--l2-background); // elevated surface
color: var(--l2-foreground); // secondary text
border-color: var(--l2-border);
}
.nested {
background: var(--l3-background); // nested/inset
color: var(--l3-foreground); // tertiary text
}
// Hover states
.card:hover {
background: var(--l1-background-hover);
color: var(--l1-foreground-hover);
}
// Semantic action colors (also theme-aware)
.primary {
background: var(--primary-background);
color: var(--primary-foreground);
}
.danger {
background: var(--danger-background);
color: var(--danger-foreground);
}
.success {
background: var(--success-background);
color: var(--success-foreground);
}
.warning {
background: var(--warning-background);
color: var(--warning-foreground);
}
// Accent colors (for highlights, badges, etc.)
.accent {
background: var(--accent-primary); // robin blue
background: var(--accent-forest); // green
background: var(--accent-cherry); // red
background: var(--accent-amber); // yellow
}
```
**Token hierarchy:**
- Primitive tokens (`--bg-*`, `--text-*`, etc.) have fixed values across themes.
- Semantic tokens (L1/L2/L3, `--primary-*`, `--danger-*`, etc.) automatically swap based on theme.
- L1 = base/root layer
- L2 = elevated surfaces (cards, panels)
- L3 = nested/inset elements
## Overriding @signozhq/ui Components
Components expose CSS variables for customization.
You can ensure they exist by looking at ./node_modules/@signozhq/ui/dist.
Never write a override without confirm it exists.
Override via:
### Method 1: CSS Variables (Preferred)
Each component exposes `--<component>-<property>` variables:
```scss
// Override Button
.customButton {
--button-background: var(--success-background);
--button-border-radius: var(--radius-2);
--button-padding: var(--spacing-4) var(--spacing-8);
--button-font-size: var(--periscope-font-size-base);
}
// Override Input
.customInput {
--input-height: 2.5rem;
--input-border-color: var(--l2-border);
--input-padding: var(--spacing-2) var(--spacing-6);
--input-placeholder-color: var(--l3-foreground);
}
// Override nested parts
.customInput {
--input-prefix-padding: 0 var(--spacing-4) 0 var(--spacing-6);
--input-suffix-color: var(--accent-primary);
}
```
### Method 2: Data Attributes
Components use data attributes for variants/states. Target them for state-specific overrides:
```scss
// Target variant
.wrapper :global([data-variant="outlined"]) {
--button-border-color: var(--accent-primary);
}
// Target size
.wrapper :global([data-size="sm"]) {
--button-font-size: var(--periscope-font-size-small);
}
// Target color
.wrapper :global([data-color="destructive"]) {
--button-background: var(--danger-background);
}
// Target state (Radix patterns)
.popover :global([data-state="open"]) {
opacity: 1;
}
.tooltip :global([data-side="top"]) {
margin-bottom: var(--spacing-2);
}
```
### Common Component CSS Variables
**Button:**
- `--button-background`, `--button-border-radius`, `--button-padding`
- `--button-font-size`, `--button-height`, `--button-gap`
- `--button-hover-background`, `--button-disabled-opacity`
**Input:**
- `--input-height`, `--input-border-color`, `--input-background`
- `--input-padding`, `--input-font-size`, `--input-placeholder-color`
- `--input-focus-outline-color`, `--input-hover-border-color`
- `--input-prefix-*`, `--input-suffix-*` for adornments
**General pattern:** `--<component>-<property>` or `--<component>-<state>-<property>`
## Good Patterns
### Structure
```scss
// Flat, descriptive, component-scoped
.alertHistory { }
.alertHistoryHeader { }
.alertHistoryContent { }
.alertHistoryFooter { }
// State modifiers as separate classes
.alertHistory { }
.alertHistoryLoading { }
.alertHistoryEmpty { }
.alertHistoryError { }
```
### Composition
```scss
// GOOD: Composing styles
.baseButton {
padding: var(--spacing-2) var(--spacing-4);
border-radius: var(--radius-2);
}
.primaryButton {
composes: baseButton;
background: var(--primary-background);
}
```
### Pseudo Elements
```scss
.button {
// States
&:hover { opacity: 0.9; }
&:focus { outline: 2px solid var(--ring); outline-offset: 2px; }
&:disabled { opacity: 0.5; cursor: not-allowed; }
// Pseudo elements
&::before { content: ''; }
&::after { content: ''; }
}
```
### Media Queries
```scss
.container {
display: flex;
flex-direction: column;
@media (min-width: 768px) {
flex-direction: row;
}
}
```
### Keyframes (Local Scoping)
Without `:local()`, keyframe names are global and can clash across modules:
```scss
// BAD: Global keyframe - can conflict with other modules
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
// GOOD: Locally scoped keyframe
:local(@keyframes fadeIn) {
from { opacity: 0; }
to { opacity: 1; }
}
.modal {
animation: fadeIn 200ms ease;
}
```
## JS Import Patterns
```tsx
// GOOD
import styles from './Component.module.scss';
<div className={styles.container}>
<span className={styles.title}>Title</span>
</div>
// GOOD: Conditional classes
<div className={`${styles.button} ${isActive ? styles.buttonActive : ''}`}>
// GOOD: With clsx/classnames
<div className={clsx(styles.button, { [styles.buttonActive]: isActive })}>
// BAD: Bracket access (may be undefined)
<div className={styles['button-active']}> // undefined if CSS has .button-active
// BAD: String interpolation for class names
<div className={`${styles.button}-active`}> // won't work
// BAD: Dynamic class lookup - can't be statically analyzed
const cls = styles[`variant${props.type}`]; // Vite can't tree-shake or type-check
// GOOD: Explicit map for dynamic variants
const variantMap = {
primary: styles.variantPrimary,
secondary: styles.variantSecondary,
ghost: styles.variantGhost,
};
const cls = variantMap[props.type];
```
## Lint Rules
### JS/TS (oxlint)
| Rule | Severity | Catches |
|------|----------|---------|
| `signoz/no-css-module-bracket-access` | warn | `styles['kebab-case']`, dynamic access |
### CSS/SCSS (stylelint)
| Rule | Severity | Catches |
|------|----------|---------|
| `local/no-deep-nesting` | warning | class nesting >3 levels (pseudo-classes/elements and parent-reference selectors `&.foo`, `&#bar` not counted; configurable via `maxDepth` secondary option) |
| `local/no-id-selectors` | error | `#id` selectors |
| `local/no-bare-element-selectors` | error | root-level `div`, `span` etc |
| `local/prefer-css-variables` | warning | hardcoded colors |
| `local/class-name-pattern` | warning | kebab-case, snake_case, PascalCase |
Run: `pnpm lint:styles` to check CSS modules.

View File

@@ -45,8 +45,8 @@
"@dnd-kit/utilities": "3.2.2",
"@grafana/data": "^11.6.14",
"@monaco-editor/react": "^4.7.0",
"@sentry/react": "8.41.0",
"@sentry/vite-plugin": "2.22.6",
"@sentry/react": "10.57.0",
"@sentry/vite-plugin": "5.3.0",
"@signozhq/design-tokens": "2.1.4",
"@signozhq/icons": "0.4.0",
"@signozhq/resizable": "0.0.2",
@@ -192,9 +192,9 @@
"lint-staged": "^17.0.4",
"msw": "1.3.2",
"orval": "8.9.1",
"oxfmt": "0.47.0",
"oxlint": "1.62.0",
"oxlint-tsgolint": "0.22.1",
"oxfmt": "0.54.0",
"oxlint": "1.69.0",
"oxlint-tsgolint": "0.23.0",
"postcss": "8.5.14",
"postcss-scss": "4.0.9",
"react-resizable": "3.0.4",

View File

@@ -0,0 +1,144 @@
/**
* Rule: no-css-module-bracket-access
*
* Prevents bracket access on CSS module imports that may fail with camelCaseOnly config.
*
* With Vite's `localsConvention: 'camelCaseOnly'`, kebab-case class names are
* converted to camelCase and the original key is NOT exported.
*
* This rule catches patterns like:
* styles['my-class'] // BAD - undefined if CSS has .my-class
* styles['myClass'] // OK but prefer dot notation
* styles.myClass // GOOD
*
* Catches:
* - Bracket access with kebab-case strings (always fails)
* - Bracket access with any string literal (warn - prefer dot notation)
* - Dynamic bracket access (warn - risky)
*/
const CSS_MODULE_IMPORT_NAMES = new Set([
'styles',
'classes',
'css',
'classNames',
]);
function looksLikeCssModuleImport(name) {
// Common patterns: styles, componentStyles, alertHistoryStyles
return (
CSS_MODULE_IMPORT_NAMES.has(name) ||
name.endsWith('Styles') ||
name.endsWith('Classes') ||
name.endsWith('Css')
);
}
function isKebabCase(str) {
return str.includes('-');
}
function isSnakeCase(str) {
return str.includes('_');
}
export default {
create(context) {
return {
MemberExpression(node) {
// Only check bracket notation: styles['...']
if (!node.computed) {
return;
}
const object = node.object;
if (object.type !== 'Identifier') {
return;
}
// Check if this looks like a CSS module import
if (!looksLikeCssModuleImport(object.name)) {
return;
}
const property = node.property;
// Dynamic access: styles[variable]
if (property.type === 'Identifier') {
context.report({
node,
message: `Dynamic CSS module access '${object.name}[${property.name}]' is risky. With 'camelCaseOnly' config, kebab-case keys don't exist. Use dot notation or verify the key exists.`,
});
return;
}
// Template literal: styles[\`...\`]
if (property.type === 'TemplateLiteral') {
context.report({
node,
message: `Template literal CSS module access is risky. With 'camelCaseOnly' config, kebab-case keys don't exist. Prefer dot notation.`,
});
return;
}
// Numeric / boolean / null literal: styles[0]. Not a class lookup; ignore.
if (property.type === 'Literal' && typeof property.value !== 'string') {
return;
}
// String literal: styles['...']
if (property.type === 'Literal' && typeof property.value === 'string') {
const className = property.value;
// Kebab-case will definitely fail
if (isKebabCase(className)) {
context.report({
node,
message: `CSS module class '${className}' uses kebab-case which won't work with 'camelCaseOnly' config. Use '${object.name}.${toCamelCase(className)}' instead.`,
});
return;
}
// Snake_case is suspicious
if (isSnakeCase(className)) {
context.report({
node,
message: `CSS module class '${className}' uses snake_case which may not work as expected. Prefer camelCase: '${object.name}.${toCamelCase(className)}'.`,
});
return;
}
// Valid camelCase but using bracket notation - prefer dot
if (/^[a-z][a-zA-Z0-9]*$/.test(className)) {
context.report({
node,
message: `Prefer dot notation: '${object.name}.${className}' instead of '${object.name}['${className}']'.`,
});
}
return;
}
// Catch-all for other dynamic expressions:
// styles['prefix' + suffix] (BinaryExpression)
// styles[isActive && 'foo'] (LogicalExpression)
// styles[isActive ? 'a' : 'b'] (ConditionalExpression)
// styles[fn()] (CallExpression)
context.report({
node,
message: `Dynamic CSS module access on '${object.name}' is risky. With 'camelCaseOnly' config, kebab-case keys don't exist. Use dot notation or verify each key resolves to an exported camelCase class.`,
});
},
};
},
};
function toCamelCase(str) {
return str
.split(/[-_]/)
.map((part, i) =>
i === 0
? part.toLowerCase()
: part.charAt(0).toUpperCase() + part.slice(1).toLowerCase(),
)
.join('');
}

View File

@@ -11,6 +11,7 @@ import noUnsupportedAssetPattern from './rules/no-unsupported-asset-pattern.mjs'
import noRawAbsolutePath from './rules/no-raw-absolute-path.mjs';
import noAntdComponents from './rules/no-antd-components.mjs';
import noSignozhqUiBarrel from './rules/no-signozhq-ui-barrel.mjs';
import noCssModuleBracketAccess from './rules/no-css-module-bracket-access.mjs';
export default {
meta: {
@@ -23,5 +24,6 @@ export default {
'no-raw-absolute-path': noRawAbsolutePath,
'no-antd-components': noAntdComponents,
'no-signozhq-ui-barrel': noSignozhqUiBarrel,
'no-css-module-bracket-access': noCssModuleBracketAccess,
},
};

696
frontend/pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -64,10 +64,17 @@ export const TraceDetail = Loadable(
),
);
export const TraceDetailOldRedirect = Loadable(
() =>
import(
/* webpackChunkName: "TraceDetailOldRedirect" */ 'pages/TraceDetailOldRedirect/index'
),
);
export const TraceDetailV3 = Loadable(
() =>
import(
/* webpackChunkName: "TraceDetailV3 Page" */ 'pages/TraceDetailV3Page/index'
/* webpackChunkName: "TraceDetailV3 Page" */ 'pages/TraceDetailsV3/index'
),
);

View File

@@ -47,7 +47,7 @@ import {
SomethingWentWrong,
StatusPage,
SupportPage,
TraceDetail,
TraceDetailOldRedirect,
TraceDetailV3,
TraceFilter,
TracesExplorer,
@@ -139,13 +139,11 @@ const routes: AppRoutes[] = [
exact: true,
key: 'LOGS_SAVE_VIEWS',
},
// V3 trace details is gated until release: /trace serves V2 (public),
// /trace-old serves V3 (URL-only access). Flip the two `component`
// values back to release V3.
// Legacy /trace-old/:id redirects to the current /trace/:id view.
{
path: ROUTES.TRACE_DETAIL_OLD,
exact: true,
component: TraceDetail,
component: TraceDetailOldRedirect,
isPrivate: true,
key: 'TRACE_DETAIL_OLD',
},

View File

@@ -55,6 +55,9 @@ import type {
ThreadDetailResponseDTO,
ThreadListResponseDTO,
ThreadSummaryDTO,
ChipDTO,
ChipsResponseDTO,
PageTypeDTO,
ToolCallEventDTO,
ToolResultEventDTO,
} from './sigNozAIAssistantAPI.schemas';
@@ -541,3 +544,19 @@ export async function submitFeedback(
comment: comment ?? null,
});
}
// ---------------------------------------------------------------------------
// Contextual empty-state chips
// GET /api/v1/assistant/empty-state/chips?page_type=… → { chips }
// ---------------------------------------------------------------------------
export async function getEmptyStateChips(
pageType: PageTypeDTO,
signal?: AbortSignal,
): Promise<ChipDTO[]> {
const response = await AIAssistantInstance.get<ChipsResponseDTO>(
'/empty-state/chips',
{ params: { page_type: pageType }, signal },
);
return response.data.chips;
}

View File

@@ -26,6 +26,7 @@ import type {
CancelApiV1AssistantCancelPostHeaders,
CancelRequestDTO,
CancelResponseDTO,
ChipsResponseDTO,
ClarifyApiV1AssistantClarifyPostHeaders,
ClarifyRequestDTO,
ClarifyResponseDTO,
@@ -39,8 +40,11 @@ import type {
ErrorResponseDTO,
FeedbackRequestDTO,
FeedbackResponseDTO,
GetChipsApiV1AssistantEmptyStateChipsGetHeaders,
GetChipsApiV1AssistantEmptyStateChipsGetParams,
GetThreadApiV1AssistantThreadsThreadIdGetHeaders,
GetThreadApiV1AssistantThreadsThreadIdGetPathParameters,
GetUsageApiV1AssistantUsageGetHeaders,
HTTPValidationErrorDTO,
HealthResponseDTO,
ListThreadsApiV1AssistantThreadsGetHeaders,
@@ -65,93 +69,89 @@ import type {
UpdateThreadApiV1AssistantThreadsThreadIdPatchHeaders,
UpdateThreadApiV1AssistantThreadsThreadIdPatchPathParameters,
UpdateThreadRequestDTO,
UsageResponseDTO,
} from './sigNozAIAssistantAPI.schemas';
import {
GeneratedAPIInstance,
getGeneratedAPIQueryKeyHeaders,
} from '../generatedAPIInstance';
import { GeneratedAPIInstance } from '../generatedAPIInstance';
import type { ErrorType, BodyType } from '../generatedAPIInstance';
/**
* @summary Health
* @summary Healthz
*/
export const healthHealthGet = (signal?: AbortSignal) => {
export const healthzHealthzGet = (signal?: AbortSignal) => {
return GeneratedAPIInstance<HealthResponseDTO>({
url: `/health`,
url: `/healthz`,
method: 'GET',
signal,
});
};
export const getHealthHealthGetQueryKey = () => {
return [`/health`] as const;
export const getHealthzHealthzGetQueryKey = () => {
return [`/healthz`] as const;
};
export const getHealthHealthGetQueryOptions = <
TData = Awaited<ReturnType<typeof healthHealthGet>>,
export const getHealthzHealthzGetQueryOptions = <
TData = Awaited<ReturnType<typeof healthzHealthzGet>>,
TError = ErrorType<unknown>,
>(options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof healthHealthGet>>,
Awaited<ReturnType<typeof healthzHealthzGet>>,
TError,
TData
>;
}) => {
const { query: queryOptions } = options ?? {};
const queryKey = queryOptions?.queryKey ?? getHealthHealthGetQueryKey();
const queryKey = queryOptions?.queryKey ?? getHealthzHealthzGetQueryKey();
const queryFn: QueryFunction<Awaited<ReturnType<typeof healthHealthGet>>> = ({
signal,
}) => healthHealthGet(signal);
const queryFn: QueryFunction<
Awaited<ReturnType<typeof healthzHealthzGet>>
> = ({ signal }) => healthzHealthzGet(signal);
return { queryKey, queryFn, ...queryOptions } as UseQueryOptions<
Awaited<ReturnType<typeof healthHealthGet>>,
Awaited<ReturnType<typeof healthzHealthzGet>>,
TError,
TData
> & { queryKey: QueryKey };
};
export type HealthHealthGetQueryResult = NonNullable<
Awaited<ReturnType<typeof healthHealthGet>>
export type HealthzHealthzGetQueryResult = NonNullable<
Awaited<ReturnType<typeof healthzHealthzGet>>
>;
export type HealthHealthGetQueryError = ErrorType<unknown>;
export type HealthzHealthzGetQueryError = ErrorType<unknown>;
/**
* @summary Health
* @summary Healthz
*/
export function useHealthHealthGet<
TData = Awaited<ReturnType<typeof healthHealthGet>>,
export function useHealthzHealthzGet<
TData = Awaited<ReturnType<typeof healthzHealthzGet>>,
TError = ErrorType<unknown>,
>(options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof healthHealthGet>>,
Awaited<ReturnType<typeof healthzHealthzGet>>,
TError,
TData
>;
}): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
const queryOptions = getHealthHealthGetQueryOptions(options);
const queryOptions = getHealthzHealthzGetQueryOptions(options);
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
queryKey: QueryKey;
};
query.queryKey = queryOptions.queryKey;
return query;
return { ...query, queryKey: queryOptions.queryKey };
}
/**
* @summary Health
* @summary Healthz
*/
export const invalidateHealthHealthGet = async (
export const invalidateHealthzHealthzGet = async (
queryClient: QueryClient,
options?: InvalidateOptions,
): Promise<QueryClient> => {
await queryClient.invalidateQueries(
{ queryKey: getHealthHealthGetQueryKey() },
{ queryKey: getHealthzHealthzGetQueryKey() },
options,
);
@@ -159,84 +159,82 @@ export const invalidateHealthHealthGet = async (
};
/**
* @summary Ready
* @summary Readyz
*/
export const readyReadyGet = (signal?: AbortSignal) => {
export const readyzReadyzGet = (signal?: AbortSignal) => {
return GeneratedAPIInstance<ReadinessResponseDTO>({
url: `/ready`,
url: `/readyz`,
method: 'GET',
signal,
});
};
export const getReadyReadyGetQueryKey = () => {
return [`/ready`] as const;
export const getReadyzReadyzGetQueryKey = () => {
return [`/readyz`] as const;
};
export const getReadyReadyGetQueryOptions = <
TData = Awaited<ReturnType<typeof readyReadyGet>>,
export const getReadyzReadyzGetQueryOptions = <
TData = Awaited<ReturnType<typeof readyzReadyzGet>>,
TError = ErrorType<ReadinessResponseDTO>,
>(options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof readyReadyGet>>,
Awaited<ReturnType<typeof readyzReadyzGet>>,
TError,
TData
>;
}) => {
const { query: queryOptions } = options ?? {};
const queryKey = queryOptions?.queryKey ?? getReadyReadyGetQueryKey();
const queryKey = queryOptions?.queryKey ?? getReadyzReadyzGetQueryKey();
const queryFn: QueryFunction<Awaited<ReturnType<typeof readyReadyGet>>> = ({
const queryFn: QueryFunction<Awaited<ReturnType<typeof readyzReadyzGet>>> = ({
signal,
}) => readyReadyGet(signal);
}) => readyzReadyzGet(signal);
return { queryKey, queryFn, ...queryOptions } as UseQueryOptions<
Awaited<ReturnType<typeof readyReadyGet>>,
Awaited<ReturnType<typeof readyzReadyzGet>>,
TError,
TData
> & { queryKey: QueryKey };
};
export type ReadyReadyGetQueryResult = NonNullable<
Awaited<ReturnType<typeof readyReadyGet>>
export type ReadyzReadyzGetQueryResult = NonNullable<
Awaited<ReturnType<typeof readyzReadyzGet>>
>;
export type ReadyReadyGetQueryError = ErrorType<ReadinessResponseDTO>;
export type ReadyzReadyzGetQueryError = ErrorType<ReadinessResponseDTO>;
/**
* @summary Ready
* @summary Readyz
*/
export function useReadyReadyGet<
TData = Awaited<ReturnType<typeof readyReadyGet>>,
export function useReadyzReadyzGet<
TData = Awaited<ReturnType<typeof readyzReadyzGet>>,
TError = ErrorType<ReadinessResponseDTO>,
>(options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof readyReadyGet>>,
Awaited<ReturnType<typeof readyzReadyzGet>>,
TError,
TData
>;
}): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
const queryOptions = getReadyReadyGetQueryOptions(options);
const queryOptions = getReadyzReadyzGetQueryOptions(options);
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
queryKey: QueryKey;
};
query.queryKey = queryOptions.queryKey;
return query;
return { ...query, queryKey: queryOptions.queryKey };
}
/**
* @summary Ready
* @summary Readyz
*/
export const invalidateReadyReadyGet = async (
export const invalidateReadyzReadyzGet = async (
queryClient: QueryClient,
options?: InvalidateOptions,
): Promise<QueryClient> => {
await queryClient.invalidateQueries(
{ queryKey: getReadyReadyGetQueryKey() },
{ queryKey: getReadyzReadyzGetQueryKey() },
options,
);
@@ -247,7 +245,7 @@ export const invalidateReadyReadyGet = async (
* @summary Create a new thread
*/
export const createThreadApiV1AssistantThreadsPost = (
createThreadApiV1AssistantThreadsPostBody: BodyType<CreateThreadApiV1AssistantThreadsPostBody>,
createThreadApiV1AssistantThreadsPostBody?: BodyType<CreateThreadApiV1AssistantThreadsPostBody>,
headers?: CreateThreadApiV1AssistantThreadsPostHeaders,
signal?: AbortSignal,
) => {
@@ -268,7 +266,7 @@ export const getCreateThreadApiV1AssistantThreadsPostMutationOptions = <
Awaited<ReturnType<typeof createThreadApiV1AssistantThreadsPost>>,
TError,
{
data: BodyType<CreateThreadApiV1AssistantThreadsPostBody>;
data?: BodyType<CreateThreadApiV1AssistantThreadsPostBody>;
headers?: CreateThreadApiV1AssistantThreadsPostHeaders;
},
TContext
@@ -277,7 +275,7 @@ export const getCreateThreadApiV1AssistantThreadsPostMutationOptions = <
Awaited<ReturnType<typeof createThreadApiV1AssistantThreadsPost>>,
TError,
{
data: BodyType<CreateThreadApiV1AssistantThreadsPostBody>;
data?: BodyType<CreateThreadApiV1AssistantThreadsPostBody>;
headers?: CreateThreadApiV1AssistantThreadsPostHeaders;
},
TContext
@@ -294,7 +292,7 @@ export const getCreateThreadApiV1AssistantThreadsPostMutationOptions = <
const mutationFn: MutationFunction<
Awaited<ReturnType<typeof createThreadApiV1AssistantThreadsPost>>,
{
data: BodyType<CreateThreadApiV1AssistantThreadsPostBody>;
data?: BodyType<CreateThreadApiV1AssistantThreadsPostBody>;
headers?: CreateThreadApiV1AssistantThreadsPostHeaders;
}
> = (props) => {
@@ -310,7 +308,8 @@ export type CreateThreadApiV1AssistantThreadsPostMutationResult = NonNullable<
Awaited<ReturnType<typeof createThreadApiV1AssistantThreadsPost>>
>;
export type CreateThreadApiV1AssistantThreadsPostMutationBody =
BodyType<CreateThreadApiV1AssistantThreadsPostBody>;
| BodyType<CreateThreadApiV1AssistantThreadsPostBody>
| undefined;
export type CreateThreadApiV1AssistantThreadsPostMutationError = ErrorType<
ErrorResponseDTO | HTTPValidationErrorDTO
>;
@@ -326,7 +325,7 @@ export const useCreateThreadApiV1AssistantThreadsPost = <
Awaited<ReturnType<typeof createThreadApiV1AssistantThreadsPost>>,
TError,
{
data: BodyType<CreateThreadApiV1AssistantThreadsPostBody>;
data?: BodyType<CreateThreadApiV1AssistantThreadsPostBody>;
headers?: CreateThreadApiV1AssistantThreadsPostHeaders;
},
TContext
@@ -335,15 +334,14 @@ export const useCreateThreadApiV1AssistantThreadsPost = <
Awaited<ReturnType<typeof createThreadApiV1AssistantThreadsPost>>,
TError,
{
data: BodyType<CreateThreadApiV1AssistantThreadsPostBody>;
data?: BodyType<CreateThreadApiV1AssistantThreadsPostBody>;
headers?: CreateThreadApiV1AssistantThreadsPostHeaders;
},
TContext
> => {
const mutationOptions =
getCreateThreadApiV1AssistantThreadsPostMutationOptions(options);
return useMutation(mutationOptions);
return useMutation(
getCreateThreadApiV1AssistantThreadsPostMutationOptions(options),
);
};
/**
* Cursor-based pagination, sorted by updatedAt desc. Use `archived=true|false|all` to filter.
@@ -365,13 +363,8 @@ export const listThreadsApiV1AssistantThreadsGet = (
export const getListThreadsApiV1AssistantThreadsGetQueryKey = (
params?: ListThreadsApiV1AssistantThreadsGetParams,
headers?: ListThreadsApiV1AssistantThreadsGetHeaders,
) => {
return [
`/api/v1/assistant/threads`,
...(params ? [params] : []),
...getGeneratedAPIQueryKeyHeaders(headers),
] as const;
return [`/api/v1/assistant/threads`, ...(params ? [params] : [])] as const;
};
export const getListThreadsApiV1AssistantThreadsGetQueryOptions = <
@@ -392,7 +385,7 @@ export const getListThreadsApiV1AssistantThreadsGetQueryOptions = <
const queryKey =
queryOptions?.queryKey ??
getListThreadsApiV1AssistantThreadsGetQueryKey(params, headers);
getListThreadsApiV1AssistantThreadsGetQueryKey(params);
const queryFn: QueryFunction<
Awaited<ReturnType<typeof listThreadsApiV1AssistantThreadsGet>>
@@ -441,9 +434,7 @@ export function useListThreadsApiV1AssistantThreadsGet<
queryKey: QueryKey;
};
query.queryKey = queryOptions.queryKey;
return query;
return { ...query, queryKey: queryOptions.queryKey };
}
/**
@@ -456,7 +447,7 @@ export const invalidateListThreadsApiV1AssistantThreadsGet = async (
options?: InvalidateOptions,
): Promise<QueryClient> => {
await queryClient.invalidateQueries(
{ queryKey: getListThreadsApiV1AssistantThreadsGetQueryKey(params, headers) },
{ queryKey: getListThreadsApiV1AssistantThreadsGetQueryKey(params) },
options,
);
@@ -480,14 +471,10 @@ export const getThreadApiV1AssistantThreadsThreadIdGet = (
});
};
export const getGetThreadApiV1AssistantThreadsThreadIdGetQueryKey = (
{ threadId }: GetThreadApiV1AssistantThreadsThreadIdGetPathParameters,
headers?: GetThreadApiV1AssistantThreadsThreadIdGetHeaders,
) => {
return [
`/api/v1/assistant/threads/${threadId}`,
...getGeneratedAPIQueryKeyHeaders(headers),
] as const;
export const getGetThreadApiV1AssistantThreadsThreadIdGetQueryKey = ({
threadId,
}: GetThreadApiV1AssistantThreadsThreadIdGetPathParameters) => {
return [`/api/v1/assistant/threads/${threadId}`] as const;
};
export const getGetThreadApiV1AssistantThreadsThreadIdGetQueryOptions = <
@@ -508,7 +495,7 @@ export const getGetThreadApiV1AssistantThreadsThreadIdGetQueryOptions = <
const queryKey =
queryOptions?.queryKey ??
getGetThreadApiV1AssistantThreadsThreadIdGetQueryKey({ threadId }, headers);
getGetThreadApiV1AssistantThreadsThreadIdGetQueryKey({ threadId });
const queryFn: QueryFunction<
Awaited<ReturnType<typeof getThreadApiV1AssistantThreadsThreadIdGet>>
@@ -562,9 +549,7 @@ export function useGetThreadApiV1AssistantThreadsThreadIdGet<
queryKey: QueryKey;
};
query.queryKey = queryOptions.queryKey;
return query;
return { ...query, queryKey: queryOptions.queryKey };
}
/**
@@ -578,10 +563,7 @@ export const invalidateGetThreadApiV1AssistantThreadsThreadIdGet = async (
): Promise<QueryClient> => {
await queryClient.invalidateQueries(
{
queryKey: getGetThreadApiV1AssistantThreadsThreadIdGetQueryKey(
{ threadId },
headers,
),
queryKey: getGetThreadApiV1AssistantThreadsThreadIdGetQueryKey({ threadId }),
},
options,
);
@@ -596,12 +578,14 @@ export const updateThreadApiV1AssistantThreadsThreadIdPatch = (
{ threadId }: UpdateThreadApiV1AssistantThreadsThreadIdPatchPathParameters,
updateThreadRequestDTO: BodyType<UpdateThreadRequestDTO>,
headers?: UpdateThreadApiV1AssistantThreadsThreadIdPatchHeaders,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<ThreadSummaryDTO>({
url: `/api/v1/assistant/threads/${threadId}`,
method: 'PATCH',
headers: { 'Content-Type': 'application/json', ...headers },
data: updateThreadRequestDTO,
signal,
});
};
@@ -695,10 +679,9 @@ export const useUpdateThreadApiV1AssistantThreadsThreadIdPatch = <
},
TContext
> => {
const mutationOptions =
getUpdateThreadApiV1AssistantThreadsThreadIdPatchMutationOptions(options);
return useMutation(mutationOptions);
return useMutation(
getUpdateThreadApiV1AssistantThreadsThreadIdPatchMutationOptions(options),
);
};
/**
* Persists the user message, creates an execution (state: queued), kicks off the agent loop asynchronously, and returns immediately. Open `GET /executions/{executionId}/events` for the SSE stream.
@@ -825,12 +808,11 @@ export const useCreateMessageApiV1AssistantThreadsThreadIdMessagesPost = <
},
TContext
> => {
const mutationOptions =
return useMutation(
getCreateMessageApiV1AssistantThreadsThreadIdMessagesPostMutationOptions(
options,
);
return useMutation(mutationOptions);
),
);
};
/**
* Clean-slate regeneration. Starts a fresh execution with conversation history up to (excluding) the original assistant response.
@@ -961,12 +943,11 @@ export const useRegenerateMessageApiV1AssistantMessagesMessageIdRegeneratePost =
},
TContext
> => {
const mutationOptions =
return useMutation(
getRegenerateMessageApiV1AssistantMessagesMessageIdRegeneratePostMutationOptions(
options,
);
return useMutation(mutationOptions);
),
);
};
/**
* Triggers a replay execution that runs the stored tool call with exact params. Returns a new executionId — open SSE for that execution.
@@ -1066,10 +1047,9 @@ export const useApproveApiV1AssistantApprovePost = <
},
TContext
> => {
const mutationOptions =
getApproveApiV1AssistantApprovePostMutationOptions(options);
return useMutation(mutationOptions);
return useMutation(
getApproveApiV1AssistantApprovePostMutationOptions(options),
);
};
/**
* Marks the approval as rejected. The execution completes with no tool execution.
@@ -1169,10 +1149,7 @@ export const useRejectApiV1AssistantRejectPost = <
},
TContext
> => {
const mutationOptions =
getRejectApiV1AssistantRejectPostMutationOptions(options);
return useMutation(mutationOptions);
return useMutation(getRejectApiV1AssistantRejectPostMutationOptions(options));
};
/**
* Provides structured answers to a clarification request. Persists the answers as a user transcript message, emits `user_message` as the first replayable event on the new execution stream, and resumes the agent with the answers as tool results.
@@ -1272,10 +1249,9 @@ export const useClarifyApiV1AssistantClarifyPost = <
},
TContext
> => {
const mutationOptions =
getClarifyApiV1AssistantClarifyPostMutationOptions(options);
return useMutation(mutationOptions);
return useMutation(
getClarifyApiV1AssistantClarifyPostMutationOptions(options),
);
};
/**
* Cooperative cancel. The agent loop finishes its current step, emits a truncated message if streaming, and transitions to canceled.
@@ -1375,10 +1351,7 @@ export const useCancelApiV1AssistantCancelPost = <
},
TContext
> => {
const mutationOptions =
getCancelApiV1AssistantCancelPostMutationOptions(options);
return useMutation(mutationOptions);
return useMutation(getCancelApiV1AssistantCancelPostMutationOptions(options));
};
/**
* Deletes the resource that was created by the assistant.
@@ -1477,9 +1450,7 @@ export const useUndoApiV1AssistantUndoPost = <
},
TContext
> => {
const mutationOptions = getUndoApiV1AssistantUndoPostMutationOptions(options);
return useMutation(mutationOptions);
return useMutation(getUndoApiV1AssistantUndoPostMutationOptions(options));
};
/**
* Rolls back the resource to its pre-change snapshot.
@@ -1579,10 +1550,7 @@ export const useRevertApiV1AssistantRevertPost = <
},
TContext
> => {
const mutationOptions =
getRevertApiV1AssistantRevertPostMutationOptions(options);
return useMutation(mutationOptions);
return useMutation(getRevertApiV1AssistantRevertPostMutationOptions(options));
};
/**
* Recreates the resource from its pre-delete snapshot.
@@ -1682,10 +1650,9 @@ export const useRestoreApiV1AssistantRestorePost = <
},
TContext
> => {
const mutationOptions =
getRestoreApiV1AssistantRestorePostMutationOptions(options);
return useMutation(mutationOptions);
return useMutation(
getRestoreApiV1AssistantRestorePostMutationOptions(options),
);
};
/**
* @summary Submit feedback on an assistant message
@@ -1811,10 +1778,221 @@ export const useSubmitFeedbackApiV1AssistantMessagesMessageIdFeedbackPost = <
},
TContext
> => {
const mutationOptions =
return useMutation(
getSubmitFeedbackApiV1AssistantMessagesMessageIdFeedbackPostMutationOptions(
options,
);
return useMutation(mutationOptions);
),
);
};
/**
* @summary Current rate-limit usage for the authenticated user + org
*/
export const getUsageApiV1AssistantUsageGet = (
headers?: GetUsageApiV1AssistantUsageGetHeaders,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<UsageResponseDTO>({
url: `/api/v1/assistant/usage`,
method: 'GET',
headers,
signal,
});
};
export const getGetUsageApiV1AssistantUsageGetQueryKey = () => {
return [`/api/v1/assistant/usage`] as const;
};
export const getGetUsageApiV1AssistantUsageGetQueryOptions = <
TData = Awaited<ReturnType<typeof getUsageApiV1AssistantUsageGet>>,
TError = ErrorType<ErrorResponseDTO | HTTPValidationErrorDTO>,
>(
headers?: GetUsageApiV1AssistantUsageGetHeaders,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getUsageApiV1AssistantUsageGet>>,
TError,
TData
>;
},
) => {
const { query: queryOptions } = options ?? {};
const queryKey =
queryOptions?.queryKey ?? getGetUsageApiV1AssistantUsageGetQueryKey();
const queryFn: QueryFunction<
Awaited<ReturnType<typeof getUsageApiV1AssistantUsageGet>>
> = ({ signal }) => getUsageApiV1AssistantUsageGet(headers, signal);
return { queryKey, queryFn, ...queryOptions } as UseQueryOptions<
Awaited<ReturnType<typeof getUsageApiV1AssistantUsageGet>>,
TError,
TData
> & { queryKey: QueryKey };
};
export type GetUsageApiV1AssistantUsageGetQueryResult = NonNullable<
Awaited<ReturnType<typeof getUsageApiV1AssistantUsageGet>>
>;
export type GetUsageApiV1AssistantUsageGetQueryError = ErrorType<
ErrorResponseDTO | HTTPValidationErrorDTO
>;
/**
* @summary Current rate-limit usage for the authenticated user + org
*/
export function useGetUsageApiV1AssistantUsageGet<
TData = Awaited<ReturnType<typeof getUsageApiV1AssistantUsageGet>>,
TError = ErrorType<ErrorResponseDTO | HTTPValidationErrorDTO>,
>(
headers?: GetUsageApiV1AssistantUsageGetHeaders,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getUsageApiV1AssistantUsageGet>>,
TError,
TData
>;
},
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
const queryOptions = getGetUsageApiV1AssistantUsageGetQueryOptions(
headers,
options,
);
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
queryKey: QueryKey;
};
return { ...query, queryKey: queryOptions.queryKey };
}
/**
* @summary Current rate-limit usage for the authenticated user + org
*/
export const invalidateGetUsageApiV1AssistantUsageGet = async (
queryClient: QueryClient,
headers?: GetUsageApiV1AssistantUsageGetHeaders,
options?: InvalidateOptions,
): Promise<QueryClient> => {
await queryClient.invalidateQueries(
{ queryKey: getGetUsageApiV1AssistantUsageGetQueryKey() },
options,
);
return queryClient;
};
/**
* @summary Contextual empty-state chips
*/
export const getChipsApiV1AssistantEmptyStateChipsGet = (
params: GetChipsApiV1AssistantEmptyStateChipsGetParams,
headers?: GetChipsApiV1AssistantEmptyStateChipsGetHeaders,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<ChipsResponseDTO>({
url: `/api/v1/assistant/empty-state/chips`,
method: 'GET',
headers,
params,
signal,
});
};
export const getGetChipsApiV1AssistantEmptyStateChipsGetQueryKey = (
params?: GetChipsApiV1AssistantEmptyStateChipsGetParams,
) => {
return [
`/api/v1/assistant/empty-state/chips`,
...(params ? [params] : []),
] as const;
};
export const getGetChipsApiV1AssistantEmptyStateChipsGetQueryOptions = <
TData = Awaited<ReturnType<typeof getChipsApiV1AssistantEmptyStateChipsGet>>,
TError = ErrorType<ErrorResponseDTO | HTTPValidationErrorDTO>,
>(
params: GetChipsApiV1AssistantEmptyStateChipsGetParams,
headers?: GetChipsApiV1AssistantEmptyStateChipsGetHeaders,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getChipsApiV1AssistantEmptyStateChipsGet>>,
TError,
TData
>;
},
) => {
const { query: queryOptions } = options ?? {};
const queryKey =
queryOptions?.queryKey ??
getGetChipsApiV1AssistantEmptyStateChipsGetQueryKey(params);
const queryFn: QueryFunction<
Awaited<ReturnType<typeof getChipsApiV1AssistantEmptyStateChipsGet>>
> = ({ signal }) =>
getChipsApiV1AssistantEmptyStateChipsGet(params, headers, signal);
return { queryKey, queryFn, ...queryOptions } as UseQueryOptions<
Awaited<ReturnType<typeof getChipsApiV1AssistantEmptyStateChipsGet>>,
TError,
TData
> & { queryKey: QueryKey };
};
export type GetChipsApiV1AssistantEmptyStateChipsGetQueryResult = NonNullable<
Awaited<ReturnType<typeof getChipsApiV1AssistantEmptyStateChipsGet>>
>;
export type GetChipsApiV1AssistantEmptyStateChipsGetQueryError = ErrorType<
ErrorResponseDTO | HTTPValidationErrorDTO
>;
/**
* @summary Contextual empty-state chips
*/
export function useGetChipsApiV1AssistantEmptyStateChipsGet<
TData = Awaited<ReturnType<typeof getChipsApiV1AssistantEmptyStateChipsGet>>,
TError = ErrorType<ErrorResponseDTO | HTTPValidationErrorDTO>,
>(
params: GetChipsApiV1AssistantEmptyStateChipsGetParams,
headers?: GetChipsApiV1AssistantEmptyStateChipsGetHeaders,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getChipsApiV1AssistantEmptyStateChipsGet>>,
TError,
TData
>;
},
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
const queryOptions = getGetChipsApiV1AssistantEmptyStateChipsGetQueryOptions(
params,
headers,
options,
);
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
queryKey: QueryKey;
};
return { ...query, queryKey: queryOptions.queryKey };
}
/**
* @summary Contextual empty-state chips
*/
export const invalidateGetChipsApiV1AssistantEmptyStateChipsGet = async (
queryClient: QueryClient,
params: GetChipsApiV1AssistantEmptyStateChipsGetParams,
headers?: GetChipsApiV1AssistantEmptyStateChipsGetHeaders,
options?: InvalidateOptions,
): Promise<QueryClient> => {
await queryClient.invalidateQueries(
{ queryKey: getGetChipsApiV1AssistantEmptyStateChipsGetQueryKey(params) },
options,
);
return queryClient;
};

View File

@@ -159,6 +159,25 @@ export interface CancelResponseDTO {
state: ExecutionStateDTO;
}
export interface ChipDTO {
/**
* @type string
* @description Stable chip id. Rule-engine chips use intent ids.
*/
id: string;
/**
* @type string
*/
text: string;
}
export interface ChipsResponseDTO {
/**
* @type array
*/
chips: ChipDTO[];
}
export type ClarificationFieldDTOOptions = string[] | null;
export type ClarificationFieldDTODefault = string | string[] | null;
@@ -386,15 +405,74 @@ export type ErrorBodyDTOErrors = ErrorResponseAdditionalDTO[] | null;
export type ErrorBodyDTOUrl = string | null;
/**
* Machine-readable error codes carried on ``ErrorBody.code``.
**Extensible set.** This enum is the single source of truth for every code
the backend can emit, on both the REST envelope and the SSE ``ErrorEvent``.
It is published in the OpenAPI schema (and therefore the generated TS
client) so clients get autocomplete and a typed discriminant. The set is
expected to *grow*: adding a member is a backward-compatible change (the
wire is still a plain JSON string), so clients MUST treat unknown codes
gracefully — branch on the codes they handle and keep a default fallback,
never hard-reject an unrecognized value. Re-exported from ``app.errors``
for convenience; ``AssistantError(code=...)`` requires a member of this
enum so a typo can never reach a client.
*/
export enum ErrorCodeDTO {
missing_signoz_url = 'missing_signoz_url',
invalid_signoz_url = 'invalid_signoz_url',
invalid_content_length = 'invalid_content_length',
invalid_fork_target = 'invalid_fork_target',
rate_limit_override_exceeds_ceiling = 'rate_limit_override_exceeds_ceiling',
thread_message_limit = 'thread_message_limit',
validation_error = 'validation_error',
missing_token = 'missing_token',
invalid_token = 'invalid_token',
permission_denied = 'permission_denied',
user_disabled = 'user_disabled',
org_disabled = 'org_disabled',
thread_not_found = 'thread_not_found',
message_not_found = 'message_not_found',
execution_not_found = 'execution_not_found',
approval_not_found = 'approval_not_found',
clarification_not_found = 'clarification_not_found',
action_metadata_not_found = 'action_metadata_not_found',
user_not_found = 'user_not_found',
region_not_configured = 'region_not_configured',
thread_busy = 'thread_busy',
thread_has_active_execution = 'thread_has_active_execution',
no_active_execution = 'no_active_execution',
approval_superseded = 'approval_superseded',
clarification_superseded = 'clarification_superseded',
undo_conflict = 'undo_conflict',
revert_conflict = 'revert_conflict',
revert_expired = 'revert_expired',
restore_expired = 'restore_expired',
connection_limit_exceeded = 'connection_limit_exceeded',
hourly_message_limit = 'hourly_message_limit',
daily_message_limit = 'daily_message_limit',
daily_token_limit = 'daily_token_limit',
daily_cost_limit = 'daily_cost_limit',
upstream_auth_error = 'upstream_auth_error',
max_turns_exceeded = 'max_turns_exceeded',
budget_exceeded = 'budget_exceeded',
agent_execution_error = 'agent_execution_error',
cli_not_found = 'cli_not_found',
cli_connection_error = 'cli_connection_error',
cli_process_error = 'cli_process_error',
sandbox_unavailable = 'sandbox_unavailable',
mcp_unavailable = 'mcp_unavailable',
internal_error = 'internal_error',
region_unreachable = 'region_unreachable',
heartbeat_expired = 'heartbeat_expired',
replay_unavailable = 'replay_unavailable',
}
/**
* Inner error object — matches Go ErrorsJSON.
*/
export interface ErrorBodyDTO {
/**
* @type string
* @pattern ^[a-z_]+$
*/
code: string;
code: ErrorCodeDTO;
/**
* @type string
*/
@@ -490,6 +568,23 @@ export type MessageActionDTOQuery = MessageActionDTOQueryAnyOf | null;
export type MessageActionDTOUrl = string | null;
/**
* Explorer namespace a saved view belongs to — its ``sourcePage``.
Mirrors the SigNoz product's saved-view ``sourcePage`` values so the
frontend can route an ``open_resource`` action for a view to the right
Explorer via its existing ``SOURCEPAGE_VS_ROUTES`` map. ``meter`` is the
Cost Meter Explorer and is intentionally distinct from ``metrics`` (the
product persists and lists meter views under ``sourcePage="meter"``).
*/
export enum SavedViewEntityDTO {
logs = 'logs',
traces = 'traces',
metrics = 'metrics',
meter = 'meter',
}
export type MessageActionDTOEntity = SavedViewEntityDTO | null;
export enum MessageActionKindDTO {
undo = 'undo',
revert = 'revert',
@@ -500,7 +595,7 @@ export enum MessageActionKindDTO {
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.
* 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. open_resource for a saved view also carries entity (logs/traces/metrics/meter) so the frontend routes to the correct Explorer.
*/
export interface MessageActionDTO {
kind: MessageActionKindDTO;
@@ -517,6 +612,7 @@ export interface MessageActionDTO {
signal?: MessageActionDTOSignal;
query?: MessageActionDTOQuery;
url?: MessageActionDTOUrl;
entity?: MessageActionDTOEntity;
}
export enum MessageContentTypeDTO {
@@ -590,6 +686,26 @@ export interface MessageSummaryDTO {
updatedAt: string;
}
export enum PageTypeDTO {
homepage = 'homepage',
dashboard_detail = 'dashboard_detail',
dashboard_list = 'dashboard_list',
panel_edit = 'panel_edit',
panel_fullscreen = 'panel_fullscreen',
logs_explorer = 'logs_explorer',
log_detail = 'log_detail',
traces_explorer = 'traces_explorer',
trace_detail = 'trace_detail',
metrics_explorer = 'metrics_explorer',
service_detail = 'service_detail',
services_list = 'services_list',
alert_edit = 'alert_edit',
alert_list = 'alert_list',
alert_new = 'alert_new',
alerts_triggered = 'alerts_triggered',
infra_entity_detail = 'infra_entity_detail',
other = 'other',
}
export enum ReadinessChecksDTODatabase {
ok = 'ok',
failed = 'failed',
@@ -990,8 +1106,10 @@ export type MessageActionEventDTOQuery = MessageActionEventDTOQueryAnyOf | null;
export type MessageActionEventDTOUrl = string | null;
export type MessageActionEventDTOEntity = SavedViewEntityDTO | 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.
* 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. open_resource for a saved view also carries entity (logs/traces/metrics/meter) so the frontend routes to the correct Explorer.
*/
export interface MessageActionEventDTO {
kind: MessageActionKindDTO;
@@ -1008,6 +1126,7 @@ export interface MessageActionEventDTO {
signal?: MessageActionEventDTOSignal;
query?: MessageActionEventDTOQuery;
url?: MessageActionEventDTOUrl;
entity?: MessageActionEventDTOEntity;
}
export type MessageEventDTOActions = MessageActionEventDTO[] | null;
@@ -1385,3 +1504,21 @@ export type GetUsageApiV1AssistantUsageGetHeaders = {
*/
'X-SigNoz-URL'?: string | null;
};
export type GetChipsApiV1AssistantEmptyStateChipsGetParams = {
/**
* @description Frontend-declared page type. Typed as an enum, but unrecognized values are coerced to 'other' (not rejected) so a new frontend page type works before the backend knows it. The page type alone identifies the focused entity (e.g. trace_detail) for the 'Explain this …' chip; the agent reads the concrete entity from page context once a chip is clicked, so no separate entity id is needed.
*/
page_type: PageTypeDTO;
};
export type GetChipsApiV1AssistantEmptyStateChipsGetHeaders = {
/**
* @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,15 +18,20 @@ import type {
} from 'react-query';
import type {
CloneDashboardV2201,
CloneDashboardV2PathParameters,
CreateDashboardV2201,
CreateDashboardView201,
CreatePublicDashboard201,
CreatePublicDashboardPathParameters,
DashboardtypesPatchableDashboardV2DTO,
DashboardtypesPostableDashboardV2DTO,
DashboardtypesPostableDashboardViewDTO,
DashboardtypesPostablePublicDashboardDTO,
DashboardtypesUpdatableDashboardV2DTO,
DashboardtypesUpdatablePublicDashboardDTO,
DeleteDashboardV2PathParameters,
DeleteDashboardViewPathParameters,
DeletePublicDashboardPathParameters,
GetDashboardV2200,
GetDashboardV2PathParameters,
@@ -36,6 +41,7 @@ import type {
GetPublicDashboardPathParameters,
GetPublicDashboardWidgetQueryRange200,
GetPublicDashboardWidgetQueryRangePathParameters,
ListDashboardViews200,
ListDashboardsForUserV2200,
ListDashboardsForUserV2Params,
ListDashboardsV2200,
@@ -49,6 +55,8 @@ import type {
UnpinDashboardV2PathParameters,
UpdateDashboardV2200,
UpdateDashboardV2PathParameters,
UpdateDashboardView200,
UpdateDashboardViewPathParameters,
UpdatePublicDashboardPathParameters,
} from '../sigNoz.schemas';
@@ -63,7 +71,7 @@ export const deletePublicDashboard = (
{ id }: DeletePublicDashboardPathParameters,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<string>({
return GeneratedAPIInstance<void>({
url: `/api/v1/dashboards/${id}/public`,
method: 'DELETE',
signal,
@@ -346,7 +354,7 @@ export const updatePublicDashboard = (
dashboardtypesUpdatablePublicDashboardDTO?: BodyType<DashboardtypesUpdatablePublicDashboardDTO>,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<string>({
return GeneratedAPIInstance<void>({
url: `/api/v1/dashboards/${id}/public`,
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
@@ -648,6 +656,354 @@ export const invalidateGetPublicDashboardWidgetQueryRange = async (
return queryClient;
};
/**
* Returns every saved view in the calling user's org. Saved views are shared org-wide.
* @summary List dashboard saved views
*/
export const listDashboardViews = (signal?: AbortSignal) => {
return GeneratedAPIInstance<ListDashboardViews200>({
url: `/api/v2/dashboard_views`,
method: 'GET',
signal,
});
};
export const getListDashboardViewsQueryKey = () => {
return [`/api/v2/dashboard_views`] as const;
};
export const getListDashboardViewsQueryOptions = <
TData = Awaited<ReturnType<typeof listDashboardViews>>,
TError = ErrorType<RenderErrorResponseDTO>,
>(options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof listDashboardViews>>,
TError,
TData
>;
}) => {
const { query: queryOptions } = options ?? {};
const queryKey = queryOptions?.queryKey ?? getListDashboardViewsQueryKey();
const queryFn: QueryFunction<
Awaited<ReturnType<typeof listDashboardViews>>
> = ({ signal }) => listDashboardViews(signal);
return { queryKey, queryFn, ...queryOptions } as UseQueryOptions<
Awaited<ReturnType<typeof listDashboardViews>>,
TError,
TData
> & { queryKey: QueryKey };
};
export type ListDashboardViewsQueryResult = NonNullable<
Awaited<ReturnType<typeof listDashboardViews>>
>;
export type ListDashboardViewsQueryError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary List dashboard saved views
*/
export function useListDashboardViews<
TData = Awaited<ReturnType<typeof listDashboardViews>>,
TError = ErrorType<RenderErrorResponseDTO>,
>(options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof listDashboardViews>>,
TError,
TData
>;
}): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
const queryOptions = getListDashboardViewsQueryOptions(options);
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
queryKey: QueryKey;
};
return { ...query, queryKey: queryOptions.queryKey };
}
/**
* @summary List dashboard saved views
*/
export const invalidateListDashboardViews = async (
queryClient: QueryClient,
options?: InvalidateOptions,
): Promise<QueryClient> => {
await queryClient.invalidateQueries(
{ queryKey: getListDashboardViewsQueryKey() },
options,
);
return queryClient;
};
/**
* Persists the calling user's dashboard listing state (query, sort, order) as a named, reusable view shared across the org.
* @summary Create dashboard saved view
*/
export const createDashboardView = (
dashboardtypesPostableDashboardViewDTO?: BodyType<DashboardtypesPostableDashboardViewDTO>,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<CreateDashboardView201>({
url: `/api/v2/dashboard_views`,
method: 'POST',
headers: { 'Content-Type': 'application/json' },
data: dashboardtypesPostableDashboardViewDTO,
signal,
});
};
export const getCreateDashboardViewMutationOptions = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof createDashboardView>>,
TError,
{ data?: BodyType<DashboardtypesPostableDashboardViewDTO> },
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof createDashboardView>>,
TError,
{ data?: BodyType<DashboardtypesPostableDashboardViewDTO> },
TContext
> => {
const mutationKey = ['createDashboardView'];
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 createDashboardView>>,
{ data?: BodyType<DashboardtypesPostableDashboardViewDTO> }
> = (props) => {
const { data } = props ?? {};
return createDashboardView(data);
};
return { mutationFn, ...mutationOptions };
};
export type CreateDashboardViewMutationResult = NonNullable<
Awaited<ReturnType<typeof createDashboardView>>
>;
export type CreateDashboardViewMutationBody =
| BodyType<DashboardtypesPostableDashboardViewDTO>
| undefined;
export type CreateDashboardViewMutationError =
ErrorType<RenderErrorResponseDTO>;
/**
* @summary Create dashboard saved view
*/
export const useCreateDashboardView = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof createDashboardView>>,
TError,
{ data?: BodyType<DashboardtypesPostableDashboardViewDTO> },
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof createDashboardView>>,
TError,
{ data?: BodyType<DashboardtypesPostableDashboardViewDTO> },
TContext
> => {
return useMutation(getCreateDashboardViewMutationOptions(options));
};
/**
* Removes a saved view. Saved views are shared org-wide. Deleting a non-existent view returns 404.
* @summary Delete dashboard saved view
*/
export const deleteDashboardView = (
{ id }: DeleteDashboardViewPathParameters,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<void>({
url: `/api/v2/dashboard_views/${id}`,
method: 'DELETE',
signal,
});
};
export const getDeleteDashboardViewMutationOptions = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof deleteDashboardView>>,
TError,
{ pathParams: DeleteDashboardViewPathParameters },
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof deleteDashboardView>>,
TError,
{ pathParams: DeleteDashboardViewPathParameters },
TContext
> => {
const mutationKey = ['deleteDashboardView'];
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 deleteDashboardView>>,
{ pathParams: DeleteDashboardViewPathParameters }
> = (props) => {
const { pathParams } = props ?? {};
return deleteDashboardView(pathParams);
};
return { mutationFn, ...mutationOptions };
};
export type DeleteDashboardViewMutationResult = NonNullable<
Awaited<ReturnType<typeof deleteDashboardView>>
>;
export type DeleteDashboardViewMutationError =
ErrorType<RenderErrorResponseDTO>;
/**
* @summary Delete dashboard saved view
*/
export const useDeleteDashboardView = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof deleteDashboardView>>,
TError,
{ pathParams: DeleteDashboardViewPathParameters },
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof deleteDashboardView>>,
TError,
{ pathParams: DeleteDashboardViewPathParameters },
TContext
> => {
return useMutation(getDeleteDashboardViewMutationOptions(options));
};
/**
* Replaces a saved view's name and data. Saved views are shared org-wide.
* @summary Update dashboard saved view
*/
export const updateDashboardView = (
{ id }: UpdateDashboardViewPathParameters,
dashboardtypesPostableDashboardViewDTO?: BodyType<DashboardtypesPostableDashboardViewDTO>,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<UpdateDashboardView200>({
url: `/api/v2/dashboard_views/${id}`,
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
data: dashboardtypesPostableDashboardViewDTO,
signal,
});
};
export const getUpdateDashboardViewMutationOptions = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof updateDashboardView>>,
TError,
{
pathParams: UpdateDashboardViewPathParameters;
data?: BodyType<DashboardtypesPostableDashboardViewDTO>;
},
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof updateDashboardView>>,
TError,
{
pathParams: UpdateDashboardViewPathParameters;
data?: BodyType<DashboardtypesPostableDashboardViewDTO>;
},
TContext
> => {
const mutationKey = ['updateDashboardView'];
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 updateDashboardView>>,
{
pathParams: UpdateDashboardViewPathParameters;
data?: BodyType<DashboardtypesPostableDashboardViewDTO>;
}
> = (props) => {
const { pathParams, data } = props ?? {};
return updateDashboardView(pathParams, data);
};
return { mutationFn, ...mutationOptions };
};
export type UpdateDashboardViewMutationResult = NonNullable<
Awaited<ReturnType<typeof updateDashboardView>>
>;
export type UpdateDashboardViewMutationBody =
| BodyType<DashboardtypesPostableDashboardViewDTO>
| undefined;
export type UpdateDashboardViewMutationError =
ErrorType<RenderErrorResponseDTO>;
/**
* @summary Update dashboard saved view
*/
export const useUpdateDashboardView = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof updateDashboardView>>,
TError,
{
pathParams: UpdateDashboardViewPathParameters;
data?: BodyType<DashboardtypesPostableDashboardViewDTO>;
},
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof updateDashboardView>>,
TError,
{
pathParams: UpdateDashboardViewPathParameters;
data?: BodyType<DashboardtypesPostableDashboardViewDTO>;
},
TContext
> => {
return useMutation(getUpdateDashboardViewMutationOptions(options));
};
/**
* Returns a page of v2-shape dashboards for the org. This is the pure, user-independent list — it carries no pin state. Use ListDashboardsForUserV2 for the personalized, pin-aware list. Supports a filter DSL (`query`), sort (`updated_at`/`created_at`/`name`), order (`asc`/`desc`), and offset-based pagination (`limit`/`offset`).
* @summary List dashboards (v2)
@@ -836,7 +1192,7 @@ export const deleteDashboardV2 = (
{ id }: DeleteDashboardV2PathParameters,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<string>({
return GeneratedAPIInstance<void>({
url: `/api/v2/dashboards/${id}`,
method: 'DELETE',
signal,
@@ -1206,6 +1562,85 @@ export const useUpdateDashboardV2 = <
> => {
return useMutation(getUpdateDashboardV2MutationOptions(options));
};
/**
* This endpoint clones an existing v2-shape dashboard. User and integration dashboards can be cloned; system dashboards are rejected. The clone keeps the source's display name, panels, and tags, but gets a freshly generated unique internal name and is always created as an unlocked user dashboard owned by the caller.
* @summary Clone dashboard (v2)
*/
export const cloneDashboardV2 = (
{ id }: CloneDashboardV2PathParameters,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<CloneDashboardV2201>({
url: `/api/v2/dashboards/${id}/clone`,
method: 'POST',
signal,
});
};
export const getCloneDashboardV2MutationOptions = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof cloneDashboardV2>>,
TError,
{ pathParams: CloneDashboardV2PathParameters },
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof cloneDashboardV2>>,
TError,
{ pathParams: CloneDashboardV2PathParameters },
TContext
> => {
const mutationKey = ['cloneDashboardV2'];
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 cloneDashboardV2>>,
{ pathParams: CloneDashboardV2PathParameters }
> = (props) => {
const { pathParams } = props ?? {};
return cloneDashboardV2(pathParams);
};
return { mutationFn, ...mutationOptions };
};
export type CloneDashboardV2MutationResult = NonNullable<
Awaited<ReturnType<typeof cloneDashboardV2>>
>;
export type CloneDashboardV2MutationError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary Clone dashboard (v2)
*/
export const useCloneDashboardV2 = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof cloneDashboardV2>>,
TError,
{ pathParams: CloneDashboardV2PathParameters },
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof cloneDashboardV2>>,
TError,
{ pathParams: CloneDashboardV2PathParameters },
TContext
> => {
return useMutation(getCloneDashboardV2MutationOptions(options));
};
/**
* This endpoint unlocks a v2-shape dashboard. Only the dashboard's creator or an org admin may lock or unlock.
* @summary Unlock dashboard (v2)
@@ -1214,7 +1649,7 @@ export const unlockDashboardV2 = (
{ id }: UnlockDashboardV2PathParameters,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<string>({
return GeneratedAPIInstance<void>({
url: `/api/v2/dashboards/${id}/lock`,
method: 'DELETE',
signal,
@@ -1293,7 +1728,7 @@ export const lockDashboardV2 = (
{ id }: LockDashboardV2PathParameters,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<string>({
return GeneratedAPIInstance<void>({
url: `/api/v2/dashboards/${id}/lock`,
method: 'PUT',
signal,
@@ -1471,7 +1906,7 @@ export const unpinDashboardV2 = (
{ id }: UnpinDashboardV2PathParameters,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<string>({
return GeneratedAPIInstance<void>({
url: `/api/v2/users/me/dashboards/${id}/pins`,
method: 'DELETE',
signal,
@@ -1550,7 +1985,7 @@ export const pinDashboardV2 = (
{ id }: PinDashboardV2PathParameters,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<string>({
return GeneratedAPIInstance<void>({
url: `/api/v2/users/me/dashboards/${id}/pins`,
method: 'PUT',
signal,

View File

@@ -23,6 +23,7 @@ import type {
GetLLMPricingRulePathParameters,
ListLLMPricingRules200,
ListLLMPricingRulesParams,
ListUnmappedLLMModels200,
LlmpricingruletypesUpdatableLLMPricingRulesDTO,
RenderErrorResponseDTO,
} from '../sigNoz.schemas';
@@ -393,3 +394,87 @@ export const invalidateGetLLMPricingRule = async (
return queryClient;
};
/**
* Returns models seen in the last hour of trace data (gen_ai.request.model) that no pricing rule pattern matches, so the user can add them to an existing rule or create a new one.
* @summary List unmapped models
*/
export const listUnmappedLLMModels = (signal?: AbortSignal) => {
return GeneratedAPIInstance<ListUnmappedLLMModels200>({
url: `/api/v1/llm_pricing_rules/unmapped_models`,
method: 'GET',
signal,
});
};
export const getListUnmappedLLMModelsQueryKey = () => {
return [`/api/v1/llm_pricing_rules/unmapped_models`] as const;
};
export const getListUnmappedLLMModelsQueryOptions = <
TData = Awaited<ReturnType<typeof listUnmappedLLMModels>>,
TError = ErrorType<RenderErrorResponseDTO>,
>(options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof listUnmappedLLMModels>>,
TError,
TData
>;
}) => {
const { query: queryOptions } = options ?? {};
const queryKey = queryOptions?.queryKey ?? getListUnmappedLLMModelsQueryKey();
const queryFn: QueryFunction<
Awaited<ReturnType<typeof listUnmappedLLMModels>>
> = ({ signal }) => listUnmappedLLMModels(signal);
return { queryKey, queryFn, ...queryOptions } as UseQueryOptions<
Awaited<ReturnType<typeof listUnmappedLLMModels>>,
TError,
TData
> & { queryKey: QueryKey };
};
export type ListUnmappedLLMModelsQueryResult = NonNullable<
Awaited<ReturnType<typeof listUnmappedLLMModels>>
>;
export type ListUnmappedLLMModelsQueryError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary List unmapped models
*/
export function useListUnmappedLLMModels<
TData = Awaited<ReturnType<typeof listUnmappedLLMModels>>,
TError = ErrorType<RenderErrorResponseDTO>,
>(options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof listUnmappedLLMModels>>,
TError,
TData
>;
}): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
const queryOptions = getListUnmappedLLMModelsQueryOptions(options);
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
queryKey: QueryKey;
};
return { ...query, queryKey: queryOptions.queryKey };
}
/**
* @summary List unmapped models
*/
export const invalidateListUnmappedLLMModels = async (
queryClient: QueryClient,
options?: InvalidateOptions,
): Promise<QueryClient> => {
await queryClient.invalidateQueries(
{ queryKey: getListUnmappedLLMModelsQueryKey() },
options,
);
return queryClient;
};

View File

@@ -37,7 +37,7 @@ export const handleExportRawDataPOST = (
params?: HandleExportRawDataPOSTParams,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<string>({
return GeneratedAPIInstance<void>({
url: `/api/v1/export_raw_data`,
method: 'POST',
headers: { 'Content-Type': 'application/json' },

View File

@@ -680,7 +680,7 @@ export const updateMetricMetadata = (
metricsexplorertypesUpdateMetricMetadataRequestDTO?: BodyType<MetricsexplorertypesUpdateMetricMetadataRequestDTO>,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<string>({
return GeneratedAPIInstance<void>({
url: `/api/v2/metrics/${metricName}/metadata`,
method: 'POST',
headers: { 'Content-Type': 'application/json' },

View File

@@ -203,7 +203,7 @@ export const deleteRole = (
{ id }: DeleteRolePathParameters,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<string>({
return GeneratedAPIInstance<void>({
url: `/api/v1/roles/${id}`,
method: 'DELETE',
signal,
@@ -372,7 +372,7 @@ export const patchRole = (
authtypesPatchableRoleDTO?: BodyType<AuthtypesPatchableRoleDTO>,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<string>({
return GeneratedAPIInstance<void>({
url: `/api/v1/roles/${id}`,
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
@@ -572,7 +572,7 @@ export const patchObjects = (
coretypesPatchableObjectsDTO?: BodyType<CoretypesPatchableObjectsDTO>,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<string>({
return GeneratedAPIInstance<void>({
url: `/api/v1/roles/${id}/relations/${relation}/objects`,
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },

View File

@@ -222,7 +222,7 @@ export const deleteServiceAccount = (
{ id }: DeleteServiceAccountPathParameters,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<string>({
return GeneratedAPIInstance<void>({
url: `/api/v1/service_accounts/${id}`,
method: 'DELETE',
signal,
@@ -405,7 +405,7 @@ export const updateServiceAccount = (
serviceaccounttypesPostableServiceAccountDTO?: BodyType<ServiceaccounttypesPostableServiceAccountDTO>,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<string>({
return GeneratedAPIInstance<void>({
url: `/api/v1/service_accounts/${id}`,
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
@@ -707,7 +707,7 @@ export const revokeServiceAccountKey = (
{ id, fid }: RevokeServiceAccountKeyPathParameters,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<string>({
return GeneratedAPIInstance<void>({
url: `/api/v1/service_accounts/${id}/keys/${fid}`,
method: 'DELETE',
signal,
@@ -788,7 +788,7 @@ export const updateServiceAccountKey = (
serviceaccounttypesUpdatableFactorAPIKeyDTO?: BodyType<ServiceaccounttypesUpdatableFactorAPIKeyDTO>,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<string>({
return GeneratedAPIInstance<void>({
url: `/api/v1/service_accounts/${id}/keys/${fid}`,
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
@@ -1090,7 +1090,7 @@ export const deleteServiceAccountRole = (
{ id, rid }: DeleteServiceAccountRolePathParameters,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<string>({
return GeneratedAPIInstance<void>({
url: `/api/v1/service_accounts/${id}/roles/${rid}`,
method: 'DELETE',
signal,
@@ -1254,7 +1254,7 @@ export const updateMyServiceAccount = (
serviceaccounttypesPostableServiceAccountDTO?: BodyType<ServiceaccounttypesPostableServiceAccountDTO>,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<string>({
return GeneratedAPIInstance<void>({
url: `/api/v1/service_accounts/me`,
method: 'PUT',
headers: { 'Content-Type': 'application/json' },

View File

@@ -2143,6 +2143,10 @@ export interface ErrorsResponseerroradditionalDTO {
* @type string
*/
message?: string;
/**
* @type array
*/
suggestions?: string[];
}
export interface ErrorsResponseretryjsonDTO {
@@ -2158,10 +2162,6 @@ export interface ErrorsJSONDTO {
* @type array
*/
errors?: ErrorsResponseerroradditionalDTO[];
/**
* @type array
*/
invalidReferences?: string[];
/**
* @type string
*/
@@ -2645,6 +2645,14 @@ export enum CloudintegrationtypesServiceIDDTO {
appservice = 'appservice',
containerapp = 'containerapp',
aks = 'aks',
sqldatabase = 'sqldatabase',
sqldatabasemi = 'sqldatabasemi',
mysqlflexibleserver = 'mysqlflexibleserver',
postgresqlflexibleserver = 'postgresqlflexibleserver',
mongodb = 'mongodb',
cosmosdb = 'cosmosdb',
cassandradb = 'cassandradb',
redis = 'redis',
}
export type CloudintegrationtypesCloudIntegrationServiceDTOAnyOf = {
/**
@@ -4625,6 +4633,54 @@ export interface DashboardtypesDashboardSpecDTO {
variables: DashboardtypesVariableDTO[];
}
export enum DashboardtypesListOrderDTO {
asc = 'asc',
desc = 'desc',
}
export enum DashboardtypesListSortDTO {
updated_at = 'updated_at',
created_at = 'created_at',
name = 'name',
}
export interface DashboardtypesDashboardViewDataDTO {
order?: DashboardtypesListOrderDTO;
/**
* @type string
*/
query?: string;
sort?: DashboardtypesListSortDTO;
/**
* @type string
*/
version: string;
}
export interface DashboardtypesDashboardViewDTO {
/**
* @type string
* @format date-time
*/
createdAt?: string;
data: DashboardtypesDashboardViewDataDTO;
/**
* @type string
*/
id: string;
/**
* @type string
*/
name: string;
/**
* @type string
*/
orgId: string;
/**
* @type string
* @format date-time
*/
updatedAt?: string;
}
export enum DashboardtypesDatasourcePluginKindDTO {
'signoz/Datasource' = 'signoz/Datasource',
}
@@ -4736,15 +4792,6 @@ export interface DashboardtypesJSONPatchOperationDTO {
value?: unknown;
}
export enum DashboardtypesListOrderDTO {
asc = 'asc',
desc = 'desc',
}
export enum DashboardtypesListSortDTO {
updated_at = 'updated_at',
created_at = 'created_at',
name = 'name',
}
export interface DashboardtypesListedDashboardV2SpecDTO {
display?: DashboardtypesDisplayDTO;
}
@@ -4887,6 +4934,13 @@ export interface DashboardtypesListableDashboardV2DTO {
total: number;
}
export interface DashboardtypesListableDashboardViewDTO {
/**
* @type array
*/
views: DashboardtypesDashboardViewDTO[];
}
export enum DashboardtypesPanelPluginKindDTO {
'signoz/TimeSeriesPanel' = 'signoz/TimeSeriesPanel',
'signoz/BarChartPanel' = 'signoz/BarChartPanel',
@@ -4938,6 +4992,14 @@ export interface DashboardtypesPostableDashboardV2DTO {
tags: TagtypesPostableTagDTO[] | null;
}
export interface DashboardtypesPostableDashboardViewDTO {
data: DashboardtypesDashboardViewDataDTO;
/**
* @type string
*/
name: string;
}
export interface DashboardtypesPostablePublicDashboardDTO {
/**
* @type string
@@ -6475,6 +6537,33 @@ export interface LlmpricingruletypesGettablePricingRulesDTO {
total: number;
}
export interface LlmpricingruletypesUnmappedModelDTO {
/**
* @type string
*/
modelName: string;
/**
* @type string
*/
provider?: string;
/**
* @type integer
* @minimum 0
*/
spanCount: number;
}
export interface LlmpricingruletypesGettableUnmappedModelsDTO {
/**
* @type array,null
*/
items: LlmpricingruletypesUnmappedModelDTO[] | null;
/**
* @type integer
*/
total: number;
}
export interface LlmpricingruletypesUpdatableLLMPricingRuleDTO {
/**
* @type boolean
@@ -9412,6 +9501,14 @@ export type GetLLMPricingRule200 = {
status: string;
};
export type ListUnmappedLLMModels200 = {
data: LlmpricingruletypesGettableUnmappedModelsDTO;
/**
* @type string
*/
status: string;
};
export type ListPromotedAndIndexedPaths200 = {
/**
* @type array,null
@@ -9736,6 +9833,19 @@ export type UpdateSpanMapperPathParameters = {
groupId: string;
mapperId: string;
};
export type GetStats200Data = { [key: string]: unknown };
export type GetStats200 = {
/**
* @type object
*/
data: GetStats200Data;
/**
* @type string
*/
status: string;
};
export type GetTraceAggregationsPathParameters = {
traceID: string;
};
@@ -9816,6 +9926,36 @@ export type GetUserPreference200 = {
export type UpdateUserPreferencePathParameters = {
name: string;
};
export type ListDashboardViews200 = {
data: DashboardtypesListableDashboardViewDTO;
/**
* @type string
*/
status: string;
};
export type CreateDashboardView201 = {
data: DashboardtypesDashboardViewDTO;
/**
* @type string
*/
status: string;
};
export type DeleteDashboardViewPathParameters = {
id: string;
};
export type UpdateDashboardViewPathParameters = {
id: string;
};
export type UpdateDashboardView200 = {
data: DashboardtypesDashboardViewDTO;
/**
* @type string
*/
status: string;
};
export type ListDashboardsV2Params = {
/**
* @type string
@@ -9894,6 +10034,17 @@ export type UpdateDashboardV2200 = {
status: string;
};
export type CloneDashboardV2PathParameters = {
id: string;
};
export type CloneDashboardV2201 = {
data: DashboardtypesGettableDashboardV2DTO;
/**
* @type string
*/
status: string;
};
export type UnlockDashboardV2PathParameters = {
id: string;
};

View File

@@ -0,0 +1,96 @@
/**
* ! Do not edit manually
* * The file has been auto-generated using Orval for SigNoz
* * regenerate with 'pnpm generate:api'
* SigNoz
*/
import { useQuery } from 'react-query';
import type {
InvalidateOptions,
QueryClient,
QueryFunction,
QueryKey,
UseQueryOptions,
UseQueryResult,
} from 'react-query';
import type { GetStats200, RenderErrorResponseDTO } from '../sigNoz.schemas';
import { GeneratedAPIInstance } from '../../../generatedAPIInstance';
import type { ErrorType } from '../../../generatedAPIInstance';
/**
* This endpoint returns the collected stats for the organization
* @summary Get stats
*/
export const getStats = (signal?: AbortSignal) => {
return GeneratedAPIInstance<GetStats200>({
url: `/api/v1/stats`,
method: 'GET',
signal,
});
};
export const getGetStatsQueryKey = () => {
return [`/api/v1/stats`] as const;
};
export const getGetStatsQueryOptions = <
TData = Awaited<ReturnType<typeof getStats>>,
TError = ErrorType<RenderErrorResponseDTO>,
>(options?: {
query?: UseQueryOptions<Awaited<ReturnType<typeof getStats>>, TError, TData>;
}) => {
const { query: queryOptions } = options ?? {};
const queryKey = queryOptions?.queryKey ?? getGetStatsQueryKey();
const queryFn: QueryFunction<Awaited<ReturnType<typeof getStats>>> = ({
signal,
}) => getStats(signal);
return { queryKey, queryFn, ...queryOptions } as UseQueryOptions<
Awaited<ReturnType<typeof getStats>>,
TError,
TData
> & { queryKey: QueryKey };
};
export type GetStatsQueryResult = NonNullable<
Awaited<ReturnType<typeof getStats>>
>;
export type GetStatsQueryError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary Get stats
*/
export function useGetStats<
TData = Awaited<ReturnType<typeof getStats>>,
TError = ErrorType<RenderErrorResponseDTO>,
>(options?: {
query?: UseQueryOptions<Awaited<ReturnType<typeof getStats>>, TError, TData>;
}): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
const queryOptions = getGetStatsQueryOptions(options);
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
queryKey: QueryKey;
};
return { ...query, queryKey: queryOptions.queryKey };
}
/**
* @summary Get stats
*/
export const invalidateGetStats = async (
queryClient: QueryClient,
options?: InvalidateOptions,
): Promise<QueryClient> => {
await queryClient.invalidateQueries(
{ queryKey: getGetStatsQueryKey() },
options,
);
return queryClient;
};

View File

@@ -0,0 +1,24 @@
import axios from 'api';
import { AxiosResponse } from 'axios';
import { ViewProps } from 'types/api/saveViews/types';
/**
* Fetches a single saved view by ID (`GET /api/v1/explorer/views/{viewId}`).
*
* Hand-maintained alongside the other `api/saveView/*` clients — explorer views
* are not in `docs/api/openapi.yml`, so Orval does not generate a hook here
* (unlike e.g. `useGetChannelByID` under `api/generated/services/channels`).
*
* Used by the AI assistant "Open view" action to load `compositeQuery` and
* navigate to the correct explorer without listing every view per source page.
* See `container/AIAssistant/components/ActionsSection/utils/openSavedView.ts`.
*/
export interface GetViewByIdProps {
status: string;
data: ViewProps;
}
export const getViewById = (
viewKey: string,
): Promise<AxiosResponse<GetViewByIdProps>> =>
axios.get(`/explorer/views/${viewKey}`);

View File

@@ -38,8 +38,8 @@ export enum LOCALSTORAGE {
DISSMISSED_COST_METER_INFO = 'DISMISSED_COST_METER_INFO',
DISMISSED_API_KEYS_DEPRECATION_BANNER = 'DISMISSED_API_KEYS_DEPRECATION_BANNER',
TRACE_DETAILS_SPAN_DETAILS_POSITION = 'TRACE_DETAILS_SPAN_DETAILS_POSITION',
TRACE_DETAILS_PREFER_OLD_VIEW = 'TRACE_DETAILS_PREFER_OLD_VIEW',
LICENSE_KEY_CALLOUT_DISMISSED = 'LICENSE_KEY_CALLOUT_DISMISSED',
TRACE_DETAILS_PREFER_OLD_VIEW = 'TRACE_DETAILS_PREFER_OLD_VIEW',
DASHBOARD_PREFERENCES = 'DASHBOARD_PREFERENCES',
ACTIVE_SIGNOZ_INSTANCE_URL = 'ACTIVE_SIGNOZ_INSTANCE_URL',
DASHBOARDS_LIST_VISIBLE_COLUMNS = 'DASHBOARDS_LIST_VISIBLE_COLUMNS',

View File

@@ -113,4 +113,7 @@ export const REACT_QUERY_KEY = {
// Fields Selector Query Keys
GET_FIELDS_SELECTOR_SUGGESTIONS: 'GET_FIELDS_SELECTOR_SUGGESTIONS',
// AI Assistant Query Keys
AI_ASSISTANT_EMPTY_STATE_CHIPS: 'AI_ASSISTANT_EMPTY_STATE_CHIPS',
} as const;

View File

@@ -0,0 +1,150 @@
import { QueryParams } from 'constants/query';
import ROUTES from 'constants/routes';
import { getAutoContexts } from '../getAutoContexts';
describe('getAutoContexts', () => {
it('returns alert detail context on alert overview with ruleId', () => {
const ruleId = 'rule-abc';
const search = `?${QueryParams.ruleId}=${ruleId}&${QueryParams.relativeTime}=1h`;
const contexts = getAutoContexts(ROUTES.ALERT_OVERVIEW, search);
expect(contexts).toStrictEqual([
{
source: 'auto',
type: 'alert',
resourceId: ruleId,
metadata: {
page: 'alert_detail',
ruleId,
},
},
]);
});
it('returns alert detail context on alert history with ruleId', () => {
const ruleId = 'rule-xyz';
const startTime = '1700000000000';
const endTime = '1700003600000';
const search = `?${QueryParams.ruleId}=${ruleId}&${QueryParams.startTime}=${startTime}&${QueryParams.endTime}=${endTime}`;
const contexts = getAutoContexts(ROUTES.ALERT_HISTORY, search);
expect(contexts).toStrictEqual([
{
source: 'auto',
type: 'alert',
resourceId: ruleId,
metadata: {
page: 'alert_detail',
ruleId,
timeRange: {
start: Number(startTime),
end: Number(endTime),
},
},
},
]);
});
it('returns triggered alerts context on alert history without ruleId', () => {
const contexts = getAutoContexts(ROUTES.ALERT_HISTORY, '');
expect(contexts).toStrictEqual([
{
source: 'auto',
type: 'alert',
resourceId: null,
metadata: {
page: 'alerts_triggered',
},
},
]);
});
it('resolves alert list tabs on /alerts', () => {
expect(getAutoContexts(ROUTES.LIST_ALL_ALERT, '')).toStrictEqual([
{
source: 'auto',
type: 'alert',
resourceId: null,
metadata: { page: 'alert_list' },
},
]);
expect(
getAutoContexts(ROUTES.LIST_ALL_ALERT, '?tab=AlertRules'),
).toStrictEqual([
{
source: 'auto',
type: 'alert',
resourceId: null,
metadata: { page: 'alert_list' },
},
]);
expect(
getAutoContexts(ROUTES.LIST_ALL_ALERT, '?tab=TriggeredAlerts'),
).toStrictEqual([
{
source: 'auto',
type: 'alert',
resourceId: null,
metadata: { page: 'alerts_triggered' },
},
]);
expect(
getAutoContexts(ROUTES.LIST_ALL_ALERT, '?tab=Configuration'),
).toStrictEqual([
{
source: 'auto',
type: 'alert',
resourceId: null,
metadata: { page: 'alert_list' },
},
]);
});
it('returns dashboard detail context on dashboard page', () => {
const dashboardId = 'dash-123';
const pathname = ROUTES.DASHBOARD.replace(':dashboardId', dashboardId);
const contexts = getAutoContexts(pathname, '');
expect(contexts).toStrictEqual([
{
source: 'auto',
type: 'dashboard',
resourceId: dashboardId,
metadata: {
page: 'dashboard_detail',
},
},
]);
});
it('returns empty array on alert overview without ruleId', () => {
const contexts = getAutoContexts(ROUTES.ALERT_OVERVIEW, '');
expect(contexts).toStrictEqual([]);
});
it('emits no auto-context on /home (no attachable resource)', () => {
expect(getAutoContexts(ROUTES.HOME, '')).toStrictEqual([]);
});
it('emits no auto-context on infrastructure monitoring routes', () => {
expect(
getAutoContexts(ROUTES.INFRASTRUCTURE_MONITORING_BASE, ''),
).toStrictEqual([]);
expect(
getAutoContexts(
ROUTES.INFRASTRUCTURE_MONITORING_HOSTS,
'?selectedItem=host-1',
),
).toStrictEqual([]);
});
});

View File

@@ -0,0 +1,87 @@
import { PageTypeDTO } from 'api/ai-assistant/sigNozAIAssistantAPI.schemas';
import { QueryParams } from 'constants/query';
import ROUTES from 'constants/routes';
import { resolvePageType } from '../resolvePageType';
describe('resolvePageType', () => {
it('returns other for the standalone assistant surface', () => {
expect(
resolvePageType('/services', '', { isStandaloneAssistant: true }),
).toBe(PageTypeDTO.other);
});
it('returns dashboard_detail on a dashboard page', () => {
const pathname = ROUTES.DASHBOARD.replace(':dashboardId', 'dash-123');
expect(resolvePageType(pathname, '')).toBe(PageTypeDTO.dashboard_detail);
});
it('returns alerts_triggered on alert history without ruleId', () => {
expect(resolvePageType(ROUTES.ALERT_HISTORY, '')).toBe(
PageTypeDTO.alerts_triggered,
);
});
it('resolves alert list tabs on /alerts', () => {
expect(resolvePageType(ROUTES.LIST_ALL_ALERT, '')).toBe(
PageTypeDTO.alert_list,
);
expect(resolvePageType(ROUTES.LIST_ALL_ALERT, '?tab=AlertRules')).toBe(
PageTypeDTO.alert_list,
);
expect(resolvePageType(ROUTES.LIST_ALL_ALERT, '?tab=TriggeredAlerts')).toBe(
PageTypeDTO.alerts_triggered,
);
expect(resolvePageType(ROUTES.LIST_ALL_ALERT, '?tab=Configuration')).toBe(
PageTypeDTO.alert_list,
);
});
it('returns log_detail when logs explorer has activeLogId', () => {
const search = `?${QueryParams.activeLogId}=log-1`;
expect(resolvePageType(ROUTES.LOGS_EXPLORER, search)).toBe(
PageTypeDTO.log_detail,
);
});
it('returns other for unmapped routes', () => {
expect(resolvePageType(ROUTES.ALERT_OVERVIEW, '')).toBe(PageTypeDTO.other);
});
it('returns other for the app root route (no contextual mapping)', () => {
expect(resolvePageType(ROUTES.HOME_PAGE, '')).toBe(PageTypeDTO.other);
});
it('returns homepage on /home', () => {
expect(resolvePageType(ROUTES.HOME, '')).toBe(PageTypeDTO.homepage);
});
it('returns infra_entity_detail on infrastructure monitoring routes', () => {
expect(resolvePageType(ROUTES.INFRASTRUCTURE_MONITORING_BASE, '')).toBe(
PageTypeDTO.infra_entity_detail,
);
expect(resolvePageType(ROUTES.INFRASTRUCTURE_MONITORING_HOSTS, '')).toBe(
PageTypeDTO.infra_entity_detail,
);
expect(resolvePageType(ROUTES.INFRASTRUCTURE_MONITORING_KUBERNETES, '')).toBe(
PageTypeDTO.infra_entity_detail,
);
});
it('returns metrics_explorer on all metrics explorer routes', () => {
expect(resolvePageType(ROUTES.METRICS_EXPLORER_BASE, '')).toBe(
PageTypeDTO.metrics_explorer,
);
expect(resolvePageType(ROUTES.METRICS_EXPLORER, '')).toBe(
PageTypeDTO.metrics_explorer,
);
expect(resolvePageType(ROUTES.METRICS_EXPLORER_EXPLORER, '')).toBe(
PageTypeDTO.metrics_explorer,
);
expect(resolvePageType(ROUTES.METRICS_EXPLORER_VIEWS, '')).toBe(
PageTypeDTO.metrics_explorer,
);
});
});

View File

@@ -47,6 +47,15 @@ import { AIAssistantEvents, SuggestedPromptCategory } from '../../events';
import { useAIAssistantAnalyticsContext } from '../../hooks/useAIAssistantAnalyticsContext';
import { useAIAssistantStore } from '../../store/useAIAssistantStore';
import { openSavedViewByKey } from './utils/openSavedView';
import {
isSavedViewOpenAction,
resolveOpenResourceType,
resolveResourceId,
resolveSavedViewSourceHint,
} from './utils/resolveOpenResource';
import { ResourceType, resourceRoute } from './utils/resourceRoute';
import styles from './ActionsSection.module.scss';
interface ActionsSectionProps {
@@ -55,20 +64,6 @@ interface ActionsSectionProps {
messageId: string;
}
/**
* Resource-type strings the backend uses for `open_resource` and rollback
* actions. Centralized here so the route/module lookups below stay in sync.
*/
const ResourceType = {
dashboard: 'dashboard',
alert: 'alert',
service: 'service',
saved_view: 'saved_view',
logs_explorer: 'logs_explorer',
traces_explorer: 'traces_explorer',
metrics_explorer: 'metrics_explorer',
} as const;
/** Maps an open_resource action's resourceType to its product module name. */
function targetModuleForResource(resourceType: string): string | null {
switch (resourceType) {
@@ -78,6 +73,8 @@ function targetModuleForResource(resourceType: string): string | null {
return 'alerts';
case ResourceType.service:
return 'apm';
case ResourceType.channel:
return 'channels';
case ResourceType.saved_view:
return 'savedViews';
case ResourceType.logs_explorer:
@@ -140,39 +137,6 @@ function ActionIcon({
}
}
/**
* Resolves an `open_resource` action to an in-app route.
* Resource taxonomy mirrors `MessageContextDTOType`: dashboard, alert,
* saved_view, service, and the *_explorer signals.
*/
function resourceRoute(
resourceType: string,
resourceId: string,
): string | null {
switch (resourceType) {
case ResourceType.dashboard:
return ROUTES.DASHBOARD.replace(':dashboardId', resourceId);
case ResourceType.alert: {
const params = new URLSearchParams({ [QueryParams.ruleId]: resourceId });
return `${ROUTES.EDIT_ALERTS}?${params.toString()}`;
}
case ResourceType.service:
return ROUTES.SERVICE_METRICS.replace(':servicename', resourceId);
case ResourceType.saved_view:
// No detail route — saved views land on the list page.
// Caller may provide signal-aware metadata in future; default to logs.
return ROUTES.LOGS_SAVE_VIEWS;
case ResourceType.logs_explorer:
return ROUTES.LOGS_EXPLORER;
case ResourceType.traces_explorer:
return ROUTES.TRACES_EXPLORER;
case ResourceType.metrics_explorer:
return ROUTES.METRICS_EXPLORER_EXPLORER;
default:
return null;
}
}
/**
* The agent emits `action.query` as the SigNoz REST query-range request body:
*
@@ -484,6 +448,35 @@ export default function ActionsSection({
setResults((prev) => ({ ...prev, [key]: result }));
};
const runOpenSavedView = async (
key: string,
action: MessageActionDTO,
): Promise<void> => {
const resourceId = resolveResourceId(action);
if (!resourceId) {
return;
}
setResult(key, { state: 'loading' });
try {
await openSavedViewByKey(
resourceId,
resolveSavedViewSourceHint(action),
history,
);
void logEvent(AIAssistantEvents.ResourceOpened, {
threadId,
messageId,
targetModule: targetModuleForResource(ResourceType.saved_view),
resourceId,
});
setResult(key, { state: 'success' });
} catch (err) {
const message =
err instanceof Error ? err.message : 'Failed to open saved view';
setResult(key, { state: 'error', error: message });
}
};
const runRollback = async (
key: string,
action: MessageActionDTO,
@@ -502,6 +495,31 @@ export default function ActionsSection({
}
};
const handleOpenResource = (key: string, action: MessageActionDTO): void => {
if (isSavedViewOpenAction(action)) {
void runOpenSavedView(key, action);
return;
}
const resourceType = resolveOpenResourceType(action);
const resourceId = resolveResourceId(action);
if (!resourceType || !resourceId) {
return;
}
const path = resourceRoute(resourceType, resourceId);
if (!path) {
return;
}
void logEvent(AIAssistantEvents.ResourceOpened, {
threadId,
messageId,
targetModule: targetModuleForResource(resourceType),
resourceId,
});
history.push(path);
};
const handleClick = (key: string, action: MessageActionDTO): void => {
switch (action.kind) {
case MessageActionKindDTO.open_docs: {
@@ -542,21 +560,9 @@ export default function ActionsSection({
}
break;
}
case MessageActionKindDTO.open_resource: {
if (action.resourceType && action.resourceId) {
const path = resourceRoute(action.resourceType, action.resourceId);
if (path) {
void logEvent(AIAssistantEvents.ResourceOpened, {
threadId,
messageId,
targetModule: targetModuleForResource(action.resourceType),
resourceId: action.resourceId,
});
history.push(path);
}
}
case MessageActionKindDTO.open_resource:
handleOpenResource(key, action);
break;
}
case MessageActionKindDTO.undo:
case MessageActionKindDTO.revert:
case MessageActionKindDTO.restore: {

View File

@@ -0,0 +1,283 @@
import {
ApplyFilterSignalDTO,
MessageActionKindDTO,
SavedViewEntityDTO,
} from 'api/ai-assistant/sigNozAIAssistantAPI.schemas';
import { getAllViews } from 'api/saveView/getAllViews';
import { getViewById } from 'api/saveView/getViewById';
import ROUTES from 'constants/routes';
import { QueryParams } from 'constants/query';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { ICompositeMetricQuery } from 'types/api/alerts/compositeQuery';
import { AllViewsProps, ViewProps } from 'types/api/saveViews/types';
import { DataSource } from 'types/common/queryBuilder';
import { AxiosResponse } from 'axios';
import type { History } from 'history';
import {
buildExplorerNavigationUrl,
findSavedViewInLists,
openSavedView,
openSavedViewByKey,
} from '../openSavedView';
import {
entityToDataSource,
isSavedViewOpenAction,
resolveActionEntity,
resolveOpenResourceType,
resolveResourceId,
resolveResourceType,
resolveSavedViewSourceHint,
} from '../resolveOpenResource';
import { resourceRoute, ResourceType } from '../resourceRoute';
jest.mock('api/saveView/getAllViews');
jest.mock('api/saveView/getViewById');
jest.mock(
'lib/newQueryBuilder/queryBuilderMappers/mapQueryDataFromApi',
() => ({
mapQueryDataFromApi: jest.fn(() => ({
queryType: 'builder',
builder: {
queryData: [{ id: 'A' }],
queryFormulas: [],
queryTraceOperator: [],
},
})),
}),
);
const mockedGetAllViews = getAllViews as jest.MockedFunction<
typeof getAllViews
>;
const mockedGetViewById = getViewById as jest.MockedFunction<
typeof getViewById
>;
function makeView(id: string, sourcePage: DataSource): ViewProps {
return {
id,
name: `View ${id}`,
category: 'test',
createdAt: '2021-07-07T06:31:00.000Z',
createdBy: 'user',
updatedAt: '2021-07-07T06:33:00.000Z',
updatedBy: 'user',
sourcePage,
tags: [],
extraData: '',
compositeQuery: {
panelType: PANEL_TYPES.LIST,
} as ICompositeMetricQuery,
};
}
function mockViewsResponse(views: ViewProps[]): AxiosResponse<AllViewsProps> {
return {
data: { status: 'success', data: views },
} as AxiosResponse<AllViewsProps>;
}
function mockViewByIdResponse(
view: ViewProps,
): AxiosResponse<{ status: string; data: ViewProps }> {
return {
data: { status: 'success', data: view },
} as AxiosResponse<{ status: string; data: ViewProps }>;
}
describe('resourceRoute', () => {
it('returns null for saved_view so async navigation is used', () => {
expect(resourceRoute(ResourceType.saved_view, 'view-123')).toBeNull();
});
it('routes channels to the edit page', () => {
expect(resourceRoute(ResourceType.channel, 'channel-uuid-1')).toBe(
'/settings/channels/edit/channel-uuid-1',
);
});
});
describe('resolveOpenResource', () => {
it('reads entity from the action envelope', () => {
expect(
resolveActionEntity({
kind: MessageActionKindDTO.open_resource,
label: 'Open view',
entity: SavedViewEntityDTO.traces,
}),
).toBe(SavedViewEntityDTO.traces);
});
it('reads resource id from input.viewKey', () => {
expect(
resolveResourceId({
kind: MessageActionKindDTO.open_resource,
label: 'Open view',
input: { viewKey: 'abc-123' },
}),
).toBe('abc-123');
});
it('maps entity values to explorer data sources', () => {
expect(entityToDataSource('logs')).toBe(DataSource.LOGS);
expect(entityToDataSource('logs_explorer')).toBe(DataSource.LOGS);
expect(entityToDataSource('traces')).toBe(DataSource.TRACES);
});
it('prefers entity over signal for saved-view source hints', () => {
expect(
resolveSavedViewSourceHint({
kind: MessageActionKindDTO.open_resource,
label: 'Open view',
entity: SavedViewEntityDTO.traces,
signal: ApplyFilterSignalDTO.logs,
}),
).toBe(DataSource.TRACES);
});
it('falls back to signal when entity is absent', () => {
expect(
resolveSavedViewSourceHint({
kind: MessageActionKindDTO.open_resource,
label: 'Open view',
signal: ApplyFilterSignalDTO.metrics,
}),
).toBe(DataSource.METRICS);
});
it('normalises saved-view resource types', () => {
expect(
resolveResourceType({
kind: MessageActionKindDTO.open_resource,
label: 'Open view',
resourceType: 'saved-view',
}),
).toBe(ResourceType.saved_view);
});
it('detects open-view actions from label when id is present in input', () => {
expect(
isSavedViewOpenAction({
kind: MessageActionKindDTO.open_resource,
label: 'Open view',
input: { viewId: 'view-1' },
}),
).toBe(true);
});
it('resolves channel type from notification_channel alias', () => {
expect(
resolveResourceType({
kind: MessageActionKindDTO.open_resource,
label: 'Open channel',
resourceType: 'notification_channel',
}),
).toBe(ResourceType.channel);
});
it('infers channel type from Open channel label when resourceId is present', () => {
expect(
resolveOpenResourceType({
kind: MessageActionKindDTO.open_resource,
label: 'Open channel',
resourceId: 'channel-1',
}),
).toBe(ResourceType.channel);
});
});
describe('findSavedViewInLists', () => {
beforeEach(() => {
mockedGetAllViews.mockReset();
});
it('loads only the hinted source when entity is provided', async () => {
const tracesView = makeView('view-traces', DataSource.TRACES);
mockedGetAllViews.mockResolvedValueOnce(mockViewsResponse([tracesView]));
const result = await findSavedViewInLists('view-traces', DataSource.TRACES);
expect(result).toStrictEqual(tracesView);
expect(mockedGetAllViews).toHaveBeenCalledTimes(1);
expect(mockedGetAllViews).toHaveBeenCalledWith(DataSource.TRACES);
});
});
describe('buildExplorerNavigationUrl', () => {
it('encodes composite query and view selectors', () => {
const url = buildExplorerNavigationUrl(
ROUTES.LOGS_EXPLORER,
{ queryType: 'builder' } as never,
{
[QueryParams.panelTypes]: PANEL_TYPES.LIST,
[QueryParams.viewName]: 'My view',
[QueryParams.viewKey]: 'view-1',
},
);
expect(url).toContain(ROUTES.LOGS_EXPLORER);
expect(url).toContain(`${QueryParams.compositeQuery}=`);
expect(url).toContain(`${QueryParams.viewKey}=`);
});
});
describe('openSavedView', () => {
it('navigates with history.push and view query params', () => {
const push = jest.fn();
const history = { push } as unknown as History;
const view = makeView('view-logs', DataSource.LOGS);
openSavedView(view, history);
expect(push).toHaveBeenCalledTimes(1);
const pushedUrl = push.mock.calls[0][0] as string;
expect(pushedUrl).toContain(ROUTES.LOGS_EXPLORER);
expect(pushedUrl).toContain(QueryParams.viewKey);
});
});
describe('openSavedViewByKey', () => {
beforeEach(() => {
mockedGetAllViews.mockReset();
mockedGetViewById.mockReset();
});
it('prefers the direct view lookup endpoint', async () => {
const view = makeView('view-logs', DataSource.LOGS);
mockedGetViewById.mockResolvedValueOnce(mockViewByIdResponse(view));
const push = jest.fn();
const history = { push } as unknown as History;
await openSavedViewByKey('view-logs', DataSource.LOGS, history);
expect(mockedGetViewById).toHaveBeenCalledWith('view-logs');
expect(mockedGetAllViews).not.toHaveBeenCalled();
expect(push).toHaveBeenCalled();
});
it('falls back to list probing when direct lookup fails', async () => {
const view = makeView('view-traces', DataSource.TRACES);
mockedGetViewById.mockRejectedValueOnce(new Error('not found'));
mockedGetAllViews.mockResolvedValueOnce(mockViewsResponse([view]));
const push = jest.fn();
const history = { push } as unknown as History;
await openSavedViewByKey('view-traces', DataSource.TRACES, history);
expect(mockedGetAllViews).toHaveBeenCalledWith(DataSource.TRACES);
expect(push).toHaveBeenCalled();
});
it('throws when the saved view does not exist', async () => {
mockedGetViewById.mockRejectedValueOnce(new Error('not found'));
mockedGetAllViews.mockResolvedValue(mockViewsResponse([]));
await expect(
openSavedViewByKey('missing', DataSource.LOGS, {
push: jest.fn(),
} as unknown as History),
).rejects.toThrow('Saved view not found');
});
});

View File

@@ -0,0 +1,117 @@
import { getAllViews } from 'api/saveView/getAllViews';
import { getViewById } from 'api/saveView/getViewById';
import { QueryParams } from 'constants/query';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { mapQueryDataFromApi } from 'lib/newQueryBuilder/queryBuilderMappers/mapQueryDataFromApi';
import { SOURCEPAGE_VS_ROUTES } from 'pages/SaveView/constants';
import { ViewProps } from 'types/api/saveViews/types';
import { DataSource } from 'types/common/queryBuilder';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
import { History } from 'history';
type SavedViewSourceHint = DataSource | 'meter';
const DEFAULT_PROBE_SOURCES: SavedViewSourceHint[] = [
DataSource.LOGS,
DataSource.TRACES,
DataSource.METRICS,
];
export async function findSavedViewInLists(
viewKey: string,
sourceHint?: SavedViewSourceHint | null,
): Promise<ViewProps | null> {
const sources = sourceHint ? [sourceHint] : DEFAULT_PROBE_SOURCES;
for (const source of sources) {
try {
const response = await getAllViews(source);
const match = response.data.data.find((view) => view.id === viewKey);
if (match) {
return match;
}
} catch {
// Probe the next source page when no entity hint is provided.
}
}
return null;
}
async function loadSavedView(
viewKey: string,
sourceHint?: SavedViewSourceHint | null,
): Promise<ViewProps> {
try {
const response = await getViewById(viewKey);
if (response.data?.data) {
return response.data.data;
}
} catch {
// Fall back to list probing when the direct lookup fails.
}
const fromList = await findSavedViewInLists(viewKey, sourceHint);
if (fromList) {
return fromList;
}
throw new Error('Saved view not found');
}
export function explorerRouteForSourcePage(
sourcePage: DataSource | string,
): (typeof SOURCEPAGE_VS_ROUTES)[keyof typeof SOURCEPAGE_VS_ROUTES] | null {
return SOURCEPAGE_VS_ROUTES[sourcePage] ?? null;
}
/**
* Builds an explorer URL the same way `redirectWithQueryBuilderData` does —
* without inheriting stale query params from the current page's `urlQuery`.
*/
export function buildExplorerNavigationUrl(
route: string,
query: Query,
searchParams: Record<string, unknown>,
): string {
const params = new URLSearchParams();
params.set(
QueryParams.compositeQuery,
encodeURIComponent(JSON.stringify(query)),
);
Object.entries(searchParams).forEach(([key, value]) => {
params.set(key, JSON.stringify(value));
});
return `${route}?${params.toString()}`;
}
export function openSavedView(view: ViewProps, history: History): void {
const route = explorerRouteForSourcePage(view.sourcePage);
if (!route) {
throw new Error('Unsupported saved view source');
}
if (!view.compositeQuery) {
throw new Error('Saved view is missing query data');
}
const query = mapQueryDataFromApi(view.compositeQuery);
const url = buildExplorerNavigationUrl(route, query, {
[QueryParams.panelTypes]: view.compositeQuery.panelType as PANEL_TYPES,
[QueryParams.viewName]: view.name,
[QueryParams.viewKey]: view.id,
});
history.push(url);
}
export async function openSavedViewByKey(
viewKey: string,
sourceHint: SavedViewSourceHint | null | undefined,
history: History,
): Promise<void> {
const view = await loadSavedView(viewKey, sourceHint);
openSavedView(view, history);
}
/** @deprecated Use findSavedViewInLists — kept for tests. */
export const findSavedView = findSavedViewInLists;

View File

@@ -0,0 +1,203 @@
import type { MessageActionDTO } from 'api/ai-assistant/sigNozAIAssistantAPI.schemas';
import {
ApplyFilterSignalDTO,
SavedViewEntityDTO,
} from 'api/ai-assistant/sigNozAIAssistantAPI.schemas';
import { DataSource } from 'types/common/queryBuilder';
import { ResourceType } from './resourceRoute';
function readString(value: unknown): string | null {
if (typeof value !== 'string') {
return null;
}
const trimmed = value.trim();
return trimmed.length > 0 ? trimmed : null;
}
/** Normalises backend resource-type strings to the taxonomy used in the UI. */
export function normalizeResourceType(
resourceType: string | null | undefined,
): string | null {
if (!resourceType) {
return null;
}
const normalized = resourceType.trim().toLowerCase().replace(/-/g, '_');
if (normalized === 'savedview') {
return ResourceType.saved_view;
}
if (
normalized === 'notification_channel' ||
normalized === 'notificationchannel'
) {
return ResourceType.channel;
}
return normalized;
}
/** Reads a resource type from the action envelope or its `input` payload. */
export function resolveResourceType(action: MessageActionDTO): string | null {
const direct = normalizeResourceType(action.resourceType);
if (direct) {
return direct;
}
const input = action.input;
if (!input) {
return null;
}
return (
normalizeResourceType(readString(input.resourceType)) ??
normalizeResourceType(readString(input.type))
);
}
/**
* Resolves the resource type for an `open_resource` action, including label-based
* fallbacks when the backend only sends a display label + id.
*/
export function resolveOpenResourceType(
action: MessageActionDTO,
): string | null {
const fromFields = resolveResourceType(action);
if (fromFields) {
return fromFields;
}
if (/open\s+channel/i.test(action.label) && resolveResourceId(action)) {
return ResourceType.channel;
}
return null;
}
/** Reads a resource id from `resourceId` or common `input` keys. */
export function resolveResourceId(action: MessageActionDTO): string | null {
const direct = readString(action.resourceId);
if (direct) {
return direct;
}
const input = action.input;
if (!input) {
return null;
}
for (const key of [
'resourceId',
'viewId',
'viewKey',
'channelId',
'id',
] as const) {
const value = readString(input[key]);
if (value) {
return value;
}
}
return null;
}
/** Reads `entity` from the action envelope or its `input` payload. */
export function resolveActionEntity(
action: MessageActionDTO,
): SavedViewEntityDTO | null {
if (action.entity) {
return action.entity;
}
const fromInput = readString(action.input?.entity);
if (!fromInput) {
return null;
}
return normalizeToSavedViewEntity(fromInput);
}
function normalizeToSavedViewEntity(value: string): SavedViewEntityDTO | null {
const source = entityToDataSource(value);
switch (source) {
case DataSource.LOGS:
return SavedViewEntityDTO.logs;
case DataSource.TRACES:
return SavedViewEntityDTO.traces;
case DataSource.METRICS:
return SavedViewEntityDTO.metrics;
case 'meter':
return SavedViewEntityDTO.meter;
default:
return null;
}
}
/**
* Maps an action `entity` to an explorer `DataSource` for saved-view lookups.
* Accepts both short (`logs`) and taxonomy (`logs_explorer`) values.
*/
export function entityToDataSource(
entity: SavedViewEntityDTO | string,
): DataSource | 'meter' | null {
const normalized = entity.trim().toLowerCase().replace(/-/g, '_');
switch (normalized) {
case SavedViewEntityDTO.logs:
case ResourceType.logs_explorer:
return DataSource.LOGS;
case SavedViewEntityDTO.traces:
case ResourceType.traces_explorer:
return DataSource.TRACES;
case SavedViewEntityDTO.metrics:
case ResourceType.metrics_explorer:
return DataSource.METRICS;
case SavedViewEntityDTO.meter:
return 'meter';
default:
return null;
}
}
/**
* Picks which explorer source page to search when resolving a saved view.
* Prefers `entity` (open_resource); falls back to `signal` only for legacy payloads.
*/
export function resolveSavedViewSourceHint(
action: MessageActionDTO,
): DataSource | 'meter' | null {
const entity = resolveActionEntity(action);
if (entity) {
const fromEntity = entityToDataSource(entity);
if (fromEntity) {
return fromEntity;
}
}
if (action.signal) {
switch (action.signal) {
case ApplyFilterSignalDTO.logs:
return DataSource.LOGS;
case ApplyFilterSignalDTO.traces:
return DataSource.TRACES;
case ApplyFilterSignalDTO.metrics:
return DataSource.METRICS;
default: {
const _exhaustive: never = action.signal;
return _exhaustive;
}
}
}
return null;
}
export function isSavedViewOpenAction(action: MessageActionDTO): boolean {
if (resolveResourceType(action) === ResourceType.saved_view) {
return true;
}
// Defensive: some agent payloads only set a human label + id in `input`.
return /open\s+view/i.test(action.label) && resolveResourceId(action) !== null;
}

View File

@@ -0,0 +1,50 @@
import ROUTES from 'constants/routes';
import { QueryParams } from 'constants/query';
/**
* Resource-type strings the backend uses for `open_resource` and rollback
* actions. Centralized here so route/module lookups stay in sync.
*/
export const ResourceType = {
dashboard: 'dashboard',
alert: 'alert',
service: 'service',
channel: 'channel',
saved_view: 'saved_view',
logs_explorer: 'logs_explorer',
traces_explorer: 'traces_explorer',
metrics_explorer: 'metrics_explorer',
} as const;
/**
* Resolves an `open_resource` action to an in-app route for synchronous
* navigation. Returns `null` for `saved_view` — callers must load the view
* by id and navigate with query-builder state instead.
*/
export function resourceRoute(
resourceType: string,
resourceId: string,
): string | null {
switch (resourceType) {
case ResourceType.dashboard:
return ROUTES.DASHBOARD.replace(':dashboardId', resourceId);
case ResourceType.alert: {
const params = new URLSearchParams({ [QueryParams.ruleId]: resourceId });
return `${ROUTES.EDIT_ALERTS}?${params.toString()}`;
}
case ResourceType.service:
return ROUTES.SERVICE_METRICS.replace(':servicename', resourceId);
case ResourceType.channel:
return ROUTES.CHANNELS_EDIT.replace(':channelId', resourceId);
case ResourceType.saved_view:
return null;
case ResourceType.logs_explorer:
return ROUTES.LOGS_EXPLORER;
case ResourceType.traces_explorer:
return ROUTES.TRACES_EXPLORER;
case ResourceType.metrics_explorer:
return ROUTES.METRICS_EXPLORER_EXPLORER;
default:
return null;
}
}

View File

@@ -101,6 +101,8 @@ function autoContextLabel(ctx: MessageContext): string {
return 'Panel (fullscreen)';
case 'dashboard_list':
return 'Dashboards';
case 'alert_detail':
return 'Current alert';
case 'alert_edit':
return 'Editing alert';
case 'alert_new':

View File

@@ -0,0 +1,26 @@
import type { ComponentProps } from 'react';
type MarkdownExternalLinkProps = ComponentProps<'a'> & {
// react-markdown passes `node` — accept and ignore it
// eslint-disable-next-line @typescript-eslint/no-explicit-any
node?: any;
};
export default function MarkdownExternalLink({
href,
children,
node: _node,
...props
}: MarkdownExternalLinkProps): JSX.Element {
return (
<a
href={href}
target="_blank"
rel="noopener noreferrer"
data-testid="ai-markdown-link"
{...props}
>
{children}
</a>
);
}

View File

@@ -11,6 +11,7 @@ import { Message, MessageBlock } from '../../types';
import ActionsSection from '../ActionsSection';
import ActivityGroup, { ActivityItem } from '../ActivityGroup';
import { RichCodeBlock } from '../blocks';
import MarkdownExternalLink from '../MarkdownExternalLink/MarkdownExternalLink';
import { MessageContext } from '../MessageContext';
import MessageFeedback from '../MessageFeedback';
import UserMessageActions from '../UserMessageActions';
@@ -37,7 +38,11 @@ function SmartPre({ children }: { children?: React.ReactNode }): JSX.Element {
}
const MD_PLUGINS = [remarkGfm];
const MD_COMPONENTS = { code: RichCodeBlock, pre: SmartPre };
const MD_COMPONENTS = {
code: RichCodeBlock,
pre: SmartPre,
a: MarkdownExternalLink,
};
type RenderGroup =
| { kind: 'text'; id: string; content: string }

View File

@@ -50,7 +50,7 @@
font-size: 10px;
color: var(--l3-foreground);
white-space: nowrap;
padding-left: 2px;
padding-left: 8px;
border-left: 1px solid var(--l2-border);
}

View File

@@ -13,6 +13,7 @@ import { StreamingEventItem } from '../../types';
import ActivityGroup, { ActivityItem } from '../ActivityGroup';
import ApprovalCard from '../ApprovalCard';
import { RichCodeBlock } from '../blocks';
import MarkdownExternalLink from '../MarkdownExternalLink/MarkdownExternalLink';
import ClarificationForm from '../ClarificationForm';
import messageStyles from '../MessageBubble/MessageBubble.module.scss';
@@ -30,7 +31,11 @@ function SmartPre({ children }: { children?: React.ReactNode }): JSX.Element {
}
const MD_PLUGINS = [remarkGfm];
const MD_COMPONENTS = { code: RichCodeBlock, pre: SmartPre };
const MD_COMPONENTS = {
code: RichCodeBlock,
pre: SmartPre,
a: MarkdownExternalLink,
};
type RenderGroup =
| { kind: 'text'; id: string; content: string }

View File

@@ -45,7 +45,7 @@
line-height: 1.45;
}
.emptySuggestions {
.suggestions {
display: flex;
flex-direction: column;
gap: 6px;
@@ -53,11 +53,8 @@
max-width: 360px;
}
.emptyChip {
display: flex;
align-items: center;
justify-content: flex-start !important;
gap: 8px;
.suggestion {
width: 100%;
padding: 10px 14px;
border: 1px solid var(--l2-border);
border-radius: var(--radius-2);
@@ -66,21 +63,16 @@
font-size: 12.5px;
text-align: left;
cursor: pointer;
transition: all 0.15s ease;
transition:
background 0.15s ease,
border-color 0.15s ease,
color 0.15s ease;
line-height: 1.35;
overflow-wrap: anywhere;
&:hover {
background: var(--l2-background);
border-color: var(--l3-border);
color: var(--l1-foreground);
}
svg {
flex-shrink: 0;
color: var(--l3-foreground);
}
&:hover svg {
color: var(--accent-primary);
}
}

View File

@@ -1,13 +1,5 @@
import { useCallback, useEffect, useRef } from 'react';
import { Button } from '@signozhq/ui/button';
import { Virtuoso, VirtuosoHandle } from 'react-virtuoso';
import {
Activity,
TriangleAlert,
ChartBar,
Search,
Zap,
} from '@signozhq/icons';
import Noz from 'components/Noz/Noz';
import logEvent from 'api/common/logEvent';
@@ -20,29 +12,7 @@ import MessageBubble from '../MessageBubble';
import StreamingMessage from '../StreamingMessage';
import styles from './VirtualizedMessages.module.scss';
const SUGGESTIONS = [
{
icon: TriangleAlert,
text: 'Show me the top errors in the last hour',
},
{
icon: Activity,
text: 'What services have the highest latency?',
},
{
icon: ChartBar,
text: 'Give me an overview of system health',
},
{
icon: Search,
text: 'Find slow database queries',
},
{
icon: Zap,
text: 'Which endpoints have the most 5xx errors?',
},
];
import { useEmptyStateChips } from './useEmptyStateChips';
const EMPTY_EVENTS: StreamingEventItem[] = [];
@@ -173,8 +143,10 @@ export default function VirtualizedMessages({
const showStreamingSlot =
isStreaming || Boolean(pendingApproval) || Boolean(pendingClarification);
const isEmptyState = messages.length === 0 && !showStreamingSlot;
const { chips: emptyStateChips } = useEmptyStateChips(isEmptyState);
if (messages.length === 0 && !showStreamingSlot) {
if (isEmptyState) {
return (
<div className={styles.empty}>
<div className={`${styles.emptyIcon} noz-wave`}>
@@ -184,24 +156,22 @@ export default function VirtualizedMessages({
<p className={styles.emptySubtitle}>
Ask questions about your traces, logs, metrics, and infrastructure.
</p>
<div className={styles.emptySuggestions}>
{SUGGESTIONS.map((s) => (
<Button
key={s.text}
variant="outlined"
color="secondary"
className={styles.emptyChip}
<div className={styles.suggestions}>
{emptyStateChips.map((chip) => (
<div
key={chip.id}
className={styles.suggestion}
onClick={(): void => {
void logEvent(AIAssistantEvents.SuggestedPromptClicked, {
promptId: s.text,
promptId: chip.id,
category: SuggestedPromptCategory.EmptyState,
});
onSendSuggestedPrompt(s.text);
onSendSuggestedPrompt(chip.text);
}}
prefix={<s.icon size={14} />}
data-testid={`empty-state-chip-${chip.id}`}
>
{s.text}
</Button>
{chip.text}
</div>
))}
</div>
</div>

View File

@@ -0,0 +1,25 @@
import type { ChipDTO } from 'api/ai-assistant/sigNozAIAssistantAPI.schemas';
/** Static empty-state chips used when the contextual chips API is unavailable. */
export const EMPTY_STATE_CHIPS_FALLBACK: ChipDTO[] = [
{
id: 'top_errors_last_hour',
text: 'Show me the top errors in the last hour',
},
{
id: 'highest_latency_services',
text: 'What services have the highest latency?',
},
{
id: 'system_health_overview',
text: 'Give me an overview of system health',
},
{
id: 'slow_database_queries',
text: 'Find slow database queries',
},
{
id: 'endpoints_5xx_errors',
text: 'Which endpoints have the most 5xx errors?',
},
];

View File

@@ -0,0 +1,33 @@
import { useMemo } from 'react';
import { useQuery } from 'react-query';
import { getEmptyStateChips } from 'api/ai-assistant/chat';
import type { ChipDTO } from 'api/ai-assistant/sigNozAIAssistantAPI.schemas';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import { useResolvePageType } from 'hooks/aiAssistant/useResolvePageType';
import { EMPTY_STATE_CHIPS_FALLBACK } from './emptyStateChipsFallback';
interface UseEmptyStateChipsResult {
chips: ChipDTO[];
}
export function useEmptyStateChips(enabled: boolean): UseEmptyStateChipsResult {
const pageType = useResolvePageType();
const { data, isError } = useQuery(
[REACT_QUERY_KEY.AI_ASSISTANT_EMPTY_STATE_CHIPS, pageType],
({ signal }) => getEmptyStateChips(pageType, signal),
{ enabled },
);
const chips = useMemo(() => {
if (isError) {
return EMPTY_STATE_CHIPS_FALLBACK;
}
return data ?? [];
}, [data, isError]);
return { chips };
}

View File

@@ -1,6 +1,7 @@
import type { MessageContext } from 'api/ai-assistant/chat';
import { QueryParams } from 'constants/query';
import ROUTES from 'constants/routes';
import { AlertListTabs } from 'pages/AlertList/types';
import { matchPath } from 'react-router-dom';
/**
@@ -99,6 +100,30 @@ export function getAutoContexts(
// ── Alerts ────────────────────────────────────────────────────────────────
// Alert detail (overview / per-rule history) — `/alerts/overview?ruleId=…`
// or `/alerts/history?ruleId=…`. Mirrors dashboard_detail: resourceId is the
// rule id and shared metadata carries the URL time range when present.
if (
matchPath(pathname, { path: ROUTES.ALERT_OVERVIEW, exact: true }) ||
matchPath(pathname, { path: ROUTES.ALERT_HISTORY, exact: true })
) {
const ruleId = params.get(QueryParams.ruleId);
if (ruleId) {
return [
{
source: 'auto',
type: 'alert',
resourceId: ruleId,
metadata: {
page: 'alert_detail',
ruleId,
...sharedMetadata,
},
},
];
}
}
// Alert edit — `/alerts/edit?ruleId=…`.
if (matchPath(pathname, { path: ROUTES.EDIT_ALERTS, exact: true })) {
const ruleId = params.get(QueryParams.ruleId);
@@ -108,7 +133,7 @@ export function getAutoContexts(
source: 'auto',
type: 'alert',
resourceId: ruleId,
metadata: { page: 'alert_edit' },
metadata: { page: 'alert_edit', ruleId },
},
];
}
@@ -125,6 +150,7 @@ export function getAutoContexts(
];
}
// Triggered-alerts index — `/alerts/history` without a rule id.
if (matchPath(pathname, { path: ROUTES.ALERT_HISTORY, exact: true })) {
return [
{
@@ -139,13 +165,18 @@ export function getAutoContexts(
];
}
// Alerts index — `/alerts` with tab query param (defaults to Alert Rules).
if (matchPath(pathname, { path: ROUTES.LIST_ALL_ALERT, exact: true })) {
const page = resolveAlertsIndexPage(params.get(QueryParams.tab));
return [
{
source: 'auto',
type: 'alert',
resourceId: null,
metadata: { page: 'alert_list' },
metadata: {
page,
...(page === 'alerts_triggered' ? sharedMetadata : {}),
},
},
];
}
@@ -251,8 +282,9 @@ export function getAutoContexts(
// ── Metrics ───────────────────────────────────────────────────────────────
// Metrics explorer — `/metrics-explorer` and sub-routes (summary, explorer, views).
if (
matchPath(pathname, { path: ROUTES.METRICS_EXPLORER_EXPLORER, exact: false })
matchPath(pathname, { path: ROUTES.METRICS_EXPLORER_BASE, exact: false })
) {
return [
{
@@ -267,9 +299,25 @@ export function getAutoContexts(
];
}
// NOTE: Homepage (`/home`) and infrastructure monitoring
// (`/infrastructure-monitoring/*`) intentionally emit no auto-context here.
// They have no resource that maps to `MessageContextDTOType`, so attaching
// a chip would misrepresent the page (e.g. a bogus "metrics_explorer"
// context). Their `page_type` for empty-state chips is resolved directly
// from the route in `resolvePageType`.
return [];
}
type AlertsIndexPage = 'alert_list' | 'alerts_triggered';
function resolveAlertsIndexPage(tab: string | null): AlertsIndexPage {
if (tab === AlertListTabs.TRIGGERED_ALERTS) {
return 'alerts_triggered';
}
return 'alert_list';
}
/**
* Pulls metadata fields that any page may carry in its query string —
* `timeRange`, `query`, saved-view selectors, dashboard variables. Each

View File

@@ -0,0 +1,74 @@
import { PageTypeDTO } from 'api/ai-assistant/sigNozAIAssistantAPI.schemas';
import { QueryParams } from 'constants/query';
import ROUTES from 'constants/routes';
import { matchPath } from 'react-router-dom';
import { getAutoContexts } from './getAutoContexts';
const PAGE_METADATA_TO_DTO: Record<string, PageTypeDTO> = {
dashboard_detail: PageTypeDTO.dashboard_detail,
dashboard_list: PageTypeDTO.dashboard_list,
panel_edit: PageTypeDTO.panel_edit,
panel_fullscreen: PageTypeDTO.panel_fullscreen,
logs_explorer: PageTypeDTO.logs_explorer,
trace_detail: PageTypeDTO.trace_detail,
traces_explorer: PageTypeDTO.traces_explorer,
metrics_explorer: PageTypeDTO.metrics_explorer,
service_detail: PageTypeDTO.service_detail,
services_list: PageTypeDTO.services_list,
alert_edit: PageTypeDTO.alert_edit,
alert_list: PageTypeDTO.alert_list,
alert_new: PageTypeDTO.alert_new,
alerts_triggered: PageTypeDTO.alerts_triggered,
};
interface ResolvePageTypeOptions {
/** Standalone `/ai-assistant` surface — no underlying observability page. */
isStandaloneAssistant?: boolean;
}
/**
* Maps the current URL (and assistant surface) to the backend `page_type`
* enum used by contextual empty-state chips.
*/
export function resolvePageType(
pathname: string,
search: string,
options?: ResolvePageTypeOptions,
): PageTypeDTO {
if (options?.isStandaloneAssistant) {
return PageTypeDTO.other;
}
// Pseudo-pages with no attachable resource: resolved straight from the
// route. They deliberately emit no auto-context chip (see `getAutoContexts`),
// so they can't be derived from `metadata.page` like the pages below.
if (matchPath(pathname, { path: ROUTES.HOME, exact: true })) {
return PageTypeDTO.homepage;
}
if (
matchPath(pathname, {
path: ROUTES.INFRASTRUCTURE_MONITORING_BASE,
exact: false,
})
) {
return PageTypeDTO.infra_entity_detail;
}
const contexts = getAutoContexts(pathname, search);
const page = contexts[0]?.metadata?.page;
if (typeof page === 'string') {
if (page === 'logs_explorer') {
const activeLogId = new URLSearchParams(search).get(QueryParams.activeLogId);
if (activeLogId) {
return PageTypeDTO.log_detail;
}
}
const mapped = PAGE_METADATA_TO_DTO[page];
if (mapped) {
return mapped;
}
}
return PageTypeDTO.other;
}

View File

@@ -1,12 +1,10 @@
/* 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';
@@ -37,6 +35,7 @@ import {
MessageBlock,
MessageRole,
} from '../types';
import { resolveAssistantErrorMessage } from '../utils/resolveAssistantErrorMessage';
// ---------------------------------------------------------------------------
// Types used by module-level helpers
@@ -399,6 +398,7 @@ async function runStreamingLoop(
}
throw Object.assign(new Error(event.error.message), {
retryAction: event.retryAction,
code: event.error.code,
});
} else if (event.type === 'conversation' && event.title) {
set((s) => {
@@ -484,36 +484,6 @@ 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;
}
/**
* 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,
@@ -1174,14 +1144,11 @@ export const useAIAssistantStore = create<AIAssistantStore>()(
return;
}
console.error('[AIAssistant] sendMessage failed:', err);
const rateLimit = rateLimitMessage(err);
finalizeStreamingError(
convId,
rateLimit ??
'Something went wrong while fetching the response. Please try again.',
set,
rateLimit !== null,
const { message, isRateLimit } = resolveAssistantErrorMessage(
err,
'Something went wrong while fetching the response. Please try again.',
);
finalizeStreamingError(convId, message, set, isRateLimit);
}
},
@@ -1214,14 +1181,11 @@ export const useAIAssistantStore = create<AIAssistantStore>()(
return;
}
console.error('[AIAssistant] approveAction failed:', err);
const rateLimit = rateLimitMessage(err);
finalizeStreamingError(
conversationId,
rateLimit ??
'Something went wrong while processing the approval. Please try again.',
set,
rateLimit !== null,
const { message, isRateLimit } = resolveAssistantErrorMessage(
err,
'Something went wrong while processing the approval. Please try again.',
);
finalizeStreamingError(conversationId, message, set, isRateLimit);
}
},
@@ -1296,14 +1260,11 @@ export const useAIAssistantStore = create<AIAssistantStore>()(
return;
}
console.error('[AIAssistant] regenerateAssistantMessage failed:', err);
const rateLimit = rateLimitMessage(err);
finalizeStreamingError(
conversationId,
rateLimit ??
'Something went wrong while regenerating the response. Please try again.',
set,
rateLimit !== null,
const { message, isRateLimit } = resolveAssistantErrorMessage(
err,
'Something went wrong while regenerating the response. Please try again.',
);
finalizeStreamingError(conversationId, message, set, isRateLimit);
}
},
@@ -1365,14 +1326,11 @@ export const useAIAssistantStore = create<AIAssistantStore>()(
return;
}
console.error('[AIAssistant] submitClarification failed:', err);
const rateLimit = rateLimitMessage(err);
finalizeStreamingError(
conversationId,
rateLimit ??
'Something went wrong while processing your answers. Please try again.',
set,
rateLimit !== null,
const { message, isRateLimit } = resolveAssistantErrorMessage(
err,
'Something went wrong while processing your answers. Please try again.',
);
finalizeStreamingError(conversationId, message, set, isRateLimit);
}
},
})),

View File

@@ -0,0 +1,91 @@
import { AxiosError } from 'axios';
import { ErrorCodeDTO } from 'api/ai-assistant/sigNozAIAssistantAPI.schemas';
import { resolveAssistantErrorMessage } from '../resolveAssistantErrorMessage';
const FALLBACK = 'Something went wrong. Please try again.';
describe('resolveAssistantErrorMessage', () => {
it('returns backend message for a known error code', () => {
const err = new AxiosError('Request failed');
err.response = {
status: 400,
data: {
error: {
code: ErrorCodeDTO.thread_busy,
message: 'This thread is busy. Try again shortly.',
},
},
} as AxiosError['response'];
expect(resolveAssistantErrorMessage(err, FALLBACK)).toStrictEqual({
message: 'This thread is busy. Try again shortly.',
isRateLimit: false,
});
});
it('falls back when error code is not in ErrorCodeDTO', () => {
const err = new AxiosError('Request failed');
err.response = {
status: 400,
data: {
error: {
code: 'future_unknown_code',
message: 'Backend-only message',
},
},
} as AxiosError['response'];
expect(resolveAssistantErrorMessage(err, FALLBACK)).toStrictEqual({
message: FALLBACK,
isRateLimit: false,
});
});
it('marks HTTP 429 responses as rate limited', () => {
const err = new AxiosError('Too many requests');
err.response = {
status: 429,
data: {
error: {
code: ErrorCodeDTO.hourly_message_limit,
message: 'Hourly limit reached.',
},
},
} as AxiosError['response'];
expect(resolveAssistantErrorMessage(err, FALLBACK)).toStrictEqual({
message: 'Hourly limit reached.',
isRateLimit: true,
});
});
it('uses backend message for known SSE rate-limit error codes', () => {
const err = Object.assign(new Error('Daily token limit exceeded.'), {
code: ErrorCodeDTO.daily_token_limit,
});
expect(resolveAssistantErrorMessage(err, FALLBACK)).toStrictEqual({
message: 'Daily token limit exceeded.',
isRateLimit: true,
});
});
it('marks 429 as rate limited even when error code is unknown', () => {
const err = new AxiosError('Too many requests');
err.response = {
status: 429,
data: {
error: {
code: 'future_unknown_code',
message: 'Too many requests',
},
},
} as AxiosError['response'];
expect(resolveAssistantErrorMessage(err, FALLBACK)).toStrictEqual({
message: FALLBACK,
isRateLimit: true,
});
});
});

View File

@@ -0,0 +1,71 @@
import { isAxiosError } from 'axios';
import {
ErrorCodeDTO,
type ErrorBodyDTO,
type ErrorResponseDTO,
} from 'api/ai-assistant/sigNozAIAssistantAPI.schemas';
export interface AssistantErrorResolution {
message: string;
isRateLimit: boolean;
}
function isErrorCodeDTO(code: string | undefined): code is ErrorCodeDTO {
return (
code !== undefined && (Object.values(ErrorCodeDTO) as string[]).includes(code)
);
}
const RATE_LIMIT_ERROR_CODES = new Set<ErrorCodeDTO>([
ErrorCodeDTO.rate_limit_override_exceeds_ceiling,
ErrorCodeDTO.thread_message_limit,
ErrorCodeDTO.connection_limit_exceeded,
ErrorCodeDTO.hourly_message_limit,
ErrorCodeDTO.daily_message_limit,
ErrorCodeDTO.daily_token_limit,
ErrorCodeDTO.daily_cost_limit,
ErrorCodeDTO.budget_exceeded,
]);
function isRateLimitError(code: string | undefined, err: unknown): boolean {
if (isAxiosError(err) && err.response?.status === 429) {
return true;
}
return isErrorCodeDTO(code) && RATE_LIMIT_ERROR_CODES.has(code);
}
function getErrorBody(err: unknown): ErrorBodyDTO | null {
if (isAxiosError(err)) {
return (err.response?.data as ErrorResponseDTO | undefined)?.error ?? null;
}
const code = (err as { code?: string } | undefined)?.code;
const message = err instanceof Error ? err.message : undefined;
if (!code || !message) {
return null;
}
return { code: code as ErrorCodeDTO, message };
}
/**
* Uses `error.message` when `error.code` is a known `ErrorCodeDTO`;
* otherwise returns `fallback`.
*/
export function resolveAssistantErrorMessage(
err: unknown,
fallback: string,
): AssistantErrorResolution {
const body = getErrorBody(err);
const isRateLimit = isRateLimitError(body?.code, err);
if (body && isErrorCodeDTO(body.code) && body.message.trim()) {
return {
message: body.message.trim(),
isRateLimit,
};
}
return { message: fallback, isRateLimit: Boolean(isRateLimit) };
}

View File

@@ -29,3 +29,7 @@
color: var(--l2-foreground);
}
}
body.ai-assistant-panel-open .create-alert-v2-footer {
right: var(--ai-assistant-panel-width, 380px);
}

View File

@@ -63,5 +63,6 @@
flex: 0 0 auto;
min-height: 0;
min-width: 0;
padding: 8px;
padding-left: 12px;
padding-bottom: 12px;
}

View File

@@ -16,7 +16,7 @@ import PieArc from './PieArc';
import PieCenterLabel from './PieCenterLabel';
import styles from './Pie.module.scss';
import { PieTooltipData } from './types';
import { getFillColor } from './utils';
import { getDonutGeometry, getFillColor } from './utils';
/**
* Donut chart rendered with @visx. Splits its area into chart + legend with the
@@ -78,16 +78,12 @@ export default function Pie({
[containerWidth, containerHeight, position, data],
);
// Donut geometry derived from the allocated chart box.
const { size, radius, innerRadius } = useMemo(() => {
const nextSize = Math.min(width, height);
const nextRadius = nextSize * 0.35;
return {
size: nextSize,
radius: nextRadius,
innerRadius: nextRadius * 0.6,
};
}, [width, height]);
// Donut geometry derived from the allocated chart box, sized to leave room
// for the external leader labels (see getDonutGeometry).
const { size, radius, innerRadius } = useMemo(
() => getDonutGeometry(width, height),
[width, height],
);
const totalValue = useMemo(
() => visibleData.reduce((sum, slice) => sum + slice.value, 0),

View File

@@ -1,11 +1,40 @@
import {
getArcGeometry,
getDonutGeometry,
getFillColor,
getScaledFontSize,
lightenColor,
} from '../utils';
describe('Pie utils', () => {
describe('getDonutGeometry', () => {
it('keeps the label anchor inside the box (reserves room for leader labels)', () => {
const { radius } = getDonutGeometry(400, 300);
const half = Math.min(400, 300) / 2; // 150
// The label anchor sits at radius * 1.3 and must stay within the box
// half-extent so labels are not clipped.
expect(radius * 1.3).toBeLessThanOrEqual(half);
// And it should use the available room (anchor = half - 22 allowance).
expect(radius * 1.3).toBeCloseTo(half - 22);
});
it('derives size and inner radius from the outer radius', () => {
const { size, radius, innerRadius } = getDonutGeometry(300, 300);
expect(size).toBeCloseTo(radius * 2);
expect(innerRadius).toBeCloseTo(radius * 0.6);
});
it('sizes off the smaller dimension so it fits both axes', () => {
expect(getDonutGeometry(1000, 200)).toStrictEqual(
getDonutGeometry(200, 1000),
);
});
it('never returns a negative radius for a box too small for labels', () => {
expect(getDonutGeometry(20, 20).radius).toBe(0);
});
});
describe('getScaledFontSize', () => {
it('returns the base size for empty text', () => {
expect(getScaledFontSize({ text: '', baseSize: 30, innerRadius: 100 })).toBe(

View File

@@ -10,6 +10,16 @@ export interface ScaledFontSizeArgs {
innerRadius: number;
}
/** Donut sizing for a given chart box: the outer/inner radii and the square it spans. */
export interface DonutGeometry {
/** Outer diameter — feeds the visx Pie width/height and the render guard. */
size: number;
/** Outer radius of the donut ring. */
radius: number;
/** Inner radius (the hole) — also bounds the centre-total font. */
innerRadius: number;
}
export interface ArcGeometry {
/** Outer point where the leader label sits. */
labelX: number;

View File

@@ -3,7 +3,37 @@
* so the renderer stays declarative (per the one-component-per-file rule).
*/
import { ArcGeometry, ParsedRgb, ScaledFontSizeArgs } from './types';
import {
ArcGeometry,
DonutGeometry,
ParsedRgb,
ScaledFontSizeArgs,
} from './types';
// Leader-line + two-line label/value drawn outside the donut. `getArcGeometry`
// anchors the label at `radius * LABEL_RADIUS_RATIO`; `LABEL_TEXT_ALLOWANCE` is
// the px reserved beyond that anchor for the (10px, two-line) text so it never
// clips against the SVG edge.
const LABEL_RADIUS_RATIO = 1.3;
const LABEL_TEXT_ALLOWANCE = 22;
const INNER_RADIUS_RATIO = 0.6;
/**
* Sizes the donut to fit inside a `width × height` box *with room for the
* external leader labels*. The label anchor sits at `radius * 1.3`, so we solve
* the outer radius back from the box's half-extent minus the text allowance —
* guaranteeing the labels stay inside the SVG instead of being clipped (V1 used
* a flat `0.35 * min(w,h)`, which left too little margin on small panels).
*/
export function getDonutGeometry(width: number, height: number): DonutGeometry {
const half = Math.min(width, height) / 2;
const radius = Math.max(0, (half - LABEL_TEXT_ALLOWANCE) / LABEL_RADIUS_RATIO);
return {
size: radius * 2,
radius,
innerRadius: radius * INNER_RADIUS_RATIO,
};
}
/**
* Shrinks the centre-total font as the text gets longer so it never overflows
@@ -37,7 +67,7 @@ export function getArcGeometry(
radius: number,
): ArcGeometry {
const angle = (startAngle + endAngle) / 2;
const labelRadius = radius * 1.3;
const labelRadius = radius * LABEL_RADIUS_RATIO;
const lineEndRadius = radius * 1.1;
return {
labelX: Math.sin(angle) * labelRadius,

View File

@@ -0,0 +1,79 @@
import { LegendPosition } from 'lib/uPlotV2/components/types';
import { calculateChartDimensions } from '../utils';
const labels = (count: number, length = 20): string[] =>
Array.from({ length: count }, (_, i) =>
`label-${i}`.padEnd(length, 'x').slice(0, length),
);
describe('calculateChartDimensions', () => {
it('returns all zeros when the container has no space', () => {
expect(
calculateChartDimensions({
containerWidth: 0,
containerHeight: 300,
legendConfig: { position: LegendPosition.BOTTOM },
seriesLabels: labels(3),
}),
).toStrictEqual({
width: 0,
height: 0,
legendWidth: 0,
legendHeight: 0,
averageLegendWidth: 0,
});
});
it('RIGHT: reserves a side column capped at 30% of the width and keeps full height', () => {
const dims = calculateChartDimensions({
containerWidth: 1000,
containerHeight: 400,
legendConfig: { position: LegendPosition.RIGHT },
seriesLabels: labels(10, 40),
});
// 40-char labels approximate to 336px, capped at min(240, 30% of 1000).
expect(dims.legendWidth).toBe(240);
expect(dims.width).toBe(760);
expect(dims.height).toBe(400);
expect(dims.legendHeight).toBe(400);
});
it('BOTTOM: a single row of items reserves one legend row', () => {
const dims = calculateChartDimensions({
containerWidth: 1000,
containerHeight: 500,
legendConfig: { position: LegendPosition.BOTTOM },
seriesLabels: labels(3),
});
// One row = line height (28) + padding (12).
expect(dims.legendHeight).toBe(40);
expect(dims.height).toBe(460);
expect(dims.legendWidth).toBe(1000);
});
it('BOTTOM: many items cap at two rows on a tall container', () => {
const dims = calculateChartDimensions({
containerWidth: 1000,
containerHeight: 500,
legendConfig: { position: LegendPosition.BOTTOM },
seriesLabels: labels(40),
});
// Two rows = 2 * 40 - 12 (no trailing padding) = 68, under the 80px cap.
expect(dims.legendHeight).toBe(68);
expect(dims.height).toBe(432);
});
it('BOTTOM: on a short container the legend never takes more than 30% of the height', () => {
const dims = calculateChartDimensions({
containerWidth: 1000,
containerHeight: 160,
legendConfig: { position: LegendPosition.BOTTOM },
seriesLabels: labels(40),
});
// Without the height-relative cap the legend would take 68px of a 160px
// panel and the chart (pie especially) collapses to a sliver.
expect(dims.legendHeight).toBe(48); // 30% of 160
expect(dims.height).toBe(112);
});
});

View File

@@ -116,7 +116,15 @@ export function calculateChartDimensions({
? legendRowCount * legendRowHeight - LEGEND_PADDING
: legendRowHeight;
const maxAllowedLegendHeight = Math.min(2 * legendRowHeight, 80);
// Cap at two rows / 80px, and never more than 30% of the container height
// (the doc above always promised the %-cap; without it, short grid panels
// hand most of their area to the legend and the chart — the pie donut
// especially — collapses to a sliver). 30% mirrors the RIGHT-legend width cap.
const maxAllowedLegendHeight = Math.min(
2 * legendRowHeight,
80,
Math.floor(containerHeight * 0.3),
);
const bottomLegendHeight = Math.min(
idealBottomLegendHeight,

View File

@@ -1,6 +1,5 @@
import { useMemo } from 'react';
import { useParams } from 'react-router-dom';
import { Skeleton } from 'antd';
import { ENTITY_VERSION_V4 } from 'constants/app';
import { FeatureKeys } from 'constants/features';
import { PANEL_TYPES } from 'constants/queryBuilder';
@@ -124,24 +123,14 @@ function ServiceOverview({
/>
<Card data-testid="service_latency">
<GraphContainer>
{topLevelOperationsIsLoading && (
<Skeleton
style={{
height: '100%',
padding: '16px',
}}
/>
)}
{!topLevelOperationsIsLoading && (
<Graph
onDragSelect={onDragSelect}
widget={latencyWidget}
onClickHandler={handleGraphClick('Service')}
isQueryEnabled={isQueryEnabled}
version={ENTITY_VERSION_V4}
enableDrillDown={SERVICE_DETAIL_DRILLDOWN_ENABLED}
/>
)}
<Graph
onDragSelect={onDragSelect}
widget={latencyWidget}
onClickHandler={handleGraphClick('Service')}
isQueryEnabled={isQueryEnabled}
version={ENTITY_VERSION_V4}
enableDrillDown={SERVICE_DETAIL_DRILLDOWN_ENABLED}
/>
</GraphContainer>
</Card>
</>

View File

@@ -1,4 +1,3 @@
import { Skeleton } from 'antd';
import { Typography } from '@signozhq/ui/typography';
import axios from 'axios';
import { SOMETHING_WENT_WRONG } from 'constants/api';
@@ -29,24 +28,14 @@ function TopLevelOperation({
</Typography>
) : (
<GraphContainer>
{topLevelOperationsIsLoading && (
<Skeleton
style={{
height: '100%',
padding: '16px',
}}
/>
)}
{!topLevelOperationsIsLoading && (
<Graph
widget={widget}
onClickHandler={handleGraphClick(opName)}
onDragSelect={onDragSelect}
isQueryEnabled={!topLevelOperationsIsLoading}
version={ENTITY_VERSION_V4}
enableDrillDown={SERVICE_DETAIL_DRILLDOWN_ENABLED}
/>
)}
<Graph
widget={widget}
onClickHandler={handleGraphClick(opName)}
onDragSelect={onDragSelect}
isQueryEnabled={!topLevelOperationsIsLoading}
version={ENTITY_VERSION_V4}
enableDrillDown={SERVICE_DETAIL_DRILLDOWN_ENABLED}
/>
</GraphContainer>
)}
</Card>

View File

@@ -39,6 +39,7 @@
.right-header {
display: flex;
gap: 16px;
align-items: center;
}
}

View File

@@ -15,6 +15,7 @@ import { Flex } from 'antd';
import { Typography } from '@signozhq/ui/typography';
import logEvent from 'api/common/logEvent';
import { PrecisionOption, PrecisionOptionsEnum } from 'components/Graph/types';
import HeaderRightSection from 'components/HeaderRightSection/HeaderRightSection';
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
import { adjustQueryForV5 } from 'components/QueryBuilderV2/utils';
import { QueryParams } from 'constants/query';
@@ -820,6 +821,11 @@ function NewWidget({
</Flex>
</div>
<div className="right-header">
<HeaderRightSection
enableAnnouncements={false}
enableShare={false}
enableFeedback={false}
/>
{showSwitchToViewModeButton && (
<Button
color="primary"

View File

@@ -68,10 +68,6 @@
border-left: unset;
border-radius: 0px 4px 4px 0px;
}
.new-view-btn {
margin-left: 8px;
}
}
.second-row {

View File

@@ -0,0 +1,48 @@
import { renderHook } from '@testing-library/react';
import { PageTypeDTO } from 'api/ai-assistant/sigNozAIAssistantAPI.schemas';
import type { AIAssistantVariant } from 'container/AIAssistant/VariantContext';
import ROUTES from 'constants/routes';
import { useResolvePageType } from '../useResolvePageType';
const mockUseLocation = jest.fn();
const mockUseVariant = jest.fn();
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useLocation: (): unknown => mockUseLocation(),
}));
jest.mock('container/AIAssistant/VariantContext', () => ({
useVariant: (): unknown => mockUseVariant(),
}));
function setup(
pathname: string,
search: string,
variant: AIAssistantVariant,
): PageTypeDTO {
mockUseLocation.mockReturnValue({ pathname, search });
mockUseVariant.mockReturnValue(variant);
return renderHook(() => useResolvePageType()).result.current;
}
describe('useResolvePageType', () => {
afterEach(() => {
jest.clearAllMocks();
});
it('returns other for the standalone "page" assistant surface', () => {
const pathname = ROUTES.DASHBOARD.replace(':dashboardId', 'dash-123');
expect(setup(pathname, '', 'page')).toBe(PageTypeDTO.other);
});
it('resolves the underlying page type for embedded variants', () => {
const pathname = ROUTES.DASHBOARD.replace(':dashboardId', 'dash-123');
expect(setup(pathname, '', 'panel')).toBe(PageTypeDTO.dashboard_detail);
expect(setup(pathname, '', 'modal')).toBe(PageTypeDTO.dashboard_detail);
});
});

View File

@@ -0,0 +1,23 @@
import { useMemo } from 'react';
import { useLocation } from 'react-router-dom';
import { PageTypeDTO } from 'api/ai-assistant/sigNozAIAssistantAPI.schemas';
import { resolvePageType } from 'container/AIAssistant/resolvePageType';
import { useVariant } from 'container/AIAssistant/VariantContext';
/**
* React hook wrapper around `resolvePageType` that derives the current
* `page_type` from the active location and assistant variant.
*/
export function useResolvePageType(): PageTypeDTO {
const location = useLocation();
const variant = useVariant();
return useMemo(
() =>
resolvePageType(location.pathname, location.search, {
isStandaloneAssistant: variant === 'page',
}),
[location.pathname, location.search, variant],
);
}

View File

@@ -44,7 +44,6 @@
auto-fill,
minmax(var(--legend-average-width, 240px), 1fr)
);
row-gap: 4px;
column-gap: 12px;
}

View File

@@ -1,210 +0,0 @@
.dashboardDescriptionContainer {
box-shadow: none;
border: none;
background: unset;
color: var(--l2-foreground);
:global(.ant-card-body) {
padding: 0px;
}
.dashboardDetails {
display: flex;
justify-content: space-between;
gap: 8px;
padding: 16px 16px 0px 16px;
align-items: flex-start;
.leftSection {
display: flex;
align-items: center;
gap: 8px;
width: 45%;
height: 40px;
.dashboardImg {
height: 16px;
width: 16px;
}
.dashboardTitle {
color: var(--l1-foreground);
font-family: Inter;
font-size: 16px;
font-style: normal;
font-weight: 500;
line-height: 24px; /* 150% */
letter-spacing: -0.08px;
max-width: 80%;
display: -webkit-box;
-webkit-line-clamp: 1;
-webkit-box-orient: vertical;
overflow: hidden;
}
.clickableTitle {
cursor: pointer;
}
.titleEdit {
display: flex;
align-items: center;
gap: 4px;
width: 100%;
min-width: 0;
}
.titleInput {
flex: 1;
min-width: 0;
max-width: 70%;
}
.titleEditActionButton {
--button-height: auto;
--button-padding: 4px;
flex-shrink: 0;
}
.titleSaveActionButton {
--button-border-color: var(--text-forest-700);
--button-outlined-foreground: var(--text-forest-700);
}
.publicDashboardIcon {
margin-right: 4px;
}
}
.rightSection {
display: flex;
width: 55%;
justify-content: flex-end;
flex-wrap: wrap;
align-items: center;
gap: 14px;
height: 40px;
.icons {
display: flex;
align-items: center;
width: 32px;
height: 34px;
padding: 6px;
justify-content: center;
border-radius: 2px;
border: 1px solid var(--l1-border);
background: var(--l3-background);
color: var(--l2-foreground);
font-family: Inter;
font-size: 12px;
font-style: normal;
font-weight: 500;
line-height: 10px; /* 83.333% */
letter-spacing: 0.12px;
}
.icons:hover {
background-color: unset;
}
}
}
.dashboardTags {
display: flex;
gap: 6px;
padding: 16px 16px 0px 16px;
flex-wrap: wrap;
.tag {
display: flex;
padding: 4px 8px;
justify-content: center;
align-items: center;
border-radius: 20px;
border: 1px solid color-mix(in srgb, var(--bg-sienna-500) 20%, transparent);
background: color-mix(in srgb, var(--bg-sienna-500) 10%, transparent);
color: var(--bg-sienna-400);
text-align: center;
font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 500;
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
margin-inline-end: 0px;
}
}
.dashboardDescriptionSection {
color: var(--l2-foreground);
font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 22px; /* 157.143% */
letter-spacing: -0.07px;
padding: 20px 16px 0px 16px;
}
}
.dashboardSettings {
width: 191px;
height: 302px;
flex-shrink: 0;
:global(.ant-popover-inner) {
padding: 0px;
border-radius: 4px;
border: 1px solid var(--l1-border);
background: linear-gradient(
139deg,
color-mix(in srgb, var(--card) 80%, transparent) 0%,
color-mix(in srgb, var(--card) 90%, transparent) 98.68%
) !important;
box-shadow: 4px 10px 16px 2px rgba(0, 0, 0, 0.2);
backdrop-filter: blur(20px);
}
.menuContent {
display: flex;
flex-direction: column;
section {
display: flex;
flex-direction: column;
align-items: start;
button {
display: flex;
width: 100%;
height: unset;
padding: 8px;
align-items: center;
gap: 12px;
color: var(--l2-foreground);
font-family: Inter;
font-size: 13px;
font-style: normal;
font-weight: 400;
line-height: normal;
letter-spacing: 0.14px;
border-top: none;
}
}
.section1,
.section2 {
border-bottom: 1px solid var(--l1-border);
}
.deleteDashboard button {
color: var(--bg-cherry-400) !important;
}
}
}
.deleteModal :global(.ant-modal-confirm-body) {
align-items: center;
}

View File

@@ -1,32 +0,0 @@
import { Badge } from '@signozhq/ui/badge';
import { isEmpty } from 'lodash-es';
import styles from '../DashboardDescription.module.scss';
interface DashboardMetaProps {
tags: string[];
description: string;
}
function DashboardMeta({ tags, description }: DashboardMetaProps): JSX.Element {
return (
<>
{tags.length > 0 && (
<div className={styles.dashboardTags}>
{tags.map((tag) => (
<Badge key={tag} className={styles.tag}>
{tag}
</Badge>
))}
</div>
)}
{!isEmpty(description) && (
<section className={styles.dashboardDescriptionSection}>
{description}
</section>
)}
</>
);
}
export default DashboardMeta;

View File

@@ -1,116 +0,0 @@
import { KeyboardEvent } from 'react';
import { Check, Globe, LockKeyhole, X } from '@signozhq/icons';
import { Button } from '@signozhq/ui/button';
import { Input } from '@signozhq/ui/input';
import { TooltipSimple } from '@signozhq/ui/tooltip';
import { Typography } from '@signozhq/ui/typography';
import cx from 'classnames';
import styles from '../DashboardDescription.module.scss';
interface DashboardTitleProps {
title: string;
image: string;
isPublicDashboard: boolean;
isDashboardLocked: boolean;
isEditable: boolean;
isEditing: boolean;
draft: string;
onDraftChange: (value: string) => void;
onStartEdit: () => void;
onCommit: () => void;
onCancel: () => void;
}
function DashboardTitle({
title,
image,
isPublicDashboard,
isDashboardLocked,
isEditable,
isEditing,
draft,
onDraftChange,
onStartEdit,
onCommit,
onCancel,
}: DashboardTitleProps): JSX.Element {
const canEdit = isEditable && !isDashboardLocked;
const onKeyDown = (event: KeyboardEvent<HTMLInputElement>): void => {
if (event.key === 'Enter') {
event.preventDefault();
onCommit();
} else if (event.key === 'Escape') {
onCancel();
}
};
return (
<div className={styles.leftSection}>
<img src={image} alt="dashboard-img" className={styles.dashboardImg} />
{isEditing ? (
<div className={styles.titleEdit}>
<Input
autoFocus
value={draft}
testId="dashboard-title-input"
maxLength={120}
className={styles.titleInput}
onChange={(e): void => onDraftChange(e.target.value)}
onKeyDown={onKeyDown}
/>
<Button
type="button"
variant="outlined"
size="icon"
className={cx(styles.titleEditActionButton, styles.titleSaveActionButton)}
aria-label="Save title"
testId="dashboard-title-save"
onClick={onCommit}
>
<Check size={14} />
</Button>
<Button
type="button"
variant="outlined"
color="destructive"
size="icon"
className={styles.titleEditActionButton}
aria-label="Cancel title edit"
testId="dashboard-title-cancel"
onClick={onCancel}
>
<X size={14} />
</Button>
</div>
) : (
<TooltipSimple title={title.length > 30 ? title : ''}>
<Typography.Text
className={cx(styles.dashboardTitle, {
[styles.clickableTitle]: canEdit,
})}
data-testid="dashboard-title"
onClick={canEdit ? onStartEdit : undefined}
>
{title}
</Typography.Text>
</TooltipSimple>
)}
{isPublicDashboard && (
<TooltipSimple title="This dashboard is publicly accessible">
<Globe size={14} className={styles.publicDashboardIcon} />
</TooltipSimple>
)}
{isDashboardLocked && (
<TooltipSimple title="This dashboard is locked">
<LockKeyhole size={14} />
</TooltipSimple>
)}
</div>
);
}
export default DashboardTitle;

View File

@@ -0,0 +1,11 @@
.dashboardActionsContainer {
display: flex;
flex-wrap: wrap;
justify-content: flex-end;
gap: 12px;
}
.dashboardActionsSecondary {
display: flex;
gap: 12px;
}

View File

@@ -28,43 +28,40 @@ import { USER_ROLES } from 'types/roles';
import ConfirmDeleteDialog from '../../components/ConfirmDeleteDialog/ConfirmDeleteDialog';
import DashboardSettings from '../../DashboardSettings';
import SettingsDrawer from '../SettingsDrawer';
import styles from '../DashboardDescription.module.scss';
import styles from './DashboardActions.module.scss';
import { useDashboardStore } from '../../store/useDashboardStore';
interface DashboardActionsProps {
title: string;
dashboard: DashboardtypesGettableDashboardV2DTO;
handle: FullScreenHandle;
isDashboardLocked: boolean;
editDashboard: boolean;
isAuthor: boolean;
addPanelPermission: boolean;
onAddPanel: () => void;
onLockToggle: () => void;
onOpenRename: () => void;
}
function DashboardActions({
title,
dashboard,
handle,
isDashboardLocked,
editDashboard,
isAuthor,
addPanelPermission,
onAddPanel,
onLockToggle,
onOpenRename,
}: DashboardActionsProps): JSX.Element {
const canEdit = useDashboardStore((s) => s.isEditable);
const { user } = useAppContext();
const { t } = useTranslation(['dashboard', 'common']);
const id = dashboard.id ?? '';
const title = dashboard.spec?.display?.name ?? '';
const [isSettingsDrawerOpen, setIsSettingsDrawerOpen] =
useState<boolean>(false);
const [state, setCopy] = useCopyToClipboard();
const [isDeleteOpen, setIsDeleteOpen] = useState<boolean>(false);
const deleteDashboardMutation = useDeleteDashboard(id);
const deleteDashboardMutation = useDeleteDashboard(dashboard.id);
useEffect(() => {
if (state.error) {
@@ -103,7 +100,7 @@ function DashboardActions({
const menuItems = useMemo<MenuItem[]>(() => {
const editGroup: MenuItem[] = [];
if (!isDashboardLocked && editDashboard) {
if (canEdit) {
editGroup.push({
key: 'rename',
label: 'Rename',
@@ -159,7 +156,6 @@ function DashboardActions({
);
}, [
isDashboardLocked,
editDashboard,
isAuthor,
user.role,
dashboard.createdBy,
@@ -169,58 +165,60 @@ function DashboardActions({
exportJSON,
setCopy,
dashboardDataJSON,
canEdit,
]);
return (
<div className={styles.rightSection}>
<div className={styles.dashboardActionsContainer}>
<DateTimeSelectionV2 showAutoRefresh hideShareModal />
<DropdownMenuSimple menu={{ items: menuItems }}>
<Button
variant="ghost"
color="secondary"
size="icon"
prefix={<Ellipsis size={14} />}
className={styles.icons}
testId="options"
/>
</DropdownMenuSimple>
{!isDashboardLocked && editDashboard && (
<>
<div className={styles.dashboardActionsSecondary}>
<DropdownMenuSimple menu={{ items: menuItems }}>
<Button
variant="solid"
color="secondary"
prefix={<Configure size="md" />}
testId="show-drawer"
onClick={(): void => setIsSettingsDrawerOpen(true)}
size="icon"
prefix={<Ellipsis size="md" />}
testId="options"
/>
</DropdownMenuSimple>
{canEdit && (
<>
<Button
variant="solid"
color="secondary"
prefix={<Configure size="md" />}
testId="show-drawer"
onClick={(): void => setIsSettingsDrawerOpen(true)}
size="md"
>
Configure
</Button>
<SettingsDrawer
drawerTitle="Dashboard Configuration"
isOpen={isSettingsDrawerOpen}
onClose={(): void => setIsSettingsDrawerOpen(false)}
>
<DashboardSettings dashboard={dashboard} />
</SettingsDrawer>
</>
)}
{!isDashboardLocked && (
<Button
variant="solid"
color="primary"
onClick={onAddPanel}
prefix={<Plus size="md" />}
testId="add-panel-header"
size="md"
>
Configure
New Panel
</Button>
<SettingsDrawer
drawerTitle="Dashboard Configuration"
isOpen={isSettingsDrawerOpen}
onClose={(): void => setIsSettingsDrawerOpen(false)}
>
<DashboardSettings dashboard={dashboard} />
</SettingsDrawer>
</>
)}
{!isDashboardLocked && addPanelPermission && (
<Button
variant="solid"
color="primary"
onClick={onAddPanel}
prefix={<Plus size="md" />}
testId="add-panel-header"
size="md"
>
New Panel
</Button>
)}
)}
</div>
<ConfirmDeleteDialog
open={isDeleteOpen}
title={`Delete dashboard "${title}"?`}
description="This action cannot be undone."
title={`Delete dashboard"?`}
description={`Are you sure you want to delete this dashboard - "${title}"? This action cannot be undone.`}
isLoading={deleteDashboardMutation.isLoading}
onConfirm={handleConfirmDelete}
onClose={(): void => setIsDeleteOpen(false)}

View File

@@ -0,0 +1,61 @@
.dashboardInfo {
display: flex;
flex-direction: column;
gap: 8px;
width: 40%;
@media (min-width: 1280px) {
width: 30%;
}
}
.dashboardTitleContainer {
display: flex;
align-items: center;
gap: 8px;
width: 100%;
}
.dashboardImage {
flex-shrink: 0;
}
.dashboardTitle {
flex: 1;
min-width: 0;
max-width: fit-content;
color: var(--l1-foreground);
font-size: 18px;
font-weight: 500;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.dashboardTitleHover {
cursor: text !important;
}
.dashboardTitleEditor {
display: flex;
align-items: center;
gap: 4px;
flex: 1;
min-width: 0;
}
.dashboardTitleInput {
flex: 1;
min-width: 0;
}
.dashboardTitleActionButton {
flex-shrink: 0;
}
.dashboardTags {
display: flex;
flex-wrap: wrap;
gap: 8px;
}

View File

@@ -0,0 +1,141 @@
import { KeyboardEvent } from 'react';
import { Check, Globe, LockKeyhole, X } from '@signozhq/icons';
import { Badge } from '@signozhq/ui/badge';
import { Button } from '@signozhq/ui/button';
import { Input } from '@signozhq/ui/input';
import { TooltipSimple } from '@signozhq/ui/tooltip';
import { Typography } from '@signozhq/ui/typography';
import cx from 'classnames';
import { isEmpty } from 'lodash-es';
import styles from './DashboardInfo.module.scss';
import { useDashboardStore } from '../../store/useDashboardStore';
interface DashboardInfoProps {
title: string;
image: string;
tags: string[];
description: string;
isPublicDashboard: boolean;
isDashboardLocked: boolean;
isEditing: boolean;
draft: string;
onDraftChange: (value: string) => void;
onStartEdit: () => void;
onCommit: () => void;
onCancel: () => void;
}
function DashboardInfo({
title,
image,
tags,
description,
isPublicDashboard,
isDashboardLocked,
isEditing,
draft,
onDraftChange,
onStartEdit,
onCommit,
onCancel,
}: DashboardInfoProps): JSX.Element {
const canEdit = useDashboardStore((s) => s.isEditable);
const hasTags = tags.length > 0;
const hasDescription = !isEmpty(description);
const onKeyDown = (event: KeyboardEvent<HTMLInputElement>): void => {
if (event.key === 'Enter') {
event.preventDefault();
onCommit();
} else if (event.key === 'Escape') {
onCancel();
}
};
return (
<div className={styles.dashboardInfo}>
<div className={styles.dashboardTitleContainer}>
<img src={image} alt={title} className={styles.dashboardImage} />
{isEditing ? (
<div className={styles.dashboardTitleEditor}>
<Input
autoFocus
value={draft}
testId="dashboard-title-input"
maxLength={120}
className={styles.dashboardTitleInput}
onChange={(e): void => onDraftChange(e.target.value)}
onKeyDown={onKeyDown}
/>
<Button
type="button"
variant="outlined"
color="primary"
size="icon"
className={styles.dashboardTitleActionButton}
aria-label="Save title"
testId="dashboard-title-save"
onClick={onCommit}
>
<Check size={14} />
</Button>
<Button
type="button"
variant="outlined"
color="secondary"
size="icon"
className={styles.dashboardTitleActionButton}
aria-label="Cancel title edit"
testId="dashboard-title-cancel"
onClick={onCancel}
>
<X size={14} />
</Button>
</div>
) : (
<TooltipSimple title={title}>
<Typography.Text
className={cx(styles.dashboardTitle, {
[styles.dashboardTitleHover]: canEdit,
})}
data-testid="dashboard-title"
onClick={canEdit ? onStartEdit : undefined}
>
{title}
</Typography.Text>
</TooltipSimple>
)}
{isPublicDashboard && (
<TooltipSimple title="This dashboard is publicly accessible">
<Globe size={14} />
</TooltipSimple>
)}
{isDashboardLocked && (
<TooltipSimple title="This dashboard is locked">
<LockKeyhole size={14} />
</TooltipSimple>
)}
</div>
{hasTags && (
<div className={styles.dashboardTags}>
{tags.map((tag) => (
<Badge key={tag} color="warning" variant="outline">
{tag}
</Badge>
))}
</div>
)}
{hasDescription && (
<Typography.Text color="muted">{description}</Typography.Text>
)}
</div>
);
}
export default DashboardInfo;

View File

@@ -0,0 +1,20 @@
.dashboardPageToolbarContainer {
position: sticky;
top: 0;
z-index: 10;
color: var(--l2-foreground);
background-color: var(--l1-background);
padding: 16px;
box-shadow: 0 2px 2px 0px var(--l2-border);
}
.dashboardPageToolbarSubContainer {
width: 100%;
}
.dashboardInfoWithActions {
display: flex;
align-items: flex-start;
justify-content: space-between;
width: 100%;
}

View File

@@ -1,6 +1,5 @@
import { useCallback, useMemo } from 'react';
import { FullScreenHandle } from 'react-full-screen';
import { Card } from 'antd';
import { toast } from '@signozhq/ui/sonner';
import logEvent from 'api/common/logEvent';
import {
@@ -13,34 +12,31 @@ import type {
DashboardtypesJSONPatchOperationDTO,
} from 'api/generated/services/sigNoz.schemas';
import { Base64Icons } from 'container/DashboardContainer/DashboardSettings/General/utils';
import useComponentPermission from 'hooks/useComponentPermission';
import { useAppContext } from 'providers/App/App';
import { usePanelTypeSelectionModalStore } from 'providers/Dashboard/helpers/panelTypeSelectionModalHelper';
import { useErrorModal } from 'providers/ErrorModalProvider';
import APIError from 'types/api/error';
import DashboardHeader from '../components/DashboardHeader/DashboardHeader';
import DashboardActions from './DashboardActions/DashboardActions';
import DashboardMeta from './DashboardMeta/DashboardMeta';
import DashboardTitle from './DashboardTitle/DashboardTitle';
import { useEditableTitle } from './DashboardTitle/useEditableTitle';
import DashboardInfo from './DashboardInfo/DashboardInfo';
import { useEditableTitle } from './DashboardInfo/useEditableTitle';
import styles from './DashboardDescription.module.scss';
import styles from './DashboardPageToolbar.module.scss';
interface DashboardDescriptionProps {
interface DashboardPageToolbarProps {
dashboard: DashboardtypesGettableDashboardV2DTO;
handle: FullScreenHandle;
refetch: () => void;
}
function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
function DashboardPageToolbar(props: DashboardPageToolbarProps): JSX.Element {
const { dashboard, handle, refetch } = props;
const id = dashboard.id;
const isDashboardLocked = !!dashboard.locked;
const title = dashboard.spec?.display?.name ?? '';
const description = dashboard.spec?.display?.description ?? '';
const title = dashboard.spec.display.name;
const description = dashboard.spec.display.description ?? '';
const image = dashboard.image || Base64Icons[0];
const tags = useMemo(
() =>
@@ -51,7 +47,6 @@ function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
);
const { user } = useAppContext();
const [editDashboard] = useComponentPermission(['edit_dashboard'], user.role);
const { showErrorModal } = useErrorModal();
const setIsPanelTypeSelectionModalOpen = usePanelTypeSelectionModalStore(
(s) => s.setIsPanelTypeSelectionModalOpen,
@@ -59,9 +54,6 @@ function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
const isAuthor =
!!user?.email && !!dashboard.createdBy && dashboard.createdBy === user.email;
const addPanelPermission = !isDashboardLocked;
// V2 public dashboard wiring lives separately; treat as not-public for chrome.
const isPublicDashboard = false;
const handleLockDashboardToggle = useCallback(async (): Promise<void> => {
if (!id) {
@@ -110,7 +102,7 @@ function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
onSave: onNameSave,
});
const onEmptyWidgetHandler = useCallback((): void => {
const onAddPanel = useCallback((): void => {
void logEvent('Dashboard Detail V2: Add new panel clicked', {
dashboardId: id,
});
@@ -118,15 +110,15 @@ function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
}, [id, setIsPanelTypeSelectionModalOpen]);
return (
<Card className={styles.dashboardDescriptionContainer}>
<DashboardHeader title={title} image={image} />
<section className={styles.dashboardDetails}>
<DashboardTitle
<section className={styles.dashboardPageToolbarContainer}>
<div className={styles.dashboardInfoWithActions}>
<DashboardInfo
title={title}
image={image}
isPublicDashboard={isPublicDashboard}
tags={tags}
description={description}
isPublicDashboard={false}
isDashboardLocked={isDashboardLocked}
isEditable={editDashboard}
isEditing={isEditing}
draft={draft}
onDraftChange={setDraft}
@@ -135,20 +127,18 @@ function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
onCancel={cancel}
/>
<DashboardActions
title={title}
dashboard={dashboard}
handle={handle}
isDashboardLocked={isDashboardLocked}
editDashboard={editDashboard}
isAuthor={isAuthor}
addPanelPermission={addPanelPermission}
onAddPanel={onEmptyWidgetHandler}
onAddPanel={onAddPanel}
onLockToggle={handleLockDashboardToggle}
onOpenRename={startEdit}
/>
</section>
<DashboardMeta tags={tags} description={description} />
</Card>
</div>
</section>
);
}
export default DashboardDescription;
export default DashboardPageToolbar;

View File

@@ -1,3 +1,8 @@
.tabsContent {
padding-left: 0 !important;
padding-right: 0 !important;
}
.placeholder {
padding: 24px;
}
@@ -9,3 +14,10 @@
line-height: 1;
padding-top: 4px;
}
// shared "settings card" wrapper, used by the dashboard-info form and cross-panel sync
.settingsCard {
padding: 24px 16px;
border-radius: 3px;
border: 1px solid var(--l2-border);
}

View File

@@ -1,85 +0,0 @@
import { Dispatch, SetStateAction } from 'react';
// eslint-disable-next-line signoz/no-antd-components -- TODO: migrate Select/Input to @signozhq/ui
import { Col, Input, Select, Space } from 'antd';
import { Typography } from '@signozhq/ui/typography';
import AddTags from 'container/DashboardContainer/DashboardSettings/General/AddBadges';
import { Base64Icons } from '../utils';
import styles from '../GeneralSettings.module.scss';
const { Option } = Select;
interface GeneralFormProps {
title: string;
description: string;
image: string;
tags: string[];
onTitleChange: (value: string) => void;
onDescriptionChange: (value: string) => void;
onImageChange: (value: string) => void;
onTagsChange: Dispatch<SetStateAction<string[]>>;
}
function GeneralForm({
title,
description,
image,
tags,
onTitleChange,
onDescriptionChange,
onImageChange,
onTagsChange,
}: GeneralFormProps): JSX.Element {
return (
<Col className={styles.overviewSettings}>
<Space direction="vertical" className={styles.formSpace}>
<div>
<Typography className={styles.dashboardName}>Dashboard Name</Typography>
<section className={styles.nameIconInput}>
<Select
defaultActiveFirstOption
data-testid="dashboard-image"
suffixIcon={null}
rootClassName={styles.dashboardImageInput}
value={image}
onChange={onImageChange}
>
{Base64Icons.map((icon) => (
<Option value={icon} key={icon}>
<img
src={icon}
alt="dashboard-icon"
className={styles.listItemImage}
/>
</Option>
))}
</Select>
<Input
data-testid="dashboard-name"
className={styles.dashboardNameInput}
value={title}
onChange={(e): void => onTitleChange(e.target.value)}
/>
</section>
</div>
<div>
<Typography className={styles.dashboardName}>Description</Typography>
<Input.TextArea
data-testid="dashboard-desc"
rows={6}
value={description}
className={styles.descriptionTextArea}
onChange={(e): void => onDescriptionChange(e.target.value)}
/>
</div>
<div>
<Typography className={styles.dashboardName}>Tags</Typography>
<AddTags tags={tags} setTags={onTagsChange} />
</div>
</Space>
</Col>
);
}
export default GeneralForm;

View File

@@ -1,238 +0,0 @@
.overviewContent {
display: flex;
flex-direction: column;
gap: 24px;
padding: 20px 16px;
}
.overviewSettings {
padding: 16px;
border-radius: 3px;
border: 1px solid var(--l1-border);
}
.crossPanelSyncGroup {
display: flex;
flex-direction: column;
gap: 16px;
}
.formSpace {
width: 100%;
display: flex;
flex-direction: column;
gap: 21px;
}
.crossPanelSyncSectionTitle {
color: var(--l1-foreground);
font-family: Inter;
font-size: 14px;
font-weight: 500;
line-height: 20px;
}
.crossPanelSyncSectionHeader {
display: flex;
align-items: center;
gap: 6px;
align-self: flex-start;
}
.crossPanelSyncInfoIcon {
cursor: help;
color: var(--l3-foreground);
}
.crossPanelSyncTooltipContent {
display: flex;
flex-direction: column;
gap: 8px;
max-width: 300px;
}
.crossPanelSyncTooltipTitle {
font-size: 14px;
}
.crossPanelSyncTooltipDescription {
font-size: 12px;
line-height: 1.5;
}
.crossPanelSyncTooltipDocLink {
display: flex;
align-items: center;
gap: 4px;
color: var(--primary-background);
font-size: 12px;
margin-top: 4px;
}
.crossPanelSyncRow {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
gap: 16px;
& + & {
padding-top: 16px;
border-top: 1px solid var(--l1-border);
}
}
.crossPanelSyncInfo {
display: flex;
flex-direction: column;
gap: 4px;
}
.crossPanelSyncTitle {
color: var(--l2-foreground);
font-family: Inter;
font-size: 14px;
font-weight: 400;
line-height: 20px;
}
.crossPanelSyncDescription {
color: var(--l3-foreground);
font-family: Inter;
font-size: 13px;
font-weight: 400;
line-height: 20px;
}
.nameIconInput {
display: flex;
}
.dashboardImageInput {
:global(.ant-select-selector) {
display: flex;
width: 32px;
height: 32px;
padding: 6px;
justify-content: center;
align-items: center;
border-radius: 2px 0px 0px 2px;
border: 1px solid var(--l1-border) !important;
background: var(--l3-background) !important;
:global(.ant-select-selection-item) {
display: flex;
align-items: center;
}
}
&:global(.ant-select-dropdown) {
padding: 0px !important;
}
:global(.ant-select-item) {
padding: 0px;
align-items: center;
justify-content: center;
:global(.ant-select-item-option-content) {
display: flex;
align-items: center;
justify-content: center;
}
}
}
.listItemImage {
height: 16px;
width: 16px;
}
.dashboardNameInput {
border-radius: 0px 2px 2px 0px;
border: 1px solid var(--l1-border);
background: var(--l3-background);
}
.dashboardName {
color: var(--l2-foreground);
font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 20px;
margin-bottom: 0.5rem;
}
.descriptionTextArea {
padding: 6px 6px 6px 8px;
border-radius: 2px;
border: 1px solid var(--l1-border);
background: var(--l3-background);
}
.overviewSettingsFooter {
display: flex;
justify-content: space-between;
align-items: center;
width: -webkit-fill-available;
padding: 12px 16px 12px 0px;
position: fixed;
bottom: 0;
height: 32px;
border-top: 1px solid var(--l1-border);
background: var(--l2-background);
}
.unsaved {
display: flex;
align-items: center;
gap: 8px;
}
.unsavedDot {
width: 6px;
height: 6px;
border-radius: 50px;
background: var(--primary-background);
box-shadow: 0px 0px 6px 0px
color-mix(in srgb, var(--primary-background) 40%, transparent);
}
.unsavedChanges {
color: var(--bg-robin-400);
font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 24px;
letter-spacing: -0.07px;
}
.footerActionBtns {
display: flex;
gap: 8px;
}
.discardBtn {
display: flex;
align-items: center;
color: var(--l1-foreground);
font-family: Inter;
font-size: 12px;
font-style: normal;
font-weight: 500;
line-height: 24px;
}
.saveBtn {
display: flex;
align-items: center;
margin: 0px !important;
color: var(--l1-foreground);
font-family: Inter;
font-size: 12px;
font-style: normal;
font-weight: 500;
line-height: 24px;
}

View File

@@ -0,0 +1,86 @@
.crossPanelSyncGroup {
display: flex;
flex-direction: column;
gap: 20px;
}
.crossPanelSyncSectionHeader {
display: flex;
align-items: center;
gap: 6px;
}
.crossPanelsSyncSectionTitle {
color: var(--l1-foreground);
font-size: 14px;
font-weight: 500;
}
.crossPanelSyncInfoIcon {
cursor: help;
}
.crossPanelSyncTooltipContent {
display: flex;
flex-direction: column;
gap: 8px;
padding: 8px;
max-width: 200px;
}
.crossPanelSyncTooltipTitle {
font-size: 14px;
}
.crossPanelSyncTooltipDescription {
font-size: 12px;
}
.crossPanelSyncTooltipDocLink {
color: var(--primary-background);
font-size: 12px;
margin-top: 16px;
vertical-align: middle;
// typography override
--typography-text-display: inline-flex;
align-items: center;
gap: 5px;
}
.crossPanelSyncRow {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
align-items: center;
gap: 16px;
& + & {
padding-top: 16px;
border-top: 1px dashed var(--l2-border);
}
}
.crossPanelSyncInfo {
display: flex;
flex: 1 1 80px;
min-width: 0;
flex-direction: column;
gap: 4px;
}
.crossPanelSyncTitle {
color: var(--l2-foreground);
font-family: Inter;
font-size: 14px;
font-weight: 400;
line-height: 20px;
}
.crossPanelSyncDescription {
color: var(--l3-foreground);
font-family: Inter;
font-size: 13px;
font-weight: 400;
line-height: 20px;
}

View File

@@ -1,6 +1,5 @@
// eslint-disable-next-line signoz/no-antd-components -- TODO: migrate Radio to @signozhq/ui/radio-group
import { Col, Radio, Tooltip } from 'antd';
import { ExternalLink, SolidInfoCircle } from '@signozhq/icons';
import { TooltipSimple } from '@signozhq/ui/tooltip';
import { Typography } from '@signozhq/ui/typography';
import logEvent from 'api/common/logEvent';
import { Events } from 'constants/events';
@@ -13,7 +12,9 @@ import {
import { getAbsoluteUrl } from 'utils/basePath';
import cx from 'classnames';
import styles from '../GeneralSettings.module.scss';
import SegmentedControl from '../SegmentedControl/SegmentedControl';
import settingsStyles from '../../DashboardSettings.module.scss';
import styles from './CrossPanelSync.module.scss';
interface CrossPanelSyncProps {
dashboardId: string;
@@ -26,12 +27,15 @@ function CrossPanelSync({ dashboardId }: CrossPanelSyncProps): JSX.Element {
useSyncTooltipFilterMode(dashboardId);
return (
<Col className={cx(styles.overviewSettings, styles.crossPanelSyncGroup)}>
<div className={cx(settingsStyles.settingsCard, styles.crossPanelSyncGroup)}>
<div className={styles.crossPanelSyncSectionHeader}>
<Typography.Text className={styles.crossPanelSyncSectionTitle}>
<Typography.Text className={styles.crossPanelsSyncSectionTitle}>
Cross-Panel Sync
</Typography.Text>
<Tooltip
<TooltipSimple
side="top"
withPortal={false}
title={
<div className={styles.crossPanelSyncTooltipContent}>
<strong className={styles.crossPanelSyncTooltipTitle}>
@@ -40,7 +44,7 @@ function CrossPanelSync({ dashboardId }: CrossPanelSyncProps): JSX.Element {
<span className={styles.crossPanelSyncTooltipDescription}>
Sync crosshair and tooltip across all the dashboard panels
</span>
<a
<Typography.Link
href="https://signoz.io/docs/dashboards/interactivity/#cross-panel-sync"
target="_blank"
rel="noopener noreferrer"
@@ -48,15 +52,14 @@ function CrossPanelSync({ dashboardId }: CrossPanelSyncProps): JSX.Element {
>
Learn more
<ExternalLink size={12} />
</a>
</Typography.Link>
</div>
}
placement="top"
mouseEnterDelay={0.5}
>
<SolidInfoCircle size="md" className={styles.crossPanelSyncInfoIcon} />
</Tooltip>
</TooltipSimple>
</div>
<div className={styles.crossPanelSyncRow}>
<div className={styles.crossPanelSyncInfo}>
<Typography.Text className={styles.crossPanelSyncTitle}>
@@ -66,19 +69,18 @@ function CrossPanelSync({ dashboardId }: CrossPanelSyncProps): JSX.Element {
Sync crosshair and tooltip across all the dashboard panels
</Typography.Text>
</div>
<Radio.Group
<SegmentedControl
testId="cursor-sync-mode"
value={cursorSyncMode}
onChange={(e): void => {
setCursorSyncMode(e.target.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>
onChange={setCursorSyncMode}
options={[
{ label: 'No Sync', value: DashboardCursorSync.None },
{ label: 'Crosshair', value: DashboardCursorSync.Crosshair },
{ label: 'Tooltip', value: DashboardCursorSync.Tooltip },
]}
/>
</div>
{cursorSyncMode === DashboardCursorSync.Tooltip && (
<div className={styles.crossPanelSyncRow}>
<div className={styles.crossPanelSyncInfo}>
@@ -90,24 +92,25 @@ function CrossPanelSync({ dashboardId }: CrossPanelSyncProps): JSX.Element {
matching ones highlighted
</Typography.Text>
</div>
<Radio.Group
<SegmentedControl
testId="sync-tooltip-filter-mode"
value={syncTooltipFilterMode}
onChange={(e): void => {
onChange={(value): void => {
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);
}}
>
<Radio.Button value={SyncTooltipFilterMode.All}>All</Radio.Button>
<Radio.Button value={SyncTooltipFilterMode.Filtered}>
Filtered
</Radio.Button>
</Radio.Group>
options={[
{ label: 'All', value: SyncTooltipFilterMode.All },
{ label: 'Filtered', value: SyncTooltipFilterMode.Filtered },
]}
/>
</div>
)}
</Col>
</div>
);
}

View File

@@ -0,0 +1,80 @@
.formSpace {
display: flex;
flex-direction: column;
gap: 20px;
}
.infoItemContainer {
display: flex;
flex-direction: column;
gap: 4px;
}
.infoTitle {
color: var(--l2-foreground);
font-family: Inter;
font-size: 14px;
}
.nameIconInput {
display: flex;
gap: 4px;
[data-radix-popper-content-wrapper] {
z-index: 1100 !important;
}
}
.dashboardImageInput {
display: flex;
width: 32px;
min-width: 32px;
height: 32px;
padding: 6px;
justify-content: center;
align-items: center;
border-radius: 2px 0px 0px 2px;
background: var(--l3-background);
// icon-only trigger: drop the dropdown chevron, keep just the selected icon
svg {
display: none;
}
}
.dashboardImageOptions {
min-width: min-content;
}
.dashboardImageSelectItem {
width: min-content;
span {
vertical-align: middle;
}
}
.listItemImage {
height: 16px;
width: 16px;
}
.dashboardNameInput {
border-radius: 0px 2px 2px 0px;
border: 1px solid var(--l2-border);
}
.descriptionTextArea {
border-radius: 2px;
border: 1px solid var(--l2-border);
}
// the V1 tags input ships borderless; give the field a visible box to match
.tagsField {
display: flex;
align-items: center;
padding: 6px 8px;
border-radius: 2px;
border: 1px solid var(--l2-border);
// background: var(--l3-background);
}

View File

@@ -0,0 +1,101 @@
import { Dispatch, SetStateAction } from 'react';
import { Input } from '@signozhq/ui/input';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
} from '@signozhq/ui/select';
import { Typography } from '@signozhq/ui/typography';
// eslint-disable-next-line signoz/no-antd-components -- multiline TextArea has no @signozhq/ui equivalent yet
import { Input as AntdInput } from 'antd';
import AddTags from 'container/DashboardContainer/DashboardSettings/General/AddBadges';
import { Base64Icons } from '../utils';
import settingsStyles from '../../DashboardSettings.module.scss';
import styles from './DashboardInfoForm.module.scss';
interface DashboardInfoFormProps {
title: string;
description: string;
image: string;
tags: string[];
onTitleChange: (value: string) => void;
onDescriptionChange: (value: string) => void;
onImageChange: (value: string) => void;
onTagsChange: Dispatch<SetStateAction<string[]>>;
}
function DashboardInfoForm({
title,
description,
image,
tags,
onTitleChange,
onDescriptionChange,
onImageChange,
onTagsChange,
}: DashboardInfoFormProps): JSX.Element {
return (
<div className={settingsStyles.settingsCard}>
<div className={styles.formSpace}>
<div className={styles.infoItemContainer}>
<Typography className={styles.infoTitle}>Dashboard Name</Typography>
<section className={styles.nameIconInput}>
<Select
value={image}
onChange={(value): void => onImageChange(value as string)}
>
<SelectTrigger className={styles.dashboardImageInput} />
<SelectContent
className={styles.dashboardImageOptions}
withPortal={false}
>
{Base64Icons.map((icon) => (
<SelectItem
key={icon}
value={icon}
className={styles.dashboardImageSelectItem}
>
<img
src={icon}
alt="dashboard-icon"
className={styles.listItemImage}
/>
</SelectItem>
))}
</SelectContent>
</Select>
<Input
testId="dashboard-name"
className={styles.dashboardNameInput}
value={title}
onChange={(e): void => onTitleChange(e.target.value)}
/>
</section>
</div>
<div className={styles.infoItemContainer}>
<Typography className={styles.infoTitle}>Description</Typography>
<AntdInput.TextArea
data-testid="dashboard-desc"
rows={6}
value={description}
className={styles.descriptionTextArea}
onChange={(e): void => onDescriptionChange(e.target.value)}
/>
</div>
<div className={styles.infoItemContainer}>
<Typography className={styles.infoTitle}>Tags</Typography>
<div className={styles.tagsField}>
<AddTags tags={tags} setTags={onTagsChange} />
</div>
</div>
</div>
</div>
);
}
export default DashboardInfoForm;

View File

@@ -0,0 +1,5 @@
.overviewContent {
display: flex;
flex-direction: column;
gap: 24px;
}

View File

@@ -0,0 +1,61 @@
.segmented {
// override RadioGroup's default vertical grid; lay segments out connected
display: inline-flex;
grid-auto-flow: column;
gap: 0;
flex-shrink: 0;
border: 1px solid var(--l2-border);
border-radius: 2px;
}
.segment {
position: relative;
display: flex;
align-items: center;
&:not(:last-child) {
border-right: 1px solid var(--l2-border);
}
// the visible segment is the radio's label (htmlFor-wired, so clicks register)
label {
display: flex;
align-items: center;
min-height: 24px;
padding: 6px 14px;
font-family: Inter;
font-size: 13px;
line-height: 20px;
color: var(--l2-foreground);
white-space: nowrap;
cursor: pointer;
user-select: none;
}
}
// collapse the radio circle into a transparent full-cell click target
.segmentInput {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
margin: 0;
padding: 0;
border: none;
background: transparent;
cursor: pointer;
// hide the default radio dot/indicator
* {
display: none;
}
}
// highlight the selected segment as a raised, lighter pill (data-state is a
// stable Radix attribute). --l3-background is the lightest layer, so lift it
// further with a subtle foreground tint rather than going darker.
.segmentInput[data-state='checked'] + label {
background: var(--l3-background);
color: var(--l1-foreground);
font-weight: 500;
}

View File

@@ -0,0 +1,51 @@
import { RadioGroup, RadioGroupItem } from '@signozhq/ui/radio-group';
import styles from './SegmentedControl.module.scss';
export interface SegmentedControlOption<T extends string> {
label: string;
value: T;
}
interface SegmentedControlProps<T extends string> {
value: T;
options: SegmentedControlOption<T>[];
onChange: (value: T) => void;
testId?: string;
}
/**
* Connected pill segmented control composed on top of @signozhq/ui RadioGroup:
* the radio circle is collapsed into a transparent full-cell click target and
* the label becomes the visible segment (highlighted via the radio's stable
* `data-state="checked"`). Keeps radio semantics + keyboard nav.
*/
function SegmentedControl<T extends string>({
value,
options,
onChange,
testId,
}: SegmentedControlProps<T>): JSX.Element {
return (
<RadioGroup
className={styles.segmented}
value={value}
onChange={(next): void => onChange(next as T)}
testId={testId}
>
{options.map((option) => (
<RadioGroupItem
key={option.value}
value={option.value}
containerClassName={styles.segment}
className={styles.segmentInput}
testId={testId ? `${testId}-${option.value}` : undefined}
>
{option.label}
</RadioGroupItem>
))}
</RadioGroup>
);
}
export default SegmentedControl;

View File

@@ -0,0 +1,39 @@
.overviewSettingsFooter {
display: flex;
justify-content: space-between;
align-items: center;
width: -webkit-fill-available;
padding: 12px 16px 12px 16px;
position: fixed;
bottom: 0;
background: var(--l2-background);
}
.unsaved {
display: flex;
align-items: center;
gap: 8px;
}
.unsavedDot {
width: 6px;
height: 6px;
border-radius: 50px;
background: var(--primary-background);
box-shadow: 0px 0px 6px 0px
color-mix(in srgb, var(--primary-background) 40%, transparent);
}
.unsavedChanges {
color: var(--bg-robin-400);
font-family: Inter;
font-size: 14px;
font-weight: 400;
line-height: 24px;
letter-spacing: -0.07px;
}
.footerActionButtons {
display: flex;
gap: 8px;
}

View File

@@ -3,7 +3,7 @@ import { Button } from '@signozhq/ui/button';
import { Check, X } from '@signozhq/icons';
import { Typography } from '@signozhq/ui/typography';
import styles from '../GeneralSettings.module.scss';
import styles from './UnsavedChangesFooter.module.scss';
interface UnsavedChangesFooterProps {
count: number;
@@ -29,13 +29,13 @@ function UnsavedChangesFooter({
{count > 1 && 's'}
</Typography.Text>
</div>
<div className={styles.footerActionBtns}>
<div className={styles.footerActionButtons}>
<Button
variant="ghost"
color="secondary"
disabled={isSaving}
prefix={<X size={14} />}
onClick={onDiscard}
className={styles.discardBtn}
>
Discard
</Button>
@@ -47,7 +47,6 @@ function UnsavedChangesFooter({
prefix={<Check size={14} />}
testId="save-dashboard-config"
onClick={onSave}
className={styles.saveBtn}
>
{t('save')}
</Button>

View File

@@ -11,22 +11,22 @@ import APIError from 'types/api/error';
import { useDashboardStore } from '../../store/useDashboardStore';
import CrossPanelSync from './CrossPanelSync/CrossPanelSync';
import GeneralForm from './GeneralForm/GeneralForm';
import DashboardInfoForm from './DashboardInfoForm/DashboardInfoForm';
import UnsavedChangesFooter from './UnsavedChangesFooter/UnsavedChangesFooter';
import { Base64Icons, stringsToTags, tagsToStrings } from './utils';
import styles from './GeneralSettings.module.scss';
import styles from './Overview.module.scss';
interface GeneralSettingsProps {
interface OverviewProps {
dashboard: DashboardtypesGettableDashboardV2DTO;
}
function GeneralSettings({ dashboard }: GeneralSettingsProps): JSX.Element {
function Overview({ dashboard }: OverviewProps): JSX.Element {
const id = dashboard.id;
const refetch = useDashboardStore((s) => s.refetch);
const title = dashboard.spec?.display?.name ?? '';
const description = dashboard.spec?.display?.description ?? '';
const title = dashboard.spec.display.name;
const description = dashboard.spec.display.description ?? '';
const image = dashboard.image || Base64Icons[0];
const tagsAsStrings = useMemo(
() => tagsToStrings(dashboard.tags ?? []),
@@ -64,7 +64,7 @@ function GeneralSettings({ dashboard }: GeneralSettingsProps): JSX.Element {
value,
});
if (updatedTitle !== title) {
if (updatedTitle !== title && updatedTitle !== '') {
ops.push(replace('/spec/display/name', updatedTitle));
}
if (updatedDescription !== description) {
@@ -89,9 +89,6 @@ function GeneralSettings({ dashboard }: GeneralSettingsProps): JSX.Element {
]);
const onSaveHandler = useCallback(async (): Promise<void> => {
if (!id) {
return;
}
const ops = buildPatch();
if (ops.length === 0) {
return;
@@ -110,7 +107,7 @@ function GeneralSettings({ dashboard }: GeneralSettingsProps): JSX.Element {
}, [id, buildPatch, refetch, showErrorModal]);
useEffect(() => {
let n = 0;
let numberOfUnsavedChanges = 0;
const initialValues = [title, description, tagsAsStrings, image];
const updatedValues = [
updatedTitle,
@@ -120,10 +117,10 @@ function GeneralSettings({ dashboard }: GeneralSettingsProps): JSX.Element {
];
initialValues.forEach((val, index) => {
if (!isEqual(val, updatedValues[index])) {
n += 1;
numberOfUnsavedChanges += 1;
}
});
setNumberOfUnsavedChanges(n);
setNumberOfUnsavedChanges(numberOfUnsavedChanges);
}, [
description,
image,
@@ -144,7 +141,7 @@ function GeneralSettings({ dashboard }: GeneralSettingsProps): JSX.Element {
return (
<div className={styles.overviewContent}>
<GeneralForm
<DashboardInfoForm
title={updatedTitle}
description={updatedDescription}
image={updatedImage}
@@ -167,4 +164,4 @@ function GeneralSettings({ dashboard }: GeneralSettingsProps): JSX.Element {
);
}
export default GeneralSettings;
export default Overview;

View File

@@ -0,0 +1,106 @@
// settings card wrapper — mirrors the V1 public dashboard treatment
.publicDashboardCard {
display: flex;
flex-direction: column;
gap: 8px;
padding: 16px;
border-radius: 3px;
border: 1px solid var(--l2-border);
}
.statusTitle {
margin-bottom: 16px;
color: var(--l1-foreground);
font-family: Inter;
font-size: 14px;
font-weight: 500;
line-height: 20px;
}
.checkbox {
margin-bottom: 8px;
}
.timeRangeSelectGroup {
display: flex;
flex-direction: column;
gap: 4px;
margin-bottom: 8px;
}
.timeRangeSelectLabel {
color: var(--l2-foreground);
font-family: Inter;
font-size: 12px;
font-weight: 500;
line-height: 18px;
}
.timeRangeSelect {
width: 200px;
}
.urlGroup {
display: flex;
flex-direction: column;
gap: 4px;
}
.urlLabel {
color: var(--l2-foreground);
font-family: Inter;
font-size: 12px;
font-weight: 500;
line-height: 18px;
}
.urlContainer {
display: flex;
align-items: center;
gap: 8px;
padding: 0 4px;
border-radius: 4px;
border: 1px solid var(--l1-border);
background: var(--l3-background);
}
.urlText {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
color: var(--l2-foreground);
font-family: Inter;
font-size: 13px;
line-height: 32px;
}
.callout {
display: flex;
align-items: center;
gap: 8px;
margin-top: 12px;
padding: 12px 8px;
border-radius: 3px;
background: color-mix(in srgb, var(--primary-background) 10%, transparent);
}
.calloutIcon {
flex-shrink: 0;
color: var(--text-robin-300);
}
.calloutText {
color: var(--text-robin-300);
font-family: Inter;
font-size: 11px;
font-weight: 400;
line-height: 16px;
}
.actions {
display: flex;
justify-content: flex-end;
gap: 8px;
margin-top: 32px;
}

View File

@@ -0,0 +1,71 @@
import { Globe, Trash } from '@signozhq/icons';
import { Button } from '@signozhq/ui/button';
import styles from './PublicDashboard.module.scss';
interface PublicDashboardActionsProps {
isPublic: boolean;
disabled: boolean;
isPublishing: boolean;
isUpdating: boolean;
isUnpublishing: boolean;
onPublish: () => void;
onUpdate: () => void;
onUnpublish: () => void;
}
function PublicDashboardActions({
isPublic,
disabled,
isPublishing,
isUpdating,
isUnpublishing,
onPublish,
onUpdate,
onUnpublish,
}: PublicDashboardActionsProps): JSX.Element {
return (
<div className={styles.actions}>
{isPublic ? (
<>
<Button
variant="outlined"
color="destructive"
disabled={disabled}
loading={isUnpublishing}
prefix={<Trash size={14} />}
testId="public-dashboard-unpublish"
onClick={onUnpublish}
>
Unpublish dashboard
</Button>
<Button
variant="solid"
color="primary"
disabled={disabled}
loading={isUpdating}
prefix={<Globe size={14} />}
testId="public-dashboard-update"
onClick={onUpdate}
>
Update published dashboard
</Button>
</>
) : (
<Button
variant="solid"
color="primary"
disabled={disabled}
loading={isPublishing}
prefix={<Globe size={14} />}
testId="public-dashboard-publish"
onClick={onPublish}
>
Publish dashboard
</Button>
)}
</div>
);
}
export default PublicDashboardActions;

View File

@@ -0,0 +1,17 @@
import { Info } from '@signozhq/icons';
import { Typography } from '@signozhq/ui/typography';
import styles from './PublicDashboard.module.scss';
function PublicDashboardCallout(): JSX.Element {
return (
<div className={styles.callout}>
<Info size={12} className={styles.calloutIcon} />
<Typography.Text className={styles.calloutText}>
Dashboard variables won&apos;t work in public dashboards
</Typography.Text>
</div>
);
}
export default PublicDashboardCallout;

View File

@@ -0,0 +1,54 @@
import { Checkbox } from '@signozhq/ui/checkbox';
import { SelectSimple } from '@signozhq/ui/select';
import { Typography } from '@signozhq/ui/typography';
import { TIME_RANGE_PRESETS_OPTIONS } from './constants';
import styles from './PublicDashboard.module.scss';
interface PublicDashboardSettingsFormProps {
timeRangeEnabled: boolean;
defaultTimeRange: string;
disabled: boolean;
onTimeRangeEnabledChange: (value: boolean) => void;
onDefaultTimeRangeChange: (value: string) => void;
}
function PublicDashboardSettingsForm({
timeRangeEnabled,
defaultTimeRange,
disabled,
onTimeRangeEnabledChange,
onDefaultTimeRangeChange,
}: PublicDashboardSettingsFormProps): JSX.Element {
return (
<>
<Checkbox
id="public-dashboard-enable-time-range"
className={styles.checkbox}
testId="public-dashboard-time-range-toggle"
value={timeRangeEnabled}
disabled={disabled}
onChange={(checked): void => onTimeRangeEnabledChange(checked === true)}
>
Enable time range
</Checkbox>
<div className={styles.timeRangeSelectGroup}>
<Typography.Text className={styles.timeRangeSelectLabel}>
Default time range
</Typography.Text>
<SelectSimple
className={styles.timeRangeSelect}
testId="public-dashboard-default-time-range"
placeholder="Select default time range"
items={TIME_RANGE_PRESETS_OPTIONS}
value={defaultTimeRange}
disabled={disabled}
onChange={(value): void => onDefaultTimeRangeChange(value as string)}
/>
</div>
</>
);
}
export default PublicDashboardSettingsForm;

View File

@@ -0,0 +1,21 @@
import { Typography } from '@signozhq/ui/typography';
import styles from './PublicDashboard.module.scss';
interface PublicDashboardStatusProps {
isPublic: boolean;
}
function PublicDashboardStatus({
isPublic,
}: PublicDashboardStatusProps): JSX.Element {
return (
<Typography.Text className={styles.statusTitle}>
{isPublic
? 'This dashboard is publicly accessible. Anyone with the link can view it.'
: 'This dashboard is private. Publish it to make it accessible to anyone with the link.'}
</Typography.Text>
);
}
export default PublicDashboardStatus;

View File

@@ -0,0 +1,49 @@
import { Copy, ExternalLink } from '@signozhq/icons';
import { Button } from '@signozhq/ui/button';
import { Typography } from '@signozhq/ui/typography';
import styles from './PublicDashboard.module.scss';
interface PublicDashboardUrlProps {
url: string;
onCopy: () => void;
onOpen: () => void;
}
function PublicDashboardUrl({
url,
onCopy,
onOpen,
}: PublicDashboardUrlProps): JSX.Element {
return (
<div className={styles.urlGroup}>
<Typography.Text className={styles.urlLabel}>
Public dashboard URL
</Typography.Text>
<div className={styles.urlContainer}>
<Typography.Text className={styles.urlText}>{url}</Typography.Text>
<Button
variant="ghost"
size="icon"
aria-label="Copy public dashboard URL"
testId="public-dashboard-copy-url"
onClick={onCopy}
>
<Copy size={14} />
</Button>
<Button
variant="ghost"
size="icon"
aria-label="Open public dashboard in new tab"
testId="public-dashboard-open-url"
onClick={onOpen}
>
<ExternalLink size={14} />
</Button>
</div>
</div>
);
}
export default PublicDashboardUrl;

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