Compare commits
39 Commits
v0.67.0
...
fix/range-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8c7ae68f35 | ||
|
|
c565c2b865 | ||
|
|
4ec1e66c7e | ||
|
|
89541862cc | ||
|
|
610f4d43e7 | ||
|
|
044a124cc1 | ||
|
|
0cf9003e3a | ||
|
|
644135a933 | ||
|
|
b465f74e4a | ||
|
|
e00e365964 | ||
|
|
5c45e1f7b3 | ||
|
|
16e61b45ac | ||
|
|
fdcdbf021a | ||
|
|
c92ef53e9c | ||
|
|
268f283785 | ||
|
|
c574adc634 | ||
|
|
939ab5270e | ||
|
|
42525b6067 | ||
|
|
c66cd3ce4e | ||
|
|
e9618d64bc | ||
|
|
8e11a988be | ||
|
|
92299e1b08 | ||
|
|
bab8c8274c | ||
|
|
265c67e5bd | ||
|
|
efc8c95d59 | ||
|
|
5708079c3c | ||
|
|
dbe78e55a9 | ||
|
|
a60371fb80 | ||
|
|
d5b847c091 | ||
|
|
c106f1c9a9 | ||
|
|
d6bfd95302 | ||
|
|
68ee677630 | ||
|
|
3ff862b483 | ||
|
|
f91badbce9 | ||
|
|
2ead4fbb66 | ||
|
|
56b17bcfef | ||
|
|
5839b65f7a | ||
|
|
3787c5ca24 | ||
|
|
458cd28cc2 |
@@ -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
|
||||
70
conf/example.yaml
Normal file
70
conf/example.yaml
Normal file
@@ -0,0 +1,70 @@
|
||||
##################### SigNoz Configuration Example #####################
|
||||
#
|
||||
# Do not modify this file
|
||||
#
|
||||
|
||||
##################### Instrumentation #####################
|
||||
instrumentation:
|
||||
logs:
|
||||
level: info
|
||||
enabled: false
|
||||
processors:
|
||||
batch:
|
||||
exporter:
|
||||
otlp:
|
||||
endpoint: localhost:4317
|
||||
traces:
|
||||
enabled: false
|
||||
processors:
|
||||
batch:
|
||||
exporter:
|
||||
otlp:
|
||||
endpoint: localhost:4317
|
||||
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
|
||||
@@ -130,7 +130,7 @@ services:
|
||||
restart_policy:
|
||||
condition: on-failure
|
||||
query-service:
|
||||
image: signoz/query-service:0.67.0
|
||||
image: signoz/query-service:0.68.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.67.0
|
||||
image: signoz/frontend:0.68.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.22
|
||||
image: signoz/signoz-otel-collector:0.111.23
|
||||
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.22
|
||||
image: signoz/signoz-schema-migrator:0.111.23
|
||||
deploy:
|
||||
restart_policy:
|
||||
condition: on-failure
|
||||
|
||||
@@ -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.22}
|
||||
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.111.23}
|
||||
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.22}
|
||||
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.111.23}
|
||||
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:
|
||||
|
||||
@@ -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.67.0}
|
||||
image: signoz/query-service:${DOCKER_TAG:-0.68.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.67.0}
|
||||
image: signoz/frontend:${DOCKER_TAG:-0.68.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.22}
|
||||
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.111.23}
|
||||
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.22}
|
||||
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.111.23}
|
||||
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.22}
|
||||
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.111.23}
|
||||
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
|
||||
|
||||
@@ -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.67.0}
|
||||
image: signoz/query-service:${DOCKER_TAG:-0.68.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.67.0}
|
||||
image: signoz/frontend:${DOCKER_TAG:-0.68.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.22}
|
||||
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.111.23}
|
||||
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.22}
|
||||
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.111.23}
|
||||
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
|
||||
|
||||
@@ -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 "+++++++++++++++++++++++++++++++++++++++++++++++++"
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -30,7 +30,6 @@ import (
|
||||
"go.signoz.io/signoz/ee/query-service/interfaces"
|
||||
"go.signoz.io/signoz/ee/query-service/rules"
|
||||
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 +39,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"
|
||||
@@ -81,7 +81,6 @@ type ServerOptions struct {
|
||||
GatewayUrl string
|
||||
UseLogsNewSchema bool
|
||||
UseTraceNewSchema bool
|
||||
SkipWebFrontend bool
|
||||
}
|
||||
|
||||
// Server runs HTTP api service
|
||||
@@ -201,13 +200,6 @@ 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)
|
||||
if err != nil {
|
||||
@@ -221,6 +213,13 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) {
|
||||
)
|
||||
}
|
||||
|
||||
cloudIntegrationsController, err := cloudintegrations.NewController(localDB)
|
||||
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,
|
||||
@@ -271,6 +270,7 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) {
|
||||
FeatureFlags: lm,
|
||||
LicenseManager: lm,
|
||||
IntegrationsController: integrationsController,
|
||||
CloudIntegrationsController: cloudIntegrationsController,
|
||||
LogsParsingPipelineController: logParsingPipelineController,
|
||||
Cache: c,
|
||||
FluxInterval: fluxInterval,
|
||||
@@ -342,7 +342,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()
|
||||
|
||||
@@ -370,6 +370,7 @@ func (s *Server) createPublicServer(apiHandler *api.APIHandler, web *web.Web) (*
|
||||
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 +387,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{
|
||||
@@ -502,32 +501,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
|
||||
|
||||
@@ -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"
|
||||
@@ -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")
|
||||
@@ -126,7 +125,6 @@ func main() {
|
||||
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,19 +134,18 @@ 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))
|
||||
}
|
||||
@@ -171,7 +168,6 @@ func main() {
|
||||
GatewayUrl: gatewayUrl,
|
||||
UseLogsNewSchema: useLogsNewSchema,
|
||||
UseTraceNewSchema: useTraceNewSchema,
|
||||
SkipWebFrontend: skipWebFrontend,
|
||||
}
|
||||
|
||||
// Read the jwt secret key
|
||||
|
||||
@@ -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/'],
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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[] =
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -14,6 +14,7 @@ export const getInfraAttributesValues = async ({
|
||||
filterAttributeKeyDataType,
|
||||
tagType,
|
||||
searchText,
|
||||
aggregateAttribute,
|
||||
}: IGetAttributeValuesPayload): Promise<
|
||||
SuccessResponse<IAttributeValuesResponse> | ErrorResponse
|
||||
> => {
|
||||
@@ -23,6 +24,7 @@ export const getInfraAttributesValues = async ({
|
||||
dataSource,
|
||||
attributeKey,
|
||||
searchText,
|
||||
aggregateAttribute,
|
||||
})}&filterAttributeKeyDataType=${filterAttributeKeyDataType}&tagType=${tagType}`,
|
||||
);
|
||||
|
||||
|
||||
65
frontend/src/api/infraMonitoring/getK8sNodesList.ts
Normal file
65
frontend/src/api/infraMonitoring/getK8sNodesList.ts
Normal 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);
|
||||
}
|
||||
};
|
||||
93
frontend/src/api/infraMonitoring/getK8sPodsList.ts
Normal file
93
frontend/src/api/infraMonitoring/getK8sPodsList.ts
Normal 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);
|
||||
}
|
||||
};
|
||||
@@ -71,6 +71,7 @@ function RangePickerModal(props: RangePickerModalProps): JSX.Element {
|
||||
showTime={{
|
||||
use12Hours: true,
|
||||
format: 'hh:mm A',
|
||||
defaultValue: [dayjs().tz(timezone.value), dayjs().tz(timezone.value)],
|
||||
}}
|
||||
format={(date: Dayjs): string =>
|
||||
date.tz(timezone.value).format('YYYY-MM-DD hh:mm A')
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -219,12 +219,14 @@ function ListLogView({
|
||||
<LogStateIndicator type={logType} fontSize={fontSize} />
|
||||
<div>
|
||||
<LogContainer fontSize={fontSize}>
|
||||
<LogGeneralField
|
||||
fieldKey="Log"
|
||||
fieldValue={flattenLogData.body}
|
||||
linesPerRow={linesPerRow}
|
||||
fontSize={fontSize}
|
||||
/>
|
||||
{updatedSelecedFields.some((field) => field.name === 'body') && (
|
||||
<LogGeneralField
|
||||
fieldKey="Log"
|
||||
fieldValue={flattenLogData.body}
|
||||
linesPerRow={linesPerRow}
|
||||
fontSize={fontSize}
|
||||
/>
|
||||
)}
|
||||
{flattenLogData.stream && (
|
||||
<LogGeneralField
|
||||
fieldKey="Stream"
|
||||
@@ -232,23 +234,27 @@ function ListLogView({
|
||||
fontSize={fontSize}
|
||||
/>
|
||||
)}
|
||||
<LogGeneralField
|
||||
fieldKey="Timestamp"
|
||||
fieldValue={timestampValue}
|
||||
fontSize={fontSize}
|
||||
/>
|
||||
|
||||
{updatedSelecedFields.map((field) =>
|
||||
isValidLogField(flattenLogData[field.name] as never) ? (
|
||||
<LogSelectedField
|
||||
key={field.name}
|
||||
fieldKey={field.name}
|
||||
fieldValue={flattenLogData[field.name] as never}
|
||||
onAddToQuery={onAddToQuery}
|
||||
fontSize={fontSize}
|
||||
/>
|
||||
) : null,
|
||||
{updatedSelecedFields.some((field) => field.name === 'timestamp') && (
|
||||
<LogGeneralField
|
||||
fieldKey="Timestamp"
|
||||
fieldValue={timestampValue}
|
||||
fontSize={fontSize}
|
||||
/>
|
||||
)}
|
||||
|
||||
{updatedSelecedFields
|
||||
.filter((field) => !['timestamp', 'body'].includes(field.name))
|
||||
.map((field) =>
|
||||
isValidLogField(flattenLogData[field.name] as never) ? (
|
||||
<LogSelectedField
|
||||
key={field.name}
|
||||
fieldKey={field.name}
|
||||
fieldValue={flattenLogData[field.name] as never}
|
||||
onAddToQuery={onAddToQuery}
|
||||
fontSize={fontSize}
|
||||
/>
|
||||
) : null,
|
||||
)}
|
||||
</LogContainer>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -73,6 +73,7 @@ function RawLogView({
|
||||
);
|
||||
|
||||
const attributesValues = updatedSelecedFields
|
||||
.filter((field) => !['timestamp', 'body'].includes(field.name))
|
||||
.map((field) => flattenLogData[field.name])
|
||||
.filter((attribute) => {
|
||||
// loadash isEmpty doesnot work with numbers
|
||||
@@ -92,19 +93,40 @@ function RawLogView({
|
||||
const { formatTimezoneAdjustedTimestamp } = useTimezone();
|
||||
|
||||
const text = useMemo(() => {
|
||||
const date =
|
||||
typeof data.timestamp === 'string'
|
||||
? formatTimezoneAdjustedTimestamp(data.timestamp, 'YYYY-MM-DD HH:mm:ss.SSS')
|
||||
: formatTimezoneAdjustedTimestamp(
|
||||
data.timestamp / 1e6,
|
||||
'YYYY-MM-DD HH:mm:ss.SSS',
|
||||
);
|
||||
const parts = [];
|
||||
|
||||
return `${date} | ${attributesText} ${data.body}`;
|
||||
// Check if timestamp is selected
|
||||
const showTimestamp = selectedFields.some(
|
||||
(field) => field.name === 'timestamp',
|
||||
);
|
||||
if (showTimestamp) {
|
||||
const date =
|
||||
typeof data.timestamp === 'string'
|
||||
? formatTimezoneAdjustedTimestamp(
|
||||
data.timestamp,
|
||||
'YYYY-MM-DD HH:mm:ss.SSS',
|
||||
)
|
||||
: formatTimezoneAdjustedTimestamp(
|
||||
data.timestamp / 1e6,
|
||||
'YYYY-MM-DD HH:mm:ss.SSS',
|
||||
);
|
||||
parts.push(date);
|
||||
}
|
||||
|
||||
// Check if body is selected
|
||||
const showBody = selectedFields.some((field) => field.name === 'body');
|
||||
if (showBody) {
|
||||
parts.push(`${attributesText} ${data.body}`);
|
||||
} else {
|
||||
parts.push(attributesText);
|
||||
}
|
||||
|
||||
return parts.join(' | ');
|
||||
}, [
|
||||
selectedFields,
|
||||
attributesText,
|
||||
data.timestamp,
|
||||
data.body,
|
||||
attributesText,
|
||||
formatTimezoneAdjustedTimestamp,
|
||||
]);
|
||||
|
||||
|
||||
@@ -48,7 +48,7 @@ export const useTableView = (props: UseTableViewProps): UseTableViewResult => {
|
||||
|
||||
const columns: ColumnsType<Record<string, unknown>> = useMemo(() => {
|
||||
const fieldColumns: ColumnsType<Record<string, unknown>> = fields
|
||||
.filter((e) => e.name !== 'id')
|
||||
.filter((e) => !['id', 'body', 'timestamp'].includes(e.name))
|
||||
.map(({ name }) => ({
|
||||
title: name,
|
||||
dataIndex: name,
|
||||
@@ -91,55 +91,67 @@ export const useTableView = (props: UseTableViewProps): UseTableViewResult => {
|
||||
),
|
||||
}),
|
||||
},
|
||||
{
|
||||
title: 'timestamp',
|
||||
dataIndex: 'timestamp',
|
||||
key: 'timestamp',
|
||||
// https://github.com/ant-design/ant-design/discussions/36886
|
||||
render: (field): ColumnTypeRender<Record<string, unknown>> => {
|
||||
const date =
|
||||
typeof field === 'string'
|
||||
? formatTimezoneAdjustedTimestamp(field, 'YYYY-MM-DD HH:mm:ss.SSS')
|
||||
: formatTimezoneAdjustedTimestamp(
|
||||
field / 1e6,
|
||||
'YYYY-MM-DD HH:mm:ss.SSS',
|
||||
);
|
||||
return {
|
||||
children: (
|
||||
<div className="table-timestamp">
|
||||
<Typography.Paragraph ellipsis className={cx('text', fontSize)}>
|
||||
{date}
|
||||
</Typography.Paragraph>
|
||||
</div>
|
||||
),
|
||||
};
|
||||
},
|
||||
},
|
||||
...(fields.some((field) => field.name === 'timestamp')
|
||||
? [
|
||||
{
|
||||
title: 'timestamp',
|
||||
dataIndex: 'timestamp',
|
||||
key: 'timestamp',
|
||||
// https://github.com/ant-design/ant-design/discussions/36886
|
||||
render: (
|
||||
field: string | number,
|
||||
): ColumnTypeRender<Record<string, unknown>> => {
|
||||
const date =
|
||||
typeof field === 'string'
|
||||
? formatTimezoneAdjustedTimestamp(field, 'YYYY-MM-DD HH:mm:ss.SSS')
|
||||
: formatTimezoneAdjustedTimestamp(
|
||||
field / 1e6,
|
||||
'YYYY-MM-DD HH:mm:ss.SSS',
|
||||
);
|
||||
return {
|
||||
children: (
|
||||
<div className="table-timestamp">
|
||||
<Typography.Paragraph ellipsis className={cx('text', fontSize)}>
|
||||
{date}
|
||||
</Typography.Paragraph>
|
||||
</div>
|
||||
),
|
||||
};
|
||||
},
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(appendTo === 'center' ? fieldColumns : []),
|
||||
{
|
||||
title: 'body',
|
||||
dataIndex: 'body',
|
||||
key: 'body',
|
||||
render: (field): ColumnTypeRender<Record<string, unknown>> => ({
|
||||
props: {
|
||||
style: defaultTableStyle,
|
||||
},
|
||||
children: (
|
||||
<TableBodyContent
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: convert.toHtml(
|
||||
dompurify.sanitize(unescapeString(field), {
|
||||
FORBID_TAGS: [...FORBID_DOM_PURIFY_TAGS],
|
||||
}),
|
||||
...(fields.some((field) => field.name === 'body')
|
||||
? [
|
||||
{
|
||||
title: 'body',
|
||||
dataIndex: 'body',
|
||||
key: 'body',
|
||||
render: (
|
||||
field: string | number,
|
||||
): ColumnTypeRender<Record<string, unknown>> => ({
|
||||
props: {
|
||||
style: defaultTableStyle,
|
||||
},
|
||||
children: (
|
||||
<TableBodyContent
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: convert.toHtml(
|
||||
dompurify.sanitize(unescapeString(field as string), {
|
||||
FORBID_TAGS: [...FORBID_DOM_PURIFY_TAGS],
|
||||
}),
|
||||
),
|
||||
}}
|
||||
fontSize={fontSize}
|
||||
linesPerRow={linesPerRow}
|
||||
isDarkMode={isDarkMode}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
fontSize={fontSize}
|
||||
linesPerRow={linesPerRow}
|
||||
isDarkMode={isDarkMode}
|
||||
/>
|
||||
),
|
||||
}),
|
||||
},
|
||||
}),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(appendTo === 'end' ? fieldColumns : []),
|
||||
];
|
||||
}, [
|
||||
|
||||
@@ -53,7 +53,7 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
width: calc(100% - 24px);
|
||||
cursor: pointer;
|
||||
|
||||
&.filter-disabled {
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -21,4 +21,5 @@ 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',
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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',
|
||||
|
||||
|
||||
@@ -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 ? (
|
||||
|
||||
@@ -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',
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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',
|
||||
);
|
||||
});
|
||||
});
|
||||
47
frontend/src/container/CreateAlertRule/constants.ts
Normal file
47
frontend/src/container/CreateAlertRule/constants.ts
Normal 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',
|
||||
};
|
||||
@@ -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);
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -168,7 +168,8 @@ function HostsList(): JSX.Element {
|
||||
const showHostsEmptyState =
|
||||
!isFetching &&
|
||||
!isLoading &&
|
||||
(!sentAnyHostMetricsData || isSendingIncorrectK8SAgentMetrics);
|
||||
(!sentAnyHostMetricsData || isSendingIncorrectK8SAgentMetrics) &&
|
||||
!filters.items.length;
|
||||
|
||||
return (
|
||||
<div className="hosts-list">
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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="" />,
|
||||
|
||||
@@ -0,0 +1,878 @@
|
||||
.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
322
frontend/src/container/InfraMonitoringK8s/InfraMonitoringK8s.tsx
Normal file
322
frontend/src/container/InfraMonitoringK8s/InfraMonitoringK8s.tsx
Normal file
@@ -0,0 +1,322 @@
|
||||
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 { Container, Workflow } from 'lucide-react';
|
||||
import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFallback';
|
||||
import { useState } from 'react';
|
||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
import {
|
||||
K8sCategories,
|
||||
NodesQuickFiltersConfig,
|
||||
PodsQuickFiltersConfig,
|
||||
} from './constants';
|
||||
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>Namespace</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}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Sentry.ErrorBoundary>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
165
frontend/src/container/InfraMonitoringK8s/K8sHeader.tsx
Normal file
165
frontend/src/container/InfraMonitoringK8s/K8sHeader.tsx
Normal 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;
|
||||
@@ -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;
|
||||
@@ -0,0 +1,17 @@
|
||||
.infra-monitoring-container {
|
||||
.nodes-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;
|
||||
}
|
||||
}
|
||||
}
|
||||
498
frontend/src/container/InfraMonitoringK8s/Nodes/K8sNodesList.tsx
Normal file
498
frontend/src/container/InfraMonitoringK8s/Nodes/K8sNodesList.tsx
Normal file
@@ -0,0 +1,498 @@
|
||||
/* eslint-disable no-restricted-syntax */
|
||||
/* eslint-disable @typescript-eslint/explicit-function-return-type */
|
||||
import '../InfraMonitoringK8s.styles.scss';
|
||||
import './K8sNodesList.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 { K8sNodesListPayload } from 'api/infraMonitoring/getK8sNodesList';
|
||||
import { useGetK8sNodesList } from 'hooks/infraMonitoring/useGetK8sNodesList';
|
||||
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 NodeDetails from './NodeDetails';
|
||||
import {
|
||||
defaultAddedColumns,
|
||||
formatDataForTable,
|
||||
getK8sNodesListColumns,
|
||||
getK8sNodesListQuery,
|
||||
K8sNodesRowData,
|
||||
} from './utils';
|
||||
|
||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||
function K8sNodesList({
|
||||
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 [selectedNodeUID, setselectedNodeUID] = useState<string | null>(null);
|
||||
|
||||
const pageSize = 10;
|
||||
|
||||
const [groupBy, setGroupBy] = useState<IBuilderQuery['groupBy']>([]);
|
||||
|
||||
const [selectedRowData, setSelectedRowData] = useState<K8sNodesRowData | 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: K8sNodesRowData,
|
||||
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 = getK8sNodesListQuery();
|
||||
|
||||
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,
|
||||
} = useGetK8sNodesList(fetchGroupedByRowDataQuery as K8sNodesListPayload, {
|
||||
queryKey: ['nodeList', fetchGroupedByRowDataQuery],
|
||||
enabled: !!fetchGroupedByRowDataQuery && !!selectedRowData,
|
||||
});
|
||||
|
||||
const {
|
||||
data: groupByFiltersData,
|
||||
isLoading: isLoadingGroupByFilters,
|
||||
} = useGetAggregateKeys(
|
||||
{
|
||||
dataSource: currentQuery.builder.queryData[0].dataSource,
|
||||
aggregateAttribute: K8sEntityToAggregateAttributeMapping[K8sCategory.NODES],
|
||||
aggregateOperator: 'noop',
|
||||
searchText: '',
|
||||
tagType: '',
|
||||
},
|
||||
{
|
||||
queryKey: [currentQuery.builder.queryData[0].dataSource, 'noop'],
|
||||
},
|
||||
true,
|
||||
K8sCategory.NODES,
|
||||
);
|
||||
|
||||
const query = useMemo(() => {
|
||||
const baseQuery = getK8sNodesListQuery();
|
||||
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 formattedGroupedByNodesData = useMemo(
|
||||
() =>
|
||||
formatDataForTable(groupedByRowData?.payload?.data?.records || [], groupBy),
|
||||
[groupedByRowData, groupBy],
|
||||
);
|
||||
|
||||
const { data, isFetching, isLoading, isError } = useGetK8sNodesList(
|
||||
query as K8sNodesListPayload,
|
||||
{
|
||||
queryKey: ['nodeList', query],
|
||||
enabled: !!query,
|
||||
},
|
||||
);
|
||||
|
||||
const nodesData = useMemo(() => data?.payload?.data?.records || [], [data]);
|
||||
const totalCount = data?.payload?.data?.total || 0;
|
||||
|
||||
const formattedNodesData = useMemo(
|
||||
() => formatDataForTable(nodesData, groupBy),
|
||||
[nodesData, groupBy],
|
||||
);
|
||||
|
||||
const columns = useMemo(() => getK8sNodesListColumns(groupBy), [groupBy]);
|
||||
|
||||
const handleGroupByRowClick = (record: K8sNodesRowData): 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<K8sNodesRowData>['onChange'] = useCallback(
|
||||
(
|
||||
pagination: TablePaginationConfig,
|
||||
_filters: Record<string, (string | number | boolean)[] | null>,
|
||||
sorter: SorterResult<K8sNodesRowData> | SorterResult<K8sNodesRowData>[],
|
||||
): 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 selectedNodeData = useMemo(() => {
|
||||
if (!selectedNodeUID) return null;
|
||||
return nodesData.find((node) => node.nodeUID === selectedNodeUID) || null;
|
||||
}, [selectedNodeUID, nodesData]);
|
||||
|
||||
const handleRowClick = (record: K8sNodesRowData): void => {
|
||||
if (groupBy.length === 0) {
|
||||
setSelectedRowData(null);
|
||||
setselectedNodeUID(record.nodeUID);
|
||||
} else {
|
||||
handleGroupByRowClick(record);
|
||||
}
|
||||
|
||||
logEvent('Infra Monitoring: K8s node list item clicked', {
|
||||
nodeUID: record.nodeUID,
|
||||
});
|
||||
};
|
||||
|
||||
const nestedColumns = useMemo(() => getK8sNodesListColumns([]), []);
|
||||
|
||||
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
|
||||
className="expanded-table-view"
|
||||
columns={nestedColumns as ColumnType<K8sNodesRowData>[]}
|
||||
dataSource={formattedGroupedByNodesData}
|
||||
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: K8sNodesRowData,
|
||||
e: React.MouseEvent<HTMLButtonElement>,
|
||||
) => void;
|
||||
record: K8sNodesRowData;
|
||||
}): 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 handleCloseNodeDetail = (): void => {
|
||||
setselectedNodeUID(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);
|
||||
}
|
||||
}
|
||||
|
||||
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 nodes-list-table"
|
||||
dataSource={isFetching || isLoading ? [] : formattedNodesData}
|
||||
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,
|
||||
}}
|
||||
/>
|
||||
|
||||
<NodeDetails
|
||||
node={selectedNodeData}
|
||||
isModalTimeSelection
|
||||
onClose={handleCloseNodeDetail}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default K8sNodesList;
|
||||
@@ -0,0 +1,16 @@
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { Typography } from 'antd';
|
||||
import { Ghost } from 'lucide-react';
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
export default function NoEventsContainer(): React.ReactElement {
|
||||
return (
|
||||
<div className="no-logs-found">
|
||||
<Text type="secondary">
|
||||
<Ghost size={24} color={Color.BG_AMBER_500} /> No events found for this node
|
||||
in the selected time range.
|
||||
</Text>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,289 @@
|
||||
.node-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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.node-events-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
|
||||
padding: 12px;
|
||||
border-radius: 3px;
|
||||
border: 1px solid var(--bg-slate-500);
|
||||
}
|
||||
|
||||
.node-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(.nodename-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(.nodename-column-value) {
|
||||
background: var(--bg-ink-400);
|
||||
}
|
||||
|
||||
.nodename-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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.node-events-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;
|
||||
}
|
||||
}
|
||||
|
||||
.node-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;
|
||||
}
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
@@ -0,0 +1,360 @@
|
||||
/* eslint-disable no-nested-ternary */
|
||||
import './NodeEvents.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 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 { getNodesEventsQueryPayload } from './constants';
|
||||
import NoEventsContainer from './NoEventsContainer';
|
||||
|
||||
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 INodeEventsProps {
|
||||
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;
|
||||
}
|
||||
|
||||
const EventsPageSize = 10;
|
||||
|
||||
export default function Events({
|
||||
timeRange,
|
||||
handleChangeEventFilters,
|
||||
filters,
|
||||
isModalTimeSelection,
|
||||
handleTimeChange,
|
||||
selectedInterval,
|
||||
}: INodeEventsProps): JSX.Element {
|
||||
const { currentQuery } = useQueryBuilder();
|
||||
|
||||
const [formattedNodeEvents, setFormattedNodeEvents] = 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 = getNodesEventsQueryPayload(
|
||||
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: ['nodeEvents', 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,
|
||||
}),
|
||||
);
|
||||
|
||||
setFormattedNodeEvents(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 (!formattedNodeEvents.length) return;
|
||||
|
||||
setPage(page - 1);
|
||||
|
||||
const firstEvent = formattedNodeEvents[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 (!formattedNodeEvents.length) return;
|
||||
|
||||
setPage(page + 1);
|
||||
const lastEvent = formattedNodeEvents[formattedNodeEvents.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="node-events-container">
|
||||
<div className="node-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 && formattedNodeEvents.length === 0 && (
|
||||
<NoEventsContainer />
|
||||
)}
|
||||
|
||||
{isError && !isLoading && <LogsError />}
|
||||
|
||||
{!isLoading && !isError && formattedNodeEvents.length > 0 && (
|
||||
<div className="node-events-list-container">
|
||||
<div className="node-events-list-card">
|
||||
<Table<EventDataType>
|
||||
loading={isLoading && page > 1}
|
||||
columns={columns}
|
||||
expandable={{
|
||||
expandedRowRender: handleExpandRow,
|
||||
rowExpandable: (record): boolean => record.body !== 'Not Expandable',
|
||||
expandIcon: handleExpandRowIcon,
|
||||
}}
|
||||
dataSource={formattedNodeEvents}
|
||||
pagination={false}
|
||||
rowKey={(record): string => record.id}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!isError && formattedNodeEvents.length > 0 && (
|
||||
<div className="node-events-footer">
|
||||
<Button
|
||||
className="node-events-footer-button periscope-btn ghost"
|
||||
type="link"
|
||||
onClick={handlePrev}
|
||||
disabled={page === 1 || isFetching || isLoading}
|
||||
>
|
||||
{!isFetching && <ChevronLeft size={14} />}
|
||||
Prev
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
className="node-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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults';
|
||||
import { 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 { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
export const getNodesEventsQueryPayload = (
|
||||
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,
|
||||
});
|
||||
@@ -0,0 +1,3 @@
|
||||
import NodeEvents from './NodeEvents';
|
||||
|
||||
export default NodeEvents;
|
||||
@@ -0,0 +1,16 @@
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { Typography } from 'antd';
|
||||
import { Ghost } from 'lucide-react';
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
export default function NoLogsContainer(): React.ReactElement {
|
||||
return (
|
||||
<div className="no-logs-found">
|
||||
<Text type="secondary">
|
||||
<Ghost size={24} color={Color.BG_AMBER_500} /> No logs found for this node
|
||||
in the selected time range.
|
||||
</Text>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
.node-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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.node-logs-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
|
||||
padding: 12px;
|
||||
border-radius: 3px;
|
||||
border: 1px solid var(--bg-slate-500);
|
||||
}
|
||||
|
||||
.node-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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.node-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;
|
||||
}
|
||||
}
|
||||
|
||||
.node-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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,216 @@
|
||||
/* eslint-disable no-nested-ternary */
|
||||
import './NodeLogs.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 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 { QUERY_KEYS } from '../constants';
|
||||
import { getNodeLogsQueryPayload } from './constants';
|
||||
import NoLogsContainer from './NoLogsContainer';
|
||||
|
||||
interface Props {
|
||||
timeRange: {
|
||||
startTime: number;
|
||||
endTime: number;
|
||||
};
|
||||
handleChangeLogFilters: (filters: IBuilderQuery['filters']) => void;
|
||||
filters: IBuilderQuery['filters'];
|
||||
}
|
||||
|
||||
function PodLogs({
|
||||
timeRange,
|
||||
handleChangeLogFilters,
|
||||
filters,
|
||||
}: 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' &&
|
||||
![QUERY_KEYS.K8S_NODE_NAME, QUERY_KEYS.K8S_CLUSTER_NAME].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 = getNodeLogsQueryPayload(
|
||||
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: ['nodeLogs', 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}
|
||||
/>
|
||||
),
|
||||
[],
|
||||
);
|
||||
|
||||
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="node-logs-list-card">
|
||||
<OverlayScrollbar isVirtuoso>
|
||||
<Virtuoso
|
||||
className="node-logs-virtuoso"
|
||||
key="node-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="node-logs">
|
||||
{isLoading && <LogsLoading />}
|
||||
{!isLoading && !isError && logs.length === 0 && <NoLogsContainer />}
|
||||
{isError && !isLoading && <LogsError />}
|
||||
{!isLoading && !isError && logs.length > 0 && (
|
||||
<div className="node-logs-list-container">{renderContent}</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default PodLogs;
|
||||
@@ -0,0 +1,99 @@
|
||||
import './NodeLogs.styles.scss';
|
||||
|
||||
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 NodeLogs from './NodeLogs';
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
function NodeLogsDetailedView({
|
||||
timeRange,
|
||||
isModalTimeSelection,
|
||||
handleTimeChange,
|
||||
handleChangeLogFilters,
|
||||
logFilters,
|
||||
selectedInterval,
|
||||
}: 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="node-logs-container">
|
||||
<div className="node-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>
|
||||
<NodeLogs
|
||||
timeRange={timeRange}
|
||||
handleChangeLogFilters={handleChangeLogFilters}
|
||||
filters={logFilters}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default NodeLogsDetailedView;
|
||||
@@ -0,0 +1,65 @@
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults';
|
||||
import { 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 { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
export const getNodeLogsQueryPayload = (
|
||||
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,
|
||||
});
|
||||
@@ -0,0 +1,3 @@
|
||||
import NodeLogs from './NodeLogsDetailedView';
|
||||
|
||||
export default NodeLogs;
|
||||
@@ -0,0 +1,45 @@
|
||||
.empty-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.node-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);
|
||||
}
|
||||
|
||||
.node-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%);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
import './NodeMetrics.styles.scss';
|
||||
|
||||
import { Card, Col, Row, Skeleton, Typography } from 'antd';
|
||||
import { K8sNodesData } from 'api/infraMonitoring/getK8sNodesList';
|
||||
import cx from 'classnames';
|
||||
import Uplot from 'components/Uplot';
|
||||
import { ENTITY_VERSION_V4 } from 'constants/app';
|
||||
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 } 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 { getNodeQueryPayload, nodeWidgetInfo } from './constants';
|
||||
|
||||
interface NodeMetricsProps {
|
||||
timeRange: {
|
||||
startTime: number;
|
||||
endTime: number;
|
||||
};
|
||||
isModalTimeSelection: boolean;
|
||||
handleTimeChange: (
|
||||
interval: Time | CustomTimeType,
|
||||
dateTimeRange?: [number, number],
|
||||
) => void;
|
||||
selectedInterval: Time;
|
||||
node: K8sNodesData;
|
||||
}
|
||||
|
||||
function NodeMetrics({
|
||||
selectedInterval,
|
||||
node,
|
||||
timeRange,
|
||||
handleTimeChange,
|
||||
isModalTimeSelection,
|
||||
}: NodeMetricsProps): JSX.Element {
|
||||
const queryPayloads = useMemo(
|
||||
() => getNodeQueryPayload(node, timeRange.startTime, timeRange.endTime),
|
||||
[node, timeRange.startTime, timeRange.endTime],
|
||||
);
|
||||
|
||||
const queries = useQueries(
|
||||
queryPayloads.map((payload) => ({
|
||||
queryKey: ['node-metrics', payload, ENTITY_VERSION_V4, 'NODE'],
|
||||
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 }) => getUPlotChartData(data?.payload)),
|
||||
[queries],
|
||||
);
|
||||
|
||||
const options = useMemo(
|
||||
() =>
|
||||
queries.map(({ data }, idx) =>
|
||||
getUPlotChartOptions({
|
||||
apiResponse: data?.payload,
|
||||
isDarkMode,
|
||||
dimensions,
|
||||
yAxisUnit: nodeWidgetInfo[idx].yAxisUnit,
|
||||
softMax: null,
|
||||
softMin: null,
|
||||
minTimeScale: timeRange.startTime,
|
||||
maxTimeScale: timeRange.endTime,
|
||||
}),
|
||||
),
|
||||
[queries, isDarkMode, dimensions, 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>;
|
||||
}
|
||||
return (
|
||||
<div
|
||||
className={cx('chart-container', {
|
||||
'no-data-container':
|
||||
!query.isLoading && !query?.data?.payload?.data?.result?.length,
|
||||
})}
|
||||
>
|
||||
<Uplot options={options[idx]} 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="node-metrics-container">
|
||||
{queries.map((query, idx) => (
|
||||
<Col span={12} key={nodeWidgetInfo[idx].title}>
|
||||
<Typography.Text>{nodeWidgetInfo[idx].title}</Typography.Text>
|
||||
<Card bordered className="node-metrics-card" ref={graphRef}>
|
||||
{renderCardContent(query, idx)}
|
||||
</Card>
|
||||
</Col>
|
||||
))}
|
||||
</Row>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default NodeMetrics;
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,3 @@
|
||||
import NodeMetrics from './NodeMetrics';
|
||||
|
||||
export default NodeMetrics;
|
||||
@@ -0,0 +1,7 @@
|
||||
import { K8sNodesData } from 'api/infraMonitoring/getK8sNodesList';
|
||||
|
||||
export type NodeDetailsProps = {
|
||||
node: K8sNodesData | null;
|
||||
isModalTimeSelection: boolean;
|
||||
onClose: () => void;
|
||||
};
|
||||
@@ -0,0 +1,247 @@
|
||||
.node-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);
|
||||
}
|
||||
|
||||
.node-detail-drawer__node {
|
||||
.node-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;
|
||||
}
|
||||
|
||||
.node-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;
|
||||
}
|
||||
|
||||
.node-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);
|
||||
}
|
||||
|
||||
.node-detail-drawer {
|
||||
.title {
|
||||
color: var(--text-ink-300);
|
||||
}
|
||||
|
||||
.node-detail-drawer__node {
|
||||
.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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,547 @@
|
||||
/* eslint-disable sonarjs/no-identical-functions */
|
||||
import './NodeDetails.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 { 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/entityDetailUtils';
|
||||
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 { QUERY_KEYS } from './constants';
|
||||
import NodeEvents from './Events';
|
||||
import NodeLogs from './Logs';
|
||||
import NodeMetrics from './Metrics';
|
||||
import { NodeDetailsProps } from './NodeDetails.interfaces';
|
||||
import NodeTraces from './Traces';
|
||||
|
||||
function NodeDetails({
|
||||
node,
|
||||
onClose,
|
||||
isModalTimeSelection,
|
||||
}: NodeDetailsProps): 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_NODE_NAME,
|
||||
dataType: DataTypes.String,
|
||||
type: 'resource',
|
||||
isColumn: false,
|
||||
isJSON: false,
|
||||
id: 'k8s_node_name--string--resource--false',
|
||||
},
|
||||
op: '=',
|
||||
value: node?.meta.k8s_node_name || '',
|
||||
},
|
||||
],
|
||||
}),
|
||||
[node?.meta.k8s_node_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: 'Node',
|
||||
},
|
||||
{
|
||||
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: node?.meta.k8s_node_name || '',
|
||||
},
|
||||
],
|
||||
}),
|
||||
[node?.meta.k8s_node_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: Nodes list details page visited', {
|
||||
node: node?.nodeUID,
|
||||
});
|
||||
// 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: Nodes list details time updated', {
|
||||
node: node?.nodeUID,
|
||||
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_NODE_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_NODE_NAME,
|
||||
);
|
||||
|
||||
logEvent('Infra Monitoring: Nodes list details logs filters applied', {
|
||||
node: node?.nodeUID,
|
||||
});
|
||||
|
||||
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_NODE_NAME, QUERY_KEYS.K8S_CLUSTER_NAME].includes(
|
||||
item.key?.key ?? '',
|
||||
),
|
||||
);
|
||||
|
||||
logEvent('Infra Monitoring: Nodes list details traces filters applied', {
|
||||
node: node?.nodeUID,
|
||||
});
|
||||
|
||||
return {
|
||||
op: 'AND',
|
||||
items: filterDuplicateFilters(
|
||||
[
|
||||
...primaryFilters,
|
||||
...value.items.filter(
|
||||
(item) => item.key?.key !== QUERY_KEYS.K8S_NODE_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 nodeKindFilter = prevFilters.items.find(
|
||||
(item) => item.key?.key === QUERY_KEYS.K8S_OBJECT_KIND,
|
||||
);
|
||||
const nodeNameFilter = prevFilters.items.find(
|
||||
(item) => item.key?.key === QUERY_KEYS.K8S_OBJECT_NAME,
|
||||
);
|
||||
|
||||
logEvent('Infra Monitoring: Nodes list details events filters applied', {
|
||||
node: node?.nodeUID,
|
||||
});
|
||||
|
||||
return {
|
||||
op: 'AND',
|
||||
items: [
|
||||
nodeKindFilter,
|
||||
nodeNameFilter,
|
||||
...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: Nodes list details explore clicked', {
|
||||
node: node?.nodeUID,
|
||||
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">
|
||||
{node?.meta.k8s_node_name}
|
||||
</Typography.Text>
|
||||
</>
|
||||
}
|
||||
placement="right"
|
||||
onClose={handleClose}
|
||||
open={!!node}
|
||||
style={{
|
||||
overscrollBehavior: 'contain',
|
||||
background: isDarkMode ? Color.BG_INK_400 : Color.BG_VANILLA_100,
|
||||
}}
|
||||
className="node-detail-drawer"
|
||||
destroyOnClose
|
||||
closeIcon={<X size={16} style={{ marginTop: Spacing.MARGIN_1 }} />}
|
||||
>
|
||||
{node && (
|
||||
<>
|
||||
<div className="node-detail-drawer__node">
|
||||
<div className="node-details-grid">
|
||||
<div className="labels-row">
|
||||
<Typography.Text
|
||||
type="secondary"
|
||||
className="node-details-metadata-label"
|
||||
>
|
||||
Node Name
|
||||
</Typography.Text>
|
||||
<Typography.Text
|
||||
type="secondary"
|
||||
className="node-details-metadata-label"
|
||||
>
|
||||
Cluster Name
|
||||
</Typography.Text>
|
||||
</div>
|
||||
<div className="values-row">
|
||||
<Typography.Text className="node-details-metadata-value">
|
||||
<Tooltip title={node.meta.k8s_node_name}>
|
||||
{node.meta.k8s_node_name}
|
||||
</Tooltip>
|
||||
</Typography.Text>
|
||||
<Typography.Text className="node-details-metadata-value">
|
||||
<Tooltip title="Cluster name">{node.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 && (
|
||||
<NodeMetrics
|
||||
timeRange={modalTimeRange}
|
||||
isModalTimeSelection={isModalTimeSelection}
|
||||
handleTimeChange={handleTimeChange}
|
||||
selectedInterval={selectedInterval}
|
||||
node={node}
|
||||
/>
|
||||
)}
|
||||
{selectedView === VIEW_TYPES.LOGS && (
|
||||
<NodeLogs
|
||||
timeRange={modalTimeRange}
|
||||
isModalTimeSelection={isModalTimeSelection}
|
||||
handleTimeChange={handleTimeChange}
|
||||
handleChangeLogFilters={handleChangeLogFilters}
|
||||
logFilters={logFilters}
|
||||
selectedInterval={selectedInterval}
|
||||
/>
|
||||
)}
|
||||
{selectedView === VIEW_TYPES.TRACES && (
|
||||
<NodeTraces
|
||||
timeRange={modalTimeRange}
|
||||
isModalTimeSelection={isModalTimeSelection}
|
||||
handleTimeChange={handleTimeChange}
|
||||
handleChangeTracesFilters={handleChangeTracesFilters}
|
||||
tracesFilters={tracesFilters}
|
||||
selectedInterval={selectedInterval}
|
||||
/>
|
||||
)}
|
||||
{selectedView === VIEW_TYPES.EVENTS && (
|
||||
<NodeEvents
|
||||
timeRange={modalTimeRange}
|
||||
handleChangeEventFilters={handleChangeEventsFilters}
|
||||
filters={eventsFilters}
|
||||
isModalTimeSelection={isModalTimeSelection}
|
||||
handleTimeChange={handleTimeChange}
|
||||
selectedInterval={selectedInterval}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Drawer>
|
||||
);
|
||||
}
|
||||
|
||||
export default NodeDetails;
|
||||
@@ -0,0 +1,193 @@
|
||||
.node-metric-traces {
|
||||
margin-top: 1rem;
|
||||
|
||||
.node-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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.node-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(.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: rgba(171, 189, 255, 0.01);
|
||||
}
|
||||
|
||||
.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-table-container::after {
|
||||
content: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.lightMode {
|
||||
.host-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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.host-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(.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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,199 @@
|
||||
import './NodeTraces.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 { getNodeTracesQueryPayload, selectedColumns } from './constants';
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
function NodeTraces({
|
||||
timeRange,
|
||||
isModalTimeSelection,
|
||||
handleTimeChange,
|
||||
handleChangeTracesFilters,
|
||||
tracesFilters,
|
||||
selectedInterval,
|
||||
}: 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(
|
||||
() =>
|
||||
getNodeTracesQueryPayload(
|
||||
timeRange.startTime,
|
||||
timeRange.endTime,
|
||||
paginationQueryData?.offset || offset,
|
||||
tracesFilters,
|
||||
),
|
||||
[
|
||||
timeRange.startTime,
|
||||
timeRange.endTime,
|
||||
offset,
|
||||
tracesFilters,
|
||||
paginationQueryData,
|
||||
],
|
||||
);
|
||||
|
||||
const { data, isLoading, isFetching, isError } = useQuery({
|
||||
queryKey: [
|
||||
'hostMetricTraces',
|
||||
timeRange.startTime,
|
||||
timeRange.endTime,
|
||||
offset,
|
||||
tracesFilters,
|
||||
DEFAULT_ENTITY_VERSION,
|
||||
paginationQueryData,
|
||||
],
|
||||
queryFn: () => GetMetricQueryRange(queryPayload, DEFAULT_ENTITY_VERSION),
|
||||
enabled: !!queryPayload,
|
||||
});
|
||||
|
||||
const traceListColumns = getListColumns(selectedColumns);
|
||||
|
||||
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="node-metric-traces">
|
||||
<div className="node-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="node-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 NodeTraces;
|
||||
@@ -0,0 +1,200 @@
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults';
|
||||
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';
|
||||
|
||||
export const columns = [
|
||||
{
|
||||
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 selectedColumns: 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 getNodeTracesQueryPayload = (
|
||||
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,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,3 @@
|
||||
import NodeTraces from './NodeTraces';
|
||||
|
||||
export default NodeTraces;
|
||||
@@ -0,0 +1,6 @@
|
||||
export const QUERY_KEYS = {
|
||||
K8S_OBJECT_KIND: 'k8s.object.kind',
|
||||
K8S_OBJECT_NAME: 'k8s.object.name',
|
||||
K8S_NODE_NAME: 'k8s.node.name',
|
||||
K8S_CLUSTER_NAME: 'k8s.cluster.name',
|
||||
};
|
||||
@@ -0,0 +1,3 @@
|
||||
import NodeDetails from './NodeDetails';
|
||||
|
||||
export default NodeDetails;
|
||||
213
frontend/src/container/InfraMonitoringK8s/Nodes/utils.tsx
Normal file
213
frontend/src/container/InfraMonitoringK8s/Nodes/utils.tsx
Normal file
@@ -0,0 +1,213 @@
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { Tag, Tooltip } from 'antd';
|
||||
import { ColumnType } from 'antd/es/table';
|
||||
import {
|
||||
K8sNodesData,
|
||||
K8sNodesListPayload,
|
||||
} from 'api/infraMonitoring/getK8sNodesList';
|
||||
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: 'Node Name',
|
||||
value: 'nodeName',
|
||||
id: 'nodeName',
|
||||
canRemove: false,
|
||||
},
|
||||
{
|
||||
label: 'Cluster Name',
|
||||
value: 'clusterStatus',
|
||||
id: 'clusterStatus',
|
||||
canRemove: false,
|
||||
},
|
||||
{
|
||||
label: 'CPU Usage (cores)',
|
||||
value: 'cpu',
|
||||
id: 'cpu',
|
||||
canRemove: false,
|
||||
},
|
||||
{
|
||||
label: 'CPU Alloc (cores)',
|
||||
value: 'cpu_allocatable',
|
||||
id: 'cpu_allocatable',
|
||||
canRemove: false,
|
||||
},
|
||||
{
|
||||
label: 'Memory Usage (bytes)',
|
||||
value: 'memory',
|
||||
id: 'memory',
|
||||
canRemove: false,
|
||||
},
|
||||
{
|
||||
label: 'Memory Alloc (bytes)',
|
||||
value: 'memory_allocatable',
|
||||
id: 'memory_allocatable',
|
||||
canRemove: false,
|
||||
},
|
||||
];
|
||||
|
||||
export interface K8sNodesRowData {
|
||||
key: string;
|
||||
nodeUID: string;
|
||||
nodeName: React.ReactNode;
|
||||
clusterName: string;
|
||||
cpu: React.ReactNode;
|
||||
cpu_allocatable: React.ReactNode;
|
||||
memory: React.ReactNode;
|
||||
memory_allocatable: React.ReactNode;
|
||||
groupedByMeta?: any;
|
||||
}
|
||||
|
||||
const nodeGroupColumnConfig = {
|
||||
title: (
|
||||
<div className="column-header entity-group-header">
|
||||
<Group size={14} /> NODE GROUP
|
||||
</div>
|
||||
),
|
||||
dataIndex: 'nodeGroup',
|
||||
key: 'nodeGroup',
|
||||
ellipsis: true,
|
||||
width: 150,
|
||||
align: 'left',
|
||||
sorter: false,
|
||||
className: 'column entity-group-header',
|
||||
};
|
||||
|
||||
export const getK8sNodesListQuery = (): K8sNodesListPayload => ({
|
||||
filters: {
|
||||
items: [],
|
||||
op: 'and',
|
||||
},
|
||||
orderBy: { columnName: 'cpu', order: 'desc' },
|
||||
});
|
||||
|
||||
const columnsConfig = [
|
||||
{
|
||||
title: <div className="column-header-left name-header">Node Name</div>,
|
||||
dataIndex: 'nodeName',
|
||||
key: 'nodeName',
|
||||
ellipsis: true,
|
||||
width: 80,
|
||||
sorter: false,
|
||||
align: 'left',
|
||||
},
|
||||
{
|
||||
title: <div className="column-header-left name-header">Cluster Name</div>,
|
||||
dataIndex: 'clusterName',
|
||||
key: 'clusterName',
|
||||
ellipsis: true,
|
||||
width: 80,
|
||||
sorter: false,
|
||||
align: 'left',
|
||||
},
|
||||
{
|
||||
title: <div className="column-header-left">CPU Usage (cores)</div>,
|
||||
dataIndex: 'cpu',
|
||||
key: 'cpu',
|
||||
width: 80,
|
||||
sorter: true,
|
||||
align: 'left',
|
||||
},
|
||||
{
|
||||
title: <div className="column-header-left">CPU Alloc (cores)</div>,
|
||||
dataIndex: 'cpu_allocatable',
|
||||
key: 'cpu_allocatable',
|
||||
width: 80,
|
||||
sorter: true,
|
||||
align: 'left',
|
||||
},
|
||||
{
|
||||
title: <div className="column-header-left">Memory Usage (bytes)</div>,
|
||||
dataIndex: 'memory',
|
||||
key: 'memory',
|
||||
width: 80,
|
||||
sorter: true,
|
||||
align: 'left',
|
||||
},
|
||||
{
|
||||
title: <div className="column-header-left">Memory Alloc (bytes)</div>,
|
||||
dataIndex: 'memory_allocatable',
|
||||
key: 'memory_allocatable',
|
||||
width: 80,
|
||||
sorter: true,
|
||||
align: 'left',
|
||||
},
|
||||
];
|
||||
|
||||
export const getK8sNodesListColumns = (
|
||||
groupBy: IBuilderQuery['groupBy'],
|
||||
): ColumnType<K8sNodesRowData>[] => {
|
||||
if (groupBy.length > 0) {
|
||||
const filteredColumns = [...columnsConfig].filter(
|
||||
(column) => column.key !== 'nodeName',
|
||||
);
|
||||
filteredColumns.unshift(nodeGroupColumnConfig);
|
||||
return filteredColumns as ColumnType<K8sNodesRowData>[];
|
||||
}
|
||||
|
||||
return columnsConfig as ColumnType<K8sNodesRowData>[];
|
||||
};
|
||||
|
||||
const getGroupByEle = (
|
||||
node: K8sNodesData,
|
||||
groupBy: IBuilderQuery['groupBy'],
|
||||
): React.ReactNode => {
|
||||
const groupByValues: string[] = [];
|
||||
|
||||
groupBy.forEach((group) => {
|
||||
groupByValues.push(node.meta[group.key as keyof typeof node.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: K8sNodesData[],
|
||||
groupBy: IBuilderQuery['groupBy'],
|
||||
): K8sNodesRowData[] =>
|
||||
data.map((node, index) => ({
|
||||
key: `${node.nodeUID}-${index}`,
|
||||
nodeUID: node.nodeUID || '',
|
||||
nodeName: (
|
||||
<Tooltip title={node.meta.k8s_node_name}>
|
||||
{node.meta.k8s_node_name || ''}
|
||||
</Tooltip>
|
||||
),
|
||||
clusterName: node.meta.k8s_cluster_name,
|
||||
cpu: (
|
||||
<ValidateColumnValueWrapper value={node.nodeCPUUsage}>
|
||||
{node.nodeCPUUsage}
|
||||
</ValidateColumnValueWrapper>
|
||||
),
|
||||
memory: (
|
||||
<ValidateColumnValueWrapper value={node.nodeMemoryUsage}>
|
||||
{formatBytes(node.nodeMemoryUsage)}
|
||||
</ValidateColumnValueWrapper>
|
||||
),
|
||||
cpu_allocatable: (
|
||||
<ValidateColumnValueWrapper value={node.nodeCPUAllocatable}>
|
||||
{node.nodeCPUAllocatable}
|
||||
</ValidateColumnValueWrapper>
|
||||
),
|
||||
memory_allocatable: (
|
||||
<ValidateColumnValueWrapper value={node.nodeMemoryAllocatable}>
|
||||
{formatBytes(node.nodeMemoryAllocatable)}
|
||||
</ValidateColumnValueWrapper>
|
||||
),
|
||||
nodeGroup: getGroupByEle(node, groupBy),
|
||||
meta: node.meta,
|
||||
...node.meta,
|
||||
groupedByMeta: node.meta,
|
||||
}));
|
||||
565
frontend/src/container/InfraMonitoringK8s/Pods/K8sPodLists.tsx
generated
Normal file
565
frontend/src/container/InfraMonitoringK8s/Pods/K8sPodLists.tsx
generated
Normal file
@@ -0,0 +1,565 @@
|
||||
/* eslint-disable no-restricted-syntax */
|
||||
import '../InfraMonitoringK8s.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 get from 'api/browser/localstorage/get';
|
||||
import set from 'api/browser/localstorage/set';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { K8sPodsListPayload } from 'api/infraMonitoring/getK8sPodsList';
|
||||
import classNames from 'classnames';
|
||||
import { useGetK8sPodsList } from 'hooks/infraMonitoring/useGetK8sPodsList';
|
||||
import { useGetAggregateKeys } from 'hooks/queryBuilder/useGetAggregateKeys';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { useQueryOperations } from 'hooks/queryBuilder/useQueryBuilderOperations';
|
||||
import { ChevronDown, ChevronRight, CornerDownRight } 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 {
|
||||
defaultAddedColumns,
|
||||
defaultAvailableColumns,
|
||||
formatDataForTable,
|
||||
getK8sPodsListColumns,
|
||||
getK8sPodsListQuery,
|
||||
IEntityColumn,
|
||||
K8sPodsRowData,
|
||||
} from '../utils';
|
||||
import PodDetails from './PodDetails/PodDetails';
|
||||
|
||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||
function K8sPodsList({
|
||||
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 [addedColumns, setAddedColumns] = useState<IEntityColumn[]>([]);
|
||||
|
||||
const [availableColumns, setAvailableColumns] = useState<IEntityColumn[]>(
|
||||
defaultAvailableColumns,
|
||||
);
|
||||
|
||||
const [groupBy, setGroupBy] = useState<IBuilderQuery['groupBy']>([]);
|
||||
|
||||
const [selectedRowData, setSelectedRowData] = useState<K8sPodsRowData | null>(
|
||||
null,
|
||||
);
|
||||
|
||||
const [expandedRowKeys, setExpandedRowKeys] = useState<string[]>([]);
|
||||
|
||||
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],
|
||||
);
|
||||
|
||||
const {
|
||||
data: groupByFiltersData,
|
||||
isLoading: isLoadingGroupByFilters,
|
||||
} = useGetAggregateKeys(
|
||||
{
|
||||
dataSource: currentQuery.builder.queryData[0].dataSource,
|
||||
aggregateAttribute: K8sEntityToAggregateAttributeMapping[K8sCategory.PODS],
|
||||
aggregateOperator: 'noop',
|
||||
searchText: '',
|
||||
tagType: '',
|
||||
},
|
||||
{
|
||||
queryKey: [currentQuery.builder.queryData[0].dataSource, 'noop'],
|
||||
},
|
||||
true, // isInfraMonitoring
|
||||
K8sCategory.PODS, // infraMonitoringEntity
|
||||
);
|
||||
|
||||
// Reset pagination every time quick filters are changed
|
||||
useEffect(() => {
|
||||
setCurrentPage(1);
|
||||
}, [quickFiltersLastUpdated]);
|
||||
|
||||
useEffect(() => {
|
||||
const addedColumns = JSON.parse(get('k8sPodsAddedColumns') ?? '[]');
|
||||
|
||||
if (addedColumns && addedColumns.length > 0) {
|
||||
const availableColumns = defaultAvailableColumns.filter(
|
||||
(column) => !addedColumns.includes(column.id),
|
||||
);
|
||||
|
||||
const newAddedColumns = defaultAvailableColumns.filter((column) =>
|
||||
addedColumns.includes(column.id),
|
||||
);
|
||||
|
||||
setAvailableColumns(availableColumns);
|
||||
setAddedColumns(newAddedColumns);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const [orderBy, setOrderBy] = useState<{
|
||||
columnName: string;
|
||||
order: 'asc' | 'desc';
|
||||
} | null>({ columnName: 'cpu', order: 'desc' });
|
||||
|
||||
const [selectedPodUID, setSelectedPodUID] = useState<string | null>(null);
|
||||
|
||||
const pageSize = 10;
|
||||
|
||||
const query = useMemo(() => {
|
||||
const baseQuery = getK8sPodsListQuery();
|
||||
|
||||
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 { data, isFetching, isLoading, isError } = useGetK8sPodsList(
|
||||
query as K8sPodsListPayload,
|
||||
{
|
||||
queryKey: ['hostList', query],
|
||||
enabled: !!query,
|
||||
},
|
||||
);
|
||||
|
||||
const createFiltersForSelectedRowData = (
|
||||
selectedRowData: K8sPodsRowData,
|
||||
): IBuilderQuery['filters'] => {
|
||||
const baseFilters: IBuilderQuery['filters'] = {
|
||||
items: [...query.filters.items],
|
||||
op: 'and',
|
||||
};
|
||||
|
||||
if (!selectedRowData) return baseFilters;
|
||||
|
||||
const { groupedByMeta } = selectedRowData;
|
||||
|
||||
for (const key of Object.keys(groupedByMeta)) {
|
||||
baseFilters.items.push({
|
||||
key: {
|
||||
key,
|
||||
type: null,
|
||||
},
|
||||
op: '=',
|
||||
value: groupedByMeta[key],
|
||||
id: key,
|
||||
});
|
||||
}
|
||||
|
||||
return baseFilters;
|
||||
};
|
||||
|
||||
const fetchGroupedByRowDataQuery = useMemo(() => {
|
||||
if (!selectedRowData) return null;
|
||||
|
||||
const baseQuery = getK8sPodsListQuery();
|
||||
|
||||
const filters = createFiltersForSelectedRowData(selectedRowData);
|
||||
|
||||
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]);
|
||||
|
||||
const {
|
||||
data: groupedByRowData,
|
||||
isFetching: isFetchingGroupedByRowData,
|
||||
isLoading: isLoadingGroupedByRowData,
|
||||
isError: isErrorGroupedByRowData,
|
||||
refetch: fetchGroupedByRowData,
|
||||
} = useGetK8sPodsList(fetchGroupedByRowDataQuery as K8sPodsListPayload, {
|
||||
queryKey: ['hostList', fetchGroupedByRowDataQuery],
|
||||
enabled: !!fetchGroupedByRowDataQuery && !!selectedRowData,
|
||||
});
|
||||
|
||||
const podsData = useMemo(() => data?.payload?.data?.records || [], [data]);
|
||||
const totalCount = data?.payload?.data?.total || 0;
|
||||
|
||||
const formattedPodsData = useMemo(
|
||||
() => formatDataForTable(podsData, groupBy),
|
||||
[podsData, groupBy],
|
||||
);
|
||||
|
||||
const formattedGroupedByPodsData = useMemo(
|
||||
() =>
|
||||
formatDataForTable(groupedByRowData?.payload?.data?.records || [], groupBy),
|
||||
[groupedByRowData, groupBy],
|
||||
);
|
||||
|
||||
const columns = useMemo(() => getK8sPodsListColumns(addedColumns, groupBy), [
|
||||
addedColumns,
|
||||
groupBy,
|
||||
]);
|
||||
|
||||
const handleTableChange: TableProps<K8sPodsRowData>['onChange'] = useCallback(
|
||||
(
|
||||
pagination: TablePaginationConfig,
|
||||
_filters: Record<string, (string | number | boolean)[] | null>,
|
||||
sorter: SorterResult<K8sPodsRowData> | SorterResult<K8sPodsRowData>[],
|
||||
): 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],
|
||||
);
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
setGroupBy(groupBy);
|
||||
|
||||
setExpandedRowKeys([]);
|
||||
},
|
||||
[groupByFiltersData],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
logEvent('Infra Monitoring: K8s list page visited', {});
|
||||
}, []);
|
||||
|
||||
const selectedPodData = useMemo(() => {
|
||||
if (!selectedPodUID) return null;
|
||||
return podsData.find((pod) => pod.podUID === selectedPodUID) || null;
|
||||
}, [selectedPodUID, podsData]);
|
||||
|
||||
const handleGroupByRowClick = (record: K8sPodsRowData): 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 handleRowClick = (record: K8sPodsRowData): void => {
|
||||
if (groupBy.length === 0) {
|
||||
setSelectedPodUID(record.podUID);
|
||||
setSelectedRowData(null);
|
||||
} else {
|
||||
handleGroupByRowClick(record);
|
||||
}
|
||||
|
||||
logEvent('Infra Monitoring: K8s list item clicked', {
|
||||
podUID: record.podUID,
|
||||
});
|
||||
};
|
||||
|
||||
const handleClosePodDetail = (): void => {
|
||||
setSelectedPodUID(null);
|
||||
};
|
||||
|
||||
const handleAddColumn = useCallback(
|
||||
(column: IEntityColumn): void => {
|
||||
setAddedColumns((prev) => [...prev, column]);
|
||||
|
||||
setAvailableColumns((prev) => prev.filter((c) => c.value !== column.value));
|
||||
},
|
||||
[setAddedColumns, setAvailableColumns],
|
||||
);
|
||||
|
||||
// Update local storage when added columns updated
|
||||
useEffect(() => {
|
||||
const addedColumnIDs = addedColumns.map((column) => column.id);
|
||||
|
||||
set('k8sPodsAddedColumns', JSON.stringify(addedColumnIDs));
|
||||
}, [addedColumns]);
|
||||
|
||||
useEffect(() => {
|
||||
if (groupByFiltersData?.payload) {
|
||||
setGroupByOptions(
|
||||
groupByFiltersData?.payload?.attributeKeys?.map((filter) => ({
|
||||
value: filter.key,
|
||||
label: filter.key,
|
||||
})) || [],
|
||||
);
|
||||
}
|
||||
}, [groupByFiltersData]);
|
||||
|
||||
const handleRemoveColumn = useCallback(
|
||||
(column: IEntityColumn): void => {
|
||||
setAddedColumns((prev) => prev.filter((c) => c.value !== column.value));
|
||||
|
||||
setAvailableColumns((prev) => [...prev, column]);
|
||||
},
|
||||
[setAddedColumns, setAvailableColumns],
|
||||
);
|
||||
|
||||
const nestedColumns = useMemo(() => getK8sPodsListColumns(addedColumns, []), [
|
||||
addedColumns,
|
||||
]);
|
||||
|
||||
const isGroupedByAttribute = groupBy.length > 0;
|
||||
|
||||
const handleExpandedRowViewAllClick = (): void => {
|
||||
if (!selectedRowData) return;
|
||||
|
||||
const filters = createFiltersForSelectedRowData(selectedRowData);
|
||||
|
||||
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<K8sPodsRowData>[]}
|
||||
dataSource={formattedGroupedByPodsData}
|
||||
pagination={false}
|
||||
scroll={{ x: true }}
|
||||
tableLayout="fixed"
|
||||
showHeader={false}
|
||||
loading={{
|
||||
spinning: isFetchingGroupedByRowData || isLoadingGroupedByRowData,
|
||||
indicator: <Spin indicator={<LoadingOutlined size={14} spin />} />,
|
||||
}}
|
||||
/>
|
||||
|
||||
{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}
|
||||
>
|
||||
<CornerDownRight size={14} />
|
||||
View All
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
const expandRowIconRenderer = ({
|
||||
expanded,
|
||||
onExpand,
|
||||
record,
|
||||
}: {
|
||||
expanded: boolean;
|
||||
onExpand: (
|
||||
record: K8sPodsRowData,
|
||||
e: React.MouseEvent<HTMLButtonElement>,
|
||||
) => void;
|
||||
record: K8sPodsRowData;
|
||||
}): 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>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="k8s-list">
|
||||
<K8sHeader
|
||||
selectedGroupBy={groupBy}
|
||||
groupByOptions={groupByOptions}
|
||||
isLoadingGroupByFilters={isLoadingGroupByFilters}
|
||||
isFiltersVisible={isFiltersVisible}
|
||||
handleFilterVisibilityChange={handleFilterVisibilityChange}
|
||||
defaultAddedColumns={defaultAddedColumns}
|
||||
addedColumns={addedColumns}
|
||||
availableColumns={availableColumns}
|
||||
handleFiltersChange={handleFiltersChange}
|
||||
handleGroupByChange={handleGroupByChange}
|
||||
onAddColumn={handleAddColumn}
|
||||
onRemoveColumn={handleRemoveColumn}
|
||||
entity={K8sCategory.PODS}
|
||||
/>
|
||||
{isError && <Typography>{data?.error || 'Something went wrong'}</Typography>}
|
||||
|
||||
<Table
|
||||
className={classNames('k8s-list-table', {
|
||||
'expanded-k8s-list-table': isGroupedByAttribute,
|
||||
})}
|
||||
dataSource={isFetching || isLoading ? [] : formattedPodsData}
|
||||
columns={columns}
|
||||
pagination={{
|
||||
current: currentPage,
|
||||
pageSize,
|
||||
total: totalCount,
|
||||
showSizeChanger: false,
|
||||
hideOnSinglePage: 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>
|
||||
),
|
||||
}}
|
||||
scroll={{ x: true }}
|
||||
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,
|
||||
}}
|
||||
/>
|
||||
|
||||
{selectedPodData && (
|
||||
<PodDetails
|
||||
pod={selectedPodData}
|
||||
isModalTimeSelection
|
||||
onClose={handleClosePodDetail}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default K8sPodsList;
|
||||
322
frontend/src/container/InfraMonitoringK8s/Pods/PodDetails/Events/Events.styles.scss
generated
Normal file
322
frontend/src/container/InfraMonitoringK8s/Pods/PodDetails/Events/Events.styles.scss
generated
Normal file
@@ -0,0 +1,322 @@
|
||||
.pod-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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.pod-events-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
|
||||
padding: 12px;
|
||||
border-radius: 3px;
|
||||
border: 1px solid var(--bg-slate-500);
|
||||
}
|
||||
|
||||
.pod-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(.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: rgb(18, 19, 23);
|
||||
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: 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.pod-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;
|
||||
}
|
||||
}
|
||||
|
||||
.pod-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;
|
||||
}
|
||||
}
|
||||
|
||||
.pod-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 {
|
||||
&.ghost {
|
||||
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;
|
||||
}
|
||||
360
frontend/src/container/InfraMonitoringK8s/Pods/PodDetails/Events/Events.tsx
generated
Normal file
360
frontend/src/container/InfraMonitoringK8s/Pods/PodDetails/Events/Events.tsx
generated
Normal file
@@ -0,0 +1,360 @@
|
||||
/* eslint-disable no-nested-ternary */
|
||||
import './Events.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 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 { getPodsEventsQueryPayload } from './constants';
|
||||
import NoEventsContainer from './NoEventsContainer';
|
||||
|
||||
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 IPodEventsProps {
|
||||
timeRange: {
|
||||
startTime: number;
|
||||
endTime: number;
|
||||
};
|
||||
handleChangeLogFilters: (filters: IBuilderQuery['filters']) => void;
|
||||
filters: IBuilderQuery['filters'];
|
||||
isModalTimeSelection: boolean;
|
||||
handleTimeChange: (
|
||||
interval: Time | CustomTimeType,
|
||||
dateTimeRange?: [number, number],
|
||||
) => void;
|
||||
selectedInterval: Time;
|
||||
}
|
||||
|
||||
const EventsPageSize = 10;
|
||||
|
||||
export default function Events({
|
||||
timeRange,
|
||||
handleChangeLogFilters,
|
||||
filters,
|
||||
isModalTimeSelection,
|
||||
handleTimeChange,
|
||||
selectedInterval,
|
||||
}: IPodEventsProps): JSX.Element {
|
||||
const { currentQuery } = useQueryBuilder();
|
||||
|
||||
const [formattedPodEvents, setFormattedPodEvents] = 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 = getPodsEventsQueryPayload(
|
||||
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: ['podEvents', 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,
|
||||
}),
|
||||
);
|
||||
|
||||
setFormattedPodEvents(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 (!formattedPodEvents.length) return;
|
||||
|
||||
setPage(page - 1);
|
||||
|
||||
const firstEvent = formattedPodEvents[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'];
|
||||
|
||||
handleChangeLogFilters(newFilters);
|
||||
};
|
||||
|
||||
const handleNext = (): void => {
|
||||
if (!formattedPodEvents.length) return;
|
||||
|
||||
setPage(page + 1);
|
||||
const lastEvent = formattedPodEvents[formattedPodEvents.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'];
|
||||
|
||||
handleChangeLogFilters(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="pod-events-container">
|
||||
<div className="pod-events-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>
|
||||
|
||||
{isLoading && <LoadingContainer />}
|
||||
|
||||
{!isLoading && !isError && formattedPodEvents.length === 0 && (
|
||||
<NoEventsContainer />
|
||||
)}
|
||||
|
||||
{isError && !isLoading && <LogsError />}
|
||||
|
||||
{!isLoading && !isError && formattedPodEvents.length > 0 && (
|
||||
<div className="pod-events-list-container">
|
||||
<div className="pod-events-list-card">
|
||||
<Table<EventDataType>
|
||||
loading={isLoading && page > 1}
|
||||
columns={columns}
|
||||
expandable={{
|
||||
expandedRowRender: handleExpandRow,
|
||||
rowExpandable: (record): boolean => record.body !== 'Not Expandable',
|
||||
expandIcon: handleExpandRowIcon,
|
||||
}}
|
||||
dataSource={formattedPodEvents}
|
||||
pagination={false}
|
||||
rowKey={(record): string => record.id}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!isError && formattedPodEvents.length > 0 && (
|
||||
<div className="pod-events-footer">
|
||||
<Button
|
||||
className="pod-events-footer-button periscope-btn ghost"
|
||||
type="link"
|
||||
onClick={handlePrev}
|
||||
disabled={page === 1 || isFetching || isLoading}
|
||||
>
|
||||
{!isFetching && <ChevronLeft size={14} />}
|
||||
Prev
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
className="pod-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>
|
||||
);
|
||||
}
|
||||
16
frontend/src/container/InfraMonitoringK8s/Pods/PodDetails/Events/NoEventsContainer.tsx
generated
Normal file
16
frontend/src/container/InfraMonitoringK8s/Pods/PodDetails/Events/NoEventsContainer.tsx
generated
Normal file
@@ -0,0 +1,16 @@
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { Typography } from 'antd';
|
||||
import { Ghost } from 'lucide-react';
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
export default function NoEventsContainer(): React.ReactElement {
|
||||
return (
|
||||
<div className="no-logs-found">
|
||||
<Text type="secondary">
|
||||
<Ghost size={24} color={Color.BG_AMBER_500} /> No events found for this pod
|
||||
in the selected time range.
|
||||
</Text>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
65
frontend/src/container/InfraMonitoringK8s/Pods/PodDetails/Events/constants.ts
generated
Normal file
65
frontend/src/container/InfraMonitoringK8s/Pods/PodDetails/Events/constants.ts
generated
Normal file
@@ -0,0 +1,65 @@
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults';
|
||||
import { 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 { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
export const getPodsEventsQueryPayload = (
|
||||
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,
|
||||
});
|
||||
45
frontend/src/container/InfraMonitoringK8s/Pods/PodDetails/Metrics/Metrics.styles.scss
generated
Normal file
45
frontend/src/container/InfraMonitoringK8s/Pods/PodDetails/Metrics/Metrics.styles.scss
generated
Normal file
@@ -0,0 +1,45 @@
|
||||
.empty-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.host-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);
|
||||
}
|
||||
|
||||
.host-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%);
|
||||
}
|
||||
}
|
||||
140
frontend/src/container/InfraMonitoringK8s/Pods/PodDetails/Metrics/Metrics.tsx
generated
Normal file
140
frontend/src/container/InfraMonitoringK8s/Pods/PodDetails/Metrics/Metrics.tsx
generated
Normal file
@@ -0,0 +1,140 @@
|
||||
import './Metrics.styles.scss';
|
||||
|
||||
import { Card, Col, Row, Skeleton, Typography } from 'antd';
|
||||
import { K8sPodsData } from 'api/infraMonitoring/getK8sPodsList';
|
||||
import cx from 'classnames';
|
||||
import Uplot from 'components/Uplot';
|
||||
import { ENTITY_VERSION_V4 } from 'constants/app';
|
||||
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 } 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 { getPodQueryPayload, podWidgetInfo } from '../../constants';
|
||||
|
||||
interface MetricsTabProps {
|
||||
timeRange: {
|
||||
startTime: number;
|
||||
endTime: number;
|
||||
};
|
||||
isModalTimeSelection: boolean;
|
||||
handleTimeChange: (
|
||||
interval: Time | CustomTimeType,
|
||||
dateTimeRange?: [number, number],
|
||||
) => void;
|
||||
selectedInterval: Time;
|
||||
pod: K8sPodsData;
|
||||
}
|
||||
|
||||
function Metrics({
|
||||
selectedInterval,
|
||||
pod,
|
||||
timeRange,
|
||||
handleTimeChange,
|
||||
isModalTimeSelection,
|
||||
}: MetricsTabProps): JSX.Element {
|
||||
const queryPayloads = useMemo(
|
||||
() => getPodQueryPayload(pod, timeRange.startTime, timeRange.endTime),
|
||||
[pod, timeRange.startTime, timeRange.endTime],
|
||||
);
|
||||
|
||||
const queries = useQueries(
|
||||
queryPayloads.map((payload) => ({
|
||||
queryKey: ['pod-metrics', payload, ENTITY_VERSION_V4, 'POD'],
|
||||
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 }) => getUPlotChartData(data?.payload)),
|
||||
[queries],
|
||||
);
|
||||
|
||||
const options = useMemo(
|
||||
() =>
|
||||
queries.map(({ data }, idx) =>
|
||||
getUPlotChartOptions({
|
||||
apiResponse: data?.payload,
|
||||
isDarkMode,
|
||||
dimensions,
|
||||
yAxisUnit: podWidgetInfo[idx].yAxisUnit,
|
||||
softMax: null,
|
||||
softMin: null,
|
||||
minTimeScale: timeRange.startTime,
|
||||
maxTimeScale: timeRange.endTime,
|
||||
}),
|
||||
),
|
||||
[queries, isDarkMode, dimensions, 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>;
|
||||
}
|
||||
return (
|
||||
<div
|
||||
className={cx('chart-container', {
|
||||
'no-data-container':
|
||||
!query.isLoading && !query?.data?.payload?.data?.result?.length,
|
||||
})}
|
||||
>
|
||||
<Uplot options={options[idx]} 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="host-metrics-container">
|
||||
{queries.map((query, idx) => (
|
||||
<Col span={12} key={podWidgetInfo[idx].title}>
|
||||
<Typography.Text>{podWidgetInfo[idx].title}</Typography.Text>
|
||||
<Card bordered className="host-metrics-card" ref={graphRef}>
|
||||
{renderCardContent(query, idx)}
|
||||
</Card>
|
||||
</Col>
|
||||
))}
|
||||
</Row>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default Metrics;
|
||||
7
frontend/src/container/InfraMonitoringK8s/Pods/PodDetails/PodDetail.interfaces.ts
generated
Normal file
7
frontend/src/container/InfraMonitoringK8s/Pods/PodDetails/PodDetail.interfaces.ts
generated
Normal file
@@ -0,0 +1,7 @@
|
||||
import { K8sPodsData } from 'api/infraMonitoring/getK8sPodsList';
|
||||
|
||||
export type PodDetailProps = {
|
||||
pod: K8sPodsData | null;
|
||||
isModalTimeSelection: boolean;
|
||||
onClose: () => void;
|
||||
};
|
||||
247
frontend/src/container/InfraMonitoringK8s/Pods/PodDetails/PodDetails.styles.scss
generated
Normal file
247
frontend/src/container/InfraMonitoringK8s/Pods/PodDetails/PodDetails.styles.scss
generated
Normal file
@@ -0,0 +1,247 @@
|
||||
.pod-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);
|
||||
}
|
||||
|
||||
.pod-detail-drawer__pod {
|
||||
.pod-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;
|
||||
}
|
||||
|
||||
.pod-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;
|
||||
}
|
||||
|
||||
.pod-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);
|
||||
}
|
||||
|
||||
.pod-detail-drawer {
|
||||
.title {
|
||||
color: var(--text-ink-300);
|
||||
}
|
||||
|
||||
.pod-detail-drawer__pod {
|
||||
.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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
586
frontend/src/container/InfraMonitoringK8s/Pods/PodDetails/PodDetails.tsx
generated
Normal file
586
frontend/src/container/InfraMonitoringK8s/Pods/PodDetails/PodDetails.tsx
generated
Normal file
@@ -0,0 +1,586 @@
|
||||
/* eslint-disable sonarjs/no-identical-functions */
|
||||
/* eslint-disable sonarjs/no-duplicate-string */
|
||||
import './PodDetails.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 { 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/entityDetailUtils';
|
||||
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 { QUERY_KEYS } from './constants';
|
||||
import Events from './Events/Events';
|
||||
import Metrics from './Metrics/Metrics';
|
||||
import { PodDetailProps } from './PodDetail.interfaces';
|
||||
import PodLogsDetailedView from './PodLogs/PodLogsDetailedView';
|
||||
import PodTraces from './PodTraces/PodTraces';
|
||||
|
||||
const TimeRangeOffset = 1000000000;
|
||||
|
||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||
function PodDetails({
|
||||
pod,
|
||||
onClose,
|
||||
isModalTimeSelection,
|
||||
}: PodDetailProps): JSX.Element {
|
||||
const { maxTime, minTime, selectedTime } = useSelector<
|
||||
AppState,
|
||||
GlobalReducer
|
||||
>((state) => state.globalTime);
|
||||
|
||||
const startMs = useMemo(() => Math.floor(Number(minTime) / TimeRangeOffset), [
|
||||
minTime,
|
||||
]);
|
||||
const endMs = useMemo(() => Math.floor(Number(maxTime) / TimeRangeOffset), [
|
||||
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_POD_NAME,
|
||||
dataType: DataTypes.String,
|
||||
type: 'resource',
|
||||
isColumn: false,
|
||||
isJSON: false,
|
||||
id: 'k8s_pod_name--string--resource--false',
|
||||
},
|
||||
op: '=',
|
||||
value: pod?.meta.k8s_pod_name || '',
|
||||
},
|
||||
{
|
||||
id: uuidv4(),
|
||||
key: {
|
||||
key: QUERY_KEYS.K8S_NAMESPACE_NAME,
|
||||
dataType: DataTypes.String,
|
||||
type: 'resource',
|
||||
isColumn: false,
|
||||
isJSON: false,
|
||||
id: 'k8s_pod_name--string--resource--false',
|
||||
},
|
||||
op: '=',
|
||||
value: pod?.meta.k8s_namespace_name || '',
|
||||
},
|
||||
],
|
||||
}),
|
||||
[pod?.meta.k8s_namespace_name, pod?.meta.k8s_pod_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: 'Pod',
|
||||
},
|
||||
{
|
||||
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: pod?.meta.k8s_pod_name || '',
|
||||
},
|
||||
],
|
||||
}),
|
||||
[pod?.meta.k8s_pod_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: Pods list details page visited', {
|
||||
pod: pod?.podUID,
|
||||
});
|
||||
// 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 / TimeRangeOffset),
|
||||
endTime: Math.floor(maxTime / TimeRangeOffset),
|
||||
});
|
||||
}
|
||||
}, [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 / TimeRangeOffset),
|
||||
endTime: Math.floor(maxTime / TimeRangeOffset),
|
||||
});
|
||||
}
|
||||
|
||||
logEvent('Infra Monitoring: Pods list details time updated', {
|
||||
pod: pod?.podUID,
|
||||
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_POD_NAME,
|
||||
QUERY_KEYS.K8S_CLUSTER_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_CLUSTER_NAME,
|
||||
);
|
||||
|
||||
logEvent('Infra Monitoring: Pods list details logs filters applied', {
|
||||
pod: pod?.podUID,
|
||||
});
|
||||
|
||||
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_POD_NAME,
|
||||
QUERY_KEYS.K8S_CLUSTER_NAME,
|
||||
QUERY_KEYS.K8S_NAMESPACE_NAME,
|
||||
].includes(item.key?.key ?? ''),
|
||||
);
|
||||
|
||||
logEvent('Infra Monitoring: Pods list details traces filters applied', {
|
||||
pod: pod?.podUID,
|
||||
});
|
||||
|
||||
return {
|
||||
op: 'AND',
|
||||
items: filterDuplicateFilters(
|
||||
[
|
||||
...primaryFilters,
|
||||
...value.items.filter(
|
||||
(item) => item.key?.key !== QUERY_KEYS.K8S_POD_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 podKindFilter = prevFilters.items.find(
|
||||
(item) => item.key?.key === QUERY_KEYS.K8S_OBJECT_KIND,
|
||||
);
|
||||
const podNameFilter = prevFilters.items.find(
|
||||
(item) => item.key?.key === QUERY_KEYS.K8S_OBJECT_NAME,
|
||||
);
|
||||
|
||||
logEvent('Infra Monitoring: Pods list details events filters applied', {
|
||||
pod: pod?.podUID,
|
||||
});
|
||||
|
||||
return {
|
||||
op: 'AND',
|
||||
items: [
|
||||
podKindFilter,
|
||||
podNameFilter,
|
||||
...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: Pods list details explore clicked', {
|
||||
pod: pod?.podUID,
|
||||
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 / TimeRangeOffset),
|
||||
endTime: Math.floor(maxTime / TimeRangeOffset),
|
||||
});
|
||||
}
|
||||
setSelectedView(VIEW_TYPES.METRICS);
|
||||
onClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<Drawer
|
||||
width="70%"
|
||||
title={
|
||||
<>
|
||||
<Divider type="vertical" />
|
||||
<Typography.Text className="title">
|
||||
{pod?.meta.k8s_pod_name}
|
||||
</Typography.Text>
|
||||
</>
|
||||
}
|
||||
placement="right"
|
||||
onClose={handleClose}
|
||||
open={!!pod}
|
||||
style={{
|
||||
overscrollBehavior: 'contain',
|
||||
background: isDarkMode ? Color.BG_INK_400 : Color.BG_VANILLA_100,
|
||||
}}
|
||||
className="pod-detail-drawer"
|
||||
destroyOnClose
|
||||
closeIcon={<X size={16} style={{ marginTop: Spacing.MARGIN_1 }} />}
|
||||
>
|
||||
{pod && (
|
||||
<>
|
||||
<div className="pod-detail-drawer__pod">
|
||||
<div className="pod-details-grid">
|
||||
<div className="labels-row">
|
||||
<Typography.Text
|
||||
type="secondary"
|
||||
className="pod-details-metadata-label"
|
||||
>
|
||||
NAMESPACE
|
||||
</Typography.Text>
|
||||
<Typography.Text
|
||||
type="secondary"
|
||||
className="pod-details-metadata-label"
|
||||
>
|
||||
Cluster Name
|
||||
</Typography.Text>
|
||||
<Typography.Text
|
||||
type="secondary"
|
||||
className="pod-details-metadata-label"
|
||||
>
|
||||
Node
|
||||
</Typography.Text>
|
||||
</div>
|
||||
|
||||
<div className="values-row">
|
||||
<Typography.Text className="pod-details-metadata-value">
|
||||
<Tooltip title={pod.meta.k8s_namespace_name}>
|
||||
{pod.meta.k8s_namespace_name}
|
||||
</Tooltip>
|
||||
</Typography.Text>
|
||||
|
||||
<Typography.Text className="pod-details-metadata-value">
|
||||
<Tooltip title={pod.meta.k8s_cluster_name}>
|
||||
{pod.meta.k8s_cluster_name}
|
||||
</Tooltip>
|
||||
</Typography.Text>
|
||||
|
||||
<Typography.Text className="pod-details-metadata-value">
|
||||
<Tooltip title={pod.meta.k8s_node_name}>
|
||||
{pod.meta.k8s_node_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 && (
|
||||
<Metrics
|
||||
pod={pod}
|
||||
selectedInterval={selectedInterval}
|
||||
timeRange={modalTimeRange}
|
||||
handleTimeChange={handleTimeChange}
|
||||
isModalTimeSelection={isModalTimeSelection}
|
||||
/>
|
||||
)}
|
||||
{selectedView === VIEW_TYPES.LOGS && (
|
||||
<PodLogsDetailedView
|
||||
timeRange={modalTimeRange}
|
||||
isModalTimeSelection={isModalTimeSelection}
|
||||
handleTimeChange={handleTimeChange}
|
||||
handleChangeLogFilters={handleChangeLogFilters}
|
||||
logFilters={logFilters}
|
||||
selectedInterval={selectedInterval}
|
||||
/>
|
||||
)}
|
||||
{selectedView === VIEW_TYPES.TRACES && (
|
||||
<PodTraces
|
||||
timeRange={modalTimeRange}
|
||||
isModalTimeSelection={isModalTimeSelection}
|
||||
handleTimeChange={handleTimeChange}
|
||||
handleChangeTracesFilters={handleChangeTracesFilters}
|
||||
tracesFilters={tracesFilters}
|
||||
selectedInterval={selectedInterval}
|
||||
/>
|
||||
)}
|
||||
|
||||
{selectedView === VIEW_TYPES.EVENTS && (
|
||||
<Events
|
||||
timeRange={modalTimeRange}
|
||||
isModalTimeSelection={isModalTimeSelection}
|
||||
handleTimeChange={handleTimeChange}
|
||||
handleChangeLogFilters={handleChangeEventsFilters}
|
||||
filters={eventsFilters}
|
||||
selectedInterval={selectedInterval}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Drawer>
|
||||
);
|
||||
}
|
||||
|
||||
export default PodDetails;
|
||||
16
frontend/src/container/InfraMonitoringK8s/Pods/PodDetails/PodLogs/NoLogsContainer.tsx
generated
Normal file
16
frontend/src/container/InfraMonitoringK8s/Pods/PodDetails/PodLogs/NoLogsContainer.tsx
generated
Normal file
@@ -0,0 +1,16 @@
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { Typography } from 'antd';
|
||||
import { Ghost } from 'lucide-react';
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
export default function NoLogsContainer(): React.ReactElement {
|
||||
return (
|
||||
<div className="no-logs-found">
|
||||
<Text type="secondary">
|
||||
<Ghost size={24} color={Color.BG_AMBER_500} /> No logs found for this pod in
|
||||
the selected time range.
|
||||
</Text>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
133
frontend/src/container/InfraMonitoringK8s/Pods/PodDetails/PodLogs/PodLogs.styles.scss
generated
Normal file
133
frontend/src/container/InfraMonitoringK8s/Pods/PodDetails/PodLogs/PodLogs.styles.scss
generated
Normal file
@@ -0,0 +1,133 @@
|
||||
.pod-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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.pod-logs-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
|
||||
padding: 12px;
|
||||
border-radius: 3px;
|
||||
border: 1px solid var(--bg-slate-500);
|
||||
}
|
||||
|
||||
.pod-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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.pod-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;
|
||||
}
|
||||
}
|
||||
|
||||
.pod-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);
|
||||
}
|
||||
}
|
||||
}
|
||||
218
frontend/src/container/InfraMonitoringK8s/Pods/PodDetails/PodLogs/PodLogs.tsx
generated
Normal file
218
frontend/src/container/InfraMonitoringK8s/Pods/PodDetails/PodLogs/PodLogs.tsx
generated
Normal file
@@ -0,0 +1,218 @@
|
||||
/* eslint-disable no-nested-ternary */
|
||||
import './PodLogs.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 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 { QUERY_KEYS } from '../constants';
|
||||
import { getPodLogsQueryPayload } from './constants';
|
||||
import NoLogsContainer from './NoLogsContainer';
|
||||
|
||||
interface Props {
|
||||
timeRange: {
|
||||
startTime: number;
|
||||
endTime: number;
|
||||
};
|
||||
handleChangeLogFilters: (filters: IBuilderQuery['filters']) => void;
|
||||
filters: IBuilderQuery['filters'];
|
||||
}
|
||||
|
||||
function PodLogs({
|
||||
timeRange,
|
||||
handleChangeLogFilters,
|
||||
filters,
|
||||
}: 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' &&
|
||||
![
|
||||
QUERY_KEYS.K8S_POD_NAME,
|
||||
QUERY_KEYS.K8S_CLUSTER_NAME,
|
||||
QUERY_KEYS.K8S_NAMESPACE_NAME,
|
||||
].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 = getPodLogsQueryPayload(
|
||||
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: ['podLogs', 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}
|
||||
/>
|
||||
),
|
||||
[],
|
||||
);
|
||||
|
||||
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="pod-logs-list-card">
|
||||
<OverlayScrollbar isVirtuoso>
|
||||
<Virtuoso
|
||||
className="pod-logs-virtuoso"
|
||||
key="pod-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="pod-logs">
|
||||
{isLoading && <LogsLoading />}
|
||||
{!isLoading && !isError && logs.length === 0 && <NoLogsContainer />}
|
||||
{isError && !isLoading && <LogsError />}
|
||||
{!isLoading && !isError && logs.length > 0 && (
|
||||
<div className="pod-logs-list-container">{renderContent}</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default PodLogs;
|
||||
99
frontend/src/container/InfraMonitoringK8s/Pods/PodDetails/PodLogs/PodLogsDetailedView.tsx
generated
Normal file
99
frontend/src/container/InfraMonitoringK8s/Pods/PodDetails/PodLogs/PodLogsDetailedView.tsx
generated
Normal file
@@ -0,0 +1,99 @@
|
||||
import './PodLogs.styles.scss';
|
||||
|
||||
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 PodLogs from './PodLogs';
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
function PodLogsDetailedView({
|
||||
timeRange,
|
||||
isModalTimeSelection,
|
||||
handleTimeChange,
|
||||
handleChangeLogFilters,
|
||||
logFilters,
|
||||
selectedInterval,
|
||||
}: 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="host-metrics-logs-container">
|
||||
<div className="host-metrics-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>
|
||||
<PodLogs
|
||||
timeRange={timeRange}
|
||||
handleChangeLogFilters={handleChangeLogFilters}
|
||||
filters={logFilters}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default PodLogsDetailedView;
|
||||
65
frontend/src/container/InfraMonitoringK8s/Pods/PodDetails/PodLogs/constants.ts
generated
Normal file
65
frontend/src/container/InfraMonitoringK8s/Pods/PodDetails/PodLogs/constants.ts
generated
Normal file
@@ -0,0 +1,65 @@
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults';
|
||||
import { 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 { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
export const getPodLogsQueryPayload = (
|
||||
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,
|
||||
});
|
||||
193
frontend/src/container/InfraMonitoringK8s/Pods/PodDetails/PodTraces/PodTraces.styles.scss
generated
Normal file
193
frontend/src/container/InfraMonitoringK8s/Pods/PodDetails/PodTraces/PodTraces.styles.scss
generated
Normal file
@@ -0,0 +1,193 @@
|
||||
.pod-metric-traces {
|
||||
margin-top: 1rem;
|
||||
|
||||
.pod-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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.pod-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(.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: rgba(171, 189, 255, 0.01);
|
||||
}
|
||||
|
||||
.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-table-container::after {
|
||||
content: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.lightMode {
|
||||
.host-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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.host-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(.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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
199
frontend/src/container/InfraMonitoringK8s/Pods/PodDetails/PodTraces/PodTraces.tsx
generated
Normal file
199
frontend/src/container/InfraMonitoringK8s/Pods/PodDetails/PodTraces/PodTraces.tsx
generated
Normal file
@@ -0,0 +1,199 @@
|
||||
import './PodTraces.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 { getPodTracesQueryPayload, selectedColumns } from './constants';
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
function PodTraces({
|
||||
timeRange,
|
||||
isModalTimeSelection,
|
||||
handleTimeChange,
|
||||
handleChangeTracesFilters,
|
||||
tracesFilters,
|
||||
selectedInterval,
|
||||
}: 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(
|
||||
() =>
|
||||
getPodTracesQueryPayload(
|
||||
timeRange.startTime,
|
||||
timeRange.endTime,
|
||||
paginationQueryData?.offset || offset,
|
||||
tracesFilters,
|
||||
),
|
||||
[
|
||||
timeRange.startTime,
|
||||
timeRange.endTime,
|
||||
offset,
|
||||
tracesFilters,
|
||||
paginationQueryData,
|
||||
],
|
||||
);
|
||||
|
||||
const { data, isLoading, isFetching, isError } = useQuery({
|
||||
queryKey: [
|
||||
'podTraces',
|
||||
timeRange.startTime,
|
||||
timeRange.endTime,
|
||||
offset,
|
||||
tracesFilters,
|
||||
DEFAULT_ENTITY_VERSION,
|
||||
paginationQueryData,
|
||||
],
|
||||
queryFn: () => GetMetricQueryRange(queryPayload, DEFAULT_ENTITY_VERSION),
|
||||
enabled: !!queryPayload,
|
||||
});
|
||||
|
||||
const traceListColumns = getListColumns(selectedColumns);
|
||||
|
||||
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="host-metric-traces">
|
||||
<div className="host-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="pod-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 PodTraces;
|
||||
200
frontend/src/container/InfraMonitoringK8s/Pods/PodDetails/PodTraces/constants.ts
generated
Normal file
200
frontend/src/container/InfraMonitoringK8s/Pods/PodDetails/PodTraces/constants.ts
generated
Normal file
@@ -0,0 +1,200 @@
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults';
|
||||
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';
|
||||
|
||||
export const columns = [
|
||||
{
|
||||
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 selectedColumns: 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 getPodTracesQueryPayload = (
|
||||
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,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
7
frontend/src/container/InfraMonitoringK8s/Pods/PodDetails/constants.ts
generated
Normal file
7
frontend/src/container/InfraMonitoringK8s/Pods/PodDetails/constants.ts
generated
Normal file
@@ -0,0 +1,7 @@
|
||||
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',
|
||||
};
|
||||
3
frontend/src/container/InfraMonitoringK8s/Pods/PodDetails/index.tsx
generated
Normal file
3
frontend/src/container/InfraMonitoringK8s/Pods/PodDetails/index.tsx
generated
Normal file
@@ -0,0 +1,3 @@
|
||||
import PodDetails from './PodDetails';
|
||||
|
||||
export default PodDetails;
|
||||
2672
frontend/src/container/InfraMonitoringK8s/Pods/constants.ts
generated
Normal file
2672
frontend/src/container/InfraMonitoringK8s/Pods/constants.ts
generated
Normal file
File diff suppressed because it is too large
Load Diff
175
frontend/src/container/InfraMonitoringK8s/commonUtils.tsx
Normal file
175
frontend/src/container/InfraMonitoringK8s/commonUtils.tsx
Normal file
@@ -0,0 +1,175 @@
|
||||
/* eslint-disable react/require-default-props */
|
||||
/* eslint-disable @typescript-eslint/explicit-function-return-type */
|
||||
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { Tooltip, Typography } from 'antd';
|
||||
import { ColumnsType } from 'antd/es/table';
|
||||
import { Progress } from 'antd/lib';
|
||||
import { ResizeTable } from 'components/ResizeTable';
|
||||
import FieldRenderer from 'container/LogDetailedView/FieldRenderer';
|
||||
import { DataType } from 'container/LogDetailedView/TableView';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { getInvalidValueTooltipText, K8sCategory } from './constants';
|
||||
|
||||
/**
|
||||
* Converts size in bytes to a human-readable string with appropriate units
|
||||
*/
|
||||
export function formatBytes(bytes: number, decimals = 2): string {
|
||||
if (bytes === 0) return '0 Bytes';
|
||||
|
||||
const k = 1024;
|
||||
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
|
||||
return `${parseFloat((bytes / k ** i).toFixed(decimals))} ${sizes[i]}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper component that renders its children for valid values or renders '-' for invalid values (-1)
|
||||
*/
|
||||
export function ValidateColumnValueWrapper({
|
||||
children,
|
||||
value,
|
||||
entity,
|
||||
attribute,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
value: number;
|
||||
entity?: K8sCategory;
|
||||
attribute?: string;
|
||||
}): JSX.Element {
|
||||
if (value === -1) {
|
||||
let element = <div>-</div>;
|
||||
if (entity && attribute) {
|
||||
element = (
|
||||
<Tooltip title={getInvalidValueTooltipText(entity, attribute)}>
|
||||
{element}
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
return element;
|
||||
}
|
||||
|
||||
return <div>{children}</div>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns stroke color for request utilization parameters according to current value
|
||||
*/
|
||||
export function getStrokeColorForRequestUtilization(value: number): string {
|
||||
const percent = Number((value * 100).toFixed(1));
|
||||
// Orange
|
||||
if (percent <= 50) {
|
||||
return Color.BG_AMBER_500;
|
||||
}
|
||||
// Green
|
||||
if (percent > 50 && percent <= 100) {
|
||||
return Color.BG_FOREST_500;
|
||||
}
|
||||
// Regular Red
|
||||
if (percent > 100 && percent <= 150) {
|
||||
return Color.BG_SAKURA_500;
|
||||
}
|
||||
// Dark Red
|
||||
return Color.BG_CHERRY_600;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns stroke color for limit utilization parameters according to current value
|
||||
*/
|
||||
export function getStrokeColorForLimitUtilization(value: number): string {
|
||||
const percent = Number((value * 100).toFixed(1));
|
||||
// Green
|
||||
if (percent <= 60) {
|
||||
return Color.BG_FOREST_500;
|
||||
}
|
||||
// Yellow
|
||||
if (percent > 60 && percent <= 80) {
|
||||
return Color.BG_AMBER_200;
|
||||
}
|
||||
// Orange
|
||||
if (percent > 80 && percent <= 95) {
|
||||
return Color.BG_AMBER_500;
|
||||
}
|
||||
// Red
|
||||
return Color.BG_SAKURA_500;
|
||||
}
|
||||
|
||||
export const getProgressBarText = (percent: number): React.ReactNode =>
|
||||
`${percent}%`;
|
||||
|
||||
export function EntityProgressBar({
|
||||
value,
|
||||
type,
|
||||
}: {
|
||||
value: number;
|
||||
type: 'request' | 'limit';
|
||||
}): JSX.Element {
|
||||
const percentage = Number((value * 100).toFixed(1));
|
||||
|
||||
return (
|
||||
<div className="entity-progress-bar">
|
||||
<Progress
|
||||
percent={percentage}
|
||||
strokeLinecap="butt"
|
||||
size="small"
|
||||
status="normal"
|
||||
strokeColor={
|
||||
type === 'limit'
|
||||
? getStrokeColorForLimitUtilization(value)
|
||||
: getStrokeColorForRequestUtilization(value)
|
||||
}
|
||||
className="progress-bar"
|
||||
showInfo={false}
|
||||
/>
|
||||
<Typography.Text style={{ fontSize: '10px' }}>{percentage}%</Typography.Text>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function EventContents({
|
||||
data,
|
||||
}: {
|
||||
data: Record<string, string> | undefined;
|
||||
}): JSX.Element {
|
||||
const tableData = useMemo(
|
||||
() =>
|
||||
data ? Object.keys(data).map((key) => ({ key, value: data[key] })) : [],
|
||||
[data],
|
||||
);
|
||||
|
||||
const columns: ColumnsType<DataType> = [
|
||||
{
|
||||
title: 'Key',
|
||||
dataIndex: 'key',
|
||||
key: 'key',
|
||||
width: 50,
|
||||
align: 'left',
|
||||
className: 'attribute-pin value-field-container',
|
||||
render: (field: string): JSX.Element => <FieldRenderer field={field} />,
|
||||
},
|
||||
{
|
||||
title: 'Value',
|
||||
dataIndex: 'value',
|
||||
key: 'value',
|
||||
width: 50,
|
||||
align: 'left',
|
||||
ellipsis: true,
|
||||
className: 'attribute-name',
|
||||
render: (field: string): JSX.Element => <FieldRenderer field={field} />,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<ResizeTable
|
||||
columns={columns}
|
||||
tableLayout="fixed"
|
||||
dataSource={tableData}
|
||||
pagination={false}
|
||||
showHeader={false}
|
||||
className="event-content-container"
|
||||
/>
|
||||
);
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user