Compare commits

..

157 Commits

Author SHA1 Message Date
Srikanth Chekuri
2ab83a0e96 Merge branch 'main' into e2e/alert_manager 2026-07-16 01:25:46 +05:30
Vinicius Lourenço
65fde71b72 feat(infrastructure-monitoring-v2): add counts cards (#12120)
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(entity-count): add base structure to show count

* feat(entity-count): add count for clusters

* feat(entity-count): add count for namespaces

* chore(infra-monitoring): move component to be under components folder

* fix(pr): address comments
2026-07-15 13:39:48 +00:00
Vinicius Lourenço
7f5f63b20a feat(infra-monitoring): add docs for every chart (#12037)
* feat(infra-monitoring): add docs for every chart

* fix(docs): add missing docs for each column/chart

* fix(infra-monitoring): keep referer

---------

Co-authored-by: Nikhil Mantri <nikhil.mantri1999@gmail.com>
2026-07-15 13:25:53 +00:00
Tushar Vats
63cfbe8bfb fix: convert key not found to warnings for traces (#12091) 2026-07-15 12:33:47 +00:00
Aditya Singh
0c28b8b251 feat: scalar/table serializers for client-side export (3) (#12070)
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
* refactor(data-export): extract the withUnit header helper

Both the timeseries and the upcoming table serializers append units to headers (and skip display-only ids like 'short'/'none') — move the helper to its own module.

* feat(data-export): add generic table-model serializer

exportTableData serializes any prepared antd-style table model ({name, key, isValueColumn} columns + record rows) into a SerializedTable — raw values in display order, units on value columns only, blanks for missing cells. Surface-agnostic: QueryTable-based tables, dashboard tables and plain antd tables all adapt in a line or two.

* feat(data-export): add scalar queryRange serializer

exportScalarData takes the queryRange response object (formatForWeb webTables payload) + the builder query and serializes via createTableColumnsFromQuery — the exact preparer QueryTable renders from — so exports inherit the on-screen merge, naming and column order 1:1.
2026-07-15 05:37:15 +00:00
Aditya Singh
10a0d262d9 feat: add download option to trace view (#12116) 2026-07-15 05:36:59 +00:00
Vinicius Lourenço
334155a226 fix(infra-monitoring-v2): ensure workloads has filters by cluster+namespace (#12076)
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(tanstack): add support for itemKey be object

* feat(infra-monitoring): add base structure for multiple selected items

* refactor(infra-monitoring): add cluster for namespaces, cluster/namespace for volumes + update other entities

* refactor(infra-monitoring): add cluster + namespace for statefulsets

* refactor(infra-monitoring): add cluster + namespace for jobs

* refactor(infra-monitoring): add cluster + namespace for deployments

* refactor(infra-monitoring): add cluster + namespace for daemonsets

* test(infra-monitoring): add new tests for changes of k8s base list

* fix(pr): address comments
2026-07-14 18:24:42 +00:00
Pandey
42de840534 fix(rules): scope alert rule store operations by org (#12117)
* fix(rules): scope alert rule store operations by org

The rule store predicates filtered on the rule id only, so on multi-org
deployments (Cloud/EE/multi-org self-hosted, where orgs share the
instance via the noop sharder) an authenticated user could read, edit or
delete another org's alert rules by supplying the target rule UUID.
ViewAccess/EditAccess only check the caller's own-org role, never the
resource's org, so nothing enforced tenant isolation on the rule itself.

Enforce org scoping at the store layer, which is the durable fix:

- GetStoredRule and DeleteRule now take the caller's orgID and add
  `org_id = ?` to their predicates.
- EditRule adds `org_id = ?` (from the model, which is now always
  sourced from an org-scoped read).
- The manager passes claims.OrgID on every by-id path; GetRule now
  derives the org from claims (it previously fetched by id alone).

Cross-org ids now resolve to NotFound instead of leaking or mutating
another org's rule. Single-org OSS instances are unaffected.

CWE-639 (authorization bypass through user-controlled key).

* refactor(rules): derive claims org id with valuer.MustNewUUID

Claims are pre-validated by the auth middleware, so the NewUUID error
branch is dead code. Follow the handler convention (docs/contributing/
go/handler.md) and use the Must constructor on the by-id rule paths.
2026-07-14 18:00:09 +00:00
Nityananda Gohain
891106d1ca fix: set correct openapi response struct for span mapper list (#12094)
Some checks failed
build-staging / prepare (push) Has been cancelled
build-staging / js-build (push) Has been cancelled
build-staging / go-build (push) Has been cancelled
build-staging / staging (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
* fix: set correct opapi response model for span mapper list

* fix: change group_id to groupId in response

* fix: format properly

* fix: update fixtures
2026-07-14 15:34:52 +00:00
Naman Verma
707f01bd9c chore: support non-dsl search in dashboards v2 list apis (#12051)
* chore: support non-dsl search in dashboards v2 list apis

* chore: move free text compilation to visitor

* chore: shorten comments

* fix: support both plain text and dsl together

* fix: ensure empty description dashboards are not excluded in NOT checks
2026-07-14 13:02:58 +00:00
Vinicius Lourenço
3cd0c21703 feat(daemonsets): add ready/misscheduled columns (#12102) 2026-07-14 12:33:21 +00:00
Abhi kumar
181feb3eb9 fix(dashboard-v2): only refetch on-screen panels on time change/auto-refresh (#12113)
Panel visibility was latched (isObserverOnce), so once every panel had
scrolled into view they all stayed query-enabled. A time change or
auto-refresh re-keys every panel's query, refetching all of them at once.

Track the live viewport instead so off-screen panels stay query-disabled
and skip the refetch; they fetch lazily when scrolled back in. Add
staleTime: Infinity so re-entering an unchanged window serves cache rather
than thrash-refetching (key changes and manual refetch still run).
2026-07-14 12:23:04 +00:00
Naman Verma
bf35d12398 chore: add functional unique index for tags (#12029)
* feat: add functional unique index

* chore: add tag migration

* chore: add failing scratch test (to discuss)

* chore: fetch expressions as well in getindices call

* fix: remove tag unique index (will be added in a separate PR)

* fix: remove tag unique index (will be added in a separate PR)

* chore: remove temporary tests

* chore: add functional unique index for tags

* fix: go lint fix

* fix: go lint fix

* chore: better comment for unique index

* test: add test for case insensitive expression equality

* test: add equality test for columns with quotes and capital letters

* test: add test for quoted columns

* chore: add separate type for unique indices with expressions

* fix: update migration file to use correct index

* test: add test for postgres provider

* test: add test for postgres provider

* fix: remove round trip tests
2026-07-14 11:08:59 +00:00
Abhi kumar
1a1f7b2d78 fix(dashboard-v2): bug bash fixes IV (#12056)
* fix(context-menu): keep overlay clickable when opened inside a modal

Portal the popover to body and set pointer-events:auto so a modal's dialog/backdrop can't trap it.

* fix(dashboard-v2): center scrolled-to panels and sections

Default the scroll block to 'center' so a targeted panel lands mid-viewport.

* fix(dashboard-v2): make the view modal refresh button secondary

Use outlined/secondary so refresh doesn't compete with the primary action.

* fix(dashboard-v2): fetch only on-screen panels on dashboard load

RGL unfolds panels from the top-left origin on mount, briefly piling them into the viewport.

The latched observer then marked all visible; suppress the item transition for the first frame.

* chore: readded entry animation
2026-07-14 10:43:14 +00:00
dependabot[bot]
a2778aa45f chore(deps): bump golang.org/x/net from 0.54.0 to 0.55.0 (#11965)
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.54.0 to 0.55.0.
- [Commits](https://github.com/golang/net/compare/v0.54.0...v0.55.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-version: 0.55.0
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-14 10:01:13 +00:00
Srikanth Chekuri
16c1fce326 fix(ruletypes): address rule read round-trip gaps (#12110)
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-07-14 08:36:30 +00:00
Gaurav Tewari
06b762b7ef fix(llm o11y): update unpriced models tab test case (#12108)
* fix: update unpriced models tab test case

* chore: update ci timming

* chore: update comment

* chore: remove comment

---------

Co-authored-by: Gaurav Tewari <tewarig@users.noreply.github.com>
2026-07-14 08:22:03 +00:00
Aditya Singh
466edf1f1c feat: timeseries export in logs, traces and metrics explorers (2) (#12055)
* feat(data-export): surface raw V5 response and legend map from GetMetricQueryRange

Keeps the pre-conversion V5 queryRange response and the per-query legend map on the returned MetricQueryRangeSuccessResponse so explorer views can feed client-side export without a refetch. Additive optional fields; the legacy V3 render path is untouched. One chokepoint covers Logs, Traces and Metrics.

* fix(data-export): omit display-only format ids from export headers

'short'/'none' are Grafana-style display formats, not physical units — meaningful on a chart axis but misleading as 'value (short)' in a CSV header. Real units (ms, bytes, ...) are unchanged.

* feat(data-export): timeseries export menu and opt-in TimeSeriesView header

New TimeseriesExportMenu (csv/jsonl, LONG shape default; same look as DownloadOptionsMenu, which stays raw/list-only) rendered from a new opt-in TimeSeriesView header row: unit selector left (onYAxisUnitChange — rendering only, unit state stays with each view), download right (allowExport, needs the surfaced raw V5). Both default off, so views opt in explicitly. Includes gating + menu tests.

* feat(explorer): enable timeseries export in Logs, Traces and Metrics

Logs and Traces pass allowExport + onYAxisUnitChange and drop their external unit-filter headers (one header row: unit left, download right). Metrics passes allowExport only, per chart — its YAxisUnitSelector stays screen-level (persists to the metric, spans N charts). convertDataValueToMs now converts the raw V5 tree too, so Traces duration exports carry the ms values the chart shows instead of raw nanoseconds.

* refactor(data-export): use @signozhq/ui components in the export popover

Review feedback: swap antd Button/Popover/Tooltip for the design-system equivalents (radix-style Popover with controlled open, TooltipSimple, Button with native loading). Styles restructured for the DS DOM with periscope font tokens; test renders via tests/test-utils for the TooltipProvider.
2026-07-14 06:54:51 +00:00
Nikhil Mantri
849353b5c0 feat(infra-monitoring): counts query for counting related entities (#12000)
* chore: added types and open api spec changes

* chore: added method to calculate reason

* chore: per group pod status counts with req metric checks method added

* chore: wired up pod status counts

* chore: pod restarts type added

* chore: added restart counts for the group

* chore: bug in query fix

* chore: onboarding API changes

* chore: integration tests added

* chore: added podcountsbyphase in other entities

* chore: added pod status counts for other entities

* chore: added integration tests for other entities

* chore: added checks api changes for other entities

* chore: rearrangement

* chore: removed succeeded status and mark it as completed

* chore: query beautified

* chore: corrected metrics list for metadata lookup

* chore: removed dead constants

* chore: goroutines for ListHosts

* chore: goroutines for ListPods

* chore: goroutines for ListNodes

* chore: goroutines for ListNamespaces

* chore: goroutines for ListClusters

* chore: goroutines for ListDeployments

* chore: goroutines for ListStatefulsets

* chore: added goroutines for ListStatefulsets, ListJobs and ListDaemonsets

* chore: added function

* chore: added struct changes

* chore: added count attr keys

* chore: wired counts to the response fields

* chore: regenerated API spec

* chore: merged main, resolved conflicts

* chore: nodes count surfacing

* chore: integration tests added

* fix: use tuple mapping to uniquely identify attrs:

* chore: integration tests update
2026-07-14 06:38:27 +00:00
Niladri Adhikary
821dc233bf fix(alertmanager): integration tests fixes (#11710)
* fix: integration tests

Signed-off-by: Niladri Adhikary <niladrix719@gmail.com>

* test(alertmanager): add logs and traces e2e tests for custom templating

Signed-off-by: Niladri Adhikary <niladrix719@gmail.com>

* fix: missing annotations

Signed-off-by: Niladri Adhikary <niladrix719@gmail.com>

* fix: annotations and templating fixes

Signed-off-by: Niladri Adhikary <niladrix719@gmail.com>

* chore: removed annotations

Signed-off-by: Niladri Adhikary <niladrix719@gmail.com>

* fix: tests removed annotations

Signed-off-by: Niladri Adhikary <niladrix719@gmail.com>

* fix: integration tests

Signed-off-by: Niladri Adhikary <niladrix719@gmail.com>

* chore: py-lint run

Signed-off-by: Niladri Adhikary <niladrix719@gmail.com>

---------

Signed-off-by: Niladri Adhikary <niladrix719@gmail.com>
2026-06-15 11:21:39 +05:30
Srikanth Chekuri
05c3657ff9 Merge branch 'main' into e2e/alert_manager 2026-06-13 17:06:51 +05:30
Niladri Adhikary
9c066a3389 test(alertmanager): fix integration tests 2026-06-13 17:06:04 +05:30
srikanthccv
5cb04161c2 chore: stale changes 2026-05-25 23:07:21 +05:30
srikanthccv
1a23dbf4df Merge branch 'main' into e2e/alert_manager 2026-05-25 22:58:19 +05:30
Abhishek Kumar Singh
7ab9272a71 chore: moved alertmanager test to new package 2026-04-24 08:50:38 +05:30
Abhishek Kumar Singh
df35a8e0a4 chore: increased alert time to avoid flaky tests 2026-04-23 21:53:09 +05:30
Abhishek Kumar Singh
bee369309c chore: updated email test data with newline changes 2026-04-23 21:38:22 +05:30
Abhishek Kumar Singh
096602cf8b chore: supress line size linter for notification template 2026-04-23 21:32:55 +05:30
Abhishek Kumar Singh
96d423eca3 chore: updates test data based on new changes in templater 2026-04-23 21:29:18 +05:30
Abhishek Kumar Singh
64c736d549 chore: refactor as per new code 2026-04-23 21:03:01 +05:30
Abhishek Kumar Singh
04ee77d533 Merge branch 'chore/alert_templater_connecting_pieces' into e2e/alert_manager 2026-04-23 20:43:42 +05:30
Abhishek Kumar Singh
d40b0cb5da Merge branch 'main' into chore/alert_templater_connecting_pieces 2026-04-23 20:43:21 +05:30
Abhishek Kumar Singh
bcc17f1c0d Merge branch 'chore/alert_templater_connecting_pieces' into e2e/alert_manager 2026-04-23 20:41:06 +05:30
Abhishek Kumar Singh
05de5c6a17 Merge branch 'main' into chore/alert_templater_connecting_pieces 2026-04-23 20:22:02 +05:30
Abhishek Kumar Singh
ffe05af591 chore: updated email template based on new template struct 2026-04-23 20:21:20 +05:30
Abhishek Kumar Singh
64b45a36a8 Merge branch 'chore/alert_templater_connecting_pieces' into e2e/alert_manager 2026-04-23 16:56:38 +05:30
Abhishek Kumar Singh
75728c9918 chore: remove private annotations from pagerduty notifier 2026-04-23 16:55:53 +05:30
Abhishek Kumar Singh
b08578acb4 Merge branch 'chore/alert_templater_connecting_pieces' into e2e/alert_manager 2026-04-23 12:05:28 +05:30
Srikanth Chekuri
99208b89c1 Merge branch 'main' into chore/alert_templater_connecting_pieces 2026-04-23 03:05:05 +05:30
Abhishek Kumar Singh
8454cc8609 refactor: msteams skip logs and traces as factsset, slack code refactor 2026-04-22 20:02:15 +05:30
Abhishek Kumar Singh
bf9220596c chore: updated webhook notifier to send templated title and body in notification 2026-04-22 19:56:01 +05:30
Abhishek Kumar Singh
89fc758e7f chore: removed notification processor 2026-04-22 19:06:24 +05:30
Abhishek Kumar Singh
53f17ac362 refactor: changes as per internal review 2026-04-22 19:04:10 +05:30
Abhishek Kumar Singh
87e66b041d Merge branch 'feat/markdown_renderer' into chore/alert_templater_connecting_pieces 2026-04-20 12:34:33 +05:30
Abhishek Kumar Singh
36e3bad95d Merge branch 'main' into feat/markdown_renderer 2026-04-20 12:24:27 +05:30
Abhishek Kumar Singh
9b7a3a46ee refactor: changed markdown renderer from interface to package-level functions 2026-04-20 12:23:18 +05:30
Abhishek Kumar Singh
fadd421f9d Merge branch 'feat/markdown_renderer' into chore/alert_templater_connecting_pieces 2026-04-17 19:41:33 +05:30
Abhishek Kumar Singh
c36c18f79e Merge branch 'main' into feat/markdown_renderer 2026-04-17 19:37:36 +05:30
Abhishek Kumar Singh
5bb4079951 refactor: removed logger as markdown renderer dependency 2026-04-17 19:36:06 +05:30
Abhishek Kumar Singh
15a036904f chore: removed special handling for softline break 2026-04-17 19:32:09 +05:30
Abhishek Kumar Singh
af607bd249 Merge branch 'feat/alert_manager_template' into feat/markdown_renderer 2026-04-17 18:35:35 +05:30
Srikanth Chekuri
2eadc895a3 Merge branch 'main' into feat/alert_manager_template 2026-04-17 18:05:31 +05:30
Abhishek Kumar Singh
66e34c9b5e chore: lint issue 2026-04-17 17:50:05 +05:30
Abhishek Kumar Singh
799de1ece3 refactor: changes as per internal review 2026-04-17 17:15:07 +05:30
Abhishek Kumar Singh
c5c450c58c fix: concurrent rendering in markdown renderer 2026-04-16 18:20:25 +05:30
Abhishek Kumar Singh
dc67f8551f Merge branch 'feat/alert_manager_template' into feat/markdown_renderer 2026-04-16 17:59:31 +05:30
Abhishek Kumar Singh
c46c0e105a chore: removed notifier test files 2026-04-16 17:29:34 +05:30
Abhishek Kumar Singh
4aaa6ae5a1 fix: increase wait time for alertmanager server setup in notifier tests 2026-04-16 17:22:56 +05:30
Abhishek Kumar Singh
82abb9b113 chore: applied formatter + lint supressed for long config lines 2026-04-16 17:01:44 +05:30
Abhishek Kumar Singh
cc5a0b93ae Merge branch 'main' into feat/alert_manager_template 2026-04-16 16:46:44 +05:30
Abhishek Kumar Singh
e5d67f87eb chore: add alertmanager to integration CI workflow 2026-04-16 14:55:08 +05:30
Abhishek Kumar Singh
f2c56a9978 chore: maildev version bump 2026-04-16 14:52:35 +05:30
Abhishek Kumar Singh
15c593d797 chore: added maildev and notification channel to teardown 2026-04-16 14:44:35 +05:30
Abhishek Kumar Singh
9a47d3e553 Merge branch 'chore/alert_templater_connecting_pieces' into e2e/alert_manager 2026-04-16 14:29:22 +05:30
Abhishek Kumar Singh
b3fe077deb chore: remove static templates from pagerduty notifications 2026-04-16 14:28:27 +05:30
Abhishek Kumar Singh
14d30fa754 fix: email template directory for notification processor 2026-04-16 14:28:03 +05:30
Abhishek Kumar Singh
556bfe44d2 test: added e2e tests for notification content templating 2026-04-16 14:22:05 +05:30
Abhishek Kumar Singh
a9ab0bc480 chore: moved default notification channels to fixtures 2026-04-16 13:06:46 +05:30
Abhishek Kumar Singh
5eda220f88 test: alert manager supported notifier test 2026-04-14 20:25:16 +05:30
Abhishek Kumar Singh
a6ef54d6b9 Merge branch 'feat/markdown_renderer' into chore/alert_templater_connecting_pieces 2026-04-14 17:51:17 +05:30
Abhishek Kumar Singh
d1e332fb16 Merge branch 'feat/alert_manager_template' into feat/markdown_renderer 2026-04-14 17:51:05 +05:30
Abhishek Kumar Singh
c9f3e1ae26 Merge branch 'chore/am_custom_notifiers' into feat/alert_manager_template 2026-04-14 17:50:45 +05:30
Abhishek Kumar Singh
41ded342a1 Merge branch 'main' into chore/am_custom_notifiers 2026-04-14 17:50:27 +05:30
Abhishek Kumar Singh
7f22cb0442 chore: integrated slack mrkdwn renderer and added NoOp formatter 2026-04-14 17:46:33 +05:30
Abhishek Kumar Singh
6b77835050 feat: custom raw html renderer to escape <no value> 2026-04-14 17:44:57 +05:30
Abhishek Kumar Singh
909c3a80b1 feat: slack mrkdwn renderer 2026-04-14 17:44:15 +05:30
Abhishek Kumar Singh
42726747d8 Merge branch 'feat/alert_manager_template' into feat/markdown_renderer 2026-04-14 17:35:19 +05:30
Abhishek Kumar Singh
64ce90e418 fix: variables with symbols in template 2026-04-14 17:27:41 +05:30
Abhishek Kumar Singh
2fcffb7cdc feat: return single templating result from with flag for template type 2026-04-14 17:24:40 +05:30
Abhishek Kumar Singh
5ceb9255d1 chore: error logging + NoOp type definition 2026-04-14 16:28:54 +05:30
Abhishek Kumar Singh
1df7d75d43 feat: added Literal for CompareOperator and MatchType and expose from ruleManager 2026-04-14 15:18:21 +05:30
Abhishek Kumar Singh
1bbee9bc63 chore: fix linter and merge conflict issues 2026-04-14 14:59:55 +05:30
Abhishek Kumar Singh
581e7c8b19 Merge branch 'feat/markdown_renderer' into chore/alert_templater_connecting_pieces 2026-04-14 14:28:37 +05:30
Abhishek Kumar Singh
782eee23d2 Merge branch 'feat/alert_manager_template' into feat/markdown_renderer 2026-04-14 14:16:08 +05:30
Abhishek Kumar Singh
abc0d71c16 Merge branch 'chore/am_custom_notifiers' into feat/alert_manager_template 2026-04-13 22:13:25 +05:30
Abhishek Kumar Singh
2e2dd4c42b Merge branch 'main' into chore/am_custom_notifiers 2026-04-13 22:04:08 +05:30
Abhishek Kumar Singh
51621a3131 chore: added action links to email and slack notifiers 2026-04-06 20:44:24 +05:30
Abhishek Kumar Singh
0fd3979de5 chore: integration of custom templating in rule manager 2026-04-02 11:51:03 +05:30
Abhishek Kumar Singh
4f75075df0 feat: email rendering with custom template in notification processor 2026-03-31 20:28:02 +05:30
Abhishek Kumar Singh
b905d5cc5d feat: added no value extension to render <no value> in html 2026-03-31 20:16:32 +05:30
Abhishek Kumar Singh
6d1b9738b5 chore: lint fixes 2026-03-31 18:27:03 +05:30
Abhishek Kumar Singh
710cd8bdb3 Merge branch 'feat/markdown_renderer' into chore/alert_templater_connecting_pieces 2026-03-31 18:22:15 +05:30
Abhishek Kumar Singh
629929c6a6 Merge branch 'feat/alert_manager_template' into feat/markdown_renderer 2026-03-31 17:57:21 +05:30
Abhishek Kumar Singh
0ce76a94d6 Merge branch 'chore/am_custom_notifiers' into feat/alert_manager_template 2026-03-31 17:57:02 +05:30
Abhishek Kumar Singh
46ae74ced5 chore: updated email notifier from upstream 2026-03-31 11:52:08 +05:30
Abhishek Kumar Singh
2d8c1b7c86 chore: updated licenses for notifiers 2026-03-31 11:51:45 +05:30
Abhishek Kumar Singh
6602c8c523 refactor: lint fixes 2026-03-31 10:46:52 +05:30
Abhishek Kumar Singh
c22dbcbf74 refactor: review comments 2026-03-31 10:44:37 +05:30
Abhishek Kumar Singh
250bd9abeb Merge branch 'main' into chore/am_custom_notifiers 2026-03-31 10:15:39 +05:30
Abhishek Kumar Singh
605b218836 test: added test in notification procesor for no value 2026-03-30 19:53:49 +05:30
Abhishek Kumar Singh
99af679a62 fix: handled <no value> in templated response 2026-03-30 19:47:40 +05:30
Abhishek Kumar Singh
46123f925f fix: added handling for labels and annotations with . and - 2026-03-30 18:25:07 +05:30
Abhishek Kumar Singh
3e5e90f904 fix: webhook notifier update annotations before preparing data 2026-03-30 15:17:36 +05:30
Abhishek Kumar Singh
f8a614478c feat: updated slack notifier with slack mrkdwn format 2026-03-29 21:12:39 +05:30
Abhishek Kumar Singh
ffc54137ca test: add new test cases for Slack MRKDWN rendering 2026-03-29 20:08:08 +05:30
Abhishek Kumar Singh
34655db8cc test: simplify TestRenderSlackMrkdwn 2026-03-29 19:34:54 +05:30
Abhishek Kumar Singh
020140643c feat: added new format in markdown renderer 2026-03-29 19:22:28 +05:30
Abhishek Kumar Singh
6b8a4e4441 feat: slack mrkdwn renderer 2026-03-29 19:06:49 +05:30
Abhishek Kumar Singh
c345f579bb chore: updated alertmanagernotify package with updated notifier signature 2026-03-27 20:18:01 +05:30
Abhishek Kumar Singh
819c7e1103 feat: added notification processor in webhook notifier 2026-03-27 20:17:34 +05:30
Abhishek Kumar Singh
f0a1d07213 chore: added IsCustomTemplated helper function in result struct 2026-03-27 20:06:55 +05:30
Abhishek Kumar Singh
895e10b986 feat: added notification processor in pagerduty notifier 2026-03-27 20:05:04 +05:30
Abhishek Kumar Singh
78228b97ff feat: added notification processor in slack notifier 2026-03-27 19:37:13 +05:30
Abhishek Kumar Singh
826d763b89 feat: added notification processor in opsgenie notifier 2026-03-27 17:38:37 +05:30
Abhishek Kumar Singh
cb74acefc7 chore: msteams note 2026-03-27 17:00:24 +05:30
Abhishek Kumar Singh
eb79494e73 refactor: ms teams notifier 2026-03-27 16:58:52 +05:30
Abhishek Kumar Singh
28698d1af4 feat: update ms team notifier with notification processor 2026-03-27 16:45:33 +05:30
Abhishek Kumar Singh
be55cef462 feat: updated email notifier 2026-03-27 16:05:52 +05:30
Abhishek Kumar Singh
183e400280 chore: return isDefaultTemplated true even in case of blank default template 2026-03-26 19:14:01 +05:30
Abhishek Kumar Singh
5f0b43d975 chore: refactor notification processor and send processor in ReceiverIntegrations 2026-03-26 19:03:39 +05:30
Abhishek Kumar Singh
09adb8bef0 feat: alert notification processor 2026-03-26 16:04:28 +05:30
Abhishek Kumar Singh
77f5522e47 chore: return missing variables as sorted list 2026-03-26 16:03:41 +05:30
Abhishek Kumar Singh
c68154a031 feat: added no-op formatter in markdown rederer 2026-03-26 14:27:55 +05:30
Abhishek Kumar Singh
ec94a6555b refactor: alert manager templater 2026-03-25 20:01:12 +05:30
Abhishek Kumar Singh
f132dc28c3 chore: updated br with new line in test and logs added 2026-03-23 17:02:30 +05:30
Abhishek Kumar Singh
834df680f0 Merge branch 'feat/alert_manager_template' into feat/markdown_renderer 2026-03-23 16:48:26 +05:30
Abhishek Kumar Singh
48b9f15e18 feat: integrated slack blockit in markdownrenderer package and removed plaintext format 2026-03-23 16:45:29 +05:30
Abhishek Kumar Singh
55fa03fe7e test: added test for html rendering 2026-03-23 16:32:25 +05:30
Abhishek Kumar Singh
933717f309 feat: slack blockkit renderer using goldmark 2026-03-23 15:36:32 +05:30
Abhishek Kumar Singh
9ffc1203da chore: updated newline to markdown format 2026-03-19 22:50:24 +05:30
Abhishek Kumar Singh
205a78f0e6 feat: added basic html markdown templater 2026-03-17 20:17:57 +05:30
Abhishek Kumar Singh
79518b6823 Merge branch 'chore/am_custom_notifiers' into feat/alert_manager_template 2026-03-17 20:15:00 +05:30
Abhishek Kumar Singh
e6a9f49cec Merge branch 'main' into chore/am_custom_notifiers 2026-03-17 20:14:30 +05:30
Abhishek Kumar Singh
fd5fc40823 chore: updated comments 2026-03-16 18:19:03 +05:30
Abhishek Kumar Singh
db2e2a4617 chore: lint fix 2026-03-16 15:54:22 +05:30
Abhishek Kumar Singh
9368d3f393 refactor: comments and test improvements 2026-03-16 15:47:45 +05:30
Abhishek Kumar Singh
0c97ba36d6 refactor: test case and sb related changed 2026-03-16 15:12:23 +05:30
Abhishek Kumar Singh
2e1bdbc2fd chore: added test for missing function 2026-03-16 14:49:52 +05:30
Abhishek Kumar Singh
330737f779 chore: renamed the interface 2026-03-13 19:20:49 +05:30
Abhishek Kumar Singh
f0c531ae2b chore: lint fix 2026-03-13 19:11:34 +05:30
Abhishek Kumar Singh
54477ee786 feat: added support for and in templating 2026-03-13 19:09:39 +05:30
Abhishek Kumar Singh
d281f7b6a2 test: fix preprocessor test case 2026-03-13 17:31:28 +05:30
Abhishek Kumar Singh
378dc350ef refactor: added extractCommonKV instead of 2 different functions 2026-03-13 17:10:13 +05:30
Abhishek Kumar Singh
89c38ed9bc feat: converted alerttemplater to interface and updated tests 2026-03-13 17:02:26 +05:30
Abhishek Kumar Singh
04c4869b12 chore: added handling for missing variable used in template 2026-03-13 14:10:13 +05:30
Abhishek Kumar Singh
388a1184ca chore: fix lint issues 2026-03-12 21:51:13 +05:30
Abhishek Kumar Singh
03901b353b chore: hooked preProcess function in expandTitle and body, added labels and annotations in alertdata 2026-03-12 21:47:43 +05:30
Abhishek Kumar Singh
74441c74a8 feat: added preprocessor for alert templater 2026-03-12 20:54:28 +05:30
Abhishek Kumar Singh
93d332bef2 chore: exposed templates for alertmanager types 2026-03-12 18:52:40 +05:30
Abhishek Kumar Singh
1e730cae8c chore: added utils for using variables with $ notation 2026-03-12 16:34:43 +05:30
Abhishek Kumar Singh
01a09cf6d2 chore: updated test name + code for timeout errors 2026-03-12 10:22:42 +05:30
Abhishek Kumar Singh
403dddab85 feat: alert manager template to template title and notification body 2026-03-11 21:55:09 +05:30
Abhishek Kumar Singh
d07a833574 chore: added tracing to msteamsv2 notifier 2026-03-11 16:05:00 +05:30
Abhishek Kumar Singh
b39bec7245 Merge branch 'main' into chore/am_custom_notifiers 2026-03-10 22:37:24 +05:30
Abhishek Kumar Singh
6ff55c48be chore: fix email linter 2026-03-10 22:19:05 +05:30
Abhishek Kumar Singh
b15fa0f88f chore: lint fixs 2026-03-10 21:57:53 +05:30
Abhishek Kumar Singh
19fe4f860e chore: custom notifiers in alert manager 2026-03-10 13:20:04 +05:30
212 changed files with 6670 additions and 3936 deletions

View File

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

View File

@@ -1495,7 +1495,6 @@ components:
- cassandradb
- redis
- cloudsql_postgres
- memorystore_redis
type: string
CloudintegrationtypesServiceMetadata:
properties:
@@ -4260,6 +4259,34 @@ components:
type: number
clusterName:
type: string
counts:
properties:
daemonSets:
format: int64
type: integer
deployments:
format: int64
type: integer
jobs:
format: int64
type: integer
namespaces:
format: int64
type: integer
nodes:
format: int64
type: integer
statefulSets:
format: int64
type: integer
required:
- nodes
- namespaces
- deployments
- daemonSets
- jobs
- statefulSets
type: object
meta:
additionalProperties:
type: string
@@ -4280,6 +4307,7 @@ components:
- nodeCountsByReadiness
- podCountsByPhase
- podCountsByStatus
- counts
- meta
type: object
InframonitoringtypesClusters:
@@ -4801,6 +4829,26 @@ components:
type: object
InframonitoringtypesNamespaceRecord:
properties:
counts:
properties:
daemonSets:
format: int64
type: integer
deployments:
format: int64
type: integer
jobs:
format: int64
type: integer
statefulSets:
format: int64
type: integer
required:
- deployments
- daemonSets
- jobs
- statefulSets
type: object
meta:
additionalProperties:
type: string
@@ -4824,6 +4872,7 @@ components:
- namespaceMemory
- podCountsByPhase
- podCountsByStatus
- counts
- meta
type: object
InframonitoringtypesNamespaces:
@@ -7999,6 +8048,15 @@ components:
required:
- items
type: object
SpantypesGettableSpanMappers:
properties:
items:
items:
$ref: '#/components/schemas/SpantypesSpanMapper'
type: array
required:
- items
type: object
SpantypesGettableTraceAggregations:
properties:
aggregations:
@@ -8151,7 +8209,7 @@ components:
type: boolean
fieldContext:
$ref: '#/components/schemas/SpantypesFieldContext'
group_id:
groupId:
type: string
id:
type: string
@@ -8164,7 +8222,7 @@ components:
type: string
required:
- id
- group_id
- groupId
- name
- fieldContext
- config
@@ -13743,7 +13801,7 @@ paths:
schema:
properties:
data:
$ref: '#/components/schemas/SpantypesGettableSpanMapperGroups'
$ref: '#/components/schemas/SpantypesGettableSpanMappers'
status:
type: string
required:

View File

@@ -2814,7 +2814,6 @@ export enum CloudintegrationtypesServiceIDDTO {
cassandradb = 'cassandradb',
redis = 'redis',
cloudsql_postgres = 'cloudsql_postgres',
memorystore_redis = 'memorystore_redis',
}
export type CloudintegrationtypesCloudIntegrationServiceDTOAnyOf = {
/**
@@ -5670,6 +5669,39 @@ export interface InframonitoringtypesChecksDTO {
type: InframonitoringtypesCheckTypeDTO;
}
export type InframonitoringtypesClusterRecordDTOCounts = {
/**
* @type integer
* @format int64
*/
daemonSets: number;
/**
* @type integer
* @format int64
*/
deployments: number;
/**
* @type integer
* @format int64
*/
jobs: number;
/**
* @type integer
* @format int64
*/
namespaces: number;
/**
* @type integer
* @format int64
*/
nodes: number;
/**
* @type integer
* @format int64
*/
statefulSets: number;
};
export type InframonitoringtypesClusterRecordDTOMetaAnyOf = {
[key: string]: string;
};
@@ -5814,6 +5846,10 @@ export interface InframonitoringtypesClusterRecordDTO {
* @type string
*/
clusterName: string;
/**
* @type object
*/
counts: InframonitoringtypesClusterRecordDTOCounts;
/**
* @type object,null
*/
@@ -6369,6 +6405,29 @@ export interface InframonitoringtypesJobsDTO {
warning?: Querybuildertypesv5QueryWarnDataDTO;
}
export type InframonitoringtypesNamespaceRecordDTOCounts = {
/**
* @type integer
* @format int64
*/
daemonSets: number;
/**
* @type integer
* @format int64
*/
deployments: number;
/**
* @type integer
* @format int64
*/
jobs: number;
/**
* @type integer
* @format int64
*/
statefulSets: number;
};
export type InframonitoringtypesNamespaceRecordDTOMetaAnyOf = {
[key: string]: string;
};
@@ -6380,6 +6439,10 @@ export type InframonitoringtypesNamespaceRecordDTOMeta =
InframonitoringtypesNamespaceRecordDTOMetaAnyOf | null;
export interface InframonitoringtypesNamespaceRecordDTO {
/**
* @type object
*/
counts: InframonitoringtypesNamespaceRecordDTOCounts;
/**
* @type object,null
*/
@@ -9195,6 +9258,76 @@ export interface SpantypesGettableSpanMapperGroupsDTO {
items: SpantypesSpanMapperGroupDTO[];
}
export enum SpantypesSpanMapperOperationDTO {
move = 'move',
copy = 'copy',
}
export interface SpantypesSpanMapperSourceDTO {
context: SpantypesFieldContextDTO;
/**
* @type string
*/
key: string;
operation: SpantypesSpanMapperOperationDTO;
/**
* @type integer
*/
priority: number;
}
export interface SpantypesSpanMapperConfigDTO {
/**
* @type array,null
*/
sources: SpantypesSpanMapperSourceDTO[] | null;
}
export interface SpantypesSpanMapperDTO {
config: SpantypesSpanMapperConfigDTO;
/**
* @type string
* @format date-time
*/
createdAt?: string;
/**
* @type string
*/
createdBy?: string;
/**
* @type boolean
*/
enabled: boolean;
fieldContext: SpantypesFieldContextDTO;
/**
* @type string
*/
groupId: string;
/**
* @type string
*/
id: string;
/**
* @type string
*/
name: string;
/**
* @type string
* @format date-time
*/
updatedAt?: string;
/**
* @type string
*/
updatedBy?: string;
}
export interface SpantypesGettableSpanMappersDTO {
/**
* @type array
*/
items: SpantypesSpanMapperDTO[];
}
export enum SpantypesSpanAggregationTypeDTO {
span_count = 'span_count',
execution_time_percentage = 'execution_time_percentage',
@@ -9441,30 +9574,6 @@ export interface SpantypesPostableFlamegraphDTO {
selectedSpanId?: string;
}
export enum SpantypesSpanMapperOperationDTO {
move = 'move',
copy = 'copy',
}
export interface SpantypesSpanMapperSourceDTO {
context: SpantypesFieldContextDTO;
/**
* @type string
*/
key: string;
operation: SpantypesSpanMapperOperationDTO;
/**
* @type integer
*/
priority: number;
}
export interface SpantypesSpanMapperConfigDTO {
/**
* @type array,null
*/
sources: SpantypesSpanMapperSourceDTO[] | null;
}
export interface SpantypesPostableSpanMapperDTO {
config: SpantypesSpanMapperConfigDTO;
/**
@@ -9513,45 +9622,6 @@ export interface SpantypesPostableWaterfallDTO {
uncollapsedSpans?: string[] | null;
}
export interface SpantypesSpanMapperDTO {
config: SpantypesSpanMapperConfigDTO;
/**
* @type string
* @format date-time
*/
createdAt?: string;
/**
* @type string
*/
createdBy?: string;
/**
* @type boolean
*/
enabled: boolean;
fieldContext: SpantypesFieldContextDTO;
/**
* @type string
*/
group_id: string;
/**
* @type string
*/
id: string;
/**
* @type string
*/
name: string;
/**
* @type string
* @format date-time
*/
updatedAt?: string;
/**
* @type string
*/
updatedBy?: string;
}
export interface SpantypesUpdatableSpanMapperDTO {
config?: SpantypesSpanMapperConfigDTO;
/**
@@ -10853,7 +10923,7 @@ export type ListSpanMappersPathParameters = {
groupId: string;
};
export type ListSpanMappers200 = {
data: SpantypesGettableSpanMapperGroupsDTO;
data: SpantypesGettableSpanMappersDTO;
/**
* @type string
*/

View File

@@ -3,6 +3,7 @@ import { Button, Popover, Tooltip } from 'antd';
import { RadioGroup, RadioGroupItem } from '@signozhq/ui/radio-group';
import { Typography } from '@signozhq/ui/typography';
import { TelemetryFieldKey } from 'api/v5/v5';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { useExportRawData } from 'hooks/useExportData/useServerExport';
import { Download, LoaderCircle } from '@signozhq/icons';
import { DataSource } from 'types/common/queryBuilder';
@@ -18,11 +19,13 @@ import './DownloadOptionsMenu.styles.scss';
interface DownloadOptionsMenuProps {
dataSource: DataSource;
selectedColumns?: TelemetryFieldKey[];
panelType?: PANEL_TYPES;
}
export default function DownloadOptionsMenu({
dataSource,
selectedColumns,
panelType,
}: DownloadOptionsMenuProps): JSX.Element {
const [exportFormat, setExportFormat] = useState<string>(DownloadFormats.CSV);
const [rowLimit, setRowLimit] = useState<number>(DownloadRowCounts.TEN_K);
@@ -33,6 +36,7 @@ export default function DownloadOptionsMenu({
const { isDownloading, handleExportRawData } = useExportRawData({
dataSource,
panelType,
});
const handleExport = useCallback(async (): Promise<void> => {

View File

@@ -11,17 +11,17 @@ import { FlatItem, TableRowContext } from './types';
import tableStyles from './TanStackTable.module.scss';
type VirtuosoTableRowProps<TData> = ComponentProps<
type VirtuosoTableRowProps<TData, TItemKey = string> = ComponentProps<
NonNullable<
TableComponents<FlatItem<TData>, TableRowContext<TData>>['TableRow']
TableComponents<FlatItem<TData>, TableRowContext<TData, TItemKey>>['TableRow']
>
>;
function TanStackCustomTableRow<TData>({
function TanStackCustomTableRow<TData, TItemKey = string>({
item,
context,
...props
}: VirtuosoTableRowProps<TData>): JSX.Element {
}: VirtuosoTableRowProps<TData, TItemKey>): JSX.Element {
const rowId = item.row.id;
const rowData = item.row.original;
@@ -84,9 +84,9 @@ function TanStackCustomTableRow<TData>({
// This looks overkill but ensures the table is stable and doesn't re-render on every change
// If you add any new prop to context, remember to update this function
// eslint-disable-next-line sonarjs/cognitive-complexity
function areTableRowPropsEqual<TData>(
prev: Readonly<VirtuosoTableRowProps<TData>>,
next: Readonly<VirtuosoTableRowProps<TData>>,
function areTableRowPropsEqual<TData, TItemKey = string>(
prev: Readonly<VirtuosoTableRowProps<TData, TItemKey>>,
next: Readonly<VirtuosoTableRowProps<TData, TItemKey>>,
): boolean {
if (prev.item.row.id !== next.item.row.id) {
return false;
@@ -141,7 +141,9 @@ function areTableRowPropsEqual<TData>(
return true;
}
export default memo(
TanStackCustomTableRow,
areTableRowPropsEqual,
) as typeof TanStackCustomTableRow;
export default memo(TanStackCustomTableRow, areTableRowPropsEqual as any) as <
TData,
TItemKey = string,
>(
props: VirtuosoTableRowProps<TData, TItemKey>,
) => JSX.Element;

View File

@@ -8,23 +8,23 @@ import { TableRowContext } from './types';
import tableStyles from './TanStackTable.module.scss';
type TanStackRowCellsProps<TData> = {
type TanStackRowCellsProps<TData, TItemKey = string> = {
row: TanStackRowModel<TData>;
context: TableRowContext<TData> | undefined;
context: TableRowContext<TData, TItemKey> | undefined;
itemKind: 'row' | 'expansion';
hasSingleColumn: boolean;
columnOrderKey: string;
columnVisibilityKey: string;
};
function TanStackRowCellsInner<TData>({
function TanStackRowCellsInner<TData, TItemKey = string>({
row,
context,
itemKind,
hasSingleColumn,
columnOrderKey: _columnOrderKey,
columnVisibilityKey: _columnVisibilityKey,
}: TanStackRowCellsProps<TData>): JSX.Element {
}: TanStackRowCellsProps<TData, TItemKey>): JSX.Element {
const hasHovered = useIsRowHovered(row.id);
const rowData = row.original;
const visibleCells = row.getVisibleCells();
@@ -40,8 +40,10 @@ function TanStackRowCellsInner<TData>({
const handleClick = useCallback(
(event: MouseEvent<HTMLTableCellElement>) => {
// Fall back to an empty key so row clicks still fire for consumers
// that don't provide getRowKey (e.g. Logs Explorer / Live Logs).
const keyData = getRowKeyData?.(rowIndex);
const itemKey = keyData?.itemKey ?? '';
const itemKey = keyData?.itemKey ?? ('' as TItemKey);
// Handle ctrl+click or cmd+click (open in new tab)
if ((event.ctrlKey || event.metaKey) && onRowClickNewTab) {
@@ -131,6 +133,8 @@ function areRowCellsPropsEqual<TData>(
const TanStackRowCells = memo(
TanStackRowCellsInner,
areRowCellsPropsEqual as any,
) as <T>(props: TanStackRowCellsProps<T>) => JSX.Element;
) as <T, TItemKey = string>(
props: TanStackRowCellsProps<T, TItemKey>,
) => JSX.Element;
export default TanStackRowCells;

View File

@@ -67,7 +67,7 @@ const INCREASE_VIEWPORT_BY = { top: 500, bottom: 500 };
const noopColumnVisibility = (): void => {};
// eslint-disable-next-line sonarjs/cognitive-complexity
function TanStackTableInner<TData>(
function TanStackTableInner<TData, TItemKey = string>(
{
data,
columns,
@@ -107,7 +107,7 @@ function TanStackTableInner<TData>(
suffixPaginationContent,
enableAlternatingRowColors,
disableVirtualScroll,
}: TanStackTableProps<TData>,
}: TanStackTableProps<TData, TItemKey>,
forwardedRef: React.ForwardedRef<TanStackTableHandle>,
): JSX.Element {
if (disableVirtualScroll && onEndReached) {
@@ -193,7 +193,7 @@ function TanStackTableInner<TData>(
skeletonRowCount,
});
const { rowKeyData, getRowKeyData } = useRowKeyData({
const { rowKeyData, getRowKeyData } = useRowKeyData<TData, TItemKey>({
data: effectiveData,
isLoading,
getRowKey,
@@ -229,7 +229,7 @@ function TanStackTableInner<TData>(
const tanstackColumns = useMemo<ColumnDef<TData>[]>(
() =>
effectiveColumns.map((colDef) =>
buildTanstackColumnDef(colDef, isRowActive, getRowKeyData),
buildTanstackColumnDef<TData, TItemKey>(colDef, isRowActive, getRowKeyData),
),
[effectiveColumns, isRowActive, getRowKeyData],
);
@@ -356,7 +356,7 @@ function TanStackTableInner<TData>(
[effectiveVisibility, columnIds],
);
const virtuosoContext = useMemo<TableRowContext<TData>>(
const virtuosoContext = useMemo<TableRowContext<TData, TItemKey>>(
() => ({
getRowStyle,
getRowClassName,
@@ -520,13 +520,15 @@ function TanStackTableInner<TData>(
);
type VirtuosoTableComponentProps = ComponentProps<
NonNullable<TableComponents<FlatItem<TData>, TableRowContext<TData>>['Table']>
NonNullable<
TableComponents<FlatItem<TData>, TableRowContext<TData, TItemKey>>['Table']
>
>;
// Use refs in virtuosoComponents to keep the component reference stable during resize
// This prevents Virtuoso from re-rendering all rows when columns are resized
const virtuosoComponents = useMemo(
() => ({
(): TableComponents<FlatItem<TData>, TableRowContext<TData, TItemKey>> => ({
Table: ({ style, children }: VirtuosoTableComponentProps): JSX.Element => (
<table className={tableStyles.tanStackTable} style={style}>
<VirtuosoTableColGroup
@@ -582,7 +584,7 @@ function TanStackTableInner<TData>(
</table>
</div>
) : (
<TableVirtuoso<FlatItem<TData>, TableRowContext<TData>>
<TableVirtuoso<FlatItem<TData>, TableRowContext<TData, TItemKey>>
className={virtuosoClassName}
ref={virtuosoRef}
{...restTableScrollerProps}
@@ -660,8 +662,11 @@ function TanStackTableInner<TData>(
);
}
const TanStackTableForward = forwardRef(TanStackTableInner) as <TData>(
props: TanStackTableProps<TData> & {
const TanStackTableForward = forwardRef(TanStackTableInner) as <
TData,
TItemKey = string,
>(
props: TanStackTableProps<TData, TItemKey> & {
ref?: React.Ref<TanStackTableHandle>;
},
) => JSX.Element;

View File

@@ -57,6 +57,39 @@ describe('TanStackRowCells', () => {
});
it('calls onRowClick when a cell is clicked', async () => {
const user = userEvent.setup();
const onRowClick = jest.fn();
const ctx: TableRowContext<Row> = {
colCount: 1,
onRowClick,
getRowKeyData: () => ({ finalKey: 'r1', itemKey: 'r1' }),
hasSingleColumn: false,
columnOrderKey: '',
columnVisibilityKey: '',
};
const row = buildMockRow([{ id: 'body' }]);
render(
<table>
<tbody>
<tr>
<TanStackRowCells<Row>
row={row as never}
context={ctx}
itemKind="row"
hasSingleColumn={false}
columnOrderKey=""
columnVisibilityKey=""
/>
</tr>
</tbody>
</table>,
);
await user.click(screen.getAllByRole('cell')[0]);
expect(onRowClick).toHaveBeenCalledWith({ id: 'r1' }, 'r1');
});
it('fires onRowClick with empty itemKey when getRowKeyData is not provided', async () => {
// Mirrors Logs Explorer / Live Logs, which set onRowClick but no getRowKey.
const user = userEvent.setup();
const onRowClick = jest.fn();
const ctx: TableRowContext<Row> = {
@@ -84,7 +117,6 @@ describe('TanStackRowCells', () => {
</table>,
);
await user.click(screen.getAllByRole('cell')[0]);
// onRowClick receives (rowData, itemKey) - itemKey is empty when getRowKeyData not provided
expect(onRowClick).toHaveBeenCalledWith({ id: 'r1' }, '');
});
@@ -97,6 +129,7 @@ describe('TanStackRowCells', () => {
onRowClick,
onRowDeactivate,
isRowActive: () => true,
getRowKeyData: () => ({ finalKey: 'r1', itemKey: 'r1' }),
hasSingleColumn: false,
columnOrderKey: '',
columnVisibilityKey: '',
@@ -194,6 +227,7 @@ describe('TanStackRowCells', () => {
colCount: 1,
onRowClick,
onRowClickNewTab,
getRowKeyData: () => ({ finalKey: 'r1', itemKey: 'r1' }),
hasSingleColumn: false,
columnOrderKey: '',
columnVisibilityKey: '',
@@ -216,7 +250,7 @@ describe('TanStackRowCells', () => {
</table>,
);
fireEvent.click(screen.getAllByRole('cell')[0], { ctrlKey: true });
expect(onRowClickNewTab).toHaveBeenCalledWith({ id: 'r1' }, '');
expect(onRowClickNewTab).toHaveBeenCalledWith({ id: 'r1' }, 'r1');
expect(onRowClick).not.toHaveBeenCalled();
});
@@ -227,6 +261,7 @@ describe('TanStackRowCells', () => {
colCount: 1,
onRowClick,
onRowClickNewTab,
getRowKeyData: () => ({ finalKey: 'r1', itemKey: 'r1' }),
hasSingleColumn: false,
columnOrderKey: '',
columnVisibilityKey: '',
@@ -249,7 +284,7 @@ describe('TanStackRowCells', () => {
</table>,
);
fireEvent.click(screen.getAllByRole('cell')[0], { metaKey: true });
expect(onRowClickNewTab).toHaveBeenCalledWith({ id: 'r1' }, '');
expect(onRowClickNewTab).toHaveBeenCalledWith({ id: 'r1' }, 'r1');
expect(onRowClick).not.toHaveBeenCalled();
});
@@ -260,6 +295,7 @@ describe('TanStackRowCells', () => {
colCount: 1,
onRowClick,
onRowClickNewTab,
getRowKeyData: () => ({ finalKey: 'r1', itemKey: 'r1' }),
hasSingleColumn: false,
columnOrderKey: '',
columnVisibilityKey: '',

View File

@@ -614,6 +614,34 @@ describe('TanStackTableView Integration', () => {
);
});
it('calls onRowClick with object itemKey when getItemKey returns object', async () => {
type SelectionParams = { id: string; name: string };
const user = userEvent.setup();
const onRowClick = jest.fn<void, [unknown, SelectionParams]>();
renderTanStackTable<
(typeof import('./testUtils').defaultData)[0],
SelectionParams
>({
props: {
onRowClick,
getRowKey: (row) => row.id,
getItemKey: (row) => ({ id: row.id, name: row.name }),
},
});
await waitFor(() => {
expect(screen.getByText('Item 1')).toBeInTheDocument();
});
await user.click(screen.getByText('Item 1'));
expect(onRowClick).toHaveBeenCalledWith(
expect.objectContaining({ id: '1', name: 'Item 1' }),
{ id: '1', name: 'Item 1' },
);
});
it('applies active class when isRowActive returns true', async () => {
renderTanStackTable({
props: {

View File

@@ -42,14 +42,14 @@ export const defaultData: TestRow[] = [
{ id: '3', name: 'Item 3', value: 300 },
];
export type RenderTanStackTableOptions<T> = {
props?: Partial<TanStackTableProps<T>>;
export type RenderTanStackTableOptions<T, TItemKey = string> = {
props?: Partial<TanStackTableProps<T, TItemKey>>;
queryParams?: Record<string, string>;
onUrlUpdate?: OnUrlUpdateFunction;
};
export function renderTanStackTable<T = TestRow>(
options: RenderTanStackTableOptions<T> = {},
export function renderTanStackTable<T = TestRow, TItemKey = string>(
options: RenderTanStackTableOptions<T, TItemKey> = {},
): RenderResult {
const { props = {}, queryParams, onUrlUpdate } = options;
@@ -57,7 +57,7 @@ export function renderTanStackTable<T = TestRow>(
data: defaultData as unknown as T[],
columns: defaultColumns as unknown as TableColumnDef<T>[],
...props,
} as TanStackTableProps<T>;
} as TanStackTableProps<T, TItemKey>;
return render(
<NuqsTestingAdapter searchParams={queryParams} onUrlUpdate={onUrlUpdate}>
@@ -65,7 +65,7 @@ export function renderTanStackTable<T = TestRow>(
value={{ viewportHeight: 500, itemHeight: 50 }}
>
<TooltipProvider>
<TanStackTable<T> {...mergedProps} />
<TanStackTable<T, TItemKey> {...mergedProps} />
</TooltipProvider>
</VirtuosoMockContext.Provider>
</NuqsTestingAdapter>,

View File

@@ -123,6 +123,22 @@ export * from './useTableParams';
* />
* ```
*
* @example Object itemKey — use generic `TItemKey` when selection needs compound keys.
* ```tsx
* type SelectionParams = { id: string; cluster: string; namespace: string };
*
* <TanStackTable<Row, SelectionParams>
* data={data}
* columns={columns}
* getRowKey={(row) => row.uid}
* getItemKey={(row) => ({ id: row.name, cluster: row.cluster, namespace: row.namespace })}
* onRowClick={(row, itemKey) => {
* // itemKey is typed as SelectionParams
* setSelection(itemKey);
* }}
* />
* ```
*
* @example Expandable rows. `renderExpandedRow` receives `(row, rowKey, groupMeta?)`.
* ```tsx
* <TanStackTable

View File

@@ -24,17 +24,15 @@ export type TableCellContext<TData, TValue> = {
isExpanded: boolean;
canExpand: boolean;
toggleExpanded: () => void;
/** Business/selection key for the row */
itemKey: string;
/** Group metadata when row is part of a grouped view */
groupMeta?: Record<string, string>;
};
export type RowKeyData = {
export type RowKeyData<TItemKey = string> = {
/** Final unique key (with duplicate suffix if needed) */
finalKey: string;
/** Business/selection key */
itemKey: string;
itemKey: TItemKey;
/** Group metadata */
groupMeta?: Record<string, string>;
};
@@ -82,14 +80,14 @@ export type FlatItem<TData> =
| { kind: 'row'; row: TanStackRowType<TData> }
| { kind: 'expansion'; row: TanStackRowType<TData> };
export type TableRowContext<TData> = {
export type TableRowContext<TData, TItemKey = string> = {
getRowStyle?: (row: TData) => CSSProperties;
getRowClassName?: (row: TData) => string;
isRowActive?: (row: TData) => boolean;
renderRowActions?: (row: TData) => ReactNode;
onRowClick?: (row: TData, itemKey: string) => void;
onRowClick?: (row: TData, itemKey: TItemKey) => void;
/** Called when ctrl+click or cmd+click on a row */
onRowClickNewTab?: (row: TData, itemKey: string) => void;
onRowClickNewTab?: (row: TData, itemKey: TItemKey) => void;
onRowDeactivate?: () => void;
renderExpandedRow?: (
row: TData,
@@ -97,7 +95,7 @@ export type TableRowContext<TData> = {
groupMeta?: Record<string, string>,
) => ReactNode;
/** Get key data for a row by index */
getRowKeyData?: (index: number) => RowKeyData | undefined;
getRowKeyData?: (index: number) => RowKeyData<TItemKey> | undefined;
colCount: number;
isDarkMode?: boolean;
/** When set, primitive cell output (string/number/boolean) is wrapped with typography + line-clamp (see `plainTextCellLineClamp` on the table). */
@@ -147,7 +145,7 @@ export type TanstackTableQueryParamsConfig = {
expanded?: string;
};
export type TanStackTableProps<TData> = {
export type TanStackTableProps<TData, TItemKey = string> = {
data: TData[];
columns: TableColumnDef<TData>[];
/** Storage key for column state persistence (visibility, sizing, ordering). When set, enables unified column management. */
@@ -172,7 +170,7 @@ export type TanStackTableProps<TData> = {
* When set, enables automatic duplicate key detection and group-aware key composition. */
getRowKey?: (row: TData) => string;
/** Function to get the business/selection key. Defaults to getRowKey result. */
getItemKey?: (row: TData) => string;
getItemKey?: (row: TData) => TItemKey;
/** When set, enables group-aware key generation (prefixes rowKey with group values). */
groupBy?: Array<{ key: string }>;
/** Extract group metadata from a row. Required when groupBy is set. */
@@ -181,9 +179,9 @@ export type TanStackTableProps<TData> = {
getRowClassName?: (row: TData) => string;
isRowActive?: (row: TData) => boolean;
renderRowActions?: (row: TData) => ReactNode;
onRowClick?: (row: TData, itemKey: string) => void;
onRowClick?: (row: TData, itemKey: TItemKey) => void;
/** Called when ctrl+click or cmd+click on a row */
onRowClickNewTab?: (row: TData, itemKey: string) => void;
onRowClickNewTab?: (row: TData, itemKey: TItemKey) => void;
onRowDeactivate?: () => void;
activeRowIndex?: number;
renderExpandedRow?: (

View File

@@ -1,51 +1,51 @@
import { useCallback, useMemo } from 'react';
export interface RowKeyDataItem {
export interface RowKeyDataItem<TItemKey = string> {
/** Final unique key for the row (with dedup suffix if needed) */
finalKey: string;
/** Item key for tracking (may differ from finalKey) */
itemKey: string;
itemKey: TItemKey;
/** Group metadata when grouped */
groupMeta: Record<string, string> | undefined;
}
export interface UseRowKeyDataOptions<TData> {
export interface UseRowKeyDataOptions<TData, TItemKey = string> {
data: TData[];
isLoading: boolean;
getRowKey?: (item: TData) => string;
getItemKey?: (item: TData) => string;
getItemKey?: (item: TData) => TItemKey;
groupBy?: Array<{ key: string }>;
getGroupKey?: (item: TData) => Record<string, string>;
}
export interface UseRowKeyDataResult {
export interface UseRowKeyDataResult<TItemKey = string> {
/** Array of key data for each row, undefined if getRowKey not provided or loading */
rowKeyData: RowKeyDataItem[] | undefined;
getRowKeyData: (index: number) => RowKeyDataItem | undefined;
rowKeyData: RowKeyDataItem<TItemKey>[] | undefined;
getRowKeyData: (index: number) => RowKeyDataItem<TItemKey> | undefined;
}
/**
* Computes unique row keys with duplicate handling and group prefixes.
*/
export function useRowKeyData<TData>({
export function useRowKeyData<TData, TItemKey = string>({
data,
isLoading,
getRowKey,
getItemKey,
groupBy,
getGroupKey,
}: UseRowKeyDataOptions<TData>): UseRowKeyDataResult {
}: UseRowKeyDataOptions<TData, TItemKey>): UseRowKeyDataResult<TItemKey> {
// eslint-disable-next-line sonarjs/cognitive-complexity
const rowKeyData = useMemo((): RowKeyDataItem[] | undefined => {
const rowKeyData = useMemo((): RowKeyDataItem<TItemKey>[] | undefined => {
if (!getRowKey || isLoading) {
return undefined;
}
const keyCount = new Map<string, number>();
return data.map((item, index): RowKeyDataItem => {
return data.map((item, index): RowKeyDataItem<TItemKey> => {
const itemIdentifier = getRowKey(item);
const itemKey = getItemKey?.(item) ?? itemIdentifier;
const itemKey = getItemKey?.(item) ?? (itemIdentifier as TItemKey);
const groupMeta = groupBy?.length ? getGroupKey?.(item) : undefined;
// Build rowKey with group prefix when grouped

View File

@@ -86,10 +86,10 @@ const buildAccessorFn = <TData>(
};
};
export function buildTanstackColumnDef<TData>(
export function buildTanstackColumnDef<TData, TItemKey = string>(
colDef: TableColumnDef<TData>,
isRowActive?: (row: TData) => boolean,
getRowKeyData?: (index: number) => RowKeyData | undefined,
getRowKeyData?: (index: number) => RowKeyData<TItemKey> | undefined,
): ColumnDef<TData> {
const isFixed = colDef.width?.fixed != null;
const headerFn =
@@ -140,7 +140,6 @@ export function buildTanstackColumnDef<TData>(
toggleExpanded: (): void => {
row.toggleExpanded();
},
itemKey: keyData?.itemKey ?? '',
groupMeta: keyData?.groupMeta,
});
},

View File

@@ -10,6 +10,7 @@ import {
import { K8sDetailsMetadataConfig } from 'container/InfraMonitoringK8sV2/Base/K8sBaseDetails';
import { formatValueForExpression } from 'components/QueryBuilderV2/utils';
import { INFRA_MONITORING_ATTR_KEYS } from 'container/InfraMonitoringK8sV2/constants';
import { SelectedItemParams } from 'container/InfraMonitoringK8sV2/hooks';
import {
getHostQueryPayload,
hostWidgetInfo,
@@ -107,8 +108,10 @@ export function getHostMetricsQueryPayload(
export { hostWidgetInfo };
export const hostGetSelectedItemExpression = (hostName: string): string =>
`host.name = ${formatValueForExpression(hostName)}`;
export const hostGetSelectedItemExpression = (
params: SelectedItemParams,
): string =>
`host.name = ${formatValueForExpression(params.selectedItem ?? '')}`;
export function hostInitialLogTracesExpression(
host: InframonitoringtypesHostRecordDTO,

View File

@@ -51,7 +51,7 @@ function ColumnHeader({
<a
href={`${DOCS_BASE_URL}${docPath}`}
target="_blank"
rel="noopener noreferrer"
rel="noopener"
onClick={(e): void => e.stopPropagation()}
>
Learn more.

View File

@@ -30,7 +30,7 @@ function EntityGroupHeader({
<a
href={`${DOCS_BASE_URL}${docPath}`}
target="_blank"
rel="noopener noreferrer"
rel="noopener"
onClick={(e): void => e.stopPropagation()}
>
Learn more.

View File

@@ -52,9 +52,10 @@ import EntityMetrics from '../EntityDetailsUtils/EntityMetrics';
import EntityTraces from '../EntityDetailsUtils/EntityTraces';
import { K8S_ENTITY_TRACES_EXPRESSION_KEY } from '../EntityDetailsUtils/EntityTraces/hooks';
import {
SelectedItemParams,
useInfraMonitoringEventsFilters,
useInfraMonitoringLogFilters,
useInfraMonitoringSelectedItem,
useInfraMonitoringSelectedItemParams,
useInfraMonitoringTracesFilters,
useInfraMonitoringView,
} from '../hooks';
@@ -62,6 +63,10 @@ import LoadingContainer from '../LoadingContainer';
import '../EntityDetailsUtils/entityDetails.styles.scss';
import { parseAsString, useQueryState } from 'nuqs';
import {
EntityCountConfig,
EntityCountsSection,
} from './components/EntityCountsSection/EntityCountsSection';
const TimeRangeOffset = 1000000000;
@@ -71,6 +76,8 @@ export interface K8sDetailsMetadataConfig<T> {
render?: (value: string | number, entity: T) => React.ReactNode;
}
export type K8sDetailsCountConfig<T> = EntityCountConfig<T>;
export interface K8sDetailsFilters {
filter: { expression: string };
start: number;
@@ -81,7 +88,7 @@ export interface K8sBaseDetailsProps<T> {
category: InfraMonitoringEntity;
eventCategory: string;
// Data fetching configuration
getSelectedItemExpression: (selectedItem: string) => string;
getSelectedItemExpression: (params: SelectedItemParams) => string;
fetchEntityData: (
filters: K8sDetailsFilters,
signal?: AbortSignal,
@@ -91,9 +98,12 @@ export interface K8sBaseDetailsProps<T> {
getInitialLogTracesExpression: (entity: T) => string;
getInitialEventsExpression: (entity: T) => string;
metadataConfig: K8sDetailsMetadataConfig<T>[];
countsConfig?: K8sDetailsCountConfig<T>[];
getCountsFilterExpression?: (entity: T) => string;
entityWidgetInfo: {
title: string;
yAxisUnit: string;
docPath?: string;
}[];
getEntityQueryPayload: (
entity: T,
@@ -136,6 +146,8 @@ export default function K8sBaseDetails<T>({
getInitialLogTracesExpression,
getInitialEventsExpression,
metadataConfig,
countsConfig,
getCountsFilterExpression,
entityWidgetInfo,
getEntityQueryPayload,
queryKeyPrefix,
@@ -152,7 +164,9 @@ export default function K8sBaseDetails<T>({
const isDarkMode = useIsDarkMode();
const [selectedItem, setSelectedItem] = useInfraMonitoringSelectedItem();
const [selectedItemParams, setSelectedItemParams] =
useInfraMonitoringSelectedItemParams();
const selectedItem = selectedItemParams.selectedItem;
const entityQueryKey = useMemo(
() =>
@@ -160,8 +174,17 @@ export default function K8sBaseDetails<T>({
selectedTime,
`${queryKeyPrefix}EntityDetails`,
selectedItem,
selectedItemParams.clusterName,
selectedItemParams.namespaceName,
),
[queryKeyPrefix, selectedItem, selectedTime, getAutoRefreshQueryKey],
[
queryKeyPrefix,
selectedItem,
selectedItemParams.clusterName,
selectedItemParams.namespaceName,
selectedTime,
getAutoRefreshQueryKey,
],
);
const {
@@ -178,7 +201,7 @@ export default function K8sBaseDetails<T>({
const { minTime, maxTime } = getMinMaxTime();
const start = Math.floor(minTime / NANO_SECOND_MULTIPLIER);
const end = Math.floor(maxTime / NANO_SECOND_MULTIPLIER);
const expression = getSelectedItemExpression(selectedItem);
const expression = getSelectedItemExpression(selectedItemParams);
return fetchEntityData({ filter: { expression }, start, end }, signal);
},
@@ -203,8 +226,8 @@ export default function K8sBaseDetails<T>({
}, [entity, getInitialEventsExpression]);
const handleClose = useCallback((): void => {
setSelectedItem(null);
}, [setSelectedItem]);
setSelectedItemParams(null);
}, [setSelectedItemParams]);
const entityName = entity ? getEntityName(entity) : '';
@@ -467,6 +490,19 @@ export default function K8sBaseDetails<T>({
})}
</div>
</div>
{countsConfig &&
countsConfig.length > 0 &&
selectedItem &&
getCountsFilterExpression && (
<EntityCountsSection
entity={entity}
countsConfig={countsConfig}
selectedItem={selectedItem}
filterExpression={getCountsFilterExpression(entity)}
closeDrawer={handleClose}
/>
)}
</div>
{!hideDetailViewTabs && (

View File

@@ -10,7 +10,6 @@ import TanStackTable, {
} from 'components/TanStackTableView';
import { InfraMonitoringEvents } from 'constants/events';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { parseAsString, useQueryState } from 'nuqs';
import { useGlobalTimeStore } from 'store/globalTime';
import { NANO_SECOND_MULTIPLIER } from 'store/globalTime/utils';
import { Querybuildertypesv5QueryWarnDataDTO } from 'api/generated/services/sigNoz.schemas';
@@ -21,8 +20,10 @@ import {
InfraMonitoringEntity,
} from '../constants';
import {
SelectedItemParams,
useInfraMonitoringGroupBy,
useInfraMonitoringOrderBy,
useInfraMonitoringSelectedItemParams,
useInfraMonitoringStatusFilter,
} from '../hooks';
import { useInfraMonitoringLineClamp } from '../components';
@@ -49,7 +50,10 @@ export type K8sBaseListEmptyStateContext = {
/** Base type constraint for K8s entity data */
export type K8sEntityData = { meta?: Record<string, string> | null };
export type K8sBaseListProps<T extends K8sEntityData> = {
export type K8sBaseListProps<
T extends K8sEntityData,
TItemKey extends string | SelectedItemParams = string,
> = {
controlListPrefix?: React.ReactNode;
leftFilters?: React.ReactNode;
entity: InfraMonitoringEntity;
@@ -69,8 +73,8 @@ export type K8sBaseListProps<T extends K8sEntityData> = {
}>;
/** Function to get the unique key for a row. */
getRowKey?: (record: T) => string;
/** Function to get the item key used for selection. Defaults to getRowKey if not provided. */
getItemKey?: (record: T) => string;
/** Function to get the item key used for selection. Can return string or SelectedItemParams. */
getItemKey?: (record: T) => TItemKey;
eventCategory: InfraMonitoringEvents;
renderEmptyState?: (
context: K8sBaseListEmptyStateContext,
@@ -78,7 +82,10 @@ export type K8sBaseListProps<T extends K8sEntityData> = {
extraQueryKeyParts?: string[];
};
export function K8sBaseList<T extends K8sEntityData>({
export function K8sBaseList<
T extends K8sEntityData,
TItemKey extends string | SelectedItemParams = string,
>({
controlListPrefix,
leftFilters,
entity,
@@ -89,17 +96,16 @@ export function K8sBaseList<T extends K8sEntityData>({
eventCategory,
renderEmptyState,
extraQueryKeyParts = [],
}: K8sBaseListProps<T>): JSX.Element {
}: K8sBaseListProps<T, TItemKey>): JSX.Element {
const { currentQuery } = useQueryBuilder();
const expression = currentQuery.builder.queryData[0]?.filter?.expression || '';
const lineClamp = useInfraMonitoringLineClamp();
const [groupBy] = useInfraMonitoringGroupBy();
const [orderBy] = useInfraMonitoringOrderBy();
const [statusFilter] = useInfraMonitoringStatusFilter();
const [selectedItem, setSelectedItem] = useQueryState(
'selectedItem',
parseAsString,
);
const [selectedItemParams, setSelectedItemParams] =
useInfraMonitoringSelectedItemParams();
const selectedItem = selectedItemParams.selectedItem;
const columnStorageKey = `k8s-${entity}-columns`;
const hiddenColumnIds = useHiddenColumnIds(columnStorageKey);
@@ -226,9 +232,17 @@ export function K8sBaseList<T extends K8sEntityData>({
}, [eventCategory, totalCount]);
const handleRowClick = useCallback(
(_record: T, itemKey: string): void => {
(_record: T, itemKey: TItemKey): void => {
if (groupBy.length === 0) {
void setSelectedItem(itemKey);
if (typeof itemKey === 'object' && itemKey !== null) {
setSelectedItemParams(itemKey);
} else {
setSelectedItemParams({
selectedItem: itemKey,
clusterName: null,
namespaceName: null,
});
}
}
void logEvent(InfraMonitoringEvents.ItemClicked, {
@@ -237,18 +251,43 @@ export function K8sBaseList<T extends K8sEntityData>({
category: eventCategory,
});
},
[eventCategory, groupBy.length, setSelectedItem],
[eventCategory, groupBy.length, setSelectedItemParams],
);
const handleRowClickNewTab = useCallback(
(_record: T, itemKey: string): void => {
(_record: T, itemKey: TItemKey): void => {
if (groupBy.length > 0) {
return;
}
// Build URL with selectedItem param
// Build URL with selectedItem params
const url = new URL(window.location.href);
url.searchParams.set('selectedItem', itemKey);
if (typeof itemKey === 'object' && itemKey !== null) {
const params = itemKey;
if (params.selectedItem) {
url.searchParams.set(
INFRA_MONITORING_K8S_PARAMS_KEYS.SELECTED_ITEM,
params.selectedItem,
);
}
if (params.clusterName) {
url.searchParams.set(
INFRA_MONITORING_K8S_PARAMS_KEYS.SELECTED_ITEM_CLUSTER_NAME,
params.clusterName,
);
}
if (params.namespaceName) {
url.searchParams.set(
INFRA_MONITORING_K8S_PARAMS_KEYS.SELECTED_ITEM_NAMESPACE_NAME,
params.namespaceName,
);
}
} else {
url.searchParams.set(
INFRA_MONITORING_K8S_PARAMS_KEYS.SELECTED_ITEM,
itemKey,
);
}
openInNewTab(url.pathname + url.search);
void logEvent(InfraMonitoringEvents.ItemClicked, {
@@ -274,7 +313,7 @@ export function K8sBaseList<T extends K8sEntityData>({
rowKey: string,
groupMeta?: Record<string, string>,
): JSX.Element => (
<K8sExpandedRow<T>
<K8sExpandedRow<T, TItemKey>
rowKey={rowKey}
groupMeta={groupMeta}
entity={entity}
@@ -347,7 +386,7 @@ export function K8sBaseList<T extends K8sEntityData>({
{showEmptyState ? (
<div className={styles.emptyStateContainer}>{emptyTableMessage}</div>
) : (
<TanStackTable<T>
<TanStackTable<T, TItemKey>
data={pageData}
columns={tableColumns}
columnStorageKey={columnStorageKey}

View File

@@ -22,10 +22,11 @@ import { parseAsJsonNoValidate } from 'utils/nuqsParsers';
import { InfraMonitoringEntity } from '../constants';
import {
SelectedItemParams,
useInfraMonitoringGroupBy,
useInfraMonitoringOrderBy,
useInfraMonitoringPageListing,
useInfraMonitoringSelectedItem,
useInfraMonitoringSelectedItemParams,
} from '../hooks';
import { K8sBaseFilters } from './types';
@@ -34,7 +35,7 @@ import { buildExpressionFromGroupMeta } from './utils';
const EXPANDED_ROW_LIMIT = 10;
export type K8sExpandedRowProps<T> = {
export type K8sExpandedRowProps<T, TItemKey = string> = {
/** Pre-computed row key from parent table (includes group prefix + duplicate handling) */
rowKey: string;
/** Group metadata for building filters */
@@ -59,10 +60,10 @@ export type K8sExpandedRowProps<T> = {
/** Function to get the unique key for a row. */
getRowKey?: (record: T) => string;
/** Function to get the item key used for selection. Defaults to getRowKey if not provided. */
getItemKey?: (record: T) => string;
getItemKey?: (record: T) => TItemKey;
};
export function K8sExpandedRow<T>({
export function K8sExpandedRow<T, TItemKey = string>({
rowKey,
groupMeta,
entity,
@@ -71,13 +72,13 @@ export function K8sExpandedRow<T>({
extraQueryKeyParts = [],
getRowKey,
getItemKey,
}: K8sExpandedRowProps<T>): JSX.Element {
}: K8sExpandedRowProps<T, TItemKey>): JSX.Element {
const [, setGroupBy] = useInfraMonitoringGroupBy();
const [, setCurrentPage] = useInfraMonitoringPageListing();
const { currentQuery } = useQueryBuilder();
const parentExpression =
currentQuery.builder.queryData[0]?.filter?.expression || '';
const [, setSelectedItem] = useInfraMonitoringSelectedItem();
const [, setSelectedItemParams] = useInfraMonitoringSelectedItemParams();
const [, setMainOrderBy] = useInfraMonitoringOrderBy();
const { safeNavigate } = useSafeNavigate();
const urlQuery = useUrlQuery();
@@ -175,10 +176,18 @@ export function K8sExpandedRow<T>({
const expandedData = data?.data ?? [];
const handleRowClick = useCallback(
(_row: T, itemKey: string): void => {
void setSelectedItem(itemKey);
(_row: T, itemKey: TItemKey): void => {
if (typeof itemKey === 'object' && itemKey !== null) {
setSelectedItemParams(itemKey as unknown as SelectedItemParams);
} else {
setSelectedItemParams({
selectedItem: itemKey as string,
clusterName: null,
namespaceName: null,
});
}
},
[setSelectedItem],
[setSelectedItemParams],
);
const handleViewAllClick = (): void => {
@@ -239,7 +248,7 @@ export function K8sExpandedRow<T>({
<div data-testid="expanded-table">
<TanStackTableStateProvider>
<TanStackTable<T>
<TanStackTable<T, TItemKey>
data={expandedData}
columns={tableColumns}
columnStorageKey={storageKey}

View File

@@ -22,6 +22,7 @@ import { openInNewTab } from 'utils/navigation';
import { TableColumnDef } from 'components/TanStackTableView';
import { InfraMonitoringEntity } from '../../constants';
import { SelectedItemParams } from '../../hooks';
window.ResizeObserver =
window.ResizeObserver ||
@@ -165,11 +166,14 @@ function createTestColumnsWithGroup(): TableColumnDef<TestItemWithGroup>[] {
}
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
function renderComponent<T extends K8sEntityData>({
function renderComponent<
T extends K8sEntityData,
TItemKey extends string | SelectedItemParams = string,
>({
queryParams,
onUrlUpdate,
...props
}: K8sBaseListProps<T> & {
}: K8sBaseListProps<T, TItemKey> & {
queryParams?: Record<string, string>;
onUrlUpdate?: OnUrlUpdateFunction;
}) {
@@ -196,7 +200,7 @@ function renderComponent<T extends K8sEntityData>({
value={{ viewportHeight: 800, itemHeight: 50 }}
>
<TooltipProvider>
<K8sBaseList {...props} />
<K8sBaseList<T, TItemKey> {...props} />
</TooltipProvider>
</VirtuosoMockContext.Provider>
</NuqsTestingAdapter>
@@ -941,4 +945,113 @@ describe('K8sBaseList', () => {
});
});
});
describe('with object itemKey (selectedItem + cluster + namespace params)', () => {
const itemId = 'obj-item';
const onUrlUpdateMock = jest.fn<void, [UrlUpdateEvent]>();
const fetchListDataMock = jest.fn<
ReturnType<
NonNullable<K8sBaseListProps<TestItemWithTitle>['fetchListData']>
>,
Parameters<NonNullable<K8sBaseListProps<TestItemWithTitle>['fetchListData']>>
>();
const getLatestParam = (key: string): string | undefined =>
onUrlUpdateMock.mock.calls
.map((call) => call[0].searchParams.get(key))
.filter(Boolean)
.pop() as string | undefined;
beforeEach(() => {
onUrlUpdateMock.mockClear();
fetchListDataMock.mockClear();
openInNewTabMock.mockClear();
fetchListDataMock.mockResolvedValue({
data: [{ id: `PodId:${itemId}`, title: `PodTitle:${itemId}` }],
total: 1,
error: null,
});
});
it('should set selectedItem, cluster and namespace params on row click', async () => {
const user = userEvent.setup();
renderComponent<TestItemWithTitle, SelectedItemParams>({
onUrlUpdate: onUrlUpdateMock,
entity: InfraMonitoringEntity.PODS,
eventCategory: InfraMonitoringEvents.Pod,
fetchListData: fetchListDataMock,
tableColumns: createTestColumnsWithTitle(),
getRowKey: (row): string => row.id,
getItemKey: (row): SelectedItemParams => ({
selectedItem: row.id,
clusterName: 'prod-cluster',
namespaceName: 'default-ns',
}),
});
const firstRowEl = await screen.findByText(`PodId:${itemId}`);
await user.click(firstRowEl);
await waitFor(() => {
expect(getLatestParam('selectedItem')).toBe(`PodId:${itemId}`);
expect(getLatestParam('selectedItemClusterName')).toBe('prod-cluster');
expect(getLatestParam('selectedItemNamespaceName')).toBe('default-ns');
});
});
it('should include cluster and namespace params in new tab URL on ctrl+click', async () => {
renderComponent<TestItemWithTitle, SelectedItemParams>({
onUrlUpdate: onUrlUpdateMock,
entity: InfraMonitoringEntity.PODS,
eventCategory: InfraMonitoringEvents.Pod,
fetchListData: fetchListDataMock,
tableColumns: createTestColumnsWithTitle(),
getRowKey: (row): string => row.id,
getItemKey: (row): SelectedItemParams => ({
selectedItem: row.id,
clusterName: 'prod-cluster',
namespaceName: 'default-ns',
}),
});
const firstRow = await screen.findByText(`PodId:${itemId}`);
fireEvent.click(firstRow, { ctrlKey: true });
await waitFor(() => {
expect(openInNewTabMock).toHaveBeenCalledTimes(1);
});
const url = openInNewTabMock.mock.calls[0][0] as string;
expect(url).toContain(`selectedItem=PodId%3A${itemId}`);
expect(url).toContain('selectedItemClusterName=prod-cluster');
expect(url).toContain('selectedItemNamespaceName=default-ns');
});
it('should omit null cluster/namespace params in new tab URL', async () => {
renderComponent<TestItemWithTitle, SelectedItemParams>({
onUrlUpdate: onUrlUpdateMock,
entity: InfraMonitoringEntity.PODS,
eventCategory: InfraMonitoringEvents.Pod,
fetchListData: fetchListDataMock,
tableColumns: createTestColumnsWithTitle(),
getRowKey: (row): string => row.id,
getItemKey: (row): SelectedItemParams => ({
selectedItem: row.id,
clusterName: null,
namespaceName: null,
}),
});
const firstRow = await screen.findByText(`PodId:${itemId}`);
fireEvent.click(firstRow, { ctrlKey: true });
await waitFor(() => {
expect(openInNewTabMock).toHaveBeenCalledTimes(1);
});
const url = openInNewTabMock.mock.calls[0][0] as string;
expect(url).toContain(`selectedItem=PodId%3A${itemId}`);
expect(url).not.toContain('selectedItemClusterName');
expect(url).not.toContain('selectedItemNamespaceName');
});
});
});

View File

@@ -0,0 +1,132 @@
import {
buildEventsExpression,
buildExpressionFromSelectedItemParams,
buildLogsTracesExpression,
} from 'container/InfraMonitoringK8sV2/Base/utils';
describe('buildExpressionFromSelectedItemParams', () => {
it('should build expression from params with all values', () => {
const result = buildExpressionFromSelectedItemParams(
{
selectedItem: 'nginx',
clusterName: 'prod',
namespaceName: 'default',
},
'k8s.deployment.name',
);
expect(result).toBe(
"k8s.deployment.name = 'nginx' AND k8s.cluster.name = 'prod' AND k8s.namespace.name = 'default'",
);
});
it('should skip null values', () => {
const result = buildExpressionFromSelectedItemParams(
{
selectedItem: 'nginx',
clusterName: null,
namespaceName: 'default',
},
'k8s.deployment.name',
);
expect(result).toBe(
"k8s.deployment.name = 'nginx' AND k8s.namespace.name = 'default'",
);
});
it('should handle only selectedItem', () => {
const result = buildExpressionFromSelectedItemParams(
{
selectedItem: 'pod-123',
clusterName: null,
namespaceName: null,
},
'k8s.pod.uid',
);
expect(result).toBe("k8s.pod.uid = 'pod-123'");
});
});
describe('buildEventsExpression', () => {
it('should build expression with kind, name, cluster and attribute-context namespace', () => {
const result = buildEventsExpression({
objectKind: 'Deployment',
objectName: 'nginx',
clusterName: 'prod',
namespaceName: 'default',
});
expect(result).toBe(
"k8s.object.kind = 'Deployment' AND k8s.object.name = 'nginx' AND k8s.cluster.name = 'prod' AND attribute.k8s.namespace.name = 'default'",
);
});
it('should always include kind and name, even when name is empty', () => {
const result = buildEventsExpression({
objectKind: 'Node',
objectName: '',
});
expect(result).toBe("k8s.object.kind = 'Node' AND k8s.object.name = ''");
});
it('should skip cluster and namespace clauses when values are missing', () => {
const result = buildEventsExpression({
objectKind: 'Pod',
objectName: 'pod-1',
clusterName: null,
namespaceName: undefined,
});
expect(result).toBe("k8s.object.kind = 'Pod' AND k8s.object.name = 'pod-1'");
});
it('should escape values with quotes', () => {
const result = buildEventsExpression({
objectKind: 'Pod',
objectName: "po'd",
namespaceName: 'ns-1',
});
expect(result).toBe(
`k8s.object.kind = 'Pod' AND k8s.object.name = 'po\\'d' AND attribute.k8s.namespace.name = 'ns-1'`,
);
});
});
describe('buildLogsTracesExpression', () => {
it('should build expression with main attribute, cluster and namespace', () => {
const result = buildLogsTracesExpression({
mainAttributeKey: 'k8s.deployment.name',
mainAttributeValue: 'nginx',
clusterName: 'prod',
namespaceName: 'default',
});
expect(result).toBe(
"k8s.deployment.name = 'nginx' AND k8s.cluster.name = 'prod' AND k8s.namespace.name = 'default'",
);
});
it('should skip clauses with empty values', () => {
const result = buildLogsTracesExpression({
mainAttributeKey: 'k8s.node.name',
mainAttributeValue: 'node-1',
clusterName: '',
namespaceName: null,
});
expect(result).toBe("k8s.node.name = 'node-1'");
});
it('should return empty string when all values are missing', () => {
const result = buildLogsTracesExpression({
mainAttributeKey: 'k8s.pod.name',
mainAttributeValue: '',
});
expect(result).toBe('');
});
});

View File

@@ -0,0 +1,41 @@
.countsContainer {
display: flex;
gap: var(--spacing-6);
margin-top: var(--spacing-8);
flex-wrap: wrap;
}
.countCard {
position: relative;
flex: 1;
min-width: 120px;
max-width: 180px;
border: 1px solid var(--l3-border);
border-radius: 4px;
display: flex;
flex-direction: column;
gap: var(--spacing-2);
padding: var(--spacing-6);
}
.countLabel {
color: var(--l2-foreground);
letter-spacing: var(--letter-spacing-wide);
text-transform: uppercase;
}
.countValue {
color: var(--l1-foreground);
font-family: var(--periscope-font-family-mono);
font-size: var(--font-size-xl);
}
.navigateButton {
position: absolute;
top: var(--spacing-4);
right: var(--spacing-4);
--button-padding: var(--spacing-1);
--button-height: 24px;
--button-width: 24px;
}

View File

@@ -0,0 +1,107 @@
import { Tooltip } from 'antd';
import { Button } from '@signozhq/ui/button';
import { Typography } from '@signozhq/ui/typography';
import { Compass } from '@signozhq/icons';
import { QueryParams } from 'constants/query';
import { initialQueriesMap } from 'constants/queryBuilder';
import ROUTES from 'constants/routes';
import { Link } from 'react-router-dom';
import { DataSource } from 'types/common/queryBuilder';
import { v4 as uuid } from 'uuid';
import {
INFRA_MONITORING_K8S_PARAMS_KEYS,
InfraMonitoringEntity,
} from '../../../constants';
import styles from './EntityCountsSection.module.scss';
export interface EntityCountConfig<T> {
label: string;
getValue: (entity: T) => number;
targetCategory: InfraMonitoringEntity;
}
interface EntityCountsSectionProps<T> {
entity: T;
countsConfig: EntityCountConfig<T>[];
selectedItem: string;
filterExpression: string;
closeDrawer: () => void;
}
export function EntityCountsSection<T>({
entity,
countsConfig,
selectedItem,
filterExpression,
closeDrawer,
}: EntityCountsSectionProps<T>): JSX.Element {
const buildNavigationUrl = (targetCategory: InfraMonitoringEntity): string => {
const defaultQuery = initialQueriesMap[DataSource.METRICS];
const compositeQuery = {
...defaultQuery,
id: uuid(),
builder: {
...defaultQuery.builder,
queryData: defaultQuery.builder.queryData.map((query) => ({
...query,
filter: { expression: filterExpression },
filters: { items: [], op: 'AND' as const },
})),
},
};
// TODO(H4ad): After https://github.com/SigNoz/signoz/pull/12038, inherit custom time of drawer to list
const urlParams = new URLSearchParams();
urlParams.set(INFRA_MONITORING_K8S_PARAMS_KEYS.CATEGORY, targetCategory);
urlParams.set(
QueryParams.compositeQuery,
encodeURIComponent(JSON.stringify(compositeQuery)),
);
return `${ROUTES.INFRASTRUCTURE_MONITORING_KUBERNETES}?${urlParams.toString()}`;
};
return (
<div className={styles.countsContainer}>
{countsConfig.map((config) => (
<div
key={config.label}
className={styles.countCard}
data-testid={`count-card-${config.label.toLowerCase().replace(/\s+/g, '-')}`}
>
<Typography.Text
color="muted"
size="small"
weight="medium"
className={styles.countLabel}
>
{config.label}
</Typography.Text>
<Typography.Text className={styles.countValue} size="xl" weight="semibold">
{config.getValue(entity) || '-'}
</Typography.Text>
<Link
to={buildNavigationUrl(config.targetCategory)}
onClick={closeDrawer}
data-testid={`navigate-${config.label.toLowerCase().replace(/\s+/g, '-')}`}
>
<Tooltip
title={`View ${config.label.toLowerCase()} of '${selectedItem}'`}
placement="top"
>
<Button
size="icon"
variant="ghost"
color="secondary"
className={styles.navigateButton}
prefix={<Compass size={14} />}
/>
</Tooltip>
</Link>
</div>
))}
</div>
);
}

View File

@@ -2,7 +2,12 @@ import { Badge } from '@signozhq/ui/badge';
import styles from './utils.module.scss';
import { TagFilterItem } from 'types/api/queryBuilder/queryBuilderData';
import { convertFiltersToExpression } from 'components/QueryBuilderV2/utils';
import {
convertFiltersToExpression,
formatValueForExpression,
} from 'components/QueryBuilderV2/utils';
import { SelectedItemParams } from 'container/InfraMonitoringK8sV2/hooks';
import { INFRA_MONITORING_ATTR_KEYS } from 'container/InfraMonitoringK8sV2/constants';
const dotToUnder: Record<string, string> = {
'os.type': 'os_type',
@@ -89,3 +94,91 @@ export function buildExpressionFromGroupMeta(
}
return parent || metaExpression;
}
export interface EventsExpressionParams {
objectKind: string;
objectName: string;
clusterName?: string | null;
namespaceName?: string | null;
}
export function buildEventsExpression(params: EventsExpressionParams): string {
const clauses: string[] = [
`${INFRA_MONITORING_ATTR_KEYS.K8S_OBJECT_KIND} = ${formatValueForExpression(params.objectKind)}`,
`${INFRA_MONITORING_ATTR_KEYS.K8S_OBJECT_NAME} = ${formatValueForExpression(params.objectName)}`,
];
if (params.clusterName) {
clauses.push(
`${INFRA_MONITORING_ATTR_KEYS.K8S_CLUSTER_NAME} = ${formatValueForExpression(params.clusterName)}`,
);
}
// the other attributes are resource., and fallbacks correctly without prefix
// this one needs attribute. prefix otherwise it fails the query
if (params.namespaceName) {
clauses.push(
`attribute.${INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME} = ${formatValueForExpression(params.namespaceName)}`,
);
}
return clauses.join(' AND ');
}
export interface LogsTracesExpressionParams {
mainAttributeKey: string;
mainAttributeValue?: string | null;
clusterName?: string | null;
namespaceName?: string | null;
}
export function buildLogsTracesExpression(
params: LogsTracesExpressionParams,
): string {
const clauses: string[] = [];
if (params.mainAttributeValue) {
clauses.push(
`${params.mainAttributeKey} = ${formatValueForExpression(params.mainAttributeValue)}`,
);
}
if (params.clusterName) {
clauses.push(
`${INFRA_MONITORING_ATTR_KEYS.K8S_CLUSTER_NAME} = ${formatValueForExpression(params.clusterName)}`,
);
}
if (params.namespaceName) {
clauses.push(
`${INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME} = ${formatValueForExpression(params.namespaceName)}`,
);
}
return clauses.join(' AND ');
}
export function buildExpressionFromSelectedItemParams(
params: SelectedItemParams,
mainAttributeKey: string,
): string {
const clauses: string[] = [];
if (params.selectedItem) {
clauses.push(
`${mainAttributeKey} = ${formatValueForExpression(params.selectedItem)}`,
);
}
if (params.clusterName) {
clauses.push(
`${INFRA_MONITORING_ATTR_KEYS.K8S_CLUSTER_NAME} = ${formatValueForExpression(params.clusterName)}`,
);
}
if (params.namespaceName) {
clauses.push(
`${INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME} = ${formatValueForExpression(params.namespaceName)}`,
);
}
return clauses.join(' AND ');
}

View File

@@ -14,7 +14,9 @@ import { InfraMonitoringEntity } from '../constants';
import {
clusterWidgetInfo,
getClusterMetricsQueryPayload,
k8sClusterDetailsCountsConfig,
k8sClusterDetailsMetadataConfig,
k8sClusterGetCountsFilterExpression,
k8sClusterGetEntityName,
k8sClusterGetSelectedItemExpression,
k8sClusterInitialEventsExpression,
@@ -136,6 +138,8 @@ function K8sClustersList({
getInitialLogTracesExpression={k8sClusterInitialLogTracesExpression}
getInitialEventsExpression={k8sClusterInitialEventsExpression}
metadataConfig={k8sClusterDetailsMetadataConfig}
countsConfig={k8sClusterDetailsCountsConfig}
getCountsFilterExpression={k8sClusterGetCountsFilterExpression}
entityWidgetInfo={clusterWidgetInfo}
getEntityQueryPayload={getClusterMetricsQueryPayload}
queryKeyPrefix="cluster"

View File

@@ -6,67 +6,131 @@ import { EQueryType } from 'types/common/dashboard';
import { DataSource, ReduceOperators } from 'types/common/queryBuilder';
import { v4 } from 'uuid';
import { K8sDetailsMetadataConfig } from '../Base/K8sBaseDetails';
import {
K8sDetailsCountConfig,
K8sDetailsMetadataConfig,
} from '../Base/K8sBaseDetails';
import { formatValueForExpression } from 'components/QueryBuilderV2/utils';
import { INFRA_MONITORING_ATTR_KEYS } from '../constants';
import {
INFRA_MONITORING_ATTR_KEYS,
InfraMonitoringEntity,
} from '../constants';
import { SelectedItemParams } from '../hooks';
import {
buildEventsExpression,
buildLogsTracesExpression,
} from 'container/InfraMonitoringK8sV2/Base/utils';
export const k8sClusterGetSelectedItemExpression = (
selectedItemId: string,
): string => `k8s.cluster.name = ${formatValueForExpression(selectedItemId)}`;
params: SelectedItemParams,
): string =>
`k8s.cluster.name = ${formatValueForExpression(params.selectedItem ?? '')}`;
export const k8sClusterDetailsMetadataConfig: K8sDetailsMetadataConfig<InframonitoringtypesClusterRecordDTO>[] =
[{ label: 'Cluster Name', getValue: (p): string => p.clusterName || '' }];
export const k8sClusterDetailsCountsConfig: K8sDetailsCountConfig<InframonitoringtypesClusterRecordDTO>[] =
[
{
label: 'Namespaces',
getValue: (p): number => p.counts?.namespaces ?? 0,
targetCategory: InfraMonitoringEntity.NAMESPACES,
},
{
label: 'Nodes',
getValue: (p): number => p.counts?.nodes ?? 0,
targetCategory: InfraMonitoringEntity.NODES,
},
{
label: 'Deployments',
getValue: (p): number => p.counts?.deployments ?? 0,
targetCategory: InfraMonitoringEntity.DEPLOYMENTS,
},
{
label: 'StatefulSets',
getValue: (p): number => p.counts?.statefulSets ?? 0,
targetCategory: InfraMonitoringEntity.STATEFULSETS,
},
{
label: 'DaemonSets',
getValue: (p): number => p.counts?.daemonSets ?? 0,
targetCategory: InfraMonitoringEntity.DAEMONSETS,
},
{
label: 'Jobs',
getValue: (p): number => p.counts?.jobs ?? 0,
targetCategory: InfraMonitoringEntity.JOBS,
},
];
export const k8sClusterInitialEventsExpression = (
item: InframonitoringtypesClusterRecordDTO,
): string => {
const objectName = formatValueForExpression(item.clusterName || '');
return `${INFRA_MONITORING_ATTR_KEYS.K8S_OBJECT_KIND} = 'Cluster' AND ${INFRA_MONITORING_ATTR_KEYS.K8S_OBJECT_NAME} = ${objectName}`;
};
): string =>
buildEventsExpression({
objectKind: 'Cluster',
objectName: item.clusterName || '',
});
export const k8sClusterInitialLogTracesExpression = (
item: InframonitoringtypesClusterRecordDTO,
): string => {
const clusterName = formatValueForExpression(item.clusterName || '');
return `${INFRA_MONITORING_ATTR_KEYS.K8S_CLUSTER_NAME} = ${clusterName}`;
};
): string =>
buildLogsTracesExpression({
mainAttributeKey: INFRA_MONITORING_ATTR_KEYS.K8S_CLUSTER_NAME,
mainAttributeValue: item.clusterName,
});
export const k8sClusterGetEntityName = (
item: InframonitoringtypesClusterRecordDTO,
): string => item.clusterName || '';
export const k8sClusterGetCountsFilterExpression = (
item: InframonitoringtypesClusterRecordDTO,
): string =>
`k8s.cluster.name = ${formatValueForExpression(item.clusterName ?? '')}`;
export const clusterWidgetInfo = [
{
title: 'CPU Usage, allocatable',
yAxisUnit: '',
docPath:
'/infrastructure-monitoring/kubernetes/clusters/#cpu-usage-allocatable',
},
{
title: 'Memory Usage, allocatable',
yAxisUnit: 'bytes',
docPath:
'/infrastructure-monitoring/kubernetes/clusters/#memory-usage-allocatable',
},
{
title: 'Ready Nodes',
yAxisUnit: '',
docPath: '/infrastructure-monitoring/kubernetes/clusters/#ready-nodes',
},
{
title: 'NotReady Nodes',
yAxisUnit: '',
docPath: '/infrastructure-monitoring/kubernetes/clusters/#notready-nodes',
},
{
title: 'Deployments available and desired',
yAxisUnit: '',
docPath:
'/infrastructure-monitoring/kubernetes/clusters/#deployments-available-and-desired',
},
{
title: 'Statefulset pods',
yAxisUnit: '',
docPath: '/infrastructure-monitoring/kubernetes/clusters/#statefulset-pods',
},
{
title: 'Daemonset nodes',
yAxisUnit: '',
docPath: '/infrastructure-monitoring/kubernetes/clusters/#daemonset-nodes',
},
{
title: 'Jobs',
yAxisUnit: '',
docPath: '/infrastructure-monitoring/kubernetes/clusters/#jobs',
},
];

View File

@@ -10,6 +10,7 @@ import K8sBaseDetails, { K8sDetailsFilters } from '../Base/K8sBaseDetails';
import { K8sBaseList } from '../Base/K8sBaseList';
import { K8sBaseFilters } from '../Base/types';
import { InfraMonitoringEntity } from '../constants';
import { SelectedItemParams } from '../hooks';
import {
daemonSetWidgetInfo,
getDaemonSetMetricsQueryPayload,
@@ -111,7 +112,7 @@ function K8sDaemonSetsList({
);
return (
<>
<K8sBaseList<InframonitoringtypesDaemonSetRecordDTO>
<K8sBaseList<InframonitoringtypesDaemonSetRecordDTO, SelectedItemParams>
controlListPrefix={controlListPrefix}
entity={InfraMonitoringEntity.DAEMONSETS}
tableColumns={k8sDaemonSetsColumnsConfig}

View File

@@ -7,13 +7,21 @@ import { DataSource, ReduceOperators } from 'types/common/queryBuilder';
import { v4 } from 'uuid';
import { K8sDetailsMetadataConfig } from '../Base/K8sBaseDetails';
import { formatValueForExpression } from 'components/QueryBuilderV2/utils';
import { INFRA_MONITORING_ATTR_KEYS } from '../constants';
import { SelectedItemParams } from '../hooks';
import {
buildEventsExpression,
buildExpressionFromSelectedItemParams,
buildLogsTracesExpression,
} from 'container/InfraMonitoringK8sV2/Base/utils';
export const k8sDaemonSetGetSelectedItemExpression = (
selectedItemId: string,
params: SelectedItemParams,
): string =>
`${INFRA_MONITORING_ATTR_KEYS.K8S_DAEMONSET_NAME} = ${formatValueForExpression(selectedItemId)}`;
buildExpressionFromSelectedItemParams(
params,
INFRA_MONITORING_ATTR_KEYS.K8S_DAEMONSET_NAME,
);
export const k8sDaemonSetDetailsMetadataConfig: K8sDetailsMetadataConfig<InframonitoringtypesDaemonSetRecordDTO>[] =
[
@@ -37,12 +45,23 @@ export const k8sDaemonSetDetailsMetadataConfig: K8sDetailsMetadataConfig<Inframo
export const k8sDaemonSetInitialEventsExpression = (
item: InframonitoringtypesDaemonSetRecordDTO,
): string =>
`${INFRA_MONITORING_ATTR_KEYS.K8S_OBJECT_KIND} = 'DaemonSet' AND ${INFRA_MONITORING_ATTR_KEYS.K8S_OBJECT_NAME} = ${formatValueForExpression(item.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_DAEMONSET_NAME] ?? '')}`;
buildEventsExpression({
objectKind: 'DaemonSet',
objectName: item.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_DAEMONSET_NAME] ?? '',
clusterName: item.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_CLUSTER_NAME],
namespaceName: item.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME],
});
export const k8sDaemonSetInitialLogTracesExpression = (
item: InframonitoringtypesDaemonSetRecordDTO,
): string =>
`${INFRA_MONITORING_ATTR_KEYS.K8S_DAEMONSET_NAME} = ${formatValueForExpression(item.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_DAEMONSET_NAME] ?? '')} AND ${INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME} = ${formatValueForExpression(item.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] ?? '')}`;
buildLogsTracesExpression({
mainAttributeKey: INFRA_MONITORING_ATTR_KEYS.K8S_DAEMONSET_NAME,
mainAttributeValue:
item.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_DAEMONSET_NAME],
clusterName: item.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_CLUSTER_NAME],
namespaceName: item.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME],
});
export const k8sDaemonSetGetEntityName = (
item: InframonitoringtypesDaemonSetRecordDTO,
@@ -52,18 +71,25 @@ export const daemonSetWidgetInfo = [
{
title: 'CPU usage, request, limits',
yAxisUnit: '',
docPath:
'/infrastructure-monitoring/kubernetes/daemonsets/#cpu-usage-request-limits',
},
{
title: 'Memory usage, request, limits',
yAxisUnit: 'bytes',
docPath:
'/infrastructure-monitoring/kubernetes/daemonsets/#memory-usage-request-limits',
},
{
title: 'Network IO',
yAxisUnit: 'binBps',
docPath: '/infrastructure-monitoring/kubernetes/daemonsets/#network-io',
},
{
title: 'Network errors count',
yAxisUnit: '',
docPath:
'/infrastructure-monitoring/kubernetes/daemonsets/#network-errors-count',
},
];
@@ -115,6 +141,40 @@ export const getDaemonSetMetricsQueryPayload = (
? 'k8s.namespace.name'
: 'k8s_namespace_name';
const k8sClusterNameKey = dotMetricsEnabled
? 'k8s.cluster.name'
: 'k8s_cluster_name';
const clusterName =
daemonSet.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_CLUSTER_NAME] ?? '';
const namespaceName =
daemonSet.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] ?? '';
const filters = [
{
id: 'f1',
key: {
dataType: DataTypes.String,
id: 'k8s_cluster_name--string--tag--false',
key: k8sClusterNameKey,
type: 'tag',
},
op: '=',
value: clusterName,
},
{
id: 'f2',
key: {
dataType: DataTypes.String,
id: 'k8s_namespace_name--string--tag--false',
key: k8sNamespaceNameKey,
type: 'tag',
},
op: '=',
value: namespaceName,
},
];
return [
{
selectedTime: 'GLOBAL_TIME',
@@ -148,19 +208,7 @@ export const getDaemonSetMetricsQueryPayload = (
daemonSet.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_DAEMONSET_NAME] ??
'',
},
{
id: '47b3adae',
key: {
dataType: DataTypes.String,
id: 'k8s_namespace_name--string--tag--false',
key: k8sNamespaceNameKey,
type: 'tag',
},
op: '=',
value:
daemonSet.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] ??
'',
},
...filters,
],
op: 'AND',
},
@@ -202,19 +250,7 @@ export const getDaemonSetMetricsQueryPayload = (
daemonSet.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_DAEMONSET_NAME] ??
'',
},
{
id: '47b3adae',
key: {
dataType: DataTypes.String,
id: 'k8s_namespace_name--string--tag--false',
key: k8sNamespaceNameKey,
type: 'tag',
},
op: '=',
value:
daemonSet.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] ??
'',
},
...filters,
],
op: 'AND',
},
@@ -256,19 +292,7 @@ export const getDaemonSetMetricsQueryPayload = (
daemonSet.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_DAEMONSET_NAME] ??
'',
},
{
id: '47b3adae',
key: {
dataType: DataTypes.String,
id: 'k8s_namespace_name--string--tag--false',
key: k8sNamespaceNameKey,
type: 'tag',
},
op: '=',
value:
daemonSet.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] ??
'',
},
...filters,
],
op: 'AND',
},
@@ -344,19 +368,7 @@ export const getDaemonSetMetricsQueryPayload = (
daemonSet.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_DAEMONSET_NAME] ??
'',
},
{
id: '47b3adae',
key: {
dataType: DataTypes.String,
id: 'k8s_namespace_name--string--tag--false',
key: k8sNamespaceNameKey,
type: 'tag',
},
op: '=',
value:
daemonSet.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] ??
'',
},
...filters,
],
op: 'AND',
},
@@ -398,19 +410,7 @@ export const getDaemonSetMetricsQueryPayload = (
daemonSet.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_DAEMONSET_NAME] ??
'',
},
{
id: '47b3adae',
key: {
dataType: DataTypes.String,
id: 'k8s_namespace_name--string--tag--false',
key: k8sNamespaceNameKey,
type: 'tag',
},
op: '=',
value:
daemonSet.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] ??
'',
},
...filters,
],
op: 'AND',
},
@@ -452,19 +452,7 @@ export const getDaemonSetMetricsQueryPayload = (
daemonSet.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_DAEMONSET_NAME] ??
'',
},
{
id: '47b3adae',
key: {
dataType: DataTypes.String,
id: 'k8s_namespace_name--string--tag--false',
key: k8sNamespaceNameKey,
type: 'tag',
},
op: '=',
value:
daemonSet.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] ??
'',
},
...filters,
],
op: 'AND',
},
@@ -540,19 +528,7 @@ export const getDaemonSetMetricsQueryPayload = (
daemonSet.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_DAEMONSET_NAME] ??
'',
},
{
id: '47b3adae',
key: {
dataType: DataTypes.String,
id: 'k8s_namespace_name--string--tag--false',
key: k8sNamespaceNameKey,
type: 'tag',
},
op: '=',
value:
daemonSet.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] ??
'',
},
...filters,
],
op: 'AND',
},
@@ -641,19 +617,7 @@ export const getDaemonSetMetricsQueryPayload = (
daemonSet.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_DAEMONSET_NAME] ??
'',
},
{
id: '47b3adae',
key: {
dataType: DataTypes.String,
id: 'k8s_namespace_name--string--tag--false',
key: k8sNamespaceNameKey,
type: 'tag',
},
op: '=',
value:
daemonSet.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] ??
'',
},
...filters,
],
op: 'AND',
},

View File

@@ -6,6 +6,7 @@ import { ExpandButtonWrapper } from 'container/InfraMonitoringK8sV2/components';
import ColumnHeader from '../Base/ColumnHeader';
import EntityGroupHeader from '../Base/EntityGroupHeader';
import K8sGroupCell from '../Base/K8sGroupCell';
import { SelectedItemParams } from '../hooks';
import { formatBytes, getPodPhaseStatusItems } from '../commonUtils';
import {
CellValueTooltip,
@@ -31,8 +32,15 @@ export function getK8sDaemonSetRowKey(
export function getK8sDaemonSetItemKey(
daemonSet: InframonitoringtypesDaemonSetRecordDTO,
): string {
return daemonSet.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_DAEMONSET_NAME] || '';
): SelectedItemParams {
return {
selectedItem:
daemonSet.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_DAEMONSET_NAME] ?? null,
clusterName:
daemonSet.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_CLUSTER_NAME] ?? null,
namespaceName:
daemonSet.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] ?? null,
};
}
export type DaemonSetTableColumnConfig =
@@ -132,28 +140,38 @@ export const k8sDaemonSetsColumnsConfig: DaemonSetTableColumnConfig[] = [
},
},
{
id: 'node_status',
id: 'scheduled_nodes',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/daemonsets#node-status">
Node Status
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/daemonsets#scheduled-nodes">
Scheduled Nodes
</ColumnHeader>
),
accessorFn: (row): number => row.currentNodes,
width: { min: 180 },
width: { min: 210 },
enableSort: false,
enableResize: true,
cell: ({ row }): React.ReactNode => (
<GroupedStatusCounts
items={[
{
value: row.readyNodes,
label: 'Ready',
color: Color.BG_FOREST_500,
},
{
value: row.currentNodes,
label: 'Current',
color: Color.BG_FOREST_500,
color: Color.BG_ROBIN_500,
},
{
value: row.desiredNodes,
label: 'Desired',
color: Color.BG_ROBIN_500,
color: Color.BG_SAKURA_400,
},
{
value: row.misscheduledNodes,
label: 'Misscheduled',
color: Color.BG_AMBER_500,
},
]}
/>
@@ -312,6 +330,30 @@ export const k8sDaemonSetsColumnsConfig: DaemonSetTableColumnConfig[] = [
);
},
},
{
id: 'ready_nodes',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/daemonsets#ready">
Ready Nodes
</ColumnHeader>
),
accessorFn: (row): number => row.readyNodes,
width: { min: 140 },
enableSort: true,
defaultVisibility: false,
cell: ({ value }): React.ReactNode => {
const readyNodes = value as number;
return (
<ValidateColumnValueWrapper
value={readyNodes}
entity={InfraMonitoringEntity.DAEMONSETS}
attribute="ready node"
>
<TanStackTable.Text>{readyNodes}</TanStackTable.Text>
</ValidateColumnValueWrapper>
);
},
},
{
id: 'current_nodes',
header: (): React.ReactNode => (
@@ -360,4 +402,28 @@ export const k8sDaemonSetsColumnsConfig: DaemonSetTableColumnConfig[] = [
);
},
},
{
id: 'misscheduled_nodes',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/daemonsets#misscheduled">
Misscheduled Nodes
</ColumnHeader>
),
accessorFn: (row): number => row.misscheduledNodes,
width: { min: 140 },
enableSort: true,
defaultVisibility: false,
cell: ({ value }): React.ReactNode => {
const misscheduledNodes = value as number;
return (
<ValidateColumnValueWrapper
value={misscheduledNodes}
entity={InfraMonitoringEntity.DAEMONSETS}
attribute="misscheduled node"
>
<TanStackTable.Text>{misscheduledNodes}</TanStackTable.Text>
</ValidateColumnValueWrapper>
);
},
},
];

View File

@@ -11,6 +11,7 @@ import K8sBaseDetails, { K8sDetailsFilters } from '../Base/K8sBaseDetails';
import { K8sBaseList } from '../Base/K8sBaseList';
import { K8sBaseFilters } from '../Base/types';
import { InfraMonitoringEntity } from '../constants';
import { SelectedItemParams } from '../hooks';
import {
deploymentWidgetInfo,
getDeploymentMetricsQueryPayload,
@@ -117,7 +118,7 @@ function K8sDeploymentsList({
return (
<>
<K8sBaseList<InframonitoringtypesDeploymentRecordDTO>
<K8sBaseList<InframonitoringtypesDeploymentRecordDTO, SelectedItemParams>
controlListPrefix={controlListPrefix}
entity={InfraMonitoringEntity.DEPLOYMENTS}
tableColumns={k8sDeploymentsColumnsConfig}

View File

@@ -7,13 +7,21 @@ import { DataSource, ReduceOperators } from 'types/common/queryBuilder';
import { v4 } from 'uuid';
import { K8sDetailsMetadataConfig } from '../Base/K8sBaseDetails';
import { formatValueForExpression } from 'components/QueryBuilderV2/utils';
import { INFRA_MONITORING_ATTR_KEYS } from '../constants';
import { SelectedItemParams } from '../hooks';
import {
buildEventsExpression,
buildExpressionFromSelectedItemParams,
buildLogsTracesExpression,
} from 'container/InfraMonitoringK8sV2/Base/utils';
export const k8sDeploymentGetSelectedItemExpression = (
selectedItemId: string,
params: SelectedItemParams,
): string =>
`${INFRA_MONITORING_ATTR_KEYS.K8S_DEPLOYMENT_NAME} = ${formatValueForExpression(selectedItemId)}`;
buildExpressionFromSelectedItemParams(
params,
INFRA_MONITORING_ATTR_KEYS.K8S_DEPLOYMENT_NAME,
);
export const k8sDeploymentDetailsMetadataConfig: K8sDetailsMetadataConfig<InframonitoringtypesDeploymentRecordDTO>[] =
[
@@ -36,24 +44,24 @@ export const k8sDeploymentDetailsMetadataConfig: K8sDetailsMetadataConfig<Infram
export const k8sDeploymentInitialEventsExpression = (
item: InframonitoringtypesDeploymentRecordDTO,
): string => {
const objectName = formatValueForExpression(
item.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_DEPLOYMENT_NAME] ?? '',
);
return `${INFRA_MONITORING_ATTR_KEYS.K8S_OBJECT_KIND} = 'Deployment' AND ${INFRA_MONITORING_ATTR_KEYS.K8S_OBJECT_NAME} = ${objectName}`;
};
): string =>
buildEventsExpression({
objectKind: 'Deployment',
objectName: item.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_DEPLOYMENT_NAME] ?? '',
clusterName: item.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_CLUSTER_NAME],
namespaceName: item.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME],
});
export const k8sDeploymentInitialLogTracesExpression = (
item: InframonitoringtypesDeploymentRecordDTO,
): string => {
const deploymentName = formatValueForExpression(
item.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_DEPLOYMENT_NAME] ?? '',
);
const namespaceName = formatValueForExpression(
item.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] ?? '',
);
return `${INFRA_MONITORING_ATTR_KEYS.K8S_DEPLOYMENT_NAME} = ${deploymentName} AND ${INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME} = ${namespaceName}`;
};
): string =>
buildLogsTracesExpression({
mainAttributeKey: INFRA_MONITORING_ATTR_KEYS.K8S_DEPLOYMENT_NAME,
mainAttributeValue:
item.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_DEPLOYMENT_NAME],
clusterName: item.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_CLUSTER_NAME],
namespaceName: item.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME],
});
export const k8sDeploymentGetEntityName = (
item: InframonitoringtypesDeploymentRecordDTO,
@@ -63,18 +71,25 @@ export const deploymentWidgetInfo = [
{
title: 'CPU usage, request, limits',
yAxisUnit: '',
docPath:
'/infrastructure-monitoring/kubernetes/deployments/#cpu-usage-request-limits',
},
{
title: 'Memory usage, request, limits',
yAxisUnit: 'bytes',
docPath:
'/infrastructure-monitoring/kubernetes/deployments/#memory-usage-request-limits',
},
{
title: 'Network IO',
yAxisUnit: 'binBps',
docPath: '/infrastructure-monitoring/kubernetes/deployments/#network-io',
},
{
title: 'Network error count',
yAxisUnit: '',
docPath:
'/infrastructure-monitoring/kubernetes/deployments/#network-error-count',
},
];
@@ -121,6 +136,43 @@ export const getDeploymentMetricsQueryPayload = (
: 'k8s_deployment_name';
const k8sPodNameKey = dotMetricsEnabled ? 'k8s.pod.name' : 'k8s_pod_name';
const k8sClusterNameKey = dotMetricsEnabled
? 'k8s.cluster.name'
: 'k8s_cluster_name';
const k8sNamespaceNameKey = dotMetricsEnabled
? 'k8s.namespace.name'
: 'k8s_namespace_name';
const clusterName =
deployment.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_CLUSTER_NAME] ?? '';
const namespaceName =
deployment.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] ?? '';
const filters = [
{
id: 'f1',
key: {
dataType: DataTypes.String,
id: 'k8s_cluster_name--string--tag--false',
key: k8sClusterNameKey,
type: 'tag',
},
op: '=',
value: clusterName,
},
{
id: 'f2',
key: {
dataType: DataTypes.String,
id: 'k8s_namespace_name--string--tag--false',
key: k8sNamespaceNameKey,
type: 'tag',
},
op: '=',
value: namespaceName,
},
];
return [
{
@@ -155,6 +207,7 @@ export const getDeploymentMetricsQueryPayload = (
deployment.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_DEPLOYMENT_NAME] ??
'',
},
...filters,
],
op: 'AND',
},
@@ -196,6 +249,7 @@ export const getDeploymentMetricsQueryPayload = (
deployment.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_DEPLOYMENT_NAME] ??
'',
},
...filters,
],
op: 'AND',
},
@@ -237,6 +291,7 @@ export const getDeploymentMetricsQueryPayload = (
deployment.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_DEPLOYMENT_NAME] ??
'',
},
...filters,
],
op: 'AND',
},
@@ -312,6 +367,7 @@ export const getDeploymentMetricsQueryPayload = (
deployment.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_DEPLOYMENT_NAME] ??
'',
},
...filters,
],
op: 'AND',
},
@@ -353,6 +409,7 @@ export const getDeploymentMetricsQueryPayload = (
deployment.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_DEPLOYMENT_NAME] ??
'',
},
...filters,
],
op: 'AND',
},
@@ -394,6 +451,7 @@ export const getDeploymentMetricsQueryPayload = (
deployment.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_DEPLOYMENT_NAME] ??
'',
},
...filters,
],
op: 'AND',
},
@@ -469,6 +527,7 @@ export const getDeploymentMetricsQueryPayload = (
deployment.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_DEPLOYMENT_NAME] ??
'',
},
...filters,
],
op: 'AND',
},
@@ -557,6 +616,7 @@ export const getDeploymentMetricsQueryPayload = (
deployment.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_DEPLOYMENT_NAME] ??
'',
},
...filters,
],
op: 'AND',
},

View File

@@ -6,6 +6,7 @@ import { ExpandButtonWrapper } from 'container/InfraMonitoringK8sV2/components';
import ColumnHeader from '../Base/ColumnHeader';
import EntityGroupHeader from '../Base/EntityGroupHeader';
import K8sGroupCell from '../Base/K8sGroupCell';
import { SelectedItemParams } from '../hooks';
import { formatBytes, getPodPhaseStatusItems } from '../commonUtils';
import {
CellValueTooltip,
@@ -31,8 +32,15 @@ export function getK8sDeploymentRowKey(
export function getK8sDeploymentItemKey(
deployment: InframonitoringtypesDeploymentRecordDTO,
): string {
return deployment.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_DEPLOYMENT_NAME] || '';
): SelectedItemParams {
return {
selectedItem:
deployment.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_DEPLOYMENT_NAME] ?? null,
clusterName:
deployment.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_CLUSTER_NAME] ?? null,
namespaceName:
deployment.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] ?? null,
};
}
export const k8sDeploymentsColumnsConfig: TableColumnDef<InframonitoringtypesDeploymentRecordDTO>[] =
@@ -125,10 +133,10 @@ export const k8sDeploymentsColumnsConfig: TableColumnDef<InframonitoringtypesDep
},
},
{
id: 'replica_status',
id: 'pod_replicas',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/deployments#replica-status">
Replica Status
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/deployments#pod-replicas">
Pod Replicas
</ColumnHeader>
),
accessorFn: (row): number => row.availablePods,

View File

@@ -0,0 +1,32 @@
.chartHeader {
display: inline-flex;
align-items: center;
gap: var(--spacing-2);
}
.infoIcon {
display: inline-flex;
align-items: center;
color: var(--l2-foreground);
cursor: pointer;
&:hover {
color: var(--text-slate-primary);
}
}
.metricsExplorerLink {
display: inline-flex;
align-items: center;
color: var(--l2-foreground);
transition: opacity 0.2s;
&:hover {
color: var(--l3-foreground);
}
}
.chartHeaderLabel {
font-size: var(--periscope-font-size-base);
color: var(--l2-foreground);
}

View File

@@ -0,0 +1,83 @@
import { Link } from 'react-router-dom';
import { Compass, Info } from '@signozhq/icons';
import { Tooltip } from 'antd';
import styles from './ChartHeader.module.scss';
const DOCS_BASE_URL = `${process.env.DOCS_BASE_URL}/docs`;
interface ChartHeaderProps {
title: string;
docPath?: string;
tooltip?: string;
metricsExplorerUrl?: string;
metricsExplorerTestId?: string;
}
function ChartHeader({
title,
docPath,
tooltip,
metricsExplorerUrl,
metricsExplorerTestId = 'open-metrics-explorer',
}: ChartHeaderProps): JSX.Element {
const renderInfoIcon = (): React.ReactNode => {
if (docPath) {
const tooltipTitle = tooltip || 'Not sure what this represents?';
return (
<Tooltip
arrow
title={
<>
{tooltipTitle}{' '}
<a
href={`${DOCS_BASE_URL}${docPath}`}
target="_blank"
rel="noopener"
onClick={(e): void => e.stopPropagation()}
>
Learn more.
</a>
</>
}
>
<span className={styles.infoIcon} data-testid="chart-header-info-icon">
<Info size="md" />
</span>
</Tooltip>
);
}
if (tooltip) {
return (
<Tooltip title={tooltip}>
<span className={styles.infoIcon} data-testid="chart-header-info-icon">
<Info size="md" />
</span>
</Tooltip>
);
}
return null;
};
return (
<div className={styles.chartHeader} data-testid="chart-header">
<span className={styles.chartHeaderLabel}>{title}</span>
{renderInfoIcon()}
{metricsExplorerUrl && (
<Tooltip title="Open in Metrics Explorer">
<Link
to={metricsExplorerUrl}
className={styles.metricsExplorerLink}
data-testid={metricsExplorerTestId}
>
<Compass size={14} />
</Link>
</Tooltip>
)}
</div>
);
}
export default ChartHeader;

View File

@@ -14,28 +14,6 @@
box-sizing: border-box;
}
.entityMetricsTitleContainer {
display: flex;
align-items: center;
gap: 8px;
}
.entityMetricsTitle {
font-size: var(--periscope-font-size-base);
color: var(--l2-foreground);
}
.metricsExplorerLink {
display: flex;
align-items: center;
color: var(--l2-foreground);
transition: opacity 0.2s;
&:hover {
color: var(--l3-foreground);
}
}
.metricsHeader {
display: flex;
justify-content: flex-end;

View File

@@ -1,8 +1,6 @@
import { useCallback, useMemo, useRef } from 'react';
import { UseQueryResult } from 'react-query';
import { Link } from 'react-router-dom';
import { Compass } from '@signozhq/icons';
import { Skeleton, Tooltip } from 'antd';
import { Skeleton } from 'antd';
import cx from 'classnames';
import { PANEL_TYPES } from 'constants/queryBuilder';
import TimeSeries from 'container/DashboardContainer/visualization/charts/TimeSeries/TimeSeries';
@@ -24,6 +22,7 @@ import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
import { getMetricsExplorerUrl } from 'utils/explorerUtils';
import { buildEntityMetricsChartConfig } from './configBuilder';
import ChartHeader from './ChartHeader';
import { useEntityMetrics } from './hooks';
import { isKeyNotFoundError } from '../utils';
@@ -47,6 +46,7 @@ interface EntityMetricsProps<T> {
entityWidgetInfo: {
title: string;
yAxisUnit: string;
docPath?: string;
}[];
getEntityQueryPayload: (
node: T,
@@ -207,31 +207,24 @@ function EntityMetrics<T>({
key={entityWidgetInfo[idx].title}
className={styles.entityMetricsCol}
>
<div className={styles.entityMetricsTitleContainer}>
<span className={styles.entityMetricsTitle}>
{entityWidgetInfo[idx].title}
</span>
{queryPayloads[idx] &&
queryPayloads[idx].graphType !== PANEL_TYPES.TABLE && (
<Tooltip title="Open in Metrics Explorer">
<Link
to={getMetricsExplorerUrl({
query: queryPayloads[idx].query,
...(selectedInterval && selectedInterval !== 'custom'
? { relativeTime: selectedInterval }
: {
startTimeMs: timeRange.startTime * 1000,
endTimeMs: timeRange.endTime * 1000,
}),
})}
className={styles.metricsExplorerLink}
data-testid={`open-metrics-explorer-${idx}`}
>
<Compass size={14} />
</Link>
</Tooltip>
)}
</div>
<ChartHeader
title={entityWidgetInfo[idx].title}
docPath={entityWidgetInfo[idx].docPath}
metricsExplorerUrl={
queryPayloads[idx] && queryPayloads[idx].graphType !== PANEL_TYPES.TABLE
? getMetricsExplorerUrl({
query: queryPayloads[idx].query,
...(selectedInterval && selectedInterval !== 'custom'
? { relativeTime: selectedInterval }
: {
startTimeMs: timeRange.startTime * 1000,
endTimeMs: timeRange.endTime * 1000,
}),
})
: undefined
}
metricsExplorerTestId={`open-metrics-explorer-${idx}`}
/>
<div className={styles.entityMetricsCard} ref={graphRef}>
{renderCardContent(query, idx)}
</div>

View File

@@ -44,7 +44,7 @@ import {
useInfraMonitoringCategory,
useInfraMonitoringGroupBy,
useInfraMonitoringOrderBy,
useInfraMonitoringSelectedItem,
useInfraMonitoringSelectedItemParams,
} from './hooks';
import K8sJobsList from './Jobs/K8sJobsList';
import K8sNamespacesList from './Namespaces/K8sNamespacesList';
@@ -63,7 +63,7 @@ export default function InfraMonitoringK8s(): JSX.Element {
const [selectedCategory, setSelectedCategory] = useInfraMonitoringCategory();
const [, setGroupBy] = useInfraMonitoringGroupBy();
const [, setOrderBy] = useInfraMonitoringOrderBy();
const [, setSelectedItem] = useInfraMonitoringSelectedItem();
const [, setSelectedItemParams] = useInfraMonitoringSelectedItemParams();
const compositeQuery = useGetCompositeQueryParam();
const { currentQuery, redirectWithQueryBuilderData } = useQueryBuilder();
@@ -179,7 +179,7 @@ export default function InfraMonitoringK8s(): JSX.Element {
void setSelectedCategory(key as string);
void setOrderBy(null);
void setGroupBy(null);
void setSelectedItem(null);
setSelectedItemParams(null);
redirectWithQueryBuilderData({
...currentQuery,
builder: {

View File

@@ -11,6 +11,7 @@ import K8sBaseDetails, { K8sDetailsFilters } from '../Base/K8sBaseDetails';
import { K8sBaseList } from '../Base/K8sBaseList';
import { K8sBaseFilters } from '../Base/types';
import { InfraMonitoringEntity } from '../constants';
import { SelectedItemParams } from '../hooks';
import {
getJobMetricsQueryPayload,
jobWidgetInfo,
@@ -117,7 +118,7 @@ function K8sJobsList({
return (
<>
<K8sBaseList<InframonitoringtypesJobRecordDTO>
<K8sBaseList<InframonitoringtypesJobRecordDTO, SelectedItemParams>
controlListPrefix={controlListPrefix}
entity={InfraMonitoringEntity.JOBS}
tableColumns={k8sJobsColumnsConfig}

View File

@@ -7,13 +7,21 @@ import { DataSource, ReduceOperators } from 'types/common/queryBuilder';
import { v4 } from 'uuid';
import { K8sDetailsMetadataConfig } from '../Base/K8sBaseDetails';
import { formatValueForExpression } from 'components/QueryBuilderV2/utils';
import { INFRA_MONITORING_ATTR_KEYS } from '../constants';
import { SelectedItemParams } from '../hooks';
import {
buildEventsExpression,
buildExpressionFromSelectedItemParams,
buildLogsTracesExpression,
} from 'container/InfraMonitoringK8sV2/Base/utils';
export const k8sJobGetSelectedItemExpression = (
selectedItemId: string,
params: SelectedItemParams,
): string =>
`${INFRA_MONITORING_ATTR_KEYS.K8S_JOB_NAME} = ${formatValueForExpression(selectedItemId)}`;
buildExpressionFromSelectedItemParams(
params,
INFRA_MONITORING_ATTR_KEYS.K8S_JOB_NAME,
);
export const k8sJobDetailsMetadataConfig: K8sDetailsMetadataConfig<InframonitoringtypesJobRecordDTO>[] =
[
@@ -36,24 +44,23 @@ export const k8sJobDetailsMetadataConfig: K8sDetailsMetadataConfig<Inframonitori
export const k8sJobInitialEventsExpression = (
item: InframonitoringtypesJobRecordDTO,
): string => {
const name = formatValueForExpression(
item.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_JOB_NAME] ?? '',
);
return `${INFRA_MONITORING_ATTR_KEYS.K8S_OBJECT_KIND} = 'Job' AND ${INFRA_MONITORING_ATTR_KEYS.K8S_OBJECT_NAME} = ${name}`;
};
): string =>
buildEventsExpression({
objectKind: 'Job',
objectName: item.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_JOB_NAME] ?? '',
clusterName: item.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_CLUSTER_NAME],
namespaceName: item.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME],
});
export const k8sJobInitialLogTracesExpression = (
item: InframonitoringtypesJobRecordDTO,
): string => {
const jobName = formatValueForExpression(
item.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_JOB_NAME] ?? '',
);
const namespaceName = formatValueForExpression(
item.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] ?? '',
);
return `${INFRA_MONITORING_ATTR_KEYS.K8S_JOB_NAME} = ${jobName} AND ${INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME} = ${namespaceName}`;
};
): string =>
buildLogsTracesExpression({
mainAttributeKey: INFRA_MONITORING_ATTR_KEYS.K8S_JOB_NAME,
mainAttributeValue: item.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_JOB_NAME],
clusterName: item.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_CLUSTER_NAME],
namespaceName: item.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME],
});
export const k8sJobGetEntityName = (
item: InframonitoringtypesJobRecordDTO,
@@ -63,18 +70,22 @@ export const jobWidgetInfo = [
{
title: 'CPU usage',
yAxisUnit: '',
docPath: '/infrastructure-monitoring/kubernetes/jobs/#cpu-usage',
},
{
title: 'Memory Usage',
yAxisUnit: 'bytes',
docPath: '/infrastructure-monitoring/kubernetes/jobs/#memory-usage',
},
{
title: 'Network IO',
yAxisUnit: 'binBps',
docPath: '/infrastructure-monitoring/kubernetes/jobs/#network-io',
},
{
title: 'Network errors count',
yAxisUnit: '',
docPath: '/infrastructure-monitoring/kubernetes/jobs/#network-errors-count',
},
];
@@ -97,10 +108,54 @@ export const getJobMetricsQueryPayload = (
? 'k8s.pod.network.errors'
: 'k8s_pod_network_errors';
const k8sJobNameKey = dotMetricsEnabled ? 'k8s.job.name' : 'k8s_job_name';
const k8sClusterNameKey = dotMetricsEnabled
? 'k8s.cluster.name'
: 'k8s_cluster_name';
const k8sNamespaceNameKey = dotMetricsEnabled
? 'k8s.namespace.name'
: 'k8s_namespace_name';
const clusterName =
job.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_CLUSTER_NAME] ?? '';
const namespaceName =
job.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] ?? '';
const filters = [
{
id: 'f1',
key: {
dataType: DataTypes.String,
id: 'k8s_job_name--string--tag--false',
key: k8sJobNameKey,
type: 'tag',
},
op: '=',
value: job.jobName,
},
{
id: 'f2',
key: {
dataType: DataTypes.String,
id: 'k8s_cluster_name--string--tag--false',
key: k8sClusterNameKey,
type: 'tag',
},
op: '=',
value: clusterName,
},
{
id: 'f3',
key: {
dataType: DataTypes.String,
id: 'k8s_namespace_name--string--tag--false',
key: k8sNamespaceNameKey,
type: 'tag',
},
op: '=',
value: namespaceName,
},
];
return [
{
selectedTime: 'GLOBAL_TIME',
@@ -120,31 +175,7 @@ export const getJobMetricsQueryPayload = (
disabled: false,
expression: 'A',
filters: {
items: [
{
id: '6b59b690',
key: {
dataType: DataTypes.String,
id: 'k8s_job_name--string--tag--false',
key: k8sJobNameKey,
type: 'tag',
},
op: '=',
value: job.jobName,
},
{
id: '47b3adae',
key: {
dataType: DataTypes.String,
id: 'k8s_namespace_name--string--tag--false',
key: k8sNamespaceNameKey,
type: 'tag',
},
op: '=',
value:
job.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] ?? '',
},
],
items: [...filters],
op: 'AND',
},
functions: [],
@@ -205,31 +236,7 @@ export const getJobMetricsQueryPayload = (
disabled: false,
expression: 'A',
filters: {
items: [
{
id: '8c217f4d',
key: {
dataType: DataTypes.String,
id: 'k8s_job_name--string--tag--false',
key: k8sJobNameKey,
type: 'tag',
},
op: '=',
value: job.jobName,
},
{
id: '47b3adae',
key: {
dataType: DataTypes.String,
id: 'k8s_namespace_name--string--tag--false',
key: k8sNamespaceNameKey,
type: 'tag',
},
op: '=',
value:
job.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] ?? '',
},
],
items: [...filters],
op: 'AND',
},
functions: [],
@@ -290,31 +297,7 @@ export const getJobMetricsQueryPayload = (
disabled: false,
expression: 'A',
filters: {
items: [
{
id: '2bbf9d0c',
key: {
dataType: DataTypes.String,
id: 'k8s_job_name--string--tag--false',
key: k8sJobNameKey,
type: 'tag',
},
op: '=',
value: job.jobName,
},
{
id: '47b3adae',
key: {
dataType: DataTypes.String,
id: 'k8s_namespace_name--string--tag--false',
key: k8sNamespaceNameKey,
type: 'tag',
},
op: '=',
value:
job.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] ?? '',
},
],
items: [...filters],
op: 'AND',
},
functions: [],
@@ -388,31 +371,7 @@ export const getJobMetricsQueryPayload = (
disabled: false,
expression: 'A',
filters: {
items: [
{
id: '448e6cf7',
key: {
dataType: DataTypes.String,
id: 'k8s_job_name--string--tag--false',
key: k8sJobNameKey,
type: 'tag',
},
op: '=',
value: job.jobName,
},
{
id: '47b3adae',
key: {
dataType: DataTypes.String,
id: 'k8s_namespace_name--string--tag--false',
key: k8sNamespaceNameKey,
type: 'tag',
},
op: '=',
value:
job.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] ?? '',
},
],
items: [...filters],
op: 'AND',
},
functions: [],

View File

@@ -6,6 +6,7 @@ import { ExpandButtonWrapper } from 'container/InfraMonitoringK8sV2/components';
import ColumnHeader from '../Base/ColumnHeader';
import EntityGroupHeader from '../Base/EntityGroupHeader';
import K8sGroupCell from '../Base/K8sGroupCell';
import { SelectedItemParams } from '../hooks';
import { formatBytes, getPodPhaseStatusItems } from '../commonUtils';
import {
CellValueTooltip,
@@ -27,8 +28,13 @@ export function getK8sJobRowKey(job: InframonitoringtypesJobRecordDTO): string {
export function getK8sJobItemKey(
job: InframonitoringtypesJobRecordDTO,
): string {
return job.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_JOB_NAME] || '';
): SelectedItemParams {
return {
selectedItem: job.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_JOB_NAME] ?? null,
clusterName: job.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_CLUSTER_NAME] ?? null,
namespaceName:
job.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] ?? null,
};
}
export type JobTableColumnConfig =
@@ -126,10 +132,10 @@ export const k8sJobsColumnsConfig: JobTableColumnConfig[] = [
},
},
{
id: 'completion_status',
id: 'completion',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/jobs#completion-status">
Completion Status
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/jobs#completion">
Completions
</ColumnHeader>
),
accessorFn: (row): number => row.successfulPods,
@@ -149,7 +155,7 @@ export const k8sJobsColumnsConfig: JobTableColumnConfig[] = [
{
value: row.desiredSuccessfulPods,
label: 'Desired',
color: Color.BG_ROBIN_500,
color: Color.BG_AMBER_500,
},
]}
/>

View File

@@ -11,9 +11,12 @@ import K8sBaseDetails, { K8sDetailsFilters } from '../Base/K8sBaseDetails';
import { K8sBaseList } from '../Base/K8sBaseList';
import { K8sBaseFilters } from '../Base/types';
import { InfraMonitoringEntity } from '../constants';
import { SelectedItemParams } from '../hooks';
import {
getNamespaceMetricsQueryPayload,
k8sNamespaceDetailsCountsConfig,
k8sNamespaceDetailsMetadataConfig,
k8sNamespaceGetCountsFilterExpression,
k8sNamespaceGetEntityName,
k8sNamespaceGetSelectedItemExpression,
k8sNamespaceInitialEventsExpression,
@@ -117,7 +120,7 @@ function K8sNamespacesList({
return (
<>
<K8sBaseList<InframonitoringtypesNamespaceRecordDTO>
<K8sBaseList<InframonitoringtypesNamespaceRecordDTO, SelectedItemParams>
controlListPrefix={controlListPrefix}
entity={InfraMonitoringEntity.NAMESPACES}
tableColumns={k8sNamespacesColumnsConfig}
@@ -136,6 +139,8 @@ function K8sNamespacesList({
getInitialLogTracesExpression={k8sNamespaceInitialLogTracesExpression}
getInitialEventsExpression={k8sNamespaceInitialEventsExpression}
metadataConfig={k8sNamespaceDetailsMetadataConfig}
countsConfig={k8sNamespaceDetailsCountsConfig}
getCountsFilterExpression={k8sNamespaceGetCountsFilterExpression}
entityWidgetInfo={namespaceWidgetInfo}
getEntityQueryPayload={getNamespaceMetricsQueryPayload}
queryKeyPrefix="namespace"

View File

@@ -6,14 +6,29 @@ import { EQueryType } from 'types/common/dashboard';
import { DataSource, ReduceOperators } from 'types/common/queryBuilder';
import { v4 } from 'uuid';
import { K8sDetailsMetadataConfig } from '../Base/K8sBaseDetails';
import {
K8sDetailsCountConfig,
K8sDetailsMetadataConfig,
} from '../Base/K8sBaseDetails';
import {
INFRA_MONITORING_ATTR_KEYS,
InfraMonitoringEntity,
} from '../constants';
import { SelectedItemParams } from '../hooks';
import { formatValueForExpression } from 'components/QueryBuilderV2/utils';
import { INFRA_MONITORING_ATTR_KEYS } from '../constants';
import {
buildEventsExpression,
buildExpressionFromSelectedItemParams,
buildLogsTracesExpression,
} from 'container/InfraMonitoringK8sV2/Base/utils';
export const k8sNamespaceGetSelectedItemExpression = (
selectedItemId: string,
params: SelectedItemParams,
): string =>
`${INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME} = ${formatValueForExpression(selectedItemId)}`;
buildExpressionFromSelectedItemParams(
params,
INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME,
);
export const k8sNamespaceDetailsMetadataConfig: K8sDetailsMetadataConfig<InframonitoringtypesNamespaceRecordDTO>[] =
[
@@ -25,64 +40,124 @@ export const k8sNamespaceDetailsMetadataConfig: K8sDetailsMetadataConfig<Inframo
},
];
export const k8sNamespaceDetailsCountsConfig: K8sDetailsCountConfig<InframonitoringtypesNamespaceRecordDTO>[] =
[
{
label: 'Deployments',
getValue: (p): number => p.counts?.deployments ?? 0,
targetCategory: InfraMonitoringEntity.DEPLOYMENTS,
},
{
label: 'StatefulSets',
getValue: (p): number => p.counts?.statefulSets ?? 0,
targetCategory: InfraMonitoringEntity.STATEFULSETS,
},
{
label: 'DaemonSets',
getValue: (p): number => p.counts?.daemonSets ?? 0,
targetCategory: InfraMonitoringEntity.DAEMONSETS,
},
{
label: 'Jobs',
getValue: (p): number => p.counts?.jobs ?? 0,
targetCategory: InfraMonitoringEntity.JOBS,
},
];
export const k8sNamespaceInitialEventsExpression = (
item: InframonitoringtypesNamespaceRecordDTO,
): string => {
const name = formatValueForExpression(item.namespaceName || '');
return `${INFRA_MONITORING_ATTR_KEYS.K8S_OBJECT_KIND} = 'Namespace' AND ${INFRA_MONITORING_ATTR_KEYS.K8S_OBJECT_NAME} = ${name}`;
};
): string =>
buildEventsExpression({
objectKind: 'Namespace',
objectName: item.namespaceName || '',
clusterName: item.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_CLUSTER_NAME],
});
export const k8sNamespaceInitialLogTracesExpression = (
item: InframonitoringtypesNamespaceRecordDTO,
): string => {
const name = formatValueForExpression(item.namespaceName || '');
return `${INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME} = ${name}`;
};
): string =>
buildLogsTracesExpression({
mainAttributeKey: INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME,
mainAttributeValue: item.namespaceName,
clusterName: item.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_CLUSTER_NAME],
});
export const k8sNamespaceGetEntityName = (
item: InframonitoringtypesNamespaceRecordDTO,
): string => item.namespaceName || '';
export const k8sNamespaceGetCountsFilterExpression = (
item: InframonitoringtypesNamespaceRecordDTO,
): string => {
const clusterName = item.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_CLUSTER_NAME];
const clauses: string[] = [];
if (clusterName) {
clauses.push(
`${INFRA_MONITORING_ATTR_KEYS.K8S_CLUSTER_NAME} = ${formatValueForExpression(clusterName)}`,
);
}
if (item.namespaceName) {
clauses.push(
`${INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME} = ${formatValueForExpression(item.namespaceName)}`,
);
}
return clauses.join(' AND ');
};
export const namespaceWidgetInfo = [
{
title: 'CPU Usage (cores)',
yAxisUnit: '',
docPath: '/infrastructure-monitoring/kubernetes/namespaces/#cpu-usage-cores',
},
{
title: 'Memory Usage (bytes)',
yAxisUnit: 'bytes',
docPath:
'/infrastructure-monitoring/kubernetes/namespaces/#memory-usage-bytes',
},
{
title: 'Pods CPU (top 10)',
yAxisUnit: '',
docPath: '/infrastructure-monitoring/kubernetes/namespaces/#pods-cpu-top-10',
},
{
title: 'Pods Memory (top 10)',
yAxisUnit: 'bytes',
docPath:
'/infrastructure-monitoring/kubernetes/namespaces/#pods-memory-top-10',
},
{
title: 'Network rate',
yAxisUnit: 'binBps',
docPath: '/infrastructure-monitoring/kubernetes/namespaces/#network-rate',
},
{
title: 'Network errors',
yAxisUnit: '',
docPath: '/infrastructure-monitoring/kubernetes/namespaces/#network-errors',
},
{
title: 'StatefulSets',
title: 'StatefulSets (pods)',
yAxisUnit: '',
docPath: '/infrastructure-monitoring/kubernetes/namespaces/#statefulsets',
},
{
title: 'ReplicaSets',
title: 'ReplicaSets (pods)',
yAxisUnit: '',
docPath: '/infrastructure-monitoring/kubernetes/namespaces/#replicasets',
},
{
title: 'DaemonSets',
title: 'DaemonSets (nodes)',
yAxisUnit: '',
docPath: '/infrastructure-monitoring/kubernetes/namespaces/#daemonsets',
},
{
title: 'Deployments',
title: 'Deployments (pods)',
yAxisUnit: '',
docPath: '/infrastructure-monitoring/kubernetes/namespaces/#deployments',
},
];
@@ -179,6 +254,24 @@ export const getNamespaceMetricsQueryPayload = (
'k8s.deployment.name',
'k8s_deployment_name',
);
const k8sClusterNameKey = getKey('k8s.cluster.name', 'k8s_cluster_name');
const clusterName =
namespace.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_CLUSTER_NAME] ?? '';
const filters = [
{
id: 'f1',
key: {
dataType: DataTypes.String,
id: 'k8s_cluster_name--string--tag--false',
key: k8sClusterNameKey,
type: 'tag',
},
op: '=',
value: clusterName,
},
];
return [
{
@@ -211,6 +304,7 @@ export const getNamespaceMetricsQueryPayload = (
op: '=',
value: namespace.namespaceName,
},
...filters,
],
op: 'AND',
},
@@ -250,6 +344,7 @@ export const getNamespaceMetricsQueryPayload = (
op: '=',
value: namespace.namespaceName,
},
...filters,
],
op: 'AND',
},
@@ -289,6 +384,7 @@ export const getNamespaceMetricsQueryPayload = (
op: '=',
value: namespace.namespaceName,
},
...filters,
],
op: 'AND',
},
@@ -328,6 +424,7 @@ export const getNamespaceMetricsQueryPayload = (
op: '=',
value: namespace.namespaceName,
},
...filters,
],
op: 'AND',
},
@@ -401,6 +498,7 @@ export const getNamespaceMetricsQueryPayload = (
op: '=',
value: namespace.namespaceName,
},
...filters,
],
op: 'AND',
},
@@ -440,6 +538,7 @@ export const getNamespaceMetricsQueryPayload = (
op: '=',
value: namespace.namespaceName,
},
...filters,
],
op: 'AND',
},
@@ -479,6 +578,7 @@ export const getNamespaceMetricsQueryPayload = (
op: '=',
value: namespace.namespaceName,
},
...filters,
],
op: 'AND',
},
@@ -518,6 +618,7 @@ export const getNamespaceMetricsQueryPayload = (
op: '=',
value: namespace.namespaceName,
},
...filters,
],
op: 'AND',
},
@@ -557,6 +658,7 @@ export const getNamespaceMetricsQueryPayload = (
op: '=',
value: namespace.namespaceName,
},
...filters,
],
op: 'AND',
},
@@ -596,6 +698,7 @@ export const getNamespaceMetricsQueryPayload = (
op: '=',
value: namespace.namespaceName,
},
...filters,
],
op: 'AND',
},
@@ -669,6 +772,7 @@ export const getNamespaceMetricsQueryPayload = (
op: '=',
value: namespace.namespaceName,
},
...filters,
],
op: 'AND',
},
@@ -749,6 +853,7 @@ export const getNamespaceMetricsQueryPayload = (
op: '=',
value: namespace.namespaceName,
},
...filters,
],
op: 'AND',
},
@@ -829,6 +934,7 @@ export const getNamespaceMetricsQueryPayload = (
op: '=',
value: namespace.namespaceName,
},
...filters,
],
op: 'AND',
},
@@ -915,6 +1021,7 @@ export const getNamespaceMetricsQueryPayload = (
op: '=',
value: namespace.namespaceName,
},
...filters,
],
op: 'AND',
},
@@ -1001,6 +1108,7 @@ export const getNamespaceMetricsQueryPayload = (
op: '=',
value: namespace.namespaceName,
},
...filters,
],
op: 'AND',
},
@@ -1047,6 +1155,7 @@ export const getNamespaceMetricsQueryPayload = (
op: '=',
value: namespace.namespaceName,
},
...filters,
],
op: 'AND',
},
@@ -1093,6 +1202,7 @@ export const getNamespaceMetricsQueryPayload = (
op: '=',
value: namespace.namespaceName,
},
...filters,
],
op: 'AND',
},
@@ -1173,6 +1283,7 @@ export const getNamespaceMetricsQueryPayload = (
op: '=',
value: namespace.namespaceName,
},
...filters,
],
op: 'AND',
},
@@ -1225,6 +1336,7 @@ export const getNamespaceMetricsQueryPayload = (
op: '=',
value: namespace.namespaceName,
},
...filters,
],
op: 'AND',
},
@@ -1311,6 +1423,7 @@ export const getNamespaceMetricsQueryPayload = (
op: '=',
value: namespace.namespaceName,
},
...filters,
],
op: 'AND',
},
@@ -1357,6 +1470,7 @@ export const getNamespaceMetricsQueryPayload = (
op: '=',
value: namespace.namespaceName,
},
...filters,
],
op: 'AND',
},
@@ -1403,6 +1517,7 @@ export const getNamespaceMetricsQueryPayload = (
op: '=',
value: namespace.namespaceName,
},
...filters,
],
op: 'AND',
},
@@ -1449,6 +1564,7 @@ export const getNamespaceMetricsQueryPayload = (
op: '=',
value: namespace.namespaceName,
},
...filters,
],
op: 'AND',
},
@@ -1529,6 +1645,7 @@ export const getNamespaceMetricsQueryPayload = (
op: '=',
value: namespace.namespaceName,
},
...filters,
],
op: 'AND',
},
@@ -1575,6 +1692,7 @@ export const getNamespaceMetricsQueryPayload = (
op: '=',
value: namespace.namespaceName,
},
...filters,
],
op: 'AND',
},

View File

@@ -16,6 +16,7 @@ import {
INFRA_MONITORING_ATTR_KEYS,
InfraMonitoringEntity,
} from '../constants';
import { SelectedItemParams } from '../hooks';
export function getK8sNamespaceRowKey(
namespace: InframonitoringtypesNamespaceRecordDTO,
@@ -29,8 +30,15 @@ export function getK8sNamespaceRowKey(
export function getK8sNamespaceItemKey(
namespace: InframonitoringtypesNamespaceRecordDTO,
): string {
return namespace.namespaceName;
): SelectedItemParams {
return {
selectedItem:
namespace.namespaceName ??
namespace.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] ??
null,
clusterName:
namespace.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_CLUSTER_NAME] ?? null,
};
}
export type NamespaceTableColumnConfig =

View File

@@ -7,13 +7,19 @@ import { DataSource, ReduceOperators } from 'types/common/queryBuilder';
import { v4 } from 'uuid';
import { formatValueForExpression } from 'components/QueryBuilderV2/utils';
import {
buildEventsExpression,
buildLogsTracesExpression,
} from 'container/InfraMonitoringK8sV2/Base/utils';
import { K8sDetailsMetadataConfig } from '../Base/K8sBaseDetails';
import { INFRA_MONITORING_ATTR_KEYS } from '../constants';
import { SelectedItemParams } from '../hooks';
export const k8sNodeGetSelectedItemExpression = (
selectedItemId: string,
): string => `k8s.node.name = ${formatValueForExpression(selectedItemId)}`;
params: SelectedItemParams,
): string =>
`k8s.node.name = ${formatValueForExpression(params.selectedItem ?? '')}`;
export const k8sNodeDetailsMetadataConfig: K8sDetailsMetadataConfig<InframonitoringtypesNodeRecordDTO>[] =
[
@@ -28,12 +34,20 @@ export const k8sNodeDetailsMetadataConfig: K8sDetailsMetadataConfig<Inframonitor
export const k8sNodeInitialEventsExpression = (
item: InframonitoringtypesNodeRecordDTO,
): string =>
`${INFRA_MONITORING_ATTR_KEYS.K8S_OBJECT_KIND} = 'Node' AND ${INFRA_MONITORING_ATTR_KEYS.K8S_OBJECT_NAME} = ${formatValueForExpression(item.nodeName || '')}`;
buildEventsExpression({
objectKind: 'Node',
objectName: item.nodeName || '',
clusterName: item.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_CLUSTER_NAME],
});
export const k8sNodeInitialLogTracesExpression = (
item: InframonitoringtypesNodeRecordDTO,
): string =>
`${INFRA_MONITORING_ATTR_KEYS.K8S_NODE_NAME} = ${formatValueForExpression(item.nodeName || '')} AND ${INFRA_MONITORING_ATTR_KEYS.K8S_CLUSTER_NAME} = ${formatValueForExpression(item.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_CLUSTER_NAME] || '')}`;
buildLogsTracesExpression({
mainAttributeKey: INFRA_MONITORING_ATTR_KEYS.K8S_NODE_NAME,
mainAttributeValue: item.nodeName,
clusterName: item.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_CLUSTER_NAME],
});
export const k8sNodeGetEntityName = (
item: InframonitoringtypesNodeRecordDTO,
@@ -43,42 +57,53 @@ export const nodeWidgetInfo = [
{
title: 'CPU Usage (cores)',
yAxisUnit: '',
docPath: '/infrastructure-monitoring/kubernetes/nodes/#cpu-usage-cores',
},
{
title: 'Memory Usage (bytes)',
yAxisUnit: 'bytes',
docPath: '/infrastructure-monitoring/kubernetes/nodes/#memory-usage-bytes',
},
{
title: 'CPU Usage (%)',
yAxisUnit: 'percentunit',
docPath: '/infrastructure-monitoring/kubernetes/nodes/#cpu-usage-',
},
{
title: 'Memory Usage (%)',
yAxisUnit: 'percentunit',
docPath: '/infrastructure-monitoring/kubernetes/nodes/#memory-usage-',
},
{
title: 'Pods by CPU (top 10)',
yAxisUnit: '',
docPath: '/infrastructure-monitoring/kubernetes/nodes/#pods-by-cpu-top-10',
},
{
title: 'Pods by Memory (top 10)',
yAxisUnit: 'bytes',
docPath: '/infrastructure-monitoring/kubernetes/nodes/#pods-by-memory-top-10',
},
{
title: 'Network error count',
yAxisUnit: '',
docPath: '/infrastructure-monitoring/kubernetes/nodes/#network-error-count',
},
{
title: 'Network IO rate',
yAxisUnit: 'binBps',
docPath: '/infrastructure-monitoring/kubernetes/nodes/#network-io-rate',
},
{
title: 'Filesystem usage (bytes)',
yAxisUnit: 'bytes',
docPath:
'/infrastructure-monitoring/kubernetes/nodes/#filesystem-usage-bytes',
},
{
title: 'Filesystem usage (%)',
yAxisUnit: 'percentunit',
docPath: '/infrastructure-monitoring/kubernetes/nodes/#filesystem-usage-',
},
];

View File

@@ -8,11 +8,17 @@ import { v4 } from 'uuid';
import { K8sDetailsMetadataConfig } from '../Base/K8sBaseDetails';
import { formatValueForExpression } from 'components/QueryBuilderV2/utils';
import {
buildEventsExpression,
buildLogsTracesExpression,
} from 'container/InfraMonitoringK8sV2/Base/utils';
import { INFRA_MONITORING_ATTR_KEYS } from '../constants';
import { SelectedItemParams } from '../hooks';
export const k8sPodGetSelectedItemExpression = (
selectedItemId: string,
): string => `k8s.pod.uid = ${formatValueForExpression(selectedItemId)}`;
params: SelectedItemParams,
): string =>
`k8s.pod.uid = ${formatValueForExpression(params.selectedItem ?? '')}`;
export const k8sPodDetailsMetadataConfig: K8sDetailsMetadataConfig<InframonitoringtypesPodRecordDTO>[] =
[
@@ -35,21 +41,23 @@ export const k8sPodDetailsMetadataConfig: K8sDetailsMetadataConfig<Inframonitori
export const k8sPodInitialEventsExpression = (
pod: InframonitoringtypesPodRecordDTO,
): string => {
const podName = formatValueForExpression(
pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_POD_NAME] || '',
);
return `${INFRA_MONITORING_ATTR_KEYS.K8S_OBJECT_KIND} = 'Pod' AND ${INFRA_MONITORING_ATTR_KEYS.K8S_OBJECT_NAME} = ${podName}`;
};
): string =>
buildEventsExpression({
objectKind: 'Pod',
objectName: pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_POD_NAME] || '',
clusterName: pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_CLUSTER_NAME],
namespaceName: pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME],
});
export const k8sPodInitialLogTracesExpression = (
pod: InframonitoringtypesPodRecordDTO,
): string => {
const podName = formatValueForExpression(
pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_POD_NAME] || '',
);
return `${INFRA_MONITORING_ATTR_KEYS.K8S_POD_NAME} = ${podName}`;
};
): string =>
buildLogsTracesExpression({
mainAttributeKey: INFRA_MONITORING_ATTR_KEYS.K8S_POD_NAME,
mainAttributeValue: pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_POD_NAME],
clusterName: pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_CLUSTER_NAME],
namespaceName: pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME],
});
export const k8sPodGetEntityName = (
pod: InframonitoringtypesPodRecordDTO,
@@ -59,54 +67,74 @@ export const podWidgetInfo = [
{
title: 'CPU Usage (cores)',
yAxisUnit: '',
docPath: '/infrastructure-monitoring/kubernetes/pods/#cpu-usage-cores-1',
},
{
title: 'CPU Request, Limit Utilization',
yAxisUnit: 'percentunit',
docPath:
'/infrastructure-monitoring/kubernetes/pods/#cpu-request-limit-utilization',
},
{
title: 'Memory Usage (bytes)',
yAxisUnit: 'bytes',
docPath: '/infrastructure-monitoring/kubernetes/pods/#memory-usage-bytes',
},
{
title: 'Memory Request, Limit Utilization',
yAxisUnit: 'percentunit',
docPath:
'/infrastructure-monitoring/kubernetes/pods/#memory-request-limit-utilization',
},
{
title: 'Memory by State',
yAxisUnit: 'bytes',
docPath: '/infrastructure-monitoring/kubernetes/pods/#memory-by-state',
},
{
title: 'Memory Major Page Faults',
yAxisUnit: '',
docPath:
'/infrastructure-monitoring/kubernetes/pods/#memory-major-page-faults',
},
{
title: 'CPU Usage by Container (cores)',
yAxisUnit: '',
docPath:
'/infrastructure-monitoring/kubernetes/pods/#cpu-usage-by-container-cores',
},
{
title: 'CPU Request, Limit Utilization by Container',
yAxisUnit: 'percentunit',
docPath:
'/infrastructure-monitoring/kubernetes/pods/#cpu-request-limit-utilization-by-container',
},
{
title: 'Memory Usage by Container (bytes)',
yAxisUnit: 'bytes',
docPath:
'/infrastructure-monitoring/kubernetes/pods/#memory-usage-by-container-bytes',
},
{
title: 'Memory Request, Limit Utilization by Container',
yAxisUnit: 'percentunit',
docPath:
'/infrastructure-monitoring/kubernetes/pods/#memory-request-limit-utilization-by-container',
},
{
title: 'Network rate',
yAxisUnit: 'binBps',
docPath: '/infrastructure-monitoring/kubernetes/pods/#network-rate',
},
{
title: 'Network errors',
yAxisUnit: '',
docPath: '/infrastructure-monitoring/kubernetes/pods/#network-errors',
},
{
title: 'File system (bytes)',
yAxisUnit: 'bytes',
docPath: '/infrastructure-monitoring/kubernetes/pods/#file-system-bytes',
},
];

View File

@@ -152,7 +152,11 @@ export const k8sPodColumnsConfig: PodTableColumnConfig[] = [
},
{
id: 'podAge',
header: 'Age',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/pods#age">
Age
</ColumnHeader>
),
accessorFn: (row): number => row.podAge,
width: { min: 100 },
enableSort: false,
@@ -316,7 +320,11 @@ export const k8sPodColumnsConfig: PodTableColumnConfig[] = [
},
{
id: 'namespace',
header: 'Namespace',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/pods#additional-columns">
Namespace
</ColumnHeader>
),
accessorFn: (row): string =>
row.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] || '',
width: { default: 100 },
@@ -328,7 +336,11 @@ export const k8sPodColumnsConfig: PodTableColumnConfig[] = [
},
{
id: 'node',
header: 'Node',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/pods#additional-columns">
Node
</ColumnHeader>
),
accessorFn: (row): string =>
row.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NODE_NAME] || '',
width: { default: 100 },
@@ -340,7 +352,11 @@ export const k8sPodColumnsConfig: PodTableColumnConfig[] = [
},
{
id: 'cluster',
header: 'Cluster',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/pods#additional-columns">
Cluster
</ColumnHeader>
),
accessorFn: (row): string =>
row.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_CLUSTER_NAME] || '',
width: { default: 100 },

View File

@@ -11,6 +11,7 @@ import K8sBaseDetails, { K8sDetailsFilters } from '../Base/K8sBaseDetails';
import { K8sBaseList } from '../Base/K8sBaseList';
import { K8sBaseFilters } from '../Base/types';
import { InfraMonitoringEntity } from '../constants';
import { SelectedItemParams } from '../hooks';
import {
getStatefulSetMetricsQueryPayload,
k8sStatefulSetDetailsMetadataConfig,
@@ -117,7 +118,7 @@ function K8sStatefulSetsList({
return (
<>
<K8sBaseList<InframonitoringtypesStatefulSetRecordDTO>
<K8sBaseList<InframonitoringtypesStatefulSetRecordDTO, SelectedItemParams>
controlListPrefix={controlListPrefix}
entity={InfraMonitoringEntity.STATEFULSETS}
tableColumns={k8sStatefulSetsColumnsConfig}

View File

@@ -7,13 +7,21 @@ import { DataSource, ReduceOperators } from 'types/common/queryBuilder';
import { v4 } from 'uuid';
import { K8sDetailsMetadataConfig } from '../Base/K8sBaseDetails';
import { formatValueForExpression } from 'components/QueryBuilderV2/utils';
import { INFRA_MONITORING_ATTR_KEYS } from '../constants';
import { SelectedItemParams } from '../hooks';
import {
buildEventsExpression,
buildExpressionFromSelectedItemParams,
buildLogsTracesExpression,
} from 'container/InfraMonitoringK8sV2/Base/utils';
export const k8sStatefulSetGetSelectedItemExpression = (
selectedItemId: string,
params: SelectedItemParams,
): string =>
`${INFRA_MONITORING_ATTR_KEYS.K8S_STATEFULSET_NAME} = ${formatValueForExpression(selectedItemId)}`;
buildExpressionFromSelectedItemParams(
params,
INFRA_MONITORING_ATTR_KEYS.K8S_STATEFULSET_NAME,
);
export const k8sStatefulSetDetailsMetadataConfig: K8sDetailsMetadataConfig<InframonitoringtypesStatefulSetRecordDTO>[] =
[
@@ -22,6 +30,11 @@ export const k8sStatefulSetDetailsMetadataConfig: K8sDetailsMetadataConfig<Infra
getValue: (p): string =>
p.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_STATEFULSET_NAME] ?? '',
},
{
label: 'Cluster Name',
getValue: (p): string =>
p.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_CLUSTER_NAME] ?? '',
},
{
label: 'Namespace Name',
getValue: (p): string =>
@@ -31,24 +44,25 @@ export const k8sStatefulSetDetailsMetadataConfig: K8sDetailsMetadataConfig<Infra
export const k8sStatefulSetInitialEventsExpression = (
item: InframonitoringtypesStatefulSetRecordDTO,
): string => {
const objectName = formatValueForExpression(
item.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_STATEFULSET_NAME] ?? '',
);
return `${INFRA_MONITORING_ATTR_KEYS.K8S_OBJECT_KIND} = 'StatefulSet' AND ${INFRA_MONITORING_ATTR_KEYS.K8S_OBJECT_NAME} = ${objectName}`;
};
): string =>
buildEventsExpression({
objectKind: 'StatefulSet',
objectName:
item.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_STATEFULSET_NAME] ?? '',
clusterName: item.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_CLUSTER_NAME],
namespaceName: item.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME],
});
export const k8sStatefulSetInitialLogTracesExpression = (
item: InframonitoringtypesStatefulSetRecordDTO,
): string => {
const statefulSetName = formatValueForExpression(
item.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_STATEFULSET_NAME] ?? '',
);
const namespaceName = formatValueForExpression(
item.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] ?? '',
);
return `${INFRA_MONITORING_ATTR_KEYS.K8S_STATEFULSET_NAME} = ${statefulSetName} AND ${INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME} = ${namespaceName}`;
};
): string =>
buildLogsTracesExpression({
mainAttributeKey: INFRA_MONITORING_ATTR_KEYS.K8S_STATEFULSET_NAME,
mainAttributeValue:
item.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_STATEFULSET_NAME],
clusterName: item.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_CLUSTER_NAME],
namespaceName: item.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME],
});
export const k8sStatefulSetGetEntityName = (
item: InframonitoringtypesStatefulSetRecordDTO,
@@ -58,26 +72,37 @@ export const statefulSetWidgetInfo = [
{
title: 'CPU usage, request, limits',
yAxisUnit: '',
docPath:
'/infrastructure-monitoring/kubernetes/statefulsets/#cpu-usage-request-limits',
},
{
title: 'CPU request, limit util (%)',
yAxisUnit: 'percentunit',
docPath:
'/infrastructure-monitoring/kubernetes/statefulsets/#cpu-request-limit-utilization-',
},
{
title: 'Memory usage, request, limits',
yAxisUnit: 'bytes',
docPath:
'/infrastructure-monitoring/kubernetes/statefulsets/#memory-usage-request-limits',
},
{
title: 'Memory request, limit util (%)',
yAxisUnit: 'percentunit',
docPath:
'/infrastructure-monitoring/kubernetes/statefulsets/#memory-request-limit-utilization-',
},
{
title: 'Network IO',
yAxisUnit: 'binBps',
docPath: '/infrastructure-monitoring/kubernetes/statefulsets/#network-io',
},
{
title: 'Network errors count',
yAxisUnit: '',
docPath:
'/infrastructure-monitoring/kubernetes/statefulsets/#network-errors-count',
},
];
@@ -93,7 +118,42 @@ export const getStatefulSetMetricsQueryPayload = (
const k8sNamespaceNameKey = dotMetricsEnabled
? INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME
: 'k8s_namespace_name';
const k8sPodNameKey = dotMetricsEnabled ? 'k8s.pod.name' : 'k8s_pod_name';
const k8sPodNameKey = dotMetricsEnabled
? INFRA_MONITORING_ATTR_KEYS.K8S_POD_NAME
: 'k8s_pod_name';
const k8sClusterNameKey = dotMetricsEnabled
? INFRA_MONITORING_ATTR_KEYS.K8S_CLUSTER_NAME
: 'k8s_cluster_name';
const clusterName =
statefulSet.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_CLUSTER_NAME] ?? '';
const namespaceName =
statefulSet.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] ?? '';
const filters = [
{
id: 'f2',
key: {
dataType: DataTypes.String,
id: 'k8s_namespace_name--string--tag--false',
key: k8sNamespaceNameKey,
type: 'tag',
},
op: '=',
value: namespaceName,
},
{
id: 'f3',
key: {
dataType: DataTypes.String,
id: 'k8s_cluster_name--string--tag--false',
key: k8sClusterNameKey,
type: 'tag',
},
op: '=',
value: clusterName,
},
];
const k8sPodCpuUtilKey = dotMetricsEnabled
? 'k8s.pod.cpu.usage'
@@ -168,19 +228,7 @@ export const getStatefulSetMetricsQueryPayload = (
INFRA_MONITORING_ATTR_KEYS.K8S_STATEFULSET_NAME
] ?? '',
},
{
id: 'f2',
key: {
dataType: DataTypes.String,
id: 'ns_name',
key: k8sNamespaceNameKey,
type: 'tag',
},
op: '=',
value:
statefulSet.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] ??
'',
},
...filters,
],
op: 'AND',
},
@@ -223,19 +271,7 @@ export const getStatefulSetMetricsQueryPayload = (
INFRA_MONITORING_ATTR_KEYS.K8S_STATEFULSET_NAME
] ?? '',
},
{
id: 'f2',
key: {
dataType: DataTypes.String,
id: 'ns_name',
key: k8sNamespaceNameKey,
type: 'tag',
},
op: '=',
value:
statefulSet.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] ??
'',
},
...filters,
],
op: 'AND',
},
@@ -278,19 +314,7 @@ export const getStatefulSetMetricsQueryPayload = (
INFRA_MONITORING_ATTR_KEYS.K8S_STATEFULSET_NAME
] ?? '',
},
{
id: 'f2',
key: {
dataType: DataTypes.String,
id: 'ns_name',
key: k8sNamespaceNameKey,
type: 'tag',
},
op: '=',
value:
statefulSet.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] ??
'',
},
...filters,
],
op: 'AND',
},
@@ -353,19 +377,7 @@ export const getStatefulSetMetricsQueryPayload = (
INFRA_MONITORING_ATTR_KEYS.K8S_STATEFULSET_NAME
] ?? '',
},
{
id: 'f2',
key: {
dataType: DataTypes.String,
id: 'ns_name',
key: k8sNamespaceNameKey,
type: 'tag',
},
op: '=',
value:
statefulSet.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] ??
'',
},
...filters,
],
op: 'AND',
},
@@ -408,19 +420,7 @@ export const getStatefulSetMetricsQueryPayload = (
INFRA_MONITORING_ATTR_KEYS.K8S_STATEFULSET_NAME
] ?? '',
},
{
id: 'f2',
key: {
dataType: DataTypes.String,
id: 'ns_name',
key: k8sNamespaceNameKey,
type: 'tag',
},
op: '=',
value:
statefulSet.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] ??
'',
},
...filters,
],
op: 'AND',
},
@@ -483,19 +483,7 @@ export const getStatefulSetMetricsQueryPayload = (
INFRA_MONITORING_ATTR_KEYS.K8S_STATEFULSET_NAME
] ?? '',
},
{
id: 'f2',
key: {
dataType: DataTypes.String,
id: 'ns_name',
key: k8sNamespaceNameKey,
type: 'tag',
},
op: '=',
value:
statefulSet.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] ??
'',
},
...filters,
],
op: 'AND',
},
@@ -525,7 +513,7 @@ export const getStatefulSetMetricsQueryPayload = (
filters: {
items: [
{
id: 'f3',
id: 'f1',
key: {
dataType: DataTypes.String,
id: 'pod_name',
@@ -538,19 +526,7 @@ export const getStatefulSetMetricsQueryPayload = (
INFRA_MONITORING_ATTR_KEYS.K8S_STATEFULSET_NAME
] ?? '',
},
{
id: 'f2',
key: {
dataType: DataTypes.String,
id: 'ns_name',
key: k8sNamespaceNameKey,
type: 'tag',
},
op: '=',
value:
statefulSet.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] ??
'',
},
...filters,
],
op: 'AND',
},
@@ -593,19 +569,7 @@ export const getStatefulSetMetricsQueryPayload = (
INFRA_MONITORING_ATTR_KEYS.K8S_STATEFULSET_NAME
] ?? '',
},
{
id: 'f2',
key: {
dataType: DataTypes.String,
id: 'ns_name',
key: k8sNamespaceNameKey,
type: 'tag',
},
op: '=',
value:
statefulSet.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] ??
'',
},
...filters,
],
op: 'AND',
},
@@ -668,19 +632,7 @@ export const getStatefulSetMetricsQueryPayload = (
INFRA_MONITORING_ATTR_KEYS.K8S_STATEFULSET_NAME
] ?? '',
},
{
id: 'f2',
key: {
dataType: DataTypes.String,
id: 'ns_name',
key: k8sNamespaceNameKey,
type: 'tag',
},
op: '=',
value:
statefulSet.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] ??
'',
},
...filters,
],
op: 'AND',
},
@@ -723,19 +675,7 @@ export const getStatefulSetMetricsQueryPayload = (
INFRA_MONITORING_ATTR_KEYS.K8S_STATEFULSET_NAME
] ?? '',
},
{
id: 'f2',
key: {
dataType: DataTypes.String,
id: 'ns_name',
key: k8sNamespaceNameKey,
type: 'tag',
},
op: '=',
value:
statefulSet.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] ??
'',
},
...filters,
],
op: 'AND',
},
@@ -798,19 +738,7 @@ export const getStatefulSetMetricsQueryPayload = (
INFRA_MONITORING_ATTR_KEYS.K8S_STATEFULSET_NAME
] ?? '',
},
{
id: 'f2',
key: {
dataType: DataTypes.String,
id: 'ns_name',
key: k8sNamespaceNameKey,
type: 'tag',
},
op: '=',
value:
statefulSet.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] ??
'',
},
...filters,
],
op: 'AND',
},
@@ -886,19 +814,7 @@ export const getStatefulSetMetricsQueryPayload = (
INFRA_MONITORING_ATTR_KEYS.K8S_STATEFULSET_NAME
] ?? '',
},
{
id: 'f2',
key: {
dataType: DataTypes.String,
id: 'ns_name',
key: k8sNamespaceNameKey,
type: 'tag',
},
op: '=',
value:
statefulSet.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] ??
'',
},
...filters,
],
op: 'AND',
},

