Compare commits

...

8 Commits

Author SHA1 Message Date
Naman Verma
66f4a63978 fix: don't return error in v2 list dashboard api if there is a v1 dashboard (#12020)
Some checks are pending
build-staging / prepare (push) Waiting to run
build-staging / js-build (push) Blocked by required conditions
build-staging / go-build (push) Blocked by required conditions
build-staging / staging (push) Blocked by required conditions
Release Drafter / update_release_draft (push) Waiting to run
* fix: don't return error in v2 list dashboard api if there is a v1 dashboard

* chore: generate api specs

* feat(dashboard-v2): surface legacy dashboards in the v2 list (#12024)

* feat(dashboard-v2): add legacy dashboard dialog

Surfaces a copyable dashboard ID and a Contact Support action for a
pre-v2 (legacy) dashboard that has no v2 spec to render.

* feat(dashboard-v2): hide v2-only actions for legacy dashboards

A legacy dashboard has no v2 spec, so view/open/copy-link/rename/
duplicate/lock don't apply. Gate them behind !isLegacy, leaving only
Delete (and dropping its now-orphan leading divider).

* feat(dashboard-v2): surface legacy dashboards in the v2 list

Flag legacy rows with a badge, block navigation, and open the legacy
dialog on click instead. Disable the pin action with an explanatory
tooltip and gate the row's actions menu to legacy-safe items.

---------

Co-authored-by: Ashwin Bhatkal <ashwin96@gmail.com>
Co-authored-by: Srikanth Chekuri <srikanth.chekuri92@gmail.com>
2026-07-10 14:41:30 +00:00
Vinicius Lourenço
7d29a22e32 feat(infra-monitoring): use v2 APIs (#12036)
* feat(query-v2): add metricNamespace prop

* feat(tanstack): add way to modify header padding

* feat(infra-monitoring): add new components for v2 refactor

* refactor(infra-monitoring): add docs prop for entity group header

* refactor(infra-monitoring): remove old props of empty state

* refactor(infra-monitoring): use column header to show on filter side panel

* refactor(infra-monitoring): update header to use new v2 components

* refactor(infra-monitoring): update utils and types

* refactor(infra-monitoring): update k8s base list to accept v2 apis

* refactor(infra-monitoring): update k8s base details to v2 apis

* refactor(infra-monitoring): couple fixes and refactors on components

* refactor(infra-monitoring): migrate hosts to v2

* refactor(infra-monitoring): migrate clusters to v2

* refactor(infra-monitoring): migrate daemon sets to v2

* refactor(infra-monitoring): migrate deployments to v2

* refactor(infra-monitoring): migrate jobs to v2

* refactor(infra-monitoring): migrate namespaces to v2

* refactor(infra-monitoring): migrate nodes to v2

* refactor(infra-monitoring): migrate pods to v2

* refactor(infra-monitoring): migrate statefulsets to v2

* refactor(infra-monitoring): migrate volumes to v2

* refactor(infra-monitoring): cleanup on entity details

* refactor(infra-monitoring): refactor infra monitoring to use v2

* refactor(infra-monitoring): date time border with better contrast

* chore(infra-monitoring): remove todo, placeholders are valid

* fix(pr): address comments

---------

Co-authored-by: Nikhil Mantri <nikhil.mantri1999@gmail.com>
2026-07-10 14:39:46 +00:00
Nikhil Mantri
1e257a5b26 feat(infra-monitoring): daemonsets ready and misscheduled nodes count (#12008)
* chore: types and constants added

* chore: update buildDaemonSetRecords

* chore: updated orderBy:

* chore: wired in checks API:

* chore: generated api spec:

* chore: update integration tests

* fix: checks constants updated in integration tests
2026-07-10 13:10:50 +00:00
Vinicius Lourenço
7fd6852fa6 refactor(authz): use new authz components for roles (#12011) 2026-07-10 12:19:09 +00:00
Vinicius Lourenço
b5567d1550 refactor(authz): use new authz components for service accounts (#12007) 2026-07-10 12:18:55 +00:00
Yunus M
2017660eb8 feat(quick-filters): add end-to-end tests and helper functions for qu… (#12063)
* feat(quick-filters): add end-to-end tests and helper functions for quick filters settings panel

* chore: fix formatting issues

* fix: add classname to skeleton and assert on the classname

* fix: address review comments

* chore: fix formatting issues
2026-07-10 11:54:14 +00:00
Aditya Singh
ca5d888970 feat(trace-details): restore Open in Logs Explorer in span details Logs tab (#11941)
* feat(trace-details): add Open in Logs Explorer to span details Logs tab

Restores the Open in Logs Explorer action (removed with V2 in #11805): a
toolbar button above the log list and an inline action in the no-trace-logs
empty state. Also tightens the empty-state RESOURCES card layout (stacked,
content-width, no extra height).

* feat: add logs explorer btn in footer

* test(trace-details): cover Open in Logs Explorer footer button in span details

* test(trace-details): assert log-row click opens Logs Explorer with trace/span query

* style(trace-details): fix oxfmt formatting in SpanLogs test

* fix(trace-details): pin Open in Logs Explorer footer to the panel bottom

The footer lived inside the scrolling tab content, so it was pushed below
the fold whenever the span summary above the tabs was in view. Render it as
a sibling of the scrolling panelBody instead (Logs tab only), making it a
true fixed footer that is always visible.
2026-07-10 07:36:05 +00:00
Yunus M
76b4d249e2 chore: update package dependencies in package.json and pnpm-lock.yaml (#12066)
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-10 05:56:13 +00:00
168 changed files with 7690 additions and 5542 deletions

View File

@@ -3148,6 +3148,8 @@ components:
type: string
image:
type: string
legacy:
type: boolean
locked:
type: boolean
name:
@@ -3180,6 +3182,7 @@ components:
- name
- tags
- spec
- legacy
- pinned
type: object
DashboardtypesListedDashboardV2:
@@ -3193,6 +3196,8 @@ components:
type: string
image:
type: string
legacy:
type: boolean
locked:
type: boolean
name:
@@ -3223,6 +3228,7 @@ components:
- name
- tags
- spec
- legacy
type: object
DashboardtypesListedDashboardV2Spec:
properties:
@@ -4478,10 +4484,14 @@ components:
type: string
nullable: true
type: object
misscheduledNodes:
type: integer
podCountsByPhase:
$ref: '#/components/schemas/InframonitoringtypesPodCountsByPhase'
podCountsByStatus:
$ref: '#/components/schemas/InframonitoringtypesPodCountsByStatus'
readyNodes:
type: integer
required:
- daemonSetName
- daemonSetCPU
@@ -4492,6 +4502,8 @@ components:
- daemonSetMemoryLimit
- desiredNodes
- currentNodes
- readyNodes
- misscheduledNodes
- podCountsByPhase
- podCountsByStatus
- meta
@@ -15993,21 +16005,24 @@ paths:
daemonSetCPULimit, daemonSetMemoryRequest, daemonSetMemoryLimit). Each row
also reports the latest known node-level counters from kube-state-metrics:
desiredNodes (k8s.daemonset.desired_scheduled_nodes, the number of nodes the
daemonset wants to run on) and currentNodes (k8s.daemonset.current_scheduled_nodes,
the number of nodes the daemonset currently runs on) — note these are node
counts, not pod counts. It also reports per-group podCountsByPhase ({ pending,
running, succeeded, failed, unknown } from each pod''s latest k8s.pod.phase
value). Each daemonset includes metadata attributes (k8s.daemonset.name, k8s.namespace.name,
k8s.cluster.name). The response type is ''list'' for the default k8s.daemonset.name
grouping or ''grouped_list'' for custom groupBy keys; in both modes every
row aggregates pods owned by daemonsets in the group. Supports filtering via
a filter expression, custom groupBy, ordering by cpu / cpu_request / cpu_limit
/ memory / memory_request / memory_limit / desired_nodes / current_nodes,
daemonset wants to run on), currentNodes (k8s.daemonset.current_scheduled_nodes,
the number of nodes the daemonset currently runs on), readyNodes (k8s.daemonset.ready_nodes,
the number of nodes running at least one ready daemon pod) and misscheduledNodes
(k8s.daemonset.misscheduled_nodes, the number of nodes running the daemon
pod but not supposed to) — note these are node counts, not pod counts. It
also reports per-group podCountsByPhase ({ pending, running, succeeded, failed,
unknown } from each pod''s latest k8s.pod.phase value). Each daemonset includes
metadata attributes (k8s.daemonset.name, k8s.namespace.name, k8s.cluster.name).
The response type is ''list'' for the default k8s.daemonset.name grouping
or ''grouped_list'' for custom groupBy keys; in both modes every row aggregates
pods owned by daemonsets in the group. Supports filtering via a filter expression,
custom groupBy, ordering by cpu / cpu_request / cpu_limit / memory / memory_request
/ memory_limit / desired_nodes / current_nodes / ready_nodes / misscheduled_nodes,
and pagination via offset/limit. Also reports whether the requested time range
falls before the data retention boundary. Numeric metric fields (daemonSetCPU,
daemonSetCPURequest, daemonSetCPULimit, daemonSetMemory, daemonSetMemoryRequest,
daemonSetMemoryLimit, desiredNodes, currentNodes) return -1 as a sentinel
when no data is available for that field.'
daemonSetMemoryLimit, desiredNodes, currentNodes, readyNodes, misscheduledNodes)
return -1 as a sentinel when no data is available for that field.'
operationId: ListDaemonSets
requestBody:
content:

View File

@@ -75,7 +75,7 @@
"crypto-js": "4.2.0",
"d3-hierarchy": "3.1.2",
"dayjs": "^1.10.7",
"dompurify": "3.4.0",
"dompurify": "3.4.11",
"event-source-polyfill": "1.0.31",
"eventemitter3": "5.0.1",
"history": "4.10.1",
@@ -238,10 +238,18 @@
"prismjs": "1.30.0",
"got": "11.8.5",
"form-data": "4.0.6",
"brace-expansion": "^2.0.2",
"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"
"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"
}
}

152
frontend/pnpm-lock.yaml generated
View File

@@ -19,11 +19,19 @@ overrides:
prismjs: 1.30.0
got: 11.8.5
form-data: 4.0.6
brace-expansion: ^2.0.2
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
importers:
@@ -79,7 +87,7 @@ importers:
version: 0.0.2(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
'@signozhq/ui':
specifier: 0.0.23
version: 0.0.23(@emotion/is-prop-valid@1.2.0)(@signozhq/icons@0.4.0)(@types/react-dom@18.0.10)(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react-router-dom@5.3.4(react@18.2.0))(react-router@6.30.3(react@18.2.0))(react@18.2.0)
version: 0.0.23(@emotion/is-prop-valid@1.2.0)(@signozhq/icons@0.4.0)(@types/react-dom@18.0.10)(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react-router-dom@5.3.4(react@18.2.0))(react-router@6.30.4(react@18.2.0))(react@18.2.0)
'@tanstack/react-table':
specifier: 8.21.3
version: 8.21.3(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
@@ -153,8 +161,8 @@ importers:
specifier: ^1.10.7
version: 1.11.20
dompurify:
specifier: 3.4.0
version: 3.4.0
specifier: 3.4.11
version: 3.4.11
event-source-polyfill:
specifier: 1.0.31
version: 1.0.31
@@ -196,7 +204,7 @@ importers:
version: 12.4.13(@emotion/is-prop-valid@1.2.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
nuqs:
specifier: 2.8.8
version: 2.8.8(react-router-dom@5.3.4(react@18.2.0))(react-router@6.30.3(react@18.2.0))(react@18.2.0)
version: 2.8.8(react-router-dom@5.3.4(react@18.2.0))(react-router@6.30.4(react@18.2.0))(react@18.2.0)
overlayscrollbars:
specifier: ^2.8.1
version: 2.9.2
@@ -3040,6 +3048,10 @@ packages:
resolution: {integrity: sha512-Ic6m2U/rMjTkhERIa/0ZtXJP17QUi2CbWE7cqx4J58M8aA3QTfW+2UlQ4psvTX9IO1RfNVhK3pcpdjej7L+t2w==}
engines: {node: '>=14.0.0'}
'@remix-run/router@1.23.3':
resolution: {integrity: sha512-4An71tdz9X8+3sI4Qqqd2LWd9vS39J7sqd9EU4Scw7TJE/qB10Flv/UuqbPVgfQV9XoK8Np6jNquZitnZq5i+Q==}
engines: {node: '>=14.0.0'}
'@rolldown/binding-android-arm64@1.0.0-beta.53':
resolution: {integrity: sha512-Ok9V8o7o6YfSdTTYA/uHH30r3YtOxLD6G3wih/U9DO0ucBBFq8WPt/DslU53OgfteLRHITZny9N/qCUxMf9kjQ==}
engines: {node: ^20.19.0 || >=22.12.0}
@@ -4143,8 +4155,8 @@ packages:
boolbase@1.0.0:
resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==}
brace-expansion@2.0.2:
resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==}
brace-expansion@2.1.1:
resolution: {integrity: sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==}
braces@3.0.3:
resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==}
@@ -4805,14 +4817,8 @@ packages:
resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==}
engines: {node: '>= 4'}
dompurify@3.2.4:
resolution: {integrity: sha512-ysFSFEDVduQpyhzAob/kkuJjf5zWkZD8/A9ywSp1byueyuCfHamrCBa14/Oc2iiB0e51B+NpxSl5gmzn+Ms/mg==}
dompurify@3.2.7:
resolution: {integrity: sha512-WhL/YuveyGXJaerVlMYGWhvQswa7myDG17P7Vu65EWC05o8vfeNbvNf4d/BOvH99+ZW+LlQsc1GDKMa1vNK6dw==}
dompurify@3.4.0:
resolution: {integrity: sha512-nolgK9JcaUXMSmW+j1yaSvaEaoXYHwWyGJlkoCTghc97KgGDDSnpoU/PlEnw63Ah+TGKFOyY+X5LnxaWbCSfXg==}
dompurify@3.4.11:
resolution: {integrity: sha512-zhlUV12GsaRzMsf9q5M254YhA4+VuF0fG+QFqu6aYpoGlKtz+w8//jBcGVYBgQkR5GHjUomejY84AV+/uPbWdw==}
domutils@2.8.0:
resolution: {integrity: sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==}
@@ -6055,12 +6061,12 @@ packages:
js-tokens@4.0.0:
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
js-yaml@3.14.1:
resolution: {integrity: sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==}
js-yaml@3.15.0:
resolution: {integrity: sha512-ttBQIIQPDeLjpPOohtUdXuXUVoA2uIB6fEH9HyJ7234s5mBJ5wTx20njxplLZQgLaOfpmPQA7X2t5AX6tIPbog==}
hasBin: true
js-yaml@4.1.1:
resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==}
js-yaml@4.2.0:
resolution: {integrity: sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==}
hasBin: true
jsdom@20.0.3:
@@ -6244,8 +6250,8 @@ packages:
lines-and-columns@1.2.4:
resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==}
linkify-it@5.0.0:
resolution: {integrity: sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==}
linkify-it@5.0.2:
resolution: {integrity: sha512-ONTm2jCMAVZjgQa/Fy1kScXsuOoF5NPTsoFBdE1KVIZ2vAh/r9+Bqo+0jINCBYnavTPQZz38QzFTme79ENoN3Q==}
lint-staged@17.0.4:
resolution: {integrity: sha512-+rU9lSUyVOZ/hDUmRLVGzyS2v73cDdQjX+XQz1AaOdIE4RysLq0HoPW2HrrgeNCLklkhi904VBU1bmgWLHVnkA==}
@@ -6364,8 +6370,8 @@ packages:
mapbox-to-css-font@2.4.5:
resolution: {integrity: sha512-VJ6nB8emkO9VODI0Fk+TQ/0zKBTqmf/Pkt8Xv0kHstoc0iXRajA00DAid4Kc3K5xeFIOoiZrVxijEzj0GLVO2w==}
markdown-it@14.1.1:
resolution: {integrity: sha512-BuU2qnTti9YKgK5N+IeMubp14ZUKUUw7yeJbkjtosvHiP0AZ5c8IAgEMk79D0eC8F23r4Ac/q8cAIFdm2FtyoA==}
markdown-it@14.2.0:
resolution: {integrity: sha512-1TGiQiJVRQ3NPmZH6sx5Cfnmg6GQm9jvC1ch4TK511NjSJvjzKLzn5pPfZRNZkRPZP0HqCioSndqH8v2nRaWVQ==}
hasBin: true
markdown-table@3.0.4:
@@ -6429,8 +6435,8 @@ packages:
mdast-util-to-hast@12.3.0:
resolution: {integrity: sha512-pits93r8PhnIoU4Vy9bjW39M2jJ6/tdHyja9rrot9uujkN7UTU9SDnE6WNJz/IGyQk3XHX6yNNtrBH6cQzm8Hw==}
mdast-util-to-hast@13.1.0:
resolution: {integrity: sha512-/e2l/6+OdGp/FB+ctrJ9Avz71AN/GRH3oi/3KAx/kMnoUsD6q0woXlDT8lLEeViVKE7oZxE7RXzvO3T8kF2/sA==}
mdast-util-to-hast@13.2.1:
resolution: {integrity: sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==}
mdast-util-to-markdown@1.5.0:
resolution: {integrity: sha512-bbv7TPv/WC49thZPg3jXuqzuvI45IL2EVAr/KxF0BSdHsU0ceFHOmwQn6evxAh1GaoK/6GQ1wp4R4oW2+LFL/A==}
@@ -6772,7 +6778,7 @@ packages:
'@tanstack/react-router': ^1
next: '>=14.2.0'
react: '>=18.2.0 || ^19.0.0-0'
react-router: ^5 || ^6 || ^7
react-router: 6.30.4
react-router-dom: ^5 || ^6 || ^7
peerDependenciesMeta:
'@remix-run/react':
@@ -6793,7 +6799,7 @@ packages:
'@tanstack/react-router': ^1
next: '>=14.2.0'
react: '>=18.2.0 || ^19.0.0-0'
react-router: ^5 || ^6 || ^7
react-router: 6.30.4
react-router-dom: ^5 || ^6 || ^7
peerDependenciesMeta:
'@remix-run/react':
@@ -7174,8 +7180,8 @@ packages:
property-information@6.3.0:
resolution: {integrity: sha512-gVNZ74nqhRMiIUYWGQdosYetaKc83x8oT41a0LlV3AAFCAZwCpg4vmGkq8t34+cUhp3cnM4XDiU/7xlgK7HGrg==}
protocol-buffers-schema@3.6.0:
resolution: {integrity: sha512-TdDRD+/QNdrCGCE7v8340QyuXd4kIWIgapsE2+n/SaGiSSbomYl4TjHlvIoCWRpE7wFt02EpB35VVA2ImcBVqw==}
protocol-buffers-schema@3.6.1:
resolution: {integrity: sha512-VG2K63Igkiv9p76tk1lilczEK1cT+kCjKtkdhw1dQZV3k3IXJbd3o6Ho8b9zJZaHSnT2hKe4I+ObmX9w6m5SmQ==}
proxy-from-env@1.1.0:
resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==}
@@ -7690,8 +7696,8 @@ packages:
peerDependencies:
react: '>=15'
react-router@6.30.3:
resolution: {integrity: sha512-XRnlbKMTmktBkjCLE8/XcZFlnHvr2Ltdr1eJX4idL55/9BbORzyZEaIkBFDhFGCEWBBItsVrDxwx3gnisMitdw==}
react-router@6.30.4:
resolution: {integrity: sha512-SVUsDe+DybHM/WmYKIVYhZh1o5Dcuf16yM6WjG02Q9XVFMZIJyHYhwrr6bFBXZkVP6z69kNkMyBCujt8FaFLJA==}
engines: {node: '>=14.0.0'}
peerDependencies:
react: '>=16.8'
@@ -8946,8 +8952,8 @@ packages:
yallist@4.0.0:
resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==}
yaml@1.10.2:
resolution: {integrity: sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==}
yaml@1.10.3:
resolution: {integrity: sha512-vIYeF1u3CjlhAFekPPAk2h/Kv4T3mAkMox5OymRiJQB0spDP10LHvt+K7G9Ny6NuuMAb25/6n1qyUjAcGNf/AA==}
engines: {node: '>= 6'}
yaml@2.8.2:
@@ -9068,7 +9074,7 @@ snapshots:
dependencies:
'@jsdevtools/ono': 7.1.3
'@types/json-schema': 7.0.15
js-yaml: 4.1.1
js-yaml: 4.2.0
'@babel/code-frame@7.29.0':
dependencies:
@@ -10327,7 +10333,7 @@ snapshots:
'@types/string-hash': 1.1.3
d3-interpolate: 3.0.1
date-fns: 4.1.0
dompurify: 3.2.4
dompurify: 3.4.11
eventemitter3: 5.0.1
fast_array_intersect: 1.1.0
history: 4.10.1
@@ -10478,7 +10484,7 @@ snapshots:
camelcase: 5.3.1
find-up: 4.1.0
get-package-type: 0.1.0
js-yaml: 3.14.1
js-yaml: 3.15.0
resolve-from: 5.0.0
'@istanbuljs/schema@0.1.3': {}
@@ -11805,6 +11811,8 @@ snapshots:
'@remix-run/router@1.23.2': {}
'@remix-run/router@1.23.3': {}
'@rolldown/binding-android-arm64@1.0.0-beta.53':
optional: true
@@ -12042,7 +12050,7 @@ snapshots:
- react-dom
- tailwindcss
'@signozhq/ui@0.0.23(@emotion/is-prop-valid@1.2.0)(@signozhq/icons@0.4.0)(@types/react-dom@18.0.10)(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react-router-dom@5.3.4(react@18.2.0))(react-router@6.30.3(react@18.2.0))(react@18.2.0)':
'@signozhq/ui@0.0.23(@emotion/is-prop-valid@1.2.0)(@signozhq/icons@0.4.0)(@types/react-dom@18.0.10)(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react-router-dom@5.3.4(react@18.2.0))(react-router@6.30.4(react@18.2.0))(react@18.2.0)':
dependencies:
'@chenglou/pretext': 0.0.5
'@radix-ui/react-checkbox': 1.3.3(@types/react-dom@18.0.10)(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
@@ -12071,7 +12079,7 @@ snapshots:
lodash-es: 4.18.1
motion: 11.18.2(@emotion/is-prop-valid@1.2.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
next-themes: 0.4.6(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
nuqs: 2.8.9(react-router-dom@5.3.4(react@18.2.0))(react-router@6.30.3(react@18.2.0))(react@18.2.0)
nuqs: 2.8.9(react-router-dom@5.3.4(react@18.2.0))(react-router@6.30.4(react@18.2.0))(react@18.2.0)
react: 18.2.0
react-day-picker: 9.9.0(react@18.2.0)
react-dom: 18.2.0(react@18.2.0)
@@ -13057,7 +13065,7 @@ snapshots:
boolbase@1.0.0: {}
brace-expansion@2.0.2:
brace-expansion@2.1.1:
dependencies:
balanced-match: 1.0.2
@@ -13381,14 +13389,14 @@ snapshots:
import-fresh: 3.3.1
parse-json: 5.2.0
path-type: 4.0.0
yaml: 1.10.2
yaml: 1.10.3
optional: true
cosmiconfig@9.0.1(typescript@5.9.3):
dependencies:
env-paths: 2.2.1
import-fresh: 3.3.1
js-yaml: 4.1.1
js-yaml: 4.2.0
parse-json: 5.2.0
optionalDependencies:
typescript: 5.9.3
@@ -13720,15 +13728,7 @@ snapshots:
dependencies:
domelementtype: 2.3.0
dompurify@3.2.4:
optionalDependencies:
'@types/trusted-types': 2.0.7
dompurify@3.2.7:
optionalDependencies:
'@types/trusted-types': 2.0.7
dompurify@3.4.0:
dompurify@3.4.11:
optionalDependencies:
'@types/trusted-types': 2.0.7
@@ -14416,7 +14416,7 @@ snapshots:
hast-util-from-parse5: 8.0.1
hast-util-to-parse5: 8.0.0
html-void-elements: 3.0.0
mdast-util-to-hast: 13.1.0
mdast-util-to-hast: 13.2.1
parse5: 7.3.0
unist-util-position: 5.0.0
unist-util-visit: 5.0.0
@@ -15342,12 +15342,12 @@ snapshots:
js-tokens@4.0.0: {}
js-yaml@3.14.1:
js-yaml@3.15.0:
dependencies:
argparse: 1.0.10
esprima: 4.0.1
js-yaml@4.1.1:
js-yaml@4.2.0:
dependencies:
argparse: 2.0.1
@@ -15396,7 +15396,7 @@ snapshots:
'@types/json-schema': 7.0.15
'@types/lodash': 4.17.24
is-glob: 4.0.3
js-yaml: 4.1.1
js-yaml: 4.2.0
lodash: 4.18.1
minimist: 1.2.8
prettier: 3.8.3
@@ -15533,7 +15533,7 @@ snapshots:
lines-and-columns@1.2.4: {}
linkify-it@5.0.0:
linkify-it@5.0.2:
dependencies:
uc.micro: 2.1.0
@@ -15654,11 +15654,11 @@ snapshots:
mapbox-to-css-font@2.4.5: {}
markdown-it@14.1.1:
markdown-it@14.2.0:
dependencies:
argparse: 2.0.1
entities: 4.5.0
linkify-it: 5.0.0
linkify-it: 5.0.2
mdurl: 2.0.0
punycode.js: 2.3.1
uc.micro: 2.1.0
@@ -15772,7 +15772,7 @@ snapshots:
unist-util-position: 4.0.4
unist-util-visit: 4.1.2
mdast-util-to-hast@13.1.0:
mdast-util-to-hast@13.2.1:
dependencies:
'@types/hast': 3.0.4
'@types/mdast': 4.0.3
@@ -16049,19 +16049,19 @@ snapshots:
minimatch@10.2.5:
dependencies:
brace-expansion: 2.0.2
brace-expansion: 2.1.1
minimatch@3.1.5:
dependencies:
brace-expansion: 2.0.2
brace-expansion: 2.1.1
minimatch@5.1.9:
dependencies:
brace-expansion: 2.0.2
brace-expansion: 2.1.1
minimatch@9.0.9:
dependencies:
brace-expansion: 2.0.2
brace-expansion: 2.1.1
minimist@1.2.8: {}
@@ -16075,7 +16075,7 @@ snapshots:
monaco-editor@0.55.1:
dependencies:
dompurify: 3.2.7
dompurify: 3.4.11
marked: 14.0.0
motion-dom@11.18.1:
@@ -16216,20 +16216,20 @@ snapshots:
dependencies:
boolbase: 1.0.0
nuqs@2.8.8(react-router-dom@5.3.4(react@18.2.0))(react-router@6.30.3(react@18.2.0))(react@18.2.0):
nuqs@2.8.8(react-router-dom@5.3.4(react@18.2.0))(react-router@6.30.4(react@18.2.0))(react@18.2.0):
dependencies:
'@standard-schema/spec': 1.0.0
react: 18.2.0
optionalDependencies:
react-router: 6.30.3(react@18.2.0)
react-router: 6.30.4(react@18.2.0)
react-router-dom: 5.3.4(react@18.2.0)
nuqs@2.8.9(react-router-dom@5.3.4(react@18.2.0))(react-router@6.30.3(react@18.2.0))(react@18.2.0):
nuqs@2.8.9(react-router-dom@5.3.4(react@18.2.0))(react-router@6.30.4(react@18.2.0))(react@18.2.0):
dependencies:
'@standard-schema/spec': 1.0.0
react: 18.2.0
optionalDependencies:
react-router: 6.30.3(react@18.2.0)
react-router: 6.30.4(react@18.2.0)
react-router-dom: 5.3.4(react@18.2.0)
nwsapi@2.2.23: {}
@@ -16336,7 +16336,7 @@ snapshots:
find-up: 8.0.0
fs-extra: 11.3.3
jiti: 2.6.1
js-yaml: 4.1.1
js-yaml: 4.2.0
remeda: 2.34.0
string-argv: 0.3.2
tsconfck: 3.1.6(typescript@5.9.3)
@@ -16549,7 +16549,7 @@ snapshots:
postcss-load-config@3.1.4(postcss@8.5.14)(ts-node@10.9.1(@types/node@16.18.25)(typescript@5.9.3)):
dependencies:
lilconfig: 2.1.0
yaml: 1.10.2
yaml: 1.10.3
optionalDependencies:
postcss: 8.5.14
ts-node: 10.9.1(@types/node@16.18.25)(typescript@5.9.3)
@@ -16654,7 +16654,7 @@ snapshots:
property-information@6.3.0: {}
protocol-buffers-schema@3.6.0: {}
protocol-buffers-schema@3.6.1: {}
proxy-from-env@1.1.0: {}
@@ -17264,7 +17264,7 @@ snapshots:
history: 5.3.0
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
react-router: 6.30.3(react@18.2.0)
react-router: 6.30.4(react@18.2.0)
react-router-dom: 5.3.4(react@18.2.0)
react-router-dom@5.3.4(react@18.2.0):
@@ -17291,9 +17291,9 @@ snapshots:
tiny-invariant: 1.3.3
tiny-warning: 1.0.3
react-router@6.30.3(react@18.2.0):
react-router@6.30.4(react@18.2.0):
dependencies:
'@remix-run/router': 1.23.2
'@remix-run/router': 1.23.3
react: 18.2.0
react-style-singleton@2.2.3(@types/react@18.0.26)(react@18.2.0):
@@ -17517,7 +17517,7 @@ snapshots:
resolve-protobuf-schema@2.1.0:
dependencies:
protocol-buffers-schema: 3.6.0
protocol-buffers-schema: 3.6.1
resolve@1.22.11:
dependencies:
@@ -18209,7 +18209,7 @@ snapshots:
dependencies:
'@gerrit0/mini-shiki': 3.23.0
lunr: 2.3.9
markdown-it: 14.1.1
markdown-it: 14.2.0
minimatch: 10.2.5
typescript: 5.9.3
yaml: 2.8.4
@@ -18673,7 +18673,7 @@ snapshots:
yallist@4.0.0: {}
yaml@1.10.2: {}
yaml@1.10.3: {}
yaml@2.8.2: {}

View File

@@ -219,7 +219,7 @@ export const useListClusters = <
return useMutation(getListClustersMutationOptions(options));
};
/**
* Returns a paginated list of Kubernetes DaemonSets with key aggregated pod metrics: CPU usage and memory working set summed across pods owned by the daemonset, plus average CPU/memory request and limit utilization (daemonSetCPURequest, daemonSetCPULimit, daemonSetMemoryRequest, daemonSetMemoryLimit). Each row also reports the latest known node-level counters from kube-state-metrics: desiredNodes (k8s.daemonset.desired_scheduled_nodes, the number of nodes the daemonset wants to run on) and currentNodes (k8s.daemonset.current_scheduled_nodes, the number of nodes the daemonset currently runs on) — note these are node counts, not pod counts. It also reports per-group podCountsByPhase ({ pending, running, succeeded, failed, unknown } from each pod's latest k8s.pod.phase value). Each daemonset includes metadata attributes (k8s.daemonset.name, k8s.namespace.name, k8s.cluster.name). The response type is 'list' for the default k8s.daemonset.name grouping or 'grouped_list' for custom groupBy keys; in both modes every row aggregates pods owned by daemonsets in the group. Supports filtering via a filter expression, custom groupBy, ordering by cpu / cpu_request / cpu_limit / memory / memory_request / memory_limit / desired_nodes / current_nodes, and pagination via offset/limit. Also reports whether the requested time range falls before the data retention boundary. Numeric metric fields (daemonSetCPU, daemonSetCPURequest, daemonSetCPULimit, daemonSetMemory, daemonSetMemoryRequest, daemonSetMemoryLimit, desiredNodes, currentNodes) return -1 as a sentinel when no data is available for that field.
* Returns a paginated list of Kubernetes DaemonSets with key aggregated pod metrics: CPU usage and memory working set summed across pods owned by the daemonset, plus average CPU/memory request and limit utilization (daemonSetCPURequest, daemonSetCPULimit, daemonSetMemoryRequest, daemonSetMemoryLimit). Each row also reports the latest known node-level counters from kube-state-metrics: desiredNodes (k8s.daemonset.desired_scheduled_nodes, the number of nodes the daemonset wants to run on), currentNodes (k8s.daemonset.current_scheduled_nodes, the number of nodes the daemonset currently runs on), readyNodes (k8s.daemonset.ready_nodes, the number of nodes running at least one ready daemon pod) and misscheduledNodes (k8s.daemonset.misscheduled_nodes, the number of nodes running the daemon pod but not supposed to) — note these are node counts, not pod counts. It also reports per-group podCountsByPhase ({ pending, running, succeeded, failed, unknown } from each pod's latest k8s.pod.phase value). Each daemonset includes metadata attributes (k8s.daemonset.name, k8s.namespace.name, k8s.cluster.name). The response type is 'list' for the default k8s.daemonset.name grouping or 'grouped_list' for custom groupBy keys; in both modes every row aggregates pods owned by daemonsets in the group. Supports filtering via a filter expression, custom groupBy, ordering by cpu / cpu_request / cpu_limit / memory / memory_request / memory_limit / desired_nodes / current_nodes / ready_nodes / misscheduled_nodes, and pagination via offset/limit. Also reports whether the requested time range falls before the data retention boundary. Numeric metric fields (daemonSetCPU, daemonSetCPURequest, daemonSetCPULimit, daemonSetMemory, daemonSetMemoryRequest, daemonSetMemoryLimit, desiredNodes, currentNodes, readyNodes, misscheduledNodes) return -1 as a sentinel when no data is available for that field.
* @summary List DaemonSets for Infra Monitoring
*/
export const listDaemonSets = (

View File

@@ -5005,6 +5005,10 @@ export interface DashboardtypesListedDashboardForUserV2DTO {
* @type string
*/
image?: string;
/**
* @type boolean
*/
legacy: boolean;
/**
* @type boolean
*/
@@ -5080,6 +5084,10 @@ export interface DashboardtypesListedDashboardV2DTO {
* @type string
*/
image?: string;
/**
* @type boolean
*/
legacy: boolean;
/**
* @type boolean
*/
@@ -6082,8 +6090,16 @@ export interface InframonitoringtypesDaemonSetRecordDTO {
* @type object,null
*/
meta: InframonitoringtypesDaemonSetRecordDTOMeta;
/**
* @type integer
*/
misscheduledNodes: number;
podCountsByPhase: InframonitoringtypesPodCountsByPhaseDTO;
podCountsByStatus: InframonitoringtypesPodCountsByStatusDTO;
/**
* @type integer
*/
readyNodes: number;
}
export interface InframonitoringtypesDaemonSetsDTO {

View File

@@ -22,6 +22,7 @@ export const getKeySuggestions = (
fieldContext = '',
fieldDataType = '',
signalSource = '',
metricNamespace = '',
} = props;
const encodedSignal = encodeURIComponent(signal);
@@ -30,8 +31,9 @@ export const getKeySuggestions = (
const encodedFieldContext = encodeURIComponent(fieldContext);
const encodedFieldDataType = encodeURIComponent(fieldDataType);
const encodedSource = encodeURIComponent(signalSource);
const encodedMetricNamespace = encodeURIComponent(metricNamespace);
return axios.get(
`/fields/keys?signal=${encodedSignal}&searchText=${encodedSearchText}&metricName=${encodedMetricName}&fieldContext=${encodedFieldContext}&fieldDataType=${encodedFieldDataType}&source=${encodedSource}`,
`/fields/keys?signal=${encodedSignal}&searchText=${encodedSearchText}&metricName=${encodedMetricName}&fieldContext=${encodedFieldContext}&fieldDataType=${encodedFieldDataType}&source=${encodedSource}&metricNamespace=${encodedMetricNamespace}`,
);
};

View File

@@ -2,7 +2,7 @@ import { Controller, useForm } from 'react-hook-form';
import { useQueryClient } from 'react-query';
import { X } from '@signozhq/icons';
import { Button } from '@signozhq/ui/button';
import AuthZTooltip from 'lib/authz/components/AuthZTooltip/AuthZTooltip';
import AuthZButton from 'lib/authz/components/AuthZButton/AuthZButton';
import { SACreatePermission } from 'lib/authz/hooks/useAuthZ/permissions/service-account.permissions';
import { DialogFooter, DialogWrapper } from '@signozhq/ui/dialog';
import { Input } from '@signozhq/ui/input';
@@ -134,18 +134,17 @@ function CreateServiceAccountModal(): JSX.Element {
Cancel
</Button>
<AuthZTooltip checks={[SACreatePermission]}>
<Button
type="submit"
form="create-sa-form"
variant="solid"
color="primary"
loading={isSubmitting}
disabled={!isValid}
>
Create Service Account
</Button>
</AuthZTooltip>
<AuthZButton
checks={[SACreatePermission]}
type="submit"
form="create-sa-form"
variant="solid"
color="primary"
loading={isSubmitting}
disabled={!isValid}
>
Create Service Account
</AuthZButton>
</DialogFooter>
</DialogWrapper>
);

View File

@@ -89,6 +89,7 @@ interface QuerySearchProps {
onChange: (value: string) => void;
queryData: IBuilderQuery;
dataSource: DataSource;
metricNamespace?: string;
signalSource?: string;
hardcodedAttributeKeys?: QueryKeyDataSuggestionsProps[];
onRun?: (query: string) => void;
@@ -107,6 +108,7 @@ function QuerySearch({
hardcodedAttributeKeys,
showFilterSuggestionsWithoutMetric,
initialExpression,
metricNamespace,
}: QuerySearchProps): JSX.Element {
const isDarkMode = useIsDarkMode();
const [valueSuggestions, setValueSuggestions] = useState<any[]>([]);
@@ -299,6 +301,7 @@ function QuerySearch({
searchText: searchText || '',
metricName: debouncedMetricName ?? undefined,
signalSource: signalSource as 'meter' | '',
metricNamespace,
});
if (response.data.data) {
@@ -331,6 +334,7 @@ function QuerySearch({
signalSource,
hardcodedAttributeKeys,
showFilterSuggestionsWithoutMetric,
metricNamespace,
],
);

View File

@@ -20,6 +20,7 @@ function OtherFiltersSkeleton(): JSX.Element {
<Skeleton.Input
active
size="small"
className="qf-other-filters-skeleton"
// eslint-disable-next-line react/no-array-index-key
key={index}
/>

View File

@@ -4,7 +4,7 @@ import { Button } from '@signozhq/ui/button';
import { Input } from '@signozhq/ui/input';
import { ToggleGroupSimple } from '@signozhq/ui/toggle-group';
import { DatePicker } from 'antd';
import AuthZTooltip from 'lib/authz/components/AuthZTooltip/AuthZTooltip';
import AuthZButton from 'lib/authz/components/AuthZButton/AuthZButton';
import {
APIKeyCreatePermission,
buildSAAttachPermission,
@@ -109,24 +109,21 @@ function KeyFormPhase({
<Button variant="solid" color="secondary" onClick={onClose}>
Cancel
</Button>
<AuthZTooltip
<AuthZButton
checks={[
APIKeyCreatePermission,
buildSAAttachPermission(accountId ?? ''),
]}
enabled={!!accountId}
authZEnabled={!!accountId}
type="submit"
form={FORM_ID}
variant="solid"
color="primary"
loading={isSubmitting}
disabled={!isValid}
>
<Button
type="submit"
form={FORM_ID}
variant="solid"
color="primary"
loading={isSubmitting}
disabled={!isValid}
>
Create Key
</Button>
</AuthZTooltip>
Create Key
</AuthZButton>
</div>
</div>
</>

View File

@@ -1,7 +1,7 @@
import { useQueryClient } from 'react-query';
import { Trash2, X } from '@signozhq/icons';
import { Button } from '@signozhq/ui/button';
import AuthZTooltip from 'lib/authz/components/AuthZTooltip/AuthZTooltip';
import AuthZButton from 'lib/authz/components/AuthZButton/AuthZButton';
import { buildSADeletePermission } from 'lib/authz/hooks/useAuthZ/permissions/service-account.permissions';
import { DialogWrapper } from '@signozhq/ui/dialog';
import { toast } from '@signozhq/ui/sonner';
@@ -84,20 +84,17 @@ function DeleteAccountModal(): JSX.Element {
<X size={12} />
Cancel
</Button>
<AuthZTooltip
<AuthZButton
checks={[buildSADeletePermission(accountId ?? '')]}
enabled={!!accountId}
authZEnabled={!!accountId}
variant="solid"
color="destructive"
loading={isDeleting}
onClick={handleConfirm}
>
<Button
variant="solid"
color="destructive"
loading={isDeleting}
onClick={handleConfirm}
>
<Trash2 size={12} />
Delete
</Button>
</AuthZTooltip>
<Trash2 size={12} />
Delete
</AuthZButton>
</div>
);

View File

@@ -7,6 +7,7 @@ import { Input } from '@signozhq/ui/input';
import { ToggleGroupSimple } from '@signozhq/ui/toggle-group';
import { DatePicker } from 'antd';
import type { ServiceaccounttypesGettableFactorAPIKeyDTO } from 'api/generated/services/sigNoz.schemas';
import AuthZButton from 'lib/authz/components/AuthZButton/AuthZButton';
import AuthZTooltip from 'lib/authz/components/AuthZTooltip/AuthZTooltip';
import {
buildAPIKeyDeletePermission,
@@ -158,38 +159,36 @@ function EditKeyForm({
</form>
<div className="edit-key-modal__footer">
<AuthZTooltip
<AuthZButton
checks={[
buildAPIKeyDeletePermission(keyItem?.id ?? ''),
buildSADetachPermission(accountId ?? ''),
]}
enabled={!!accountId && !!keyItem?.id}
authZEnabled={!!accountId && !!keyItem?.id}
variant="link"
color="destructive"
onClick={onRevokeClick}
>
<Button variant="link" color="destructive" onClick={onRevokeClick}>
<Trash2 size={12} />
Revoke Key
</Button>
</AuthZTooltip>
<Trash2 size={12} />
Revoke Key
</AuthZButton>
<div className="edit-key-modal__footer-right">
<Button variant="solid" color="secondary" onClick={onClose}>
<X size={12} />
Cancel
</Button>
<AuthZTooltip
<AuthZButton
checks={[buildAPIKeyUpdatePermission(keyItem?.id ?? '')]}
enabled={!!accountId && !!keyItem?.id}
authZEnabled={!!accountId && !!keyItem?.id}
type="submit"
form={FORM_ID}
variant="solid"
color="primary"
loading={isSaving}
disabled={!isDirty}
>
<Button
type="submit"
form={FORM_ID}
variant="solid"
color="primary"
loading={isSaving}
disabled={!isDirty}
>
Save Changes
</Button>
</AuthZTooltip>
Save Changes
</AuthZButton>
</div>
</div>
</>

View File

@@ -1,12 +1,13 @@
import React, { useCallback, useMemo } from 'react';
import { KeyRound, X } from '@signozhq/icons';
import { Button } from '@signozhq/ui/button';
import { Skeleton, Table, Tooltip } from 'antd';
import { Pagination, Skeleton, Table, Tooltip } from 'antd';
import type { ColumnsType } from 'antd/es/table/interface';
import type { ServiceaccounttypesGettableFactorAPIKeyDTO } from 'api/generated/services/sigNoz.schemas';
import AuthZTooltip from 'lib/authz/components/AuthZTooltip/AuthZTooltip';
import AuthZButton from 'lib/authz/components/AuthZButton/AuthZButton';
import { withAuthZContent } from 'lib/authz/components/withAuthZ/withAuthZContent';
import {
APIKeyCreatePermission,
APIKeyListPermission,
buildAPIKeyDeletePermission,
buildSAAttachPermission,
buildSADetachPermission,
@@ -24,10 +25,10 @@ interface KeysTabProps {
keys: ServiceaccounttypesGettableFactorAPIKeyDTO[];
isLoading: boolean;
isDisabled?: boolean;
canUpdate?: boolean;
accountId?: string;
currentPage: number;
pageSize: number;
onPageChange: (page: number) => void;
}
interface BuildColumnsParams {
@@ -113,29 +114,26 @@ function buildColumns({
render: (_, record): JSX.Element => {
const tooltipTitle = isDisabled ? 'Service account disabled' : 'Revoke Key';
return (
<AuthZTooltip
checks={[
buildAPIKeyDeletePermission(record.id),
buildSADetachPermission(accountId),
]}
enabled={!isDisabled && !!accountId}
>
<Tooltip title={tooltipTitle}>
<Button
variant="ghost"
size="sm"
color="destructive"
disabled={isDisabled}
onClick={(e): void => {
e.stopPropagation();
onRevokeClick(record.id);
}}
className="keys-tab__revoke-btn"
>
<X size={12} />
</Button>
</Tooltip>
</AuthZTooltip>
<Tooltip title={tooltipTitle}>
<AuthZButton
checks={[
buildAPIKeyDeletePermission(record.id),
buildSADetachPermission(accountId),
]}
authZEnabled={!isDisabled && !!accountId}
variant="ghost"
size="sm"
color="destructive"
disabled={isDisabled}
onClick={(e): void => {
e.stopPropagation();
onRevokeClick(record.id);
}}
className="keys-tab__revoke-btn"
>
<X size={12} />
</AuthZButton>
</Tooltip>
);
},
},
@@ -149,6 +147,7 @@ function KeysTab({
accountId = '',
currentPage,
pageSize,
onPageChange,
}: KeysTabProps): JSX.Element {
const [, setIsAddKeyOpen] = useQueryState(
'add-key',
@@ -212,21 +211,18 @@ function KeysTab({
Learn more
</a>
</p>
<AuthZTooltip
<AuthZButton
checks={[APIKeyCreatePermission, buildSAAttachPermission(accountId)]}
enabled={!isDisabled && !!accountId}
authZEnabled={!isDisabled && !!accountId}
variant="link"
color="primary"
onClick={async (): Promise<void> => {
await setIsAddKeyOpen(true);
}}
disabled={isDisabled}
>
<Button
variant="link"
color="primary"
onClick={async (): Promise<void> => {
await setIsAddKeyOpen(true);
}}
disabled={isDisabled}
>
+ Add your first key
</Button>
</AuthZTooltip>
+ Add your first key
</AuthZButton>
</div>
);
}
@@ -278,6 +274,24 @@ function KeysTab({
})}
/>
<Pagination
current={currentPage}
pageSize={pageSize}
total={keys.length}
showTotal={(total: number, range: number[]): JSX.Element => (
<>
<span className="sa-drawer__pagination-range">
{range[0]} &#8212; {range[1]}
</span>
<span className="sa-drawer__pagination-total"> of {total}</span>
</>
)}
showSizeChanger={false}
hideOnSinglePage
onChange={onPageChange}
className="sa-drawer__keys-pagination"
/>
<EditKeyModal keyItem={editKey} />
<RevokeKeyModal />
@@ -285,4 +299,7 @@ function KeysTab({
);
}
export default KeysTab;
export default withAuthZContent(KeysTab, {
checks: [APIKeyListPermission],
fallbackOnLoading: <Skeleton active paragraph={{ rows: 6 }} />,
});

View File

@@ -6,15 +6,20 @@ import { Input } from '@signozhq/ui/input';
import { useCopyToClipboard } from 'react-use';
import type { AuthtypesRoleDTO } 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';
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
import { ServiceAccountRow } from 'container/ServiceAccountsSettings/utils';
import { buildSAUpdatePermission } from 'lib/authz/hooks/useAuthZ/permissions/service-account.permissions';
import {
buildSAReadPermission,
buildSAUpdatePermission,
} from 'lib/authz/hooks/useAuthZ/permissions/service-account.permissions';
import { useTimezone } from 'providers/Timezone';
import APIError from 'types/api/error';
import SaveErrorItem from './SaveErrorItem';
import type { SaveError } from './utils';
import { Skeleton } from 'antd';
interface OverviewTabProps {
account: ServiceAccountRow;
@@ -23,7 +28,6 @@ interface OverviewTabProps {
localRoles: string[];
onRolesChange: (v: string[]) => void;
isDisabled: boolean;
canUpdate?: boolean;
availableRoles: AuthtypesRoleDTO[];
rolesLoading?: boolean;
rolesError?: boolean;
@@ -39,7 +43,6 @@ function OverviewTab({
localRoles,
onRolesChange,
isDisabled,
canUpdate = true,
availableRoles,
rolesLoading,
rolesError,
@@ -86,23 +89,22 @@ function OverviewTab({
<label className="sa-drawer__label" htmlFor="sa-name">
Name
</label>
{isDisabled || !canUpdate ? (
<AuthZTooltip
checks={[buildSAUpdatePermission(account.id)]}
enabled={!isDisabled && !canUpdate}
>
{isDisabled ? (
<AuthZTooltip checks={[buildSAUpdatePermission(account.id)]}>
<div className="sa-drawer__input-wrapper sa-drawer__input-wrapper--disabled">
<span className="sa-drawer__input-text">{localName || '—'}</span>
<LockKeyhole size={14} className="sa-drawer__lock-icon" />
</div>
</AuthZTooltip>
) : (
<Input
id="sa-name"
value={localName}
onChange={(e): void => onNameChange(e.target.value)}
placeholder="Enter name"
/>
<AuthZTooltip checks={[buildSAUpdatePermission(account.id)]}>
<Input
id="sa-name"
value={localName}
onChange={(e): void => onNameChange(e.target.value)}
placeholder="Enter name"
/>
</AuthZTooltip>
)}
</div>
@@ -220,4 +222,9 @@ function OverviewTab({
);
}
export default OverviewTab;
export default withAuthZContent(OverviewTab, {
checks: (props): ReturnType<typeof buildSAReadPermission>[] => [
buildSAReadPermission(props.account.id),
],
fallbackOnLoading: <Skeleton active paragraph={{ rows: 6 }} />,
});

View File

@@ -1,7 +1,7 @@
import { useQueryClient } from 'react-query';
import { Trash2, X } from '@signozhq/icons';
import { Button } from '@signozhq/ui/button';
import AuthZTooltip from 'lib/authz/components/AuthZTooltip/AuthZTooltip';
import AuthZButton from 'lib/authz/components/AuthZButton/AuthZButton';
import {
buildAPIKeyDeletePermission,
buildSADetachPermission,
@@ -45,23 +45,20 @@ export function RevokeKeyFooter({
<X size={12} />
Cancel
</Button>
<AuthZTooltip
<AuthZButton
checks={[
buildAPIKeyDeletePermission(keyId ?? ''),
buildSADetachPermission(accountId ?? ''),
]}
enabled={!!accountId && !!keyId}
authZEnabled={!!accountId && !!keyId}
variant="solid"
color="destructive"
loading={isRevoking}
onClick={onConfirm}
>
<Button
variant="solid"
color="destructive"
loading={isRevoking}
onClick={onConfirm}
>
<Trash2 size={12} />
Revoke Key
</Button>
</AuthZTooltip>
<Trash2 size={12} />
Revoke Key
</AuthZButton>
</>
);
}
@@ -111,7 +108,7 @@ function RevokeKeyModal(): JSX.Element {
}
function handleCancel(): void {
setRevokeKeyId(null);
void setRevokeKeyId(null);
}
return (

View File

@@ -1,11 +1,17 @@
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import React, {
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from 'react';
import { useQueryClient } from 'react-query';
import { Key, LayoutGrid, Plus, Trash2, X } from '@signozhq/icons';
import { Button } from '@signozhq/ui/button';
import { DrawerWrapper } from '@signozhq/ui/drawer';
import { toast } from '@signozhq/ui/sonner';
import { ToggleGroupSimple } from '@signozhq/ui/toggle-group';
import { Pagination, Skeleton } from 'antd';
import { Skeleton } from 'antd';
import { convertToApiError } from 'api/ErrorResponseHandlerForGeneratedAPIs';
import {
getListServiceAccountsQueryKey,
@@ -16,7 +22,6 @@ import {
import type { RenderErrorResponseDTO } from 'api/generated/services/sigNoz.schemas';
import { AxiosError } from 'axios';
import ErrorInPlace from 'components/ErrorInPlace/ErrorInPlace';
import PermissionDeniedCallout from 'lib/authz/components/PermissionDeniedCallout/PermissionDeniedCallout';
import { useRoles } from 'components/RolesSelect';
import { SA_QUERY_PARAMS } from 'container/ServiceAccountsSettings/constants';
import {
@@ -28,15 +33,13 @@ import {
RoleUpdateFailure,
useServiceAccountRoleManager,
} from 'hooks/serviceAccount/useServiceAccountRoleManager';
import AuthZButton from 'lib/authz/components/AuthZButton/AuthZButton';
import {
APIKeyCreatePermission,
APIKeyListPermission,
buildSAAttachPermission,
buildSADeletePermission,
buildSAReadPermission,
buildSAUpdatePermission,
} from 'lib/authz/hooks/useAuthZ/permissions/service-account.permissions';
import { useAuthZ } from 'lib/authz/hooks/useAuthZ/useAuthZ';
import {
parseAsBoolean,
parseAsInteger,
@@ -47,7 +50,6 @@ import {
import APIError from 'types/api/error';
import { toAPIError } from 'utils/errorUtils';
import AuthZTooltip from 'lib/authz/components/AuthZTooltip/AuthZTooltip';
import AddKeyModal from './AddKeyModal';
import DeleteAccountModal from './DeleteAccountModal';
import KeysTab from './KeysTab';
@@ -70,14 +72,12 @@ function toSaveApiError(err: unknown): APIError {
);
}
// eslint-disable-next-line sonarjs/cognitive-complexity
function ServiceAccountDrawer({
onSuccess,
}: ServiceAccountDrawerProps): JSX.Element {
const [selectedAccountId, setSelectedAccountId] = useQueryState(
SA_QUERY_PARAMS.ACCOUNT,
);
const open = !!selectedAccountId;
const [activeTab, setActiveTab] = useQueryState(
SA_QUERY_PARAMS.TAB,
parseAsStringEnum<ServiceAccountDrawerTab>(
@@ -100,28 +100,14 @@ function ServiceAccountDrawer({
SA_QUERY_PARAMS.DELETE_SA,
parseAsBoolean.withDefault(false),
);
const [localName, setLocalName] = useState('');
const [localRoles, setLocalRoles] = useState<string[]>([]);
const [isSaving, setIsSaving] = useState(false);
const [saveErrors, setSaveErrors] = useState<SaveError[]>([]);
const queryClient = useQueryClient();
const { permissions: drawerPermissions, isLoading: isAuthZLoading } = useAuthZ(
selectedAccountId
? [
buildSAReadPermission(selectedAccountId),
buildSAUpdatePermission(selectedAccountId),
buildSADeletePermission(selectedAccountId),
APIKeyListPermission,
]
: [],
{ enabled: !!selectedAccountId },
);
const canRead =
drawerPermissions?.[buildSAReadPermission(selectedAccountId ?? '')]
?.isGranted ?? false;
const open = !!selectedAccountId;
const {
data: accountData,
@@ -131,7 +117,7 @@ function ServiceAccountDrawer({
refetch: refetchAccount,
} = useGetServiceAccount(
{ id: selectedAccountId ?? '' },
{ query: { enabled: canRead && !!selectedAccountId } },
{ query: { enabled: !!selectedAccountId } },
);
const account = useMemo(
@@ -145,7 +131,7 @@ function ServiceAccountDrawer({
isLoading: isRolesLoading,
applyDiff,
} = useServiceAccountRoleManager(selectedAccountId ?? '', {
enabled: canRead && !!selectedAccountId,
enabled: !!selectedAccountId,
});
const roleSessionRef = useRef<string | null>(null);
@@ -194,16 +180,9 @@ function ServiceAccountDrawer({
refetch: refetchRoles,
} = useRoles();
const canListKeys =
drawerPermissions?.[APIKeyListPermission]?.isGranted ?? false;
const canUpdate =
drawerPermissions?.[buildSAUpdatePermission(selectedAccountId ?? '')]
?.isGranted ?? true;
const { data: keysData, isLoading: keysLoading } = useListServiceAccountKeys(
{ id: selectedAccountId ?? '' },
{ query: { enabled: !!selectedAccountId && canListKeys } },
{ query: { enabled: !!selectedAccountId } },
);
const keys = keysData?.data ?? [];
@@ -217,7 +196,6 @@ function ServiceAccountDrawer({
}
}, [keysLoading, keys.length, keysPage, setKeysPage]);
// the retry for this mutation is safe due to the api being idempotent on backend
const { mutateAsync: updateMutateAsync } = useUpdateServiceAccount();
const retryNameUpdate = useCallback(async (): Promise<void> => {
@@ -375,23 +353,71 @@ function ServiceAccountDrawer({
]);
const handleClose = useCallback((): void => {
void setIsDeleteOpen(null);
void setIsAddKeyOpen(null);
void setSelectedAccountId(null);
setSaveErrors([]);
void setActiveTab(null);
void setKeysPage(null);
void setEditKeyId(null);
setSaveErrors([]);
void setIsAddKeyOpen(null);
void setIsDeleteOpen(null);
void setSelectedAccountId(null);
}, [
setSelectedAccountId,
setActiveTab,
setKeysPage,
setEditKeyId,
setIsAddKeyOpen,
setIsDeleteOpen,
setSelectedAccountId,
]);
const drawerContent = (
const footer = useMemo(
() =>
activeTab === ServiceAccountDrawerTab.Overview && !isDeleted && open ? (
<div className="sa-drawer__footer">
<AuthZButton
checks={[buildSADeletePermission(selectedAccountId ?? '')]}
authZEnabled={!!selectedAccountId}
variant="link"
color="destructive"
onClick={(): void => {
void setIsDeleteOpen(true);
}}
>
<Trash2 size={12} />
Delete Service Account
</AuthZButton>
<div className="sa-drawer__footer-right">
<Button variant="outlined" color="secondary" onClick={handleClose}>
<X size={14} />
Cancel
</Button>
<AuthZButton
checks={[buildSAUpdatePermission(selectedAccountId ?? '')]}
authZEnabled={!!selectedAccountId}
variant="solid"
color="primary"
loading={isSaving}
disabled={!isDirty}
onClick={handleSave}
>
Save Changes
</AuthZButton>
</div>
</div>
) : null,
[
activeTab,
isDeleted,
open,
selectedAccountId,
isSaving,
isDirty,
handleClose,
handleSave,
setIsDeleteOpen,
],
);
const body = (
<div className="sa-drawer__layout">
<div className="sa-drawer__tabs">
<ToggleGroupSimple
@@ -433,26 +459,23 @@ function ServiceAccountDrawer({
]}
/>
{activeTab === ServiceAccountDrawerTab.Keys && (
<AuthZTooltip
<AuthZButton
checks={[
APIKeyCreatePermission,
buildSAAttachPermission(selectedAccountId ?? ''),
]}
enabled={!isDeleted && !!selectedAccountId}
authZEnabled={!isDeleted && !!selectedAccountId}
variant="outlined"
size="sm"
color="secondary"
disabled={isDeleted}
onClick={(): void => {
void setIsAddKeyOpen(true);
}}
>
<Button
variant="outlined"
size="sm"
color="secondary"
disabled={isDeleted}
onClick={(): void => {
void setIsAddKeyOpen(true);
}}
>
<Plus size={12} />
Add Key
</Button>
</AuthZTooltip>
<Plus size={12} />
Add Key
</AuthZButton>
)}
</div>
@@ -461,9 +484,7 @@ function ServiceAccountDrawer({
activeTab === ServiceAccountDrawerTab.Keys ? ' sa-drawer__body--keys' : ''
}`}
>
{(isAuthZLoading || isAccountLoading) && (
<Skeleton active paragraph={{ rows: 6 }} />
)}
{isAccountLoading && <Skeleton active paragraph={{ rows: 6 }} />}
{isAccountError && (
<ErrorInPlace
error={toAPIError(
@@ -472,141 +493,73 @@ function ServiceAccountDrawer({
)}
/>
)}
{!isAuthZLoading &&
!isAccountLoading &&
!isAccountError &&
selectedAccountId && (
<>
{activeTab === ServiceAccountDrawerTab.Overview &&
(canRead && account ? (
<OverviewTab
account={account}
localName={localName}
onNameChange={handleNameChange}
localRoles={localRoles}
onRolesChange={(roles): void => {
setLocalRoles(roles);
clearRoleErrors();
}}
isDisabled={isDeleted}
canUpdate={canUpdate}
availableRoles={availableRoles}
rolesLoading={rolesLoading}
rolesError={rolesError}
rolesErrorObj={rolesErrorObj}
onRefetchRoles={refetchRoles}
saveErrors={saveErrors}
/>
) : (
<PermissionDeniedCallout permissionName="serviceaccount:read" />
))}
{activeTab === ServiceAccountDrawerTab.Keys &&
(canListKeys ? (
<KeysTab
keys={keys}
isLoading={keysLoading}
isDisabled={isDeleted}
canUpdate={canUpdate}
accountId={selectedAccountId}
currentPage={keysPage}
pageSize={PAGE_SIZE}
/>
) : (
<PermissionDeniedCallout permissionName="factor-api-key:list" />
))}
</>
)}
{!isAccountLoading && !isAccountError && (
<>
{activeTab === ServiceAccountDrawerTab.Overview &&
(account ? (
<OverviewTab
account={account}
localName={localName}
onNameChange={handleNameChange}
localRoles={localRoles}
onRolesChange={(roles): void => {
setLocalRoles(roles);
clearRoleErrors();
}}
isDisabled={isDeleted}
availableRoles={availableRoles}
rolesLoading={rolesLoading}
rolesError={rolesError}
rolesErrorObj={rolesErrorObj}
onRefetchRoles={refetchRoles}
saveErrors={saveErrors}
/>
) : (
<Skeleton active />
))}
{activeTab === ServiceAccountDrawerTab.Keys && (
<KeysTab
keys={keys}
isLoading={keysLoading}
isDisabled={isDeleted}
accountId={selectedAccountId ?? ''}
currentPage={keysPage}
pageSize={PAGE_SIZE}
onPageChange={(page): void => {
void setKeysPage(page);
}}
/>
)}
</>
)}
</div>
</div>
);
const footer = (
<div className="sa-drawer__footer">
{activeTab === ServiceAccountDrawerTab.Keys ? (
<Pagination
current={keysPage}
pageSize={PAGE_SIZE}
total={keys.length}
showTotal={(total: number, range: number[]): JSX.Element => (
<>
<span className="sa-drawer__pagination-range">
{range[0]} &#8212; {range[1]}
</span>
<span className="sa-drawer__pagination-total"> of {total}</span>
</>
)}
showSizeChanger={false}
hideOnSinglePage
onChange={(page): void => {
void setKeysPage(page);
}}
className="sa-drawer__keys-pagination"
/>
) : (
return (
<DrawerWrapper
open={open}
onOpenChange={(isOpen): void => {
if (!isOpen) {
handleClose();
}
}}
direction="right"
showCloseButton
showOverlay={false}
title="Service Account Details"
className="sa-drawer"
width="wide"
footer={footer}
>
{open && (
<>
{!isDeleted && (
<AuthZTooltip
checks={[buildSADeletePermission(selectedAccountId ?? '')]}
enabled={!!selectedAccountId}
>
<Button
variant="link"
color="destructive"
onClick={(): void => {
void setIsDeleteOpen(true);
}}
>
<Trash2 size={12} />
Delete Service Account
</Button>
</AuthZTooltip>
)}
{!isDeleted && (
<div className="sa-drawer__footer-right">
<Button variant="outlined" color="secondary" onClick={handleClose}>
<X size={14} />
Cancel
</Button>
<Button
variant="solid"
color="primary"
loading={isSaving}
disabled={!isDirty}
onClick={handleSave}
>
Save Changes
</Button>
</div>
)}
{body}
<DeleteAccountModal />
<AddKeyModal />
</>
)}
</div>
);
return (
<>
<DrawerWrapper
open={open}
onOpenChange={(isOpen): void => {
if (!isOpen) {
handleClose();
}
}}
direction="right"
showCloseButton
showOverlay={false}
title="Service Account Details"
className="sa-drawer"
width="wide"
footer={footer}
>
{drawerContent}
</DrawerWrapper>
<DeleteAccountModal />
<AddKeyModal />
</>
</DrawerWrapper>
);
}

View File

@@ -1,4 +1,5 @@
import { toast } from '@signozhq/ui/sonner';
import { setupAuthzAdmin } from 'lib/authz/utils/authz-test-utils';
import { rest, server } from 'mocks-server/server';
import { NuqsTestingAdapter } from 'nuqs/adapters/testing';
import {
@@ -59,6 +60,7 @@ describe('AddKeyModal', () => {
rest.post(SA_KEYS_ENDPOINT, (_, res, ctx) =>
res(ctx.status(201), ctx.json(createdKeyResponse)),
),
setupAuthzAdmin(),
);
});

View File

@@ -1,5 +1,6 @@
import { toast } from '@signozhq/ui/sonner';
import type { ServiceaccounttypesGettableFactorAPIKeyDTO } from 'api/generated/services/sigNoz.schemas';
import { setupAuthzAdmin } from 'lib/authz/utils/authz-test-utils';
import { rest, server } from 'mocks-server/server';
import { NuqsTestingAdapter } from 'nuqs/adapters/testing';
import { render, screen, userEvent, waitFor } from 'tests/test-utils';
@@ -61,6 +62,7 @@ describe('EditKeyModal (URL-controlled)', () => {
rest.delete(SA_KEY_ENDPOINT, (_, res, ctx) =>
res(ctx.status(200), ctx.json({ status: 'success', data: {} })),
),
setupAuthzAdmin(),
);
});

View File

@@ -1,5 +1,6 @@
import { toast } from '@signozhq/ui/sonner';
import { ServiceaccounttypesGettableFactorAPIKeyDTO } from 'api/generated/services/sigNoz.schemas';
import { setupAuthzAdmin } from 'lib/authz/utils/authz-test-utils';
import { rest, server } from 'mocks-server/server';
import { NuqsTestingAdapter } from 'nuqs/adapters/testing';
import { render, screen, userEvent, waitFor } from 'tests/test-utils';
@@ -35,7 +36,7 @@ const keys: ServiceaccounttypesGettableFactorAPIKeyDTO[] = [
{
id: 'key-2',
name: 'Staging Key',
expiresAt: 1924905600, // 2030-12-31
expiresAt: 1924948800, // 2030-12-31 12:00 UTC (noon to avoid timezone issues)
lastObservedAt: '2026-03-10T10:00:00Z',
serviceAccountId: 'sa-1',
},
@@ -47,6 +48,7 @@ const defaultProps = {
isDisabled: false,
currentPage: 1,
pageSize: 10,
onPageChange: jest.fn(),
};
function renderKeysTab(
@@ -67,6 +69,7 @@ describe('KeysTab', () => {
rest.delete(SA_KEY_ENDPOINT, (_, res, ctx) =>
res(ctx.status(200), ctx.json({ status: 'success', data: {} })),
),
setupAuthzAdmin(),
);
});
@@ -74,9 +77,12 @@ describe('KeysTab', () => {
server.resetHandlers();
});
it('renders loading state', () => {
it('renders loading state', async () => {
renderKeysTab({ isLoading: true });
expect(document.querySelector('.ant-skeleton')).toBeInTheDocument();
// Wait for authz to complete, then check for skeleton
await waitFor(() => {
expect(document.querySelector('.ant-skeleton')).toBeInTheDocument();
});
});
it('renders empty state when no keys and clicking add sets add-key param', async () => {
@@ -91,9 +97,9 @@ describe('KeysTab', () => {
</NuqsTestingAdapter>,
);
expect(
screen.getByText(/No keys. Start by creating one./i),
).toBeInTheDocument();
await expect(
screen.findByText(/No keys. Start by creating one./i),
).resolves.toBeInTheDocument();
const addBtn = screen.getByRole('button', { name: /\+ Add your first key/i });
await user.click(addBtn);
expect(onUrlUpdate).toHaveBeenCalledWith(
@@ -103,10 +109,12 @@ describe('KeysTab', () => {
);
});
it('renders table with keys', () => {
it('renders table with keys', async () => {
renderKeysTab();
expect(screen.getByText('Production Key')).toBeInTheDocument();
await expect(
screen.findByText('Production Key'),
).resolves.toBeInTheDocument();
expect(screen.getByText('Staging Key')).toBeInTheDocument();
expect(screen.getByText('Never')).toBeInTheDocument();
expect(screen.getByText('Dec 31, 2030')).toBeInTheDocument();
@@ -122,7 +130,7 @@ describe('KeysTab', () => {
</NuqsTestingAdapter>,
);
const row = screen.getByText('Production Key').closest('tr');
const row = (await screen.findByText('Production Key')).closest('tr');
if (!row) {
throw new Error('Row not found');
}
@@ -146,6 +154,8 @@ describe('KeysTab', () => {
</NuqsTestingAdapter>,
);
// Wait for authz to complete and table to render
await screen.findByText('Production Key');
const revokeBtns = screen
.getAllByRole('button')
.filter((btn) => btn.className.includes('keys-tab__revoke-btn'));
@@ -163,7 +173,8 @@ describe('KeysTab', () => {
renderKeysTab();
// Seed the keys cache so RevokeKeyModal can read the key name
// Wait for authz to complete and table to render
await screen.findByText('Production Key');
const revokeBtns = screen
.getAllByRole('button')
.filter((btn) => btn.className.includes('keys-tab__revoke-btn'));
@@ -177,9 +188,11 @@ describe('KeysTab', () => {
});
});
it('disables actions when isDisabled is true', () => {
it('disables actions when isDisabled is true', async () => {
renderKeysTab({ isDisabled: true });
// Wait for authz to complete and table to render
await screen.findByText('Production Key');
const revokeBtns = screen
.getAllByRole('button')
.filter((btn) => btn.className.includes('keys-tab__revoke-btn'));

View File

@@ -1,4 +1,3 @@
import type { ReactNode } from 'react';
import { listRolesSuccessResponse } from 'mocks-server/__mockdata__/roles';
import { rest, server } from 'mocks-server/server';
import { NuqsTestingAdapter } from 'nuqs/adapters/testing';
@@ -32,30 +31,6 @@ const activeAccountResponse = {
updatedAt: '2026-01-02T00:00:00Z',
};
jest.mock('@signozhq/ui/drawer', () => ({
...jest.requireActual('@signozhq/ui/drawer'),
DrawerWrapper: ({
children,
footer,
open,
}: {
children?: ReactNode;
footer?: ReactNode;
open: boolean;
}): JSX.Element | null =>
open ? (
<div>
{children}
{footer}
</div>
) : null,
}));
jest.mock('@signozhq/ui/sonner', () => ({
...jest.requireActual('@signozhq/ui/sonner'),
toast: { success: jest.fn(), error: jest.fn() },
}));
function renderDrawer(
searchParams: Record<string, string> = { account: 'sa-1' },
): ReturnType<typeof render> {
@@ -118,7 +93,7 @@ describe('ServiceAccountDrawer — permissions', () => {
renderDrawer();
await waitFor(() => {
expect(screen.getByText(/serviceaccount:read/)).toBeInTheDocument();
expect(screen.getByText(/read:serviceaccount/)).toBeInTheDocument();
});
});
@@ -140,7 +115,7 @@ describe('ServiceAccountDrawer — permissions', () => {
fireEvent.click(screen.getByRole('radio', { name: /keys/i }));
await waitFor(() => {
expect(screen.getByText(/factor-api-key:list/)).toBeInTheDocument();
expect(screen.getByText(/list:factor-api-key/)).toBeInTheDocument();
});
});

View File

@@ -1,4 +1,3 @@
import type { ReactNode } from 'react';
import { listRolesSuccessResponse } from 'mocks-server/__mockdata__/roles';
import { rest, server } from 'mocks-server/server';
import { NuqsTestingAdapter } from 'nuqs/adapters/testing';
@@ -7,30 +6,6 @@ import { setupAuthzAdmin } from 'lib/authz/utils/authz-test-utils';
import ServiceAccountDrawer from '../ServiceAccountDrawer';
jest.mock('@signozhq/ui/drawer', () => ({
...jest.requireActual('@signozhq/ui/drawer'),
DrawerWrapper: ({
children,
footer,
open,
}: {
children?: ReactNode;
footer?: ReactNode;
open: boolean;
}): JSX.Element | null =>
open ? (
<div>
{children}
{footer}
</div>
) : null,
}));
jest.mock('@signozhq/ui/sonner', () => ({
...jest.requireActual('@signozhq/ui/sonner'),
toast: { success: jest.fn(), error: jest.fn() },
}));
const ROLES_ENDPOINT = '*/api/v1/roles';
const SA_KEYS_ENDPOINT = '*/api/v1/service_accounts/:id/keys';
const SA_ENDPOINT = '*/api/v1/service_accounts/sa-1';

View File

@@ -2,10 +2,22 @@
position: sticky;
top: 0;
z-index: 2;
padding: var(--tanstack-cell-padding-top, 0.3rem)
var(--tanstack-cell-padding-right, 0.3rem)
var(--tanstack-cell-padding-bottom, 0.3rem)
var(--tanstack-cell-padding-left, 0.3rem);
padding: var(
--tanstack-cell-header-padding-top-override,
var(--tanstack-cell-padding-top, 0.3rem)
)
var(
--tanstack-cell-header-padding-right-override,
var(--tanstack-cell-padding-right, 0.3rem)
)
var(
--tanstack-cell-header-padding-bottom-override,
var(--tanstack-cell-padding-bottom, 0.3rem)
)
var(
--tanstack-cell-header-padding-left-override,
var(--tanstack-cell-padding-left, 0.3rem)
);
transform: translate3d(
var(--tanstack-header-translate-x, 0px),
var(--tanstack-header-translate-y, 0px),
@@ -239,6 +251,7 @@
text-overflow: ellipsis;
white-space: nowrap;
min-width: 0;
width: var(--tanstack-table-header-label-width);
}
.tanstackSortIndicator {

View File

@@ -43,7 +43,7 @@
&__title {
color: var(--l1-foreground);
font-size: 14px;
font-size: var(--periscope-font-size-base);
font-weight: 500;
line-height: 20px;
letter-spacing: -0.07px;
@@ -51,14 +51,14 @@
&__subtitle {
color: var(--l2-foreground);
font-size: 14px;
font-size: var(--periscope-font-size-base);
font-weight: 400;
line-height: 20px;
letter-spacing: -0.07px;
}
&__description {
font-size: 14px;
font-size: var(--periscope-font-size-base);
color: var(--l2-foreground);
line-height: 20px;
}
@@ -67,7 +67,7 @@
margin: 0;
margin-top: 8px;
color: var(--l2-foreground);
font-size: 14px;
font-size: var(--periscope-font-size-base);
font-weight: 400;
line-height: 20px;
letter-spacing: -0.07px;
@@ -106,7 +106,7 @@
border: 1px dashed var(--l1-border);
background: transparent;
color: var(--l2-foreground);
font-size: 14px;
font-size: var(--periscope-font-size-base);
font-weight: 400;
line-height: 18px;
letter-spacing: -0.07px;
@@ -120,15 +120,15 @@
gap: 6px;
}
// Stack the message and the resources card; card matches the content
// width above it and is capped so it doesn't sprawl in a wide panel.
&__row {
display: flex;
flex-direction: row;
flex-wrap: wrap;
align-items: flex-end;
max-width: 825px;
gap: 25px;
justify-content: center;
margin-left: 21px;
flex-direction: column;
align-items: stretch;
gap: 16px;
width: fit-content;
max-width: 600px;
}
&__content {
@@ -142,7 +142,7 @@
background: var(--l2-background);
border: 1px solid var(--l1-border);
border-radius: 4px;
width: 332px;
width: 100%; // match the content width above
}
&__resources-title {
@@ -155,7 +155,6 @@
text-transform: uppercase;
padding: 16px 16px 12px;
border-bottom: 1px solid var(--l1-border);
height: 46px;
}
&__resources-links {

View File

@@ -1,33 +1,37 @@
import { useCallback, useEffect, useRef, useState } from 'react';
import { Button, Tooltip } from 'antd';
import { Typography } from '@signozhq/ui/typography';
import logEvent from 'api/common/logEvent';
import { listHosts } from 'api/generated/services/inframonitoring';
import {
InframonitoringtypesHostRecordDTO,
InframonitoringtypesHostStatusDTO,
InframonitoringtypesResponseTypeDTO,
Querybuildertypesv5OrderDirectionDTO,
} from 'api/generated/services/sigNoz.schemas';
import QuickFilters from 'components/QuickFilters/QuickFilters';
import { QuickFiltersSource } from 'components/QuickFilters/types';
import { InfraMonitoringEvents } from 'constants/events';
import { FeatureKeys } from 'constants/features';
import K8sBaseDetails from 'container/InfraMonitoringK8sV2/Base/K8sBaseDetails';
import { initialQueriesMap } from 'constants/queryBuilder';
import K8sBaseDetails, {
K8sDetailsFilters,
} from 'container/InfraMonitoringK8sV2/Base/K8sBaseDetails';
import { K8sBaseList } from 'container/InfraMonitoringK8sV2/Base/K8sBaseList';
import StatusFilter from 'container/InfraMonitoringHostsV2/StatusFilter';
import { K8sBaseFilters } from 'container/InfraMonitoringK8sV2/Base/types';
import { InfraMonitoringEntity } from 'container/InfraMonitoringK8sV2/constants';
import {
useInfraMonitoringFiltersK8s,
useInfraMonitoringPageListing,
} from 'container/InfraMonitoringK8sV2/hooks';
import { useGetCompositeQueryParam } from 'hooks/queryBuilder/useGetCompositeQueryParam';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useQueryOperations } from 'hooks/queryBuilder/useQueryBuilderOperations';
import { useAppContext } from 'providers/App/App';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
import { DataSource } from 'types/common/queryBuilder';
import {
fetchHostEntityData,
fetchHostListData,
getHostMetricsQueryPayload,
hostDetailsMetadataConfig,
hostGetEntityName,
hostGetSelectedItemFilters,
hostInitialEventsFilter,
hostInitialLogTracesFilter,
hostGetSelectedItemExpression,
hostInitialEventsExpression,
hostInitialLogTracesExpression,
hostWidgetInfo,
} from './constants';
import {
@@ -42,85 +46,135 @@ import { ArrowUpToLine, Filter } from '@signozhq/icons';
function Hosts(): JSX.Element {
const [showFilters, setShowFilters] = useState(true);
const [, setCurrentPage] = useInfraMonitoringPageListing();
const [urlFilters, setUrlFilters] = useInfraMonitoringFiltersK8s();
const compositeQuery = useGetCompositeQueryParam();
const { redirectWithQueryBuilderData } = useQueryBuilder();
const isInitialized = useRef(false);
useEffect(() => {
if (isInitialized.current) {
return;
}
isInitialized.current = true;
if (!compositeQuery) {
const defaultQuery = initialQueriesMap[DataSource.METRICS];
redirectWithQueryBuilderData({
...defaultQuery,
builder: {
...defaultQuery.builder,
queryData: defaultQuery.builder.queryData.map((query) => ({
...query,
filter: { expression: '' },
filters: { items: [], op: 'AND' as const },
})),
},
});
}
}, [compositeQuery, redirectWithQueryBuilderData]);
const { featureFlags } = useAppContext();
const dotMetricsEnabled =
featureFlags?.find((flag) => flag.name === FeatureKeys.DOT_METRICS_ENABLED)
?.active || false;
const { currentQuery } = useQueryBuilder();
const { handleChangeQueryData } = useQueryOperations({
index: 0,
query: currentQuery.builder.queryData[0],
entityVersion: '',
});
// Track previous urlFilters to only sync when the value actually changes
// (not when handleChangeQueryData changes due to query updates)
const prevUrlFiltersRef = useRef<string | null>(null);
useEffect(() => {
const currentFiltersJson = urlFilters ? JSON.stringify(urlFilters) : null;
// Only sync if urlFilters value has actually changed
if (prevUrlFiltersRef.current !== currentFiltersJson) {
prevUrlFiltersRef.current = currentFiltersJson;
// Sync filters to query builder, using empty filter when urlFilters is null
handleChangeQueryData('filters', urlFilters || { items: [], op: 'and' });
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [urlFilters]); // handleChangeQueryData intentionally omitted - we call the current version but don't re-run when it changes
const handleFilterVisibilityChange = (): void => {
setShowFilters(!showFilters);
};
const handleQuickFiltersChange = (query: Query): void => {
const filters = query.builder.queryData[0].filters;
// Nuqs batches these calls into a single URL update
// The useEffect will sync filters to query builder
setUrlFilters(filters || null);
setCurrentPage(1);
logEvent(InfraMonitoringEvents.FilterApplied, {
entity: InfraMonitoringEvents.HostEntity,
page: InfraMonitoringEvents.ListPage,
});
};
const fetchListData = useCallback(
async (filters: K8sBaseFilters, signal?: AbortSignal) => {
filters.orderBy ||= {
columnName: 'cpu',
order: 'desc',
};
try {
const response = await listHosts(
{
filter: {
expression: filters.filter.expression,
filterByStatus: filters.filter.filterByStatus
? (filters.filter.filterByStatus as InframonitoringtypesHostStatusDTO)
: undefined,
},
groupBy: filters.groupBy?.map((g) => ({ name: g.name })),
offset: filters.offset,
limit: filters.limit ?? 10,
start: filters.start,
end: filters.end,
orderBy: filters.orderBy
? {
key: { name: filters.orderBy.key.name },
direction:
filters.orderBy.direction === 'asc'
? Querybuildertypesv5OrderDirectionDTO.asc
: Querybuildertypesv5OrderDirectionDTO.desc,
}
: undefined,
},
signal,
);
return fetchHostListData(filters, signal);
const data = response.data;
return {
type:
data.type === InframonitoringtypesResponseTypeDTO.grouped_list
? ('grouped_list' as const)
: ('list' as const),
records: data.records,
total: data.total,
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,
};
}
},
[],
);
const fetchEntityData = useCallback(
async (
filters: Parameters<typeof fetchHostEntityData>[0],
filters: K8sDetailsFilters,
signal?: AbortSignal,
) => fetchHostEntityData(filters, signal),
): Promise<{
data: InframonitoringtypesHostRecordDTO | null;
error?: string | null;
}> => {
try {
const response = await listHosts(
{
filter: { expression: filters.filter.expression },
start: filters.start,
end: filters.end,
limit: 1,
offset: 0,
},
signal,
);
return {
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,
};
}
},
[],
);
const getSelectedItemFilters = useCallback(
(selectedItem: string) =>
hostGetSelectedItemFilters(selectedItem, dotMetricsEnabled),
const getInitialLogTracesExpression = useCallback(
(host: InframonitoringtypesHostRecordDTO) =>
hostInitialLogTracesExpression(host, dotMetricsEnabled),
[dotMetricsEnabled],
);
const getInitialLogTracesFilters = useCallback(
(host: import('api/infraMonitoring/getHostLists').HostData) =>
hostInitialLogTracesFilter(host, dotMetricsEnabled),
[dotMetricsEnabled],
);
const controlListPrefix = !showFilters ? (
<div className={styles.quickFiltersToggleContainer}>
<Button
@@ -154,7 +208,6 @@ function Hosts(): JSX.Element {
source={QuickFiltersSource.INFRA_MONITORING}
config={getHostsQuickFiltersConfig(dotMetricsEnabled)}
handleFilterVisibilityChange={handleFilterVisibilityChange}
onFilterChange={handleQuickFiltersChange}
/>
</div>
)}
@@ -163,8 +216,9 @@ function Hosts(): JSX.Element {
showFilters ? ` ${styles.listContainerFiltersVisible}` : ''
}`}
>
<K8sBaseList
<K8sBaseList<InframonitoringtypesHostRecordDTO>
controlListPrefix={controlListPrefix}
leftFilters={<StatusFilter />}
entity={InfraMonitoringEntity.HOSTS}
tableColumns={hostColumnsConfig}
fetchListData={fetchListData}
@@ -178,11 +232,11 @@ function Hosts(): JSX.Element {
<K8sBaseDetails
category={InfraMonitoringEntity.HOSTS}
eventCategory={InfraMonitoringEvents.HostEntity}
getSelectedItemFilters={getSelectedItemFilters}
getSelectedItemExpression={hostGetSelectedItemExpression}
fetchEntityData={fetchEntityData}
getEntityName={hostGetEntityName}
getInitialLogTracesFilters={getInitialLogTracesFilters}
getInitialEventsFilters={hostInitialEventsFilter}
getInitialLogTracesExpression={getInitialLogTracesExpression}
getInitialEventsExpression={hostInitialEventsExpression}
metadataConfig={hostDetailsMetadataConfig}
entityWidgetInfo={hostWidgetInfo}
getEntityQueryPayload={getHostMetricsQueryPayload}

View File

@@ -26,8 +26,8 @@
}
&::-webkit-scrollbar {
width: 0.1rem;
height: 0.1rem;
width: 8px;
height: 8px;
}
&::-webkit-scrollbar-track {
@@ -35,11 +35,12 @@
}
&::-webkit-scrollbar-thumb {
background: var(--accent-primary);
background: color-mix(in srgb, var(--l1-foreground) 20%, transparent);
border-radius: 9999px;
}
&::-webkit-scrollbar-thumb:hover {
background: colox-mix(var(--accent-primary), var(--bg-vanilla-100), 20%);
background: color-mix(in srgb, var(--l1-foreground) 30%, transparent);
}
}
@@ -98,7 +99,7 @@
}
&::-webkit-scrollbar-thumb:hover {
background: colox-mix(var(--accent-primary), var(--bg-vanilla-100), 20%);
background: var(--accent-primary);
}
}
}
@@ -113,32 +114,17 @@
.listContainer {
flex: 1;
min-width: 0;
max-width: 100%;
display: flex;
flex-direction: column;
align-items: stretch;
min-width: 595px;
> * {
min-width: 0;
max-width: 100%;
box-sizing: border-box;
}
> :global(.ant-table-wrapper) {
width: 100%;
max-width: 100%;
:global(.ant-spin-container) {
display: flex !important;
flex-direction: column;
}
:global(.ant-table),
:global(.ant-table-container) {
max-width: 100%;
}
}
}
.listContainerFiltersVisible {
@@ -146,7 +132,7 @@
}
.quickFiltersToggleContainer {
padding: 0 8px;
grid-area: quickFilters;
}
.infraMonitoringTags {

View File

@@ -0,0 +1,56 @@
.statusFilterContainer {
display: flex;
align-items: center;
--toggle-group-item-size: 100%;
--toggle-group-radius: 0px 2px 2px 0px;
--toggle-group-item-font-size: var(--periscope-font-size-base);
}
.statusLabel {
min-width: max-content;
font-size: var(--periscope-font-size-base);
font-weight: var(--periscope-font-weight-regular);
line-height: 18px;
border-radius: 2px 0px 0px 2px;
border: 1px solid var(--l2-border);
border-right: none;
display: flex;
height: 32px;
padding: var(--spacing-3) var(--spacing-3) var(--spacing-3) var(--spacing-4);
justify-content: center;
align-items: center;
gap: var(--spacing-2);
}
.statusToggleGroup {
height: 32px;
}
.statusToggleItem {
flex: unset;
}
.statusDot {
width: 8px;
height: 8px;
min-height: 8px;
min-width: 8px;
border-radius: 50%;
display: inline-block;
}
.allDot {
border: 1px solid var(--text-muted);
background-color: var(--bg-slate-100);
}
.activeDot {
background-color: var(--bg-forest-500);
}
.inactiveDot {
background-color: var(--bg-amber-500);
}

View File

@@ -0,0 +1,152 @@
import { QueryClient, QueryClientProvider } from 'react-query';
// eslint-disable-next-line no-restricted-imports
import { Provider } from 'react-redux';
import { MemoryRouter } from 'react-router-dom';
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
import {
NuqsTestingAdapter,
OnUrlUpdateFunction,
UrlUpdateEvent,
} from 'nuqs/adapters/testing';
import { AppContext } from 'providers/App/App';
import TimezoneProvider from 'providers/Timezone';
import store from 'store';
import { getAppContextMock } from 'tests/test-utils';
import StatusFilter from './StatusFilter';
const queryClient = new QueryClient({
defaultOptions: {
queries: { retry: false },
mutations: { retry: false },
},
});
function renderStatusFilter({
searchParams = {},
onUrlUpdate,
}: {
searchParams?: Record<string, string>;
onUrlUpdate?: OnUrlUpdateFunction;
}): ReturnType<typeof render> {
return render(
<MemoryRouter>
<TimezoneProvider>
<QueryClientProvider client={queryClient}>
<Provider store={store}>
<AppContext.Provider value={getAppContextMock('ADMIN')}>
<NuqsTestingAdapter
searchParams={searchParams}
onUrlUpdate={onUrlUpdate}
>
<StatusFilter />
</NuqsTestingAdapter>
</AppContext.Provider>
</Provider>
</QueryClientProvider>
</TimezoneProvider>
</MemoryRouter>,
);
}
describe('StatusFilter', () => {
beforeEach(() => {
queryClient.clear();
});
it('renders all status options', () => {
renderStatusFilter({});
expect(screen.getByText('Status')).toBeInTheDocument();
expect(screen.getByRole('radio', { name: 'All' })).toBeInTheDocument();
expect(screen.getByRole('radio', { name: 'Active' })).toBeInTheDocument();
expect(screen.getByRole('radio', { name: 'Inactive' })).toBeInTheDocument();
});
it('selects "All" by default when no URL param', () => {
renderStatusFilter({});
const allButton = screen.getByRole('radio', { name: 'All' });
expect(allButton).toHaveAttribute('aria-checked', 'true');
});
it('reads "active" from URL and shows Active selected', () => {
renderStatusFilter({ searchParams: { statusFilter: 'active' } });
const activeButton = screen.getByRole('radio', { name: 'Active' });
expect(activeButton).toHaveAttribute('aria-checked', 'true');
});
it('reads "inactive" from URL and shows Inactive selected', () => {
renderStatusFilter({ searchParams: { statusFilter: 'inactive' } });
const inactiveButton = screen.getByRole('radio', { name: 'Inactive' });
expect(inactiveButton).toHaveAttribute('aria-checked', 'true');
});
it('updates URL to "active" when Active clicked', async () => {
const onUrlUpdate = jest.fn<void, [UrlUpdateEvent]>();
renderStatusFilter({ onUrlUpdate });
const activeButton = screen.getByRole('radio', { name: 'Active' });
fireEvent.click(activeButton);
await waitFor(() => {
const statusFilterValue = onUrlUpdate.mock.calls
.map((call) => call[0].searchParams.get('statusFilter'))
.filter(Boolean)
.pop();
expect(statusFilterValue).toBe('active');
});
});
it('updates URL to "inactive" when Inactive clicked', async () => {
const onUrlUpdate = jest.fn<void, [UrlUpdateEvent]>();
renderStatusFilter({ onUrlUpdate });
const inactiveButton = screen.getByRole('radio', { name: 'Inactive' });
fireEvent.click(inactiveButton);
await waitFor(() => {
const statusFilterValue = onUrlUpdate.mock.calls
.map((call) => call[0].searchParams.get('statusFilter'))
.filter(Boolean)
.pop();
expect(statusFilterValue).toBe('inactive');
});
});
it('removes statusFilter from URL when All clicked', async () => {
const onUrlUpdate = jest.fn<void, [UrlUpdateEvent]>();
renderStatusFilter({
searchParams: { statusFilter: 'active' },
onUrlUpdate,
});
const allButton = screen.getByRole('radio', { name: 'All' });
fireEvent.click(allButton);
await waitFor(() => {
const lastCall = onUrlUpdate.mock.calls[onUrlUpdate.mock.calls.length - 1];
expect(lastCall[0].searchParams.get('statusFilter')).toBeNull();
});
});
it('resets page when filter changes', async () => {
const onUrlUpdate = jest.fn<void, [UrlUpdateEvent]>();
renderStatusFilter({
searchParams: { page: '3' },
onUrlUpdate,
});
const activeButton = screen.getByRole('radio', { name: 'Active' });
fireEvent.click(activeButton);
await waitFor(() => {
const lastCall = onUrlUpdate.mock.calls[onUrlUpdate.mock.calls.length - 1];
const pageValue = lastCall[0].searchParams.get('page');
// page=1 is default, so nuqs removes it from URL (null) or keeps as "1"
expect(pageValue === null || pageValue === '1').toBe(true);
});
});
});

View File

@@ -0,0 +1,63 @@
import { ToggleGroup, ToggleGroupItem } from '@signozhq/ui/toggle-group';
import {
StatusFilterValue,
useInfraMonitoringPageListing,
useInfraMonitoringStatusFilter,
} from 'container/InfraMonitoringK8sV2/hooks';
import styles from './StatusFilter.module.scss';
const statusOptions: Array<{
label: string;
value: StatusFilterValue | 'all';
}> = [
{ label: 'All', value: 'all' },
{ label: 'Active', value: 'active' },
{ label: 'Inactive', value: 'inactive' },
];
function StatusFilter(): JSX.Element {
const [statusFilter, setStatusFilter] = useInfraMonitoringStatusFilter();
const [, setCurrentPage] = useInfraMonitoringPageListing();
const handleChange = (value: string): void => {
if (value !== undefined) {
void setStatusFilter(value === 'all' ? '' : (value as StatusFilterValue));
void setCurrentPage(1);
}
};
return (
<div className={styles.statusFilterContainer}>
<div className={styles.statusLabel}>Status</div>
<ToggleGroup
type="single"
value={statusFilter === '' ? 'all' : statusFilter}
onChange={handleChange}
className={styles.statusToggleGroup}
>
{statusOptions.map((option) => (
<ToggleGroupItem
key={option.value}
value={option.value}
aria-label={option.label}
className={styles.statusToggleItem}
>
<span
className={`${styles.statusDot} ${
option.value === 'active'
? styles.activeDot
: option.value === 'inactive'
? styles.inactiveDot
: styles.allDot
}`}
/>
{option.label}
</ToggleGroupItem>
))}
</ToggleGroup>
</div>
);
}
export default StatusFilter;

View File

@@ -4,26 +4,16 @@ import { Badge } from '@signozhq/ui/badge';
import { Progress } from '@signozhq/ui/progress';
import { Typography } from '@signozhq/ui/typography';
import {
getHostLists,
HostData,
HostListPayload,
} from 'api/infraMonitoring/getHostLists';
import {
createFilterItem,
K8sDetailsFilters,
K8sDetailsMetadataConfig,
} from 'container/InfraMonitoringK8sV2/Base/K8sBaseDetails';
import { K8sBaseFilters } from 'container/InfraMonitoringK8sV2/Base/types';
InframonitoringtypesHostRecordDTO,
InframonitoringtypesHostStatusDTO,
} from 'api/generated/services/sigNoz.schemas';
import { K8sDetailsMetadataConfig } from 'container/InfraMonitoringK8sV2/Base/K8sBaseDetails';
import { formatValueForExpression } from 'components/QueryBuilderV2/utils';
import { INFRA_MONITORING_ATTR_KEYS } from 'container/InfraMonitoringK8sV2/constants';
import {
getHostQueryPayload,
hostWidgetInfo,
} from 'container/LogDetailedView/InfraMetrics/constants';
import {
TagFilter,
TagFilterItem,
} from 'types/api/queryBuilder/queryBuilderData';
import { getHostListsQuery } from './utils';
import infraHostsStyles from './InfraMonitoringHosts.module.scss';
@@ -47,24 +37,32 @@ export function getMemoryProgressColor(percent: number): string {
return Color.BG_FOREST_500;
}
export const hostDetailsMetadataConfig: K8sDetailsMetadataConfig<HostData>[] = [
export type HostDetailMetadataConfigType =
K8sDetailsMetadataConfig<InframonitoringtypesHostRecordDTO>;
export const hostDetailsMetadataConfig: HostDetailMetadataConfigType[] = [
{
label: 'STATUS',
getValue: (h): string => (h.active ? 'ACTIVE' : 'INACTIVE'),
render: (value, h): React.ReactNode => (
<Badge
variant="outline"
className={`${infraHostsStyles.infraMonitoringTags} ${
h.active ? infraHostsStyles.tagsActive : infraHostsStyles.tagsInactive
}`}
>
{value}
</Badge>
),
getValue: (h): string =>
h.status === InframonitoringtypesHostStatusDTO.active
? 'ACTIVE'
: 'INACTIVE',
render: (value, h): React.ReactNode => {
const isActive = h.status === InframonitoringtypesHostStatusDTO.active;
return (
<Badge
variant="outline"
className={`${infraHostsStyles.infraMonitoringTags} ${
isActive ? infraHostsStyles.tagsActive : infraHostsStyles.tagsInactive
}`}
>
{value}
</Badge>
);
},
},
{
label: 'OPERATING SYSTEM',
getValue: (h): string => h.os || '-',
getValue: (h): string => h.meta?.['os.type'] || '-',
render: (value): React.ReactNode =>
value !== '-' ? (
<Badge variant="outline" className={infraHostsStyles.infraMonitoringTags}>
@@ -99,7 +97,7 @@ export const hostDetailsMetadataConfig: K8sDetailsMetadataConfig<HostData>[] = [
];
export function getHostMetricsQueryPayload(
host: HostData,
host: InframonitoringtypesHostRecordDTO,
start: number,
end: number,
dotMetricsEnabled: boolean,
@@ -109,83 +107,26 @@ export function getHostMetricsQueryPayload(
export { hostWidgetInfo };
export function hostGetSelectedItemFilters(
selectedItem: string,
export const hostGetSelectedItemExpression = (hostName: string): string =>
`host.name = ${formatValueForExpression(hostName)}`;
export function hostInitialLogTracesExpression(
host: InframonitoringtypesHostRecordDTO,
dotMetricsEnabled: boolean,
): TagFilter {
const hostKey = dotMetricsEnabled ? 'host.name' : 'host_name';
return {
op: 'AND',
items: [createFilterItem(hostKey, selectedItem)],
};
): string {
const hostKey = dotMetricsEnabled
? INFRA_MONITORING_ATTR_KEYS.HOST_NAME
: 'host_name';
const hostName = formatValueForExpression(host.hostName || '');
return `${hostKey} = ${hostName}`;
}
export function hostInitialLogTracesFilter(
host: HostData,
dotMetricsEnabled: boolean,
): TagFilterItem[] {
const hostKey = dotMetricsEnabled ? 'host.name' : 'host_name';
return [createFilterItem(hostKey, host.hostName || '')];
export function hostInitialEventsExpression(
_host: InframonitoringtypesHostRecordDTO,
): string {
return '';
}
export function hostInitialEventsFilter(_host: HostData): TagFilterItem[] {
return [];
}
export const hostGetEntityName = (host: HostData): string => host.hostName;
export async function fetchHostListData(
filters: K8sBaseFilters,
signal?: AbortSignal,
): Promise<{
data: HostData[];
total: number;
error?: string | null;
rawData?: unknown;
}> {
const baseQuery = getHostListsQuery();
const payload: HostListPayload = {
...baseQuery,
limit: filters.limit,
offset: filters.offset,
filters: filters.filters ?? { items: [], op: 'and' },
orderBy: filters.orderBy,
start: filters.start,
end: filters.end,
groupBy: filters.groupBy ?? [],
};
const response = await getHostLists(payload, signal);
return {
data: response.payload?.data?.records || [],
total: response.payload?.data?.total || 0,
error: response.error,
rawData: response.payload?.data,
};
}
export async function fetchHostEntityData(
filters: K8sDetailsFilters,
signal?: AbortSignal,
): Promise<{ data: HostData | null; error?: string | null }> {
const response = await getHostLists(
{
...getHostListsQuery(),
filters: filters.filters,
start: filters.start,
end: filters.end,
limit: 1,
offset: 0,
groupBy: [],
},
signal,
);
const records = response.payload?.data?.records || [];
return {
data: records.length > 0 ? records[0] : null,
error: response.error,
};
}
export const hostGetEntityName = (
host: InframonitoringtypesHostRecordDTO,
): string => host.hostName;

View File

@@ -1,55 +1,91 @@
import React from 'react';
import { Tooltip } from 'antd';
import { Badge } from '@signozhq/ui/badge';
import { HostData } from 'api/infraMonitoring/getHostLists';
import { Color } from '@signozhq/design-tokens';
import { Container } from '@signozhq/icons';
import {
InframonitoringtypesHostRecordDTO,
InframonitoringtypesHostStatusDTO,
} from 'api/generated/services/sigNoz.schemas';
import TanStackTable, { TableColumnDef } from 'components/TanStackTableView';
import { getGroupByEl } from 'container/InfraMonitoringK8sV2/Base/utils';
import {
EntityProgressBar,
ExpandButtonWrapper,
GroupedStatusCounts,
ValidateColumnValueWrapper,
} from 'container/InfraMonitoringK8sV2/components';
import { InfraMonitoringEntity } from 'container/InfraMonitoringK8sV2/constants';
import {
INFRA_MONITORING_ATTR_KEYS,
InfraMonitoringEntity,
} from 'container/InfraMonitoringK8sV2/constants';
import { useInfraMonitoringGroupBy } from 'container/InfraMonitoringK8sV2/hooks';
import ColumnHeader from 'container/InfraMonitoringK8sV2/Base/ColumnHeader';
import EntityGroupHeader from 'container/InfraMonitoringK8sV2/Base/EntityGroupHeader';
import { HostnameCell } from './utils';
import styles from './table.module.scss';
import { Container, Info } from '@signozhq/icons';
import { Badge, BadgeColor } from '@signozhq/ui/badge';
function hostRowSource(host: HostData): { meta: Record<string, string> } {
const statusMap: Record<
InframonitoringtypesHostStatusDTO,
{
label: string;
color: BadgeColor;
}
> = {
[InframonitoringtypesHostStatusDTO.active]: {
label: 'ACTIVE',
color: 'forest',
},
[InframonitoringtypesHostStatusDTO.inactive]: {
label: 'INACTIVE',
color: 'amber',
},
['']: {
label: 'UNKNOWN',
color: 'secondary',
},
};
function hostRowSource(host: InframonitoringtypesHostRecordDTO): {
meta: Record<string, string>;
} {
return {
meta: {
...(host.meta ?? {}),
host_name: host.hostName ?? '',
'host.name': host.hostName ?? '',
os_type: host.os ?? '',
'os.type': host.os ?? '',
[INFRA_MONITORING_ATTR_KEYS.HOST_NAME]: host.hostName ?? '',
},
};
}
export function getHostRowKey(host: HostData): string {
export function getHostRowKey(host: InframonitoringtypesHostRecordDTO): string {
return host.hostName || 'unknown';
}
export function getHostItemKey(host: HostData): string {
export function getHostItemKey(
host: InframonitoringtypesHostRecordDTO,
): string {
return host.hostName ?? '';
}
function HostGroupCell({ row }: { row: HostData }): JSX.Element {
function HostGroupCell({
row,
}: {
row: InframonitoringtypesHostRecordDTO;
}): JSX.Element {
const [groupBy] = useInfraMonitoringGroupBy();
const synthetic = hostRowSource(row);
return getGroupByEl(synthetic, groupBy) as JSX.Element;
}
export const hostColumnsConfig: TableColumnDef<HostData>[] = [
export type HostColumnConfigType =
TableColumnDef<InframonitoringtypesHostRecordDTO>;
export const hostColumnsConfig: HostColumnConfigType[] = [
{
id: 'hostGroup',
header: (): React.ReactNode => <EntityGroupHeader title="HOST GROUP" />,
header: (): React.ReactNode => <EntityGroupHeader title="Host Group" />,
accessorFn: (row): string => row.hostName ?? '',
width: { min: 300 },
width: { min: 290 },
enableSort: false,
enableRemove: false,
enableMove: false,
@@ -64,7 +100,11 @@ export const hostColumnsConfig: TableColumnDef<HostData>[] = [
{
id: 'hostName',
header: (): React.ReactNode => (
<EntityGroupHeader title="Hostname" icon={<Container size={14} />} />
<EntityGroupHeader
title="Hostname"
icon={<Container size={14} />}
docPath="/infrastructure-monitoring/host-monitoring#hostname"
/>
),
accessorFn: (row): string => row.hostName ?? '',
width: { min: 290 },
@@ -78,27 +118,48 @@ export const hostColumnsConfig: TableColumnDef<HostData>[] = [
),
},
{
id: 'active',
id: 'status',
header: (): React.ReactNode => (
<div className={styles.statusHeader}>
<ColumnHeader
tooltip="Sent system metrics in last 10 mins."
docPath="/infrastructure-monitoring/host-monitoring#status"
>
Status
<Tooltip title="Sent system metrics in last 10 mins">
<Info size="md" />
</Tooltip>
</div>
</ColumnHeader>
),
accessorFn: (row): boolean => row.active,
width: { min: 150, default: 150 },
accessorFn: (row): string => row.status,
width: { min: 140 },
enableSort: false,
cell: ({ value }): React.ReactNode => {
const active = value as boolean;
cell: ({ value, groupMeta, row }): React.ReactNode => {
const status = value as InframonitoringtypesHostStatusDTO;
if (groupMeta) {
return (
<GroupedStatusCounts
items={[
{
value: row.activeHostCount,
label: 'Active',
color: Color.BG_FOREST_500,
},
{
value: row.inactiveHostCount,
label: 'Inactive',
color: Color.BG_AMBER_500,
},
]}
/>
);
}
const statusDetails = statusMap[status] || statusMap[''];
return (
<Badge
className={`${styles.statusTag} ${
active ? styles.statusTagActive : styles.statusTagInactive
}`}
variant="outline"
color={statusDetails.color}
className={`${styles.statusTag}`}
>
{active ? 'ACTIVE' : 'INACTIVE'}
{statusDetails.label}
</Badge>
);
},
@@ -106,7 +167,9 @@ export const hostColumnsConfig: TableColumnDef<HostData>[] = [
{
id: 'cpu',
header: (): React.ReactNode => (
<div className={styles.columnHeaderRight}>CPU Usage</div>
<ColumnHeader docPath="/infrastructure-monitoring/host-monitoring#cpu-usage">
CPU Usage
</ColumnHeader>
),
accessorFn: (row): number => row.cpu,
width: { min: 220 },
@@ -129,15 +192,16 @@ export const hostColumnsConfig: TableColumnDef<HostData>[] = [
{
id: 'memory',
header: (): React.ReactNode => (
<div className={`${styles.columnHeaderRight} ${styles.memoryUsageHeader}`}>
Memory Usage
<Tooltip title="Excluding cache memory">
<Info size="md" />
</Tooltip>
</div>
<ColumnHeader
tooltip="Excluding cache memory."
docPath="/infrastructure-monitoring/host-monitoring#memory-usage"
className={styles.memoryUsageHeader}
>
Memory Usage (WSS)
</ColumnHeader>
),
accessorFn: (row): number => row.memory,
width: { min: 220 },
width: { min: 240 },
enableSort: true,
cell: ({ value }): React.ReactNode => {
const memory = value as number;
@@ -157,10 +221,12 @@ export const hostColumnsConfig: TableColumnDef<HostData>[] = [
{
id: 'wait',
header: (): React.ReactNode => (
<div className={styles.columnHeaderRight}>IOWait</div>
<ColumnHeader docPath="/infrastructure-monitoring/host-monitoring#iowait">
IOWait
</ColumnHeader>
),
accessorFn: (row): number => row.wait,
width: { min: 100, default: 100 },
width: { min: 120 },
enableSort: true,
cell: ({ value }): React.ReactNode => {
const wait = value as number;
@@ -171,7 +237,9 @@ export const hostColumnsConfig: TableColumnDef<HostData>[] = [
entity={InfraMonitoringEntity.HOSTS}
attribute="IOWait metric"
>
<TanStackTable.Text>{`${Number((wait * 100).toFixed(1))}%`}</TanStackTable.Text>
<TanStackTable.Text>
{`${Number((wait * 100).toFixed(1))}%`}
</TanStackTable.Text>
</ValidateColumnValueWrapper>
);
},
@@ -179,13 +247,15 @@ export const hostColumnsConfig: TableColumnDef<HostData>[] = [
{
id: 'load15',
header: (): React.ReactNode => (
<div className={styles.columnHeaderRight}>Load Avg</div>
<ColumnHeader docPath="/infrastructure-monitoring/host-monitoring#load-avg">
Load Avg (15min)
</ColumnHeader>
),
accessorFn: (row): number => row.load15,
width: { min: 100, default: 100 },
width: { min: 200 },
enableSort: true,
cell: ({ value }): React.ReactNode => {
const load15 = value as number;
const load15 = Number(value);
return (
<ValidateColumnValueWrapper
@@ -193,9 +263,35 @@ export const hostColumnsConfig: TableColumnDef<HostData>[] = [
entity={InfraMonitoringEntity.HOSTS}
attribute="load average metric"
>
<TanStackTable.Text>{load15}</TanStackTable.Text>
<TanStackTable.Text>{load15.toFixed(2)}</TanStackTable.Text>
</ValidateColumnValueWrapper>
);
},
},
{
id: 'diskUsage',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/host-monitoring#disk-usage">
Disk Usage
</ColumnHeader>
),
accessorFn: (row): number => row.diskUsage,
width: { min: 160 },
enableSort: true,
cell: ({ value }): React.ReactNode => {
const diskUsage = value as number;
return (
<div className={styles.progressContainer}>
<ValidateColumnValueWrapper
value={diskUsage}
entity={InfraMonitoringEntity.HOSTS}
attribute="disk usage metric"
>
<EntityProgressBar value={diskUsage} type="disk" />
</ValidateColumnValueWrapper>
</div>
);
},
},
];

View File

@@ -9,13 +9,6 @@
display: block;
}
.statusHeader {
display: inline-flex;
align-items: center;
gap: var(--spacing-2);
width: 100%;
}
.columnHeaderRight {
text-align: right;
}

View File

@@ -2,14 +2,16 @@ import React from 'react';
import { Color } from '@signozhq/design-tokens';
import { Tooltip } from 'antd';
import { Typography } from '@signozhq/ui/typography';
import { HostListPayload } from 'api/infraMonitoring/getHostLists';
import {
FiltersType,
IQuickFiltersConfig,
} from 'components/QuickFilters/types';
import { TriangleAlert } from '@signozhq/icons';
import { INFRA_MONITORING_ATTR_KEYS } from 'container/InfraMonitoringK8sV2/constants';
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';
@@ -21,13 +23,15 @@ export function HostnameCell({
}): React.ReactElement {
const isEmpty = !hostName || !hostName.trim();
if (!isEmpty) {
return <div className="hostname-column-value">{hostName}</div>;
return (
<CellValueTooltip value={hostName}>
<TanStackTable.Text>{hostName}</TanStackTable.Text>
</CellValueTooltip>
);
}
return (
<div className="hostname-cell-missing">
<Typography.Text color="muted" className="hostname-cell-placeholder">
-
</Typography.Text>
<>
<Typography.Text color="muted">-</Typography.Text>
<Tooltip
title={
<div>
@@ -60,52 +64,16 @@ export function HostnameCell({
<TriangleAlert size={14} color={Color.BG_CHERRY_500} />
</span>
</Tooltip>
</div>
</>
);
}
export const getHostListsQuery = (): HostListPayload => ({
filters: {
items: [],
op: 'and',
},
groupBy: [],
orderBy: { columnName: 'cpu', order: 'desc' },
});
export const HostsQuickFiltersConfig: IQuickFiltersConfig[] = [
{
type: FiltersType.CHECKBOX,
title: 'Host Name',
attributeKey: {
key: 'host_name',
dataType: DataTypes.String,
type: 'resource',
},
aggregateOperator: 'noop',
aggregateAttribute: 'system_cpu_load_average_15m',
dataSource: DataSource.METRICS,
defaultOpen: true,
},
{
type: FiltersType.CHECKBOX,
title: 'OS Type',
attributeKey: {
key: 'os_type',
dataType: DataTypes.String,
type: 'resource',
},
aggregateOperator: 'noop',
aggregateAttribute: 'system_cpu_load_average_15m',
dataSource: DataSource.METRICS,
defaultOpen: true,
},
];
export function getHostsQuickFiltersConfig(
dotMetricsEnabled: boolean,
): IQuickFiltersConfig[] {
const hostNameKey = dotMetricsEnabled ? 'host.name' : 'host_name';
const hostNameKey = dotMetricsEnabled
? INFRA_MONITORING_ATTR_KEYS.HOST_NAME
: 'host_name';
const osTypeKey = dotMetricsEnabled ? 'os.type' : 'os_type';
const metricName = dotMetricsEnabled
? 'system.cpu.load_average.15m'

View File

@@ -0,0 +1,16 @@
.columnHeader {
display: inline-flex;
align-items: center;
gap: var(--spacing-2);
}
.infoIcon {
display: inline-flex;
align-items: center;
cursor: pointer;
}
.columnHeaderLabel {
text-align: center;
padding: var(--spacing-2) var(--spacing-2) var(--spacing-2) 0px;
}

View File

@@ -0,0 +1,90 @@
import { Info } from '@signozhq/icons';
import { TooltipSimple } from '@signozhq/ui/tooltip';
import styles from './ColumnHeader.module.scss';
import cx from 'classnames';
const DOCS_BASE_URL = `${process.env.DOCS_BASE_URL}/docs`;
interface ColumnHeaderProps {
children?: React.ReactNode;
title?: string;
docPath?: string;
tooltip?: string;
className?: string;
}
function ColumnHeader({
children,
title,
docPath,
tooltip,
className,
}: ColumnHeaderProps): JSX.Element {
const renderContent = (): React.ReactNode => {
if (children) {
return children;
}
if (title) {
const parts = title.split('\n');
return parts.map((part, index) => (
<div key={`${part}-${index}`}>
{part}
{index < parts.length - 1 && <br />}
</div>
));
}
return null;
};
const renderInfoIcon = (): React.ReactNode => {
if (docPath) {
const tooltipTitle = tooltip || 'Not sure what this means?';
return (
<TooltipSimple
arrow
title={
<>
{tooltipTitle}{' '}
<a
href={`${DOCS_BASE_URL}${docPath}`}
target="_blank"
rel="noopener noreferrer"
onClick={(e): void => e.stopPropagation()}
>
Learn more.
</a>
</>
}
>
<div className={styles.infoIcon}>
<Info size="md" />
</div>
</TooltipSimple>
);
}
if (tooltip) {
return (
<TooltipSimple title={tooltip}>
<div className={styles.infoIcon}>
<Info size="md" />
</div>
</TooltipSimple>
);
}
return null;
};
return (
<div className={cx(styles.columnHeader, className)} data-slot="column-header">
<div className={styles.columnHeaderLabel}>{renderContent()}</div>
{renderInfoIcon()}
</div>
);
}
export default ColumnHeader;

View File

@@ -2,4 +2,16 @@
display: flex;
align-items: center;
gap: var(--spacing-5);
padding-left: 4px;
}
.infoIcon {
display: inline-flex;
align-items: center;
color: var(--text-slate-secondary);
cursor: pointer;
&:hover {
color: var(--text-slate-primary);
}
}

View File

@@ -1,19 +1,70 @@
import { Group } from '@signozhq/icons';
import { Group, Info } from '@signozhq/icons';
import { TooltipSimple } from '@signozhq/ui/tooltip';
import styles from './EntityGroupHeader.module.scss';
const DOCS_BASE_URL = `${process.env.DOCS_BASE_URL}/docs`;
interface EntityGroupHeaderProps {
title: string;
icon?: React.ReactNode;
docPath?: string;
tooltip?: string;
}
function EntityGroupHeader({
title,
icon,
docPath,
tooltip,
}: EntityGroupHeaderProps): JSX.Element {
const renderInfoIcon = (): React.ReactNode => {
if (docPath) {
const tooltipTitle = tooltip || 'Not sure what this means?';
return (
<TooltipSimple
arrow
title={
<>
{tooltipTitle}{' '}
<a
href={`${DOCS_BASE_URL}${docPath}`}
target="_blank"
rel="noopener noreferrer"
onClick={(e): void => e.stopPropagation()}
>
Learn more.
</a>
</>
}
>
<span className={styles.infoIcon}>
<Info size="md" />
</span>
</TooltipSimple>
);
}
if (tooltip) {
return (
<TooltipSimple title={tooltip}>
<span className={styles.infoIcon}>
<Info size="md" />
</span>
</TooltipSimple>
);
}
return null;
};
return (
<div className={styles.entityGroupHeader}>
{icon || <Group size={14} data-hide-expanded="true" />} {title}
<div className={styles.entityGroupHeader} data-slot="entity-group-header">
<span data-slot="icon">
{icon || <Group size={14} data-hide-expanded="true" />}
</span>{' '}
{title}
{renderInfoIcon()}
</div>
);
}

View File

@@ -6,7 +6,6 @@ import React, {
useState,
} from 'react';
import { useQuery } from 'react-query';
// eslint-disable-next-line no-restricted-imports
import { Color, Spacing } from '@signozhq/design-tokens';
import { Button, Drawer, Tooltip } from 'antd';
import { ToggleGroupSimple } from '@signozhq/ui/toggle-group';
@@ -14,7 +13,6 @@ import { Divider } from '@signozhq/ui/divider';
import { Typography } from '@signozhq/ui/typography';
import logEvent from 'api/common/logEvent';
import { combineInitialAndUserExpression } from 'components/QueryBuilderV2/QueryV2/QuerySearch/utils';
import { convertFiltersToExpression } from 'components/QueryBuilderV2/utils';
import { InfraMonitoringEvents } from 'constants/events';
import { QueryParams } from 'constants/query';
import {
@@ -40,17 +38,11 @@ import {
} from '@signozhq/icons';
import { isCustomTimeRange, useGlobalTimeStore } from 'store/globalTime';
import { NANO_SECOND_MULTIPLIER } from 'store/globalTime/utils';
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
import {
TagFilter,
TagFilterItem,
} from 'types/api/queryBuilder/queryBuilderData';
import {
LogsAggregatorOperator,
TracesAggregatorOperator,
} from 'types/common/queryBuilder';
import { openInNewTab } from 'utils/navigation';
import { v4 as uuidv4 } from 'uuid';
import { InfraMonitoringEntity, VIEW_TYPES } from '../constants';
import EntityEvents from '../EntityDetailsUtils/EntityEvents';
@@ -80,7 +72,7 @@ export interface K8sDetailsMetadataConfig<T> {
}
export interface K8sDetailsFilters {
filters: TagFilter;
filter: { expression: string };
start: number;
end: number;
}
@@ -89,15 +81,15 @@ export interface K8sBaseDetailsProps<T> {
category: InfraMonitoringEntity;
eventCategory: string;
// Data fetching configuration
getSelectedItemFilters: (selectedItem: string) => TagFilter;
getSelectedItemExpression: (selectedItem: string) => string;
fetchEntityData: (
filters: K8sDetailsFilters,
signal?: AbortSignal,
) => Promise<{ data: T | null; error?: string | null }>;
// Entity configuration
getEntityName: (entity: T) => string;
getInitialLogTracesFilters: (entity: T) => TagFilterItem[];
getInitialEventsFilters: (entity: T) => TagFilterItem[];
getInitialLogTracesExpression: (entity: T) => string;
getInitialEventsExpression: (entity: T) => string;
metadataConfig: K8sDetailsMetadataConfig<T>[];
entityWidgetInfo: {
title: string;
@@ -134,33 +126,15 @@ export interface K8sBaseDetailsProps<T> {
}>;
}
export function createFilterItem(
key: string,
value: string,
dataType: DataTypes = DataTypes.String,
): TagFilterItem {
return {
id: uuidv4(),
key: {
key,
dataType,
type: 'resource',
id: `${key}--string--resource--false`,
},
op: '=',
value,
};
}
// eslint-disable-next-line sonarjs/cognitive-complexity
export default function K8sBaseDetails<T>({
category,
eventCategory,
getSelectedItemFilters,
getSelectedItemExpression,
fetchEntityData,
getEntityName,
getInitialLogTracesFilters,
getInitialEventsFilters,
getInitialLogTracesExpression,
getInitialEventsExpression,
metadataConfig,
entityWidgetInfo,
getEntityQueryPayload,
@@ -201,17 +175,12 @@ export default function K8sBaseDetails<T>({
if (!selectedItem) {
return { data: null };
}
const filters = getSelectedItemFilters(selectedItem);
const { minTime, maxTime } = getMinMaxTime();
const start = Math.floor(minTime / NANO_SECOND_MULTIPLIER);
const end = Math.floor(maxTime / NANO_SECOND_MULTIPLIER);
const expression = getSelectedItemExpression(selectedItem);
return fetchEntityData(
{
filters,
start: Math.floor(minTime / NANO_SECOND_MULTIPLIER),
end: Math.floor(maxTime / NANO_SECOND_MULTIPLIER),
},
signal,
);
return fetchEntityData({ filter: { expression }, start, end }, signal);
},
enabled: !!selectedItem,
});
@@ -223,23 +192,15 @@ export default function K8sBaseDetails<T>({
if (!entity) {
return '';
}
const primaryFiltersOnly = {
op: 'AND' as const,
items: getInitialLogTracesFilters(entity),
};
return convertFiltersToExpression(primaryFiltersOnly).expression;
}, [entity, getInitialLogTracesFilters]);
return getInitialLogTracesExpression(entity);
}, [entity, getInitialLogTracesExpression]);
const eventsInitialExpression = useMemo(() => {
if (!entity) {
return '';
}
const primaryFiltersOnly = {
op: 'AND' as const,
items: getInitialEventsFilters(entity),
};
return convertFiltersToExpression(primaryFiltersOnly).expression;
}, [entity, getInitialEventsFilters]);
return getInitialEventsExpression(entity);
}, [entity, getInitialEventsExpression]);
const handleClose = useCallback((): void => {
setSelectedItem(null);

View File

@@ -1,3 +1,10 @@
.tableContainer {
display: flex;
flex: 1;
flex-direction: column;
min-height: 0;
}
.emptyStateContainer {
display: flex;
flex: 1;
@@ -7,7 +14,6 @@
}
.k8SListTable {
padding-left: var(--spacing-2);
--tanstack-table-header-cell-bg: var(--l2-background);
--tanstack-table-header-cell-color: var(--l2-foreground);
--tanstack-table-cell-bg: var(--l2-background);
@@ -18,9 +24,12 @@
--tanstack-table-resize-handle-hover-bg: var(--l2-border);
--tanstack-table-row-height: 42px;
--tanstack-table-header-label-width: 100%;
--tanstack-cell-padding-top-override: 5px;
--tanstack-cell-padding-bottom-override: 5px;
--tanstack-cell-padding-left-override: 5px;
--tanstack-cell-header-padding-left-override: 5px;
--tanstack-cell-padding-left-override: 26px;
--tanstack-cell-padding-right-override: 5px;
--tanstack-expansion-first-col-padding-left: 30px;

View File

@@ -1,15 +1,19 @@
import { useCallback, useEffect, useMemo } from 'react';
import { useQuery } from 'react-query';
import { useQuery, useQueryClient } from 'react-query';
import { Typography } from '@signozhq/ui/typography';
import logEvent from 'api/common/logEvent';
import TanStackTable, {
TableColumnDef,
useCalculatedPageSize,
useHiddenColumnIds,
useTableParams,
} 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 {
@@ -17,15 +21,15 @@ import {
InfraMonitoringEntity,
} from '../constants';
import {
useInfraMonitoringFiltersK8s,
useInfraMonitoringGroupBy,
useInfraMonitoringOrderBy,
useInfraMonitoringPageListing,
useInfraMonitoringPageSizeListing,
useInfraMonitoringStatusFilter,
} from '../hooks';
import { useInfraMonitoringLineClamp } from '../components';
import { K8sEmptyState } from './K8sEmptyState';
import { K8sExpandedRow } from './K8sExpandedRow';
import K8sHeader from './K8sHeader';
import { K8sPaginationWarning } from './K8sPaginationWarning';
import { K8sBaseFilters } from './types';
import { getGroupedByMeta } from './utils';
@@ -38,24 +42,30 @@ export type K8sBaseListEmptyStateContext = {
totalCount: number;
hasFilters: boolean;
isLoading: boolean;
endTimeBeforeRetention?: boolean;
rawData?: unknown;
};
/** Base type constraint for K8s entity data */
export type K8sEntityData = { meta?: Record<string, string> };
export type K8sEntityData = { meta?: Record<string, string> | null };
export type K8sBaseListProps<T extends K8sEntityData> = {
controlListPrefix?: React.ReactNode;
leftFilters?: React.ReactNode;
entity: InfraMonitoringEntity;
tableColumns: TableColumnDef<T>[];
fetchListData: (
filters: K8sBaseFilters,
signal?: AbortSignal,
) => Promise<{
data: T[];
type?: 'list' | 'grouped_list';
records?: T[];
data?: T[];
total: number;
error?: string | null;
rawData?: unknown;
endTimeBeforeRetention?: boolean;
warning?: Querybuildertypesv5QueryWarnDataDTO | null;
}>;
/** Function to get the unique key for a row. */
getRowKey?: (record: T) => string;
@@ -65,10 +75,12 @@ export type K8sBaseListProps<T extends K8sEntityData> = {
renderEmptyState?: (
context: K8sBaseListEmptyStateContext,
) => React.ReactNode | null;
extraQueryKeyParts?: string[];
};
export function K8sBaseList<T extends K8sEntityData>({
controlListPrefix,
leftFilters,
entity,
tableColumns,
fetchListData,
@@ -76,12 +88,14 @@ export function K8sBaseList<T extends K8sEntityData>({
getItemKey,
eventCategory,
renderEmptyState,
extraQueryKeyParts = [],
}: K8sBaseListProps<T>): JSX.Element {
const [queryFilters] = useInfraMonitoringFiltersK8s();
const [currentPage] = useInfraMonitoringPageListing();
const [currentPageSize] = useInfraMonitoringPageSizeListing();
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,
@@ -90,6 +104,27 @@ export function K8sBaseList<T extends K8sEntityData>({
const columnStorageKey = `k8s-${entity}-columns`;
const hiddenColumnIds = useHiddenColumnIds(columnStorageKey);
const { containerRef, calculatedPageSize } = useCalculatedPageSize({
rowHeight: 42,
});
const {
page: currentPage,
limit: currentPageSize,
setLimit,
} = useTableParams(
{
page: INFRA_MONITORING_K8S_PARAMS_KEYS.PAGE,
limit: INFRA_MONITORING_K8S_PARAMS_KEYS.PAGE_SIZE,
},
{
page: 1,
limit: 10,
storageKey: `k8s-${entity}`,
calculatedPageSize,
},
);
const selectedTime = useGlobalTimeStore((s) => s.selectedTime);
const refreshInterval = useGlobalTimeStore((s) => s.refreshInterval);
const isRefreshEnabled = useGlobalTimeStore((s) => s.isRefreshEnabled);
@@ -105,9 +140,11 @@ export function K8sBaseList<T extends K8sEntityData>({
entity,
String(currentPageSize),
String(currentPage),
JSON.stringify(queryFilters),
expression || '',
JSON.stringify(orderBy),
JSON.stringify(groupBy),
statusFilter,
...extraQueryKeyParts,
);
}, [
getAutoRefreshQueryKey,
@@ -115,35 +152,64 @@ export function K8sBaseList<T extends K8sEntityData>({
entity,
currentPageSize,
currentPage,
queryFilters,
expression,
orderBy,
groupBy,
statusFilter,
extraQueryKeyParts,
]);
const { data, isLoading, isError } = useQuery({
queryKey,
queryFn: ({ signal }) => {
const { minTime, maxTime } = getMinMaxTime();
const queryClient = useQueryClient();
return fetchListData(
const { data, isLoading, isFetching, isError } = useQuery({
queryKey,
queryFn: async ({ signal }) => {
const { minTime, maxTime } = getMinMaxTime();
const start = Math.floor(minTime / NANO_SECOND_MULTIPLIER);
const end = Math.floor(maxTime / NANO_SECOND_MULTIPLIER);
const response = await fetchListData(
{
limit: currentPageSize,
filter: {
expression: expression || '',
filterByStatus:
statusFilter === 'active' || statusFilter === 'inactive'
? statusFilter
: undefined,
},
groupBy:
groupBy && groupBy.length > 0
? groupBy.map((g) => ({ name: g }))
: undefined,
offset: (currentPage - 1) * currentPageSize,
filters: queryFilters || { items: [], op: 'AND' },
start: Math.floor(minTime / NANO_SECOND_MULTIPLIER),
end: Math.floor(maxTime / NANO_SECOND_MULTIPLIER),
orderBy: orderBy || undefined,
groupBy: groupBy?.length > 0 ? groupBy : undefined,
limit: currentPageSize,
start,
end,
orderBy: orderBy
? { key: { name: orderBy.columnName }, direction: orderBy.order }
: undefined,
},
signal,
);
return {
data: response.records || response.data || [],
total: response.total,
error: response.error,
endTimeBeforeRetention: response.endTimeBeforeRetention,
rawData: response.rawData ?? response,
warning: response.warning ?? null,
};
},
refetchInterval: isRefreshEnabled ? refreshInterval : false,
});
const cancelQuery = useCallback((): void => {
void queryClient.cancelQueries({ queryKey });
}, [queryClient, queryKey]);
const pageData = data?.data ?? [];
const totalCount = data?.total || 0;
const hasFilters = (queryFilters?.items?.length ?? 0) > 0;
const hasFilters = !!expression?.trim();
const getGroupKeyFn = useCallback(
(item: T) => getGroupedByMeta(item, groupBy),
@@ -151,7 +217,7 @@ export function K8sBaseList<T extends K8sEntityData>({
);
useEffect(() => {
logEvent(InfraMonitoringEvents.PageVisited, {
void logEvent(InfraMonitoringEvents.PageVisited, {
entity: InfraMonitoringEvents.K8sEntity,
page: InfraMonitoringEvents.ListPage,
category: eventCategory,
@@ -162,10 +228,10 @@ export function K8sBaseList<T extends K8sEntityData>({
const handleRowClick = useCallback(
(_record: T, itemKey: string): void => {
if (groupBy.length === 0) {
setSelectedItem(itemKey);
void setSelectedItem(itemKey);
}
logEvent(InfraMonitoringEvents.ItemClicked, {
void logEvent(InfraMonitoringEvents.ItemClicked, {
entity: InfraMonitoringEvents.K8sEntity,
page: InfraMonitoringEvents.ListPage,
category: eventCategory,
@@ -185,7 +251,7 @@ export function K8sBaseList<T extends K8sEntityData>({
url.searchParams.set('selectedItem', itemKey);
openInNewTab(url.pathname + url.search);
logEvent(InfraMonitoringEvents.ItemClicked, {
void logEvent(InfraMonitoringEvents.ItemClicked, {
entity: InfraMonitoringEvents.K8sEntity,
page: InfraMonitoringEvents.ListPage,
category: eventCategory,
@@ -214,11 +280,19 @@ export function K8sBaseList<T extends K8sEntityData>({
entity={entity}
tableColumns={expandedRowColumns}
fetchListData={fetchListData}
extraQueryKeyParts={extraQueryKeyParts}
getRowKey={getRowKey}
getItemKey={getItemKey}
/>
),
[entity, fetchListData, getRowKey, getItemKey, expandedRowColumns],
[
entity,
fetchListData,
getRowKey,
getItemKey,
expandedRowColumns,
extraQueryKeyParts,
],
);
const getRowCanExpand = useCallback(
@@ -234,64 +308,78 @@ export function K8sBaseList<T extends K8sEntityData>({
totalCount,
hasFilters,
isLoading: showTableLoadingState,
endTimeBeforeRetention: data?.endTimeBeforeRetention,
rawData: data?.rawData,
}) || (
<K8sEmptyState
isError={isError}
error={data?.error}
isLoading={showTableLoadingState}
rawData={data?.rawData}
endTimeBeforeRetention={data?.endTimeBeforeRetention}
/>
);
const showEmptyState = !showTableLoadingState && pageData.length === 0;
const paginationWarningContent = data?.warning ? (
<K8sPaginationWarning warning={data.warning} />
) : null;
return (
<>
<K8sHeader
controlListPrefix={controlListPrefix}
leftFilters={leftFilters}
entity={entity}
showAutoRefresh={!selectedItem}
columns={tableColumns}
columnStorageKey={columnStorageKey}
isFetching={isFetching}
cancelQuery={cancelQuery}
/>
{isError && (
<Typography>{data?.error?.toString() || 'Something went wrong'}</Typography>
)}
<div ref={containerRef} className={styles.tableContainer}>
{isError && (
<Typography>
{data?.error?.toString() || 'Something went wrong'}
</Typography>
)}
{showEmptyState ? (
<div className={styles.emptyStateContainer}>{emptyTableMessage}</div>
) : (
<TanStackTable<T>
data={pageData}
columns={tableColumns}
columnStorageKey={columnStorageKey}
isLoading={showTableLoadingState}
getRowKey={getRowKey}
getItemKey={getItemKey}
groupBy={groupBy}
getGroupKey={getGroupKeyFn}
onRowClick={handleRowClick}
onRowClickNewTab={handleRowClickNewTab}
renderExpandedRow={isGroupedByAttribute ? renderExpandedRow : undefined}
getRowCanExpand={isGroupedByAttribute ? getRowCanExpand : undefined}
className={cx(styles.k8SListTable, expandedRowColumns)}
enableQueryParams={{
page: INFRA_MONITORING_K8S_PARAMS_KEYS.PAGE,
limit: INFRA_MONITORING_K8S_PARAMS_KEYS.PAGE_SIZE,
orderBy: INFRA_MONITORING_K8S_PARAMS_KEYS.ORDER_BY,
expanded: INFRA_MONITORING_K8S_PARAMS_KEYS.EXPANDED,
}}
pagination={{
total: totalCount,
defaultLimit: 10,
defaultPage: 1,
showTotalCount: true,
totalCountLabel: entity.charAt(0).toUpperCase() + entity.slice(1),
}}
paginationClassname={styles.paginationContainer}
/>
)}
{showEmptyState ? (
<div className={styles.emptyStateContainer}>{emptyTableMessage}</div>
) : (
<TanStackTable<T>
data={pageData}
columns={tableColumns}
columnStorageKey={columnStorageKey}
isLoading={showTableLoadingState}
getRowKey={getRowKey}
getItemKey={getItemKey}
groupBy={groupBy.map((g) => ({ key: g }))}
getGroupKey={getGroupKeyFn}
onRowClick={handleRowClick}
onRowClickNewTab={handleRowClickNewTab}
renderExpandedRow={isGroupedByAttribute ? renderExpandedRow : undefined}
getRowCanExpand={isGroupedByAttribute ? getRowCanExpand : undefined}
className={cx(styles.k8SListTable, expandedRowColumns)}
enableQueryParams={{
page: INFRA_MONITORING_K8S_PARAMS_KEYS.PAGE,
limit: INFRA_MONITORING_K8S_PARAMS_KEYS.PAGE_SIZE,
orderBy: INFRA_MONITORING_K8S_PARAMS_KEYS.ORDER_BY,
expanded: INFRA_MONITORING_K8S_PARAMS_KEYS.EXPANDED,
}}
pagination={{
total: totalCount,
showTotalCount: true,
totalCountLabel: entity.charAt(0).toUpperCase() + entity.slice(1),
calculatedPageSize,
onLimitChange: setLimit,
}}
plainTextCellLineClamp={lineClamp}
prefixPaginationContent={paginationWarningContent}
paginationClassname={styles.paginationContainer}
/>
)}
</div>
</>
);
}

View File

@@ -11,12 +11,6 @@ import type { K8sBaseListEmptyStateContext } from './K8sBaseList';
import styles from './K8sEmptyState.module.scss';
export interface K8sListResponseMetadata {
sentAnyHostMetricsData?: boolean;
isSendingK8SAgentMetrics?: boolean;
endTimeBeforeRetention?: boolean;
}
type K8sEmptyStateProps = Partial<K8sBaseListEmptyStateContext>;
const handleContactSupport = (isCloudUser: boolean): void => {
@@ -31,7 +25,7 @@ export function K8sEmptyState({
isError,
error,
isLoading,
rawData,
endTimeBeforeRetention,
}: K8sEmptyStateProps): JSX.Element | null {
const { isCloudUser } = useGetTenantLicense();
@@ -70,47 +64,7 @@ export function K8sEmptyState({
);
}
const metadata = rawData as K8sListResponseMetadata | undefined;
if (metadata?.sentAnyHostMetricsData === false) {
return (
<div className={styles.container}>
<div className={styles.content}>
<img className={styles.eyesEmoji} src={eyesEmojiUrl} alt="eyes emoji" />
<div className={styles.noDataMessage}>
<h5 className={styles.title}>No host metrics data received yet</h5>
<span className={styles.message}>
Please refer to{' '}
<a
href="https://signoz.io/docs/infrastructure-monitoring/hostmetrics/"
target="_blank"
rel="noreferrer"
>
our documentation
</a>{' '}
to learn how to send host metrics.
</span>
</div>
</div>
</div>
);
}
if (metadata?.isSendingK8SAgentMetrics) {
return (
<div className={styles.container}>
<div className={styles.content}>
<img className={styles.eyesEmoji} src={eyesEmojiUrl} alt="eyes emoji" />
<span className={styles.message}>
To see K8s metrics, upgrade to the latest version of SigNoz k8s-infra
chart. Please contact support if you need help.
</span>
</div>
</div>
);
}
if (metadata?.endTimeBeforeRetention) {
if (endTimeBeforeRetention) {
return (
<div className={styles.container}>
<div className={styles.content}>

View File

@@ -1,5 +1,7 @@
import { useCallback, useEffect, useMemo } from 'react';
import { useQuery } from 'react-query';
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 TanStackTable, {
@@ -7,16 +9,19 @@ import TanStackTable, {
TableColumnDef,
TanStackTableStateProvider,
} from 'components/TanStackTableView';
import { QueryParams } from 'constants/query';
import { CornerDownRight } from '@signozhq/icons';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useSafeNavigate } from 'hooks/useSafeNavigate';
import useUrlQuery from 'hooks/useUrlQuery';
import { v4 as uuid } from 'uuid';
import { useQueryState } from 'nuqs';
import { useGlobalTimeStore } from 'store/globalTime';
import { NANO_SECOND_MULTIPLIER } from 'store/globalTime/utils';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import { parseAsJsonNoValidate } from 'utils/nuqsParsers';
import { InfraMonitoringEntity } from '../constants';
import {
useInfraMonitoringFiltersK8s,
useInfraMonitoringGroupBy,
useInfraMonitoringOrderBy,
useInfraMonitoringPageListing,
@@ -25,6 +30,7 @@ import {
import { K8sBaseFilters } from './types';
import styles from './K8sExpandedRow.module.scss';
import { buildExpressionFromGroupMeta } from './utils';
const EXPANDED_ROW_LIMIT = 10;
@@ -35,15 +41,21 @@ export type K8sExpandedRowProps<T> = {
groupMeta?: Record<string, string>;
entity: InfraMonitoringEntity;
tableColumns: TableColumnDef<T>[];
fetchListData: (
/** API fetch function for expanded row data */
fetchListData?: (
filters: K8sBaseFilters,
signal?: AbortSignal,
) => Promise<{
data: T[];
type?: 'list' | 'grouped_list';
records?: T[];
data?: T[];
total: number;
error?: string | null;
rawData?: unknown;
warning?: Querybuildertypesv5QueryWarnDataDTO | null;
}>;
/** Extra parts to include in the react-query cache key (e.g., status filter). */
extraQueryKeyParts?: string[];
/** 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. */
@@ -56,14 +68,20 @@ export function K8sExpandedRow<T>({
entity,
tableColumns,
fetchListData,
extraQueryKeyParts = [],
getRowKey,
getItemKey,
}: K8sExpandedRowProps<T>): JSX.Element {
const [, setGroupBy] = useInfraMonitoringGroupBy();
const [, setCurrentPage] = useInfraMonitoringPageListing();
const [queryFilters, setFilters] = useInfraMonitoringFiltersK8s();
const { currentQuery } = useQueryBuilder();
const parentExpression =
currentQuery.builder.queryData[0]?.filter?.expression || '';
const [, setSelectedItem] = useInfraMonitoringSelectedItem();
const [, setMainOrderBy] = useInfraMonitoringOrderBy();
const { safeNavigate } = useSafeNavigate();
const urlQuery = useUrlQuery();
const location = useLocation();
const orderByParamKey = useMemo(
() => `orderBy_${rowKey.replace(/[^a-zA-Z0-9]/g, '_')}`,
@@ -85,35 +103,10 @@ export function K8sExpandedRow<T>({
const storageKey = `k8s-${entity}-columns-expanded`;
const createFiltersForRecord = useCallback((): NonNullable<
IBuilderQuery['filters']
> => {
const baseFilters: IBuilderQuery['filters'] = {
items: [...(queryFilters?.items || [])],
op: 'and',
};
const metaKeys = groupMeta ?? {};
for (const key of Object.keys(metaKeys)) {
const value = metaKeys[key];
// Skip empty values to avoid creating invalid filters
if (value === '' || value === undefined || value === null) {
continue;
}
baseFilters.items.push({
key: {
key,
type: 'resource',
},
op: '=',
value,
id: key,
});
}
return baseFilters;
}, [queryFilters?.items, groupMeta]);
const expressionForRecord = useMemo(
() => buildExpressionFromGroupMeta(parentExpression || '', groupMeta),
[parentExpression, groupMeta],
);
const selectedTime = useGlobalTimeStore((s) => s.selectedTime);
const refreshInterval = useGlobalTimeStore((s) => s.refreshInterval);
@@ -130,8 +123,9 @@ export function K8sExpandedRow<T>({
'k8sExpandedRow',
JSON.stringify(groupMeta),
rowKey,
JSON.stringify(queryFilters),
expressionForRecord,
JSON.stringify(orderBy),
...extraQueryKeyParts,
);
}, [
getAutoRefreshQueryKey,
@@ -139,49 +133,83 @@ export function K8sExpandedRow<T>({
entity,
groupMeta,
rowKey,
queryFilters,
expressionForRecord,
orderBy,
extraQueryKeyParts,
]);
const { data, isLoading, isError } = useQuery({
queryKey,
queryFn: async ({ signal }) => {
if (!fetchListData) {
return { data: [] as T[], total: 0 };
}
const { minTime, maxTime } = getMinMaxTime();
return await fetchListData(
const response = await fetchListData(
{
filter: { expression: expressionForRecord },
limit: EXPANDED_ROW_LIMIT,
offset: 0,
filters: createFiltersForRecord(),
start: Math.floor(minTime / NANO_SECOND_MULTIPLIER),
end: Math.floor(maxTime / NANO_SECOND_MULTIPLIER),
orderBy: orderBy || undefined,
orderBy: orderBy
? { key: { name: orderBy.columnName }, direction: orderBy.order }
: undefined,
groupBy: undefined,
},
signal,
);
return {
data: response.records || response.data || [],
total: response.total,
error: response.error,
};
},
staleTime: 1000 * 60 * 30,
refetchInterval: isRefreshEnabled ? refreshInterval : false,
enabled: !!fetchListData,
});
const expandedData = data?.data ?? [];
const handleRowClick = useCallback(
(_row: T, itemKey: string): void => {
setSelectedItem(itemKey);
void setSelectedItem(itemKey);
},
[setSelectedItem],
);
const handleViewAllClick = (): void => {
const filters = createFiltersForRecord();
setGroupBy([]);
setCurrentPage(1);
setFilters(filters);
void setGroupBy([]);
void setCurrentPage(1);
if (orderBy) {
setMainOrderBy(orderBy);
void setMainOrderBy(orderBy);
}
const updatedQuery = {
...currentQuery,
id: uuid(),
builder: {
...currentQuery.builder,
queryData: [
{
...(currentQuery.builder.queryData[0] || {}),
filter: { expression: expressionForRecord },
filters: { items: [], op: 'AND' as const },
},
],
},
};
const newUrlQuery = new URLSearchParams(urlQuery.toString());
newUrlQuery.set(
QueryParams.compositeQuery,
encodeURIComponent(JSON.stringify(updatedQuery)),
);
safeNavigate(`${location.pathname}?${newUrlQuery.toString()}`);
};
const total = data?.total ?? 0;

View File

@@ -24,11 +24,37 @@
.columnsList {
display: flex;
flex-direction: column;
[data-slot='icon'] {
display: none;
}
[data-slot='entity-group-header'] {
padding-left: 0;
}
[data-slot='column-header'] {
display: flex;
width: auto;
span {
padding: 0;
text-align: left;
}
br {
display: none;
}
}
}
.columnItem {
justify-content: flex-start !important;
width: 100%;
> span {
width: auto !important;
}
}
.horizontalDivider {

View File

@@ -1,4 +1,4 @@
import { useMemo } from 'react';
import { ReactNode, useMemo } from 'react';
import { Button } from '@signozhq/ui/button';
import { DrawerWrapper } from '@signozhq/ui/drawer';
import {
@@ -12,7 +12,7 @@ import styles from './K8sFiltersSidePanel.module.scss';
type ColumnPickerItem = {
id: string;
label: string;
label: ReactNode;
canBeHidden: boolean;
visibilityBehavior:
| 'hidden-on-expand'
@@ -20,15 +20,16 @@ type ColumnPickerItem = {
| 'always-visible';
};
/**
* Converts TableColumnDef to column picker item format
*/
function renderHeader(header: string | (() => ReactNode)): ReactNode {
return typeof header === 'function' ? header() : header;
}
function toColumnPickerItems<T>(
columns: TableColumnDef<T>[],
): ColumnPickerItem[] {
return columns.map((col) => ({
id: col.id,
label: typeof col.header === 'string' ? col.header : col.id,
label: renderHeader(col.header),
canBeHidden: col.canBeHidden !== false && col.enableRemove !== false,
visibilityBehavior: col.visibilityBehavior ?? 'always-visible',
}));

View File

@@ -2,7 +2,7 @@ import { getGroupByEl } from './utils';
import { useInfraMonitoringGroupBy } from '../hooks';
interface K8sEntityWithMeta {
meta?: Record<string, string>;
meta?: Record<string, string> | null;
}
function K8sGroupCell<T extends K8sEntityWithMeta>({

View File

@@ -1,13 +1,14 @@
.k8SListControls {
padding: var(--spacing-4);
display: flex;
justify-content: space-between;
align-items: center;
flex-direction: column;
align-items: stretch;
gap: var(--spacing-4);
width: 100%;
:global(.ant-select-selector) {
border-radius: 2px;
border: 1px solid var(--border) !important;
border: 1px solid var(--l2-border) !important;
background-color: var(--l2-background) !important;
input {
@@ -20,28 +21,81 @@
}
}
.k8SListControlsLeft {
flex: 1;
.k8SListControlsRow {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: var(--spacing-4);
width: 100%;
align-items: center;
justify-content: flex-end;
}
.k8SQbSearchContainer {
flex: 1;
min-width: 240px;
max-width: 60%;
.k8SQbSearchContainer {
width: 100%;
}
.k8SFiltersGroupByRow {
display: flex;
flex: 1 1 auto;
align-items: center;
justify-content: flex-end;
gap: var(--spacing-4);
min-width: 0;
@media (max-width: 1600px) {
flex: 1 1 100%;
order: 9;
}
}
.k8SAttributeSearchContainer {
flex: 1;
min-width: 240px;
max-width: 40%;
flex: 1 1 240px;
max-width: 400px;
display: flex;
align-items: center;
}
.k8SRunButton {
flex: 0 0 auto;
}
.k8SFiltersButton {
flex: 0 0 auto;
}
.k8SDateTimeSelection {
flex: 0 0 auto;
display: flex;
justify-content: flex-end;
--date-time-selector-border-color: var(--l2-border);
@media (max-width: 1200px) {
flex: 1 1 100%;
order: 10;
}
:global(.timeSelection-input) {
height: 32px;
}
:global(.ant-input-affix-wrapper) {
border-color: var(--l2-border);
&:hover {
border-color: var(--l2-border) !important;
}
}
:global(.refresh-actions .ant-form-item-control-input) {
&,
:global(.ant-btn) {
height: 30px;
min-height: unset;
}
}
}
.groupByLabel {
min-width: max-content;
font-size: var(--periscope-font-size-base, 13px);
@@ -50,7 +104,7 @@
letter-spacing: -0.07px;
border-radius: 2px 0px 0px 2px;
border: 1px solid var(--border);
border: 1px solid var(--l2-border);
border-right: none;
border-top-right-radius: 0px;
border-bottom-right-radius: 0px;
@@ -64,6 +118,8 @@
}
.groupBySelect {
width: 100%;
:global(.ant-select-selector) {
border-left: none;
border-top-left-radius: 0px;

View File

@@ -1,167 +1,215 @@
import React, { useCallback, useMemo, useState } from 'react';
import React, { useCallback, useMemo, useRef, useState } from 'react';
import { useLocation } from 'react-router-dom';
import { Button } from '@signozhq/ui/button';
import { Select } from 'antd';
import logEvent from 'api/common/logEvent';
import { useGetFieldsKeys } from 'api/generated/services/fields';
import {
TelemetrytypesFieldContextDTO,
TelemetrytypesSignalDTO,
} from 'api/generated/services/sigNoz.schemas';
import { TableColumnDef } from 'components/TanStackTableView';
import QuerySearch from 'components/QueryBuilderV2/QueryV2/QuerySearch/QuerySearch';
import { InfraMonitoringEvents } from 'constants/events';
import { FeatureKeys } from 'constants/features';
import { initialQueriesMap } from 'constants/queryBuilder';
import QueryBuilderSearch from 'container/QueryBuilder/filters/QueryBuilderSearch';
import { QueryParams } from 'constants/query';
import RunQueryBtn from 'container/QueryBuilder/components/RunQueryBtn/RunQueryBtn';
import DateTimeSelectionV2 from 'container/TopNav/DateTimeSelectionV2';
import { useGetAggregateKeys } from 'hooks/queryBuilder/useGetAggregateKeys';
import { useQueryOperations } from 'hooks/queryBuilder/useQueryBuilderOperations';
import { SlidersHorizontal } from '@signozhq/icons';
import { useAppContext } from 'providers/App/App';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import { useSafeNavigate } from 'hooks/useSafeNavigate';
import { SlidersHorizontal } from '@signozhq/icons';
import { v4 as uuid } from 'uuid';
import {
useGlobalTimeQueryInvalidate,
useGlobalTimeStore,
} from 'store/globalTime';
import { NANO_SECOND_MULTIPLIER } from 'store/globalTime/utils';
import { DataSource } from 'types/common/queryBuilder';
import {
GetK8sEntityToAggregateAttribute,
ENTITY_FILTER_PLACEHOLDERS,
InfraMonitoringEntity,
METRIC_NAMESPACE_BY_ENTITY,
} from '../constants';
import {
useInfraMonitoringFiltersK8s,
useInfraMonitoringGroupBy,
useInfraMonitoringPageListing,
} from '../hooks';
import K8sFiltersSidePanel from './K8sFiltersSidePanel';
import styles from './K8sHeader.module.scss';
import { TooltipSimple } from '@signozhq/ui/tooltip';
interface K8sHeaderProps<TData> {
controlListPrefix?: React.ReactNode;
leftFilters?: React.ReactNode;
entity: InfraMonitoringEntity;
showAutoRefresh: boolean;
columns: TableColumnDef<TData>[];
columnStorageKey: string;
isFetching?: boolean;
cancelQuery: () => void;
}
function K8sHeader<TData>({
controlListPrefix,
leftFilters,
entity,
showAutoRefresh,
columns,
columnStorageKey,
isFetching = false,
cancelQuery,
}: K8sHeaderProps<TData>): JSX.Element {
const [isFiltersSidePanelOpen, setIsFiltersSidePanelOpen] = useState(false);
const [urlFilters, setUrlFilters] = useInfraMonitoringFiltersK8s();
// null = user never touched the search box; fall back to the current
// query expression on Run so an untouched search doesn't wipe filters
const stagedExpressionRef = useRef<string | null>(null);
const currentQuery = initialQueriesMap[DataSource.METRICS];
const { currentQuery } = useQueryBuilder();
const { safeNavigate } = useSafeNavigate();
const location = useLocation();
const invalidateQueries = useGlobalTimeQueryInvalidate();
const updatedCurrentQuery = useMemo(() => {
let { filters } = currentQuery.builder.queryData[0];
if (urlFilters) {
filters = urlFilters;
}
return {
...currentQuery,
builder: {
...currentQuery.builder,
queryData: [
{
...currentQuery.builder.queryData[0],
aggregateOperator: 'noop',
aggregateAttribute: {
...currentQuery.builder.queryData[0].aggregateAttribute,
},
filters,
},
],
},
};
}, [currentQuery, urlFilters]);
const query = useMemo(
() => updatedCurrentQuery?.builder?.queryData[0] || null,
[updatedCurrentQuery],
const queryData = useMemo(
(): IBuilderQuery => ({
...currentQuery.builder.queryData[0],
aggregateOperator: 'noop',
}),
[currentQuery],
);
const { handleChangeQueryData } = useQueryOperations({
index: 0,
query: currentQuery.builder.queryData[0],
entityVersion: '',
});
const [, setCurrentPage] = useInfraMonitoringPageListing();
const handleChangeTagFilters = useCallback(
(value: IBuilderQuery['filters']) => {
setUrlFilters(value || null);
handleChangeQueryData('filters', value);
setCurrentPage(1);
if (value?.items && value?.items?.length > 0) {
logEvent(InfraMonitoringEvents.FilterApplied, {
const handleRunQuery = useCallback(
(newExpression?: string): void => {
const currentExpression =
currentQuery.builder.queryData[0]?.filter?.expression || '';
const finalExpression = newExpression ?? currentExpression;
void setCurrentPage(1);
const updatedQuery = {
...currentQuery,
id: uuid(),
builder: {
...currentQuery.builder,
queryData: currentQuery.builder.queryData.map((query, idx) =>
idx === 0
? {
...query,
filter: { expression: finalExpression || '' },
filters: { items: [], op: 'AND' as const },
}
: query,
),
},
};
// Use window.location.search to get fresh URL params (avoids stale hook state)
const newUrlQuery = new URLSearchParams(window.location.search);
newUrlQuery.set(
QueryParams.compositeQuery,
encodeURIComponent(JSON.stringify(updatedQuery)),
);
safeNavigate(`${location.pathname}?${newUrlQuery.toString()}`);
void invalidateQueries();
if (finalExpression?.trim()) {
void logEvent(InfraMonitoringEvents.FilterApplied, {
entity: InfraMonitoringEvents.K8sEntity,
page: InfraMonitoringEvents.ListPage,
category: InfraMonitoringEvents.Pod,
category: entity,
});
}
},
[handleChangeQueryData, setCurrentPage, setUrlFilters],
[
currentQuery,
safeNavigate,
location.pathname,
setCurrentPage,
entity,
invalidateQueries,
],
);
const { featureFlags } = useAppContext();
const dotMetricsEnabled =
featureFlags?.find((flag) => flag.name === FeatureKeys.DOT_METRICS_ENABLED)
?.active || false;
const handleStageRunQuery = useCallback((): void => {
handleRunQuery(stagedExpressionRef.current ?? undefined);
}, [handleRunQuery]);
const handleExpressionChange = useCallback((value: string): void => {
stagedExpressionRef.current = value;
}, []);
const handleCancelQuery = useCallback((): void => {
cancelQuery();
}, [cancelQuery]);
const getMinMaxTime = useGlobalTimeStore((s) => s.getMinMaxTime);
const { minTime, maxTime } = getMinMaxTime();
const startUnixMilli = Math.floor(minTime / NANO_SECOND_MULTIPLIER);
const endUnixMilli = Math.floor(maxTime / NANO_SECOND_MULTIPLIER);
const { data: groupByFiltersData, isLoading: isLoadingGroupByFilters } =
useGetAggregateKeys(
useGetFieldsKeys(
{
dataSource: currentQuery.builder.queryData[0].dataSource,
aggregateAttribute: GetK8sEntityToAggregateAttribute(
entity,
dotMetricsEnabled,
),
aggregateOperator: 'noop',
searchText: '',
tagType: '',
signal: TelemetrytypesSignalDTO.metrics,
metricNamespace: METRIC_NAMESPACE_BY_ENTITY[entity],
limit: 100,
startUnixMilli,
endUnixMilli,
fieldContext: TelemetrytypesFieldContextDTO.resource,
// the search text is intentionally not included
// searchText: expression,
},
{
queryKey: [currentQuery.builder.queryData[0].dataSource, 'noop'],
query: {
queryKey: ['getFieldsKeys', entity, startUnixMilli, endUnixMilli],
},
},
true,
entity,
);
const flatFieldKeys = useMemo(() => {
const keys = groupByFiltersData?.data?.keys;
if (!keys) {
return [];
}
const allKeys = Object.values(keys).flat();
const seen = new Set<string>();
return allKeys.filter((field) => {
if (seen.has(field.name)) {
return false;
}
seen.add(field.name);
return true;
});
}, [groupByFiltersData]);
const groupByOptions = useMemo(
() =>
groupByFiltersData?.payload?.attributeKeys?.map((filter) => ({
value: filter.key,
label: filter.key,
})) || [],
[groupByFiltersData],
flatFieldKeys.map((field) => ({
value: field.name,
label: field.name,
})),
[flatFieldKeys],
);
const [groupBy, setGroupBy] = useInfraMonitoringGroupBy();
const handleGroupByChange = useCallback(
(value: IBuilderQuery['groupBy']) => {
const newGroupBy = [];
(value: string[]) => {
void setCurrentPage(1);
void setGroupBy(value);
for (let index = 0; index < value.length; index++) {
const element = value[index] as unknown as string;
const key = groupByFiltersData?.payload?.attributeKeys?.find(
(k) => k.key === element,
);
if (key) {
newGroupBy.push(key);
}
}
// Reset pagination on switching to groupBy
setCurrentPage(1);
setGroupBy(newGroupBy);
logEvent(InfraMonitoringEvents.GroupByChanged, {
void logEvent(InfraMonitoringEvents.GroupByChanged, {
entity: InfraMonitoringEvents.K8sEntity,
page: InfraMonitoringEvents.ListPage,
category: InfraMonitoringEvents.Pod,
});
},
[groupByFiltersData, setCurrentPage, setGroupBy],
[setCurrentPage, setGroupBy],
);
const onClickOutside = useCallback(() => {
@@ -170,53 +218,69 @@ function K8sHeader<TData>({
return (
<div className={styles.k8SListControls}>
<div className={styles.k8SListControlsLeft}>
<div className={styles.k8SListControlsRow}>
{controlListPrefix}
<div className={styles.k8SQbSearchContainer}>
<QueryBuilderSearch
query={query as IBuilderQuery}
onChange={handleChangeTagFilters}
isInfraMonitoring
disableNavigationShortcuts
entity={entity}
<div className={styles.k8SFiltersGroupByRow}>
{leftFilters}
<div className={styles.k8SAttributeSearchContainer}>
<div className={styles.groupByLabel}>Group by</div>
<Select
className={styles.groupBySelect}
loading={isLoadingGroupByFilters}
mode="multiple"
value={groupBy}
allowClear
maxTagCount="responsive"
placeholder="Search for attribute"
options={groupByOptions}
onChange={handleGroupByChange}
/>
</div>
</div>
<div className={styles.k8SDateTimeSelection}>
<DateTimeSelectionV2
showAutoRefresh={showAutoRefresh}
showRefreshText={false}
hideShareModal
/>
</div>
<div className={styles.k8SAttributeSearchContainer}>
<div className={styles.groupByLabel}> Group by </div>
<Select
className={styles.groupBySelect}
loading={isLoadingGroupByFilters}
mode="multiple"
value={groupBy}
allowClear
maxTagCount="responsive"
placeholder="Search for attribute"
style={{ width: '100%' }}
options={groupByOptions}
onChange={handleGroupByChange}
/>
</div>
</div>
<div className={styles.k8SListControlsRight}>
<DateTimeSelectionV2
showAutoRefresh={showAutoRefresh}
showRefreshText={false}
hideShareModal
<RunQueryBtn
isLoadingQueries={isFetching}
onStageRunQuery={handleStageRunQuery}
handleCancelQuery={handleCancelQuery}
className={styles.k8SRunButton}
/>
<Button
type="button"
variant="ghost"
size="icon"
color="none"
data-testid="k8s-list-filters-button"
onClick={(): void => setIsFiltersSidePanelOpen(true)}
>
<SlidersHorizontal size={14} />
</Button>
<TooltipSimple title="Click to add more columns to this table">
<Button
type="button"
variant="outlined"
size="icon"
color="secondary"
data-testid="k8s-list-filters-button"
onClick={(): void => setIsFiltersSidePanelOpen(true)}
className={styles.k8SFiltersButton}
>
<SlidersHorizontal size={14} />
</Button>
</TooltipSimple>
</div>
<div className={styles.k8SQbSearchContainer}>
<QuerySearch
queryData={queryData}
dataSource={DataSource.METRICS}
onChange={handleExpressionChange}
onRun={handleRunQuery}
signalSource=""
showFilterSuggestionsWithoutMetric
placeholder={ENTITY_FILTER_PLACEHOLDERS[entity]}
metricNamespace={METRIC_NAMESPACE_BY_ENTITY[entity]}
/>
</div>
<K8sFiltersSidePanel

View File

@@ -0,0 +1,15 @@
.paginationWarning {
border: 1px solid var(--callout-warning-border);
background-color: var(--callout-warning-background);
padding: var(--spacing-2) var(--spacing-4);
display: flex;
justify-content: center;
align-items: center;
gap: var(--spacing-4);
}
.warningIcon {
cursor: pointer;
}

View File

@@ -0,0 +1,37 @@
import { Color } from '@signozhq/design-tokens';
import WarningPopover from 'components/WarningPopover/WarningPopover';
import { Querybuildertypesv5QueryWarnDataDTO } from 'api/generated/services/sigNoz.schemas';
import styles from './K8sPaginationWarning.module.scss';
import TriangleAlert from '@signozhq/icons/TriangleAlert';
export type K8sPaginationWarningProps = {
warning: Querybuildertypesv5QueryWarnDataDTO;
};
export function K8sPaginationWarning({
warning,
}: K8sPaginationWarningProps): JSX.Element {
return (
<span data-testid="k8s-list-warning-popover">
<WarningPopover
warningData={{
code: 'WARNING',
message: warning.message ?? '',
url: warning.url ?? '',
warnings:
warning.warnings?.map((w) => ({ message: w.message ?? '' })) ?? [],
}}
>
<div className={styles.paginationWarning}>
Your data contains some warnings
<TriangleAlert
size={16}
className={styles.warningIcon}
color={Color.BG_AMBER_500}
/>
</div>
</WarningPopover>
</span>
);
}

View File

@@ -22,6 +22,15 @@ import { openInNewTab } from 'utils/navigation';
import { TableColumnDef } from 'components/TanStackTableView';
import { InfraMonitoringEntity } from '../../constants';
window.ResizeObserver =
window.ResizeObserver ||
jest.fn().mockImplementation(() => ({
disconnect: jest.fn(),
observe: jest.fn(),
unobserve: jest.fn(),
}));
import { K8sBaseList, K8sBaseListProps, K8sEntityData } from '../K8sBaseList';
jest.mock('utils/navigation', () => ({
@@ -206,8 +215,10 @@ describe('K8sBaseList', () => {
const itemId2 = Math.random().toString(36).slice(7);
const onUrlUpdateMock = jest.fn<void, [UrlUpdateEvent]>();
const fetchListDataMock = jest.fn<
ReturnType<K8sBaseListProps<TestItemWithTitle>['fetchListData']>,
Parameters<K8sBaseListProps<TestItemWithTitle>['fetchListData']>
ReturnType<
NonNullable<K8sBaseListProps<TestItemWithTitle>['fetchListData']>
>,
Parameters<NonNullable<K8sBaseListProps<TestItemWithTitle>['fetchListData']>>
>();
beforeEach(() => {
@@ -259,7 +270,10 @@ describe('K8sBaseList', () => {
const [filters] = fetchListDataMock.mock.calls[0];
expect(filters.limit).toBe(10);
expect(filters.offset).toBe(0);
expect(filters.filters).toStrictEqual({ items: [], op: 'AND' });
expect(filters.filter).toStrictEqual({
expression: '',
filterByStatus: undefined,
});
expect(filters.groupBy).toBeUndefined();
expect(filters.orderBy).toBeUndefined();
});
@@ -419,11 +433,11 @@ describe('K8sBaseList', () => {
});
});
describe('with URL params (orderBy, groupBy, pagination)', () => {
describe('with URL params (orderBy, groupBy old format, pagination)', () => {
const onUrlUpdateMock = jest.fn<void, [UrlUpdateEvent]>();
const fetchListDataMock = jest.fn<
ReturnType<K8sBaseListProps<TestItem>['fetchListData']>,
Parameters<K8sBaseListProps<TestItem>['fetchListData']>
ReturnType<NonNullable<K8sBaseListProps<TestItem>['fetchListData']>>,
Parameters<NonNullable<K8sBaseListProps<TestItem>['fetchListData']>>
>();
const groupByValue = [
{ key: 'k8s.namespace.name', dataType: 'string', type: 'resource' },
@@ -462,8 +476,11 @@ describe('K8sBaseList', () => {
});
const [filters] = fetchListDataMock.mock.calls[0];
expect(filters.orderBy).toStrictEqual({ columnName: 'cpu', order: 'desc' });
expect(filters.groupBy).toStrictEqual(groupByValue);
expect(filters.orderBy).toStrictEqual({
key: { name: 'cpu' },
direction: 'desc',
});
expect(filters.groupBy).toStrictEqual([{ name: 'k8s.namespace.name' }]);
expect(filters.offset).toBe(20); // (3 - 1) * 10 = 20
expect(filters.limit).toBe(10);
});
@@ -487,14 +504,84 @@ describe('K8sBaseList', () => {
(c) => c[0].groupBy && c[0].groupBy.length > 0,
);
expect(callWithGroupBy).toBeDefined();
expect(callWithGroupBy?.[0].groupBy).toStrictEqual(groupByValue);
expect(callWithGroupBy?.[0].groupBy).toStrictEqual([
{ name: 'k8s.namespace.name' },
]);
});
});
describe('with URL params (groupBy new format - string array)', () => {
const onUrlUpdateMock = jest.fn<void, [UrlUpdateEvent]>();
const fetchListDataMock = jest.fn<
ReturnType<NonNullable<K8sBaseListProps<TestItem>['fetchListData']>>,
Parameters<NonNullable<K8sBaseListProps<TestItem>['fetchListData']>>
>();
const groupByValue = ['k8s.namespace.name'];
beforeEach(() => {
onUrlUpdateMock.mockClear();
fetchListDataMock.mockClear();
fetchListDataMock.mockResolvedValue({
data: [
{ id: 'namespace-default', meta: { 'k8s.namespace.name': 'default' } },
],
total: 50,
error: null,
});
renderComponent<TestItem>({
onUrlUpdate: onUrlUpdateMock,
entity: InfraMonitoringEntity.PODS,
eventCategory: InfraMonitoringEvents.Pod,
fetchListData: fetchListDataMock,
queryParams: {
orderBy: JSON.stringify({ columnName: 'cpu', order: 'desc' }),
groupBy: JSON.stringify(groupByValue),
page: '3',
},
tableColumns: createTestColumns(),
getRowKey: (row): string => row.id,
getItemKey: (row): string => row.id,
});
});
it('should call fetchListData with groupBy from new format URL', async () => {
await waitFor(() => {
expect(fetchListDataMock).toHaveBeenCalled();
});
const [filters] = fetchListDataMock.mock.calls[0];
expect(filters.groupBy).toStrictEqual([{ name: 'k8s.namespace.name' }]);
});
it('should render expand icons when groupBy is set with new format', async () => {
await waitFor(() => {
expect(screen.getByText('namespace-default')).toBeInTheDocument();
});
const expandButtons = screen.getAllByRole('button');
expect(expandButtons.length).toBeGreaterThan(0);
});
it('should render data with new groupBy format', async () => {
await waitFor(() => {
expect(screen.getByText('namespace-default')).toBeInTheDocument();
});
const callWithGroupBy = fetchListDataMock.mock.calls.find(
(c) => c[0].groupBy && c[0].groupBy.length > 0,
);
expect(callWithGroupBy).toBeDefined();
expect(callWithGroupBy?.[0].groupBy).toStrictEqual([
{ name: 'k8s.namespace.name' },
]);
});
});
describe('with empty data', () => {
const fetchListDataMock = jest.fn<
ReturnType<K8sBaseListProps<TestItem>['fetchListData']>,
Parameters<K8sBaseListProps<TestItem>['fetchListData']>
ReturnType<NonNullable<K8sBaseListProps<TestItem>['fetchListData']>>,
Parameters<NonNullable<K8sBaseListProps<TestItem>['fetchListData']>>
>();
beforeEach(() => {
@@ -503,10 +590,6 @@ describe('K8sBaseList', () => {
data: [],
total: 0,
error: null,
rawData: {
sentAnyHostMetricsData: true,
isSendingK8SAgentMetrics: false,
},
});
renderComponent<TestItem>({
@@ -534,8 +617,8 @@ describe('K8sBaseList', () => {
describe('with error response', () => {
const fetchListDataMock = jest.fn<
ReturnType<K8sBaseListProps<TestItem>['fetchListData']>,
Parameters<K8sBaseListProps<TestItem>['fetchListData']>
ReturnType<NonNullable<K8sBaseListProps<TestItem>['fetchListData']>>,
Parameters<NonNullable<K8sBaseListProps<TestItem>['fetchListData']>>
>();
beforeEach(() => {
@@ -569,95 +652,10 @@ describe('K8sBaseList', () => {
});
});
describe('with no metrics data (sentAnyHostMetricsData=false)', () => {
const fetchListDataMock = jest.fn<
ReturnType<K8sBaseListProps<TestItem>['fetchListData']>,
Parameters<K8sBaseListProps<TestItem>['fetchListData']>
>();
beforeEach(() => {
fetchListDataMock.mockClear();
fetchListDataMock.mockResolvedValue({
data: [],
total: 0,
error: null,
rawData: {
sentAnyHostMetricsData: false,
isSendingK8SAgentMetrics: false,
},
});
renderComponent<TestItem>({
entity: InfraMonitoringEntity.PODS,
eventCategory: InfraMonitoringEvents.Pod,
fetchListData: fetchListDataMock,
tableColumns: createTestColumns(),
getRowKey: (row): string => row.id,
getItemKey: (row): string => row.id,
});
});
it('should display no metrics data message', async () => {
await waitFor(() => {
expect(
screen.getByText(/No host metrics data received yet/i),
).toBeInTheDocument();
});
});
it('should display link to documentation', async () => {
await waitFor(() => {
const link = screen.getByRole('link', { name: /our documentation/i });
expect(link).toBeInTheDocument();
expect(link).toHaveAttribute(
'href',
'https://signoz.io/docs/infrastructure-monitoring/hostmetrics/',
);
});
});
});
describe('with incorrect K8s agent metrics (isSendingK8SAgentMetrics=true)', () => {
const fetchListDataMock = jest.fn<
ReturnType<K8sBaseListProps<TestItem>['fetchListData']>,
Parameters<K8sBaseListProps<TestItem>['fetchListData']>
>();
beforeEach(() => {
fetchListDataMock.mockClear();
fetchListDataMock.mockResolvedValue({
data: [],
total: 0,
error: null,
rawData: {
sentAnyHostMetricsData: true,
isSendingK8SAgentMetrics: true,
},
});
renderComponent<TestItem>({
entity: InfraMonitoringEntity.PODS,
eventCategory: InfraMonitoringEvents.Pod,
fetchListData: fetchListDataMock,
tableColumns: createTestColumns(),
getRowKey: (row): string => row.id,
getItemKey: (row): string => row.id,
});
});
it('should display upgrade message', async () => {
await waitFor(() => {
expect(
screen.getByText(/upgrade to the latest version of SigNoz k8s-infra/i),
).toBeInTheDocument();
});
});
});
describe('with end time before retention (endTimeBeforeRetention=true)', () => {
const fetchListDataMock = jest.fn<
ReturnType<K8sBaseListProps<TestItem>['fetchListData']>,
Parameters<K8sBaseListProps<TestItem>['fetchListData']>
ReturnType<NonNullable<K8sBaseListProps<TestItem>['fetchListData']>>,
Parameters<NonNullable<K8sBaseListProps<TestItem>['fetchListData']>>
>();
beforeEach(() => {
@@ -666,11 +664,7 @@ describe('K8sBaseList', () => {
data: [],
total: 0,
error: null,
rawData: {
sentAnyHostMetricsData: true,
isSendingK8SAgentMetrics: false,
endTimeBeforeRetention: true,
},
endTimeBeforeRetention: true,
});
renderComponent<TestItem>({
@@ -697,8 +691,8 @@ describe('K8sBaseList', () => {
describe('column visibility based on TanStack columns', () => {
const fetchListDataMock = jest.fn<
ReturnType<K8sBaseListProps<TestItemWithName>['fetchListData']>,
Parameters<K8sBaseListProps<TestItemWithName>['fetchListData']>
ReturnType<NonNullable<K8sBaseListProps<TestItemWithName>['fetchListData']>>,
Parameters<NonNullable<K8sBaseListProps<TestItemWithName>['fetchListData']>>
>();
beforeEach(() => {
@@ -736,8 +730,10 @@ describe('K8sBaseList', () => {
describe('column behavior with groupBy (expanded/collapsed)', () => {
const fetchListDataMock = jest.fn<
ReturnType<K8sBaseListProps<TestItemWithGroup>['fetchListData']>,
Parameters<K8sBaseListProps<TestItemWithGroup>['fetchListData']>
ReturnType<
NonNullable<K8sBaseListProps<TestItemWithGroup>['fetchListData']>
>,
Parameters<NonNullable<K8sBaseListProps<TestItemWithGroup>['fetchListData']>>
>();
beforeEach(() => {
@@ -807,8 +803,8 @@ describe('K8sBaseList', () => {
describe('column visibility in expanded row (nested table)', () => {
const fetchListDataMock = jest.fn<
ReturnType<K8sBaseListProps<TestItem>['fetchListData']>,
Parameters<K8sBaseListProps<TestItem>['fetchListData']>
ReturnType<NonNullable<K8sBaseListProps<TestItem>['fetchListData']>>,
Parameters<NonNullable<K8sBaseListProps<TestItem>['fetchListData']>>
>();
const groupByValue = [
{ key: 'k8s.namespace.name', dataType: 'string', type: 'resource' },
@@ -852,8 +848,8 @@ describe('K8sBaseList', () => {
describe('TanStack table column rendering', () => {
const fetchListDataMock = jest.fn<
ReturnType<K8sBaseListProps<TestItemWithName>['fetchListData']>,
Parameters<K8sBaseListProps<TestItemWithName>['fetchListData']>
ReturnType<NonNullable<K8sBaseListProps<TestItemWithName>['fetchListData']>>,
Parameters<NonNullable<K8sBaseListProps<TestItemWithName>['fetchListData']>>
>();
beforeEach(() => {
@@ -909,4 +905,40 @@ describe('K8sBaseList', () => {
});
});
});
describe('with warnings from API', () => {
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,
warning: {
message: 'Some data may be incomplete',
url: 'https://docs.example.com/partial-data',
warnings: [{ message: 'Node xyz did not report metrics' }],
},
});
renderComponent<TestItem>({
entity: InfraMonitoringEntity.PODS,
eventCategory: InfraMonitoringEvents.Pod,
fetchListData: fetchListDataMock,
tableColumns: createTestColumns(),
getRowKey: (row): string => row.id,
getItemKey: (row): string => row.id,
});
});
it('should render warning popover in pagination area', async () => {
await waitFor(() => {
expect(screen.getByTestId('k8s-list-warning-popover')).toBeInTheDocument();
});
});
});
});

View File

@@ -1,16 +1,25 @@
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
import { OrderBySchemaType } from '../schemas';
export type K8sBaseFilters = {
filters: TagFilter;
groupBy?: BaseAutocompleteData[];
filter: {
expression: string;
filterByStatus?: 'active' | 'inactive' | '';
};
groupBy?: Array<{ name: string }>;
offset?: number;
limit?: number;
start: number;
end: number;
orderBy?: OrderBySchemaType;
orderBy?: {
key: { name: string };
direction: 'asc' | 'desc';
};
};
export type K8sListResponse<T> = {
type: 'list' | 'grouped_list';
records: T[];
total: number;
endTimeBeforeRetention?: boolean;
error?: string | null;
};
/**

View File

@@ -1,8 +1,8 @@
import { Badge } from '@signozhq/ui/badge';
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import styles from './utils.module.scss';
import { TagFilterItem } from 'types/api/queryBuilder/queryBuilderData';
import { convertFiltersToExpression } from 'components/QueryBuilderV2/utils';
const dotToUnder: Record<string, string> = {
'os.type': 'os_type',
@@ -22,15 +22,13 @@ const dotToUnder: Record<string, string> = {
'k8s.persistentvolumeclaim.name': 'k8s_persistentvolumeclaim_name',
};
export function getGroupedByMeta<T extends { meta?: Record<string, string> }>(
itemData: T,
groupBy: BaseAutocompleteData[],
): Record<string, string> {
export function getGroupedByMeta<
T extends { meta?: Record<string, string> | null },
>(itemData: T, groupBy: string[]): Record<string, string> {
const result: Record<string, string> = {};
const meta = itemData.meta ?? {};
groupBy.forEach((group) => {
const rawKey = group.key as string;
groupBy.forEach((rawKey) => {
const metaKey = (dotToUnder[rawKey] ?? rawKey) as keyof typeof meta;
result[rawKey] = (meta[metaKey] || meta[rawKey]) ?? '';
});
@@ -38,45 +36,13 @@ export function getGroupedByMeta<T extends { meta?: Record<string, string> }>(
return result;
}
export function getRowKey<T extends { meta?: Record<string, string> }>(
itemData: T,
getItemIdentifier: () => string,
groupBy: BaseAutocompleteData[],
): string {
const nodeIdentifier = getItemIdentifier();
const meta = itemData.meta ?? {};
if (groupBy.length === 0) {
return nodeIdentifier || JSON.stringify(meta);
}
const groupedMeta = getGroupedByMeta(itemData, groupBy);
const groupKey = Object.values(groupedMeta).join('-');
if (groupKey && nodeIdentifier) {
return `${groupKey}-${nodeIdentifier}`;
}
if (groupKey) {
return groupKey;
}
if (nodeIdentifier) {
return nodeIdentifier;
}
return JSON.stringify(meta);
}
export function getGroupByEl<T extends { meta?: Record<string, string> }>(
itemData: T,
groupBy: IBuilderQuery['groupBy'],
): React.ReactNode {
export function getGroupByEl<
T extends { meta?: Record<string, string> | null },
>(itemData: T, groupBy: string[]): React.ReactNode {
const groupByValues: string[] = [];
const meta = itemData.meta ?? {};
groupBy.forEach((group) => {
const rawKey = group.key as string;
// Choose mapped key if present, otherwise use rawKey
groupBy.forEach((rawKey) => {
const metaKey = (dotToUnder[rawKey] ?? rawKey) as keyof typeof meta;
const value = meta[metaKey] || meta[rawKey] || '<no-value>';
@@ -98,3 +64,28 @@ export function getGroupByEl<T extends { meta?: Record<string, string> }>(
</div>
);
}
export function buildExpressionFromGroupMeta(
parentExpression: string,
groupMeta: Record<string, string> | undefined,
): string {
const items: TagFilterItem[] = Object.entries(groupMeta ?? {})
.filter(([, value]) => value !== '' && value !== undefined && value !== null)
.map(([key, value]) => ({
key: { key, type: 'resource' },
op: '=',
value,
id: key,
}));
const metaExpression = convertFiltersToExpression({
items,
op: 'AND',
}).expression;
const parent = parentExpression?.trim();
if (parent && metaExpression) {
return `${parent} AND ${metaExpression}`;
}
return parent || metaExpression;
}

View File

@@ -1,21 +1,24 @@
import React, { useCallback } from 'react';
import { useCallback } from 'react';
import { listClusters } from 'api/generated/services/inframonitoring';
import {
InframonitoringtypesClusterRecordDTO,
InframonitoringtypesResponseTypeDTO,
Querybuildertypesv5OrderDirectionDTO,
} from 'api/generated/services/sigNoz.schemas';
import { InfraMonitoringEvents } from 'constants/events';
import { FeatureKeys } from 'constants/features';
import { useAppContext } from 'providers/App/App';
import K8sBaseDetails, { K8sDetailsFilters } from '../Base/K8sBaseDetails';
import { K8sBaseList } from '../Base/K8sBaseList';
import { K8sBaseFilters } from '../Base/types';
import { InfraMonitoringEntity } from '../constants';
import { getK8sClustersList, K8sClusterData } from './api';
import {
clusterWidgetInfo,
getClusterMetricsQueryPayload,
k8sClusterDetailsMetadataConfig,
k8sClusterGetEntityName,
k8sClusterGetSelectedItemFilters,
k8sClusterInitialEventsFilter,
k8sClusterInitialLogTracesFilter,
k8sClusterGetSelectedItemExpression,
k8sClusterInitialEventsExpression,
k8sClusterInitialLogTracesExpression,
} from './constants';
import {
getK8sClusterItemKey,
@@ -28,66 +31,93 @@ function K8sClustersList({
}: {
controlListPrefix?: React.ReactNode;
}): JSX.Element {
const { featureFlags } = useAppContext();
const dotMetricsEnabled =
featureFlags?.find((flag) => flag.name === FeatureKeys.DOT_METRICS_ENABLED)
?.active || false;
const fetchListData = useCallback(
async (filters: K8sBaseFilters, signal?: AbortSignal) => {
filters.orderBy ||= {
columnName: 'cpu',
order: 'desc',
};
try {
const response = await listClusters(
{
filter: { expression: filters.filter.expression },
groupBy: filters.groupBy?.map((g) => ({ name: g.name })),
offset: filters.offset,
limit: filters.limit ?? 10,
start: filters.start,
end: filters.end,
orderBy: filters.orderBy
? {
key: { name: filters.orderBy.key.name },
direction:
filters.orderBy.direction === 'asc'
? Querybuildertypesv5OrderDirectionDTO.asc
: Querybuildertypesv5OrderDirectionDTO.desc,
}
: undefined,
},
signal,
);
const response = await getK8sClustersList(
filters,
signal,
undefined,
dotMetricsEnabled,
);
return {
data: response.payload?.data.records || [],
total: response.payload?.data.total || 0,
error: response.error,
rawData: response.payload?.data,
};
const data = response.data;
return {
type:
data.type === InframonitoringtypesResponseTypeDTO.grouped_list
? ('grouped_list' as const)
: ('list' as const),
records: data.records,
total: data.total,
endTimeBeforeRetention: data.endTimeBeforeRetention,
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,
};
}
},
[dotMetricsEnabled],
[],
);
const fetchEntityData = useCallback(
async (
filters: K8sDetailsFilters,
signal?: AbortSignal,
): Promise<{ data: K8sClusterData | null; error?: string | null }> => {
const response = await getK8sClustersList(
{
filters: filters.filters,
start: filters.start,
end: filters.end,
limit: 1,
offset: 0,
},
signal,
undefined,
dotMetricsEnabled,
);
): Promise<{
data: InframonitoringtypesClusterRecordDTO | null;
error?: string | null;
}> => {
try {
const response = await listClusters(
{
filter: { expression: filters.filter.expression },
start: filters.start,
end: filters.end,
limit: 1,
offset: 0,
},
signal,
);
const records = response.payload?.data.records || [];
return {
data: records.length > 0 ? records[0] : null,
error: response.error,
};
return {
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,
};
}
},
[dotMetricsEnabled],
[],
);
return (
<>
<K8sBaseList<K8sClusterData>
<K8sBaseList<InframonitoringtypesClusterRecordDTO>
controlListPrefix={controlListPrefix}
entity={InfraMonitoringEntity.CLUSTERS}
tableColumns={k8sClustersColumnsConfig}
@@ -97,14 +127,14 @@ function K8sClustersList({
eventCategory={InfraMonitoringEvents.Cluster}
/>
<K8sBaseDetails<K8sClusterData>
<K8sBaseDetails<InframonitoringtypesClusterRecordDTO>
category={InfraMonitoringEntity.CLUSTERS}
eventCategory={InfraMonitoringEvents.Cluster}
getSelectedItemFilters={k8sClusterGetSelectedItemFilters}
getSelectedItemExpression={k8sClusterGetSelectedItemExpression}
fetchEntityData={fetchEntityData}
getEntityName={k8sClusterGetEntityName}
getInitialLogTracesFilters={k8sClusterInitialLogTracesFilter}
getInitialEventsFilters={k8sClusterInitialEventsFilter}
getInitialLogTracesExpression={k8sClusterInitialLogTracesExpression}
getInitialEventsExpression={k8sClusterInitialEventsExpression}
metadataConfig={k8sClusterDetailsMetadataConfig}
entityWidgetInfo={clusterWidgetInfo}
getEntityQueryPayload={getClusterMetricsQueryPayload}

View File

@@ -1,125 +0,0 @@
import axios from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { UnderscoreToDotMap } from 'api/utils';
import { AxiosError } from 'axios';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
import { K8sBaseFilters } from '../Base/types';
export interface K8sClustersListPayload {
filters: TagFilter;
groupBy?: BaseAutocompleteData[];
offset?: number;
limit?: number;
orderBy?: {
columnName: string;
order: 'asc' | 'desc';
};
}
export interface K8sClusterData {
clusterUID: string;
cpuUsage: number;
cpuAllocatable: number;
memoryUsage: number;
memoryAllocatable: number;
meta: {
k8s_cluster_name: string;
k8s_cluster_uid: string;
};
}
export interface K8sClustersListResponse {
status: string;
data: {
type: string;
records: K8sClusterData[];
groups: null;
total: number;
sentAnyHostMetricsData: boolean;
isSendingK8SAgentMetrics: boolean;
};
}
// TODO(H4ad): Erase this whole file when migrating to openapi
export const clustersMetaMap = [
{ dot: 'k8s.cluster.name', under: 'k8s_cluster_name' },
{ dot: 'k8s.cluster.uid', under: 'k8s_cluster_uid' },
] as const;
export function mapClustersMeta(
raw: Record<string, unknown>,
): K8sClusterData['meta'] {
const out: Record<string, unknown> = { ...raw };
clustersMetaMap.forEach(({ dot, under }) => {
if (dot in raw) {
const v = raw[dot];
out[under] = typeof v === 'string' ? v : raw[under];
}
});
return out as K8sClusterData['meta'];
}
export const getK8sClustersList = async (
props: K8sBaseFilters,
signal?: AbortSignal,
headers?: Record<string, string>,
dotMetricsEnabled = false,
): Promise<SuccessResponse<K8sClustersListResponse> | ErrorResponse> => {
try {
const requestProps = dotMetricsEnabled
? {
...props,
filters: {
...props.filters,
items: props.filters.items.reduce<typeof props.filters.items>(
(acc, item) => {
if (item.value === undefined) {
return acc;
}
if (
item.key &&
typeof item.key === 'object' &&
'key' in item.key &&
typeof item.key.key === 'string'
) {
const mappedKey = UnderscoreToDotMap[item.key.key] ?? item.key.key;
acc.push({
...item,
key: { ...item.key, key: mappedKey },
});
} else {
acc.push(item);
}
return acc;
},
[] as typeof props.filters.items,
),
},
}
: props;
const response = await axios.post('/clusters/list', requestProps, {
signal,
headers,
});
const payload: K8sClustersListResponse = response.data;
payload.data.records = payload.data.records.map((record) => ({
...record,
meta: mapClustersMeta(record.meta as Record<string, unknown>),
}));
return {
statusCode: 200,
error: null,
message: 'Success',
payload,
params: requestProps,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);
}
};

View File

@@ -1,53 +1,39 @@
import { InframonitoringtypesClusterRecordDTO } from 'api/generated/services/sigNoz.schemas';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults';
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
import { EQueryType } from 'types/common/dashboard';
import { DataSource, ReduceOperators } from 'types/common/queryBuilder';
import { v4 } from 'uuid';
import {
createFilterItem,
K8sDetailsMetadataConfig,
} from '../Base/K8sBaseDetails';
import { QUERY_KEYS } from '../EntityDetailsUtils/utils';
import { K8sClusterData } from './api';
import { K8sDetailsMetadataConfig } from '../Base/K8sBaseDetails';
import { formatValueForExpression } from 'components/QueryBuilderV2/utils';
import { INFRA_MONITORING_ATTR_KEYS } from '../constants';
export const k8sClusterGetSelectedItemFilters = (
export const k8sClusterGetSelectedItemExpression = (
selectedItemId: string,
): TagFilter => ({
op: 'AND',
items: [
{
id: 'k8s_cluster_name',
key: {
key: 'k8s_cluster_name',
type: null,
},
op: '=',
value: selectedItemId,
},
],
});
): string => `k8s.cluster.name = ${formatValueForExpression(selectedItemId)}`;
export const k8sClusterDetailsMetadataConfig: K8sDetailsMetadataConfig<K8sClusterData>[] =
[{ label: 'Cluster Name', getValue: (p): string => p.meta.k8s_cluster_name }];
export const k8sClusterDetailsMetadataConfig: K8sDetailsMetadataConfig<InframonitoringtypesClusterRecordDTO>[] =
[{ label: 'Cluster Name', getValue: (p): string => p.clusterName || '' }];
export const k8sClusterInitialEventsFilter = (
item: K8sClusterData,
): ReturnType<typeof createFilterItem>[] => [
createFilterItem(QUERY_KEYS.K8S_OBJECT_KIND, 'Cluster'),
createFilterItem(QUERY_KEYS.K8S_OBJECT_NAME, item.meta.k8s_cluster_name),
];
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}`;
};
export const k8sClusterInitialLogTracesFilter = (
item: K8sClusterData,
): ReturnType<typeof createFilterItem>[] => [
createFilterItem(QUERY_KEYS.K8S_CLUSTER_NAME, item.meta.k8s_cluster_name),
];
export const k8sClusterInitialLogTracesExpression = (
item: InframonitoringtypesClusterRecordDTO,
): string => {
const clusterName = formatValueForExpression(item.clusterName || '');
return `${INFRA_MONITORING_ATTR_KEYS.K8S_CLUSTER_NAME} = ${clusterName}`;
};
export const k8sClusterGetEntityName = (item: K8sClusterData): string =>
item.meta.k8s_cluster_name;
export const k8sClusterGetEntityName = (
item: InframonitoringtypesClusterRecordDTO,
): string => item.clusterName || '';
export const clusterWidgetInfo = [
{
@@ -85,7 +71,7 @@ export const clusterWidgetInfo = [
];
export const getClusterMetricsQueryPayload = (
cluster: K8sClusterData,
cluster: InframonitoringtypesClusterRecordDTO,
start: number,
end: number,
dotMetricsEnabled: boolean,
@@ -207,7 +193,7 @@ export const getClusterMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: cluster.meta.k8s_cluster_name,
value: cluster.clusterName,
},
],
op: 'AND',
@@ -246,7 +232,7 @@ export const getClusterMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: cluster.meta.k8s_cluster_name,
value: cluster.clusterName,
},
],
op: 'AND',
@@ -285,7 +271,7 @@ export const getClusterMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: cluster.meta.k8s_cluster_name,
value: cluster.clusterName,
},
],
op: 'AND',
@@ -324,7 +310,7 @@ export const getClusterMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: cluster.meta.k8s_cluster_name,
value: cluster.clusterName,
},
],
op: 'AND',
@@ -397,7 +383,7 @@ export const getClusterMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: cluster.meta.k8s_cluster_name,
value: cluster.clusterName,
},
],
op: 'AND',
@@ -436,7 +422,7 @@ export const getClusterMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: cluster.meta.k8s_cluster_name,
value: cluster.clusterName,
},
],
op: 'AND',
@@ -475,7 +461,7 @@ export const getClusterMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: cluster.meta.k8s_cluster_name,
value: cluster.clusterName,
},
],
op: 'AND',
@@ -514,7 +500,7 @@ export const getClusterMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: cluster.meta.k8s_cluster_name,
value: cluster.clusterName,
},
],
op: 'AND',
@@ -587,7 +573,7 @@ export const getClusterMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: cluster.meta.k8s_cluster_name,
value: cluster.clusterName,
},
],
op: 'AND',
@@ -673,7 +659,7 @@ export const getClusterMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: cluster.meta.k8s_cluster_name,
value: cluster.clusterName,
},
],
op: 'AND',
@@ -759,7 +745,7 @@ export const getClusterMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: cluster.meta.k8s_cluster_name,
value: cluster.clusterName,
},
],
op: 'AND',
@@ -811,7 +797,7 @@ export const getClusterMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: cluster.meta.k8s_cluster_name,
value: cluster.clusterName,
},
],
op: 'AND',
@@ -909,7 +895,7 @@ export const getClusterMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: cluster.meta.k8s_cluster_name,
value: cluster.clusterName,
},
],
op: 'AND',
@@ -961,7 +947,7 @@ export const getClusterMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: cluster.meta.k8s_cluster_name,
value: cluster.clusterName,
},
],
op: 'AND',
@@ -1013,7 +999,7 @@ export const getClusterMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: cluster.meta.k8s_cluster_name,
value: cluster.clusterName,
},
],
op: 'AND',
@@ -1065,7 +1051,7 @@ export const getClusterMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: cluster.meta.k8s_cluster_name,
value: cluster.clusterName,
},
],
op: 'AND',
@@ -1187,7 +1173,7 @@ export const getClusterMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: cluster.meta.k8s_cluster_name,
value: cluster.clusterName,
},
],
op: 'AND',
@@ -1233,7 +1219,7 @@ export const getClusterMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: cluster.meta.k8s_cluster_name,
value: cluster.clusterName,
},
],
op: 'AND',
@@ -1279,7 +1265,7 @@ export const getClusterMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: cluster.meta.k8s_cluster_name,
value: cluster.clusterName,
},
],
op: 'AND',
@@ -1383,7 +1369,7 @@ export const getClusterMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: cluster.meta.k8s_cluster_name,
value: cluster.clusterName,
},
],
op: 'AND',
@@ -1435,7 +1421,7 @@ export const getClusterMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: cluster.meta.k8s_cluster_name,
value: cluster.clusterName,
},
],
op: 'AND',
@@ -1487,7 +1473,7 @@ export const getClusterMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: cluster.meta.k8s_cluster_name,
value: cluster.clusterName,
},
],
op: 'AND',
@@ -1539,7 +1525,7 @@ export const getClusterMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: cluster.meta.k8s_cluster_name,
value: cluster.clusterName,
},
],
op: 'AND',

View File

@@ -1,42 +1,48 @@
import { Tooltip } from 'antd';
import { Color } from '@signozhq/design-tokens';
import { Boxes } from '@signozhq/icons';
import { InframonitoringtypesClusterRecordDTO } from 'api/generated/services/sigNoz.schemas';
import { TableColumnDef } from 'components/TanStackTableView';
import TanStackTable from 'components/TanStackTableView';
import { ExpandButtonWrapper } from 'container/InfraMonitoringK8sV2/components';
import ColumnHeader from '../Base/ColumnHeader';
import EntityGroupHeader from '../Base/EntityGroupHeader';
import K8sGroupCell from '../Base/K8sGroupCell';
import { formatBytes } from '../commonUtils';
import { ValidateColumnValueWrapper } from '../components';
import { InfraMonitoringEntity } from '../constants';
import { K8sClusterData, K8sClustersListPayload } from './api';
import { Boxes } from '@signozhq/icons';
import { formatBytes, getPodPhaseStatusItems } from '../commonUtils';
import {
CellValueTooltip,
GroupedStatusCounts,
ValidateColumnValueWrapper,
} from '../components';
import {
INFRA_MONITORING_ATTR_KEYS,
InfraMonitoringEntity,
} from '../constants';
export function getK8sClusterRowKey(cluster: K8sClusterData): string {
export function getK8sClusterRowKey(
cluster: InframonitoringtypesClusterRecordDTO,
): string {
return (
cluster.clusterUID ||
cluster.meta.k8s_cluster_uid ||
cluster.meta.k8s_cluster_name
cluster.clusterName ||
cluster.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_CLUSTER_UID] ||
''
);
}
export function getK8sClusterItemKey(cluster: K8sClusterData): string {
return cluster.meta.k8s_cluster_name;
export function getK8sClusterItemKey(
cluster: InframonitoringtypesClusterRecordDTO,
): string {
return cluster.clusterName;
}
export const getK8sClustersListQuery = (): K8sClustersListPayload => ({
filters: {
items: [],
op: 'and',
},
orderBy: { columnName: 'cpu', order: 'desc' },
});
export const k8sClustersColumnsConfig: TableColumnDef<K8sClusterData>[] = [
export type ClusterTableColumnConfig =
TableColumnDef<InframonitoringtypesClusterRecordDTO>;
export const k8sClustersColumnsConfig: ClusterTableColumnConfig[] = [
{
id: 'clusterGroup',
header: (): React.ReactNode => <EntityGroupHeader title="CLUSTER GROUP" />,
accessorFn: (row): string => row.meta.k8s_cluster_name || '',
width: { min: 300 },
header: (): React.ReactNode => <EntityGroupHeader title="Cluster Group" />,
accessorFn: (row): string => row.clusterName || '',
width: { min: 290 },
enableSort: false,
enableRemove: false,
enableMove: false,
@@ -59,9 +65,10 @@ export const k8sClustersColumnsConfig: TableColumnDef<K8sClusterData>[] = [
<EntityGroupHeader
title="Cluster Name"
icon={<Boxes data-hide-expanded="true" size={14} />}
docPath="/infrastructure-monitoring/kubernetes/clusters#cluster-name"
/>
),
accessorFn: (row): string => row.meta.k8s_cluster_name || '',
accessorFn: (row): string => row.clusterName || '',
width: { min: 290 },
enableSort: false,
enableRemove: false,
@@ -71,55 +78,127 @@ export const k8sClustersColumnsConfig: TableColumnDef<K8sClusterData>[] = [
cell: ({ value }): React.ReactNode => {
const clusterName = value as string;
return (
<Tooltip title={clusterName}>
<CellValueTooltip value={clusterName}>
<TanStackTable.Text>{clusterName}</TanStackTable.Text>
</Tooltip>
</CellValueTooltip>
);
},
},
{
id: 'nodeCountsByReadiness',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/clusters#node-readiness">
Node Readiness
</ColumnHeader>
),
accessorFn: (
row,
): InframonitoringtypesClusterRecordDTO['nodeCountsByReadiness'] =>
row.nodeCountsByReadiness,
width: { min: 180 },
enableSort: false,
cell: ({ row }): React.ReactNode => {
if (!row.nodeCountsByReadiness) {
return <TanStackTable.Text>-</TanStackTable.Text>;
}
return (
<GroupedStatusCounts
items={[
{
value: row.nodeCountsByReadiness.ready,
label: 'Ready',
color: Color.BG_FOREST_500,
},
{
value: row.nodeCountsByReadiness.notReady,
label: 'Not Ready',
color: Color.BG_AMBER_500,
},
]}
/>
);
},
},
{
id: 'podCountsByPhase',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/clusters#pod-counts-by-phase">
Pod Phases
</ColumnHeader>
),
accessorFn: (row): InframonitoringtypesClusterRecordDTO['podCountsByPhase'] =>
row.podCountsByPhase,
width: { min: 250 },
enableSort: false,
cell: ({ row }): React.ReactNode => {
const podCountsByPhase = row.podCountsByPhase;
if (!podCountsByPhase) {
return <TanStackTable.Text>-</TanStackTable.Text>;
}
return (
<GroupedStatusCounts items={getPodPhaseStatusItems(row.podCountsByPhase)} />
);
},
},
{
id: 'cpu',
header: 'CPU Usage (cores)',
accessorFn: (row): number => row.cpuUsage,
width: { min: 220 },
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/clusters#cpu-usage-cores">
CPU Usage
<br /> (cores)
</ColumnHeader>
),
accessorFn: (row): number => row.clusterCPU,
width: { min: 160 },
enableSort: true,
cell: ({ value }): React.ReactNode => {
const cpu = value as number;
const cpu = Number(value);
return (
<ValidateColumnValueWrapper
value={cpu}
entity={InfraMonitoringEntity.CLUSTERS}
attribute="CPU metric"
>
<TanStackTable.Text>{cpu}</TanStackTable.Text>
<TanStackTable.Text>{cpu.toFixed(2)}</TanStackTable.Text>
</ValidateColumnValueWrapper>
);
},
},
{
id: 'cpu_allocatable',
header: 'CPU Alloc (cores)',
accessorFn: (row): number => row.cpuAllocatable,
width: { min: 220 },
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/clusters#cpu-alloc-cores">
CPU Allocatable
<br /> (cores)
</ColumnHeader>
),
accessorFn: (row): number => row.clusterCPUAllocatable,
width: { min: 200 },
enableSort: true,
cell: ({ value }): React.ReactNode => {
const cpuAllocatable = value as number;
const cpuAllocatable = Number(value);
return (
<ValidateColumnValueWrapper
value={cpuAllocatable}
entity={InfraMonitoringEntity.CLUSTERS}
attribute="CPU allocatable metric"
>
<TanStackTable.Text>{cpuAllocatable}</TanStackTable.Text>
<TanStackTable.Text>{cpuAllocatable.toFixed(2)}</TanStackTable.Text>
</ValidateColumnValueWrapper>
);
},
},
{
id: 'memory',
header: 'Memory Usage (WSS)',
accessorFn: (row): number => row.memoryUsage,
width: { min: 220 },
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/clusters#memory-usage-wss">
Memory Usage
<br /> (WSS)
</ColumnHeader>
),
accessorFn: (row): number => row.clusterMemory,
width: { min: 180 },
enableSort: true,
cell: ({ value }): React.ReactNode => {
const memory = value as number;
@@ -136,9 +215,14 @@ export const k8sClustersColumnsConfig: TableColumnDef<K8sClusterData>[] = [
},
{
id: 'memory_allocatable',
header: 'Memory Allocatable',
accessorFn: (row): number => row.memoryAllocatable,
width: { min: 220 },
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/clusters#memory-allocatable">
Memory
<br /> Allocatable
</ColumnHeader>
),
accessorFn: (row): number => row.clusterMemoryAllocatable,
width: { min: 180 },
enableSort: true,
cell: ({ value }): React.ReactNode => {
const memoryAllocatable = value as number;

View File

@@ -1,93 +1,117 @@
import React, { useCallback } from 'react';
import { useCallback } from 'react';
import { listDaemonSets } from 'api/generated/services/inframonitoring';
import {
InframonitoringtypesDaemonSetRecordDTO,
InframonitoringtypesResponseTypeDTO,
Querybuildertypesv5OrderDirectionDTO,
} from 'api/generated/services/sigNoz.schemas';
import { InfraMonitoringEvents } from 'constants/events';
import { FeatureKeys } from 'constants/features';
import { useAppContext } from 'providers/App/App';
import K8sBaseDetails, { K8sDetailsFilters } from '../Base/K8sBaseDetails';
import { K8sBaseList } from '../Base/K8sBaseList';
import { K8sBaseFilters } from '../Base/types';
import { InfraMonitoringEntity } from '../constants';
import { getK8sDaemonSetsList, K8sDaemonSetsData } from './api';
import {
daemonSetWidgetInfo,
getDaemonSetMetricsQueryPayload,
k8sDaemonSetDetailsMetadataConfig,
k8sDaemonSetGetEntityName,
k8sDaemonSetGetSelectedItemFilters,
k8sDaemonSetInitialEventsFilter,
k8sDaemonSetInitialLogTracesFilter,
k8sDaemonSetGetSelectedItemExpression,
k8sDaemonSetInitialEventsExpression,
k8sDaemonSetInitialLogTracesExpression,
} from './constants';
import {
getK8sDaemonSetItemKey,
getK8sDaemonSetRowKey,
k8sDaemonSetsColumnsConfig,
} from './table.config';
function K8sDaemonSetsList({
controlListPrefix,
}: {
controlListPrefix?: React.ReactNode;
}): JSX.Element {
const { featureFlags } = useAppContext();
const dotMetricsEnabled =
featureFlags?.find((flag) => flag.name === FeatureKeys.DOT_METRICS_ENABLED)
?.active || false;
const fetchListData = useCallback(
async (filters: K8sBaseFilters, signal?: AbortSignal) => {
filters.orderBy ||= {
columnName: 'cpu',
order: 'desc',
};
const response = await getK8sDaemonSetsList(
filters,
signal,
undefined,
dotMetricsEnabled,
);
return {
data: response.payload?.data.records || [],
total: response.payload?.data.total || 0,
error: response.error,
rawData: response.payload?.data,
};
try {
const response = await listDaemonSets(
{
filter: { expression: filters.filter.expression },
groupBy: filters.groupBy?.map((g) => ({ name: g.name })),
offset: filters.offset,
limit: filters.limit ?? 10,
start: filters.start,
end: filters.end,
orderBy: filters.orderBy
? {
key: { name: filters.orderBy.key.name },
direction:
filters.orderBy.direction === 'asc'
? Querybuildertypesv5OrderDirectionDTO.asc
: Querybuildertypesv5OrderDirectionDTO.desc,
}
: undefined,
},
signal,
);
const data = response.data;
return {
type:
data.type === InframonitoringtypesResponseTypeDTO.grouped_list
? ('grouped_list' as const)
: ('list' as const),
records: data.records,
total: data.total,
endTimeBeforeRetention: data.endTimeBeforeRetention,
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,
};
}
},
[dotMetricsEnabled],
[],
);
const fetchEntityData = useCallback(
async (
filters: K8sDetailsFilters,
signal?: AbortSignal,
): Promise<{ data: K8sDaemonSetsData | null; error?: string | null }> => {
const response = await getK8sDaemonSetsList(
{
filters: filters.filters,
start: filters.start,
end: filters.end,
limit: 1,
offset: 0,
},
signal,
undefined,
dotMetricsEnabled,
);
const records = response.payload?.data.records || [];
return {
data: records.length > 0 ? records[0] : null,
error: response.error,
};
): Promise<{
data: InframonitoringtypesDaemonSetRecordDTO | null;
error?: string | null;
}> => {
try {
const response = await listDaemonSets(
{
filter: { expression: filters.filter.expression },
start: filters.start,
end: filters.end,
limit: 1,
offset: 0,
},
signal,
);
return {
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,
};
}
},
[dotMetricsEnabled],
[],
);
return (
<>
<K8sBaseList<K8sDaemonSetsData>
<K8sBaseList<InframonitoringtypesDaemonSetRecordDTO>
controlListPrefix={controlListPrefix}
entity={InfraMonitoringEntity.DAEMONSETS}
tableColumns={k8sDaemonSetsColumnsConfig}
@@ -96,15 +120,14 @@ function K8sDaemonSetsList({
getItemKey={getK8sDaemonSetItemKey}
eventCategory={InfraMonitoringEvents.DaemonSet}
/>
<K8sBaseDetails<K8sDaemonSetsData>
<K8sBaseDetails<InframonitoringtypesDaemonSetRecordDTO>
category={InfraMonitoringEntity.DAEMONSETS}
eventCategory={InfraMonitoringEvents.DaemonSet}
getSelectedItemFilters={k8sDaemonSetGetSelectedItemFilters}
getSelectedItemExpression={k8sDaemonSetGetSelectedItemExpression}
fetchEntityData={fetchEntityData}
getEntityName={k8sDaemonSetGetEntityName}
getInitialLogTracesFilters={k8sDaemonSetInitialLogTracesFilter}
getInitialEventsFilters={k8sDaemonSetInitialEventsFilter}
getInitialLogTracesExpression={k8sDaemonSetInitialLogTracesExpression}
getInitialEventsExpression={k8sDaemonSetInitialEventsExpression}
metadataConfig={k8sDaemonSetDetailsMetadataConfig}
entityWidgetInfo={daemonSetWidgetInfo}
getEntityQueryPayload={getDaemonSetMetricsQueryPayload}
@@ -113,5 +136,4 @@ function K8sDaemonSetsList({
</>
);
}
export default K8sDaemonSetsList;

View File

@@ -1,119 +0,0 @@
import axios from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { UnderscoreToDotMap } from 'api/utils';
import { AxiosError } from 'axios';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { K8sBaseFilters } from '../Base/types';
export interface K8sDaemonSetsData {
daemonSetName: string;
cpuUsage: number;
memoryUsage: number;
cpuRequest: number;
memoryRequest: number;
cpuLimit: number;
memoryLimit: number;
restarts: number;
desiredNodes: number;
availableNodes: number;
meta: {
k8s_cluster_name: string;
k8s_daemonset_name: string;
k8s_namespace_name: string;
};
}
export interface K8sDaemonSetsListResponse {
status: string;
data: {
type: string;
records: K8sDaemonSetsData[];
groups: null;
total: number;
sentAnyHostMetricsData: boolean;
isSendingK8SAgentMetrics: boolean;
};
}
// TODO(H4ad): Erase this whole file when migrating to openapi
export const daemonSetsMetaMap = [
{ dot: 'k8s.namespace.name', under: 'k8s_namespace_name' },
{ dot: 'k8s.daemonset.name', under: 'k8s_daemonset_name' },
{ dot: 'k8s.cluster.name', under: 'k8s_cluster_name' },
] as const;
export function mapDaemonSetsMeta(
raw: Record<string, unknown>,
): K8sDaemonSetsData['meta'] {
const out: Record<string, unknown> = { ...raw };
daemonSetsMetaMap.forEach(({ dot, under }) => {
if (dot in raw) {
const v = raw[dot];
out[under] = typeof v === 'string' ? v : raw[under];
}
});
return out as K8sDaemonSetsData['meta'];
}
export const getK8sDaemonSetsList = async (
props: K8sBaseFilters,
signal?: AbortSignal,
headers?: Record<string, string>,
dotMetricsEnabled = false,
): Promise<SuccessResponse<K8sDaemonSetsListResponse> | ErrorResponse> => {
try {
const requestProps = dotMetricsEnabled
? {
...props,
filters: {
...props.filters,
items: props.filters.items.reduce<typeof props.filters.items>(
(acc, item) => {
if (item.value === undefined) {
return acc;
}
if (
item.key &&
typeof item.key === 'object' &&
'key' in item.key &&
typeof item.key.key === 'string'
) {
const mappedKey = UnderscoreToDotMap[item.key.key] ?? item.key.key;
acc.push({
...item,
key: { ...item.key, key: mappedKey },
});
} else {
acc.push(item);
}
return acc;
},
[] as typeof props.filters.items,
),
},
}
: props;
const response = await axios.post('/daemonsets/list', requestProps, {
signal,
headers,
});
const payload: K8sDaemonSetsListResponse = response.data;
payload.data.records = payload.data.records.map((record) => ({
...record,
meta: mapDaemonSetsMeta(record.meta as Record<string, unknown>),
}));
return {
statusCode: 200,
error: null,
message: 'Success',
payload,
params: requestProps,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);
}
};

View File

@@ -1,67 +1,52 @@
import { InframonitoringtypesDaemonSetRecordDTO } from 'api/generated/services/sigNoz.schemas';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults';
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
import { EQueryType } from 'types/common/dashboard';
import { DataSource, ReduceOperators } from 'types/common/queryBuilder';
import { v4 } from 'uuid';
import {
createFilterItem,
K8sDetailsMetadataConfig,
} from '../Base/K8sBaseDetails';
import { QUERY_KEYS } from '../EntityDetailsUtils/utils';
import { K8sDaemonSetsData } from './api';
import { K8sDetailsMetadataConfig } from '../Base/K8sBaseDetails';
import { formatValueForExpression } from 'components/QueryBuilderV2/utils';
import { INFRA_MONITORING_ATTR_KEYS } from '../constants';
export const k8sDaemonSetGetSelectedItemFilters = (
export const k8sDaemonSetGetSelectedItemExpression = (
selectedItemId: string,
): TagFilter => ({
op: 'AND',
items: [
{
id: 'k8s_daemonset_name',
key: {
key: 'k8s_daemonset_name',
type: null,
},
op: '=',
value: selectedItemId,
},
],
});
): string =>
`${INFRA_MONITORING_ATTR_KEYS.K8S_DAEMONSET_NAME} = ${formatValueForExpression(selectedItemId)}`;
export const k8sDaemonSetDetailsMetadataConfig: K8sDetailsMetadataConfig<K8sDaemonSetsData>[] =
export const k8sDaemonSetDetailsMetadataConfig: K8sDetailsMetadataConfig<InframonitoringtypesDaemonSetRecordDTO>[] =
[
{
label: 'Daemonset Name',
getValue: (p): string => p.meta.k8s_daemonset_name,
getValue: (p): string =>
p.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_DAEMONSET_NAME] ?? '',
},
{
label: 'Cluster Name',
getValue: (p): string => p.meta.k8s_cluster_name,
getValue: (p): string =>
p.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_CLUSTER_NAME] ?? '',
},
{
label: 'Namespace Name',
getValue: (p): string => p.meta.k8s_namespace_name,
getValue: (p): string =>
p.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] ?? '',
},
];
export const k8sDaemonSetInitialEventsFilter = (
item: K8sDaemonSetsData,
): ReturnType<typeof createFilterItem>[] => [
createFilterItem(QUERY_KEYS.K8S_OBJECT_KIND, 'DaemonSet'),
createFilterItem(QUERY_KEYS.K8S_OBJECT_NAME, item.meta.k8s_daemonset_name),
];
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] ?? '')}`;
export const k8sDaemonSetInitialLogTracesFilter = (
item: K8sDaemonSetsData,
): ReturnType<typeof createFilterItem>[] => [
createFilterItem(QUERY_KEYS.K8S_DAEMON_SET_NAME, item.meta.k8s_daemonset_name),
createFilterItem(QUERY_KEYS.K8S_NAMESPACE_NAME, item.meta.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] ?? '')}`;
export const k8sDaemonSetGetEntityName = (item: K8sDaemonSetsData): string =>
item.meta.k8s_daemonset_name;
export const k8sDaemonSetGetEntityName = (
item: InframonitoringtypesDaemonSetRecordDTO,
): string => item.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_DAEMONSET_NAME] ?? '';
export const daemonSetWidgetInfo = [
{
@@ -83,7 +68,7 @@ export const daemonSetWidgetInfo = [
];
export const getDaemonSetMetricsQueryPayload = (
daemonSet: K8sDaemonSetsData,
daemonSet: InframonitoringtypesDaemonSetRecordDTO,
start: number,
end: number,
dotMetricsEnabled: boolean,
@@ -159,7 +144,9 @@ export const getDaemonSetMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: daemonSet.meta.k8s_daemonset_name,
value:
daemonSet.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_DAEMONSET_NAME] ??
'',
},
{
id: '47b3adae',
@@ -170,7 +157,9 @@ export const getDaemonSetMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: daemonSet.meta.k8s_namespace_name,
value:
daemonSet.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] ??
'',
},
],
op: 'AND',
@@ -209,7 +198,9 @@ export const getDaemonSetMetricsQueryPayload = (
type: 'tag',
},
op: 'contains',
value: daemonSet.meta.k8s_daemonset_name,
value:
daemonSet.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_DAEMONSET_NAME] ??
'',
},
{
id: '47b3adae',
@@ -220,7 +211,9 @@ export const getDaemonSetMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: daemonSet.meta.k8s_namespace_name,
value:
daemonSet.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] ??
'',
},
],
op: 'AND',
@@ -259,7 +252,9 @@ export const getDaemonSetMetricsQueryPayload = (
type: 'tag',
},
op: 'contains',
value: daemonSet.meta.k8s_daemonset_name,
value:
daemonSet.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_DAEMONSET_NAME] ??
'',
},
{
id: '47b3adae',
@@ -270,7 +265,9 @@ export const getDaemonSetMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: daemonSet.meta.k8s_namespace_name,
value:
daemonSet.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] ??
'',
},
],
op: 'AND',
@@ -343,7 +340,9 @@ export const getDaemonSetMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: daemonSet.meta.k8s_daemonset_name,
value:
daemonSet.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_DAEMONSET_NAME] ??
'',
},
{
id: '47b3adae',
@@ -354,7 +353,9 @@ export const getDaemonSetMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: daemonSet.meta.k8s_namespace_name,
value:
daemonSet.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] ??
'',
},
],
op: 'AND',
@@ -393,7 +394,9 @@ export const getDaemonSetMetricsQueryPayload = (
type: 'tag',
},
op: 'contains',
value: daemonSet.meta.k8s_daemonset_name,
value:
daemonSet.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_DAEMONSET_NAME] ??
'',
},
{
id: '47b3adae',
@@ -404,7 +407,9 @@ export const getDaemonSetMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: daemonSet.meta.k8s_namespace_name,
value:
daemonSet.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] ??
'',
},
],
op: 'AND',
@@ -443,7 +448,9 @@ export const getDaemonSetMetricsQueryPayload = (
type: 'tag',
},
op: 'contains',
value: daemonSet.meta.k8s_daemonset_name,
value:
daemonSet.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_DAEMONSET_NAME] ??
'',
},
{
id: '47b3adae',
@@ -454,7 +461,9 @@ export const getDaemonSetMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: daemonSet.meta.k8s_namespace_name,
value:
daemonSet.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] ??
'',
},
],
op: 'AND',
@@ -527,7 +536,9 @@ export const getDaemonSetMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: daemonSet.meta.k8s_daemonset_name,
value:
daemonSet.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_DAEMONSET_NAME] ??
'',
},
{
id: '47b3adae',
@@ -538,7 +549,9 @@ export const getDaemonSetMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: daemonSet.meta.k8s_namespace_name,
value:
daemonSet.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] ??
'',
},
],
op: 'AND',
@@ -624,7 +637,9 @@ export const getDaemonSetMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: daemonSet.meta.k8s_daemonset_name,
value:
daemonSet.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_DAEMONSET_NAME] ??
'',
},
{
id: '47b3adae',
@@ -635,7 +650,9 @@ export const getDaemonSetMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: daemonSet.meta.k8s_namespace_name,
value:
daemonSet.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] ??
'',
},
],
op: 'AND',

View File

@@ -1,34 +1,49 @@
import { Tooltip } from 'antd';
import { TableColumnDef } from 'components/TanStackTableView';
import TanStackTable from 'components/TanStackTableView';
import { Color } from '@signozhq/design-tokens';
import { InframonitoringtypesDaemonSetRecordDTO } from 'api/generated/services/sigNoz.schemas';
import TanStackTable, { TableColumnDef } from 'components/TanStackTableView';
import { ExpandButtonWrapper } from 'container/InfraMonitoringK8sV2/components';
import ColumnHeader from '../Base/ColumnHeader';
import EntityGroupHeader from '../Base/EntityGroupHeader';
import K8sGroupCell from '../Base/K8sGroupCell';
import { formatBytes } from '../commonUtils';
import { EntityProgressBar, ValidateColumnValueWrapper } from '../components';
import { InfraMonitoringEntity } from '../constants';
import { K8sDaemonSetsData } from './api';
import { formatBytes, getPodPhaseStatusItems } from '../commonUtils';
import {
CellValueTooltip,
EntityProgressBar,
GroupedStatusCounts,
ValidateColumnValueWrapper,
} from '../components';
import {
INFRA_MONITORING_ATTR_KEYS,
InfraMonitoringEntity,
} from '../constants';
import { Group } from '@signozhq/icons';
export function getK8sDaemonSetRowKey(daemonSet: K8sDaemonSetsData): string {
export function getK8sDaemonSetRowKey(
daemonSet: InframonitoringtypesDaemonSetRecordDTO,
): string {
return (
daemonSet.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_DAEMONSET_NAME] ||
daemonSet.daemonSetName ||
daemonSet.meta.k8s_daemonset_name ||
`${daemonSet.meta.k8s_namespace_name}-${daemonSet.meta.k8s_daemonset_name}`
`${daemonSet.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME]}-${daemonSet.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_DAEMONSET_NAME]}`
);
}
export function getK8sDaemonSetItemKey(daemonSet: K8sDaemonSetsData): string {
return daemonSet.meta.k8s_daemonset_name;
export function getK8sDaemonSetItemKey(
daemonSet: InframonitoringtypesDaemonSetRecordDTO,
): string {
return daemonSet.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_DAEMONSET_NAME] || '';
}
export const k8sDaemonSetsColumnsConfig: TableColumnDef<K8sDaemonSetsData>[] = [
export type DaemonSetTableColumnConfig =
TableColumnDef<InframonitoringtypesDaemonSetRecordDTO>;
export const k8sDaemonSetsColumnsConfig: DaemonSetTableColumnConfig[] = [
{
id: 'daemonSetGroup',
header: (): React.ReactNode => <EntityGroupHeader title="DAEMONSET GROUP" />,
accessorFn: (row): string => row.meta.k8s_daemonset_name || '',
width: { min: 300 },
header: (): React.ReactNode => <EntityGroupHeader title="DaemonSet Group" />,
accessorFn: (row): string =>
row.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_DAEMONSET_NAME] || '',
width: { min: 290 },
enableSort: false,
enableRemove: false,
enableMove: false,
@@ -51,9 +66,11 @@ export const k8sDaemonSetsColumnsConfig: TableColumnDef<K8sDaemonSetsData>[] = [
<EntityGroupHeader
title="DaemonSet Name"
icon={<Group data-hide-expanded="true" size={14} />}
docPath="/infrastructure-monitoring/kubernetes/daemonsets#daemonset-name"
/>
),
accessorFn: (row): string => row.meta.k8s_daemonset_name || '',
accessorFn: (row): string =>
row.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_DAEMONSET_NAME] || '',
width: { min: 290 },
enableSort: false,
enableRemove: false,
@@ -63,71 +80,98 @@ export const k8sDaemonSetsColumnsConfig: TableColumnDef<K8sDaemonSetsData>[] = [
cell: ({ value }): React.ReactNode => {
const daemonsetName = value as string;
return (
<Tooltip title={daemonsetName}>
<CellValueTooltip value={daemonsetName}>
<TanStackTable.Text>{daemonsetName}</TanStackTable.Text>
</Tooltip>
</CellValueTooltip>
);
},
},
{
id: 'namespaceName',
header: 'Namespace Name',
accessorFn: (row): string => row.meta.k8s_namespace_name || '',
width: { default: 100 },
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/daemonsets#namespace-name">
Namespace
</ColumnHeader>
),
accessorFn: (row): string =>
row.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] || '',
width: { min: 160 },
enableSort: false,
enableResize: true,
cell: ({ value }): React.ReactNode => {
const namespaceName = value as string;
return (
<Tooltip title={namespaceName}>
<CellValueTooltip value={namespaceName}>
<TanStackTable.Text>{namespaceName}</TanStackTable.Text>
</Tooltip>
</CellValueTooltip>
);
},
},
{
id: 'available_nodes',
header: 'Available',
accessorFn: (row): number => row.availableNodes,
width: { min: 140 },
enableSort: true,
cell: ({ value }): React.ReactNode => {
const availableNodes = value as number;
id: 'pod_counts_by_phase',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/daemonsets#pod-counts-by-phase">
Pod Phases
</ColumnHeader>
),
accessorFn: (
row,
): InframonitoringtypesDaemonSetRecordDTO['podCountsByPhase'] =>
row.podCountsByPhase,
width: { min: 250 },
enableSort: false,
enableResize: true,
cell: ({ row }): React.ReactNode => {
const podCountsByPhase = row.podCountsByPhase;
if (!podCountsByPhase) {
return <TanStackTable.Text>-</TanStackTable.Text>;
}
return (
<ValidateColumnValueWrapper
value={availableNodes}
entity={InfraMonitoringEntity.DAEMONSETS}
attribute="available node"
>
<TanStackTable.Text>{availableNodes}</TanStackTable.Text>
</ValidateColumnValueWrapper>
<GroupedStatusCounts items={getPodPhaseStatusItems(podCountsByPhase)} />
);
},
},
{
id: 'desired_nodes',
header: 'Desired',
accessorFn: (row): number => row.desiredNodes,
width: { min: 140 },
enableSort: true,
cell: ({ value }): React.ReactNode => {
const desiredNodes = value as number;
return (
<ValidateColumnValueWrapper
value={desiredNodes}
entity={InfraMonitoringEntity.DAEMONSETS}
attribute="desired node"
>
<TanStackTable.Text>{desiredNodes}</TanStackTable.Text>
</ValidateColumnValueWrapper>
);
},
id: 'node_status',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/daemonsets#node-status">
Node Status
</ColumnHeader>
),
accessorFn: (row): number => row.currentNodes,
width: { min: 180 },
enableSort: false,
enableResize: true,
cell: ({ row }): React.ReactNode => (
<GroupedStatusCounts
items={[
{
value: row.currentNodes,
label: 'Current',
color: Color.BG_FOREST_500,
},
{
value: row.desiredNodes,
label: 'Desired',
color: Color.BG_ROBIN_500,
},
]}
/>
),
},
{
id: 'cpu_request',
header: 'CPU Req Usage (%)',
accessorFn: (row): number => row.cpuRequest,
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/daemonsets#cpu-req-usage-">
CPU Request
<br /> Usage (%)
</ColumnHeader>
),
accessorFn: (row): number => row.daemonSetCPURequest,
width: { min: 200, default: 200 },
enableSort: true,
enableResize: true,
defaultVisibility: false,
cell: ({ value }): React.ReactNode => {
const cpuRequest = value as number;
return (
@@ -143,10 +187,16 @@ export const k8sDaemonSetsColumnsConfig: TableColumnDef<K8sDaemonSetsData>[] = [
},
{
id: 'cpu_limit',
header: 'CPU Limit Usage (%)',
accessorFn: (row): number => row.cpuLimit,
width: { min: 200, default: 200 },
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/daemonsets#cpu-limit-usage-">
CPU Limit
<br /> Usage (%)
</ColumnHeader>
),
accessorFn: (row): number => row.daemonSetCPULimit,
width: { min: 160 },
enableSort: true,
enableResize: true,
cell: ({ value }): React.ReactNode => {
const cpuLimit = value as number;
return (
@@ -162,30 +212,42 @@ export const k8sDaemonSetsColumnsConfig: TableColumnDef<K8sDaemonSetsData>[] = [
},
{
id: 'cpu',
header: 'CPU Usage (cores)',
accessorFn: (row): number => row.cpuUsage,
width: { min: 190 },
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/daemonsets#cpu-usage-cores">
CPU Usage
<br /> (cores)
</ColumnHeader>
),
accessorFn: (row): number => row.daemonSetCPU,
width: { min: 160 },
enableSort: true,
enableResize: true,
defaultVisibility: false,
cell: ({ value }): React.ReactNode => {
const cpu = value as number;
const cpu = Number(value);
return (
<ValidateColumnValueWrapper
value={cpu}
entity={InfraMonitoringEntity.DAEMONSETS}
attribute="CPU metric"
>
<TanStackTable.Text>{cpu}</TanStackTable.Text>
<TanStackTable.Text>{cpu.toFixed(2)}</TanStackTable.Text>
</ValidateColumnValueWrapper>
);
},
},
{
id: 'memory_request',
header: 'Mem Req Usage (%)',
accessorFn: (row): number => row.memoryRequest,
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/daemonsets#mem-req-usage-">
Memory Request
<br /> Usage (%)
</ColumnHeader>
),
accessorFn: (row): number => row.daemonSetMemoryRequest,
width: { min: 190 },
enableSort: true,
enableResize: true,
defaultVisibility: false,
cell: ({ value }): React.ReactNode => {
const memoryRequest = value as number;
@@ -202,10 +264,16 @@ export const k8sDaemonSetsColumnsConfig: TableColumnDef<K8sDaemonSetsData>[] = [
},
{
id: 'memory_limit',
header: 'Mem Limit Usage (%)',
accessorFn: (row): number => row.memoryLimit,
width: { min: 180 },
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/daemonsets#mem-limit-usage-">
Memory Limit
<br /> Usage (%)
</ColumnHeader>
),
accessorFn: (row): number => row.daemonSetMemoryLimit,
width: { min: 190 },
enableSort: true,
enableResize: true,
cell: ({ value }): React.ReactNode => {
const memoryLimit = value as number;
return (
@@ -221,10 +289,16 @@ export const k8sDaemonSetsColumnsConfig: TableColumnDef<K8sDaemonSetsData>[] = [
},
{
id: 'memory',
header: 'Mem Usage (WSS)',
accessorFn: (row): number => row.memoryUsage,
width: { min: 160 },
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/daemonsets#mem-usage-wss">
Memory Usage
<br /> (WSS)
</ColumnHeader>
),
accessorFn: (row): number => row.daemonSetMemory,
width: { min: 180 },
enableSort: true,
enableResize: true,
cell: ({ value }): React.ReactNode => {
const memory = value as number;
return (
@@ -238,4 +312,52 @@ export const k8sDaemonSetsColumnsConfig: TableColumnDef<K8sDaemonSetsData>[] = [
);
},
},
{
id: 'current_nodes',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/daemonsets#current">
Current Nodes
</ColumnHeader>
),
accessorFn: (row): number => row.currentNodes,
width: { min: 140 },
enableSort: true,
defaultVisibility: false,
cell: ({ value }): React.ReactNode => {
const currentNodes = value as number;
return (
<ValidateColumnValueWrapper
value={currentNodes}
entity={InfraMonitoringEntity.DAEMONSETS}
attribute="current node"
>
<TanStackTable.Text>{currentNodes}</TanStackTable.Text>
</ValidateColumnValueWrapper>
);
},
},
{
id: 'desired_nodes',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/daemonsets#desired">
Desired Nodes
</ColumnHeader>
),
accessorFn: (row): number => row.desiredNodes,
width: { min: 140 },
enableSort: true,
defaultVisibility: false,
cell: ({ value }): React.ReactNode => {
const desiredNodes = value as number;
return (
<ValidateColumnValueWrapper
value={desiredNodes}
entity={InfraMonitoringEntity.DAEMONSETS}
attribute="desired node"
>
<TanStackTable.Text>{desiredNodes}</TanStackTable.Text>
</ValidateColumnValueWrapper>
);
},
},
];

View File

@@ -1,21 +1,24 @@
import React, { useCallback } from 'react';
import { useCallback } from 'react';
import { listDeployments } from 'api/generated/services/inframonitoring';
import {
InframonitoringtypesDeploymentRecordDTO,
InframonitoringtypesResponseTypeDTO,
Querybuildertypesv5OrderDirectionDTO,
} from 'api/generated/services/sigNoz.schemas';
import { InfraMonitoringEvents } from 'constants/events';
import { FeatureKeys } from 'constants/features';
import { useAppContext } from 'providers/App/App';
import K8sBaseDetails, { K8sDetailsFilters } from '../Base/K8sBaseDetails';
import { K8sBaseList } from '../Base/K8sBaseList';
import { K8sBaseFilters } from '../Base/types';
import { InfraMonitoringEntity } from '../constants';
import { getK8sDeploymentsList, K8sDeploymentsData } from './api';
import {
deploymentWidgetInfo,
getDeploymentMetricsQueryPayload,
k8sDeploymentDetailsMetadataConfig,
k8sDeploymentGetEntityName,
k8sDeploymentGetSelectedItemFilters,
k8sDeploymentInitialEventsFilter,
k8sDeploymentInitialLogTracesFilter,
k8sDeploymentGetSelectedItemExpression,
k8sDeploymentInitialEventsExpression,
k8sDeploymentInitialLogTracesExpression,
} from './constants';
import {
getK8sDeploymentItemKey,
@@ -28,66 +31,93 @@ function K8sDeploymentsList({
}: {
controlListPrefix?: React.ReactNode;
}): JSX.Element {
const { featureFlags } = useAppContext();
const dotMetricsEnabled =
featureFlags?.find((flag) => flag.name === FeatureKeys.DOT_METRICS_ENABLED)
?.active || false;
const fetchListData = useCallback(
async (filters: K8sBaseFilters, signal?: AbortSignal) => {
filters.orderBy ||= {
columnName: 'cpu',
order: 'desc',
};
try {
const response = await listDeployments(
{
filter: { expression: filters.filter.expression },
groupBy: filters.groupBy?.map((g) => ({ name: g.name })),
offset: filters.offset,
limit: filters.limit ?? 10,
start: filters.start,
end: filters.end,
orderBy: filters.orderBy
? {
key: { name: filters.orderBy.key.name },
direction:
filters.orderBy.direction === 'asc'
? Querybuildertypesv5OrderDirectionDTO.asc
: Querybuildertypesv5OrderDirectionDTO.desc,
}
: undefined,
},
signal,
);
const response = await getK8sDeploymentsList(
filters,
signal,
undefined,
dotMetricsEnabled,
);
return {
data: response.payload?.data.records || [],
total: response.payload?.data.total || 0,
error: response.error,
rawData: response.payload?.data,
};
const data = response.data;
return {
type:
data.type === InframonitoringtypesResponseTypeDTO.grouped_list
? ('grouped_list' as const)
: ('list' as const),
records: data.records,
total: data.total,
endTimeBeforeRetention: data.endTimeBeforeRetention,
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,
};
}
},
[dotMetricsEnabled],
[],
);
const fetchEntityData = useCallback(
async (
filters: K8sDetailsFilters,
signal?: AbortSignal,
): Promise<{ data: K8sDeploymentsData | null; error?: string | null }> => {
const response = await getK8sDeploymentsList(
{
filters: filters.filters,
start: filters.start,
end: filters.end,
limit: 1,
offset: 0,
},
signal,
undefined,
dotMetricsEnabled,
);
): Promise<{
data: InframonitoringtypesDeploymentRecordDTO | null;
error?: string | null;
}> => {
try {
const response = await listDeployments(
{
filter: { expression: filters.filter.expression },
start: filters.start,
end: filters.end,
limit: 1,
offset: 0,
},
signal,
);
const records = response.payload?.data.records || [];
return {
data: records.length > 0 ? records[0] : null,
error: response.error,
};
return {
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,
};
}
},
[dotMetricsEnabled],
[],
);
return (
<>
<K8sBaseList<K8sDeploymentsData>
<K8sBaseList<InframonitoringtypesDeploymentRecordDTO>
controlListPrefix={controlListPrefix}
entity={InfraMonitoringEntity.DEPLOYMENTS}
tableColumns={k8sDeploymentsColumnsConfig}
@@ -97,14 +127,14 @@ function K8sDeploymentsList({
eventCategory={InfraMonitoringEvents.Deployment}
/>
<K8sBaseDetails<K8sDeploymentsData>
<K8sBaseDetails<InframonitoringtypesDeploymentRecordDTO>
category={InfraMonitoringEntity.DEPLOYMENTS}
eventCategory={InfraMonitoringEvents.Deployment}
getSelectedItemFilters={k8sDeploymentGetSelectedItemFilters}
getSelectedItemExpression={k8sDeploymentGetSelectedItemExpression}
fetchEntityData={fetchEntityData}
getEntityName={k8sDeploymentGetEntityName}
getInitialLogTracesFilters={k8sDeploymentInitialLogTracesFilter}
getInitialEventsFilters={k8sDeploymentInitialEventsFilter}
getInitialLogTracesExpression={k8sDeploymentInitialLogTracesExpression}
getInitialEventsExpression={k8sDeploymentInitialEventsExpression}
metadataConfig={k8sDeploymentDetailsMetadataConfig}
entityWidgetInfo={deploymentWidgetInfo}
getEntityQueryPayload={getDeploymentMetricsQueryPayload}

View File

@@ -1,132 +0,0 @@
import axios from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { UnderscoreToDotMap } from 'api/utils';
import { AxiosError } from 'axios';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
import { K8sBaseFilters } from '../Base/types';
export interface K8sDeploymentsListPayload {
filters: TagFilter;
groupBy?: BaseAutocompleteData[];
offset?: number;
limit?: number;
orderBy?: {
columnName: string;
order: 'asc' | 'desc';
};
}
export interface K8sDeploymentsData {
deploymentName: string;
cpuUsage: number;
memoryUsage: number;
desiredPods: number;
availablePods: number;
cpuRequest: number;
memoryRequest: number;
cpuLimit: number;
memoryLimit: number;
restarts: number;
meta: {
k8s_cluster_name: string;
k8s_deployment_name: string;
k8s_namespace_name: string;
};
}
export interface K8sDeploymentsListResponse {
status: string;
data: {
type: string;
records: K8sDeploymentsData[];
groups: null;
total: number;
sentAnyHostMetricsData: boolean;
isSendingK8SAgentMetrics: boolean;
};
}
// TODO(H4ad): Erase this whole file when migrating to openapi
export const deploymentsMetaMap = [
{ dot: 'k8s.cluster.name', under: 'k8s_cluster_name' },
{ dot: 'k8s.deployment.name', under: 'k8s_deployment_name' },
{ dot: 'k8s.namespace.name', under: 'k8s_namespace_name' },
] as const;
export function mapDeploymentsMeta(
raw: Record<string, unknown>,
): K8sDeploymentsData['meta'] {
const out: Record<string, unknown> = { ...raw };
deploymentsMetaMap.forEach(({ dot, under }) => {
if (dot in raw) {
const v = raw[dot];
out[under] = typeof v === 'string' ? v : raw[under];
}
});
return out as K8sDeploymentsData['meta'];
}
export const getK8sDeploymentsList = async (
props: K8sBaseFilters,
signal?: AbortSignal,
headers?: Record<string, string>,
dotMetricsEnabled = false,
): Promise<SuccessResponse<K8sDeploymentsListResponse> | ErrorResponse> => {
try {
const requestProps = dotMetricsEnabled
? {
...props,
filters: {
...props.filters,
items: props.filters.items.reduce<typeof props.filters.items>(
(acc, item) => {
if (item.value === undefined) {
return acc;
}
if (
item.key &&
typeof item.key === 'object' &&
'key' in item.key &&
typeof item.key.key === 'string'
) {
const mappedKey = UnderscoreToDotMap[item.key.key] ?? item.key.key;
acc.push({
...item,
key: { ...item.key, key: mappedKey },
});
} else {
acc.push(item);
}
return acc;
},
[] as typeof props.filters.items,
),
},
}
: props;
const response = await axios.post('/deployments/list', requestProps, {
signal,
headers,
});
const payload: K8sDeploymentsListResponse = response.data;
payload.data.records = payload.data.records.map((record) => ({
...record,
meta: mapDeploymentsMeta(record.meta as Record<string, unknown>),
}));
return {
statusCode: 200,
error: null,
message: 'Success',
payload,
params: requestProps,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);
}
};

View File

@@ -1,70 +1,63 @@
import { InframonitoringtypesDeploymentRecordDTO } from 'api/generated/services/sigNoz.schemas';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults';
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
import { EQueryType } from 'types/common/dashboard';
import { DataSource, ReduceOperators } from 'types/common/queryBuilder';
import { v4 } from 'uuid';
import {
createFilterItem,
K8sDetailsMetadataConfig,
} from '../Base/K8sBaseDetails';
import { QUERY_KEYS } from '../EntityDetailsUtils/utils';
import { K8sDeploymentsData } from './api';
import { K8sDetailsMetadataConfig } from '../Base/K8sBaseDetails';
import { formatValueForExpression } from 'components/QueryBuilderV2/utils';
import { INFRA_MONITORING_ATTR_KEYS } from '../constants';
export const k8sDeploymentGetSelectedItemFilters = (
export const k8sDeploymentGetSelectedItemExpression = (
selectedItemId: string,
): TagFilter => ({
op: 'AND',
items: [
{
id: 'k8s_deployment_name',
key: {
key: 'k8s_deployment_name',
type: null,
},
op: '=',
value: selectedItemId,
},
],
});
): string =>
`${INFRA_MONITORING_ATTR_KEYS.K8S_DEPLOYMENT_NAME} = ${formatValueForExpression(selectedItemId)}`;
export const k8sDeploymentDetailsMetadataConfig: K8sDetailsMetadataConfig<K8sDeploymentsData>[] =
export const k8sDeploymentDetailsMetadataConfig: K8sDetailsMetadataConfig<InframonitoringtypesDeploymentRecordDTO>[] =
[
{
label: 'Deployment Name',
getValue: (p): string => p.meta.k8s_deployment_name,
getValue: (p): string =>
p.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_DEPLOYMENT_NAME] ?? '',
},
{
label: 'Cluster Name',
getValue: (p): string => p.meta.k8s_cluster_name,
getValue: (p): string =>
p.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_CLUSTER_NAME] ?? '',
},
{
label: 'Namespace Name',
getValue: (p): string => p.meta.k8s_namespace_name,
getValue: (p): string =>
p.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] ?? '',
},
];
export const k8sDeploymentInitialEventsFilter = (
item: K8sDeploymentsData,
): ReturnType<typeof createFilterItem>[] => [
createFilterItem(QUERY_KEYS.K8S_OBJECT_KIND, 'Deployment'),
createFilterItem(QUERY_KEYS.K8S_OBJECT_NAME, item.meta.k8s_deployment_name),
];
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}`;
};
export const k8sDeploymentInitialLogTracesFilter = (
item: K8sDeploymentsData,
): ReturnType<typeof createFilterItem>[] => [
createFilterItem(
QUERY_KEYS.K8S_DEPLOYMENT_NAME,
item.meta.k8s_deployment_name,
),
createFilterItem(QUERY_KEYS.K8S_NAMESPACE_NAME, item.meta.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}`;
};
export const k8sDeploymentGetEntityName = (item: K8sDeploymentsData): string =>
item.meta.k8s_deployment_name;
export const k8sDeploymentGetEntityName = (
item: InframonitoringtypesDeploymentRecordDTO,
): string => item.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_DEPLOYMENT_NAME] ?? '';
export const deploymentWidgetInfo = [
{
@@ -86,7 +79,7 @@ export const deploymentWidgetInfo = [
];
export const getDeploymentMetricsQueryPayload = (
deployment: K8sDeploymentsData,
deployment: InframonitoringtypesDeploymentRecordDTO,
start: number,
end: number,
dotMetricsEnabled: boolean,
@@ -158,7 +151,9 @@ export const getDeploymentMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: deployment.meta.k8s_deployment_name,
value:
deployment.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_DEPLOYMENT_NAME] ??
'',
},
],
op: 'AND',
@@ -197,7 +192,9 @@ export const getDeploymentMetricsQueryPayload = (
type: 'tag',
},
op: 'contains',
value: deployment.meta.k8s_deployment_name,
value:
deployment.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_DEPLOYMENT_NAME] ??
'',
},
],
op: 'AND',
@@ -236,7 +233,9 @@ export const getDeploymentMetricsQueryPayload = (
type: 'tag',
},
op: 'contains',
value: deployment.meta.k8s_deployment_name,
value:
deployment.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_DEPLOYMENT_NAME] ??
'',
},
],
op: 'AND',
@@ -309,7 +308,9 @@ export const getDeploymentMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: deployment.meta.k8s_deployment_name,
value:
deployment.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_DEPLOYMENT_NAME] ??
'',
},
],
op: 'AND',
@@ -348,7 +349,9 @@ export const getDeploymentMetricsQueryPayload = (
type: 'tag',
},
op: 'contains',
value: deployment.meta.k8s_deployment_name,
value:
deployment.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_DEPLOYMENT_NAME] ??
'',
},
],
op: 'AND',
@@ -387,7 +390,9 @@ export const getDeploymentMetricsQueryPayload = (
type: 'tag',
},
op: 'contains',
value: deployment.meta.k8s_deployment_name,
value:
deployment.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_DEPLOYMENT_NAME] ??
'',
},
],
op: 'AND',
@@ -460,7 +465,9 @@ export const getDeploymentMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: deployment.meta.k8s_deployment_name,
value:
deployment.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_DEPLOYMENT_NAME] ??
'',
},
],
op: 'AND',
@@ -546,7 +553,9 @@ export const getDeploymentMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: deployment.meta.k8s_deployment_name,
value:
deployment.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_DEPLOYMENT_NAME] ??
'',
},
],
op: 'AND',

View File

@@ -1,35 +1,50 @@
import { Tooltip } from 'antd';
import { TableColumnDef } from 'components/TanStackTableView';
import TanStackTable from 'components/TanStackTableView';
import { Color } from '@signozhq/design-tokens';
import { InframonitoringtypesDeploymentRecordDTO } from 'api/generated/services/sigNoz.schemas';
import TanStackTable, { TableColumnDef } from 'components/TanStackTableView';
import { ExpandButtonWrapper } from 'container/InfraMonitoringK8sV2/components';
import ColumnHeader from '../Base/ColumnHeader';
import EntityGroupHeader from '../Base/EntityGroupHeader';
import K8sGroupCell from '../Base/K8sGroupCell';
import { formatBytes } from '../commonUtils';
import { EntityProgressBar, ValidateColumnValueWrapper } from '../components';
import { InfraMonitoringEntity } from '../constants';
import { K8sDeploymentsData } from './api';
import { formatBytes, getPodPhaseStatusItems } from '../commonUtils';
import {
CellValueTooltip,
EntityProgressBar,
GroupedStatusCounts,
ValidateColumnValueWrapper,
} from '../components';
import {
INFRA_MONITORING_ATTR_KEYS,
InfraMonitoringEntity,
} from '../constants';
import { Computer } from '@signozhq/icons';
export function getK8sDeploymentRowKey(deployment: K8sDeploymentsData): string {
return deployment.meta.k8s_deployment_name || deployment.deploymentName;
export function getK8sDeploymentRowKey(
deployment: InframonitoringtypesDeploymentRecordDTO,
): string {
return (
deployment.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_DEPLOYMENT_NAME] ||
deployment.deploymentName ||
''
);
}
export function getK8sDeploymentItemKey(
deployment: K8sDeploymentsData,
deployment: InframonitoringtypesDeploymentRecordDTO,
): string {
return deployment.meta.k8s_deployment_name;
return deployment.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_DEPLOYMENT_NAME] || '';
}
export const k8sDeploymentsColumnsConfig: TableColumnDef<K8sDeploymentsData>[] =
export const k8sDeploymentsColumnsConfig: TableColumnDef<InframonitoringtypesDeploymentRecordDTO>[] =
[
{
id: 'deploymentGroup',
header: (): React.ReactNode => (
<EntityGroupHeader title="DEPLOYMENT GROUP" />
<EntityGroupHeader title="Deployment Group" />
),
accessorFn: (row): string => row.meta.k8s_deployment_name || '',
width: { min: 220 },
accessorFn: (row): string =>
row.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_DEPLOYMENT_NAME] || '',
width: { min: 290 },
enableSort: false,
enableRemove: false,
enableMove: false,
@@ -52,10 +67,12 @@ export const k8sDeploymentsColumnsConfig: TableColumnDef<K8sDeploymentsData>[] =
<EntityGroupHeader
title="Deployment Name"
icon={<Computer data-hide-expanded="true" size={14} />}
docPath="/infrastructure-monitoring/kubernetes/deployments#deployment-name"
/>
),
accessorFn: (row): string => row.meta.k8s_deployment_name || '',
width: { min: 210 },
accessorFn: (row): string =>
row.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_DEPLOYMENT_NAME] || '',
width: { min: 290 },
enableSort: false,
enableRemove: false,
enableMove: false,
@@ -64,17 +81,22 @@ export const k8sDeploymentsColumnsConfig: TableColumnDef<K8sDeploymentsData>[] =
cell: ({ value }): React.ReactNode => {
const deploymentName = value as string;
return (
<Tooltip title={deploymentName}>
<CellValueTooltip value={deploymentName}>
<TanStackTable.Text>{deploymentName}</TanStackTable.Text>
</Tooltip>
</CellValueTooltip>
);
},
},
{
id: 'namespaceName',
header: 'Namespace Name',
accessorFn: (row): string => row.meta.k8s_namespace_name || '',
width: { default: 220 },
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/deployments#namespace-name">
Namespace
</ColumnHeader>
),
accessorFn: (row): string =>
row.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] || '',
width: { min: 160 },
enableSort: false,
enableResize: true,
cell: ({ value }): React.ReactNode => (
@@ -82,12 +104,216 @@ export const k8sDeploymentsColumnsConfig: TableColumnDef<K8sDeploymentsData>[] =
),
},
{
id: 'available_pods',
header: 'Available',
accessorFn: (row): number => row.availablePods,
width: { min: 100 },
id: 'podCountsByPhase',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/deployments#pod-counts-by-phase">
Pod Phases
</ColumnHeader>
),
accessorFn: (row): object | undefined => row.podCountsByPhase,
width: { min: 250 },
enableSort: false,
enableResize: true,
cell: ({ row }): React.ReactNode => {
const podCountsByPhase = row.podCountsByPhase;
if (!podCountsByPhase) {
return <TanStackTable.Text>-</TanStackTable.Text>;
}
return (
<GroupedStatusCounts items={getPodPhaseStatusItems(podCountsByPhase)} />
);
},
},
{
id: 'replica_status',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/deployments#replica-status">
Replica Status
</ColumnHeader>
),
accessorFn: (row): number => row.availablePods,
width: { min: 180 },
enableSort: false,
enableResize: true,
cell: ({ row }): React.ReactNode => (
<GroupedStatusCounts
items={[
{
value: row.availablePods,
label: 'Available',
color: Color.BG_FOREST_500,
},
{
value: row.desiredPods,
label: 'Desired',
color: Color.BG_ROBIN_500,
},
]}
/>
),
},
{
id: 'cpu_request',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/deployments#cpu-req-usage-">
CPU Request
<br /> Usage (%)
</ColumnHeader>
),
accessorFn: (row): number => row.deploymentCPURequest,
width: { min: 210 },
enableSort: true,
enableResize: true,
defaultVisibility: false,
cell: ({ value }): React.ReactNode => {
const cpuRequest = value as number;
return (
<ValidateColumnValueWrapper
value={cpuRequest}
entity={InfraMonitoringEntity.DEPLOYMENTS}
attribute="CPU Request"
>
<EntityProgressBar value={cpuRequest} type="request" />
</ValidateColumnValueWrapper>
);
},
},
{
id: 'cpu_limit',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/deployments#cpu-limit-usage-">
CPU Limit
<br /> Usage (%)
</ColumnHeader>
),
accessorFn: (row): number => row.deploymentCPULimit,
width: { min: 210 },
enableSort: true,
enableResize: true,
cell: ({ value }): React.ReactNode => {
const cpuLimit = Number(value);
return (
<ValidateColumnValueWrapper
value={cpuLimit}
entity={InfraMonitoringEntity.DEPLOYMENTS}
attribute="CPU Limit"
>
<EntityProgressBar value={cpuLimit} type="limit" />
</ValidateColumnValueWrapper>
);
},
},
{
id: 'cpu',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/deployments#cpu-usage-cores">
CPU Usage
<br /> (cores)
</ColumnHeader>
),
accessorFn: (row): number => row.deploymentCPU,
width: { min: 160 },
enableSort: true,
enableResize: true,
cell: ({ value }): React.ReactNode => {
const cpu = Number(value);
return (
<ValidateColumnValueWrapper
value={cpu}
entity={InfraMonitoringEntity.DEPLOYMENTS}
attribute="CPU metric"
>
<TanStackTable.Text>{cpu.toFixed(2)}</TanStackTable.Text>
</ValidateColumnValueWrapper>
);
},
},
{
id: 'memory_request',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/deployments#mem-req-usage-">
Memory Request
<br /> Usage (%)
</ColumnHeader>
),
accessorFn: (row): number => row.deploymentMemoryRequest,
width: { min: 210 },
enableSort: true,
enableResize: true,
defaultVisibility: false,
cell: ({ value }): React.ReactNode => {
const memoryRequest = Number(value);
return (
<ValidateColumnValueWrapper
value={memoryRequest}
entity={InfraMonitoringEntity.DEPLOYMENTS}
attribute="Memory Request"
>
<EntityProgressBar value={memoryRequest} type="request" />
</ValidateColumnValueWrapper>
);
},
},
{
id: 'memory_limit',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/deployments#mem-limit-usage-">
Memory Limit
<br /> Usage (%)
</ColumnHeader>
),
accessorFn: (row): number => row.deploymentMemoryLimit,
width: { min: 210 },
enableSort: true,
enableResize: true,
cell: ({ value }): React.ReactNode => {
const memoryLimit = Number(value);
return (
<ValidateColumnValueWrapper
value={memoryLimit}
entity={InfraMonitoringEntity.DEPLOYMENTS}
attribute="Memory Limit"
>
<EntityProgressBar value={memoryLimit} type="limit" />
</ValidateColumnValueWrapper>
);
},
},
{
id: 'memory',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/deployments#mem-usage-wss">
Memory Usage
<br /> (WSS)
</ColumnHeader>
),
accessorFn: (row): number => row.deploymentMemory,
width: { min: 180 },
enableSort: true,
enableResize: true,
cell: ({ value }): React.ReactNode => {
const memory = value as number;
return (
<ValidateColumnValueWrapper
value={memory}
entity={InfraMonitoringEntity.DEPLOYMENTS}
attribute="memory metric"
>
<TanStackTable.Text>{formatBytes(memory)}</TanStackTable.Text>
</ValidateColumnValueWrapper>
);
},
},
{
id: 'available_pods',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/deployments#available">
Available Pods
</ColumnHeader>
),
accessorFn: (row): number => row.availablePods,
width: { min: 120 },
enableSort: true,
defaultVisibility: false,
cell: ({ value }): React.ReactNode => {
const availablePods = value as number;
@@ -104,11 +330,14 @@ export const k8sDeploymentsColumnsConfig: TableColumnDef<K8sDeploymentsData>[] =
},
{
id: 'desired_pods',
header: 'Desired',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/deployments#desired">
Desired Pods
</ColumnHeader>
),
accessorFn: (row): number => row.desiredPods,
width: { min: 80 },
enableSort: false,
enableResize: true,
width: { min: 120 },
enableSort: true,
defaultVisibility: false,
cell: ({ value }): React.ReactNode => {
const desiredPods = value as number;
@@ -123,124 +352,4 @@ export const k8sDeploymentsColumnsConfig: TableColumnDef<K8sDeploymentsData>[] =
);
},
},
{
id: 'cpu_request',
header: 'CPU Req Usage (%)',
accessorFn: (row): number => row.cpuRequest,
width: { min: 210 },
enableSort: true,
enableResize: true,
cell: ({ value }): React.ReactNode => {
const cpuRequest = value as number;
return (
<ValidateColumnValueWrapper
value={cpuRequest}
entity={InfraMonitoringEntity.DEPLOYMENTS}
attribute="CPU Request"
>
<EntityProgressBar value={cpuRequest} type="request" />
</ValidateColumnValueWrapper>
);
},
},
{
id: 'cpu_limit',
header: 'CPU Limit Usage (%)',
accessorFn: (row): number => row.cpuLimit,
width: { min: 210 },
enableSort: true,
enableResize: true,
cell: ({ value }): React.ReactNode => {
const cpuLimit = value as number;
return (
<ValidateColumnValueWrapper
value={cpuLimit}
entity={InfraMonitoringEntity.DEPLOYMENTS}
attribute="CPU Limit"
>
<EntityProgressBar value={cpuLimit} type="limit" />
</ValidateColumnValueWrapper>
);
},
},
{
id: 'cpu',
header: 'CPU Usage (cores)',
accessorFn: (row): number => row.cpuUsage,
width: { min: 210 },
enableSort: true,
enableResize: true,
cell: ({ value }): React.ReactNode => {
const cpu = value as number;
return (
<ValidateColumnValueWrapper
value={cpu}
entity={InfraMonitoringEntity.DEPLOYMENTS}
attribute="CPU metric"
>
<TanStackTable.Text>{cpu}</TanStackTable.Text>
</ValidateColumnValueWrapper>
);
},
},
{
id: 'memory_request',
header: 'Mem Req Usage (%)',
accessorFn: (row): number => row.memoryRequest,
width: { min: 210 },
enableSort: true,
enableResize: true,
cell: ({ value }): React.ReactNode => {
const memoryRequest = value as number;
return (
<ValidateColumnValueWrapper
value={memoryRequest}
entity={InfraMonitoringEntity.DEPLOYMENTS}
attribute="Memory Request"
>
<EntityProgressBar value={memoryRequest} type="request" />
</ValidateColumnValueWrapper>
);
},
},
{
id: 'memory_limit',
header: 'Mem Limit Usage (%)',
accessorFn: (row): number => row.memoryLimit,
width: { min: 210 },
enableSort: true,
enableResize: true,
cell: ({ value }): React.ReactNode => {
const memoryLimit = value as number;
return (
<ValidateColumnValueWrapper
value={memoryLimit}
entity={InfraMonitoringEntity.DEPLOYMENTS}
attribute="Memory Limit"
>
<EntityProgressBar value={memoryLimit} type="limit" />
</ValidateColumnValueWrapper>
);
},
},
{
id: 'memory',
header: 'Mem Usage (WSS)',
accessorFn: (row): number => row.memoryUsage,
width: { min: 140 },
enableSort: true,
enableResize: true,
cell: ({ value }): React.ReactNode => {
const memory = value as number;
return (
<ValidateColumnValueWrapper
value={memory}
entity={InfraMonitoringEntity.DEPLOYMENTS}
attribute="memory metric"
>
<TanStackTable.Text>{formatBytes(memory)}</TanStackTable.Text>
</ValidateColumnValueWrapper>
);
},
},
];

View File

@@ -14,7 +14,12 @@ import {
import APIError from 'types/api/error';
import { v4 as uuidv4 } from 'uuid';
const K8S_EVENT_KEYS = ['k8s.object.kind', 'k8s.object.name'];
import { INFRA_MONITORING_ATTR_KEYS } from '../../constants';
const K8S_EVENT_KEYS = [
INFRA_MONITORING_ATTR_KEYS.K8S_OBJECT_KIND,
INFRA_MONITORING_ATTR_KEYS.K8S_OBJECT_NAME,
];
export function isEventsKeyNotFoundError(error: unknown): boolean {
if (!(error instanceof APIError)) {

View File

@@ -1,144 +1,8 @@
import { PANEL_TYPES } from 'constants/queryBuilder';
import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults';
import {
BaseAutocompleteData,
DataTypes,
} from 'types/api/queryBuilder/queryAutocompleteResponse';
import {
IBuilderQuery,
TagFilterItem,
} from 'types/api/queryBuilder/queryBuilderData';
import APIError from 'types/api/error';
import { EQueryType } from 'types/common/dashboard';
import { DataSource, ReduceOperators } from 'types/common/queryBuilder';
import { nanoToMilli } from 'utils/timeUtils';
import { v4 as uuidv4 } from 'uuid';
export function isKeyNotFoundError(error: unknown): boolean {
if (!(error instanceof APIError)) {
return false;
}
const errorDetails = error.getErrorDetails();
if (errorDetails.error.code !== 'invalid_input') {
return false;
}
const errors = errorDetails.error.errors || [];
return errors.some((err) => err.message?.includes('not found'));
}
export const QUERY_KEYS = {
K8S_OBJECT_KIND: 'k8s.object.kind',
K8S_OBJECT_NAME: 'k8s.object.name',
K8S_POD_NAME: 'k8s.pod.name',
K8S_NAMESPACE_NAME: 'k8s.namespace.name',
K8S_CLUSTER_NAME: 'k8s.cluster.name',
K8S_NODE_NAME: 'k8s.node.name',
K8S_DEPLOYMENT_NAME: 'k8s.deployment.name',
K8S_STATEFUL_SET_NAME: 'k8s.statefulset.name',
K8S_JOB_NAME: 'k8s.job.name',
K8S_DAEMON_SET_NAME: 'k8s.daemonset.name',
K8S_PERSISTENT_VOLUME_CLAIM_NAME: 'k8s.persistentvolumeclaim.name',
};
/**
* Returns the payload configuration to fetch events for a K8s entity
*/
export const getEntityEventsOrLogsQueryPayload = (
start: number,
end: number,
filters: IBuilderQuery['filters'],
): GetQueryResultsProps => ({
graphType: PANEL_TYPES.LIST,
selectedTime: 'GLOBAL_TIME',
query: {
clickhouse_sql: [],
promql: [],
builder: {
queryData: [
{
dataSource: DataSource.LOGS,
queryName: 'A',
aggregateOperator: 'noop',
aggregateAttribute: {
id: '------false',
dataType: DataTypes.String,
key: '',
type: '',
},
timeAggregation: 'rate',
spaceAggregation: 'sum',
functions: [],
filters,
expression: 'A',
disabled: false,
stepInterval: 60,
having: [],
limit: null,
orderBy: [
{
columnName: 'timestamp',
order: 'desc',
},
],
groupBy: [],
legend: '',
reduceTo: ReduceOperators.AVG,
offset: 0,
pageSize: 100,
},
],
queryFormulas: [],
queryTraceOperator: [],
},
id: uuidv4(),
queryType: EQueryType.QUERY_BUILDER,
},
start,
end,
});
export const entityTracesColumns = [
{
dataIndex: 'timestamp',
key: 'timestamp',
title: 'Timestamp',
width: 200,
render: (timestamp: string): string => new Date(timestamp).toLocaleString(),
},
{
title: 'Service Name',
dataIndex: ['data', 'serviceName'],
key: 'serviceName-string-tag',
width: 150,
},
{
title: 'Name',
dataIndex: ['data', 'name'],
key: 'name-string-tag',
width: 145,
},
{
title: 'Duration',
dataIndex: ['data', 'durationNano'],
key: 'durationNano-float64-tag',
width: 145,
render: (duration: number): string => `${nanoToMilli(duration)}ms`,
},
{
title: 'HTTP Method',
dataIndex: ['data', 'httpMethod'],
key: 'httpMethod-string-tag',
width: 145,
},
{
title: 'Status Code',
dataIndex: ['data', 'responseStatusCode'],
key: 'responseStatusCode-string-tag',
width: 145,
},
];
export const selectedEntityTracesColumns: BaseAutocompleteData[] = [
{
@@ -173,110 +37,16 @@ export const selectedEntityTracesColumns: BaseAutocompleteData[] = [
},
];
export const getEntityTracesQueryPayload = (
start: number,
end: number,
offset = 0,
filters: IBuilderQuery['filters'],
): GetQueryResultsProps => ({
query: {
promql: [],
clickhouse_sql: [],
builder: {
queryData: [
{
dataSource: DataSource.TRACES,
queryName: 'A',
aggregateOperator: 'noop',
aggregateAttribute: {
id: '------false',
dataType: DataTypes.EMPTY,
key: '',
type: '',
},
timeAggregation: 'rate',
spaceAggregation: 'sum',
functions: [],
filters,
expression: 'A',
disabled: false,
stepInterval: 60,
having: [],
limit: null,
orderBy: [
{
columnName: 'timestamp',
order: 'desc',
},
],
groupBy: [],
legend: '',
reduceTo: ReduceOperators.AVG,
},
],
queryFormulas: [],
queryTraceOperator: [],
},
id: '572f1d91-6ac0-46c0-b726-c21488b34434',
queryType: EQueryType.QUERY_BUILDER,
},
graphType: PANEL_TYPES.LIST,
selectedTime: 'GLOBAL_TIME',
start,
end,
params: {
dataSource: DataSource.TRACES,
},
tableParams: {
pagination: {
limit: 10,
offset,
},
selectColumns: [
{
key: 'serviceName',
dataType: 'string',
type: 'tag',
id: 'serviceName--string--tag--true',
isIndexed: false,
},
{
key: 'name',
dataType: 'string',
type: 'tag',
id: 'name--string--tag--true',
isIndexed: false,
},
{
key: 'durationNano',
dataType: 'float64',
type: 'tag',
id: 'durationNano--float64--tag--true',
isIndexed: false,
},
{
key: 'httpMethod',
dataType: 'string',
type: 'tag',
id: 'httpMethod--string--tag--true',
isIndexed: false,
},
{
key: 'responseStatusCode',
dataType: 'string',
type: 'tag',
id: 'responseStatusCode--string--tag--true',
isIndexed: false,
},
],
},
});
export function isKeyNotFoundError(error: unknown): boolean {
if (!(error instanceof APIError)) {
return false;
}
export const filterOutPrimaryFilters = (
filters: TagFilterItem[],
primaryKeys: string[],
): TagFilterItem[] =>
filters.filter(
(filter) =>
!primaryKeys.includes(filter.key?.key ?? '') && filter.key?.key !== 'id',
);
const errorDetails = error.getErrorDetails();
if (errorDetails.error.code !== 'invalid_input') {
return false;
}
const errors = errorDetails.error.errors || [];
return errors.some((err) => err.message?.includes('not found'));
}

View File

@@ -26,8 +26,8 @@
}
&::-webkit-scrollbar {
width: 0.1rem;
height: 0.1rem;
width: 8px;
height: 8px;
}
&::-webkit-scrollbar-track {
@@ -35,11 +35,12 @@
}
&::-webkit-scrollbar-thumb {
background: var(--accent-primary);
background: color-mix(in srgb, var(--l1-foreground) 20%, transparent);
border-radius: 9999px;
}
&::-webkit-scrollbar-thumb:hover {
background: colox-mix(var(--accent-primary), var(--bg-vanilla-100), 20%);
background: color-mix(in srgb, var(--l1-foreground) 30%, transparent);
}
}
@@ -84,7 +85,7 @@
}
&::-webkit-scrollbar-thumb:hover {
background: colox-mix(var(--accent-primary), var(--bg-vanilla-100), 20%);
background: var(--accent-primary);
}
}
}
@@ -99,11 +100,12 @@
.listContainer {
flex: 1;
min-width: 0;
max-width: 100%;
display: flex;
flex-direction: column;
align-items: stretch;
min-width: 595px;
overflow-x: auto;
> * {
min-width: 0;
@@ -233,3 +235,7 @@
background: var(--accent-primary);
}
}
.k8SOpenQuickFilters {
grid-area: quickFilters;
}

View File

@@ -2,12 +2,11 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import * as Sentry from '@sentry/react';
import { Button, Tooltip } from 'antd';
import { Typography } from '@signozhq/ui/typography';
import logEvent from 'api/common/logEvent';
import QuickFilters from 'components/QuickFilters/QuickFilters';
import { QuickFiltersSource } from 'components/QuickFilters/types';
import { InfraMonitoringEvents } from 'constants/events';
import { initialQueriesMap } from 'constants/queryBuilder';
import { useGetCompositeQueryParam } from 'hooks/queryBuilder/useGetCompositeQueryParam';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useQueryOperations } from 'hooks/queryBuilder/useQueryBuilderOperations';
import {
ArrowUpDown,
ArrowUpToLine,
@@ -22,7 +21,7 @@ import {
Workflow,
} from '@signozhq/icons';
import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFallback';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
import { DataSource } from 'types/common/queryBuilder';
import { FeatureKeys } from '../../constants/features';
import { useAppContext } from '../../providers/App/App';
@@ -43,9 +42,9 @@ import K8sDaemonSetsList from './DaemonSets/K8sDaemonSetsList';
import K8sDeploymentsList from './Deployments/K8sDeploymentsList';
import {
useInfraMonitoringCategory,
useInfraMonitoringFiltersK8s,
useInfraMonitoringGroupBy,
useInfraMonitoringOrderBy,
useInfraMonitoringSelectedItem,
} from './hooks';
import K8sJobsList from './Jobs/K8sJobsList';
import K8sNamespacesList from './Namespaces/K8sNamespacesList';
@@ -55,62 +54,60 @@ import K8sStatefulSetsList from './StatefulSets/K8sStatefulSetsList';
import K8sVolumesList from './Volumes/K8sVolumesList';
import styles from './InfraMonitoringK8s.module.scss';
import { InfraMonitoringEvents } from 'constants/events';
import logEvent from 'api/common/logEvent';
export default function InfraMonitoringK8s(): JSX.Element {
const [showFilters, setShowFilters] = useState(true);
const [selectedCategory, setSelectedCategory] = useInfraMonitoringCategory();
const [urlFilters, setUrlFilters] = useInfraMonitoringFiltersK8s();
const [, setGroupBy] = useInfraMonitoringGroupBy();
const [, setOrderBy] = useInfraMonitoringOrderBy();
const [, setSelectedItem] = useInfraMonitoringSelectedItem();
const { currentQuery } = useQueryBuilder();
const handleFilterVisibilityChange = useCallback((): void => {
setShowFilters((show) => !show);
}, []);
const { handleChangeQueryData } = useQueryOperations({
index: 0,
query: currentQuery.builder.queryData[0],
entityVersion: '',
});
// Track previous urlFilters to only sync when the value actually changes
// (not when handleChangeQueryData changes due to query updates)
const prevUrlFiltersRef = useRef<string | null>(null);
const compositeQuery = useGetCompositeQueryParam();
const { currentQuery, redirectWithQueryBuilderData } = useQueryBuilder();
const isInitialized = useRef(false);
useEffect(() => {
const currentFiltersJson = urlFilters ? JSON.stringify(urlFilters) : null;
// Only sync if urlFilters value has actually changed
if (prevUrlFiltersRef.current !== currentFiltersJson) {
prevUrlFiltersRef.current = currentFiltersJson;
// Sync filters to query builder, using empty filter when urlFilters is null
handleChangeQueryData('filters', urlFilters || { items: [], op: 'and' });
if (isInitialized.current) {
return;
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [urlFilters]); // handleChangeQueryData intentionally omitted - we call the current version but don't re-run when it changes
isInitialized.current = true;
const { featureFlags } = useAppContext();
const dotMetricsEnabled =
featureFlags?.find((flag) => flag.name === FeatureKeys.DOT_METRICS_ENABLED)
?.active || false;
if (!compositeQuery) {
const defaultQuery = initialQueriesMap[DataSource.METRICS];
redirectWithQueryBuilderData({
...defaultQuery,
builder: {
...defaultQuery.builder,
queryData: defaultQuery.builder.queryData.map((query) => ({
...query,
filter: { expression: '' },
filters: { items: [], op: 'AND' as const },
})),
},
});
}
}, [compositeQuery, redirectWithQueryBuilderData]);
const handleFilterChange = (query: Query): void => {
// update the current query with the new filters
// in infra monitoring k8s, we are using only one query, hence updating the 0th index of queryData
const filters = query.builder.queryData[0].filters;
// The useEffect will sync filters to query builder, avoiding double state updates
setUrlFilters(filters || null);
logEvent(InfraMonitoringEvents.FilterApplied, {
useEffect(() => {
void logEvent(InfraMonitoringEvents.FilterApplied, {
entity: InfraMonitoringEvents.K8sEntity,
page: InfraMonitoringEvents.ListPage,
category: selectedCategory,
view: InfraMonitoringEvents.QuickFiltersView,
});
};
}, [selectedCategory]);
const handleFilterVisibilityChange = useCallback((): void => {
setShowFilters((show) => !show);
}, []);
const { featureFlags } = useAppContext();
const dotMetricsEnabled =
featureFlags?.find((flag) => flag.name === FeatureKeys.DOT_METRICS_ENABLED)
?.active || false;
const categories = useMemo(
() => [
@@ -179,12 +176,23 @@ export default function InfraMonitoringK8s(): JSX.Element {
const handleCategorySelect = (key: string): void => {
if (key !== selectedCategory) {
setSelectedCategory(key);
// Reset filters
setUrlFilters(null);
setOrderBy(null);
setGroupBy(null);
handleChangeQueryData('filters', { items: [], op: 'and' });
void setSelectedCategory(key as string);
void setOrderBy(null);
void setGroupBy(null);
void setSelectedItem(null);
redirectWithQueryBuilderData({
...currentQuery,
builder: {
...currentQuery.builder,
queryData: [
{
...(currentQuery.builder.queryData[0] || {}),
filter: { expression: '' },
filters: { items: [], op: 'AND' as const },
},
],
},
});
}
};
@@ -192,7 +200,7 @@ export default function InfraMonitoringK8s(): JSX.Element {
return (
<>
{!showFilters && (
<div>
<div className={styles.k8SOpenQuickFilters}>
<Button
className="periscope-btn ghost"
type="text"
@@ -261,7 +269,6 @@ export default function InfraMonitoringK8s(): JSX.Element {
source={QuickFiltersSource.INFRA_MONITORING}
config={selectedCategoryConfig}
handleFilterVisibilityChange={handleFilterVisibilityChange}
onFilterChange={handleFilterChange}
/>
)}
</div>

View File

@@ -1,21 +1,24 @@
import React, { useCallback } from 'react';
import { useCallback } from 'react';
import { listJobs } from 'api/generated/services/inframonitoring';
import {
InframonitoringtypesJobRecordDTO,
InframonitoringtypesResponseTypeDTO,
Querybuildertypesv5OrderDirectionDTO,
} from 'api/generated/services/sigNoz.schemas';
import { InfraMonitoringEvents } from 'constants/events';
import { FeatureKeys } from 'constants/features';
import { useAppContext } from 'providers/App/App';
import K8sBaseDetails, { K8sDetailsFilters } from '../Base/K8sBaseDetails';
import { K8sBaseList } from '../Base/K8sBaseList';
import { K8sBaseFilters } from '../Base/types';
import { InfraMonitoringEntity } from '../constants';
import { getK8sJobsList, K8sJobsData } from './api';
import {
getJobMetricsQueryPayload,
jobWidgetInfo,
k8sJobDetailsMetadataConfig,
k8sJobGetEntityName,
k8sJobGetSelectedItemFilters,
k8sJobInitialEventsFilter,
k8sJobInitialLogTracesFilter,
k8sJobGetSelectedItemExpression,
k8sJobInitialEventsExpression,
k8sJobInitialLogTracesExpression,
} from './constants';
import {
getK8sJobItemKey,
@@ -28,66 +31,93 @@ function K8sJobsList({
}: {
controlListPrefix?: React.ReactNode;
}): JSX.Element {
const { featureFlags } = useAppContext();
const dotMetricsEnabled =
featureFlags?.find((flag) => flag.name === FeatureKeys.DOT_METRICS_ENABLED)
?.active || false;
const fetchListData = useCallback(
async (filters: K8sBaseFilters, signal?: AbortSignal) => {
filters.orderBy ||= {
columnName: 'cpu',
order: 'desc',
};
try {
const response = await listJobs(
{
filter: { expression: filters.filter.expression },
groupBy: filters.groupBy?.map((g) => ({ name: g.name })),
offset: filters.offset,
limit: filters.limit ?? 10,
start: filters.start,
end: filters.end,
orderBy: filters.orderBy
? {
key: { name: filters.orderBy.key.name },
direction:
filters.orderBy.direction === 'asc'
? Querybuildertypesv5OrderDirectionDTO.asc
: Querybuildertypesv5OrderDirectionDTO.desc,
}
: undefined,
},
signal,
);
const response = await getK8sJobsList(
filters,
signal,
undefined,
dotMetricsEnabled,
);
return {
data: response.payload?.data.records || [],
total: response.payload?.data.total || 0,
error: response.error,
rawData: response.payload?.data,
};
const data = response.data;
return {
type:
data.type === InframonitoringtypesResponseTypeDTO.grouped_list
? ('grouped_list' as const)
: ('list' as const),
records: data.records,
total: data.total,
endTimeBeforeRetention: data.endTimeBeforeRetention,
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,
};
}
},
[dotMetricsEnabled],
[],
);
const fetchEntityData = useCallback(
async (
filters: K8sDetailsFilters,
signal?: AbortSignal,
): Promise<{ data: K8sJobsData | null; error?: string | null }> => {
const response = await getK8sJobsList(
{
filters: filters.filters,
start: filters.start,
end: filters.end,
limit: 1,
offset: 0,
},
signal,
undefined,
dotMetricsEnabled,
);
): Promise<{
data: InframonitoringtypesJobRecordDTO | null;
error?: string | null;
}> => {
try {
const response = await listJobs(
{
filter: { expression: filters.filter.expression },
start: filters.start,
end: filters.end,
limit: 1,
offset: 0,
},
signal,
);
const records = response.payload?.data.records || [];
return {
data: records.length > 0 ? records[0] : null,
error: response.error,
};
return {
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,
};
}
},
[dotMetricsEnabled],
[],
);
return (
<>
<K8sBaseList<K8sJobsData>
<K8sBaseList<InframonitoringtypesJobRecordDTO>
controlListPrefix={controlListPrefix}
entity={InfraMonitoringEntity.JOBS}
tableColumns={k8sJobsColumnsConfig}
@@ -97,14 +127,14 @@ function K8sJobsList({
eventCategory={InfraMonitoringEvents.Job}
/>
<K8sBaseDetails<K8sJobsData>
<K8sBaseDetails<InframonitoringtypesJobRecordDTO>
category={InfraMonitoringEntity.JOBS}
eventCategory={InfraMonitoringEvents.Job}
getSelectedItemFilters={k8sJobGetSelectedItemFilters}
getSelectedItemExpression={k8sJobGetSelectedItemExpression}
fetchEntityData={fetchEntityData}
getEntityName={k8sJobGetEntityName}
getInitialLogTracesFilters={k8sJobInitialLogTracesFilter}
getInitialEventsFilters={k8sJobInitialEventsFilter}
getInitialLogTracesExpression={k8sJobInitialLogTracesExpression}
getInitialEventsExpression={k8sJobInitialEventsExpression}
metadataConfig={k8sJobDetailsMetadataConfig}
entityWidgetInfo={jobWidgetInfo}
getEntityQueryPayload={getJobMetricsQueryPayload}

View File

@@ -1,119 +0,0 @@
import axios from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { UnderscoreToDotMap } from 'api/utils';
import { AxiosError } from 'axios';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { K8sBaseFilters } from '../Base/types';
export interface K8sJobsData {
jobName: string;
cpuUsage: number;
memoryUsage: number;
cpuRequest: number;
memoryRequest: number;
cpuLimit: number;
memoryLimit: number;
restarts: number;
desiredSuccessfulPods: number;
activePods: number;
failedPods: number;
successfulPods: number;
meta: {
k8s_cluster_name: string;
k8s_job_name: string;
k8s_namespace_name: string;
};
}
export interface K8sJobsListResponse {
status: string;
data: {
type: string;
records: K8sJobsData[];
groups: null;
total: number;
sentAnyHostMetricsData: boolean;
isSendingK8SAgentMetrics: boolean;
};
}
// TODO(H4ad): Erase this whole file when migrating to openapi
export const jobsMetaMap = [
{ dot: 'k8s.cluster.name', under: 'k8s_cluster_name' },
{ dot: 'k8s.job.name', under: 'k8s_job_name' },
{ dot: 'k8s.namespace.name', under: 'k8s_namespace_name' },
] as const;
export function mapJobsMeta(raw: Record<string, unknown>): K8sJobsData['meta'] {
const out: Record<string, unknown> = { ...raw };
jobsMetaMap.forEach(({ dot, under }) => {
if (dot in raw) {
const v = raw[dot];
out[under] = typeof v === 'string' ? v : raw[under];
}
});
return out as K8sJobsData['meta'];
}
export const getK8sJobsList = async (
props: K8sBaseFilters,
signal?: AbortSignal,
headers?: Record<string, string>,
dotMetricsEnabled = false,
): Promise<SuccessResponse<K8sJobsListResponse> | ErrorResponse> => {
try {
const requestProps = dotMetricsEnabled
? {
...props,
filters: {
...props.filters,
items: props.filters?.items.reduce<typeof props.filters.items>(
(acc, item) => {
if (item.value === undefined) {
return acc;
}
if (
item.key &&
typeof item.key === 'object' &&
'key' in item.key &&
typeof item.key.key === 'string'
) {
const mappedKey = UnderscoreToDotMap[item.key.key] ?? item.key.key;
acc.push({
...item,
key: { ...item.key, key: mappedKey },
});
} else {
acc.push(item);
}
return acc;
},
[] as typeof props.filters.items,
),
},
}
: props;
const response = await axios.post('/jobs/list', requestProps, {
signal,
headers,
});
const payload: K8sJobsListResponse = response.data;
payload.data.records = payload.data.records.map((record) => ({
...record,
meta: mapJobsMeta(record.meta as Record<string, unknown>),
}));
return {
statusCode: 200,
error: null,
message: 'Success',
payload,
params: requestProps,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);
}
};

View File

@@ -1,67 +1,63 @@
import { InframonitoringtypesJobRecordDTO } from 'api/generated/services/sigNoz.schemas';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults';
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
import { EQueryType } from 'types/common/dashboard';
import { DataSource, ReduceOperators } from 'types/common/queryBuilder';
import { v4 } from 'uuid';
import {
createFilterItem,
K8sDetailsMetadataConfig,
} from '../Base/K8sBaseDetails';
import { QUERY_KEYS } from '../EntityDetailsUtils/utils';
import { K8sJobsData } from './api';
import { K8sDetailsMetadataConfig } from '../Base/K8sBaseDetails';
import { formatValueForExpression } from 'components/QueryBuilderV2/utils';
import { INFRA_MONITORING_ATTR_KEYS } from '../constants';
export const k8sJobGetSelectedItemFilters = (
export const k8sJobGetSelectedItemExpression = (
selectedItemId: string,
): TagFilter => ({
op: 'AND',
items: [
{
id: 'k8s_job_name',
key: {
key: 'k8s_job_name',
type: null,
},
op: '=',
value: selectedItemId,
},
],
});
): string =>
`${INFRA_MONITORING_ATTR_KEYS.K8S_JOB_NAME} = ${formatValueForExpression(selectedItemId)}`;
export const k8sJobDetailsMetadataConfig: K8sDetailsMetadataConfig<K8sJobsData>[] =
export const k8sJobDetailsMetadataConfig: K8sDetailsMetadataConfig<InframonitoringtypesJobRecordDTO>[] =
[
{
label: 'Job Name',
getValue: (p): string => p.meta.k8s_job_name,
getValue: (p): string =>
p.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_JOB_NAME] ?? '',
},
{
label: 'Cluster Name',
getValue: (p): string => p.meta.k8s_cluster_name,
getValue: (p): string =>
p.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_CLUSTER_NAME] ?? '',
},
{
label: 'Namespace Name',
getValue: (p): string => p.meta.k8s_namespace_name,
getValue: (p): string =>
p.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] ?? '',
},
];
export const k8sJobInitialEventsFilter = (
item: K8sJobsData,
): ReturnType<typeof createFilterItem>[] => [
createFilterItem(QUERY_KEYS.K8S_OBJECT_KIND, 'Job'),
createFilterItem(QUERY_KEYS.K8S_OBJECT_NAME, item.meta.k8s_job_name),
];
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}`;
};
export const k8sJobInitialLogTracesFilter = (
item: K8sJobsData,
): ReturnType<typeof createFilterItem>[] => [
createFilterItem(QUERY_KEYS.K8S_JOB_NAME, item.meta.k8s_job_name),
createFilterItem(QUERY_KEYS.K8S_NAMESPACE_NAME, item.meta.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}`;
};
export const k8sJobGetEntityName = (item: K8sJobsData): string =>
item.meta.k8s_job_name;
export const k8sJobGetEntityName = (
item: InframonitoringtypesJobRecordDTO,
): string => item.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_JOB_NAME] ?? '';
export const jobWidgetInfo = [
{
@@ -83,7 +79,7 @@ export const jobWidgetInfo = [
];
export const getJobMetricsQueryPayload = (
job: K8sJobsData,
job: InframonitoringtypesJobRecordDTO,
start: number,
end: number,
dotMetricsEnabled: boolean,
@@ -145,7 +141,8 @@ export const getJobMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: job.meta.k8s_namespace_name,
value:
job.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] ?? '',
},
],
op: 'AND',
@@ -229,7 +226,8 @@ export const getJobMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: job.meta.k8s_namespace_name,
value:
job.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] ?? '',
},
],
op: 'AND',
@@ -313,7 +311,8 @@ export const getJobMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: job.meta.k8s_namespace_name,
value:
job.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] ?? '',
},
],
op: 'AND',
@@ -410,7 +409,8 @@ export const getJobMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: job.meta.k8s_namespace_name,
value:
job.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] ?? '',
},
],
op: 'AND',

View File

@@ -1,30 +1,45 @@
import { Tooltip } from 'antd';
import { TableColumnDef } from 'components/TanStackTableView';
import TanStackTable from 'components/TanStackTableView';
import { Color } from '@signozhq/design-tokens';
import { InframonitoringtypesJobRecordDTO } from 'api/generated/services/sigNoz.schemas';
import TanStackTable, { TableColumnDef } from 'components/TanStackTableView';
import { ExpandButtonWrapper } from 'container/InfraMonitoringK8sV2/components';
import ColumnHeader from '../Base/ColumnHeader';
import EntityGroupHeader from '../Base/EntityGroupHeader';
import K8sGroupCell from '../Base/K8sGroupCell';
import { formatBytes } from '../commonUtils';
import { EntityProgressBar, ValidateColumnValueWrapper } from '../components';
import { InfraMonitoringEntity } from '../constants';
import { K8sJobsData } from './api';
import { formatBytes, getPodPhaseStatusItems } from '../commonUtils';
import {
CellValueTooltip,
EntityProgressBar,
GroupedStatusCounts,
ValidateColumnValueWrapper,
} from '../components';
import {
INFRA_MONITORING_ATTR_KEYS,
InfraMonitoringEntity,
} from '../constants';
import { Bolt } from '@signozhq/icons';
export function getK8sJobRowKey(job: K8sJobsData): string {
return job.jobName || job.meta.k8s_job_name || '';
export function getK8sJobRowKey(job: InframonitoringtypesJobRecordDTO): string {
return (
job.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_JOB_NAME] || job.jobName || ''
);
}
export function getK8sJobItemKey(job: K8sJobsData): string {
return job.meta.k8s_job_name;
export function getK8sJobItemKey(
job: InframonitoringtypesJobRecordDTO,
): string {
return job.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_JOB_NAME] || '';
}
export const k8sJobsColumnsConfig: TableColumnDef<K8sJobsData>[] = [
export type JobTableColumnConfig =
TableColumnDef<InframonitoringtypesJobRecordDTO>;
export const k8sJobsColumnsConfig: JobTableColumnConfig[] = [
{
id: 'jobGroup',
header: (): React.ReactNode => <EntityGroupHeader title="JOB GROUP" />,
accessorFn: (row): string => row.meta.k8s_job_name || '',
width: { min: 270 },
header: (): React.ReactNode => <EntityGroupHeader title="Job Group" />,
accessorFn: (row): string =>
row.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_JOB_NAME] || '',
width: { min: 290 },
enableSort: false,
enableRemove: false,
enableMove: false,
@@ -47,10 +62,12 @@ export const k8sJobsColumnsConfig: TableColumnDef<K8sJobsData>[] = [
<EntityGroupHeader
title="Job Name"
icon={<Bolt data-hide-expanded="true" size={14} />}
docPath="/infrastructure-monitoring/kubernetes/jobs#job-name"
/>
),
accessorFn: (row): string => row.meta.k8s_job_name || '',
width: { min: 260 },
accessorFn: (row): string =>
row.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_JOB_NAME] || '',
width: { min: 290 },
enableSort: false,
enableRemove: false,
enableMove: false,
@@ -59,109 +76,98 @@ export const k8sJobsColumnsConfig: TableColumnDef<K8sJobsData>[] = [
cell: ({ value }): React.ReactNode => {
const jobName = value as string;
return (
<Tooltip title={jobName}>
<CellValueTooltip value={jobName}>
<TanStackTable.Text>{jobName}</TanStackTable.Text>
</Tooltip>
</CellValueTooltip>
);
},
},
{
id: 'namespaceName',
header: 'Namespace Name',
accessorFn: (row): string => row.meta.k8s_namespace_name || '',
width: { default: 150 },
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/jobs#namespace-name">
Namespace
</ColumnHeader>
),
accessorFn: (row): string =>
row.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] || '',
width: { min: 160 },
enableSort: false,
enableResize: true,
cell: ({ value }): React.ReactNode => {
const namespaceName = value as string;
return (
<Tooltip title={namespaceName}>
<CellValueTooltip value={namespaceName}>
<TanStackTable.Text>{namespaceName}</TanStackTable.Text>
</Tooltip>
</CellValueTooltip>
);
},
},
{
id: 'successful_pods',
header: 'Successful',
id: 'pod_counts_by_phase',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/jobs#pod-counts-by-phase">
Pod Phases
</ColumnHeader>
),
accessorFn: (row): InframonitoringtypesJobRecordDTO['podCountsByPhase'] =>
row.podCountsByPhase,
width: { min: 250 },
enableSort: false,
enableResize: true,
cell: ({ row }): React.ReactNode => {
const podCountsByPhase = row.podCountsByPhase;
if (!podCountsByPhase) {
return <TanStackTable.Text>-</TanStackTable.Text>;
}
return (
<GroupedStatusCounts items={getPodPhaseStatusItems(podCountsByPhase)} />
);
},
},
{
id: 'completion_status',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/jobs#completion-status">
Completion Status
</ColumnHeader>
),
accessorFn: (row): number => row.successfulPods,
width: { min: 120 },
enableSort: true,
cell: ({ value }): React.ReactNode => {
const successfulPods = value as number;
return (
<ValidateColumnValueWrapper
value={successfulPods}
entity={InfraMonitoringEntity.JOBS}
attribute="successful pod"
>
<TanStackTable.Text>{successfulPods}</TanStackTable.Text>
</ValidateColumnValueWrapper>
);
},
},
{
id: 'failed_pods',
header: 'Failed',
accessorFn: (row): number => row.failedPods,
width: { min: 100 },
enableSort: true,
cell: ({ value }): React.ReactNode => {
const failedPods = value as number;
return (
<ValidateColumnValueWrapper
value={failedPods}
entity={InfraMonitoringEntity.JOBS}
attribute="failed pod"
>
<TanStackTable.Text>{failedPods}</TanStackTable.Text>
</ValidateColumnValueWrapper>
);
},
},
{
id: 'desired_successful_pods',
header: 'Desired Successful',
accessorFn: (row): number => row.desiredSuccessfulPods,
width: { min: 160 },
enableSort: true,
cell: ({ value }): React.ReactNode => {
const desiredSuccessfulPods = value as number;
return (
<ValidateColumnValueWrapper
value={desiredSuccessfulPods}
entity={InfraMonitoringEntity.JOBS}
attribute="desired successful pod"
>
<TanStackTable.Text>{desiredSuccessfulPods}</TanStackTable.Text>
</ValidateColumnValueWrapper>
);
},
},
{
id: 'active_pods',
header: 'Active',
accessorFn: (row): number => row.activePods,
width: { min: 100 },
enableSort: true,
cell: ({ value }): React.ReactNode => {
const activePods = value as number;
return (
<ValidateColumnValueWrapper
value={activePods}
entity={InfraMonitoringEntity.JOBS}
attribute="active pod"
>
<TanStackTable.Text>{activePods}</TanStackTable.Text>
</ValidateColumnValueWrapper>
);
},
width: { min: 210 },
enableSort: false,
enableResize: true,
cell: ({ row }): React.ReactNode => (
<GroupedStatusCounts
items={[
{ value: row.activePods, label: 'Active', color: Color.BG_ROBIN_500 },
{ value: row.failedPods, label: 'Failed', color: Color.BG_CHERRY_500 },
{
value: row.successfulPods,
label: 'Successful',
color: Color.BG_FOREST_500,
},
{
value: row.desiredSuccessfulPods,
label: 'Desired',
color: Color.BG_ROBIN_500,
},
]}
/>
),
},
{
id: 'cpu_request',
header: 'CPU Req Usage (%)',
accessorFn: (row): number => row.cpuRequest,
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/jobs#cpu-req-usage-">
CPU Request
<br /> Usage (%)
</ColumnHeader>
),
accessorFn: (row): number => row.jobCPURequest,
width: { min: 200, default: 200 },
enableSort: true,
enableResize: true,
defaultVisibility: false,
cell: ({ value }): React.ReactNode => {
const cpuRequest = value as number;
return (
@@ -177,10 +183,16 @@ export const k8sJobsColumnsConfig: TableColumnDef<K8sJobsData>[] = [
},
{
id: 'cpu_limit',
header: 'CPU Limit Usage (%)',
accessorFn: (row): number => row.cpuLimit,
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/jobs#cpu-limit-usage-">
CPU Limit
<br /> Usage (%)
</ColumnHeader>
),
accessorFn: (row): number => row.jobCPULimit,
width: { min: 200, default: 200 },
enableSort: true,
enableResize: true,
cell: ({ value }): React.ReactNode => {
const cpuLimit = value as number;
return (
@@ -196,29 +208,42 @@ export const k8sJobsColumnsConfig: TableColumnDef<K8sJobsData>[] = [
},
{
id: 'cpu',
header: 'CPU Usage (cores)',
accessorFn: (row): number => row.cpuUsage,
width: { min: 190 },
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/jobs#cpu-usage-cores">
CPU Usage
<br /> (cores)
</ColumnHeader>
),
accessorFn: (row): number => row.jobCPU,
width: { min: 160 },
enableSort: true,
enableResize: true,
cell: ({ value }): React.ReactNode => {
const cpu = value as number;
const cpu = Number(value);
return (
<ValidateColumnValueWrapper
value={cpu}
entity={InfraMonitoringEntity.JOBS}
attribute="CPU metric"
>
<TanStackTable.Text>{cpu}</TanStackTable.Text>
<TanStackTable.Text>{cpu.toFixed(2)}</TanStackTable.Text>
</ValidateColumnValueWrapper>
);
},
},
{
id: 'memory_request',
header: 'Mem Req Usage (%)',
accessorFn: (row): number => row.memoryRequest,
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/jobs#mem-req-usage-">
Memory Request
<br /> Usage (%)
</ColumnHeader>
),
accessorFn: (row): number => row.jobMemoryRequest,
width: { min: 190 },
enableSort: true,
enableResize: true,
defaultVisibility: false,
cell: ({ value }): React.ReactNode => {
const memoryRequest = value as number;
return (
@@ -234,10 +259,16 @@ export const k8sJobsColumnsConfig: TableColumnDef<K8sJobsData>[] = [
},
{
id: 'memory_limit',
header: 'Mem Limit Usage (%)',
accessorFn: (row): number => row.memoryLimit,
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/jobs#mem-limit-usage-">
Memory Limit
<br /> Usage (%)
</ColumnHeader>
),
accessorFn: (row): number => row.jobMemoryLimit,
width: { min: 180 },
enableSort: true,
enableResize: true,
cell: ({ value }): React.ReactNode => {
const memoryLimit = value as number;
return (
@@ -253,10 +284,16 @@ export const k8sJobsColumnsConfig: TableColumnDef<K8sJobsData>[] = [
},
{
id: 'memory',
header: 'Mem Usage (WSS)',
accessorFn: (row): number => row.memoryUsage,
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/jobs#mem-usage-wss">
Memory Usage
<br /> (WSS)
</ColumnHeader>
),
accessorFn: (row): number => row.jobMemory,
width: { min: 160 },
enableSort: true,
enableResize: true,
cell: ({ value }): React.ReactNode => {
const memory = value as number;
return (
@@ -270,4 +307,100 @@ export const k8sJobsColumnsConfig: TableColumnDef<K8sJobsData>[] = [
);
},
},
{
id: 'active_pods',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/jobs#active">
Active Pods
</ColumnHeader>
),
accessorFn: (row): number => row.activePods,
width: { min: 100 },
enableSort: true,
defaultVisibility: false,
cell: ({ value }): React.ReactNode => {
const activePods = value as number;
return (
<ValidateColumnValueWrapper
value={activePods}
entity={InfraMonitoringEntity.JOBS}
attribute="active pod"
>
<TanStackTable.Text>{activePods}</TanStackTable.Text>
</ValidateColumnValueWrapper>
);
},
},
{
id: 'failed_pods',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/jobs#failed">
Failed Pods
</ColumnHeader>
),
accessorFn: (row): number => row.failedPods,
width: { min: 100 },
enableSort: true,
defaultVisibility: false,
cell: ({ value }): React.ReactNode => {
const failedPods = value as number;
return (
<ValidateColumnValueWrapper
value={failedPods}
entity={InfraMonitoringEntity.JOBS}
attribute="failed pod"
>
<TanStackTable.Text>{failedPods}</TanStackTable.Text>
</ValidateColumnValueWrapper>
);
},
},
{
id: 'successful_pods',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/jobs#successful">
Successful Pods
</ColumnHeader>
),
accessorFn: (row): number => row.successfulPods,
width: { min: 120 },
enableSort: true,
defaultVisibility: false,
cell: ({ value }): React.ReactNode => {
const successfulPods = value as number;
return (
<ValidateColumnValueWrapper
value={successfulPods}
entity={InfraMonitoringEntity.JOBS}
attribute="successful pod"
>
<TanStackTable.Text>{successfulPods}</TanStackTable.Text>
</ValidateColumnValueWrapper>
);
},
},
{
id: 'desired_successful_pods',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/jobs#desired-successful">
Desired Successful Pods
</ColumnHeader>
),
accessorFn: (row): number => row.desiredSuccessfulPods,
width: { min: 160 },
enableSort: true,
defaultVisibility: false,
cell: ({ value }): React.ReactNode => {
const desiredSuccessfulPods = value as number;
return (
<ValidateColumnValueWrapper
value={desiredSuccessfulPods}
entity={InfraMonitoringEntity.JOBS}
attribute="desired successful pod"
>
<TanStackTable.Text>{desiredSuccessfulPods}</TanStackTable.Text>
</ValidateColumnValueWrapper>
);
},
},
];

View File

@@ -1,20 +1,23 @@
import React, { useCallback } from 'react';
import { useCallback } from 'react';
import { listNamespaces } from 'api/generated/services/inframonitoring';
import {
InframonitoringtypesNamespaceRecordDTO,
InframonitoringtypesResponseTypeDTO,
Querybuildertypesv5OrderDirectionDTO,
} from 'api/generated/services/sigNoz.schemas';
import { InfraMonitoringEvents } from 'constants/events';
import { FeatureKeys } from 'constants/features';
import { useAppContext } from 'providers/App/App';
import K8sBaseDetails, { K8sDetailsFilters } from '../Base/K8sBaseDetails';
import { K8sBaseList } from '../Base/K8sBaseList';
import { K8sBaseFilters } from '../Base/types';
import { InfraMonitoringEntity } from '../constants';
import { getK8sNamespacesList, K8sNamespacesData } from './api';
import {
getNamespaceMetricsQueryPayload,
k8sNamespaceDetailsMetadataConfig,
k8sNamespaceGetEntityName,
k8sNamespaceGetSelectedItemFilters,
k8sNamespaceInitialEventsFilter,
k8sNamespaceInitialLogTracesFilter,
k8sNamespaceGetSelectedItemExpression,
k8sNamespaceInitialEventsExpression,
k8sNamespaceInitialLogTracesExpression,
namespaceWidgetInfo,
} from './constants';
import {
@@ -28,66 +31,93 @@ function K8sNamespacesList({
}: {
controlListPrefix?: React.ReactNode;
}): JSX.Element {
const { featureFlags } = useAppContext();
const dotMetricsEnabled =
featureFlags?.find((flag) => flag.name === FeatureKeys.DOT_METRICS_ENABLED)
?.active || false;
const fetchListData = useCallback(
async (filters: K8sBaseFilters, signal?: AbortSignal) => {
filters.orderBy ||= {
columnName: 'cpu',
order: 'desc',
};
try {
const response = await listNamespaces(
{
filter: { expression: filters.filter.expression },
groupBy: filters.groupBy?.map((g) => ({ name: g.name })),
offset: filters.offset,
limit: filters.limit ?? 10,
start: filters.start,
end: filters.end,
orderBy: filters.orderBy
? {
key: { name: filters.orderBy.key.name },
direction:
filters.orderBy.direction === 'asc'
? Querybuildertypesv5OrderDirectionDTO.asc
: Querybuildertypesv5OrderDirectionDTO.desc,
}
: undefined,
},
signal,
);
const response = await getK8sNamespacesList(
filters,
signal,
undefined,
dotMetricsEnabled,
);
return {
data: response.payload?.data.records || [],
total: response.payload?.data.total || 0,
error: response.error,
rawData: response.payload?.data,
};
const data = response.data;
return {
type:
data.type === InframonitoringtypesResponseTypeDTO.grouped_list
? ('grouped_list' as const)
: ('list' as const),
records: data.records,
total: data.total,
endTimeBeforeRetention: data.endTimeBeforeRetention,
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,
};
}
},
[dotMetricsEnabled],
[],
);
const fetchEntityData = useCallback(
async (
filters: K8sDetailsFilters,
signal?: AbortSignal,
): Promise<{ data: K8sNamespacesData | null; error?: string | null }> => {
const response = await getK8sNamespacesList(
{
filters: filters.filters,
start: filters.start,
end: filters.end,
limit: 1,
offset: 0,
},
signal,
undefined,
dotMetricsEnabled,
);
): Promise<{
data: InframonitoringtypesNamespaceRecordDTO | null;
error?: string | null;
}> => {
try {
const response = await listNamespaces(
{
filter: { expression: filters.filter.expression },
start: filters.start,
end: filters.end,
limit: 1,
offset: 0,
},
signal,
);
const records = response.payload?.data.records || [];
return {
data: records.length > 0 ? records[0] : null,
error: response.error,
};
return {
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,
};
}
},
[dotMetricsEnabled],
[],
);
return (
<>
<K8sBaseList<K8sNamespacesData>
<K8sBaseList<InframonitoringtypesNamespaceRecordDTO>
controlListPrefix={controlListPrefix}
entity={InfraMonitoringEntity.NAMESPACES}
tableColumns={k8sNamespacesColumnsConfig}
@@ -97,14 +127,14 @@ function K8sNamespacesList({
eventCategory={InfraMonitoringEvents.Namespace}
/>
<K8sBaseDetails<K8sNamespacesData>
<K8sBaseDetails<InframonitoringtypesNamespaceRecordDTO>
category={InfraMonitoringEntity.NAMESPACES}
eventCategory={InfraMonitoringEvents.Namespace}
getSelectedItemFilters={k8sNamespaceGetSelectedItemFilters}
getSelectedItemExpression={k8sNamespaceGetSelectedItemExpression}
fetchEntityData={fetchEntityData}
getEntityName={k8sNamespaceGetEntityName}
getInitialLogTracesFilters={k8sNamespaceInitialLogTracesFilter}
getInitialEventsFilters={k8sNamespaceInitialEventsFilter}
getInitialLogTracesExpression={k8sNamespaceInitialLogTracesExpression}
getInitialEventsExpression={k8sNamespaceInitialEventsExpression}
metadataConfig={k8sNamespaceDetailsMetadataConfig}
entityWidgetInfo={namespaceWidgetInfo}
getEntityQueryPayload={getNamespaceMetricsQueryPayload}

View File

@@ -1,123 +0,0 @@
import axios from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { UnderscoreToDotMap } from 'api/utils';
import { AxiosError } from 'axios';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
import { K8sBaseFilters } from '../Base/types';
export interface K8sNamespacesListPayload {
filters: TagFilter;
groupBy?: BaseAutocompleteData[];
offset?: number;
limit?: number;
orderBy?: {
columnName: string;
order: 'asc' | 'desc';
};
}
export interface K8sNamespacesData {
namespaceName: string;
cpuUsage: number;
memoryUsage: number;
meta: {
k8s_cluster_name: string;
k8s_namespace_name: string;
};
}
export interface K8sNamespacesListResponse {
status: string;
data: {
type: string;
records: K8sNamespacesData[];
groups: null;
total: number;
sentAnyHostMetricsData: boolean;
isSendingK8SAgentMetrics: boolean;
};
}
// TODO(H4ad): Erase this whole file when migrating to openapi
export const namespacesMetaMap = [
{ dot: 'k8s.cluster.name', under: 'k8s_cluster_name' },
{ dot: 'k8s.namespace.name', under: 'k8s_namespace_name' },
] as const;
export function mapNamespacesMeta(
raw: Record<string, unknown>,
): K8sNamespacesData['meta'] {
const out: Record<string, unknown> = { ...raw };
namespacesMetaMap.forEach(({ dot, under }) => {
if (dot in raw) {
const v = raw[dot];
out[under] = typeof v === 'string' ? v : raw[under];
}
});
return out as K8sNamespacesData['meta'];
}
export const getK8sNamespacesList = async (
props: K8sBaseFilters,
signal?: AbortSignal,
headers?: Record<string, string>,
dotMetricsEnabled = false,
): Promise<SuccessResponse<K8sNamespacesListResponse> | ErrorResponse> => {
try {
const requestProps = dotMetricsEnabled
? {
...props,
filters: {
...props.filters,
items: props.filters?.items.reduce<typeof props.filters.items>(
(acc, item) => {
if (item.value === undefined) {
return acc;
}
if (
item.key &&
typeof item.key === 'object' &&
'key' in item.key &&
typeof item.key.key === 'string'
) {
const mappedKey = UnderscoreToDotMap[item.key.key] ?? item.key.key;
acc.push({
...item,
key: { ...item.key, key: mappedKey },
});
} else {
acc.push(item);
}
return acc;
},
[] as typeof props.filters.items,
),
},
}
: props;
const response = await axios.post('/namespaces/list', requestProps, {
signal,
headers,
});
const payload: K8sNamespacesListResponse = response.data;
payload.data.records = payload.data.records.map((record) => ({
...record,
meta: mapNamespacesMeta(record.meta as Record<string, unknown>),
}));
return {
statusCode: 200,
error: null,
message: 'Success',
payload,
params: requestProps,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);
}
};

View File

@@ -1,67 +1,47 @@
import { InframonitoringtypesNamespaceRecordDTO } from 'api/generated/services/sigNoz.schemas';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults';
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
import {
TagFilter,
TagFilterItem,
} from 'types/api/queryBuilder/queryBuilderData';
import { EQueryType } from 'types/common/dashboard';
import { DataSource, ReduceOperators } from 'types/common/queryBuilder';
import { v4 } from 'uuid';
import {
createFilterItem,
K8sDetailsMetadataConfig,
} from '../Base/K8sBaseDetails';
import { QUERY_KEYS } from '../EntityDetailsUtils/utils';
import { K8sNamespacesData } from './api';
import { K8sDetailsMetadataConfig } from '../Base/K8sBaseDetails';
import { formatValueForExpression } from 'components/QueryBuilderV2/utils';
import { INFRA_MONITORING_ATTR_KEYS } from '../constants';
export const k8sNamespaceGetSelectedItemFilters = (
export const k8sNamespaceGetSelectedItemExpression = (
selectedItemId: string,
): TagFilter => ({
op: 'AND',
items: [
{
id: 'k8s_namespace_name',
key: {
key: 'k8s_namespace_name',
type: null,
},
op: '=',
value: selectedItemId,
},
],
});
): string =>
`${INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME} = ${formatValueForExpression(selectedItemId)}`;
export const k8sNamespaceDetailsMetadataConfig: K8sDetailsMetadataConfig<K8sNamespacesData>[] =
export const k8sNamespaceDetailsMetadataConfig: K8sDetailsMetadataConfig<InframonitoringtypesNamespaceRecordDTO>[] =
[
{ label: 'Namespace Name', getValue: (p): string => p.namespaceName },
{ label: 'Namespace Name', getValue: (p): string => p.namespaceName || '' },
{
label: 'Cluster Name',
getValue: (p): string => p.meta.k8s_cluster_name,
getValue: (p): string =>
p.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_CLUSTER_NAME] || '',
},
];
export const k8sNamespaceInitialFilters = [
QUERY_KEYS.K8S_NAMESPACE_NAME,
QUERY_KEYS.K8S_CLUSTER_NAME,
];
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}`;
};
export const k8sNamespaceInitialEventsFilter = (
item: K8sNamespacesData,
): TagFilterItem[] => [
createFilterItem(QUERY_KEYS.K8S_OBJECT_KIND, 'Namespace'),
createFilterItem(QUERY_KEYS.K8S_OBJECT_NAME, item.namespaceName),
];
export const k8sNamespaceInitialLogTracesExpression = (
item: InframonitoringtypesNamespaceRecordDTO,
): string => {
const name = formatValueForExpression(item.namespaceName || '');
return `${INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME} = ${name}`;
};
export const k8sNamespaceInitialLogTracesFilter = (
item: K8sNamespacesData,
): TagFilterItem[] => [
createFilterItem(QUERY_KEYS.K8S_NAMESPACE_NAME, item.namespaceName),
];
export const k8sNamespaceGetEntityName = (item: K8sNamespacesData): string =>
item.namespaceName;
export const k8sNamespaceGetEntityName = (
item: InframonitoringtypesNamespaceRecordDTO,
): string => item.namespaceName || '';
export const namespaceWidgetInfo = [
{
@@ -107,7 +87,7 @@ export const namespaceWidgetInfo = [
];
export const getNamespaceMetricsQueryPayload = (
namespace: K8sNamespacesData,
namespace: InframonitoringtypesNamespaceRecordDTO,
start: number,
end: number,
dotMetricsEnabled: boolean,

View File

@@ -1,37 +1,46 @@
import { Tooltip } from 'antd';
import { FilePenLine } from '@signozhq/icons';
import { InframonitoringtypesNamespaceRecordDTO } from 'api/generated/services/sigNoz.schemas';
import TanStackTable, { TableColumnDef } from 'components/TanStackTableView';
import { ExpandButtonWrapper } from 'container/InfraMonitoringK8sV2/components';
import ColumnHeader from '../Base/ColumnHeader';
import EntityGroupHeader from '../Base/EntityGroupHeader';
import K8sGroupCell from '../Base/K8sGroupCell';
import { formatBytes } from '../commonUtils';
import { ValidateColumnValueWrapper } from '../components';
import { InfraMonitoringEntity } from '../constants';
import { K8sNamespacesData, K8sNamespacesListPayload } from './api';
import { FilePenLine } from '@signozhq/icons';
import { formatBytes, getPodPhaseStatusItems } from '../commonUtils';
import {
CellValueTooltip,
GroupedStatusCounts,
ValidateColumnValueWrapper,
} from '../components';
import {
INFRA_MONITORING_ATTR_KEYS,
InfraMonitoringEntity,
} from '../constants';
export function getK8sNamespaceRowKey(namespace: K8sNamespacesData): string {
return namespace.namespaceName || namespace.meta.k8s_namespace_name;
export function getK8sNamespaceRowKey(
namespace: InframonitoringtypesNamespaceRecordDTO,
): string {
return (
namespace.namespaceName ||
namespace.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] ||
''
);
}
export function getK8sNamespaceItemKey(namespace: K8sNamespacesData): string {
return namespace.meta.k8s_namespace_name;
export function getK8sNamespaceItemKey(
namespace: InframonitoringtypesNamespaceRecordDTO,
): string {
return namespace.namespaceName;
}
export const getK8sNamespacesListQuery = (): K8sNamespacesListPayload => ({
filters: {
items: [],
op: 'and',
},
orderBy: { columnName: 'cpu', order: 'desc' },
});
export const k8sNamespacesColumnsConfig: TableColumnDef<K8sNamespacesData>[] = [
export type NamespaceTableColumnConfig =
TableColumnDef<InframonitoringtypesNamespaceRecordDTO>;
export const k8sNamespacesColumnsConfig: NamespaceTableColumnConfig[] = [
{
id: 'namespaceGroup',
header: (): React.ReactNode => <EntityGroupHeader title="NAMESPACE GROUP" />,
accessorFn: (row): string => row.meta.k8s_namespace_name || '',
width: { min: 300 },
header: (): React.ReactNode => <EntityGroupHeader title="Namespace Group" />,
accessorFn: (row): string => row.namespaceName || '',
width: { min: 290 },
enableSort: false,
enableRemove: false,
enableMove: false,
@@ -54,6 +63,7 @@ export const k8sNamespacesColumnsConfig: TableColumnDef<K8sNamespacesData>[] = [
<EntityGroupHeader
title="Namespace Name"
icon={<FilePenLine data-hide-expanded="true" size={14} />}
docPath="/infrastructure-monitoring/kubernetes/namespaces#namespace-name"
/>
),
accessorFn: (row): string => row.namespaceName || '',
@@ -66,49 +76,85 @@ export const k8sNamespacesColumnsConfig: TableColumnDef<K8sNamespacesData>[] = [
cell: ({ value }): React.ReactNode => {
const namespaceName = value as string;
return (
<Tooltip title={namespaceName}>
<CellValueTooltip value={namespaceName}>
<TanStackTable.Text>{namespaceName}</TanStackTable.Text>
</Tooltip>
</CellValueTooltip>
);
},
},
{
id: 'clusterName',
header: 'Cluster Name',
accessorFn: (row): string => row.meta.k8s_cluster_name || '',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/namespaces#cluster-name">
Cluster Name
</ColumnHeader>
),
accessorFn: (row): string =>
row.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_CLUSTER_NAME] || '',
width: { default: 150 },
enableSort: false,
cell: ({ value }): React.ReactNode => (
<TanStackTable.Text>{value as string}</TanStackTable.Text>
),
},
{
id: 'podCountsByPhase',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/namespaces#pod-counts-by-phase">
Pod Phases
</ColumnHeader>
),
accessorFn: (
row,
): InframonitoringtypesNamespaceRecordDTO['podCountsByPhase'] =>
row.podCountsByPhase,
width: { min: 250 },
enableSort: false,
cell: ({ row }): React.ReactNode => {
const podCountsByPhase = row.podCountsByPhase;
if (!podCountsByPhase) {
return <TanStackTable.Text>-</TanStackTable.Text>;
}
return (
<GroupedStatusCounts items={getPodPhaseStatusItems(row.podCountsByPhase)} />
);
},
},
{
id: 'cpu',
header: 'CPU Usage (cores)',
accessorFn: (row): number => row.cpuUsage,
width: { min: 220 },
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/namespaces#cpu-usage-cores">
CPU Usage (cores)
</ColumnHeader>
),
accessorFn: (row): number => row.namespaceCPU,
width: { min: 190 },
enableSort: true,
cell: ({ value }): React.ReactNode => {
const cpu = value as number;
const cpu = Number(value);
return (
<ValidateColumnValueWrapper
value={cpu}
entity={InfraMonitoringEntity.NAMESPACES}
attribute="CPU metric"
>
<TanStackTable.Text>{cpu}</TanStackTable.Text>
<TanStackTable.Text>{cpu.toFixed(2)}</TanStackTable.Text>
</ValidateColumnValueWrapper>
);
},
},
{
id: 'memory',
header: 'Mem Usage (WSS)',
accessorFn: (row): number => row.memoryUsage,
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/namespaces#mem-usage-wss">
Memory Usage (WSS)
</ColumnHeader>
),
accessorFn: (row): number => row.namespaceMemory,
width: { min: 220 },
enableSort: true,
cell: ({ value }): React.ReactNode => {
const memory = value as number;
const memory = Number(value);
return (
<ValidateColumnValueWrapper
value={memory}

View File

@@ -1,20 +1,23 @@
import React, { useCallback } from 'react';
import { useCallback } from 'react';
import { listNodes } from 'api/generated/services/inframonitoring';
import {
InframonitoringtypesNodeRecordDTO,
InframonitoringtypesResponseTypeDTO,
Querybuildertypesv5OrderDirectionDTO,
} from 'api/generated/services/sigNoz.schemas';
import { InfraMonitoringEvents } from 'constants/events';
import { FeatureKeys } from 'constants/features';
import { useAppContext } from 'providers/App/App';
import K8sBaseDetails, { K8sDetailsFilters } from '../Base/K8sBaseDetails';
import { K8sBaseList } from '../Base/K8sBaseList';
import { K8sBaseFilters } from '../Base/types';
import { InfraMonitoringEntity } from '../constants';
import { getK8sNodesList, K8sNodeData } from './api';
import {
getNodeMetricsQueryPayload,
k8sNodeDetailsMetadataConfig,
k8sNodeGetEntityName,
k8sNodeGetSelectedItemFilters,
k8sNodeInitialEventsFilter,
k8sNodeInitialLogTracesFilter,
k8sNodeGetSelectedItemExpression,
k8sNodeInitialEventsExpression,
k8sNodeInitialLogTracesExpression,
nodeWidgetInfo,
} from './constants';
import {
@@ -28,66 +31,93 @@ function K8sNodesList({
}: {
controlListPrefix?: React.ReactNode;
}): JSX.Element {
const { featureFlags } = useAppContext();
const dotMetricsEnabled =
featureFlags?.find((flag) => flag.name === FeatureKeys.DOT_METRICS_ENABLED)
?.active || false;
const fetchListData = useCallback(
async (filters: K8sBaseFilters, signal?: AbortSignal) => {
filters.orderBy ||= {
columnName: 'cpu',
order: 'desc',
};
try {
const response = await listNodes(
{
filter: { expression: filters.filter.expression },
groupBy: filters.groupBy?.map((g) => ({ name: g.name })),
offset: filters.offset,
limit: filters.limit ?? 10,
start: filters.start,
end: filters.end,
orderBy: filters.orderBy
? {
key: { name: filters.orderBy.key.name },
direction:
filters.orderBy.direction === 'asc'
? Querybuildertypesv5OrderDirectionDTO.asc
: Querybuildertypesv5OrderDirectionDTO.desc,
}
: undefined,
},
signal,
);
const response = await getK8sNodesList(
filters,
signal,
undefined,
dotMetricsEnabled,
);
return {
data: response.payload?.data.records || [],
total: response.payload?.data.total || 0,
error: response.error,
rawData: response.payload?.data,
};
const data = response.data;
return {
type:
data.type === InframonitoringtypesResponseTypeDTO.grouped_list
? ('grouped_list' as const)
: ('list' as const),
records: data.records,
total: data.total,
endTimeBeforeRetention: data.endTimeBeforeRetention,
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,
};
}
},
[dotMetricsEnabled],
[],
);
const fetchEntityData = useCallback(
async (
filters: K8sDetailsFilters,
signal?: AbortSignal,
): Promise<{ data: K8sNodeData | null; error?: string | null }> => {
const response = await getK8sNodesList(
{
filters: filters.filters,
start: filters.start,
end: filters.end,
limit: 1,
offset: 0,
},
signal,
undefined,
dotMetricsEnabled,
);
): Promise<{
data: InframonitoringtypesNodeRecordDTO | null;
error?: string | null;
}> => {
try {
const response = await listNodes(
{
filter: { expression: filters.filter.expression },
start: filters.start,
end: filters.end,
limit: 1,
offset: 0,
},
signal,
);
const records = response.payload?.data.records || [];
return {
data: records.length > 0 ? records[0] : null,
error: response.error,
};
return {
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,
};
}
},
[dotMetricsEnabled],
[],
);
return (
<>
<K8sBaseList<K8sNodeData>
<K8sBaseList<InframonitoringtypesNodeRecordDTO>
controlListPrefix={controlListPrefix}
entity={InfraMonitoringEntity.NODES}
tableColumns={k8sNodesColumnsConfig}
@@ -97,14 +127,14 @@ function K8sNodesList({
eventCategory={InfraMonitoringEvents.Node}
/>
<K8sBaseDetails<K8sNodeData>
<K8sBaseDetails<InframonitoringtypesNodeRecordDTO>
category={InfraMonitoringEntity.NODES}
eventCategory={InfraMonitoringEvents.Node}
getSelectedItemFilters={k8sNodeGetSelectedItemFilters}
getSelectedItemExpression={k8sNodeGetSelectedItemExpression}
fetchEntityData={fetchEntityData}
getEntityName={k8sNodeGetEntityName}
getInitialLogTracesFilters={k8sNodeInitialLogTracesFilter}
getInitialEventsFilters={k8sNodeInitialEventsFilter}
getInitialLogTracesExpression={k8sNodeInitialLogTracesExpression}
getInitialEventsExpression={k8sNodeInitialEventsExpression}
metadataConfig={k8sNodeDetailsMetadataConfig}
entityWidgetInfo={nodeWidgetInfo}
getEntityQueryPayload={getNodeMetricsQueryPayload}

View File

@@ -1,129 +0,0 @@
import axios from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { UnderscoreToDotMap } from 'api/utils';
import { AxiosError } from 'axios';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
import { K8sBaseFilters } from '../Base/types';
export interface K8sNodesListPayload {
filters: TagFilter;
groupBy?: BaseAutocompleteData[];
offset?: number;
limit?: number;
orderBy?: {
columnName: string;
order: 'asc' | 'desc';
};
}
export interface K8sNodeData {
nodeUID: string;
nodeCPUUsage: number;
nodeCPUAllocatable: number;
nodeMemoryUsage: number;
nodeMemoryAllocatable: number;
meta: {
k8s_node_name: string;
k8s_node_uid: string;
k8s_cluster_name: string;
};
}
export interface K8sNodesListResponse {
status: string;
data: {
type: string;
records: K8sNodeData[];
groups: null;
total: number;
sentAnyHostMetricsData: boolean;
isSendingK8SAgentMetrics: boolean;
};
}
// TODO(H4ad): Erase this whole file when migrating to openapi
export const nodesMetaMap = [
{ dot: 'k8s.node.name', under: 'k8s_node_name' },
{ dot: 'k8s.node.uid', under: 'k8s_node_uid' },
{ dot: 'k8s.cluster.name', under: 'k8s_cluster_name' },
] as const;
export function mapNodesMeta(
raw: Record<string, unknown>,
): K8sNodeData['meta'] {
const out: Record<string, unknown> = { ...raw };
nodesMetaMap.forEach(({ dot, under }) => {
if (dot in raw) {
const v = raw[dot];
out[under] = typeof v === 'string' ? v : raw[under];
}
});
return out as K8sNodeData['meta'];
}
export const getK8sNodesList = async (
props: K8sBaseFilters,
signal?: AbortSignal,
headers?: Record<string, string>,
dotMetricsEnabled = false,
): Promise<SuccessResponse<K8sNodesListResponse> | ErrorResponse> => {
try {
const requestProps =
dotMetricsEnabled && Array.isArray(props.filters?.items)
? {
...props,
filters: {
...props.filters,
items: props.filters.items.reduce<typeof props.filters.items>(
(acc, item) => {
if (item.value === undefined) {
return acc;
}
if (
item.key &&
typeof item.key === 'object' &&
'key' in item.key &&
typeof item.key.key === 'string'
) {
const mappedKey = UnderscoreToDotMap[item.key.key] ?? item.key.key;
acc.push({
...item,
key: { ...item.key, key: mappedKey },
});
} else {
acc.push(item);
}
return acc;
},
[] as typeof props.filters.items,
),
},
}
: props;
const response = await axios.post('/nodes/list', requestProps, {
signal,
headers,
});
const payload: K8sNodesListResponse = response.data;
// one-liner to map dot→underscore
payload.data.records = payload.data.records.map((record) => ({
...record,
meta: mapNodesMeta(record.meta as Record<string, unknown>),
}));
return {
statusCode: 200,
error: null,
message: 'Success',
payload,
params: requestProps,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);
}
};

View File

@@ -1,67 +1,43 @@
import { InframonitoringtypesNodeRecordDTO } from 'api/generated/services/sigNoz.schemas';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults';
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
import { EQueryType } from 'types/common/dashboard';
import { DataSource, ReduceOperators } from 'types/common/queryBuilder';
import { v4 } from 'uuid';
import {
createFilterItem,
K8sDetailsMetadataConfig,
} from '../Base/K8sBaseDetails';
import { QUERY_KEYS } from '../EntityDetailsUtils/utils';
import { K8sNodeData } from './api';
import { formatValueForExpression } from 'components/QueryBuilderV2/utils';
export const k8sNodeGetSelectedItemFilters = (
import { K8sDetailsMetadataConfig } from '../Base/K8sBaseDetails';
import { INFRA_MONITORING_ATTR_KEYS } from '../constants';
export const k8sNodeGetSelectedItemExpression = (
selectedItemId: string,
): TagFilter => {
return {
op: 'AND',
items: [
{
id: 'k8s_node_name',
key: {
key: 'k8s_node_name',
type: null,
},
op: '=',
value: selectedItemId,
},
],
};
};
): string => `k8s.node.name = ${formatValueForExpression(selectedItemId)}`;
export const k8sNodeDetailsMetadataConfig: K8sDetailsMetadataConfig<K8sNodeData>[] =
export const k8sNodeDetailsMetadataConfig: K8sDetailsMetadataConfig<InframonitoringtypesNodeRecordDTO>[] =
[
{ label: 'Node Name', getValue: (p): string => p.meta.k8s_node_name },
{ label: 'Node Name', getValue: (p): string => p.nodeName || '' },
{
label: 'Cluster Name',
getValue: (p): string => p.meta.k8s_cluster_name,
getValue: (p): string =>
p.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_CLUSTER_NAME] || '',
},
];
export const k8sNodeInitialFilters = [
QUERY_KEYS.K8S_NODE_NAME,
QUERY_KEYS.K8S_CLUSTER_NAME,
];
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 || '')}`;
export const k8sNodeInitialEventsFilter = (
item: K8sNodeData,
): ReturnType<typeof createFilterItem>[] => [
createFilterItem(QUERY_KEYS.K8S_OBJECT_KIND, 'Node'),
createFilterItem(QUERY_KEYS.K8S_OBJECT_NAME, item.meta.k8s_node_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] || '')}`;
export const k8sNodeInitialLogTracesFilter = (
item: K8sNodeData,
): ReturnType<typeof createFilterItem>[] => [
createFilterItem(QUERY_KEYS.K8S_NODE_NAME, item.meta.k8s_node_name),
createFilterItem(QUERY_KEYS.K8S_CLUSTER_NAME, item.meta.k8s_cluster_name),
];
export const k8sNodeGetEntityName = (item: K8sNodeData): string =>
item.meta.k8s_node_name;
export const k8sNodeGetEntityName = (
item: InframonitoringtypesNodeRecordDTO,
): string => item.nodeName || '';
export const nodeWidgetInfo = [
{
@@ -107,7 +83,7 @@ export const nodeWidgetInfo = [
];
export const getNodeMetricsQueryPayload = (
node: K8sNodeData,
node: InframonitoringtypesNodeRecordDTO,
start: number,
end: number,
dotMetricsEnabled: boolean,
@@ -222,7 +198,7 @@ export const getNodeMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: node.meta.k8s_node_name,
value: node.nodeName,
},
],
op: 'AND',
@@ -261,7 +237,7 @@ export const getNodeMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: node.meta.k8s_node_name,
value: node.nodeName,
},
],
op: 'AND',
@@ -300,7 +276,7 @@ export const getNodeMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: node.meta.k8s_node_name,
value: node.nodeName,
},
],
op: 'AND',
@@ -339,7 +315,7 @@ export const getNodeMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: node.meta.k8s_node_name,
value: node.nodeName,
},
],
op: 'AND',
@@ -378,7 +354,7 @@ export const getNodeMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: node.meta.k8s_node_name,
value: node.nodeName,
},
],
op: 'AND',
@@ -451,7 +427,7 @@ export const getNodeMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: node.meta.k8s_node_name,
value: node.nodeName,
},
],
op: 'AND',
@@ -490,7 +466,7 @@ export const getNodeMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: node.meta.k8s_node_name,
value: node.nodeName,
},
],
op: 'AND',
@@ -529,7 +505,7 @@ export const getNodeMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: node.meta.k8s_node_name,
value: node.nodeName,
},
],
op: 'AND',
@@ -568,7 +544,7 @@ export const getNodeMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: node.meta.k8s_node_name,
value: node.nodeName,
},
],
op: 'AND',
@@ -607,7 +583,7 @@ export const getNodeMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: node.meta.k8s_node_name,
value: node.nodeName,
},
],
op: 'AND',
@@ -646,7 +622,7 @@ export const getNodeMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: node.meta.k8s_node_name,
value: node.nodeName,
},
],
op: 'AND',
@@ -685,7 +661,7 @@ export const getNodeMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: node.meta.k8s_node_name,
value: node.nodeName,
},
],
op: 'AND',
@@ -758,7 +734,7 @@ export const getNodeMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: node.meta.k8s_node_name,
value: node.nodeName,
},
],
op: 'AND',
@@ -797,7 +773,7 @@ export const getNodeMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: node.meta.k8s_node_name,
value: node.nodeName,
},
],
op: 'AND',
@@ -836,7 +812,7 @@ export const getNodeMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: node.meta.k8s_node_name,
value: node.nodeName,
},
],
op: 'AND',
@@ -922,7 +898,7 @@ export const getNodeMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: node.meta.k8s_node_name,
value: node.nodeName,
},
],
op: 'AND',
@@ -961,7 +937,7 @@ export const getNodeMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: node.meta.k8s_node_name,
value: node.nodeName,
},
],
op: 'AND',
@@ -1000,7 +976,7 @@ export const getNodeMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: node.meta.k8s_node_name,
value: node.nodeName,
},
],
op: 'AND',
@@ -1086,7 +1062,7 @@ export const getNodeMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: node.meta.k8s_node_name,
value: node.nodeName,
},
],
op: 'AND',
@@ -1166,7 +1142,7 @@ export const getNodeMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: node.meta.k8s_node_name,
value: node.nodeName,
},
],
op: 'AND',
@@ -1246,7 +1222,7 @@ export const getNodeMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: node.meta.k8s_node_name,
value: node.nodeName,
},
],
op: 'AND',
@@ -1332,7 +1308,7 @@ export const getNodeMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: node.meta.k8s_node_name,
value: node.nodeName,
},
],
op: 'AND',
@@ -1418,7 +1394,7 @@ export const getNodeMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: node.meta.k8s_node_name,
value: node.nodeName,
},
],
op: 'AND',
@@ -1457,7 +1433,7 @@ export const getNodeMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: node.meta.k8s_node_name,
value: node.nodeName,
},
],
op: 'AND',
@@ -1496,7 +1472,7 @@ export const getNodeMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: node.meta.k8s_node_name,
value: node.nodeName,
},
],
op: 'AND',
@@ -1569,7 +1545,7 @@ export const getNodeMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: node.meta.k8s_node_name,
value: node.nodeName,
},
],
op: 'AND',
@@ -1608,7 +1584,7 @@ export const getNodeMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: node.meta.k8s_node_name,
value: node.nodeName,
},
],
op: 'AND',

View File

@@ -1,38 +1,56 @@
import { Tooltip } from 'antd';
import { TableColumnDef } from 'components/TanStackTableView';
import TanStackTable from 'components/TanStackTableView';
import { Color } from '@signozhq/design-tokens';
import { Badge, BadgeColor } from '@signozhq/ui/badge';
import { InframonitoringtypesNodeRecordDTO } from 'api/generated/services/sigNoz.schemas';
import TanStackTable, { TableColumnDef } from 'components/TanStackTableView';
import { ExpandButtonWrapper } from 'container/InfraMonitoringK8sV2/components';
import ColumnHeader from '../Base/ColumnHeader';
import EntityGroupHeader from '../Base/EntityGroupHeader';
import K8sGroupCell from '../Base/K8sGroupCell';
import { formatBytes } from '../commonUtils';
import { ValidateColumnValueWrapper } from '../components';
import { formatBytes, getPodPhaseStatusItems } from '../commonUtils';
import { INFRA_MONITORING_ATTR_KEYS } from '../constants';
import {
CellValueTooltip,
GroupedStatusCounts,
ValidateColumnValueWrapper,
} from '../components';
import { InfraMonitoringEntity } from '../constants';
import { K8sNodeData, K8sNodesListPayload } from './api';
import { Workflow } from '@signozhq/icons';
export function getK8sNodeRowKey(node: K8sNodeData): string {
return node.nodeUID || node.meta.k8s_node_uid || node.meta.k8s_node_name;
export function getK8sNodeRowKey(
node: InframonitoringtypesNodeRecordDTO,
): string {
return (
node.nodeName || node.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NODE_UID] || ''
);
}
export function getK8sNodeItemKey(node: K8sNodeData): string {
return node.meta.k8s_node_name;
export function getK8sNodeItemKey(
node: InframonitoringtypesNodeRecordDTO,
): string {
return node.nodeName;
}
export const getK8sNodesListQuery = (): K8sNodesListPayload => ({
filters: {
items: [],
op: 'and',
},
orderBy: { columnName: 'cpu', order: 'desc' },
});
const NODE_CONDITION_COLORS: Record<string, BadgeColor> = {
ready: 'forest',
not_ready: 'amber',
no_data: 'secondary',
};
export const k8sNodesColumnsConfig: TableColumnDef<K8sNodeData>[] = [
const NODE_CONDITION_LABEL_MAP: Record<string, string> = {
ready: 'Ready',
not_ready: 'Not Ready',
no_data: 'No Data',
};
export type NodeTableColumnConfig =
TableColumnDef<InframonitoringtypesNodeRecordDTO>;
export const k8sNodesColumnsConfig: NodeTableColumnConfig[] = [
{
id: 'nodeGroup',
header: (): React.ReactNode => <EntityGroupHeader title="NODE GROUP" />,
accessorFn: (row): string => row.meta.k8s_node_name || '',
width: { min: 300 },
header: (): React.ReactNode => <EntityGroupHeader title="Node Group" />,
accessorFn: (row): string => row.nodeName || '',
width: { min: 290 },
enableSort: false,
enableRemove: false,
enableMove: false,
@@ -55,9 +73,10 @@ export const k8sNodesColumnsConfig: TableColumnDef<K8sNodeData>[] = [
<EntityGroupHeader
title="Node Name"
icon={<Workflow data-hide-expanded="true" size={14} />}
docPath="/infrastructure-monitoring/kubernetes/nodes#node-name"
/>
),
accessorFn: (row): string => row.meta.k8s_node_name || '',
accessorFn: (row): string => row.nodeName || '',
width: { min: 290 },
enableSort: false,
enableRemove: false,
@@ -67,65 +86,146 @@ export const k8sNodesColumnsConfig: TableColumnDef<K8sNodeData>[] = [
cell: ({ value }): React.ReactNode => {
const nodeName = value as string;
return (
<Tooltip title={nodeName}>
<CellValueTooltip value={nodeName}>
<TanStackTable.Text>{nodeName}</TanStackTable.Text>
</Tooltip>
</CellValueTooltip>
);
},
},
{
id: 'condition',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/nodes#node-condition">
Status
</ColumnHeader>
),
accessorFn: (row): string => row.condition,
width: { min: 120 },
enableSort: false,
cell: ({ row, groupMeta }): React.ReactNode => {
if (!groupMeta) {
const color =
NODE_CONDITION_COLORS[row.condition] || NODE_CONDITION_COLORS.no_data;
return (
<Badge color={color} variant="outline">
{NODE_CONDITION_LABEL_MAP[row.condition] || 'Unknown'}
</Badge>
);
}
return (
<GroupedStatusCounts
items={[
{
value: row.nodeCountsByReadiness?.ready ?? 0,
label: 'Ready',
color: Color.BG_FOREST_500,
},
{
value: row.nodeCountsByReadiness?.notReady ?? 0,
label: 'Not Ready',
color: Color.BG_AMBER_500,
},
]}
/>
);
},
},
{
id: 'podCountsByPhase',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/nodes#pod-counts-by-phase">
Pod Phases
</ColumnHeader>
),
accessorFn: (row): InframonitoringtypesNodeRecordDTO['podCountsByPhase'] =>
row.podCountsByPhase,
width: { min: 250 },
enableSort: false,
cell: ({ row }): React.ReactNode => {
const podCountsByPhase = row.podCountsByPhase;
if (!podCountsByPhase) {
return <TanStackTable.Text>-</TanStackTable.Text>;
}
return (
<GroupedStatusCounts items={getPodPhaseStatusItems(row.podCountsByPhase)} />
);
},
},
{
id: 'clusterName',
header: 'Cluster Name',
accessorFn: (row): string => row.meta.k8s_cluster_name || '',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/nodes#cluster-name">
Cluster Name
</ColumnHeader>
),
accessorFn: (row): string =>
row.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_CLUSTER_NAME] || '',
width: { min: 150, default: 150 },
enableSort: false,
defaultVisibility: false,
cell: ({ value }): React.ReactNode => (
<TanStackTable.Text>{value as string}</TanStackTable.Text>
),
},
{
id: 'cpu',
header: 'CPU Usage (cores)',
accessorFn: (row): number => row.nodeCPUUsage,
width: { min: 200, default: 200 },
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/nodes#cpu-usage-cores">
CPU Usage
<br /> (cores)
</ColumnHeader>
),
accessorFn: (row): number => row.nodeCPU,
width: { min: 160, default: 160 },
enableSort: true,
cell: ({ value }): React.ReactNode => {
const cpu = value as number;
const cpu = Number(value);
return (
<ValidateColumnValueWrapper
value={cpu}
entity={InfraMonitoringEntity.NODES}
attribute="CPU metric"
>
<TanStackTable.Text>{cpu}</TanStackTable.Text>
<TanStackTable.Text>{cpu.toFixed(2)}</TanStackTable.Text>
</ValidateColumnValueWrapper>
);
},
},
{
id: 'cpu_allocatable',
header: 'CPU Alloc (cores)',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/nodes#cpu-alloc-cores">
CPU Allocatable
<br /> (cores)
</ColumnHeader>
),
accessorFn: (row): number => row.nodeCPUAllocatable,
width: { min: 200, default: 200 },
width: { min: 160, default: 190 },
enableSort: true,
cell: ({ value }): React.ReactNode => {
const cpuAllocatable = value as number;
const cpuAllocatable = Number(value);
return (
<ValidateColumnValueWrapper
value={cpuAllocatable}
entity={InfraMonitoringEntity.NODES}
attribute="CPU allocatable metric"
>
<TanStackTable.Text>{cpuAllocatable}</TanStackTable.Text>
<TanStackTable.Text>{cpuAllocatable.toFixed(2)}</TanStackTable.Text>
</ValidateColumnValueWrapper>
);
},
},
{
id: 'memory',
header: 'Memory Usage (WSS)',
accessorFn: (row): number => row.nodeMemoryUsage,
width: { min: 240, default: 240 },
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/nodes#memory-usage-wss">
Memory Usage
<br /> (WSS)
</ColumnHeader>
),
accessorFn: (row): number => row.nodeMemory,
width: { min: 220, default: 220 },
enableSort: true,
cell: ({ value }): React.ReactNode => {
const memory = value as number;
@@ -142,9 +242,14 @@ export const k8sNodesColumnsConfig: TableColumnDef<K8sNodeData>[] = [
},
{
id: 'memory_allocatable',
header: 'Memory Allocatable',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/nodes#memory-allocatable">
Memory
<br /> Allocatable
</ColumnHeader>
),
accessorFn: (row): number => row.nodeMemoryAllocatable,
width: { min: 240, default: 240 },
width: { min: 200, default: 200 },
enableSort: true,
cell: ({ value }): React.ReactNode => {
const memoryAllocatable = value as number;

View File

@@ -1,20 +1,23 @@
import React, { useCallback } from 'react';
import { useCallback } from 'react';
import { listPods } from 'api/generated/services/inframonitoring';
import {
InframonitoringtypesPodRecordDTO,
InframonitoringtypesResponseTypeDTO,
Querybuildertypesv5OrderDirectionDTO,
} from 'api/generated/services/sigNoz.schemas';
import { InfraMonitoringEvents } from 'constants/events';
import { FeatureKeys } from 'constants/features';
import { useAppContext } from 'providers/App/App';
import K8sBaseDetails, { K8sDetailsFilters } from '../Base/K8sBaseDetails';
import { K8sBaseList } from '../Base/K8sBaseList';
import { K8sBaseFilters } from '../Base/types';
import { InfraMonitoringEntity } from '../constants';
import { getK8sPodsList, K8sPodsData } from './api';
import {
getPodMetricsQueryPayload,
k8sPodDetailsMetadataConfig,
k8sPodGetEntityName,
k8sPodGetSelectedItemFilters,
k8sPodInitialEventsFilter,
k8sPodInitialLogTracesFilter,
k8sPodGetSelectedItemExpression,
k8sPodInitialEventsExpression,
k8sPodInitialLogTracesExpression,
podWidgetInfo,
} from './constants';
import {
@@ -28,66 +31,93 @@ function K8sPodsList({
}: {
controlListPrefix?: React.ReactNode;
}): JSX.Element {
const { featureFlags } = useAppContext();
const dotMetricsEnabled =
featureFlags?.find((flag) => flag.name === FeatureKeys.DOT_METRICS_ENABLED)
?.active || false;
const fetchListData = useCallback(
async (filters: K8sBaseFilters, signal?: AbortSignal) => {
filters.orderBy ||= {
columnName: 'cpu',
order: 'desc',
};
try {
const response = await listPods(
{
filter: { expression: filters.filter.expression },
groupBy: filters.groupBy?.map((g) => ({ name: g.name })),
offset: filters.offset,
limit: filters.limit ?? 10,
start: filters.start,
end: filters.end,
orderBy: filters.orderBy
? {
key: { name: filters.orderBy.key.name },
direction:
filters.orderBy.direction === 'asc'
? Querybuildertypesv5OrderDirectionDTO.asc
: Querybuildertypesv5OrderDirectionDTO.desc,
}
: undefined,
},
signal,
);
const response = await getK8sPodsList(
filters,
signal,
undefined,
dotMetricsEnabled,
);
return {
data: response.payload?.data.records || [],
total: response.payload?.data.total || 0,
error: response.error,
rawData: response.payload?.data,
};
const data = response.data;
return {
type:
data.type === InframonitoringtypesResponseTypeDTO.grouped_list
? ('grouped_list' as const)
: ('list' as const),
records: data.records,
total: data.total,
endTimeBeforeRetention: data.endTimeBeforeRetention,
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,
};
}
},
[dotMetricsEnabled],
[],
);
const fetchEntityData = useCallback(
async (
filters: K8sDetailsFilters,
signal?: AbortSignal,
): Promise<{ data: K8sPodsData | null; error?: string | null }> => {
const response = await getK8sPodsList(
{
filters: filters.filters,
start: filters.start,
end: filters.end,
limit: 1,
offset: 0,
},
signal,
undefined,
dotMetricsEnabled,
);
): Promise<{
data: InframonitoringtypesPodRecordDTO | null;
error?: string | null;
}> => {
try {
const response = await listPods(
{
filter: { expression: filters.filter.expression },
start: filters.start,
end: filters.end,
limit: 1,
offset: 0,
},
signal,
);
const records = response.payload?.data.records || [];
return {
data: records.length > 0 ? records[0] : null,
error: response.error,
};
return {
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,
};
}
},
[dotMetricsEnabled],
[],
);
return (
<>
<K8sBaseList<K8sPodsData>
<K8sBaseList<InframonitoringtypesPodRecordDTO>
controlListPrefix={controlListPrefix}
entity={InfraMonitoringEntity.PODS}
tableColumns={k8sPodColumnsConfig}
@@ -97,14 +127,14 @@ function K8sPodsList({
eventCategory={InfraMonitoringEvents.Pod}
/>
<K8sBaseDetails<K8sPodsData>
<K8sBaseDetails<InframonitoringtypesPodRecordDTO>
category={InfraMonitoringEntity.PODS}
eventCategory={InfraMonitoringEvents.Pod}
getSelectedItemFilters={k8sPodGetSelectedItemFilters}
getSelectedItemExpression={k8sPodGetSelectedItemExpression}
fetchEntityData={fetchEntityData}
getEntityName={k8sPodGetEntityName}
getInitialLogTracesFilters={k8sPodInitialLogTracesFilter}
getInitialEventsFilters={k8sPodInitialEventsFilter}
getInitialLogTracesExpression={k8sPodInitialLogTracesExpression}
getInitialEventsExpression={k8sPodInitialEventsExpression}
metadataConfig={k8sPodDetailsMetadataConfig}
entityWidgetInfo={podWidgetInfo}
getEntityQueryPayload={getPodMetricsQueryPayload}

View File

@@ -1,148 +0,0 @@
import axios from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { UnderscoreToDotMap } from 'api/utils';
import { AxiosError } from 'axios';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { K8sBaseFilters } from '../Base/types';
// TODO(H4ad): Erase this whole file when migrating to openapi
export const podsMetaMap = [
{ dot: 'k8s.cronjob.name', under: 'k8s_cronjob_name' },
{ dot: 'k8s.daemonset.name', under: 'k8s_daemonset_name' },
{ dot: 'k8s.deployment.name', under: 'k8s_deployment_name' },
{ dot: 'k8s.job.name', under: 'k8s_job_name' },
{ dot: 'k8s.namespace.name', under: 'k8s_namespace_name' },
{ dot: 'k8s.node.name', under: 'k8s_node_name' },
{ dot: 'k8s.pod.name', under: 'k8s_pod_name' },
{ dot: 'k8s.pod.uid', under: 'k8s_pod_uid' },
{ dot: 'k8s.statefulset.name', under: 'k8s_statefulset_name' },
{ dot: 'k8s.cluster.name', under: 'k8s_cluster_name' },
] as const;
export function mapPodsMeta(raw: Record<string, unknown>): K8sPodsData['meta'] {
// clone everything
const out: Record<string, unknown> = { ...raw };
// overlay only the dot→under mappings
podsMetaMap.forEach(({ dot, under }) => {
if (dot in raw) {
const v = raw[dot];
out[under] = typeof v === 'string' ? v : raw[under];
}
});
return out as K8sPodsData['meta'];
}
export interface TimeSeriesValue {
timestamp: number;
value: string;
}
export interface TimeSeries {
labels: Record<string, string>;
labelsArray: Array<Record<string, string>>;
values: TimeSeriesValue[];
}
export interface K8sPodsData {
podUID: string;
podCPU: number;
podCPURequest: number;
podCPULimit: number;
podMemory: number;
podMemoryRequest: number;
podMemoryLimit: number;
restartCount: number;
meta: {
k8s_cronjob_name: string;
k8s_daemonset_name: string;
k8s_deployment_name: string;
k8s_job_name: string;
k8s_namespace_name: string;
k8s_node_name: string;
k8s_pod_name: string;
k8s_pod_uid: string;
k8s_statefulset_name: string;
k8s_cluster_name: string;
};
countByPhase: {
pending: number;
running: number;
succeeded: number;
failed: number;
unknown: number;
};
}
export interface K8sPodsListResponse {
status: string;
data: {
type: string;
records: K8sPodsData[];
groups: null;
total: number;
sentAnyHostMetricsData: boolean;
isSendingK8SAgentMetrics: boolean;
};
}
export const getK8sPodsList = async (
props: K8sBaseFilters,
signal?: AbortSignal,
headers?: Record<string, string>,
dotMetricsEnabled = false,
): Promise<SuccessResponse<K8sPodsListResponse> | ErrorResponse> => {
try {
const requestProps = dotMetricsEnabled
? {
...props,
filters: {
...props.filters,
items: props.filters?.items.reduce<typeof props.filters.items>(
(acc, item) => {
if (item.value === undefined) {
return acc;
}
if (
item.key &&
typeof item.key === 'object' &&
'key' in item.key &&
typeof item.key.key === 'string'
) {
const mappedKey = UnderscoreToDotMap[item.key.key] ?? item.key.key;
acc.push({
...item,
key: { ...item.key, key: mappedKey },
});
} else {
acc.push(item);
}
return acc;
},
[] as typeof props.filters.items,
),
},
}
: props;
const response = await axios.post('/pods/list', requestProps, {
signal,
headers,
});
const payload: K8sPodsListResponse = response.data;
payload.data.records = payload.data.records.map((record) => ({
...record,
meta: mapPodsMeta(record.meta as Record<string, unknown>),
}));
return {
statusCode: 200,
error: null,
message: 'Success',
payload,
params: requestProps,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);
}
};

View File

@@ -1,62 +1,59 @@
import { InframonitoringtypesPodRecordDTO } from 'api/generated/services/sigNoz.schemas';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults';
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
import { EQueryType } from 'types/common/dashboard';
import { DataSource, ReduceOperators } from 'types/common/queryBuilder';
import { v4 } from 'uuid';
import {
createFilterItem,
K8sDetailsMetadataConfig,
} from '../Base/K8sBaseDetails';
import { QUERY_KEYS } from '../EntityDetailsUtils/utils';
import { K8sPodsData } from './api';
import { K8sDetailsMetadataConfig } from '../Base/K8sBaseDetails';
import { formatValueForExpression } from 'components/QueryBuilderV2/utils';
import { INFRA_MONITORING_ATTR_KEYS } from '../constants';
export const k8sPodGetSelectedItemFilters = (
export const k8sPodGetSelectedItemExpression = (
selectedItemId: string,
): TagFilter => {
return {
op: 'AND',
items: [
{
id: 'k8s_pod_uid',
key: {
key: 'k8s_pod_uid',
type: null,
},
op: '=',
value: selectedItemId,
},
],
};
};
): string => `k8s.pod.uid = ${formatValueForExpression(selectedItemId)}`;
export const k8sPodDetailsMetadataConfig: K8sDetailsMetadataConfig<K8sPodsData>[] =
export const k8sPodDetailsMetadataConfig: K8sDetailsMetadataConfig<InframonitoringtypesPodRecordDTO>[] =
[
{ label: 'NAMESPACE', getValue: (p): string => p.meta.k8s_namespace_name },
{
label: 'NAMESPACE',
getValue: (p): string =>
p.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] || '',
},
{
label: 'Cluster Name',
getValue: (p): string => p.meta.k8s_cluster_name,
getValue: (p): string =>
p.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_CLUSTER_NAME] || '',
},
{
label: 'Node',
getValue: (p): string =>
p.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NODE_NAME] || '',
},
{ label: 'Node', getValue: (p): string => p.meta.k8s_node_name },
];
export const k8sPodInitialEventsFilter = (
pod: K8sPodsData,
): ReturnType<typeof createFilterItem>[] => [
createFilterItem(QUERY_KEYS.K8S_OBJECT_KIND, 'Pod'),
createFilterItem(QUERY_KEYS.K8S_OBJECT_NAME, pod.meta.k8s_pod_name),
];
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}`;
};
export const k8sPodInitialLogTracesFilter = (
pod: K8sPodsData,
): ReturnType<typeof createFilterItem>[] => [
createFilterItem(QUERY_KEYS.K8S_POD_NAME, pod.meta.k8s_pod_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}`;
};
export const k8sPodGetEntityName = (pod: K8sPodsData): string =>
pod.meta.k8s_pod_name;
export const k8sPodGetEntityName = (
pod: InframonitoringtypesPodRecordDTO,
): string => pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_POD_NAME] || '';
export const podWidgetInfo = [
{
@@ -114,7 +111,7 @@ export const podWidgetInfo = [
];
export const getPodMetricsQueryPayload = (
pod: K8sPodsData,
pod: InframonitoringtypesPodRecordDTO,
start: number,
end: number,
dotMetricsEnabled: boolean,
@@ -224,9 +221,15 @@ export const getPodMetricsQueryPayload = (
'k8s_pod_filesystem_usage',
);
const k8sPodNameKey = getKey('k8s.pod.name', 'k8s_pod_name');
const k8sPodNameKey = getKey(
INFRA_MONITORING_ATTR_KEYS.K8S_POD_NAME,
'k8s_pod_name',
);
const k8sNamespaceNameKey = getKey('k8s.namespace.name', 'k8s_namespace_name');
const k8sNamespaceNameKey = getKey(
INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME,
'k8s_namespace_name',
);
return [
{
selectedTime: 'GLOBAL_TIME',
@@ -256,7 +259,7 @@ export const getPodMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: pod.meta.k8s_pod_name,
value: pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_POD_NAME] || '',
},
{
id: '067b2dc4',
@@ -267,7 +270,8 @@ export const getPodMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: pod.meta.k8s_namespace_name,
value:
pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] || '',
},
],
op: 'AND',
@@ -306,7 +310,7 @@ export const getPodMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: pod.meta.k8s_pod_name,
value: pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_POD_NAME] || '',
},
{
id: '379af416',
@@ -317,7 +321,8 @@ export const getPodMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: pod.meta.k8s_namespace_name,
value:
pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] || '',
},
],
op: 'AND',
@@ -356,7 +361,7 @@ export const getPodMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: pod.meta.k8s_pod_name,
value: pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_POD_NAME] || '',
},
{
id: '39ee0dbd',
@@ -367,7 +372,8 @@ export const getPodMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: pod.meta.k8s_namespace_name,
value:
pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] || '',
},
],
op: 'AND',
@@ -440,7 +446,8 @@ export const getPodMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: pod.meta.k8s_namespace_name,
value:
pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] || '',
},
{
id: '755c8a9d',
@@ -451,7 +458,7 @@ export const getPodMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: pod.meta.k8s_pod_name,
value: pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_POD_NAME] || '',
},
],
op: 'AND',
@@ -490,7 +497,8 @@ export const getPodMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: pod.meta.k8s_namespace_name,
value:
pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] || '',
},
{
id: '1e3d01ee',
@@ -501,7 +509,7 @@ export const getPodMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: pod.meta.k8s_pod_name,
value: pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_POD_NAME] || '',
},
],
op: 'AND',
@@ -540,7 +548,8 @@ export const getPodMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: pod.meta.k8s_namespace_name,
value:
pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] || '',
},
{
id: '0c8b2662',
@@ -551,7 +560,7 @@ export const getPodMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: pod.meta.k8s_pod_name,
value: pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_POD_NAME] || '',
},
],
op: 'AND',
@@ -590,7 +599,8 @@ export const getPodMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: pod.meta.k8s_namespace_name,
value:
pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] || '',
},
{
id: 'e915da76',
@@ -601,7 +611,7 @@ export const getPodMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: pod.meta.k8s_pod_name,
value: pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_POD_NAME] || '',
},
],
op: 'AND',
@@ -640,7 +650,8 @@ export const getPodMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: pod.meta.k8s_namespace_name,
value:
pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] || '',
},
{
id: '32c15c03',
@@ -651,7 +662,7 @@ export const getPodMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: pod.meta.k8s_pod_name,
value: pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_POD_NAME] || '',
},
],
op: 'AND',
@@ -690,7 +701,8 @@ export const getPodMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: pod.meta.k8s_namespace_name,
value:
pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] || '',
},
{
id: '703fced1',
@@ -701,7 +713,7 @@ export const getPodMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: pod.meta.k8s_pod_name,
value: pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_POD_NAME] || '',
},
],
op: 'AND',
@@ -774,7 +786,8 @@ export const getPodMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: pod.meta.k8s_namespace_name,
value:
pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] || '',
},
{
id: '5418405c',
@@ -785,7 +798,7 @@ export const getPodMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: pod.meta.k8s_pod_name,
value: pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_POD_NAME] || '',
},
],
op: 'AND',
@@ -824,7 +837,8 @@ export const getPodMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: pod.meta.k8s_namespace_name,
value:
pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] || '',
},
{
id: '1ea5c602',
@@ -835,7 +849,7 @@ export const getPodMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: pod.meta.k8s_pod_name,
value: pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_POD_NAME] || '',
},
],
op: 'AND',
@@ -874,7 +888,8 @@ export const getPodMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: pod.meta.k8s_namespace_name,
value:
pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] || '',
},
{
id: 'd7632974',
@@ -885,7 +900,7 @@ export const getPodMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: pod.meta.k8s_pod_name,
value: pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_POD_NAME] || '',
},
],
op: 'AND',
@@ -958,7 +973,8 @@ export const getPodMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: pod.meta.k8s_namespace_name,
value:
pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] || '',
},
{
id: '5a822bec',
@@ -969,7 +985,7 @@ export const getPodMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: pod.meta.k8s_pod_name,
value: pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_POD_NAME] || '',
},
],
op: 'AND',
@@ -1008,7 +1024,8 @@ export const getPodMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: pod.meta.k8s_namespace_name,
value:
pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] || '',
},
{
id: '3362c603',
@@ -1019,7 +1036,7 @@ export const getPodMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: pod.meta.k8s_pod_name,
value: pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_POD_NAME] || '',
},
],
op: 'AND',
@@ -1058,7 +1075,8 @@ export const getPodMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: pod.meta.k8s_namespace_name,
value:
pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] || '',
},
{
id: 'ce88008b',
@@ -1069,7 +1087,7 @@ export const getPodMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: pod.meta.k8s_pod_name,
value: pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_POD_NAME] || '',
},
],
op: 'AND',
@@ -1108,7 +1126,8 @@ export const getPodMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: pod.meta.k8s_namespace_name,
value:
pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] || '',
},
{
id: '53fc92fd',
@@ -1119,7 +1138,7 @@ export const getPodMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: pod.meta.k8s_pod_name,
value: pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_POD_NAME] || '',
},
],
op: 'AND',
@@ -1158,7 +1177,8 @@ export const getPodMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: pod.meta.k8s_namespace_name,
value:
pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] || '',
},
{
id: '61dfa9f6',
@@ -1169,7 +1189,7 @@ export const getPodMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: pod.meta.k8s_pod_name,
value: pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_POD_NAME] || '',
},
],
op: 'AND',
@@ -1208,7 +1228,8 @@ export const getPodMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: pod.meta.k8s_namespace_name,
value:
pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] || '',
},
{
id: '508cdf26',
@@ -1219,7 +1240,7 @@ export const getPodMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: pod.meta.k8s_pod_name,
value: pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_POD_NAME] || '',
},
],
op: 'AND',
@@ -1292,7 +1313,8 @@ export const getPodMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: pod.meta.k8s_namespace_name,
value:
pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] || '',
},
{
id: '80528f79',
@@ -1303,7 +1325,7 @@ export const getPodMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: pod.meta.k8s_pod_name,
value: pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_POD_NAME] || '',
},
],
op: 'AND',
@@ -1342,7 +1364,8 @@ export const getPodMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: pod.meta.k8s_namespace_name,
value:
pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] || '',
},
{
id: '780cc786',
@@ -1353,7 +1376,7 @@ export const getPodMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: pod.meta.k8s_pod_name,
value: pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_POD_NAME] || '',
},
],
op: 'AND',
@@ -1433,7 +1456,8 @@ export const getPodMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: pod.meta.k8s_namespace_name,
value:
pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] || '',
},
{
id: '8b2a539b',
@@ -1444,7 +1468,7 @@ export const getPodMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: pod.meta.k8s_pod_name,
value: pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_POD_NAME] || '',
},
],
op: 'AND',
@@ -1517,7 +1541,8 @@ export const getPodMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: pod.meta.k8s_namespace_name,
value:
pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] || '',
},
{
id: '4b4382be',
@@ -1528,7 +1553,7 @@ export const getPodMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: pod.meta.k8s_pod_name,
value: pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_POD_NAME] || '',
},
],
op: 'AND',
@@ -1608,7 +1633,8 @@ export const getPodMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: pod.meta.k8s_namespace_name,
value:
pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] || '',
},
{
id: '9301d7c0',
@@ -1619,7 +1645,7 @@ export const getPodMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: pod.meta.k8s_pod_name,
value: pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_POD_NAME] || '',
},
],
op: 'AND',
@@ -1665,7 +1691,8 @@ export const getPodMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: pod.meta.k8s_namespace_name,
value:
pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] || '',
},
{
id: '92b99374',
@@ -1676,7 +1703,7 @@ export const getPodMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: pod.meta.k8s_pod_name,
value: pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_POD_NAME] || '',
},
],
op: 'AND',
@@ -1722,7 +1749,8 @@ export const getPodMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: pod.meta.k8s_namespace_name,
value:
pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] || '',
},
{
id: '1c7de95d',
@@ -1733,7 +1761,7 @@ export const getPodMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: pod.meta.k8s_pod_name,
value: pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_POD_NAME] || '',
},
],
op: 'AND',
@@ -1826,7 +1854,8 @@ export const getPodMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: pod.meta.k8s_namespace_name,
value:
pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] || '',
},
{
id: '964fd905',
@@ -1837,7 +1866,7 @@ export const getPodMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: pod.meta.k8s_pod_name,
value: pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_POD_NAME] || '',
},
],
op: 'AND',
@@ -1883,7 +1912,8 @@ export const getPodMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: pod.meta.k8s_namespace_name,
value:
pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] || '',
},
{
id: '76a586be',
@@ -1894,7 +1924,7 @@ export const getPodMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: pod.meta.k8s_pod_name,
value: pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_POD_NAME] || '',
},
],
op: 'AND',
@@ -1940,7 +1970,8 @@ export const getPodMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: pod.meta.k8s_namespace_name,
value:
pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] || '',
},
{
id: '7c5f875b',
@@ -1951,7 +1982,7 @@ export const getPodMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: pod.meta.k8s_pod_name,
value: pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_POD_NAME] || '',
},
],
op: 'AND',
@@ -2031,7 +2062,8 @@ export const getPodMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: pod.meta.k8s_namespace_name,
value:
pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] || '',
},
{
id: '80216712',
@@ -2042,7 +2074,7 @@ export const getPodMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: pod.meta.k8s_pod_name,
value: pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_POD_NAME] || '',
},
],
op: 'AND',
@@ -2088,7 +2120,8 @@ export const getPodMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: pod.meta.k8s_namespace_name,
value:
pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] || '',
},
{
id: '84b59a9f',
@@ -2099,7 +2132,7 @@ export const getPodMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: pod.meta.k8s_pod_name,
value: pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_POD_NAME] || '',
},
],
op: 'AND',
@@ -2145,7 +2178,8 @@ export const getPodMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: pod.meta.k8s_namespace_name,
value:
pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] || '',
},
{
id: 'd649ad0c',
@@ -2156,7 +2190,7 @@ export const getPodMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: pod.meta.k8s_pod_name,
value: pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_POD_NAME] || '',
},
],
op: 'AND',
@@ -2249,7 +2283,8 @@ export const getPodMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: pod.meta.k8s_namespace_name,
value:
pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] || '',
},
{
id: 'bc9c5cf3',
@@ -2260,7 +2295,7 @@ export const getPodMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: pod.meta.k8s_pod_name,
value: pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_POD_NAME] || '',
},
],
op: 'AND',
@@ -2346,7 +2381,8 @@ export const getPodMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: pod.meta.k8s_namespace_name,
value:
pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] || '',
},
{
id: 'b54a3d78',
@@ -2357,7 +2393,7 @@ export const getPodMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: pod.meta.k8s_pod_name,
value: pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_POD_NAME] || '',
},
],
op: 'AND',
@@ -2443,7 +2479,8 @@ export const getPodMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: pod.meta.k8s_namespace_name,
value:
pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] || '',
},
{
id: '1a97cb95',
@@ -2454,7 +2491,7 @@ export const getPodMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: pod.meta.k8s_pod_name,
value: pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_POD_NAME] || '',
},
],
op: 'AND',
@@ -2493,7 +2530,8 @@ export const getPodMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: pod.meta.k8s_namespace_name,
value:
pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] || '',
},
{
id: 'ddfbd379',
@@ -2504,7 +2542,7 @@ export const getPodMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: pod.meta.k8s_pod_name,
value: pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_POD_NAME] || '',
},
],
op: 'AND',
@@ -2543,7 +2581,8 @@ export const getPodMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: pod.meta.k8s_namespace_name,
value:
pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] || '',
},
{
id: '1963fc96',
@@ -2554,7 +2593,7 @@ export const getPodMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: pod.meta.k8s_pod_name,
value: pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_POD_NAME] || '',
},
],
op: 'AND',
@@ -2562,7 +2601,7 @@ export const getPodMetricsQueryPayload = (
functions: [],
groupBy: [],
having: [],
legend: 'Uage',
legend: 'Usage',
limit: null,
orderBy: [],
queryName: 'C',

View File

@@ -1,30 +1,63 @@
import { Tooltip } from 'antd';
import { TableColumnDef } from 'components/TanStackTableView';
import TanStackTable from 'components/TanStackTableView';
import { Container } from '@signozhq/icons';
import { Badge, BadgeColor } from '@signozhq/ui/badge';
import { TooltipSimple } from '@signozhq/ui/tooltip';
import {
InframonitoringtypesPodPhaseDTO,
InframonitoringtypesPodRecordDTO,
} from 'api/generated/services/sigNoz.schemas';
import TanStackTable, { TableColumnDef } from 'components/TanStackTableView';
import { ExpandButtonWrapper } from 'container/InfraMonitoringK8sV2/components';
import ColumnHeader from '../Base/ColumnHeader';
import EntityGroupHeader from '../Base/EntityGroupHeader';
import K8sGroupCell from '../Base/K8sGroupCell';
import { formatBytes } from '../commonUtils';
import { EntityProgressBar, ValidateColumnValueWrapper } from '../components';
import { InfraMonitoringEntity } from '../constants';
import { K8sPodsData } from './api';
import { Container } from '@signozhq/icons';
import { formatBytes, getPodPhaseStatusItems } from '../commonUtils';
import {
CellValueTooltip,
EntityProgressBar,
GroupedStatusCounts,
ValidateColumnValueWrapper,
} from '../components';
import {
INFRA_MONITORING_ATTR_KEYS,
InfraMonitoringEntity,
} from '../constants';
import { Typography } from '@signozhq/ui/typography';
import { formatAge } from 'utils/timeUtils';
export function getK8sPodRowKey(pod: K8sPodsData): string {
return pod.podUID || pod.meta.k8s_pod_uid || pod.meta.k8s_pod_name;
export function getK8sPodRowKey(pod: InframonitoringtypesPodRecordDTO): string {
return (
pod.podUID ||
pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_POD_UID] ||
pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_POD_NAME] ||
''
);
}
export function getK8sPodItemKey(pod: K8sPodsData): string {
export function getK8sPodItemKey(
pod: InframonitoringtypesPodRecordDTO,
): string {
return pod.podUID;
}
export const k8sPodColumnsConfig: TableColumnDef<K8sPodsData>[] = [
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[] = [
{
id: 'podGroup',
header: (): React.ReactNode => <EntityGroupHeader title="POD GROUP" />,
accessorFn: (row): string => row.meta.k8s_pod_name || '',
width: { min: 300 },
header: (): React.ReactNode => <EntityGroupHeader title="Pod Group" />,
accessorFn: (row): string =>
row.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_POD_NAME] || '',
width: { min: 290 },
enableSort: false,
enableRemove: false,
enableMove: false,
@@ -47,9 +80,11 @@ export const k8sPodColumnsConfig: TableColumnDef<K8sPodsData>[] = [
<EntityGroupHeader
title="Pod Name"
icon={<Container data-hide-expanded="true" size={14} />}
docPath="/infrastructure-monitoring/kubernetes/pods#pod-name"
/>
),
accessorFn: (row): string => row.meta.k8s_pod_name || '',
accessorFn: (row): string =>
row.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_POD_NAME] || '',
width: { min: 290 },
enableSort: false,
enableRemove: false,
@@ -59,18 +94,92 @@ export const k8sPodColumnsConfig: TableColumnDef<K8sPodsData>[] = [
cell: ({ value }): React.ReactNode => {
const podName = value as string;
return (
<Tooltip title={podName}>
<CellValueTooltip value={podName}>
<TanStackTable.Text>{podName}</TanStackTable.Text>
</Tooltip>
</CellValueTooltip>
);
},
},
{
id: 'podPhase',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/pods#pod-phase">
Phase
</ColumnHeader>
),
accessorFn: (row): string => row.podPhase,
width: { min: 120 },
enableSort: false,
visibilityBehavior: 'hidden-on-expand',
cell: ({ row }): React.ReactNode => {
if (!row.podPhase) {
return <></>;
}
const color = POD_PHASE_COLORS[row.podPhase] || POD_PHASE_COLORS.unknown;
const label =
row.podPhase === InframonitoringtypesPodPhaseDTO.no_data
? 'No Data'
: row.podPhase.charAt(0).toUpperCase() + row.podPhase.slice(1);
return (
<Badge color={color} variant="outline">
{label}
</Badge>
);
},
},
{
id: 'podCountsByPhase',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/pods#pod-phase">
Phases
</ColumnHeader>
),
accessorFn: (row): InframonitoringtypesPodRecordDTO['podCountsByPhase'] =>
row.podCountsByPhase,
width: { min: 250 },
enableSort: false,
visibilityBehavior: 'hidden-on-collapse',
cell: ({ row }): React.ReactNode => {
const podCountsByPhase = row.podCountsByPhase;
if (!podCountsByPhase) {
return <TanStackTable.Text>-</TanStackTable.Text>;
}
return (
<GroupedStatusCounts items={getPodPhaseStatusItems(row.podCountsByPhase)} />
);
},
},
{
id: 'podAge',
header: 'Age',
accessorFn: (row): number => row.podAge,
width: { min: 100 },
enableSort: false,
cell: ({ value }): React.ReactNode => {
const age = value as number;
if (age === -1) {
return (
<TooltipSimple title="No data">
<Typography.Text>-</Typography.Text>
</TooltipSimple>
);
}
return <TanStackTable.Text>{formatAge(age)}</TanStackTable.Text>;
},
},
{
id: 'cpu_request',
header: 'CPU Req Usage (%)',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/pods#cpu-req-usage-">
CPU Request
<br /> Usage (%)
</ColumnHeader>
),
accessorFn: (row): number => row.podCPURequest,
width: { min: 210 },
enableSort: true,
defaultVisibility: false,
cell: ({ value }): React.ReactNode => {
const cpuRequest = value as number;
return (
@@ -86,9 +195,14 @@ export const k8sPodColumnsConfig: TableColumnDef<K8sPodsData>[] = [
},
{
id: 'cpu_limit',
header: 'CPU Limit Usage (%)',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/pods#cpu-limit-usage-">
CPU Limit
<br /> Usage (%)
</ColumnHeader>
),
accessorFn: (row): number => row.podCPULimit,
width: { min: 210 },
width: { min: 220 },
enableSort: true,
cell: ({ value }): React.ReactNode => {
const cpuLimit = value as number;
@@ -105,29 +219,40 @@ export const k8sPodColumnsConfig: TableColumnDef<K8sPodsData>[] = [
},
{
id: 'cpu',
header: 'CPU Usage (cores)',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/pods#cpu-usage-cores">
CPU Usage
<br /> (cores)
</ColumnHeader>
),
accessorFn: (row): number => row.podCPU,
width: { min: 210 },
width: { min: 160 },
enableSort: true,
cell: ({ value }): React.ReactNode => {
const cpu = value as number;
const cpu = Number(value);
return (
<ValidateColumnValueWrapper
value={cpu}
entity={InfraMonitoringEntity.PODS}
attribute="CPU metric"
>
<TanStackTable.Text>{cpu}</TanStackTable.Text>
<TanStackTable.Text>{cpu.toFixed(2)}</TanStackTable.Text>
</ValidateColumnValueWrapper>
);
},
},
{
id: 'memory_request',
header: 'Mem Req Usage (%)',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/pods#mem-req-usage-">
Memory Request
<br /> Usage (%)
</ColumnHeader>
),
accessorFn: (row): number => row.podMemoryRequest,
width: { min: 210 },
enableSort: true,
defaultVisibility: false,
cell: ({ value }): React.ReactNode => {
const memoryRequest = value as number;
return (
@@ -143,9 +268,14 @@ export const k8sPodColumnsConfig: TableColumnDef<K8sPodsData>[] = [
},
{
id: 'memory_limit',
header: 'Mem Limit Usage (%)',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/pods#mem-limit-usage-">
Memory Limit
<br /> Usage (%)
</ColumnHeader>
),
accessorFn: (row): number => row.podMemoryLimit,
width: { min: 210 },
width: { min: 220 },
enableSort: true,
cell: ({ value }): React.ReactNode => {
const memoryLimit = value as number;
@@ -162,7 +292,12 @@ export const k8sPodColumnsConfig: TableColumnDef<K8sPodsData>[] = [
},
{
id: 'memory',
header: 'Mem Usage (WSS)',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/pods#mem-usage-wss">
Memory Usage
<br /> (WSS)
</ColumnHeader>
),
accessorFn: (row): number => row.podMemory,
width: { min: 210, default: '100%' },
enableSort: true,
@@ -182,7 +317,8 @@ export const k8sPodColumnsConfig: TableColumnDef<K8sPodsData>[] = [
{
id: 'namespace',
header: 'Namespace',
accessorFn: (row): string => row.meta.k8s_namespace_name || '',
accessorFn: (row): string =>
row.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] || '',
width: { default: 100 },
enableSort: false,
defaultVisibility: false,
@@ -193,7 +329,8 @@ export const k8sPodColumnsConfig: TableColumnDef<K8sPodsData>[] = [
{
id: 'node',
header: 'Node',
accessorFn: (row): string => row.meta.k8s_node_name || '',
accessorFn: (row): string =>
row.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NODE_NAME] || '',
width: { default: 100 },
enableSort: false,
defaultVisibility: false,
@@ -204,7 +341,8 @@ export const k8sPodColumnsConfig: TableColumnDef<K8sPodsData>[] = [
{
id: 'cluster',
header: 'Cluster',
accessorFn: (row): string => row.meta.k8s_cluster_name || '',
accessorFn: (row): string =>
row.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_CLUSTER_NAME] || '',
width: { default: 100 },
enableSort: false,
defaultVisibility: false,

View File

@@ -1,20 +1,23 @@
import React, { useCallback } from 'react';
import { useCallback } from 'react';
import { listStatefulSets } from 'api/generated/services/inframonitoring';
import {
InframonitoringtypesResponseTypeDTO,
InframonitoringtypesStatefulSetRecordDTO,
Querybuildertypesv5OrderDirectionDTO,
} from 'api/generated/services/sigNoz.schemas';
import { InfraMonitoringEvents } from 'constants/events';
import { FeatureKeys } from 'constants/features';
import { useAppContext } from 'providers/App/App';
import K8sBaseDetails, { K8sDetailsFilters } from '../Base/K8sBaseDetails';
import { K8sBaseList } from '../Base/K8sBaseList';
import { K8sBaseFilters } from '../Base/types';
import { InfraMonitoringEntity } from '../constants';
import { getK8sStatefulSetsList, K8sStatefulSetsData } from './api';
import {
getStatefulSetMetricsQueryPayload,
k8sStatefulSetDetailsMetadataConfig,
k8sStatefulSetGetEntityName,
k8sStatefulSetGetSelectedItemFilters,
k8sStatefulSetInitialEventsFilter,
k8sStatefulSetInitialLogTracesFilter,
k8sStatefulSetGetSelectedItemExpression,
k8sStatefulSetInitialEventsExpression,
k8sStatefulSetInitialLogTracesExpression,
statefulSetWidgetInfo,
} from './constants';
import {
@@ -28,66 +31,93 @@ function K8sStatefulSetsList({
}: {
controlListPrefix?: React.ReactNode;
}): JSX.Element {
const { featureFlags } = useAppContext();
const dotMetricsEnabled =
featureFlags?.find((flag) => flag.name === FeatureKeys.DOT_METRICS_ENABLED)
?.active || false;
const fetchListData = useCallback(
async (filters: K8sBaseFilters, signal?: AbortSignal) => {
filters.orderBy ||= {
columnName: 'cpu',
order: 'desc',
};
try {
const response = await listStatefulSets(
{
filter: { expression: filters.filter.expression },
groupBy: filters.groupBy?.map((g) => ({ name: g.name })),
offset: filters.offset,
limit: filters.limit ?? 10,
start: filters.start,
end: filters.end,
orderBy: filters.orderBy
? {
key: { name: filters.orderBy.key.name },
direction:
filters.orderBy.direction === 'asc'
? Querybuildertypesv5OrderDirectionDTO.asc
: Querybuildertypesv5OrderDirectionDTO.desc,
}
: undefined,
},
signal,
);
const response = await getK8sStatefulSetsList(
filters,
signal,
undefined,
dotMetricsEnabled,
);
return {
data: response.payload?.data.records || [],
total: response.payload?.data.total || 0,
error: response.error,
rawData: response.payload?.data,
};
const data = response.data;
return {
type:
data.type === InframonitoringtypesResponseTypeDTO.grouped_list
? ('grouped_list' as const)
: ('list' as const),
records: data.records,
total: data.total,
endTimeBeforeRetention: data.endTimeBeforeRetention,
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,
};
}
},
[dotMetricsEnabled],
[],
);
const fetchEntityData = useCallback(
async (
filters: K8sDetailsFilters,
signal?: AbortSignal,
): Promise<{ data: K8sStatefulSetsData | null; error?: string | null }> => {
const response = await getK8sStatefulSetsList(
{
filters: filters.filters,
start: filters.start,
end: filters.end,
limit: 1,
offset: 0,
},
signal,
undefined,
dotMetricsEnabled,
);
): Promise<{
data: InframonitoringtypesStatefulSetRecordDTO | null;
error?: string | null;
}> => {
try {
const response = await listStatefulSets(
{
filter: { expression: filters.filter.expression },
start: filters.start,
end: filters.end,
limit: 1,
offset: 0,
},
signal,
);
const records = response.payload?.data.records || [];
return {
data: records.length > 0 ? records[0] : null,
error: response.error,
};
return {
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,
};
}
},
[dotMetricsEnabled],
[],
);
return (
<>
<K8sBaseList<K8sStatefulSetsData>
<K8sBaseList<InframonitoringtypesStatefulSetRecordDTO>
controlListPrefix={controlListPrefix}
entity={InfraMonitoringEntity.STATEFULSETS}
tableColumns={k8sStatefulSetsColumnsConfig}
@@ -97,14 +127,14 @@ function K8sStatefulSetsList({
eventCategory={InfraMonitoringEvents.StatefulSet}
/>
<K8sBaseDetails<K8sStatefulSetsData>
<K8sBaseDetails<InframonitoringtypesStatefulSetRecordDTO>
category={InfraMonitoringEntity.STATEFULSETS}
eventCategory={InfraMonitoringEvents.StatefulSet}
getSelectedItemFilters={k8sStatefulSetGetSelectedItemFilters}
getSelectedItemExpression={k8sStatefulSetGetSelectedItemExpression}
fetchEntityData={fetchEntityData}
getEntityName={k8sStatefulSetGetEntityName}
getInitialLogTracesFilters={k8sStatefulSetInitialLogTracesFilter}
getInitialEventsFilters={k8sStatefulSetInitialEventsFilter}
getInitialLogTracesExpression={k8sStatefulSetInitialLogTracesExpression}
getInitialEventsExpression={k8sStatefulSetInitialEventsExpression}
metadataConfig={k8sStatefulSetDetailsMetadataConfig}
entityWidgetInfo={statefulSetWidgetInfo}
getEntityQueryPayload={getStatefulSetMetricsQueryPayload}

View File

@@ -1,127 +0,0 @@
import axios from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { UnderscoreToDotMap } from 'api/utils';
import { AxiosError } from 'axios';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
import { K8sBaseFilters } from '../Base/types';
export interface K8sStatefulSetsListPayload {
filters: TagFilter;
groupBy?: BaseAutocompleteData[];
offset?: number;
limit?: number;
orderBy?: {
columnName: string;
order: 'asc' | 'desc';
};
}
export interface K8sStatefulSetsData {
statefulSetName: string;
cpuUsage: number;
memoryUsage: number;
desiredPods: number;
availablePods: number;
cpuRequest: number;
memoryRequest: number;
cpuLimit: number;
memoryLimit: number;
restarts: number;
meta: {
k8s_statefulset_name: string;
k8s_namespace_name: string;
};
}
export interface K8sStatefulSetsListResponse {
status: string;
data: {
type: string;
records: K8sStatefulSetsData[];
groups: null;
total: number;
sentAnyHostMetricsData: boolean;
isSendingK8SAgentMetrics: boolean;
};
}
// TODO(H4ad): Erase this whole file when migrating to openapi
export const statefulSetsMetaMap = [
{ dot: 'k8s.statefulset.name', under: 'k8s_statefulset_name' },
{ dot: 'k8s.namespace.name', under: 'k8s_namespace_name' },
] as const;
export function mapStatefulSetsMeta(
raw: Record<string, unknown>,
): K8sStatefulSetsData['meta'] {
const out: Record<string, unknown> = { ...raw };
statefulSetsMetaMap.forEach(({ dot, under }) => {
if (dot in raw) {
const v = raw[dot];
out[under] = typeof v === 'string' ? v : raw[under];
}
});
return out as K8sStatefulSetsData['meta'];
}
export const getK8sStatefulSetsList = async (
props: K8sBaseFilters,
signal?: AbortSignal,
headers?: Record<string, string>,
dotMetricsEnabled = false,
): Promise<SuccessResponse<K8sStatefulSetsListResponse> | ErrorResponse> => {
try {
const requestProps = dotMetricsEnabled
? {
...props,
filters: {
...props.filters,
items: props.filters.items.reduce<TagFilter['items']>(
(acc, item) => {
if (item.value === undefined) {
return acc;
}
if (
item.key &&
typeof item.key === 'object' &&
'key' in item.key &&
typeof item.key.key === 'string'
) {
const mappedKey = UnderscoreToDotMap[item.key.key] ?? item.key.key;
acc.push({ ...item, key: { ...item.key, key: mappedKey } });
} else {
acc.push(item);
}
return acc;
},
[] as TagFilter['items'],
),
},
}
: props;
const response = await axios.post('/statefulsets/list', requestProps, {
signal,
headers,
});
const payload: K8sStatefulSetsListResponse = response.data;
payload.data.records = payload.data.records.map((record) => ({
...record,
meta: mapStatefulSetsMeta(record.meta as Record<string, unknown>),
}));
return {
statusCode: 200,
error: null,
message: 'Success',
payload,
params: requestProps,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);
}
};

View File

@@ -1,67 +1,58 @@
import { InframonitoringtypesStatefulSetRecordDTO } from 'api/generated/services/sigNoz.schemas';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults';
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
import { EQueryType } from 'types/common/dashboard';
import { DataSource, ReduceOperators } from 'types/common/queryBuilder';
import { v4 } from 'uuid';
import {
createFilterItem,
K8sDetailsMetadataConfig,
} from '../Base/K8sBaseDetails';
import { QUERY_KEYS } from '../EntityDetailsUtils/utils';
import { K8sStatefulSetsData } from './api';
import { K8sDetailsMetadataConfig } from '../Base/K8sBaseDetails';
import { formatValueForExpression } from 'components/QueryBuilderV2/utils';
import { INFRA_MONITORING_ATTR_KEYS } from '../constants';
export const k8sStatefulSetGetSelectedItemFilters = (
export const k8sStatefulSetGetSelectedItemExpression = (
selectedItemId: string,
): TagFilter => ({
op: 'AND',
items: [
{
id: 'k8s_statefulset_name',
key: {
key: 'k8s_statefulset_name',
type: null,
},
op: '=',
value: selectedItemId,
},
],
});
): string =>
`${INFRA_MONITORING_ATTR_KEYS.K8S_STATEFULSET_NAME} = ${formatValueForExpression(selectedItemId)}`;
export const k8sStatefulSetDetailsMetadataConfig: K8sDetailsMetadataConfig<K8sStatefulSetsData>[] =
export const k8sStatefulSetDetailsMetadataConfig: K8sDetailsMetadataConfig<InframonitoringtypesStatefulSetRecordDTO>[] =
[
{
label: 'Statefulset Name',
getValue: (p): string => p.meta.k8s_statefulset_name,
getValue: (p): string =>
p.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_STATEFULSET_NAME] ?? '',
},
{
label: 'Namespace Name',
getValue: (p): string => p.meta.k8s_namespace_name,
getValue: (p): string =>
p.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] ?? '',
},
];
export const k8sStatefulSetInitialEventsFilter = (
item: K8sStatefulSetsData,
): ReturnType<typeof createFilterItem>[] => [
createFilterItem(QUERY_KEYS.K8S_OBJECT_KIND, 'StatefulSet'),
createFilterItem(QUERY_KEYS.K8S_OBJECT_NAME, item.meta.k8s_statefulset_name),
];
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}`;
};
export const k8sStatefulSetInitialLogTracesFilter = (
item: K8sStatefulSetsData,
): ReturnType<typeof createFilterItem>[] => [
createFilterItem(
QUERY_KEYS.K8S_STATEFUL_SET_NAME,
item.meta.k8s_statefulset_name,
),
createFilterItem(QUERY_KEYS.K8S_NAMESPACE_NAME, item.meta.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}`;
};
export const k8sStatefulSetGetEntityName = (
item: K8sStatefulSetsData,
): string => item.meta.k8s_statefulset_name;
item: InframonitoringtypesStatefulSetRecordDTO,
): string => item.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_STATEFULSET_NAME] ?? '';
export const statefulSetWidgetInfo = [
{
@@ -91,16 +82,16 @@ export const statefulSetWidgetInfo = [
];
export const getStatefulSetMetricsQueryPayload = (
statefulSet: K8sStatefulSetsData,
statefulSet: InframonitoringtypesStatefulSetRecordDTO,
start: number,
end: number,
dotMetricsEnabled: boolean,
): GetQueryResultsProps[] => {
const k8sStatefulSetNameKey = dotMetricsEnabled
? 'k8s.statefulset.name'
? INFRA_MONITORING_ATTR_KEYS.K8S_STATEFULSET_NAME
: 'k8s_statefulset_name';
const k8sNamespaceNameKey = dotMetricsEnabled
? 'k8s.namespace.name'
? INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME
: 'k8s_namespace_name';
const k8sPodNameKey = dotMetricsEnabled ? 'k8s.pod.name' : 'k8s_pod_name';
@@ -172,7 +163,10 @@ export const getStatefulSetMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: statefulSet.meta.k8s_statefulset_name,
value:
statefulSet.meta?.[
INFRA_MONITORING_ATTR_KEYS.K8S_STATEFULSET_NAME
] ?? '',
},
{
id: 'f2',
@@ -183,7 +177,9 @@ export const getStatefulSetMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: statefulSet.meta.k8s_namespace_name,
value:
statefulSet.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] ??
'',
},
],
op: 'AND',
@@ -222,7 +218,10 @@ export const getStatefulSetMetricsQueryPayload = (
type: 'tag',
},
op: 'contains',
value: statefulSet.meta.k8s_statefulset_name,
value:
statefulSet.meta?.[
INFRA_MONITORING_ATTR_KEYS.K8S_STATEFULSET_NAME
] ?? '',
},
{
id: 'f2',
@@ -233,7 +232,9 @@ export const getStatefulSetMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: statefulSet.meta.k8s_namespace_name,
value:
statefulSet.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] ??
'',
},
],
op: 'AND',
@@ -272,7 +273,10 @@ export const getStatefulSetMetricsQueryPayload = (
type: 'tag',
},
op: 'contains',
value: statefulSet.meta.k8s_statefulset_name,
value:
statefulSet.meta?.[
INFRA_MONITORING_ATTR_KEYS.K8S_STATEFULSET_NAME
] ?? '',
},
{
id: 'f2',
@@ -283,7 +287,9 @@ export const getStatefulSetMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: statefulSet.meta.k8s_namespace_name,
value:
statefulSet.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] ??
'',
},
],
op: 'AND',
@@ -342,7 +348,10 @@ export const getStatefulSetMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: statefulSet.meta.k8s_statefulset_name,
value:
statefulSet.meta?.[
INFRA_MONITORING_ATTR_KEYS.K8S_STATEFULSET_NAME
] ?? '',
},
{
id: 'f2',
@@ -353,7 +362,9 @@ export const getStatefulSetMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: statefulSet.meta.k8s_namespace_name,
value:
statefulSet.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] ??
'',
},
],
op: 'AND',
@@ -392,7 +403,10 @@ export const getStatefulSetMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: statefulSet.meta.k8s_statefulset_name,
value:
statefulSet.meta?.[
INFRA_MONITORING_ATTR_KEYS.K8S_STATEFULSET_NAME
] ?? '',
},
{
id: 'f2',
@@ -403,7 +417,9 @@ export const getStatefulSetMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: statefulSet.meta.k8s_namespace_name,
value:
statefulSet.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] ??
'',
},
],
op: 'AND',
@@ -462,7 +478,10 @@ export const getStatefulSetMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: statefulSet.meta.k8s_statefulset_name,
value:
statefulSet.meta?.[
INFRA_MONITORING_ATTR_KEYS.K8S_STATEFULSET_NAME
] ?? '',
},
{
id: 'f2',
@@ -473,7 +492,9 @@ export const getStatefulSetMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: statefulSet.meta.k8s_namespace_name,
value:
statefulSet.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] ??
'',
},
],
op: 'AND',
@@ -512,7 +533,10 @@ export const getStatefulSetMetricsQueryPayload = (
type: 'tag',
},
op: 'contains',
value: statefulSet.meta.k8s_statefulset_name,
value:
statefulSet.meta?.[
INFRA_MONITORING_ATTR_KEYS.K8S_STATEFULSET_NAME
] ?? '',
},
{
id: 'f2',
@@ -523,7 +547,9 @@ export const getStatefulSetMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: statefulSet.meta.k8s_namespace_name,
value:
statefulSet.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] ??
'',
},
],
op: 'AND',
@@ -562,7 +588,10 @@ export const getStatefulSetMetricsQueryPayload = (
type: 'tag',
},
op: 'contains',
value: statefulSet.meta.k8s_statefulset_name,
value:
statefulSet.meta?.[
INFRA_MONITORING_ATTR_KEYS.K8S_STATEFULSET_NAME
] ?? '',
},
{
id: 'f2',
@@ -573,7 +602,9 @@ export const getStatefulSetMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: statefulSet.meta.k8s_namespace_name,
value:
statefulSet.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] ??
'',
},
],
op: 'AND',
@@ -632,7 +663,10 @@ export const getStatefulSetMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: statefulSet.meta.k8s_statefulset_name,
value:
statefulSet.meta?.[
INFRA_MONITORING_ATTR_KEYS.K8S_STATEFULSET_NAME
] ?? '',
},
{
id: 'f2',
@@ -643,7 +677,9 @@ export const getStatefulSetMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: statefulSet.meta.k8s_namespace_name,
value:
statefulSet.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] ??
'',
},
],
op: 'AND',
@@ -682,7 +718,10 @@ export const getStatefulSetMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: statefulSet.meta.k8s_statefulset_name,
value:
statefulSet.meta?.[
INFRA_MONITORING_ATTR_KEYS.K8S_STATEFULSET_NAME
] ?? '',
},
{
id: 'f2',
@@ -693,7 +732,9 @@ export const getStatefulSetMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: statefulSet.meta.k8s_namespace_name,
value:
statefulSet.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] ??
'',
},
],
op: 'AND',
@@ -752,7 +793,10 @@ export const getStatefulSetMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: statefulSet.meta.k8s_statefulset_name,
value:
statefulSet.meta?.[
INFRA_MONITORING_ATTR_KEYS.K8S_STATEFULSET_NAME
] ?? '',
},
{
id: 'f2',
@@ -763,7 +807,9 @@ export const getStatefulSetMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: statefulSet.meta.k8s_namespace_name,
value:
statefulSet.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] ??
'',
},
],
op: 'AND',
@@ -835,7 +881,10 @@ export const getStatefulSetMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: statefulSet.meta.k8s_statefulset_name,
value:
statefulSet.meta?.[
INFRA_MONITORING_ATTR_KEYS.K8S_STATEFULSET_NAME
] ?? '',
},
{
id: 'f2',
@@ -846,7 +895,9 @@ export const getStatefulSetMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: statefulSet.meta.k8s_namespace_name,
value:
statefulSet.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] ??
'',
},
],
op: 'AND',

View File

@@ -1,39 +1,52 @@
import { Tooltip } from 'antd';
import { TableColumnDef } from 'components/TanStackTableView';
import TanStackTable from 'components/TanStackTableView';
import { Color } from '@signozhq/design-tokens';
import { InframonitoringtypesStatefulSetRecordDTO } from 'api/generated/services/sigNoz.schemas';
import TanStackTable, { TableColumnDef } from 'components/TanStackTableView';
import { ExpandButtonWrapper } from 'container/InfraMonitoringK8sV2/components';
import ColumnHeader from '../Base/ColumnHeader';
import EntityGroupHeader from '../Base/EntityGroupHeader';
import K8sGroupCell from '../Base/K8sGroupCell';
import { formatBytes } from '../commonUtils';
import { EntityProgressBar, ValidateColumnValueWrapper } from '../components';
import { InfraMonitoringEntity } from '../constants';
import { K8sStatefulSetsData } from './api';
import { formatBytes, getPodPhaseStatusItems } from '../commonUtils';
import {
CellValueTooltip,
EntityProgressBar,
GroupedStatusCounts,
ValidateColumnValueWrapper,
} from '../components';
import {
INFRA_MONITORING_ATTR_KEYS,
InfraMonitoringEntity,
} from '../constants';
import { ArrowUpDown } from '@signozhq/icons';
export function getK8sStatefulSetRowKey(
statefulSet: K8sStatefulSetsData,
statefulSet: InframonitoringtypesStatefulSetRecordDTO,
): string {
return (
statefulSet.statefulSetName || statefulSet.meta.k8s_statefulset_name || ''
statefulSet.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_STATEFULSET_NAME] ||
statefulSet.statefulSetName ||
''
);
}
export function getK8sStatefulSetItemKey(
statefulSet: K8sStatefulSetsData,
statefulSet: InframonitoringtypesStatefulSetRecordDTO,
): string {
return statefulSet.meta.k8s_statefulset_name;
return (
statefulSet.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_STATEFULSET_NAME] || ''
);
}
export const k8sStatefulSetsColumnsConfig: TableColumnDef<K8sStatefulSetsData>[] =
export const k8sStatefulSetsColumnsConfig: TableColumnDef<InframonitoringtypesStatefulSetRecordDTO>[] =
[
{
id: 'statefulSetGroup',
header: (): React.ReactNode => (
<EntityGroupHeader title="STATEFULSET GROUP" />
<EntityGroupHeader title="StatefulSet Group" />
),
accessorFn: (row): string => row.meta.k8s_statefulset_name || '',
width: { min: 210 },
accessorFn: (row): string =>
row.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_STATEFULSET_NAME] || '',
width: { min: 290 },
enableSort: false,
enableRemove: false,
enableMove: false,
@@ -56,10 +69,12 @@ export const k8sStatefulSetsColumnsConfig: TableColumnDef<K8sStatefulSetsData>[]
<EntityGroupHeader
title="StatefulSet Name"
icon={<ArrowUpDown data-hide-expanded="true" size={14} />}
docPath="/infrastructure-monitoring/kubernetes/statefulsets#statefulset-name"
/>
),
accessorFn: (row): string => row.meta.k8s_statefulset_name || '',
width: { min: 200 },
accessorFn: (row): string =>
row.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_STATEFULSET_NAME] || '',
width: { min: 290 },
enableSort: false,
enableRemove: false,
enableMove: false,
@@ -68,75 +83,98 @@ export const k8sStatefulSetsColumnsConfig: TableColumnDef<K8sStatefulSetsData>[]
cell: ({ value }): React.ReactNode => {
const statefulsetName = value as string;
return (
<Tooltip title={statefulsetName}>
<CellValueTooltip value={statefulsetName}>
<TanStackTable.Text>{statefulsetName}</TanStackTable.Text>
</Tooltip>
</CellValueTooltip>
);
},
},
{
id: 'namespaceName',
header: 'Namespace Name',
accessorFn: (row): string => row.meta.k8s_namespace_name || '',
width: { default: 150 },
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/statefulsets#namespace-name">
Namespace
</ColumnHeader>
),
accessorFn: (row): string =>
row.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] || '',
width: { min: 180 },
enableSort: false,
enableResize: true,
cell: ({ value }): React.ReactNode => {
const namespaceName = value as string;
return (
<Tooltip title={namespaceName}>
<CellValueTooltip value={namespaceName}>
<TanStackTable.Text>{namespaceName}</TanStackTable.Text>
</Tooltip>
</CellValueTooltip>
);
},
},
{
id: 'available_pods',
header: 'Available',
accessorFn: (row): number => row.availablePods,
width: { min: 100, default: 140 },
enableSort: true,
id: 'pod_counts_by_phase',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/statefulsets#pod-counts-by-phase">
Pod Phases
</ColumnHeader>
),
accessorFn: (
row,
): InframonitoringtypesStatefulSetRecordDTO['podCountsByPhase'] =>
row.podCountsByPhase,
width: { min: 250 },
enableSort: false,
enableResize: true,
cell: ({ value }): React.ReactNode => {
const availablePods = value as number;
cell: ({ row }): React.ReactNode => {
const podCountsByPhase = row.podCountsByPhase;
if (!podCountsByPhase) {
return <TanStackTable.Text>-</TanStackTable.Text>;
}
return (
<ValidateColumnValueWrapper
value={availablePods}
entity={InfraMonitoringEntity.STATEFULSETS}
attribute="available pod"
>
<TanStackTable.Text>{availablePods}</TanStackTable.Text>
</ValidateColumnValueWrapper>
<GroupedStatusCounts items={getPodPhaseStatusItems(podCountsByPhase)} />
);
},
},
{
id: 'desired_pods',
header: 'Desired',
accessorFn: (row): number => row.desiredPods,
width: { min: 100, default: 140 },
enableSort: true,
id: 'pod_status',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/statefulsets#pod-status">
Pod Status
</ColumnHeader>
),
accessorFn: (row): number => row.currentPods,
width: { min: 140 },
enableSort: false,
enableResize: true,
cell: ({ value }): React.ReactNode => {
const desiredPods = value as number;
return (
<ValidateColumnValueWrapper
value={desiredPods}
entity={InfraMonitoringEntity.STATEFULSETS}
attribute="desired pod"
>
<TanStackTable.Text>{desiredPods}</TanStackTable.Text>
</ValidateColumnValueWrapper>
);
},
cell: ({ row }): React.ReactNode => (
<GroupedStatusCounts
items={[
{
value: row.currentPods,
label: 'Current',
color: Color.BG_FOREST_500,
},
{
value: row.desiredPods,
label: 'Desired',
color: Color.BG_ROBIN_500,
},
]}
/>
),
},
{
id: 'cpu_request',
header: 'CPU Req Usage (%)',
accessorFn: (row): number => row.cpuRequest,
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/statefulsets#cpu-req-usage-">
CPU Request
<br /> Usage (%)
</ColumnHeader>
),
accessorFn: (row): number => row.statefulSetCPURequest,
width: { min: 200, default: 200 },
enableSort: true,
enableResize: true,
defaultVisibility: false,
cell: ({ value }): React.ReactNode => {
const cpuRequest = value as number;
return (
@@ -152,8 +190,13 @@ export const k8sStatefulSetsColumnsConfig: TableColumnDef<K8sStatefulSetsData>[]
},
{
id: 'cpu_limit',
header: 'CPU Limit Usage (%)',
accessorFn: (row): number => row.cpuLimit,
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/statefulsets#cpu-limit-usage-">
CPU Limit
<br /> Usage (%)
</ColumnHeader>
),
accessorFn: (row): number => row.statefulSetCPULimit,
width: { min: 200, default: 200 },
enableSort: true,
enableResize: true,
@@ -172,32 +215,43 @@ export const k8sStatefulSetsColumnsConfig: TableColumnDef<K8sStatefulSetsData>[]
},
{
id: 'cpu',
header: 'CPU Usage (cores)',
accessorFn: (row): number => row.cpuUsage,
width: { min: 190 },
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/statefulsets#cpu-usage-cores">
CPU Usage
<br /> (cores)
</ColumnHeader>
),
accessorFn: (row): number => row.statefulSetCPU,
width: { min: 160 },
enableSort: true,
enableResize: true,
defaultVisibility: false,
cell: ({ value }): React.ReactNode => {
const cpu = value as number;
const cpu = Number(value);
return (
<ValidateColumnValueWrapper
value={cpu}
entity={InfraMonitoringEntity.STATEFULSETS}
attribute="CPU metric"
>
<TanStackTable.Text>{cpu}</TanStackTable.Text>
<TanStackTable.Text>{cpu.toFixed(2)}</TanStackTable.Text>
</ValidateColumnValueWrapper>
);
},
},
{
id: 'memory_request',
header: 'Mem Req Usage (%)',
accessorFn: (row): number => row.memoryRequest,
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/statefulsets#mem-req-usage-">
Memory Request
<br /> Usage (%)
</ColumnHeader>
),
accessorFn: (row): number => row.statefulSetMemoryRequest,
width: { min: 190 },
enableSort: true,
enableResize: true,
defaultVisibility: false,
cell: ({ value }): React.ReactNode => {
const memoryRequest = value as number;
return (
@@ -213,8 +267,13 @@ export const k8sStatefulSetsColumnsConfig: TableColumnDef<K8sStatefulSetsData>[]
},
{
id: 'memory_limit',
header: 'Mem Limit Usage (%)',
accessorFn: (row): number => row.memoryLimit,
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/statefulsets#mem-limit-usage-">
Memory Limit
<br /> Usage (%)
</ColumnHeader>
),
accessorFn: (row): number => row.statefulSetMemoryLimit,
width: { min: 180 },
enableSort: true,
enableResize: true,
@@ -233,8 +292,13 @@ export const k8sStatefulSetsColumnsConfig: TableColumnDef<K8sStatefulSetsData>[]
},
{
id: 'memory',
header: 'Mem Usage (WSS)',
accessorFn: (row): number => row.memoryUsage,
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/statefulsets#mem-usage-wss">
Memory Usage
<br /> (WSS)
</ColumnHeader>
),
accessorFn: (row): number => row.statefulSetMemory,
width: { min: 160 },
enableSort: true,
enableResize: true,
@@ -252,4 +316,52 @@ export const k8sStatefulSetsColumnsConfig: TableColumnDef<K8sStatefulSetsData>[]
);
},
},
{
id: 'current_pods',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/statefulsets#current">
Current Pods
</ColumnHeader>
),
accessorFn: (row): number => row.currentPods,
width: { min: 120 },
enableSort: true,
defaultVisibility: false,
cell: ({ value }): React.ReactNode => {
const currentPods = value as number;
return (
<ValidateColumnValueWrapper
value={currentPods}
entity={InfraMonitoringEntity.STATEFULSETS}
attribute="current pod"
>
<TanStackTable.Text>{currentPods}</TanStackTable.Text>
</ValidateColumnValueWrapper>
);
},
},
{
id: 'desired_pods',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/statefulsets#desired">
Desired Pods
</ColumnHeader>
),
accessorFn: (row): number => row.desiredPods,
width: { min: 120 },
enableSort: true,
defaultVisibility: false,
cell: ({ value }): React.ReactNode => {
const desiredPods = value as number;
return (
<ValidateColumnValueWrapper
value={desiredPods}
entity={InfraMonitoringEntity.STATEFULSETS}
attribute="desired pod"
>
<TanStackTable.Text>{desiredPods}</TanStackTable.Text>
</ValidateColumnValueWrapper>
);
},
},
];

View File

@@ -1,20 +1,23 @@
import React, { useCallback } from 'react';
import { useCallback } from 'react';
import { listVolumes } from 'api/generated/services/inframonitoring';
import {
InframonitoringtypesResponseTypeDTO,
InframonitoringtypesVolumeRecordDTO,
Querybuildertypesv5OrderDirectionDTO,
} from 'api/generated/services/sigNoz.schemas';
import { InfraMonitoringEvents } from 'constants/events';
import { FeatureKeys } from 'constants/features';
import { useAppContext } from 'providers/App/App';
import K8sBaseDetails, { K8sDetailsFilters } from '../Base/K8sBaseDetails';
import { K8sBaseList } from '../Base/K8sBaseList';
import { K8sBaseFilters } from '../Base/types';
import { InfraMonitoringEntity } from '../constants';
import { getK8sVolumesList, K8sVolumesData } from './api';
import {
getVolumeMetricsQueryPayload,
k8sVolumeDetailsMetadataConfig,
k8sVolumeGetEntityName,
k8sVolumeGetSelectedItemFilters,
k8sVolumeInitialEventsFilter,
k8sVolumeInitialLogTracesFilter,
k8sVolumeGetSelectedItemExpression,
k8sVolumeInitialEventsExpression,
k8sVolumeInitialLogTracesExpression,
volumeWidgetInfo,
} from './constants';
import {
@@ -28,66 +31,93 @@ function K8sVolumesList({
}: {
controlListPrefix?: React.ReactNode;
}): JSX.Element {
const { featureFlags } = useAppContext();
const dotMetricsEnabled =
featureFlags?.find((flag) => flag.name === FeatureKeys.DOT_METRICS_ENABLED)
?.active || false;
const fetchListData = useCallback(
async (filters: K8sBaseFilters, signal?: AbortSignal) => {
filters.orderBy ||= {
columnName: 'usage',
order: 'desc',
};
try {
const response = await listVolumes(
{
filter: { expression: filters.filter.expression },
groupBy: filters.groupBy?.map((g) => ({ name: g.name })),
offset: filters.offset,
limit: filters.limit ?? 10,
start: filters.start,
end: filters.end,
orderBy: filters.orderBy
? {
key: { name: filters.orderBy.key.name },
direction:
filters.orderBy.direction === 'asc'
? Querybuildertypesv5OrderDirectionDTO.asc
: Querybuildertypesv5OrderDirectionDTO.desc,
}
: undefined,
},
signal,
);
const response = await getK8sVolumesList(
filters,
signal,
undefined,
dotMetricsEnabled,
);
return {
data: response.payload?.data.records || [],
total: response.payload?.data.total || 0,
error: response.error,
rawData: response.payload?.data,
};
const data = response.data;
return {
type:
data.type === InframonitoringtypesResponseTypeDTO.grouped_list
? ('grouped_list' as const)
: ('list' as const),
records: data.records,
total: data.total,
endTimeBeforeRetention: data.endTimeBeforeRetention,
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,
};
}
},
[dotMetricsEnabled],
[],
);
const fetchEntityData = useCallback(
async (
filters: K8sDetailsFilters,
signal?: AbortSignal,
): Promise<{ data: K8sVolumesData | null; error?: string | null }> => {
const response = await getK8sVolumesList(
{
filters: filters.filters,
start: filters.start,
end: filters.end,
limit: 1,
offset: 0,
},
signal,
undefined,
dotMetricsEnabled,
);
): Promise<{
data: InframonitoringtypesVolumeRecordDTO | null;
error?: string | null;
}> => {
try {
const response = await listVolumes(
{
filter: { expression: filters.filter.expression },
start: filters.start,
end: filters.end,
limit: 1,
offset: 0,
},
signal,
);
const records = response.payload?.data.records || [];
return {
data: records.length > 0 ? records[0] : null,
error: response.error,
};
return {
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,
};
}
},
[dotMetricsEnabled],
[],
);
return (
<>
<K8sBaseList<K8sVolumesData>
<K8sBaseList<InframonitoringtypesVolumeRecordDTO>
controlListPrefix={controlListPrefix}
entity={InfraMonitoringEntity.VOLUMES}
tableColumns={k8sVolumesColumnsConfig}
@@ -97,14 +127,14 @@ function K8sVolumesList({
eventCategory={InfraMonitoringEvents.Volumes}
/>
<K8sBaseDetails<K8sVolumesData>
<K8sBaseDetails<InframonitoringtypesVolumeRecordDTO>
category={InfraMonitoringEntity.VOLUMES}
eventCategory={InfraMonitoringEvents.Volume}
getSelectedItemFilters={k8sVolumeGetSelectedItemFilters}
getSelectedItemExpression={k8sVolumeGetSelectedItemExpression}
fetchEntityData={fetchEntityData}
getEntityName={k8sVolumeGetEntityName}
getInitialLogTracesFilters={k8sVolumeInitialLogTracesFilter}
getInitialEventsFilters={k8sVolumeInitialEventsFilter}
getInitialLogTracesExpression={k8sVolumeInitialLogTracesExpression}
getInitialEventsExpression={k8sVolumeInitialEventsExpression}
metadataConfig={k8sVolumeDetailsMetadataConfig}
entityWidgetInfo={volumeWidgetInfo}
getEntityQueryPayload={getVolumeMetricsQueryPayload}

View File

@@ -1,136 +0,0 @@
import axios from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { UnderscoreToDotMap } from 'api/utils';
import { AxiosError } from 'axios';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { K8sBaseFilters } from '../Base/types';
export interface K8sVolumesData {
persistentVolumeClaimName: string;
volumeAvailable: number;
volumeCapacity: number;
volumeInodes: number;
volumeInodesFree: number;
volumeInodesUsed: number;
volumeUsage: number;
meta: {
k8s_cluster_name: string;
k8s_namespace_name: string;
k8s_node_name: string;
k8s_persistentvolumeclaim_name: string;
k8s_pod_name: string;
k8s_pod_uid: string;
k8s_statefulset_name: string;
};
}
export interface K8sVolumesListResponse {
status: string;
data: {
type: string;
records: K8sVolumesData[];
groups: null;
total: number;
sentAnyHostMetricsData: boolean;
isSendingK8SAgentMetrics: boolean;
};
}
// TODO(H4ad): Erase this whole file when migrating to openapi
export const volumesMetaMap: Array<{
dot: keyof Record<string, unknown>;
under: keyof K8sVolumesData['meta'];
}> = [
{ dot: 'k8s.cluster.name', under: 'k8s_cluster_name' },
{ dot: 'k8s.namespace.name', under: 'k8s_namespace_name' },
{ dot: 'k8s.node.name', under: 'k8s_node_name' },
{
dot: 'k8s.persistentvolumeclaim.name',
under: 'k8s_persistentvolumeclaim_name',
},
{ dot: 'k8s.pod.name', under: 'k8s_pod_name' },
{ dot: 'k8s.pod.uid', under: 'k8s_pod_uid' },
{ dot: 'k8s.statefulset.name', under: 'k8s_statefulset_name' },
];
export function mapVolumesMeta(
rawMeta: Record<string, unknown>,
): K8sVolumesData['meta'] {
const out: Record<string, unknown> = { ...rawMeta };
volumesMetaMap.forEach(({ dot, under }) => {
if (dot in rawMeta) {
const val = rawMeta[dot];
out[under] = typeof val === 'string' ? val : rawMeta[under];
}
});
return out as K8sVolumesData['meta'];
}
export const getK8sVolumesList = async (
props: K8sBaseFilters,
signal?: AbortSignal,
headers?: Record<string, string>,
dotMetricsEnabled = false,
): Promise<SuccessResponse<K8sVolumesListResponse> | ErrorResponse> => {
try {
const { orderBy, ...rest } = props;
const basePayload = {
...rest,
filters: props.filters ?? { items: [], op: 'and' },
...(orderBy != null ? { orderBy } : {}),
};
const requestProps = dotMetricsEnabled
? {
...basePayload,
filters: {
...basePayload.filters,
items: basePayload.filters.items.reduce<typeof basePayload.filters.items>(
(acc, item) => {
if (item.value === undefined) {
return acc;
}
if (
item.key &&
typeof item.key === 'object' &&
'key' in item.key &&
typeof item.key.key === 'string'
) {
const mappedKey = UnderscoreToDotMap[item.key.key] ?? item.key.key;
acc.push({ ...item, key: { ...item.key, key: mappedKey } });
} else {
acc.push(item);
}
return acc;
},
[] as typeof basePayload.filters.items,
),
},
}
: basePayload;
const response = await axios.post('/pvcs/list', requestProps, {
signal,
headers,
});
const payload: K8sVolumesListResponse = response.data;
payload.data.records = payload.data.records.map((record) => ({
...record,
meta: mapVolumesMeta(record.meta as Record<string, unknown>),
}));
return {
statusCode: 200,
error: null,
message: 'Success',
payload,
params: requestProps,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);
}
};

View File

@@ -1,70 +1,61 @@
import { InframonitoringtypesVolumeRecordDTO } from 'api/generated/services/sigNoz.schemas';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults';
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
import { EQueryType } from 'types/common/dashboard';
import { DataSource, ReduceOperators } from 'types/common/queryBuilder';
import { v4 } from 'uuid';
import {
createFilterItem,
K8sDetailsMetadataConfig,
} from '../Base/K8sBaseDetails';
import { QUERY_KEYS } from '../EntityDetailsUtils/utils';
import { K8sVolumesData } from './api';
import { formatValueForExpression } from 'components/QueryBuilderV2/utils';
export const k8sVolumeGetSelectedItemFilters = (
import { K8sDetailsMetadataConfig } from '../Base/K8sBaseDetails';
import { INFRA_MONITORING_ATTR_KEYS } from '../constants';
export const k8sVolumeGetSelectedItemExpression = (
selectedItemId: string,
): TagFilter => ({
op: 'AND',
items: [
{
id: 'k8s_persistentvolumeclaim_name',
key: {
key: 'k8s_persistentvolumeclaim_name',
type: null,
},
op: '=',
value: selectedItemId,
},
],
});
): string =>
`${INFRA_MONITORING_ATTR_KEYS.K8S_PERSISTENT_VOLUME_CLAIM_NAME} = ${formatValueForExpression(selectedItemId)}`;
export const k8sVolumeDetailsMetadataConfig: K8sDetailsMetadataConfig<K8sVolumesData>[] =
export const k8sVolumeDetailsMetadataConfig: K8sDetailsMetadataConfig<InframonitoringtypesVolumeRecordDTO>[] =
[
{
label: 'PVC Name',
getValue: (p): string => p.persistentVolumeClaimName,
getValue: (p): string => p.persistentVolumeClaimName || '',
},
{
label: 'Cluster Name',
getValue: (p): string => p.meta.k8s_cluster_name,
getValue: (p): string =>
p.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_CLUSTER_NAME] || '',
},
{
label: 'Namespace Name',
getValue: (p): string => p.meta.k8s_namespace_name,
getValue: (p): string =>
p.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] || '',
},
];
export const k8sVolumeInitialEventsFilter = (
item: K8sVolumesData,
): ReturnType<typeof createFilterItem>[] => [
createFilterItem(QUERY_KEYS.K8S_OBJECT_KIND, 'PersistentVolumeClaim'),
createFilterItem(QUERY_KEYS.K8S_OBJECT_NAME, item.persistentVolumeClaimName),
];
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}`;
};
export const k8sVolumeInitialLogTracesFilter = (
item: K8sVolumesData,
): ReturnType<typeof createFilterItem>[] => [
createFilterItem(
QUERY_KEYS.K8S_PERSISTENT_VOLUME_CLAIM_NAME,
item.meta.k8s_persistentvolumeclaim_name,
),
createFilterItem(QUERY_KEYS.K8S_NAMESPACE_NAME, item.meta.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}`;
};
export const k8sVolumeGetEntityName = (item: K8sVolumesData): string =>
item.persistentVolumeClaimName;
export const k8sVolumeGetEntityName = (
item: InframonitoringtypesVolumeRecordDTO,
): string => item.persistentVolumeClaimName || '';
export const volumeWidgetInfo = [
{
@@ -90,37 +81,37 @@ export const volumeWidgetInfo = [
];
export const getVolumeMetricsQueryPayload = (
volume: K8sVolumesData,
volume: InframonitoringtypesVolumeRecordDTO,
start: number,
end: number,
dotMetricsEnabled: boolean,
): GetQueryResultsProps[] => {
const k8sClusterNameKey = dotMetricsEnabled
? 'k8s.cluster.name'
? INFRA_MONITORING_ATTR_KEYS.K8S_CLUSTER_NAME
: 'k8s_cluster_name';
const k8sNamespaceNameKey = dotMetricsEnabled
? 'k8s.namespace.name'
? INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME
: 'k8s_namespace_name';
const k8sVolumeAvailableKey = dotMetricsEnabled
? 'k8s.volume.available'
? INFRA_MONITORING_ATTR_KEYS.K8S_VOLUME_AVAILABLE
: 'k8s_volume_available';
const k8sVolumeCapacityKey = dotMetricsEnabled
? 'k8s.volume.capacity'
? INFRA_MONITORING_ATTR_KEYS.K8S_VOLUME_CAPACITY
: 'k8s_volume_capacity';
const k8sVolumeInodesUsedKey = dotMetricsEnabled
? 'k8s.volume.inodes.used'
? INFRA_MONITORING_ATTR_KEYS.K8S_VOLUME_INODES_USED
: 'k8s_volume_inodes_used';
const k8sVolumeInodesKey = dotMetricsEnabled
? 'k8s.volume.inodes'
? INFRA_MONITORING_ATTR_KEYS.K8S_VOLUME_INODES
: 'k8s_volume_inodes';
const k8sVolumeInodesFreeKey = dotMetricsEnabled
? 'k8s.volume.inodes.free'
? INFRA_MONITORING_ATTR_KEYS.K8S_VOLUME_INODES_FREE
: 'k8s_volume_inodes_free';
const k8sVolumeTypeKey = dotMetricsEnabled
? 'k8s.volume.type'
? INFRA_MONITORING_ATTR_KEYS.K8S_VOLUME_TYPE
: 'k8s_volume_type';
const k8sPVCNameKey = dotMetricsEnabled
? 'k8s.persistentvolumeclaim.name'
? INFRA_MONITORING_ATTR_KEYS.K8S_PERSISTENT_VOLUME_CLAIM_NAME
: 'k8s_persistentvolumeclaim_name';
return [
@@ -152,7 +143,8 @@ export const getVolumeMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: volume.meta.k8s_cluster_name,
value:
volume.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_CLUSTER_NAME] || '',
},
{
id: 'c2',
@@ -163,7 +155,9 @@ export const getVolumeMetricsQueryPayload = (
type: 'tag',
},
op: 'in',
value: [volume.meta.k8s_namespace_name],
value: [
volume.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] || '',
],
},
{
id: 'c3',
@@ -244,7 +238,8 @@ export const getVolumeMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: volume.meta.k8s_cluster_name,
value:
volume.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_CLUSTER_NAME] || '',
},
{
id: 'c2',
@@ -255,7 +250,9 @@ export const getVolumeMetricsQueryPayload = (
type: 'tag',
},
op: 'in',
value: [volume.meta.k8s_namespace_name],
value: [
volume.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] || '',
],
},
{
id: 'c3',
@@ -336,7 +333,8 @@ export const getVolumeMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: volume.meta.k8s_cluster_name,
value:
volume.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_CLUSTER_NAME] || '',
},
{
id: 'c2',
@@ -347,7 +345,9 @@ export const getVolumeMetricsQueryPayload = (
type: 'tag',
},
op: 'in',
value: [volume.meta.k8s_namespace_name],
value: [
volume.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] || '',
],
},
{
id: 'c3',
@@ -428,7 +428,8 @@ export const getVolumeMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: volume.meta.k8s_cluster_name,
value:
volume.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_CLUSTER_NAME] || '',
},
{
id: 'c2',
@@ -439,7 +440,9 @@ export const getVolumeMetricsQueryPayload = (
type: 'tag',
},
op: 'in',
value: [volume.meta.k8s_namespace_name],
value: [
volume.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] || '',
],
},
{
id: 'c3',
@@ -520,7 +523,8 @@ export const getVolumeMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: volume.meta.k8s_cluster_name,
value:
volume.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_CLUSTER_NAME] || '',
},
{
id: 'c2',
@@ -531,7 +535,9 @@ export const getVolumeMetricsQueryPayload = (
type: 'tag',
},
op: 'in',
value: [volume.meta.k8s_namespace_name],
value: [
volume.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] || '',
],
},
{
id: 'c3',

View File

@@ -1,34 +1,43 @@
import { Tooltip } from 'antd';
import { 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';
import EntityGroupHeader from '../Base/EntityGroupHeader';
import K8sGroupCell from '../Base/K8sGroupCell';
import { formatBytes } from '../commonUtils';
import { ValidateColumnValueWrapper } from '../components';
import { InfraMonitoringEntity } from '../constants';
import { K8sVolumesData } from './api';
import { CellValueTooltip, ValidateColumnValueWrapper } from '../components';
import {
INFRA_MONITORING_ATTR_KEYS,
InfraMonitoringEntity,
} from '../constants';
import { HardDrive } from '@signozhq/icons';
export function getK8sVolumeRowKey(volume: K8sVolumesData): string {
export function getK8sVolumeRowKey(
volume: InframonitoringtypesVolumeRecordDTO,
): string {
return (
volume.persistentVolumeClaimName ||
volume.meta.k8s_persistentvolumeclaim_name ||
volume.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_PERSISTENT_VOLUME_CLAIM_NAME] ||
''
);
}
export function getK8sVolumeItemKey(volume: K8sVolumesData): string {
export function getK8sVolumeItemKey(
volume: InframonitoringtypesVolumeRecordDTO,
): string {
return volume.persistentVolumeClaimName;
}
export const k8sVolumesColumnsConfig: TableColumnDef<K8sVolumesData>[] = [
export type VolumeTableColumnConfig =
TableColumnDef<InframonitoringtypesVolumeRecordDTO>;
export const k8sVolumesColumnsConfig: VolumeTableColumnConfig[] = [
{
id: 'volumeGroup',
header: (): React.ReactNode => <EntityGroupHeader title="VOLUME GROUP" />,
header: (): React.ReactNode => <EntityGroupHeader title="Volume Group" />,
accessorFn: (row): string => row.persistentVolumeClaimName || '',
width: { min: 300 },
width: { min: 290 },
enableSort: false,
enableRemove: false,
enableMove: false,
@@ -51,6 +60,7 @@ export const k8sVolumesColumnsConfig: TableColumnDef<K8sVolumesData>[] = [
<EntityGroupHeader
title="PVC Name"
icon={<HardDrive data-hide-expanded="true" size={14} />}
docPath="/infrastructure-monitoring/kubernetes/volumes#pvc-name"
/>
),
accessorFn: (row): string => row.persistentVolumeClaimName || '',
@@ -63,30 +73,39 @@ export const k8sVolumesColumnsConfig: TableColumnDef<K8sVolumesData>[] = [
cell: ({ value }): React.ReactNode => {
const pvcName = value as string;
return (
<Tooltip title={pvcName}>
<CellValueTooltip value={pvcName}>
<TanStackTable.Text>{pvcName}</TanStackTable.Text>
</Tooltip>
</CellValueTooltip>
);
},
},
{
id: 'namespaceName',
header: 'Namespace Name',
accessorFn: (row): string => row.meta.k8s_namespace_name || '',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/volumes#namespace-name">
Namespace
</ColumnHeader>
),
accessorFn: (row): string =>
row.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] || '',
width: { min: 220 },
enableSort: false,
cell: ({ value }): React.ReactNode => {
const namespaceName = value as string;
return (
<Tooltip title={namespaceName}>
<CellValueTooltip value={namespaceName}>
<TanStackTable.Text>{namespaceName}</TanStackTable.Text>
</Tooltip>
</CellValueTooltip>
);
},
},
{
id: 'capacity',
header: 'Capacity',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/volumes#volume-capacity">
Capacity
</ColumnHeader>
),
accessorFn: (row): number => row.volumeCapacity,
width: { min: 140 },
enableSort: true,
@@ -105,7 +124,11 @@ export const k8sVolumesColumnsConfig: TableColumnDef<K8sVolumesData>[] = [
},
{
id: 'usage',
header: 'Used',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/volumes#volume-used">
Used
</ColumnHeader>
),
accessorFn: (row): number => row.volumeUsage,
width: { min: 140 },
enableSort: true,
@@ -124,7 +147,11 @@ export const k8sVolumesColumnsConfig: TableColumnDef<K8sVolumesData>[] = [
},
{
id: 'available',
header: 'Available',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/volumes#volume-available">
Available
</ColumnHeader>
),
accessorFn: (row): number => row.volumeAvailable,
width: { min: 140 },
enableSort: true,
@@ -143,7 +170,11 @@ export const k8sVolumesColumnsConfig: TableColumnDef<K8sVolumesData>[] = [
},
{
id: 'inodes',
header: 'Inodes',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/volumes#volume-inodes">
Inodes
</ColumnHeader>
),
accessorFn: (row): number => row.volumeInodes,
width: { min: 140 },
enableSort: true,
@@ -162,7 +193,11 @@ export const k8sVolumesColumnsConfig: TableColumnDef<K8sVolumesData>[] = [
},
{
id: 'inodesUsed',
header: 'Inodes Used',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/volumes#volume-inodes-used">
Inodes Used
</ColumnHeader>
),
accessorFn: (row): number => row.volumeInodesUsed,
width: { min: 160 },
enableSort: true,
@@ -181,7 +216,11 @@ export const k8sVolumesColumnsConfig: TableColumnDef<K8sVolumesData>[] = [
},
{
id: 'inodesFree',
header: 'Inodes Free',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/volumes#volume-inodes-free">
Inodes Free
</ColumnHeader>
),
accessorFn: (row): number => row.volumeInodesFree,
width: { min: 160 },
enableSort: true,

View File

@@ -1,4 +1,7 @@
import { Color } from '@signozhq/design-tokens';
import { InframonitoringtypesPodCountsByPhaseDTO } from 'api/generated/services/sigNoz.schemas';
import { StatusCountItem } from './components/GroupedStatusCounts';
/**
* Converts size in bytes to a human-readable string with appropriate units
@@ -60,3 +63,18 @@ export function getStrokeColorForLimitUtilization(value: number): string {
// Red
return Color.BG_SAKURA_500;
}
/**
* Builds StatusCountItem[] for GroupedStatusCounts from pod phase counts.
*/
export function getPodPhaseStatusItems(
counts: InframonitoringtypesPodCountsByPhaseDTO,
): StatusCountItem[] {
return [
{ value: counts.running, label: 'Running', color: Color.BG_FOREST_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 },
];
}

View File

@@ -0,0 +1,54 @@
.tooltipContent {
display: flex;
flex-direction: column;
max-width: 280px;
}
.actions {
display: flex;
align-items: center;
gap: var(--spacing-1);
padding: var(--spacing-2);
justify-content: flex-end;
border-bottom-right-radius: 4px;
border-bottom-left-radius: 4px;
}
.actionButton {
display: inline-flex;
align-items: center;
justify-content: center;
width: 26px;
height: 26px;
padding: 0;
border: none;
border-radius: var(--radius-1);
background: transparent;
color: var(--l2-foreground);
cursor: pointer;
transition:
background 0.15s ease,
color 0.15s ease;
&:hover:not(:disabled) {
background: var(--l2-background-hover);
color: var(--l1-foreground);
}
&:disabled {
opacity: 0.4;
cursor: not-allowed;
}
}
.tooltipContentWrapper {
--tooltip-background: var(--l3-background);
--tooltip-border-radius: 4px;
--tooltip-border-width: 1px;
--tooltip-border-color: rgba(255, 255, 255, 0.14);
--tooltip-padding: 0px;
}
.divider {
--divider-color: rgba(255, 255, 255, 0.14);
}

View File

@@ -0,0 +1,100 @@
import { useCallback, type ReactNode, type MouseEvent } from 'react';
import { TooltipSimple } from '@signozhq/ui/tooltip';
import { toast } from '@signozhq/ui/sonner';
import { Copy, Minus, Plus } from '@signozhq/icons';
import { useCopyToClipboard } from 'react-use';
import { useInfraMonitoringCellActionsStore } from './useInfraMonitoringCellActionsStore';
import styles from './CellValueTooltip.module.scss';
import { Divider } from '@signozhq/ui/divider';
export interface CellValueTooltipProps {
value: string;
children: ReactNode;
}
export function CellValueTooltip({
value,
children,
}: CellValueTooltipProps): JSX.Element {
const [, copyToClipboard] = useCopyToClipboard();
const { lineClamp, increaseLineClamp, decreaseLineClamp } =
useInfraMonitoringCellActionsStore();
const handleCopy = useCallback(
(e: MouseEvent): void => {
e.stopPropagation();
copyToClipboard(value);
toast.success('Copied to clipboard');
},
[copyToClipboard, value],
);
const handleIncreaseLineClamp = useCallback(
(e: MouseEvent): void => {
e.stopPropagation();
increaseLineClamp();
},
[increaseLineClamp],
);
const handleDecreaseLineClamp = useCallback(
(e: MouseEvent): void => {
e.stopPropagation();
decreaseLineClamp();
},
[decreaseLineClamp],
);
const tooltipContent = (
<div className={styles.tooltipContent}>
<div className={styles.actions}>
<button
type="button"
className={styles.actionButton}
onClick={handleDecreaseLineClamp}
disabled={lineClamp <= 1}
data-testid="cell-value-decrease-line-height"
title="Decrease line height"
>
<Minus size={14} />
</button>
<button
type="button"
className={styles.actionButton}
onClick={handleIncreaseLineClamp}
disabled={lineClamp >= 10}
data-testid="cell-value-increase-line-height"
title="Increase line height"
>
<Plus size={14} />
</button>
<Divider type="vertical" className={styles.divider} />
<button
type="button"
className={styles.actionButton}
onClick={handleCopy}
data-testid="cell-value-copy"
title="Copy value"
>
<Copy size={14} />
</button>
</div>
</div>
);
return (
<TooltipSimple
title={tooltipContent}
arrow
tooltipContentProps={{
className: styles.tooltipContentWrapper,
}}
>
{children}
</TooltipSimple>
);
}

View File

@@ -0,0 +1,8 @@
export {
CellValueTooltip,
type CellValueTooltipProps,
} from './CellValueTooltip';
export {
useInfraMonitoringCellActionsStore,
useInfraMonitoringLineClamp,
} from './useInfraMonitoringCellActionsStore';

View File

@@ -0,0 +1,39 @@
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
export interface IInfraMonitoringCellActionsStore {
lineClamp: number;
increaseLineClamp: () => void;
decreaseLineClamp: () => void;
}
const MIN_LINE_CLAMP = 1;
const MAX_LINE_CLAMP = 10;
const DEFAULT_LINE_CLAMP = 2;
export const useInfraMonitoringCellActionsStore =
create<IInfraMonitoringCellActionsStore>()(
persist(
(set, get) => ({
lineClamp: DEFAULT_LINE_CLAMP,
increaseLineClamp: (): void => {
const current = get().lineClamp;
if (current < MAX_LINE_CLAMP) {
set({ lineClamp: current + 1 });
}
},
decreaseLineClamp: (): void => {
const current = get().lineClamp;
if (current > MIN_LINE_CLAMP) {
set({ lineClamp: current - 1 });
}
},
}),
{
name: '@signoz/infra-monitoring-cell-actions',
},
),
);
export const useInfraMonitoringLineClamp = (): number =>
useInfraMonitoringCellActionsStore((s) => s.lineClamp);

View File

@@ -12,7 +12,7 @@ import {
import styles from './EntityProgressBar.module.scss';
type EntityProgressBarType = 'request' | 'limit' | 'cpu' | 'memory';
type EntityProgressBarType = 'request' | 'limit' | 'cpu' | 'memory' | 'disk';
function getStrokeColor(type: EntityProgressBarType, value: number): string {
switch (type) {
@@ -24,6 +24,8 @@ function getStrokeColor(type: EntityProgressBarType, value: number): string {
return getProgressColor(Number((value * 100).toFixed(1)));
case 'memory':
return getMemoryProgressColor(Number((value * 100).toFixed(1)));
case 'disk':
return getProgressColor(Number((value * 100).toFixed(1)));
default:
return getStrokeColorForRequestUtilization(value);
}
@@ -36,9 +38,8 @@ export function EntityProgressBar({
value: number;
type: EntityProgressBarType;
}): JSX.Element {
const percentage = Number.isNaN(+value)
? null
: Number((value * 100).toFixed(1));
const isNoData = value === -1 || Number.isNaN(+value);
const percentage = isNoData ? null : Number((value * 100).toFixed(1));
if (percentage === null) {
return (

View File

@@ -0,0 +1,25 @@
.container {
display: flex;
align-items: center;
gap: 4px;
}
.itemWrapper {
display: flex;
align-items: center;
gap: 4px;
}
.separator {
width: 3px;
height: 14px;
border-radius: 1px;
flex-shrink: 0;
}
.value {
font-variant-numeric: tabular-nums;
min-width: 4ch;
text-align: left;
cursor: default;
}

View File

@@ -0,0 +1,47 @@
import { TooltipSimple } from '@signozhq/ui/tooltip';
import styles from './GroupedStatusCounts.module.scss';
import TanStackTable from 'components/TanStackTableView';
export interface StatusCountItem {
value: number;
label: string;
color: string;
}
interface GroupedStatusCountsProps {
items: StatusCountItem[];
showZeroValues?: boolean;
}
export function GroupedStatusCounts({
items,
showZeroValues = true,
}: GroupedStatusCountsProps): JSX.Element {
const visibleItems =
showZeroValues === false ? items.filter((item) => item.value > 0) : items;
if (visibleItems.length === 0) {
return <TanStackTable.Text>-</TanStackTable.Text>;
}
return (
<div className={styles.container}>
{visibleItems.map((item) => (
<div key={item.label} className={styles.itemWrapper}>
<div
className={styles.separator}
style={{ backgroundColor: item.color }}
/>
<TooltipSimple title={`${item.label}: ${item.value}`}>
<span>
<TanStackTable.Text className={styles.value}>
{item.value || '-'}
</TanStackTable.Text>
</span>
</TooltipSimple>
</div>
))}
</div>
);
}

View File

@@ -17,7 +17,7 @@ export function ValidateColumnValueWrapper({
entity?: InfraMonitoringEntity;
attribute?: string;
}): JSX.Element {
if (value === -1) {
if (value === -1 || Number.isNaN(value)) {
let element = <TanStackTable.Text>-</TanStackTable.Text>;
if (entity && attribute) {
element = (

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