Compare commits

...

36 Commits

Author SHA1 Message Date
Vinícius Lourenço
89558a3f27 fix(query-search): do not override token classes 2026-07-16 14:44:23 -03:00
Gaurav Tewari
98c0c53a2b feat: add dashboard in llm o11y (#12136)
* feat: add locked dashboard

* feat: update test cases

* chore: add comments

* chore: update comment

---------

Co-authored-by: Gaurav Tewari <tewarig@users.noreply.github.com>
2026-07-16 15:31:06 +00:00
Gaurav Tewari
6d3861d60f feat(llm-attribute-mapping): group + mapper drawers + diff-save [3-4/5] (#11780)
* fix(llm-pricing): constrain currency dropdown width, drop tab URL param

- Currency SelectSimple stretched to fill the filters bar; give it a fixed
  160px width (min-width couldn't cap the trigger).
- Model costs is the only enabled tab for now, so use Tabs defaultValue
  instead of a URL-backed param. Removes the nuqs tab state plus the now-unused
  TAB_KEYS/TAB_QUERY_KEY constants and TabKey type.

* chore: self review changes

* fix: add skeleton loading

* refactor: self review changes

* refactor: initial prop

* fix: update styling

* fix: add comments in utils

* feat(llm-pricing): add model cost drawer and wire into listing page

* fix(llm-pricing): restrict pricing management to admins

Align the frontend write gate with the backend, which protects the
LLM pricing create/update/delete endpoints with AdminAccess (admin
only). Previously manage_llm_pricing allowed EDITOR/AUTHOR, so those
roles saw the Add/Save affordances but their writes were rejected with
a 403. Also removes the AUTHOR entry, which could never reach the page
(the route gate excludes it).

* fix(llm-pricing): read-only drawer shows View title, hides source picker

Non-managers open the drawer in view mode (write APIs are Admin-only), so:
- the heading reads "View model cost" instead of "Edit model cost"
- the Source (auto vs. override) picker is hidden, since switching source is
  a manager-only action with nothing actionable for a viewer.

* refactor: form in edit / add modal

* chore: update color tokens

* fix: add error handling

* chore: update more self review changes

* chore: self review changes

* chore: self review changes

* fix: minor grammer thing

* fix: route thing

* feat(llm-attribute-mapping): add attribute mapping foundation (route, permission, page shell)

* feat(llm-attribute-mapping): read-only groups and mappers listing

* feat(llm-attribute-mapping): group create/edit/delete via drawer with diff-save

* fix: css styling

* style(llm-attribute-mapping): use design-token css variables for spacing and typography

* fix(llm-attribute-mapping): wrap long condition keys so the context badge isn't clipped

* fix(llm-attribute-mapping): unlock filters column shrink so long keys wrap within the cell

* fix: text overflow in attribute mapping

* fix: fetch data only when tab is open

* feat: add attribute mapping tab

* fix: update col count

* chore: review comments

* fix: input

* fix: update logic

* fix: update drawer

* feat: update draft and mapper

* refactor: migrate to css moduel

* refactor: migrate to css module

* refactor: migrate to css module

* refactor: css module

* feat(llm-attribute-mapping): read-only listing on CSS modules [2/5]

Rebase the listing slice onto the foundation's CSS-module refactor
(which deleted the global stylesheet) and migrate it accordingly:

- Merge listing styles into LLMObservabilityAttributeMapping.module.scss
  (groups/mappers tables, source chips, index badge, error/footer).
- Convert all listing components from global BEM classNames to
  styles.* module access; drop dead/style-less classes (am-table,
  am-row-actions, am-add-row, *_edited, mappers-table__error).
- Adopt theme-aware semantic tokens (--l2/l3-*, --accent-primary,
  --callout-error-*) in place of --bg-* primitives.

* refactor(llm-attribute-mapping): migrate styles to CSS modules

* refactor: migrate to tanstack table

* docs: clarify price precision comment

* chore: remove comment

* feat(llm-attribute-mapping): add attribute mapping foundation (route, permission, page shell)

* fix: css styling

* refactor: css module

* feat(llm-attribute-mapping): read-only listing on CSS modules [2/5]

Rebase the listing slice onto the foundation's CSS-module refactor
(which deleted the global stylesheet) and migrate it accordingly:

- Merge listing styles into LLMObservabilityAttributeMapping.module.scss
  (groups/mappers tables, source chips, index badge, error/footer).
- Convert all listing components from global BEM classNames to
  styles.* module access; drop dead/style-less classes (am-table,
  am-row-actions, am-add-row, *_edited, mappers-table__error).
- Adopt theme-aware semantic tokens (--l2/l3-*, --accent-primary,
  --callout-error-*) in place of --bg-* primitives.

* chore: migrate tanstack table

* chore(llm-attribute-mapping): remove orphaned IndexBadge

The TanStack mappers table dropped the leading index column, leaving
IndexBadge and its .indexBadge style unused. Remove both.

* chore: remove comment

* fix: disable isDirty in case of llm pricing

* refactor: number

* feat: add search , dropdown and flag

* feat: feature flag on entire route and add mode costs tabs

* fix: add isFetchingFeatureFlags

* chore: update flag

* refactor: shell

* fix: add key to route

* feat: add flags

* chore: additional refactor

* chore: add commet in utis

* chore: self review changes

* refactor: types and other things

* refactor: types and other things

* chore: add disable on source id

* empty commit

* chore: empty commit

* fix: add demo side nav on sidenav

* chore: remove demo side nav

* refactor: update routes

* chore: remove usd selector for now

* fix: layout shift

* refactor: styles

* refactor: typography component

* refactor: more changes

* refactor: typograhy

* refactor(llm-pricing): break model-cost drawer into per-component files + tokens

Apply the CSS-module/component conventions to the drawer that came from
drawer-3:
- Move the drawer under ModelCostTabPanel/components/ModelCostDrawer/ to mirror
  the ModelCostsTable structure
- Split the single 395-LOC ModelCostDrawer.module.scss into per-component
  co-located modules; cross-component selectors live in shared.module.scss and
  are pulled in via CSS-modules `composes`
- shared.module.scss is a composes target (parsed as plain CSS), so it is kept
  flat with block comments — no SCSS nesting or // comments
- Use --text-vanilla-* (not --bg-vanilla-*) for text colors, matching the
  listing code

* refactor: more changes

* refactor: styling and components

* refactor: styling and components

* chore: add a tooltip on hover

* feat: add delete confirm modal

* fix: update title

* refactor: css variables

* refactor: use signoz button and minor css update

* chore: sync table

* chore: remove extra comment

* chore: use typograpgy test in table config

* fix: minior issues

* fix: llm pricing listing

* refactor: remove extra classes

* refactor: side nav changes

* fix: update missing styles

* chore: update edit and delete options

* chore: remove extra comment

* chore: revert env changes

* chore: add enable check

* chore: remove divider

* refactor: use delete confirm dialog

* chore: remove scss file

* feat: move ui to easily accessable tabs

* feat: update test cases

* chore: update text

* chore: self review changes

* chore: self review refactor

* chore: self review changes

* chore: remove worktree

* chore: revert env.ts

* chore: add attribute mapping foundation

* chore: update ui and add animation

* refactor: components update

* chore: typography changes

* chore: typography changes

* chore: use badge

* refactor: basic components

* chore: remove hardcoded value

* chore: add comments & tests

* chore: update mapper

* chore: update test

* chore: minior ux update

* chore: lodash isEqual

* chore: use lodash

* chore: update save draft

* chore: update env.ts

* chore: update tests

* chore: self review changes

* chore: update test cases

* chore: remove extra comments

* refactor(llm-pricing): share toast copy via constants

* chore: use constants

* chore: redclared constants

* chore: update test cases

* chore: remove unused component

* chore: update types

* chore: update design

* chore: update ui

* refactor: minor things

* chore: break down thingsinto comps

* chore: update files

* fix: update mapping

* chore: remove draft logic no need for now

* chore: more refactor

* chore: remove comments

* chore: refactor route

* chore: update query refech on mount

* chore: self review changes

* chore: update skeleton

* refactor: code

* chore: sync up with group mappers

* chore: only 1 mapper row at a time

* refactor: code

* refactor: eslint disable

* chore: update selector

* chore: sort

* refactor: review changes

* chore: use refetch group

* feat(llm-attribute-mapping): mapper drawer + persistMappers [4/5] (#11781)

* feat(llm-attribute-mapping): mapper drawer + persistMappers [4/5]

Core of the mapper drawer work: the MapperFormDrawer + SourceAttributeRow
UI, persistMappers store logic, mapper row/actions, and supporting
utils/types/saveDraft changes.

Removes the barrel (index.ts) files introduced by this PR
(MapperFormDrawer, MapperActionsMenu, SourceAttributeRow) and imports
those modules directly instead.

Tests, public locales, the conditions tooltip, and the KeySearchInput
relocation follow in a stacked follow-up PR.

* test(llm-attribute-mapping): tooltip, locales, tests + drop barrels [4/5 follow-up]

Follow-up to the mapper drawer core PR. Contains:
- AttributeMappingsTab + LLMObservabilityAttributeMapping test suites
- public locale (titles.json) updates
- the group ConditionsTooltip and its GroupHeader wiring
- relocation of KeySearchInput to the shared components/ dir
- removal of the remaining barrel (index.ts) files across the
  AttributeMapping feature, importing each module directly

* chore: remove comments

* chore: reduce width

* fix: no refetch on open

* chore: add discard dialog

---------

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

* chore: remove reduced motion

* chore: self review changes

* chore: c should be capital

* chore: review changes

* refactor: sync server store

* chore: update tests

* Revert "chore: update tests"

This reverts commit 5f39c910df.

* Revert "refactor: sync server store"

This reverts commit b92f902d9f.

* chore: rename to editor

* chore: resolve review comments

---------

Co-authored-by: Gaurav Tewari <tewarig@users.noreply.github.com>
2026-07-16 14:32:35 +00:00
Nikhil Mantri
20e13f66e0 fix(infra-monitoring): use proper metric names corresponding to entities to be counted (#12130)
* chore: write proper lists of metricNames to lookup for counts

* chore: updated integration tests
2026-07-16 12:54:21 +00:00
Ashwin Bhatkal
07fe353b94 refactor(dashboard-v2): organize variable subsystem into components/ hooks/ utils/ (#12135)
* chore(dashboard-v2): drop self-explanatory comments from the variable subsystem

Remove comments that restate a well-named symbol or narrate obvious steps, and
trim verbose JSDoc to the non-obvious "why". No behavior change.

* refactor(dashboard-v2): organize VariablesBar into components/ hooks/ utils/

Split the flat VariablesBar directory into components/ (each component in its
own folder, co-locating a component-specific .module.scss), hooks/ (all use*
including useVariableOptions, moved out of selectors/), and utils/ (pure
modules). The bar's shared cross-cutting classes and .control stay in the root
VariablesBar.module.scss. Import paths updated across the subsystem; no
behavior change. Also updates a stale URL assertion in the seed test left over
from the store-source-of-truth change.

* refactor(dashboard-v2): organize DashboardSettings/Variables into components/ hooks/ utils/

Move the loose files out of the Variables root: pure modules into utils/
(rename variableDependencies -> variableCycleDetection to end the name clash
with VariablesBar's graph engine), useSaveVariables into hooks/, and VariableRow
and VariablesList into their own component folders with co-located .module.scss.
The shared model, adapters and types stay at the root as the folder's public
API. Import paths updated across the subsystem; no behavior change.

* refactor(dashboard-v2): factor the variable tooltip ref lists into one directional component

Extract TooltipRefSection so "Depends on" and "Used by" share one component,
differing only by color and an up/down arrow that depicts the dependency
direction (up = parents this variable depends on, down = children that use it).

* refactor(dashboard-v2): stack the variable name above its control

Move each variable's name into a notch label above the control: fixed-width
columns, name truncated with a hover title and a spaced/centered info icon,
single-line multi-select, ~33px control height, and a visible dropdown caret.
Nudge the toolbar spacing so the time selector aligns with the shorter bar.
Also render the impact dialog's "still references" warning via Typography
color="warning" instead of a bespoke CSS class.

* fix: top margin
2026-07-16 10:27:44 +00:00
Ashwin Bhatkal
9fb89fafc8 feat(dashboard-v2): variable runtime — resolver, fetch engine, panel loading, impact dialog (#12125)
Some checks failed
build-staging / prepare (push) Has been cancelled
build-staging / js-build (push) Has been cancelled
build-staging / go-build (push) Has been cancelled
build-staging / staging (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
* feat(dashboard-v2): single default resolver for variable selection

One source of truth for a variable's value (seed → reconcile → payload), shared by the bar, the fetch gate and the query payload so they can't disagree. Precedence stays URL → store → default; ALL materializes once, without an extra round-trip.

* feat(dashboard-v2): dependency-ordered, value-gated variable fetch engine

Query roots + dynamics fetch immediately; query children unblock only once their parents hold a committed value, so a chain fetches each variable once. The cycle is idempotent per (dashboard·order·time) and resets on page unmount, so a re-mount or editor return can't double it or inherit stale state.

* refactor(dashboard-v2): consolidate variable selectors; add bar tooltip + loading bar

Fold the per-type Query/Dynamic/Custom selectors into one VariableValueControl backed by useVariableOptions. Add a hover tooltip showing a variable's dependencies (Depends on / Used by) and a loading bar flush along the control's bottom edge; refine name/dependency colors to design-system tokens.

* feat(dashboard-v2): hold panels in loading until referenced variables resolve

A panel waits until every QUERY/DYNAMIC variable it references is ready — a concrete pick and a DYNAMIC ALL are ready immediately, an unselected value or a QUERY ALL waits until it resolves. The wait folds into the panel's loading flags (a disabled query reports neither), so it never flashes "no data". Only referenced variables re-key the cache.

* feat(dashboard-v2): rename/delete variable impact dialog

Renaming or deleting a referenced variable is blocked behind a dialog listing every usage (QB / PromQL / ClickHouse panels and other variable defs), with a current → resulting preview, per-usage selection, and inline edits; applied as one JSON-Patch.

* fix(dashboard-v2): don't flash ALL on a multi-select while its options load

CustomMultiSelect derives "all selected" from options + the current value; with options still empty a concrete selection reads as covering everything. Only enable ALL detection once options have loaded.

* refactor(dashboard-v2): make the store the source of truth for variable selection

The persisted store (localStorage) is now the sole source of truth for runtime
variable values. Stop writing ?variables= on every selection change; the URL
param is read once on load (a share link) to seed the store and then dropped,
so dashboard navigation and drilldown no longer round-trip through the URL. The
panel editor reads the selection from the store (a lagging URL snapshot could
clobber a just-changed value). Drops the now-unused withVariablesSearch.

* refactor(dashboard-v2): address review feedback on the variable subsystem

- consolidate the default resolver: one configuredDefault normalizer shared by
  resolveDefaultSelection and configuredDefaultValue, plus a textDefault helper
  for TEXT (drops the dead textValue/empty-string fallbacks)
- reuse computeVariableDependencies for the bar tooltip's depends-on / used-by
- split the fetch machinery out of useVariableOptions into useFetchedVariableOptions
- extract useVariableListActions from the variables settings page
- reuse removeVariableFromExpression for delete-clause removal (handles dangling
  syntax / parentheses) and drop the local removeVariableReferenceClause
- grey the impact dialog's Current field on uncheck; comment + spacing nits;
  guard useIsPanelWaitingOnVariable so it reads variableTypes without optional chaining
- fix the seed test's read-once URL assertion
2026-07-16 08:54:38 +00:00
Abhi kumar
0655328fa9 feat(dashboard-v2): export panels & dashboards from explorers (#12043)
* feat(dashboard-v2): new-panel editor route + query-preserving seed

Add the /dashboard/:id/panel/new route helpers (buildExportPanelLink,
parseNewPanelKind, layoutIndex) and buildNewPanelSeed, which converts an
exported explorer compositeQuery into a seeded panel.

Coerces a builder-only kind (List) to a query-compatible one (PromQL ->
TimeSeries, ClickHouse -> Table) so a non-builder query is preserved rather
than dropped. PanelEditorPage consumes the effective kind; new List panels
seed columns from the query's resolved signal.

* feat(dashboard-v2): flag-aware export-to-dashboard hooks

Add the hooks shared by the explorer "Add to dashboard" flow:

- useGetExportToDashboardLink: builds the V2 panel-editor link (or the V1
  new-widget link) based on the use_dashboard_v2 flag.
- useExportDashboards: flag-aware, search-filtered dashboard source. V2
  searches server-side via the name-contains filter DSL (debounced); V1
  filters its already-complete list in memory.
- useCreateExportDashboard: flag-aware "create a new dashboard" - V2 via the
  Perses createDashboardV2, V1 via the legacy create - normalized to Dashboard.

* feat(dashboard-v2): rebuild "Add to dashboard" export dialog

Replace the antd Modal-based ExportPanel with a @signozhq/ui DialogWrapper
dialog driven by the export hooks. The picker searches server-side and pins the
selected dashboard as an option so it survives a narrowing search; "New
dashboard" creates via the flag-aware hook and navigates to the panel editor.

Removes the old index.tsx / styles.ts.

* feat(dashboard-v2): wire explorers to flag-aware export

Point the Logs/Traces/Metrics explorer "Add to dashboard" actions at
useGetExportToDashboardLink, so an exported query lands in the V2 panel editor
when use_dashboard_v2 is on (the V1 new-widget link otherwise). ExplorerOptions
now renders the rebuilt ExportPanel dialog directly instead of wrapping it in an
antd Modal.

* chore: fixed failing tests

* refactor(dashboard-v2): address explorer-export review feedback

- rename isQueryTypeSupported → isQueryTypeSupportedByPanelKind for clarity
- buildExportPanelLink returns null (no silent TimeSeries default) when a
  panel type has no V2 kind; explorers guard navigation on the null link
- introduce a neutral ExportDashboard {id,title} for the picker instead of
  reusing the V1 Dashboard entity with faked fields
- note why an exported Query's single unit doesn't seed columnUnits

* chore: pr review changes
2026-07-16 05:49:06 +00:00
Vinicius Lourenço
f24102c0db feat(infrastructure-monitoring-v2): pod status/counts & couple more fixes (#12127)
* refactor(infrastructure-monitoring-v2): remove pod phase and add pod status/counts

* fix(column-header): align to left

* fix(cell-value): ensure tooltip is correctly aligned

* fix(entity-traces): use - instead of N/A

* test(entity-traces): fix broken test
2026-07-16 03:34:12 +00:00
Vinicius Lourenço
edee102b52 feat(infrastructure-monitoring): add pod metrics tab (#12106)
* fix(k8s-base-details): do not allow untoggle tabs

* feat(constants): add base queries for all workloads

Ref: https://github.com/SigNoz/engineering-pod/issues/4032#issuecomment-4893103716

* feat(daemon-sets): add pod metrics tab

* feat(deployments): add pod metrics tab

* feat(jobs): add pod metrics tab

* feat(namespaces): add pod metrics tab

* feat(statefulsets): add pod metrics tab

* fix(k8s-base-details): do not keep selected invalid tab

* fix(pod-metrics): add new badge

* Revert "fix(pod-metrics): add new badge"

This reverts commit 09b8624f52.

* chore(k8s-base-details): mark tab as possible null
2026-07-16 03:30:33 +00:00
Srikanth Chekuri
9a26998d18 chore: add integration tests for metrics under reduction - query part (#11979)
Some checks failed
build-staging / prepare (push) Has been cancelled
build-staging / js-build (push) Has been cancelled
build-staging / go-build (push) Has been cancelled
build-staging / staging (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
* chore: add integration tests for metrics under reduction - query part

* chore: add to ci

* chore: add todos

* chore: update tests

* chore: trigger build

* chore: address review comments

* chore: undo the changes to tests/fixtures/http.py

* chore: remove __normalized and add fast path

* chore: fix unit tests

* chore: update test

* chore: sunset 25.5.6, update migrator version, and remove version arg

* chore: also update devenv

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-15 21:27:39 +00:00
Vinicius Lourenço
fbf2e62044 feat(infrastructure-monitoring-v2): add onboarding checks (#12119)
* feat(infrastructure-monitoring): add onboarding checks

* refactor(infrastructure-monitoring): use error content to show error message

* test(infrastructure-monitoring): add tests for onboarding checks
2026-07-15 19:47:30 +00:00
Tushar Vats
62c44f5eac chore: pass org id for flagger (#12126) 2026-07-15 19:47:24 +00:00
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
Ashwin Bhatkal
8e4d7c68b3 feat(dashboard-v2): query-first DSL filter for the dashboards list (#12046)
* feat(dashboard-v2): stage-aware DSL autocomplete engine for the list filter

* feat(dashboard-v2): single query-string list-filter state with reflect/splice

* feat(dashboard-v2): drive saved/built-in views off the query string

* feat(dashboard-v2): CodeMirror query box with DSL autocomplete popup

* feat(dashboard-v2): antd Created-by/Updated filter selects, drop the Tags dropdown

* feat(dashboard-v2): wire the DSL filter into the dashboards list (sticky header, pagination)

* fix: small css elevation

* refactor(dashboard-v2): reuse shared operator constants in the DSL grammar

Build STRING_OPS / OPERATOR_MATRIX / VALUELESS_OPERATORS from the shared OPERATORS + negateOperator (constants/antlrQueryConstants) instead of hardcoded strings; drop a stale lint-disable in FilterChips. Addresses review comments.

* fix(dashboard-v2): wrap IN/NOT IN autocomplete values in a bracketed list

Picking a value for an IN/NOT IN clause in the DSL search box inserted a
bare literal ('x') instead of a list (['x']). The value stage now wraps a
fresh pick in a closed bracketed list, appends subsequent picks inside the
existing brackets (deduping already-entered values), and keeps the caret
inside the list so multi-select continues.

---------

Co-authored-by: Srikanth Chekuri <srikanth.chekuri92@gmail.com>
2026-07-14 05:29:21 +00:00
Gaurav Tewari
2704317c6a feat(llm-pricing): add Unpriced Models tab with billing-model mapping [6/6] (#11813)
* feat(llm-pricing): add model pricing foundation (route, permission, page shell)

* feat(llm-pricing): add listing page and table

* chore(llm-pricing): drop search + source filters from list request

The list API does not honour the q (search) and source params yet, so
the controls did nothing. Remove the search input and source dropdown
along with the params we sent, and trim useModelPricingFilters to the
URL-backed page state that pagination still needs. Currency dropdown,
tabs, table and pagination are unchanged. Filters will return once the
backend supports them.

* refactor(llm-pricing): extract getRelativeTime helper in utils

Pull the relative-time formatting out of getRelativeLastSeen into a
small local getRelativeTime helper. Kept feature-local (not in the
shared utils/timeUtils) so the LLM pricing module owns its own dayjs
config; the local relativeTime extend stays for test self-sufficiency.

* refactor(llm-pricing): drop dead NaN guard in formatPricePerMillion

Pricing fields are typed as required numbers and JSON can't carry NaN,
so Number.isNaN was unreachable. Keep the null/undefined guard as API
defensiveness (toFixed on a missing value would crash the row). Also
trims the now-redundant dayjs.extend comment.

* refactor(llm-pricing): centralize constants and shared types

Extract PAGE_SIZE, PAGE_KEY, COLUMN_COUNT and CURRENCY_OPTIONS into a
new constants.ts, and move the ModelPricingFilters contract into
types.ts. Component prop interfaces stay colocated with their
components, matching the convention in the drawer PR.

* refactor(llm-pricing): use nuqs for list pagination URL state

Replace the hand-rolled useHistory + URLSearchParams plumbing in
useModelPricingFilters with nuqs useQueryState, matching the convention
used by the dashboards, alerts and k8s list pages. Behaviour is
unchanged: parseAsInteger.withDefault(1) keeps ?page=1 out of the URL
and history:'replace' avoids polluting the back-stack.

* refactor(llm-pricing): inline pagination, drop useModelPricingFilters

The hook had shrunk to a one-line nuqs wrapper after search/source were
removed, so inline the useQueryState call into the container and remove
the hook file plus the now-unused ModelPricingFilters type. When the
filters return (once the API honours them) they can move back into a
dedicated hook.

* feat(llm-pricing): disable currency selector (USD-only for now)

Only USD is priced today, so render the currency SelectSimple in a
disabled state pinned to USD. A disabled select can't fire onChange, so
the currency useState is dead — drop it (and the now-unused useState
import).

* refactor(llm-pricing): render model costs inside its tab + tab URL param

The listing was rendered outside the Tabs, so the tab was decorative.
Move all model-cost content (currency control, list query, table,
pagination, footer) into a ModelCostsTab component rendered as the
'Model costs' tab's children, and drive the active tab from a 'tab' URL
query param (nuqs). The container is now just the page shell. Unpriced
models stays a disabled placeholder for a later PR.

* style(llm-pricing): target @signozhq table slots, drop dead antd/leftover rules

The component uses @signozhq/ui Table/Tabs (Radix-based), not antd, so the
.ant-table-* and .ant-tabs-nav selectors never matched — the intended
uppercase/muted header styling wasn't applied. Retarget header/cell rules to
[data-slot='table-head'|'table-cell'] (no !important needed). Also remove dead
rules left over from the removed search/source/add UI (.filters-bar__search,
__source, __add, .page-header__actions) and the unused .source-badge--auto/
--override modifiers.

* fix(llm-pricing): constrain currency dropdown width, drop tab URL param

- Currency SelectSimple stretched to fill the filters bar; give it a fixed
  160px width (min-width couldn't cap the trigger).
- Model costs is the only enabled tab for now, so use Tabs defaultValue
  instead of a URL-backed param. Removes the nuqs tab state plus the now-unused
  TAB_KEYS/TAB_QUERY_KEY constants and TabKey type.

* chore: self review changes

* fix: add skeleton loading

* refactor: self review changes

* refactor: initial prop

* fix: update styling

* fix: add comments in utils

* feat(llm-pricing): add model cost drawer and wire into listing page

* fix(llm-pricing): restrict pricing management to admins

Align the frontend write gate with the backend, which protects the
LLM pricing create/update/delete endpoints with AdminAccess (admin
only). Previously manage_llm_pricing allowed EDITOR/AUTHOR, so those
roles saw the Add/Save affordances but their writes were rejected with
a 403. Also removes the AUTHOR entry, which could never reach the page
(the route gate excludes it).

* fix(llm-pricing): read-only drawer shows View title, hides source picker

Non-managers open the drawer in view mode (write APIs are Admin-only), so:
- the heading reads "View model cost" instead of "Edit model cost"
- the Source (auto vs. override) picker is hidden, since switching source is
  a manager-only action with nothing actionable for a viewer.

* refactor: form in edit / add modal

* chore: update color tokens

* fix: add error handling

* chore: update more self review changes

* chore: self review changes

* chore: self review changes

* fix: minor grammer thing

* fix: route thing

* refactor: migrate to css moduel

* refactor: migrate to css module

* refactor: migrate to css module

* refactor: migrate to tanstack table

* docs: clarify price precision comment

* chore: remove comment

* chore: remove comment

* fix: disable isDirty in case of llm pricing

* refactor: number

* feat: add search , dropdown and flag

* feat: feature flag on entire route and add mode costs tabs

* fix: add isFetchingFeatureFlags

* feat: llm pricing unpriced model

* chore: update flag

* refactor: shell

* fix: add key to route

* feat: add flags

* chore: additional refactor

* chore: add commet in utis

* chore: self review changes

* refactor: types and other things

* refactor: types and other things

* chore: add disable on source id

* empty commit

* feat: add search to search model name

* chore: empty commit

* fix: add demo side nav on sidenav

* chore: remove demo side nav

* refactor: update routes

* chore: remove usd selector for now

* fix: layout shift

* refactor: styles

* refactor: typography component

* refactor: more changes

* refactor: typograhy

* refactor(llm-pricing): break model-cost drawer into per-component files + tokens

Apply the CSS-module/component conventions to the drawer that came from
drawer-3:
- Move the drawer under ModelCostTabPanel/components/ModelCostDrawer/ to mirror
  the ModelCostsTable structure
- Split the single 395-LOC ModelCostDrawer.module.scss into per-component
  co-located modules; cross-component selectors live in shared.module.scss and
  are pulled in via CSS-modules `composes`
- shared.module.scss is a composes target (parsed as plain CSS), so it is kept
  flat with block comments — no SCSS nesting or // comments
- Use --text-vanilla-* (not --bg-vanilla-*) for text colors, matching the
  listing code

* refactor: more changes

* refactor: styling and components

* refactor: styling and components

* chore: add a tooltip on hover

* feat: add delete confirm modal

* fix: update title

* refactor: css variables

* refactor: use signoz button and minor css update

* chore: sync table

* chore: remove extra comment

* chore: use typograpgy test in table config

* fix: minior issues

* fix: llm pricing listing

* refactor: remove extra classes

* refactor: side nav changes

* fix: update missing styles

* chore: update edit and delete options

* chore: remove extra comment

* chore: revert env changes

* chore: add enable check

* chore: remove divider

* refactor: use delete confirm dialog

* chore: remove scss file

* feat: move ui to easily accessable tabs

* feat: update test cases

* chore: update text

* chore: self review changes

* chore: self review refactor

* chore: self review changes

* feat(llm-pricing): batch-map unpriced models via top Save + confirm drawer

Replace the per-row "Map to model" button + inline confirm with a single
top-level Save button that opens a confirm drawer listing every pending
mapping in a table (model -> billing model + pricing).

- Batch the save: group selections by target rule so multiple models mapped
  to the same rule append their patterns in one PUT instead of clobbering
- Capped, skeleton-loaded unpriced table (max-height) to avoid layout shift,
  matching the model-costs table
- Restore the dropped .modelCellName style; drop dead inline-confirm styles
- Tests: batch/clobber hook test, tab integration flow, and fix the stale
  'unpriced tab disabled' assertion (the tab is now enabled)

* chore: remove worktree

* chore: revert env.ts

* chore: update llm pricing

* chore: update ui

* chore: sync unpriced modal

* chore(llm-pricing): address Copilot review on unpriced confirm table

- formatSpanCount: use Intl.NumberFormat compact so 999,999 renders as
  "1M" (not "1000.0K") and trailing ".0" is dropped
- confirm drawer extra-buckets container: non-wrapping inline-flex so
  buckets stay on one line at the fixed row height
- soften the misleading "renders identically" comment (extra buckets are
  plain inline text here, not the model-costs Badge chips)

* chore: flex wrap

* chore: self review changes

* feat: add support to remove model

* chore: update ux

* chore: update test cases

* chore: remove unused test cases

* chore: update tests

* chore: remove extra integration

* chore: refactor

* refactor: imports

* refactor: un priced modal

* chore: update unpriced modal

* chore: update map billing mode select

* refactor: code

* refactor: remove comments

* chore: make bg great again

---------

Co-authored-by: Gaurav Tewari <tewarig@users.noreply.github.com>
2026-07-14 04:43:28 +00:00
Vinicius Lourenço
9476d8f70f feat(infrastructure-monitoring-v2): add button to open chart at metrics explorer (#12107)
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-13 19:29:46 +00:00
Vinicius Lourenço
69a240fdb2 chore(frontend): bump deps (#12052)
* chore(frontend): bump deps

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* chore(pnpm-lock): missing update

---------

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-07-13 17:14:22 +00:00
Vinicius Lourenço
6975c4d90c chore(codeowners): update infra monitoring file owner (#12073)
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-13 11:47:09 +00:00
Vikrant Gupta
9bfa6e6d59 feat(authz): store role transaction groups as JSON document (#12028)
* feat(authz): store role transaction groups as document of record

Persist a role's transaction groups as JSON on the role row so the role
details page is reconstructed deterministically from SQL instead of being
rebuilt from OpenFGA tuples (which will soon carry opaque hashed telemetry
selectors):

- authtypes: TransactionGroups gains Value/Scan (validated via
  NewTransactionGroups) and MarshalJSON (nil renders as []); NewRole takes
  transactionGroups; NewManagedRoles fills managed docs from the registry;
  RoleWithTransactionGroups removed - Role carries the wire field and the
  AuthZ interface, handler, and OpenAPI responses use *Role; GettableRole
  (without transactionGroups) is the list response
- sqlmigration 099: add role.transaction_groups, backfill custom roles from
  their permission tuples (dual dialect) and managed roles from the registry
- sqlmigration 059: pin insert columns so the live Role model addition does
  not break fresh installs (059 runs before 099)
- ee provider: writes persist the doc alongside FGA tuples (FGA first, SQL
  second, as before); GetWithTransactionGroups reads the doc; the per-type
  ReadTuples fan-out (readAllTuplesForRole) is removed
- audit middleware: log and skip resolved resources carrying a resolution
  error
- frontend: regenerated OpenAPI spec and API types; role list consumers
  retyped to GettableRole; role GET keeps transactionGroups

* fix(authz): reconcile role tuples from openfga state, decouple migration 059

- Update and Delete derive their diff/deletion base from the tuples openfga
  actually holds for the role (readAllTuplesForRole) instead of the stored
  JSON record, so every mutation sweeps drift and residue; the record stays
  a display-only artifact written after the tuple write
- ReadTuples restored on the AuthZ interface with plain passthroughs in both
  providers and the ee server
- TransactionGroups.Value marshals unconditionally (nil renders as [] via
  MarshalJSON) instead of returning a nil driver.Value
- migration 059 uses a migration-local role struct and constructor so live
  Role model changes cannot alter its insert; migration 099 drops the manual
  column-exists guard (AddColumn emits IF NOT EXISTS)

* refactor(authz): split role into domain Role and StorableRole

Replace the Scan/Value/MarshalJSON codecs on TransactionGroups with the
storable pattern: StorableRole is the bun model carrying transaction groups
as raw JSON text, Role is the pure domain/wire type, and
NewStorableRoleFromRole/NewRoleFromStorableRole convert at the store
boundary (nil groups persist as [], reads parse through the validating
constructor). RoleStore and sqlauthzstore speak StorableRole; both
providers convert; handlers and the wire contract are unchanged.

* revert(authz): restore TransactionGroups codecs over the storable split

Role is a bun relation target (UserRole.Role, ServiceAccountRole.Role), so
splitting it into StorableRole/Role cascaded: relations must point at the
bun model, which broke the user-roles join and leaked the storable shape
into user and service account responses. Keep the single Role model with
Scan/Value/MarshalJSON on TransactionGroups; the storable split fits leaf
models only.

This reverts commit 73aa7d32b1 and keeps transaction_test.go deleted.

* refactor(authz): use bun models in migration 099, wire oss role get

- migration 099 follows the migration-local row struct pattern: bun
  NewSelect/NewUpdate for the role table reads and backfill writes; the
  openfga store and tuple lookups stay raw like 081/083
- oss provider Get reads the role from the store instead of returning
  unsupported

* refactor(authz): org-scoped backfill in migration 099, empty groups on null scan

- migration 099 iterates organizations: per org it backfills custom roles
  from their permission tuples (readRoleTuples helper) and managed roles
  from the registry (JSON precomputed per role name)
- TransactionGroups scans SQL NULL as an empty slice so the api always
  renders transactionGroups as []; nullzero keeps writing NULL for nil

* fix(authz): pass unique constraints to add column in migration 099
2026-07-13 11:42:42 +00:00
492 changed files with 21968 additions and 6261 deletions

View File

@@ -1,6 +1,6 @@
services:
init-clickhouse:
image: clickhouse/clickhouse-server:25.5.6
image: clickhouse/clickhouse-server:25.12.5
container_name: init-clickhouse
command:
- bash
@@ -18,7 +18,7 @@ services:
volumes:
- ${PWD}/fs/tmp/var/lib/clickhouse/user_scripts/:/var/lib/clickhouse/user_scripts/
clickhouse:
image: clickhouse/clickhouse-server:25.5.6
image: clickhouse/clickhouse-server:25.12.5
container_name: clickhouse
volumes:
- ${PWD}/fs/etc/clickhouse-server/config.d/config.xml:/etc/clickhouse-server/config.d/config.xml
@@ -67,7 +67,7 @@ services:
timeout: 5s
retries: 3
telemetrystore-migrator:
image: signoz/signoz-otel-collector:v0.142.0
image: signoz/signoz-otel-collector:v0.144.6
container_name: telemetrystore-migrator
environment:
- SIGNOZ_OTEL_COLLECTOR_CLICKHOUSE_DSN=tcp://clickhouse:9000

2
.github/CODEOWNERS vendored
View File

@@ -189,7 +189,9 @@ go.mod @therealpandey
## Infrastructure Monitoring
/frontend/src/pages/InfrastructureMonitoring/ @SigNoz/pulse-frontend
/frontend/src/container/InfraMonitoringHosts/ @SigNoz/pulse-frontend
/frontend/src/container/InfraMonitoringHostsV2/ @SigNoz/pulse-frontend
/frontend/src/container/InfraMonitoringK8s/ @SigNoz/pulse-frontend
/frontend/src/container/InfraMonitoringK8sV2/ @SigNoz/pulse-frontend
## Alerts
/frontend/src/pages/AlertList/ @SigNoz/pulse-frontend

View File

@@ -60,16 +60,17 @@ jobs:
- querier_json_body
- querier_skip_resource_fingerprint
- ttl
- clickhousecluster
- metricreduction
sqlstore-provider:
- postgres
- sqlite
sqlite-mode:
- wal
clickhouse-version:
- 25.5.6
- 25.12.5
schema-migrator-version:
- v0.144.3
- v0.144.6
postgres-version:
- 15
if: |

View File

@@ -543,6 +543,31 @@ components:
required:
- id
type: object
AuthtypesGettableRole:
properties:
createdAt:
format: date-time
type: string
description:
type: string
id:
type: string
name:
type: string
orgId:
type: string
type:
type: string
updatedAt:
format: date-time
type: string
required:
- id
- name
- description
- type
- orgId
type: object
AuthtypesGettableToken:
properties:
accessToken:
@@ -694,43 +719,6 @@ components:
- detach
type: string
AuthtypesRole:
properties:
createdAt:
format: date-time
type: string
description:
type: string
id:
type: string
name:
type: string
orgId:
type: string
type:
type: string
updatedAt:
format: date-time
type: string
required:
- id
- name
- description
- type
- orgId
type: object
AuthtypesRoleMapping:
properties:
defaultRole:
type: string
groupMappings:
additionalProperties:
type: string
nullable: true
type: object
useRoleAttribute:
type: boolean
type: object
AuthtypesRoleWithTransactionGroups:
properties:
createdAt:
format: date-time
@@ -758,6 +746,18 @@ components:
- orgId
- transactionGroups
type: object
AuthtypesRoleMapping:
properties:
defaultRole:
type: string
groupMappings:
additionalProperties:
type: string
nullable: true
type: object
useRoleAttribute:
type: boolean
type: object
AuthtypesSamlConfig:
properties:
attributeMapping:
@@ -4259,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
@@ -4279,6 +4307,7 @@ components:
- nodeCountsByReadiness
- podCountsByPhase
- podCountsByStatus
- counts
- meta
type: object
InframonitoringtypesClusters:
@@ -4800,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
@@ -4823,6 +4872,7 @@ components:
- namespaceMemory
- podCountsByPhase
- podCountsByStatus
- counts
- meta
type: object
InframonitoringtypesNamespaces:
@@ -7998,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:
@@ -8150,7 +8209,7 @@ components:
type: boolean
fieldContext:
$ref: '#/components/schemas/SpantypesFieldContext'
group_id:
groupId:
type: string
id:
type: string
@@ -8163,7 +8222,7 @@ components:
type: string
required:
- id
- group_id
- groupId
- name
- fieldContext
- config
@@ -12022,7 +12081,7 @@ paths:
properties:
data:
items:
$ref: '#/components/schemas/AuthtypesRole'
$ref: '#/components/schemas/AuthtypesGettableRole'
type: array
status:
type: string
@@ -12206,7 +12265,7 @@ paths:
schema:
properties:
data:
$ref: '#/components/schemas/AuthtypesRoleWithTransactionGroups'
$ref: '#/components/schemas/AuthtypesRole'
status:
type: string
required:
@@ -13742,7 +13801,7 @@ paths:
schema:
properties:
data:
$ref: '#/components/schemas/SpantypesGettableSpanMapperGroups'
$ref: '#/components/schemas/SpantypesGettableSpanMappers'
status:
type: string
required:

View File

@@ -119,10 +119,6 @@ func (provider *provider) CheckTransactions(ctx context.Context, subject string,
return results, nil
}
func (provider *provider) ListObjects(ctx context.Context, subject string, relation authtypes.Relation, objectType coretypes.Type) ([]*coretypes.Object, error) {
return provider.openfgaServer.ListObjects(ctx, subject, relation, objectType)
}
func (provider *provider) Write(ctx context.Context, additions []*openfgav1.TupleKey, deletions []*openfgav1.TupleKey) error {
return provider.openfgaServer.Write(ctx, additions, deletions)
}
@@ -131,10 +127,6 @@ func (provider *provider) ReadTuples(ctx context.Context, tupleKey *openfgav1.Re
return provider.openfgaServer.ReadTuples(ctx, tupleKey)
}
func (provider *provider) Get(ctx context.Context, orgID valuer.UUID, id valuer.UUID) (*authtypes.Role, error) {
return provider.pkgAuthzService.Get(ctx, orgID, id)
}
func (provider *provider) GetByOrgIDAndName(ctx context.Context, orgID valuer.UUID, name string) (*authtypes.Role, error) {
return provider.pkgAuthzService.GetByOrgIDAndName(ctx, orgID, name)
}
@@ -183,7 +175,7 @@ func (provider *provider) CreateManagedUserRoleTransactions(ctx context.Context,
return provider.Write(ctx, tuples, nil)
}
func (provider *provider) Create(ctx context.Context, orgID valuer.UUID, role *authtypes.RoleWithTransactionGroups) error {
func (provider *provider) Create(ctx context.Context, orgID valuer.UUID, role *authtypes.Role) error {
_, err := provider.licensing.GetActive(ctx, orgID)
if err != nil {
return errors.New(errors.TypeLicenseUnavailable, errors.CodeLicenseUnavailable, "a valid license is not available").WithAdditional("this feature requires a valid license").WithAdditional(err.Error())
@@ -208,70 +200,31 @@ func (provider *provider) Create(ctx context.Context, orgID valuer.UUID, role *a
return err
}
if err := provider.store.Create(ctx, role.Role); err != nil {
return err
}
return nil
return provider.store.Create(ctx, role)
}
func (provider *provider) GetOrCreate(ctx context.Context, orgID valuer.UUID, role *authtypes.Role) (*authtypes.Role, error) {
_, err := provider.licensing.GetActive(ctx, orgID)
if err != nil {
return nil, errors.New(errors.TypeLicenseUnavailable, errors.CodeLicenseUnavailable, "a valid license is not available").WithAdditional("this feature requires a valid license").WithAdditional(err.Error())
}
existingRole, err := provider.store.GetByOrgIDAndName(ctx, role.OrgID, role.Name)
if err != nil {
if !errors.Ast(err, errors.TypeNotFound) {
return nil, err
}
}
if existingRole != nil {
return existingRole, nil
}
err = provider.store.Create(ctx, role)
if err != nil {
return nil, err
}
return role, nil
func (provider *provider) Get(ctx context.Context, orgID valuer.UUID, id valuer.UUID) (*authtypes.Role, error) {
return provider.store.Get(ctx, orgID, id)
}
func (provider *provider) GetWithTransactionGroups(ctx context.Context, orgID valuer.UUID, id valuer.UUID) (*authtypes.RoleWithTransactionGroups, error) {
_, err := provider.licensing.GetActive(ctx, orgID)
if err != nil {
return nil, errors.New(errors.TypeLicenseUnavailable, errors.CodeLicenseUnavailable, "a valid license is not available").WithAdditional("this feature requires a valid license").WithAdditional(err.Error())
}
role, err := provider.store.Get(ctx, orgID, id)
if err != nil {
return nil, err
}
tuples, err := provider.readAllTuplesForRole(ctx, role.Name, orgID)
if err != nil {
return nil, err
}
transactionGroups := authtypes.MustNewTransactionGroupsFromTuples(tuples)
return authtypes.MakeRoleWithTransactionGroups(role, transactionGroups), nil
}
func (provider *provider) Update(ctx context.Context, orgID valuer.UUID, updatedRole *authtypes.RoleWithTransactionGroups) error {
func (provider *provider) Update(ctx context.Context, orgID valuer.UUID, updatedRole *authtypes.Role) error {
_, err := provider.licensing.GetActive(ctx, orgID)
if err != nil {
return errors.New(errors.TypeLicenseUnavailable, errors.CodeLicenseUnavailable, "a valid license is not available").WithAdditional("this feature requires a valid license").WithAdditional(err.Error())
}
existingRole, err := provider.GetWithTransactionGroups(ctx, orgID, updatedRole.ID)
existingRole, err := provider.Get(ctx, orgID, updatedRole.ID)
if err != nil {
return err
}
additions, deletions := existingRole.TransactionGroups.Diff(updatedRole.TransactionGroups)
existingTuples, err := provider.readAllTuplesForRole(ctx, existingRole.Name, orgID)
if err != nil {
return err
}
existingGroups := authtypes.MustNewTransactionGroupsFromTuples(existingTuples)
additions, deletions := existingGroups.Diff(updatedRole.TransactionGroups)
additionTuples, err := authtypes.NewTuplesFromTransactionGroups(existingRole.Name, orgID, additions)
if err != nil {
return err
@@ -287,7 +240,7 @@ func (provider *provider) Update(ctx context.Context, orgID valuer.UUID, updated
return err
}
return provider.store.Update(ctx, orgID, updatedRole.Role)
return provider.store.Update(ctx, orgID, updatedRole)
}
func (provider *provider) Delete(ctx context.Context, orgID valuer.UUID, id valuer.UUID) error {
@@ -296,7 +249,7 @@ func (provider *provider) Delete(ctx context.Context, orgID valuer.UUID, id valu
return errors.New(errors.TypeLicenseUnavailable, errors.CodeLicenseUnavailable, "a valid license is not available").WithAdditional("this feature requires a valid license").WithAdditional(err.Error())
}
role, err := provider.GetWithTransactionGroups(ctx, orgID, id)
role, err := provider.Get(ctx, orgID, id)
if err != nil {
return err
}
@@ -312,7 +265,7 @@ func (provider *provider) Delete(ctx context.Context, orgID valuer.UUID, id valu
}
}
tuples, err := authtypes.NewTuplesFromTransactionGroups(role.Name, orgID, role.TransactionGroups)
tuples, err := provider.readAllTuplesForRole(ctx, role.Name, orgID)
if err != nil {
return err
}
@@ -324,6 +277,24 @@ func (provider *provider) Delete(ctx context.Context, orgID valuer.UUID, id valu
return provider.store.Delete(ctx, orgID, id)
}
func (provider *provider) readAllTuplesForRole(ctx context.Context, roleName string, orgID valuer.UUID) ([]*openfgav1.TupleKey, error) {
subject := authtypes.MustNewSubject(coretypes.NewResourceRole(), roleName, orgID, &coretypes.VerbAssignee)
tuples := make([]*openfgav1.TupleKey, 0)
for _, objectType := range provider.registry.Types() {
typeTuples, err := provider.openfgaServer.ReadTuples(ctx, &openfgav1.ReadRequestTupleKey{
User: subject,
Object: objectType.StringValue() + ":",
})
if err != nil {
return nil, err
}
tuples = append(tuples, typeTuples...)
}
return tuples, nil
}
func (provider *provider) getManagedRoleGrantTuples(orgID valuer.UUID, userID valuer.UUID) []*openfgav1.TupleKey {
tuples := []*openfgav1.TupleKey{}
@@ -375,21 +346,3 @@ func (provider *provider) getManagedRoleTransactionTuples(orgID valuer.UUID) []*
return tuples
}
func (provider *provider) readAllTuplesForRole(ctx context.Context, roleName string, orgID valuer.UUID) ([]*openfgav1.TupleKey, error) {
subject := authtypes.MustNewSubject(coretypes.NewResourceRole(), roleName, orgID, &coretypes.VerbAssignee)
tuples := make([]*openfgav1.TupleKey, 0)
for _, objectType := range provider.registry.Types() {
typeTuples, err := provider.ReadTuples(ctx, &openfgav1.ReadRequestTupleKey{
User: subject,
Object: objectType.StringValue() + ":",
})
if err != nil {
return nil, err
}
tuples = append(tuples, typeTuples...)
}
return tuples, nil
}

View File

@@ -105,10 +105,6 @@ func (server *Server) BatchCheck(ctx context.Context, tupleReq map[string]*openf
return server.pkgAuthzService.BatchCheck(ctx, tupleReq)
}
func (server *Server) ListObjects(ctx context.Context, subject string, relation authtypes.Relation, objectType coretypes.Type) ([]*coretypes.Object, error) {
return server.pkgAuthzService.ListObjects(ctx, subject, relation, objectType)
}
func (server *Server) Write(ctx context.Context, additions []*openfgav1.TupleKey, deletions []*openfgav1.TupleKey) error {
return server.pkgAuthzService.Write(ctx, additions, deletions)
}

View File

@@ -1,29 +0,0 @@
import { PropsWithChildren } from 'react';
type CommonProps = PropsWithChildren<{
className?: string;
minSize?: number;
maxSize?: number;
defaultSize?: number;
direction?: 'horizontal' | 'vertical';
autoSaveId?: string;
withHandle?: boolean;
}>;
export function ResizablePanelGroup({
children,
className,
}: CommonProps): JSX.Element {
return <div className={className}>{children}</div>;
}
export function ResizablePanel({
children,
className,
}: CommonProps): JSX.Element {
return <div className={className}>{children}</div>;
}
export function ResizableHandle({ className }: CommonProps): JSX.Element {
return <div className={className} />;
}

View File

@@ -21,7 +21,6 @@ const config: Config.InitialOptions = {
'\\.md$': '<rootDir>/__mocks__/cssMock.ts',
'^uplot$': '<rootDir>/__mocks__/uplotMock.ts',
'^motion/react$': '<rootDir>/__mocks__/motionMock.tsx',
'^@signozhq/resizable$': '<rootDir>/__mocks__/resizableMock.tsx',
'^hooks/useSafeNavigate$': USE_SAFE_NAVIGATE_MOCK_PATH,
'^src/hooks/useSafeNavigate$': USE_SAFE_NAVIGATE_MOCK_PATH,
'^.*/useSafeNavigate$': USE_SAFE_NAVIGATE_MOCK_PATH,

View File

@@ -49,7 +49,6 @@
"@sentry/vite-plugin": "5.3.0",
"@signozhq/design-tokens": "2.1.4",
"@signozhq/icons": "0.4.0",
"@signozhq/resizable": "0.0.2",
"@signozhq/ui": "0.0.23",
"@tanstack/react-table": "8.21.3",
"@tanstack/react-virtual": "3.13.22",
@@ -128,7 +127,7 @@
"timestamp-nano": "^1.0.0",
"typescript": "5.9.3",
"uplot": "1.6.31",
"uuid": "^8.3.2",
"uuid": "14.0.1",
"vite": "npm:rolldown-vite@7.3.1",
"vite-plugin-html": "3.2.2",
"zod": "4.3.6",
@@ -222,34 +221,5 @@
"*.(scss|css)": [
"stylelint"
]
},
"resolutions": {
"@types/react": "18.0.26",
"@types/react-dom": "18.0.10",
"debug": "4.3.4",
"semver": "7.5.4",
"xml2js": "0.5.0",
"phin": "^3.7.1",
"body-parser": "1.20.3",
"http-proxy-middleware": "4.1.1",
"cross-spawn": "7.0.5",
"cookie": "^0.7.1",
"serialize-javascript": "6.0.2",
"prismjs": "1.30.0",
"got": "11.8.5",
"form-data": "4.0.6",
"brace-expansion": "^2.0.3",
"on-headers": "^1.1.0",
"js-cookie": "^3.0.7",
"tmp": "0.2.7",
"vite": "npm:rolldown-vite@7.3.1",
"dompurify": "3.4.11",
"js-yaml@3": "3.15.0",
"js-yaml@4": "4.2.0",
"yaml@1": "1.10.3",
"react-router@6": "6.30.4",
"markdown-it": "14.2.0",
"mdast-util-to-hast@13": "13.2.1",
"protocol-buffers-schema": "3.6.1"
}
}

1864
frontend/pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,53 @@
trustPolicy: no-downgrade
blockExoticSubdeps: true
minimumReleaseAge: 2880 # 2d
minimumReleaseAgeStrict: true
minimumReleaseAgeExclude:
- '@signozhq/*'
blockExoticSubdeps: true
minimumReleaseAgeStrict: true
# Security floors for vulnerable transitive deps. Where possible, targets are
# capped to avoid crossing breaking versions (major; and minor for 0.x).
# Some entries may still force a breaking bump when no safe release exists
# within the consumers constraint—see per-entry notes.
overrides:
# via: direct devDep @babel/core ^7.22.11 (+ babel plugin peers)
# remove: bump @babel/core in package.json to ^7.29.6
'@babel/core@<=7.29.0': '>=7.29.6 <8'
# via: jest > babel-plugin-istanbul > @istanbuljs/load-nyc-config@1.1.0 (js-yaml ^3.13.1)
# remove: blocked — 1.1.0 is latest and still depends on js-yaml 3.x
'@istanbuljs/load-nyc-config>js-yaml': '>=4.2.0 <5'
# via: msw@1.3.2 (devDep) > cookie ^0.4.2
# remove: upgrade msw to >=2 (ships cookie ^1). Do NOT open the cap: cookie >=1 is
# ESM-only and breaks msw under jest's CJS sandbox (kills every test suite)
cookie@<0.7.0: '>=0.7.1 <1'
# via: direct dep dompurify 3.4.0; @grafana/data@11.6.15 (3.4.0/3.2.4 exact);
# @monaco-editor/react > monaco-editor@0.55.1 (3.2.7 exact)
# remove: bump direct dep to 3.4.11; @grafana/data (latest 13.1.0) and
# monaco-editor (latest 0.55.1) still pin vulnerable versions — blocked
dompurify@<=3.4.10: '>=3.4.11 <4'
# via: rolldown-vite@7.3.1 (esbuild ^0.27.0); orval@8.9.1 (^0.27.4); ts-jest@29.4.9 (~0.27.4)
# remove: blocked on rolldown-vite (7.3.1 is latest, still ^0.27.0);
# orval >=8.20.0 and ts-jest >=29.4.11 already fixed on their side
esbuild@>=0.27.3 <0.28.1: '>=0.28.1 <0.29.0'
# via: react-use@17.5.1 (direct, js-cookie ^2.2.1); @grafana/data > react-use@17.6.0
# remove: bump react-use to >=17.6.1 (js-cookie ^3); @grafana/data side blocked
js-cookie@<=3.0.5: '>=3.0.7 <4'
# via: @orval/core@8.9.1 (devDep, js-yaml 4.1.1 EXACT pin — not deletable);
# json-schema-to-typescript@15 > @apidevtools/json-schema-ref-parser (^4.1.0)
# remove: upgrade orval to >=8.20.0 (drops js-yaml dependency entirely)
js-yaml@>=4.0.0 <=4.1.1: '>=4.2.0 <5'
# via: react-syntax-highlighter@15.5.0 (prismjs ^1.27.0 + refractor@3 ~1.27.0 tilde-pinned)
# remove: bump react-syntax-highlighter to >=16.1.1 (prismjs ^1.30.0, refractor@5)
prismjs@<1.30.0: '>=1.30.0 <2'
# via: direct dep react-router-dom-v5-compat@6.30.3 (react-router 6.30.3 exact)
# remove: bump react-router-dom-v5-compat to 6.30.4. Do NOT open the cap:
# react-router >=7 requires React 19 and breaks the app-wide CompatRouter
react-router@>=6.7.0 <6.30.4: '>=6.30.4 <7'
# via: msw@1.3.2 (devDep) > inquirer@8 > external-editor@3.1.0 (tmp ^0.0.33)
# remove: upgrade msw to >=2 (drops the inquirer/external-editor chain)
tmp@<0.2.6: '>=0.2.6 <0.3.0'
# via: jest > babel-plugin-macros > cosmiconfig@7 (yaml ^1.10.0);
# typescript-plugin-css-modules > postcss-load-config@3 (^1.10.2)
# remove: blocked — babel-plugin-macros 3.1.0 (latest) still uses cosmiconfig@7
yaml@>=1.0.0 <1.10.3: '>=1.10.3 <2'
trustPolicy: no-downgrade
trustPolicyExclude:
- 'semver@6.3.1 || 5.7.2'

View File

@@ -63,5 +63,6 @@
"INTEGRATIONS_DETAIL": "SigNoz | Integration",
"PUBLIC_DASHBOARD": "SigNoz | Dashboard",
"LLM_OBSERVABILITY_OVERVIEW": "SigNoz | LLM Observability Overview",
"LLM_OBSERVABILITY_CONFIGURATION": "SigNoz | LLM Observability Configuration"
"LLM_OBSERVABILITY_CONFIGURATION": "SigNoz | LLM Observability Configuration",
"LLM_OBSERVABILITY_ATTRIBUTE_MAPPING": "SigNoz | LLM Observability Attribute Mapping"
}

View File

@@ -88,5 +88,6 @@
"INTEGRATIONS_DETAIL": "SigNoz | Integration",
"PUBLIC_DASHBOARD": "SigNoz | Dashboard",
"LLM_OBSERVABILITY_OVERVIEW": "SigNoz | LLM Observability Overview",
"LLM_OBSERVABILITY_CONFIGURATION": "SigNoz | LLM Observability Configuration"
"LLM_OBSERVABILITY_CONFIGURATION": "SigNoz | LLM Observability Configuration",
"LLM_OBSERVABILITY_ATTRIBUTE_MAPPING": "SigNoz | LLM Observability Attribute Mapping"
}

View File

@@ -2082,6 +2082,39 @@ export interface AuthtypesGettableAuthDomainDTO {
updatedAt?: string;
}
export interface AuthtypesGettableRoleDTO {
/**
* @type string
* @format date-time
*/
createdAt?: string;
/**
* @type string
*/
description: string;
/**
* @type string
*/
id: string;
/**
* @type string
*/
name: string;
/**
* @type string
*/
orgId: string;
/**
* @type string
*/
type: string;
/**
* @type string
* @format date-time
*/
updatedAt?: string;
}
export interface AuthtypesGettableTokenDTO {
/**
* @type string
@@ -2325,39 +2358,6 @@ export interface AuthtypesPostableUserRoleDTO {
}
export interface AuthtypesRoleDTO {
/**
* @type string
* @format date-time
*/
createdAt?: string;
/**
* @type string
*/
description: string;
/**
* @type string
*/
id: string;
/**
* @type string
*/
name: string;
/**
* @type string
*/
orgId: string;
/**
* @type string
*/
type: string;
/**
* @type string
* @format date-time
*/
updatedAt?: string;
}
export interface AuthtypesRoleWithTransactionGroupsDTO {
/**
* @type string
* @format date-time
@@ -5669,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;
};
@@ -5813,6 +5846,10 @@ export interface InframonitoringtypesClusterRecordDTO {
* @type string
*/
clusterName: string;
/**
* @type object
*/
counts: InframonitoringtypesClusterRecordDTOCounts;
/**
* @type object,null
*/
@@ -6368,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;
};
@@ -6379,6 +6439,10 @@ export type InframonitoringtypesNamespaceRecordDTOMeta =
InframonitoringtypesNamespaceRecordDTOMetaAnyOf | null;
export interface InframonitoringtypesNamespaceRecordDTO {
/**
* @type object
*/
counts: InframonitoringtypesNamespaceRecordDTOCounts;
/**
* @type object,null
*/
@@ -9194,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',
@@ -9440,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;
/**
@@ -9512,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;
/**
@@ -10614,7 +10685,7 @@ export type ListRoles200 = {
/**
* @type array
*/
data: AuthtypesRoleDTO[];
data: AuthtypesGettableRoleDTO[];
/**
* @type string
*/
@@ -10636,7 +10707,7 @@ export type GetRolePathParameters = {
id: string;
};
export type GetRole200 = {
data: AuthtypesRoleWithTransactionGroupsDTO;
data: AuthtypesRoleDTO;
/**
* @type string
*/
@@ -10852,7 +10923,7 @@ export type ListSpanMappersPathParameters = {
groupId: string;
};
export type ListSpanMappers200 = {
data: SpantypesGettableSpanMapperGroupsDTO;
data: SpantypesGettableSpanMappersDTO;
/**
* @type string
*/

View File

@@ -12,5 +12,4 @@
import '@signozhq/design-tokens';
import '@signozhq/icons';
import '@signozhq/resizable';
import '@signozhq/ui';

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

@@ -247,11 +247,6 @@
var(--l2-background)
) !important;
&,
.ͼ1a {
color: var(--query-builder-v2-color, var(--l2-foreground));
}
::-moz-selection {
background: var(
--query-builder-v2-selection-background-color,

View File

@@ -293,11 +293,6 @@ $max-recents-shown: 5;
var(--l2-background)
) !important;
&,
.ͼ1a {
color: var(--query-builder-v2-color, var(--l2-foreground));
}
::-moz-selection {
background: var(
--query-builder-v2-selection-background-color,

View File

@@ -3,7 +3,7 @@ import { Select } from 'antd';
import { Checkbox } from '@signozhq/ui/checkbox';
import { convertToApiError } from 'api/ErrorResponseHandlerForGeneratedAPIs';
import { useListRoles } from 'api/generated/services/role';
import type { AuthtypesRoleDTO } from 'api/generated/services/sigNoz.schemas';
import type { AuthtypesGettableRoleDTO } from 'api/generated/services/sigNoz.schemas';
import cx from 'classnames';
import APIError from 'types/api/error';
import { popupContainer } from 'utils/selectPopupContainer';
@@ -16,7 +16,7 @@ export interface RoleOption {
}
export function useRoles(): {
roles: AuthtypesRoleDTO[];
roles: AuthtypesGettableRoleDTO[];
isLoading: boolean;
isError: boolean;
error: APIError | undefined;
@@ -33,7 +33,7 @@ export function useRoles(): {
}
export function getRoleOptions(
roles: AuthtypesRoleDTO[],
roles: AuthtypesGettableRoleDTO[],
valueField: 'id' | 'name',
): RoleOption[] {
return roles.map((role) => ({
@@ -79,7 +79,7 @@ interface BaseProps {
placeholder?: string;
className?: string;
getPopupContainer?: (trigger: HTMLElement) => HTMLElement;
roles?: AuthtypesRoleDTO[];
roles?: AuthtypesGettableRoleDTO[];
loading?: boolean;
isError?: boolean;
error?: APIError;

View File

@@ -4,7 +4,7 @@ import { Badge } from '@signozhq/ui/badge';
import { Button } from '@signozhq/ui/button';
import { Input } from '@signozhq/ui/input';
import { useCopyToClipboard } from 'react-use';
import type { AuthtypesRoleDTO } from 'api/generated/services/sigNoz.schemas';
import type { AuthtypesGettableRoleDTO } from 'api/generated/services/sigNoz.schemas';
import AuthZTooltip from 'lib/authz/components/AuthZTooltip/AuthZTooltip';
import { withAuthZContent } from 'lib/authz/components/withAuthZ/withAuthZContent';
import RolesSelect from 'components/RolesSelect';
@@ -28,7 +28,7 @@ interface OverviewTabProps {
localRoles: string[];
onRolesChange: (v: string[]) => void;
isDisabled: boolean;
availableRoles: AuthtypesRoleDTO[];
availableRoles: AuthtypesGettableRoleDTO[];
rolesLoading?: boolean;
rolesError?: boolean;
rolesErrorObj?: APIError | undefined;

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

@@ -53,6 +53,7 @@ import {
defaultTraceSelectedColumns,
} from 'container/OptionsMenu/constants';
import { OptionsQuery } from 'container/OptionsMenu/types';
import { ExportDashboard } from 'hooks/dashboard/useExportDashboards';
import { useGetSearchQueryParam } from 'hooks/queryBuilder/useGetSearchQueryParam';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useGetAllViews } from 'hooks/saveViews/useGetAllViews';
@@ -66,7 +67,6 @@ import { mapCompositeQueryFromQuery } from 'lib/newQueryBuilder/queryBuilderMapp
import { cloneDeep, isEqual, omit } from 'lodash-es';
import { useAppContext } from 'providers/App/App';
import { FormattingOptions } from 'providers/preferences/types';
import { Dashboard } from 'types/api/dashboard/getAll';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
import { ViewProps } from 'types/api/saveViews/types';
import { DataSource, StringOperators } from 'types/common/queryBuilder';
@@ -1031,26 +1031,19 @@ function ExplorerOptions({
/>
</div>
</Modal>
<Modal
footer={null}
onOk={onCancel(false)}
onCancel={onCancel(false)}
<ExportPanelContainer
open={isExport}
centered
destroyOnClose
>
<ExportPanelContainer
query={isOneChartPerQuery ? queryToExport : query}
isLoading={isLoading}
onExport={(dashboard, isNewDashboard): void => {
if (isOneChartPerQuery && queryToExport) {
onExport(dashboard, isNewDashboard, queryToExport);
} else {
onExport(dashboard, isNewDashboard);
}
}}
/>
</Modal>
onClose={onCancel(false)}
query={isOneChartPerQuery ? queryToExport : query}
isLoading={isLoading}
onExport={(dashboard, isNewDashboard): void => {
if (isOneChartPerQuery && queryToExport) {
onExport(dashboard, isNewDashboard, queryToExport);
} else {
onExport(dashboard, isNewDashboard);
}
}}
/>
</div>
);
}
@@ -1058,7 +1051,7 @@ function ExplorerOptions({
export interface ExplorerOptionsProps {
isLoading?: boolean;
onExport: (
dashboard: Dashboard | null,
dashboard: ExportDashboard | null,
isNewDashboard?: boolean,
queryToExport?: Query,
) => void;

View File

@@ -1,6 +1,7 @@
import { useHistory } from 'react-router-dom';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { MOCK_QUERY } from 'container/QueryTable/Drilldown/__tests__/mockTableData';
import { ExportDashboard } from 'hooks/dashboard/useExportDashboards';
import { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard';
import { rest, server } from 'mocks-server/server';
import { render, screen, userEvent, waitFor } from 'tests/test-utils';
@@ -80,7 +81,7 @@ const renderExplorerOptionWrapper = (
isLoading: false,
onExport: jest.fn() as jest.MockedFunction<
(
dashboard: Dashboard | null,
dashboard: ExportDashboard | null,
isNewDashboard?: boolean,
queryToExport?: Query,
) => void
@@ -150,7 +151,7 @@ describe('ExplorerOptionWrapper', () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
const testOnExport = jest.fn() as jest.MockedFunction<
(
dashboard: Dashboard | null,
dashboard: ExportDashboard | null,
isNewDashboard?: boolean,
queryToExport?: Query,
) => void
@@ -179,15 +180,16 @@ describe('ExplorerOptionWrapper', () => {
expect(screen.getByRole('dialog')).toBeInTheDocument();
});
// Click the "New Dashboard" button
const newDashboardButton = screen.getByRole('button', {
name: /new dashboard/i,
});
// Click the "New dashboard" button
const newDashboardButton = screen.getByTestId('export-panel-new-dashboard');
await user.click(newDashboardButton);
// Wait for the API call to complete and onExport to be called
await waitFor(() => {
expect(testOnExport).toHaveBeenCalledWith(mockNewDashboard, true);
expect(testOnExport).toHaveBeenCalledWith(
{ id: NEW_DASHBOARD_ID, title: TEST_DASHBOARD_TITLE },
true,
);
});
});
@@ -195,7 +197,7 @@ describe('ExplorerOptionWrapper', () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
const testOnExport = jest.fn() as jest.MockedFunction<
(
dashboard: Dashboard | null,
dashboard: ExportDashboard | null,
isNewDashboard?: boolean,
queryToExport?: Query,
) => void
@@ -229,13 +231,12 @@ describe('ExplorerOptionWrapper', () => {
expect(screen.getByRole('dialog')).toBeInTheDocument();
});
// Wait for dashboards to load and then click on the dashboard select dropdown
// Wait for the dashboard select dropdown to render inside the dialog
const modal = screen.getByRole('dialog');
await waitFor(() => {
expect(screen.getByText('Select Dashboard')).toBeInTheDocument();
expect(modal.querySelector('[role="combobox"]')).toBeTruthy();
});
// Get the modal and find the dashboard select dropdown within it
const modal = screen.getByRole('dialog');
const dashboardSelect = modal.querySelector(
'[role="combobox"]',
) as HTMLElement;
@@ -251,19 +252,21 @@ describe('ExplorerOptionWrapper', () => {
const dashboardOption = screen.getByText(mockDashboard1.data.title);
await user.click(dashboardOption);
// Wait for the selection to be made and the Export button to be enabled
// Wait for the selection to be made and the export button to be enabled
await waitFor(() => {
const exportButton = screen.getByRole('button', { name: /export/i });
expect(exportButton).not.toBeDisabled();
expect(screen.getByTestId('export-panel-export')).not.toBeDisabled();
});
// Click the Export button
const exportButton = screen.getByRole('button', { name: /export/i });
// Click the export button
const exportButton = screen.getByTestId('export-panel-export');
await user.click(exportButton);
// Wait for onExport to be called with the selected dashboard
await waitFor(() => {
expect(testOnExport).toHaveBeenCalledWith(mockDashboard1, false);
expect(testOnExport).toHaveBeenCalledWith(
{ id: 'dashboard-1', title: 'Dashboard 1' },
false,
);
});
});
@@ -284,7 +287,7 @@ describe('ExplorerOptionWrapper', () => {
// Create a real handleExport function similar to LogsExplorerViews
// This should NOT call useUpdateDashboard (as per PR #8029)
const handleExport = (dashboard: Dashboard | null): void => {
const handleExport = (dashboard: ExportDashboard | null): void => {
if (!dashboard) {
return;
}
@@ -326,13 +329,12 @@ describe('ExplorerOptionWrapper', () => {
expect(screen.getByRole('dialog')).toBeInTheDocument();
});
// Wait for dashboards to load and then click on the dashboard select dropdown
// Wait for the dashboard select dropdown to render inside the dialog
const modal = screen.getByRole('dialog');
await waitFor(() => {
expect(screen.getByText('Select Dashboard')).toBeInTheDocument();
expect(modal.querySelector('[role="combobox"]')).toBeTruthy();
});
// Get the modal and find the dashboard select dropdown within it
const modal = screen.getByRole('dialog');
const dashboardSelect = modal.querySelector(
'[role="combobox"]',
) as HTMLElement;
@@ -348,14 +350,13 @@ describe('ExplorerOptionWrapper', () => {
const dashboardOption = screen.getByText(mockDashboard.data.title);
await user.click(dashboardOption);
// Wait for the selection to be made and the Export button to be enabled
// Wait for the selection to be made and the export button to be enabled
await waitFor(() => {
const exportButton = screen.getByRole('button', { name: /export/i });
expect(exportButton).not.toBeDisabled();
expect(screen.getByTestId('export-panel-export')).not.toBeDisabled();
});
// Click the Export button
const exportButton = screen.getByRole('button', { name: /export/i });
// Click the export button
const exportButton = screen.getByTestId('export-panel-export');
await user.click(exportButton);
// Wait for the handleExport function to be called and navigation to occur
@@ -375,7 +376,7 @@ describe('ExplorerOptionWrapper', () => {
it('should not show export buttons when component is disabled', () => {
const testOnExport = jest.fn() as jest.MockedFunction<
(
dashboard: Dashboard | null,
dashboard: ExportDashboard | null,
isNewDashboard?: boolean,
queryToExport?: Query,
) => void

View File

@@ -0,0 +1,74 @@
import { useMemo } from 'react';
// eslint-disable-next-line signoz/no-antd-components
import { Select, SelectProps } from 'antd';
import { ExportDashboard } from 'hooks/dashboard/useExportDashboards';
import { getSelectOptions } from './utils';
import styles from './ExportPanel.module.scss';
interface ExportDashboardSelectProps {
dashboards: ExportDashboard[];
value: string | null;
/** The picked dashboard, pinned as an option so its label survives a later search. */
selectedDashboard: ExportDashboard | null;
loading?: boolean;
disabled?: boolean;
onChange: (dashboardId: string) => void;
onSearch: (search: string) => void;
}
/**
* Dashboard picker for the "Add to dashboard" dialog. Server-side search (`filterOption`
* off, typing via `onSearch`); the selected dashboard is pinned as an option so its label
* survives a narrowing search, and `getPopupContainer` keeps the overlay from clipping the
* dropdown.
*/
function ExportDashboardSelect({
dashboards,
value,
selectedDashboard,
loading,
disabled,
onChange,
onSearch,
}: ExportDashboardSelectProps): JSX.Element {
const options = useMemo<SelectProps['options']>(() => {
const base = getSelectOptions(dashboards) ?? [];
if (
selectedDashboard &&
!base.some((option) => option.value === selectedDashboard.id)
) {
return [
{ label: selectedDashboard.title, value: selectedDashboard.id },
...base,
];
}
return base;
}, [dashboards, selectedDashboard]);
return (
<Select
className={styles.dashboardSelect}
placeholder="Select a dashboard"
showSearch
filterOption={false}
loading={loading}
disabled={disabled}
value={value}
onChange={onChange}
onSearch={onSearch}
data-testid="export-dashboard-select"
options={options}
getPopupContainer={(trigger): HTMLElement =>
trigger.parentElement ?? document.body
}
/>
);
}
ExportDashboardSelect.defaultProps = {
loading: false,
disabled: false,
};
export default ExportDashboardSelect;

View File

@@ -0,0 +1,42 @@
.body {
display: flex;
flex-direction: column;
gap: 20px;
}
.field {
display: flex;
flex-direction: column;
gap: 6px;
}
.label {
font-size: 12px;
color: var(--l2-foreground);
}
.dashboardSelect {
width: 100%;
}
.newDashboard {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
padding-top: 16px;
border-top: 1px solid var(--l2-border);
}
.hint {
font-size: 13px;
color: var(--l2-foreground);
}
.footer {
display: flex;
align-items: center;
justify-content: flex-end;
gap: 8px;
width: 100%;
}

View File

@@ -1,127 +1,159 @@
import { useCallback, useMemo, useState } from 'react';
import { useCallback, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useMutation } from 'react-query';
import { Button } from 'antd';
import { Plus } from '@signozhq/icons';
import { Button } from '@signozhq/ui/button';
import { DialogWrapper } from '@signozhq/ui/dialog';
import { Typography } from '@signozhq/ui/typography';
import createDashboard from 'api/v1/dashboards/create';
import { ENTITY_VERSION_V5 } from 'constants/app';
import { useGetAllDashboard } from 'hooks/dashboard/useGetAllDashboard';
import { useErrorModal } from 'providers/ErrorModalProvider';
import APIError from 'types/api/error';
import { ExportPanelProps } from '.';
import { useCreateExportDashboard } from 'hooks/dashboard/useCreateExportDashboard';
import {
DashboardSelect,
NewDashboardButton,
SelectWrapper,
Title,
Wrapper,
} from './styles';
import { filterOptions, getSelectOptions } from './utils';
ExportDashboard,
useExportDashboards,
} from 'hooks/dashboard/useExportDashboards';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
import ExportDashboardSelect from './ExportDashboardSelect';
import styles from './ExportPanel.module.scss';
export interface ExportPanelProps {
isLoading?: boolean;
onExport: (
dashboard: ExportDashboard | null,
isNewDashboard?: boolean,
) => void;
query: Query | null;
/** Controlled open state of the dialog. */
open: boolean;
/** Called when the dialog requests to close (Cancel / overlay / Esc). */
onClose: () => void;
}
/**
* "Add to dashboard" dialog: export the panel into an existing dashboard or a newly
* created one. Navigation is the caller's job via `onExport` (flag-aware V1/V2 editor).
*/
function ExportPanelContainer({
isLoading,
onExport,
open,
onClose,
}: ExportPanelProps): JSX.Element {
const { t } = useTranslation(['dashboard']);
const [dashboardId, setDashboardId] = useState<string | null>(null);
// Track the object, not just the id, so export survives a search that narrows it out.
const [selectedDashboard, setSelectedDashboard] =
useState<ExportDashboard | null>(null);
const [searchText, setSearchText] = useState('');
const {
data,
dashboards,
isLoading: isAllDashboardsLoading,
refetch,
} = useGetAllDashboard();
isFetching: isDashboardsFetching,
} = useExportDashboards(searchText);
const { showErrorModal } = useErrorModal();
const { mutate: createNewDashboard, isLoading: createDashboardLoading } =
useMutation(createDashboard, {
onSuccess: (data) => {
if (data.data) {
onExport(data?.data, true);
}
refetch();
},
onError: (error) => {
showErrorModal(error as APIError);
},
const { create: createNewDashboard, isLoading: createDashboardLoading } =
useCreateExportDashboard({
title: t('new_dashboard_title', { ns: 'dashboard' }),
onCreated: (dashboard) => onExport(dashboard, true),
});
const options = useMemo(() => getSelectOptions(data?.data || []), [data]);
const handleExportClick = useCallback((): void => {
const currentSelectedDashboard = data?.data?.find(
({ id }) => id === dashboardId,
);
onExport(currentSelectedDashboard || null, false);
}, [data, dashboardId, onExport]);
// Reset on close so each open starts fresh (the dialog stays mounted).
useEffect(() => {
if (!open) {
setSelectedDashboard(null);
setSearchText('');
}
}, [open]);
const handleSelect = useCallback(
(selectedDashboardId: string): void => {
setDashboardId(selectedDashboardId);
(dashboardId: string): void => {
setSelectedDashboard(
dashboards.find(({ id }) => id === dashboardId) ?? null,
);
},
[setDashboardId],
[dashboards],
);
const handleNewDashboard = useCallback(async () => {
try {
await createNewDashboard({
title: t('new_dashboard_title', {
ns: 'dashboard',
}),
uploadedGrafana: false,
version: ENTITY_VERSION_V5,
});
} catch (error) {
showErrorModal(error as APIError);
}
}, [createNewDashboard, t, showErrorModal]);
const handleExportClick = useCallback((): void => {
onExport(selectedDashboard, false);
}, [selectedDashboard, onExport]);
const isDashboardLoading = isAllDashboardsLoading || createDashboardLoading;
const isDisabled =
isAllDashboardsLoading || !options?.length || !dashboardId || isLoading;
const isExportDisabled =
isAllDashboardsLoading || !selectedDashboard || isLoading;
return (
<Wrapper direction="vertical">
<Title>Export Panel</Title>
<DialogWrapper
open={open}
onOpenChange={(isOpen): void => {
if (!isOpen) {
onClose();
}
}}
title="Add to dashboard"
testId="export-panel-dialog"
footer={
<div className={styles.footer}>
<Button
variant="outlined"
color="secondary"
size="md"
onClick={onClose}
testId="export-panel-cancel"
>
Cancel
</Button>
<Button
color="primary"
size="md"
loading={isLoading}
disabled={isExportDisabled}
onClick={handleExportClick}
testId="export-panel-export"
>
Add to dashboard
</Button>
</div>
}
>
<div className={styles.body}>
<div className={styles.field}>
<Typography.Text className={styles.label}>
Select a dashboard
</Typography.Text>
<ExportDashboardSelect
dashboards={dashboards}
value={selectedDashboard?.id ?? null}
selectedDashboard={selectedDashboard}
loading={isDashboardsFetching}
disabled={isAllDashboardsLoading || createDashboardLoading}
onChange={handleSelect}
onSearch={setSearchText}
/>
</div>
<SelectWrapper direction="horizontal">
<DashboardSelect
placeholder="Select Dashboard"
options={options}
showSearch
loading={isDashboardLoading}
disabled={isDashboardLoading}
value={dashboardId}
onSelect={handleSelect}
filterOption={filterOptions}
/>
<Button
type="primary"
loading={isLoading}
disabled={isDisabled}
onClick={handleExportClick}
>
Export
</Button>
</SelectWrapper>
<Typography>
Or create dashboard with this panel -
<NewDashboardButton
disabled={createDashboardLoading}
loading={createDashboardLoading}
type="link"
onClick={handleNewDashboard}
>
New Dashboard
</NewDashboardButton>
</Typography>
</Wrapper>
<div className={styles.newDashboard}>
<Typography.Text className={styles.hint}>
Or create a new dashboard with this panel
</Typography.Text>
<Button
variant="outlined"
color="secondary"
size="md"
prefix={<Plus size={14} />}
loading={createDashboardLoading}
disabled={createDashboardLoading}
onClick={createNewDashboard}
testId="export-panel-new-dashboard"
>
New dashboard
</Button>
</div>
</div>
</DialogWrapper>
);
}
ExportPanelContainer.defaultProps = {
isLoading: false,
};
export default ExportPanelContainer;

View File

@@ -1,49 +0,0 @@
import { useCallback, useState } from 'react';
import { Modal } from 'antd';
import { Dashboard } from 'types/api/dashboard/getAll';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
import ExportPanelContainer from './ExportPanelContainer';
function ExportPanel({
isLoading,
onExport,
query,
}: ExportPanelProps): JSX.Element {
const [isExport, setIsExport] = useState<boolean>(false);
const onModalToggle = useCallback((value: boolean) => {
setIsExport(value);
}, []);
const onCancel = (value: boolean) => (): void => {
onModalToggle(value);
};
return (
<Modal
footer={null}
onOk={onCancel(false)}
onCancel={onCancel(false)}
open={isExport}
centered
destroyOnClose
>
<ExportPanelContainer
query={query}
isLoading={isLoading}
onExport={onExport}
/>
</Modal>
);
}
export interface ExportPanelProps {
isLoading?: boolean;
onExport: (dashboard: Dashboard | null, isNewDashboard?: boolean) => void;
query: Query | null;
}
ExportPanel.defaultProps = { isLoading: false };
export default ExportPanel;

View File

@@ -1,34 +0,0 @@
import { FunctionComponent } from 'react';
import { Button, Select, SelectProps, Space } from 'antd';
import { Typography } from '@signozhq/ui/typography';
import styled from 'styled-components';
export const DashboardSelect: FunctionComponent<SelectProps> = styled(
Select,
)<SelectProps>`
width: 100%;
`;
export const SelectWrapper = styled(Space)`
width: 100%;
margin-bottom: 1rem;
.ant-space-item:first-child {
width: 100%;
max-width: 20rem;
}
`;
export const Wrapper = styled(Space)`
width: 100%;
`;
export const NewDashboardButton = styled(Button)`
&&& {
padding: 0 0.125rem;
}
`;
export const Title = styled(Typography.Text)`
font-size: 1rem;
`;

View File

@@ -1,16 +1,10 @@
import { SelectProps } from 'antd';
import { Dashboard } from 'types/api/dashboard/getAll';
import { ExportDashboard } from 'hooks/dashboard/useExportDashboards';
export const getSelectOptions = (data: Dashboard[]): SelectProps['options'] =>
data.map(({ id, data }) => ({
label: data.title,
export const getSelectOptions = (
data: ExportDashboard[],
): SelectProps['options'] =>
data.map(({ id, title }) => ({
label: title,
value: id,
}));
export const filterOptions: SelectProps['filterOption'] = (
input,
options,
): boolean =>
(options?.label?.toString() ?? '')
?.toLowerCase()
.includes(input.toLowerCase());

View File

@@ -1,13 +1,17 @@
import { useCallback, useEffect, useRef, useState } from 'react';
import { Button, Tooltip } from 'antd';
import { Typography } from '@signozhq/ui/typography';
import { convertToApiError } from 'api/ErrorResponseHandlerForGeneratedAPIs';
import { listHosts } from 'api/generated/services/inframonitoring';
import {
InframonitoringtypesHostRecordDTO,
InframonitoringtypesHostStatusDTO,
InframonitoringtypesResponseTypeDTO,
Querybuildertypesv5OrderDirectionDTO,
RenderErrorResponseDTO,
} from 'api/generated/services/sigNoz.schemas';
import { AxiosError } from 'axios';
import APIError from 'types/api/error';
import QuickFilters from 'components/QuickFilters/QuickFilters';
import { QuickFiltersSource } from 'components/QuickFilters/types';
import { InfraMonitoringEvents } from 'constants/events';
@@ -122,13 +126,12 @@ function Hosts(): JSX.Element {
endTimeBeforeRetention: data.endTimeBeforeRetention,
};
} catch (error) {
const errMsg =
error instanceof Error ? error.message : 'Failed to fetch hosts';
return {
type: 'list' as const,
records: [] as InframonitoringtypesHostRecordDTO[],
total: 0,
error: errMsg,
error:
convertToApiError(error as AxiosError<RenderErrorResponseDTO>) ?? null,
};
}
},
@@ -141,7 +144,7 @@ function Hosts(): JSX.Element {
signal?: AbortSignal,
): Promise<{
data: InframonitoringtypesHostRecordDTO | null;
error?: string | null;
error?: APIError | null;
}> => {
try {
const response = await listHosts(
@@ -159,11 +162,10 @@ function Hosts(): JSX.Element {
data: response.data.records.length > 0 ? response.data.records[0] : null,
};
} catch (error) {
const errMsg =
error instanceof Error ? error.message : 'Failed to fetch host';
return {
data: null,
error: errMsg,
error:
convertToApiError(error as AxiosError<RenderErrorResponseDTO>) ?? null,
};
}
},

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

@@ -11,8 +11,6 @@ import { INFRA_MONITORING_ATTR_KEYS } from 'container/InfraMonitoringK8sV2/const
import { CellValueTooltip } from 'container/InfraMonitoringK8sV2/components';
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { DataSource } from 'types/common/queryBuilder';
import TanStackTable from 'components/TanStackTableView';
const HOSTNAME_DOCS_URL =
'https://signoz.io/docs/infrastructure-monitoring/hostmetrics/#host-name-is-blankempty';
@@ -23,11 +21,7 @@ export function HostnameCell({
}): React.ReactElement {
const isEmpty = !hostName || !hostName.trim();
if (!isEmpty) {
return (
<CellValueTooltip value={hostName}>
<TanStackTable.Text>{hostName}</TanStackTable.Text>
</CellValueTooltip>
);
return <CellValueTooltip value={hostName} />;
}
return (
<>

View File

@@ -11,6 +11,6 @@
}
.columnHeaderLabel {
text-align: center;
text-align: left;
padding: var(--spacing-2) var(--spacing-2) var(--spacing-2) 0px;
}

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

@@ -12,6 +12,8 @@ import { ToggleGroupSimple } from '@signozhq/ui/toggle-group';
import { Divider } from '@signozhq/ui/divider';
import { Typography } from '@signozhq/ui/typography';
import logEvent from 'api/common/logEvent';
import ErrorContent from 'components/ErrorModal/components/ErrorContent';
import APIError from 'types/api/error';
import { combineInitialAndUserExpression } from 'components/QueryBuilderV2/QueryV2/QuerySearch/utils';
import { InfraMonitoringEvents } from 'constants/events';
import { QueryParams } from 'constants/query';
@@ -52,9 +54,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 +65,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,29 +78,51 @@ 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;
end: number;
}
export interface CustomTabRenderProps<T> {
entity: T;
timeRange: { startTime: number; endTime: number };
selectedInterval: Time;
handleTimeChange: (
interval: Time | CustomTimeType,
dateTimeRange?: [number, number],
) => void;
}
export interface CustomTab<T> {
key: string;
label: string;
icon: React.ReactNode;
render: (props: CustomTabRenderProps<T>) => React.ReactNode;
}
export interface K8sBaseDetailsProps<T> {
category: InfraMonitoringEntity;
eventCategory: string;
// Data fetching configuration
getSelectedItemExpression: (selectedItem: string) => string;
getSelectedItemExpression: (params: SelectedItemParams) => string;
fetchEntityData: (
filters: K8sDetailsFilters,
signal?: AbortSignal,
) => Promise<{ data: T | null; error?: string | null }>;
) => Promise<{ data: T | null; error?: APIError | null }>;
// Entity configuration
getEntityName: (entity: T) => string;
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,
@@ -110,20 +139,7 @@ export interface K8sBaseDetailsProps<T> {
showTraces?: boolean;
showEvents?: boolean;
};
customTabs?: Array<{
key: string;
label: string;
icon: React.ReactNode;
render: (props: {
entity: T;
timeRange: { startTime: number; endTime: number };
selectedInterval: Time;
handleTimeChange: (
interval: Time | CustomTimeType,
dateTimeRange?: [number, number],
) => void;
}) => React.ReactNode;
}>;
customTabs?: Array<CustomTab<T>>;
}
// eslint-disable-next-line sonarjs/cognitive-complexity
@@ -136,6 +152,8 @@ export default function K8sBaseDetails<T>({
getInitialLogTracesExpression,
getInitialEventsExpression,
metadataConfig,
countsConfig,
getCountsFilterExpression,
entityWidgetInfo,
getEntityQueryPayload,
queryKeyPrefix,
@@ -152,7 +170,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 +180,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 +207,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 +232,8 @@ export default function K8sBaseDetails<T>({
}, [entity, getInitialEventsExpression]);
const handleClose = useCallback((): void => {
setSelectedItem(null);
}, [setSelectedItem]);
setSelectedItemParams(null);
}, [setSelectedItemParams]);
const entityName = entity ? getEntityName(entity) : '';
@@ -246,6 +275,33 @@ export default function K8sBaseDetails<T>({
const [selectedView, setSelectedView] = useInfraMonitoringView();
const effectiveView = hideDetailViewTabs ? VIEW_TYPES.METRICS : selectedView;
const validTabs = useMemo(() => {
const tabs: string[] = [];
if (tabVisibility.showMetrics) {
tabs.push(VIEW_TYPES.METRICS);
}
if (tabVisibility.showLogs) {
tabs.push(VIEW_TYPES.LOGS);
}
if (tabVisibility.showTraces) {
tabs.push(VIEW_TYPES.TRACES);
}
if (tabVisibility.showEvents) {
tabs.push(VIEW_TYPES.EVENTS);
}
if (customTabs) {
tabs.push(...customTabs.map((t) => t.key));
}
return tabs;
}, [tabVisibility, customTabs]);
useEffect(() => {
if (!hideDetailViewTabs && !validTabs.includes(selectedView)) {
const firstValid = validTabs[0] || VIEW_TYPES.METRICS;
void setSelectedView(firstValid);
}
}, [hideDetailViewTabs, selectedView, validTabs, setSelectedView]);
const [, setLogFiltersParam] = useInfraMonitoringLogFilters();
const [, setTracesFiltersParam] = useInfraMonitoringTracesFilters();
const [, setEventsFiltersParam] = useInfraMonitoringEventsFilters();
@@ -281,7 +337,10 @@ export default function K8sBaseDetails<T>({
}
}, [getMinMaxTime, selectedTime]);
const handleTabChange = (value: string): void => {
const handleTabChange = (value: string | null): void => {
if (!value) {
return;
}
setSelectedView(value);
setLogFiltersParam(null);
setTracesFiltersParam(null);
@@ -424,12 +483,18 @@ export default function K8sBaseDetails<T>({
{isEntityLoading && <LoadingContainer />}
{(isEntityError || hasResponseError) && (
<div className="entity-error-container">
<Typography.Text color="danger">
{entityResponse?.error ||
(entityError instanceof Error
? entityError.message
: 'Failed to load entity details')}
</Typography.Text>
<ErrorContent
error={
entityResponse?.error ??
(entityError instanceof APIError ? entityError : null) ?? {
code: 500,
message:
entityError instanceof Error
? entityError.message
: 'Failed to load entity details',
}
}
/>
</div>
)}
{entity && !isEntityLoading && !hasResponseError && (
@@ -467,6 +532,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,19 +10,21 @@ 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';
import { openInNewTab } from 'utils/navigation';
import APIError from 'types/api/error';
import {
INFRA_MONITORING_K8S_PARAMS_KEYS,
InfraMonitoringEntity,
} from '../constants';
import {
SelectedItemParams,
useInfraMonitoringGroupBy,
useInfraMonitoringOrderBy,
useInfraMonitoringSelectedItemParams,
useInfraMonitoringStatusFilter,
} from '../hooks';
import { useInfraMonitoringLineClamp } from '../components';
@@ -32,13 +34,14 @@ import K8sHeader from './K8sHeader';
import { K8sPaginationWarning } from './K8sPaginationWarning';
import { K8sBaseFilters } from './types';
import { getGroupedByMeta } from './utils';
import { K8sInstrumentationChecksCallout } from './components/K8sInstrumentationChecksCallout/K8sInstrumentationChecksCallout';
import styles from './K8sBaseList.module.scss';
import cx from 'classnames';
export type K8sBaseListEmptyStateContext = {
isError: boolean;
error?: string | null;
error?: APIError | null;
totalCount: number;
hasFilters: boolean;
isLoading: boolean;
@@ -49,7 +52,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;
@@ -62,15 +68,15 @@ export type K8sBaseListProps<T extends K8sEntityData> = {
records?: T[];
data?: T[];
total: number;
error?: string | null;
error?: APIError | null;
rawData?: unknown;
endTimeBeforeRetention?: boolean;
warning?: Querybuildertypesv5QueryWarnDataDTO | null;
}>;
/** 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 +84,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 +98,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 +234,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 +253,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 +315,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}
@@ -338,6 +379,8 @@ export function K8sBaseList<T extends K8sEntityData>({
cancelQuery={cancelQuery}
/>
<div ref={containerRef} className={styles.tableContainer}>
<K8sInstrumentationChecksCallout entity={entity} />
{isError && (
<Typography>
{data?.error?.toString() || 'Something went wrong'}
@@ -347,7 +390,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

@@ -44,13 +44,11 @@
width: 32px;
}
.errorIcon {
color: var(--danger-background);
}
.actions {
.errorContent {
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
margin-top: 16px;
gap: var(--spacing-3);
max-width: 500px;
padding: 24px;
}

View File

@@ -1,8 +1,4 @@
import { useCallback } from 'react';
import { Button } from '@signozhq/ui/button';
import { useGetTenantLicense } from 'hooks/useGetTenantLicense';
import history from 'lib/history';
import { LifeBuoy, TriangleAlert } from '@signozhq/icons';
import ErrorContent from 'components/ErrorModal/components/ErrorContent';
import emptyStateUrl from '@/assets/Icons/emptyState.svg';
import eyesEmojiUrl from '@/assets/Images/eyesEmoji.svg';
@@ -13,26 +9,12 @@ import styles from './K8sEmptyState.module.scss';
type K8sEmptyStateProps = Partial<K8sBaseListEmptyStateContext>;
const handleContactSupport = (isCloudUser: boolean): void => {
if (isCloudUser) {
history.push('/support');
} else {
window.open('https://signoz.io/slack', '_blank');
}
};
export function K8sEmptyState({
isError,
error,
isLoading,
endTimeBeforeRetention,
}: K8sEmptyStateProps): JSX.Element | null {
const { isCloudUser } = useGetTenantLicense();
const handleSupport = useCallback(() => {
handleContactSupport(isCloudUser);
}, [isCloudUser]);
if (isLoading) {
return null;
}
@@ -40,25 +22,15 @@ export function K8sEmptyState({
if (isError || error) {
return (
<div className={styles.container}>
<div className={styles.content}>
<TriangleAlert size={32} className={styles.errorIcon} />
<span className={styles.message}>
{error || 'An error occurred while fetching data.'}
</span>
<p>
Our team is getting on top to resolve this. Please reach out to support if
the issue persists.
</p>
<div className={styles.actions}>
<Button
onClick={handleSupport}
variant="solid"
color="secondary"
prefix={<LifeBuoy size={14} />}
>
Contact Support
</Button>
</div>
<div className={styles.errorContent}>
<ErrorContent
error={
error ?? {
code: 500,
message: 'An error occurred while fetching data.',
}
}
/>
</div>
</div>
);

View File

@@ -4,6 +4,7 @@ import { useLocation } from 'react-router-dom';
import { Querybuildertypesv5QueryWarnDataDTO } from 'api/generated/services/sigNoz.schemas';
import { Button } from '@signozhq/ui/button';
import { Typography } from '@signozhq/ui/typography';
import APIError from 'types/api/error';
import TanStackTable, {
SortState,
TableColumnDef,
@@ -22,10 +23,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 +36,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 */
@@ -50,7 +52,7 @@ export type K8sExpandedRowProps<T> = {
records?: T[];
data?: T[];
total: number;
error?: string | null;
error?: APIError | null;
rawData?: unknown;
warning?: Querybuildertypesv5QueryWarnDataDTO | null;
}>;
@@ -59,10 +61,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 +73,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 +177,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 +249,7 @@ export function K8sExpandedRow<T>({
<div data-testid="expanded-table">
<TanStackTableStateProvider>
<TanStackTable<T>
<TanStackTable<T, TItemKey>
data={expandedData}
columns={tableColumns}
columnStorageKey={storageKey}

View File

@@ -0,0 +1,221 @@
import { Box } from '@signozhq/icons';
import { screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { NuqsTestingAdapter } from 'nuqs/adapters/testing';
import { act, render, waitFor } from 'tests/test-utils';
import {
InfraMonitoringEntity,
INFRA_MONITORING_K8S_PARAMS_KEYS,
VIEW_TYPES,
} from '../../constants';
import K8sBaseDetails from '../K8sBaseDetails';
jest.mock('container/TopNav/DateTimeSelectionV2/index.tsx', () => ({
__esModule: true,
default: (): JSX.Element => <div data-testid="mock-datetime" />,
}));
type TestEntity = {
name: string;
namespace: string;
cluster: string;
};
const mockEntity: TestEntity = {
name: 'test-pod',
namespace: 'default',
cluster: 'test-cluster',
};
function createBaseProps() {
return {
category: InfraMonitoringEntity.PODS,
eventCategory: 'Pod',
getSelectedItemExpression: (): string => 'k8s.pod.name = "test-pod"',
fetchEntityData: jest
.fn()
.mockResolvedValue({ data: mockEntity, error: null }),
getEntityName: (e: TestEntity): string => e.name,
getInitialLogTracesExpression: (): string => 'k8s.pod.name = "test-pod"',
getInitialEventsExpression: (): string => 'k8s.pod.name = "test-pod"',
metadataConfig: [
{ label: 'Name', getValue: (e: TestEntity): string => e.name },
],
entityWidgetInfo: [{ title: 'CPU', yAxisUnit: 'percent' }],
getEntityQueryPayload: jest.fn().mockReturnValue([]),
queryKeyPrefix: 'testPod',
};
}
interface RenderOptions {
view?: string;
tabsConfig?: {
showMetrics?: boolean;
showLogs?: boolean;
showTraces?: boolean;
showEvents?: boolean;
};
customTabs?: Array<{
key: string;
label: string;
icon: React.ReactNode;
render: () => React.ReactNode;
}>;
}
function renderK8sBaseDetails({
view = VIEW_TYPES.METRICS,
tabsConfig,
customTabs,
}: RenderOptions = {}) {
const searchParams: Record<string, string> = {
[INFRA_MONITORING_K8S_PARAMS_KEYS.SELECTED_ITEM]: 'test-pod',
[INFRA_MONITORING_K8S_PARAMS_KEYS.VIEW]: view,
};
return render(
<NuqsTestingAdapter searchParams={searchParams}>
<K8sBaseDetails<TestEntity>
{...createBaseProps()}
tabsConfig={tabsConfig}
customTabs={customTabs}
/>
</NuqsTestingAdapter>,
);
}
function getSelectedTabText(): string | null {
const selectedTab = document.querySelector('[aria-checked="true"]');
return selectedTab?.textContent ?? null;
}
describe('K8sBaseDetails - Tab Validation', () => {
it('should reset view to METRICS when selected view is invalid', async () => {
act(() => {
renderK8sBaseDetails({ view: 'invalid-tab' });
});
await waitFor(() => {
expect(screen.getAllByText('test-pod').length).toBeGreaterThan(0);
});
await waitFor(() => {
expect(getSelectedTabText()).toContain('Metrics');
});
});
it('should reset to first available tab when METRICS is disabled and view is invalid', async () => {
act(() => {
renderK8sBaseDetails({
view: 'invalid-tab',
tabsConfig: { showMetrics: false },
});
});
await waitFor(() => {
expect(screen.getAllByText('test-pod').length).toBeGreaterThan(0);
});
await waitFor(() => {
expect(getSelectedTabText()).toContain('Logs');
});
});
it('should reset to custom tab when all standard tabs disabled and custom tab exists', async () => {
const customTabKey = 'pod-metrics';
act(() => {
renderK8sBaseDetails({
view: 'invalid-tab',
tabsConfig: {
showMetrics: false,
showLogs: false,
showTraces: false,
showEvents: false,
},
customTabs: [
{
key: customTabKey,
label: 'Pod Metrics',
icon: <Box size={14} />,
render: (): React.ReactNode => <div>Custom Tab</div>,
},
],
});
});
await waitFor(() => {
expect(screen.getAllByText('test-pod').length).toBeGreaterThan(0);
});
await waitFor(() => {
expect(getSelectedTabText()).toContain('Pod Metrics');
});
});
it('should NOT reset view when selected view is valid', async () => {
act(() => {
renderK8sBaseDetails({ view: VIEW_TYPES.LOGS });
});
await waitFor(() => {
expect(screen.getAllByText('test-pod').length).toBeGreaterThan(0);
});
await waitFor(() => {
expect(getSelectedTabText()).toContain('Logs');
});
});
it('should NOT reset view when custom tab is selected and exists', async () => {
const customTabKey = 'pod-metrics';
act(() => {
renderK8sBaseDetails({
view: customTabKey,
customTabs: [
{
key: customTabKey,
label: 'Pod Metrics',
icon: <Box size={14} />,
render: (): React.ReactNode => <div>Custom Tab</div>,
},
],
});
});
await waitFor(() => {
expect(screen.getAllByText('test-pod').length).toBeGreaterThan(0);
});
await waitFor(() => {
expect(getSelectedTabText()).toContain('Pod Metrics');
});
});
it('should keep the selected tab when the active tab is clicked again (untoggle guard)', async () => {
const user = userEvent.setup();
act(() => {
renderK8sBaseDetails({ view: VIEW_TYPES.LOGS });
});
await waitFor(() => {
expect(screen.getAllByText('test-pod').length).toBeGreaterThan(0);
});
await waitFor(() => {
expect(getSelectedTabText()).toContain('Logs');
});
const selectedTab = document.querySelector('[aria-checked="true"]');
expect(selectedTab).not.toBeNull();
await user.click(selectedTab as Element);
await waitFor(() => {
expect(getSelectedTabText()).toContain('Logs');
});
});
});

View File

@@ -9,6 +9,8 @@ import { TooltipProvider } from '@signozhq/ui/tooltip';
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { InfraMonitoringEvents } from 'constants/events';
import { server } from 'mocks-server/server';
import { rest } from 'msw';
import {
NuqsTestingAdapter,
OnUrlUpdateFunction,
@@ -17,11 +19,13 @@ import {
import { AppProvider } from 'providers/App/App';
import TimezoneProvider from 'providers/Timezone';
import store from 'store';
import APIError from 'types/api/error';
import { openInNewTab } from 'utils/navigation';
import { TableColumnDef } from 'components/TanStackTableView';
import { InfraMonitoringEntity } from '../../constants';
import { SelectedItemParams } from '../../hooks';
window.ResizeObserver =
window.ResizeObserver ||
@@ -165,11 +169,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 +203,7 @@ function renderComponent<T extends K8sEntityData>({
value={{ viewportHeight: 800, itemHeight: 50 }}
>
<TooltipProvider>
<K8sBaseList {...props} />
<K8sBaseList<T, TItemKey> {...props} />
</TooltipProvider>
</VirtuosoMockContext.Provider>
</NuqsTestingAdapter>
@@ -626,7 +633,15 @@ describe('K8sBaseList', () => {
fetchListDataMock.mockResolvedValue({
data: [],
total: 0,
error: 'Failed to fetch pods',
error: new APIError({
httpStatusCode: 500,
error: {
code: '500',
message: 'Failed to fetch pods',
url: '',
errors: [],
},
}),
});
renderComponent<TestItem>({
@@ -941,4 +956,413 @@ 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');
});
});
describe('instrumentation checks callout', () => {
const fetchListDataMock = jest.fn<
ReturnType<NonNullable<K8sBaseListProps<TestItem>['fetchListData']>>,
Parameters<NonNullable<K8sBaseListProps<TestItem>['fetchListData']>>
>();
beforeEach(() => {
fetchListDataMock.mockClear();
fetchListDataMock.mockResolvedValue({
data: [{ id: 'item-1' }],
total: 1,
error: null,
});
});
it('should not render callout when ready is true', async () => {
server.use(
rest.get('http://localhost/api/v2/infra_monitoring/checks', (_, res, ctx) =>
res(
ctx.json({
status: 'success',
data: {
ready: true,
type: 'pods',
presentDefaultEnabledMetrics: [
{
associatedComponent: { name: 'otel-collector' },
metrics: ['k8s.pod.cpu.usage'],
},
],
},
}),
),
),
);
renderComponent<TestItem>({
entity: InfraMonitoringEntity.PODS,
eventCategory: InfraMonitoringEvents.Pod,
fetchListData: fetchListDataMock,
tableColumns: createTestColumns(),
getRowKey: (row): string => row.id,
getItemKey: (row): string => row.id,
});
await screen.findByText('item-1');
expect(screen.queryByText('Instrumentation checks')).not.toBeInTheDocument();
});
it('should not render callout when no entries exist', async () => {
server.use(
rest.get('http://localhost/api/v2/infra_monitoring/checks', (_, res, ctx) =>
res(
ctx.json({
status: 'success',
data: {
ready: false,
type: 'pods',
presentDefaultEnabledMetrics: null,
presentOptionalMetrics: null,
presentRequiredAttributes: null,
missingDefaultEnabledMetrics: null,
missingOptionalMetrics: null,
missingRequiredAttributes: null,
},
}),
),
),
);
renderComponent<TestItem>({
entity: InfraMonitoringEntity.PODS,
eventCategory: InfraMonitoringEvents.Pod,
fetchListData: fetchListDataMock,
tableColumns: createTestColumns(),
getRowKey: (row): string => row.id,
getItemKey: (row): string => row.id,
});
await screen.findByText('item-1');
expect(screen.queryByText('Instrumentation checks')).not.toBeInTheDocument();
});
it('should render callout with present entries', async () => {
server.use(
rest.get('http://localhost/api/v2/infra_monitoring/checks', (_, res, ctx) =>
res(
ctx.json({
status: 'success',
data: {
ready: false,
type: 'pods',
presentDefaultEnabledMetrics: [
{
associatedComponent: { name: 'otel-collector' },
metrics: ['k8s.pod.cpu.usage', 'k8s.pod.memory.usage'],
},
],
presentOptionalMetrics: null,
presentRequiredAttributes: null,
missingDefaultEnabledMetrics: null,
missingOptionalMetrics: null,
missingRequiredAttributes: null,
},
}),
),
),
);
renderComponent<TestItem>({
entity: InfraMonitoringEntity.PODS,
eventCategory: InfraMonitoringEvents.Pod,
fetchListData: fetchListDataMock,
tableColumns: createTestColumns(),
getRowKey: (row): string => row.id,
getItemKey: (row): string => row.id,
});
await screen.findByText('Instrumentation checks');
await expect(
screen.findByText('Default enabled metrics'),
).resolves.toBeInTheDocument();
await expect(
screen.findByText('k8s.pod.cpu.usage, k8s.pod.memory.usage'),
).resolves.toBeInTheDocument();
await expect(
screen.findByText('otel-collector'),
).resolves.toBeInTheDocument();
});
it('should render callout with missing entries', async () => {
server.use(
rest.get('http://localhost/api/v2/infra_monitoring/checks', (_, res, ctx) =>
res(
ctx.json({
status: 'success',
data: {
ready: false,
type: 'pods',
presentDefaultEnabledMetrics: null,
presentOptionalMetrics: null,
presentRequiredAttributes: null,
missingDefaultEnabledMetrics: [
{
associatedComponent: { name: 'otel-collector' },
metrics: ['k8s.pod.cpu.limit'],
documentationLink: 'https://example.com/docs',
},
],
missingOptionalMetrics: null,
missingRequiredAttributes: null,
},
}),
),
),
);
renderComponent<TestItem>({
entity: InfraMonitoringEntity.PODS,
eventCategory: InfraMonitoringEvents.Pod,
fetchListData: fetchListDataMock,
tableColumns: createTestColumns(),
getRowKey: (row): string => row.id,
getItemKey: (row): string => row.id,
});
await screen.findByText('Instrumentation checks');
await expect(
screen.findByText('Missing default metrics'),
).resolves.toBeInTheDocument();
await expect(
screen.findByText('k8s.pod.cpu.limit'),
).resolves.toBeInTheDocument();
await expect(screen.findByText('Learn here')).resolves.toBeInTheDocument();
});
it('should trigger recheck on button click', async () => {
let recheckCallCount = 0;
server.use(
rest.get(
'http://localhost/api/v2/infra_monitoring/checks',
(_, res, ctx) => {
recheckCallCount++;
return res(
ctx.json({
status: 'success',
data: {
ready: false,
type: 'pods',
presentDefaultEnabledMetrics: [
{
associatedComponent: { name: 'otel-collector' },
metrics: ['k8s.pod.cpu.usage'],
},
],
},
}),
);
},
),
);
const user = userEvent.setup();
renderComponent<TestItem>({
entity: InfraMonitoringEntity.PODS,
eventCategory: InfraMonitoringEvents.Pod,
fetchListData: fetchListDataMock,
tableColumns: createTestColumns(),
getRowKey: (row): string => row.id,
getItemKey: (row): string => row.id,
});
await screen.findByText('Instrumentation checks');
const initialCallCount = recheckCallCount;
const recheckBtn = screen.getByTestId('instrumentation-checks-recheck-btn');
await user.click(recheckBtn);
await waitFor(() => {
expect(recheckCallCount).toBeGreaterThan(initialCallCount);
});
});
it('should render both present and missing entries', async () => {
server.use(
rest.get('http://localhost/api/v2/infra_monitoring/checks', (_, res, ctx) =>
res(
ctx.json({
status: 'success',
data: {
ready: false,
type: 'pods',
presentDefaultEnabledMetrics: [
{
associatedComponent: { name: 'otel-collector' },
metrics: ['k8s.pod.cpu.usage'],
},
],
presentOptionalMetrics: null,
presentRequiredAttributes: [
{
associatedComponent: { name: 'otel-collector' },
attributes: ['k8s.namespace.name'],
},
],
missingDefaultEnabledMetrics: [
{
associatedComponent: { name: 'otel-collector' },
metrics: ['k8s.pod.memory.limit'],
},
],
missingOptionalMetrics: null,
missingRequiredAttributes: null,
},
}),
),
),
);
renderComponent<TestItem>({
entity: InfraMonitoringEntity.PODS,
eventCategory: InfraMonitoringEvents.Pod,
fetchListData: fetchListDataMock,
tableColumns: createTestColumns(),
getRowKey: (row): string => row.id,
getItemKey: (row): string => row.id,
});
await screen.findByText('Instrumentation checks');
// Present entries
await expect(
screen.findByText('Default enabled metrics'),
).resolves.toBeInTheDocument();
await expect(
screen.findByText('k8s.pod.cpu.usage'),
).resolves.toBeInTheDocument();
await expect(
screen.findByText('Required attributes'),
).resolves.toBeInTheDocument();
await expect(
screen.findByText('k8s.namespace.name'),
).resolves.toBeInTheDocument();
// Missing entries
await expect(
screen.findByText('Missing default metrics'),
).resolves.toBeInTheDocument();
await expect(
screen.findByText('k8s.pod.memory.limit'),
).resolves.toBeInTheDocument();
});
});
});

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

@@ -0,0 +1,24 @@
.checkContainer {
padding: 0px var(--spacing-4) var(--spacing-4) var(--spacing-4);
position: relative;
}
.container {
display: flex;
flex-direction: column;
gap: 8px;
width: 100%;
}
.header {
display: flex;
align-items: center;
justify-content: space-between;
}
.entriesList {
display: flex;
flex-direction: column;
gap: 4px;
}

View File

@@ -0,0 +1,162 @@
import { MouseEvent, useCallback, useMemo, useState } from 'react';
import { useQueryClient } from 'react-query';
import { Button } from '@signozhq/ui/button';
import { Callout } from '@signozhq/ui/callout';
import { RefreshCw } from '@signozhq/icons';
import setLocalStorage from 'api/browser/localstorage/set';
import {
invalidateGetChecks,
useGetChecks,
} from 'api/generated/services/inframonitoring';
import { InframonitoringtypesCheckTypeDTO } from 'api/generated/services/sigNoz.schemas';
import { InfraMonitoringEntity } from '../../../constants';
import styles from './K8sInstrumentationChecksCallout.module.scss';
import { MissingEntryRow } from './MissingEntryRow';
import { PresentEntryRow } from './PresentEntryRow';
import {
ENTITY_TO_CHECK_TYPE,
getStorageKey,
hasAnyEntries,
hasMissingEntries,
readExpandedState,
} from './utils';
export interface InstrumentationChecksCalloutProps {
entity: InfraMonitoringEntity;
}
export function K8sInstrumentationChecksCallout({
entity,
}: InstrumentationChecksCalloutProps): JSX.Element | null {
const checkType = ENTITY_TO_CHECK_TYPE[entity];
const queryClient = useQueryClient();
const [isExpanded, setIsExpanded] = useState(() => readExpandedState(entity));
const { data, isLoading, isFetching } = useGetChecks(
{ type: checkType as InframonitoringtypesCheckTypeDTO },
{ query: { enabled: !!checkType } },
);
const handleRecheck = useCallback(
(e: MouseEvent): void => {
e.preventDefault();
e.stopPropagation();
if (checkType) {
void invalidateGetChecks(queryClient, { type: checkType });
}
},
[queryClient, checkType],
);
const handleExpandToggle = useCallback((): void => {
setIsExpanded((prev) => {
const next = !prev;
setLocalStorage(getStorageKey(entity), String(next));
return next;
});
}, [entity]);
const checksData = data?.data;
const hasMissingItems = useMemo(
() => (checksData ? hasMissingEntries(checksData) : false),
[checksData],
);
if (!checkType || isLoading) {
return null;
}
if (!checksData || checksData.ready) {
return null;
}
if (!hasAnyEntries(checksData)) {
return null;
}
return (
<div className={styles.checkContainer}>
<Callout
type={hasMissingItems ? 'warning' : 'info'}
showIcon
size="medium"
title={
<div className={styles.header}>
Instrumentation checks
<Button
variant="outlined"
color="warning"
size="sm"
onClick={handleRecheck}
loading={isFetching}
prefix={<RefreshCw size={12} />}
data-testid="instrumentation-checks-recheck-btn"
>
Recheck
</Button>
</div>
}
action="expandable"
defaultExpanded={isExpanded}
onClick={handleExpandToggle}
>
<div className={styles.container} onClick={(e) => e.stopPropagation()}>
<div className={styles.entriesList}>
{checksData.presentDefaultEnabledMetrics?.map((entry) => (
<PresentEntryRow
key={`present-default-${entry.associatedComponent.name}`}
entry={entry}
typeLabel="Default enabled metrics"
itemType="metrics"
/>
))}
{checksData.presentOptionalMetrics?.map((entry) => (
<PresentEntryRow
key={`present-optional-${entry.associatedComponent.name}`}
entry={entry}
typeLabel="Optional metrics"
itemType="metrics"
/>
))}
{checksData.presentRequiredAttributes?.map((entry) => (
<PresentEntryRow
key={`present-attrs-${entry.associatedComponent.name}`}
entry={entry}
typeLabel="Required attributes"
itemType="attributes"
/>
))}
{checksData.missingDefaultEnabledMetrics?.map((entry) => (
<MissingEntryRow
key={`missing-default-${entry.associatedComponent.name}`}
entry={entry}
typeLabel="Missing default metrics"
itemType="metrics"
/>
))}
{checksData.missingOptionalMetrics?.map((entry) => (
<MissingEntryRow
key={`missing-optional-${entry.associatedComponent.name}`}
entry={entry}
typeLabel="Missing optional metrics"
itemType="metrics"
/>
))}
{checksData.missingRequiredAttributes?.map((entry) => (
<MissingEntryRow
key={`missing-attrs-${entry.associatedComponent.name}`}
entry={entry}
typeLabel="Missing required attributes"
itemType="attributes"
/>
))}
</div>
</div>
</Callout>
</div>
);
}

View File

@@ -0,0 +1,27 @@
.entryRow {
display: flex;
align-items: center;
gap: 6px;
flex-wrap: wrap;
color: var(--l1-foreground);
}
.missingIcon {
color: var(--warning-background);
flex-shrink: 0;
}
.learnMoreLink {
display: inline-flex;
align-items: center;
gap: 4px;
margin-left: 4px;
svg {
margin-left: 4px;
}
}
.divider {
--divider-color: var(--callout-warning-border);
}

View File

@@ -0,0 +1,66 @@
import styles from './MissingEntryRow.module.scss';
import { ExternalLink, TriangleAlert } from '@signozhq/icons';
import { Typography } from '@signozhq/ui/typography';
import { Divider } from '@signozhq/ui/divider';
import type {
InframonitoringtypesMissingAttributesComponentEntryDTO,
InframonitoringtypesMissingMetricsComponentEntryDTO,
} from 'api/generated/services/sigNoz.schemas';
type MissingEntryRowProps =
| {
entry: InframonitoringtypesMissingMetricsComponentEntryDTO;
typeLabel: string;
itemType: 'metrics';
}
| {
entry: InframonitoringtypesMissingAttributesComponentEntryDTO;
typeLabel: string;
itemType: 'attributes';
};
export function MissingEntryRow({
entry,
typeLabel,
itemType,
}: MissingEntryRowProps): JSX.Element {
const items = itemType === 'metrics' ? entry.metrics : entry.attributes;
return (
<div className={styles.entryRow}>
<TriangleAlert size={14} className={styles.missingIcon} />
<Typography.Text size="base" weight="medium">
{typeLabel}
</Typography.Text>
<Typography.Text size="base" color="muted">
-
</Typography.Text>
<Typography.Text
size="base"
weight="semibold"
className={styles.entryRowMetric}
>
{items?.join(', ')}
</Typography.Text>
<Typography.Text size="base" color="muted">
-
</Typography.Text>
<Typography.Text size="base" italic color="muted">
{entry.associatedComponent.name}
</Typography.Text>
{entry.documentationLink && (
<Typography.Link
href={entry.documentationLink}
target="_blank"
rel="noopener noreferrer"
size="base"
className={styles.learnMoreLink}
>
Learn here
<ExternalLink size={12} />
</Typography.Link>
)}
<Divider className={styles.divider} />
</div>
);
}

View File

@@ -0,0 +1,16 @@
.entryRow {
display: flex;
align-items: center;
gap: 6px;
flex-wrap: wrap;
color: var(--l1-foreground);
}
.presentIcon {
color: var(--success-background);
flex-shrink: 0;
}
.divider {
--divider-color: var(--callout-warning-border);
}

View File

@@ -0,0 +1,54 @@
import styles from './PresentEntryRow.module.scss';
import { CircleCheck } from '@signozhq/icons';
import { Typography } from '@signozhq/ui/typography';
import { Divider } from '@signozhq/ui/divider';
import type {
InframonitoringtypesAttributesComponentEntryDTO,
InframonitoringtypesMetricsComponentEntryDTO,
} from 'api/generated/services/sigNoz.schemas';
type PresentEntryRowProps =
| {
entry: InframonitoringtypesMetricsComponentEntryDTO;
typeLabel: string;
itemType: 'metrics';
}
| {
entry: InframonitoringtypesAttributesComponentEntryDTO;
typeLabel: string;
itemType: 'attributes';
};
export function PresentEntryRow({
entry,
typeLabel,
itemType,
}: PresentEntryRowProps): JSX.Element {
const items = itemType === 'metrics' ? entry.metrics : entry.attributes;
return (
<div className={styles.entryRow}>
<CircleCheck size={14} className={styles.presentIcon} />
<Typography.Text size="base" weight="medium">
{typeLabel}
</Typography.Text>
<Typography.Text size="base" color="muted">
-
</Typography.Text>
<Typography.Text
size="base"
weight="semibold"
className={styles.entryRowMetric}
>
{items?.join(', ')}
</Typography.Text>
<Typography.Text size="base" color="muted">
-
</Typography.Text>
<Typography.Text size="base" italic color="muted">
{entry.associatedComponent.name}
</Typography.Text>
<Divider className={styles.divider} />
</div>
);
}

View File

@@ -0,0 +1,60 @@
import getLocalStorage from 'api/browser/localstorage/get';
import {
InframonitoringtypesChecksDTO,
InframonitoringtypesCheckTypeDTO,
} from 'api/generated/services/sigNoz.schemas';
import { InfraMonitoringEntity } from '../../../constants';
export const ENTITY_TO_CHECK_TYPE: Record<
InfraMonitoringEntity,
InframonitoringtypesCheckTypeDTO
> = {
[InfraMonitoringEntity.HOSTS]: InframonitoringtypesCheckTypeDTO.hosts,
[InfraMonitoringEntity.PODS]: InframonitoringtypesCheckTypeDTO.pods,
[InfraMonitoringEntity.NODES]: InframonitoringtypesCheckTypeDTO.nodes,
[InfraMonitoringEntity.NAMESPACES]:
InframonitoringtypesCheckTypeDTO.namespaces,
[InfraMonitoringEntity.CLUSTERS]: InframonitoringtypesCheckTypeDTO.clusters,
[InfraMonitoringEntity.DEPLOYMENTS]:
InframonitoringtypesCheckTypeDTO.deployments,
[InfraMonitoringEntity.STATEFULSETS]:
InframonitoringtypesCheckTypeDTO.statefulsets,
[InfraMonitoringEntity.DAEMONSETS]:
InframonitoringtypesCheckTypeDTO.daemonsets,
[InfraMonitoringEntity.JOBS]: InframonitoringtypesCheckTypeDTO.jobs,
[InfraMonitoringEntity.VOLUMES]: InframonitoringtypesCheckTypeDTO.volumes,
[InfraMonitoringEntity.CONTAINERS]:
InframonitoringtypesCheckTypeDTO.kube_containers,
};
export function hasMissingEntries(
data: InframonitoringtypesChecksDTO,
): boolean {
return (
(data.missingDefaultEnabledMetrics?.length ?? 0) > 0 ||
(data.missingOptionalMetrics?.length ?? 0) > 0 ||
(data.missingRequiredAttributes?.length ?? 0) > 0
);
}
export function hasAnyEntries(data: InframonitoringtypesChecksDTO): boolean {
return (
(data.presentDefaultEnabledMetrics?.length ?? 0) > 0 ||
(data.presentOptionalMetrics?.length ?? 0) > 0 ||
(data.presentRequiredAttributes?.length ?? 0) > 0 ||
(data.missingDefaultEnabledMetrics?.length ?? 0) > 0 ||
(data.missingOptionalMetrics?.length ?? 0) > 0 ||
(data.missingRequiredAttributes?.length ?? 0) > 0
);
}
const STORAGE_PREFIX = '@signozhq/k8s-instrumentation-checks-expanded';
export function getStorageKey(entity: InfraMonitoringEntity): string {
return `${STORAGE_PREFIX}-${entity}`;
}
export function readExpandedState(entity: InfraMonitoringEntity): boolean {
const stored = getLocalStorage(getStorageKey(entity));
return stored === null ? true : stored === 'true';
}

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

@@ -1,11 +1,15 @@
import { useCallback } from 'react';
import { convertToApiError } from 'api/ErrorResponseHandlerForGeneratedAPIs';
import { listClusters } from 'api/generated/services/inframonitoring';
import { RenderErrorResponseDTO } from 'api/generated/services/sigNoz.schemas';
import { AxiosError } from 'axios';
import {
InframonitoringtypesClusterRecordDTO,
InframonitoringtypesResponseTypeDTO,
Querybuildertypesv5OrderDirectionDTO,
} from 'api/generated/services/sigNoz.schemas';
import { InfraMonitoringEvents } from 'constants/events';
import APIError from 'types/api/error';
import K8sBaseDetails, { K8sDetailsFilters } from '../Base/K8sBaseDetails';
import { K8sBaseList } from '../Base/K8sBaseList';
@@ -14,7 +18,9 @@ import { InfraMonitoringEntity } from '../constants';
import {
clusterWidgetInfo,
getClusterMetricsQueryPayload,
k8sClusterDetailsCountsConfig,
k8sClusterDetailsMetadataConfig,
k8sClusterGetCountsFilterExpression,
k8sClusterGetEntityName,
k8sClusterGetSelectedItemExpression,
k8sClusterInitialEventsExpression,
@@ -67,13 +73,12 @@ function K8sClustersList({
warning: data.warning,
};
} catch (error) {
const errMsg =
error instanceof Error ? error.message : 'Failed to fetch clusters';
return {
type: 'list' as const,
records: [] as InframonitoringtypesClusterRecordDTO[],
total: 0,
error: errMsg,
error:
convertToApiError(error as AxiosError<RenderErrorResponseDTO>) ?? null,
};
}
},
@@ -86,7 +91,7 @@ function K8sClustersList({
signal?: AbortSignal,
): Promise<{
data: InframonitoringtypesClusterRecordDTO | null;
error?: string | null;
error?: APIError | null;
}> => {
try {
const response = await listClusters(
@@ -104,11 +109,10 @@ function K8sClustersList({
data: response.data.records.length > 0 ? response.data.records[0] : null,
};
} catch (error) {
const errMsg =
error instanceof Error ? error.message : 'Failed to fetch cluster';
return {
data: null,
error: errMsg,
error:
convertToApiError(error as AxiosError<RenderErrorResponseDTO>) ?? null,
};
}
},
@@ -136,6 +140,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

@@ -8,7 +8,7 @@ import { ExpandButtonWrapper } from 'container/InfraMonitoringK8sV2/components';
import ColumnHeader from '../Base/ColumnHeader';
import EntityGroupHeader from '../Base/EntityGroupHeader';
import K8sGroupCell from '../Base/K8sGroupCell';
import { formatBytes, getPodPhaseStatusItems } from '../commonUtils';
import { formatBytes, getPodStatusItems } from '../commonUtils';
import {
CellValueTooltip,
GroupedStatusCounts,
@@ -77,11 +77,7 @@ export const k8sClustersColumnsConfig: ClusterTableColumnConfig[] = [
visibilityBehavior: 'hidden-on-expand',
cell: ({ value }): React.ReactNode => {
const clusterName = value as string;
return (
<CellValueTooltip value={clusterName}>
<TanStackTable.Text>{clusterName}</TanStackTable.Text>
</CellValueTooltip>
);
return <CellValueTooltip value={clusterName} />;
},
},
{
@@ -121,23 +117,25 @@ export const k8sClustersColumnsConfig: ClusterTableColumnConfig[] = [
},
},
{
id: 'podCountsByPhase',
id: 'podCountsByStatus',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/clusters#pod-counts-by-phase">
Pod Phases
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/clusters#pod-counts-by-status">
Pod Status
</ColumnHeader>
),
accessorFn: (row): InframonitoringtypesClusterRecordDTO['podCountsByPhase'] =>
row.podCountsByPhase,
accessorFn: (
row,
): InframonitoringtypesClusterRecordDTO['podCountsByStatus'] =>
row.podCountsByStatus,
width: { min: 250 },
enableSort: false,
cell: ({ row }): React.ReactNode => {
const podCountsByPhase = row.podCountsByPhase;
if (!podCountsByPhase) {
const podCountsByStatus = row.podCountsByStatus;
if (!podCountsByStatus) {
return <TanStackTable.Text>-</TanStackTable.Text>;
}
return (
<GroupedStatusCounts items={getPodPhaseStatusItems(row.podCountsByPhase)} />
<GroupedStatusCounts items={getPodStatusItems(row.podCountsByStatus)} />
);
},
},

View File

@@ -1,18 +1,25 @@
import { useCallback } from 'react';
import { useCallback, useMemo } from 'react';
import { convertToApiError } from 'api/ErrorResponseHandlerForGeneratedAPIs';
import { listDaemonSets } from 'api/generated/services/inframonitoring';
import { RenderErrorResponseDTO } from 'api/generated/services/sigNoz.schemas';
import { AxiosError } from 'axios';
import {
InframonitoringtypesDaemonSetRecordDTO,
InframonitoringtypesResponseTypeDTO,
Querybuildertypesv5OrderDirectionDTO,
} from 'api/generated/services/sigNoz.schemas';
import { InfraMonitoringEvents } from 'constants/events';
import APIError from 'types/api/error';
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,
getDaemonSetPodMetricsQueryPayload,
k8sDaemonSetDetailsMetadataConfig,
k8sDaemonSetGetEntityName,
k8sDaemonSetGetSelectedItemExpression,
@@ -24,6 +31,8 @@ import {
getK8sDaemonSetRowKey,
k8sDaemonSetsColumnsConfig,
} from './table.config';
import { createPodMetricsTab } from 'container/InfraMonitoringK8sV2/EntityDetailsUtils/createPodMetricsTab';
function K8sDaemonSetsList({
controlListPrefix,
}: {
@@ -64,13 +73,12 @@ function K8sDaemonSetsList({
warning: data.warning,
};
} catch (error) {
const errMsg =
error instanceof Error ? error.message : 'Failed to fetch daemonsets';
return {
type: 'list' as const,
records: [] as InframonitoringtypesDaemonSetRecordDTO[],
total: 0,
error: errMsg,
error:
convertToApiError(error as AxiosError<RenderErrorResponseDTO>) ?? null,
};
}
},
@@ -82,7 +90,7 @@ function K8sDaemonSetsList({
signal?: AbortSignal,
): Promise<{
data: InframonitoringtypesDaemonSetRecordDTO | null;
error?: string | null;
error?: APIError | null;
}> => {
try {
const response = await listDaemonSets(
@@ -99,19 +107,29 @@ function K8sDaemonSetsList({
data: response.data.records.length > 0 ? response.data.records[0] : null,
};
} catch (error) {
const errMsg =
error instanceof Error ? error.message : 'Failed to fetch daemonset';
return {
data: null,
error: errMsg,
error:
convertToApiError(error as AxiosError<RenderErrorResponseDTO>) ?? null,
};
}
},
[],
);
const customTabs = useMemo(
() => [
createPodMetricsTab<InframonitoringtypesDaemonSetRecordDTO>({
getQueryPayload: getDaemonSetPodMetricsQueryPayload,
category: InfraMonitoringEntity.DAEMONSETS,
queryKey: 'daemonSetPodMetrics',
}),
],
[],
);
return (
<>
<K8sBaseList<InframonitoringtypesDaemonSetRecordDTO>
<K8sBaseList<InframonitoringtypesDaemonSetRecordDTO, SelectedItemParams>
controlListPrefix={controlListPrefix}
entity={InfraMonitoringEntity.DAEMONSETS}
tableColumns={k8sDaemonSetsColumnsConfig}
@@ -132,6 +150,7 @@ function K8sDaemonSetsList({
entityWidgetInfo={daemonSetWidgetInfo}
getEntityQueryPayload={getDaemonSetMetricsQueryPayload}
queryKeyPrefix="daemonset"
customTabs={customTabs}
/>
</>
);

View File

@@ -7,13 +7,24 @@ 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 {
getPodUtilizationByPodQueryPayloads,
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 +48,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 +74,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 +144,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 +211,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 +253,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 +295,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 +371,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 +413,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 +455,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 +531,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 +620,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',
},
@@ -712,3 +679,29 @@ export const getDaemonSetMetricsQueryPayload = (
},
];
};
export const getDaemonSetPodMetricsQueryPayload = (
daemonSet: InframonitoringtypesDaemonSetRecordDTO,
start: number,
end: number,
dotMetricsEnabled: boolean,
): GetQueryResultsProps[] => {
const k8sDaemonSetNameKey = dotMetricsEnabled
? 'k8s.daemonset.name'
: 'k8s_daemonset_name';
return getPodUtilizationByPodQueryPayloads(
{
workloadNameKey: k8sDaemonSetNameKey,
workloadNameValue:
daemonSet.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_DAEMONSET_NAME] ?? '',
clusterName:
daemonSet.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_CLUSTER_NAME] ?? '',
namespaceName:
daemonSet.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] ?? '',
},
start,
end,
dotMetricsEnabled,
);
};

View File

@@ -6,7 +6,8 @@ import { ExpandButtonWrapper } from 'container/InfraMonitoringK8sV2/components';
import ColumnHeader from '../Base/ColumnHeader';
import EntityGroupHeader from '../Base/EntityGroupHeader';
import K8sGroupCell from '../Base/K8sGroupCell';
import { formatBytes, getPodPhaseStatusItems } from '../commonUtils';
import { SelectedItemParams } from '../hooks';
import { formatBytes, getPodStatusItems } from '../commonUtils';
import {
CellValueTooltip,
EntityProgressBar,
@@ -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 =
@@ -79,11 +87,7 @@ export const k8sDaemonSetsColumnsConfig: DaemonSetTableColumnConfig[] = [
visibilityBehavior: 'hidden-on-expand',
cell: ({ value }): React.ReactNode => {
const daemonsetName = value as string;
return (
<CellValueTooltip value={daemonsetName}>
<TanStackTable.Text>{daemonsetName}</TanStackTable.Text>
</CellValueTooltip>
);
return <CellValueTooltip value={daemonsetName} />;
},
},
{
@@ -100,60 +104,64 @@ export const k8sDaemonSetsColumnsConfig: DaemonSetTableColumnConfig[] = [
enableResize: true,
cell: ({ value }): React.ReactNode => {
const namespaceName = value as string;
return (
<CellValueTooltip value={namespaceName}>
<TanStackTable.Text>{namespaceName}</TanStackTable.Text>
</CellValueTooltip>
);
return <CellValueTooltip value={namespaceName} />;
},
},
{
id: 'pod_counts_by_phase',
id: 'pod_counts_by_status',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/daemonsets#pod-counts-by-phase">
Pod Phases
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/daemonsets#pod-counts-by-status">
Pod Status
</ColumnHeader>
),
accessorFn: (
row,
): InframonitoringtypesDaemonSetRecordDTO['podCountsByPhase'] =>
row.podCountsByPhase,
): InframonitoringtypesDaemonSetRecordDTO['podCountsByStatus'] =>
row.podCountsByStatus,
width: { min: 250 },
enableSort: false,
enableResize: true,
cell: ({ row }): React.ReactNode => {
const podCountsByPhase = row.podCountsByPhase;
if (!podCountsByPhase) {
const podCountsByStatus = row.podCountsByStatus;
if (!podCountsByStatus) {
return <TanStackTable.Text>-</TanStackTable.Text>;
}
return (
<GroupedStatusCounts items={getPodPhaseStatusItems(podCountsByPhase)} />
);
return <GroupedStatusCounts items={getPodStatusItems(podCountsByStatus)} />;
},
},
{
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 +320,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 +392,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

@@ -1,19 +1,25 @@
import { useCallback } from 'react';
import { useCallback, useMemo } from 'react';
import { convertToApiError } from 'api/ErrorResponseHandlerForGeneratedAPIs';
import { listDeployments } from 'api/generated/services/inframonitoring';
import { RenderErrorResponseDTO } from 'api/generated/services/sigNoz.schemas';
import { AxiosError } from 'axios';
import {
InframonitoringtypesDeploymentRecordDTO,
InframonitoringtypesResponseTypeDTO,
Querybuildertypesv5OrderDirectionDTO,
} from 'api/generated/services/sigNoz.schemas';
import { InfraMonitoringEvents } from 'constants/events';
import APIError from 'types/api/error';
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,
getDeploymentPodMetricsQueryPayload,
k8sDeploymentDetailsMetadataConfig,
k8sDeploymentGetEntityName,
k8sDeploymentGetSelectedItemExpression,
@@ -25,6 +31,7 @@ import {
getK8sDeploymentRowKey,
k8sDeploymentsColumnsConfig,
} from './table.config';
import { createPodMetricsTab } from 'container/InfraMonitoringK8sV2/EntityDetailsUtils/createPodMetricsTab';
function K8sDeploymentsList({
controlListPrefix,
@@ -67,13 +74,12 @@ function K8sDeploymentsList({
warning: data.warning,
};
} catch (error) {
const errMsg =
error instanceof Error ? error.message : 'Failed to fetch deployments';
return {
type: 'list' as const,
records: [] as InframonitoringtypesDeploymentRecordDTO[],
total: 0,
error: errMsg,
error:
convertToApiError(error as AxiosError<RenderErrorResponseDTO>) ?? null,
};
}
},
@@ -86,7 +92,7 @@ function K8sDeploymentsList({
signal?: AbortSignal,
): Promise<{
data: InframonitoringtypesDeploymentRecordDTO | null;
error?: string | null;
error?: APIError | null;
}> => {
try {
const response = await listDeployments(
@@ -104,20 +110,30 @@ function K8sDeploymentsList({
data: response.data.records.length > 0 ? response.data.records[0] : null,
};
} catch (error) {
const errMsg =
error instanceof Error ? error.message : 'Failed to fetch deployment';
return {
data: null,
error: errMsg,
error:
convertToApiError(error as AxiosError<RenderErrorResponseDTO>) ?? null,
};
}
},
[],
);
const customTabs = useMemo(
() => [
createPodMetricsTab<InframonitoringtypesDeploymentRecordDTO>({
getQueryPayload: getDeploymentPodMetricsQueryPayload,
category: InfraMonitoringEntity.DEPLOYMENTS,
queryKey: 'deploymentPodMetrics',
}),
],
[],
);
return (
<>
<K8sBaseList<InframonitoringtypesDeploymentRecordDTO>
<K8sBaseList<InframonitoringtypesDeploymentRecordDTO, SelectedItemParams>
controlListPrefix={controlListPrefix}
entity={InfraMonitoringEntity.DEPLOYMENTS}
tableColumns={k8sDeploymentsColumnsConfig}
@@ -139,6 +155,7 @@ function K8sDeploymentsList({
entityWidgetInfo={deploymentWidgetInfo}
getEntityQueryPayload={getDeploymentMetricsQueryPayload}
queryKeyPrefix="deployment"
customTabs={customTabs}
/>
</>
);

View File

@@ -7,13 +7,24 @@ 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 {
getPodUtilizationByPodQueryPayloads,
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 +47,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 +74,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 +139,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 +210,7 @@ export const getDeploymentMetricsQueryPayload = (
deployment.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_DEPLOYMENT_NAME] ??
'',
},
...filters,
],
op: 'AND',
},
@@ -196,6 +252,7 @@ export const getDeploymentMetricsQueryPayload = (
deployment.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_DEPLOYMENT_NAME] ??
'',
},
...filters,
],
op: 'AND',
},
@@ -237,6 +294,7 @@ export const getDeploymentMetricsQueryPayload = (
deployment.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_DEPLOYMENT_NAME] ??
'',
},
...filters,
],
op: 'AND',
},
@@ -312,6 +370,7 @@ export const getDeploymentMetricsQueryPayload = (
deployment.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_DEPLOYMENT_NAME] ??
'',
},
...filters,
],
op: 'AND',
},
@@ -353,6 +412,7 @@ export const getDeploymentMetricsQueryPayload = (
deployment.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_DEPLOYMENT_NAME] ??
'',
},
...filters,
],
op: 'AND',
},
@@ -394,6 +454,7 @@ export const getDeploymentMetricsQueryPayload = (
deployment.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_DEPLOYMENT_NAME] ??
'',
},
...filters,
],
op: 'AND',
},
@@ -469,6 +530,7 @@ export const getDeploymentMetricsQueryPayload = (
deployment.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_DEPLOYMENT_NAME] ??
'',
},
...filters,
],
op: 'AND',
},
@@ -557,6 +619,7 @@ export const getDeploymentMetricsQueryPayload = (
deployment.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_DEPLOYMENT_NAME] ??
'',
},
...filters,
],
op: 'AND',
},
@@ -615,3 +678,29 @@ export const getDeploymentMetricsQueryPayload = (
},
];
};
export const getDeploymentPodMetricsQueryPayload = (
deployment: InframonitoringtypesDeploymentRecordDTO,
start: number,
end: number,
dotMetricsEnabled: boolean,
): GetQueryResultsProps[] => {
const k8sDeploymentNameKey = dotMetricsEnabled
? 'k8s.deployment.name'
: 'k8s_deployment_name';
return getPodUtilizationByPodQueryPayloads(
{
workloadNameKey: k8sDeploymentNameKey,
workloadNameValue:
deployment.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_DEPLOYMENT_NAME] ?? '',
clusterName:
deployment.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_CLUSTER_NAME] ?? '',
namespaceName:
deployment.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] ?? '',
},
start,
end,
dotMetricsEnabled,
);
};

View File

@@ -6,7 +6,8 @@ import { ExpandButtonWrapper } from 'container/InfraMonitoringK8sV2/components';
import ColumnHeader from '../Base/ColumnHeader';
import EntityGroupHeader from '../Base/EntityGroupHeader';
import K8sGroupCell from '../Base/K8sGroupCell';
import { formatBytes, getPodPhaseStatusItems } from '../commonUtils';
import { SelectedItemParams } from '../hooks';
import { formatBytes, getPodStatusItems } from '../commonUtils';
import {
CellValueTooltip,
EntityProgressBar,
@@ -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>[] =
@@ -80,11 +88,7 @@ export const k8sDeploymentsColumnsConfig: TableColumnDef<InframonitoringtypesDep
visibilityBehavior: 'hidden-on-expand',
cell: ({ value }): React.ReactNode => {
const deploymentName = value as string;
return (
<CellValueTooltip value={deploymentName}>
<TanStackTable.Text>{deploymentName}</TanStackTable.Text>
</CellValueTooltip>
);
return <CellValueTooltip value={deploymentName} />;
},
},
{
@@ -104,31 +108,29 @@ export const k8sDeploymentsColumnsConfig: TableColumnDef<InframonitoringtypesDep
),
},
{
id: 'podCountsByPhase',
id: 'podCountsByStatus',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/deployments#pod-counts-by-phase">
Pod Phases
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/deployments#pod-counts-by-status">
Pod Status
</ColumnHeader>
),
accessorFn: (row): object | undefined => row.podCountsByPhase,
accessorFn: (row): object | undefined => row.podCountsByStatus,
width: { min: 250 },
enableSort: false,
enableResize: true,
cell: ({ row }): React.ReactNode => {
const podCountsByPhase = row.podCountsByPhase;
if (!podCountsByPhase) {
const podCountsByStatus = row.podCountsByStatus;
if (!podCountsByStatus) {
return <TanStackTable.Text>-</TanStackTable.Text>;
}
return (
<GroupedStatusCounts items={getPodPhaseStatusItems(podCountsByPhase)} />
);
return <GroupedStatusCounts items={getPodStatusItems(podCountsByStatus)} />;
},
},
{
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,11 +14,6 @@
box-sizing: border-box;
}
.entityMetricsTitle {
font-size: var(--periscope-font-size-base);
color: var(--l2-foreground);
}
.metricsHeader {
display: flex;
justify-content: flex-end;

View File

@@ -19,8 +19,10 @@ import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults';
import { useTimezone } from 'providers/Timezone';
import { SuccessResponse } from 'types/api';
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';
@@ -44,6 +46,7 @@ interface EntityMetricsProps<T> {
entityWidgetInfo: {
title: string;
yAxisUnit: string;
docPath?: string;
}[];
getEntityQueryPayload: (
node: T,
@@ -204,9 +207,24 @@ function EntityMetrics<T>({
key={entityWidgetInfo[idx].title}
className={styles.entityMetricsCol}
>
<span className={styles.entityMetricsTitle}>
{entityWidgetInfo[idx].title}
</span>
<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

@@ -1,4 +1,5 @@
import { render, screen } from '@testing-library/react';
import { MemoryRouter } from 'react-router-dom';
import { InfraMonitoringEntity } from 'container/InfraMonitoringK8sV2/constants';
import { Time } from 'container/TopNav/DateTimeSelectionV2/types';
import * as appContextHooks from 'providers/App/App';
@@ -295,17 +296,19 @@ const renderEntityMetrics = (overrides = {}): any => {
};
return render(
<EntityMetrics
timeRange={defaultProps.timeRange}
isModalTimeSelection={defaultProps.isModalTimeSelection}
handleTimeChange={defaultProps.handleTimeChange}
selectedInterval={defaultProps.selectedInterval}
entity={defaultProps.entity}
entityWidgetInfo={defaultProps.entityWidgetInfo}
getEntityQueryPayload={defaultProps.getEntityQueryPayload}
queryKey={defaultProps.queryKey}
category={defaultProps.category}
/>,
<MemoryRouter>
<EntityMetrics
timeRange={defaultProps.timeRange}
isModalTimeSelection={defaultProps.isModalTimeSelection}
handleTimeChange={defaultProps.handleTimeChange}
selectedInterval={defaultProps.selectedInterval}
entity={defaultProps.entity}
entityWidgetInfo={defaultProps.entityWidgetInfo}
getEntityQueryPayload={defaultProps.getEntityQueryPayload}
queryKey={defaultProps.queryKey}
category={defaultProps.category}
/>
</MemoryRouter>,
);
};
@@ -334,8 +337,8 @@ const mockTableData: (import('../utils').MetricsTableData[] | null)[] = [
];
const mockQueryPayloads = [
{ graphType: 'graph' }, // time_series
{ graphType: 'table' }, // table
{ graphType: 'graph', query: { queryType: 'builder' } }, // time_series
{ graphType: 'table', query: { queryType: 'builder' } }, // table
];
describe('EntityMetrics', () => {
@@ -442,6 +445,34 @@ describe('EntityMetrics', () => {
);
});
it('renders metrics explorer link only for non-table panels', () => {
renderEntityMetrics();
expect(screen.getByTestId('open-metrics-explorer-0')).toBeInTheDocument();
expect(
screen.queryByTestId('open-metrics-explorer-1'),
).not.toBeInTheDocument();
});
it('builds metrics explorer link with relativeTime when a relative interval is selected', () => {
renderEntityMetrics({ selectedInterval: '5m' as Time });
const href = screen
.getByTestId('open-metrics-explorer-0')
.getAttribute('href');
expect(href).toContain('relativeTime=5m');
expect(href).not.toContain('startTime=');
expect(href).not.toContain('endTime=');
});
it('builds metrics explorer link with absolute time range in milliseconds for custom interval', () => {
renderEntityMetrics({ selectedInterval: 'custom' as Time });
const href = screen
.getByTestId('open-metrics-explorer-0')
.getAttribute('href');
expect(href).toContain(`startTime=${mockTimeRange.startTime * 1000}`);
expect(href).toContain(`endTime=${mockTimeRange.endTime * 1000}`);
expect(href).not.toContain('relativeTime=');
});
it('passes correct parameters to useEntityMetrics hook', () => {
renderEntityMetrics();
expect(mockUseEntityMetrics).toHaveBeenCalledWith(

View File

@@ -87,7 +87,7 @@ describe('EntityTraces - Table Rendering', () => {
expect(badge).toHaveAttribute('data-variant', 'outline');
});
it('should render N/A when http method is empty', async () => {
it('should render - when http method is empty', async () => {
mockQueryRangeV5WithTracesResponse({
customTraces: [{ httpMethod: '', responseStatusCode: '200' }],
});
@@ -96,7 +96,7 @@ describe('EntityTraces - Table Rendering', () => {
renderEntityTraces();
});
await expect(screen.findByText('N/A')).resolves.toBeInTheDocument();
await expect(screen.findByText('-')).resolves.toBeInTheDocument();
expect(screen.queryByTestId('httpMethod')).not.toBeInTheDocument();
});

View File

@@ -4,4 +4,8 @@
.cellText {
color: var(--l2-foreground);
&[data-novalue='true'] {
opacity: 0.6;
}
}

View File

@@ -89,8 +89,12 @@ export const getTraceListColumns = (
if (value === '') {
return (
<BlockLink to={getTraceLink(itemData)} openInNewTab>
<Typography data-testid={key} className={styles.cellText}>
N/A
<Typography
data-testid={key}
className={styles.cellText}
data-novalue="true"
>
-
</Typography>
</BlockLink>
);
@@ -102,7 +106,9 @@ export const getTraceListColumns = (
if (!httpMethod) {
return (
<BlockLink to={getTraceLink(itemData)} openInNewTab>
<Typography className={styles.cellText}>N/A</Typography>
<Typography className={styles.cellText} data-novalue="true">
-
</Typography>
</BlockLink>
);
}
@@ -129,8 +135,11 @@ export const getTraceListColumns = (
if (!isValidCode) {
return (
<BlockLink to={getTraceLink(itemData)} openInNewTab>
<Typography className={styles.cellText}>
{numericCode === 0 || !statusCode ? 'N/A' : statusCode}
<Typography
className={styles.cellText}
data-novalue={numericCode === 0 || !statusCode}
>
{numericCode === 0 || !statusCode ? '-' : statusCode}
</Typography>
</BlockLink>
);

View File

@@ -0,0 +1,47 @@
import { Container } from '@signozhq/icons';
import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults';
import { CustomTab } from '../Base/K8sBaseDetails';
import {
InfraMonitoringEntity,
podUtilizationByPodWidgetInfo,
VIEW_TYPES,
} from '../constants';
import EntityMetrics from './EntityMetrics';
interface CreatePodMetricsTabParams<T> {
getQueryPayload: (
entity: T,
start: number,
end: number,
dotMetricsEnabled: boolean,
) => GetQueryResultsProps[];
category: InfraMonitoringEntity;
queryKey: string;
}
export function createPodMetricsTab<T>({
getQueryPayload,
category,
queryKey,
}: CreatePodMetricsTabParams<T>): CustomTab<T> {
return {
key: VIEW_TYPES.POD_METRICS,
label: 'Pod Metrics',
icon: <Container size={14} />,
render: ({ entity, timeRange, selectedInterval, handleTimeChange }) => (
<EntityMetrics
entity={entity}
selectedInterval={selectedInterval}
timeRange={timeRange}
handleTimeChange={handleTimeChange}
isModalTimeSelection
entityWidgetInfo={podUtilizationByPodWidgetInfo}
getEntityQueryPayload={getQueryPayload}
category={category}
queryKey={queryKey}
/>
),
};
}

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

@@ -1,18 +1,24 @@
import { useCallback } from 'react';
import { useCallback, useMemo } from 'react';
import { convertToApiError } from 'api/ErrorResponseHandlerForGeneratedAPIs';
import { listJobs } from 'api/generated/services/inframonitoring';
import { RenderErrorResponseDTO } from 'api/generated/services/sigNoz.schemas';
import { AxiosError } from 'axios';
import {
InframonitoringtypesJobRecordDTO,
InframonitoringtypesResponseTypeDTO,
Querybuildertypesv5OrderDirectionDTO,
} from 'api/generated/services/sigNoz.schemas';
import { InfraMonitoringEvents } from 'constants/events';
import APIError from 'types/api/error';
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,
getJobPodMetricsQueryPayload,
jobWidgetInfo,
k8sJobDetailsMetadataConfig,
k8sJobGetEntityName,
@@ -25,6 +31,7 @@ import {
getK8sJobRowKey,
k8sJobsColumnsConfig,
} from './table.config';
import { createPodMetricsTab } from 'container/InfraMonitoringK8sV2/EntityDetailsUtils/createPodMetricsTab';
function K8sJobsList({
controlListPrefix,
@@ -67,13 +74,12 @@ function K8sJobsList({
warning: data.warning,
};
} catch (error) {
const errMsg =
error instanceof Error ? error.message : 'Failed to fetch jobs';
return {
type: 'list' as const,
records: [] as InframonitoringtypesJobRecordDTO[],
total: 0,
error: errMsg,
error:
convertToApiError(error as AxiosError<RenderErrorResponseDTO>) ?? null,
};
}
},
@@ -86,7 +92,7 @@ function K8sJobsList({
signal?: AbortSignal,
): Promise<{
data: InframonitoringtypesJobRecordDTO | null;
error?: string | null;
error?: APIError | null;
}> => {
try {
const response = await listJobs(
@@ -104,20 +110,30 @@ function K8sJobsList({
data: response.data.records.length > 0 ? response.data.records[0] : null,
};
} catch (error) {
const errMsg =
error instanceof Error ? error.message : 'Failed to fetch job';
return {
data: null,
error: errMsg,
error:
convertToApiError(error as AxiosError<RenderErrorResponseDTO>) ?? null,
};
}
},
[],
);
const customTabs = useMemo(
() => [
createPodMetricsTab<InframonitoringtypesJobRecordDTO>({
getQueryPayload: getJobPodMetricsQueryPayload,
category: InfraMonitoringEntity.JOBS,
queryKey: 'jobPodMetrics',
}),
],
[],
);
return (
<>
<K8sBaseList<InframonitoringtypesJobRecordDTO>
<K8sBaseList<InframonitoringtypesJobRecordDTO, SelectedItemParams>
controlListPrefix={controlListPrefix}
entity={InfraMonitoringEntity.JOBS}
tableColumns={k8sJobsColumnsConfig}
@@ -139,6 +155,7 @@ function K8sJobsList({
entityWidgetInfo={jobWidgetInfo}
getEntityQueryPayload={getJobMetricsQueryPayload}
queryKeyPrefix="job"
customTabs={customTabs}
/>
</>
);

View File

@@ -7,13 +7,24 @@ 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 {
getPodUtilizationByPodQueryPayloads,
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 +47,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 +73,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 +111,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 +178,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 +239,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 +300,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 +374,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: [],
@@ -470,3 +432,25 @@ export const getJobMetricsQueryPayload = (
},
];
};
export const getJobPodMetricsQueryPayload = (
job: InframonitoringtypesJobRecordDTO,
start: number,
end: number,
dotMetricsEnabled: boolean,
): GetQueryResultsProps[] => {
const k8sJobNameKey = dotMetricsEnabled ? 'k8s.job.name' : 'k8s_job_name';
return getPodUtilizationByPodQueryPayloads(
{
workloadNameKey: k8sJobNameKey,
workloadNameValue: job.jobName ?? '',
clusterName: job.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_CLUSTER_NAME] ?? '',
namespaceName:
job.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] ?? '',
},
start,
end,
dotMetricsEnabled,
);
};

View File

@@ -6,7 +6,8 @@ import { ExpandButtonWrapper } from 'container/InfraMonitoringK8sV2/components';
import ColumnHeader from '../Base/ColumnHeader';
import EntityGroupHeader from '../Base/EntityGroupHeader';
import K8sGroupCell from '../Base/K8sGroupCell';
import { formatBytes, getPodPhaseStatusItems } from '../commonUtils';
import { SelectedItemParams } from '../hooks';
import { formatBytes, getPodStatusItems } from '../commonUtils';
import {
CellValueTooltip,
EntityProgressBar,
@@ -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 =
@@ -75,11 +81,7 @@ export const k8sJobsColumnsConfig: JobTableColumnConfig[] = [
visibilityBehavior: 'hidden-on-expand',
cell: ({ value }): React.ReactNode => {
const jobName = value as string;
return (
<CellValueTooltip value={jobName}>
<TanStackTable.Text>{jobName}</TanStackTable.Text>
</CellValueTooltip>
);
return <CellValueTooltip value={jobName} />;
},
},
{
@@ -96,40 +98,34 @@ export const k8sJobsColumnsConfig: JobTableColumnConfig[] = [
enableResize: true,
cell: ({ value }): React.ReactNode => {
const namespaceName = value as string;
return (
<CellValueTooltip value={namespaceName}>
<TanStackTable.Text>{namespaceName}</TanStackTable.Text>
</CellValueTooltip>
);
return <CellValueTooltip value={namespaceName} />;
},
},
{
id: 'pod_counts_by_phase',
id: 'pod_counts_by_status',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/jobs#pod-counts-by-phase">
Pod Phases
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/jobs#pod-counts-by-status">
Pod Status
</ColumnHeader>
),
accessorFn: (row): InframonitoringtypesJobRecordDTO['podCountsByPhase'] =>
row.podCountsByPhase,
accessorFn: (row): InframonitoringtypesJobRecordDTO['podCountsByStatus'] =>
row.podCountsByStatus,
width: { min: 250 },
enableSort: false,
enableResize: true,
cell: ({ row }): React.ReactNode => {
const podCountsByPhase = row.podCountsByPhase;
if (!podCountsByPhase) {
const podCountsByStatus = row.podCountsByStatus;
if (!podCountsByStatus) {
return <TanStackTable.Text>-</TanStackTable.Text>;
}
return (
<GroupedStatusCounts items={getPodPhaseStatusItems(podCountsByPhase)} />
);
return <GroupedStatusCounts items={getPodStatusItems(podCountsByStatus)} />;
},
},
{
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 +145,7 @@ export const k8sJobsColumnsConfig: JobTableColumnConfig[] = [
{
value: row.desiredSuccessfulPods,
label: 'Desired',
color: Color.BG_ROBIN_500,
color: Color.BG_AMBER_500,
},
]}
/>

View File

@@ -1,19 +1,27 @@
import { useCallback } from 'react';
import { useCallback, useMemo } from 'react';
import { convertToApiError } from 'api/ErrorResponseHandlerForGeneratedAPIs';
import { listNamespaces } from 'api/generated/services/inframonitoring';
import { RenderErrorResponseDTO } from 'api/generated/services/sigNoz.schemas';
import { AxiosError } from 'axios';
import {
InframonitoringtypesNamespaceRecordDTO,
InframonitoringtypesResponseTypeDTO,
Querybuildertypesv5OrderDirectionDTO,
} from 'api/generated/services/sigNoz.schemas';
import { InfraMonitoringEvents } from 'constants/events';
import APIError from 'types/api/error';
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,
getNamespacePodMetricsQueryPayload,
k8sNamespaceDetailsCountsConfig,
k8sNamespaceDetailsMetadataConfig,
k8sNamespaceGetCountsFilterExpression,
k8sNamespaceGetEntityName,
k8sNamespaceGetSelectedItemExpression,
k8sNamespaceInitialEventsExpression,
@@ -25,6 +33,7 @@ import {
getK8sNamespaceRowKey,
k8sNamespacesColumnsConfig,
} from './table.config';
import { createPodMetricsTab } from 'container/InfraMonitoringK8sV2/EntityDetailsUtils/createPodMetricsTab';
function K8sNamespacesList({
controlListPrefix,
@@ -67,13 +76,12 @@ function K8sNamespacesList({
warning: data.warning,
};
} catch (error) {
const errMsg =
error instanceof Error ? error.message : 'Failed to fetch namespaces';
return {
type: 'list' as const,
records: [] as InframonitoringtypesNamespaceRecordDTO[],
total: 0,
error: errMsg,
error:
convertToApiError(error as AxiosError<RenderErrorResponseDTO>) ?? null,
};
}
},
@@ -86,7 +94,7 @@ function K8sNamespacesList({
signal?: AbortSignal,
): Promise<{
data: InframonitoringtypesNamespaceRecordDTO | null;
error?: string | null;
error?: APIError | null;
}> => {
try {
const response = await listNamespaces(
@@ -104,20 +112,30 @@ function K8sNamespacesList({
data: response.data.records.length > 0 ? response.data.records[0] : null,
};
} catch (error) {
const errMsg =
error instanceof Error ? error.message : 'Failed to fetch namespace';
return {
data: null,
error: errMsg,
error:
convertToApiError(error as AxiosError<RenderErrorResponseDTO>) ?? null,
};
}
},
[],
);
const customTabs = useMemo(
() => [
createPodMetricsTab<InframonitoringtypesNamespaceRecordDTO>({
getQueryPayload: getNamespacePodMetricsQueryPayload,
category: InfraMonitoringEntity.NAMESPACES,
queryKey: 'namespacePodMetrics',
}),
],
[],
);
return (
<>
<K8sBaseList<InframonitoringtypesNamespaceRecordDTO>
<K8sBaseList<InframonitoringtypesNamespaceRecordDTO, SelectedItemParams>
controlListPrefix={controlListPrefix}
entity={InfraMonitoringEntity.NAMESPACES}
tableColumns={k8sNamespacesColumnsConfig}
@@ -136,9 +154,12 @@ function K8sNamespacesList({
getInitialLogTracesExpression={k8sNamespaceInitialLogTracesExpression}
getInitialEventsExpression={k8sNamespaceInitialEventsExpression}
metadataConfig={k8sNamespaceDetailsMetadataConfig}
countsConfig={k8sNamespaceDetailsCountsConfig}
getCountsFilterExpression={k8sNamespaceGetCountsFilterExpression}
entityWidgetInfo={namespaceWidgetInfo}
getEntityQueryPayload={getNamespaceMetricsQueryPayload}
queryKeyPrefix="namespace"
customTabs={customTabs}
/>
</>
);

View File

@@ -6,14 +6,30 @@ 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 {
getPodUtilizationByPodQueryPayloads,
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 +41,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 +255,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 +305,7 @@ export const getNamespaceMetricsQueryPayload = (
op: '=',
value: namespace.namespaceName,
},
...filters,
],
op: 'AND',
},
@@ -250,6 +345,7 @@ export const getNamespaceMetricsQueryPayload = (
op: '=',
value: namespace.namespaceName,
},
...filters,
],
op: 'AND',
},
@@ -289,6 +385,7 @@ export const getNamespaceMetricsQueryPayload = (
op: '=',
value: namespace.namespaceName,
},
...filters,
],
op: 'AND',
},
@@ -328,6 +425,7 @@ export const getNamespaceMetricsQueryPayload = (
op: '=',
value: namespace.namespaceName,
},
...filters,
],
op: 'AND',
},
@@ -401,6 +499,7 @@ export const getNamespaceMetricsQueryPayload = (
op: '=',
value: namespace.namespaceName,
},
...filters,
],
op: 'AND',
},
@@ -440,6 +539,7 @@ export const getNamespaceMetricsQueryPayload = (
op: '=',
value: namespace.namespaceName,
},
...filters,
],
op: 'AND',
},
@@ -479,6 +579,7 @@ export const getNamespaceMetricsQueryPayload = (
op: '=',
value: namespace.namespaceName,
},
...filters,
],
op: 'AND',
},
@@ -518,6 +619,7 @@ export const getNamespaceMetricsQueryPayload = (
op: '=',
value: namespace.namespaceName,
},
...filters,
],
op: 'AND',
},
@@ -557,6 +659,7 @@ export const getNamespaceMetricsQueryPayload = (
op: '=',
value: namespace.namespaceName,
},
...filters,
],
op: 'AND',
},
@@ -596,6 +699,7 @@ export const getNamespaceMetricsQueryPayload = (
op: '=',
value: namespace.namespaceName,
},
...filters,
],
op: 'AND',
},
@@ -669,6 +773,7 @@ export const getNamespaceMetricsQueryPayload = (
op: '=',
value: namespace.namespaceName,
},
...filters,
],
op: 'AND',
},
@@ -749,6 +854,7 @@ export const getNamespaceMetricsQueryPayload = (
op: '=',
value: namespace.namespaceName,
},
...filters,
],
op: 'AND',
},
@@ -829,6 +935,7 @@ export const getNamespaceMetricsQueryPayload = (
op: '=',
value: namespace.namespaceName,
},
...filters,
],
op: 'AND',
},
@@ -915,6 +1022,7 @@ export const getNamespaceMetricsQueryPayload = (
op: '=',
value: namespace.namespaceName,
},
...filters,
],
op: 'AND',
},
@@ -1001,6 +1109,7 @@ export const getNamespaceMetricsQueryPayload = (
op: '=',
value: namespace.namespaceName,
},
...filters,
],
op: 'AND',
},
@@ -1047,6 +1156,7 @@ export const getNamespaceMetricsQueryPayload = (
op: '=',
value: namespace.namespaceName,
},
...filters,
],
op: 'AND',
},
@@ -1093,6 +1203,7 @@ export const getNamespaceMetricsQueryPayload = (
op: '=',
value: namespace.namespaceName,
},
...filters,
],
op: 'AND',
},
@@ -1173,6 +1284,7 @@ export const getNamespaceMetricsQueryPayload = (
op: '=',
value: namespace.namespaceName,
},
...filters,
],
op: 'AND',
},
@@ -1225,6 +1337,7 @@ export const getNamespaceMetricsQueryPayload = (
op: '=',
value: namespace.namespaceName,
},
...filters,
],
op: 'AND',
},
@@ -1311,6 +1424,7 @@ export const getNamespaceMetricsQueryPayload = (
op: '=',
value: namespace.namespaceName,
},
...filters,
],
op: 'AND',
},
@@ -1357,6 +1471,7 @@ export const getNamespaceMetricsQueryPayload = (
op: '=',
value: namespace.namespaceName,
},
...filters,
],
op: 'AND',
},
@@ -1403,6 +1518,7 @@ export const getNamespaceMetricsQueryPayload = (
op: '=',
value: namespace.namespaceName,
},
...filters,
],
op: 'AND',
},
@@ -1449,6 +1565,7 @@ export const getNamespaceMetricsQueryPayload = (
op: '=',
value: namespace.namespaceName,
},
...filters,
],
op: 'AND',
},
@@ -1529,6 +1646,7 @@ export const getNamespaceMetricsQueryPayload = (
op: '=',
value: namespace.namespaceName,
},
...filters,
],
op: 'AND',
},
@@ -1575,6 +1693,7 @@ export const getNamespaceMetricsQueryPayload = (
op: '=',
value: namespace.namespaceName,
},
...filters,
],
op: 'AND',
},
@@ -1634,3 +1753,26 @@ export const getNamespaceMetricsQueryPayload = (
},
];
};
export const getNamespacePodMetricsQueryPayload = (
namespace: InframonitoringtypesNamespaceRecordDTO,
start: number,
end: number,
dotMetricsEnabled: boolean,
): GetQueryResultsProps[] => {
const k8sNamespaceNameKey = dotMetricsEnabled
? 'k8s.namespace.name'
: 'k8s_namespace_name';
return getPodUtilizationByPodQueryPayloads(
{
workloadNameKey: k8sNamespaceNameKey,
workloadNameValue: namespace.namespaceName ?? '',
clusterName:
namespace.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_CLUSTER_NAME] ?? '',
},
start,
end,
dotMetricsEnabled,
);
};

View File

@@ -6,7 +6,7 @@ import { ExpandButtonWrapper } from 'container/InfraMonitoringK8sV2/components';
import ColumnHeader from '../Base/ColumnHeader';
import EntityGroupHeader from '../Base/EntityGroupHeader';
import K8sGroupCell from '../Base/K8sGroupCell';
import { formatBytes, getPodPhaseStatusItems } from '../commonUtils';
import { formatBytes, getPodStatusItems } from '../commonUtils';
import {
CellValueTooltip,
GroupedStatusCounts,
@@ -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 =
@@ -75,11 +83,7 @@ export const k8sNamespacesColumnsConfig: NamespaceTableColumnConfig[] = [
visibilityBehavior: 'hidden-on-expand',
cell: ({ value }): React.ReactNode => {
const namespaceName = value as string;
return (
<CellValueTooltip value={namespaceName}>
<TanStackTable.Text>{namespaceName}</TanStackTable.Text>
</CellValueTooltip>
);
return <CellValueTooltip value={namespaceName} />;
},
},
{
@@ -98,25 +102,25 @@ export const k8sNamespacesColumnsConfig: NamespaceTableColumnConfig[] = [
),
},
{
id: 'podCountsByPhase',
id: 'podCountsByStatus',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/namespaces#pod-counts-by-phase">
Pod Phases
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/namespaces#pod-counts-by-status">
Pod Status
</ColumnHeader>
),
accessorFn: (
row,
): InframonitoringtypesNamespaceRecordDTO['podCountsByPhase'] =>
row.podCountsByPhase,
): InframonitoringtypesNamespaceRecordDTO['podCountsByStatus'] =>
row.podCountsByStatus,
width: { min: 250 },
enableSort: false,
cell: ({ row }): React.ReactNode => {
const podCountsByPhase = row.podCountsByPhase;
if (!podCountsByPhase) {
const podCountsByStatus = row.podCountsByStatus;
if (!podCountsByStatus) {
return <TanStackTable.Text>-</TanStackTable.Text>;
}
return (
<GroupedStatusCounts items={getPodPhaseStatusItems(row.podCountsByPhase)} />
<GroupedStatusCounts items={getPodStatusItems(row.podCountsByStatus)} />
);
},
},

View File

@@ -1,11 +1,15 @@
import { useCallback } from 'react';
import { convertToApiError } from 'api/ErrorResponseHandlerForGeneratedAPIs';
import { listNodes } from 'api/generated/services/inframonitoring';
import { RenderErrorResponseDTO } from 'api/generated/services/sigNoz.schemas';
import { AxiosError } from 'axios';
import {
InframonitoringtypesNodeRecordDTO,
InframonitoringtypesResponseTypeDTO,
Querybuildertypesv5OrderDirectionDTO,
} from 'api/generated/services/sigNoz.schemas';
import { InfraMonitoringEvents } from 'constants/events';
import APIError from 'types/api/error';
import K8sBaseDetails, { K8sDetailsFilters } from '../Base/K8sBaseDetails';
import { K8sBaseList } from '../Base/K8sBaseList';
@@ -67,13 +71,12 @@ function K8sNodesList({
warning: data.warning,
};
} catch (error) {
const errMsg =
error instanceof Error ? error.message : 'Failed to fetch nodes';
return {
type: 'list' as const,
records: [] as InframonitoringtypesNodeRecordDTO[],
total: 0,
error: errMsg,
error:
convertToApiError(error as AxiosError<RenderErrorResponseDTO>) ?? null,
};
}
},
@@ -86,7 +89,7 @@ function K8sNodesList({
signal?: AbortSignal,
): Promise<{
data: InframonitoringtypesNodeRecordDTO | null;
error?: string | null;
error?: APIError | null;
}> => {
try {
const response = await listNodes(
@@ -104,11 +107,10 @@ function K8sNodesList({
data: response.data.records.length > 0 ? response.data.records[0] : null,
};
} catch (error) {
const errMsg =
error instanceof Error ? error.message : 'Failed to fetch node';
return {
data: null,
error: errMsg,
error:
convertToApiError(error as AxiosError<RenderErrorResponseDTO>) ?? null,
};
}
},

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

@@ -7,7 +7,7 @@ import { ExpandButtonWrapper } from 'container/InfraMonitoringK8sV2/components';
import ColumnHeader from '../Base/ColumnHeader';
import EntityGroupHeader from '../Base/EntityGroupHeader';
import K8sGroupCell from '../Base/K8sGroupCell';
import { formatBytes, getPodPhaseStatusItems } from '../commonUtils';
import { formatBytes, getPodStatusItems } from '../commonUtils';
import { INFRA_MONITORING_ATTR_KEYS } from '../constants';
import {
CellValueTooltip,
@@ -85,11 +85,7 @@ export const k8sNodesColumnsConfig: NodeTableColumnConfig[] = [
visibilityBehavior: 'hidden-on-expand',
cell: ({ value }): React.ReactNode => {
const nodeName = value as string;
return (
<CellValueTooltip value={nodeName}>
<TanStackTable.Text>{nodeName}</TanStackTable.Text>
</CellValueTooltip>
);
return <CellValueTooltip value={nodeName} />;
},
},
{
@@ -132,23 +128,23 @@ export const k8sNodesColumnsConfig: NodeTableColumnConfig[] = [
},
},
{
id: 'podCountsByPhase',
id: 'podCountsByStatus',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/nodes#pod-counts-by-phase">
Pod Phases
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/nodes#pod-counts-by-status">
Pod Status
</ColumnHeader>
),
accessorFn: (row): InframonitoringtypesNodeRecordDTO['podCountsByPhase'] =>
row.podCountsByPhase,
accessorFn: (row): InframonitoringtypesNodeRecordDTO['podCountsByStatus'] =>
row.podCountsByStatus,
width: { min: 250 },
enableSort: false,
cell: ({ row }): React.ReactNode => {
const podCountsByPhase = row.podCountsByPhase;
if (!podCountsByPhase) {
const podCountsByStatus = row.podCountsByStatus;
if (!podCountsByStatus) {
return <TanStackTable.Text>-</TanStackTable.Text>;
}
return (
<GroupedStatusCounts items={getPodPhaseStatusItems(row.podCountsByPhase)} />
<GroupedStatusCounts items={getPodStatusItems(row.podCountsByStatus)} />
);
},
},

View File

@@ -1,11 +1,15 @@
import { useCallback } from 'react';
import { convertToApiError } from 'api/ErrorResponseHandlerForGeneratedAPIs';
import { listPods } from 'api/generated/services/inframonitoring';
import { RenderErrorResponseDTO } from 'api/generated/services/sigNoz.schemas';
import { AxiosError } from 'axios';
import {
InframonitoringtypesPodRecordDTO,
InframonitoringtypesResponseTypeDTO,
Querybuildertypesv5OrderDirectionDTO,
} from 'api/generated/services/sigNoz.schemas';
import { InfraMonitoringEvents } from 'constants/events';
import APIError from 'types/api/error';
import K8sBaseDetails, { K8sDetailsFilters } from '../Base/K8sBaseDetails';
import { K8sBaseList } from '../Base/K8sBaseList';
@@ -67,13 +71,12 @@ function K8sPodsList({
warning: data.warning,
};
} catch (error) {
const errMsg =
error instanceof Error ? error.message : 'Failed to fetch pods';
return {
type: 'list' as const,
records: [] as InframonitoringtypesPodRecordDTO[],
total: 0,
error: errMsg,
error:
convertToApiError(error as AxiosError<RenderErrorResponseDTO>) ?? null,
};
}
},
@@ -86,7 +89,7 @@ function K8sPodsList({
signal?: AbortSignal,
): Promise<{
data: InframonitoringtypesPodRecordDTO | null;
error?: string | null;
error?: APIError | null;
}> => {
try {
const response = await listPods(
@@ -104,11 +107,10 @@ function K8sPodsList({
data: response.data.records.length > 0 ? response.data.records[0] : null,
};
} catch (error) {
const errMsg =
error instanceof Error ? error.message : 'Failed to fetch pod';
return {
data: null,
error: errMsg,
error:
convertToApiError(error as AxiosError<RenderErrorResponseDTO>) ?? null,
};
}
},

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

@@ -1,9 +1,9 @@
import { Container } from '@signozhq/icons';
import { Badge, BadgeColor } from '@signozhq/ui/badge';
import { Badge } from '@signozhq/ui/badge';
import { TooltipSimple } from '@signozhq/ui/tooltip';
import {
InframonitoringtypesPodPhaseDTO,
InframonitoringtypesPodRecordDTO,
InframonitoringtypesPodStatusDTO,
} from 'api/generated/services/sigNoz.schemas';
import TanStackTable, { TableColumnDef } from 'components/TanStackTableView';
import { ExpandButtonWrapper } from 'container/InfraMonitoringK8sV2/components';
@@ -11,7 +11,11 @@ import { ExpandButtonWrapper } from 'container/InfraMonitoringK8sV2/components';
import ColumnHeader from '../Base/ColumnHeader';
import EntityGroupHeader from '../Base/EntityGroupHeader';
import K8sGroupCell from '../Base/K8sGroupCell';
import { formatBytes, getPodPhaseStatusItems } from '../commonUtils';
import {
formatBytes,
getPodStatusItems,
POD_STATUS_COLORS,
} from '../commonUtils';
import {
CellValueTooltip,
EntityProgressBar,
@@ -40,15 +44,6 @@ export function getK8sPodItemKey(
return pod.podUID;
}
const POD_PHASE_COLORS: Record<string, BadgeColor> = {
running: 'forest',
pending: 'amber',
succeeded: 'robin',
failed: 'cherry',
unknown: 'vanilla',
no_data: 'vanilla',
};
export type PodTableColumnConfig =
TableColumnDef<InframonitoringtypesPodRecordDTO>;
export const k8sPodColumnsConfig: PodTableColumnConfig[] = [
@@ -93,34 +88,30 @@ export const k8sPodColumnsConfig: PodTableColumnConfig[] = [
visibilityBehavior: 'hidden-on-expand',
cell: ({ value }): React.ReactNode => {
const podName = value as string;
return (
<CellValueTooltip value={podName}>
<TanStackTable.Text>{podName}</TanStackTable.Text>
</CellValueTooltip>
);
return <CellValueTooltip value={podName} />;
},
},
{
id: 'podPhase',
id: 'podStatus',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/pods#pod-phase">
Phase
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/pods#pod-status">
Status
</ColumnHeader>
),
accessorFn: (row): string => row.podPhase,
width: { min: 120 },
accessorFn: (row): string => row.podStatus,
width: { min: 160 },
enableSort: false,
visibilityBehavior: 'hidden-on-expand',
cell: ({ row }): React.ReactNode => {
if (!row.podPhase) {
if (!row.podStatus) {
return <></>;
}
const color = POD_PHASE_COLORS[row.podPhase] || POD_PHASE_COLORS.unknown;
const color = POD_STATUS_COLORS[row.podStatus] || POD_STATUS_COLORS.unknown;
const label =
row.podPhase === InframonitoringtypesPodPhaseDTO.no_data
row.podStatus === InframonitoringtypesPodStatusDTO.no_data
? 'No Data'
: row.podPhase.charAt(0).toUpperCase() + row.podPhase.slice(1);
: row.podStatus.charAt(0).toUpperCase() + row.podStatus.slice(1);
return (
<Badge color={color} variant="outline">
{label}
@@ -129,30 +120,34 @@ export const k8sPodColumnsConfig: PodTableColumnConfig[] = [
},
},
{
id: 'podCountsByPhase',
id: 'podCountsByStatus',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/pods#pod-phase">
Phases
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/pods#pod-status">
Status
</ColumnHeader>
),
accessorFn: (row): InframonitoringtypesPodRecordDTO['podCountsByPhase'] =>
row.podCountsByPhase,
accessorFn: (row): InframonitoringtypesPodRecordDTO['podCountsByStatus'] =>
row.podCountsByStatus,
width: { min: 250 },
enableSort: false,
visibilityBehavior: 'hidden-on-collapse',
cell: ({ row }): React.ReactNode => {
const podCountsByPhase = row.podCountsByPhase;
if (!podCountsByPhase) {
const podCountsByStatus = row.podCountsByStatus;
if (!podCountsByStatus) {
return <TanStackTable.Text>-</TanStackTable.Text>;
}
return (
<GroupedStatusCounts items={getPodPhaseStatusItems(row.podCountsByPhase)} />
<GroupedStatusCounts items={getPodStatusItems(row.podCountsByStatus)} />
);
},
},
{
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,
@@ -168,6 +163,28 @@ export const k8sPodColumnsConfig: PodTableColumnConfig[] = [
return <TanStackTable.Text>{formatAge(age)}</TanStackTable.Text>;
},
},
{
id: 'podRestarts',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/pods#restarts">
Restarts
</ColumnHeader>
),
accessorFn: (row): number => row.podRestarts,
width: { min: 100 },
enableSort: true,
cell: ({ value }): React.ReactNode => {
const restarts = value as number;
if (restarts === -1) {
return (
<TooltipSimple title="No data">
<Typography.Text>-</Typography.Text>
</TooltipSimple>
);
}
return <TanStackTable.Text>{restarts}</TanStackTable.Text>;
},
},
{
id: 'cpu_request',
header: (): React.ReactNode => (
@@ -316,7 +333,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 +349,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 +365,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

@@ -1,18 +1,24 @@
import { useCallback } from 'react';
import { useCallback, useMemo } from 'react';
import { convertToApiError } from 'api/ErrorResponseHandlerForGeneratedAPIs';
import { listStatefulSets } from 'api/generated/services/inframonitoring';
import { RenderErrorResponseDTO } from 'api/generated/services/sigNoz.schemas';
import { AxiosError } from 'axios';
import {
InframonitoringtypesResponseTypeDTO,
InframonitoringtypesStatefulSetRecordDTO,
Querybuildertypesv5OrderDirectionDTO,
} from 'api/generated/services/sigNoz.schemas';
import { InfraMonitoringEvents } from 'constants/events';
import APIError from 'types/api/error';
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,
getStatefulSetPodMetricsQueryPayload,
k8sStatefulSetDetailsMetadataConfig,
k8sStatefulSetGetEntityName,
k8sStatefulSetGetSelectedItemExpression,
@@ -25,6 +31,7 @@ import {
getK8sStatefulSetRowKey,
k8sStatefulSetsColumnsConfig,
} from './table.config';
import { createPodMetricsTab } from 'container/InfraMonitoringK8sV2/EntityDetailsUtils/createPodMetricsTab';
function K8sStatefulSetsList({
controlListPrefix,
@@ -67,13 +74,12 @@ function K8sStatefulSetsList({
warning: data.warning,
};
} catch (error) {
const errMsg =
error instanceof Error ? error.message : 'Failed to fetch statefulsets';
return {
type: 'list' as const,
records: [] as InframonitoringtypesStatefulSetRecordDTO[],
total: 0,
error: errMsg,
error:
convertToApiError(error as AxiosError<RenderErrorResponseDTO>) ?? null,
};
}
},
@@ -86,7 +92,7 @@ function K8sStatefulSetsList({
signal?: AbortSignal,
): Promise<{
data: InframonitoringtypesStatefulSetRecordDTO | null;
error?: string | null;
error?: APIError | null;
}> => {
try {
const response = await listStatefulSets(
@@ -104,20 +110,30 @@ function K8sStatefulSetsList({
data: response.data.records.length > 0 ? response.data.records[0] : null,
};
} catch (error) {
const errMsg =
error instanceof Error ? error.message : 'Failed to fetch statefulset';
return {
data: null,
error: errMsg,
error:
convertToApiError(error as AxiosError<RenderErrorResponseDTO>) ?? null,
};
}
},
[],
);
const customTabs = useMemo(
() => [
createPodMetricsTab<InframonitoringtypesStatefulSetRecordDTO>({
getQueryPayload: getStatefulSetPodMetricsQueryPayload,
category: InfraMonitoringEntity.STATEFULSETS,
queryKey: 'statefulSetPodMetrics',
}),
],
[],
);
return (
<>
<K8sBaseList<InframonitoringtypesStatefulSetRecordDTO>
<K8sBaseList<InframonitoringtypesStatefulSetRecordDTO, SelectedItemParams>
controlListPrefix={controlListPrefix}
entity={InfraMonitoringEntity.STATEFULSETS}
tableColumns={k8sStatefulSetsColumnsConfig}
@@ -139,6 +155,7 @@ function K8sStatefulSetsList({
entityWidgetInfo={statefulSetWidgetInfo}
getEntityQueryPayload={getStatefulSetMetricsQueryPayload}
queryKeyPrefix="statefulSet"
customTabs={customTabs}
/>
</>
);

View File

@@ -7,13 +7,24 @@ 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 {
getPodUtilizationByPodQueryPayloads,
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 +33,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 +47,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 +75,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 +121,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 +231,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 +274,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 +317,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 +380,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 +423,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 +486,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 +516,7 @@ export const getStatefulSetMetricsQueryPayload = (
filters: {
items: [
{
id: 'f3',
id: 'f1',
key: {
dataType: DataTypes.String,
id: 'pod_name',
@@ -538,19 +529,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 +572,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 +635,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 +678,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 +741,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 +817,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',
},
@@ -943,3 +862,29 @@ export const getStatefulSetMetricsQueryPayload = (
},
];
};
export const getStatefulSetPodMetricsQueryPayload = (
statefulSet: InframonitoringtypesStatefulSetRecordDTO,
start: number,
end: number,
dotMetricsEnabled: boolean,
): GetQueryResultsProps[] => {
const k8sStatefulSetNameKey = dotMetricsEnabled
? INFRA_MONITORING_ATTR_KEYS.K8S_STATEFULSET_NAME
: 'k8s_statefulset_name';
return getPodUtilizationByPodQueryPayloads(
{
workloadNameKey: k8sStatefulSetNameKey,
workloadNameValue:
statefulSet.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_STATEFULSET_NAME] ?? '',
clusterName:
statefulSet.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_CLUSTER_NAME] ?? '',
namespaceName:
statefulSet.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] ?? '',
},
start,
end,
dotMetricsEnabled,
);
};

View File

@@ -6,7 +6,8 @@ import { ExpandButtonWrapper } from 'container/InfraMonitoringK8sV2/components';
import ColumnHeader from '../Base/ColumnHeader';
import EntityGroupHeader from '../Base/EntityGroupHeader';
import K8sGroupCell from '../Base/K8sGroupCell';
import { formatBytes, getPodPhaseStatusItems } from '../commonUtils';
import { SelectedItemParams } from '../hooks';
import { formatBytes, getPodStatusItems } from '../commonUtils';
import {
CellValueTooltip,
EntityProgressBar,
@@ -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>[] =
@@ -82,11 +88,7 @@ export const k8sStatefulSetsColumnsConfig: TableColumnDef<InframonitoringtypesSt
visibilityBehavior: 'hidden-on-expand',
cell: ({ value }): React.ReactNode => {
const statefulsetName = value as string;
return (
<CellValueTooltip value={statefulsetName}>
<TanStackTable.Text>{statefulsetName}</TanStackTable.Text>
</CellValueTooltip>
);
return <CellValueTooltip value={statefulsetName} />;
},
},
{
@@ -103,42 +105,36 @@ export const k8sStatefulSetsColumnsConfig: TableColumnDef<InframonitoringtypesSt
enableResize: true,
cell: ({ value }): React.ReactNode => {
const namespaceName = value as string;
return (
<CellValueTooltip value={namespaceName}>
<TanStackTable.Text>{namespaceName}</TanStackTable.Text>
</CellValueTooltip>
);
return <CellValueTooltip value={namespaceName} />;
},
},
{
id: 'pod_counts_by_phase',
id: 'pod_counts_by_status',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/statefulsets#pod-counts-by-phase">
Pod Phases
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/statefulsets#pod-counts-by-status">
Pod Status
</ColumnHeader>
),
accessorFn: (
row,
): InframonitoringtypesStatefulSetRecordDTO['podCountsByPhase'] =>
row.podCountsByPhase,
): InframonitoringtypesStatefulSetRecordDTO['podCountsByStatus'] =>
row.podCountsByStatus,
width: { min: 250 },
enableSort: false,
enableResize: true,
cell: ({ row }): React.ReactNode => {
const podCountsByPhase = row.podCountsByPhase;
if (!podCountsByPhase) {
const podCountsByStatus = row.podCountsByStatus;
if (!podCountsByStatus) {
return <TanStackTable.Text>-</TanStackTable.Text>;
}
return (
<GroupedStatusCounts items={getPodPhaseStatusItems(podCountsByPhase)} />
);
return <GroupedStatusCounts items={getPodStatusItems(podCountsByStatus)} />;
},
},
{
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

@@ -1,16 +1,21 @@
import { useCallback } from 'react';
import { convertToApiError } from 'api/ErrorResponseHandlerForGeneratedAPIs';
import { listVolumes } from 'api/generated/services/inframonitoring';
import { RenderErrorResponseDTO } from 'api/generated/services/sigNoz.schemas';
import { AxiosError } from 'axios';
import {
InframonitoringtypesResponseTypeDTO,
InframonitoringtypesVolumeRecordDTO,
Querybuildertypesv5OrderDirectionDTO,
} from 'api/generated/services/sigNoz.schemas';
import { InfraMonitoringEvents } from 'constants/events';
import APIError from 'types/api/error';
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,
@@ -67,13 +72,12 @@ function K8sVolumesList({
warning: data.warning,
};
} catch (error) {
const errMsg =
error instanceof Error ? error.message : 'Failed to fetch volumes';
return {
type: 'list' as const,
records: [] as InframonitoringtypesVolumeRecordDTO[],
total: 0,
error: errMsg,
error:
convertToApiError(error as AxiosError<RenderErrorResponseDTO>) ?? null,
};
}
},
@@ -86,7 +90,7 @@ function K8sVolumesList({
signal?: AbortSignal,
): Promise<{
data: InframonitoringtypesVolumeRecordDTO | null;
error?: string | null;
error?: APIError | null;
}> => {
try {
const response = await listVolumes(
@@ -104,11 +108,10 @@ function K8sVolumesList({
data: response.data.records.length > 0 ? response.data.records[0] : null,
};
} catch (error) {
const errMsg =
error instanceof Error ? error.message : 'Failed to fetch volume';
return {
data: null,
error: errMsg,
error:
convertToApiError(error as AxiosError<RenderErrorResponseDTO>) ?? null,
};
}
},
@@ -117,7 +120,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 =
@@ -72,11 +81,7 @@ export const k8sVolumesColumnsConfig: VolumeTableColumnConfig[] = [
visibilityBehavior: 'hidden-on-expand',
cell: ({ value }): React.ReactNode => {
const pvcName = value as string;
return (
<CellValueTooltip value={pvcName}>
<TanStackTable.Text>{pvcName}</TanStackTable.Text>
</CellValueTooltip>
);
return <CellValueTooltip value={pvcName} />;
},
},
{
@@ -92,11 +97,7 @@ export const k8sVolumesColumnsConfig: VolumeTableColumnConfig[] = [
enableSort: false,
cell: ({ value }): React.ReactNode => {
const namespaceName = value as string;
return (
<CellValueTooltip value={namespaceName}>
<TanStackTable.Text>{namespaceName}</TanStackTable.Text>
</CellValueTooltip>
);
return <CellValueTooltip value={namespaceName} />;
},
},
{

View File

@@ -1,5 +1,9 @@
import { Color } from '@signozhq/design-tokens';
import { InframonitoringtypesPodCountsByPhaseDTO } from 'api/generated/services/sigNoz.schemas';
import { BadgeColor } from '@signozhq/ui/badge';
import {
InframonitoringtypesPodCountsByStatusDTO,
InframonitoringtypesPodStatusDTO,
} from 'api/generated/services/sigNoz.schemas';
import { StatusCountItem } from './components/GroupedStatusCounts';
@@ -64,17 +68,106 @@ export function getStrokeColorForLimitUtilization(value: number): string {
return Color.BG_SAKURA_500;
}
/**
* Builds StatusCountItem[] for GroupedStatusCounts from pod phase counts.
*/
export function getPodPhaseStatusItems(
counts: InframonitoringtypesPodCountsByPhaseDTO,
export const POD_STATUS_COLORS: Record<
InframonitoringtypesPodStatusDTO,
BadgeColor
> = {
[InframonitoringtypesPodStatusDTO.running]: 'forest',
[InframonitoringtypesPodStatusDTO.completed]: 'robin',
[InframonitoringtypesPodStatusDTO.pending]: 'amber',
[InframonitoringtypesPodStatusDTO.unknown]: 'vanilla',
[InframonitoringtypesPodStatusDTO.no_data]: 'vanilla',
[InframonitoringtypesPodStatusDTO.failed]: 'cherry',
[InframonitoringtypesPodStatusDTO.crashloopbackoff]: 'cherry',
[InframonitoringtypesPodStatusDTO.imagepullbackoff]: 'cherry',
[InframonitoringtypesPodStatusDTO.errimagepull]: 'cherry',
[InframonitoringtypesPodStatusDTO.createcontainerconfigerror]: 'cherry',
[InframonitoringtypesPodStatusDTO.containercreating]: 'amber',
[InframonitoringtypesPodStatusDTO.oomkilled]: 'cherry',
[InframonitoringtypesPodStatusDTO.error]: 'cherry',
[InframonitoringtypesPodStatusDTO.containercannotrun]: 'cherry',
[InframonitoringtypesPodStatusDTO.evicted]: 'cherry',
[InframonitoringtypesPodStatusDTO.nodeaffinity]: 'cherry',
[InframonitoringtypesPodStatusDTO.nodelost]: 'cherry',
[InframonitoringtypesPodStatusDTO.shutdown]: 'cherry',
[InframonitoringtypesPodStatusDTO.unexpectedadmissionerror]: 'cherry',
};
type PodStatusCategory =
| 'running'
| 'completed'
| 'pending'
| 'unknown'
| 'error';
const POD_STATUS_CATEGORY_MAP: Record<
keyof InframonitoringtypesPodCountsByStatusDTO,
PodStatusCategory
> = {
running: 'running',
completed: 'completed',
pending: 'pending',
unknown: 'unknown',
failed: 'error',
crashLoopBackOff: 'error',
imagePullBackOff: 'error',
errImagePull: 'error',
createContainerConfigError: 'error',
containerCreating: 'error',
oomKilled: 'error',
error: 'error',
containerCannotRun: 'error',
evicted: 'error',
nodeAffinity: 'error',
nodeLost: 'error',
shutdown: 'error',
unexpectedAdmissionError: 'error',
};
type ErrorStatusKey = {
[K in keyof InframonitoringtypesPodCountsByStatusDTO]: (typeof POD_STATUS_CATEGORY_MAP)[K] extends 'error'
? K
: never;
}[keyof InframonitoringtypesPodCountsByStatusDTO];
const ERROR_STATUS_LABELS: Record<ErrorStatusKey, string> = {
failed: 'Failed',
crashLoopBackOff: 'CrashLoopBackOff',
imagePullBackOff: 'ImagePullBackOff',
errImagePull: 'ErrImagePull',
createContainerConfigError: 'CreateContainerConfigError',
containerCreating: 'ContainerCreating',
oomKilled: 'OOMKilled',
error: 'Error',
containerCannotRun: 'ContainerCannotRun',
evicted: 'Evicted',
nodeAffinity: 'NodeAffinity',
nodeLost: 'NodeLost',
shutdown: 'Shutdown',
unexpectedAdmissionError: 'UnexpectedAdmissionError',
};
export function getPodStatusItems(
counts: InframonitoringtypesPodCountsByStatusDTO,
): StatusCountItem[] {
const errorKeys = Object.keys(ERROR_STATUS_LABELS) as ErrorStatusKey[];
const errorTotal = errorKeys.reduce((sum, key) => sum + counts[key], 0);
const errorBreakdown = errorKeys.map((key) => ({
label: ERROR_STATUS_LABELS[key],
value: counts[key],
}));
return [
{ value: counts.running, label: 'Running', color: Color.BG_FOREST_500 },
{ value: counts.completed, label: 'Completed', color: Color.BG_ROBIN_500 },
{ value: counts.pending, label: 'Pending', color: Color.BG_AMBER_500 },
{ value: counts.succeeded, label: 'Succeeded', color: Color.BG_ROBIN_500 },
{ value: counts.failed, label: 'Failed', color: Color.BG_CHERRY_500 },
{ value: counts.unknown, label: 'Unknown', color: Color.BG_SLATE_400 },
{
value: errorTotal,
label: 'Error Status',
color: Color.BG_CHERRY_500,
breakdown: errorBreakdown,
},
];
}

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