View File

@@ -6,6 +6,7 @@ import { ExpandButtonWrapper } from 'container/InfraMonitoringK8sV2/components';
import ColumnHeader from '../Base/ColumnHeader';
import EntityGroupHeader from '../Base/EntityGroupHeader';
import K8sGroupCell from '../Base/K8sGroupCell';
import { SelectedItemParams } from '../hooks';
import { formatBytes, getPodPhaseStatusItems } from '../commonUtils';
import {
CellValueTooltip,
@@ -31,10 +32,15 @@ export function getK8sStatefulSetRowKey(
export function getK8sStatefulSetItemKey(
statefulSet: InframonitoringtypesStatefulSetRecordDTO,
): string {
return (
statefulSet.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_STATEFULSET_NAME] || ''
);
): SelectedItemParams {
return {
selectedItem:
statefulSet.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_STATEFULSET_NAME] ?? null,
clusterName:
statefulSet.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_CLUSTER_NAME] ?? null,
namespaceName:
statefulSet.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] ?? null,
};
}
export const k8sStatefulSetsColumnsConfig: TableColumnDef<InframonitoringtypesStatefulSetRecordDTO>[] =
@@ -135,10 +141,10 @@ export const k8sStatefulSetsColumnsConfig: TableColumnDef<InframonitoringtypesSt
},
},
{
id: 'pod_status',
id: 'pod_replicas',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/statefulsets#pod-status">
Pod Status
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/statefulsets#pod-replicas">
Pod Replicas
</ColumnHeader>
),
accessorFn: (row): number => row.currentPods,

