Compare commits

..

60 Commits

Author SHA1 Message Date
Prashant Shahi
c30c882aae chore: integrate goreleaser for user_scripts and restructuring (#6911)
* chore: integrate goreleaser for user_scripts and restructuring
* ci(goreleaser): add goreleaser workflow
* chore(goreleaser): update Makefile and config

---------

Signed-off-by: Prashant Shahi <prashant@signoz.io>
2025-01-25 17:10:21 +05:30
Vibhu Pandey
001122db2c feat(instrumentation): adopt slog (#6907)
### Summary

feat(instrumentation): adopt slog
2025-01-24 09:23:02 +00:00
Nityananda Gohain
b544a54c40 fix: logging middleware add excluded routes (#6917)
* fix: logging middleware add excluded routes

* fix: consistant name and update example

* fix: consistant name
2025-01-24 14:43:28 +05:30
SagarRajput-7
af6b54aeb4 fix: added safety check on uplot-xaxis logic to solve sentry issue (#6869) 2025-01-24 03:48:47 +05:30
Vikrant Gupta
1e61e6c2f6 feat(trace-details): query service changes for trace details (#6906)
* feat(trace-details): query service changes for trace details

* feat(trace-detail): refactor query service trace apis

* feat(trace-detail): minor bug fixes

* feat(trace-detail): address review comments

* feat(trace-detail): address review comments
2025-01-24 00:16:38 +05:30
Shivanshu Raj Shrivastava
390b04c015 chore: add global to inner join (#6912)
Signed-off-by: Shivanshu Raj Shrivastava <shivanshu1333@gmail.com>
2025-01-23 14:05:22 +00:00
Nityananda Gohain
c56345e1db fix: move analytics middleware (#6901)
* fix: move analytics middleware

* fix: use logger from params

* fix: use remove custom response writer

* fix: use correct names
2025-01-23 16:21:08 +05:30
Nityananda Gohain
7f25674640 fix: move log comment middleware (#6899)
* fix: move log comment middleware

* fix: move log comment to logger middleware

* fix: make logcomment a method of logging struct
2025-01-23 13:27:21 +05:30
Nityananda Gohain
df5ab64c83 fix: use common timeout middleware (#6866)
* fix: use common timeout middleware

* fix: use apiserver factory for config

* fix: add backward compatibility for old variables

* fix: remove apiserver provider and use config directly

* fix: remove apiserver interface

* fix: address comments

* fix: address minor comments

* fix: address minor comments
2025-01-22 17:48:47 +00:00
primus-bot[bot]
726f2b0fa2 chore(release): bump to v0.69.0 (#6898)
#### Summary
 - Release SigNoz v0.69.0
 - Bump SigNoz OTel Collector to v0.111.24

 Created by [Primus-Bot](https://github.com/apps/primus-bot)
2025-01-22 15:12:06 +05:30
Nityananda Gohain
4a0b0aafbd fix: use common logging middleware (#6865)
* feat: use common logging middleware

* fix: update comment

* fix: remove privatePort:true

* fix: remove staticfields from logging

* fix: remove staticfields from logging
2025-01-22 07:42:21 +00:00
Vibhu Pandey
837f434fe9 feat(sqlstore): open a connection to sql once (#6867) 2025-01-22 07:19:38 +00:00
Srikanth Chekuri
0baf0e9453 chore: address some gaps in infra (#6871) 2025-01-21 23:25:15 +05:30
Amlan Kumar Nandy
403043e076 feat: implement deployments, clusters and namespaces in k8s infra monitoring (#6786) 2025-01-21 21:57:32 +05:30
Shaheer Kochai
7730f76128 Revert "feat: show/hide timestamp and body fields in logs explorer (raw, defa…" (#6868)
This reverts commit b465f74e4a.
2025-01-21 15:03:03 +00:00
Vishal Sharma
6e3ffd555d fix: update azure event hub receiver config (#6864) 2025-01-21 14:35:23 +05:30
Nityananda Gohain
c565c2b865 fix: remove depricated message from ingestion dashboard (#6862) 2025-01-21 07:17:21 +00:00
Srikanth Chekuri
4ec1e66c7e chore: clickhouse sql support for v3 (#6778) 2025-01-21 04:09:40 +00:00
Srikanth Chekuri
89541862cc chore: correct order within page result (#6847) 2025-01-20 13:38:49 +00:00
Prashant Shahi
610f4d43e7 chore(install-script): install.sh script improvements (#6857)
### Summary

- support for docker compose plugin
- check for occupied port when installing SigNoz for the first time
- remove unused code

Signed-off-by: Prashant Shahi <prashant@signoz.io>
2025-01-20 13:26:40 +00:00
Ekansh Gupta
044a124cc1 feat: add search span scope in the list view tab to add the scope at per Query level (#6810)
* feat: add search span scope in the list view tab to add the scope at per query level

* feat: add search span scope in the list view tab to add the scope at per query level

* feat: add search span scope in the list view tab to add the scope at per query level

* feat: add search span scope in the list view tab to add the scope at per query level

* feat: add search span scope in the list view tab to add the scope at per query level

* feat: add search span scope in the list view tab to add the scope at per query level

* feat: add search span scope in the list view tab to add the scope at per query level

* feat: add search span scope in the list view tab to add the scope at per query level

* feat: add search span scope in the list view tab to add the scope at per query level

* feat: add search span scope in the list view tab to add the scope at per query level

* feat: add search span scope in the list view tab to add the scope at per query level
2025-01-20 18:04:19 +05:30
Vibhu Pandey
0cf9003e3a feat(.): initialize all factories (#6844)
### Summary

feat(.): initialize all factories

#### Related Issues / PR's

Removed all redundant commits of https://github.com/SigNoz/signoz/pull/6843
Closes https://github.com/SigNoz/signoz/pull/6782
2025-01-20 17:45:33 +05:30
Nityananda Gohain
644135a933 fix: remove analytics schema migrations (#6856) 2025-01-20 17:09:55 +05:30
Shaheer Kochai
b465f74e4a feat: show/hide timestamp and body fields in logs explorer (raw, default, column views) (#6831)
* feat: show/hide timestamp and body fields in logs explorer (raw, default, column views)

* fix: add width to log indicator column to ensure that a single column doesn't take half the space

---------

Co-authored-by: Nityananda Gohain <nityanandagohain@gmail.com>
2025-01-20 05:43:48 +00:00
Shaheer Kochai
e00e365964 chore: new alert rule documentation redirection tests (#6822)
* chore: new alert rule documentation redirection tests

* fix: fix the failing test

* fix: add test cases for anomaly based alert and fix the failing tests
2025-01-20 05:18:41 +00:00
Amlan Kumar Nandy
5c45e1f7b3 chore: infra monitoring bug fixes (#6795) 2025-01-18 10:55:50 +00:00
Nityananda Gohain
16e61b45ac fix: support in operator for json search (#6689)
* fix: support in in json search

* fix: remove else condition and update test cases

---------

Co-authored-by: Srikanth Chekuri <srikanth.chekuri92@gmail.com>
2025-01-18 01:44:25 +05:30
Vibhu Pandey
fdcdbf021a refactor(web): move to provider pattern (#6838)
### Summary

move to provider pattern
2025-01-17 20:03:21 +05:30
Vibhu Pandey
c92ef53e9c refactor(cache): move to provider pattern (#6837)
### Summary

Move cache to provider pattern
2025-01-17 18:09:39 +05:30
Vibhu Pandey
268f283785 feat(sqlmigrator): add sqlmigrator package (#6836)
### Summary

- add sqlmigrator package
2025-01-17 16:52:55 +05:30
Vibhu Pandey
c574adc634 feat(sqlstore): add sqlstore package (#6835)
### Summary

Add `sqlstore` package
2025-01-17 15:54:48 +05:30
Vibhu Pandey
939ab5270e feat(instrumentation): use new config factory (#6834)
### Summary

Use new config factory and remove redundant configuration possibilities from the upstream
2025-01-17 14:54:33 +05:30
Vibhu Pandey
42525b6067 feat(factory): add factory package (#6832)
- Introduces `Config`, `ConfigFactory`, `ProviderFactory`, and `Service` interfaces in `config.go`, `provider.go`, and `service.go`.
- Implements `NamedMap` for managing named factories in `named.go`.
- Adds `ProviderSettings` and `ScopedProviderSettings` for managing provider settings in `setting.go`.
2025-01-17 09:13:11 +00:00
Nishanth Arcot
c66cd3ce4e feat: update page titles for dashboards and alerts (#6706) 2025-01-17 09:02:41 +00:00
Vikrant Gupta
e9618d64bc fix(infra-monitoring): use proper axios instance for retrying the 401 req (#6841) 2025-01-17 10:33:09 +05:30
Raj Kamal Singh
8e11a988be chore: cloud integrations: include cloud account id in account status response (#6833) 2025-01-16 22:51:35 +05:30
Srikanth Chekuri
92299e1b08 chore: add count based limits for metrics (#6738) 2025-01-16 18:29:53 +05:30
Raj Kamal Singh
bab8c8274c feat: aws integration UI facing api: services (#6803)
* feat: cloud service integrations: get model and repo interface started

* feat: cloud service integrations: flesh out more of cloud services model

* feat: cloud integrations: reorganize things a little

* feat: cloud integrations: get svc controller started

* feat: cloud integrations: add stubs for EC2 and RDS postgres services

* feat: cloud integrations: add validation for listing and getting available svcs and some cleanup

* feat: cloud integrations: refactor helpers in existing integrations code for reuse

* feat: cloud integrations: parsing of cloud service definitions

* feat: cloud integrations: impl for getCloudProviderService

* feat: cloud integrations: some reorganization

* feat: cloud integrations: some more cleanup

* feat: cloud integrations: add validation for listing available cloud provider services

* feat: cloud integrations: API endpoint for listing available cloud provider services

* feat: cloud integrations: add validation for getting details of a particular service

* feat: cloud integrations: API endpoint for getting details of a service

* feat: cloud integrations: add controller validation for configuring cloud services

* feat: cloud integrations: get serviceConfigRepo started

* feat: cloud integrations: service config in service list summaries when queried for cloud account id

* feat: cloud integrations: only a supported service for a connected cloud account can be configured

* feat: cloud integrations: add validation for configuring services via the API

* feat: cloud integrations: API for configuring services

* feat: cloud integrations: some cleanup

* feat: cloud integrations: fix broken test

---------

Co-authored-by: Srikanth Chekuri <srikanth.chekuri92@gmail.com>
2025-01-16 17:36:09 +05:30
primus-bot[bot]
265c67e5bd chore(release): bump to v0.68.0 (#6824)
Co-authored-by: primus-bot[bot] <171087277+primus-bot[bot]@users.noreply.github.com>
2025-01-15 15:15:09 +05:30
SagarRajput-7
efc8c95d59 fix: fixed tables view not honouring order by in traces explorer (#6769)
* fix: fixed tables view not honouring order by in traces explorer

* fix: fixed order by count not being honoured in table view - trace
2025-01-15 15:00:40 +05:30
aniketio-ctrl
5708079c3c chore: added series aggregation for group by with value type panel (#6744) 2025-01-14 23:13:47 +05:30
dependabot[bot]
dbe78e55a9 chore(deps): bump golang.org/x/net from 0.29.0 to 0.33.0 (#6820)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-01-14 15:06:51 +00:00
Vishal Sharma
a60371fb80 chore: update telemetry events (#6804) 2025-01-14 01:03:12 +05:30
Raj Kamal Singh
d5b847c091 feat: aws integration: UI facing QS api for cloud account management (#6771)
* feat: init app/cloud_integrations

* feat: get API test started for cloudintegrations account lifecycle

* feat: cloudintegrations: get controller started

* feat: cloud integrations: add cloudintegrations.Controller to APIHandler and servers

* feat: cloud integrations: get routes started

* feat: cloud integrations: get accounts table schema started

* feat: cloud integrations: get cloudProviderAccountsSQLRepository started

* feat: cloud integrations: cloudProviderAccountsSQLRepository.listAccounts

* feat: cloud integrations: http handler and controller plumbing for /generate-connection-url

* feat: cloud integrations: cloudProviderAccountsSQLRepository.upsert

* feat: cloud integrations: finish up with /generate-connection-url

* feat: cloud integrations: add cloudProviderAccountsRepository.get

* feat: cloud integrations: add API test expectation for being able to get account status

* feat: cloud integrations: add http handler and controller method for getting account status

* feat: cloud integrations: ensure unconnected accounts aren't included in list of connected accounts

* feat: cloud integrations: add test expectation for agent check in request

* feat: cloud integrations: agent check in API

* feat: cloud integrations: ensure polling for status after agent check in works

* feat: cloud integrations: ensure account included in connected account list after agent check in

* feat: cloud integrations: add API expectation for updating account config

* feat: cloud integrations: API for updating cloud account config

* feat: cloud integrations: expectation for agent receiving latest config after account config update

* feat: cloud integrations: expectation for disconnecting cloud accounts from UI

* feat: cloud integrations: API for disconnecting cloud accounts

* feat: cloud integrations: some cleanup

* feat: cloud integrations: some more cleanup

* feat: cloud integrations: repo: scope rows by cloud provider

* feat: testutils: refactor out helper for creating a test sqlite DB

* feat: cloud integrations: controller: add test validating regeneration of connection url

* feat: cloud integrations: controller: validations for agent check ins

* feat: cloud integrations: connected account response structure

* feat: cloud integrations: API response account structure

* feat: cloud integrations: some more cleanup

* feat: cloud integrations: remove cloudProviderAccountsRepository.GetById

* feat: cloud integrations: shouldn't be able to disconnect non-existent account

* feat: cloud integrations: validate agents can't check in to cloud account with 2 signoz ids

* feat: cloud integrations: ensure agents can't check in to cloud account with 2 signoz ids

* feat: cloud integrations: remove stray import of ee/model in cloudintegrations controller
2025-01-10 18:43:35 +05:30
primus-bot[bot]
c106f1c9a9 chore(release): bump to v0.67.1 (#6792)
#### Summary
 - Release SigNoz v0.67.1

 Created by [Primus-Bot](https://github.com/apps/primus-bot)
2025-01-10 01:30:33 +05:30
SagarRajput-7
d6bfd95302 chore: added sentry alert for dashboard when query_range not called (#6791) 2025-01-10 00:57:53 +05:30
Vishal Sharma
68ee677630 chore: update default cloud plan text to Teams Cloud and Teams (#6790) 2025-01-09 19:04:47 +00:00
SagarRajput-7
3ff862b483 feat: updated the logic for variable update queue (#6586) (#6788)
* feat: updated the logic for variable update queue (#6586)

* feat: updated the logic for variable update queue

* feat: added API limiting to reduce unnecessary api call for dashboard variables (#6609)

* feat: added API limiting to reduce unneccesary api call for dashboard variables

* feat: fixed dropdown open triggering the api calls for single-select and misc

* feat: add jest test cases for new logic's utils, functions and processors - dashboardVariables (#6621)

* feat: added API limiting to reduce unneccesary api call for dashboard variables

* feat: fixed dropdown open triggering the api calls for single-select and misc

* feat: add jest test cases for new logic's utils, functions and processors - dashboardVariables

* feat: added test for checkAPIInvocation

* feat: refactor code

* feat: added more test on graph utilities

* feat: resolved comments and removed mount related handlings

* feat: fixed test cases and added multiple variable formats

---------

Co-authored-by: Srikanth Chekuri <srikanth.chekuri92@gmail.com>

* feat: made getDependency function dependent of variable name

---------

Co-authored-by: Srikanth Chekuri <srikanth.chekuri92@gmail.com>
2025-01-09 23:24:18 +05:30
Amlan Kumar Nandy
f91badbce9 feat: toggle off infra monitoring (#6787) 2025-01-09 19:55:08 +05:30
Shivanshu Raj Shrivastava
2ead4fbb66 fix: Modifies messaging queue paylod (#6783)
* fix: use filterset

Signed-off-by: Shivanshu Raj Shrivastava <shivanshu1333@gmail.com>
2025-01-09 16:26:06 +05:30
Vikrant Gupta
56b17bcfef Revert "feat: updated the logic for variable update queue (#6586)" (#6781)
This reverts commit dad72dd295.
2025-01-08 19:31:54 +00:00
Amlan Kumar Nandy
5839b65f7a fix: resolve k8s pods list local storage issue (#6775) 2025-01-08 19:37:08 +05:30
SagarRajput-7
3787c5ca24 chore: fix cookie vulnerability (#6773) 2025-01-08 17:44:12 +05:30
Yunus M
458cd28cc2 feat: pods and nodes implementation for k8s infra monitoring (#6558) 2025-01-08 16:13:54 +05:30
primus-bot[bot]
64a4606275 chore(release): bump to v0.67.0 (#6772)
#### Summary
 - Release SigNoz v0.67.0
 - Bump SigNoz OTel Collector to v0.111.22

 Created by [Primus-Bot](https://github.com/apps/primus-bot)
2025-01-08 15:30:00 +05:30
Shivanshu Raj Shrivastava
505757b971 [feat] Add overview APIs for Celery and Kafka in messaging queues integration (#6756)
* feat: added queue overview api for generic messaging system
Signed-off-by: Shivanshu Raj Shrivastava <shivanshu1333@gmail.com>
2025-01-08 06:50:11 +00:00
Vikrant Gupta
80740f646c fix(portal): use window.location.origin instead of the window.location.href (#6770)
* fix(portal): use window.location.origin instead of the whole URL

* fix(portal): use window.location.origin instead of the whole URL
2025-01-08 05:56:18 +00:00
Yunus M
e92d055c30 feat: enable billing and org-settings in workspace blocked state (#6761)
* feat: enable billing and org-settings in workspace blocked state

* feat: disable sidebar items in workspace blocked state
2025-01-08 05:46:41 +00:00
Yunus M
5c546e8efd feat: return is data is fetching (#6765) 2025-01-07 19:28:49 +05:30
Vikrant Gupta
ecd50f7232 feat(timeline): add new timeline v2 component (#6760)
* feat(timeline): base commit for timeline v2

* feat(timeline): svg rendering for timeline v2

* feat(timeline): dynamic scale based on screen size

* feat(timeline): cleanup code

* feat(timeline): make position functioning of timeline height
2025-01-07 14:09:06 +05:30
342 changed files with 31211 additions and 2239 deletions

35
.github/workflows/goreleaser.yaml vendored Normal file
View File

@@ -0,0 +1,35 @@
name: goreleaser
on:
push:
tags:
- v*
- histogram-quantile/v*
permissions:
contents: write
jobs:
goreleaser:
runs-on: ubuntu-latest
strategy:
matrix:
workdirs:
- scripts/clickhouse/histogramquantile
steps:
- name: checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: set-up-go
uses: actions/setup-go@v5
- name: run-goreleaser
uses: goreleaser/goreleaser-action@v6
with:
distribution: goreleaser-pro
version: '~> v2'
args: release --clean
workdir: ${{ matrix.workdirs }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GORELEASER_KEY: ${{ secrets.GORELEASER_KEY }}

2
.gitignore vendored
View File

@@ -70,3 +70,5 @@ vendor/
# git-town
.git-branches.toml
# goreleaser
dist/

View File

@@ -17,6 +17,7 @@ QUERY_SERVICE_DIRECTORY ?= pkg/query-service
EE_QUERY_SERVICE_DIRECTORY ?= ee/query-service
STANDALONE_DIRECTORY ?= deploy/docker/clickhouse-setup
SWARM_DIRECTORY ?= deploy/docker-swarm/clickhouse-setup
CH_HISTOGRAM_QUANTILE_DIRECTORY ?= scripts/clickhouse/histogramquantile
GOOS ?= $(shell go env GOOS)
GOARCH ?= $(shell go env GOARCH)
@@ -191,3 +192,15 @@ check-no-ee-references:
test:
go test ./pkg/...
goreleaser-snapshot:
@if [[ ${GORELEASER_WORKDIR} ]]; then \
cd ${GORELEASER_WORKDIR} && \
goreleaser release --clean --snapshot; \
cd -; \
else \
goreleaser release --clean --snapshot; \
fi
goreleaser-snapshot-histogram-quantile:
make GORELEASER_WORKDIR=$(CH_HISTOGRAM_QUANTILE_DIRECTORY) goreleaser-snapshot

View File

@@ -1,32 +0,0 @@
##################### SigNoz Configuration Defaults #####################
#
# Do not modify this file
#
##################### Web #####################
web:
# The prefix to serve web on
prefix: /
# The directory containing the static build files.
directory: /etc/signoz/web
##################### Cache #####################
cache:
# specifies the caching provider to use.
provider: memory
# memory: Uses in-memory caching.
memory:
# Time-to-live for cache entries in memory. Specify the duration in ns
ttl: 60000000000
# The interval at which the cache will be cleaned up
cleanupInterval:
# redis: Uses Redis as the caching backend.
redis:
# The hostname or IP address of the Redis server.
host: localhost
# The port on which the Redis server is running. Default is usually 6379.
port: 6379
# The password for authenticating with the Redis server, if required.
password:
# The Redis database number to use
db: 0

80
conf/example.yaml Normal file
View File

@@ -0,0 +1,80 @@
##################### SigNoz Configuration Example #####################
#
# Do not modify this file
#
##################### Instrumentation #####################
instrumentation:
logs:
# The log level to use.
level: info
traces:
# Whether to enable tracing.
enabled: false
processors:
batch:
exporter:
otlp:
endpoint: localhost:4317
metrics:
# Whether to enable metrics.
enabled: true
readers:
pull:
exporter:
prometheus:
host: "0.0.0.0"
port: 9090
##################### Web #####################
web:
# Whether to enable the web frontend
enabled: true
# The prefix to serve web on
prefix: /
# The directory containing the static build files.
directory: /etc/signoz/web
##################### Cache #####################
cache:
# specifies the caching provider to use.
provider: memory
# memory: Uses in-memory caching.
memory:
# Time-to-live for cache entries in memory. Specify the duration in ns
ttl: 60000000000
# The interval at which the cache will be cleaned up
cleanupInterval: 1m
# redis: Uses Redis as the caching backend.
redis:
# The hostname or IP address of the Redis server.
host: localhost
# The port on which the Redis server is running. Default is usually 6379.
port: 6379
# The password for authenticating with the Redis server, if required.
password:
# The Redis database number to use
db: 0
##################### SQLStore #####################
sqlstore:
# specifies the SQLStore provider to use.
provider: sqlite
# The maximum number of open connections to the database.
max_open_conns: 100
sqlite:
# The path to the SQLite database file.
path: /var/lib/signoz/signoz.db
##################### APIServer #####################
apiserver:
timeout:
default: 60s
max: 600s
excluded_routes:
- /api/v1/logs/tail
- /api/v3/logs/livetail
logging:
excluded_routes:
- /api/v1/health

View File

@@ -130,7 +130,7 @@ services:
restart_policy:
condition: on-failure
query-service:
image: signoz/query-service:0.66.0
image: signoz/query-service:0.69.0
command: ["-config=/root/config/prometheus.yml", "--use-logs-new-schema=true", "--use-trace-new-schema=true"]
# ports:
# - "6060:6060" # pprof port
@@ -158,7 +158,7 @@ services:
condition: on-failure
!!merge <<: *db-depend
frontend:
image: signoz/frontend:0.66.0
image: signoz/frontend:0.69.0
deploy:
restart_policy:
condition: on-failure
@@ -170,7 +170,7 @@ services:
volumes:
- ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf
otel-collector:
image: signoz/signoz-otel-collector:0.111.21
image: signoz/signoz-otel-collector:0.111.24
command: ["--config=/etc/otel-collector-config.yaml", "--manager-config=/etc/manager-config.yaml", "--feature-gates=-pkg.translator.prometheus.NormalizeName"]
user: root # required for reading docker container logs
volumes:
@@ -202,7 +202,7 @@ services:
- otel-collector-migrator
- query-service
otel-collector-migrator:
image: signoz/signoz-schema-migrator:0.111.21
image: signoz/signoz-schema-migrator:0.111.24
deploy:
restart_policy:
condition: on-failure

View File

@@ -57,7 +57,7 @@ services:
- --queryService.url=http://query-service:8085
- --storage.path=/data
otel-collector-migrator:
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.111.21}
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.111.24}
container_name: otel-migrator
command:
- "sync"
@@ -73,7 +73,7 @@ services:
# Notes for Maintainers/Contributors who will change Line Numbers of Frontend & Query-Section. Please Update Line Numbers in `./scripts/commentLinesForSetup.sh` & `./CONTRIBUTING.md`
otel-collector:
container_name: signoz-otel-collector
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.111.21}
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.111.24}
command: ["--config=/etc/otel-collector-config.yaml", "--manager-config=/etc/manager-config.yaml", "--copy-path=/var/tmp/collector-config.yaml", "--feature-gates=-pkg.translator.prometheus.NormalizeName"]
# user: root # required for reading docker container logs
volumes:

View File

@@ -145,7 +145,7 @@ services:
- --storage.path=/data
# Notes for Maintainers/Contributors who will change Line Numbers of Frontend & Query-Section. Please Update Line Numbers in `./scripts/commentLinesForSetup.sh` & `./CONTRIBUTING.md`
query-service:
image: signoz/query-service:${DOCKER_TAG:-0.66.0}
image: signoz/query-service:${DOCKER_TAG:-0.69.0}
container_name: signoz-query-service
command: ["-config=/root/config/prometheus.yml", "--use-logs-new-schema=true", "--use-trace-new-schema=true"]
# ports:
@@ -172,7 +172,7 @@ services:
retries: 3
!!merge <<: *db-depend
frontend:
image: signoz/frontend:${DOCKER_TAG:-0.66.0}
image: signoz/frontend:${DOCKER_TAG:-0.69.0}
container_name: signoz-frontend
restart: on-failure
depends_on:
@@ -183,7 +183,7 @@ services:
volumes:
- ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf
otel-collector-migrator-sync:
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.111.21}
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.111.24}
container_name: otel-migrator-sync
command:
- "sync"
@@ -197,7 +197,7 @@ services:
# clickhouse-3:
# condition: service_healthy
otel-collector-migrator-async:
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.111.21}
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.111.24}
container_name: otel-migrator-async
command:
- "async"
@@ -213,7 +213,7 @@ services:
# clickhouse-3:
# condition: service_healthy
otel-collector:
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.111.21}
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.111.24}
container_name: signoz-otel-collector
command: ["--config=/etc/otel-collector-config.yaml", "--manager-config=/etc/manager-config.yaml", "--copy-path=/var/tmp/collector-config.yaml", "--feature-gates=-pkg.translator.prometheus.NormalizeName"]
user: root # required for reading docker container logs

View File

@@ -148,7 +148,7 @@ services:
- --storage.path=/data
# Notes for Maintainers/Contributors who will change Line Numbers of Frontend & Query-Section. Please Update Line Numbers in `./scripts/commentLinesForSetup.sh` & `./CONTRIBUTING.md`
query-service:
image: signoz/query-service:${DOCKER_TAG:-0.66.0}
image: signoz/query-service:${DOCKER_TAG:-0.69.0}
container_name: signoz-query-service
command: ["-config=/root/config/prometheus.yml", "-gateway-url=https://api.staging.signoz.cloud", "--use-logs-new-schema=true", "--use-trace-new-schema=true"]
# ports:
@@ -176,7 +176,7 @@ services:
retries: 3
!!merge <<: *db-depend
frontend:
image: signoz/frontend:${DOCKER_TAG:-0.66.0}
image: signoz/frontend:${DOCKER_TAG:-0.69.0}
container_name: signoz-frontend
restart: on-failure
depends_on:
@@ -187,7 +187,7 @@ services:
volumes:
- ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf
otel-collector-migrator:
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.111.21}
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.111.24}
container_name: otel-migrator
command:
- "--dsn=tcp://clickhouse:9000"
@@ -199,7 +199,7 @@ services:
# clickhouse-3:
# condition: service_healthy
otel-collector:
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.111.21}
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.111.24}
container_name: signoz-otel-collector
command: ["--config=/etc/otel-collector-config.yaml", "--manager-config=/etc/manager-config.yaml", "--copy-path=/var/tmp/collector-config.yaml", "--feature-gates=-pkg.translator.prometheus.NormalizeName"]
user: root # required for reading docker container logs

View File

@@ -0,0 +1,2 @@
This directory is deprecated and will be removed in the future.
Please use the new directory for Clickhouse setup scripts: `scripts/clickhouse` instead.

View File

@@ -32,6 +32,11 @@ has_cmd() {
command -v "$1" > /dev/null 2>&1
}
# Check if docker compose plugin is present
has_docker_compose_plugin() {
docker compose version > /dev/null 2>&1
}
is_mac() {
[[ $OSTYPE == darwin* ]]
}
@@ -183,9 +188,7 @@ install_docker() {
$sudo_cmd yum-config-manager --add-repo https://download.docker.com/linux/$os/docker-ce.repo
echo "Installing docker"
$yum_cmd install docker-ce docker-ce-cli containerd.io
fi
}
compose_version () {
@@ -227,12 +230,6 @@ start_docker() {
echo "Starting docker service"
$sudo_cmd systemctl start docker.service
fi
# if [[ -z $sudo_cmd ]]; then
# docker ps > /dev/null && true
# if [[ $? -ne 0 ]]; then
# request_sudo
# fi
# fi
if [[ -z $sudo_cmd ]]; then
if ! docker ps > /dev/null && true; then
request_sudo
@@ -265,7 +262,7 @@ bye() { # Prints a friendly good bye message and exits the script.
echo "🔴 The containers didn't seem to start correctly. Please run the following command to check containers that may have errored out:"
echo ""
echo -e "$sudo_cmd docker-compose -f ./docker/clickhouse-setup/docker-compose.yaml ps -a"
echo -e "$sudo_cmd $docker_compose_cmd -f ./docker/clickhouse-setup/docker-compose.yaml ps -a"
echo "Please read our troubleshooting guide https://signoz.io/docs/install/troubleshooting/"
echo "or reach us for support in #help channel in our Slack Community https://signoz.io/slack"
@@ -296,11 +293,6 @@ request_sudo() {
if (( $EUID != 0 )); then
sudo_cmd="sudo"
echo -e "Please enter your sudo password, if prompted."
# $sudo_cmd -l | grep -e "NOPASSWD: ALL" > /dev/null
# if [[ $? -ne 0 ]] && ! $sudo_cmd -v; then
# echo "Need sudo privileges to proceed with the installation."
# exit 1;
# fi
if ! $sudo_cmd -l | grep -e "NOPASSWD: ALL" > /dev/null && ! $sudo_cmd -v; then
echo "Need sudo privileges to proceed with the installation."
exit 1;
@@ -317,6 +309,7 @@ echo -e "👋 Thank you for trying out SigNoz! "
echo ""
sudo_cmd=""
docker_compose_cmd=""
# Check sudo permissions
if (( $EUID != 0 )); then
@@ -362,28 +355,8 @@ else
SIGNOZ_INSTALLATION_ID=$(echo "$sysinfo" | $digest_cmd | grep -E -o '[a-zA-Z0-9]{64}')
fi
# echo ""
# echo -e "👉 ${RED}Two ways to go forward\n"
# echo -e "${RED}1) ClickHouse as database (default)\n"
# read -p "⚙️ Enter your preference (1/2):" choice_setup
# while [[ $choice_setup != "1" && $choice_setup != "2" && $choice_setup != "" ]]
# do
# # echo $choice_setup
# echo -e "\n❌ ${CYAN}Please enter either 1 or 2"
# read -p "⚙️ Enter your preference (1/2): " choice_setup
# # echo $choice_setup
# done
# if [[ $choice_setup == "1" || $choice_setup == "" ]];then
# setup_type='clickhouse'
# fi
setup_type='clickhouse'
# echo -e "\n✅ ${CYAN}You have chosen: ${setup_type} setup\n"
# Run bye if failure happens
trap bye EXIT
@@ -455,8 +428,6 @@ if [[ $desired_os -eq 0 ]]; then
send_event "os_not_supported"
fi
# check_ports_occupied
# Check is Docker daemon is installed and available. If not, the install & start Docker for Linux machines. We cannot automatically install Docker Desktop on Mac OS
if ! is_command_present docker; then
@@ -486,27 +457,39 @@ if ! is_command_present docker; then
fi
fi
if has_docker_compose_plugin; then
echo "docker compose plugin is present, using it"
docker_compose_cmd="docker compose"
# Install docker-compose
if ! is_command_present docker-compose; then
request_sudo
install_docker_compose
else
docker_compose_cmd="docker-compose"
if ! is_command_present docker-compose; then
request_sudo
install_docker_compose
fi
fi
start_docker
# $sudo_cmd docker-compose -f ./docker/clickhouse-setup/docker-compose.yaml up -d --remove-orphans || true
# check for open ports, if signoz is not installed
if is_command_present docker-compose; then
if $sudo_cmd $docker_compose_cmd -f ./docker/clickhouse-setup/docker-compose.yaml ps | grep "signoz-query-service" | grep -q "healthy" > /dev/null 2>&1; then
echo "SigNoz already installed, skipping the occupied ports check"
else
check_ports_occupied
fi
fi
echo ""
echo -e "\n🟡 Pulling the latest container images for SigNoz.\n"
$sudo_cmd docker-compose -f ./docker/clickhouse-setup/docker-compose.yaml pull
$sudo_cmd $docker_compose_cmd -f ./docker/clickhouse-setup/docker-compose.yaml pull
echo ""
echo "🟡 Starting the SigNoz containers. It may take a few minutes ..."
echo
# The docker-compose command does some nasty stuff for the `--detach` functionality. So we add a `|| true` so that the
# The $docker_compose_cmd command does some nasty stuff for the `--detach` functionality. So we add a `|| true` so that the
# script doesn't exit because this command looks like it failed to do it's thing.
$sudo_cmd docker-compose -f ./docker/clickhouse-setup/docker-compose.yaml up --detach --remove-orphans || true
$sudo_cmd $docker_compose_cmd -f ./docker/clickhouse-setup/docker-compose.yaml up --detach --remove-orphans || true
wait_for_containers_start 60
echo ""
@@ -516,7 +499,7 @@ if [[ $status_code -ne 200 ]]; then
echo "🔴 The containers didn't seem to start correctly. Please run the following command to check containers that may have errored out:"
echo ""
echo -e "$sudo_cmd docker-compose -f ./docker/clickhouse-setup/docker-compose.yaml ps -a"
echo -e "$sudo_cmd $docker_compose_cmd -f ./docker/clickhouse-setup/docker-compose.yaml ps -a"
echo "Please read our troubleshooting guide https://signoz.io/docs/install/troubleshooting/"
echo "or reach us on SigNoz for support https://signoz.io/slack"
@@ -537,7 +520,7 @@ else
echo " By default, retention period is set to 15 days for logs and traces, and 30 days for metrics."
echo -e "To change this, navigate to the General tab on the Settings page of SigNoz UI. For more details, refer to https://signoz.io/docs/userguide/retention-period \n"
echo " To bring down SigNoz and clean volumes : $sudo_cmd docker-compose -f ./docker/clickhouse-setup/docker-compose.yaml down -v"
echo " To bring down SigNoz and clean volumes : $sudo_cmd $docker_compose_cmd -f ./docker/clickhouse-setup/docker-compose.yaml down -v"
echo ""
echo "+++++++++++++++++++++++++++++++++++++++++++++++++"

View File

@@ -12,6 +12,7 @@ import (
"go.signoz.io/signoz/ee/query-service/license"
"go.signoz.io/signoz/ee/query-service/usage"
baseapp "go.signoz.io/signoz/pkg/query-service/app"
"go.signoz.io/signoz/pkg/query-service/app/cloudintegrations"
"go.signoz.io/signoz/pkg/query-service/app/integrations"
"go.signoz.io/signoz/pkg/query-service/app/logparsingpipeline"
"go.signoz.io/signoz/pkg/query-service/cache"
@@ -34,6 +35,7 @@ type APIHandlerOptions struct {
FeatureFlags baseint.FeatureLookup
LicenseManager *license.Manager
IntegrationsController *integrations.Controller
CloudIntegrationsController *cloudintegrations.Controller
LogsParsingPipelineController *logparsingpipeline.LogParsingPipelineController
Cache cache.Cache
Gateway *httputil.ReverseProxy
@@ -62,6 +64,7 @@ func NewAPIHandler(opts APIHandlerOptions) (*APIHandler, error) {
RuleManager: opts.RulesManager,
FeatureFlags: opts.FeatureFlags,
IntegrationsController: opts.IntegrationsController,
CloudIntegrationsController: opts.CloudIntegrationsController,
LogsParsingPipelineController: opts.LogsParsingPipelineController,
Cache: opts.Cache,
FluxInterval: opts.FluxInterval,

View File

@@ -7,6 +7,7 @@ import (
"github.com/jmoiron/sqlx"
"go.signoz.io/signoz/pkg/cache"
basechr "go.signoz.io/signoz/pkg/query-service/app/clickhouseReader"
"go.signoz.io/signoz/pkg/query-service/interfaces"
)
@@ -27,8 +28,10 @@ func NewDataConnector(
cluster string,
useLogsNewSchema bool,
useTraceNewSchema bool,
fluxIntervalForTraceDetail time.Duration,
cache cache.Cache,
) *ClickhouseReader {
ch := basechr.NewReader(localDB, promConfigPath, lm, maxIdleConns, maxOpenConns, dialTimeout, cluster, useLogsNewSchema, useTraceNewSchema)
ch := basechr.NewReader(localDB, promConfigPath, lm, maxIdleConns, maxOpenConns, dialTimeout, cluster, useLogsNewSchema, useTraceNewSchema, fluxIntervalForTraceDetail, cache)
return &ClickhouseReader{
conn: ch.GetConn(),
appdb: localDB,

View File

@@ -29,8 +29,8 @@ import (
"go.signoz.io/signoz/ee/query-service/integrations/gateway"
"go.signoz.io/signoz/ee/query-service/interfaces"
"go.signoz.io/signoz/ee/query-service/rules"
"go.signoz.io/signoz/pkg/http/middleware"
baseauth "go.signoz.io/signoz/pkg/query-service/auth"
"go.signoz.io/signoz/pkg/query-service/migrate"
v3 "go.signoz.io/signoz/pkg/query-service/model/v3"
"go.signoz.io/signoz/pkg/signoz"
"go.signoz.io/signoz/pkg/web"
@@ -40,6 +40,7 @@ import (
"go.signoz.io/signoz/pkg/query-service/agentConf"
baseapp "go.signoz.io/signoz/pkg/query-service/app"
"go.signoz.io/signoz/pkg/query-service/app/cloudintegrations"
"go.signoz.io/signoz/pkg/query-service/app/dashboards"
baseexplorer "go.signoz.io/signoz/pkg/query-service/app/explorer"
"go.signoz.io/signoz/pkg/query-service/app/integrations"
@@ -63,25 +64,26 @@ import (
const AppDbEngine = "sqlite"
type ServerOptions struct {
Config signoz.Config
SigNoz *signoz.SigNoz
PromConfigPath string
SkipTopLvlOpsPath string
HTTPHostPort string
PrivateHostPort string
// alert specific params
DisableRules bool
RuleRepoURL string
PreferSpanMetrics bool
MaxIdleConns int
MaxOpenConns int
DialTimeout time.Duration
CacheConfigPath string
FluxInterval string
Cluster string
GatewayUrl string
UseLogsNewSchema bool
UseTraceNewSchema bool
SkipWebFrontend bool
DisableRules bool
RuleRepoURL string
PreferSpanMetrics bool
MaxIdleConns int
MaxOpenConns int
DialTimeout time.Duration
CacheConfigPath string
FluxInterval string
FluxIntervalForTraceDetail string
Cluster string
GatewayUrl string
UseLogsNewSchema bool
UseTraceNewSchema bool
}
// Server runs HTTP api service
@@ -112,25 +114,22 @@ func (s Server) HealthCheckStatus() chan healthcheck.Status {
// NewServer creates and initializes Server
func NewServer(serverOptions *ServerOptions) (*Server, error) {
modelDao, err := dao.InitDao("sqlite", baseconst.RELATIONAL_DATASOURCE_PATH)
modelDao, err := dao.InitDao(serverOptions.SigNoz.SQLStore.SQLxDB())
if err != nil {
return nil, err
}
baseexplorer.InitWithDSN(baseconst.RELATIONAL_DATASOURCE_PATH)
if err := preferences.InitDB(baseconst.RELATIONAL_DATASOURCE_PATH); err != nil {
if err := baseexplorer.InitWithDSN(serverOptions.SigNoz.SQLStore.SQLxDB()); err != nil {
return nil, err
}
localDB, err := dashboards.InitDB(baseconst.RELATIONAL_DATASOURCE_PATH)
if err != nil {
if err := preferences.InitDB(serverOptions.SigNoz.SQLStore.SQLxDB()); err != nil {
return nil, err
}
localDB.SetMaxOpenConns(10)
if err := dashboards.InitDB(serverOptions.SigNoz.SQLStore.SQLxDB()); err != nil {
return nil, err
}
gatewayProxy, err := gateway.NewProxy(serverOptions.GatewayUrl, gateway.RoutePrefix)
if err != nil {
@@ -138,7 +137,7 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) {
}
// initiate license manager
lm, err := licensepkg.StartManager("sqlite", localDB)
lm, err := licensepkg.StartManager(serverOptions.SigNoz.SQLStore.SQLxDB())
if err != nil {
return nil, err
}
@@ -147,12 +146,17 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) {
modelDao.SetFlagProvider(lm)
readerReady := make(chan bool)
fluxIntervalForTraceDetail, err := time.ParseDuration(serverOptions.FluxIntervalForTraceDetail)
if err != nil {
return nil, err
}
var reader interfaces.DataConnector
storage := os.Getenv("STORAGE")
if storage == "clickhouse" {
zap.L().Info("Using ClickHouse as datastore ...")
qb := db.NewDataConnector(
localDB,
serverOptions.SigNoz.SQLStore.SQLxDB(),
serverOptions.PromConfigPath,
lm,
serverOptions.MaxIdleConns,
@@ -161,6 +165,8 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) {
serverOptions.Cluster,
serverOptions.UseLogsNewSchema,
serverOptions.UseTraceNewSchema,
fluxIntervalForTraceDetail,
serverOptions.SigNoz.Cache,
)
go qb.Start(readerReady)
reader = qb
@@ -188,7 +194,7 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) {
rm, err := makeRulesManager(serverOptions.PromConfigPath,
baseconst.GetAlertManagerApiPrefix(),
serverOptions.RuleRepoURL,
localDB,
serverOptions.SigNoz.SQLStore.SQLxDB(),
reader,
c,
serverOptions.DisableRules,
@@ -201,29 +207,29 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) {
return nil, err
}
go func() {
err = migrate.ClickHouseMigrate(reader.GetConn(), serverOptions.Cluster)
if err != nil {
zap.L().Error("error while running clickhouse migrations", zap.Error(err))
}
}()
// initiate opamp
_, err = opAmpModel.InitDB(localDB)
_, err = opAmpModel.InitDB(serverOptions.SigNoz.SQLStore.SQLxDB())
if err != nil {
return nil, err
}
integrationsController, err := integrations.NewController(localDB)
integrationsController, err := integrations.NewController(serverOptions.SigNoz.SQLStore.SQLxDB())
if err != nil {
return nil, fmt.Errorf(
"couldn't create integrations controller: %w", err,
)
}
cloudIntegrationsController, err := cloudintegrations.NewController(serverOptions.SigNoz.SQLStore.SQLxDB())
if err != nil {
return nil, fmt.Errorf(
"couldn't create cloud provider integrations controller: %w", err,
)
}
// ingestion pipelines manager
logParsingPipelineController, err := logparsingpipeline.NewLogParsingPipelinesController(
localDB, "sqlite", integrationsController.GetPipelinesForInstalledIntegrations,
serverOptions.SigNoz.SQLStore.SQLxDB(), integrationsController.GetPipelinesForInstalledIntegrations,
)
if err != nil {
return nil, err
@@ -231,8 +237,7 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) {
// initiate agent config handler
agentConfMgr, err := agentConf.Initiate(&agentConf.ManagerOptions{
DB: localDB,
DBEngine: AppDbEngine,
DB: serverOptions.SigNoz.SQLStore.SQLxDB(),
AgentFeatures: []agentConf.AgentFeature{logParsingPipelineController},
})
if err != nil {
@@ -240,7 +245,7 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) {
}
// start the usagemanager
usageManager, err := usage.New("sqlite", modelDao, lm.GetRepo(), reader.GetConn())
usageManager, err := usage.New(modelDao, lm.GetRepo(), reader.GetConn())
if err != nil {
return nil, err
}
@@ -253,7 +258,6 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) {
telemetry.GetInstance().SetSaasOperator(constants.SaasSegmentKey)
fluxInterval, err := time.ParseDuration(serverOptions.FluxInterval)
if err != nil {
return nil, err
}
@@ -271,6 +275,7 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) {
FeatureFlags: lm,
LicenseManager: lm,
IntegrationsController: integrationsController,
CloudIntegrationsController: cloudIntegrationsController,
LogsParsingPipelineController: logParsingPipelineController,
Cache: c,
FluxInterval: fluxInterval,
@@ -319,10 +324,13 @@ func (s *Server) createPrivateServer(apiHandler *api.APIHandler) (*http.Server,
r := baseapp.NewRouter()
r.Use(setTimeoutMiddleware)
r.Use(s.analyticsMiddleware)
r.Use(loggingMiddlewarePrivate)
r.Use(baseapp.LogCommentEnricher)
r.Use(middleware.NewTimeout(zap.L(),
s.serverOptions.Config.APIServer.Timeout.ExcludedRoutes,
s.serverOptions.Config.APIServer.Timeout.Default,
s.serverOptions.Config.APIServer.Timeout.Max,
).Wrap)
r.Use(middleware.NewAnalytics(zap.L()).Wrap)
r.Use(middleware.NewLogging(zap.L(), s.serverOptions.Config.APIServer.Logging.ExcludedRoutes).Wrap)
apiHandler.RegisterPrivateRoutes(r)
@@ -342,7 +350,7 @@ func (s *Server) createPrivateServer(apiHandler *api.APIHandler) (*http.Server,
}, nil
}
func (s *Server) createPublicServer(apiHandler *api.APIHandler, web *web.Web) (*http.Server, error) {
func (s *Server) createPublicServer(apiHandler *api.APIHandler, web web.Web) (*http.Server, error) {
r := baseapp.NewRouter()
@@ -362,14 +370,18 @@ func (s *Server) createPublicServer(apiHandler *api.APIHandler, web *web.Web) (*
}
am := baseapp.NewAuthMiddleware(getUserFromRequest)
r.Use(setTimeoutMiddleware)
r.Use(s.analyticsMiddleware)
r.Use(loggingMiddleware)
r.Use(baseapp.LogCommentEnricher)
r.Use(middleware.NewTimeout(zap.L(),
s.serverOptions.Config.APIServer.Timeout.ExcludedRoutes,
s.serverOptions.Config.APIServer.Timeout.Default,
s.serverOptions.Config.APIServer.Timeout.Max,
).Wrap)
r.Use(middleware.NewAnalytics(zap.L()).Wrap)
r.Use(middleware.NewLogging(zap.L(), s.serverOptions.Config.APIServer.Logging.ExcludedRoutes).Wrap)
apiHandler.RegisterRoutes(r, am)
apiHandler.RegisterLogsRoutes(r, am)
apiHandler.RegisterIntegrationRoutes(r, am)
apiHandler.RegisterCloudIntegrationsRoutes(r, am)
apiHandler.RegisterQueryRangeV3Routes(r, am)
apiHandler.RegisterInfraMetricsRoutes(r, am)
apiHandler.RegisterQueryRangeV4Routes(r, am)
@@ -386,11 +398,9 @@ func (s *Server) createPublicServer(apiHandler *api.APIHandler, web *web.Web) (*
handler = handlers.CompressHandler(handler)
if !s.serverOptions.SkipWebFrontend {
err := web.AddToRouter(r)
if err != nil {
return nil, err
}
err := web.AddToRouter(r)
if err != nil {
return nil, err
}
return &http.Server{
@@ -398,31 +408,6 @@ func (s *Server) createPublicServer(apiHandler *api.APIHandler, web *web.Web) (*
}, nil
}
// TODO(remove): Implemented at pkg/http/middleware/logging.go
// loggingMiddleware is used for logging public api calls
func loggingMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
route := mux.CurrentRoute(r)
path, _ := route.GetPathTemplate()
startTime := time.Now()
next.ServeHTTP(w, r)
zap.L().Info(path, zap.Duration("timeTaken", time.Since(startTime)), zap.String("path", path))
})
}
// TODO(remove): Implemented at pkg/http/middleware/logging.go
// loggingMiddlewarePrivate is used for logging private api calls
// from internal services like alert manager
func loggingMiddlewarePrivate(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
route := mux.CurrentRoute(r)
path, _ := route.GetPathTemplate()
startTime := time.Now()
next.ServeHTTP(w, r)
zap.L().Info(path, zap.Duration("timeTaken", time.Since(startTime)), zap.String("path", path), zap.Bool("tprivatePort", true))
})
}
// TODO(remove): Implemented at pkg/http/middleware/logging.go
type loggingResponseWriter struct {
http.ResponseWriter
@@ -502,32 +487,29 @@ func extractQueryRangeData(path string, r *http.Request) (map[string]interface{}
zap.L().Error("error while matching the trace explorer: ", zap.Error(err))
}
signozMetricsUsed := false
signozLogsUsed := false
signozTracesUsed := false
if postData != nil {
queryInfoResult := telemetry.GetInstance().CheckQueryInfo(postData)
if postData.CompositeQuery != nil {
data["queryType"] = postData.CompositeQuery.QueryType
data["panelType"] = postData.CompositeQuery.PanelType
signozLogsUsed, signozMetricsUsed, signozTracesUsed = telemetry.GetInstance().CheckSigNozSignals(postData)
}
}
if signozMetricsUsed || signozLogsUsed || signozTracesUsed {
if signozMetricsUsed {
if (queryInfoResult.MetricsUsed || queryInfoResult.LogsUsed || queryInfoResult.TracesUsed) && (queryInfoResult.FilterApplied) {
if queryInfoResult.MetricsUsed {
telemetry.GetInstance().AddActiveMetricsUser()
}
if signozLogsUsed {
if queryInfoResult.LogsUsed {
telemetry.GetInstance().AddActiveLogsUser()
}
if signozTracesUsed {
if queryInfoResult.TracesUsed {
telemetry.GetInstance().AddActiveTracesUser()
}
data["metricsUsed"] = signozMetricsUsed
data["logsUsed"] = signozLogsUsed
data["tracesUsed"] = signozTracesUsed
data["metricsUsed"] = queryInfoResult.MetricsUsed
data["logsUsed"] = queryInfoResult.LogsUsed
data["tracesUsed"] = queryInfoResult.TracesUsed
data["filterApplied"] = queryInfoResult.FilterApplied
data["groupByApplied"] = queryInfoResult.GroupByApplied
data["aggregateOperator"] = queryInfoResult.AggregateOperator
data["aggregateAttributeKey"] = queryInfoResult.AggregateAttributeKey
data["numberOfQueries"] = queryInfoResult.NumberOfQueries
data["queryType"] = queryInfoResult.QueryType
data["panelType"] = queryInfoResult.PanelType
userEmail, err := baseauth.GetEmailFromJwt(r.Context())
if err == nil {
// switch case to set data["screen"] based on the referrer
@@ -594,23 +576,6 @@ func (s *Server) analyticsMiddleware(next http.Handler) http.Handler {
})
}
// TODO(remove): Implemented at pkg/http/middleware/timeout.go
func setTimeoutMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
var cancel context.CancelFunc
// check if route is not excluded
url := r.URL.Path
if _, ok := baseconst.TimeoutExcludedRoutes[url]; !ok {
ctx, cancel = context.WithTimeout(r.Context(), baseconst.ContextTimeout)
defer cancel()
}
r = r.WithContext(ctx)
next.ServeHTTP(w, r)
})
}
// initListeners initialises listeners of the server
func (s *Server) initListeners() error {
// listen on public port

View File

@@ -1,18 +1,10 @@
package dao
import (
"fmt"
"github.com/jmoiron/sqlx"
"go.signoz.io/signoz/ee/query-service/dao/sqlite"
)
func InitDao(engine, path string) (ModelDao, error) {
switch engine {
case "sqlite":
return sqlite.InitDB(path)
default:
return nil, fmt.Errorf("qsdb type: %s is not supported in query service", engine)
}
func InitDao(inputDB *sqlx.DB) (ModelDao, error) {
return sqlite.InitDB(inputDB)
}

View File

@@ -65,8 +65,8 @@ func columnExists(db *sqlx.DB, tableName, columnName string) bool {
}
// InitDB creates and extends base model DB repository
func InitDB(dataSourceName string) (*modelDao, error) {
dao, err := basedsql.InitDB(dataSourceName)
func InitDB(inputDB *sqlx.DB) (*modelDao, error) {
dao, err := basedsql.InitDB(inputDB)
if err != nil {
return nil, err
}

View File

@@ -28,13 +28,8 @@ func NewLicenseRepo(db *sqlx.DB) Repo {
}
}
func (r *Repo) InitDB(engine string) error {
switch engine {
case "sqlite3", "sqlite":
return sqlite.InitDB(r.db)
default:
return fmt.Errorf("unsupported db")
}
func (r *Repo) InitDB(inputDB *sqlx.DB) error {
return sqlite.InitDB(inputDB)
}
func (r *Repo) GetLicenses(ctx context.Context) ([]model.License, error) {

View File

@@ -51,13 +51,13 @@ type Manager struct {
activeFeatures basemodel.FeatureSet
}
func StartManager(dbType string, db *sqlx.DB, features ...basemodel.Feature) (*Manager, error) {
func StartManager(db *sqlx.DB, features ...basemodel.Feature) (*Manager, error) {
if LM != nil {
return LM, nil
}
repo := NewLicenseRepo(db)
err := repo.InitDB(dbType)
err := repo.InitDB(db)
if err != nil {
return nil, fmt.Errorf("failed to initiate license repo: %v", err)

View File

@@ -10,12 +10,12 @@ import (
"syscall"
"time"
"go.opentelemetry.io/collector/confmap"
"go.opentelemetry.io/otel/sdk/resource"
semconv "go.opentelemetry.io/otel/semconv/v1.4.0"
"go.signoz.io/signoz/ee/query-service/app"
signozconfig "go.signoz.io/signoz/pkg/config"
"go.signoz.io/signoz/pkg/confmap/provider/signozenvprovider"
"go.signoz.io/signoz/pkg/config"
"go.signoz.io/signoz/pkg/config/envprovider"
"go.signoz.io/signoz/pkg/config/fileprovider"
"go.signoz.io/signoz/pkg/query-service/auth"
baseconst "go.signoz.io/signoz/pkg/query-service/constants"
"go.signoz.io/signoz/pkg/query-service/migrate"
@@ -99,7 +99,7 @@ func main() {
var useLogsNewSchema bool
var useTraceNewSchema bool
var cacheConfigPath, fluxInterval string
var cacheConfigPath, fluxInterval, fluxIntervalForTraceDetail string
var enableQueryServiceLogOTLPExport bool
var preferSpanMetrics bool
@@ -108,7 +108,6 @@ func main() {
var dialTimeout time.Duration
var gatewayUrl string
var useLicensesV3 bool
var skipWebFrontend bool
flag.BoolVar(&useLogsNewSchema, "use-logs-new-schema", false, "use logs_v2 schema for logs")
flag.BoolVar(&useTraceNewSchema, "use-trace-new-schema", false, "use new schema for traces")
@@ -122,11 +121,11 @@ func main() {
flag.StringVar(&ruleRepoURL, "rules.repo-url", baseconst.AlertHelpPage, "(host address used to build rule link in alert messages)")
flag.StringVar(&cacheConfigPath, "experimental.cache-config", "", "(cache config to use)")
flag.StringVar(&fluxInterval, "flux-interval", "5m", "(the interval to exclude data from being cached to avoid incorrect cache for data in motion)")
flag.StringVar(&fluxIntervalForTraceDetail, "flux-interval-trace-detail", "2m", "(the interval to exclude data from being cached to avoid incorrect cache for trace data in motion)")
flag.BoolVar(&enableQueryServiceLogOTLPExport, "enable.query.service.log.otlp.export", false, "(enable query service log otlp export)")
flag.StringVar(&cluster, "cluster", "cluster", "(cluster name - defaults to 'cluster')")
flag.StringVar(&gatewayUrl, "gateway-url", "", "(url to the gateway)")
flag.BoolVar(&useLicensesV3, "use-licenses-v3", false, "use licenses_v3 schema for licenses")
flag.BoolVar(&skipWebFrontend, "skip-web-frontend", false, "skip web frontend")
flag.Parse()
loggerMgr := initZapLog(enableQueryServiceLogOTLPExport)
@@ -136,42 +135,42 @@ func main() {
version.PrintVersion()
config, err := signozconfig.New(context.Background(), signozconfig.ProviderSettings{
ResolverSettings: confmap.ResolverSettings{
URIs: []string{"signozenv:"},
ProviderFactories: []confmap.ProviderFactory{
signozenvprovider.NewFactory(),
},
config, err := signoz.NewConfig(context.Background(), config.ResolverConfig{
Uris: []string{"env:"},
ProviderFactories: []config.ProviderFactory{
envprovider.NewFactory(),
fileprovider.NewFactory(),
},
})
if err != nil {
zap.L().Fatal("Failed to create config", zap.Error(err))
}
signoz, err := signoz.New(config, skipWebFrontend)
signoz, err := signoz.New(context.Background(), config, signoz.NewProviderConfig())
if err != nil {
zap.L().Fatal("Failed to create signoz struct", zap.Error(err))
}
serverOptions := &app.ServerOptions{
SigNoz: signoz,
HTTPHostPort: baseconst.HTTPHostPort,
PromConfigPath: promConfigPath,
SkipTopLvlOpsPath: skipTopLvlOpsPath,
PreferSpanMetrics: preferSpanMetrics,
PrivateHostPort: baseconst.PrivateHostPort,
DisableRules: disableRules,
RuleRepoURL: ruleRepoURL,
MaxIdleConns: maxIdleConns,
MaxOpenConns: maxOpenConns,
DialTimeout: dialTimeout,
CacheConfigPath: cacheConfigPath,
FluxInterval: fluxInterval,
Cluster: cluster,
GatewayUrl: gatewayUrl,
UseLogsNewSchema: useLogsNewSchema,
UseTraceNewSchema: useTraceNewSchema,
SkipWebFrontend: skipWebFrontend,
Config: config,
SigNoz: signoz,
HTTPHostPort: baseconst.HTTPHostPort,
PromConfigPath: promConfigPath,
SkipTopLvlOpsPath: skipTopLvlOpsPath,
PreferSpanMetrics: preferSpanMetrics,
PrivateHostPort: baseconst.PrivateHostPort,
DisableRules: disableRules,
RuleRepoURL: ruleRepoURL,
MaxIdleConns: maxIdleConns,
MaxOpenConns: maxOpenConns,
DialTimeout: dialTimeout,
CacheConfigPath: cacheConfigPath,
FluxInterval: fluxInterval,
FluxIntervalForTraceDetail: fluxIntervalForTraceDetail,
Cluster: cluster,
GatewayUrl: gatewayUrl,
UseLogsNewSchema: useLogsNewSchema,
UseTraceNewSchema: useTraceNewSchema,
}
// Read the jwt secret key
@@ -183,7 +182,7 @@ func main() {
zap.L().Info("JWT secret key set successfully.")
}
if err := migrate.Migrate(baseconst.RELATIONAL_DATASOURCE_PATH); err != nil {
if err := migrate.Migrate(signoz.SQLStore.SQLxDB()); err != nil {
zap.L().Error("Failed to migrate", zap.Error(err))
} else {
zap.L().Info("Migration successful")

View File

@@ -46,7 +46,7 @@ type Manager struct {
tenantID string
}
func New(dbType string, modelDao dao.ModelDao, licenseRepo *license.Repo, clickhouseConn clickhouse.Conn) (*Manager, error) {
func New(modelDao dao.ModelDao, licenseRepo *license.Repo, clickhouseConn clickhouse.Conn) (*Manager, error) {
hostNameRegex := regexp.MustCompile(`tcp://(?P<hostname>.*):`)
hostNameRegexMatches := hostNameRegex.FindStringSubmatch(os.Getenv("ClickHouseUrl"))

View File

@@ -24,7 +24,7 @@ const config: Config.InitialOptions = {
'^.+\\.(js|jsx)$': 'babel-jest',
},
transformIgnorePatterns: [
'node_modules/(?!(lodash-es|react-dnd|core-dnd|@react-dnd|dnd-core|react-dnd-html5-backend|axios|@signozhq/design-tokens|d3-interpolate|d3-color)/)',
'node_modules/(?!(lodash-es|react-dnd|core-dnd|@react-dnd|dnd-core|react-dnd-html5-backend|axios|@signozhq/design-tokens|d3-interpolate|d3-color|api)/)',
],
setupFilesAfterEnv: ['<rootDir>jest.setup.ts'],
testPathIgnorePatterns: ['/node_modules/', '/public/'],

View File

@@ -245,6 +245,7 @@
"phin": "^3.7.1",
"body-parser": "1.20.3",
"http-proxy-middleware": "3.0.3",
"cross-spawn": "7.0.5"
"cross-spawn": "7.0.5",
"cookie": "^0.7.1"
}
}

View File

@@ -43,5 +43,6 @@
"DEFAULT": "Open source Observability Platform | SigNoz",
"ALERT_HISTORY": "SigNoz | Alert Rule History",
"ALERT_OVERVIEW": "SigNoz | Alert Rule Overview",
"INFRASTRUCTURE_MONITORING_HOSTS": "SigNoz | Infra Monitoring"
"INFRASTRUCTURE_MONITORING_HOSTS": "SigNoz | Infra Monitoring",
"INFRASTRUCTURE_MONITORING_KUBERNETES": "SigNoz | Infra Monitoring"
}

View File

@@ -3,7 +3,9 @@
"billing": "Billing",
"manage_billing_and_costs": "Manage your billing information, invoices, and monitor costs.",
"enterprise_cloud": "Enterprise Cloud",
"teams_cloud": "Teams Cloud",
"enterprise": "Enterprise",
"teams": "Teams",
"card_details_recieved_and_billing_info": "We have received your card details, your billing will only start after the end of your free trial period.",
"upgrade_plan": "Upgrade Plan",
"manage_billing": "Manage Billing",

View File

@@ -56,5 +56,6 @@
"ALERT_HISTORY": "SigNoz | Alert Rule History",
"ALERT_OVERVIEW": "SigNoz | Alert Rule Overview",
"MESSAGING_QUEUES": "SigNoz | Messaging Queues",
"INFRASTRUCTURE_MONITORING_HOSTS": "SigNoz | Infra Monitoring"
"INFRASTRUCTURE_MONITORING_HOSTS": "SigNoz | Infra Monitoring",
"INFRASTRUCTURE_MONITORING_KUBERNETES": "SigNoz | Infra Monitoring"
}

View File

@@ -11,6 +11,7 @@ import { useQuery } from 'react-query';
import { matchPath, useLocation } from 'react-router-dom';
import { LicenseState, LicenseStatus } from 'types/api/licensesV3/getActive';
import { Organization } from 'types/api/user/getOrganization';
import { USER_ROLES } from 'types/roles';
import { isCloudUser } from 'utils/app';
import { routePermission } from 'utils/permission';
@@ -36,6 +37,8 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element {
activeLicenseV3,
isFetchingActiveLicenseV3,
} = useAppContext();
const isAdmin = user.role === USER_ROLES.ADMIN;
const mapRoutes = useMemo(
() =>
new Map(
@@ -113,7 +116,17 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element {
const navigateToWorkSpaceBlocked = (route: any): void => {
const { path } = route;
if (path && path !== ROUTES.WORKSPACE_LOCKED) {
const isRouteEnabledForWorkspaceBlockedState =
isAdmin &&
(path === ROUTES.ORG_SETTINGS ||
path === ROUTES.BILLING ||
path === ROUTES.MY_SETTINGS);
if (
path &&
path !== ROUTES.WORKSPACE_LOCKED &&
!isRouteEnabledForWorkspaceBlockedState
) {
history.push(ROUTES.WORKSPACE_LOCKED);
}
};
@@ -127,6 +140,7 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element {
navigateToWorkSpaceBlocked(currentRoute);
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isFetchingLicenses, licenses?.workSpaceBlock, mapRoutes, pathname]);
const navigateToWorkSpaceSuspended = (route: any): void => {

View File

@@ -415,6 +415,13 @@ const routes: AppRoutes[] = [
key: 'INFRASTRUCTURE_MONITORING_HOSTS',
isPrivate: true,
},
{
path: ROUTES.INFRASTRUCTURE_MONITORING_KUBERNETES,
exact: true,
component: InfrastructureMonitoring,
key: 'INFRASTRUCTURE_MONITORING_KUBERNETES',
isPrivate: true,
},
];
export const SUPPORT_ROUTE: AppRoutes = {

View File

@@ -2,6 +2,7 @@ import { ApiBaseInstance } from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError, AxiosResponse } from 'axios';
import { baseAutoCompleteIdKeysOrder } from 'constants/queryBuilder';
import { K8sCategory } from 'container/InfraMonitoringK8s/constants';
import { createIdFromObjectFields } from 'lib/createIdFromObjectFields';
import { ErrorResponse, SuccessResponse } from 'types/api';
import {
@@ -11,12 +12,18 @@ import {
export const getHostAttributeKeys = async (
searchText = '',
entity: K8sCategory,
): Promise<SuccessResponse<IQueryAutocompleteResponse> | ErrorResponse> => {
try {
const response: AxiosResponse<{
data: IQueryAutocompleteResponse;
}> = await ApiBaseInstance.get(
`/hosts/attribute_keys?dataSource=metrics&searchText=${searchText}`,
`/${entity}/attribute_keys?dataSource=metrics&searchText=${searchText}`,
{
params: {
limit: 500,
},
},
);
const payload: BaseAutocompleteData[] =

View File

@@ -1,4 +1,4 @@
import { ApiBaseInstance } from 'api';
import axios from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import { ErrorResponse, SuccessResponse } from 'types/api';
@@ -59,7 +59,7 @@ export const getHostLists = async (
headers?: Record<string, string>,
): Promise<SuccessResponse<HostListResponse> | ErrorResponse> => {
try {
const response = await ApiBaseInstance.post('/hosts/list', props, {
const response = await axios.post('/hosts/list', props, {
signal,
headers,
});

View File

@@ -1,4 +1,4 @@
import { ApiBaseInstance } from 'api';
import axios from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import createQueryParams from 'lib/createQueryParams';
@@ -14,15 +14,17 @@ export const getInfraAttributesValues = async ({
filterAttributeKeyDataType,
tagType,
searchText,
aggregateAttribute,
}: IGetAttributeValuesPayload): Promise<
SuccessResponse<IAttributeValuesResponse> | ErrorResponse
> => {
try {
const response = await ApiBaseInstance.get(
const response = await axios.get(
`/hosts/attribute_values?${createQueryParams({
dataSource,
attributeKey,
searchText,
aggregateAttribute,
})}&filterAttributeKeyDataType=${filterAttributeKeyDataType}&tagType=${tagType}`,
);

View File

@@ -0,0 +1,64 @@
import axios from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
export interface K8sClustersListPayload {
filters: TagFilter;
groupBy?: BaseAutocompleteData[];
offset?: number;
limit?: number;
orderBy?: {
columnName: string;
order: 'asc' | 'desc';
};
}
export interface K8sClustersData {
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: K8sClustersData[];
groups: null;
total: number;
sentAnyHostMetricsData: boolean;
isSendingK8SAgentMetrics: boolean;
};
}
export const getK8sClustersList = async (
props: K8sClustersListPayload,
signal?: AbortSignal,
headers?: Record<string, string>,
): Promise<SuccessResponse<K8sClustersListResponse> | ErrorResponse> => {
try {
const response = await axios.post('/clusters/list', props, {
signal,
headers,
});
return {
statusCode: 200,
error: null,
message: 'Success',
payload: response.data,
params: props,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);
}
};

View File

@@ -0,0 +1,70 @@
import axios from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
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;
};
}
export const getK8sDeploymentsList = async (
props: K8sDeploymentsListPayload,
signal?: AbortSignal,
headers?: Record<string, string>,
): Promise<SuccessResponse<K8sDeploymentsListResponse> | ErrorResponse> => {
try {
const response = await axios.post('/deployments/list', props, {
signal,
headers,
});
return {
statusCode: 200,
error: null,
message: 'Success',
payload: response.data,
params: props,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);
}
};

View File

@@ -0,0 +1,62 @@
import axios from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
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;
};
}
export const getK8sNamespacesList = async (
props: K8sNamespacesListPayload,
signal?: AbortSignal,
headers?: Record<string, string>,
): Promise<SuccessResponse<K8sNamespacesListResponse> | ErrorResponse> => {
try {
const response = await axios.post('/namespaces/list', props, {
signal,
headers,
});
return {
statusCode: 200,
error: null,
message: 'Success',
payload: response.data,
params: props,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);
}
};

View File

@@ -0,0 +1,65 @@
import axios from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
export interface K8sNodesListPayload {
filters: TagFilter;
groupBy?: BaseAutocompleteData[];
offset?: number;
limit?: number;
orderBy?: {
columnName: string;
order: 'asc' | 'desc';
};
}
export interface K8sNodesData {
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: K8sNodesData[];
groups: null;
total: number;
sentAnyHostMetricsData: boolean;
isSendingK8SAgentMetrics: boolean;
};
}
export const getK8sNodesList = async (
props: K8sNodesListPayload,
signal?: AbortSignal,
headers?: Record<string, string>,
): Promise<SuccessResponse<K8sNodesListResponse> | ErrorResponse> => {
try {
const response = await axios.post('/nodes/list', props, {
signal,
headers,
});
return {
statusCode: 200,
error: null,
message: 'Success',
payload: response.data,
params: props,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);
}
};

View File

@@ -0,0 +1,93 @@
import axios from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
export interface K8sPodsListPayload {
filters: TagFilter;
groupBy?: BaseAutocompleteData[];
offset?: number;
limit?: number;
orderBy?: {
columnName: string;
order: 'asc' | 'desc';
};
}
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: K8sPodsListPayload,
signal?: AbortSignal,
headers?: Record<string, string>,
): Promise<SuccessResponse<K8sPodsListResponse> | ErrorResponse> => {
try {
const response = await axios.post('/pods/list', props, {
signal,
headers,
});
return {
statusCode: 200,
error: null,
message: 'Success',
payload: response.data,
params: props,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);
}
};

View File

@@ -127,6 +127,18 @@ function HostMetricsLogs({
data={logToRender}
linesPerRow={5}
fontSize={FontSize.MEDIUM}
selectedFields={[
{
dataType: 'string',
type: '',
name: 'body',
},
{
dataType: 'string',
type: '',
name: 'timestamp',
},
]}
/>
),
[],

View File

@@ -4,6 +4,7 @@ export enum VIEWS {
TRACES = 'traces',
CONTAINERS = 'containers',
PROCESSES = 'processes',
EVENTS = 'events',
}
export const VIEW_TYPES = {
@@ -12,4 +13,5 @@ export const VIEW_TYPES = {
TRACES: VIEWS.TRACES,
CONTAINERS: VIEWS.CONTAINERS,
PROCESSES: VIEWS.PROCESSES,
EVENTS: VIEWS.EVENTS,
};

View File

@@ -53,7 +53,7 @@
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
width: calc(100% - 24px);
cursor: pointer;
&.filter-disabled {

View File

@@ -8,10 +8,12 @@ import { Button, Checkbox, Input, Skeleton, Typography } from 'antd';
import cx from 'classnames';
import { IQuickFiltersConfig } from 'components/QuickFilters/QuickFilters';
import { OPERATORS } from 'constants/queryBuilder';
import { DEBOUNCE_DELAY } from 'constants/queryBuilderFilterConfig';
import { getOperatorValue } from 'container/QueryBuilder/filters/QueryBuilderSearch/utils';
import { useGetAggregateValues } from 'hooks/queryBuilder/useGetAggregateValues';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { cloneDeep, isArray, isEmpty, isEqual } from 'lodash-es';
import useDebouncedFn from 'hooks/useDebouncedFunction';
import { cloneDeep, isArray, isEmpty, isEqual, isFunction } from 'lodash-es';
import { ChevronDown, ChevronRight } from 'lucide-react';
import { useMemo, useState } from 'react';
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
@@ -34,10 +36,11 @@ function setDefaultValues(
}
interface ICheckboxProps {
filter: IQuickFiltersConfig;
onFilterChange?: (query: Query) => void;
}
export default function CheckboxFilter(props: ICheckboxProps): JSX.Element {
const { filter } = props;
const { filter, onFilterChange } = props;
const [searchText, setSearchText] = useState<string>('');
const [isOpen, setIsOpen] = useState<boolean>(filter.defaultOpen);
const [visibleItemsCount, setVisibleItemsCount] = useState<number>(10);
@@ -50,9 +53,9 @@ export default function CheckboxFilter(props: ICheckboxProps): JSX.Element {
const { data, isLoading } = useGetAggregateValues(
{
aggregateOperator: 'noop',
dataSource: DataSource.LOGS,
aggregateAttribute: '',
aggregateOperator: filter.aggregateOperator || 'noop',
dataSource: filter.dataSource || DataSource.LOGS,
aggregateAttribute: filter.aggregateAttribute || '',
attributeKey: filter.attributeKey.key,
filterAttributeKeyDataType: filter.attributeKey.dataType || DataTypes.EMPTY,
tagType: filter.attributeKey.type || '',
@@ -72,7 +75,11 @@ export default function CheckboxFilter(props: ICheckboxProps): JSX.Element {
);
const currentAttributeKeys = attributeValues.slice(0, visibleItemsCount);
// derive the state of each filter key here in the renderer itself and keep it in sync with staged query
const setSearchTextDebounced = useDebouncedFn((...args) => {
setSearchText(args[0] as string);
}, DEBOUNCE_DELAY);
// derive the state of each filter key here in the renderer itself and keep it in sync with current query
// also we need to keep a note of last focussed query.
// eslint-disable-next-line sonarjs/cognitive-complexity
const currentFilterState = useMemo(() => {
@@ -159,7 +166,12 @@ export default function CheckboxFilter(props: ICheckboxProps): JSX.Element {
})),
},
};
redirectWithQueryBuilderData(preparedQuery);
if (onFilterChange && isFunction(onFilterChange)) {
onFilterChange(preparedQuery);
} else {
redirectWithQueryBuilderData(preparedQuery);
}
};
const isSomeFilterPresentForCurrentAttribute = currentQuery.builder.queryData?.[
@@ -391,7 +403,11 @@ export default function CheckboxFilter(props: ICheckboxProps): JSX.Element {
},
};
redirectWithQueryBuilderData(finalQuery);
if (onFilterChange && isFunction(onFilterChange)) {
onFilterChange(finalQuery);
} else {
redirectWithQueryBuilderData(finalQuery);
}
};
return (
@@ -440,7 +456,7 @@ export default function CheckboxFilter(props: ICheckboxProps): JSX.Element {
<section className="search">
<Input
placeholder="Filter values"
onChange={(e): void => setSearchText(e.target.value)}
onChange={(e): void => setSearchTextDebounced(e.target.value)}
disabled={isFilterDisabled}
/>
</section>
@@ -511,3 +527,7 @@ export default function CheckboxFilter(props: ICheckboxProps): JSX.Element {
</div>
);
}
CheckboxFilter.defaultProps = {
onFilterChange: null,
};

View File

@@ -15,6 +15,8 @@
display: flex;
align-items: center;
gap: 6px;
width: 100%;
justify-content: flex-start;
.text {
color: var(--bg-vanilla-400);
@@ -50,6 +52,8 @@
display: flex;
align-items: center;
gap: 12px;
width: 100%;
justify-content: flex-end;
.divider-filter {
width: 1px;

View File

@@ -7,9 +7,10 @@ import {
} from '@ant-design/icons';
import { Tooltip, Typography } from 'antd';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { cloneDeep } from 'lodash-es';
import { cloneDeep, isFunction } from 'lodash-es';
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
import { DataSource } from 'types/common/queryBuilder';
import Checkbox from './FilterRenderers/Checkbox/Checkbox';
import Slider from './FilterRenderers/Slider/Slider';
@@ -33,6 +34,9 @@ export interface IQuickFiltersConfig {
type: FiltersType;
title: string;
attributeKey: BaseAutocompleteData;
aggregateOperator?: string;
aggregateAttribute?: string;
dataSource?: DataSource;
customRendererForValue?: (value: string) => JSX.Element;
defaultOpen: boolean;
}
@@ -40,10 +44,12 @@ export interface IQuickFiltersConfig {
interface IQuickFiltersProps {
config: IQuickFiltersConfig[];
handleFilterVisibilityChange: () => void;
source?: string | null;
onFilterChange?: (query: Query) => void;
}
export default function QuickFilters(props: IQuickFiltersProps): JSX.Element {
const { config, handleFilterVisibilityChange } = props;
const { config, handleFilterVisibilityChange, source, onFilterChange } = props;
const {
currentQuery,
@@ -78,47 +84,63 @@ export default function QuickFilters(props: IQuickFiltersProps): JSX.Element {
})),
},
};
redirectWithQueryBuilderData(preparedQuery);
if (onFilterChange && isFunction(onFilterChange)) {
onFilterChange(preparedQuery);
} else {
redirectWithQueryBuilderData(preparedQuery);
}
};
const lastQueryName =
currentQuery.builder.queryData?.[lastUsedQuery || 0]?.queryName;
const isInfraMonitoring = source === 'infra-monitoring';
return (
<div className="quick-filters">
<section className="header">
<section className="left-actions">
<FilterOutlined />
<Typography.Text className="text">Filters for</Typography.Text>
<Tooltip title={`Filter currently in sync with query ${lastQueryName}`}>
<Typography.Text className="sync-tag">{lastQueryName}</Typography.Text>
</Tooltip>
{!isInfraMonitoring && (
<section className="header">
<section className="left-actions">
<FilterOutlined />
<Typography.Text className="text">Filters for</Typography.Text>
<Tooltip title={`Filter currently in sync with query ${lastQueryName}`}>
<Typography.Text className="sync-tag">{lastQueryName}</Typography.Text>
</Tooltip>
</section>
<section className="right-actions">
<Tooltip title="Reset All">
<SyncOutlined className="sync-icon" onClick={handleReset} />
</Tooltip>
<div className="divider-filter" />
<Tooltip title="Collapse Filters">
<VerticalAlignTopOutlined
rotate={270}
onClick={handleFilterVisibilityChange}
/>
</Tooltip>
</section>
</section>
<section className="right-actions">
<Tooltip title="Reset All">
<SyncOutlined className="sync-icon" onClick={handleReset} />
</Tooltip>
<div className="divider-filter" />
<Tooltip title="Collapse Filters">
<VerticalAlignTopOutlined
rotate={270}
onClick={handleFilterVisibilityChange}
/>
</Tooltip>
</section>
</section>
)}
<section className="filters">
{config.map((filter) => {
switch (filter.type) {
case FiltersType.CHECKBOX:
return <Checkbox filter={filter} />;
return <Checkbox filter={filter} onFilterChange={onFilterChange} />;
case FiltersType.SLIDER:
return <Slider filter={filter} />;
default:
return <Checkbox filter={filter} />;
return <Checkbox filter={filter} onFilterChange={onFilterChange} />;
}
})}
</section>
</div>
);
}
QuickFilters.defaultProps = {
source: null,
onFilterChange: null,
};

View File

@@ -0,0 +1,4 @@
.timeline-v2-container {
flex: 1;
overflow: visible;
}

View File

@@ -0,0 +1,90 @@
import './TimelineV2.styles.scss';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { useEffect, useState } from 'react';
import { useMeasure } from 'react-use';
import {
getIntervals,
getMinimumIntervalsBasedOnWidth,
Interval,
} from './utils';
interface ITimelineV2Props {
startTimestamp: number;
endTimestamp: number;
timelineHeight: number;
}
function TimelineV2(props: ITimelineV2Props): JSX.Element {
const { startTimestamp, endTimestamp, timelineHeight } = props;
const [intervals, setIntervals] = useState<Interval[]>([]);
const [ref, { width }] = useMeasure<HTMLDivElement>();
const isDarkMode = useIsDarkMode();
useEffect(() => {
const spread = endTimestamp - startTimestamp;
if (spread < 0) {
return;
}
const minIntervals = getMinimumIntervalsBasedOnWidth(width);
const intervalisedSpread = (spread / minIntervals) * 1.0;
setIntervals(getIntervals(intervalisedSpread, spread));
}, [startTimestamp, endTimestamp, width]);
if (endTimestamp < startTimestamp) {
console.error(
'endTimestamp cannot be less than startTimestamp',
startTimestamp,
endTimestamp,
);
return <div />;
}
return (
<div ref={ref as never} className="timeline-v2-container">
<svg
width={width}
height={timelineHeight}
xmlns="http://www.w3.org/2000/svg"
overflow="visible"
>
<line
x1="0"
y1={timelineHeight}
x2={width}
y2={timelineHeight}
stroke={isDarkMode ? 'white' : 'black'}
strokeWidth="1"
/>
{intervals &&
intervals.length > 0 &&
intervals.map((interval, index) => (
<g
transform={`translate(${(interval.percentage * width) / 100},0)`}
key={`${interval.percentage + interval.label + index}`}
textAnchor="middle"
fontSize="0.6rem"
>
<text
x={index === intervals.length - 1 ? -10 : 0}
y={2 * Math.floor(timelineHeight / 4)}
fill={isDarkMode ? 'white' : 'black'}
>
{interval.label}
</text>
<line
y1={3 * Math.floor(timelineHeight / 4)}
y2={timelineHeight + 0.5}
stroke={isDarkMode ? 'white' : 'black'}
strokeWidth="1"
/>
</g>
))}
</svg>
</div>
);
}
export default TimelineV2;

View File

@@ -0,0 +1,118 @@
import { toFixed } from 'utils/toFixed';
type TTimeUnitName = 'ms' | 's' | 'm' | 'hr' | 'day' | 'week';
export interface IIntervalUnit {
name: TTimeUnitName;
multiplier: number;
}
export interface Interval {
label: string;
percentage: number;
}
export const INTERVAL_UNITS: IIntervalUnit[] = [
{
name: 'ms',
multiplier: 1,
},
{
name: 's',
multiplier: 1 / 1e3,
},
{
name: 'm',
multiplier: 1 / (1e3 * 60),
},
{
name: 'hr',
multiplier: 1 / (1e3 * 60 * 60),
},
{
name: 'day',
multiplier: 1 / (1e3 * 60 * 60 * 24),
},
{
name: 'week',
multiplier: 1 / (1e3 * 60 * 60 * 24 * 7),
},
];
export const getMinimumIntervalsBasedOnWidth = (width: number): number => {
// S
if (width < 640) {
return 5;
}
// M
if (width < 768) {
return 6;
}
// L
if (width < 1024) {
return 8;
}
return 10;
};
export const resolveTimeFromInterval = (
intervalTime: number,
intervalUnit: IIntervalUnit,
): number => intervalTime * intervalUnit.multiplier;
export function getIntervals(
intervalSpread: number,
baseSpread: number,
): Interval[] {
const integerPartString = intervalSpread.toString().split('.')[0];
const integerPartLength = integerPartString.length;
const intervalSpreadNormalized =
intervalSpread < 1.0
? intervalSpread
: Math.floor(Number(integerPartString) / 10 ** (integerPartLength - 1)) *
10 ** (integerPartLength - 1);
let intervalUnit = INTERVAL_UNITS[0];
for (let idx = INTERVAL_UNITS.length - 1; idx >= 0; idx -= 1) {
const standardInterval = INTERVAL_UNITS[idx];
if (intervalSpread * standardInterval.multiplier >= 1) {
intervalUnit = INTERVAL_UNITS[idx];
break;
}
}
intervalUnit = intervalUnit || INTERVAL_UNITS[0];
const intervals: Interval[] = [
{
label: `${toFixed(resolveTimeFromInterval(0, intervalUnit), 2)}${
intervalUnit.name
}`,
percentage: 0,
},
];
let tempBaseSpread = baseSpread;
let elapsedIntervals = 0;
while (tempBaseSpread && intervals.length < 20) {
let intervalTime;
if (tempBaseSpread <= 1.5 * intervalSpreadNormalized) {
intervalTime = elapsedIntervals + tempBaseSpread;
tempBaseSpread = 0;
} else {
intervalTime = elapsedIntervals + intervalSpreadNormalized;
tempBaseSpread -= intervalSpreadNormalized;
}
elapsedIntervals = intervalTime;
const interval: Interval = {
label: `${toFixed(resolveTimeFromInterval(intervalTime, intervalUnit), 2)}${
intervalUnit.name
}`,
percentage: (intervalTime / baseSpread) * 100,
};
intervals.push(interval);
}
return intervals;
}

View File

@@ -21,4 +21,9 @@ export const REACT_QUERY_KEY = {
GET_HOST_LIST: 'GET_HOST_LIST',
UPDATE_ALERT_RULE: 'UPDATE_ALERT_RULE',
GET_ACTIVE_LICENSE_V3: 'GET_ACTIVE_LICENSE_V3',
GET_POD_LIST: 'GET_POD_LIST',
GET_NODE_LIST: 'GET_NODE_LIST',
GET_DEPLOYMENT_LIST: 'GET_DEPLOYMENT_LIST',
GET_CLUSTER_LIST: 'GET_CLUSTER_LIST',
GET_NAMESPACE_LIST: 'GET_NAMESPACE_LIST',
};

View File

@@ -62,6 +62,7 @@ const ROUTES = {
MESSAGING_QUEUES: '/messaging-queues',
MESSAGING_QUEUES_DETAIL: '/messaging-queues/detail',
INFRASTRUCTURE_MONITORING_HOSTS: '/infrastructure-monitoring/hosts',
INFRASTRUCTURE_MONITORING_KUBERNETES: '/infrastructure-monitoring/kubernetes',
} as const;
export default ROUTES;

View File

@@ -1,8 +1,7 @@
import 'uplot/dist/uPlot.min.css';
import './AnomalyAlertEvaluationView.styles.scss';
import { Checkbox, Typography } from 'antd';
import Search from 'antd/es/input/Search';
import { Checkbox, Input, Typography } from 'antd';
import { useIsDarkMode } from 'hooks/useDarkMode';
import useDebouncedFn from 'hooks/useDebouncedFunction';
import { useResizeObserver } from 'hooks/useDimensions';
@@ -16,6 +15,8 @@ import uPlot from 'uplot';
import tooltipPlugin from './tooltipPlugin';
const { Search } = Input;
function UplotChart({
data,
options,

View File

@@ -272,8 +272,8 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
const handleFailedPayment = (): void => {
manageCreditCard({
licenseKey: activeLicenseV3?.key || '',
successURL: window.location.href,
cancelURL: window.location.href,
successURL: window.location.origin,
cancelURL: window.location.origin,
});
};
@@ -292,8 +292,9 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
const isDashboardListView = (): boolean => routeKey === 'ALL_DASHBOARD';
const isAlertHistory = (): boolean => routeKey === 'ALERT_HISTORY';
const isAlertOverview = (): boolean => routeKey === 'ALERT_OVERVIEW';
const isInfraMonitoringHosts = (): boolean =>
routeKey === 'INFRASTRUCTURE_MONITORING_HOSTS';
const isInfraMonitoring = (): boolean =>
routeKey === 'INFRASTRUCTURE_MONITORING_HOSTS' ||
routeKey === 'INFRASTRUCTURE_MONITORING_KUBERNETES';
const isPathMatch = (regex: RegExp): boolean => regex.test(pathname);
const isDashboardView = (): boolean =>
@@ -422,7 +423,7 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
isAlertHistory() ||
isAlertOverview() ||
isMessagingQueues() ||
isInfraMonitoringHosts()
isInfraMonitoring()
? 0
: '0 1rem',

View File

@@ -430,7 +430,7 @@ export default function BillingContainer(): JSX.Element {
<Flex justify="space-between" align="center">
<Flex vertical>
<Typography.Title level={5} style={{ marginTop: 2, fontWeight: 500 }}>
{isCloudUserVal ? t('enterprise_cloud') : t('enterprise')}{' '}
{isCloudUserVal ? t('teams_cloud') : t('teams')}{' '}
{isFreeTrial ? <Tag color="success"> Free Trial </Tag> : ''}
</Typography.Title>
{!isLoading && !isFetchingBillingData ? (

View File

@@ -0,0 +1,146 @@
import ROUTES from 'constants/routes';
import CreateAlertPage from 'pages/CreateAlert';
import { MemoryRouter, Route } from 'react-router-dom';
import { act, fireEvent, render } from 'tests/test-utils';
import { AlertTypes } from 'types/api/alerts/alertTypes';
import { ALERT_TYPE_TO_TITLE, ALERT_TYPE_URL_MAP } from './constants';
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useLocation: (): { pathname: string } => ({
pathname: `${process.env.FRONTEND_API_ENDPOINT}${ROUTES.ALERTS_NEW}`,
}),
}));
jest.mock('uplot', () => {
const paths = {
spline: jest.fn(),
bars: jest.fn(),
};
const uplotMock = jest.fn(() => ({
paths,
}));
return {
paths,
default: uplotMock,
};
});
let mockWindowOpen: jest.Mock;
window.ResizeObserver =
window.ResizeObserver ||
jest.fn().mockImplementation(() => ({
disconnect: jest.fn(),
observe: jest.fn(),
unobserve: jest.fn(),
}));
function findLinkForAlertType(
links: HTMLElement[],
alertType: AlertTypes,
): HTMLElement {
const link = links.find(
(el) =>
el.closest('[data-testid]')?.getAttribute('data-testid') ===
`alert-type-card-${alertType}`,
);
expect(link).toBeTruthy();
return link as HTMLElement;
}
function clickLinkAndVerifyRedirect(
link: HTMLElement,
expectedUrl: string,
): void {
fireEvent.click(link);
expect(mockWindowOpen).toHaveBeenCalledWith(expectedUrl, '_blank');
}
describe('Alert rule documentation redirection', () => {
let renderResult: ReturnType<typeof render>;
beforeAll(() => {
mockWindowOpen = jest.fn();
window.open = mockWindowOpen;
});
beforeEach(() => {
act(() => {
renderResult = render(
<MemoryRouter initialEntries={['/alerts/new']}>
<Route path={ROUTES.ALERTS_NEW}>
<CreateAlertPage />
</Route>
</MemoryRouter>,
);
});
});
it('should render alert type cards', () => {
const { getByText, getAllByText } = renderResult;
// Check for the heading
expect(getByText('choose_alert_type')).toBeInTheDocument();
// Check for alert type titles and descriptions
Object.values(AlertTypes).forEach((alertType) => {
const title = ALERT_TYPE_TO_TITLE[alertType];
expect(getByText(title)).toBeInTheDocument();
expect(getByText(`${title}_desc`)).toBeInTheDocument();
});
const clickHereLinks = getAllByText(
'Click here to see how to create a sample alert.',
);
expect(clickHereLinks).toHaveLength(5);
});
it('should redirect to correct documentation for each alert type', () => {
const { getAllByText } = renderResult;
const clickHereLinks = getAllByText(
'Click here to see how to create a sample alert.',
);
const alertTypeCount = Object.keys(AlertTypes).length;
expect(clickHereLinks).toHaveLength(alertTypeCount);
Object.values(AlertTypes).forEach((alertType) => {
const linkForAlertType = findLinkForAlertType(clickHereLinks, alertType);
const expectedUrl = ALERT_TYPE_URL_MAP[alertType];
clickLinkAndVerifyRedirect(linkForAlertType, expectedUrl.selection);
});
expect(mockWindowOpen).toHaveBeenCalledTimes(alertTypeCount);
});
Object.values(AlertTypes)
.filter((type) => type !== AlertTypes.ANOMALY_BASED_ALERT)
.forEach((alertType) => {
it(`should redirect to create alert page for ${alertType} and "Check an example alert" should redirect to the correct documentation`, () => {
const { getByTestId, getByRole } = renderResult;
const alertTypeLink = getByTestId(`alert-type-card-${alertType}`);
act(() => {
fireEvent.click(alertTypeLink);
});
act(() => {
fireEvent.click(
getByRole('button', {
name: /alert setup guide/i,
}),
);
});
expect(mockWindowOpen).toHaveBeenCalledWith(
ALERT_TYPE_URL_MAP[alertType].creation,
'_blank',
);
});
});
});

View File

@@ -0,0 +1,71 @@
import ROUTES from 'constants/routes';
import CreateAlertPage from 'pages/CreateAlert';
import { MemoryRouter, Route } from 'react-router-dom';
import { act, fireEvent, render } from 'tests/test-utils';
import { AlertTypes } from 'types/api/alerts/alertTypes';
import { ALERT_TYPE_URL_MAP } from './constants';
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useLocation: (): { pathname: string; search: string } => ({
pathname: `${process.env.FRONTEND_API_ENDPOINT}${ROUTES.ALERTS_NEW}`,
search: 'ruleType=anomaly_rule',
}),
}));
jest.mock('uplot', () => {
const paths = {
spline: jest.fn(),
bars: jest.fn(),
};
const uplotMock = jest.fn(() => ({
paths,
}));
return {
paths,
default: uplotMock,
};
});
window.ResizeObserver =
window.ResizeObserver ||
jest.fn().mockImplementation(() => ({
disconnect: jest.fn(),
observe: jest.fn(),
unobserve: jest.fn(),
}));
describe('Anomaly Alert Documentation Redirection', () => {
let mockWindowOpen: jest.Mock;
beforeAll(() => {
mockWindowOpen = jest.fn();
window.open = mockWindowOpen;
});
it('should handle anomaly alert documentation redirection correctly', () => {
const { getByRole } = render(
<MemoryRouter initialEntries={['/alerts/new']}>
<Route path={ROUTES.ALERTS_NEW}>
<CreateAlertPage />
</Route>
</MemoryRouter>,
);
const alertType = AlertTypes.ANOMALY_BASED_ALERT;
act(() => {
fireEvent.click(
getByRole('button', {
name: /alert setup guide/i,
}),
);
});
expect(mockWindowOpen).toHaveBeenCalledWith(
ALERT_TYPE_URL_MAP[alertType].creation,
'_blank',
);
});
});

View File

@@ -0,0 +1,47 @@
import { AlertTypes } from 'types/api/alerts/alertTypes';
// since we don't have a card in alert creation for anomaly based alert
export const ALERT_TYPE_URL_MAP: Record<
AlertTypes,
{ selection: string; creation: string }
> = {
[AlertTypes.METRICS_BASED_ALERT]: {
selection:
'https://signoz.io/docs/alerts-management/metrics-based-alerts/?utm_source=product&utm_medium=alert-source-selection-page#examples',
creation:
'https://signoz.io/docs/alerts-management/metrics-based-alerts/?utm_source=product&utm_medium=alert-creation-page',
},
[AlertTypes.LOGS_BASED_ALERT]: {
selection:
'https://signoz.io/docs/alerts-management/log-based-alerts/?utm_source=product&utm_medium=alert-source-selection-page#examples',
creation:
'https://signoz.io/docs/alerts-management/log-based-alerts/?utm_source=product&utm_medium=alert-creation-page',
},
[AlertTypes.TRACES_BASED_ALERT]: {
selection:
'https://signoz.io/docs/alerts-management/trace-based-alerts/?utm_source=product&utm_medium=alert-source-selection-page#examples',
creation:
'https://signoz.io/docs/alerts-management/trace-based-alerts/?utm_source=product&utm_medium=alert-creation-page',
},
[AlertTypes.EXCEPTIONS_BASED_ALERT]: {
selection:
'https://signoz.io/docs/alerts-management/exceptions-based-alerts/?utm_source=product&utm_medium=alert-source-selection-page#examples',
creation:
'https://signoz.io/docs/alerts-management/exceptions-based-alerts/?utm_source=product&utm_medium=alert-creation-page',
},
[AlertTypes.ANOMALY_BASED_ALERT]: {
selection:
'https://signoz.io/docs/alerts-management/anomaly-based-alerts/?utm_source=product&utm_medium=alert-source-selection-page#examples',
creation:
'https://signoz.io/docs/alerts-management/anomaly-based-alerts/?utm_source=product&utm_medium=alert-creation-page',
},
};
export const ALERT_TYPE_TO_TITLE: Record<AlertTypes, string> = {
[AlertTypes.METRICS_BASED_ALERT]: 'metric_based_alert',
[AlertTypes.LOGS_BASED_ALERT]: 'log_based_alert',
[AlertTypes.TRACES_BASED_ALERT]: 'traces_based_alert',
[AlertTypes.EXCEPTIONS_BASED_ALERT]: 'exceptions_based_alert',
[AlertTypes.ANOMALY_BASED_ALERT]: 'anomaly_based_alert',
};

View File

@@ -79,6 +79,10 @@ export default function CustomDomainSettings(): JSX.Element {
});
useEffect(() => {
if (isFetchingDeploymentsData) {
return;
}
if (deploymentsData?.data?.status === 'success') {
setHosts(deploymentsData.data.data.hosts);
@@ -102,7 +106,12 @@ export default function CustomDomainSettings(): JSX.Element {
if (deploymentsData?.data?.data.state === 'HEALTHY') {
setIsPollingEnabled(false);
}
}, [deploymentsData, refetchDeploymentsData, isPollingEnabled]);
}, [
deploymentsData,
refetchDeploymentsData,
isPollingEnabled,
isFetchingDeploymentsData,
]);
const onUpdateCustomDomainSettings = (): void => {
editForm

View File

@@ -44,6 +44,7 @@ function GridCardGraph({
toScrollWidgetId,
setToScrollWidgetId,
variablesToGetUpdated,
setDashboardQueryRangeCalled,
} = useDashboard();
const { minTime, maxTime, selectedTime: globalSelectedInterval } = useSelector<
AppState,
@@ -202,11 +203,13 @@ function GridCardGraph({
refetchOnMount: false,
onError: (error) => {
setErrorMessage(error.message);
setDashboardQueryRangeCalled(true);
},
onSettled: (data) => {
dataAvailable?.(
isDataAvailableByPanelType(data?.payload?.data, widget?.panelTypes),
);
setDashboardQueryRangeCalled(true);
},
},
);

View File

@@ -1,5 +1,6 @@
import './GridCardLayout.styles.scss';
import * as Sentry from '@sentry/react';
import { Color } from '@signozhq/design-tokens';
import { Button, Form, Input, Modal, Typography } from 'antd';
import { useForm } from 'antd/es/form/Form';
@@ -61,6 +62,8 @@ function GraphLayout(props: GraphLayoutProps): JSX.Element {
setPanelMap,
setSelectedDashboard,
isDashboardLocked,
dashboardQueryRangeCalled,
setDashboardQueryRangeCalled,
} = useDashboard();
const { data } = selectedDashboard || {};
const { pathname } = useLocation();
@@ -124,6 +127,25 @@ function GraphLayout(props: GraphLayoutProps): JSX.Element {
setDashboardLayout(sortLayout(layouts));
}, [layouts]);
useEffect(() => {
setDashboardQueryRangeCalled(false);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useEffect(() => {
const timeoutId = setTimeout(() => {
// Send Sentry event if query_range is not called within expected timeframe (2 mins) when there are widgets
if (!dashboardQueryRangeCalled && data?.widgets?.length) {
Sentry.captureEvent({
message: `Dashboard query range not called within expected timeframe even when there are ${data?.widgets?.length} widgets`,
level: 'warning',
});
}
}, 120000);
return (): void => clearTimeout(timeoutId);
}, [dashboardQueryRangeCalled, data?.widgets?.length]);
const logEventCalledRef = useRef(false);
useEffect(() => {
if (!logEventCalledRef.current && !isUndefined(data)) {

View File

@@ -168,7 +168,8 @@ function HostsList(): JSX.Element {
const showHostsEmptyState =
!isFetching &&
!isLoading &&
(!sentAnyHostMetricsData || isSendingIncorrectK8SAgentMetrics);
(!sentAnyHostMetricsData || isSendingIncorrectK8SAgentMetrics) &&
!filters.items.length;
return (
<div className="hosts-list">

View File

@@ -1,5 +1,6 @@
import './InfraMonitoring.styles.scss';
import { K8sCategory } from 'container/InfraMonitoringK8s/constants';
import QueryBuilderSearch from 'container/QueryBuilder/filters/QueryBuilderSearch';
import DateTimeSelectionV2 from 'container/TopNav/DateTimeSelectionV2';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
@@ -47,6 +48,7 @@ function HostsListControls({
onChange={handleChangeTagFilters}
isInfraMonitoring
disableNavigationShortcuts
entity={K8sCategory.HOSTS}
/>
</div>

View File

@@ -93,7 +93,7 @@
}
.hostname-column-value {
color: var(--Vanilla-100, #fff);
color: var(--bg-vanilla-100);
font-family: 'Geist Mono';
font-style: normal;
font-weight: 600;
@@ -137,6 +137,9 @@
.column-header-right {
text-align: right;
}
.column-header-left {
text-align: left;
}
.ant-table-tbody > tr > td {
border-bottom: none;
}

View File

@@ -26,6 +26,7 @@ export const getHostListsQuery = (): HostListPayload => ({
groupBy: [],
orderBy: { columnName: 'cpu', order: 'desc' },
});
export const getTabsItems = (): TabsProps['items'] => [
{
label: <TabLabel label="List View" isDisabled={false} tooltipText="" />,

View File

@@ -0,0 +1,7 @@
import { K8sClustersData } from 'api/infraMonitoring/getK8sClustersList';
export type ClusterDetailsProps = {
cluster: K8sClustersData | null;
isModalTimeSelection: boolean;
onClose: () => void;
};

View File

@@ -0,0 +1,558 @@
/* eslint-disable sonarjs/no-identical-functions */
import '../../EntityDetailsUtils/entityDetails.styles.scss';
import { Color, Spacing } from '@signozhq/design-tokens';
import { Button, Divider, Drawer, Radio, Tooltip, Typography } from 'antd';
import { RadioChangeEvent } from 'antd/lib';
import logEvent from 'api/common/logEvent';
import { K8sClustersData } from 'api/infraMonitoring/getK8sClustersList';
import { VIEW_TYPES, VIEWS } from 'components/HostMetricsDetail/constants';
import { QueryParams } from 'constants/query';
import {
initialQueryBuilderFormValuesMap,
initialQueryState,
} from 'constants/queryBuilder';
import ROUTES from 'constants/routes';
import { filterDuplicateFilters } from 'container/InfraMonitoringK8s/commonUtils';
import { K8sCategory } from 'container/InfraMonitoringK8s/constants';
import { QUERY_KEYS } from 'container/InfraMonitoringK8s/EntityDetailsUtils/utils';
import {
CustomTimeType,
Time,
} from 'container/TopNav/DateTimeSelectionV2/config';
import { useIsDarkMode } from 'hooks/useDarkMode';
import useUrlQuery from 'hooks/useUrlQuery';
import GetMinMax from 'lib/getMinMax';
import {
BarChart2,
ChevronsLeftRight,
Compass,
DraftingCompass,
ScrollText,
X,
} from 'lucide-react';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
import {
IBuilderQuery,
TagFilterItem,
} from 'types/api/queryBuilder/queryBuilderData';
import {
LogsAggregatorOperator,
TracesAggregatorOperator,
} from 'types/common/queryBuilder';
import { GlobalReducer } from 'types/reducer/globalTime';
import { v4 as uuidv4 } from 'uuid';
import ClusterEvents from '../../EntityDetailsUtils/EntityEvents';
import ClusterLogs from '../../EntityDetailsUtils/EntityLogs';
import ClusterMetrics from '../../EntityDetailsUtils/EntityMetrics';
import ClusterTraces from '../../EntityDetailsUtils/EntityTraces';
import { ClusterDetailsProps } from './ClusterDetails.interfaces';
import { clusterWidgetInfo, getClusterMetricsQueryPayload } from './constants';
function ClusterDetails({
cluster,
onClose,
isModalTimeSelection,
}: ClusterDetailsProps): JSX.Element {
const { maxTime, minTime, selectedTime } = useSelector<
AppState,
GlobalReducer
>((state) => state.globalTime);
const startMs = useMemo(() => Math.floor(Number(minTime) / 1000000000), [
minTime,
]);
const endMs = useMemo(() => Math.floor(Number(maxTime) / 1000000000), [
maxTime,
]);
const urlQuery = useUrlQuery();
const [modalTimeRange, setModalTimeRange] = useState(() => ({
startTime: startMs,
endTime: endMs,
}));
const [selectedInterval, setSelectedInterval] = useState<Time>(
selectedTime as Time,
);
const [selectedView, setSelectedView] = useState<VIEWS>(VIEWS.METRICS);
const isDarkMode = useIsDarkMode();
const initialFilters = useMemo(
() => ({
op: 'AND',
items: [
{
id: uuidv4(),
key: {
key: QUERY_KEYS.K8S_CLUSTER_NAME,
dataType: DataTypes.String,
type: 'resource',
isColumn: false,
isJSON: false,
id: 'k8s_cluster_name--string--resource--false',
},
op: '=',
value: cluster?.meta.k8s_cluster_name || '',
},
],
}),
[cluster?.meta.k8s_cluster_name],
);
const initialEventsFilters = useMemo(
() => ({
op: 'AND',
items: [
{
id: uuidv4(),
key: {
key: QUERY_KEYS.K8S_OBJECT_KIND,
dataType: DataTypes.String,
type: 'resource',
isColumn: false,
isJSON: false,
id: 'k8s.object.kind--string--resource--false',
},
op: '=',
value: 'Cluster',
},
{
id: uuidv4(),
key: {
key: QUERY_KEYS.K8S_OBJECT_NAME,
dataType: DataTypes.String,
type: 'resource',
isColumn: false,
isJSON: false,
id: 'k8s.object.name--string--resource--false',
},
op: '=',
value: cluster?.meta.k8s_cluster_name || '',
},
],
}),
[cluster?.meta.k8s_cluster_name],
);
const [logFilters, setLogFilters] = useState<IBuilderQuery['filters']>(
initialFilters,
);
const [tracesFilters, setTracesFilters] = useState<IBuilderQuery['filters']>(
initialFilters,
);
const [eventsFilters, setEventsFilters] = useState<IBuilderQuery['filters']>(
initialEventsFilters,
);
useEffect(() => {
logEvent('Infra Monitoring: Clusters list details page visited', {
cluster: cluster?.clusterUID,
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useEffect(() => {
setLogFilters(initialFilters);
setTracesFilters(initialFilters);
setEventsFilters(initialEventsFilters);
}, [initialFilters, initialEventsFilters]);
useEffect(() => {
setSelectedInterval(selectedTime as Time);
if (selectedTime !== 'custom') {
const { maxTime, minTime } = GetMinMax(selectedTime);
setModalTimeRange({
startTime: Math.floor(minTime / 1000000000),
endTime: Math.floor(maxTime / 1000000000),
});
}
}, [selectedTime, minTime, maxTime]);
const handleTabChange = (e: RadioChangeEvent): void => {
setSelectedView(e.target.value);
};
const handleTimeChange = useCallback(
(interval: Time | CustomTimeType, dateTimeRange?: [number, number]): void => {
setSelectedInterval(interval as Time);
if (interval === 'custom' && dateTimeRange) {
setModalTimeRange({
startTime: Math.floor(dateTimeRange[0] / 1000),
endTime: Math.floor(dateTimeRange[1] / 1000),
});
} else {
const { maxTime, minTime } = GetMinMax(interval);
setModalTimeRange({
startTime: Math.floor(minTime / 1000000000),
endTime: Math.floor(maxTime / 1000000000),
});
}
logEvent('Infra Monitoring: Clusters list details time updated', {
cluster: cluster?.clusterUID,
interval,
});
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[],
);
const handleChangeLogFilters = useCallback(
(value: IBuilderQuery['filters']) => {
setLogFilters((prevFilters) => {
const primaryFilters = prevFilters.items.filter((item) =>
[QUERY_KEYS.K8S_CLUSTER_NAME].includes(item.key?.key ?? ''),
);
const paginationFilter = value.items.find((item) => item.key?.key === 'id');
const newFilters = value.items.filter(
(item) =>
item.key?.key !== 'id' && item.key?.key !== QUERY_KEYS.K8S_CLUSTER_NAME,
);
logEvent('Infra Monitoring: Clusters list details logs filters applied', {
cluster: cluster?.clusterUID,
});
return {
op: 'AND',
items: filterDuplicateFilters(
[
...primaryFilters,
...newFilters,
...(paginationFilter ? [paginationFilter] : []),
].filter((item): item is TagFilterItem => item !== undefined),
),
};
});
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[],
);
const handleChangeTracesFilters = useCallback(
(value: IBuilderQuery['filters']) => {
setTracesFilters((prevFilters) => {
const primaryFilters = prevFilters.items.filter((item) =>
[QUERY_KEYS.K8S_CLUSTER_NAME].includes(item.key?.key ?? ''),
);
logEvent('Infra Monitoring: Clusters list details traces filters applied', {
cluster: cluster?.clusterUID,
});
return {
op: 'AND',
items: filterDuplicateFilters(
[
...primaryFilters,
...value.items.filter(
(item) => item.key?.key !== QUERY_KEYS.K8S_CLUSTER_NAME,
),
].filter((item): item is TagFilterItem => item !== undefined),
),
};
});
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[],
);
const handleChangeEventsFilters = useCallback(
(value: IBuilderQuery['filters']) => {
setEventsFilters((prevFilters) => {
const clusterKindFilter = prevFilters.items.find(
(item) => item.key?.key === QUERY_KEYS.K8S_OBJECT_KIND,
);
const clusterNameFilter = prevFilters.items.find(
(item) => item.key?.key === QUERY_KEYS.K8S_OBJECT_NAME,
);
logEvent('Infra Monitoring: Clusters list details events filters applied', {
cluster: cluster?.clusterUID,
});
return {
op: 'AND',
items: filterDuplicateFilters(
[
clusterKindFilter,
clusterNameFilter,
...value.items.filter(
(item) =>
item.key?.key !== QUERY_KEYS.K8S_OBJECT_KIND &&
item.key?.key !== QUERY_KEYS.K8S_OBJECT_NAME,
),
].filter((item): item is TagFilterItem => item !== undefined),
),
};
});
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[],
);
const handleExplorePagesRedirect = (): void => {
if (selectedInterval !== 'custom') {
urlQuery.set(QueryParams.relativeTime, selectedInterval);
} else {
urlQuery.delete(QueryParams.relativeTime);
urlQuery.set(QueryParams.startTime, modalTimeRange.startTime.toString());
urlQuery.set(QueryParams.endTime, modalTimeRange.endTime.toString());
}
logEvent('Infra Monitoring: Clusters list details explore clicked', {
cluster: cluster?.clusterUID,
view: selectedView,
});
if (selectedView === VIEW_TYPES.LOGS) {
const filtersWithoutPagination = {
...logFilters,
items: logFilters.items.filter((item) => item.key?.key !== 'id'),
};
const compositeQuery = {
...initialQueryState,
queryType: 'builder',
builder: {
...initialQueryState.builder,
queryData: [
{
...initialQueryBuilderFormValuesMap.logs,
aggregateOperator: LogsAggregatorOperator.NOOP,
filters: filtersWithoutPagination,
},
],
},
};
urlQuery.set('compositeQuery', JSON.stringify(compositeQuery));
window.open(
`${window.location.origin}${ROUTES.LOGS_EXPLORER}?${urlQuery.toString()}`,
'_blank',
);
} else if (selectedView === VIEW_TYPES.TRACES) {
const compositeQuery = {
...initialQueryState,
queryType: 'builder',
builder: {
...initialQueryState.builder,
queryData: [
{
...initialQueryBuilderFormValuesMap.traces,
aggregateOperator: TracesAggregatorOperator.NOOP,
filters: tracesFilters,
},
],
},
};
urlQuery.set('compositeQuery', JSON.stringify(compositeQuery));
window.open(
`${window.location.origin}${ROUTES.TRACES_EXPLORER}?${urlQuery.toString()}`,
'_blank',
);
}
};
const handleClose = (): void => {
setSelectedInterval(selectedTime as Time);
if (selectedTime !== 'custom') {
const { maxTime, minTime } = GetMinMax(selectedTime);
setModalTimeRange({
startTime: Math.floor(minTime / 1000000000),
endTime: Math.floor(maxTime / 1000000000),
});
}
setSelectedView(VIEW_TYPES.METRICS);
onClose();
};
return (
<Drawer
width="70%"
title={
<>
<Divider type="vertical" />
<Typography.Text className="title">
{cluster?.meta.k8s_cluster_name}
</Typography.Text>
</>
}
placement="right"
onClose={handleClose}
open={!!cluster}
style={{
overscrollBehavior: 'contain',
background: isDarkMode ? Color.BG_INK_400 : Color.BG_VANILLA_100,
}}
className="entity-detail-drawer"
destroyOnClose
closeIcon={<X size={16} style={{ marginTop: Spacing.MARGIN_1 }} />}
>
{cluster && (
<>
<div className="entity-detail-drawer__entity">
<div className="entity-details-grid">
<div className="labels-row">
<Typography.Text
type="secondary"
className="entity-details-metadata-label"
>
Cluster Name
</Typography.Text>
<Typography.Text
type="secondary"
className="entity-details-metadata-label"
>
Cluster Name
</Typography.Text>
</div>
<div className="values-row">
<Typography.Text className="entity-details-metadata-value">
<Tooltip title={cluster.meta.k8s_cluster_name}>
{cluster.meta.k8s_cluster_name}
</Tooltip>
</Typography.Text>
<Typography.Text className="entity-details-metadata-value">
<Tooltip title="Cluster name">{cluster.meta.k8s_cluster_name}</Tooltip>
</Typography.Text>
</div>
</div>
</div>
<div className="views-tabs-container">
<Radio.Group
className="views-tabs"
onChange={handleTabChange}
value={selectedView}
>
<Radio.Button
className={
// eslint-disable-next-line sonarjs/no-duplicate-string
selectedView === VIEW_TYPES.METRICS ? 'selected_view tab' : 'tab'
}
value={VIEW_TYPES.METRICS}
>
<div className="view-title">
<BarChart2 size={14} />
Metrics
</div>
</Radio.Button>
<Radio.Button
className={
selectedView === VIEW_TYPES.LOGS ? 'selected_view tab' : 'tab'
}
value={VIEW_TYPES.LOGS}
>
<div className="view-title">
<ScrollText size={14} />
Logs
</div>
</Radio.Button>
<Radio.Button
className={
selectedView === VIEW_TYPES.TRACES ? 'selected_view tab' : 'tab'
}
value={VIEW_TYPES.TRACES}
>
<div className="view-title">
<DraftingCompass size={14} />
Traces
</div>
</Radio.Button>
<Radio.Button
className={
selectedView === VIEW_TYPES.EVENTS ? 'selected_view tab' : 'tab'
}
value={VIEW_TYPES.EVENTS}
>
<div className="view-title">
<ChevronsLeftRight size={14} />
Events
</div>
</Radio.Button>
</Radio.Group>
{(selectedView === VIEW_TYPES.LOGS ||
selectedView === VIEW_TYPES.TRACES) && (
<Button
icon={<Compass size={18} />}
className="compass-button"
onClick={handleExplorePagesRedirect}
/>
)}
</div>
{selectedView === VIEW_TYPES.METRICS && (
<ClusterMetrics<K8sClustersData>
timeRange={modalTimeRange}
isModalTimeSelection={isModalTimeSelection}
handleTimeChange={handleTimeChange}
selectedInterval={selectedInterval}
entity={cluster}
entityWidgetInfo={clusterWidgetInfo}
getEntityQueryPayload={getClusterMetricsQueryPayload}
category={K8sCategory.CLUSTERS}
queryKey="clusterMetrics"
/>
)}
{selectedView === VIEW_TYPES.LOGS && (
<ClusterLogs
timeRange={modalTimeRange}
isModalTimeSelection={isModalTimeSelection}
handleTimeChange={handleTimeChange}
handleChangeLogFilters={handleChangeLogFilters}
logFilters={logFilters}
selectedInterval={selectedInterval}
queryKey="clusterLogs"
category={K8sCategory.CLUSTERS}
queryKeyFilters={[QUERY_KEYS.K8S_CLUSTER_NAME]}
/>
)}
{selectedView === VIEW_TYPES.TRACES && (
<ClusterTraces
timeRange={modalTimeRange}
isModalTimeSelection={isModalTimeSelection}
handleTimeChange={handleTimeChange}
handleChangeTracesFilters={handleChangeTracesFilters}
tracesFilters={tracesFilters}
selectedInterval={selectedInterval}
queryKey="clusterTraces"
/>
)}
{selectedView === VIEW_TYPES.EVENTS && (
<ClusterEvents
timeRange={modalTimeRange}
handleChangeEventFilters={handleChangeEventsFilters}
filters={eventsFilters}
isModalTimeSelection={isModalTimeSelection}
handleTimeChange={handleTimeChange}
selectedInterval={selectedInterval}
category={K8sCategory.CLUSTERS}
queryKey="clusterEvents"
/>
)}
</>
)}
</Drawer>
);
}
export default ClusterDetails;

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,3 @@
import ClusterDetails from './ClusterDetails';
export default ClusterDetails;

View File

@@ -0,0 +1,17 @@
.infra-monitoring-container {
.clusters-list-table {
.expanded-table-container {
padding-left: 40px;
}
.ant-table-cell {
min-width: 223px !important;
max-width: 223px !important;
}
.ant-table-row-expand-icon-cell {
min-width: 30px !important;
max-width: 30px !important;
}
}
}

View File

@@ -0,0 +1,512 @@
/* eslint-disable no-restricted-syntax */
/* eslint-disable @typescript-eslint/explicit-function-return-type */
import '../InfraMonitoringK8s.styles.scss';
import './K8sClustersList.styles.scss';
import { LoadingOutlined } from '@ant-design/icons';
import {
Button,
Spin,
Table,
TablePaginationConfig,
TableProps,
Typography,
} from 'antd';
import { ColumnType, SorterResult } from 'antd/es/table/interface';
import logEvent from 'api/common/logEvent';
import { K8sClustersListPayload } from 'api/infraMonitoring/getK8sClustersList';
import { useGetK8sClustersList } from 'hooks/infraMonitoring/useGetK8sClustersList';
import { useGetAggregateKeys } from 'hooks/queryBuilder/useGetAggregateKeys';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useQueryOperations } from 'hooks/queryBuilder/useQueryBuilderOperations';
import { ChevronDown, ChevronRight } from 'lucide-react';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import { GlobalReducer } from 'types/reducer/globalTime';
import {
K8sCategory,
K8sEntityToAggregateAttributeMapping,
} from '../constants';
import K8sHeader from '../K8sHeader';
import LoadingContainer from '../LoadingContainer';
import ClusterDetails from './ClusterDetails';
import {
defaultAddedColumns,
formatDataForTable,
getK8sClustersListColumns,
getK8sClustersListQuery,
K8sClustersRowData,
} from './utils';
// eslint-disable-next-line sonarjs/cognitive-complexity
function K8sClustersList({
isFiltersVisible,
handleFilterVisibilityChange,
quickFiltersLastUpdated,
}: {
isFiltersVisible: boolean;
handleFilterVisibilityChange: () => void;
quickFiltersLastUpdated: number;
}): JSX.Element {
const { maxTime, minTime } = useSelector<AppState, GlobalReducer>(
(state) => state.globalTime,
);
const [currentPage, setCurrentPage] = useState(1);
const [expandedRowKeys, setExpandedRowKeys] = useState<string[]>([]);
const [orderBy, setOrderBy] = useState<{
columnName: string;
order: 'asc' | 'desc';
} | null>({ columnName: 'cpu', order: 'desc' });
const [selectedClusterName, setselectedClusterName] = useState<string | null>(
null,
);
const pageSize = 10;
const [groupBy, setGroupBy] = useState<IBuilderQuery['groupBy']>([]);
const [
selectedRowData,
setSelectedRowData,
] = useState<K8sClustersRowData | null>(null);
const [groupByOptions, setGroupByOptions] = useState<
{ value: string; label: string }[]
>([]);
const { currentQuery } = useQueryBuilder();
const queryFilters = useMemo(
() =>
currentQuery?.builder?.queryData[0]?.filters || {
items: [],
op: 'and',
},
[currentQuery?.builder?.queryData],
);
// Reset pagination every time quick filters are changed
useEffect(() => {
setCurrentPage(1);
}, [quickFiltersLastUpdated]);
const createFiltersForSelectedRowData = (
selectedRowData: K8sClustersRowData,
groupBy: IBuilderQuery['groupBy'],
): IBuilderQuery['filters'] => {
const baseFilters: IBuilderQuery['filters'] = {
items: [...queryFilters.items],
op: 'and',
};
if (!selectedRowData) return baseFilters;
const { groupedByMeta } = selectedRowData;
for (const key of groupBy) {
baseFilters.items.push({
key: {
key: key.key,
type: null,
},
op: '=',
value: groupedByMeta[key.key],
id: key.key,
});
}
return baseFilters;
};
const fetchGroupedByRowDataQuery = useMemo(() => {
if (!selectedRowData) return null;
const baseQuery = getK8sClustersListQuery();
const filters = createFiltersForSelectedRowData(selectedRowData, groupBy);
return {
...baseQuery,
limit: 10,
offset: 0,
filters,
start: Math.floor(minTime / 1000000),
end: Math.floor(maxTime / 1000000),
orderBy,
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [minTime, maxTime, orderBy, selectedRowData, groupBy]);
const {
data: groupedByRowData,
isFetching: isFetchingGroupedByRowData,
isLoading: isLoadingGroupedByRowData,
isError: isErrorGroupedByRowData,
refetch: fetchGroupedByRowData,
} = useGetK8sClustersList(
fetchGroupedByRowDataQuery as K8sClustersListPayload,
{
queryKey: ['clusterList', fetchGroupedByRowDataQuery],
enabled: !!fetchGroupedByRowDataQuery && !!selectedRowData,
},
);
const {
data: groupByFiltersData,
isLoading: isLoadingGroupByFilters,
} = useGetAggregateKeys(
{
dataSource: currentQuery.builder.queryData[0].dataSource,
aggregateAttribute:
K8sEntityToAggregateAttributeMapping[K8sCategory.CLUSTERS],
aggregateOperator: 'noop',
searchText: '',
tagType: '',
},
{
queryKey: [currentQuery.builder.queryData[0].dataSource, 'noop'],
},
true,
K8sCategory.NODES,
);
const query = useMemo(() => {
const baseQuery = getK8sClustersListQuery();
const queryPayload = {
...baseQuery,
limit: pageSize,
offset: (currentPage - 1) * pageSize,
filters: queryFilters,
start: Math.floor(minTime / 1000000),
end: Math.floor(maxTime / 1000000),
orderBy,
};
if (groupBy.length > 0) {
queryPayload.groupBy = groupBy;
}
return queryPayload;
}, [currentPage, minTime, maxTime, orderBy, groupBy, queryFilters]);
const formattedGroupedByClustersData = useMemo(
() =>
formatDataForTable(groupedByRowData?.payload?.data?.records || [], groupBy),
[groupedByRowData, groupBy],
);
const { data, isFetching, isLoading, isError } = useGetK8sClustersList(
query as K8sClustersListPayload,
{
queryKey: ['clusterList', query],
enabled: !!query,
},
);
const clustersData = useMemo(() => data?.payload?.data?.records || [], [data]);
const totalCount = data?.payload?.data?.total || 0;
const formattedClustersData = useMemo(
() => formatDataForTable(clustersData, groupBy),
[clustersData, groupBy],
);
const columns = useMemo(() => getK8sClustersListColumns(groupBy), [groupBy]);
const handleGroupByRowClick = (record: K8sClustersRowData): void => {
setSelectedRowData(record);
if (expandedRowKeys.includes(record.key)) {
setExpandedRowKeys(expandedRowKeys.filter((key) => key !== record.key));
} else {
setExpandedRowKeys([record.key]);
}
};
useEffect(() => {
if (selectedRowData) {
fetchGroupedByRowData();
}
}, [selectedRowData, fetchGroupedByRowData]);
const handleTableChange: TableProps<K8sClustersRowData>['onChange'] = useCallback(
(
pagination: TablePaginationConfig,
_filters: Record<string, (string | number | boolean)[] | null>,
sorter:
| SorterResult<K8sClustersRowData>
| SorterResult<K8sClustersRowData>[],
): void => {
if (pagination.current) {
setCurrentPage(pagination.current);
}
if ('field' in sorter && sorter.order) {
setOrderBy({
columnName: sorter.field as string,
order: sorter.order === 'ascend' ? 'asc' : 'desc',
});
} else {
setOrderBy(null);
}
},
[],
);
const { handleChangeQueryData } = useQueryOperations({
index: 0,
query: currentQuery.builder.queryData[0],
entityVersion: '',
});
const handleFiltersChange = useCallback(
(value: IBuilderQuery['filters']): void => {
handleChangeQueryData('filters', value);
setCurrentPage(1);
logEvent('Infra Monitoring: K8s list filters applied', {
filters: value,
});
},
[handleChangeQueryData],
);
useEffect(() => {
logEvent('Infra Monitoring: K8s list page visited', {});
}, []);
const selectedClusterData = useMemo(() => {
if (!selectedClusterName) return null;
return (
clustersData.find(
(cluster) => cluster.meta.k8s_cluster_name === selectedClusterName,
) || null
);
}, [selectedClusterName, clustersData]);
const handleRowClick = (record: K8sClustersRowData): void => {
if (groupBy.length === 0) {
setSelectedRowData(null);
setselectedClusterName(record.clusterUID);
} else {
handleGroupByRowClick(record);
}
logEvent('Infra Monitoring: K8s cluster list item clicked', {
clusterName: record.clusterName,
});
};
const nestedColumns = useMemo(() => getK8sClustersListColumns([]), []);
const isGroupedByAttribute = groupBy.length > 0;
const handleExpandedRowViewAllClick = (): void => {
if (!selectedRowData) return;
const filters = createFiltersForSelectedRowData(selectedRowData, groupBy);
handleFiltersChange(filters);
setCurrentPage(1);
setSelectedRowData(null);
setGroupBy([]);
setOrderBy(null);
};
const expandedRowRender = (): JSX.Element => (
<div className="expanded-table-container">
{isErrorGroupedByRowData && (
<Typography>{groupedByRowData?.error || 'Something went wrong'}</Typography>
)}
{isFetchingGroupedByRowData || isLoadingGroupedByRowData ? (
<LoadingContainer />
) : (
<div className="expanded-table">
<Table
columns={nestedColumns as ColumnType<K8sClustersRowData>[]}
dataSource={formattedGroupedByClustersData}
pagination={false}
scroll={{ x: true }}
tableLayout="fixed"
size="small"
loading={{
spinning: isFetchingGroupedByRowData || isLoadingGroupedByRowData,
indicator: <Spin indicator={<LoadingOutlined size={14} spin />} />,
}}
showHeader={false}
/>
{groupedByRowData?.payload?.data?.total &&
groupedByRowData?.payload?.data?.total > 10 ? (
<div className="expanded-table-footer">
<Button
type="default"
size="small"
className="periscope-btn secondary"
onClick={handleExpandedRowViewAllClick}
>
View All
</Button>
</div>
) : null}
</div>
)}
</div>
);
const expandRowIconRenderer = ({
expanded,
onExpand,
record,
}: {
expanded: boolean;
onExpand: (
record: K8sClustersRowData,
e: React.MouseEvent<HTMLButtonElement>,
) => void;
record: K8sClustersRowData;
}): JSX.Element | null => {
if (!isGroupedByAttribute) {
return null;
}
return expanded ? (
<Button
className="periscope-btn ghost"
onClick={(e: React.MouseEvent<HTMLButtonElement>): void =>
onExpand(record, e)
}
role="button"
>
<ChevronDown size={14} />
</Button>
) : (
<Button
className="periscope-btn ghost"
onClick={(e: React.MouseEvent<HTMLButtonElement>): void =>
onExpand(record, e)
}
role="button"
>
<ChevronRight size={14} />
</Button>
);
};
const handleCloseClusterDetail = (): void => {
setselectedClusterName(null);
};
const handleGroupByChange = useCallback(
(value: IBuilderQuery['groupBy']) => {
const groupBy = [];
for (let index = 0; index < value.length; index++) {
const element = (value[index] as unknown) as string;
const key = groupByFiltersData?.payload?.attributeKeys?.find(
(key) => key.key === element,
);
if (key) {
groupBy.push(key);
}
}
// Reset pagination on switching to groupBy
setCurrentPage(1);
setGroupBy(groupBy);
setExpandedRowKeys([]);
},
[groupByFiltersData],
);
useEffect(() => {
if (groupByFiltersData?.payload) {
setGroupByOptions(
groupByFiltersData?.payload?.attributeKeys?.map((filter) => ({
value: filter.key,
label: filter.key,
})) || [],
);
}
}, [groupByFiltersData]);
return (
<div className="k8s-list">
<K8sHeader
isFiltersVisible={isFiltersVisible}
handleFilterVisibilityChange={handleFilterVisibilityChange}
defaultAddedColumns={defaultAddedColumns}
handleFiltersChange={handleFiltersChange}
groupByOptions={groupByOptions}
isLoadingGroupByFilters={isLoadingGroupByFilters}
handleGroupByChange={handleGroupByChange}
selectedGroupBy={groupBy}
entity={K8sCategory.NODES}
/>
{isError && <Typography>{data?.error || 'Something went wrong'}</Typography>}
<Table
className="k8s-list-table clusters-list-table"
dataSource={isFetching || isLoading ? [] : formattedClustersData}
columns={columns}
pagination={{
current: currentPage,
pageSize,
total: totalCount,
showSizeChanger: false,
hideOnSinglePage: true,
}}
scroll={{ x: true }}
loading={{
spinning: isFetching || isLoading,
indicator: <Spin indicator={<LoadingOutlined size={14} spin />} />,
}}
locale={{
emptyText:
isFetching || isLoading ? null : (
<div className="no-filtered-hosts-message-container">
<div className="no-filtered-hosts-message-content">
<img
src="/Icons/emptyState.svg"
alt="thinking-emoji"
className="empty-state-svg"
/>
<Typography.Text className="no-filtered-hosts-message">
This query had no results. Edit your query and try again!
</Typography.Text>
</div>
</div>
),
}}
tableLayout="fixed"
onChange={handleTableChange}
onRow={(record): { onClick: () => void; className: string } => ({
onClick: (): void => handleRowClick(record),
className: 'clickable-row',
})}
expandable={{
expandedRowRender: isGroupedByAttribute ? expandedRowRender : undefined,
expandIcon: expandRowIconRenderer,
expandedRowKeys,
}}
/>
<ClusterDetails
cluster={selectedClusterData}
isModalTimeSelection
onClose={handleCloseClusterDetail}
/>
</div>
);
}
export default K8sClustersList;

View File

@@ -0,0 +1,196 @@
import { Color } from '@signozhq/design-tokens';
import { Tag, Tooltip } from 'antd';
import { ColumnType } from 'antd/es/table';
import {
K8sClustersData,
K8sClustersListPayload,
} from 'api/infraMonitoring/getK8sClustersList';
import { Group } from 'lucide-react';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import { formatBytes, ValidateColumnValueWrapper } from '../commonUtils';
import { IEntityColumn } from '../utils';
export const defaultAddedColumns: IEntityColumn[] = [
{
label: 'Cluster Name',
value: 'clusterName',
id: 'cluster',
canRemove: false,
},
{
label: 'CPU Usage (cores)',
value: 'cpu',
id: 'cpu',
canRemove: false,
},
{
label: 'CPU Allocatable (cores)',
value: 'cpu_allocatable',
id: 'cpu_allocatable',
canRemove: false,
},
{
label: 'Mem Usage',
value: 'memory',
id: 'memory',
canRemove: false,
},
{
label: 'Mem Allocatable',
value: 'memory_allocatable',
id: 'memory_allocatable',
canRemove: false,
},
];
export interface K8sClustersRowData {
key: string;
clusterUID: string;
clusterName: React.ReactNode;
cpu: React.ReactNode;
memory: React.ReactNode;
cpu_allocatable: React.ReactNode;
memory_allocatable: React.ReactNode;
groupedByMeta?: any;
}
const clusterGroupColumnConfig = {
title: (
<div className="column-header entity-group-header">
<Group size={14} /> CLUSTER GROUP
</div>
),
dataIndex: 'clusterGroup',
key: 'clusterGroup',
ellipsis: true,
width: 150,
align: 'left',
sorter: false,
className: 'column entity-group-header',
};
export const getK8sClustersListQuery = (): K8sClustersListPayload => ({
filters: {
items: [],
op: 'and',
},
orderBy: { columnName: 'cpu', order: 'desc' },
});
const columnsConfig = [
{
title: <div className="column-header-left">Cluster Name</div>,
dataIndex: 'clusterName',
key: 'clusterName',
ellipsis: true,
width: 150,
sorter: false,
align: 'left',
},
{
title: <div className="column-header-left">CPU Utilization (cores)</div>,
dataIndex: 'cpu',
key: 'cpu',
width: 80,
sorter: true,
align: 'left',
},
{
title: <div className="column-header-left">CPU Allocatable (cores)</div>,
dataIndex: 'cpu_allocatable',
key: 'cpu_allocatable',
width: 80,
sorter: true,
align: 'left',
},
{
title: <div className="column-header-left">Memory Utilization (bytes)</div>,
dataIndex: 'memory',
key: 'memory',
width: 80,
sorter: true,
align: 'left',
},
{
title: <div className="column-header-left">Memory Allocatable (bytes)</div>,
dataIndex: 'memory_allocatable',
key: 'memory_allocatable',
width: 80,
sorter: true,
align: 'left',
},
];
export const getK8sClustersListColumns = (
groupBy: IBuilderQuery['groupBy'],
): ColumnType<K8sClustersRowData>[] => {
if (groupBy.length > 0) {
const filteredColumns = [...columnsConfig].filter(
(column) => column.key !== 'clusterName',
);
filteredColumns.unshift(clusterGroupColumnConfig);
return filteredColumns as ColumnType<K8sClustersRowData>[];
}
return columnsConfig as ColumnType<K8sClustersRowData>[];
};
const getGroupByEle = (
cluster: K8sClustersData,
groupBy: IBuilderQuery['groupBy'],
): React.ReactNode => {
const groupByValues: string[] = [];
groupBy.forEach((group) => {
groupByValues.push(cluster.meta[group.key as keyof typeof cluster.meta]);
});
return (
<div className="pod-group">
{groupByValues.map((value) => (
<Tag key={value} color={Color.BG_SLATE_400} className="pod-group-tag-item">
{value === '' ? '<no-value>' : value}
</Tag>
))}
</div>
);
};
export const formatDataForTable = (
data: K8sClustersData[],
groupBy: IBuilderQuery['groupBy'],
): K8sClustersRowData[] =>
data.map((cluster, index) => ({
key: index.toString(),
clusterUID: cluster.meta.k8s_cluster_name,
clusterName: (
<Tooltip title={cluster.meta.k8s_cluster_name}>
{cluster.meta.k8s_cluster_name}
</Tooltip>
),
cpu: (
<ValidateColumnValueWrapper value={cluster.cpuUsage}>
{cluster.cpuUsage}
</ValidateColumnValueWrapper>
),
memory: (
<ValidateColumnValueWrapper value={cluster.memoryUsage}>
{formatBytes(cluster.memoryUsage)}
</ValidateColumnValueWrapper>
),
cpu_allocatable: (
<ValidateColumnValueWrapper value={cluster.cpuAllocatable}>
{cluster.cpuAllocatable}
</ValidateColumnValueWrapper>
),
memory_allocatable: (
<ValidateColumnValueWrapper value={cluster.memoryAllocatable}>
{formatBytes(cluster.memoryAllocatable)}
</ValidateColumnValueWrapper>
),
clusterGroup: getGroupByEle(cluster, groupBy),
meta: cluster.meta,
...cluster.meta,
groupedByMeta: cluster.meta,
}));

View File

@@ -0,0 +1,7 @@
import { K8sDeploymentsData } from 'api/infraMonitoring/getK8sDeploymentsList';
export type DeploymentDetailsProps = {
deployment: K8sDeploymentsData | null;
isModalTimeSelection: boolean;
onClose: () => void;
};

View File

@@ -0,0 +1,604 @@
/* eslint-disable sonarjs/no-identical-functions */
import '../../EntityDetailsUtils/entityDetails.styles.scss';
import { Color, Spacing } from '@signozhq/design-tokens';
import { Button, Divider, Drawer, Radio, Tooltip, Typography } from 'antd';
import { RadioChangeEvent } from 'antd/lib';
import logEvent from 'api/common/logEvent';
import { K8sDeploymentsData } from 'api/infraMonitoring/getK8sDeploymentsList';
import { VIEW_TYPES, VIEWS } from 'components/HostMetricsDetail/constants';
import { QueryParams } from 'constants/query';
import {
initialQueryBuilderFormValuesMap,
initialQueryState,
} from 'constants/queryBuilder';
import ROUTES from 'constants/routes';
import { filterDuplicateFilters } from 'container/InfraMonitoringK8s/commonUtils';
import { K8sCategory } from 'container/InfraMonitoringK8s/constants';
import { QUERY_KEYS } from 'container/InfraMonitoringK8s/EntityDetailsUtils/utils';
import {
CustomTimeType,
Time,
} from 'container/TopNav/DateTimeSelectionV2/config';
import { useIsDarkMode } from 'hooks/useDarkMode';
import useUrlQuery from 'hooks/useUrlQuery';
import GetMinMax from 'lib/getMinMax';
import {
BarChart2,
ChevronsLeftRight,
Compass,
DraftingCompass,
ScrollText,
X,
} from 'lucide-react';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
import {
IBuilderQuery,
TagFilterItem,
} from 'types/api/queryBuilder/queryBuilderData';
import {
LogsAggregatorOperator,
TracesAggregatorOperator,
} from 'types/common/queryBuilder';
import { GlobalReducer } from 'types/reducer/globalTime';
import { v4 as uuidv4 } from 'uuid';
import DeploymentEvents from '../../EntityDetailsUtils/EntityEvents';
import DeploymentLogs from '../../EntityDetailsUtils/EntityLogs';
import DeploymentMetrics from '../../EntityDetailsUtils/EntityMetrics';
import DeploymentTraces from '../../EntityDetailsUtils/EntityTraces';
import {
deploymentWidgetInfo,
getDeploymentMetricsQueryPayload,
} from './constants';
import { DeploymentDetailsProps } from './DeploymentDetails.interfaces';
function DeploymentDetails({
deployment,
onClose,
isModalTimeSelection,
}: DeploymentDetailsProps): JSX.Element {
const { maxTime, minTime, selectedTime } = useSelector<
AppState,
GlobalReducer
>((state) => state.globalTime);
const startMs = useMemo(() => Math.floor(Number(minTime) / 1000000000), [
minTime,
]);
const endMs = useMemo(() => Math.floor(Number(maxTime) / 1000000000), [
maxTime,
]);
const urlQuery = useUrlQuery();
const [modalTimeRange, setModalTimeRange] = useState(() => ({
startTime: startMs,
endTime: endMs,
}));
const [selectedInterval, setSelectedInterval] = useState<Time>(
selectedTime as Time,
);
const [selectedView, setSelectedView] = useState<VIEWS>(VIEWS.METRICS);
const isDarkMode = useIsDarkMode();
const initialFilters = useMemo(
() => ({
op: 'AND',
items: [
{
id: uuidv4(),
key: {
key: QUERY_KEYS.K8S_DEPLOYMENT_NAME,
dataType: DataTypes.String,
type: 'resource',
isColumn: false,
isJSON: false,
id: 'k8s_deployment_name--string--resource--false',
},
op: '=',
value: deployment?.meta.k8s_deployment_name || '',
},
{
id: uuidv4(),
key: {
key: QUERY_KEYS.K8S_NAMESPACE_NAME,
dataType: DataTypes.String,
type: 'resource',
isColumn: false,
isJSON: false,
id: 'k8s_deployment_name--string--resource--false',
},
op: '=',
value: deployment?.meta.k8s_namespace_name || '',
},
],
}),
[deployment?.meta.k8s_deployment_name, deployment?.meta.k8s_namespace_name],
);
const initialEventsFilters = useMemo(
() => ({
op: 'AND',
items: [
{
id: uuidv4(),
key: {
key: QUERY_KEYS.K8S_OBJECT_KIND,
dataType: DataTypes.String,
type: 'resource',
isColumn: false,
isJSON: false,
id: 'k8s.object.kind--string--resource--false',
},
op: '=',
value: 'Deployment',
},
{
id: uuidv4(),
key: {
key: QUERY_KEYS.K8S_OBJECT_NAME,
dataType: DataTypes.String,
type: 'resource',
isColumn: false,
isJSON: false,
id: 'k8s.object.name--string--resource--false',
},
op: '=',
value: deployment?.meta.k8s_deployment_name || '',
},
],
}),
[deployment?.meta.k8s_deployment_name],
);
const [logFilters, setLogFilters] = useState<IBuilderQuery['filters']>(
initialFilters,
);
const [tracesFilters, setTracesFilters] = useState<IBuilderQuery['filters']>(
initialFilters,
);
const [eventsFilters, setEventsFilters] = useState<IBuilderQuery['filters']>(
initialEventsFilters,
);
useEffect(() => {
logEvent('Infra Monitoring: Deployments list details page visited', {
deployment: deployment?.deploymentName,
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useEffect(() => {
setLogFilters(initialFilters);
setTracesFilters(initialFilters);
setEventsFilters(initialEventsFilters);
}, [initialFilters, initialEventsFilters]);
useEffect(() => {
setSelectedInterval(selectedTime as Time);
if (selectedTime !== 'custom') {
const { maxTime, minTime } = GetMinMax(selectedTime);
setModalTimeRange({
startTime: Math.floor(minTime / 1000000000),
endTime: Math.floor(maxTime / 1000000000),
});
}
}, [selectedTime, minTime, maxTime]);
const handleTabChange = (e: RadioChangeEvent): void => {
setSelectedView(e.target.value);
};
const handleTimeChange = useCallback(
(interval: Time | CustomTimeType, dateTimeRange?: [number, number]): void => {
setSelectedInterval(interval as Time);
if (interval === 'custom' && dateTimeRange) {
setModalTimeRange({
startTime: Math.floor(dateTimeRange[0] / 1000),
endTime: Math.floor(dateTimeRange[1] / 1000),
});
} else {
const { maxTime, minTime } = GetMinMax(interval);
setModalTimeRange({
startTime: Math.floor(minTime / 1000000000),
endTime: Math.floor(maxTime / 1000000000),
});
}
logEvent('Infra Monitoring: Deployments list details time updated', {
deployment: deployment?.deploymentName,
interval,
});
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[],
);
const handleChangeLogFilters = useCallback(
(value: IBuilderQuery['filters']) => {
setLogFilters((prevFilters) => {
const primaryFilters = prevFilters.items.filter((item) =>
[QUERY_KEYS.K8S_DEPLOYMENT_NAME, QUERY_KEYS.K8S_NAMESPACE_NAME].includes(
item.key?.key ?? '',
),
);
const paginationFilter = value.items.find((item) => item.key?.key === 'id');
const newFilters = value.items.filter(
(item) =>
item.key?.key !== 'id' &&
item.key?.key !== QUERY_KEYS.K8S_DEPLOYMENT_NAME,
);
logEvent(
'Infra Monitoring: Deployments list details logs filters applied',
{
deployment: deployment?.deploymentName,
},
);
return {
op: 'AND',
items: filterDuplicateFilters(
[
...primaryFilters,
...newFilters,
...(paginationFilter ? [paginationFilter] : []),
].filter((item): item is TagFilterItem => item !== undefined),
),
};
});
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[],
);
const handleChangeTracesFilters = useCallback(
(value: IBuilderQuery['filters']) => {
setTracesFilters((prevFilters) => {
const primaryFilters = prevFilters.items.filter((item) =>
[QUERY_KEYS.K8S_DEPLOYMENT_NAME, QUERY_KEYS.K8S_NAMESPACE_NAME].includes(
item.key?.key ?? '',
),
);
logEvent(
'Infra Monitoring: Deployments list details traces filters applied',
{
deployment: deployment?.deploymentName,
},
);
return {
op: 'AND',
items: filterDuplicateFilters(
[
...primaryFilters,
...value.items.filter(
(item) => item.key?.key !== QUERY_KEYS.K8S_DEPLOYMENT_NAME,
),
].filter((item): item is TagFilterItem => item !== undefined),
),
};
});
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[],
);
const handleChangeEventsFilters = useCallback(
(value: IBuilderQuery['filters']) => {
setEventsFilters((prevFilters) => {
const deploymentKindFilter = prevFilters.items.find(
(item) => item.key?.key === QUERY_KEYS.K8S_OBJECT_KIND,
);
const deploymentNameFilter = prevFilters.items.find(
(item) => item.key?.key === QUERY_KEYS.K8S_OBJECT_NAME,
);
logEvent(
'Infra Monitoring: Deployments list details events filters applied',
{
deployment: deployment?.deploymentName,
},
);
return {
op: 'AND',
items: filterDuplicateFilters(
[
deploymentKindFilter,
deploymentNameFilter,
...value.items.filter(
(item) =>
item.key?.key !== QUERY_KEYS.K8S_OBJECT_KIND &&
item.key?.key !== QUERY_KEYS.K8S_OBJECT_NAME,
),
].filter((item): item is TagFilterItem => item !== undefined),
),
};
});
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[],
);
const handleExplorePagesRedirect = (): void => {
if (selectedInterval !== 'custom') {
urlQuery.set(QueryParams.relativeTime, selectedInterval);
} else {
urlQuery.delete(QueryParams.relativeTime);
urlQuery.set(QueryParams.startTime, modalTimeRange.startTime.toString());
urlQuery.set(QueryParams.endTime, modalTimeRange.endTime.toString());
}
logEvent('Infra Monitoring: Deployments list details explore clicked', {
deployment: deployment?.deploymentName,
view: selectedView,
});
if (selectedView === VIEW_TYPES.LOGS) {
const filtersWithoutPagination = {
...logFilters,
items: logFilters.items.filter((item) => item.key?.key !== 'id'),
};
const compositeQuery = {
...initialQueryState,
queryType: 'builder',
builder: {
...initialQueryState.builder,
queryData: [
{
...initialQueryBuilderFormValuesMap.logs,
aggregateOperator: LogsAggregatorOperator.NOOP,
filters: filtersWithoutPagination,
},
],
},
};
urlQuery.set('compositeQuery', JSON.stringify(compositeQuery));
window.open(
`${window.location.origin}${ROUTES.LOGS_EXPLORER}?${urlQuery.toString()}`,
'_blank',
);
} else if (selectedView === VIEW_TYPES.TRACES) {
const compositeQuery = {
...initialQueryState,
queryType: 'builder',
builder: {
...initialQueryState.builder,
queryData: [
{
...initialQueryBuilderFormValuesMap.traces,
aggregateOperator: TracesAggregatorOperator.NOOP,
filters: tracesFilters,
},
],
},
};
urlQuery.set('compositeQuery', JSON.stringify(compositeQuery));
window.open(
`${window.location.origin}${ROUTES.TRACES_EXPLORER}?${urlQuery.toString()}`,
'_blank',
);
}
};
const handleClose = (): void => {
setSelectedInterval(selectedTime as Time);
if (selectedTime !== 'custom') {
const { maxTime, minTime } = GetMinMax(selectedTime);
setModalTimeRange({
startTime: Math.floor(minTime / 1000000000),
endTime: Math.floor(maxTime / 1000000000),
});
}
setSelectedView(VIEW_TYPES.METRICS);
onClose();
};
return (
<Drawer
width="70%"
title={
<>
<Divider type="vertical" />
<Typography.Text className="title">
{deployment?.meta.k8s_deployment_name}
</Typography.Text>
</>
}
placement="right"
onClose={handleClose}
open={!!deployment}
style={{
overscrollBehavior: 'contain',
background: isDarkMode ? Color.BG_INK_400 : Color.BG_VANILLA_100,
}}
className="entity-detail-drawer"
destroyOnClose
closeIcon={<X size={16} style={{ marginTop: Spacing.MARGIN_1 }} />}
>
{deployment && (
<>
<div className="entity-detail-drawer__entity">
<div className="entity-details-grid">
<div className="labels-row">
<Typography.Text
type="secondary"
className="entity-details-metadata-label"
>
Deployment Name
</Typography.Text>
<Typography.Text
type="secondary"
className="entity-details-metadata-label"
>
Cluster Name
</Typography.Text>
<Typography.Text
type="secondary"
className="entity-details-metadata-label"
>
Namespace Name
</Typography.Text>
</div>
<div className="values-row">
<Typography.Text className="entity-details-metadata-value">
<Tooltip title={deployment.meta.k8s_deployment_name}>
{deployment.meta.k8s_deployment_name}
</Tooltip>
</Typography.Text>
<Typography.Text className="entity-details-metadata-value">
<Tooltip title={deployment.meta.k8s_cluster_name}>
{deployment.meta.k8s_cluster_name}
</Tooltip>
</Typography.Text>
<Typography.Text className="entity-details-metadata-value">
<Tooltip title={deployment.meta.k8s_namespace_name}>
{deployment.meta.k8s_namespace_name}
</Tooltip>
</Typography.Text>
</div>
</div>
</div>
<div className="views-tabs-container">
<Radio.Group
className="views-tabs"
onChange={handleTabChange}
value={selectedView}
>
<Radio.Button
className={
// eslint-disable-next-line sonarjs/no-duplicate-string
selectedView === VIEW_TYPES.METRICS ? 'selected_view tab' : 'tab'
}
value={VIEW_TYPES.METRICS}
>
<div className="view-title">
<BarChart2 size={14} />
Metrics
</div>
</Radio.Button>
<Radio.Button
className={
selectedView === VIEW_TYPES.LOGS ? 'selected_view tab' : 'tab'
}
value={VIEW_TYPES.LOGS}
>
<div className="view-title">
<ScrollText size={14} />
Logs
</div>
</Radio.Button>
<Radio.Button
className={
selectedView === VIEW_TYPES.TRACES ? 'selected_view tab' : 'tab'
}
value={VIEW_TYPES.TRACES}
>
<div className="view-title">
<DraftingCompass size={14} />
Traces
</div>
</Radio.Button>
<Radio.Button
className={
selectedView === VIEW_TYPES.EVENTS ? 'selected_view tab' : 'tab'
}
value={VIEW_TYPES.EVENTS}
>
<div className="view-title">
<ChevronsLeftRight size={14} />
Events
</div>
</Radio.Button>
</Radio.Group>
{(selectedView === VIEW_TYPES.LOGS ||
selectedView === VIEW_TYPES.TRACES) && (
<Button
icon={<Compass size={18} />}
className="compass-button"
onClick={handleExplorePagesRedirect}
/>
)}
</div>
{selectedView === VIEW_TYPES.METRICS && (
<DeploymentMetrics<K8sDeploymentsData>
timeRange={modalTimeRange}
isModalTimeSelection={isModalTimeSelection}
handleTimeChange={handleTimeChange}
selectedInterval={selectedInterval}
entity={deployment}
entityWidgetInfo={deploymentWidgetInfo}
getEntityQueryPayload={getDeploymentMetricsQueryPayload}
category={K8sCategory.DEPLOYMENTS}
queryKey="deploymentMetrics"
/>
)}
{selectedView === VIEW_TYPES.LOGS && (
<DeploymentLogs
timeRange={modalTimeRange}
isModalTimeSelection={isModalTimeSelection}
handleTimeChange={handleTimeChange}
handleChangeLogFilters={handleChangeLogFilters}
logFilters={logFilters}
selectedInterval={selectedInterval}
queryKeyFilters={[
QUERY_KEYS.K8S_DEPLOYMENT_NAME,
QUERY_KEYS.K8S_NAMESPACE_NAME,
]}
queryKey="deploymentLogs"
category={K8sCategory.DEPLOYMENTS}
/>
)}
{selectedView === VIEW_TYPES.TRACES && (
<DeploymentTraces
timeRange={modalTimeRange}
isModalTimeSelection={isModalTimeSelection}
handleTimeChange={handleTimeChange}
handleChangeTracesFilters={handleChangeTracesFilters}
tracesFilters={tracesFilters}
selectedInterval={selectedInterval}
queryKey="deploymentTraces"
/>
)}
{selectedView === VIEW_TYPES.EVENTS && (
<DeploymentEvents
timeRange={modalTimeRange}
handleChangeEventFilters={handleChangeEventsFilters}
filters={eventsFilters}
isModalTimeSelection={isModalTimeSelection}
handleTimeChange={handleTimeChange}
selectedInterval={selectedInterval}
category={K8sCategory.DEPLOYMENTS}
queryKey="deploymentEvents"
/>
)}
</>
)}
</Drawer>
);
}
export default DeploymentDetails;

View File

@@ -0,0 +1,544 @@
/* eslint-disable sonarjs/no-duplicate-string */
import { K8sDeploymentsData } from 'api/infraMonitoring/getK8sDeploymentsList';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults';
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { EQueryType } from 'types/common/dashboard';
import { DataSource } from 'types/common/queryBuilder';
import { v4 } from 'uuid';
export const deploymentWidgetInfo = [
{
title: 'CPU usage, request, limits',
yAxisUnit: '',
},
{
title: 'Memory usage, request, limits)',
yAxisUnit: 'bytes',
},
{
title: 'Network IO',
yAxisUnit: 'binBps',
},
{
title: 'Network error count',
yAxisUnit: '',
},
];
export const getDeploymentMetricsQueryPayload = (
deployment: K8sDeploymentsData,
start: number,
end: number,
): GetQueryResultsProps[] => [
{
selectedTime: 'GLOBAL_TIME',
graphType: PANEL_TYPES.TIME_SERIES,
query: {
builder: {
queryData: [
{
aggregateAttribute: {
dataType: DataTypes.Float64,
id: 'k8s_pod_cpu_utilization--float64--Gauge--true',
isColumn: true,
isJSON: false,
key: 'k8s_pod_cpu_utilization',
type: 'Gauge',
},
aggregateOperator: 'avg',
dataSource: DataSource.METRICS,
disabled: false,
expression: 'A',
filters: {
items: [
{
id: 'aec60cba',
key: {
dataType: DataTypes.String,
id: 'k8s_deployment_name--string--tag--false',
isColumn: false,
isJSON: false,
key: 'k8s_deployment_name',
type: 'tag',
},
op: '=',
value: deployment.meta.k8s_deployment_name,
},
],
op: 'AND',
},
functions: [],
groupBy: [],
having: [],
legend: 'usage',
limit: null,
orderBy: [],
queryName: 'A',
reduceTo: 'avg',
spaceAggregation: 'sum',
stepInterval: 60,
timeAggregation: 'avg',
},
{
aggregateAttribute: {
dataType: DataTypes.Float64,
id: 'k8s_container_cpu_request--float64--Gauge--true',
isColumn: true,
isJSON: false,
key: 'k8s_container_cpu_request',
type: 'Gauge',
},
aggregateOperator: 'avg',
dataSource: DataSource.METRICS,
disabled: false,
expression: 'B',
filters: {
items: [
{
id: 'd047ec14',
key: {
dataType: DataTypes.String,
id: 'k8s_pod_name--string--tag--false',
isColumn: false,
isJSON: false,
key: 'k8s_pod_name',
type: 'tag',
},
op: 'contains',
value: deployment.meta.k8s_deployment_name,
},
],
op: 'AND',
},
functions: [],
groupBy: [],
having: [],
legend: 'requests',
limit: null,
orderBy: [],
queryName: 'B',
reduceTo: 'avg',
spaceAggregation: 'sum',
stepInterval: 60,
timeAggregation: 'avg',
},
{
aggregateAttribute: {
dataType: DataTypes.Float64,
id: 'k8s_container_cpu_limit--float64--Gauge--true',
isColumn: true,
isJSON: false,
key: 'k8s_container_cpu_limit',
type: 'Gauge',
},
aggregateOperator: 'avg',
dataSource: DataSource.METRICS,
disabled: false,
expression: 'C',
filters: {
items: [
{
id: '750b7856',
key: {
dataType: DataTypes.String,
id: 'k8s_pod_name--string--tag--false',
isColumn: false,
isJSON: false,
key: 'k8s_pod_name',
type: 'tag',
},
op: 'contains',
value: deployment.meta.k8s_deployment_name,
},
],
op: 'AND',
},
functions: [],
groupBy: [],
having: [],
legend: 'limits',
limit: null,
orderBy: [],
queryName: 'C',
reduceTo: 'avg',
spaceAggregation: 'sum',
stepInterval: 60,
timeAggregation: 'avg',
},
],
queryFormulas: [],
},
clickhouse_sql: [
{
disabled: false,
legend: '',
name: 'A',
query: '',
},
],
id: v4(),
promql: [
{
disabled: false,
legend: '',
name: 'A',
query: '',
},
],
queryType: EQueryType.QUERY_BUILDER,
},
variables: {},
formatForWeb: false,
start,
end,
},
{
selectedTime: 'GLOBAL_TIME',
graphType: PANEL_TYPES.TIME_SERIES,
query: {
builder: {
queryData: [
{
aggregateAttribute: {
dataType: DataTypes.Float64,
id: 'k8s_pod_memory_usage--float64--Gauge--true',
isColumn: true,
isJSON: false,
key: 'k8s_pod_memory_usage',
type: 'Gauge',
},
aggregateOperator: 'avg',
dataSource: DataSource.METRICS,
disabled: false,
expression: 'A',
filters: {
items: [
{
id: '768c2f47',
key: {
dataType: DataTypes.String,
id: 'k8s_deployment_name--string--tag--false',
isColumn: false,
isJSON: false,
key: 'k8s_deployment_name',
type: 'tag',
},
op: '=',
value: deployment.meta.k8s_deployment_name,
},
],
op: 'AND',
},
functions: [],
groupBy: [],
having: [],
legend: 'usage',
limit: null,
orderBy: [],
queryName: 'A',
reduceTo: 'avg',
spaceAggregation: 'sum',
stepInterval: 60,
timeAggregation: 'avg',
},
{
aggregateAttribute: {
dataType: DataTypes.Float64,
id: 'k8s_container_memory_request--float64--Gauge--true',
isColumn: true,
isJSON: false,
key: 'k8s_container_memory_request',
type: 'Gauge',
},
aggregateOperator: 'avg',
dataSource: DataSource.METRICS,
disabled: false,
expression: 'B',
filters: {
items: [
{
id: '1a96fa81',
key: {
dataType: DataTypes.String,
id: 'k8s_pod_name--string--tag--false',
isColumn: false,
isJSON: false,
key: 'k8s_pod_name',
type: 'tag',
},
op: 'contains',
value: deployment.meta.k8s_deployment_name,
},
],
op: 'AND',
},
functions: [],
groupBy: [],
having: [],
legend: 'requests',
limit: null,
orderBy: [],
queryName: 'B',
reduceTo: 'avg',
spaceAggregation: 'sum',
stepInterval: 60,
timeAggregation: 'avg',
},
{
aggregateAttribute: {
dataType: DataTypes.Float64,
id: 'k8s_container_memory_limit--float64--Gauge--true',
isColumn: true,
isJSON: false,
key: 'k8s_container_memory_limit',
type: 'Gauge',
},
aggregateOperator: 'avg',
dataSource: DataSource.METRICS,
disabled: false,
expression: 'C',
filters: {
items: [
{
id: 'e69a2b7e',
key: {
dataType: DataTypes.String,
id: 'k8s_pod_name--string--tag--false',
isColumn: false,
isJSON: false,
key: 'k8s_pod_name',
type: 'tag',
},
op: 'contains',
value: deployment.meta.k8s_deployment_name,
},
],
op: 'AND',
},
functions: [],
groupBy: [],
having: [],
legend: 'limits',
limit: null,
orderBy: [],
queryName: 'C',
reduceTo: 'avg',
spaceAggregation: 'sum',
stepInterval: 60,
timeAggregation: 'avg',
},
],
queryFormulas: [],
},
clickhouse_sql: [
{
disabled: false,
legend: '',
name: 'A',
query: '',
},
],
id: v4(),
promql: [
{
disabled: false,
legend: '',
name: 'A',
query: '',
},
],
queryType: EQueryType.QUERY_BUILDER,
},
variables: {},
formatForWeb: false,
start,
end,
},
{
selectedTime: 'GLOBAL_TIME',
graphType: PANEL_TYPES.TIME_SERIES,
query: {
builder: {
queryData: [
{
aggregateAttribute: {
dataType: DataTypes.Float64,
id: 'k8s_pod_network_io--float64--Sum--true',
isColumn: true,
isJSON: false,
key: 'k8s_pod_network_io',
type: 'Sum',
},
aggregateOperator: 'rate',
dataSource: DataSource.METRICS,
disabled: false,
expression: 'A',
filters: {
items: [
{
id: '8b550f6d',
key: {
dataType: DataTypes.String,
id: 'k8s_deployment_name--string--tag--false',
isColumn: false,
isJSON: false,
key: 'k8s_deployment_name',
type: 'tag',
},
op: '=',
value: deployment.meta.k8s_deployment_name,
},
],
op: 'AND',
},
functions: [],
groupBy: [
{
dataType: DataTypes.String,
id: 'direction--string--tag--false',
isColumn: false,
isJSON: false,
key: 'direction',
type: 'tag',
},
{
dataType: DataTypes.String,
id: 'interface--string--tag--false',
isColumn: false,
isJSON: false,
key: 'interface',
type: 'tag',
},
],
having: [],
legend: '{{direction}} :: {{interface}}',
limit: null,
orderBy: [],
queryName: 'A',
reduceTo: 'avg',
spaceAggregation: 'sum',
stepInterval: 60,
timeAggregation: 'rate',
},
],
queryFormulas: [],
},
clickhouse_sql: [
{
disabled: false,
legend: '',
name: 'A',
query: '',
},
],
id: v4(),
promql: [
{
disabled: false,
legend: '',
name: 'A',
query: '',
},
],
queryType: EQueryType.QUERY_BUILDER,
},
variables: {},
formatForWeb: false,
start,
end,
},
{
selectedTime: 'GLOBAL_TIME',
graphType: PANEL_TYPES.TIME_SERIES,
query: {
builder: {
queryData: [
{
aggregateAttribute: {
dataType: DataTypes.Float64,
id: 'k8s_pod_network_errors--float64--Sum--true',
isColumn: true,
isJSON: false,
key: 'k8s_pod_network_errors',
type: 'Sum',
},
aggregateOperator: 'increase',
dataSource: DataSource.METRICS,
disabled: false,
expression: 'A',
filters: {
items: [
{
id: 'e16c1e4a',
key: {
dataType: DataTypes.String,
id: 'k8s_deployment_name--string--tag--false',
isColumn: false,
isJSON: false,
key: 'k8s_deployment_name',
type: 'tag',
},
op: '=',
value: deployment.meta.k8s_deployment_name,
},
],
op: 'AND',
},
functions: [],
groupBy: [
{
dataType: DataTypes.String,
id: 'direction--string--tag--false',
isColumn: false,
isJSON: false,
key: 'direction',
type: 'tag',
},
{
dataType: DataTypes.String,
id: 'interface--string--tag--false',
isColumn: false,
isJSON: false,
key: 'interface',
type: 'tag',
},
],
having: [],
legend: '{{direction}} :: {{interface}}',
limit: null,
orderBy: [],
queryName: 'A',
reduceTo: 'avg',
spaceAggregation: 'sum',
stepInterval: 60,
timeAggregation: 'increase',
},
],
queryFormulas: [],
},
clickhouse_sql: [
{
disabled: false,
legend: '',
name: 'A',
query: '',
},
],
id: v4(),
promql: [
{
disabled: false,
legend: '',
name: 'A',
query: '',
},
],
queryType: EQueryType.QUERY_BUILDER,
},
variables: {},
formatForWeb: false,
start,
end,
},
];

View File

@@ -0,0 +1,3 @@
import DeploymentDetails from './DeploymentDetails';
export default DeploymentDetails;

View File

@@ -0,0 +1,57 @@
.infra-monitoring-container {
.deployments-list-table {
.expanded-table-container {
padding-left: 20px;
}
.ant-table-row-expand-icon-cell {
min-width: 30px !important;
max-width: 30px !important;
}
}
}
.ant-table-cell {
&:has(.deployment-name-header) {
min-width: 250px !important;
max-width: 250px !important;
}
}
.ant-table-cell {
&:has(.namespace-name-header) {
min-width: 220px !important;
max-width: 220px !important;
}
}
.ant-table-cell {
&:has(.med-col) {
min-width: 200px !important;
max-width: 200px !important;
}
}
.ant-table-cell {
&:has(.small-col) {
min-width: 120px !important;
max-width: 120px !important;
}
}
.progress-container {
display: flex !important;
justify-content: start !important;
}
.expanded-deployments-list-table {
.ant-table-cell {
min-width: 180px !important;
max-width: 180px !important;
}
.ant-table-row-expand-icon-cell {
min-width: 30px !important;
max-width: 30px !important;
}
}

View File

@@ -0,0 +1,519 @@
/* eslint-disable no-restricted-syntax */
/* eslint-disable @typescript-eslint/explicit-function-return-type */
import '../InfraMonitoringK8s.styles.scss';
import './K8sDeploymentsList.styles.scss';
import { LoadingOutlined } from '@ant-design/icons';
import {
Button,
Spin,
Table,
TablePaginationConfig,
TableProps,
Typography,
} from 'antd';
import { ColumnType, SorterResult } from 'antd/es/table/interface';
import logEvent from 'api/common/logEvent';
import { K8sDeploymentsListPayload } from 'api/infraMonitoring/getK8sDeploymentsList';
import classNames from 'classnames';
import { useGetK8sDeploymentsList } from 'hooks/infraMonitoring/useGetK8sDeploymentsList';
import { useGetAggregateKeys } from 'hooks/queryBuilder/useGetAggregateKeys';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useQueryOperations } from 'hooks/queryBuilder/useQueryBuilderOperations';
import { ChevronDown, ChevronRight } from 'lucide-react';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import { GlobalReducer } from 'types/reducer/globalTime';
import {
K8sCategory,
K8sEntityToAggregateAttributeMapping,
} from '../constants';
import K8sHeader from '../K8sHeader';
import LoadingContainer from '../LoadingContainer';
import DeploymentDetails from './DeploymentDetails';
import {
defaultAddedColumns,
formatDataForTable,
getK8sDeploymentsListColumns,
getK8sDeploymentsListQuery,
K8sDeploymentsRowData,
} from './utils';
// eslint-disable-next-line sonarjs/cognitive-complexity
function K8sDeploymentsList({
isFiltersVisible,
handleFilterVisibilityChange,
quickFiltersLastUpdated,
}: {
isFiltersVisible: boolean;
handleFilterVisibilityChange: () => void;
quickFiltersLastUpdated: number;
}): JSX.Element {
const { maxTime, minTime } = useSelector<AppState, GlobalReducer>(
(state) => state.globalTime,
);
const [currentPage, setCurrentPage] = useState(1);
const [expandedRowKeys, setExpandedRowKeys] = useState<string[]>([]);
const [orderBy, setOrderBy] = useState<{
columnName: string;
order: 'asc' | 'desc';
} | null>(null);
const [selectedDeploymentUID, setselectedDeploymentUID] = useState<
string | null
>(null);
const pageSize = 10;
const [groupBy, setGroupBy] = useState<IBuilderQuery['groupBy']>([]);
const [
selectedRowData,
setSelectedRowData,
] = useState<K8sDeploymentsRowData | null>(null);
const [groupByOptions, setGroupByOptions] = useState<
{ value: string; label: string }[]
>([]);
const { currentQuery } = useQueryBuilder();
const queryFilters = useMemo(
() =>
currentQuery?.builder?.queryData[0]?.filters || {
items: [],
op: 'and',
},
[currentQuery?.builder?.queryData],
);
// Reset pagination every time quick filters are changed
useEffect(() => {
setCurrentPage(1);
}, [quickFiltersLastUpdated]);
const createFiltersForSelectedRowData = (
selectedRowData: K8sDeploymentsRowData,
groupBy: IBuilderQuery['groupBy'],
): IBuilderQuery['filters'] => {
const baseFilters: IBuilderQuery['filters'] = {
items: [...queryFilters.items],
op: 'and',
};
if (!selectedRowData) return baseFilters;
const { groupedByMeta } = selectedRowData;
for (const key of groupBy) {
baseFilters.items.push({
key: {
key: key.key,
type: null,
},
op: '=',
value: groupedByMeta[key.key],
id: key.key,
});
}
return baseFilters;
};
const fetchGroupedByRowDataQuery = useMemo(() => {
if (!selectedRowData) return null;
const baseQuery = getK8sDeploymentsListQuery();
const filters = createFiltersForSelectedRowData(selectedRowData, groupBy);
return {
...baseQuery,
limit: 10,
offset: 0,
filters,
start: Math.floor(minTime / 1000000),
end: Math.floor(maxTime / 1000000),
orderBy,
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [minTime, maxTime, orderBy, selectedRowData, groupBy]);
const {
data: groupedByRowData,
isFetching: isFetchingGroupedByRowData,
isLoading: isLoadingGroupedByRowData,
isError: isErrorGroupedByRowData,
refetch: fetchGroupedByRowData,
} = useGetK8sDeploymentsList(
fetchGroupedByRowDataQuery as K8sDeploymentsListPayload,
{
queryKey: ['deploymentList', fetchGroupedByRowDataQuery],
enabled: !!fetchGroupedByRowDataQuery && !!selectedRowData,
},
);
const {
data: groupByFiltersData,
isLoading: isLoadingGroupByFilters,
} = useGetAggregateKeys(
{
dataSource: currentQuery.builder.queryData[0].dataSource,
aggregateAttribute:
K8sEntityToAggregateAttributeMapping[K8sCategory.DEPLOYMENTS],
aggregateOperator: 'noop',
searchText: '',
tagType: '',
},
{
queryKey: [currentQuery.builder.queryData[0].dataSource, 'noop'],
},
true,
K8sCategory.NODES,
);
const query = useMemo(() => {
const baseQuery = getK8sDeploymentsListQuery();
const queryPayload = {
...baseQuery,
limit: pageSize,
offset: (currentPage - 1) * pageSize,
filters: queryFilters,
start: Math.floor(minTime / 1000000),
end: Math.floor(maxTime / 1000000),
orderBy,
};
if (groupBy.length > 0) {
queryPayload.groupBy = groupBy;
}
return queryPayload;
}, [currentPage, minTime, maxTime, orderBy, groupBy, queryFilters]);
const formattedGroupedByDeploymentsData = useMemo(
() =>
formatDataForTable(groupedByRowData?.payload?.data?.records || [], groupBy),
[groupedByRowData, groupBy],
);
const { data, isFetching, isLoading, isError } = useGetK8sDeploymentsList(
query as K8sDeploymentsListPayload,
{
queryKey: ['deploymentList', query],
enabled: !!query,
},
);
const deploymentsData = useMemo(() => data?.payload?.data?.records || [], [
data,
]);
const totalCount = data?.payload?.data?.total || 0;
const formattedDeploymentsData = useMemo(
() => formatDataForTable(deploymentsData, groupBy),
[deploymentsData, groupBy],
);
const columns = useMemo(() => getK8sDeploymentsListColumns(groupBy), [
groupBy,
]);
const handleGroupByRowClick = (record: K8sDeploymentsRowData): void => {
setSelectedRowData(record);
if (expandedRowKeys.includes(record.key)) {
setExpandedRowKeys(expandedRowKeys.filter((key) => key !== record.key));
} else {
setExpandedRowKeys([record.key]);
}
};
useEffect(() => {
if (selectedRowData) {
fetchGroupedByRowData();
}
}, [selectedRowData, fetchGroupedByRowData]);
const handleTableChange: TableProps<K8sDeploymentsRowData>['onChange'] = useCallback(
(
pagination: TablePaginationConfig,
_filters: Record<string, (string | number | boolean)[] | null>,
sorter:
| SorterResult<K8sDeploymentsRowData>
| SorterResult<K8sDeploymentsRowData>[],
): void => {
if (pagination.current) {
setCurrentPage(pagination.current);
}
if ('field' in sorter && sorter.order) {
setOrderBy({
columnName: sorter.field as string,
order: sorter.order === 'ascend' ? 'asc' : 'desc',
});
} else {
setOrderBy(null);
}
},
[],
);
const { handleChangeQueryData } = useQueryOperations({
index: 0,
query: currentQuery.builder.queryData[0],
entityVersion: '',
});
const handleFiltersChange = useCallback(
(value: IBuilderQuery['filters']): void => {
handleChangeQueryData('filters', value);
setCurrentPage(1);
logEvent('Infra Monitoring: K8s list filters applied', {
filters: value,
});
},
[handleChangeQueryData],
);
useEffect(() => {
logEvent('Infra Monitoring: K8s list page visited', {});
}, []);
const selectedDeploymentData = useMemo(() => {
if (!selectedDeploymentUID) return null;
return (
deploymentsData.find(
(deployment) => deployment.deploymentName === selectedDeploymentUID,
) || null
);
}, [selectedDeploymentUID, deploymentsData]);
const handleRowClick = (record: K8sDeploymentsRowData): void => {
if (groupBy.length === 0) {
setSelectedRowData(null);
setselectedDeploymentUID(record.deploymentUID);
} else {
handleGroupByRowClick(record);
}
logEvent('Infra Monitoring: K8s deployment list item clicked', {
deploymentUID: record.deploymentName,
});
};
const nestedColumns = useMemo(() => getK8sDeploymentsListColumns([]), []);
const isGroupedByAttribute = groupBy.length > 0;
const handleExpandedRowViewAllClick = (): void => {
if (!selectedRowData) return;
const filters = createFiltersForSelectedRowData(selectedRowData, groupBy);
handleFiltersChange(filters);
setCurrentPage(1);
setSelectedRowData(null);
setGroupBy([]);
setOrderBy(null);
};
const expandedRowRender = (): JSX.Element => (
<div className="expanded-table-container">
{isErrorGroupedByRowData && (
<Typography>{groupedByRowData?.error || 'Something went wrong'}</Typography>
)}
{isFetchingGroupedByRowData || isLoadingGroupedByRowData ? (
<LoadingContainer />
) : (
<div className="expanded-table">
<Table
columns={nestedColumns as ColumnType<K8sDeploymentsRowData>[]}
dataSource={formattedGroupedByDeploymentsData}
pagination={false}
scroll={{ x: true }}
tableLayout="fixed"
size="small"
loading={{
spinning: isFetchingGroupedByRowData || isLoadingGroupedByRowData,
indicator: <Spin indicator={<LoadingOutlined size={14} spin />} />,
}}
showHeader={false}
/>
{groupedByRowData?.payload?.data?.total &&
groupedByRowData?.payload?.data?.total > 10 ? (
<div className="expanded-table-footer">
<Button
type="default"
size="small"
className="periscope-btn secondary"
onClick={handleExpandedRowViewAllClick}
>
View All
</Button>
</div>
) : null}
</div>
)}
</div>
);
const expandRowIconRenderer = ({
expanded,
onExpand,
record,
}: {
expanded: boolean;
onExpand: (
record: K8sDeploymentsRowData,
e: React.MouseEvent<HTMLButtonElement>,
) => void;
record: K8sDeploymentsRowData;
}): JSX.Element | null => {
if (!isGroupedByAttribute) {
return null;
}
return expanded ? (
<Button
className="periscope-btn ghost"
onClick={(e: React.MouseEvent<HTMLButtonElement>): void =>
onExpand(record, e)
}
role="button"
>
<ChevronDown size={14} />
</Button>
) : (
<Button
className="periscope-btn ghost"
onClick={(e: React.MouseEvent<HTMLButtonElement>): void =>
onExpand(record, e)
}
role="button"
>
<ChevronRight size={14} />
</Button>
);
};
const handleCloseDeploymentDetail = (): void => {
setselectedDeploymentUID(null);
};
const handleGroupByChange = useCallback(
(value: IBuilderQuery['groupBy']) => {
const groupBy = [];
for (let index = 0; index < value.length; index++) {
const element = (value[index] as unknown) as string;
const key = groupByFiltersData?.payload?.attributeKeys?.find(
(key) => key.key === element,
);
if (key) {
groupBy.push(key);
}
}
// Reset pagination on switching to groupBy
setCurrentPage(1);
setGroupBy(groupBy);
setExpandedRowKeys([]);
},
[groupByFiltersData],
);
useEffect(() => {
if (groupByFiltersData?.payload) {
setGroupByOptions(
groupByFiltersData?.payload?.attributeKeys?.map((filter) => ({
value: filter.key,
label: filter.key,
})) || [],
);
}
}, [groupByFiltersData]);
return (
<div className="k8s-list">
<K8sHeader
isFiltersVisible={isFiltersVisible}
handleFilterVisibilityChange={handleFilterVisibilityChange}
defaultAddedColumns={defaultAddedColumns}
handleFiltersChange={handleFiltersChange}
groupByOptions={groupByOptions}
isLoadingGroupByFilters={isLoadingGroupByFilters}
handleGroupByChange={handleGroupByChange}
selectedGroupBy={groupBy}
entity={K8sCategory.NODES}
/>
{isError && <Typography>{data?.error || 'Something went wrong'}</Typography>}
<Table
className={classNames('k8s-list-table', 'deployments-list-table', {
'expanded-deployments-list-table': isGroupedByAttribute,
})}
dataSource={isFetching || isLoading ? [] : formattedDeploymentsData}
columns={columns}
pagination={{
current: currentPage,
pageSize,
total: totalCount,
showSizeChanger: false,
hideOnSinglePage: true,
}}
scroll={{ x: true }}
loading={{
spinning: isFetching || isLoading,
indicator: <Spin indicator={<LoadingOutlined size={14} spin />} />,
}}
locale={{
emptyText:
isFetching || isLoading ? null : (
<div className="no-filtered-hosts-message-container">
<div className="no-filtered-hosts-message-content">
<img
src="/Icons/emptyState.svg"
alt="thinking-emoji"
className="empty-state-svg"
/>
<Typography.Text className="no-filtered-hosts-message">
This query had no results. Edit your query and try again!
</Typography.Text>
</div>
</div>
),
}}
tableLayout="fixed"
onChange={handleTableChange}
onRow={(record): { onClick: () => void; className: string } => ({
onClick: (): void => handleRowClick(record),
className: 'clickable-row',
})}
expandable={{
expandedRowRender: isGroupedByAttribute ? expandedRowRender : undefined,
expandIcon: expandRowIconRenderer,
expandedRowKeys,
}}
/>
<DeploymentDetails
deployment={selectedDeploymentData}
isModalTimeSelection
onClose={handleCloseDeploymentDetail}
/>
</div>
);
}
export default K8sDeploymentsList;

View File

@@ -0,0 +1,323 @@
import { Color } from '@signozhq/design-tokens';
import { Tag, Tooltip } from 'antd';
import { ColumnType } from 'antd/es/table';
import {
K8sDeploymentsData,
K8sDeploymentsListPayload,
} from 'api/infraMonitoring/getK8sDeploymentsList';
import { Group } from 'lucide-react';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import {
EntityProgressBar,
formatBytes,
ValidateColumnValueWrapper,
} from '../commonUtils';
import { IEntityColumn } from '../utils';
export const defaultAddedColumns: IEntityColumn[] = [
{
label: 'Deployment Name',
value: 'deploymentName',
id: 'deploymentName',
canRemove: false,
},
{
label: 'Namespace Name',
value: 'namespaceName',
id: 'namespaceName',
canRemove: false,
},
{
label: 'Available',
value: 'available',
id: 'available',
canRemove: false,
},
{
label: 'Desired',
value: 'desired',
id: 'desired',
canRemove: false,
},
{
label: 'CPU Request Utilization (% of limit)',
value: 'cpu_request',
id: 'cpu_request',
canRemove: false,
},
{
label: 'CPU Limit Utilization (% of request)',
value: 'cpu_limit',
id: 'cpu_limit',
canRemove: false,
},
{
label: 'CPU Utilization (cores)',
value: 'cpu',
id: 'cpu',
canRemove: false,
},
{
label: 'Memory Request Utilization (% of limit)',
value: 'memory_request',
id: 'memory_request',
canRemove: false,
},
{
label: 'Memory Limit Utilization (% of request)',
value: 'memory_limit',
id: 'memory_limit',
canRemove: false,
},
{
label: 'Memory Utilization (bytes)',
value: 'memory',
id: 'memory',
canRemove: false,
},
];
export interface K8sDeploymentsRowData {
key: string;
deploymentUID: string;
deploymentName: React.ReactNode;
availableReplicas: React.ReactNode;
desiredReplicas: React.ReactNode;
cpu_request: React.ReactNode;
cpu_limit: React.ReactNode;
cpu: React.ReactNode;
memory_request: React.ReactNode;
memory_limit: React.ReactNode;
memory: React.ReactNode;
restarts: React.ReactNode;
clusterName: string;
namespaceName: string;
groupedByMeta?: any;
}
const deploymentGroupColumnConfig = {
title: (
<div className="column-header entity-group-header">
<Group size={14} /> DEPLOYMENT GROUP
</div>
),
dataIndex: 'deploymentGroup',
key: 'deploymentGroup',
ellipsis: true,
width: 150,
align: 'left',
sorter: false,
className: 'column entity-group-header',
};
export const getK8sDeploymentsListQuery = (): K8sDeploymentsListPayload => ({
filters: {
items: [],
op: 'and',
},
orderBy: { columnName: 'cpu', order: 'desc' },
});
const columnsConfig = [
{
title: (
<div className="column-header-left deployment-name-header">
Deployment Name
</div>
),
dataIndex: 'deploymentName',
key: 'deploymentName',
ellipsis: true,
width: 150,
sorter: false,
align: 'left',
},
{
title: (
<div className="column-header-left namespace-name-header">
Namespace Name
</div>
),
dataIndex: 'namespaceName',
key: 'namespaceName',
ellipsis: true,
width: 150,
sorter: false,
align: 'left',
},
{
title: <div className="column-header-left small-col">Available</div>,
dataIndex: 'availableReplicas',
key: 'availableReplicas',
width: 100,
sorter: false,
align: 'left',
},
{
title: <div className="column-header-left small-col">Desired</div>,
dataIndex: 'desiredReplicas',
key: 'desiredReplicas',
width: 80,
sorter: false,
align: 'left',
},
{
title: <div className="column-header-left med-col">CPU Req Usage (%)</div>,
dataIndex: 'cpu_request',
key: 'cpu_request',
width: 80,
sorter: true,
align: 'left',
},
{
title: <div className="column-header-left med-col">CPU Limit Usage (%)</div>,
dataIndex: 'cpu_limit',
key: 'cpu_limit',
width: 50,
sorter: true,
align: 'left',
},
{
title: <div className="column-header- small-col">CPU Usage (cores)</div>,
dataIndex: 'cpu',
key: 'cpu',
width: 80,
sorter: true,
align: 'left',
},
{
title: <div className="column-header-left med-col">Mem Req Usage (%)</div>,
dataIndex: 'memory_request',
key: 'memory_request',
width: 50,
sorter: true,
align: 'left',
},
{
title: <div className="column-header-left med-col">Mem Limit Usage (%)</div>,
dataIndex: 'memory_limit',
key: 'memory_limit',
width: 80,
sorter: true,
align: 'left',
},
{
title: <div className="column-header-left small-col">Mem Usage</div>,
dataIndex: 'memory',
key: 'memory',
width: 80,
sorter: true,
align: 'left',
},
];
export const getK8sDeploymentsListColumns = (
groupBy: IBuilderQuery['groupBy'],
): ColumnType<K8sDeploymentsRowData>[] => {
if (groupBy.length > 0) {
const filteredColumns = [...columnsConfig].filter(
(column) => column.key !== 'deploymentName',
);
filteredColumns.unshift(deploymentGroupColumnConfig);
return filteredColumns as ColumnType<K8sDeploymentsRowData>[];
}
return columnsConfig as ColumnType<K8sDeploymentsRowData>[];
};
const getGroupByEle = (
deployment: K8sDeploymentsData,
groupBy: IBuilderQuery['groupBy'],
): React.ReactNode => {
const groupByValues: string[] = [];
groupBy.forEach((group) => {
groupByValues.push(
deployment.meta[group.key as keyof typeof deployment.meta],
);
});
return (
<div className="pod-group">
{groupByValues.map((value) => (
<Tag key={value} color={Color.BG_SLATE_400} className="pod-group-tag-item">
{value === '' ? '<no-value>' : value}
</Tag>
))}
</div>
);
};
export const formatDataForTable = (
data: K8sDeploymentsData[],
groupBy: IBuilderQuery['groupBy'],
): K8sDeploymentsRowData[] =>
data.map((deployment, index) => ({
key: index.toString(),
deploymentUID: deployment.meta.k8s_deployment_name,
deploymentName: (
<Tooltip title={deployment.meta.k8s_deployment_name}>
{deployment.meta.k8s_deployment_name}
</Tooltip>
),
availableReplicas: (
<ValidateColumnValueWrapper value={deployment.availablePods}>
{deployment.availablePods}
</ValidateColumnValueWrapper>
),
desiredReplicas: (
<ValidateColumnValueWrapper value={deployment.desiredPods}>
{deployment.desiredPods}
</ValidateColumnValueWrapper>
),
restarts: (
<ValidateColumnValueWrapper value={deployment.restarts}>
{deployment.restarts}
</ValidateColumnValueWrapper>
),
cpu: (
<ValidateColumnValueWrapper value={deployment.cpuUsage}>
{deployment.cpuUsage}
</ValidateColumnValueWrapper>
),
cpu_request: (
<ValidateColumnValueWrapper value={deployment.cpuRequest}>
<div className="progress-container">
<EntityProgressBar value={deployment.cpuRequest} type="request" />
</div>
</ValidateColumnValueWrapper>
),
cpu_limit: (
<ValidateColumnValueWrapper value={deployment.cpuLimit}>
<div className="progress-container">
<EntityProgressBar value={deployment.cpuLimit} type="limit" />
</div>
</ValidateColumnValueWrapper>
),
memory: (
<ValidateColumnValueWrapper value={deployment.memoryUsage}>
{formatBytes(deployment.memoryUsage)}
</ValidateColumnValueWrapper>
),
memory_request: (
<ValidateColumnValueWrapper value={deployment.memoryRequest}>
<div className="progress-container">
<EntityProgressBar value={deployment.memoryRequest} type="request" />
</div>
</ValidateColumnValueWrapper>
),
memory_limit: (
<ValidateColumnValueWrapper value={deployment.memoryLimit}>
<div className="progress-container">
<EntityProgressBar value={deployment.memoryLimit} type="limit" />
</div>
</ValidateColumnValueWrapper>
),
clusterName: deployment.meta.k8s_cluster_name,
namespaceName: deployment.meta.k8s_namespace_name,
deploymentGroup: getGroupByEle(deployment, groupBy),
meta: deployment.meta,
...deployment.meta,
groupedByMeta: deployment.meta,
}));

View File

@@ -0,0 +1,367 @@
/* eslint-disable no-nested-ternary */
import './entityEvents.styles.scss';
import { Color } from '@signozhq/design-tokens';
import { Button, Table, TableColumnsType } from 'antd';
import { DEFAULT_ENTITY_VERSION } from 'constants/app';
import { EventContents } from 'container/InfraMonitoringK8s/commonUtils';
import { K8sCategory } from 'container/InfraMonitoringK8s/constants';
import LoadingContainer from 'container/InfraMonitoringK8s/LoadingContainer';
import LogsError from 'container/LogsError/LogsError';
import { ORDERBY_FILTERS } from 'container/QueryBuilder/filters/OrderByFilter/config';
import QueryBuilderSearch from 'container/QueryBuilder/filters/QueryBuilderSearch';
import DateTimeSelectionV2 from 'container/TopNav/DateTimeSelectionV2';
import {
CustomTimeType,
Time,
} from 'container/TopNav/DateTimeSelectionV2/config';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { GetMetricQueryRange } from 'lib/dashboard/getQueryResults';
import { isArray } from 'lodash-es';
import { ChevronDown, ChevronLeft, ChevronRight, Loader2 } from 'lucide-react';
import { useEffect, useMemo, useState } from 'react';
import { useQuery } from 'react-query';
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import { DataSource } from 'types/common/queryBuilder';
import { v4 } from 'uuid';
import {
EntityDetailsEmptyContainer,
getEntityEventsOrLogsQueryPayload,
} from '../utils';
interface EventDataType {
key: string;
timestamp: string;
body: string;
id: string;
attributes_bool?: Record<string, boolean>;
attributes_number?: Record<string, number>;
attributes_string?: Record<string, string>;
resources_string?: Record<string, string>;
scope_name?: string;
scope_string?: Record<string, string>;
scope_version?: string;
severity_number?: number;
severity_text?: string;
span_id?: string;
trace_flags?: number;
trace_id?: string;
severity?: string;
}
interface IEntityEventsProps {
timeRange: {
startTime: number;
endTime: number;
};
handleChangeEventFilters: (filters: IBuilderQuery['filters']) => void;
filters: IBuilderQuery['filters'];
isModalTimeSelection: boolean;
handleTimeChange: (
interval: Time | CustomTimeType,
dateTimeRange?: [number, number],
) => void;
selectedInterval: Time;
category: K8sCategory;
queryKey: string;
}
const EventsPageSize = 10;
export default function Events({
timeRange,
handleChangeEventFilters,
filters,
isModalTimeSelection,
handleTimeChange,
selectedInterval,
category,
queryKey,
}: IEntityEventsProps): JSX.Element {
const { currentQuery } = useQueryBuilder();
const [formattedEntityEvents, setFormattedEntityEvents] = useState<
EventDataType[]
>([]);
const [hasReachedEndOfEvents, setHasReachedEndOfEvents] = useState(false);
const [page, setPage] = useState(1);
const updatedCurrentQuery = useMemo(
() => ({
...currentQuery,
builder: {
...currentQuery.builder,
queryData: [
{
...currentQuery.builder.queryData[0],
dataSource: DataSource.LOGS,
aggregateOperator: 'noop',
aggregateAttribute: {
...currentQuery.builder.queryData[0].aggregateAttribute,
},
filters: {
items: [],
op: 'AND',
},
},
],
},
}),
[currentQuery],
);
const query = updatedCurrentQuery?.builder?.queryData[0] || null;
const queryPayload = useMemo(() => {
const basePayload = getEntityEventsOrLogsQueryPayload(
timeRange.startTime,
timeRange.endTime,
filters,
);
basePayload.query.builder.queryData[0].pageSize = 10;
basePayload.query.builder.queryData[0].orderBy = [
{ columnName: 'timestamp', order: ORDERBY_FILTERS.DESC },
];
return basePayload;
}, [timeRange.startTime, timeRange.endTime, filters]);
const { data: eventsData, isLoading, isFetching, isError } = useQuery({
queryKey: [queryKey, timeRange.startTime, timeRange.endTime, filters],
queryFn: () => GetMetricQueryRange(queryPayload, DEFAULT_ENTITY_VERSION),
enabled: !!queryPayload,
});
const columns: TableColumnsType<EventDataType> = [
{ title: 'Severity', dataIndex: 'severity', key: 'severity', width: 100 },
{
title: 'Timestamp',
dataIndex: 'timestamp',
width: 200,
ellipsis: true,
key: 'timestamp',
},
{ title: 'Body', dataIndex: 'body', key: 'body' },
];
useEffect(() => {
if (eventsData?.payload?.data?.newResult?.data?.result) {
const responsePayload =
eventsData?.payload.data.newResult.data.result[0].list || [];
const formattedData = responsePayload?.map(
(event): EventDataType => ({
timestamp: event.timestamp,
severity: event.data.severity_text,
body: event.data.body,
id: event.data.id,
key: event.data.id,
resources_string: event.data.resources_string,
attributes_string: event.data.attributes_string,
}),
);
setFormattedEntityEvents(formattedData);
if (
!responsePayload ||
(responsePayload &&
isArray(responsePayload) &&
responsePayload.length < EventsPageSize)
) {
setHasReachedEndOfEvents(true);
} else {
setHasReachedEndOfEvents(false);
}
}
}, [eventsData]);
const handleExpandRow = (record: EventDataType): JSX.Element => (
<EventContents
data={{ ...record.attributes_string, ...record.resources_string }}
/>
);
const handlePrev = (): void => {
if (!formattedEntityEvents.length) return;
setPage(page - 1);
const firstEvent = formattedEntityEvents[0];
const newItems = [
...filters.items.filter((item) => item.key?.key !== 'id'),
{
id: v4(),
key: {
key: 'id',
type: '',
dataType: DataTypes.String,
isColumn: true,
},
op: '>',
value: firstEvent.id,
},
];
const newFilters = {
op: 'AND',
items: newItems,
} as IBuilderQuery['filters'];
handleChangeEventFilters(newFilters);
};
const handleNext = (): void => {
if (!formattedEntityEvents.length) return;
setPage(page + 1);
const lastEvent = formattedEntityEvents[formattedEntityEvents.length - 1];
const newItems = [
...filters.items.filter((item) => item.key?.key !== 'id'),
{
id: v4(),
key: {
key: 'id',
type: '',
dataType: DataTypes.String,
isColumn: true,
},
op: '<',
value: lastEvent.id,
},
];
const newFilters = {
op: 'AND',
items: newItems,
} as IBuilderQuery['filters'];
handleChangeEventFilters(newFilters);
};
const handleExpandRowIcon = ({
expanded,
onExpand,
record,
}: {
expanded: boolean;
onExpand: (
record: EventDataType,
e: React.MouseEvent<HTMLElement, MouseEvent>,
) => void;
record: EventDataType;
}): JSX.Element =>
expanded ? (
<ChevronDown
className="periscope-btn-icon"
size={14}
onClick={(e): void =>
onExpand(
record,
(e as unknown) as React.MouseEvent<HTMLElement, MouseEvent>,
)
}
/>
) : (
<ChevronRight
className="periscope-btn-icon"
size={14}
// eslint-disable-next-line sonarjs/no-identical-functions
onClick={(e): void =>
onExpand(
record,
(e as unknown) as React.MouseEvent<HTMLElement, MouseEvent>,
)
}
/>
);
return (
<div className="entity-events-container">
<div className="entity-events-header">
<div className="filter-section">
{query && (
<QueryBuilderSearch
query={query}
onChange={handleChangeEventFilters}
disableNavigationShortcuts
/>
)}
</div>
<div className="datetime-section">
<DateTimeSelectionV2
showAutoRefresh={false}
showRefreshText={false}
hideShareModal
isModalTimeSelection={isModalTimeSelection}
onTimeChange={handleTimeChange}
defaultRelativeTime="5m"
modalSelectedInterval={selectedInterval}
/>
</div>
</div>
{isLoading && <LoadingContainer />}
{!isLoading && !isError && formattedEntityEvents.length === 0 && (
<EntityDetailsEmptyContainer category={category} view="events" />
)}
{isError && !isLoading && <LogsError />}
{!isLoading && !isError && formattedEntityEvents.length > 0 && (
<div className="entity-events-list-container">
<div className="entity-events-list-card">
<Table<EventDataType>
loading={isLoading && page > 1}
columns={columns}
expandable={{
expandedRowRender: handleExpandRow,
rowExpandable: (record): boolean => record.body !== 'Not Expandable',
expandIcon: handleExpandRowIcon,
}}
dataSource={formattedEntityEvents}
pagination={false}
rowKey={(record): string => record.id}
/>
</div>
</div>
)}
{!isError && formattedEntityEvents.length > 0 && (
<div className="entity-events-footer">
<Button
className="entity-events-footer-button periscope-btn ghost"
type="link"
onClick={handlePrev}
disabled={page === 1 || isFetching || isLoading}
>
{!isFetching && <ChevronLeft size={14} />}
Prev
</Button>
<Button
className="entity-events-footer-button periscope-btn ghost"
type="link"
onClick={handleNext}
disabled={hasReachedEndOfEvents || isFetching || isLoading}
>
Next
{!isFetching && <ChevronRight size={14} />}
</Button>
{(isFetching || isLoading) && (
<Loader2 className="animate-spin" size={16} color={Color.BG_ROBIN_500} />
)}
</div>
)}
</div>
);
}

View File

@@ -0,0 +1,323 @@
// Events
.entity-events-container {
margin-top: 1rem;
.filter-section {
flex: 1;
.ant-select-selector {
border-radius: 2px;
border: 1px solid var(--bg-slate-400) !important;
background-color: var(--bg-ink-300) !important;
input {
font-size: 12px;
}
.ant-tag .ant-typography {
font-size: 12px;
}
}
}
.entity-events-header {
display: flex;
justify-content: space-between;
gap: 8px;
padding: 12px;
border-radius: 3px;
border: 1px solid var(--bg-slate-500);
}
.entity-events {
margin-top: 1rem;
.virtuoso-list {
overflow-y: hidden !important;
&::-webkit-scrollbar {
width: 0.3rem;
height: 0.3rem;
}
&::-webkit-scrollbar-track {
background: transparent;
}
&::-webkit-scrollbar-thumb {
background: var(--bg-slate-300);
}
&::-webkit-scrollbar-thumb:hover {
background: var(--bg-slate-200);
}
.ant-row {
width: fit-content;
}
}
.skeleton-container {
height: 100%;
padding: 16px;
}
}
.ant-table {
.ant-table-thead > tr > th {
padding: 12px;
font-weight: 500;
font-size: 12px;
line-height: 18px;
background: rgb(18, 19, 23);
border-bottom: none;
color: var(--Vanilla-400, #c0c1c3);
font-family: Inter;
font-size: 11px;
font-style: normal;
font-weight: 600;
line-height: 18px; /* 163.636% */
letter-spacing: 0.44px;
text-transform: uppercase;
&::before {
background-color: transparent;
}
}
.ant-table-thead > tr > th:has(.entityname-column-header) {
background: var(--bg-ink-400);
}
.ant-table-cell {
padding: 12px;
font-size: 13px;
line-height: 20px;
color: var(--bg-vanilla-100);
background: rgb(18, 19, 23);
border-bottom: none;
}
.ant-table-cell:has(.entityname-column-value) {
background: var(--bg-ink-400);
}
.entityname-column-value {
color: var(--bg-vanilla-100);
font-family: 'Geist Mono';
font-style: normal;
font-weight: 600;
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
}
.status-cell {
.active-tag {
color: var(--bg-forest-500);
padding: 4px 8px;
border-radius: 4px;
font-size: 12px;
font-weight: 500;
}
}
.progress-container {
.ant-progress-bg {
height: 8px !important;
border-radius: 4px;
}
}
.ant-table-tbody > tr:hover > td {
background: rgba(255, 255, 255, 0.04);
}
.ant-table-cell:first-child {
text-align: justify;
}
.ant-table-cell:nth-child(2) {
padding-left: 16px;
padding-right: 16px;
}
.ant-table-cell:nth-child(n + 3) {
padding-right: 24px;
}
.column-header-right {
text-align: right;
}
.ant-table-tbody > tr > td {
border-bottom: none;
}
.ant-table-thead
> tr
> th:not(:last-child):not(.ant-table-selection-column):not(.ant-table-row-expand-icon-cell):not([colspan])::before {
background-color: transparent;
}
.ant-empty-normal {
visibility: hidden;
}
}
.ant-pagination {
position: fixed;
bottom: 0;
width: calc(100% - 64px);
background: rgb(18, 19, 23);
padding: 16px;
margin: 0;
// this is to offset intercom icon till we improve the design
padding-right: 72px;
.ant-pagination-item {
border-radius: 4px;
&-active {
background: var(--bg-robin-500);
border-color: var(--bg-robin-500);
a {
color: var(--bg-ink-500) !important;
}
}
}
}
}
.entity-events-list-container {
flex: 1;
height: calc(100vh - 300px) !important;
display: flex;
height: 100%;
.raw-log-content {
width: 100%;
text-wrap: inherit;
word-wrap: break-word;
}
}
.entity-events-list-card {
width: 100%;
margin-top: 12px;
.ant-table-wrapper {
height: 100%;
overflow-y: auto;
&::-webkit-scrollbar {
width: 0.3rem;
height: 0.3rem;
}
&::-webkit-scrollbar-track {
background: transparent;
}
&::-webkit-scrollbar-thumb {
background: var(--bg-slate-300);
}
&::-webkit-scrollbar-thumb:hover {
background: var(--bg-slate-200);
}
.ant-row {
width: fit-content;
}
}
.ant-card-body {
padding: 0;
height: 100%;
width: 100%;
}
}
.logs-loading-skeleton {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 8px;
padding: 8px 0;
.ant-skeleton-input-sm {
height: 18px;
}
}
.no-logs-found {
height: 50vh;
width: 100%;
display: flex;
justify-content: center;
align-items: center;
padding: 24px;
box-sizing: border-box;
.ant-typography {
display: flex;
align-items: center;
gap: 16px;
}
}
.entity-events-footer {
display: flex;
justify-content: flex-end;
align-items: center;
gap: 8px;
padding: 8px 16px 8px 16px;
position: absolute;
bottom: 0;
right: 0;
width: 100%;
border-radius: 0px 0px 3px 3px;
border-top: 1px solid var(--bg-slate-500);
background: var(--bg-ink-300);
box-sizing: border-box;
.ant-btn {
color: var(--bg-vanilla-100);
font-size: 12px;
font-style: normal;
font-weight: 400;
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
}
.periscope-btn {
&.gentity {
border: none;
background: transparent;
}
}
}
.lightMode {
.filter-section {
border-top: 1px solid var(--bg-vanilla-300);
border-bottom: 1px solid var(--bg-vanilla-300);
.ant-select-selector {
border-color: var(--bg-vanilla-300) !important;
background-color: var(--bg-vanilla-100) !important;
color: var(--bg-ink-200);
}
}
}
.periscope-btn-icon {
cursor: pointer;
}

View File

@@ -0,0 +1,3 @@
import Events from './EntityEvents';
export default Events;

View File

@@ -0,0 +1,235 @@
/* eslint-disable no-nested-ternary */
import './entityLogs.styles.scss';
import { Card } from 'antd';
import RawLogView from 'components/Logs/RawLogView';
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
import { DEFAULT_ENTITY_VERSION } from 'constants/app';
import { K8sCategory } from 'container/InfraMonitoringK8s/constants';
import LogsError from 'container/LogsError/LogsError';
import { LogsLoading } from 'container/LogsLoading/LogsLoading';
import { FontSize } from 'container/OptionsMenu/types';
import { ORDERBY_FILTERS } from 'container/QueryBuilder/filters/OrderByFilter/config';
import { GetMetricQueryRange } from 'lib/dashboard/getQueryResults';
import { isEqual } from 'lodash-es';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useQuery } from 'react-query';
import { Virtuoso } from 'react-virtuoso';
import { ILog } from 'types/api/logs/log';
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
import {
IBuilderQuery,
TagFilterItem,
} from 'types/api/queryBuilder/queryBuilderData';
import { v4 } from 'uuid';
import {
EntityDetailsEmptyContainer,
getEntityEventsOrLogsQueryPayload,
} from '../utils';
interface Props {
timeRange: {
startTime: number;
endTime: number;
};
handleChangeLogFilters: (filters: IBuilderQuery['filters']) => void;
filters: IBuilderQuery['filters'];
queryKey: string;
category: K8sCategory;
queryKeyFilters: Array<string>;
}
function EntityLogs({
timeRange,
handleChangeLogFilters,
filters,
queryKey,
category,
queryKeyFilters,
}: Props): JSX.Element {
const [logs, setLogs] = useState<ILog[]>([]);
const [hasReachedEndOfLogs, setHasReachedEndOfLogs] = useState(false);
const [restFilters, setRestFilters] = useState<TagFilterItem[]>([]);
const [resetLogsList, setResetLogsList] = useState<boolean>(false);
useEffect(() => {
const newRestFilters = filters.items.filter(
(item) =>
item.key?.key !== 'id' && !queryKeyFilters.includes(item.key?.key ?? ''),
);
const areFiltersSame = isEqual(restFilters, newRestFilters);
if (!areFiltersSame) {
setResetLogsList(true);
}
setRestFilters(newRestFilters);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [filters]);
const queryPayload = useMemo(() => {
const basePayload = getEntityEventsOrLogsQueryPayload(
timeRange.startTime,
timeRange.endTime,
filters,
);
basePayload.query.builder.queryData[0].pageSize = 100;
basePayload.query.builder.queryData[0].orderBy = [
{ columnName: 'timestamp', order: ORDERBY_FILTERS.DESC },
];
return basePayload;
}, [timeRange.startTime, timeRange.endTime, filters]);
const [isPaginating, setIsPaginating] = useState(false);
const { data, isLoading, isFetching, isError } = useQuery({
queryKey: [queryKey, timeRange.startTime, timeRange.endTime, filters],
queryFn: () => GetMetricQueryRange(queryPayload, DEFAULT_ENTITY_VERSION),
enabled: !!queryPayload,
keepPreviousData: isPaginating,
});
useEffect(() => {
if (data?.payload?.data?.newResult?.data?.result) {
const currentData = data.payload.data.newResult.data.result;
if (resetLogsList) {
const currentLogs: ILog[] =
currentData[0].list?.map((item) => ({
...item.data,
timestamp: item.timestamp,
})) || [];
setLogs(currentLogs);
setResetLogsList(false);
}
if (currentData.length > 0 && currentData[0].list) {
const currentLogs: ILog[] =
currentData[0].list.map((item) => ({
...item.data,
timestamp: item.timestamp,
})) || [];
setLogs((prev) => [...prev, ...currentLogs]);
} else {
setHasReachedEndOfLogs(true);
}
}
}, [data, restFilters, isPaginating, resetLogsList]);
const getItemContent = useCallback(
(_: number, logToRender: ILog): JSX.Element => (
<RawLogView
isReadOnly
isTextOverflowEllipsisDisabled
key={logToRender.id}
data={logToRender}
linesPerRow={5}
fontSize={FontSize.MEDIUM}
selectedFields={[
{
dataType: 'string',
type: '',
name: 'body',
},
{
dataType: 'string',
type: '',
name: 'timestamp',
},
]}
/>
),
[],
);
const loadMoreLogs = useCallback(() => {
if (!logs.length) return;
setIsPaginating(true);
const lastLog = logs[logs.length - 1];
const newItems = [
...filters.items.filter((item) => item.key?.key !== 'id'),
{
id: v4(),
key: {
key: 'id',
type: '',
dataType: DataTypes.String,
isColumn: true,
},
op: '<',
value: lastLog.id,
},
];
const newFilters = {
op: 'AND',
items: newItems,
} as IBuilderQuery['filters'];
handleChangeLogFilters(newFilters);
}, [logs, filters, handleChangeLogFilters]);
useEffect(() => {
setIsPaginating(false);
}, [data]);
const renderFooter = useCallback(
(): JSX.Element | null => (
// eslint-disable-next-line react/jsx-no-useless-fragment
<>
{isFetching ? (
<div className="logs-loading-skeleton"> Loading more logs ... </div>
) : hasReachedEndOfLogs ? (
<div className="logs-loading-skeleton"> *** End *** </div>
) : null}
</>
),
[isFetching, hasReachedEndOfLogs],
);
const renderContent = useMemo(
() => (
<Card bordered={false} className="entity-logs-list-card">
<OverlayScrollbar isVirtuoso>
<Virtuoso
className="entity-logs-virtuoso"
key="entity-logs-virtuoso"
data={logs}
endReached={loadMoreLogs}
totalCount={logs.length}
itemContent={getItemContent}
overscan={200}
components={{
Footer: renderFooter,
}}
/>
</OverlayScrollbar>
</Card>
),
[logs, loadMoreLogs, getItemContent, renderFooter],
);
return (
<div className="entity-logs">
{isLoading && <LogsLoading />}
{!isLoading && !isError && logs.length === 0 && (
<EntityDetailsEmptyContainer category={category} view="logs" />
)}
{isError && !isLoading && <LogsError />}
{!isLoading && !isError && logs.length > 0 && (
<div className="entity-logs-list-container">{renderContent}</div>
)}
</div>
);
}
export default EntityLogs;

View File

@@ -0,0 +1,109 @@
import './entityLogs.styles.scss';
import { K8sCategory } from 'container/InfraMonitoringK8s/constants';
import QueryBuilderSearch from 'container/QueryBuilder/filters/QueryBuilderSearch';
import DateTimeSelectionV2 from 'container/TopNav/DateTimeSelectionV2';
import {
CustomTimeType,
Time,
} from 'container/TopNav/DateTimeSelectionV2/config';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useMemo } from 'react';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import { DataSource } from 'types/common/queryBuilder';
import EntityLogs from './EntityLogs';
interface Props {
timeRange: {
startTime: number;
endTime: number;
};
isModalTimeSelection: boolean;
handleTimeChange: (
interval: Time | CustomTimeType,
dateTimeRange?: [number, number],
) => void;
handleChangeLogFilters: (value: IBuilderQuery['filters']) => void;
logFilters: IBuilderQuery['filters'];
selectedInterval: Time;
queryKey: string;
category: K8sCategory;
queryKeyFilters: Array<string>;
}
function EntityLogsDetailedView({
timeRange,
isModalTimeSelection,
handleTimeChange,
handleChangeLogFilters,
logFilters,
selectedInterval,
queryKey,
category,
queryKeyFilters,
}: Props): JSX.Element {
const { currentQuery } = useQueryBuilder();
const updatedCurrentQuery = useMemo(
() => ({
...currentQuery,
builder: {
...currentQuery.builder,
queryData: [
{
...currentQuery.builder.queryData[0],
dataSource: DataSource.LOGS,
aggregateOperator: 'noop',
aggregateAttribute: {
...currentQuery.builder.queryData[0].aggregateAttribute,
},
filters: {
items: [],
op: 'AND',
},
},
],
},
}),
[currentQuery],
);
const query = updatedCurrentQuery?.builder?.queryData[0] || null;
return (
<div className="entity-logs-container">
<div className="entity-logs-header">
<div className="filter-section">
{query && (
<QueryBuilderSearch
query={query}
onChange={handleChangeLogFilters}
disableNavigationShortcuts
/>
)}
</div>
<div className="datetime-section">
<DateTimeSelectionV2
showAutoRefresh={false}
showRefreshText={false}
hideShareModal
isModalTimeSelection={isModalTimeSelection}
onTimeChange={handleTimeChange}
defaultRelativeTime="5m"
modalSelectedInterval={selectedInterval}
/>
</div>
</div>
<EntityLogs
timeRange={timeRange}
handleChangeLogFilters={handleChangeLogFilters}
filters={logFilters}
queryKey={queryKey}
category={category}
queryKeyFilters={queryKeyFilters}
/>
</div>
);
}
export default EntityLogsDetailedView;

View File

@@ -0,0 +1,133 @@
.entity-logs-container {
margin-top: 1rem;
.filter-section {
flex: 1;
.ant-select-selector {
border-radius: 2px;
border: 1px solid var(--bg-slate-400) !important;
background-color: var(--bg-ink-300) !important;
input {
font-size: 12px;
}
.ant-tag .ant-typography {
font-size: 12px;
}
}
}
.entity-logs-header {
display: flex;
justify-content: space-between;
gap: 8px;
padding: 12px;
border-radius: 3px;
border: 1px solid var(--bg-slate-500);
}
.entity-logs {
margin-top: 1rem;
.virtuoso-list {
overflow-y: hidden !important;
&::-webkit-scrollbar {
width: 0.3rem;
height: 0.3rem;
}
&::-webkit-scrollbar-track {
background: transparent;
}
&::-webkit-scrollbar-thumb {
background: var(--bg-slate-300);
}
&::-webkit-scrollbar-thumb:hover {
background: var(--bg-slate-200);
}
.ant-row {
width: fit-content;
}
}
.skeleton-container {
height: 100%;
padding: 16px;
}
}
}
.entity-logs-list-container {
flex: 1;
height: calc(100vh - 272px) !important;
display: flex;
height: 100%;
.raw-log-content {
width: 100%;
text-wrap: inherit;
word-wrap: break-word;
}
}
.entity-logs-list-card {
width: 100%;
margin-top: 12px;
.ant-card-body {
padding: 0;
height: 100%;
width: 100%;
}
}
.logs-loading-skeleton {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 8px;
padding: 8px 0;
.ant-skeleton-input-sm {
height: 18px;
}
}
.no-logs-found {
height: 50vh;
width: 100%;
display: flex;
justify-content: center;
align-items: center;
padding: 24px;
box-sizing: border-box;
.ant-typography {
display: flex;
align-items: center;
gap: 16px;
}
}
.lightMode {
.filter-section {
border-top: 1px solid var(--bg-vanilla-300);
border-bottom: 1px solid var(--bg-vanilla-300);
.ant-select-selector {
border-color: var(--bg-vanilla-300) !important;
background-color: var(--bg-vanilla-100) !important;
color: var(--bg-ink-200);
}
}
}

View File

@@ -0,0 +1,3 @@
import EntityLogs from './EntityLogsDetailedView';
export default EntityLogs;

View File

@@ -0,0 +1,189 @@
import './entityMetrics.styles.scss';
import { Card, Col, Row, Skeleton, Typography } from 'antd';
import cx from 'classnames';
import Uplot from 'components/Uplot';
import { ENTITY_VERSION_V4 } from 'constants/app';
import { PANEL_TYPES } from 'constants/queryBuilder';
import {
getMetricsTableData,
MetricsTable,
} from 'container/InfraMonitoringK8s/commonUtils';
import { K8sCategory } from 'container/InfraMonitoringK8s/constants';
import DateTimeSelectionV2 from 'container/TopNav/DateTimeSelectionV2';
import {
CustomTimeType,
Time,
} from 'container/TopNav/DateTimeSelectionV2/config';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { useResizeObserver } from 'hooks/useDimensions';
import {
GetMetricQueryRange,
GetQueryResultsProps,
} from 'lib/dashboard/getQueryResults';
import { getUPlotChartOptions } from 'lib/uPlotLib/getUplotChartOptions';
import { getUPlotChartData } from 'lib/uPlotLib/utils/getUplotChartData';
import { useMemo, useRef } from 'react';
import { useQueries, UseQueryResult } from 'react-query';
import { SuccessResponse } from 'types/api';
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
import { Options } from 'uplot';
interface EntityMetricsProps<T> {
timeRange: {
startTime: number;
endTime: number;
};
isModalTimeSelection: boolean;
handleTimeChange: (
interval: Time | CustomTimeType,
dateTimeRange?: [number, number],
) => void;
selectedInterval: Time;
entity: T;
entityWidgetInfo: {
title: string;
yAxisUnit: string;
}[];
getEntityQueryPayload: (
node: T,
start: number,
end: number,
) => GetQueryResultsProps[];
queryKey: string;
category: K8sCategory;
}
function EntityMetrics<T>({
selectedInterval,
entity,
timeRange,
handleTimeChange,
isModalTimeSelection,
entityWidgetInfo,
getEntityQueryPayload,
queryKey,
category,
}: EntityMetricsProps<T>): JSX.Element {
const queryPayloads = useMemo(
() => getEntityQueryPayload(entity, timeRange.startTime, timeRange.endTime),
[getEntityQueryPayload, entity, timeRange.startTime, timeRange.endTime],
);
const queries = useQueries(
queryPayloads.map((payload) => ({
queryKey: [queryKey, payload, ENTITY_VERSION_V4, category],
queryFn: (): Promise<SuccessResponse<MetricRangePayloadProps>> =>
GetMetricQueryRange(payload, ENTITY_VERSION_V4),
enabled: !!payload,
})),
);
const isDarkMode = useIsDarkMode();
const graphRef = useRef<HTMLDivElement>(null);
const dimensions = useResizeObserver(graphRef);
const chartData = useMemo(
() =>
queries.map(({ data }) => {
const panelType = (data?.params as any)?.compositeQuery?.panelType;
return panelType === PANEL_TYPES.TABLE
? getMetricsTableData(data)
: getUPlotChartData(data?.payload);
}),
[queries],
);
const options = useMemo(
() =>
queries.map(({ data }, idx) => {
const panelType = (data?.params as any)?.compositeQuery?.panelType;
if (panelType === PANEL_TYPES.TABLE) {
return null;
}
return getUPlotChartOptions({
apiResponse: data?.payload,
isDarkMode,
dimensions,
yAxisUnit: entityWidgetInfo[idx].yAxisUnit,
softMax: null,
softMin: null,
minTimeScale: timeRange.startTime,
maxTimeScale: timeRange.endTime,
});
}),
[
queries,
isDarkMode,
dimensions,
entityWidgetInfo,
timeRange.startTime,
timeRange.endTime,
],
);
const renderCardContent = (
query: UseQueryResult<SuccessResponse<MetricRangePayloadProps>, unknown>,
idx: number,
): JSX.Element => {
if (query.isLoading) {
return <Skeleton />;
}
if (query.error) {
const errorMessage =
(query.error as Error)?.message || 'Something went wrong';
return <div>{errorMessage}</div>;
}
const { panelType } = (query.data?.params as any).compositeQuery;
return (
<div
className={cx('chart-container', {
'no-data-container':
!query.isLoading && !query?.data?.payload?.data?.result?.length,
})}
>
{panelType === PANEL_TYPES.TABLE ? (
<MetricsTable
rows={chartData[idx][0].rows}
columns={chartData[idx][0].columns}
/>
) : (
<Uplot options={options[idx] as Options} data={chartData[idx]} />
)}
</div>
);
};
return (
<>
<div className="metrics-header">
<div className="metrics-datetime-section">
<DateTimeSelectionV2
showAutoRefresh={false}
showRefreshText={false}
hideShareModal
onTimeChange={handleTimeChange}
defaultRelativeTime="5m"
isModalTimeSelection={isModalTimeSelection}
modalSelectedInterval={selectedInterval}
/>
</div>
</div>
<Row gutter={24} className="entity-metrics-container">
{queries.map((query, idx) => (
<Col span={12} key={entityWidgetInfo[idx].title}>
<Typography.Text>{entityWidgetInfo[idx].title}</Typography.Text>
<Card bordered className="entity-metrics-card" ref={graphRef}>
{renderCardContent(query, idx)}
</Card>
</Col>
))}
</Row>
</>
);
}
export default EntityMetrics;

View File

@@ -0,0 +1,39 @@
// Metrics
.entity-metrics-container {
margin-top: 1rem;
}
.metrics-header {
display: flex;
justify-content: flex-end;
margin-top: 1rem;
gap: 8px;
padding: 12px;
border-radius: 3px;
border: 1px solid var(--bg-slate-500);
}
.entity-metrics-card {
margin: 8px 0 1rem 0;
height: 300px;
padding: 10px;
border: 1px solid var(--bg-slate-500);
.ant-card-body {
padding: 0;
}
.chart-container {
width: 100%;
height: 100%;
}
.no-data-container {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
}

View File

@@ -0,0 +1,3 @@
import EntityMetrics from './EntityMetrics';
export default EntityMetrics;

View File

@@ -0,0 +1,204 @@
import './entityTraces.styles.scss';
import { getListColumns } from 'components/HostMetricsDetail/HostMetricTraces/utils';
import { ResizeTable } from 'components/ResizeTable';
import { DEFAULT_ENTITY_VERSION } from 'constants/app';
import { QueryParams } from 'constants/query';
import EmptyLogsSearch from 'container/EmptyLogsSearch/EmptyLogsSearch';
import NoLogs from 'container/NoLogs/NoLogs';
import QueryBuilderSearch from 'container/QueryBuilder/filters/QueryBuilderSearch';
import { ErrorText } from 'container/TimeSeriesView/styles';
import DateTimeSelectionV2 from 'container/TopNav/DateTimeSelectionV2';
import {
CustomTimeType,
Time,
} from 'container/TopNav/DateTimeSelectionV2/config';
import TraceExplorerControls from 'container/TracesExplorer/Controls';
import { PER_PAGE_OPTIONS } from 'container/TracesExplorer/ListView/configs';
import { TracesLoading } from 'container/TracesExplorer/TraceLoading/TraceLoading';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { Pagination } from 'hooks/queryPagination';
import useUrlQueryData from 'hooks/useUrlQueryData';
import { GetMetricQueryRange } from 'lib/dashboard/getQueryResults';
import { useEffect, useMemo, useState } from 'react';
import { useQuery } from 'react-query';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import { DataSource } from 'types/common/queryBuilder';
import {
getEntityTracesQueryPayload,
selectedEntityTracesColumns,
} from '../utils';
interface Props {
timeRange: {
startTime: number;
endTime: number;
};
isModalTimeSelection: boolean;
handleTimeChange: (
interval: Time | CustomTimeType,
dateTimeRange?: [number, number],
) => void;
handleChangeTracesFilters: (value: IBuilderQuery['filters']) => void;
tracesFilters: IBuilderQuery['filters'];
selectedInterval: Time;
queryKey: string;
}
function EntityTraces({
timeRange,
isModalTimeSelection,
handleTimeChange,
handleChangeTracesFilters,
tracesFilters,
selectedInterval,
queryKey,
}: Props): JSX.Element {
const [traces, setTraces] = useState<any[]>([]);
const [offset] = useState<number>(0);
const { currentQuery } = useQueryBuilder();
const updatedCurrentQuery = useMemo(
() => ({
...currentQuery,
builder: {
...currentQuery.builder,
queryData: [
{
...currentQuery.builder.queryData[0],
dataSource: DataSource.TRACES,
aggregateOperator: 'noop',
aggregateAttribute: {
...currentQuery.builder.queryData[0].aggregateAttribute,
},
filters: {
items: [],
op: 'AND',
},
},
],
},
}),
[currentQuery],
);
const query = updatedCurrentQuery?.builder?.queryData[0] || null;
const { queryData: paginationQueryData } = useUrlQueryData<Pagination>(
QueryParams.pagination,
);
const queryPayload = useMemo(
() =>
getEntityTracesQueryPayload(
timeRange.startTime,
timeRange.endTime,
paginationQueryData?.offset || offset,
tracesFilters,
),
[
timeRange.startTime,
timeRange.endTime,
offset,
tracesFilters,
paginationQueryData,
],
);
const { data, isLoading, isFetching, isError } = useQuery({
queryKey: [
queryKey,
timeRange.startTime,
timeRange.endTime,
offset,
tracesFilters,
DEFAULT_ENTITY_VERSION,
paginationQueryData,
],
queryFn: () => GetMetricQueryRange(queryPayload, DEFAULT_ENTITY_VERSION),
enabled: !!queryPayload,
});
const traceListColumns = getListColumns(selectedEntityTracesColumns);
useEffect(() => {
if (data?.payload?.data?.newResult?.data?.result) {
const currentData = data.payload.data.newResult.data.result;
if (currentData.length > 0 && currentData[0].list) {
if (offset === 0) {
setTraces(currentData[0].list ?? []);
} else {
setTraces((prev) => [...prev, ...(currentData[0].list ?? [])]);
}
}
}
}, [data, offset]);
const isDataEmpty =
!isLoading && !isFetching && !isError && traces.length === 0;
const hasAdditionalFilters = tracesFilters.items.length > 1;
const totalCount =
data?.payload?.data?.newResult?.data?.result?.[0]?.list?.length || 0;
return (
<div className="entity-metric-traces">
<div className="entity-metric-traces-header">
<div className="filter-section">
{query && (
<QueryBuilderSearch
query={query}
onChange={handleChangeTracesFilters}
disableNavigationShortcuts
/>
)}
</div>
<div className="datetime-section">
<DateTimeSelectionV2
showAutoRefresh={false}
showRefreshText={false}
hideShareModal
isModalTimeSelection={isModalTimeSelection}
onTimeChange={handleTimeChange}
defaultRelativeTime="5m"
modalSelectedInterval={selectedInterval}
/>
</div>
</div>
{isError && <ErrorText>{data?.error || 'Something went wrong'}</ErrorText>}
{isLoading && traces.length === 0 && <TracesLoading />}
{isDataEmpty && !hasAdditionalFilters && (
<NoLogs dataSource={DataSource.TRACES} />
)}
{isDataEmpty && hasAdditionalFilters && (
<EmptyLogsSearch dataSource={DataSource.TRACES} panelType="LIST" />
)}
{!isError && traces.length > 0 && (
<div className="entity-traces-table">
<TraceExplorerControls
isLoading={isFetching}
totalCount={totalCount}
perPageOptions={PER_PAGE_OPTIONS}
showSizeChanger={false}
/>
<ResizeTable
tableLayout="fixed"
pagination={false}
scroll={{ x: true }}
loading={isFetching}
dataSource={traces}
columns={traceListColumns}
/>
</div>
)}
</div>
);
}
export default EntityTraces;

View File

@@ -0,0 +1,194 @@
// Traces
.entity-metric-traces {
margin-top: 1rem;
.entity-metric-traces-header {
display: flex;
justify-content: space-between;
margin-bottom: 1rem;
gap: 8px;
padding: 12px;
border-radius: 3px;
border: 1px solid var(--bg-slate-500);
.filter-section {
flex: 1;
.ant-select-selector {
border-radius: 2px;
border: 1px solid var(--bg-slate-400) !important;
background-color: var(--bg-ink-300) !important;
input {
font-size: 12px;
}
.ant-tag .ant-typography {
font-size: 12px;
}
}
}
}
.entity-metric-traces-table {
.ant-table-content {
overflow: hidden !important;
}
.ant-table {
border-radius: 3px;
border: 1px solid var(--bg-slate-500);
.ant-table-thead > tr > th {
padding: 12px;
font-weight: 500;
font-size: 12px;
line-height: 18px;
background: rgba(171, 189, 255, 0.01);
border-bottom: none;
color: var(--Vanilla-400, #c0c1c3);
font-family: Inter;
font-size: 11px;
font-style: normal;
font-weight: 600;
line-height: 18px; /* 163.636% */
letter-spacing: 0.44px;
text-transform: uppercase;
&::before {
background-color: transparent;
}
}
.ant-table-thead > tr > th:has(.entityname-column-header) {
background: var(--bg-ink-400);
}
.ant-table-cell {
padding: 12px;
font-size: 13px;
line-height: 20px;
color: var(--bg-vanilla-100);
background: rgba(171, 189, 255, 0.01);
}
.ant-table-cell:has(.entityname-column-value) {
background: var(--bg-ink-400);
}
.entityname-column-value {
color: var(--bg-vanilla-100);
font-family: 'Geist Mono';
font-style: normal;
font-weight: 600;
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
}
.status-cell {
.active-tag {
color: var(--bg-forest-500);
padding: 4px 8px;
border-radius: 4px;
font-size: 12px;
font-weight: 500;
}
}
.progress-container {
.ant-progress-bg {
height: 8px !important;
border-radius: 4px;
}
}
.ant-table-tbody > tr:hover > td {
background: rgba(255, 255, 255, 0.04);
}
.ant-table-cell:first-child {
text-align: justify;
}
.ant-table-cell:nth-child(2) {
padding-left: 16px;
padding-right: 16px;
}
.ant-table-cell:nth-child(n + 3) {
padding-right: 24px;
}
.column-header-right {
text-align: right;
}
.ant-table-tbody > tr > td {
border-bottom: none;
}
.ant-table-thead
> tr
> th:not(:last-child):not(.ant-table-selection-column):not(.ant-table-row-expand-icon-cell):not([colspan])::before {
background-color: transparent;
}
.ant-empty-normal {
visibility: hidden;
}
}
.ant-table-container::after {
content: none;
}
}
}
.lightMode {
.entity-metric-traces-header {
.filter-section {
border-top: 1px solid var(--bg-vanilla-300);
border-bottom: 1px solid var(--bg-vanilla-300);
.ant-select-selector {
border-color: var(--bg-vanilla-300) !important;
background-color: var(--bg-vanilla-100) !important;
color: var(--bg-ink-200);
}
}
}
.entity-metric-traces-table {
.ant-table {
border-radius: 3px;
border: 1px solid var(--bg-vanilla-300);
.ant-table-thead > tr > th {
background: var(--bg-vanilla-100);
color: var(--text-ink-300);
}
.ant-table-thead > tr > th:has(.entityname-column-header) {
background: var(--bg-vanilla-100);
}
.ant-table-cell {
background: var(--bg-vanilla-100);
color: var(--bg-ink-500);
}
.ant-table-cell:has(.entityname-column-value) {
background: var(--bg-vanilla-100);
}
.entityname-column-value {
color: var(--bg-ink-300);
}
.ant-table-tbody > tr:hover > td {
background: rgba(0, 0, 0, 0.04);
}
}
}
}

View File

@@ -0,0 +1,3 @@
import EntityTraces from './EntityTraces';
export default EntityTraces;

View File

@@ -0,0 +1,576 @@
// Entity Details
.entity-detail-drawer {
border-left: 1px solid var(--bg-slate-500);
background: var(--bg-ink-400);
box-shadow: -4px 10px 16px 2px rgba(0, 0, 0, 0.2);
.ant-drawer-header {
padding: 8px 16px;
border-bottom: none;
align-items: stretch;
border-bottom: 1px solid var(--bg-slate-500);
background: var(--bg-ink-400);
}
.ant-drawer-close {
margin-inline-end: 0px;
}
.ant-drawer-body {
display: flex;
flex-direction: column;
padding: 16px;
}
.title {
color: var(--text-vanilla-400);
font-family: 'Geist Mono';
font-size: 14px;
font-style: normal;
font-weight: 500;
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
}
.radio-button {
display: flex;
align-items: center;
justify-content: center;
padding-top: var(--padding-1);
border: 1px solid var(--bg-slate-400);
background: var(--bg-ink-300);
box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.1);
}
.entity-detail-drawer__entity {
.entity-details-grid {
.labels-row,
.values-row {
display: grid;
grid-template-columns: 1fr 1.5fr 1.5fr 1.5fr;
gap: 30px;
align-items: center;
}
.labels-row {
margin-bottom: 8px;
}
.entity-details-metadata-label {
color: var(--text-vanilla-400);
font-family: Inter;
font-size: 11px;
font-style: normal;
font-weight: 500;
line-height: 18px; /* 163.636% */
letter-spacing: 0.44px;
text-transform: uppercase;
}
.entity-details-metadata-value {
color: var(--text-vanilla-400);
font-family: 'Geist Mono';
font-size: 12px;
font-style: normal;
font-weight: 500;
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
width: 100%;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.status-tag {
margin: 0;
&.active {
color: var(--success-500);
background: var(--success-100);
border-color: var(--success-500);
}
&.inactive {
color: var(--error-500);
background: var(--error-100);
border-color: var(--error-500);
}
}
.progress-container {
width: 158px;
.ant-progress {
margin: 0;
.ant-progress-text {
font-weight: 600;
}
}
}
.ant-card {
&.ant-card-bordered {
border: 1px solid var(--bg-slate-500) !important;
}
}
}
}
.tabs-and-search {
display: flex;
justify-content: space-between;
align-items: center;
margin: 16px 0;
.action-btn {
border-radius: 2px;
border: 1px solid var(--bg-slate-400);
background: var(--bg-ink-300);
box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.1);
display: flex;
align-items: center;
justify-content: center;
}
}
.views-tabs-container {
margin-top: 1.5rem;
display: flex;
justify-content: space-between;
align-items: center;
.views-tabs {
color: var(--text-vanilla-400);
.view-title {
display: flex;
gap: var(--margin-2);
align-items: center;
justify-content: center;
font-size: var(--font-size-xs);
font-style: normal;
font-weight: var(--font-weight-normal);
}
.tab {
border: 1px solid var(--bg-slate-400);
width: 114px;
}
.tab::before {
background: var(--bg-slate-400);
}
.selected_view {
background: var(--bg-slate-300);
color: var(--text-vanilla-100);
border: 1px solid var(--bg-slate-400);
}
.selected_view::before {
background: var(--bg-slate-400);
}
}
.compass-button {
width: 30px;
height: 30px;
border-radius: 2px;
border: 1px solid var(--bg-slate-400);
background: var(--bg-ink-300);
box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.1);
}
}
.ant-drawer-close {
padding: 0px;
}
}
.lightMode {
.ant-drawer-header {
border-bottom: 1px solid var(--bg-vanilla-400);
background: var(--bg-vanilla-100);
}
.entity-detail-drawer {
.title {
color: var(--text-ink-300);
}
.entity-detail-drawer__entity {
.ant-typography {
color: var(--text-ink-300);
background: transparent;
}
}
.radio-button {
border: 1px solid var(--bg-vanilla-400);
background: var(--bg-vanilla-100);
color: var(--text-ink-300);
}
.views-tabs {
.tab {
background: var(--bg-vanilla-100);
}
.selected_view {
background: var(--bg-vanilla-300);
border: 1px solid var(--bg-slate-300);
color: var(--text-ink-400);
}
.selected_view::before {
background: var(--bg-vanilla-300);
border-left: 1px solid var(--bg-slate-300);
}
}
.compass-button {
border: 1px solid var(--bg-vanilla-300);
background: var(--bg-vanilla-100);
box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.1);
}
.tabs-and-search {
.action-btn {
border: 1px solid var(--bg-vanilla-400);
background: var(--bg-vanilla-100);
color: var(--text-ink-300);
}
}
}
}
.entity-metric-traces {
margin-top: 1rem;
.entity-metric-traces-header {
display: flex;
justify-content: space-between;
margin-bottom: 1rem;
gap: 8px;
padding: 12px;
border-radius: 3px;
border: 1px solid var(--bg-slate-500);
.filter-section {
flex: 1;
.ant-select-selector {
border-radius: 2px;
border: 1px solid var(--bg-slate-400) !important;
background-color: var(--bg-ink-300) !important;
input {
font-size: 12px;
}
.ant-tag .ant-typography {
font-size: 12px;
}
}
}
}
.entity-metric-traces-table {
.ant-table-content {
overflow: hidden !important;
}
.ant-table {
border-radius: 3px;
border: 1px solid var(--bg-slate-500);
.ant-table-thead > tr > th {
padding: 12px;
font-weight: 500;
font-size: 12px;
line-height: 18px;
background: rgba(171, 189, 255, 0.01);
border-bottom: none;
color: var(--Vanilla-400, #c0c1c3);
font-family: Inter;
font-size: 11px;
font-style: normal;
font-weight: 600;
line-height: 18px; /* 163.636% */
letter-spacing: 0.44px;
text-transform: uppercase;
&::before {
background-color: transparent;
}
}
.ant-table-thead > tr > th:has(.entityname-column-header) {
background: var(--bg-ink-400);
}
.ant-table-cell {
padding: 12px;
font-size: 13px;
line-height: 20px;
color: var(--bg-vanilla-100);
background: rgba(171, 189, 255, 0.01);
}
.ant-table-cell:has(.entityname-column-value) {
background: var(--bg-ink-400);
}
.entityname-column-value {
color: var(--bg-vanilla-100);
font-family: 'Geist Mono';
font-style: normal;
font-weight: 600;
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
}
.status-cell {
.active-tag {
color: var(--bg-forest-500);
padding: 4px 8px;
border-radius: 4px;
font-size: 12px;
font-weight: 500;
}
}
.progress-container {
.ant-progress-bg {
height: 8px !important;
border-radius: 4px;
}
}
.ant-table-tbody > tr:hover > td {
background: rgba(255, 255, 255, 0.04);
}
.ant-table-cell:first-child {
text-align: justify;
}
.ant-table-cell:nth-child(2) {
padding-left: 16px;
padding-right: 16px;
}
.ant-table-cell:nth-child(n + 3) {
padding-right: 24px;
}
.column-header-right {
text-align: right;
}
.ant-table-tbody > tr > td {
border-bottom: none;
}
.ant-table-thead
> tr
> th:not(:last-child):not(.ant-table-selection-column):not(.ant-table-row-expand-icon-cell):not([colspan])::before {
background-color: transparent;
}
.ant-empty-normal {
visibility: hidden;
}
}
.ant-table-container::after {
content: none;
}
}
}
.lightMode {
.entity-metric-traces-header {
.filter-section {
border-top: 1px solid var(--bg-vanilla-300);
border-bottom: 1px solid var(--bg-vanilla-300);
.ant-select-selector {
border-color: var(--bg-vanilla-300) !important;
background-color: var(--bg-vanilla-100) !important;
color: var(--bg-ink-200);
}
}
}
.entity-metric-traces-table {
.ant-table {
border-radius: 3px;
border: 1px solid var(--bg-vanilla-300);
.ant-table-thead > tr > th {
background: var(--bg-vanilla-100);
color: var(--text-ink-300);
}
.ant-table-thead > tr > th:has(.entityname-column-header) {
background: var(--bg-vanilla-100);
}
.ant-table-cell {
background: var(--bg-vanilla-100);
color: var(--bg-ink-500);
}
.ant-table-cell:has(.entityname-column-value) {
background: var(--bg-vanilla-100);
}
.entityname-column-value {
color: var(--bg-ink-300);
}
.ant-table-tbody > tr:hover > td {
background: rgba(0, 0, 0, 0.04);
}
}
}
}
.entity-metrics-logs-container {
margin-top: 1rem;
.filter-section {
flex: 1;
.ant-select-selector {
border-radius: 2px;
border: 1px solid var(--bg-slate-400) !important;
background-color: var(--bg-ink-300) !important;
input {
font-size: 12px;
}
.ant-tag .ant-typography {
font-size: 12px;
}
}
}
.entity-metrics-logs-header {
display: flex;
justify-content: space-between;
gap: 8px;
padding: 12px;
border-radius: 3px;
border: 1px solid var(--bg-slate-500);
}
.entity-metrics-logs {
margin-top: 1rem;
.virtuoso-list {
overflow-y: hidden !important;
&::-webkit-scrollbar {
width: 0.3rem;
height: 0.3rem;
}
&::-webkit-scrollbar-track {
background: transparent;
}
&::-webkit-scrollbar-thumb {
background: var(--bg-slate-300);
}
&::-webkit-scrollbar-thumb:hover {
background: var(--bg-slate-200);
}
.ant-row {
width: fit-content;
}
}
.skeleton-container {
height: 100%;
padding: 16px;
}
}
}
.entity-metrics-logs-list-container {
flex: 1;
height: calc(100vh - 272px) !important;
display: flex;
height: 100%;
.raw-log-content {
width: 100%;
text-wrap: inherit;
word-wrap: break-word;
}
}
.entity-metrics-logs-list-card {
width: 100%;
margin-top: 12px;
.ant-card-body {
padding: 0;
height: 100%;
width: 100%;
}
}
.logs-loading-skeleton {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 8px;
padding: 8px 0;
.ant-skeleton-input-sm {
height: 18px;
}
}
.no-logs-found {
height: 50vh;
width: 100%;
display: flex;
justify-content: center;
align-items: center;
padding: 24px;
box-sizing: border-box;
.ant-typography {
display: flex;
align-items: center;
gap: 16px;
}
}
.lightMode {
.filter-section {
border-top: 1px solid var(--bg-vanilla-300);
border-bottom: 1px solid var(--bg-vanilla-300);
.ant-select-selector {
border-color: var(--bg-vanilla-300) !important;
background-color: var(--bg-vanilla-100) !important;
color: var(--bg-ink-200);
}
}
}

View File

@@ -0,0 +1,300 @@
import { Color } from '@signozhq/design-tokens';
import { Typography } from 'antd';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults';
import { Ghost } from 'lucide-react';
import {
BaseAutocompleteData,
DataTypes,
} from 'types/api/queryBuilder/queryAutocompleteResponse';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import { EQueryType } from 'types/common/dashboard';
import { DataSource } from 'types/common/queryBuilder';
import { nanoToMilli } from 'utils/timeUtils';
import { v4 as uuidv4 } from 'uuid';
import { K8sCategory } from '../constants';
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',
};
/**
* 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: '',
isColumn: false,
type: '',
isJSON: false,
},
timeAggregation: 'rate',
spaceAggregation: 'sum',
functions: [],
filters,
expression: 'A',
disabled: false,
stepInterval: 60,
having: [],
limit: null,
orderBy: [
{
columnName: 'timestamp',
order: 'desc',
},
],
groupBy: [],
legend: '',
reduceTo: 'avg',
offset: 0,
pageSize: 100,
},
],
queryFormulas: [],
},
id: uuidv4(),
queryType: EQueryType.QUERY_BUILDER,
},
params: {
lastLogLineTimestamp: null,
},
start,
end,
});
/**
* Empty state container for entity details
*/
export function EntityDetailsEmptyContainer({
view,
category,
}: {
view: 'logs' | 'traces' | 'events';
category: K8sCategory;
}): React.ReactElement {
const label = category.slice(0, category.length);
return (
<div className="no-logs-found">
<Typography.Text type="secondary">
<Ghost size={24} color={Color.BG_AMBER_500} />
{`No ${view} found for this ${label}
in the selected time range.`}
</Typography.Text>
</div>
);
}
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[] = [
{
key: 'timestamp',
dataType: DataTypes.String,
type: 'tag',
isColumn: true,
},
{
key: 'serviceName',
dataType: DataTypes.String,
type: 'tag',
isColumn: true,
},
{
key: 'name',
dataType: DataTypes.String,
type: 'tag',
isColumn: true,
},
{
key: 'durationNano',
dataType: DataTypes.Float64,
type: 'tag',
isColumn: true,
},
{
key: 'httpMethod',
dataType: DataTypes.String,
type: 'tag',
isColumn: true,
},
{
key: 'responseStatusCode',
dataType: DataTypes.String,
type: 'tag',
isColumn: true,
},
];
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: '',
isColumn: false,
type: '',
isJSON: false,
},
timeAggregation: 'rate',
spaceAggregation: 'sum',
functions: [],
filters,
expression: 'A',
disabled: false,
stepInterval: 60,
having: [],
limit: null,
orderBy: [
{
columnName: 'timestamp',
order: 'desc',
},
],
groupBy: [],
legend: '',
reduceTo: 'avg',
},
],
queryFormulas: [],
},
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',
isColumn: true,
isJSON: false,
id: 'serviceName--string--tag--true',
isIndexed: false,
},
{
key: 'name',
dataType: 'string',
type: 'tag',
isColumn: true,
isJSON: false,
id: 'name--string--tag--true',
isIndexed: false,
},
{
key: 'durationNano',
dataType: 'float64',
type: 'tag',
isColumn: true,
isJSON: false,
id: 'durationNano--float64--tag--true',
isIndexed: false,
},
{
key: 'httpMethod',
dataType: 'string',
type: 'tag',
isColumn: true,
isJSON: false,
id: 'httpMethod--string--tag--true',
isIndexed: false,
},
{
key: 'responseStatusCode',
dataType: 'string',
type: 'tag',
isColumn: true,
isJSON: false,
id: 'responseStatusCode--string--tag--true',
isIndexed: false,
},
],
},
});

View File

@@ -0,0 +1,882 @@
.k8s-container {
display: flex;
flex-direction: row;
height: calc(100vh - 45px);
width: 100%;
.k8s-quick-filters-container {
width: 280px;
min-width: 280px;
border-right: 1px solid var(--bg-slate-400);
overflow-y: auto;
.k8s-quick-filters-container-header {
padding: 8px;
border-bottom: 1px solid var(--bg-slate-400);
display: flex;
align-items: center;
justify-content: space-between;
}
&::-webkit-scrollbar {
width: 0.1rem;
height: 0.1rem;
}
&::-webkit-scrollbar-track {
background: transparent;
}
&::-webkit-scrollbar-thumb {
background: var(--bg-ink-200);
}
&::-webkit-scrollbar-thumb:hover {
background: var(--bg-ink-100);
}
.ant-collapse-header {
border-bottom: 1px solid var(--bg-slate-400);
padding: 12px 8px;
}
.ant-collapse-content-box {
padding: 0px;
padding-block: 0px !important;
.quick-filters {
.checkbox-filter {
padding-left: 18px;
}
}
}
.quick-filters {
overflow-y: auto;
overflow-x: hidden;
&::-webkit-scrollbar {
width: 0.1rem;
height: 0.1rem;
}
&::-webkit-scrollbar-track {
background: transparent;
}
&::-webkit-scrollbar-thumb {
background: var(--bg-slate-300);
}
&::-webkit-scrollbar-thumb:hover {
background: var(--bg-slate-200);
}
}
.k8s-quick-filters-category-label {
display: flex;
align-items: center;
gap: 4px;
.k8s-quick-filters-category-label-icon {
margin-right: 8px;
}
.k8s-quick-filters-category-label-container {
display: flex;
align-items: center;
gap: 4px;
}
}
}
.k8s-list-container {
flex: 1;
max-width: 100%;
&.k8s-list-container-filters-visible {
max-width: calc(100% - 280px);
}
}
.periscope-btn {
&.ghost:not(:disabled) {
border: none;
background: transparent;
&:hover {
background: transparent;
color: var(--bg-robin-500) !important;
font-weight: 500;
}
}
}
.column-header {
display: flex;
align-items: center;
gap: 16px;
font-size: 11px;
&.pod-group-header {
padding-left: 12px;
}
}
}
.infra-monitoring-container {
display: flex;
height: 100%;
flex-direction: column;
.infra-monitoring-header {
display: flex;
justify-content: space-between;
width: 100%;
margin-bottom: 16px;
}
.k8s-list {
.column {
min-width: 180px;
max-width: 180px;
font-size: 12px !important;
}
.column-pod-name {
min-width: 200px;
max-width: 200px;
}
.column-pod-group {
min-width: 200px;
max-width: 200px;
}
.column-progress-bar {
min-width: 180px;
max-width: 180px;
}
.column-dummy {
min-width: 32px;
max-width: 32px;
}
.pod-group {
display: flex;
align-items: center;
gap: 4px;
flex-wrap: wrap;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.pod-group-tag-item {
border-radius: 2px;
font-size: 12px;
font-weight: 400;
background: var(--bg-slate-400);
color: var(--bg-vanilla-400);
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.pod-name {
width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.ant-table-container {
.ant-table-row-expand-icon-cell {
padding: 0px;
}
.ant-table-content {
&::-webkit-scrollbar {
width: 0.1rem;
height: 0.1rem;
}
&::-webkit-scrollbar-track {
background: transparent;
}
&::-webkit-scrollbar-thumb {
background: var(--bg-robin-500);
}
&::-webkit-scrollbar-thumb:hover {
background: var(--bg-robin-400);
}
}
}
}
.k8s-list-controls {
padding: 8px;
display: flex;
justify-content: space-between;
align-items: center;
gap: 8px;
.ant-select-selector {
border-radius: 2px;
border: 1px solid var(--bg-slate-400) !important;
background-color: var(--bg-ink-300) !important;
input {
font-size: 12px;
}
.ant-tag .ant-typography {
font-size: 12px;
}
}
.k8s-list-controls-left {
flex: 1;
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 8px;
.k8s-qb-search-container {
flex: 1;
min-width: 240px;
max-width: 60%;
}
.k8s-attribute-search-container {
flex: 1;
min-width: 240px;
max-width: 40%;
display: flex;
align-items: center;
.group-by-label {
min-width: max-content;
font-size: 13px;
font-style: normal;
font-weight: 400;
line-height: 18px; /* 128.571% */
letter-spacing: -0.07px;
border-radius: 2px 0px 0px 2px;
border: 1px solid var(--bg-slate-400, #1d212d);
border-right: none;
border-top-right-radius: 0px;
border-bottom-right-radius: 0px;
display: flex;
height: 32px;
padding: 6px 6px 6px 8px;
justify-content: center;
align-items: center;
gap: 4px;
}
.group-by-select {
.ant-select-selector {
border-left: none;
border-top-left-radius: 0px;
border-bottom-left-radius: 0px;
}
}
}
}
.k8s-list-controls-right {
min-width: 240px;
display: flex;
align-items: center;
gap: 4px;
}
.periscope-btn {
padding: 4px 8px;
&.ghost:not(:disabled) {
border: none;
background: transparent;
}
}
}
.progress-container {
display: flex;
align-items: center;
justify-content: center;
}
.entity-progress-bar {
display: flex;
align-items: center;
}
.progress-bar {
flex: 1;
margin-right: 8px;
margin-bottom: 0px;
min-width: 100px;
}
.clickable-row {
cursor: pointer;
}
.k8s-list-table {
.ant-table {
.ant-table-thead > tr > th {
padding: 12px;
font-weight: 500;
font-size: 12px;
line-height: 18px;
background: var(--bg-ink-500);
border-bottom: none;
color: var(--Vanilla-400, #c0c1c3);
font-family: Inter;
font-size: 11px;
font-style: normal;
font-weight: 600;
line-height: 18px; /* 163.636% */
letter-spacing: 0.44px;
text-transform: uppercase;
&::before {
background-color: transparent;
}
}
.ant-table-thead > tr > th:has(.hostname-column-header) {
background: var(--bg-ink-400);
}
.ant-table-cell {
padding: 12px;
font-size: 13px;
line-height: 20px;
color: var(--bg-vanilla-100);
background: var(--bg-ink-500);
border-bottom: none;
}
.ant-table-cell:has(.hostname-column-value) {
background: var(--bg-ink-400);
}
.hostname-column-value {
color: var(--bg-vanilla-100);
font-family: 'Geist Mono';
font-style: normal;
font-weight: 600;
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
}
.status-cell {
.active-tag {
color: var(--bg-forest-500);
padding: 4px 8px;
border-radius: 4px;
font-size: 12px;
font-weight: 500;
}
}
.progress-container {
.ant-progress-bg {
height: 8px !important;
border-radius: 4px;
}
}
.ant-table-tbody > tr:hover > td {
background: rgba(255, 255, 255, 0.04);
}
.ant-table-cell:first-child {
text-align: justify;
}
.ant-table-cell:nth-child(2) {
padding-left: 16px;
padding-right: 16px;
}
.ant-table-cell:nth-child(n + 3) {
padding-right: 24px;
}
.column-header-right {
text-align: right;
}
.ant-table-tbody > tr > td {
border-bottom: none;
}
.ant-table-thead
> tr
> th:not(:last-child):not(.ant-table-selection-column):not(.ant-table-row-expand-icon-cell):not([colspan])::before {
background-color: transparent;
}
.ant-empty-normal {
visibility: hidden;
}
}
.ant-pagination {
position: fixed;
bottom: 0;
width: calc(100% - 64px);
background: var(--bg-ink-500);
padding: 16px;
margin: 0;
// this is to offset intercom icon till we improve the design
padding-right: 72px;
.ant-pagination-item {
border-radius: 4px;
&-active {
background: var(--bg-robin-500);
border-color: var(--bg-robin-500);
a {
color: var(--bg-ink-500) !important;
}
}
}
}
.ant-table-expanded-row {
&:hover {
background: var(--bg-ink-500);
}
.ant-table-cell {
background: var(--bg-ink-500) !important;
}
.ant-table .ant-table-thead > tr > th {
padding: 4px 16px !important;
}
}
.expanded-table-container {
border: 1px solid var(--bg-ink-400);
overflow-x: auto;
padding-left: 48px;
&::-webkit-scrollbar {
width: 0.1rem;
height: 0.1rem;
}
&::-webkit-scrollbar-track {
background: transparent;
}
&::-webkit-scrollbar-thumb {
background: var(--bg-ink-200);
}
&::-webkit-scrollbar-thumb:hover {
background: var(--bg-ink-100);
}
.ant-table-expanded-row {
background: var(--bg-ink-500);
&:hover {
background: var(--bg-ink-500);
}
.ant-table-cell {
background: var(--bg-ink-500);
}
}
.expanded-table-footer {
display: flex;
justify-content: flex-start;
gap: 8px;
padding: 8px;
padding-left: 42px;
margin-top: 8px;
.periscope-btn {
font-size: 10px;
display: flex;
align-items: center;
gap: 4px;
}
.view-all-text {
font-size: 10px;
color: var(--bg-vanilla-400);
}
}
}
}
.k8s-list-container-filters-visible {
.k8s-list-table {
.ant-pagination {
width: calc(100% - 340px);
}
}
}
}
.infra-monitoring-tags {
width: fit-content;
font-family: Inter;
font-size: 11px;
font-style: normal;
font-weight: 500;
line-height: 18px; /* 163.636% */
letter-spacing: 0.44px;
text-transform: uppercase;
border-radius: 50px;
padding: 2px 8px;
&.active {
color: var(--Forest-500, #25e192);
border: 1px solid rgba(37, 225, 146, 0.2);
background: rgba(37, 225, 146, 0.1);
}
&.inactive {
color: var(--Slate-50, #62687c);
border: 1px solid rgba(98, 104, 124, 0.2);
background: rgba(98, 104, 124, 0.1);
}
}
.k8s-list-loading-state {
padding: 8px;
display: flex;
flex-direction: column;
gap: 2px;
.k8s-list-loading-state-item {
height: 48px;
width: 100%;
}
}
.no-filtered-hosts-message-container {
height: 30vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
.no-filtered-hosts-message-content {
display: flex;
flex-direction: column;
align-items: flex-start;
justify-content: center;
width: fit-content;
padding: 24px;
}
.no-filtered-hosts-message {
margin-top: 8px;
}
}
.hosts-empty-state-container {
padding: 16px;
height: 40vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
.hosts-empty-state-container-content {
padding: 16px;
display: flex;
flex-direction: column;
align-items: flex-start;
justify-content: center;
width: fit-content;
.no-hosts-message {
margin-bottom: 16px;
.no-hosts-message-title {
margin-top: 8px;
margin-bottom: 4px;
}
}
}
}
.lightMode {
.infra-monitoring-container {
.ant-table-thead > tr > th {
background: var(--bg-vanilla-100);
color: var(--bg-ink-500);
}
.ant-table-cell {
color: var(--bg-ink-500);
}
.k8s-list-controls {
border-top: 1px solid var(--bg-vanilla-300);
border-bottom: 1px solid var(--bg-vanilla-300);
.ant-select-selector {
border-color: var(--bg-vanilla-300) !important;
background-color: var(--bg-vanilla-100) !important;
color: var(--bg-ink-200);
}
}
}
.k8s-list-table {
.ant-table {
.ant-table-thead > tr > th {
background: var(--bg-vanilla-100);
color: var(--text-ink-300);
}
.ant-table-thead > tr > th:has(.hostname-column-header) {
background: var(--bg-vanilla-100);
}
.ant-table-cell {
background: var(--bg-vanilla-100);
color: var(--bg-ink-500);
}
.ant-table-cell:has(.hostname-column-value) {
background: var(--bg-vanilla-100);
}
.hostname-column-value {
color: var(--bg-ink-300);
}
.ant-table-tbody > tr:hover > td {
background: rgba(0, 0, 0, 0.04);
}
}
.ant-pagination {
background: var(--bg-vanilla-100);
.ant-pagination-item {
&-active {
background: var(--bg-robin-500);
border-color: var(--bg-robin-500);
a {
color: var(--bg-vanilla-100) !important;
}
}
}
}
}
}
.ant-table-cell {
min-width: 140px !important;
max-width: 140px !important;
}
.ant-table-cell {
&:has(.pod-name-header) {
min-width: 250px !important;
max-width: 250px !important;
}
}
.ant-table-cell {
&:has(.med-col) {
min-width: 180px !important;
max-width: 180px !important;
}
}
.expanded-k8s-list-table {
.ant-table-cell {
min-width: 180px !important;
max-width: 180px !important;
}
.ant-table-row-expand-icon-cell {
min-width: 30px !important;
max-width: 30px !important;
}
}
.ant-table-row-expand-icon-cell {
min-width: 30px !important;
max-width: 30px !important;
}
.event-content-container {
.ant-table {
background: var(--bg-ink-400);
.ant-table-row:hover {
.ant-table-cell {
.value-field {
.action-btn {
display: flex;
position: absolute;
top: 50%;
right: 16px;
transform: translateY(-50%);
gap: 4px;
}
}
}
}
.ant-table-cell {
border: 1px solid var(--bg-slate-500);
}
.attribute-name {
.ant-btn {
&:hover {
background-color: none !important;
}
}
}
.attribute-pin {
cursor: pointer;
padding: 0;
vertical-align: middle;
text-align: center;
.log-attribute-pin {
padding: 8px;
display: flex;
justify-content: center;
align-items: center;
.pin-attribute-icon {
border: none;
&.pinned svg {
fill: var(--bg-robin-500);
}
}
}
}
.value-field-container {
background: rgba(22, 25, 34, 0.4);
.value-field {
font-family: 'Geist Mono';
position: relative;
}
.action-btn {
display: none;
width: max-content;
position: absolute;
// padding: 0 16px;
right: 0;
.filter-btn {
display: flex;
align-items: center;
border: none;
box-shadow: none;
border-radius: 2px;
background: var(--bg-slate-400);
padding: 2px 3px;
gap: 3px;
height: 18px;
width: 20px;
}
}
}
}
}
.lightMode {
.infra-monitoring-container {
.k8s-list-table {
.ant-table-expanded-row {
&:hover {
background: var(--bg-vanilla-100) !important;
}
.ant-table-cell {
background: var(--bg-vanilla-100) !important;
}
.ant-table .ant-table-thead > tr > th {
padding: 4px 16px !important;
}
}
}
}
.event-content-container {
.ant-table {
background: var(--bg-vanilla-100);
}
.ant-table-cell {
border: 1px solid var(--bg-vanilla-200);
}
.value-field-container {
background: var(--bg-vanilla-300);
&.attribute-pin {
background: var(--bg-vanilla-100);
}
.action-btn {
.filter-btn {
background: var(--bg-vanilla-300);
}
}
}
}
.entity-group-header {
.ant-tag {
background-color: var(--bg-vanilla-300) !important;
color: var(--bg-slate-400) !important;
}
}
}
.entity-group-header {
width: 300px !important;
}

View File

@@ -0,0 +1,358 @@
import './InfraMonitoringK8s.styles.scss';
import { VerticalAlignTopOutlined } from '@ant-design/icons';
import * as Sentry from '@sentry/react';
import type { CollapseProps } from 'antd';
import { Collapse, Tooltip, Typography } from 'antd';
import QuickFilters from 'components/QuickFilters/QuickFilters';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useQueryOperations } from 'hooks/queryBuilder/useQueryBuilderOperations';
import {
Boxes,
Computer,
Container,
FilePenLine,
Workflow,
} from 'lucide-react';
import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFallback';
import { useState } from 'react';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
import K8sClustersList from './Clusters/K8sClustersList';
import {
ClustersQuickFiltersConfig,
DeploymentsQuickFiltersConfig,
K8sCategories,
NamespaceQuickFiltersConfig,
NodesQuickFiltersConfig,
PodsQuickFiltersConfig,
} from './constants';
import K8sDeploymentsList from './Deployments/K8sDeploymentsList';
import K8sNamespacesList from './Namespaces/K8sNamespacesList';
import K8sNodesList from './Nodes/K8sNodesList';
import K8sPodLists from './Pods/K8sPodLists';
export default function InfraMonitoringK8s(): JSX.Element {
const [showFilters, setShowFilters] = useState(true);
const [selectedCategory, setSelectedCategory] = useState(K8sCategories.PODS);
const [quickFiltersLastUpdated, setQuickFiltersLastUpdated] = useState(-1);
const { currentQuery } = useQueryBuilder();
const handleFilterVisibilityChange = (): void => {
setShowFilters(!showFilters);
};
const { handleChangeQueryData } = useQueryOperations({
index: 0,
query: currentQuery.builder.queryData[0],
entityVersion: '',
});
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
handleChangeQueryData('filters', query.builder.queryData[0].filters);
setQuickFiltersLastUpdated(Date.now());
};
const items: CollapseProps['items'] = [
{
label: (
<div className="k8s-quick-filters-category-label">
<div className="k8s-quick-filters-category-label-container">
<Container size={14} className="k8s-quick-filters-category-label-icon" />
<Typography.Text>Pods</Typography.Text>
</div>
</div>
),
key: K8sCategories.PODS,
showArrow: false,
children: (
<QuickFilters
source="infra-monitoring"
config={PodsQuickFiltersConfig}
handleFilterVisibilityChange={handleFilterVisibilityChange}
onFilterChange={handleFilterChange}
/>
),
},
{
label: (
<div className="k8s-quick-filters-category-label">
<div className="k8s-quick-filters-category-label-container">
<Workflow size={14} className="k8s-quick-filters-category-label-icon" />
<Typography.Text>Nodes</Typography.Text>
</div>
</div>
),
key: K8sCategories.NODES,
showArrow: false,
children: (
<QuickFilters
source="infra-monitoring"
config={NodesQuickFiltersConfig}
handleFilterVisibilityChange={handleFilterVisibilityChange}
onFilterChange={handleFilterChange}
/>
),
},
// NOTE - Enabled these as we release new entities
{
label: (
<div className="k8s-quick-filters-category-label">
<div className="k8s-quick-filters-category-label-container">
<FilePenLine
size={14}
className="k8s-quick-filters-category-label-icon"
/>
<Typography.Text>Namespaces</Typography.Text>
</div>
</div>
),
key: K8sCategories.NAMESPACES,
showArrow: false,
children: (
<QuickFilters
source="infra-monitoring"
config={NamespaceQuickFiltersConfig}
handleFilterVisibilityChange={handleFilterVisibilityChange}
onFilterChange={handleFilterChange}
/>
),
},
{
label: (
<div className="k8s-quick-filters-category-label">
<div className="k8s-quick-filters-category-label-container">
<Boxes size={14} className="k8s-quick-filters-category-label-icon" />
<Typography.Text>Clusters</Typography.Text>
</div>
</div>
),
key: K8sCategories.CLUSTERS,
showArrow: false,
children: (
<QuickFilters
source="infra-monitoring"
config={ClustersQuickFiltersConfig}
handleFilterVisibilityChange={handleFilterVisibilityChange}
onFilterChange={handleFilterChange}
/>
),
},
// {
// label: (
// <div className="k8s-quick-filters-category-label">
// <div className="k8s-quick-filters-category-label-container">
// <PackageOpen
// size={14}
// className="k8s-quick-filters-category-label-icon"
// />
// <Typography.Text>Containers</Typography.Text>
// </div>
// </div>
// ),
// key: K8sCategories.CONTAINERS,
// showArrow: false,
// children: (
// <QuickFilters
// source="infra-monitoring"
// config={ContainersQuickFiltersConfig}
// handleFilterVisibilityChange={handleFilterVisibilityChange}
// onFilterChange={handleFilterChange}
// />
// ),
// },
// {
// label: (
// <div className="k8s-quick-filters-category-label">
// <div className="k8s-quick-filters-category-label-container">
// <HardDrive size={14} className="k8s-quick-filters-category-label-icon" />
// <Typography.Text>Volumes</Typography.Text>
// </div>
// </div>
// ),
// key: K8sCategories.VOLUMES,
// showArrow: false,
// children: (
// <QuickFilters
// source="infra-monitoring"
// config={VolumesQuickFiltersConfig}
// handleFilterVisibilityChange={handleFilterVisibilityChange}
// onFilterChange={handleFilterChange}
// />
// ),
// },
{
label: (
<div className="k8s-quick-filters-category-label">
<div className="k8s-quick-filters-category-label-container">
<Computer size={14} className="k8s-quick-filters-category-label-icon" />
<Typography.Text>Deployments</Typography.Text>
</div>
</div>
),
key: K8sCategories.DEPLOYMENTS,
showArrow: false,
children: (
<QuickFilters
source="infra-monitoring"
config={DeploymentsQuickFiltersConfig}
handleFilterVisibilityChange={handleFilterVisibilityChange}
onFilterChange={handleFilterChange}
/>
),
},
// {
// label: (
// <div className="k8s-quick-filters-category-label">
// <div className="k8s-quick-filters-category-label-container">
// <Bolt size={14} className="k8s-quick-filters-category-label-icon" />
// <Typography.Text>Jobs</Typography.Text>
// </div>
// </div>
// ),
// key: K8sCategories.JOBS,
// showArrow: false,
// children: (
// <QuickFilters
// source="infra-monitoring"
// config={JobsQuickFiltersConfig}
// handleFilterVisibilityChange={handleFilterVisibilityChange}
// onFilterChange={handleFilterChange}
// />
// ),
// },
// {
// label: (
// <div className="k8s-quick-filters-category-label">
// <div className="k8s-quick-filters-category-label-container">
// <Group size={14} className="k8s-quick-filters-category-label-icon" />
// <Typography.Text>DaemonSets</Typography.Text>
// </div>
// </div>
// ),
// key: K8sCategories.DAEMONSETS,
// showArrow: false,
// children: (
// <QuickFilters
// source="infra-monitoring"
// config={DaemonSetsQuickFiltersConfig}
// handleFilterVisibilityChange={handleFilterVisibilityChange}
// onFilterChange={handleFilterChange}
// />
// ),
// },
// {
// label: (
// <div className="k8s-quick-filters-category-label">
// <div className="k8s-quick-filters-category-label-container">
// <ArrowUpDown
// size={14}
// className="k8s-quick-filters-category-label-icon"
// />
// <Typography.Text>StatefulSets</Typography.Text>
// </div>
// </div>
// ),
// key: K8sCategories.STATEFULSETS,
// showArrow: false,
// children: (
// <QuickFilters
// source="infra-monitoring"
// config={StatefulsetsQuickFiltersConfig}
// handleFilterVisibilityChange={handleFilterVisibilityChange}
// onFilterChange={handleFilterChange}
// />
// ),
// },
];
const handleCategoryChange = (key: string | string[]): void => {
if (Array.isArray(key) && key.length > 0) {
setSelectedCategory(key[0] as string);
// Reset filters
handleChangeQueryData('filters', { items: [], op: 'and' });
}
};
return (
<Sentry.ErrorBoundary fallback={<ErrorBoundaryFallback />}>
<div className="infra-monitoring-container">
<div className="k8s-container">
{showFilters && (
<div className="k8s-quick-filters-container">
<div className="k8s-quick-filters-container-header">
<Typography.Text>Filters</Typography.Text>
<Tooltip title="Collapse Filters">
<VerticalAlignTopOutlined
rotate={270}
onClick={handleFilterVisibilityChange}
/>
</Tooltip>
</div>
<Collapse
onChange={handleCategoryChange}
items={items}
defaultActiveKey={[selectedCategory]}
activeKey={[selectedCategory]}
accordion
bordered={false}
ghost
/>
</div>
)}
<div
className={`k8s-list-container ${
showFilters ? 'k8s-list-container-filters-visible' : ''
}`}
>
{selectedCategory === K8sCategories.PODS && (
<K8sPodLists
isFiltersVisible={showFilters}
handleFilterVisibilityChange={handleFilterVisibilityChange}
quickFiltersLastUpdated={quickFiltersLastUpdated}
/>
)}
{selectedCategory === K8sCategories.NODES && (
<K8sNodesList
isFiltersVisible={showFilters}
handleFilterVisibilityChange={handleFilterVisibilityChange}
quickFiltersLastUpdated={quickFiltersLastUpdated}
/>
)}
{selectedCategory === K8sCategories.DEPLOYMENTS && (
<K8sDeploymentsList
isFiltersVisible={showFilters}
handleFilterVisibilityChange={handleFilterVisibilityChange}
quickFiltersLastUpdated={quickFiltersLastUpdated}
/>
)}
{selectedCategory === K8sCategories.NAMESPACES && (
<K8sNamespacesList
isFiltersVisible={showFilters}
handleFilterVisibilityChange={handleFilterVisibilityChange}
quickFiltersLastUpdated={quickFiltersLastUpdated}
/>
)}
{selectedCategory === K8sCategories.CLUSTERS && (
<K8sClustersList
isFiltersVisible={showFilters}
handleFilterVisibilityChange={handleFilterVisibilityChange}
quickFiltersLastUpdated={quickFiltersLastUpdated}
/>
)}
</div>
</div>
</div>
</Sentry.ErrorBoundary>
);
}

View File

@@ -0,0 +1,32 @@
import { Typography } from 'antd';
export default function HostsEmptyOrIncorrectMetrics({
noData,
incorrectData,
}: {
noData: boolean;
incorrectData: boolean;
}): JSX.Element {
return (
<div className="hosts-empty-state-container">
<div className="hosts-empty-state-container-content">
<img className="eyes-emoji" src="/Images/eyesEmoji.svg" alt="eyes emoji" />
{noData && (
<div className="no-hosts-message">
<Typography.Title level={5} className="no-hosts-message-title">
No data received yet.
</Typography.Title>
</div>
)}
{incorrectData && (
<Typography.Text className="incorrect-metrics-message">
To see data, upgrade to the latest version of SigNoz k8s-infra chart.
Please contact support if you need help.
</Typography.Text>
)}
</div>
</div>
);
}

View File

@@ -0,0 +1,114 @@
.k8s-filters-side-panel-container {
position: absolute;
width: 100%;
height: 100vh;
background-color: rgba(0, 0, 0, 0.2);
top: 0;
left: 0;
z-index: 10;
}
.k8s-filters-side-panel {
border-radius: 4px;
border: 1px solid var(--bg-slate-500);
background: var(--bg-ink-400);
box-shadow: -4px 10px 16px 2px rgba(0, 0, 0, 0.2);
height: 88vh;
position: absolute;
width: 320px;
right: 4px;
top: 48px;
z-index: 2;
.k8s-filters-side-panel-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 8px;
height: 40px;
.k8s-filters-side-panel-header-title {
display: flex;
align-items: center;
gap: 8px;
}
}
.k8s-filters-side-panel-body {
height: calc(100% - 40px);
.k8s-filters-side-panel-body-header {
border: 1px solid var(--bg-ink-300);
border-left: none;
border-right: none;
.ant-input {
height: 40px;
}
}
.k8s-filters-side-panel-body-content {
display: flex;
flex-direction: column;
.added-columns,
.available-columns {
padding: 8px;
.filter-columns-title {
color: var(--text-slate-50);
font-family: Inter;
font-size: 11px;
font-style: normal;
font-weight: 500;
line-height: 18px; /* 163.636% */
letter-spacing: 0.88px;
text-transform: uppercase;
padding: 8px 12px;
margin-bottom: 8px;
}
.added-columns-list,
.available-columns-list {
display: flex;
flex-direction: column;
gap: 8px;
.added-column-item,
.available-column-item {
color: var(--text-vanilla-100);
font-family: Inter;
font-size: 12px;
font-style: normal;
font-weight: 400;
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
padding: 4px 0px 4px 12px;
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
cursor: pointer;
}
.added-column-item-content,
.available-column-item-content {
display: flex;
align-items: center;
gap: 8px;
}
}
}
.horizontal-divider {
border-top: 1px solid var(--bg-ink-300);
}
}
}
}

View File

@@ -0,0 +1,129 @@
/* eslint-disable @typescript-eslint/explicit-function-return-type */
/* eslint-disable jsx-a11y/no-static-element-interactions */
/* eslint-disable jsx-a11y/click-events-have-key-events */
import './K8sFiltersSidePanel.styles.scss';
import { Button, Input } from 'antd';
import { GripVertical, TableColumnsSplit, X } from 'lucide-react';
import { useEffect, useRef, useState } from 'react';
import { IEntityColumn } from '../utils';
function K8sFiltersSidePanel({
defaultAddedColumns,
onClose,
addedColumns = [],
availableColumns = [],
onAddColumn = () => {},
onRemoveColumn = () => {},
}: {
defaultAddedColumns: IEntityColumn[];
onClose: () => void;
addedColumns?: IEntityColumn[];
availableColumns?: IEntityColumn[];
onAddColumn?: (column: IEntityColumn) => void;
onRemoveColumn?: (column: IEntityColumn) => void;
}): JSX.Element {
const [searchValue, setSearchValue] = useState('');
const sidePanelRef = useRef<HTMLDivElement>(null);
const handleSearchChange = (e: React.ChangeEvent<HTMLInputElement>): void => {
setSearchValue(e.target.value);
};
useEffect(() => {
if (sidePanelRef.current) {
sidePanelRef.current.focus();
}
}, [searchValue]);
return (
<div className="k8s-filters-side-panel-container">
<div className="k8s-filters-side-panel" ref={sidePanelRef}>
<div className="k8s-filters-side-panel-header">
<span className="k8s-filters-side-panel-header-title">
<TableColumnsSplit size={16} /> Columns
</span>
<Button
className="periscope-btn ghost"
icon={<X size={14} strokeWidth={1.5} onClick={onClose} />}
/>
</div>
<div className="k8s-filters-side-panel-body">
<div className="k8s-filters-side-panel-body-header">
<Input
autoFocus
className="periscope-input borderless"
placeholder="Search for a column ..."
value={searchValue}
onChange={handleSearchChange}
/>
</div>
<div className="k8s-filters-side-panel-body-content">
<div className="added-columns">
<div className="filter-columns-title">Added Columns</div>
<div className="added-columns-list">
{[...defaultAddedColumns, ...addedColumns]
.filter((column) =>
column.label.toLowerCase().includes(searchValue.toLowerCase()),
)
.map((column) => (
<div className="added-column-item" key={column.value}>
<div className="added-column-item-content">
<GripVertical size={16} /> {column.label}
</div>
{column.canRemove && (
<X
size={14}
strokeWidth={1.5}
onClick={(): void => onRemoveColumn(column)}
/>
)}
</div>
))}
</div>
</div>
<div className="horizontal-divider" />
<div className="available-columns">
<div className="filter-columns-title">Other Columns</div>
<div className="available-columns-list">
{availableColumns
.filter((column) =>
column.label.toLowerCase().includes(searchValue.toLowerCase()),
)
.map((column) => (
<div
className="available-column-item"
key={column.value}
onClick={(): void => onAddColumn(column)}
>
<div className="available-column-item-content">
<GripVertical size={16} /> {column.label}
</div>
</div>
))}
</div>
</div>
</div>
</div>
</div>
</div>
);
}
K8sFiltersSidePanel.defaultProps = {
addedColumns: [],
availableColumns: [],
onAddColumn: () => {},
onRemoveColumn: () => {},
};
export default K8sFiltersSidePanel;

View File

@@ -0,0 +1,165 @@
/* eslint-disable @typescript-eslint/explicit-function-return-type */
import './InfraMonitoringK8s.styles.scss';
import { Button, Select } from 'antd';
import QueryBuilderSearch from 'container/QueryBuilder/filters/QueryBuilderSearch';
import DateTimeSelectionV2 from 'container/TopNav/DateTimeSelectionV2';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { Filter, SlidersHorizontal } from 'lucide-react';
import { useCallback, useMemo, useState } from 'react';
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import { K8sCategory } from './constants';
import K8sFiltersSidePanel from './K8sFiltersSidePanel/K8sFiltersSidePanel';
import { IEntityColumn } from './utils';
interface K8sHeaderProps {
selectedGroupBy: BaseAutocompleteData[];
groupByOptions: { value: string; label: string }[];
isLoadingGroupByFilters: boolean;
handleFiltersChange: (value: IBuilderQuery['filters']) => void;
handleGroupByChange: (value: IBuilderQuery['groupBy']) => void;
defaultAddedColumns: IEntityColumn[];
addedColumns?: IEntityColumn[];
availableColumns?: IEntityColumn[];
onAddColumn?: (column: IEntityColumn) => void;
onRemoveColumn?: (column: IEntityColumn) => void;
handleFilterVisibilityChange: () => void;
isFiltersVisible: boolean;
entity: K8sCategory;
}
function K8sHeader({
selectedGroupBy,
defaultAddedColumns,
groupByOptions,
isLoadingGroupByFilters,
addedColumns,
availableColumns,
handleFiltersChange,
handleGroupByChange,
onAddColumn,
onRemoveColumn,
handleFilterVisibilityChange,
isFiltersVisible,
entity,
}: K8sHeaderProps): JSX.Element {
const [isFiltersSidePanelOpen, setIsFiltersSidePanelOpen] = useState(false);
const { currentQuery } = useQueryBuilder();
const updatedCurrentQuery = useMemo(
() => ({
...currentQuery,
builder: {
...currentQuery.builder,
queryData: [
{
...currentQuery.builder.queryData[0],
aggregateOperator: 'noop',
aggregateAttribute: {
...currentQuery.builder.queryData[0].aggregateAttribute,
},
},
],
},
}),
[currentQuery],
);
const query = updatedCurrentQuery?.builder?.queryData[0] || null;
const handleChangeTagFilters = useCallback(
(value: IBuilderQuery['filters']) => {
handleFiltersChange(value);
},
[handleFiltersChange],
);
return (
<div className="k8s-list-controls">
<div className="k8s-list-controls-left">
{!isFiltersVisible && (
<div className="quick-filters-toggle-container">
<Button
className="periscope-btn ghost"
type="text"
size="small"
onClick={handleFilterVisibilityChange}
>
<Filter size={14} />
</Button>
</div>
)}
<div className="k8s-qb-search-container">
<QueryBuilderSearch
query={query}
onChange={handleChangeTagFilters}
isInfraMonitoring
disableNavigationShortcuts
entity={entity}
/>
</div>
<div className="k8s-attribute-search-container">
<div className="group-by-label"> Group by </div>
<Select
className="group-by-select"
loading={isLoadingGroupByFilters}
mode="multiple"
value={selectedGroupBy}
allowClear
maxTagCount="responsive"
placeholder="Search for attribute"
style={{ width: '100%' }}
options={groupByOptions}
onChange={handleGroupByChange}
/>
</div>
</div>
<div className="k8s-list-controls-right">
<DateTimeSelectionV2
showAutoRefresh={false}
showRefreshText={false}
hideShareModal
/>
<Button
type="text"
className="periscope-btn ghost"
disabled={selectedGroupBy?.length > 0}
onClick={(): void => setIsFiltersSidePanelOpen(true)}
>
<SlidersHorizontal size={14} />
</Button>
</div>
{isFiltersSidePanelOpen && (
<K8sFiltersSidePanel
defaultAddedColumns={defaultAddedColumns}
addedColumns={addedColumns}
availableColumns={availableColumns}
onClose={(): void => {
if (isFiltersSidePanelOpen) {
setIsFiltersSidePanelOpen(false);
}
}}
onAddColumn={onAddColumn}
onRemoveColumn={onRemoveColumn}
/>
)}
</div>
);
}
K8sHeader.defaultProps = {
addedColumns: [],
availableColumns: [],
onAddColumn: () => {},
onRemoveColumn: () => {},
};
export default K8sHeader;

View File

@@ -0,0 +1,30 @@
import './InfraMonitoringK8s.styles.scss';
import { Skeleton } from 'antd';
function LoadingContainer(): JSX.Element {
return (
<div className="k8s-list-loading-state">
<Skeleton.Input
className="k8s-list-loading-state-item"
size="large"
block
active
/>
<Skeleton.Input
className="k8s-list-loading-state-item"
size="large"
block
active
/>
<Skeleton.Input
className="k8s-list-loading-state-item"
size="large"
block
active
/>
</div>
);
}
export default LoadingContainer;

View File

@@ -0,0 +1,17 @@
.infra-monitoring-container {
.namespaces-list-table {
.expanded-table-container {
padding-left: 80px;
}
.ant-table-cell {
min-width: 223px !important;
max-width: 223px !important;
}
.ant-table-row-expand-icon-cell {
min-width: 30px !important;
max-width: 30px !important;
}
}
}

View File

@@ -0,0 +1,513 @@
/* eslint-disable no-restricted-syntax */
/* eslint-disable @typescript-eslint/explicit-function-return-type */
import '../InfraMonitoringK8s.styles.scss';
import './K8sNamespacesList.styles.scss';
import { LoadingOutlined } from '@ant-design/icons';
import {
Button,
Spin,
Table,
TablePaginationConfig,
TableProps,
Typography,
} from 'antd';
import { ColumnType, SorterResult } from 'antd/es/table/interface';
import logEvent from 'api/common/logEvent';
import { K8sNamespacesListPayload } from 'api/infraMonitoring/getK8sNamespacesList';
import { useGetK8sNamespacesList } from 'hooks/infraMonitoring/useGetK8sNamespacesList';
import { useGetAggregateKeys } from 'hooks/queryBuilder/useGetAggregateKeys';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useQueryOperations } from 'hooks/queryBuilder/useQueryBuilderOperations';
import { ChevronDown, ChevronRight } from 'lucide-react';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import { GlobalReducer } from 'types/reducer/globalTime';
import {
K8sCategory,
K8sEntityToAggregateAttributeMapping,
} from '../constants';
import K8sHeader from '../K8sHeader';
import LoadingContainer from '../LoadingContainer';
import NamespaceDetails from './NamespaceDetails';
import {
defaultAddedColumns,
formatDataForTable,
getK8sNamespacesListColumns,
getK8sNamespacesListQuery,
K8sNamespacesRowData,
} from './utils';
// eslint-disable-next-line sonarjs/cognitive-complexity
function K8sNamespacesList({
isFiltersVisible,
handleFilterVisibilityChange,
quickFiltersLastUpdated,
}: {
isFiltersVisible: boolean;
handleFilterVisibilityChange: () => void;
quickFiltersLastUpdated: number;
}): JSX.Element {
const { maxTime, minTime } = useSelector<AppState, GlobalReducer>(
(state) => state.globalTime,
);
const [currentPage, setCurrentPage] = useState(1);
const [expandedRowKeys, setExpandedRowKeys] = useState<string[]>([]);
const [orderBy, setOrderBy] = useState<{
columnName: string;
order: 'asc' | 'desc';
} | null>(null);
const [selectedNamespaceUID, setselectedNamespaceUID] = useState<
string | null
>(null);
const pageSize = 10;
const [groupBy, setGroupBy] = useState<IBuilderQuery['groupBy']>([]);
const [
selectedRowData,
setSelectedRowData,
] = useState<K8sNamespacesRowData | null>(null);
const [groupByOptions, setGroupByOptions] = useState<
{ value: string; label: string }[]
>([]);
const { currentQuery } = useQueryBuilder();
const queryFilters = useMemo(
() =>
currentQuery?.builder?.queryData[0]?.filters || {
items: [],
op: 'and',
},
[currentQuery?.builder?.queryData],
);
// Reset pagination every time quick filters are changed
useEffect(() => {
setCurrentPage(1);
}, [quickFiltersLastUpdated]);
const createFiltersForSelectedRowData = (
selectedRowData: K8sNamespacesRowData,
groupBy: IBuilderQuery['groupBy'],
): IBuilderQuery['filters'] => {
const baseFilters: IBuilderQuery['filters'] = {
items: [...queryFilters.items],
op: 'and',
};
if (!selectedRowData) return baseFilters;
const { groupedByMeta } = selectedRowData;
for (const key of groupBy) {
baseFilters.items.push({
key: {
key: key.key,
type: null,
},
op: '=',
value: groupedByMeta[key.key],
id: key.key,
});
}
return baseFilters;
};
const fetchGroupedByRowDataQuery = useMemo(() => {
if (!selectedRowData) return null;
const baseQuery = getK8sNamespacesListQuery();
const filters = createFiltersForSelectedRowData(selectedRowData, groupBy);
return {
...baseQuery,
limit: 10,
offset: 0,
filters,
start: Math.floor(minTime / 1000000),
end: Math.floor(maxTime / 1000000),
orderBy,
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [minTime, maxTime, orderBy, selectedRowData, groupBy]);
const {
data: groupedByRowData,
isFetching: isFetchingGroupedByRowData,
isLoading: isLoadingGroupedByRowData,
isError: isErrorGroupedByRowData,
refetch: fetchGroupedByRowData,
} = useGetK8sNamespacesList(
fetchGroupedByRowDataQuery as K8sNamespacesListPayload,
{
queryKey: ['namespaceList', fetchGroupedByRowDataQuery],
enabled: !!fetchGroupedByRowDataQuery && !!selectedRowData,
},
);
const {
data: groupByFiltersData,
isLoading: isLoadingGroupByFilters,
} = useGetAggregateKeys(
{
dataSource: currentQuery.builder.queryData[0].dataSource,
aggregateAttribute:
K8sEntityToAggregateAttributeMapping[K8sCategory.NAMESPACES],
aggregateOperator: 'noop',
searchText: '',
tagType: '',
},
{
queryKey: [currentQuery.builder.queryData[0].dataSource, 'noop'],
},
true,
K8sCategory.NODES,
);
const query = useMemo(() => {
const baseQuery = getK8sNamespacesListQuery();
const queryPayload = {
...baseQuery,
limit: pageSize,
offset: (currentPage - 1) * pageSize,
filters: queryFilters,
start: Math.floor(minTime / 1000000),
end: Math.floor(maxTime / 1000000),
orderBy,
};
if (groupBy.length > 0) {
queryPayload.groupBy = groupBy;
}
return queryPayload;
}, [currentPage, minTime, maxTime, orderBy, groupBy, queryFilters]);
const formattedGroupedByNamespacesData = useMemo(
() =>
formatDataForTable(groupedByRowData?.payload?.data?.records || [], groupBy),
[groupedByRowData, groupBy],
);
const { data, isFetching, isLoading, isError } = useGetK8sNamespacesList(
query as K8sNamespacesListPayload,
{
queryKey: ['namespaceList', query],
enabled: !!query,
},
);
const namespacesData = useMemo(() => data?.payload?.data?.records || [], [
data,
]);
const totalCount = data?.payload?.data?.total || 0;
const formattedNamespacesData = useMemo(
() => formatDataForTable(namespacesData, groupBy),
[namespacesData, groupBy],
);
const columns = useMemo(() => getK8sNamespacesListColumns(groupBy), [groupBy]);
const handleGroupByRowClick = (record: K8sNamespacesRowData): void => {
setSelectedRowData(record);
if (expandedRowKeys.includes(record.key)) {
setExpandedRowKeys(expandedRowKeys.filter((key) => key !== record.key));
} else {
setExpandedRowKeys([record.key]);
}
};
useEffect(() => {
if (selectedRowData) {
fetchGroupedByRowData();
}
}, [selectedRowData, fetchGroupedByRowData]);
const handleTableChange: TableProps<K8sNamespacesRowData>['onChange'] = useCallback(
(
pagination: TablePaginationConfig,
_filters: Record<string, (string | number | boolean)[] | null>,
sorter:
| SorterResult<K8sNamespacesRowData>
| SorterResult<K8sNamespacesRowData>[],
): void => {
if (pagination.current) {
setCurrentPage(pagination.current);
}
if ('field' in sorter && sorter.order) {
setOrderBy({
columnName: sorter.field as string,
order: sorter.order === 'ascend' ? 'asc' : 'desc',
});
} else {
setOrderBy(null);
}
},
[],
);
const { handleChangeQueryData } = useQueryOperations({
index: 0,
query: currentQuery.builder.queryData[0],
entityVersion: '',
});
const handleFiltersChange = useCallback(
(value: IBuilderQuery['filters']): void => {
handleChangeQueryData('filters', value);
setCurrentPage(1);
logEvent('Infra Monitoring: K8s list filters applied', {
filters: value,
});
},
[handleChangeQueryData],
);
useEffect(() => {
logEvent('Infra Monitoring: K8s list page visited', {});
}, []);
const selectedNamespaceData = useMemo(() => {
if (!selectedNamespaceUID) return null;
return (
namespacesData.find(
(namespace) => namespace.namespaceName === selectedNamespaceUID,
) || null
);
}, [selectedNamespaceUID, namespacesData]);
const handleRowClick = (record: K8sNamespacesRowData): void => {
if (groupBy.length === 0) {
setSelectedRowData(null);
setselectedNamespaceUID(record.namespaceUID);
} else {
handleGroupByRowClick(record);
}
logEvent('Infra Monitoring: K8s namespace list item clicked', {
namespaceUID: record.namespaceUID,
});
};
const nestedColumns = useMemo(() => getK8sNamespacesListColumns([]), []);
const isGroupedByAttribute = groupBy.length > 0;
const handleExpandedRowViewAllClick = (): void => {
if (!selectedRowData) return;
const filters = createFiltersForSelectedRowData(selectedRowData, groupBy);
handleFiltersChange(filters);
setCurrentPage(1);
setSelectedRowData(null);
setGroupBy([]);
setOrderBy(null);
};
const expandedRowRender = (): JSX.Element => (
<div className="expanded-table-container">
{isErrorGroupedByRowData && (
<Typography>{groupedByRowData?.error || 'Something went wrong'}</Typography>
)}
{isFetchingGroupedByRowData || isLoadingGroupedByRowData ? (
<LoadingContainer />
) : (
<div className="expanded-table">
<Table
columns={nestedColumns as ColumnType<K8sNamespacesRowData>[]}
dataSource={formattedGroupedByNamespacesData}
pagination={false}
scroll={{ x: true }}
tableLayout="fixed"
size="small"
loading={{
spinning: isFetchingGroupedByRowData || isLoadingGroupedByRowData,
indicator: <Spin indicator={<LoadingOutlined size={14} spin />} />,
}}
showHeader={false}
/>
{groupedByRowData?.payload?.data?.total &&
groupedByRowData?.payload?.data?.total > 10 ? (
<div className="expanded-table-footer">
<Button
type="default"
size="small"
className="periscope-btn secondary"
onClick={handleExpandedRowViewAllClick}
>
View All
</Button>
</div>
) : null}
</div>
)}
</div>
);
const expandRowIconRenderer = ({
expanded,
onExpand,
record,
}: {
expanded: boolean;
onExpand: (
record: K8sNamespacesRowData,
e: React.MouseEvent<HTMLButtonElement>,
) => void;
record: K8sNamespacesRowData;
}): JSX.Element | null => {
if (!isGroupedByAttribute) {
return null;
}
return expanded ? (
<Button
className="periscope-btn ghost"
onClick={(e: React.MouseEvent<HTMLButtonElement>): void =>
onExpand(record, e)
}
role="button"
>
<ChevronDown size={14} />
</Button>
) : (
<Button
className="periscope-btn ghost"
onClick={(e: React.MouseEvent<HTMLButtonElement>): void =>
onExpand(record, e)
}
role="button"
>
<ChevronRight size={14} />
</Button>
);
};
const handleCloseNamespaceDetail = (): void => {
setselectedNamespaceUID(null);
};
const handleGroupByChange = useCallback(
(value: IBuilderQuery['groupBy']) => {
const groupBy = [];
for (let index = 0; index < value.length; index++) {
const element = (value[index] as unknown) as string;
const key = groupByFiltersData?.payload?.attributeKeys?.find(
(key) => key.key === element,
);
if (key) {
groupBy.push(key);
}
}
// Reset pagination on switching to groupBy
setCurrentPage(1);
setGroupBy(groupBy);
setExpandedRowKeys([]);
},
[groupByFiltersData],
);
useEffect(() => {
if (groupByFiltersData?.payload) {
setGroupByOptions(
groupByFiltersData?.payload?.attributeKeys?.map((filter) => ({
value: filter.key,
label: filter.key,
})) || [],
);
}
}, [groupByFiltersData]);
return (
<div className="k8s-list">
<K8sHeader
isFiltersVisible={isFiltersVisible}
handleFilterVisibilityChange={handleFilterVisibilityChange}
defaultAddedColumns={defaultAddedColumns}
handleFiltersChange={handleFiltersChange}
groupByOptions={groupByOptions}
isLoadingGroupByFilters={isLoadingGroupByFilters}
handleGroupByChange={handleGroupByChange}
selectedGroupBy={groupBy}
entity={K8sCategory.NODES}
/>
{isError && <Typography>{data?.error || 'Something went wrong'}</Typography>}
<Table
className="k8s-list-table namespaces-list-table"
dataSource={isFetching || isLoading ? [] : formattedNamespacesData}
columns={columns}
pagination={{
current: currentPage,
pageSize,
total: totalCount,
showSizeChanger: false,
hideOnSinglePage: true,
}}
scroll={{ x: true }}
loading={{
spinning: isFetching || isLoading,
indicator: <Spin indicator={<LoadingOutlined size={14} spin />} />,
}}
locale={{
emptyText:
isFetching || isLoading ? null : (
<div className="no-filtered-hosts-message-container">
<div className="no-filtered-hosts-message-content">
<img
src="/Icons/emptyState.svg"
alt="thinking-emoji"
className="empty-state-svg"
/>
<Typography.Text className="no-filtered-hosts-message">
This query had no results. Edit your query and try again!
</Typography.Text>
</div>
</div>
),
}}
tableLayout="fixed"
onChange={handleTableChange}
onRow={(record): { onClick: () => void; className: string } => ({
onClick: (): void => handleRowClick(record),
className: 'clickable-row',
})}
expandable={{
expandedRowRender: isGroupedByAttribute ? expandedRowRender : undefined,
expandIcon: expandRowIconRenderer,
expandedRowKeys,
}}
/>
<NamespaceDetails
namespace={selectedNamespaceData}
isModalTimeSelection
onClose={handleCloseNamespaceDetail}
/>
</div>
);
}
export default K8sNamespacesList;

View File

@@ -0,0 +1,7 @@
import { K8sNamespacesData } from 'api/infraMonitoring/getK8sNamespacesList';
export type NamespaceDetailsProps = {
namespace: K8sNamespacesData | null;
isModalTimeSelection: boolean;
onClose: () => void;
};

View File

@@ -0,0 +1,247 @@
.namespace-detail-drawer {
border-left: 1px solid var(--bg-slate-500);
background: var(--bg-ink-400);
box-shadow: -4px 10px 16px 2px rgba(0, 0, 0, 0.2);
.ant-drawer-header {
padding: 8px 16px;
border-bottom: none;
align-items: stretch;
border-bottom: 1px solid var(--bg-slate-500);
background: var(--bg-ink-400);
}
.ant-drawer-close {
margin-inline-end: 0px;
}
.ant-drawer-body {
display: flex;
flex-direction: column;
padding: 16px;
}
.title {
color: var(--text-vanilla-400);
font-family: 'Geist Mono';
font-size: 14px;
font-style: normal;
font-weight: 500;
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
}
.radio-button {
display: flex;
align-items: center;
justify-content: center;
padding-top: var(--padding-1);
border: 1px solid var(--bg-slate-400);
background: var(--bg-ink-300);
box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.1);
}
.namespace-detail-drawer__namespace {
.namespace-details-grid {
.labels-row,
.values-row {
display: grid;
grid-template-columns: 1fr 1.5fr 1.5fr 1.5fr;
gap: 30px;
align-items: center;
}
.labels-row {
margin-bottom: 8px;
}
.namespace-details-metadata-label {
color: var(--text-vanilla-400);
font-family: Inter;
font-size: 11px;
font-style: normal;
font-weight: 500;
line-height: 18px; /* 163.636% */
letter-spacing: 0.44px;
text-transform: uppercase;
}
.namespace-details-metadata-value {
color: var(--text-vanilla-400);
font-family: 'Geist Mono';
font-size: 12px;
font-style: normal;
font-weight: 500;
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
width: 100%;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.status-tag {
margin: 0;
&.active {
color: var(--success-500);
background: var(--success-100);
border-color: var(--success-500);
}
&.inactive {
color: var(--error-500);
background: var(--error-100);
border-color: var(--error-500);
}
}
.progress-container {
width: 158px;
.ant-progress {
margin: 0;
.ant-progress-text {
font-weight: 600;
}
}
}
.ant-card {
&.ant-card-bordered {
border: 1px solid var(--bg-slate-500) !important;
}
}
}
}
.tabs-and-search {
display: flex;
justify-content: space-between;
align-items: center;
margin: 16px 0;
.action-btn {
border-radius: 2px;
border: 1px solid var(--bg-slate-400);
background: var(--bg-ink-300);
box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.1);
display: flex;
align-items: center;
justify-content: center;
}
}
.views-tabs-container {
margin-top: 1.5rem;
display: flex;
justify-content: space-between;
align-items: center;
.views-tabs {
color: var(--text-vanilla-400);
.view-title {
display: flex;
gap: var(--margin-2);
align-items: center;
justify-content: center;
font-size: var(--font-size-xs);
font-style: normal;
font-weight: var(--font-weight-normal);
}
.tab {
border: 1px solid var(--bg-slate-400);
width: 114px;
}
.tab::before {
background: var(--bg-slate-400);
}
.selected_view {
background: var(--bg-slate-300);
color: var(--text-vanilla-100);
border: 1px solid var(--bg-slate-400);
}
.selected_view::before {
background: var(--bg-slate-400);
}
}
.compass-button {
width: 30px;
height: 30px;
border-radius: 2px;
border: 1px solid var(--bg-slate-400);
background: var(--bg-ink-300);
box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.1);
}
}
.ant-drawer-close {
padding: 0px;
}
}
.lightMode {
.ant-drawer-header {
border-bottom: 1px solid var(--bg-vanilla-400);
background: var(--bg-vanilla-100);
}
.namespace-detail-drawer {
.title {
color: var(--text-ink-300);
}
.namespace-detail-drawer__namespace {
.ant-typography {
color: var(--text-ink-300);
background: transparent;
}
}
.radio-button {
border: 1px solid var(--bg-vanilla-400);
background: var(--bg-vanilla-100);
color: var(--text-ink-300);
}
.views-tabs {
.tab {
background: var(--bg-vanilla-100);
}
.selected_view {
background: var(--bg-vanilla-300);
border: 1px solid var(--bg-slate-300);
color: var(--text-ink-400);
}
.selected_view::before {
background: var(--bg-vanilla-300);
border-left: 1px solid var(--bg-slate-300);
}
}
.compass-button {
border: 1px solid var(--bg-vanilla-300);
background: var(--bg-vanilla-100);
box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.1);
}
.tabs-and-search {
.action-btn {
border: 1px solid var(--bg-vanilla-400);
background: var(--bg-vanilla-100);
color: var(--text-ink-300);
}
}
}
}

View File

@@ -0,0 +1,566 @@
/* eslint-disable sonarjs/no-identical-functions */
import './NamespaceDetails.styles.scss';
import { Color, Spacing } from '@signozhq/design-tokens';
import { Button, Divider, Drawer, Radio, Tooltip, Typography } from 'antd';
import { RadioChangeEvent } from 'antd/lib';
import logEvent from 'api/common/logEvent';
import { K8sNamespacesData } from 'api/infraMonitoring/getK8sNamespacesList';
import { VIEW_TYPES, VIEWS } from 'components/HostMetricsDetail/constants';
import { QueryParams } from 'constants/query';
import {
initialQueryBuilderFormValuesMap,
initialQueryState,
} from 'constants/queryBuilder';
import ROUTES from 'constants/routes';
import { K8sCategory } from 'container/InfraMonitoringK8s/constants';
import { QUERY_KEYS } from 'container/InfraMonitoringK8s/EntityDetailsUtils/utils';
import {
CustomTimeType,
Time,
} from 'container/TopNav/DateTimeSelectionV2/config';
import { useIsDarkMode } from 'hooks/useDarkMode';
import useUrlQuery from 'hooks/useUrlQuery';
import GetMinMax from 'lib/getMinMax';
import {
BarChart2,
ChevronsLeftRight,
Compass,
DraftingCompass,
ScrollText,
X,
} from 'lucide-react';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
import {
IBuilderQuery,
TagFilterItem,
} from 'types/api/queryBuilder/queryBuilderData';
import {
LogsAggregatorOperator,
TracesAggregatorOperator,
} from 'types/common/queryBuilder';
import { GlobalReducer } from 'types/reducer/globalTime';
import { v4 as uuidv4 } from 'uuid';
import NamespaceEvents from '../../EntityDetailsUtils/EntityEvents';
import NamespaceLogs from '../../EntityDetailsUtils/EntityLogs';
import NamespaceMetrics from '../../EntityDetailsUtils/EntityMetrics';
import NamespaceTraces from '../../EntityDetailsUtils/EntityTraces';
import {
getNamespaceMetricsQueryPayload,
namespaceWidgetInfo,
} from './constants';
import { NamespaceDetailsProps } from './NamespaceDetails.interfaces';
function NamespaceDetails({
namespace,
onClose,
isModalTimeSelection,
}: NamespaceDetailsProps): JSX.Element {
const { maxTime, minTime, selectedTime } = useSelector<
AppState,
GlobalReducer
>((state) => state.globalTime);
const startMs = useMemo(() => Math.floor(Number(minTime) / 1000000000), [
minTime,
]);
const endMs = useMemo(() => Math.floor(Number(maxTime) / 1000000000), [
maxTime,
]);
const urlQuery = useUrlQuery();
const [modalTimeRange, setModalTimeRange] = useState(() => ({
startTime: startMs,
endTime: endMs,
}));
const [selectedInterval, setSelectedInterval] = useState<Time>(
selectedTime as Time,
);
const [selectedView, setSelectedView] = useState<VIEWS>(VIEWS.METRICS);
const isDarkMode = useIsDarkMode();
const initialFilters = useMemo(
() => ({
op: 'AND',
items: [
{
id: uuidv4(),
key: {
key: QUERY_KEYS.K8S_NAMESPACE_NAME,
dataType: DataTypes.String,
type: 'resource',
isColumn: false,
isJSON: false,
id: 'k8s_namespace_name--string--resource--false',
},
op: '=',
value: namespace?.namespaceName || '',
},
],
}),
[namespace?.namespaceName],
);
const initialEventsFilters = useMemo(
() => ({
op: 'AND',
items: [
{
id: uuidv4(),
key: {
key: QUERY_KEYS.K8S_OBJECT_KIND,
dataType: DataTypes.String,
type: 'resource',
isColumn: false,
isJSON: false,
id: 'k8s.object.kind--string--resource--false',
},
op: '=',
value: 'Namespace',
},
{
id: uuidv4(),
key: {
key: QUERY_KEYS.K8S_OBJECT_NAME,
dataType: DataTypes.String,
type: 'resource',
isColumn: false,
isJSON: false,
id: 'k8s.object.name--string--resource--false',
},
op: '=',
value: namespace?.namespaceName || '',
},
],
}),
[namespace?.namespaceName],
);
const [logFilters, setLogFilters] = useState<IBuilderQuery['filters']>(
initialFilters,
);
const [tracesFilters, setTracesFilters] = useState<IBuilderQuery['filters']>(
initialFilters,
);
const [eventsFilters, setEventsFilters] = useState<IBuilderQuery['filters']>(
initialEventsFilters,
);
useEffect(() => {
logEvent('Infra Monitoring: Namespaces list details page visited', {
namespace: namespace?.namespaceName,
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useEffect(() => {
setLogFilters(initialFilters);
setTracesFilters(initialFilters);
setEventsFilters(initialEventsFilters);
}, [initialFilters, initialEventsFilters]);
useEffect(() => {
setSelectedInterval(selectedTime as Time);
if (selectedTime !== 'custom') {
const { maxTime, minTime } = GetMinMax(selectedTime);
setModalTimeRange({
startTime: Math.floor(minTime / 1000000000),
endTime: Math.floor(maxTime / 1000000000),
});
}
}, [selectedTime, minTime, maxTime]);
const handleTabChange = (e: RadioChangeEvent): void => {
setSelectedView(e.target.value);
};
const handleTimeChange = useCallback(
(interval: Time | CustomTimeType, dateTimeRange?: [number, number]): void => {
setSelectedInterval(interval as Time);
if (interval === 'custom' && dateTimeRange) {
setModalTimeRange({
startTime: Math.floor(dateTimeRange[0] / 1000),
endTime: Math.floor(dateTimeRange[1] / 1000),
});
} else {
const { maxTime, minTime } = GetMinMax(interval);
setModalTimeRange({
startTime: Math.floor(minTime / 1000000000),
endTime: Math.floor(maxTime / 1000000000),
});
}
logEvent('Infra Monitoring: Namespaces list details time updated', {
namespace: namespace?.namespaceName,
interval,
});
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[],
);
const handleChangeLogFilters = useCallback(
(value: IBuilderQuery['filters']) => {
setLogFilters((prevFilters) => {
const primaryFilters = prevFilters.items.filter((item) =>
[QUERY_KEYS.K8S_NAMESPACE_NAME, QUERY_KEYS.K8S_CLUSTER_NAME].includes(
item.key?.key ?? '',
),
);
const paginationFilter = value.items.find((item) => item.key?.key === 'id');
const newFilters = value.items.filter(
(item) =>
item.key?.key !== 'id' && item.key?.key !== QUERY_KEYS.K8S_NAMESPACE_NAME,
);
logEvent('Infra Monitoring: Namespaces list details logs filters applied', {
namespace: namespace?.namespaceName,
});
return {
op: 'AND',
items: [
...primaryFilters,
...newFilters,
...(paginationFilter ? [paginationFilter] : []),
].filter((item): item is TagFilterItem => item !== undefined),
};
});
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[],
);
const handleChangeTracesFilters = useCallback(
(value: IBuilderQuery['filters']) => {
setTracesFilters((prevFilters) => {
const primaryFilters = prevFilters.items.filter((item) =>
[QUERY_KEYS.K8S_NAMESPACE_NAME, QUERY_KEYS.K8S_CLUSTER_NAME].includes(
item.key?.key ?? '',
),
);
logEvent(
'Infra Monitoring: Namespaces list details traces filters applied',
{
namespace: namespace?.namespaceName,
},
);
return {
op: 'AND',
items: [
...primaryFilters,
...value.items.filter(
(item) => item.key?.key !== QUERY_KEYS.K8S_NAMESPACE_NAME,
),
].filter((item): item is TagFilterItem => item !== undefined),
};
});
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[],
);
const handleChangeEventsFilters = useCallback(
(value: IBuilderQuery['filters']) => {
setEventsFilters((prevFilters) => {
const namespaceKindFilter = prevFilters.items.find(
(item) => item.key?.key === QUERY_KEYS.K8S_OBJECT_KIND,
);
const namespaceNameFilter = prevFilters.items.find(
(item) => item.key?.key === QUERY_KEYS.K8S_OBJECT_NAME,
);
logEvent(
'Infra Monitoring: Namespaces list details events filters applied',
{
namespace: namespace?.namespaceName,
},
);
return {
op: 'AND',
items: [
namespaceKindFilter,
namespaceNameFilter,
...value.items.filter(
(item) =>
item.key?.key !== QUERY_KEYS.K8S_OBJECT_KIND &&
item.key?.key !== QUERY_KEYS.K8S_OBJECT_NAME,
),
].filter((item): item is TagFilterItem => item !== undefined),
};
});
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[],
);
const handleExplorePagesRedirect = (): void => {
if (selectedInterval !== 'custom') {
urlQuery.set(QueryParams.relativeTime, selectedInterval);
} else {
urlQuery.delete(QueryParams.relativeTime);
urlQuery.set(QueryParams.startTime, modalTimeRange.startTime.toString());
urlQuery.set(QueryParams.endTime, modalTimeRange.endTime.toString());
}
logEvent('Infra Monitoring: Namespaces list details explore clicked', {
namespace: namespace?.namespaceName,
view: selectedView,
});
if (selectedView === VIEW_TYPES.LOGS) {
const filtersWithoutPagination = {
...logFilters,
items: logFilters.items.filter((item) => item.key?.key !== 'id'),
};
const compositeQuery = {
...initialQueryState,
queryType: 'builder',
builder: {
...initialQueryState.builder,
queryData: [
{
...initialQueryBuilderFormValuesMap.logs,
aggregateOperator: LogsAggregatorOperator.NOOP,
filters: filtersWithoutPagination,
},
],
},
};
urlQuery.set('compositeQuery', JSON.stringify(compositeQuery));
window.open(
`${window.location.origin}${ROUTES.LOGS_EXPLORER}?${urlQuery.toString()}`,
'_blank',
);
} else if (selectedView === VIEW_TYPES.TRACES) {
const compositeQuery = {
...initialQueryState,
queryType: 'builder',
builder: {
...initialQueryState.builder,
queryData: [
{
...initialQueryBuilderFormValuesMap.traces,
aggregateOperator: TracesAggregatorOperator.NOOP,
filters: tracesFilters,
},
],
},
};
urlQuery.set('compositeQuery', JSON.stringify(compositeQuery));
window.open(
`${window.location.origin}${ROUTES.TRACES_EXPLORER}?${urlQuery.toString()}`,
'_blank',
);
}
};
const handleClose = (): void => {
setSelectedInterval(selectedTime as Time);
if (selectedTime !== 'custom') {
const { maxTime, minTime } = GetMinMax(selectedTime);
setModalTimeRange({
startTime: Math.floor(minTime / 1000000000),
endTime: Math.floor(maxTime / 1000000000),
});
}
setSelectedView(VIEW_TYPES.METRICS);
onClose();
};
return (
<Drawer
width="70%"
title={
<>
<Divider type="vertical" />
<Typography.Text className="title">
{namespace?.namespaceName}
</Typography.Text>
</>
}
placement="right"
onClose={handleClose}
open={!!namespace}
style={{
overscrollBehavior: 'contain',
background: isDarkMode ? Color.BG_INK_400 : Color.BG_VANILLA_100,
}}
className="entity-detail-drawer"
destroyOnClose
closeIcon={<X size={16} style={{ marginTop: Spacing.MARGIN_1 }} />}
>
{namespace && (
<>
<div className="entity-detail-drawer__entity">
<div className="entity-details-grid">
<div className="labels-row">
<Typography.Text
type="secondary"
className="entity-details-metadata-label"
>
Namespace Name
</Typography.Text>
<Typography.Text
type="secondary"
className="entity-details-metadata-label"
>
Cluster Name
</Typography.Text>
</div>
<div className="values-row">
<Typography.Text className="entity-details-metadata-value">
<Tooltip title={namespace.namespaceName}>
{namespace.namespaceName}
</Tooltip>
</Typography.Text>
<Typography.Text className="entity-details-metadata-value">
<Tooltip title="Cluster name">
{namespace.meta.k8s_cluster_name}
</Tooltip>
</Typography.Text>
</div>
</div>
</div>
<div className="views-tabs-container">
<Radio.Group
className="views-tabs"
onChange={handleTabChange}
value={selectedView}
>
<Radio.Button
className={
// eslint-disable-next-line sonarjs/no-duplicate-string
selectedView === VIEW_TYPES.METRICS ? 'selected_view tab' : 'tab'
}
value={VIEW_TYPES.METRICS}
>
<div className="view-title">
<BarChart2 size={14} />
Metrics
</div>
</Radio.Button>
<Radio.Button
className={
selectedView === VIEW_TYPES.LOGS ? 'selected_view tab' : 'tab'
}
value={VIEW_TYPES.LOGS}
>
<div className="view-title">
<ScrollText size={14} />
Logs
</div>
</Radio.Button>
<Radio.Button
className={
selectedView === VIEW_TYPES.TRACES ? 'selected_view tab' : 'tab'
}
value={VIEW_TYPES.TRACES}
>
<div className="view-title">
<DraftingCompass size={14} />
Traces
</div>
</Radio.Button>
<Radio.Button
className={
selectedView === VIEW_TYPES.EVENTS ? 'selected_view tab' : 'tab'
}
value={VIEW_TYPES.EVENTS}
>
<div className="view-title">
<ChevronsLeftRight size={14} />
Events
</div>
</Radio.Button>
</Radio.Group>
{(selectedView === VIEW_TYPES.LOGS ||
selectedView === VIEW_TYPES.TRACES) && (
<Button
icon={<Compass size={18} />}
className="compass-button"
onClick={handleExplorePagesRedirect}
/>
)}
</div>
{selectedView === VIEW_TYPES.METRICS && (
<NamespaceMetrics<K8sNamespacesData>
timeRange={modalTimeRange}
isModalTimeSelection={isModalTimeSelection}
handleTimeChange={handleTimeChange}
selectedInterval={selectedInterval}
entity={namespace}
entityWidgetInfo={namespaceWidgetInfo}
getEntityQueryPayload={getNamespaceMetricsQueryPayload}
category={K8sCategory.NAMESPACES}
queryKey="namespaceMetrics"
/>
)}
{selectedView === VIEW_TYPES.LOGS && (
<NamespaceLogs
timeRange={modalTimeRange}
isModalTimeSelection={isModalTimeSelection}
handleTimeChange={handleTimeChange}
handleChangeLogFilters={handleChangeLogFilters}
logFilters={logFilters}
selectedInterval={selectedInterval}
queryKey="namespaceLogs"
category={K8sCategory.NAMESPACES}
queryKeyFilters={[QUERY_KEYS.K8S_NAMESPACE_NAME]}
/>
)}
{selectedView === VIEW_TYPES.TRACES && (
<NamespaceTraces
timeRange={modalTimeRange}
isModalTimeSelection={isModalTimeSelection}
handleTimeChange={handleTimeChange}
handleChangeTracesFilters={handleChangeTracesFilters}
tracesFilters={tracesFilters}
selectedInterval={selectedInterval}
queryKey="namespaceTraces"
/>
)}
{selectedView === VIEW_TYPES.EVENTS && (
<NamespaceEvents
timeRange={modalTimeRange}
handleChangeEventFilters={handleChangeEventsFilters}
filters={eventsFilters}
isModalTimeSelection={isModalTimeSelection}
handleTimeChange={handleTimeChange}
selectedInterval={selectedInterval}
category={K8sCategory.NAMESPACES}
queryKey="namespaceEvents"
/>
)}
</>
)}
</Drawer>
);
}
export default NamespaceDetails;

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