View File

@@ -11,6 +11,7 @@ import K8sBaseDetails, { K8sDetailsFilters } from '../Base/K8sBaseDetails';
import { K8sBaseList } from '../Base/K8sBaseList';
import { K8sBaseFilters } from '../Base/types';
import { InfraMonitoringEntity } from '../constants';
import { SelectedItemParams } from '../hooks';
import {
getVolumeMetricsQueryPayload,
k8sVolumeDetailsMetadataConfig,
@@ -117,7 +118,7 @@ function K8sVolumesList({
return (
<>
<K8sBaseList<InframonitoringtypesVolumeRecordDTO>
<K8sBaseList<InframonitoringtypesVolumeRecordDTO, SelectedItemParams>
controlListPrefix={controlListPrefix}
entity={InfraMonitoringEntity.VOLUMES}
tableColumns={k8sVolumesColumnsConfig}

View File

@@ -6,15 +6,22 @@ import { EQueryType } from 'types/common/dashboard';
import { DataSource, ReduceOperators } from 'types/common/queryBuilder';
import { v4 } from 'uuid';
import { formatValueForExpression } from 'components/QueryBuilderV2/utils';
import { K8sDetailsMetadataConfig } from '../Base/K8sBaseDetails';
import { INFRA_MONITORING_ATTR_KEYS } from '../constants';
import { SelectedItemParams } from '../hooks';
import {
buildEventsExpression,
buildExpressionFromSelectedItemParams,
buildLogsTracesExpression,
} from 'container/InfraMonitoringK8sV2/Base/utils';
export const k8sVolumeGetSelectedItemExpression = (
selectedItemId: string,
params: SelectedItemParams,
): string =>
`${INFRA_MONITORING_ATTR_KEYS.K8S_PERSISTENT_VOLUME_CLAIM_NAME} = ${formatValueForExpression(selectedItemId)}`;
buildExpressionFromSelectedItemParams(
params,
INFRA_MONITORING_ATTR_KEYS.K8S_PERSISTENT_VOLUME_CLAIM_NAME,
);
export const k8sVolumeDetailsMetadataConfig: K8sDetailsMetadataConfig<InframonitoringtypesVolumeRecordDTO>[] =
[
@@ -36,22 +43,23 @@ export const k8sVolumeDetailsMetadataConfig: K8sDetailsMetadataConfig<Inframonit
export const k8sVolumeInitialEventsExpression = (
item: InframonitoringtypesVolumeRecordDTO,
): string => {
const objectName = formatValueForExpression(
item.persistentVolumeClaimName || '',
);
return `${INFRA_MONITORING_ATTR_KEYS.K8S_OBJECT_KIND} = 'PersistentVolumeClaim' AND ${INFRA_MONITORING_ATTR_KEYS.K8S_OBJECT_NAME} = ${objectName}`;
};
): string =>
buildEventsExpression({
objectKind: 'PersistentVolumeClaim',
objectName: item.persistentVolumeClaimName || '',
clusterName: item.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_CLUSTER_NAME],
namespaceName: item.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME],
});
export const k8sVolumeInitialLogTracesExpression = (
item: InframonitoringtypesVolumeRecordDTO,
): string => {
const pvcName = formatValueForExpression(item.persistentVolumeClaimName || '');
const namespaceName = formatValueForExpression(
item.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] || '',
);
return `${INFRA_MONITORING_ATTR_KEYS.K8S_PERSISTENT_VOLUME_CLAIM_NAME} = ${pvcName} AND ${INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME} = ${namespaceName}`;
};
): string =>
buildLogsTracesExpression({
mainAttributeKey: INFRA_MONITORING_ATTR_KEYS.K8S_PERSISTENT_VOLUME_CLAIM_NAME,
mainAttributeValue: item.persistentVolumeClaimName,
clusterName: item.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_CLUSTER_NAME],
namespaceName: item.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME],
});
export const k8sVolumeGetEntityName = (
item: InframonitoringtypesVolumeRecordDTO,
@@ -61,22 +69,27 @@ export const volumeWidgetInfo = [
{
title: 'Volume available',
yAxisUnit: 'bytes',
docPath: '/infrastructure-monitoring/kubernetes/volumes/#volume-available',
},
{
title: 'Volume capacity',
yAxisUnit: 'bytes',
docPath: '/infrastructure-monitoring/kubernetes/volumes/#volume-capacity',
},
{
title: 'Volume inodes used',
yAxisUnit: 'short',
docPath: '/infrastructure-monitoring/kubernetes/volumes/#volume-inodes-used',
},
{
title: 'Volume inodes',
yAxisUnit: 'short',
docPath: '/infrastructure-monitoring/kubernetes/volumes/#volume-inodes',
},
{
title: 'Volume inodes free',
yAxisUnit: 'short',
docPath: '/infrastructure-monitoring/kubernetes/volumes/#volume-inodes-free',
},
];

View File

@@ -1,6 +1,5 @@
import { TableColumnDef } from 'components/TanStackTableView';
import TanStackTable, { TableColumnDef } from 'components/TanStackTableView';
import { InframonitoringtypesVolumeRecordDTO } from 'api/generated/services/sigNoz.schemas';
import TanStackTable from 'components/TanStackTableView';
import { ExpandButtonWrapper } from 'container/InfraMonitoringK8sV2/components';
import ColumnHeader from '../Base/ColumnHeader';
@@ -12,6 +11,7 @@ import {
INFRA_MONITORING_ATTR_KEYS,
InfraMonitoringEntity,
} from '../constants';
import { SelectedItemParams } from '../hooks';
import { HardDrive } from '@signozhq/icons';
export function getK8sVolumeRowKey(
@@ -26,8 +26,17 @@ export function getK8sVolumeRowKey(
export function getK8sVolumeItemKey(
volume: InframonitoringtypesVolumeRecordDTO,
): string {
return volume.persistentVolumeClaimName;
): SelectedItemParams {
return {
selectedItem:
volume.persistentVolumeClaimName ??
volume.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_PERSISTENT_VOLUME_CLAIM_NAME] ??
null,
clusterName:
volume.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_CLUSTER_NAME] ?? null,
namespaceName:
volume.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] ?? null,
};
}
export type VolumeTableColumnConfig =

View File

@@ -897,6 +897,8 @@ export const INFRA_MONITORING_K8S_PARAMS_KEYS = {
PAGE_SIZE: 'pageSize',
EXPANDED: 'expanded',
SELECTED_ITEM: 'selectedItem',
SELECTED_ITEM_CLUSTER_NAME: 'selectedItemClusterName',
SELECTED_ITEM_NAMESPACE_NAME: 'selectedItemNamespaceName',
};
/** Metric namespace prefixes for /fields/keys and /fields/values APIs */

View File

@@ -5,8 +5,10 @@ import {
parseAsJson,
parseAsString,
useQueryState,
useQueryStates,
UseQueryStateReturn,
} from 'nuqs';
import { useCallback, useMemo } from 'react';
import {
IBuilderQuery,
TagFilter,
@@ -128,16 +130,70 @@ export const useInfraMonitoringCategory = (): UseQueryStateReturn<
parseAsString.withDefault(K8sCategories.PODS).withOptions(defaultNuqsOptions),
);
export const useInfraMonitoringSelectedItem = (): UseQueryStateReturn<
string,
string | undefined
> => {
return useQueryState(
INFRA_MONITORING_K8S_PARAMS_KEYS.SELECTED_ITEM,
parseAsString,
);
export interface SelectedItemParams {
selectedItem: string | null;
clusterName?: string | null;
namespaceName?: string | null;
}
const selectedItemParamsParsers = {
[INFRA_MONITORING_K8S_PARAMS_KEYS.SELECTED_ITEM]: parseAsString,
[INFRA_MONITORING_K8S_PARAMS_KEYS.SELECTED_ITEM_CLUSTER_NAME]: parseAsString,
[INFRA_MONITORING_K8S_PARAMS_KEYS.SELECTED_ITEM_NAMESPACE_NAME]: parseAsString,
};
export type UseSelectedItemParamsReturn = [
SelectedItemParams,
(params: SelectedItemParams | null) => void,
];
export const useInfraMonitoringSelectedItemParams =
(): UseSelectedItemParamsReturn => {
const [rawParams, setRawParams] = useQueryStates(
selectedItemParamsParsers,
defaultNuqsOptions,
);
const params: SelectedItemParams = useMemo(
() => ({
selectedItem:
rawParams[INFRA_MONITORING_K8S_PARAMS_KEYS.SELECTED_ITEM] ?? null,
clusterName:
rawParams[INFRA_MONITORING_K8S_PARAMS_KEYS.SELECTED_ITEM_CLUSTER_NAME] ??
null,
namespaceName:
rawParams[INFRA_MONITORING_K8S_PARAMS_KEYS.SELECTED_ITEM_NAMESPACE_NAME] ??
null,
}),
[rawParams],
);
const setParams = useCallback(
(newParams: Partial<SelectedItemParams> | null): void => {
if (newParams === null) {
void setRawParams({
[INFRA_MONITORING_K8S_PARAMS_KEYS.SELECTED_ITEM]: null,
[INFRA_MONITORING_K8S_PARAMS_KEYS.SELECTED_ITEM_CLUSTER_NAME]: null,
[INFRA_MONITORING_K8S_PARAMS_KEYS.SELECTED_ITEM_NAMESPACE_NAME]: null,
});
return;
}
void setRawParams({
[INFRA_MONITORING_K8S_PARAMS_KEYS.SELECTED_ITEM]:
newParams.selectedItem ?? null,
[INFRA_MONITORING_K8S_PARAMS_KEYS.SELECTED_ITEM_CLUSTER_NAME]:
newParams.clusterName ?? null,
[INFRA_MONITORING_K8S_PARAMS_KEYS.SELECTED_ITEM_NAMESPACE_NAME]:
newParams.namespaceName ?? null,
});
},
[setRawParams],
);
return [params, setParams];
};
export const useInfraMonitoringStatusFilter = (): UseQueryStateReturn<
string,
string

View File

@@ -30,7 +30,7 @@ export function makeGroup(overrides: Partial<MapperGroup> = {}): MapperGroup {
export function makeMapper(overrides: Partial<Mapper> = {}): Mapper {
return {
id: 'mapper-1',
group_id: 'group-1',
groupId: 'group-1',
name: 'gen_ai.request.model',
enabled: true,
fieldContext: FieldContext.attribute,
@@ -89,5 +89,5 @@ export const mockGroups: MapperGroup[] = [
];
export const mockMappers: Mapper[] = [
makeMapper({ id: 'mapper-1', group_id: 'group-1' }),
makeMapper({ id: 'mapper-1', groupId: 'group-1' }),
];

View File

@@ -89,21 +89,20 @@ describe('UnpricedModelsTab (integration)', () => {
await screen.findByTestId(`unpriced-model-name-${MODEL}`);
await selectRule(user, MODEL, 'rule-openai');
const trigger = screen.getByTestId(`map-to-select-${MODEL}`);
expect(
within(trigger).getByText('openai:gpt-4o ($3.00/$9.00)'),
within(screen.getByTestId(`map-to-select-${MODEL}`)).getByText(
'openai:gpt-4o ($3.00/$9.00)',
),
).toBeInTheDocument();
await user.click(await screen.findByTestId('unpriced-map-cancel-btn'));
await waitFor(() =>
await waitFor(() => {
const trigger = screen.getByTestId(`map-to-select-${MODEL}`);
expect(
within(trigger).queryByText('openai:gpt-4o ($3.00/$9.00)'),
).not.toBeInTheDocument(),
);
expect(
within(trigger).getByText('Select / Create a pricing model'),
).toBeInTheDocument();
within(trigger).getByText('Select / Create a pricing model'),
).toBeInTheDocument();
});
});
it('commits the mapping in one request when confirmed', async () => {

View File

@@ -2872,17 +2872,72 @@ export const nodeWidgetInfo = [
];
export const hostWidgetInfo = [
{ title: 'CPU Usage', yAxisUnit: 'percentunit' },
{ title: 'Memory Usage', yAxisUnit: 'bytes' },
{ title: 'System Load Average', yAxisUnit: '' },
{ title: 'Network usage (bytes)', yAxisUnit: 'bytes' },
{ title: 'Network usage (packet/s)', yAxisUnit: 'pps' },
{ title: 'Network errors', yAxisUnit: 'short' },
{ title: 'Network drops', yAxisUnit: 'short' },
{ title: 'Network connections', yAxisUnit: 'short' },
{ title: 'System disk io (bytes transferred)', yAxisUnit: 'bytes' },
{ title: 'System disk operations/s', yAxisUnit: 'short' },
{ title: 'Queue size', yAxisUnit: 'short' },
{ title: 'System disk operation time/s', yAxisUnit: 's' },
{ title: 'Disk Usage (%) by mountpoint', yAxisUnit: 'percentunit' },
{
title: 'CPU Usage',
yAxisUnit: 'percentunit',
docPath: '/infrastructure-monitoring/host-monitoring/#cpu-usage',
},
{
title: 'Memory Usage',
yAxisUnit: 'bytes',
docPath: '/infrastructure-monitoring/host-monitoring/#memory-usage',
},
{
title: 'System Load Average',
yAxisUnit: '',
docPath: '/infrastructure-monitoring/host-monitoring/#system-load-average',
},
{
title: 'Network usage (bytes)',
yAxisUnit: 'bytes',
docPath: '/infrastructure-monitoring/host-monitoring/#network-usage-bytes',
},
{
title: 'Network usage (packet/s)',
yAxisUnit: 'pps',
docPath: '/infrastructure-monitoring/host-monitoring/#network-usage-packetss',
},
{
title: 'Network errors',
yAxisUnit: 'short',
docPath: '/infrastructure-monitoring/host-monitoring/#network-errors',
},
{
title: 'Network drops',
yAxisUnit: 'short',
docPath: '/infrastructure-monitoring/host-monitoring/#network-drops',
},
{
title: 'Network connections',
yAxisUnit: 'short',
docPath: '/infrastructure-monitoring/host-monitoring/#network-connections',
},
{
title: 'System disk io (bytes transferred)',
yAxisUnit: 'bytes',
docPath: '/infrastructure-monitoring/host-monitoring/#system-disk-io-bytes',
},
{
title: 'System disk operations/s',
yAxisUnit: 'short',
docPath:
'/infrastructure-monitoring/host-monitoring/#system-disk-operationss',
},
{
title: 'Queue size',
yAxisUnit: 'short',
docPath: '/infrastructure-monitoring/host-monitoring/#queue-size',
},
{
title: 'System disk operation time/s',
yAxisUnit: 's',
docPath:
'/infrastructure-monitoring/host-monitoring/#system-disk-operation-times',
},
{
title: 'Disk Usage (%) by mountpoint',
yAxisUnit: 'percentunit',
docPath:
'/infrastructure-monitoring/host-monitoring/#disk-usage--by-mountpoint',
},
];

View File

@@ -191,14 +191,6 @@
min-height: 0;
overflow-y: visible;
.time-series-view-container-header {
display: flex;
justify-content: flex-start;
align-items: center;
padding: 12px;
flex-shrink: 0;
}
.time-series-view {
flex-shrink: 0;
height: 65vh;

View File

@@ -32,7 +32,6 @@ import {
getListQuery,
getQueryByPanelType,
} from 'container/LogsExplorerViews/explorerUtils';
import { BuilderUnitsFilter } from 'container/QueryBuilder/filters/BuilderUnitsFilter';
import TimeSeriesView from 'container/TimeSeriesView/TimeSeriesView';
import { useCopyLogLink } from 'hooks/logs/useCopyLogLink';
import { useGetExplorerQueryRange } from 'hooks/queryBuilder/useGetExplorerQueryRange';
@@ -461,18 +460,17 @@ function LogsExplorerViewsContainer({
)}
{selectedPanelType === PANEL_TYPES.TIME_SERIES && !showLiveLogs && (
<div className="time-series-view-container">
<div className="time-series-view-container-header">
<BuilderUnitsFilter onChange={onUnitChange} yAxisUnit={yAxisUnit} />
</div>
<TimeSeriesView
isLoading={isLoading || isFetching}
data={data}
isError={isError}
error={error as APIError}
yAxisUnit={yAxisUnit}
onYAxisUnitChange={onUnitChange}
isFilterApplied={!isEmpty(listQuery?.filters?.items)}
dataSource={DataSource.LOGS}
setWarning={setWarning}
allowExport
/>
</div>
)}

View File

@@ -311,6 +311,7 @@ function TimeSeries({
dataSource={DataSource.METRICS}
error={queries[index].error as APIError}
setWarning={setWarning}
allowExport
/>
</div>
);

View File

@@ -3,6 +3,14 @@
min-height: 350px;
padding: 0px 12px;
&__header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 0;
flex-shrink: 0;
}
.ant-card-body {
height: 50vh;
min-height: 350px;

View File

@@ -16,6 +16,7 @@ import Uplot from 'components/Uplot';
import { QueryParams } from 'constants/query';
import { PANEL_TYPES } from 'constants/queryBuilder';
import EmptyLogsSearch from 'container/EmptyLogsSearch/EmptyLogsSearch';
import { BuilderUnitsFilter } from 'container/QueryBuilder/filters/BuilderUnitsFilter';
import { getLocalStorageGraphVisibilityState } from 'container/GridCardLayout/GridCard/utils';
import { LogsLoading } from 'container/LogsLoading/LogsLoading';
import EmptyMetricsSearch from 'container/MetricsExplorer/Explorer/EmptyMetricsSearch';
@@ -41,11 +42,14 @@ import { SuccessResponse, Warning } from 'types/api';
import { LegendPosition } from 'types/api/dashboard/getAll';
import APIError from 'types/api/error';
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
import { QueryRangeResponseV5 } from 'types/api/v5/queryRange';
import { DataSource } from 'types/common/queryBuilder';
import { GlobalReducer } from 'types/reducer/globalTime';
import uPlot from 'uplot';
import { getTimeRange } from 'utils/getTimeRange';
import TimeseriesExportMenu from './TimeseriesExportMenu';
import './TimeSeriesView.styles.scss';
function TimeSeriesView({
@@ -59,6 +63,8 @@ function TimeSeriesView({
setWarning,
panelType = PANEL_TYPES.TIME_SERIES,
stackBarChart = false,
allowExport = false,
onYAxisUnitChange,
}: TimeSeriesViewProps): JSX.Element {
const graphRef = useRef<HTMLDivElement>(null);
@@ -244,10 +250,33 @@ function TimeSeriesView({
[baseChartOptions, stackedBands],
);
const showExport = allowExport && !!data?.rawV5Response;
const showHeader = showExport || !!onYAxisUnitChange;
return (
<div className="time-series-view">
{isError && error && <ErrorInPlace error={error as APIError} />}
{showHeader && (
<div className="time-series-view__header">
<div>
{onYAxisUnitChange && (
<BuilderUnitsFilter onChange={onYAxisUnitChange} yAxisUnit={yAxisUnit} />
)}
</div>
{showExport && data?.rawV5Response && (
<TimeseriesExportMenu
dataSource={dataSource}
yAxisUnit={yAxisUnit}
queryResponse={data.rawV5Response}
query={currentQuery}
legendMap={data.legendMap}
fileName={`${dataSource}-timeseries`}
/>
)}
</div>
)}
<div
className="graph-container"
style={{ height: '100%', width: '100%' }}
@@ -295,7 +324,11 @@ function TimeSeriesView({
}
interface TimeSeriesViewProps {
data?: SuccessResponse<MetricRangePayloadProps> & { warning?: Warning };
data?: SuccessResponse<MetricRangePayloadProps> & {
warning?: Warning;
rawV5Response?: QueryRangeResponseV5;
legendMap?: Record<string, string>;
};
yAxisUnit?: string;
isLoading: boolean;
isError: boolean;
@@ -305,6 +338,11 @@ interface TimeSeriesViewProps {
setWarning?: Dispatch<SetStateAction<Warning | undefined>>;
panelType?: PANEL_TYPES;
stackBarChart?: boolean;
// Opt-in: render the client-side export menu (Logs explorer for now).
allowExport?: boolean;
// Opt-in: render the y-axis unit selector in the header (views without their
// own selector, e.g. Logs). Metrics keeps its separate YAxisUnitSelector.
onYAxisUnitChange?: (value: string) => void;
}
TimeSeriesView.defaultProps = {

View File

@@ -0,0 +1,33 @@
.timeseries-export-popover {
width: 240px;
padding: 0 12px 12px 12px;
.title {
display: flex;
color: var(--l1-foreground);
font-family: Inter;
font-size: var(--periscope-font-size-small);
font-style: normal;
font-weight: 500;
line-height: 18px;
letter-spacing: 0.88px;
text-transform: uppercase;
margin-bottom: 8px;
}
.export-format {
padding: 12px 4px;
display: flex;
flex-direction: column;
// radio option labels — radix popover content inherits the root font
// size; pin to the app's 13px base the antd popover used to impose
label {
font-size: var(--periscope-font-size-base);
}
}
.export-button {
width: 100%;
}
}

View File

@@ -0,0 +1,91 @@
import { Download } from '@signozhq/icons';
import { Button } from '@signozhq/ui/button';
import { Popover, PopoverContent, PopoverTrigger } from '@signozhq/ui/popover';
import { RadioGroup, RadioGroupItem } from '@signozhq/ui/radio-group';
import { TooltipSimple } from '@signozhq/ui/tooltip';
import { Typography } from '@signozhq/ui/typography';
import { useClientExport } from 'hooks/useExportData/useClientExport';
import { ExportFormat } from 'lib/exportData/types';
import { useCallback, useState } from 'react';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
import { QueryRangeResponseV5 } from 'types/api/v5/queryRange';
import { DataSource } from 'types/common/queryBuilder';
import './TimeseriesExportMenu.styles.scss';
interface TimeseriesExportMenuProps {
dataSource: DataSource;
queryResponse: QueryRangeResponseV5;
query?: Query;
yAxisUnit?: string;
legendMap?: Record<string, string>;
fileName?: string;
}
// Download menu for in-memory timeseries data (client-side serialization).
// The raw/list backend export keeps its own menu in DownloadOptionsMenu.
export default function TimeseriesExportMenu({
dataSource,
queryResponse,
query,
yAxisUnit,
legendMap,
fileName,
}: TimeseriesExportMenuProps): JSX.Element {
const [exportFormat, setExportFormat] = useState<string>(ExportFormat.Csv);
const [isPopoverOpen, setIsPopoverOpen] = useState<boolean>(false);
const { isExporting, handleExport: handleClientExport } = useClientExport({
response: queryResponse,
query,
yAxisUnit,
legendMap,
fileName,
});
const handleExport = useCallback((): void => {
setIsPopoverOpen(false);
handleClientExport({ format: exportFormat as ExportFormat });
}, [exportFormat, handleClientExport]);
return (
<Popover open={isPopoverOpen} onOpenChange={setIsPopoverOpen}>
<TooltipSimple title="Download">
<PopoverTrigger asChild>
<Button
variant="ghost"
color="secondary"
size="icon"
aria-label="Download"
data-testid={`timeseries-export-${dataSource}`}
disabled={isExporting}
loading={isExporting}
>
<Download size={14} />
</Button>
</PopoverTrigger>
</TooltipSimple>
<PopoverContent align="end" className="timeseries-export-popover">
<div className="export-format">
<Typography.Text className="title">FORMAT</Typography.Text>
<RadioGroup value={exportFormat} onChange={setExportFormat}>
<RadioGroupItem value={ExportFormat.Csv}>csv</RadioGroupItem>
<RadioGroupItem value={ExportFormat.Jsonl}>jsonl</RadioGroupItem>
</RadioGroup>
</div>
<Button
variant="solid"
color="primary"
className="export-button"
onClick={handleExport}
disabled={isExporting}
loading={isExporting}
prefix={<Download size={16} />}
>
Export
</Button>
</PopoverContent>
</Popover>
);
}

View File

@@ -0,0 +1,138 @@
import { render } from '@testing-library/react';
import { MemoryRouter } from 'react-router-dom';
// eslint-disable-next-line no-restricted-imports
import { Provider } from 'react-redux';
import configureStore from 'redux-mock-store';
import store from 'store';
import { DataSource } from 'types/common/queryBuilder';
import TimeSeriesView from '../TimeSeriesView';
jest.mock('components/Uplot', () => ({
__esModule: true,
default: (): JSX.Element => <div data-testid="uplot-chart" />,
}));
jest.mock('../TimeseriesExportMenu', () => ({
__esModule: true,
default: (): JSX.Element => <div data-testid="timeseries-export-menu" />,
}));
jest.mock('container/QueryBuilder/filters/BuilderUnitsFilter', () => ({
BuilderUnitsFilter: (): JSX.Element => (
<div data-testid="builder-units-filter" />
),
}));
jest.mock('hooks/queryBuilder/useQueryBuilder', () => ({
useQueryBuilder: (): unknown => ({ currentQuery: null }),
}));
jest.mock('lib/uPlotLib/getUplotChartOptions', () => ({
getUPlotChartOptions: (): unknown => ({}),
}));
jest.mock('lib/uPlotLib/utils/getUplotChartData', () => ({
getUPlotChartData: (): number[][] => [
[1, 2],
[3, 4],
],
}));
jest.mock(
'container/DashboardContainer/visualization/charts/utils/stackSeriesUtils',
() => ({ stackSeries: (): unknown => ({ data: [], bands: [] }) }),
);
jest.mock('container/GridCardLayout/GridCard/utils', () => ({
getLocalStorageGraphVisibilityState: (): unknown => ({
graphVisibilityStates: [],
}),
}));
jest.mock('providers/Timezone', () => ({
useTimezone: (): unknown => ({ timezone: { value: 'UTC' } }),
}));
jest.mock('hooks/useDimensions', () => ({
useResizeObserver: (): unknown => ({ width: 800, height: 400 }),
}));
jest.mock('api/common/logEvent', () => ({
__esModule: true,
default: jest.fn(),
}));
const mockStore = configureStore([])({ ...store.getState() });
const rawV5Response = {
type: 'time_series',
data: { results: [] },
meta: {},
};
function makeData(withRawV5: boolean): any {
return {
statusCode: 200,
error: null,
message: '',
payload: { data: { result: [], resultType: '' } },
...(withRawV5 ? { rawV5Response, legendMap: {} } : {}),
};
}
function renderView(props: {
allowExport?: boolean;
withRawV5?: boolean;
onYAxisUnitChange?: (value: string) => void;
}): ReturnType<typeof render> {
const { allowExport, withRawV5 = true, onYAxisUnitChange } = props;
return render(
<Provider store={mockStore}>
<MemoryRouter>
<TimeSeriesView
isLoading={false}
isError={false}
isFilterApplied
dataSource={DataSource.LOGS}
data={makeData(withRawV5)}
allowExport={allowExport}
onYAxisUnitChange={onYAxisUnitChange}
/>
</MemoryRouter>
</Provider>,
);
}
describe('TimeSeriesView header gating', () => {
it('renders the export menu when allowExport is set and raw V5 data is present', () => {
const { queryByTestId } = renderView({ allowExport: true });
expect(queryByTestId('timeseries-export-menu')).toBeInTheDocument();
});
it('renders no export menu without allowExport', () => {
const { queryByTestId } = renderView({});
expect(queryByTestId('timeseries-export-menu')).not.toBeInTheDocument();
});
it('renders no export menu when the raw V5 response is missing', () => {
const { queryByTestId } = renderView({ allowExport: true, withRawV5: false });
expect(queryByTestId('timeseries-export-menu')).not.toBeInTheDocument();
});
it('renders the unit selector only when onYAxisUnitChange is passed', () => {
const withUnit = renderView({ onYAxisUnitChange: jest.fn() });
expect(withUnit.queryByTestId('builder-units-filter')).toBeInTheDocument();
withUnit.unmount();
const withoutUnit = renderView({ allowExport: true });
expect(
withoutUnit.queryByTestId('builder-units-filter'),
).not.toBeInTheDocument();
});
it('renders no header row when neither export nor unit selector is enabled', () => {
const { container } = renderView({ withRawV5: false });
expect(container.querySelector('.time-series-view__header')).toBeNull();
});
});

View File

@@ -0,0 +1,84 @@
import { fireEvent, render, screen } from 'tests/test-utils';
import { QueryRangeResponseV5 } from 'types/api/v5/queryRange';
import { DataSource } from 'types/common/queryBuilder';
import TimeseriesExportMenu from '../TimeseriesExportMenu';
const mockHandleExport = jest.fn();
let mockIsExporting = false;
jest.mock('hooks/useExportData/useClientExport', () => ({
useClientExport: (): unknown => ({
isExporting: mockIsExporting,
handleExport: mockHandleExport,
}),
}));
const response = {
type: 'time_series',
data: { results: [] },
meta: {},
} as unknown as QueryRangeResponseV5;
const TEST_ID = `timeseries-export-${DataSource.LOGS}`;
function renderMenu(): void {
render(
<TimeseriesExportMenu
dataSource={DataSource.LOGS}
queryResponse={response}
fileName="logs-timeseries"
/>,
);
}
describe('TimeseriesExportMenu', () => {
beforeEach(() => {
mockHandleExport.mockReset();
mockIsExporting = false;
});
it('renders the download trigger button', () => {
renderMenu();
expect(screen.getByTestId(TEST_ID)).toBeInTheDocument();
});
it('shows only format options — no shape, row-count, or column controls', () => {
renderMenu();
fireEvent.click(screen.getByTestId(TEST_ID));
expect(screen.getByText('FORMAT')).toBeInTheDocument();
expect(screen.getByRole('radio', { name: 'csv' })).toBeInTheDocument();
expect(screen.getByRole('radio', { name: 'jsonl' })).toBeInTheDocument();
expect(screen.queryByText('Number of Rows')).not.toBeInTheDocument();
expect(screen.queryByText('Columns')).not.toBeInTheDocument();
expect(screen.queryByRole('radio', { name: 'long' })).not.toBeInTheDocument();
expect(screen.queryByRole('radio', { name: 'wide' })).not.toBeInTheDocument();
});
it('exports as csv by default', () => {
renderMenu();
fireEvent.click(screen.getByTestId(TEST_ID));
fireEvent.click(screen.getByText('Export'));
expect(mockHandleExport).toHaveBeenCalledTimes(1);
expect(mockHandleExport).toHaveBeenCalledWith({ format: 'csv' });
});
it('exports as jsonl when selected', () => {
renderMenu();
fireEvent.click(screen.getByTestId(TEST_ID));
fireEvent.click(screen.getByRole('radio', { name: 'jsonl' }));
fireEvent.click(screen.getByText('Export'));
expect(mockHandleExport).toHaveBeenCalledWith({ format: 'jsonl' });
});
it('disables the trigger while an export is in progress', () => {
mockIsExporting = true;
renderMenu();
expect(screen.getByTestId(TEST_ID)).toBeDisabled();
});
});

View File

@@ -1,10 +1,41 @@
import { SuccessResponse } from 'types/api/index';
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
import { QueryRangeResponseV5, TimeSeriesData } from 'types/api/v5/queryRange';
import { QueryData } from 'types/api/widgets/getQuery';
type ConvertibleData = SuccessResponse<MetricRangePayloadProps> & {
rawV5Response?: QueryRangeResponseV5;
};
// Applies the same ns→ms conversion to the raw V5 tree, so client-side export
// serializes the values the chart displays (not the original nanoseconds).
function convertRawV5ValuesToMs(
response: QueryRangeResponseV5,
): QueryRangeResponseV5 {
if (response.type !== 'time_series') {
return response;
}
const results = (response.data.results as TimeSeriesData[]).map((result) => ({
...result,
aggregations: (result.aggregations ?? []).map((bucket) => ({
...bucket,
series: (bucket.series ?? []).map((series) => ({
...series,
values: (series.values ?? []).map((value) => ({
...value,
value: value.value / 1000000,
})),
})),
})),
}));
return { ...response, data: { ...response.data, results } };
}
export const convertDataValueToMs = (
data?: SuccessResponse<MetricRangePayloadProps>,
): SuccessResponse<MetricRangePayloadProps> | undefined => {
data?: ConvertibleData,
): ConvertibleData | undefined => {
const convertedData = data;
const convertedResult: QueryData[] = data?.payload?.data?.result
@@ -22,5 +53,11 @@ export const convertDataValueToMs = (
convertedData.payload.data.result = convertedResult;
}
if (convertedData?.rawV5Response) {
convertedData.rawV5Response = convertRawV5ValuesToMs(
convertedData.rawV5Response,
);
}
return convertedData;
};

View File

@@ -11,6 +11,7 @@ import {
import { useSelector } from 'react-redux';
import { Typography } from '@signozhq/ui/typography';
import logEvent from 'api/common/logEvent';
import DownloadOptionsMenu from 'components/DownloadOptionsMenu/DownloadOptionsMenu';
import ErrorInPlace from 'components/ErrorInPlace/ErrorInPlace';
import { ResizeTable } from 'components/ResizeTable';
import { ENTITY_VERSION_V5 } from 'constants/app';
@@ -152,6 +153,11 @@ function TracesView({
</Typography>
<div className="trace-explorer-controls">
<DownloadOptionsMenu
dataSource={DataSource.TRACES}
panelType={PANEL_TYPES.TRACE}
/>
<TraceExplorerControls
isLoading={isLoading}
totalCount={responseData?.length || 0}

View File

@@ -19,6 +19,7 @@ interface ExportOptions {
interface UseExportRawDataProps {
dataSource: DataSource;
panelType?: PANEL_TYPES;
}
interface UseExportRawDataReturn {
@@ -28,6 +29,7 @@ interface UseExportRawDataReturn {
export function useExportRawData({
dataSource,
panelType = PANEL_TYPES.LIST,
}: UseExportRawDataProps): UseExportRawDataReturn {
const [isDownloading, setIsDownloading] = useState<boolean>(false);
@@ -83,7 +85,7 @@ export function useExportRawData({
const { queryPayload } = prepareQueryRangePayloadV5({
query: exportQuery,
graphType: PANEL_TYPES.LIST,
graphType: panelType,
selectedTime: 'GLOBAL_TIME',
globalSelectedInterval,
});
@@ -96,7 +98,7 @@ export function useExportRawData({
setIsDownloading(false);
}
},
[stagedQuery, globalSelectedInterval, dataSource],
[stagedQuery, globalSelectedInterval, dataSource, panelType],
);
return { isDownloading, handleExportRawData };

View File

@@ -4,6 +4,8 @@ export function useIntersectionObserver<T extends HTMLElement>(
ref: RefObject<T>,
options?: IntersectionObserverInit,
isObserverOnce?: boolean,
/** Defer observation by this many ms to let a transient mount layout settle. */
startDelayMs = 0,
): boolean {
const [isIntersecting, setIntersecting] = useState(false);
@@ -23,16 +25,28 @@ export function useIntersectionObserver<T extends HTMLElement>(
}
}, options);
if (currentReference) {
observer.observe(currentReference);
const startObserving = (): void => {
if (currentReference) {
observer.observe(currentReference);
}
};
let timer: ReturnType<typeof setTimeout> | undefined;
if (startDelayMs > 0) {
timer = setTimeout(startObserving, startDelayMs);
} else {
startObserving();
}
return (): void => {
if (timer) {
clearTimeout(timer);
}
if (currentReference) {
observer.unobserve(currentReference);
}
};
}, [ref, options, isObserverOnce]);
}, [ref, options, isObserverOnce, startDelayMs]);
return isIntersecting;
}

View File

@@ -22,7 +22,11 @@ import { SuccessResponseV2, Warning } from 'types/api';
import { IDashboardVariable } from 'types/api/dashboard/getAll';
import { MetricQueryRangeSuccessResponse } from 'types/api/metrics/getQueryRange';
import { IBuilderQuery, Query } from 'types/api/queryBuilder/queryBuilderData';
import { ExecStats, MetricRangePayloadV5 } from 'types/api/v5/queryRange';
import {
ExecStats,
MetricRangePayloadV5,
QueryRangeResponseV5,
} from 'types/api/v5/queryRange';
import { QueryData } from 'types/api/widgets/getQuery';
import { EQueryType } from 'types/common/dashboard';
import { DataSource } from 'types/common/queryBuilder';
@@ -192,6 +196,8 @@ export async function GetMetricQueryRange(
| SuccessResponseV2<MetricRangePayloadV5>;
let warning: Warning | undefined;
let meta: ExecStats | undefined;
// Raw V5 response, kept before it's converted to legacy — powers client-side export.
let rawV5Response: QueryRangeResponseV5 | undefined;
const panelType = props.originalGraphType || props.graphType;
@@ -268,6 +274,8 @@ export async function GetMetricQueryRange(
endTime: props.end * 1000,
});
rawV5Response = publicResponse.data.data;
// Convert V5 response to legacy format for components
response = convertV5ResponseToLegacy(
{
@@ -288,6 +296,8 @@ export async function GetMetricQueryRange(
headers,
);
rawV5Response = v5Response.data.data;
// Convert V5 response to legacy format for components
response = convertV5ResponseToLegacy(
{
@@ -366,6 +376,8 @@ export async function GetMetricQueryRange(
...response,
warning,
meta,
rawV5Response,
legendMap,
};
}

View File

@@ -0,0 +1,94 @@
import { SuccessResponse } from 'types/api';
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
import { exportScalarData } from '../exportScalarData';
const query = {
queryType: 'builder',
builder: {
queryData: [
{
queryName: 'A',
dataSource: 'logs',
aggregations: [{ expression: 'count()' }],
groupBy: [
{ key: 'service.name', dataType: 'string', type: 'tag', id: 'svc' },
],
legend: '',
},
],
queryFormulas: [],
},
} as unknown as Query;
function makeResponse(
tables: {
queryName: string;
columns: { name: string; id?: string; isValueColumn: boolean }[];
rows: Record<string, string | number>[];
}[],
): SuccessResponse<MetricRangePayloadProps> {
return {
statusCode: 200,
error: null,
message: '',
payload: {
data: {
resultType: 'scalar',
result: tables.map((table) => ({
queryName: table.queryName,
legend: '',
series: null,
list: null,
table: {
columns: table.columns.map((col) => ({
...col,
queryName: table.queryName,
})),
rows: table.rows.map((row) => ({ data: row })),
},
})),
},
},
} as unknown as SuccessResponse<MetricRangePayloadProps>;
}
describe('exportScalarData', () => {
it('serializes the table exactly as QueryTable prepares it', () => {
const data = makeResponse([
{
queryName: 'A',
columns: [
{ name: 'service.name', id: 'service.name', isValueColumn: false },
{ name: 'count()', id: 'A', isValueColumn: true },
],
rows: [
{ 'service.name': 'frontend', A: 120 },
{ 'service.name': 'cart', A: 80 },
],
},
]);
const table = exportScalarData({ data, query });
// group + aggregation columns, raw values, on-screen order — inherited
// 1:1 from createTableColumnsFromQuery (the renderer's own preparer)
expect(table).toStrictEqual({
headers: ['service.name', 'count()'],
rows: [
['frontend', 120],
['cart', 80],
],
});
});
it('returns an empty table for an empty response', () => {
const table = exportScalarData({
data: makeResponse([]),
query,
});
expect(table.rows).toStrictEqual([]);
});
});

View File

@@ -0,0 +1,58 @@
import { exportTableData } from '../exportTableData';
const columns = [
{ name: 'service.name', key: 'service.name' },
{ name: 'count()', key: 'A', isValueColumn: true },
{ name: 'avg(duration)', key: 'B', isValueColumn: true },
];
describe('exportTableData', () => {
it('serializes raw values in display column order', () => {
const table = exportTableData({
columns,
dataSource: [
{ 'service.name': 'frontend', A: 120, B: 45.5 },
{ 'service.name': 'cart', A: 80, B: 12 },
],
});
expect(table).toStrictEqual({
headers: ['service.name', 'count()', 'avg(duration)'],
rows: [
['frontend', 120, 45.5],
['cart', 80, 12],
],
});
});
it('appends column units to value columns only, skipping display-only ids', () => {
const table = exportTableData({
columns,
dataSource: [{ 'service.name': 'frontend', A: 120, B: 45.5 }],
columnUnits: { A: 'short', B: 'ms', 'service.name': 'ms' },
});
// group column never gets a unit; 'short' is display-only and skipped
expect(table.headers).toStrictEqual([
'service.name',
'count()',
'avg(duration) (ms)',
]);
});
it('marks missing cells as blank gaps', () => {
const table = exportTableData({
columns,
dataSource: [{ 'service.name': 'frontend', A: 120 }],
});
expect(table.rows).toStrictEqual([['frontend', 120, '']]);
});
it('returns a headers-only table for empty data', () => {
expect(exportTableData({ columns, dataSource: [] })).toStrictEqual({
headers: ['service.name', 'count()', 'avg(duration)'],
rows: [],
});
});
});

View File

@@ -81,6 +81,18 @@ describe('exportTimeseriesData', () => {
]);
});
it('omits display-only format ids (short/none) from headers', () => {
const data = [
makeQuery('A', [{ series: [makeSeries({ service: 'a' }, [[1000, 1]])] }]),
];
const short = exportTimeseriesData({ data, yAxisUnit: 'short' });
expect(short.headers[short.headers.length - 1]).toBe('value');
const none = exportTimeseriesData({ data, yAxisUnit: 'none' });
expect(none.headers[none.headers.length - 1]).toBe('value');
});
it('multi-query: query is its own column; label keys are unioned', () => {
const data = [
makeQuery('A', [{ series: [makeSeries({ service: 'x' }, [[1000, 1]])] }]),

View File

@@ -0,0 +1,48 @@
import { createTableColumnsFromQuery } from 'lib/query/createTableColumnsFromQuery';
import { SuccessResponse } from 'types/api';
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
import { QueryDataV3 } from 'types/api/widgets/getQuery';
import { exportTableData } from './exportTableData';
import { SerializedTable } from './types';
interface ExportScalarDataArgs {
// The queryRange response object the table mount already holds (the
// formatForWeb payload carrying webTables).
data?: SuccessResponse<MetricRangePayloadProps>;
query: Query;
}
/**
* Serializes a scalar/table queryRange response into a table — via
* createTableColumnsFromQuery, the exact preparer QueryTable renders from, so
* the export inherits the on-screen merge, naming and column order 1:1.
*/
export function exportScalarData({
data,
query,
}: ExportScalarDataArgs): SerializedTable {
const queryTableData = (data?.payload?.data?.newResult?.data?.result ||
data?.payload?.data?.result ||
[]) as QueryDataV3[];
const { columns, dataSource } = createTableColumnsFromQuery({
query,
queryTableData,
});
return exportTableData({
// antd widens title/dataIndex; createTableColumnsFromQuery always sets strings
columns: columns.map((column) => {
const rawIndex = 'dataIndex' in column ? column.dataIndex : undefined;
const key =
typeof rawIndex === 'string' || typeof rawIndex === 'number'
? String(rawIndex)
: '';
const name = typeof column.title === 'string' ? column.title : key;
return { name, key: key || name };
}),
dataSource: dataSource as unknown as Record<string, unknown>[],
});
}

View File

@@ -0,0 +1,46 @@
import { SerializedTable } from './types';
import { withUnit } from './withUnit';
/** Generic table-model column — any prepared table (QueryTable, dashboard
* tables, plain antd tables) adapts to this in a line or two. */
export interface ExportTableColumn {
/** Display name, used as the export header (column order = array order). */
name: string;
/** Key into each dataSource record. */
key: string;
isValueColumn?: boolean;
}
interface ExportTableDataArgs {
columns: ExportTableColumn[];
dataSource: Record<string, unknown>[];
/** Per-column display unit, keyed by column key (dashboards; absent in explorer). */
columnUnits?: Record<string, string>;
}
/**
* Serializes a prepared table model into a format-agnostic table — raw values
* in display column order (lossless; no cell formatting applied).
*/
export function exportTableData({
columns,
dataSource,
columnUnits,
}: ExportTableDataArgs): SerializedTable {
const headers = columns.map((column) =>
column.isValueColumn
? withUnit(column.name, columnUnits?.[column.key])
: column.name,
);
const rows = dataSource.map((record) =>
columns.map((column) => {
const value = record[column.key];
return value === undefined || value === null
? ''
: (value as string | number);
}),
);
return { headers, rows };
}

View File

@@ -5,6 +5,7 @@ import { TimeSeries, TimeSeriesData } from 'types/api/v5/queryRange';
import { QueryData } from 'types/api/widgets/getQuery';
import { SerializedTable } from './types';
import { withUnit } from './withUnit';
interface ExportTimeseriesDataArgs {
data: TimeSeriesData[];
@@ -98,11 +99,6 @@ function flatten(
return flat;
}
// Appends the y-axis unit to the value header: `value` → `value (ms)`.
function withUnit(header: string, yAxisUnit?: string): string {
return yAxisUnit ? `${header} (${yAxisUnit})` : header;
}
function toIso(timestamp: number): string {
return new Date(timestamp).toISOString();
}

View File

@@ -0,0 +1,11 @@
// Display-format ids, not physical units — meaningful on a chart axis
// (compact-number formatting) but misleading in an export header.
const DISPLAY_ONLY_UNITS = new Set(['short', 'none']);
/** Appends a unit to a header: `value` → `value (ms)`. Skips display-only ids. */
export function withUnit(header: string, unit?: string): string {
if (!unit || DISPLAY_ONLY_UNITS.has(unit)) {
return header;
}
return `${header} (${unit})`;
}

View File

@@ -92,6 +92,7 @@ function Panel({
panelId={panelId}
data={data}
isFetching={isFetching}
isVisible={isVisible}
isPreviousData={isPreviousData}
error={error}
refetch={refetch}

View File

@@ -23,6 +23,8 @@ interface PanelBodyProps {
panelId: string;
data: PanelQueryData;
isFetching: boolean;
/** Panel not yet scrolled into view — its fetch is deferred, so show the loader rather than NoData. */
isVisible?: boolean;
/** Showing a prior page's data while the next loads; forwarded so list renderers can show skeletons. */
isPreviousData?: boolean;
error: Error | null;
@@ -54,6 +56,7 @@ function PanelBody({
panelId,
data,
isFetching,
isVisible,
isPreviousData,
error,
refetch,
@@ -105,9 +108,9 @@ function PanelBody({
);
}
// Full-panel loader only on first fetch; a refetch over existing data keeps the renderer
// mounted (e.g. list page change). A refetch over empty data loads via NoData.
if (isFetching && !hasData) {
// Full-panel loader on first fetch or while the fetch is deferred (off-screen); a refetch
// over existing data keeps the renderer mounted, empty data loads via NoData.
if ((isFetching || isVisible === false) && !hasData) {
return <PanelLoader />;
}

View File

@@ -79,6 +79,21 @@ describe('PanelBody', () => {
expect(screen.queryByTestId('mock-renderer')).not.toBeInTheDocument();
});
it('shows the loader while the fetch is deferred (panel not yet scrolled into view)', () => {
render(
<PanelBody
{...baseProps}
panel={runnablePanel()}
data={{} as PanelQueryData}
isFetching={false}
isVisible={false}
/>,
);
expect(screen.getByTestId('panel-loading')).toBeInTheDocument();
expect(screen.queryByTestId('mock-renderer')).not.toBeInTheDocument();
});
it('keeps the renderer mounted during a refetch over existing data (e.g. list page change)', () => {
render(
<PanelBody

View File

@@ -113,8 +113,8 @@ function ViewPanelModalHeader({
/>
<Button
size="icon"
variant="solid"
color="primary"
variant="outlined"
color="secondary"
onClick={onRefresh}
disabled={isFetching}
aria-label="Refresh"

View File

@@ -22,6 +22,7 @@ function SectionGrid({
sections,
}: SectionGridProps): JSX.Element {
const isEditable = useDashboardStore((s) => s.isEditable);
const rglLayout = useMemo<Layout[]>(
() =>
items.map((item) => ({

View File

@@ -10,6 +10,10 @@ const VIEWPORT_OBSERVER_OPTIONS: IntersectionObserverInit = {
rootMargin: '200px',
};
// Start observing after RGL's mount unfold settles, so a panel that only
// transiently overlaps the viewport during layout doesn't fire a throwaway fetch.
const OBSERVER_START_DELAY_MS = 350;
interface SectionGridItemProps {
panel: DashboardtypesPanelDTO;
panelId: string;
@@ -17,9 +21,9 @@ interface SectionGridItemProps {
}
/**
* Lazy-loads a single panel: watches its own viewport intersection (latched) and
* passes it to the presentational Panel as `isVisible`, so a board of many panels
* only fetches what's on screen.
* Lazy-loads a single panel: tracks its live viewport intersection and passes it to
* the presentational Panel as `isVisible`, so a board of many panels only fetches
* (and refetches on time change / auto-refresh) what's on screen.
*/
function SectionGridItem({
panel,
@@ -30,7 +34,10 @@ function SectionGridItem({
const isVisible = useIntersectionObserver(
containerRef,
VIEWPORT_OBSERVER_OPTIONS,
true,
// Not once: track the live viewport so a time change / auto-refresh only
// refetches on-screen panels (off-screen ones stay query-disabled).
false,
OBSERVER_START_DELAY_MS,
);
useScrollIntoView(panelId, containerRef);

View File

@@ -23,7 +23,7 @@ describe('useScrollIntoView', () => {
expect(scrollIntoView).toHaveBeenCalledWith({
behavior: 'smooth',
block: 'start',
block: 'center',
});
expect(useScrollIntoViewStore.getState().scrollTargetId).toBeNull();
});
@@ -38,7 +38,7 @@ describe('useScrollIntoView', () => {
expect(scrollIntoView).toHaveBeenCalledWith({
behavior: 'smooth',
block: 'start',
block: 'center',
});
expect(useScrollIntoViewStore.getState().scrollTargetId).toBeNull();
});

View File

@@ -9,7 +9,7 @@ import { useScrollIntoViewStore } from '../../../store/useScrollIntoViewStore';
export function useScrollIntoView(
id: string,
ref: RefObject<HTMLElement>,
block: ScrollLogicalPosition = 'start',
block: ScrollLogicalPosition = 'center',
): void {
const scrollTargetId = useScrollIntoViewStore((s) => s.scrollTargetId);
const setScrollTargetId = useScrollIntoViewStore((s) => s.setScrollTargetId);

View File

@@ -57,5 +57,8 @@ export function useGetQueryRangeV5({
retry: retryUnlessClientError,
keepPreviousData,
cacheTime,
// A resolved window is immutable per key, so a panel scrolled back into view
// serves cache instead of refetching; a key change or manual refetch still runs.
staleTime: Infinity,
});
}

View File

@@ -10,7 +10,6 @@ import { useSelector } from 'react-redux';
import { ENTITY_VERSION_V5 } from 'constants/app';
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import { BuilderUnitsFilter } from 'container/QueryBuilder/filters';
import TimeSeriesView from 'container/TimeSeriesView/TimeSeriesView';
import { convertDataValueToMs } from 'container/TimeSeriesView/utils';
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
@@ -116,9 +115,6 @@ function TimeSeriesViewContainer({
return (
<div className="trace-explorer-time-series-view-container">
<div className="trace-explorer-time-series-view-container-header">
<BuilderUnitsFilter onChange={onUnitChange} yAxisUnit={yAxisUnit} />
</div>
<TimeSeriesView
isFilterApplied={isFilterApplied}
isError={isError}
@@ -126,8 +122,10 @@ function TimeSeriesViewContainer({
isLoading={isLoading || isFetching}
data={responseData}
yAxisUnit={yAxisUnit}
onYAxisUnitChange={onUnitChange}
dataSource={dataSource}
setWarning={setWarning}
allowExport
/>
</div>
);

View File

@@ -107,6 +107,9 @@ export function ContextMenu({
}
}}
trigger="click"
// Anchor to body (like the backdrop), not the host container: a modal's
// transformed dialog would break `position: fixed` and trap the menu below it.
getPopupContainer={(): HTMLElement => document.body}
overlayStyle={{
position: 'fixed',
left: position.left,

View File

@@ -77,6 +77,11 @@
color: var(--muted-foreground);
}
// Body-portaled overlay: stay clickable when a modal sets `body { pointer-events: none }`.
.context-menu {
pointer-events: auto;
}
.context-menu .ant-popover-inner {
padding: 0;
border-radius: 6px;

View File

@@ -8,7 +8,11 @@ import {
IClickHouseQuery,
IPromQLQuery,
} from '../queryBuilder/queryBuilderData';
import { ExecStats, QueryRangeRequestV5 } from '../v5/queryRange';
import {
ExecStats,
QueryRangeRequestV5,
QueryRangeResponseV5,
} from '../v5/queryRange';
import { QueryData, QueryDataV3 } from '../widgets/getQuery';
export type QueryRangePayload = {
@@ -48,6 +52,9 @@ export interface MetricQueryRangeSuccessResponse extends SuccessResponse<
> {
warning?: Warning;
meta?: ExecStats;
// Raw V5 response (pre-legacy-conversion) + per-query legend map, for client-side export.
rawV5Response?: QueryRangeResponseV5;
legendMap?: Record<string, string>;
}
export interface MetricRangePayloadV3 {

2
go.mod
View File

@@ -83,7 +83,7 @@ require (
go.uber.org/zap v1.27.1
golang.org/x/crypto v0.52.0
golang.org/x/exp v0.0.0-20260312153236-7ab1446f8b90
golang.org/x/net v0.54.0
golang.org/x/net v0.55.0
golang.org/x/oauth2 v0.36.0
golang.org/x/sync v0.20.0
golang.org/x/text v0.37.0

4
go.sum
View File

@@ -1488,8 +1488,8 @@ golang.org/x/net v0.0.0-20220325170049-de3da57026de/go.mod h1:CfG3xpIq0wQ8r1q4Su
golang.org/x/net v0.0.0-20220412020605-290c469a71a5/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20220520000938-2e3eb7b945c2/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.54.0 h1:2zJIZAxAHV/OHCDTCOHAYehQzLfSXuf/5SoL/Dv6w/w=
golang.org/x/net v0.54.0/go.mod h1:Sj4oj8jK6XmHpBZU/zWHw3BV3abl4Kvi+Ut7cQcY+cQ=
golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8=
golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=

View File

@@ -98,7 +98,7 @@ func (provider *provider) addSpanMapperRoutes(router *mux.Router) error {
Description: "Returns all mappers belonging to a mapping group.",
Request: nil,
RequestContentType: "",
Response: new(spantypes.GettableSpanMapperGroups),
Response: new(spantypes.GettableSpanMappers),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24px" height="24px" viewBox="0 0 24 24"><defs><style>.cls-1,.cls-4{fill:#669df6;}.cls-2{fill:#4285f4;}.cls-2,.cls-3,.cls-4{fill-rule:evenodd;}.cls-3{fill:#aecbfa;}</style></defs><title>Icon_24px_MemoryStore_Color</title><g data-name="Product Icons"><g ><rect class="cls-1" x="2" y="3.94" width="3.33" height="2.58"/><rect class="cls-1" x="2" y="8.45" width="3.33" height="2.58"/><rect class="cls-1" x="2" y="12.97" width="3.33" height="2.58"/><rect class="cls-1" x="2" y="17.48" width="3.33" height="2.58"/><rect class="cls-1" x="18.67" y="3.94" width="3.33" height="2.58"/><rect class="cls-1" x="18.67" y="8.45" width="3.33" height="2.58"/><rect class="cls-1" x="18.67" y="12.97" width="3.33" height="2.58"/><rect class="cls-1" x="18.67" y="17.48" width="3.33" height="2.58"/><polygon class="cls-2" points="21.33 6.52 18.67 6.52 18.67 3.94 21.33 6.52"/><polygon class="cls-2" points="21.33 11.03 18.67 11.03 18.67 8.45 21.33 11.03"/><polygon class="cls-2" points="21.33 15.55 18.67 15.55 18.67 12.97 21.33 15.55"/><polygon class="cls-2" points="21.33 20.07 18.67 20.07 18.67 17.48 21.33 20.07"/><path class="cls-3" d="M5.33,22H18.67V2H5.33Zm6-9H8l4.67-7.74V11H16l-4.67,7.74Z"/><polygon class="cls-4" points="11.33 22 11.33 18.77 16 11.03 12.67 11.03 12.67 2 18.67 2 18.67 22 11.33 22"/></g></g></svg>

Before

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -1,94 +0,0 @@
{
"id": "memorystore_redis",
"title": "GCP Memorystore Redis",
"icon": "file://icon.svg",
"overview": "file://overview.md",
"supportedSignals": {
"metrics": true,
"logs": true
},
"dataCollected": {
"metrics": [
{
"name": "redis.googleapis.com/server/uptime",
"unit": "Seconds",
"type": "Gauge",
"description": ""
},
{
"name": "redis.googleapis.com/stats/memory/usage_ratio",
"unit": "None",
"type": "Gauge",
"description": ""
},
{
"name": "redis.googleapis.com/stats/memory/system_memory_usage_ratio",
"unit": "None",
"type": "Gauge",
"description": ""
},
{
"name": "redis.googleapis.com/stats/cpu_utilization_main_thread",
"unit": "Seconds",
"type": "Sum",
"description": ""
},
{
"name": "redis.googleapis.com/clients/connected",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "redis.googleapis.com/stats/cache_hit_ratio",
"unit": "None",
"type": "Gauge",
"description": ""
},
{
"name": "redis.googleapis.com/keyspace/keys",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "redis.googleapis.com/commands/usec_per_call",
"unit": "Microseconds",
"type": "Gauge",
"description": ""
},
{
"name": "redis.googleapis.com/commands/calls",
"unit": "Count",
"type": "Sum",
"description": ""
},
{
"name": "redis.googleapis.com/clients/blocked",
"unit": "Count",
"type": "Gauge",
"description": ""
},
{
"name": "redis.googleapis.com/stats/reject_connections_count",
"unit": "Count",
"type": "Sum",
"description": ""
}
],
"logs": []
},
"telemetryCollectionStrategy": {
"gcp": {}
},
"assets": {
"dashboards": [
{
"id": "overview",
"title": "GCP Memorystore Redis Overview",
"description": "Overview of GCP Memorystore Redis metrics",
"definition": "file://assets/dashboards/overview.json"
}
]
}
}

View File

@@ -1,3 +0,0 @@
### Monitor GCP Memorystore Redis with SigNoz
Collect key GCP Memorystore Redis metrics and view them with an out of the box dashboard.

View File

@@ -20,21 +20,21 @@ func (c Compiled) IsEmpty() bool {
// Compile always returns a non-nil *Compiled. An empty query (or one that
// produces no SQL) yields a Compiled with an empty SQL — callers gate on
// SQL != "" rather than a nil check.
//
// A `key OP value` term compiles to a DSL predicate; a bare word is a
// case-insensitive substring search over the dashboard name, description, and tag
// keys/values. They compose through AND/OR/NOT, so `prod payment` matches both
// words (implicit AND) and `prod OR name = 'x'` mixes free text with a filter. A
// quoted token matches literally, e.g. `"prod payment"`.
func Compile(query string, formatter sqlstore.SQLFormatter) (*Compiled, error) {
if len(query) == 0 {
if len(strings.TrimSpace(query)) == 0 {
return &Compiled{}, nil
}
queryVisitor := newVisitor(formatter)
sql, args, syntaxErrs := queryVisitor.compile(query)
if len(syntaxErrs) > 0 {
sql, args, errs := newVisitor(formatter).compile(query)
if len(errs) > 0 {
return nil, errors.NewInvalidInputf(dashboardtypes.ErrCodeDashboardListFilterInvalid,
"invalid filter query: %s", strings.Join(syntaxErrs, "; "))
}
if len(queryVisitor.errors) > 0 {
return nil, errors.NewInvalidInputf(dashboardtypes.ErrCodeDashboardListFilterInvalid,
"invalid filter query: %s", strings.Join(queryVisitor.errors, "; "))
"invalid filter query: %s", strings.Join(errs, "; "))
}
return &Compiled{

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