mirror of
https://github.com/SigNoz/signoz.git
synced 2026-02-07 18:32:12 +00:00
Compare commits
111 Commits
fix/query-
...
fix/parent
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4120b53c27 | ||
|
|
a7cbfe4587 | ||
|
|
74d480e113 | ||
|
|
e0582f6edb | ||
|
|
7c05113d8e | ||
|
|
de366c7ef6 | ||
|
|
8c5f56abd7 | ||
|
|
5820b0ba46 | ||
|
|
1e2dd240a5 | ||
|
|
b79ff25682 | ||
|
|
6d1d48e156 | ||
|
|
62c56d2150 | ||
|
|
aa544f52f3 | ||
|
|
996080aaf8 | ||
|
|
200b714306 | ||
|
|
467e8ff288 | ||
|
|
2dae184976 | ||
|
|
af7f1def55 | ||
|
|
f30d95fd5f | ||
|
|
78b4f2c698 | ||
|
|
54c1874cc2 | ||
|
|
b01f95452f | ||
|
|
04d49fceef | ||
|
|
82851e79db | ||
|
|
b1da482b2c | ||
|
|
0c9f06850a | ||
|
|
7e18087db6 | ||
|
|
b8414ad715 | ||
|
|
43f8c2dce6 | ||
|
|
50849815d5 | ||
|
|
3a38e3fff6 | ||
|
|
69d1ab3813 | ||
|
|
dc8ef8fa06 | ||
|
|
6c69dc4a0b | ||
|
|
7254772e70 | ||
|
|
3e72d3fd02 | ||
|
|
656d9a11ad | ||
|
|
6afd9258d2 | ||
|
|
d140475a80 | ||
|
|
99f5acff1d | ||
|
|
8669d94e9c | ||
|
|
5eaba94528 | ||
|
|
4a4400170f | ||
|
|
6afea4e075 | ||
|
|
b5513a2e8a | ||
|
|
307710fc5a | ||
|
|
9d94fd31b6 | ||
|
|
66e8f00749 | ||
|
|
5f28e707d1 | ||
|
|
b6f6a31ab5 | ||
|
|
f9c16b79d5 | ||
|
|
b6322fe417 | ||
|
|
8b48b955c0 | ||
|
|
a3f5f57756 | ||
|
|
cf73451020 | ||
|
|
ece8976dce | ||
|
|
d94f3a4f34 | ||
|
|
6e6e57c243 | ||
|
|
2c54354427 | ||
|
|
4c93597b6f | ||
|
|
165c14d350 | ||
|
|
bee36ee928 | ||
|
|
1eca60e9c4 | ||
|
|
2e6a6ea286 | ||
|
|
872f887646 | ||
|
|
b182bf8199 | ||
|
|
7ae62936ab | ||
|
|
0f38114b75 | ||
|
|
5aa4ae1261 | ||
|
|
a43cca9460 | ||
|
|
619146699b | ||
|
|
fcadb89f55 | ||
|
|
c43ddbc5a2 | ||
|
|
aeb0cc850f | ||
|
|
91df27861f | ||
|
|
789692953b | ||
|
|
ba2ed3ad22 | ||
|
|
eb3dfbf63b | ||
|
|
c3e048470d | ||
|
|
4563ff0e62 | ||
|
|
c9e48b6de9 | ||
|
|
06ef9ff384 | ||
|
|
26d55875f5 | ||
|
|
b1864ee328 | ||
|
|
8b62c8dced | ||
|
|
273452352d | ||
|
|
8274ebfe37 | ||
|
|
7d5e14abb6 | ||
|
|
7c17ac42b1 | ||
|
|
74ee7bb2c7 | ||
|
|
2f5640b2e6 | ||
|
|
121debcecc | ||
|
|
ff13504a74 | ||
|
|
d4e373443b | ||
|
|
3ccf822d67 | ||
|
|
0e270e6f51 | ||
|
|
749df2a979 | ||
|
|
9ee5d5d599 | ||
|
|
4940dfd46f | ||
|
|
79a31cc205 | ||
|
|
5102cf2b7b | ||
|
|
9ec5594648 | ||
|
|
b6c2ebd6d7 | ||
|
|
9a3a8c8305 | ||
|
|
2ac45b0174 | ||
|
|
2a53918ebd | ||
|
|
9daefeb881 | ||
|
|
526cf01cb7 | ||
|
|
cd4766ec2b | ||
|
|
2196b58d36 | ||
|
|
53c58b9983 |
@@ -40,7 +40,7 @@ services:
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
schema-migrator-sync:
|
||||
image: signoz/signoz-schema-migrator:v0.111.42
|
||||
image: signoz/signoz-schema-migrator:v0.128.0
|
||||
container_name: schema-migrator-sync
|
||||
command:
|
||||
- sync
|
||||
@@ -53,7 +53,7 @@ services:
|
||||
condition: service_healthy
|
||||
restart: on-failure
|
||||
schema-migrator-async:
|
||||
image: signoz/signoz-schema-migrator:v0.111.42
|
||||
image: signoz/signoz-schema-migrator:v0.128.0
|
||||
container_name: schema-migrator-async
|
||||
command:
|
||||
- async
|
||||
|
||||
2
.github/workflows/integrationci.yaml
vendored
2
.github/workflows/integrationci.yaml
vendored
@@ -22,7 +22,7 @@ jobs:
|
||||
- 24.1.2-alpine
|
||||
- 24.12-alpine
|
||||
schema-migrator-version:
|
||||
- v0.111.38
|
||||
- v0.128.0
|
||||
postgres-version:
|
||||
- 15
|
||||
if: |
|
||||
|
||||
@@ -174,7 +174,7 @@ services:
|
||||
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
|
||||
signoz:
|
||||
!!merge <<: *db-depend
|
||||
image: signoz/signoz:v0.87.0
|
||||
image: signoz/signoz:v0.88.1
|
||||
command:
|
||||
- --config=/root/config/prometheus.yml
|
||||
ports:
|
||||
@@ -194,6 +194,7 @@ services:
|
||||
- TELEMETRY_ENABLED=true
|
||||
- DEPLOYMENT_TYPE=docker-swarm
|
||||
- SIGNOZ_JWT_SECRET=secret
|
||||
- DOT_METRICS_ENABLED=true
|
||||
healthcheck:
|
||||
test:
|
||||
- CMD
|
||||
@@ -206,7 +207,7 @@ services:
|
||||
retries: 3
|
||||
otel-collector:
|
||||
!!merge <<: *db-depend
|
||||
image: signoz/signoz-otel-collector:v0.111.42
|
||||
image: signoz/signoz-otel-collector:v0.128.0
|
||||
command:
|
||||
- --config=/etc/otel-collector-config.yaml
|
||||
- --manager-config=/etc/manager-config.yaml
|
||||
@@ -230,7 +231,7 @@ services:
|
||||
- signoz
|
||||
schema-migrator:
|
||||
!!merge <<: *common
|
||||
image: signoz/signoz-schema-migrator:v0.111.42
|
||||
image: signoz/signoz-schema-migrator:v0.128.0
|
||||
deploy:
|
||||
restart_policy:
|
||||
condition: on-failure
|
||||
|
||||
@@ -100,7 +100,7 @@ services:
|
||||
# - "9000:9000"
|
||||
# - "8123:8123"
|
||||
# - "9181:9181"
|
||||
|
||||
|
||||
configs:
|
||||
- source: clickhouse-config
|
||||
target: /etc/clickhouse-server/config.xml
|
||||
@@ -110,13 +110,12 @@ services:
|
||||
target: /etc/clickhouse-server/custom-function.xml
|
||||
- source: clickhouse-cluster
|
||||
target: /etc/clickhouse-server/config.d/cluster.xml
|
||||
|
||||
volumes:
|
||||
- clickhouse:/var/lib/clickhouse/
|
||||
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
|
||||
signoz:
|
||||
!!merge <<: *db-depend
|
||||
image: signoz/signoz:v0.87.0
|
||||
image: signoz/signoz:v0.88.1
|
||||
command:
|
||||
- --config=/root/config/prometheus.yml
|
||||
ports:
|
||||
@@ -136,6 +135,7 @@ services:
|
||||
- GODEBUG=netdns=go
|
||||
- TELEMETRY_ENABLED=true
|
||||
- DEPLOYMENT_TYPE=docker-swarm
|
||||
- DOT_METRICS_ENABLED=true
|
||||
healthcheck:
|
||||
test:
|
||||
- CMD
|
||||
@@ -148,7 +148,7 @@ services:
|
||||
retries: 3
|
||||
otel-collector:
|
||||
!!merge <<: *db-depend
|
||||
image: signoz/signoz-otel-collector:v0.111.42
|
||||
image: signoz/signoz-otel-collector:v0.128.0
|
||||
command:
|
||||
- --config=/etc/otel-collector-config.yaml
|
||||
- --manager-config=/etc/manager-config.yaml
|
||||
@@ -174,7 +174,7 @@ services:
|
||||
- signoz
|
||||
schema-migrator:
|
||||
!!merge <<: *common
|
||||
image: signoz/signoz-schema-migrator:v0.111.42
|
||||
image: signoz/signoz-schema-migrator:v0.128.0
|
||||
deploy:
|
||||
restart_policy:
|
||||
condition: on-failure
|
||||
@@ -195,7 +195,6 @@ volumes:
|
||||
name: signoz-sqlite
|
||||
zookeeper-1:
|
||||
name: signoz-zookeeper-1
|
||||
|
||||
configs:
|
||||
clickhouse-config:
|
||||
file: ../common/clickhouse/config.xml
|
||||
@@ -205,7 +204,6 @@ configs:
|
||||
file: ../common/clickhouse/custom-function.xml
|
||||
clickhouse-cluster:
|
||||
file: ../common/clickhouse/cluster.xml
|
||||
|
||||
signoz-prometheus-config:
|
||||
file: ../common/signoz/prometheus.yml
|
||||
# If you have multiple dashboard files, you can list them individually:
|
||||
|
||||
@@ -26,7 +26,7 @@ processors:
|
||||
detectors: [env, system]
|
||||
timeout: 2s
|
||||
signozspanmetrics/delta:
|
||||
metrics_exporter: clickhousemetricswrite, signozclickhousemetrics
|
||||
metrics_exporter: signozclickhousemetrics
|
||||
metrics_flush_interval: 60s
|
||||
latency_histogram_buckets: [100us, 1ms, 2ms, 6ms, 10ms, 50ms, 100ms, 250ms, 500ms, 1000ms, 1400ms, 2000ms, 5s, 10s, 20s, 40s, 60s ]
|
||||
dimensions_cache_size: 100000
|
||||
@@ -60,27 +60,16 @@ exporters:
|
||||
datasource: tcp://clickhouse:9000/signoz_traces
|
||||
low_cardinal_exception_grouping: ${env:LOW_CARDINAL_EXCEPTION_GROUPING}
|
||||
use_new_schema: true
|
||||
clickhousemetricswrite:
|
||||
endpoint: tcp://clickhouse:9000/signoz_metrics
|
||||
resource_to_telemetry_conversion:
|
||||
enabled: true
|
||||
disable_v2: true
|
||||
clickhousemetricswrite/prometheus:
|
||||
endpoint: tcp://clickhouse:9000/signoz_metrics
|
||||
disable_v2: true
|
||||
signozclickhousemetrics:
|
||||
dsn: tcp://clickhouse:9000/signoz_metrics
|
||||
clickhouselogsexporter:
|
||||
dsn: tcp://clickhouse:9000/signoz_logs
|
||||
timeout: 10s
|
||||
use_new_schema: true
|
||||
# debug: {}
|
||||
service:
|
||||
telemetry:
|
||||
logs:
|
||||
encoding: json
|
||||
metrics:
|
||||
address: 0.0.0.0:8888
|
||||
extensions:
|
||||
- health_check
|
||||
- pprof
|
||||
@@ -92,11 +81,11 @@ service:
|
||||
metrics:
|
||||
receivers: [otlp]
|
||||
processors: [batch]
|
||||
exporters: [clickhousemetricswrite, signozclickhousemetrics]
|
||||
exporters: [signozclickhousemetrics]
|
||||
metrics/prometheus:
|
||||
receivers: [prometheus]
|
||||
processors: [batch]
|
||||
exporters: [clickhousemetricswrite/prometheus, signozclickhousemetrics]
|
||||
exporters: [signozclickhousemetrics]
|
||||
logs:
|
||||
receivers: [otlp]
|
||||
processors: [batch]
|
||||
|
||||
@@ -177,7 +177,7 @@ services:
|
||||
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
|
||||
signoz:
|
||||
!!merge <<: *db-depend
|
||||
image: signoz/signoz:${VERSION:-v0.87.0}
|
||||
image: signoz/signoz:${VERSION:-v0.88.1}
|
||||
container_name: signoz
|
||||
command:
|
||||
- --config=/root/config/prometheus.yml
|
||||
@@ -197,6 +197,7 @@ services:
|
||||
- GODEBUG=netdns=go
|
||||
- TELEMETRY_ENABLED=true
|
||||
- DEPLOYMENT_TYPE=docker-standalone-amd
|
||||
- DOT_METRICS_ENABLED=true
|
||||
healthcheck:
|
||||
test:
|
||||
- CMD
|
||||
@@ -210,7 +211,7 @@ services:
|
||||
# TODO: support otel-collector multiple replicas. Nginx/Traefik for loadbalancing?
|
||||
otel-collector:
|
||||
!!merge <<: *db-depend
|
||||
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-v0.111.42}
|
||||
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-v0.128.0}
|
||||
container_name: signoz-otel-collector
|
||||
command:
|
||||
- --config=/etc/otel-collector-config.yaml
|
||||
@@ -236,7 +237,7 @@ services:
|
||||
condition: service_healthy
|
||||
schema-migrator-sync:
|
||||
!!merge <<: *common
|
||||
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-v0.111.42}
|
||||
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-v0.128.0}
|
||||
container_name: schema-migrator-sync
|
||||
command:
|
||||
- sync
|
||||
@@ -247,7 +248,7 @@ services:
|
||||
condition: service_healthy
|
||||
schema-migrator-async:
|
||||
!!merge <<: *db-depend
|
||||
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-v0.111.42}
|
||||
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-v0.128.0}
|
||||
container_name: schema-migrator-async
|
||||
command:
|
||||
- async
|
||||
|
||||
@@ -110,7 +110,7 @@ services:
|
||||
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
|
||||
signoz:
|
||||
!!merge <<: *db-depend
|
||||
image: signoz/signoz:${VERSION:-v0.87.0}
|
||||
image: signoz/signoz:${VERSION:-v0.88.1}
|
||||
container_name: signoz
|
||||
command:
|
||||
- --config=/root/config/prometheus.yml
|
||||
@@ -130,6 +130,7 @@ services:
|
||||
- GODEBUG=netdns=go
|
||||
- TELEMETRY_ENABLED=true
|
||||
- DEPLOYMENT_TYPE=docker-standalone-amd
|
||||
- DOT_METRICS_ENABLED=true
|
||||
healthcheck:
|
||||
test:
|
||||
- CMD
|
||||
@@ -142,7 +143,7 @@ services:
|
||||
retries: 3
|
||||
otel-collector:
|
||||
!!merge <<: *db-depend
|
||||
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-v0.111.42}
|
||||
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-v0.128.0}
|
||||
container_name: signoz-otel-collector
|
||||
command:
|
||||
- --config=/etc/otel-collector-config.yaml
|
||||
@@ -164,7 +165,7 @@ services:
|
||||
condition: service_healthy
|
||||
schema-migrator-sync:
|
||||
!!merge <<: *common
|
||||
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-v0.111.42}
|
||||
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-v0.128.0}
|
||||
container_name: schema-migrator-sync
|
||||
command:
|
||||
- sync
|
||||
@@ -176,7 +177,7 @@ services:
|
||||
restart: on-failure
|
||||
schema-migrator-async:
|
||||
!!merge <<: *db-depend
|
||||
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-v0.111.42}
|
||||
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-v0.128.0}
|
||||
container_name: schema-migrator-async
|
||||
command:
|
||||
- async
|
||||
|
||||
@@ -26,7 +26,7 @@ processors:
|
||||
detectors: [env, system]
|
||||
timeout: 2s
|
||||
signozspanmetrics/delta:
|
||||
metrics_exporter: clickhousemetricswrite, signozclickhousemetrics
|
||||
metrics_exporter: signozclickhousemetrics
|
||||
metrics_flush_interval: 60s
|
||||
latency_histogram_buckets: [100us, 1ms, 2ms, 6ms, 10ms, 50ms, 100ms, 250ms, 500ms, 1000ms, 1400ms, 2000ms, 5s, 10s, 20s, 40s, 60s ]
|
||||
dimensions_cache_size: 100000
|
||||
@@ -60,27 +60,16 @@ exporters:
|
||||
datasource: tcp://clickhouse:9000/signoz_traces
|
||||
low_cardinal_exception_grouping: ${env:LOW_CARDINAL_EXCEPTION_GROUPING}
|
||||
use_new_schema: true
|
||||
clickhousemetricswrite:
|
||||
endpoint: tcp://clickhouse:9000/signoz_metrics
|
||||
disable_v2: true
|
||||
resource_to_telemetry_conversion:
|
||||
enabled: true
|
||||
clickhousemetricswrite/prometheus:
|
||||
endpoint: tcp://clickhouse:9000/signoz_metrics
|
||||
disable_v2: true
|
||||
signozclickhousemetrics:
|
||||
dsn: tcp://clickhouse:9000/signoz_metrics
|
||||
clickhouselogsexporter:
|
||||
dsn: tcp://clickhouse:9000/signoz_logs
|
||||
timeout: 10s
|
||||
use_new_schema: true
|
||||
# debug: {}
|
||||
service:
|
||||
telemetry:
|
||||
logs:
|
||||
encoding: json
|
||||
metrics:
|
||||
address: 0.0.0.0:8888
|
||||
extensions:
|
||||
- health_check
|
||||
- pprof
|
||||
@@ -92,11 +81,11 @@ service:
|
||||
metrics:
|
||||
receivers: [otlp]
|
||||
processors: [batch]
|
||||
exporters: [clickhousemetricswrite, signozclickhousemetrics]
|
||||
exporters: [signozclickhousemetrics]
|
||||
metrics/prometheus:
|
||||
receivers: [prometheus]
|
||||
processors: [batch]
|
||||
exporters: [clickhousemetricswrite/prometheus, signozclickhousemetrics]
|
||||
exporters: [signozclickhousemetrics]
|
||||
logs:
|
||||
receivers: [otlp]
|
||||
processors: [batch]
|
||||
|
||||
@@ -16,7 +16,7 @@ __Table of Contents__
|
||||
- [Prerequisites](#prerequisites-1)
|
||||
- [Install Helm Repo and Charts](#install-helm-repo-and-charts)
|
||||
- [Start the OpenTelemetry Demo App](#start-the-opentelemetry-demo-app-1)
|
||||
- [Moniitor with SigNoz (Kubernetes)](#monitor-with-signoz-kubernetes)
|
||||
- [Monitor with SigNoz (Kubernetes)](#monitor-with-signoz-kubernetes)
|
||||
- [What's next](#whats-next)
|
||||
|
||||
|
||||
|
||||
@@ -203,17 +203,6 @@ func NewServer(config signoz.Config, signoz *signoz.SigNoz, jwt *authtypes.JWT)
|
||||
&opAmpModel.AllAgents, agentConfMgr, signoz.Instrumentation,
|
||||
)
|
||||
|
||||
orgs, err := apiHandler.Signoz.Modules.OrgGetter.ListByOwnedKeyRange(context.Background())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, org := range orgs {
|
||||
errorList := reader.PreloadMetricsMetadata(context.Background(), org.ID)
|
||||
for _, er := range errorList {
|
||||
zap.L().Error("failed to preload metrics metadata", zap.Error(er))
|
||||
}
|
||||
}
|
||||
|
||||
return s, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"github.com/SigNoz/signoz/ee/licensing"
|
||||
"github.com/SigNoz/signoz/ee/licensing/httplicensing"
|
||||
"github.com/SigNoz/signoz/ee/query-service/app"
|
||||
"github.com/SigNoz/signoz/ee/sqlschema/postgressqlschema"
|
||||
"github.com/SigNoz/signoz/ee/sqlstore/postgressqlstore"
|
||||
"github.com/SigNoz/signoz/ee/zeus"
|
||||
"github.com/SigNoz/signoz/ee/zeus/httpzeus"
|
||||
@@ -21,6 +22,7 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/modules/organization"
|
||||
baseconst "github.com/SigNoz/signoz/pkg/query-service/constants"
|
||||
"github.com/SigNoz/signoz/pkg/signoz"
|
||||
"github.com/SigNoz/signoz/pkg/sqlschema"
|
||||
"github.com/SigNoz/signoz/pkg/sqlstore"
|
||||
"github.com/SigNoz/signoz/pkg/sqlstore/sqlstorehook"
|
||||
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
||||
@@ -145,6 +147,14 @@ func main() {
|
||||
signoz.NewEmailingProviderFactories(),
|
||||
signoz.NewCacheProviderFactories(),
|
||||
signoz.NewWebProviderFactories(),
|
||||
func(sqlstore sqlstore.SQLStore) factory.NamedMap[factory.ProviderFactory[sqlschema.SQLSchema, sqlschema.Config]] {
|
||||
existingFactories := signoz.NewSQLSchemaProviderFactories(sqlstore)
|
||||
if err := existingFactories.Add(postgressqlschema.NewFactory(sqlstore)); err != nil {
|
||||
zap.L().Fatal("Failed to add postgressqlschema factory", zap.Error(err))
|
||||
}
|
||||
|
||||
return existingFactories
|
||||
},
|
||||
sqlStoreFactories,
|
||||
signoz.NewTelemetryStoreProviderFactories(),
|
||||
)
|
||||
|
||||
36
ee/sqlschema/postgressqlschema/formatter.go
Normal file
36
ee/sqlschema/postgressqlschema/formatter.go
Normal file
@@ -0,0 +1,36 @@
|
||||
package postgressqlschema
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/sqlschema"
|
||||
)
|
||||
|
||||
type Formatter struct {
|
||||
sqlschema.Formatter
|
||||
}
|
||||
|
||||
func (formatter Formatter) SQLDataTypeOf(dataType sqlschema.DataType) string {
|
||||
if dataType == sqlschema.DataTypeTimestamp {
|
||||
return "TIMESTAMPTZ"
|
||||
}
|
||||
|
||||
return strings.ToUpper(dataType.String())
|
||||
}
|
||||
|
||||
func (formatter Formatter) DataTypeOf(dataType string) sqlschema.DataType {
|
||||
switch strings.ToUpper(dataType) {
|
||||
case "TIMESTAMPTZ", "TIMESTAMP", "TIMESTAMP WITHOUT TIME ZONE", "TIMESTAMP WITH TIME ZONE":
|
||||
return sqlschema.DataTypeTimestamp
|
||||
case "INT8":
|
||||
return sqlschema.DataTypeBigInt
|
||||
case "INT2", "INT4", "SMALLINT", "INTEGER":
|
||||
return sqlschema.DataTypeInteger
|
||||
case "BOOL", "BOOLEAN":
|
||||
return sqlschema.DataTypeBoolean
|
||||
case "VARCHAR", "CHARACTER VARYING", "CHARACTER":
|
||||
return sqlschema.DataTypeText
|
||||
}
|
||||
|
||||
return formatter.Formatter.DataTypeOf(dataType)
|
||||
}
|
||||
285
ee/sqlschema/postgressqlschema/provider.go
Normal file
285
ee/sqlschema/postgressqlschema/provider.go
Normal file
@@ -0,0 +1,285 @@
|
||||
package postgressqlschema
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/factory"
|
||||
"github.com/SigNoz/signoz/pkg/sqlschema"
|
||||
"github.com/SigNoz/signoz/pkg/sqlstore"
|
||||
"github.com/uptrace/bun"
|
||||
)
|
||||
|
||||
type provider struct {
|
||||
settings factory.ScopedProviderSettings
|
||||
fmter sqlschema.SQLFormatter
|
||||
sqlstore sqlstore.SQLStore
|
||||
operator sqlschema.SQLOperator
|
||||
}
|
||||
|
||||
func NewFactory(sqlstore sqlstore.SQLStore) factory.ProviderFactory[sqlschema.SQLSchema, sqlschema.Config] {
|
||||
return factory.NewProviderFactory(factory.MustNewName("postgres"), func(ctx context.Context, providerSettings factory.ProviderSettings, config sqlschema.Config) (sqlschema.SQLSchema, error) {
|
||||
return New(ctx, providerSettings, config, sqlstore)
|
||||
})
|
||||
}
|
||||
|
||||
func New(ctx context.Context, providerSettings factory.ProviderSettings, config sqlschema.Config, sqlstore sqlstore.SQLStore) (sqlschema.SQLSchema, error) {
|
||||
settings := factory.NewScopedProviderSettings(providerSettings, "github.com/SigNoz/signoz/pkg/sqlschema/postgressqlschema")
|
||||
fmter := Formatter{Formatter: sqlschema.NewFormatter(sqlstore.BunDB().Dialect())}
|
||||
|
||||
return &provider{
|
||||
sqlstore: sqlstore,
|
||||
fmter: fmter,
|
||||
settings: settings,
|
||||
operator: sqlschema.NewOperator(fmter, sqlschema.OperatorSupport{
|
||||
DropConstraint: true,
|
||||
ColumnIfNotExistsExists: true,
|
||||
AlterColumnSetNotNull: true,
|
||||
}),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (provider *provider) Formatter() sqlschema.SQLFormatter {
|
||||
return provider.fmter
|
||||
}
|
||||
|
||||
func (provider *provider) Operator() sqlschema.SQLOperator {
|
||||
return provider.operator
|
||||
}
|
||||
|
||||
func (provider *provider) GetTable(ctx context.Context, tableName sqlschema.TableName) (*sqlschema.Table, []*sqlschema.UniqueConstraint, error) {
|
||||
rows, err := provider.
|
||||
sqlstore.
|
||||
BunDB().
|
||||
QueryContext(ctx, `
|
||||
SELECT
|
||||
c.column_name,
|
||||
c.is_nullable = 'YES',
|
||||
c.udt_name,
|
||||
c.column_default
|
||||
FROM
|
||||
information_schema.columns AS c
|
||||
WHERE
|
||||
c.table_name = ?`, string(tableName))
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if err := rows.Close(); err != nil {
|
||||
provider.settings.Logger().ErrorContext(ctx, "error closing rows", "error", err)
|
||||
}
|
||||
}()
|
||||
|
||||
columns := make([]*sqlschema.Column, 0)
|
||||
for rows.Next() {
|
||||
var (
|
||||
name string
|
||||
sqlDataType string
|
||||
nullable bool
|
||||
defaultVal *string
|
||||
)
|
||||
if err := rows.Scan(&name, &nullable, &sqlDataType, &defaultVal); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
columnDefault := ""
|
||||
if defaultVal != nil {
|
||||
columnDefault = *defaultVal
|
||||
}
|
||||
|
||||
columns = append(columns, &sqlschema.Column{
|
||||
Name: sqlschema.ColumnName(name),
|
||||
Nullable: nullable,
|
||||
DataType: provider.fmter.DataTypeOf(sqlDataType),
|
||||
Default: columnDefault,
|
||||
})
|
||||
}
|
||||
|
||||
constraintsRows, err := provider.
|
||||
sqlstore.
|
||||
BunDB().
|
||||
QueryContext(ctx, `
|
||||
SELECT
|
||||
c.column_name,
|
||||
constraint_name,
|
||||
constraint_type
|
||||
FROM
|
||||
information_schema.table_constraints tc
|
||||
JOIN information_schema.constraint_column_usage AS ccu USING (constraint_schema, constraint_catalog, table_name, constraint_name)
|
||||
JOIN information_schema.columns AS c ON c.table_schema = tc.constraint_schema AND tc.table_name = c.table_name AND ccu.column_name = c.column_name
|
||||
WHERE
|
||||
c.table_name = ?`, string(tableName))
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if err := constraintsRows.Close(); err != nil {
|
||||
provider.settings.Logger().ErrorContext(ctx, "error closing rows", "error", err)
|
||||
}
|
||||
}()
|
||||
|
||||
var primaryKeyConstraint *sqlschema.PrimaryKeyConstraint
|
||||
uniqueConstraintsMap := make(map[string]*sqlschema.UniqueConstraint)
|
||||
for constraintsRows.Next() {
|
||||
var (
|
||||
name string
|
||||
constraintName string
|
||||
constraintType string
|
||||
)
|
||||
|
||||
if err := constraintsRows.Scan(&name, &constraintName, &constraintType); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
if constraintType == "PRIMARY KEY" {
|
||||
if primaryKeyConstraint == nil {
|
||||
primaryKeyConstraint = (&sqlschema.PrimaryKeyConstraint{
|
||||
ColumnNames: []sqlschema.ColumnName{sqlschema.ColumnName(name)},
|
||||
}).Named(constraintName).(*sqlschema.PrimaryKeyConstraint)
|
||||
} else {
|
||||
primaryKeyConstraint.ColumnNames = append(primaryKeyConstraint.ColumnNames, sqlschema.ColumnName(name))
|
||||
}
|
||||
}
|
||||
|
||||
if constraintType == "UNIQUE" {
|
||||
if _, ok := uniqueConstraintsMap[constraintName]; !ok {
|
||||
uniqueConstraintsMap[constraintName] = (&sqlschema.UniqueConstraint{
|
||||
ColumnNames: []sqlschema.ColumnName{sqlschema.ColumnName(name)},
|
||||
}).Named(constraintName).(*sqlschema.UniqueConstraint)
|
||||
} else {
|
||||
uniqueConstraintsMap[constraintName].ColumnNames = append(uniqueConstraintsMap[constraintName].ColumnNames, sqlschema.ColumnName(name))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreignKeyConstraintsRows, err := provider.
|
||||
sqlstore.
|
||||
BunDB().
|
||||
QueryContext(ctx, `
|
||||
SELECT
|
||||
tc.constraint_name,
|
||||
kcu.table_name AS referencing_table,
|
||||
kcu.column_name AS referencing_column,
|
||||
ccu.table_name AS referenced_table,
|
||||
ccu.column_name AS referenced_column
|
||||
FROM
|
||||
information_schema.key_column_usage kcu
|
||||
JOIN information_schema.table_constraints tc ON kcu.constraint_name = tc.constraint_name AND kcu.table_schema = tc.table_schema
|
||||
JOIN information_schema.constraint_column_usage ccu ON ccu.constraint_name = tc.constraint_name AND ccu.table_schema = tc.table_schema
|
||||
WHERE
|
||||
tc.constraint_type = ?
|
||||
AND kcu.table_name = ?`, "FOREIGN KEY", string(tableName))
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if err := foreignKeyConstraintsRows.Close(); err != nil {
|
||||
provider.settings.Logger().ErrorContext(ctx, "error closing rows", "error", err)
|
||||
}
|
||||
}()
|
||||
|
||||
foreignKeyConstraints := make([]*sqlschema.ForeignKeyConstraint, 0)
|
||||
for foreignKeyConstraintsRows.Next() {
|
||||
var (
|
||||
constraintName string
|
||||
referencingTable string
|
||||
referencingColumn string
|
||||
referencedTable string
|
||||
referencedColumn string
|
||||
)
|
||||
|
||||
if err := foreignKeyConstraintsRows.Scan(&constraintName, &referencingTable, &referencingColumn, &referencedTable, &referencedColumn); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
foreignKeyConstraints = append(foreignKeyConstraints, (&sqlschema.ForeignKeyConstraint{
|
||||
ReferencingColumnName: sqlschema.ColumnName(referencingColumn),
|
||||
ReferencedTableName: sqlschema.TableName(referencedTable),
|
||||
ReferencedColumnName: sqlschema.ColumnName(referencedColumn),
|
||||
}).Named(constraintName).(*sqlschema.ForeignKeyConstraint))
|
||||
}
|
||||
|
||||
uniqueConstraints := make([]*sqlschema.UniqueConstraint, 0)
|
||||
for _, uniqueConstraint := range uniqueConstraintsMap {
|
||||
uniqueConstraints = append(uniqueConstraints, uniqueConstraint)
|
||||
}
|
||||
|
||||
return &sqlschema.Table{
|
||||
Name: tableName,
|
||||
Columns: columns,
|
||||
PrimaryKeyConstraint: primaryKeyConstraint,
|
||||
ForeignKeyConstraints: foreignKeyConstraints,
|
||||
}, uniqueConstraints, nil
|
||||
}
|
||||
|
||||
func (provider *provider) GetIndices(ctx context.Context, name sqlschema.TableName) ([]sqlschema.Index, error) {
|
||||
rows, err := provider.
|
||||
sqlstore.
|
||||
BunDB().
|
||||
QueryContext(ctx, `
|
||||
SELECT
|
||||
ct.relname AS table_name,
|
||||
ci.relname AS index_name,
|
||||
i.indisunique AS unique,
|
||||
i.indisprimary AS primary,
|
||||
a.attname AS column_name
|
||||
FROM
|
||||
pg_index i
|
||||
LEFT JOIN pg_class ct ON ct.oid = i.indrelid
|
||||
LEFT JOIN pg_class ci ON ci.oid = i.indexrelid
|
||||
LEFT JOIN pg_attribute a ON a.attrelid = ct.oid
|
||||
LEFT JOIN pg_constraint con ON con.conindid = i.indexrelid
|
||||
WHERE
|
||||
a.attnum = ANY(i.indkey)
|
||||
AND con.oid IS NULL
|
||||
AND ct.relkind = 'r'
|
||||
AND ct.relname = ?`, string(name))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if err := rows.Close(); err != nil {
|
||||
provider.settings.Logger().ErrorContext(ctx, "error closing rows", "error", err)
|
||||
}
|
||||
}()
|
||||
|
||||
uniqueIndicesMap := make(map[string]*sqlschema.UniqueIndex)
|
||||
for rows.Next() {
|
||||
var (
|
||||
tableName string
|
||||
indexName string
|
||||
unique bool
|
||||
primary bool
|
||||
columnName string
|
||||
)
|
||||
|
||||
if err := rows.Scan(&tableName, &indexName, &unique, &primary, &columnName); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if unique {
|
||||
if _, ok := uniqueIndicesMap[indexName]; !ok {
|
||||
uniqueIndicesMap[indexName] = &sqlschema.UniqueIndex{
|
||||
TableName: name,
|
||||
ColumnNames: []sqlschema.ColumnName{sqlschema.ColumnName(columnName)},
|
||||
}
|
||||
} else {
|
||||
uniqueIndicesMap[indexName].ColumnNames = append(uniqueIndicesMap[indexName].ColumnNames, sqlschema.ColumnName(columnName))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
indices := make([]sqlschema.Index, 0)
|
||||
for _, index := range uniqueIndicesMap {
|
||||
indices = append(indices, index)
|
||||
}
|
||||
|
||||
return indices, nil
|
||||
}
|
||||
|
||||
func (provider *provider) ToggleFKEnforcement(_ context.Context, _ bun.IDB, _ bool) error {
|
||||
return nil
|
||||
}
|
||||
@@ -1,154 +0,0 @@
|
||||
# QuerySearch Component Documentation
|
||||
|
||||
## Overview
|
||||
The QuerySearch component is a sophisticated query builder interface that allows users to construct complex search queries with real-time validation and autocomplete functionality.
|
||||
|
||||
## Dependencies
|
||||
```typescript
|
||||
// Core UI
|
||||
import { Card, Collapse, Space, Tag, Typography } from 'antd';
|
||||
|
||||
// Code Editor
|
||||
import {
|
||||
autocompletion,
|
||||
CompletionContext,
|
||||
CompletionResult,
|
||||
startCompletion,
|
||||
} from '@codemirror/autocomplete';
|
||||
import { javascript } from '@codemirror/lang-javascript';
|
||||
import { ViewPlugin, ViewUpdate } from '@codemirror/view';
|
||||
import { copilot } from '@uiw/codemirror-theme-copilot';
|
||||
import CodeMirror, { EditorView, Extension } from '@uiw/react-codemirror';
|
||||
|
||||
// Custom Hooks and Utilities
|
||||
import { useGetQueryKeySuggestions } from 'hooks/querySuggestions/useGetQueryKeySuggestions';
|
||||
import { getValueSuggestions } from 'api/querySuggestions/getValueSuggestion';
|
||||
import { queryOperatorSuggestions, validateQuery } from 'utils/antlrQueryUtils';
|
||||
import { getQueryContextAtCursor } from 'utils/queryContextUtils';
|
||||
```
|
||||
|
||||
## Key Features
|
||||
1. Real-time query validation
|
||||
2. Context-aware autocompletion
|
||||
3. Support for various query operators (=, !=, IN, LIKE, etc.)
|
||||
4. Support for complex conditions with AND/OR operators
|
||||
5. Support for functions (HAS, HASANY, HASALL)
|
||||
6. Support for parentheses and nested conditions
|
||||
7. Query examples for common use cases
|
||||
|
||||
## State Management
|
||||
```typescript
|
||||
const [query, setQuery] = useState<string>('');
|
||||
const [valueSuggestions, setValueSuggestions] = useState<any[]>([]);
|
||||
const [activeKey, setActiveKey] = useState<string>('');
|
||||
const [isLoadingSuggestions, setIsLoadingSuggestions] = useState(false);
|
||||
const [queryContext, setQueryContext] = useState<IQueryContext | null>(null);
|
||||
const [validation, setValidation] = useState<IValidationResult>({...});
|
||||
const [editingMode, setEditingMode] = useState<'key' | 'operator' | 'value' | 'conjunction' | 'function' | 'parenthesis' | 'bracketList' | null>(null);
|
||||
```
|
||||
|
||||
## Core Functions
|
||||
|
||||
### 1. Autocomplete Handler
|
||||
```typescript
|
||||
function myCompletions(context: CompletionContext): CompletionResult | null {
|
||||
// Handles autocomplete suggestions based on context
|
||||
// Supports different contexts: key, operator, value, function, etc.
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Value Suggestions Fetcher
|
||||
```typescript
|
||||
const fetchValueSuggestions = useCallback(
|
||||
async (key: string): Promise<void> => {
|
||||
// Fetches value suggestions for a given key
|
||||
// Handles loading states and error cases
|
||||
},
|
||||
[activeKey, isLoadingSuggestions],
|
||||
);
|
||||
```
|
||||
|
||||
### 3. Query Change Handler
|
||||
```typescript
|
||||
const handleQueryChange = useCallback(async (newQuery: string) => {
|
||||
// Updates query and validates it
|
||||
// Handles validation errors
|
||||
}, []);
|
||||
```
|
||||
|
||||
## Query Context Types
|
||||
1. Key context: When editing a field name
|
||||
2. Operator context: When selecting an operator
|
||||
3. Value context: When entering a value
|
||||
4. Conjunction context: When using AND/OR
|
||||
5. Function context: When using functions
|
||||
6. Parenthesis context: When using parentheses
|
||||
7. Bracket list context: When using IN operator
|
||||
|
||||
## Example Queries
|
||||
```typescript
|
||||
const queryExamples = [
|
||||
{ label: 'Basic Query', query: "status = 'error'" },
|
||||
{ label: 'Multiple Conditions', query: "status = 'error' AND service = 'frontend'" },
|
||||
{ label: 'IN Operator', query: "status IN ['error', 'warning']" },
|
||||
{ label: 'Function Usage', query: "HAS(service, 'frontend')" },
|
||||
{ label: 'Numeric Comparison', query: 'duration > 1000' },
|
||||
// ... more examples
|
||||
];
|
||||
```
|
||||
|
||||
## Performance Optimizations
|
||||
1. Uses `useCallback` for memoized functions
|
||||
2. Tracks component mount state to prevent updates after unmount
|
||||
3. Debounces suggestion fetching
|
||||
4. Caches key suggestions
|
||||
|
||||
## Error Handling
|
||||
```typescript
|
||||
try {
|
||||
const validationResponse = validateQuery(newQuery);
|
||||
setValidation(validationResponse);
|
||||
} catch (error) {
|
||||
setValidation({
|
||||
isValid: false,
|
||||
message: 'Failed to process query',
|
||||
errors: [error as IDetailedError],
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
## Usage Example
|
||||
```typescript
|
||||
<QuerySearch />
|
||||
```
|
||||
|
||||
## Styling
|
||||
- Uses SCSS for styling
|
||||
- Custom classes for different components
|
||||
- Theme integration with CodeMirror
|
||||
|
||||
## Best Practices
|
||||
1. Always validate queries before submission
|
||||
2. Handle loading states appropriately
|
||||
3. Provide clear error messages
|
||||
4. Use appropriate operators for different data types
|
||||
5. Consider performance implications of complex queries
|
||||
|
||||
## Common Issues and Solutions
|
||||
1. Query validation errors
|
||||
- Check syntax and operator usage
|
||||
- Verify data types match operator requirements
|
||||
2. Performance issues
|
||||
- Optimize suggestion fetching
|
||||
- Cache frequently used values
|
||||
3. UI/UX issues
|
||||
- Ensure clear error messages
|
||||
- Provide helpful suggestions
|
||||
- Show appropriate loading states
|
||||
|
||||
## Future Improvements
|
||||
1. Add more query examples
|
||||
2. Enhance error messages
|
||||
3. Improve performance for large datasets
|
||||
4. Add more operator support
|
||||
5. Enhance UI/UX features
|
||||
@@ -14,8 +14,8 @@
|
||||
"remove_label_confirm": "This action will remove all the labels. Do you want to proceed?",
|
||||
"remove_label_success": "Labels cleared",
|
||||
"alert_form_step1": "Step 1 - Define the metric",
|
||||
"alert_form_step2": "Step 2 - Define Alert Conditions",
|
||||
"alert_form_step3": "Step 3 - Alert Configuration",
|
||||
"alert_form_step2": "Step {{step}} - Define Alert Conditions",
|
||||
"alert_form_step3": "Step {{step}} - Alert Configuration",
|
||||
"metric_query_max_limit": "Can not create query. You can create maximum of 5 queries",
|
||||
"confirm_save_title": "Save Changes",
|
||||
"confirm_save_content_part1": "Your alert built with",
|
||||
|
||||
@@ -7,8 +7,8 @@
|
||||
"remove_label_confirm": "This action will remove all the labels. Do you want to proceed?",
|
||||
"remove_label_success": "Labels cleared",
|
||||
"alert_form_step1": "Step 1 - Define the metric",
|
||||
"alert_form_step2": "Step 2 - Define Alert Conditions",
|
||||
"alert_form_step3": "Step 3 - Alert Configuration",
|
||||
"alert_form_step2": "Step {{step}} - Define Alert Conditions",
|
||||
"alert_form_step3": "Step {{step}} - Alert Configuration",
|
||||
"metric_query_max_limit": "Can not create query. You can create maximum of 5 queries",
|
||||
"confirm_save_title": "Save Changes",
|
||||
"confirm_save_content_part1": "Your alert built with",
|
||||
|
||||
@@ -7,8 +7,8 @@
|
||||
"remove_label_confirm": "This action will remove all the labels. Do you want to proceed?",
|
||||
"remove_label_success": "Labels cleared",
|
||||
"alert_form_step1": "Step 1 - Define the metric",
|
||||
"alert_form_step2": "Step 2 - Define Alert Conditions",
|
||||
"alert_form_step3": "Step 3 - Alert Configuration",
|
||||
"alert_form_step2": "Step {{step}} - Define Alert Conditions",
|
||||
"alert_form_step3": "Step {{step}} - Alert Configuration",
|
||||
"metric_query_max_limit": "Can not create query. You can create maximum of 5 queries",
|
||||
"confirm_save_title": "Save Changes",
|
||||
"confirm_save_content_part1": "Your alert built with",
|
||||
|
||||
@@ -191,7 +191,8 @@ function App(): JSX.Element {
|
||||
// if the user is on basic plan then remove billing
|
||||
if (isOnBasicPlan) {
|
||||
updatedRoutes = updatedRoutes.filter(
|
||||
(route) => route?.path !== ROUTES.BILLING,
|
||||
(route) =>
|
||||
route?.path !== ROUTES.BILLING && route?.path !== ROUTES.INTEGRATIONS,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -204,7 +205,8 @@ function App(): JSX.Element {
|
||||
} else {
|
||||
// if not a cloud user then remove billing and add list licenses route
|
||||
updatedRoutes = updatedRoutes.filter(
|
||||
(route) => route?.path !== ROUTES.BILLING,
|
||||
(route) =>
|
||||
route?.path !== ROUTES.BILLING && route?.path !== ROUTES.INTEGRATIONS,
|
||||
);
|
||||
updatedRoutes = [...updatedRoutes, LIST_LICENSES];
|
||||
}
|
||||
|
||||
@@ -7,5 +7,16 @@ import {
|
||||
|
||||
export const getKeySuggestions = (
|
||||
props: QueryKeyRequestProps,
|
||||
): Promise<AxiosResponse<QueryKeySuggestionsResponseProps>> =>
|
||||
axios.get(`/fields/keys?signal=${props.signal}&name=${props.name}`);
|
||||
): Promise<AxiosResponse<QueryKeySuggestionsResponseProps>> => {
|
||||
const {
|
||||
signal = '',
|
||||
searchText = '',
|
||||
metricName = '',
|
||||
fieldContext = '',
|
||||
fieldDataType = '',
|
||||
} = props;
|
||||
|
||||
return axios.get(
|
||||
`/fields/keys?signal=${signal}&searchText=${searchText}&metricName=${metricName}&fieldContext=${fieldContext}&fieldDataType=${fieldDataType}`,
|
||||
);
|
||||
};
|
||||
|
||||
@@ -7,5 +7,14 @@ import {
|
||||
|
||||
export const getValueSuggestions = (
|
||||
props: QueryKeyValueRequestProps,
|
||||
): Promise<AxiosResponse<QueryKeyValueSuggestionsResponseProps>> =>
|
||||
axios.get(`/fields/values?signal=${props.signal}&name=${props.key}`);
|
||||
): Promise<AxiosResponse<QueryKeyValueSuggestionsResponseProps>> => {
|
||||
const { signal, key, searchText } = props;
|
||||
|
||||
const encodedSignal = encodeURIComponent(signal);
|
||||
const encodedKey = encodeURIComponent(key);
|
||||
const encodedSearchText = encodeURIComponent(searchText);
|
||||
|
||||
return axios.get(
|
||||
`/fields/values?signal=${encodedSignal}&name=${encodedKey}&searchText=${encodedSearchText}`,
|
||||
);
|
||||
};
|
||||
|
||||
@@ -4,12 +4,36 @@ import { MetricRangePayloadV3 } from 'types/api/metrics/getQueryRange';
|
||||
import {
|
||||
DistributionData,
|
||||
MetricRangePayloadV5,
|
||||
QueryRangeRequestV5,
|
||||
RawData,
|
||||
ScalarData,
|
||||
TimeSeriesData,
|
||||
} from 'types/api/v5/queryRange';
|
||||
import { QueryDataV3 } from 'types/api/widgets/getQuery';
|
||||
|
||||
function getColName(
|
||||
col: ScalarData['columns'][number],
|
||||
legendMap: Record<string, string>,
|
||||
aggregationPerQuery: Record<string, any>,
|
||||
): string {
|
||||
const aggregation =
|
||||
aggregationPerQuery?.[col.queryName]?.[col.aggregationIndex];
|
||||
const legend = legendMap[col.queryName];
|
||||
const aggregationName = aggregation?.alias || aggregation?.expression || '';
|
||||
|
||||
if (col.columnType === 'group') {
|
||||
return col.name;
|
||||
}
|
||||
|
||||
if (aggregationName && aggregationPerQuery[col.queryName].length > 1) {
|
||||
if (legend) {
|
||||
return `${aggregationName}-${legend}`;
|
||||
}
|
||||
return `${col.queryName}.${aggregationName}`;
|
||||
}
|
||||
return legend || col.queryName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts V5 TimeSeriesData to legacy format
|
||||
*/
|
||||
@@ -18,11 +42,18 @@ function convertTimeSeriesData(
|
||||
legendMap: Record<string, string>,
|
||||
): QueryDataV3 {
|
||||
// Convert V5 time series format to legacy QueryDataV3 format
|
||||
|
||||
return {
|
||||
queryName: timeSeriesData.queryName,
|
||||
legend: legendMap[timeSeriesData.queryName] || timeSeriesData.queryName,
|
||||
series: timeSeriesData?.aggregations?.flatMap((aggregation) =>
|
||||
aggregation.series.map((series) => ({
|
||||
series: timeSeriesData?.aggregations?.flatMap((aggregation) => {
|
||||
const { index, alias, series } = aggregation;
|
||||
|
||||
if (!series || !series.length) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return series.map((series) => ({
|
||||
labels: series.labels
|
||||
? Object.fromEntries(
|
||||
series.labels.map((label) => [label.key.name, label.value]),
|
||||
@@ -35,8 +66,13 @@ function convertTimeSeriesData(
|
||||
timestamp: value.timestamp,
|
||||
value: String(value.value),
|
||||
})),
|
||||
})),
|
||||
),
|
||||
metaData: {
|
||||
alias,
|
||||
index,
|
||||
queryName: timeSeriesData.queryName,
|
||||
},
|
||||
}));
|
||||
}),
|
||||
list: null,
|
||||
};
|
||||
}
|
||||
@@ -47,6 +83,7 @@ function convertTimeSeriesData(
|
||||
function convertScalarDataArrayToTable(
|
||||
scalarDataArray: ScalarData[],
|
||||
legendMap: Record<string, string>,
|
||||
aggregationPerQuery: Record<string, any>,
|
||||
): QueryDataV3[] {
|
||||
// If no scalar data, return empty structure
|
||||
|
||||
@@ -59,9 +96,20 @@ function convertScalarDataArrayToTable(
|
||||
// Get query name from the first column
|
||||
const queryName = scalarData?.columns?.[0]?.queryName || '';
|
||||
|
||||
if ((scalarData as any)?.aggregations?.length > 0) {
|
||||
return {
|
||||
...convertTimeSeriesData(scalarData as any, legendMap),
|
||||
table: {
|
||||
columns: [],
|
||||
rows: [],
|
||||
},
|
||||
list: null,
|
||||
};
|
||||
}
|
||||
|
||||
// Collect columns for this specific query
|
||||
const columns = scalarData?.columns?.map((col) => ({
|
||||
name: col.columnType === 'aggregation' ? col.queryName : col.name,
|
||||
name: getColName(col, legendMap, aggregationPerQuery),
|
||||
queryName: col.queryName,
|
||||
isValueColumn: col.columnType === 'aggregation',
|
||||
}));
|
||||
@@ -71,8 +119,7 @@ function convertScalarDataArrayToTable(
|
||||
const rowData: Record<string, any> = {};
|
||||
|
||||
scalarData?.columns?.forEach((col, colIndex) => {
|
||||
const columnName =
|
||||
col.columnType === 'aggregation' ? col.queryName : col.name;
|
||||
const columnName = getColName(col, legendMap, aggregationPerQuery);
|
||||
rowData[columnName] = dataRow[colIndex];
|
||||
});
|
||||
|
||||
@@ -92,6 +139,51 @@ function convertScalarDataArrayToTable(
|
||||
});
|
||||
}
|
||||
|
||||
function convertScalerWithFormatForWeb(
|
||||
scalarDataArray: ScalarData[],
|
||||
legendMap: Record<string, string>,
|
||||
aggregationPerQuery: Record<string, any>,
|
||||
): QueryDataV3[] {
|
||||
if (!scalarDataArray || scalarDataArray.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return scalarDataArray.map((scalarData) => {
|
||||
const columns =
|
||||
scalarData.columns?.map((col) => {
|
||||
const colName = getColName(col, legendMap, aggregationPerQuery);
|
||||
|
||||
return {
|
||||
name: colName,
|
||||
queryName: col.queryName,
|
||||
isValueColumn: col.columnType === 'aggregation',
|
||||
};
|
||||
}) || [];
|
||||
|
||||
const rows =
|
||||
scalarData.data?.map((dataRow) => {
|
||||
const rowData: Record<string, any> = {};
|
||||
columns?.forEach((col, colIndex) => {
|
||||
rowData[col.name] = dataRow[colIndex];
|
||||
});
|
||||
return { data: rowData };
|
||||
}) || [];
|
||||
|
||||
const queryName = scalarData.columns?.[0]?.queryName || '';
|
||||
|
||||
return {
|
||||
queryName,
|
||||
legend: legendMap[queryName] || queryName,
|
||||
series: null,
|
||||
list: null,
|
||||
table: {
|
||||
columns,
|
||||
rows,
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts V5 RawData to legacy format
|
||||
*/
|
||||
@@ -136,6 +228,7 @@ function convertDistributionData(
|
||||
function convertV5DataByType(
|
||||
v5Data: any,
|
||||
legendMap: Record<string, string>,
|
||||
aggregationPerQuery: Record<string, any>,
|
||||
): MetricRangePayloadV3['data'] {
|
||||
switch (v5Data?.type) {
|
||||
case 'time_series': {
|
||||
@@ -150,7 +243,11 @@ function convertV5DataByType(
|
||||
case 'scalar': {
|
||||
const scalarData = v5Data.data.results as ScalarData[];
|
||||
// For scalar data, combine all results into separate table entries
|
||||
const combinedTables = convertScalarDataArrayToTable(scalarData, legendMap);
|
||||
const combinedTables = convertScalarDataArrayToTable(
|
||||
scalarData,
|
||||
legendMap,
|
||||
aggregationPerQuery,
|
||||
);
|
||||
return {
|
||||
resultType: 'scalar',
|
||||
result: combinedTables,
|
||||
@@ -183,23 +280,54 @@ function convertV5DataByType(
|
||||
/**
|
||||
* Converts V5 API response to legacy format expected by frontend components
|
||||
*/
|
||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||
export function convertV5ResponseToLegacy(
|
||||
v5Response: SuccessResponse<MetricRangePayloadV5>,
|
||||
legendMap: Record<string, string>,
|
||||
// formatForWeb?: boolean,
|
||||
formatForWeb?: boolean,
|
||||
): SuccessResponse<MetricRangePayloadV3> {
|
||||
const { payload } = v5Response;
|
||||
const { payload, params } = v5Response;
|
||||
const v5Data = payload?.data;
|
||||
|
||||
// todo - sagar
|
||||
const aggregationPerQuery =
|
||||
(params as QueryRangeRequestV5)?.compositeQuery?.queries
|
||||
?.filter((query) => query.type === 'builder_query')
|
||||
.reduce((acc, query) => {
|
||||
if (
|
||||
query.type === 'builder_query' &&
|
||||
'aggregations' in query.spec &&
|
||||
query.spec.name
|
||||
) {
|
||||
acc[query.spec.name] = query.spec.aggregations;
|
||||
}
|
||||
return acc;
|
||||
}, {} as Record<string, any>) || {};
|
||||
|
||||
// If formatForWeb is true, return as-is (like existing logic)
|
||||
// Exception: scalar data should always be converted to table format
|
||||
// if (formatForWeb && v5Data?.type !== 'scalar') {
|
||||
// return v5Response as any;
|
||||
// }
|
||||
if (formatForWeb && v5Data?.type === 'scalar') {
|
||||
const scalarData = v5Data.data.results as ScalarData[];
|
||||
const webTables = convertScalerWithFormatForWeb(
|
||||
scalarData,
|
||||
legendMap,
|
||||
aggregationPerQuery,
|
||||
);
|
||||
return {
|
||||
...v5Response,
|
||||
payload: {
|
||||
data: {
|
||||
resultType: 'scalar',
|
||||
result: webTables,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// Convert based on V5 response type
|
||||
const convertedData = convertV5DataByType(v5Data, legendMap);
|
||||
const convertedData = convertV5DataByType(
|
||||
v5Data,
|
||||
legendMap,
|
||||
aggregationPerQuery,
|
||||
);
|
||||
|
||||
// Create legacy-compatible response structure
|
||||
const legacyResponse: SuccessResponse<MetricRangePayloadV3> = {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { ApiV5Instance } from 'api';
|
||||
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
||||
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
|
||||
import { AxiosError } from 'axios';
|
||||
import { ENTITY_VERSION_V5 } from 'constants/app';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
|
||||
import {
|
||||
MetricRangePayloadV5,
|
||||
QueryRangePayloadV5,
|
||||
@@ -13,7 +13,7 @@ export const getQueryRangeV5 = async (
|
||||
version: string,
|
||||
signal: AbortSignal,
|
||||
headers?: Record<string, string>,
|
||||
): Promise<SuccessResponse<MetricRangePayloadV5> | ErrorResponse> => {
|
||||
): Promise<SuccessResponseV2<MetricRangePayloadV5>> => {
|
||||
try {
|
||||
if (version && version === ENTITY_VERSION_V5) {
|
||||
const response = await ApiV5Instance.post('/query_range', props, {
|
||||
@@ -22,11 +22,8 @@ export const getQueryRangeV5 = async (
|
||||
});
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
error: null,
|
||||
message: response.data.status,
|
||||
payload: response.data,
|
||||
params: props,
|
||||
httpStatusCode: response.status,
|
||||
data: response.data,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -37,14 +34,11 @@ export const getQueryRangeV5 = async (
|
||||
});
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
error: null,
|
||||
message: response.data.status,
|
||||
payload: response.data,
|
||||
params: props,
|
||||
httpStatusCode: response.status,
|
||||
data: response.data.data,
|
||||
};
|
||||
} catch (error) {
|
||||
return ErrorResponseHandler(error as AxiosError);
|
||||
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -74,8 +74,13 @@ function createBaseSpec(
|
||||
requestType: RequestType,
|
||||
panelType?: PANEL_TYPES,
|
||||
): BaseBuilderQuery {
|
||||
const nonEmptySelectColumns = (queryData.selectColumns as (
|
||||
| BaseAutocompleteData
|
||||
| TelemetryFieldKey
|
||||
)[])?.filter((c) => ('key' in c ? c?.key : c?.name));
|
||||
|
||||
return {
|
||||
stepInterval: queryData.stepInterval,
|
||||
stepInterval: queryData?.stepInterval || undefined,
|
||||
disabled: queryData.disabled,
|
||||
filter: queryData?.filter?.expression ? queryData.filter : undefined,
|
||||
groupBy:
|
||||
@@ -123,13 +128,15 @@ function createBaseSpec(
|
||||
})),
|
||||
}),
|
||||
),
|
||||
selectFields: isEmpty(queryData.selectColumns)
|
||||
selectFields: isEmpty(nonEmptySelectColumns)
|
||||
? undefined
|
||||
: queryData.selectColumns?.map(
|
||||
(column: BaseAutocompleteData): TelemetryFieldKey => ({
|
||||
name: column.key,
|
||||
fieldDataType: column?.dataType as FieldDataType,
|
||||
fieldContext: column?.type as FieldContext,
|
||||
: nonEmptySelectColumns?.map(
|
||||
(column: any): TelemetryFieldKey => ({
|
||||
name: column.name ?? column.key,
|
||||
fieldDataType:
|
||||
column?.fieldDataType ?? (column?.dataType as FieldDataType),
|
||||
fieldContext: column?.fieldContext ?? (column?.type as FieldContext),
|
||||
signal: column?.signal ?? undefined,
|
||||
}),
|
||||
),
|
||||
};
|
||||
@@ -157,6 +164,9 @@ export function parseAggregations(
|
||||
export function createAggregation(
|
||||
queryData: any,
|
||||
): TraceAggregation[] | LogAggregation[] | MetricAggregation[] {
|
||||
if (!queryData) {
|
||||
return [];
|
||||
}
|
||||
if (queryData.dataSource === DataSource.METRICS) {
|
||||
return [
|
||||
{
|
||||
@@ -243,7 +253,7 @@ function convertPromQueriesToV5(
|
||||
name: queryName,
|
||||
query: queryData.query,
|
||||
disabled: queryData.disabled || false,
|
||||
step: queryData.stepInterval,
|
||||
step: queryData?.stepInterval,
|
||||
stats: false, // PromQL specific field
|
||||
},
|
||||
}),
|
||||
@@ -318,6 +328,7 @@ export const prepareQueryRangePayloadV5 = ({
|
||||
start: startTime,
|
||||
end: endTime,
|
||||
formatForWeb,
|
||||
originalGraphType,
|
||||
}: GetQueryResultsProps): PrepareQueryRangePayloadV5Result => {
|
||||
let legendMap: Record<string, string> = {};
|
||||
const requestType = mapPanelTypeToRequestType(graphType);
|
||||
@@ -381,7 +392,11 @@ export const prepareQueryRangePayloadV5 = ({
|
||||
queries,
|
||||
},
|
||||
formatOptions: {
|
||||
formatTableResultForUI: !!formatForWeb,
|
||||
formatTableResultForUI:
|
||||
!!formatForWeb ||
|
||||
(originalGraphType
|
||||
? originalGraphType === PANEL_TYPES.TABLE
|
||||
: graphType === PANEL_TYPES.TABLE),
|
||||
},
|
||||
variables: Object.entries(variables).reduce((acc, [key, value]) => {
|
||||
acc[key] = { value };
|
||||
|
||||
@@ -101,13 +101,18 @@
|
||||
line-height: 28px;
|
||||
}
|
||||
|
||||
.changelog-media-image {
|
||||
.changelog-media-image,
|
||||
.changelog-media-video {
|
||||
height: auto;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--bg-slate-400, #1d212d);
|
||||
}
|
||||
|
||||
.changelog-media-video {
|
||||
margin: 12px 0;
|
||||
}
|
||||
}
|
||||
|
||||
.lightMode {
|
||||
|
||||
@@ -32,7 +32,7 @@ function renderMedia(media: Media): JSX.Element | null {
|
||||
controls
|
||||
controlsList="nodownload noplaybackrate"
|
||||
loop
|
||||
className="my-3 h-auto w-full rounded"
|
||||
className="changelog-media-video"
|
||||
>
|
||||
<source src={media.url} type={media.mime} />
|
||||
<track kind="captions" src="" label="No captions available" default />
|
||||
@@ -56,7 +56,7 @@ function ChangelogRenderer({ changelog }: Props): JSX.Element {
|
||||
</div>
|
||||
<span className="changelog-release-date">{formattedReleaseDate}</span>
|
||||
{changelog.features && changelog.features.length > 0 && (
|
||||
<div className="changelog-renderer-list flex flex-col gap-7">
|
||||
<div className="changelog-renderer-list">
|
||||
{changelog.features.map((feature) => (
|
||||
<div key={feature.id}>
|
||||
<h2>{feature.title}</h2>
|
||||
|
||||
@@ -18,7 +18,7 @@ function ErrorContent({ error }: ErrorContentProps): JSX.Element {
|
||||
errors: errorMessages,
|
||||
code: errorCode,
|
||||
message: errorMessage,
|
||||
} = error.error.error;
|
||||
} = error?.error?.error || {};
|
||||
return (
|
||||
<section className="error-content">
|
||||
{/* Summary Header */}
|
||||
|
||||
@@ -194,7 +194,7 @@ function HostMetricTraces({
|
||||
{!isError && traces.length > 0 && (
|
||||
<div className="host-metric-traces-table">
|
||||
<TraceExplorerControls
|
||||
isLoading={isFetching}
|
||||
isLoading={isFetching && traces.length === 0}
|
||||
totalCount={totalCount}
|
||||
perPageOptions={PER_PAGE_OPTIONS}
|
||||
showSizeChanger={false}
|
||||
@@ -203,7 +203,7 @@ function HostMetricTraces({
|
||||
tableLayout="fixed"
|
||||
pagination={false}
|
||||
scroll={{ x: true }}
|
||||
loading={isFetching}
|
||||
loading={isFetching && traces.length === 0}
|
||||
dataSource={traces}
|
||||
columns={traceListColumns}
|
||||
onRow={(): Record<string, unknown> => ({
|
||||
|
||||
@@ -37,7 +37,7 @@ import {
|
||||
ScrollText,
|
||||
X,
|
||||
} from 'lucide-react';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useSearchParams } from 'react-router-dom-v5-compat';
|
||||
import { AppState } from 'store/reducers';
|
||||
@@ -86,8 +86,12 @@ function HostMetricsDetails({
|
||||
endTime: endMs,
|
||||
}));
|
||||
|
||||
const lastSelectedInterval = useRef<Time | null>(null);
|
||||
|
||||
const [selectedInterval, setSelectedInterval] = useState<Time>(
|
||||
selectedTime as Time,
|
||||
lastSelectedInterval.current
|
||||
? lastSelectedInterval.current
|
||||
: (selectedTime as Time),
|
||||
);
|
||||
|
||||
const [selectedView, setSelectedView] = useState<VIEWS>(
|
||||
@@ -150,10 +154,11 @@ function HostMetricsDetails({
|
||||
}, [initialFilters]);
|
||||
|
||||
useEffect(() => {
|
||||
setSelectedInterval(selectedTime as Time);
|
||||
const currentSelectedInterval = lastSelectedInterval.current || selectedTime;
|
||||
setSelectedInterval(currentSelectedInterval as Time);
|
||||
|
||||
if (selectedTime !== 'custom') {
|
||||
const { maxTime, minTime } = GetMinMax(selectedTime);
|
||||
if (currentSelectedInterval !== 'custom') {
|
||||
const { maxTime, minTime } = GetMinMax(currentSelectedInterval);
|
||||
|
||||
setModalTimeRange({
|
||||
startTime: Math.floor(minTime / 1000000000),
|
||||
@@ -181,6 +186,7 @@ function HostMetricsDetails({
|
||||
|
||||
const handleTimeChange = useCallback(
|
||||
(interval: Time | CustomTimeType, dateTimeRange?: [number, number]): void => {
|
||||
lastSelectedInterval.current = interval as Time;
|
||||
setSelectedInterval(interval as Time);
|
||||
|
||||
if (interval === 'custom' && dateTimeRange) {
|
||||
@@ -356,6 +362,7 @@ function HostMetricsDetails({
|
||||
|
||||
const handleClose = (): void => {
|
||||
setSelectedInterval(selectedTime as Time);
|
||||
lastSelectedInterval.current = null;
|
||||
setSearchParams({});
|
||||
|
||||
if (selectedTime !== 'custom') {
|
||||
|
||||
@@ -13,13 +13,15 @@ import {
|
||||
CustomTimeType,
|
||||
Time,
|
||||
} from 'container/TopNav/DateTimeSelectionV2/config';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import { useResizeObserver } from 'hooks/useDimensions';
|
||||
import { useMultiIntersectionObserver } from 'hooks/useMultiIntersectionObserver';
|
||||
import { GetMetricQueryRange } from 'lib/dashboard/getQueryResults';
|
||||
import { getUPlotChartOptions } from 'lib/uPlotLib/getUplotChartOptions';
|
||||
import { getUPlotChartData } from 'lib/uPlotLib/utils/getUplotChartData';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useQueries, UseQueryResult } from 'react-query';
|
||||
import { QueryFunctionContext, useQueries, UseQueryResult } from 'react-query';
|
||||
import { SuccessResponse } from 'types/api';
|
||||
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
|
||||
|
||||
@@ -53,6 +55,11 @@ function Metrics({
|
||||
featureFlags?.find((flag) => flag.name === FeatureKeys.DOT_METRICS_ENABLED)
|
||||
?.active || false;
|
||||
|
||||
const {
|
||||
visibilities,
|
||||
setElement,
|
||||
} = useMultiIntersectionObserver(hostWidgetInfo.length, { threshold: 0.1 });
|
||||
|
||||
const queryPayloads = useMemo(
|
||||
() =>
|
||||
getHostQueryPayload(
|
||||
@@ -65,17 +72,22 @@ function Metrics({
|
||||
);
|
||||
|
||||
const queries = useQueries(
|
||||
queryPayloads.map((payload) => ({
|
||||
queryPayloads.map((payload, index) => ({
|
||||
queryKey: ['host-metrics', payload, ENTITY_VERSION_V4, 'HOST'],
|
||||
queryFn: (): Promise<SuccessResponse<MetricRangePayloadProps>> =>
|
||||
GetMetricQueryRange(payload, ENTITY_VERSION_V4),
|
||||
enabled: !!payload,
|
||||
queryFn: ({
|
||||
signal,
|
||||
}: QueryFunctionContext): Promise<
|
||||
SuccessResponse<MetricRangePayloadProps>
|
||||
> => GetMetricQueryRange(payload, ENTITY_VERSION_V4, signal),
|
||||
enabled: !!payload && visibilities[index],
|
||||
keepPreviousData: true,
|
||||
})),
|
||||
);
|
||||
|
||||
const isDarkMode = useIsDarkMode();
|
||||
const graphRef = useRef<HTMLDivElement>(null);
|
||||
const dimensions = useResizeObserver(graphRef);
|
||||
const { currentQuery } = useQueryBuilder();
|
||||
|
||||
const chartData = useMemo(
|
||||
() => queries.map(({ data }) => getUPlotChartData(data?.payload)),
|
||||
@@ -134,16 +146,24 @@ function Metrics({
|
||||
minTimeScale: graphTimeIntervals[idx].start,
|
||||
maxTimeScale: graphTimeIntervals[idx].end,
|
||||
onDragSelect: (start, end) => onDragSelect(start, end, idx),
|
||||
query: currentQuery,
|
||||
}),
|
||||
),
|
||||
[queries, isDarkMode, dimensions, graphTimeIntervals, onDragSelect],
|
||||
[
|
||||
queries,
|
||||
isDarkMode,
|
||||
dimensions,
|
||||
graphTimeIntervals,
|
||||
onDragSelect,
|
||||
currentQuery,
|
||||
],
|
||||
);
|
||||
|
||||
const renderCardContent = (
|
||||
query: UseQueryResult<SuccessResponse<MetricRangePayloadProps>, unknown>,
|
||||
idx: number,
|
||||
): JSX.Element => {
|
||||
if (query.isLoading) {
|
||||
if ((!query.data && query.isLoading) || !visibilities[idx]) {
|
||||
return <Skeleton />;
|
||||
}
|
||||
|
||||
@@ -181,7 +201,7 @@ function Metrics({
|
||||
</div>
|
||||
<Row gutter={24} className="host-metrics-container">
|
||||
{queries.map((query, idx) => (
|
||||
<Col span={12} key={hostWidgetInfo[idx].title}>
|
||||
<Col ref={setElement(idx)} span={12} key={hostWidgetInfo[idx].title}>
|
||||
<Typography.Text>{hostWidgetInfo[idx].title}</Typography.Text>
|
||||
<Card bordered className="host-metrics-card" ref={graphRef}>
|
||||
{renderCardContent(query, idx)}
|
||||
|
||||
@@ -71,7 +71,7 @@ function LogDetail({
|
||||
const [contextQuery, setContextQuery] = useState<Query | undefined>();
|
||||
const [filters, setFilters] = useState<TagFilter | null>(null);
|
||||
const [isEdit, setIsEdit] = useState<boolean>(false);
|
||||
const { initialDataSource, stagedQuery } = useQueryBuilder();
|
||||
const { stagedQuery } = useQueryBuilder();
|
||||
|
||||
const listQuery = useMemo(() => {
|
||||
if (!stagedQuery || stagedQuery.builder.queryData.length < 1) return null;
|
||||
@@ -81,7 +81,7 @@ function LogDetail({
|
||||
|
||||
const { options } = useOptionsMenu({
|
||||
storageKey: LOCALSTORAGE.LOGS_LIST_OPTIONS,
|
||||
dataSource: initialDataSource || DataSource.LOGS,
|
||||
dataSource: DataSource.LOGS,
|
||||
aggregateOperator: listQuery?.aggregateOperator || StringOperators.NOOP,
|
||||
});
|
||||
|
||||
|
||||
@@ -410,18 +410,18 @@ export default function LogsFormatOptionsMenu({
|
||||
)}
|
||||
|
||||
<div className="column-format">
|
||||
{addColumn?.value?.map(({ key, id }) => (
|
||||
<div className="column-name" key={id}>
|
||||
{addColumn?.value?.map(({ name }) => (
|
||||
<div className="column-name" key={name}>
|
||||
<div className="name">
|
||||
<Tooltip placement="left" title={key}>
|
||||
{key}
|
||||
<Tooltip placement="left" title={name}>
|
||||
{name}
|
||||
</Tooltip>
|
||||
</div>
|
||||
{addColumn?.value?.length > 1 && (
|
||||
<X
|
||||
className="delete-btn"
|
||||
size={14}
|
||||
onClick={(): void => addColumn.onRemove(id as string)}
|
||||
onClick={(): void => addColumn.onRemove(name)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -8,19 +8,24 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.metrics-time-aggregation-section-title {
|
||||
.non-histogram-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
&:not(.is-histogram) {
|
||||
.metrics-time-aggregation-section,
|
||||
.metrics-space-aggregation-section {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
|
||||
color: var(--Slate-50, #62687c);
|
||||
font-family: 'Geist Mono';
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
line-height: 18px; /* 150% */
|
||||
letter-spacing: 0.48px;
|
||||
.metrics-aggregation-section-content {
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,6 +55,10 @@
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
|
||||
.group-by-filter-container {
|
||||
min-width: 340px !important;
|
||||
}
|
||||
|
||||
.metrics-aggregation-section-content-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -63,10 +72,23 @@
|
||||
font-weight: 400;
|
||||
line-height: 20px; /* 142.857% */
|
||||
letter-spacing: -0.07px;
|
||||
|
||||
&.main-label {
|
||||
color: var(--Slate-50, #62687c);
|
||||
font-family: 'Geist Mono';
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
line-height: 18px; /* 150% */
|
||||
letter-spacing: 0.48px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
.metrics-aggregation-section-content-item-value {
|
||||
min-width: 320px;
|
||||
min-width: 140px;
|
||||
|
||||
.ant-select {
|
||||
width: 100%;
|
||||
@@ -77,6 +99,34 @@
|
||||
border: 1.005px solid var(--Slate-400, #1d212d);
|
||||
background: var(--Ink-300, #16181d);
|
||||
}
|
||||
|
||||
.input-with-label {
|
||||
.label {
|
||||
min-width: 80px;
|
||||
}
|
||||
|
||||
.input {
|
||||
flex: initial;
|
||||
width: 100px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.is-histogram {
|
||||
.group-by-filter-container {
|
||||
width: 420px;
|
||||
}
|
||||
|
||||
.histogram-every-input {
|
||||
.input {
|
||||
flex: initial;
|
||||
width: 100px !important;
|
||||
}
|
||||
|
||||
.label {
|
||||
min-width: 80px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
import './MetricsAggregateSection.styles.scss';
|
||||
|
||||
import { Tooltip } from 'antd';
|
||||
import cx from 'classnames';
|
||||
import InputWithLabel from 'components/InputWithLabel/InputWithLabel';
|
||||
import { ATTRIBUTE_TYPES, PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import SpaceAggregationOptions from 'container/QueryBuilder/components/SpaceAggregationOptions/SpaceAggregationOptions';
|
||||
import { GroupByFilter, OperatorsSelect } from 'container/QueryBuilder/filters';
|
||||
import { useQueryOperations } from 'hooks/queryBuilder/useQueryBuilderOperations';
|
||||
import { Info } from 'lucide-react';
|
||||
import { memo, useCallback, useMemo } from 'react';
|
||||
import { memo, useCallback, useEffect, useMemo } from 'react';
|
||||
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
import { useQueryBuilderV2Context } from '../../QueryBuilderV2Context';
|
||||
|
||||
const MetricsAggregateSection = memo(function MetricsAggregateSection({
|
||||
query,
|
||||
index,
|
||||
@@ -21,6 +24,7 @@ const MetricsAggregateSection = memo(function MetricsAggregateSection({
|
||||
version: string;
|
||||
panelType: PANEL_TYPES | null;
|
||||
}): JSX.Element {
|
||||
const { setAggregationOptions } = useQueryBuilderV2Context();
|
||||
const {
|
||||
operators,
|
||||
spaceAggregationOptions,
|
||||
@@ -33,6 +37,25 @@ const MetricsAggregateSection = memo(function MetricsAggregateSection({
|
||||
entityVersion: version,
|
||||
});
|
||||
|
||||
const isHistogram = useMemo(
|
||||
() => query.aggregateAttribute.type === ATTRIBUTE_TYPES.HISTOGRAM,
|
||||
[query.aggregateAttribute.type],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setAggregationOptions([
|
||||
{
|
||||
func: query.spaceAggregation || 'count',
|
||||
arg: query.aggregateAttribute.key || '',
|
||||
},
|
||||
]);
|
||||
}, [
|
||||
query.spaceAggregation,
|
||||
query.aggregateAttribute.key,
|
||||
setAggregationOptions,
|
||||
query,
|
||||
]);
|
||||
|
||||
const handleChangeGroupByKeys = useCallback(
|
||||
(value: IBuilderQuery['groupBy']) => {
|
||||
handleChangeQueryData('groupBy', value);
|
||||
@@ -60,90 +83,142 @@ const MetricsAggregateSection = memo(function MetricsAggregateSection({
|
||||
!query?.aggregateAttribute.key || query?.aggregateAttribute.key === '';
|
||||
|
||||
return (
|
||||
<div className="metrics-aggregate-section">
|
||||
<div className="metrics-time-aggregation-section">
|
||||
<div className="metrics-time-aggregation-section-title">
|
||||
AGGREGATE BY TIME{' '}
|
||||
<Tooltip title="AGGREGATE BY TIME">
|
||||
<Info size={12} />
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div
|
||||
className={cx('metrics-aggregate-section', {
|
||||
'is-histogram': isHistogram,
|
||||
})}
|
||||
>
|
||||
{!isHistogram && (
|
||||
<div className="non-histogram-container">
|
||||
<div className="metrics-time-aggregation-section">
|
||||
<div className="metrics-aggregation-section-content">
|
||||
<div className="metrics-aggregation-section-content-item">
|
||||
<div className="metrics-aggregation-section-content-item-label main-label">
|
||||
AGGREGATE BY TIME{' '}
|
||||
<Tooltip title="AGGREGATE BY TIME">
|
||||
<Info size={12} />
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div className="metrics-aggregation-section-content-item-value">
|
||||
<OperatorsSelect
|
||||
value={query.aggregateOperator}
|
||||
onChange={handleChangeOperator}
|
||||
operators={operators}
|
||||
className="metrics-operators-select"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="metrics-aggregation-section-content">
|
||||
<div className="metrics-aggregation-section-content-item">
|
||||
<div className="metrics-aggregation-section-content-item-label">
|
||||
Align with
|
||||
</div>
|
||||
{showAggregationInterval && (
|
||||
<div className="metrics-aggregation-section-content-item">
|
||||
<div className="metrics-aggregation-section-content-item-label">
|
||||
every
|
||||
</div>
|
||||
|
||||
<div className="metrics-aggregation-section-content-item-value">
|
||||
<OperatorsSelect
|
||||
value={query.aggregateOperator}
|
||||
onChange={handleChangeOperator}
|
||||
operators={operators}
|
||||
className="metrics-operators-select"
|
||||
/>
|
||||
<div className="metrics-aggregation-section-content-item-value">
|
||||
<InputWithLabel
|
||||
onChange={handleChangeAggregateEvery}
|
||||
label="Seconds"
|
||||
placeholder="Auto"
|
||||
labelAfter
|
||||
initialValue={query?.stepInterval ?? undefined}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="metrics-space-aggregation-section">
|
||||
<div className="metrics-aggregation-section-content">
|
||||
<div className="metrics-aggregation-section-content-item">
|
||||
<div className="metrics-aggregation-section-content-item-label main-label">
|
||||
AGGREGATE LABELS
|
||||
<Tooltip title="AGGREGATE LABELS">
|
||||
<Info size={12} />
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div className="metrics-aggregation-section-content-item-value">
|
||||
<SpaceAggregationOptions
|
||||
panelType={panelType}
|
||||
key={`${panelType}${query.spaceAggregation}${query.timeAggregation}`}
|
||||
aggregatorAttributeType={
|
||||
query?.aggregateAttribute.type as ATTRIBUTE_TYPES
|
||||
}
|
||||
selectedValue={query.spaceAggregation}
|
||||
disabled={disableOperatorSelector}
|
||||
onSelect={handleSpaceAggregationChange}
|
||||
operators={spaceAggregationOptions}
|
||||
qbVersion="v3"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{showAggregationInterval && (
|
||||
<div className="metrics-aggregation-section-content-item">
|
||||
<div className="metrics-aggregation-section-content-item-label">by</div>
|
||||
|
||||
<div className="metrics-aggregation-section-content-item-value group-by-filter-container">
|
||||
<GroupByFilter
|
||||
disabled={!query.aggregateAttribute.key}
|
||||
query={query}
|
||||
onChange={handleChangeGroupByKeys}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isHistogram && (
|
||||
<div className="metrics-space-aggregation-section">
|
||||
<div className="metrics-aggregation-section-content">
|
||||
<div className="metrics-aggregation-section-content-item">
|
||||
<div className="metrics-aggregation-section-content-item-value">
|
||||
<SpaceAggregationOptions
|
||||
panelType={panelType}
|
||||
key={`${panelType}${query.spaceAggregation}${query.timeAggregation}`}
|
||||
aggregatorAttributeType={
|
||||
query?.aggregateAttribute.type as ATTRIBUTE_TYPES
|
||||
}
|
||||
selectedValue={query.spaceAggregation}
|
||||
disabled={disableOperatorSelector}
|
||||
onSelect={handleSpaceAggregationChange}
|
||||
operators={spaceAggregationOptions}
|
||||
qbVersion="v3"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="metrics-aggregation-section-content-item">
|
||||
<div className="metrics-aggregation-section-content-item-label">by</div>
|
||||
|
||||
<div className="metrics-aggregation-section-content-item-value group-by-filter-container">
|
||||
<GroupByFilter
|
||||
disabled={!query.aggregateAttribute.key}
|
||||
query={query}
|
||||
onChange={handleChangeGroupByKeys}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="metrics-aggregation-section-content-item">
|
||||
<div className="metrics-aggregation-section-content-item-label">
|
||||
aggregated every
|
||||
every
|
||||
</div>
|
||||
|
||||
<div className="metrics-aggregation-section-content-item-value">
|
||||
<InputWithLabel
|
||||
onChange={handleChangeAggregateEvery}
|
||||
label="Seconds"
|
||||
placeholder="Enter a number"
|
||||
placeholder="Auto"
|
||||
labelAfter
|
||||
initialValue={query?.stepInterval ?? undefined}
|
||||
className="histogram-every-input"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="metrics-space-aggregation-section">
|
||||
<div className="metrics-space-aggregation-section-title">
|
||||
AGGREGATE LABELS
|
||||
<Tooltip title="AGGREGATE LABELS">
|
||||
<Info size={12} />
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
<div className="metrics-aggregation-section-content">
|
||||
<div className="metrics-aggregation-section-content-item">
|
||||
<div className="metrics-aggregation-section-content-item-value space-aggregation-select">
|
||||
<SpaceAggregationOptions
|
||||
panelType={panelType}
|
||||
key={`${panelType}${query.spaceAggregation}${query.timeAggregation}`}
|
||||
aggregatorAttributeType={
|
||||
query?.aggregateAttribute.type as ATTRIBUTE_TYPES
|
||||
}
|
||||
selectedValue={query.spaceAggregation}
|
||||
disabled={disableOperatorSelector}
|
||||
onSelect={handleSpaceAggregationChange}
|
||||
operators={spaceAggregationOptions}
|
||||
qbVersion="v3"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="metrics-aggregation-section-content-item">
|
||||
<div className="metrics-aggregation-section-content-item-label">by</div>
|
||||
|
||||
<div className="metrics-aggregation-section-content-item-value">
|
||||
<GroupByFilter
|
||||
disabled={!query.aggregateAttribute.key}
|
||||
query={query}
|
||||
onChange={handleChangeGroupByKeys}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -53,17 +53,6 @@ const havingOperators = [
|
||||
},
|
||||
];
|
||||
|
||||
// Add common value suggestions
|
||||
const commonValues = [
|
||||
{ label: '0', value: '0 ' },
|
||||
{ label: '1', value: '1 ' },
|
||||
{ label: '5', value: '5 ' },
|
||||
{ label: '10', value: '10 ' },
|
||||
{ label: '50', value: '50 ' },
|
||||
{ label: '100', value: '100 ' },
|
||||
{ label: '1000', value: '1000 ' },
|
||||
];
|
||||
|
||||
const conjunctions = [
|
||||
{ label: 'AND', value: 'AND ' },
|
||||
{ label: 'OR', value: 'OR ' },
|
||||
@@ -250,22 +239,18 @@ function HavingFilter({
|
||||
};
|
||||
}
|
||||
|
||||
// Show value suggestions after operator
|
||||
// Close dropdown after operator to allow custom value entry
|
||||
if (isAfterOperator(tokens)) {
|
||||
return {
|
||||
from: context.pos,
|
||||
options: [
|
||||
...commonValues.map((value) => ({
|
||||
...value,
|
||||
apply: applyValueCompletion,
|
||||
})),
|
||||
{
|
||||
label: 'Enter a custom number value',
|
||||
type: 'text',
|
||||
apply: applyValueCompletion,
|
||||
},
|
||||
],
|
||||
};
|
||||
return null;
|
||||
}
|
||||
|
||||
// Hide suggestions while typing a value after an operator
|
||||
if (
|
||||
!text.endsWith(' ') &&
|
||||
tokens.length >= 2 &&
|
||||
havingOperators.some((op) => op.value === tokens[tokens.length - 2])
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Suggest key/operator pairs and ( for grouping
|
||||
@@ -346,6 +331,7 @@ function HavingFilter({
|
||||
havingAutocomplete,
|
||||
javascript({ jsx: false, typescript: false }),
|
||||
stopEventsExtension,
|
||||
EditorView.lineWrapping,
|
||||
keymap.of([
|
||||
...completionKeymap,
|
||||
{
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
.query-aggregation-container {
|
||||
display: block;
|
||||
position: relative;
|
||||
|
||||
.aggregation-container {
|
||||
display: flex;
|
||||
@@ -10,55 +9,25 @@
|
||||
flex-wrap: wrap;
|
||||
|
||||
.query-aggregation-select-container {
|
||||
flex: 1;
|
||||
min-width: 400px;
|
||||
}
|
||||
|
||||
.query-aggregation-options-input {
|
||||
width: 100%;
|
||||
height: 36px;
|
||||
line-height: 36px;
|
||||
border-radius: 2px;
|
||||
border: 1px solid var(--bg-slate-400);
|
||||
box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.1);
|
||||
|
||||
font-family: 'Space Mono', monospace !important;
|
||||
|
||||
&::placeholder {
|
||||
color: var(--bg-vanilla-100);
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
.query-aggregation-interval {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
max-width: 360px;
|
||||
|
||||
.query-aggregation-interval-input-container {
|
||||
.query-aggregation-interval-input {
|
||||
input {
|
||||
max-width: 120px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.query-aggregation-select-container {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
min-width: 400px;
|
||||
position: relative;
|
||||
|
||||
.query-aggregation-select-editor {
|
||||
border-radius: 2px;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
|
||||
&.error {
|
||||
.cm-editor {
|
||||
.cm-content {
|
||||
border-color: var(--bg-cherry-500) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
.cm-content {
|
||||
padding: 0;
|
||||
}
|
||||
@@ -200,6 +169,30 @@
|
||||
}
|
||||
}
|
||||
|
||||
.query-aggregation-error-container {
|
||||
position: absolute;
|
||||
right: 8px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
z-index: 1;
|
||||
|
||||
.query-aggregation-error-content {
|
||||
padding: 8px;
|
||||
max-width: 300px;
|
||||
|
||||
.query-aggregation-error-message {
|
||||
color: var(--bg-cherry-500);
|
||||
font-size: 12px;
|
||||
line-height: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.query-aggregation-error-btn {
|
||||
padding: 4px;
|
||||
height: auto;
|
||||
min-width: auto;
|
||||
}
|
||||
}
|
||||
.close-btn {
|
||||
border-radius: 0px 2px 2px 0px;
|
||||
border: 1px solid var(--bg-slate-400);
|
||||
@@ -212,6 +205,38 @@
|
||||
border-bottom-left-radius: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
.query-aggregation-options-input {
|
||||
width: 100%;
|
||||
height: 36px;
|
||||
line-height: 36px;
|
||||
border-radius: 2px;
|
||||
border: 1px solid var(--bg-slate-400);
|
||||
box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.1);
|
||||
|
||||
font-family: 'Space Mono', monospace !important;
|
||||
|
||||
&::placeholder {
|
||||
color: var(--bg-vanilla-100);
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
.query-aggregation-interval {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
max-width: 360px;
|
||||
|
||||
.query-aggregation-interval-input-container {
|
||||
.query-aggregation-interval-input {
|
||||
input {
|
||||
max-width: 120px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -298,3 +323,12 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.query-aggregation-error-popover {
|
||||
.ant-popover-inner {
|
||||
background-color: var(--bg-slate-500);
|
||||
border: 1px solid var(--bg-slate-400);
|
||||
border-radius: 4px;
|
||||
box-shadow: 0px 4px 12px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,7 +41,15 @@ function QueryAggregationOptions({
|
||||
return (
|
||||
<div className="query-aggregation-container">
|
||||
<div className="aggregation-container">
|
||||
<QueryAggregationSelect onChange={onChange} queryData={queryData} />
|
||||
<QueryAggregationSelect
|
||||
onChange={onChange}
|
||||
queryData={queryData}
|
||||
maxAggregations={
|
||||
panelType === PANEL_TYPES.VALUE || panelType === PANEL_TYPES.PIE
|
||||
? 1
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
|
||||
{showAggregationInterval && (
|
||||
<div className="query-aggregation-interval">
|
||||
@@ -49,7 +57,7 @@ function QueryAggregationOptions({
|
||||
<div className="query-aggregation-interval-input-container">
|
||||
<InputWithLabel
|
||||
initialValue={
|
||||
queryData.stepInterval ? queryData.stepInterval : undefined
|
||||
queryData?.stepInterval ? queryData?.stepInterval : undefined
|
||||
}
|
||||
className="query-aggregation-interval-input"
|
||||
label="Seconds"
|
||||
|
||||
@@ -16,7 +16,8 @@ import {
|
||||
startCompletion,
|
||||
} from '@codemirror/autocomplete';
|
||||
import { javascript } from '@codemirror/lang-javascript';
|
||||
import { RangeSetBuilder } from '@codemirror/state';
|
||||
import { EditorState, RangeSetBuilder, Transaction } from '@codemirror/state';
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { copilot } from '@uiw/codemirror-theme-copilot';
|
||||
import CodeMirror, {
|
||||
Decoration,
|
||||
@@ -25,12 +26,13 @@ import CodeMirror, {
|
||||
ViewPlugin,
|
||||
ViewUpdate,
|
||||
} from '@uiw/react-codemirror';
|
||||
import { getAggregateAttribute } from 'api/queryBuilder/getAggregateAttribute';
|
||||
import { Button, Popover } from 'antd';
|
||||
import { getKeySuggestions } from 'api/querySuggestions/getKeySuggestions';
|
||||
import { QueryBuilderKeys } from 'constants/queryBuilder';
|
||||
import { tracesAggregateOperatorOptions } from 'constants/queryBuilderOperators';
|
||||
import { TriangleAlert } from 'lucide-react';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useQuery } from 'react-query';
|
||||
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { TracesAggregatorOperator } from 'types/common/queryBuilder';
|
||||
|
||||
@@ -140,9 +142,11 @@ const stopEventsExtension = EditorView.domEventHandlers({
|
||||
function QueryAggregationSelect({
|
||||
onChange,
|
||||
queryData,
|
||||
maxAggregations,
|
||||
}: {
|
||||
onChange?: (value: string) => void;
|
||||
queryData: IBuilderQuery;
|
||||
maxAggregations?: number;
|
||||
}): JSX.Element {
|
||||
const { setAggregationOptions } = useQueryBuilderV2Context();
|
||||
|
||||
@@ -160,9 +164,16 @@ function QueryAggregationSelect({
|
||||
const [functionArgPairs, setFunctionArgPairs] = useState<
|
||||
{ func: string; arg: string }[]
|
||||
>([]);
|
||||
const [validationError, setValidationError] = useState<string | null>(null);
|
||||
const editorRef = useRef<EditorView | null>(null);
|
||||
const [isFocused, setIsFocused] = useState(false);
|
||||
|
||||
// Get valid function names (lowercase)
|
||||
const validFunctions = useMemo(
|
||||
() => tracesAggregateOperatorOptions.map((op) => op.value.toLowerCase()),
|
||||
[],
|
||||
);
|
||||
|
||||
// Helper function to safely start completion
|
||||
const safeStartCompletion = useCallback((): void => {
|
||||
requestAnimationFrame(() => {
|
||||
@@ -206,10 +217,76 @@ function QueryAggregationSelect({
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Validation logic
|
||||
const validateAggregations = (): string | null => {
|
||||
// Check maxAggregations limit
|
||||
if (maxAggregations !== undefined && pairs.length > maxAggregations) {
|
||||
return `Maximum ${maxAggregations} aggregation${
|
||||
maxAggregations === 1 ? '' : 's'
|
||||
} allowed`;
|
||||
}
|
||||
|
||||
// Check for invalid functions
|
||||
const invalidFuncs = pairs.filter(
|
||||
(pair) => !validFunctions.includes(pair.func),
|
||||
);
|
||||
if (invalidFuncs.length > 0) {
|
||||
const funcs = invalidFuncs.map((f) => f.func).join(', ');
|
||||
return `Invalid function${invalidFuncs.length === 1 ? '' : 's'}: ${funcs}`;
|
||||
}
|
||||
|
||||
// Check for incomplete function calls
|
||||
if (/([a-zA-Z_][\w]*)\s*\([^)]*$/g.test(input)) {
|
||||
return 'Incomplete function call - missing closing parenthesis';
|
||||
}
|
||||
|
||||
// Check for empty function calls that require arguments
|
||||
const emptyFuncs = (input.match(/([a-zA-Z_][\w]*)\s*\(\s*\)/g) || [])
|
||||
.map((call) => call.match(/([a-zA-Z_][\w]*)/)?.[1])
|
||||
.filter((func): func is string => Boolean(func))
|
||||
.filter((func) => operatorArgMeta[func.toLowerCase()]?.acceptsArgs);
|
||||
|
||||
if (emptyFuncs.length > 0) {
|
||||
const isPlural = emptyFuncs.length > 1;
|
||||
return `Function${isPlural ? 's' : ''} ${emptyFuncs.join(', ')} require${
|
||||
isPlural ? '' : 's'
|
||||
} arguments`;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
setValidationError(validateAggregations());
|
||||
setFunctionArgPairs(pairs);
|
||||
setAggregationOptions(pairs);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [input]);
|
||||
}, [input, maxAggregations, validFunctions]);
|
||||
|
||||
// Transaction filter to limit aggregations
|
||||
const transactionFilterExtension = useMemo(() => {
|
||||
if (maxAggregations === undefined) return [];
|
||||
|
||||
return EditorState.transactionFilter.of((tr: Transaction) => {
|
||||
if (!tr.docChanged) return tr;
|
||||
|
||||
const regex = /([a-zA-Z_][\w]*)\s*\(([^)]*)\)/g;
|
||||
const oldMatches = [
|
||||
...tr.startState.doc.toString().matchAll(regex),
|
||||
].filter((match) => validFunctions.includes(match[1].toLowerCase()));
|
||||
const newMatches = [
|
||||
...tr.newDoc.toString().matchAll(regex),
|
||||
].filter((match) => validFunctions.includes(match[1].toLowerCase()));
|
||||
|
||||
if (
|
||||
newMatches.length > oldMatches.length &&
|
||||
newMatches.length > maxAggregations
|
||||
) {
|
||||
return []; // Cancel transaction
|
||||
}
|
||||
return tr;
|
||||
});
|
||||
}, [maxAggregations, validFunctions]);
|
||||
|
||||
// Find function context for fetching suggestions
|
||||
const functionContextForFetch = getFunctionContextAtCursor(input, cursorPos);
|
||||
@@ -220,12 +297,25 @@ function QueryAggregationSelect({
|
||||
functionContextForFetch,
|
||||
queryData.dataSource,
|
||||
],
|
||||
() =>
|
||||
getAggregateAttribute({
|
||||
() => {
|
||||
const operatorsWithoutDataType: (string | undefined)[] = [
|
||||
TracesAggregatorOperator.COUNT,
|
||||
TracesAggregatorOperator.COUNT_DISTINCT,
|
||||
TracesAggregatorOperator.RATE,
|
||||
];
|
||||
|
||||
const fieldDataType =
|
||||
functionContextForFetch &&
|
||||
operatorsWithoutDataType.includes(functionContextForFetch)
|
||||
? undefined
|
||||
: 'number';
|
||||
|
||||
return getKeySuggestions({
|
||||
signal: queryData.dataSource,
|
||||
searchText: '',
|
||||
aggregateOperator: functionContextForFetch as string,
|
||||
dataSource: queryData.dataSource,
|
||||
}),
|
||||
fieldDataType,
|
||||
});
|
||||
},
|
||||
{
|
||||
enabled:
|
||||
!!functionContextForFetch &&
|
||||
@@ -233,12 +323,6 @@ function QueryAggregationSelect({
|
||||
},
|
||||
);
|
||||
|
||||
// Get valid function names (lowercase)
|
||||
const validFunctions = useMemo(
|
||||
() => tracesAggregateOperatorOptions.map((op) => op.value.toLowerCase()),
|
||||
[],
|
||||
);
|
||||
|
||||
// Memoized chipPlugin that highlights valid function calls like count(), max(arg), min(arg)
|
||||
const chipPlugin = useMemo(
|
||||
() =>
|
||||
@@ -327,11 +411,14 @@ function QueryAggregationSelect({
|
||||
// Memoize field suggestions from API (no filtering here)
|
||||
const fieldSuggestions = useMemo(
|
||||
() =>
|
||||
aggregateAttributeData?.payload?.attributeKeys?.map(
|
||||
(attributeKey: BaseAutocompleteData) => ({
|
||||
label: attributeKey.key,
|
||||
Object.keys(aggregateAttributeData?.data.data.keys || {}).flatMap((key) => {
|
||||
const attributeKeys = aggregateAttributeData?.data.data.keys[key];
|
||||
if (!attributeKeys) return [];
|
||||
|
||||
return attributeKeys.map((attributeKey) => ({
|
||||
label: attributeKey.name,
|
||||
type: 'variable',
|
||||
info: attributeKey.dataType,
|
||||
info: attributeKey.fieldDataType,
|
||||
apply: (
|
||||
view: EditorView,
|
||||
completion: Completion,
|
||||
@@ -358,8 +445,8 @@ function QueryAggregationSelect({
|
||||
safeStartCompletion();
|
||||
}, 50);
|
||||
},
|
||||
}),
|
||||
) || [],
|
||||
}));
|
||||
}) || [],
|
||||
[aggregateAttributeData, safeStartCompletion],
|
||||
);
|
||||
|
||||
@@ -372,6 +459,21 @@ function QueryAggregationSelect({
|
||||
const cursorPos = context.pos;
|
||||
const funcName = getFunctionContextAtCursor(text, cursorPos);
|
||||
|
||||
// Check if over limit and not editing existing
|
||||
if (maxAggregations !== undefined) {
|
||||
const regex = /([a-zA-Z_][\w]*)\s*\(([^)]*)\)/g;
|
||||
const matches = [...text.matchAll(regex)].filter((match) =>
|
||||
validFunctions.includes(match[1].toLowerCase()),
|
||||
);
|
||||
if (matches.length >= maxAggregations) {
|
||||
const isEditing = matches.some((match) => {
|
||||
const start = match.index ?? 0;
|
||||
return cursorPos >= start && cursorPos <= start + match[0].length;
|
||||
});
|
||||
if (!isEditing) return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Do not show suggestions if inside count()
|
||||
if (
|
||||
funcName === TracesAggregatorOperator.COUNT &&
|
||||
@@ -470,7 +572,14 @@ function QueryAggregationSelect({
|
||||
maxRenderedOptions: 50,
|
||||
activateOnTyping: true,
|
||||
}),
|
||||
[operatorCompletions, isLoadingFields, fieldSuggestions, functionArgPairs],
|
||||
[
|
||||
operatorCompletions,
|
||||
isLoadingFields,
|
||||
fieldSuggestions,
|
||||
functionArgPairs,
|
||||
maxAggregations,
|
||||
validFunctions,
|
||||
],
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -481,11 +590,14 @@ function QueryAggregationSelect({
|
||||
setInput(value);
|
||||
onChange?.(value);
|
||||
}}
|
||||
className="query-aggregation-select-editor"
|
||||
className={`query-aggregation-select-editor ${
|
||||
validationError ? 'error' : ''
|
||||
}`}
|
||||
theme={copilot}
|
||||
extensions={[
|
||||
chipPlugin,
|
||||
aggregatorAutocomplete,
|
||||
transactionFilterExtension,
|
||||
javascript({ jsx: false, typescript: false }),
|
||||
EditorView.lineWrapping,
|
||||
stopEventsExtension,
|
||||
@@ -497,7 +609,11 @@ function QueryAggregationSelect({
|
||||
},
|
||||
]),
|
||||
]}
|
||||
placeholder="Type aggregator functions like sum(), count_distinct(...), etc."
|
||||
placeholder={
|
||||
maxAggregations !== undefined
|
||||
? `Type aggregator functions (max ${maxAggregations}) like sum(), count_distinct(...), etc.`
|
||||
: 'Type aggregator functions like sum(), count_distinct(...), etc.'
|
||||
}
|
||||
basicSetup={{
|
||||
lineNumbers: false,
|
||||
autocompletion: true,
|
||||
@@ -519,12 +635,33 @@ function QueryAggregationSelect({
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{validationError && (
|
||||
<div className="query-aggregation-error-container">
|
||||
<Popover
|
||||
placement="bottomRight"
|
||||
showArrow={false}
|
||||
content={
|
||||
<div className="query-aggregation-error-content">
|
||||
<div className="query-aggregation-error-message">{validationError}</div>
|
||||
</div>
|
||||
}
|
||||
overlayClassName="query-aggregation-error-popover"
|
||||
>
|
||||
<Button
|
||||
type="text"
|
||||
icon={<TriangleAlert size={14} color={Color.BG_CHERRY_500} />}
|
||||
className="periscope-btn ghost query-aggregation-error-btn"
|
||||
/>
|
||||
</Popover>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
QueryAggregationSelect.defaultProps = {
|
||||
onChange: undefined,
|
||||
maxAggregations: undefined,
|
||||
};
|
||||
|
||||
export default QueryAggregationSelect;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable sonarjs/cognitive-complexity */
|
||||
import './QuerySearch.styles.scss';
|
||||
|
||||
import { CheckCircleFilled } from '@ant-design/icons';
|
||||
@@ -17,6 +18,7 @@ import { Button, Card, Collapse, Popover, Tag } from 'antd';
|
||||
import { getKeySuggestions } from 'api/querySuggestions/getKeySuggestions';
|
||||
import { getValueSuggestions } from 'api/querySuggestions/getValueSuggestion';
|
||||
import cx from 'classnames';
|
||||
import { isNull } from 'lodash-es';
|
||||
import { TriangleAlert } from 'lucide-react';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import {
|
||||
@@ -27,14 +29,20 @@ import {
|
||||
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { QueryKeyDataSuggestionsProps } from 'types/api/querySuggestions/types';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
import { validateQuery } from 'utils/antlrQueryUtils';
|
||||
import {
|
||||
negationQueryOperatorSuggestions,
|
||||
QUERY_BUILDER_KEY_TYPES,
|
||||
QUERY_BUILDER_OPERATORS_BY_KEY_TYPE,
|
||||
queryOperatorSuggestions,
|
||||
validateQuery,
|
||||
} from 'utils/antlrQueryUtils';
|
||||
import { getQueryContextAtCursor } from 'utils/queryContextUtils';
|
||||
} from 'constants/antlrQueryConstants';
|
||||
import {
|
||||
getCurrentValueIndexAtCursor,
|
||||
getQueryContextAtCursor,
|
||||
} from 'utils/queryContextUtils';
|
||||
|
||||
import { queryExamples } from './constants';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
|
||||
const { Panel } = Collapse;
|
||||
|
||||
@@ -127,6 +135,8 @@ function QuerySearch({
|
||||
const lastValueRef = useRef<string>('');
|
||||
const isMountedRef = useRef<boolean>(true);
|
||||
|
||||
const { handleRunQuery } = useQueryBuilder();
|
||||
|
||||
// const {
|
||||
// data: queryKeySuggestions,
|
||||
// refetch: refetchQueryKeySuggestions,
|
||||
@@ -151,13 +161,20 @@ function QuerySearch({
|
||||
const fetchKeySuggestions = async (searchText?: string): Promise<void> => {
|
||||
const response = await getKeySuggestions({
|
||||
signal: dataSource,
|
||||
name: searchText || '',
|
||||
searchText: searchText || '',
|
||||
metricName: queryData.aggregateAttribute.key ?? undefined,
|
||||
});
|
||||
|
||||
if (response.data.data) {
|
||||
const { complete, keys } = response.data.data;
|
||||
const options = generateOptions(keys);
|
||||
setKeySuggestions((prev) => [...(prev || []), ...options]);
|
||||
// Use a Map to deduplicate by label and preserve order: new options take precedence
|
||||
const merged = new Map<string, QueryKeyDataSuggestionsProps>();
|
||||
options.forEach((opt) => merged.set(opt.label, opt));
|
||||
(keySuggestions || []).forEach((opt) => {
|
||||
if (!merged.has(opt.label)) merged.set(opt.label, opt);
|
||||
});
|
||||
setKeySuggestions(Array.from(merged.values()));
|
||||
setIsCompleteKeysList(complete);
|
||||
}
|
||||
};
|
||||
@@ -166,7 +183,7 @@ function QuerySearch({
|
||||
setKeySuggestions([]);
|
||||
fetchKeySuggestions();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [dataSource]);
|
||||
}, [dataSource, queryData.aggregateAttribute.key]);
|
||||
|
||||
// Add a state for tracking editing mode
|
||||
const [editingMode, setEditingMode] = useState<
|
||||
@@ -222,10 +239,7 @@ function QuerySearch({
|
||||
|
||||
// If we're already inside bracket list for IN operator and it's a string value
|
||||
// just wrap in quotes but not brackets (we're already in brackets)
|
||||
if (
|
||||
(type === 'value' || type === 'keyword') &&
|
||||
!/^[a-zA-Z0-9_][a-zA-Z0-9_.\[\]]*$/.test(value)
|
||||
) {
|
||||
if (type === 'value' || type === 'keyword') {
|
||||
return wrapStringValueInQuotes(value);
|
||||
}
|
||||
|
||||
@@ -302,6 +316,10 @@ function QuerySearch({
|
||||
const stringValues = values.stringValues || [];
|
||||
const numberValues = values.numberValues || [];
|
||||
|
||||
if (responseData.data?.complete) {
|
||||
setIsCompleteValuesList(responseData.data.complete);
|
||||
}
|
||||
|
||||
// Generate options from string values - explicitly handle empty strings
|
||||
const stringOptions = stringValues
|
||||
// Strict filtering for empty string - we'll handle it as a special case if needed
|
||||
@@ -312,6 +330,7 @@ function QuerySearch({
|
||||
.map((value: string) => ({
|
||||
label: value,
|
||||
type: 'value',
|
||||
apply: value,
|
||||
}));
|
||||
|
||||
// Generate options from number values
|
||||
@@ -323,6 +342,7 @@ function QuerySearch({
|
||||
.map((value: number) => ({
|
||||
label: value.toString(),
|
||||
type: 'number',
|
||||
apply: value,
|
||||
}));
|
||||
|
||||
// Combine all options and make sure we don't have duplicate labels
|
||||
@@ -356,7 +376,6 @@ function QuerySearch({
|
||||
}
|
||||
}, 10);
|
||||
}
|
||||
setIsLoadingSuggestions(false);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching suggestions:', error);
|
||||
@@ -369,7 +388,6 @@ function QuerySearch({
|
||||
apply: (): boolean => false, // Prevent selection
|
||||
},
|
||||
]);
|
||||
setIsLoadingSuggestions(false);
|
||||
}
|
||||
} finally {
|
||||
setIsLoadingSuggestions(false);
|
||||
@@ -528,7 +546,7 @@ function QuerySearch({
|
||||
completion: any,
|
||||
from: number,
|
||||
to: number,
|
||||
shouldAddSpace: boolean = true,
|
||||
shouldAddSpace = true,
|
||||
): void => {
|
||||
view.dispatch({
|
||||
changes: {
|
||||
@@ -556,24 +574,81 @@ function QuerySearch({
|
||||
from: number,
|
||||
to: number,
|
||||
): void => {
|
||||
if (queryContext.isInValue && option.type === 'value') {
|
||||
let shouldDefaultApply = true;
|
||||
|
||||
// Changes to replace the value in-place with the existing value
|
||||
const isValueType = queryContext.isInValue && option.type === 'value';
|
||||
const isOperatorType =
|
||||
queryContext.isInOperator && option.type === 'operator';
|
||||
const pair = queryContext.currentPair;
|
||||
|
||||
if (isValueType) {
|
||||
if (queryContext.isInBracketList && pair?.valuesPosition) {
|
||||
const idx = getCurrentValueIndexAtCursor(
|
||||
pair.valuesPosition,
|
||||
cursorPos.ch,
|
||||
);
|
||||
if (!isNull(idx)) {
|
||||
const { start, end } = pair.valuesPosition[idx];
|
||||
if (
|
||||
typeof start === 'number' &&
|
||||
typeof end === 'number' &&
|
||||
cursorPos.ch >= start &&
|
||||
cursorPos.ch <= end + 1
|
||||
) {
|
||||
shouldDefaultApply = false;
|
||||
addSpaceAfterSelection(
|
||||
view,
|
||||
{ apply: originalApply },
|
||||
start,
|
||||
end + 1,
|
||||
false,
|
||||
);
|
||||
}
|
||||
}
|
||||
} else if (pair?.position) {
|
||||
const { valueStart, valueEnd } = pair.position;
|
||||
if (
|
||||
typeof valueStart === 'number' &&
|
||||
typeof valueEnd === 'number' &&
|
||||
cursorPos.ch >= valueStart &&
|
||||
cursorPos.ch <= valueEnd + 1
|
||||
) {
|
||||
shouldDefaultApply = false;
|
||||
addSpaceAfterSelection(
|
||||
view,
|
||||
{ apply: originalApply },
|
||||
valueStart,
|
||||
valueEnd + 1,
|
||||
false,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Changes to replace the operator in-place with the existing operator
|
||||
if (isOperatorType && pair?.position) {
|
||||
const { operatorStart, operatorEnd } = pair.position;
|
||||
if (
|
||||
queryContext.currentPair?.position &&
|
||||
queryContext.currentPair.position.valueStart &&
|
||||
queryContext.currentPair.position.valueEnd
|
||||
typeof operatorStart === 'number' &&
|
||||
typeof operatorEnd === 'number' &&
|
||||
operatorStart !== 0 &&
|
||||
operatorEnd !== 0 &&
|
||||
cursorPos.ch >= operatorStart &&
|
||||
cursorPos.ch <= operatorEnd + 1
|
||||
) {
|
||||
const { valueStart, valueEnd } = queryContext.currentPair.position;
|
||||
shouldDefaultApply = false;
|
||||
addSpaceAfterSelection(
|
||||
view,
|
||||
{ apply: originalApply },
|
||||
valueStart,
|
||||
valueEnd + 1,
|
||||
operatorStart,
|
||||
operatorEnd + 1,
|
||||
false,
|
||||
);
|
||||
} else {
|
||||
addSpaceAfterSelection(view, { apply: originalApply }, from, to);
|
||||
}
|
||||
} else {
|
||||
}
|
||||
|
||||
if (shouldDefaultApply) {
|
||||
addSpaceAfterSelection(view, { apply: originalApply }, from, to);
|
||||
}
|
||||
},
|
||||
@@ -708,28 +783,49 @@ function QuerySearch({
|
||||
|
||||
// Filter operators based on key type
|
||||
if (keyType) {
|
||||
if (keyType === 'number') {
|
||||
if (keyType === QUERY_BUILDER_KEY_TYPES.NUMBER) {
|
||||
// Prioritize numeric operators
|
||||
options = options.map((op) => ({
|
||||
...op,
|
||||
boost: ['>', '<', '>=', '<=', '=', '!=', 'BETWEEN'].includes(op.label)
|
||||
? 100
|
||||
: 0,
|
||||
}));
|
||||
} else if (keyType === 'string' || keyType === 'keyword') {
|
||||
options = options
|
||||
.filter((op) =>
|
||||
QUERY_BUILDER_OPERATORS_BY_KEY_TYPE[
|
||||
QUERY_BUILDER_KEY_TYPES.NUMBER
|
||||
].includes(op.label),
|
||||
)
|
||||
.map((op) => ({
|
||||
...op,
|
||||
boost: ['>', '<', '>=', '<=', '=', '!=', 'BETWEEN'].includes(op.label)
|
||||
? 100
|
||||
: 0,
|
||||
}));
|
||||
} else if (
|
||||
keyType === QUERY_BUILDER_KEY_TYPES.STRING ||
|
||||
keyType === 'keyword'
|
||||
) {
|
||||
// Prioritize string operators
|
||||
options = options.map((op) => ({
|
||||
...op,
|
||||
boost: ['=', '!=', 'LIKE', 'ILIKE', 'CONTAINS', 'IN'].includes(op.label)
|
||||
? 100
|
||||
: 0,
|
||||
}));
|
||||
} else if (keyType === 'boolean') {
|
||||
options = options
|
||||
.filter((op) =>
|
||||
QUERY_BUILDER_OPERATORS_BY_KEY_TYPE[
|
||||
QUERY_BUILDER_KEY_TYPES.STRING
|
||||
].includes(op.label),
|
||||
)
|
||||
.map((op) => ({
|
||||
...op,
|
||||
boost: ['=', '!=', 'LIKE', 'ILIKE', 'CONTAINS', 'IN'].includes(op.label)
|
||||
? 100
|
||||
: 0,
|
||||
}));
|
||||
} else if (keyType === QUERY_BUILDER_KEY_TYPES.BOOLEAN) {
|
||||
// Prioritize boolean operators
|
||||
options = options.map((op) => ({
|
||||
...op,
|
||||
boost: ['=', '!='].includes(op.label) ? 100 : 0,
|
||||
}));
|
||||
options = options
|
||||
.filter((op) =>
|
||||
QUERY_BUILDER_OPERATORS_BY_KEY_TYPE[
|
||||
QUERY_BUILDER_KEY_TYPES.BOOLEAN
|
||||
].includes(op.label),
|
||||
)
|
||||
.map((op) => ({
|
||||
...op,
|
||||
boost: ['=', '!='].includes(op.label) ? 100 : 0,
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -785,12 +881,12 @@ function QuerySearch({
|
||||
}
|
||||
|
||||
// Process options to add appropriate formatting when selected
|
||||
const processedOptions = valueSuggestions.map((option) => {
|
||||
const processedOptions = options.map((option) => {
|
||||
// Clone the option to avoid modifying the original
|
||||
const processedOption = { ...option };
|
||||
|
||||
// Skip processing for non-selectable items
|
||||
if (option.apply === false || typeof option.apply === 'function') {
|
||||
if (!option.apply || typeof option.apply === 'function') {
|
||||
return option;
|
||||
}
|
||||
|
||||
@@ -946,7 +1042,7 @@ function QuerySearch({
|
||||
// options: optionsWithSpace,
|
||||
// };
|
||||
|
||||
//Don't show anything if no context detected
|
||||
// Don't show anything if no context detected
|
||||
return {
|
||||
from: word?.from ?? 0,
|
||||
options: [],
|
||||
@@ -1069,6 +1165,7 @@ function QuerySearch({
|
||||
// and instead run a custom action
|
||||
// Mod-Enter is usually Ctrl-Enter or Cmd-Enter based on OS
|
||||
run: (): boolean => {
|
||||
handleRunQuery(true, true);
|
||||
return true;
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1,10 +1,17 @@
|
||||
import { createAggregation } from 'api/v5/queryRange/prepareQueryRangePayloadV5';
|
||||
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import { Having, TagFilter } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import {
|
||||
Having,
|
||||
IBuilderQuery,
|
||||
Query,
|
||||
TagFilter,
|
||||
} from 'types/api/queryBuilder/queryBuilderData';
|
||||
import {
|
||||
LogAggregation,
|
||||
MetricAggregation,
|
||||
TraceAggregation,
|
||||
} from 'types/api/v5/queryRange';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
/**
|
||||
@@ -183,3 +190,81 @@ export const convertAggregationToExpression = (
|
||||
} as LogAggregation,
|
||||
];
|
||||
};
|
||||
|
||||
export const getQueryTitles = (currentQuery: Query): string[] => {
|
||||
if (currentQuery.queryType === EQueryType.QUERY_BUILDER) {
|
||||
const queryTitles: string[] = [];
|
||||
|
||||
// Handle builder queries with multiple aggregations
|
||||
currentQuery.builder.queryData.forEach((q) => {
|
||||
const aggregationCount = q.aggregations?.length || 1;
|
||||
|
||||
if (aggregationCount > 1) {
|
||||
// If multiple aggregations, create titles like A.0, A.1, A.2
|
||||
for (let i = 0; i < aggregationCount; i++) {
|
||||
queryTitles.push(`${q.queryName}.${i}`);
|
||||
}
|
||||
} else {
|
||||
// Single aggregation, just use query name
|
||||
queryTitles.push(q.queryName);
|
||||
}
|
||||
});
|
||||
|
||||
// Handle formulas (they don't have aggregations, so just use query name)
|
||||
const formulas = currentQuery.builder.queryFormulas.map((q) => q.queryName);
|
||||
|
||||
return [...queryTitles, ...formulas];
|
||||
}
|
||||
|
||||
if (currentQuery.queryType === EQueryType.CLICKHOUSE) {
|
||||
return currentQuery.clickhouse_sql.map((q) => q.name);
|
||||
}
|
||||
|
||||
return currentQuery.promql.map((q) => q.name);
|
||||
};
|
||||
|
||||
// function to give you label value for query name taking multiaggregation into account
|
||||
export function getQueryLabelWithAggregation(
|
||||
queryData: IBuilderQuery[],
|
||||
legendMap: Record<string, string> = {},
|
||||
): { label: string; value: string }[] {
|
||||
const labels: { label: string; value: string }[] = [];
|
||||
|
||||
const aggregationPerQuery =
|
||||
queryData.reduce((acc, query) => {
|
||||
if (query.queryName && query.aggregations?.length) {
|
||||
acc[query.queryName] = createAggregation(query).map((a: any) => ({
|
||||
alias: a.alias,
|
||||
expression: a.expression,
|
||||
}));
|
||||
}
|
||||
return acc;
|
||||
}, {} as Record<string, any>) || {};
|
||||
|
||||
Object.entries(aggregationPerQuery).forEach(([queryName, aggregations]) => {
|
||||
const legend = legendMap[queryName];
|
||||
|
||||
if (aggregations.length > 1) {
|
||||
aggregations.forEach((agg: any, index: number) => {
|
||||
const aggregationName = agg.alias || agg.expression || '';
|
||||
const label = `${queryName}.${index}`;
|
||||
const value = legend
|
||||
? `${aggregationName}-${legend}`
|
||||
: `${queryName}.${aggregationName}`;
|
||||
labels.push({
|
||||
label,
|
||||
value,
|
||||
});
|
||||
});
|
||||
} else if (aggregations.length === 1) {
|
||||
const label = legend || queryName;
|
||||
const value = legend || queryName;
|
||||
labels.push({
|
||||
label,
|
||||
value,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return labels;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
import { Tabs, TabsProps } from 'antd';
|
||||
import { useLocation, useParams } from 'react-router-dom';
|
||||
import {
|
||||
generatePath,
|
||||
matchPath,
|
||||
useLocation,
|
||||
useParams,
|
||||
} from 'react-router-dom';
|
||||
|
||||
import { RouteTabProps } from './types';
|
||||
|
||||
@@ -17,20 +22,13 @@ function RouteTab({
|
||||
const params = useParams<Params>();
|
||||
const location = useLocation();
|
||||
|
||||
// Replace dynamic parameters in routes
|
||||
const routesWithParams = routes.map((route) => ({
|
||||
...route,
|
||||
route: route.route.replace(
|
||||
/:(\w+)/g,
|
||||
(match, param) => params[param] || match,
|
||||
),
|
||||
}));
|
||||
|
||||
// Find the matching route for the current pathname
|
||||
const currentRoute = routesWithParams.find((route) => {
|
||||
const routePattern = route.route.replace(/:(\w+)/g, '([^/]+)');
|
||||
const regex = new RegExp(`^${routePattern}$`);
|
||||
return regex.test(location.pathname);
|
||||
const currentRoute = routes.find((route) => {
|
||||
const routePath = route.route.split('?')[0];
|
||||
return matchPath(location.pathname, {
|
||||
path: routePath,
|
||||
exact: true,
|
||||
});
|
||||
});
|
||||
|
||||
const onChange = (activeRoute: string): void => {
|
||||
@@ -38,14 +36,15 @@ function RouteTab({
|
||||
onChangeHandler(activeRoute);
|
||||
}
|
||||
|
||||
const selectedRoute = routesWithParams.find((e) => e.key === activeRoute);
|
||||
const selectedRoute = routes.find((e) => e.key === activeRoute);
|
||||
|
||||
if (selectedRoute) {
|
||||
history.push(selectedRoute.route);
|
||||
const resolvedRoute = generatePath(selectedRoute.route, params);
|
||||
history.push(resolvedRoute);
|
||||
}
|
||||
};
|
||||
|
||||
const items = routesWithParams.map(({ Component, name, route, key }) => ({
|
||||
const items = routes.map(({ Component, name, route, key }) => ({
|
||||
label: name,
|
||||
key,
|
||||
tabKey: route,
|
||||
|
||||
77
frontend/src/constants/antlrQueryConstants.ts
Normal file
77
frontend/src/constants/antlrQueryConstants.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
export const OPERATORS = {
|
||||
IN: 'IN',
|
||||
LIKE: 'LIKE',
|
||||
ILIKE: 'ILIKE',
|
||||
REGEXP: 'REGEXP',
|
||||
EXISTS: 'EXISTS',
|
||||
CONTAINS: 'CONTAINS',
|
||||
BETWEEN: 'BETWEEN',
|
||||
NOT: 'NOT',
|
||||
'=': '=',
|
||||
'!=': '!=',
|
||||
'>=': '>=',
|
||||
'>': '>',
|
||||
'<=': '<=',
|
||||
'<': '<',
|
||||
};
|
||||
|
||||
export const NON_VALUE_OPERATORS = [OPERATORS.EXISTS];
|
||||
|
||||
export enum QUERY_BUILDER_KEY_TYPES {
|
||||
STRING = 'string',
|
||||
NUMBER = 'number',
|
||||
BOOLEAN = 'boolean',
|
||||
}
|
||||
|
||||
export const QUERY_BUILDER_OPERATORS_BY_KEY_TYPE = {
|
||||
[QUERY_BUILDER_KEY_TYPES.STRING]: [
|
||||
OPERATORS['='],
|
||||
OPERATORS['!='],
|
||||
OPERATORS.IN,
|
||||
OPERATORS.LIKE,
|
||||
OPERATORS.ILIKE,
|
||||
OPERATORS.CONTAINS,
|
||||
OPERATORS.EXISTS,
|
||||
OPERATORS.REGEXP,
|
||||
OPERATORS.NOT,
|
||||
],
|
||||
[QUERY_BUILDER_KEY_TYPES.NUMBER]: [
|
||||
OPERATORS['='],
|
||||
OPERATORS['!='],
|
||||
OPERATORS['>='],
|
||||
OPERATORS['>'],
|
||||
OPERATORS['<='],
|
||||
OPERATORS['<'],
|
||||
OPERATORS.IN,
|
||||
OPERATORS.EXISTS,
|
||||
OPERATORS.BETWEEN,
|
||||
OPERATORS.NOT,
|
||||
],
|
||||
[QUERY_BUILDER_KEY_TYPES.BOOLEAN]: [
|
||||
OPERATORS['='],
|
||||
OPERATORS['!='],
|
||||
OPERATORS.EXISTS,
|
||||
OPERATORS.NOT,
|
||||
],
|
||||
};
|
||||
|
||||
export const negationQueryOperatorSuggestions = [
|
||||
{ label: OPERATORS['LIKE'], type: 'operator', info: 'Like' },
|
||||
{ label: OPERATORS['ILIKE'], type: 'operator', info: 'Case insensitive like' },
|
||||
{ label: OPERATORS['EXISTS'], type: 'operator', info: 'Exists' },
|
||||
{ label: OPERATORS['BETWEEN'], type: 'operator', info: 'Between' },
|
||||
{ label: OPERATORS['IN'], type: 'operator', info: 'In' },
|
||||
{ label: OPERATORS['REGEXP'], type: 'operator', info: 'Regular expression' },
|
||||
{ label: OPERATORS['CONTAINS'], type: 'operator', info: 'Contains' },
|
||||
];
|
||||
|
||||
export const queryOperatorSuggestions = [
|
||||
{ label: OPERATORS['='], type: 'operator', info: 'Equal to' },
|
||||
{ label: OPERATORS['!='], type: 'operator', info: 'Not equal to' },
|
||||
{ label: OPERATORS['>'], type: 'operator', info: 'Greater than' },
|
||||
{ label: OPERATORS['<'], type: 'operator', info: 'Less than' },
|
||||
{ label: OPERATORS['>='], type: 'operator', info: 'Greater than or equal to' },
|
||||
{ label: OPERATORS['<='], type: 'operator', info: 'Less than or equal to' },
|
||||
{ label: OPERATORS['NOT'], type: 'operator', info: 'Not' },
|
||||
...negationQueryOperatorSuggestions,
|
||||
];
|
||||
@@ -46,5 +46,6 @@ export enum QueryParams {
|
||||
msgSystem = 'msgSystem',
|
||||
destination = 'destination',
|
||||
kindString = 'kindString',
|
||||
tab = 'tab',
|
||||
selectedExplorerView = 'selectedExplorerView',
|
||||
}
|
||||
|
||||
@@ -179,7 +179,7 @@ export const initialQueryBuilderFormValues: IBuilderQuery = {
|
||||
sourceNames: alphabet,
|
||||
}),
|
||||
disabled: false,
|
||||
stepInterval: 60,
|
||||
stepInterval: undefined,
|
||||
having: [],
|
||||
limit: null,
|
||||
orderBy: [],
|
||||
|
||||
@@ -6,10 +6,6 @@ import {
|
||||
import { SelectOption } from 'types/common/select';
|
||||
|
||||
export const metricAggregateOperatorOptions: SelectOption<string, string>[] = [
|
||||
{
|
||||
value: MetricAggregateOperator.NOOP,
|
||||
label: 'NOOP',
|
||||
},
|
||||
{
|
||||
value: MetricAggregateOperator.COUNT,
|
||||
label: 'Count',
|
||||
@@ -130,10 +126,6 @@ export const metricAggregateOperatorOptions: SelectOption<string, string>[] = [
|
||||
];
|
||||
|
||||
export const tracesAggregateOperatorOptions: SelectOption<string, string>[] = [
|
||||
{
|
||||
value: TracesAggregatorOperator.NOOP,
|
||||
label: 'NOOP',
|
||||
},
|
||||
{
|
||||
value: TracesAggregatorOperator.COUNT,
|
||||
label: 'Count',
|
||||
@@ -217,10 +209,6 @@ export const tracesAggregateOperatorOptions: SelectOption<string, string>[] = [
|
||||
];
|
||||
|
||||
export const logsAggregateOperatorOptions: SelectOption<string, string>[] = [
|
||||
{
|
||||
value: LogsAggregatorOperator.NOOP,
|
||||
label: 'NOOP',
|
||||
},
|
||||
{
|
||||
value: LogsAggregatorOperator.COUNT,
|
||||
label: 'Count',
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
import { handleGraphClick } from 'container/GridCardLayout/GridCard/utils';
|
||||
import { useGraphClickToShowButton } from 'container/GridCardLayout/useGraphClickToShowButton';
|
||||
import useNavigateToExplorerPages from 'container/GridCardLayout/useNavigateToExplorerPages';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import { useResizeObserver } from 'hooks/useDimensions';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
@@ -112,6 +113,7 @@ function StatusCodeBarCharts({
|
||||
});
|
||||
|
||||
const navigateToExplorer = useNavigateToExplorer();
|
||||
const { currentQuery } = useQueryBuilder();
|
||||
|
||||
const navigateToExplorerPages = useNavigateToExplorerPages();
|
||||
const { notifications } = useNotifications();
|
||||
@@ -204,6 +206,7 @@ function StatusCodeBarCharts({
|
||||
customSeries: getCustomSeries,
|
||||
onDragSelect,
|
||||
colorMapping,
|
||||
query: currentQuery,
|
||||
}),
|
||||
[
|
||||
minTime,
|
||||
@@ -217,6 +220,7 @@ function StatusCodeBarCharts({
|
||||
getCustomSeries,
|
||||
onDragSelect,
|
||||
colorMapping,
|
||||
currentQuery,
|
||||
],
|
||||
);
|
||||
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
.app-banner-container {
|
||||
// Earlier we were having app-banner-container class
|
||||
// we change it to app-banner-wrapper as the adblocker was blocking the app-banner-container class
|
||||
// Keep an eye on What classnames are used in the codebase
|
||||
.app-banner-wrapper {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@@ -63,7 +63,6 @@ import {
|
||||
} from 'types/api/licensesV3/getActive';
|
||||
import AppReducer from 'types/reducer/app';
|
||||
import { USER_ROLES } from 'types/roles';
|
||||
import { checkVersionState } from 'utils/app';
|
||||
import { eventEmitter } from 'utils/getEventEmitter';
|
||||
import {
|
||||
getFormattedDate,
|
||||
@@ -98,16 +97,11 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
||||
|
||||
const [showSlowApiWarning, setShowSlowApiWarning] = useState(false);
|
||||
const [slowApiWarningShown, setSlowApiWarningShown] = useState(false);
|
||||
const [shouldFetchChangelog, setShouldFetchChangelog] = useState<boolean>(
|
||||
false,
|
||||
);
|
||||
|
||||
const { currentVersion, latestVersion } = useSelector<AppState, AppReducer>(
|
||||
const { latestVersion } = useSelector<AppState, AppReducer>(
|
||||
(state) => state.app,
|
||||
);
|
||||
|
||||
const isLatestVersion = checkVersionState(currentVersion, latestVersion);
|
||||
|
||||
const handleBillingOnSuccess = (
|
||||
data: SuccessResponseV2<CheckoutSuccessPayloadProps>,
|
||||
): void => {
|
||||
@@ -163,7 +157,7 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
||||
queryFn: (): Promise<SuccessResponse<ChangelogSchema> | ErrorResponse> =>
|
||||
getChangelogByVersion(latestVersion),
|
||||
queryKey: ['getChangelogByVersion', latestVersion],
|
||||
enabled: isLoggedIn && !isCloudUserVal && shouldFetchChangelog,
|
||||
enabled: isLoggedIn && !isCloudUserVal && Boolean(latestVersion),
|
||||
},
|
||||
]);
|
||||
|
||||
@@ -223,7 +217,7 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
||||
|
||||
if (
|
||||
getUserVersionResponse.isFetched &&
|
||||
getUserLatestVersionResponse.isSuccess &&
|
||||
getUserVersionResponse.isSuccess &&
|
||||
getUserVersionResponse.data &&
|
||||
getUserVersionResponse.data.payload
|
||||
) {
|
||||
@@ -261,18 +255,13 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
||||
getUserVersionResponse.isLoading,
|
||||
getUserVersionResponse.isError,
|
||||
getUserVersionResponse.data,
|
||||
getUserVersionResponse.isSuccess,
|
||||
getUserLatestVersionResponse.isFetched,
|
||||
getUserVersionResponse.isFetched,
|
||||
getUserLatestVersionResponse.isSuccess,
|
||||
notifications,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isLatestVersion) {
|
||||
setShouldFetchChangelog(true);
|
||||
}
|
||||
}, [isLatestVersion]);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
getChangelogByVersionResponse.isFetched &&
|
||||
@@ -613,7 +602,7 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
||||
</Helmet>
|
||||
|
||||
{isLoggedIn && (
|
||||
<div className={cx('app-banner-container')}>
|
||||
<div className={cx('app-banner-wrapper')}>
|
||||
{SHOW_TRIAL_EXPIRY_BANNER && (
|
||||
<div className="trial-expiry-banner">
|
||||
You are in free trial period. Your free trial will end on{' '}
|
||||
|
||||
@@ -1,30 +1,173 @@
|
||||
.empty-logs-search-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 240px;
|
||||
|
||||
.empty-logs-search-container-content {
|
||||
.empty-logs-search {
|
||||
&__container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 240px;
|
||||
}
|
||||
&__content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
|
||||
color: var(--text-vanilla-400);
|
||||
font-family: Inter;
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 18px; /* 128.571% */
|
||||
line-height: 18px;
|
||||
letter-spacing: -0.07px;
|
||||
|
||||
align-items: flex-start;
|
||||
.empty-state-svg {
|
||||
height: 50px;
|
||||
width: 50px;
|
||||
}
|
||||
}
|
||||
&__sub-text {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.sub-text {
|
||||
font-weight: 600;
|
||||
&__container {
|
||||
&--custom-message {
|
||||
height: 445px;
|
||||
.empty-state-svg {
|
||||
height: 32px;
|
||||
width: 32px;
|
||||
}
|
||||
.empty-logs-search {
|
||||
&__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
&__title {
|
||||
color: var(--bg-vanilla-100);
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
line-height: 20px;
|
||||
letter-spacing: -0.07px;
|
||||
}
|
||||
|
||||
&__subtitle {
|
||||
color: var(--bg-vanilla-400);
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
line-height: 20px;
|
||||
letter-spacing: -0.07px;
|
||||
}
|
||||
|
||||
&__description {
|
||||
font-size: 14px;
|
||||
color: var(--text-vanilla-400);
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
&__description-list {
|
||||
margin: 0;
|
||||
margin-top: 8px;
|
||||
color: var(--bg-vanilla-400);
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
line-height: 20px;
|
||||
letter-spacing: -0.07px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
font-family: Inter;
|
||||
}
|
||||
|
||||
&__description-list li {
|
||||
position: relative;
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
&__description-list li::before {
|
||||
content: '⎯';
|
||||
font-family: Inter;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
color: var(--bg-robin-400);
|
||||
font-weight: bold;
|
||||
font-size: 16px;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
&__clear-filters-btn {
|
||||
display: flex;
|
||||
width: 468px;
|
||||
font-family: Inter;
|
||||
padding: 12px;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
border-radius: 3px;
|
||||
border: 1px dashed var(--bg-slate-500);
|
||||
background: transparent;
|
||||
color: var(--bg-vanilla-400);
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
line-height: 18px;
|
||||
letter-spacing: -0.07px;
|
||||
cursor: pointer;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
&__clear-filters-btn-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
&__row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: flex-end;
|
||||
max-width: 825px;
|
||||
gap: 25px;
|
||||
justify-content: center;
|
||||
margin-left: 21px;
|
||||
}
|
||||
|
||||
&__content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
min-width: 260px;
|
||||
}
|
||||
|
||||
&__resources-card {
|
||||
background: var(--bg-ink-400);
|
||||
border: 1px solid var(--bg-slate-500);
|
||||
border-radius: 4px;
|
||||
width: 332px;
|
||||
}
|
||||
|
||||
&__resources-title {
|
||||
color: var(--bg-vanilla-400);
|
||||
font-family: Inter;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
line-height: 18px;
|
||||
letter-spacing: 0.88px;
|
||||
text-transform: uppercase;
|
||||
padding: 16px 16px 12px;
|
||||
border-bottom: 1px solid var(--bg-slate-500);
|
||||
height: 46px;
|
||||
}
|
||||
|
||||
&__resources-links {
|
||||
padding: 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
|
||||
.learn-more {
|
||||
height: 18px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,16 +2,24 @@ import './EmptyLogsSearch.styles.scss';
|
||||
|
||||
import { Typography } from 'antd';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import cx from 'classnames';
|
||||
import LearnMore from 'components/LearnMore/LearnMore';
|
||||
import { EmptyLogsListConfig } from 'container/LogsExplorerList/utils';
|
||||
import { Delete } from 'lucide-react';
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { DataSource, PanelTypeKeys } from 'types/common/queryBuilder';
|
||||
|
||||
interface EmptyLogsSearchProps {
|
||||
dataSource: DataSource;
|
||||
panelType: PanelTypeKeys;
|
||||
customMessage?: EmptyLogsListConfig;
|
||||
}
|
||||
|
||||
export default function EmptyLogsSearch({
|
||||
dataSource,
|
||||
panelType,
|
||||
}: {
|
||||
dataSource: DataSource;
|
||||
panelType: PanelTypeKeys;
|
||||
}): JSX.Element {
|
||||
customMessage,
|
||||
}: EmptyLogsSearchProps): JSX.Element {
|
||||
const logEventCalledRef = useRef(false);
|
||||
useEffect(() => {
|
||||
if (!logEventCalledRef.current) {
|
||||
@@ -30,18 +38,80 @@ export default function EmptyLogsSearch({
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="empty-logs-search-container">
|
||||
<div className="empty-logs-search-container-content">
|
||||
<img
|
||||
src="/Icons/emptyState.svg"
|
||||
alt="thinking-emoji"
|
||||
className="empty-state-svg"
|
||||
/>
|
||||
<Typography.Text>
|
||||
<span className="sub-text">This query had no results. </span>
|
||||
Edit your query and try again!
|
||||
</Typography.Text>
|
||||
<div
|
||||
className={cx('empty-logs-search__container', {
|
||||
'empty-logs-search__container--custom-message': !!customMessage,
|
||||
})}
|
||||
>
|
||||
<div className="empty-logs-search__row">
|
||||
<div className="empty-logs-search__content">
|
||||
<img
|
||||
src="/Icons/emptyState.svg"
|
||||
alt="thinking-emoji"
|
||||
className="empty-state-svg"
|
||||
/>
|
||||
{customMessage ? (
|
||||
<>
|
||||
<div className="empty-logs-search__header">
|
||||
<Typography.Text className="empty-logs-search__title">
|
||||
{customMessage.title}
|
||||
</Typography.Text>
|
||||
{customMessage.subTitle && (
|
||||
<Typography.Text className="empty-logs-search__subtitle">
|
||||
{customMessage.subTitle}
|
||||
</Typography.Text>
|
||||
)}
|
||||
</div>
|
||||
{Array.isArray(customMessage.description) ? (
|
||||
<ul className="empty-logs-search__description-list">
|
||||
{customMessage.description.map((desc) => (
|
||||
<li key={desc}>{desc}</li>
|
||||
))}
|
||||
</ul>
|
||||
) : (
|
||||
<Typography.Text className="empty-logs-search__description">
|
||||
{customMessage.description}
|
||||
</Typography.Text>
|
||||
)}
|
||||
{/* Clear filters button */}
|
||||
{customMessage.showClearFiltersButton && (
|
||||
<button
|
||||
type="button"
|
||||
className="empty-logs-search__clear-filters-btn"
|
||||
onClick={customMessage.onClearFilters}
|
||||
>
|
||||
{customMessage.clearFiltersButtonText}
|
||||
<span className="empty-logs-search__clear-filters-btn-icon">
|
||||
<Delete size={14} />
|
||||
Clear filters
|
||||
</span>
|
||||
</button>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<Typography.Text>
|
||||
<span className="empty-logs-search__sub-text">
|
||||
This query had no results.{' '}
|
||||
</span>
|
||||
Edit your query and try again!
|
||||
</Typography.Text>
|
||||
)}
|
||||
</div>
|
||||
{customMessage?.documentationLinks && (
|
||||
<div className="empty-logs-search__resources-card">
|
||||
<div className="empty-logs-search__resources-title">RESOURCES</div>
|
||||
<div className="empty-logs-search__resources-links">
|
||||
{customMessage.documentationLinks.map((link) => (
|
||||
<LearnMore key={link.text} text={link.text} url={link.url} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
EmptyLogsSearch.defaultProps = {
|
||||
customMessage: null,
|
||||
};
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
.explorer-options-container {
|
||||
position: fixed;
|
||||
bottom: 8px;
|
||||
bottom: 24px;
|
||||
left: calc(50% + 240px);
|
||||
transform: translate(calc(-50% - 120px), 0);
|
||||
transition: left 0.2s linear;
|
||||
|
||||
border-radius: 6px;
|
||||
background: var(--Ink-300, #16181d);
|
||||
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
background-color: transparent;
|
||||
|
||||
.multi-alert-button,
|
||||
@@ -34,15 +32,19 @@
|
||||
.explorer-update {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 8px;
|
||||
background: var(--Ink-300, #16181d);
|
||||
gap: 12px;
|
||||
padding: 10px 10px;
|
||||
border-radius: 50px;
|
||||
border: 1px solid var(--bg-slate-400);
|
||||
background: rgba(22, 24, 29, 0.6);
|
||||
backdrop-filter: blur(20px);
|
||||
|
||||
.action-icon {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 6px;
|
||||
padding: 8px;
|
||||
border-radius: 50px;
|
||||
border: 1px solid var(--bg-slate-400);
|
||||
background: var(--bg-slate-500);
|
||||
cursor: pointer;
|
||||
@@ -62,8 +64,10 @@
|
||||
|
||||
.explorer-options {
|
||||
padding: 10px 12px;
|
||||
background: var(--Ink-300, #16181d);
|
||||
border-radius: 2px;
|
||||
border: 1px solid var(--bg-slate-400);
|
||||
border-radius: 50px;
|
||||
background: rgba(22, 24, 29, 0.6);
|
||||
backdrop-filter: blur(20px);
|
||||
|
||||
cursor: default;
|
||||
display: flex;
|
||||
@@ -92,6 +96,27 @@
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
button {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border: none;
|
||||
|
||||
border: 1px solid #1d2023;
|
||||
|
||||
box-shadow: none !important;
|
||||
|
||||
&.ant-btn-round {
|
||||
padding: 8px 12px 8px 10px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
&.ant-btn-round:disabled {
|
||||
background-color: rgba(209, 209, 209, 0.074);
|
||||
color: #5f5f5f;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-select-focused {
|
||||
border-color: transparent !important;
|
||||
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
/* eslint-disable react/jsx-props-no-spreading */
|
||||
import './ExplorerOptions.styles.scss';
|
||||
|
||||
import { InfoCircleOutlined } from '@ant-design/icons';
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import {
|
||||
Button,
|
||||
ColorPicker,
|
||||
Divider,
|
||||
Input,
|
||||
Modal,
|
||||
RefSelectProps,
|
||||
@@ -13,6 +15,7 @@ import {
|
||||
Typography,
|
||||
} from 'antd';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { TelemetryFieldKey } from 'api/v5/v5';
|
||||
import axios from 'axios';
|
||||
import cx from 'classnames';
|
||||
import { getViewDetailsUsingViewKey } from 'components/ExplorerCard/utils';
|
||||
@@ -43,7 +46,14 @@ import { useHandleExplorerTabChange } from 'hooks/useHandleExplorerTabChange';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import { mapCompositeQueryFromQuery } from 'lib/newQueryBuilder/queryBuilderMappers/mapCompositeQueryFromQuery';
|
||||
import { cloneDeep, isEqual, omit } from 'lodash-es';
|
||||
import { Check, ConciergeBell, Disc3, Plus, X } from 'lucide-react';
|
||||
import {
|
||||
Check,
|
||||
ConciergeBell,
|
||||
Disc3,
|
||||
PanelBottomClose,
|
||||
Plus,
|
||||
X,
|
||||
} from 'lucide-react';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
import { FormattingOptions } from 'providers/preferences/types';
|
||||
import {
|
||||
@@ -58,7 +68,6 @@ import {
|
||||
} from 'react';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { Dashboard } from 'types/api/dashboard/getAll';
|
||||
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { ViewProps } from 'types/api/saveViews/types';
|
||||
import { DataSource, StringOperators } from 'types/common/queryBuilder';
|
||||
@@ -69,8 +78,10 @@ import ExplorerOptionsHideArea from './ExplorerOptionsHideArea';
|
||||
import { PreservedViewsInLocalStorage } from './types';
|
||||
import {
|
||||
DATASOURCE_VS_ROUTES,
|
||||
generateRGBAFromHex,
|
||||
getRandomColor,
|
||||
saveNewViewHandler,
|
||||
setExplorerToolBarVisibility,
|
||||
} from './utils';
|
||||
|
||||
const allowedRoles = [USER_ROLES.ADMIN, USER_ROLES.AUTHOR, USER_ROLES.EDITOR];
|
||||
@@ -242,6 +253,12 @@ function ExplorerOptions({
|
||||
const extraData = viewsData?.data?.data?.find((view) => view.id === viewKey)
|
||||
?.extraData;
|
||||
|
||||
const extraDataColor = extraData ? JSON.parse(extraData).color : '';
|
||||
const rgbaColor = generateRGBAFromHex(
|
||||
extraDataColor || Color.BG_SIENNA_500,
|
||||
0.08,
|
||||
);
|
||||
|
||||
const { options, handleOptionsChange } = useOptionsMenu({
|
||||
storageKey:
|
||||
sourcepage === DataSource.TRACES
|
||||
@@ -253,7 +270,7 @@ function ExplorerOptions({
|
||||
|
||||
const getUpdatedExtraData = (
|
||||
extraData: string | undefined,
|
||||
newSelectedColumns: BaseAutocompleteData[],
|
||||
newSelectedColumns: TelemetryFieldKey[],
|
||||
formattingOptions?: FormattingOptions,
|
||||
): string => {
|
||||
let updatedExtraData;
|
||||
@@ -337,7 +354,7 @@ function ExplorerOptions({
|
||||
const { handleExplorerTabChange } = useHandleExplorerTabChange();
|
||||
|
||||
type ExtraData = {
|
||||
selectColumns?: BaseAutocompleteData[];
|
||||
selectColumns?: TelemetryFieldKey[];
|
||||
version?: number;
|
||||
};
|
||||
|
||||
@@ -623,6 +640,27 @@ function ExplorerOptions({
|
||||
viewsData?.data?.data,
|
||||
]);
|
||||
|
||||
const infoIconText = useMemo(() => {
|
||||
if (isLogsExplorer) {
|
||||
return 'Learn more about Logs explorer';
|
||||
}
|
||||
if (isMetricsExplorer) {
|
||||
return 'Learn more about Metrics explorer';
|
||||
}
|
||||
return 'Learn more about Traces explorer';
|
||||
}, [isLogsExplorer, isMetricsExplorer]);
|
||||
|
||||
const infoIconLink = useMemo(() => {
|
||||
if (isLogsExplorer) {
|
||||
return 'https://signoz.io/docs/product-features/logs-explorer/?utm_source=product&utm_medium=logs-explorer-toolbar';
|
||||
}
|
||||
// TODO: Add metrics explorer info icon link
|
||||
if (isMetricsExplorer) {
|
||||
return '';
|
||||
}
|
||||
return 'https://signoz.io/docs/product-features/trace-explorer/?utm_source=product&utm_medium=trace-explorer-toolbar';
|
||||
}, [isLogsExplorer, isMetricsExplorer]);
|
||||
|
||||
const getQueryName = (query: Query): string => {
|
||||
if (query.builder.queryFormulas.length > 0) {
|
||||
return `Formula ${query.builder.queryFormulas[0].queryName}`;
|
||||
@@ -635,10 +673,11 @@ function ExplorerOptions({
|
||||
const selectLabel = (
|
||||
<Button
|
||||
disabled={disabled}
|
||||
className="periscope-btn ghost"
|
||||
shape="default"
|
||||
shape="round"
|
||||
icon={<ConciergeBell size={16} />}
|
||||
/>
|
||||
>
|
||||
Create an Alert
|
||||
</Button>
|
||||
);
|
||||
return (
|
||||
<Select
|
||||
@@ -667,11 +706,12 @@ function ExplorerOptions({
|
||||
return (
|
||||
<Button
|
||||
disabled={disabled}
|
||||
shape="default"
|
||||
className="periscope-btn ghost"
|
||||
shape="round"
|
||||
onClick={(): void => onCreateAlertsHandler(query)}
|
||||
icon={<ConciergeBell size={16} />}
|
||||
/>
|
||||
>
|
||||
Create an Alert
|
||||
</Button>
|
||||
);
|
||||
}, [
|
||||
disabled,
|
||||
@@ -685,11 +725,14 @@ function ExplorerOptions({
|
||||
if (isOneChartPerQuery) {
|
||||
const selectLabel = (
|
||||
<Button
|
||||
className="periscope-btn ghost"
|
||||
type="primary"
|
||||
disabled={disabled}
|
||||
shape="round"
|
||||
onClick={onAddToDashboard}
|
||||
icon={<Plus size={12} />}
|
||||
/>
|
||||
icon={<Plus size={16} />}
|
||||
>
|
||||
Add to Dashboard
|
||||
</Button>
|
||||
);
|
||||
return (
|
||||
<Select
|
||||
@@ -721,14 +764,24 @@ function ExplorerOptions({
|
||||
}
|
||||
return (
|
||||
<Button
|
||||
className="periscope-btn ghost"
|
||||
type="primary"
|
||||
disabled={disabled}
|
||||
shape="round"
|
||||
onClick={onAddToDashboard}
|
||||
icon={<Plus size={16} />}
|
||||
/>
|
||||
>
|
||||
Add to Dashboard
|
||||
</Button>
|
||||
);
|
||||
}, [disabled, isOneChartPerQuery, onAddToDashboard, splitedQueries]);
|
||||
|
||||
const hideToolbar = (): void => {
|
||||
setExplorerToolBarVisibility(false, sourcepage);
|
||||
if (setIsExplorerOptionHidden) {
|
||||
setIsExplorerOptionHidden(true);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="explorer-options-container">
|
||||
{
|
||||
@@ -744,31 +797,41 @@ function ExplorerOptions({
|
||||
>
|
||||
<Tooltip title="Clear this view" placement="top">
|
||||
<Button
|
||||
className="periscope-btn ghost"
|
||||
className="action-icon"
|
||||
onClick={handleClearSelect}
|
||||
icon={<X size={16} />}
|
||||
icon={<X size={14} />}
|
||||
/>
|
||||
</Tooltip>
|
||||
{
|
||||
// only show the update view option when the query is updated
|
||||
}
|
||||
{isQueryUpdated && (
|
||||
<Tooltip title="Update this view" placement="top">
|
||||
<Button
|
||||
className={cx(
|
||||
'periscope-btn ghost',
|
||||
isEditDeleteSupported ? '' : 'hidden',
|
||||
)}
|
||||
disabled={isViewUpdating}
|
||||
onClick={onUpdateQueryHandler}
|
||||
icon={<Disc3 size={16} />}
|
||||
<>
|
||||
<Divider
|
||||
type="vertical"
|
||||
className={isEditDeleteSupported ? '' : 'hidden'}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip title="Update this view" placement="top">
|
||||
<Button
|
||||
className={cx('action-icon', isEditDeleteSupported ? ' ' : 'hidden')}
|
||||
disabled={isViewUpdating}
|
||||
onClick={onUpdateQueryHandler}
|
||||
icon={<Disc3 size={14} />}
|
||||
/>
|
||||
</Tooltip>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{!isExplorerOptionHidden && (
|
||||
<div className="explorer-options">
|
||||
<div
|
||||
className="explorer-options"
|
||||
style={{
|
||||
background: extraData
|
||||
? `linear-gradient(90deg, rgba(0,0,0,0) -5%, ${rgbaColor} 9%, rgba(0,0,0,0) 30%)`
|
||||
: 'transparent',
|
||||
}}
|
||||
>
|
||||
<div className="view-options">
|
||||
<Select<string, { key: string; value: string }>
|
||||
showSearch
|
||||
@@ -809,23 +872,49 @@ function ExplorerOptions({
|
||||
</Select>
|
||||
|
||||
<Button
|
||||
shape="default"
|
||||
className={cx(
|
||||
'periscope-btn secondary',
|
||||
isEditDeleteSupported ? '' : 'hidden',
|
||||
)}
|
||||
shape="round"
|
||||
onClick={handleSaveViewModalToggle}
|
||||
className={isEditDeleteSupported ? '' : 'hidden'}
|
||||
disabled={viewsIsLoading || isRefetching}
|
||||
icon={<Disc3 size={12} />}
|
||||
icon={<Disc3 size={16} />}
|
||||
>
|
||||
Save this view
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<hr className={isEditDeleteSupported ? '' : 'hidden'} />
|
||||
|
||||
<div className={cx('actions', isEditDeleteSupported ? '' : 'hidden')}>
|
||||
{alertButton}
|
||||
{dashboardButton}
|
||||
</div>
|
||||
<div className="actions">
|
||||
{/* Hide the info icon for metrics explorer until we get the docs link */}
|
||||
{!isMetricsExplorer && (
|
||||
<Tooltip
|
||||
title={
|
||||
<div>
|
||||
{infoIconText}
|
||||
<Typography.Link href={infoIconLink} target="_blank">
|
||||
{' '}
|
||||
here
|
||||
</Typography.Link>{' '}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<InfoCircleOutlined className="info-icon" />
|
||||
</Tooltip>
|
||||
)}
|
||||
<Tooltip title="Hide">
|
||||
<Button
|
||||
disabled={disabled}
|
||||
shape="circle"
|
||||
onClick={hideToolbar}
|
||||
icon={<PanelBottomClose size={16} />}
|
||||
data-testid="hide-toolbar"
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<ExplorerOptionsHideArea
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
Time as TimeV2,
|
||||
} from 'container/TopNav/DateTimeSelectionV2/config';
|
||||
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import { useResizeObserver } from 'hooks/useDimensions';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
@@ -79,6 +80,7 @@ function ChartPreview({
|
||||
const threshold = alertDef?.condition.target || 0;
|
||||
const [minTimeScale, setMinTimeScale] = useState<number>();
|
||||
const [maxTimeScale, setMaxTimeScale] = useState<number>();
|
||||
const { currentQuery } = useQueryBuilder();
|
||||
|
||||
const { minTime, maxTime, selectedTime: globalSelectedInterval } = useSelector<
|
||||
AppState,
|
||||
@@ -254,6 +256,8 @@ function ChartPreview({
|
||||
tzDate: (timestamp: number) =>
|
||||
uPlot.tzDate(new Date(timestamp * 1e3), timezone.value),
|
||||
timezone: timezone.value,
|
||||
currentQuery,
|
||||
query: query || currentQuery,
|
||||
}),
|
||||
[
|
||||
yAxisUnit,
|
||||
@@ -269,6 +273,8 @@ function ChartPreview({
|
||||
alertDef?.condition.targetUnit,
|
||||
graphType,
|
||||
timezone.value,
|
||||
currentQuery,
|
||||
query,
|
||||
],
|
||||
);
|
||||
|
||||
|
||||
@@ -212,9 +212,12 @@ function QuerySection({
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const step2Label = alertDef.alertType === 'METRIC_BASED_ALERT' ? '2' : '1';
|
||||
|
||||
return (
|
||||
<>
|
||||
<StepHeading> {t('alert_form_step2')}</StepHeading>
|
||||
<StepHeading> {t('alert_form_step2', { step: step2Label })}</StepHeading>
|
||||
<FormContainer className="alert-query-section-container">
|
||||
<div>{renderTabs(alertType)}</div>
|
||||
{renderQuerySection(currentTab)}
|
||||
|
||||
@@ -371,9 +371,11 @@ function RuleOptions({
|
||||
selectedCategory?.name,
|
||||
);
|
||||
|
||||
const step3Label = alertDef.alertType === 'METRIC_BASED_ALERT' ? '3' : '2';
|
||||
|
||||
return (
|
||||
<>
|
||||
<StepHeading>{t('alert_form_step3')}</StepHeading>
|
||||
<StepHeading>{t('alert_form_step3', { step: step3Label })}</StepHeading>
|
||||
<FormContainer>
|
||||
{queryCategory === EQueryType.PROM && renderPromRuleOptions()}
|
||||
{queryCategory !== EQueryType.PROM &&
|
||||
|
||||
@@ -137,6 +137,7 @@ function GridCardGraph({
|
||||
formatForWeb: widget.panelTypes === PANEL_TYPES.TABLE,
|
||||
start: customTimeRange?.startTime || start,
|
||||
end: customTimeRange?.endTime || end,
|
||||
originalGraphType: widget.panelTypes,
|
||||
};
|
||||
}
|
||||
updatedQuery.builder.queryData[0].pageSize = 10;
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
overflow: auto;
|
||||
margin: 8px -8px;
|
||||
margin-right: 0;
|
||||
margin-bottom: 64px;
|
||||
|
||||
.react-grid-layout {
|
||||
border: none !important;
|
||||
|
||||
@@ -46,6 +46,7 @@ function useUpdatedQuery(): UseUpdatedQueryResult {
|
||||
selectedTime: widgetConfig.timePreferance,
|
||||
globalSelectedInterval,
|
||||
variables: getDashboardVariables(selectedDashboard?.data?.variables),
|
||||
originalGraphType: widgetConfig.panelTypes,
|
||||
});
|
||||
|
||||
// Execute query and process results
|
||||
|
||||
@@ -101,7 +101,7 @@ export function updateStepInterval(
|
||||
|
||||
// if user haven't enter anything manually, that is we have default value of 60 then do the interval adjustment for bar otherwise apply the user's value
|
||||
const getSteps = (queryData: IBuilderQuery): number =>
|
||||
queryData.stepInterval === 60
|
||||
queryData?.stepInterval === 60
|
||||
? stepIntervalPoints || 60
|
||||
: queryData?.stepInterval || 60;
|
||||
|
||||
|
||||
@@ -84,11 +84,12 @@ function GridTableComponent({
|
||||
const newValue = { ...val };
|
||||
Object.keys(val).forEach((k) => {
|
||||
if (columnUnits[k]) {
|
||||
// the check below takes care of not adding units for rows that have n/a values
|
||||
newValue[k] =
|
||||
val[k] !== 'n/a'
|
||||
? getYAxisFormattedValue(String(val[k]), columnUnits[k])
|
||||
: val[k];
|
||||
// the check below takes care of not adding units for rows that have n/a or null values
|
||||
if (val[k] !== 'n/a' && val[k] !== null) {
|
||||
newValue[k] = getYAxisFormattedValue(String(val[k]), columnUnits[k]);
|
||||
} else if (val[k] === null) {
|
||||
newValue[k] = 'n/a';
|
||||
}
|
||||
newValue[`${k}_without_unit`] = val[k];
|
||||
}
|
||||
});
|
||||
@@ -101,6 +102,7 @@ function GridTableComponent({
|
||||
[columnUnits],
|
||||
);
|
||||
|
||||
console.log('columnUnits', columnUnits, originalDataSource);
|
||||
const dataSource = useMemo(() => applyColumnUnits(originalDataSource), [
|
||||
applyColumnUnits,
|
||||
originalDataSource,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
/* eslint-disable sonarjs/cognitive-complexity */
|
||||
import { ColumnsType, ColumnType } from 'antd/es/table';
|
||||
import { createAggregation } from 'api/v5/queryRange/prepareQueryRangePayloadV5';
|
||||
import { convertUnit } from 'container/NewWidget/RightContainer/dataFormatCategories';
|
||||
import { ThresholdProps } from 'container/NewWidget/RightContainer/Threshold/types';
|
||||
import { QUERY_TABLE_CONFIG } from 'container/QueryTable/config';
|
||||
@@ -183,10 +184,19 @@ export function createColumnsAndDataSource(
|
||||
? getQueryLegend(currentQuery, item.queryName)
|
||||
: undefined;
|
||||
|
||||
const isMultipleAggregations =
|
||||
createAggregation(
|
||||
currentQuery.queryType === EQueryType.QUERY_BUILDER
|
||||
? currentQuery.builder?.queryData?.find(
|
||||
(query) => query.queryName === item.queryName,
|
||||
)
|
||||
: undefined,
|
||||
)?.length > 1;
|
||||
|
||||
const column: ColumnType<RowData> = {
|
||||
dataIndex: item.name,
|
||||
// if no legend present then rely on the column name value
|
||||
title: !isEmpty(legend) ? legend : item.name,
|
||||
title: !isMultipleAggregations && !isEmpty(legend) ? legend : item.name,
|
||||
width: QUERY_TABLE_CONFIG.width,
|
||||
render: renderColumnCell && renderColumnCell[item.name],
|
||||
sorter: (a: RowData, b: RowData): number => sortFunction(a, b, item),
|
||||
|
||||
@@ -96,11 +96,41 @@ function HostsList(): JSX.Element {
|
||||
};
|
||||
}, [pageSize, currentPage, filters, minTime, maxTime, orderBy]);
|
||||
|
||||
const queryKey = useMemo(() => {
|
||||
if (selectedHostName) {
|
||||
return [
|
||||
'hostList',
|
||||
String(pageSize),
|
||||
String(currentPage),
|
||||
JSON.stringify(filters),
|
||||
JSON.stringify(orderBy),
|
||||
];
|
||||
}
|
||||
return [
|
||||
'hostList',
|
||||
String(pageSize),
|
||||
String(currentPage),
|
||||
JSON.stringify(filters),
|
||||
JSON.stringify(orderBy),
|
||||
String(minTime),
|
||||
String(maxTime),
|
||||
];
|
||||
}, [
|
||||
pageSize,
|
||||
currentPage,
|
||||
filters,
|
||||
orderBy,
|
||||
selectedHostName,
|
||||
minTime,
|
||||
maxTime,
|
||||
]);
|
||||
|
||||
const { data, isFetching, isLoading, isError } = useGetHostList(
|
||||
query as HostListPayload,
|
||||
{
|
||||
queryKey: ['hostList', query],
|
||||
queryKey,
|
||||
enabled: !!query,
|
||||
keepPreviousData: true,
|
||||
},
|
||||
);
|
||||
|
||||
@@ -212,6 +242,7 @@ function HostsList(): JSX.Element {
|
||||
<HostsListControls
|
||||
filters={filters}
|
||||
handleFiltersChange={handleFiltersChange}
|
||||
showAutoRefresh={!selectedHostData}
|
||||
/>
|
||||
</div>
|
||||
<HostsListTable
|
||||
|
||||
@@ -11,9 +11,11 @@ import { DataSource } from 'types/common/queryBuilder';
|
||||
function HostsListControls({
|
||||
handleFiltersChange,
|
||||
filters,
|
||||
showAutoRefresh,
|
||||
}: {
|
||||
handleFiltersChange: (value: IBuilderQuery['filters']) => void;
|
||||
filters: IBuilderQuery['filters'];
|
||||
showAutoRefresh: boolean;
|
||||
}): JSX.Element {
|
||||
const currentQuery = initialQueriesMap[DataSource.METRICS];
|
||||
const updatedCurrentQuery = useMemo(
|
||||
@@ -58,7 +60,7 @@ function HostsListControls({
|
||||
|
||||
<div className="time-selector">
|
||||
<DateTimeSelectionV2
|
||||
showAutoRefresh
|
||||
showAutoRefresh={showAutoRefresh}
|
||||
showRefreshText={false}
|
||||
hideShareModal
|
||||
/>
|
||||
|
||||
@@ -93,9 +93,13 @@ export default function HostsListTable({
|
||||
const showHostsEmptyState =
|
||||
!isFetching &&
|
||||
!isLoading &&
|
||||
formattedHostMetricsData.length === 0 &&
|
||||
(!sentAnyHostMetricsData || isSendingIncorrectK8SAgentMetrics) &&
|
||||
!filters.items.length;
|
||||
|
||||
const showTableLoadingState =
|
||||
(isLoading || isFetching) && formattedHostMetricsData.length === 0;
|
||||
|
||||
if (isError) {
|
||||
return <Typography>{data?.error || 'Something went wrong'}</Typography>;
|
||||
}
|
||||
@@ -127,7 +131,7 @@ export default function HostsListTable({
|
||||
);
|
||||
}
|
||||
|
||||
if (isLoading || isFetching) {
|
||||
if (showTableLoadingState) {
|
||||
return (
|
||||
<div className="hosts-list-loading-state">
|
||||
<Skeleton.Input
|
||||
@@ -155,7 +159,7 @@ export default function HostsListTable({
|
||||
return (
|
||||
<Table
|
||||
className="hosts-list-table"
|
||||
dataSource={isLoading || isFetching ? [] : formattedHostMetricsData}
|
||||
dataSource={showTableLoadingState ? [] : formattedHostMetricsData}
|
||||
columns={columns}
|
||||
pagination={{
|
||||
current: currentPage,
|
||||
@@ -170,7 +174,7 @@ export default function HostsListTable({
|
||||
}}
|
||||
scroll={{ x: true }}
|
||||
loading={{
|
||||
spinning: isFetching || isLoading,
|
||||
spinning: showTableLoadingState,
|
||||
indicator: <Spin indicator={<LoadingOutlined size={14} spin />} />,
|
||||
}}
|
||||
tableLayout="fixed"
|
||||
|
||||
@@ -28,6 +28,7 @@ describe('HostsListControls', () => {
|
||||
<HostsListControls
|
||||
handleFiltersChange={mockHandleFiltersChange}
|
||||
filters={mockFilters}
|
||||
showAutoRefresh={false}
|
||||
/>,
|
||||
);
|
||||
|
||||
|
||||
@@ -59,13 +59,27 @@ describe('HostsListTable', () => {
|
||||
setPageSize: mockSetPageSize,
|
||||
} as any;
|
||||
|
||||
it('renders loading state if isLoading is true', () => {
|
||||
const { container } = render(<HostsListTable {...mockProps} isLoading />);
|
||||
it('renders loading state if isLoading is true and tableData is empty', () => {
|
||||
const { container } = render(
|
||||
<HostsListTable
|
||||
{...mockProps}
|
||||
isLoading
|
||||
hostMetricsData={[]}
|
||||
tableData={{ payload: { data: { hosts: [] } } }}
|
||||
/>,
|
||||
);
|
||||
expect(container.querySelector('.hosts-list-loading-state')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('renders loading state if isFetching is true', () => {
|
||||
const { container } = render(<HostsListTable {...mockProps} isFetching />);
|
||||
it('renders loading state if isFetching is true and tableData is empty', () => {
|
||||
const { container } = render(
|
||||
<HostsListTable
|
||||
{...mockProps}
|
||||
isFetching
|
||||
hostMetricsData={[]}
|
||||
tableData={{ payload: { data: { hosts: [] } } }}
|
||||
/>,
|
||||
);
|
||||
expect(container.querySelector('.hosts-list-loading-state')).toBeTruthy();
|
||||
});
|
||||
|
||||
@@ -75,7 +89,17 @@ describe('HostsListTable', () => {
|
||||
});
|
||||
|
||||
it('renders empty state if no hosts are found', () => {
|
||||
const { container } = render(<HostsListTable {...mockProps} />);
|
||||
const { container } = render(
|
||||
<HostsListTable
|
||||
{...mockProps}
|
||||
hostMetricsData={[]}
|
||||
tableData={{
|
||||
payload: {
|
||||
data: { hosts: [] },
|
||||
},
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
expect(container.querySelector(EMPTY_STATE_CONTAINER_CLASS)).toBeTruthy();
|
||||
});
|
||||
|
||||
@@ -83,6 +107,7 @@ describe('HostsListTable', () => {
|
||||
const { container } = render(
|
||||
<HostsListTable
|
||||
{...mockProps}
|
||||
hostMetricsData={[]}
|
||||
tableData={{
|
||||
...mockTableData,
|
||||
payload: {
|
||||
@@ -90,6 +115,7 @@ describe('HostsListTable', () => {
|
||||
data: {
|
||||
...mockTableData.payload.data,
|
||||
sentAnyHostMetricsData: false,
|
||||
hosts: [],
|
||||
},
|
||||
},
|
||||
}}
|
||||
@@ -102,6 +128,7 @@ describe('HostsListTable', () => {
|
||||
const { container } = render(
|
||||
<HostsListTable
|
||||
{...mockProps}
|
||||
hostMetricsData={[]}
|
||||
tableData={{
|
||||
...mockTableData,
|
||||
payload: {
|
||||
@@ -109,6 +136,7 @@ describe('HostsListTable', () => {
|
||||
data: {
|
||||
...mockTableData.payload.data,
|
||||
isSendingIncorrectK8SAgentMetrics: true,
|
||||
hosts: [],
|
||||
},
|
||||
},
|
||||
}}
|
||||
|
||||
@@ -38,7 +38,7 @@ import {
|
||||
ScrollText,
|
||||
X,
|
||||
} from 'lucide-react';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useSearchParams } from 'react-router-dom-v5-compat';
|
||||
import { AppState } from 'store/reducers';
|
||||
@@ -85,8 +85,12 @@ function ClusterDetails({
|
||||
endTime: endMs,
|
||||
}));
|
||||
|
||||
const lastSelectedInterval = useRef<Time | null>(null);
|
||||
|
||||
const [selectedInterval, setSelectedInterval] = useState<Time>(
|
||||
selectedTime as Time,
|
||||
lastSelectedInterval.current
|
||||
? lastSelectedInterval.current
|
||||
: (selectedTime as Time),
|
||||
);
|
||||
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
@@ -195,10 +199,11 @@ function ClusterDetails({
|
||||
}, [initialFilters, initialEventsFilters]);
|
||||
|
||||
useEffect(() => {
|
||||
setSelectedInterval(selectedTime as Time);
|
||||
const currentSelectedInterval = lastSelectedInterval.current || selectedTime;
|
||||
setSelectedInterval(currentSelectedInterval as Time);
|
||||
|
||||
if (selectedTime !== 'custom') {
|
||||
const { maxTime, minTime } = GetMinMax(selectedTime);
|
||||
if (currentSelectedInterval !== 'custom') {
|
||||
const { maxTime, minTime } = GetMinMax(currentSelectedInterval);
|
||||
|
||||
setModalTimeRange({
|
||||
startTime: Math.floor(minTime / 1000000000),
|
||||
@@ -226,6 +231,7 @@ function ClusterDetails({
|
||||
|
||||
const handleTimeChange = useCallback(
|
||||
(interval: Time | CustomTimeType, dateTimeRange?: [number, number]): void => {
|
||||
lastSelectedInterval.current = interval as Time;
|
||||
setSelectedInterval(interval as Time);
|
||||
|
||||
if (interval === 'custom' && dateTimeRange) {
|
||||
@@ -462,6 +468,7 @@ function ClusterDetails({
|
||||
};
|
||||
|
||||
const handleClose = (): void => {
|
||||
lastSelectedInterval.current = null;
|
||||
setSelectedInterval(selectedTime as Time);
|
||||
|
||||
if (selectedTime !== 'custom') {
|
||||
|
||||
@@ -51,8 +51,8 @@ export const getClusterMetricsQueryPayload = (
|
||||
const getKey = (dotKey: string, underscoreKey: string): string =>
|
||||
dotMetricsEnabled ? dotKey : underscoreKey;
|
||||
const k8sPodCpuUtilizationKey = getKey(
|
||||
'k8s.pod.cpu.utilization',
|
||||
'k8s_pod_cpu_utilization',
|
||||
'k8s.pod.cpu.usage',
|
||||
'k8s_pod_cpu_usage',
|
||||
);
|
||||
const k8sNodeAllocatableCpuKey = getKey(
|
||||
'k8s.node.allocatable_cpu',
|
||||
@@ -146,7 +146,7 @@ export const getClusterMetricsQueryPayload = (
|
||||
{
|
||||
aggregateAttribute: {
|
||||
dataType: DataTypes.Float64,
|
||||
id: 'k8s_pod_cpu_utilization--float64--Gauge--true',
|
||||
id: 'k8s_pod_cpu_usage--float64--Gauge--true',
|
||||
isColumn: true,
|
||||
isJSON: false,
|
||||
key: k8sPodCpuUtilizationKey,
|
||||
@@ -189,7 +189,7 @@ export const getClusterMetricsQueryPayload = (
|
||||
{
|
||||
aggregateAttribute: {
|
||||
dataType: DataTypes.Float64,
|
||||
id: 'k8s_pod_cpu_utilization--float64--Gauge--true',
|
||||
id: 'k8s_pod_cpu_usage--float64--Gauge--true',
|
||||
isColumn: true,
|
||||
isJSON: false,
|
||||
key: k8sPodCpuUtilizationKey,
|
||||
@@ -232,7 +232,7 @@ export const getClusterMetricsQueryPayload = (
|
||||
{
|
||||
aggregateAttribute: {
|
||||
dataType: DataTypes.Float64,
|
||||
id: 'k8s_pod_cpu_utilization--float64--Gauge--true',
|
||||
id: 'k8s_pod_cpu_usage--float64--Gauge--true',
|
||||
isColumn: true,
|
||||
isJSON: false,
|
||||
key: k8sPodCpuUtilizationKey,
|
||||
@@ -731,7 +731,7 @@ export const getClusterMetricsQueryPayload = (
|
||||
},
|
||||
{
|
||||
selectedTime: 'GLOBAL_TIME',
|
||||
graphType: PANEL_TYPES.TIME_SERIES,
|
||||
graphType: PANEL_TYPES.TABLE,
|
||||
query: {
|
||||
builder: {
|
||||
queryData: [
|
||||
@@ -751,7 +751,7 @@ export const getClusterMetricsQueryPayload = (
|
||||
filters: {
|
||||
items: [
|
||||
{
|
||||
id: 'd7779183',
|
||||
id: 'a7da59c7',
|
||||
key: {
|
||||
dataType: DataTypes.String,
|
||||
id: 'k8s_cluster_name--string--tag--false',
|
||||
@@ -786,12 +786,12 @@ export const getClusterMetricsQueryPayload = (
|
||||
},
|
||||
],
|
||||
having: [],
|
||||
legend: `{{${k8sDeploymentNameKey}}} ({{${k8sNamespaceNameKey}})`,
|
||||
legend: 'available',
|
||||
limit: null,
|
||||
orderBy: [],
|
||||
queryName: 'A',
|
||||
reduceTo: 'avg',
|
||||
spaceAggregation: 'max',
|
||||
reduceTo: 'last',
|
||||
spaceAggregation: 'sum',
|
||||
stepInterval: 60,
|
||||
timeAggregation: 'latest',
|
||||
},
|
||||
@@ -804,14 +804,14 @@ export const getClusterMetricsQueryPayload = (
|
||||
key: k8sDeploymentDesiredKey,
|
||||
type: 'Gauge',
|
||||
},
|
||||
aggregateOperator: 'latest',
|
||||
aggregateOperator: 'avg',
|
||||
dataSource: DataSource.METRICS,
|
||||
disabled: false,
|
||||
expression: 'B',
|
||||
filters: {
|
||||
items: [
|
||||
{
|
||||
id: 'd7779183',
|
||||
id: '55110885',
|
||||
key: {
|
||||
dataType: DataTypes.String,
|
||||
id: 'k8s_cluster_name--string--tag--false',
|
||||
@@ -846,14 +846,14 @@ export const getClusterMetricsQueryPayload = (
|
||||
},
|
||||
],
|
||||
having: [],
|
||||
legend: `{{${k8sDeploymentNameKey}}} ({{${k8sNamespaceNameKey}})`,
|
||||
legend: 'desired',
|
||||
limit: null,
|
||||
orderBy: [],
|
||||
queryName: 'B',
|
||||
reduceTo: 'avg',
|
||||
spaceAggregation: 'max',
|
||||
reduceTo: 'last',
|
||||
spaceAggregation: 'sum',
|
||||
stepInterval: 60,
|
||||
timeAggregation: 'latest',
|
||||
timeAggregation: 'avg',
|
||||
},
|
||||
],
|
||||
queryFormulas: [],
|
||||
@@ -890,13 +890,13 @@ export const getClusterMetricsQueryPayload = (
|
||||
queryType: EQueryType.QUERY_BUILDER,
|
||||
},
|
||||
variables: {},
|
||||
formatForWeb: false,
|
||||
formatForWeb: true,
|
||||
start,
|
||||
end,
|
||||
},
|
||||
{
|
||||
selectedTime: 'GLOBAL_TIME',
|
||||
graphType: PANEL_TYPES.TIME_SERIES,
|
||||
graphType: PANEL_TYPES.TABLE,
|
||||
query: {
|
||||
builder: {
|
||||
queryData: [
|
||||
@@ -909,14 +909,14 @@ export const getClusterMetricsQueryPayload = (
|
||||
key: k8sStatefulsetCurrentPodsKey,
|
||||
type: 'Gauge',
|
||||
},
|
||||
aggregateOperator: 'latest',
|
||||
aggregateOperator: 'max',
|
||||
dataSource: DataSource.METRICS,
|
||||
disabled: false,
|
||||
expression: 'A',
|
||||
filters: {
|
||||
items: [
|
||||
{
|
||||
id: 'd7779183',
|
||||
id: '3c57b4d1',
|
||||
key: {
|
||||
dataType: DataTypes.String,
|
||||
id: 'k8s_cluster_name--string--tag--false',
|
||||
@@ -951,14 +951,14 @@ export const getClusterMetricsQueryPayload = (
|
||||
},
|
||||
],
|
||||
having: [],
|
||||
legend: `{{${k8sStatefulsetNameKey}}} ({{${k8sNamespaceNameKey}})`,
|
||||
legend: 'current',
|
||||
limit: null,
|
||||
orderBy: [],
|
||||
queryName: 'A',
|
||||
reduceTo: 'avg',
|
||||
spaceAggregation: 'max',
|
||||
reduceTo: 'last',
|
||||
spaceAggregation: 'sum',
|
||||
stepInterval: 60,
|
||||
timeAggregation: 'latest',
|
||||
timeAggregation: 'max',
|
||||
},
|
||||
{
|
||||
aggregateAttribute: {
|
||||
@@ -969,14 +969,14 @@ export const getClusterMetricsQueryPayload = (
|
||||
key: k8sStatefulsetDesiredPodsKey,
|
||||
type: 'Gauge',
|
||||
},
|
||||
aggregateOperator: 'latest',
|
||||
aggregateOperator: 'max',
|
||||
dataSource: DataSource.METRICS,
|
||||
disabled: false,
|
||||
expression: 'B',
|
||||
filters: {
|
||||
items: [
|
||||
{
|
||||
id: 'd7779183',
|
||||
id: '0f49fe64',
|
||||
key: {
|
||||
dataType: DataTypes.String,
|
||||
id: 'k8s_cluster_name--string--tag--false',
|
||||
@@ -1011,14 +1011,14 @@ export const getClusterMetricsQueryPayload = (
|
||||
},
|
||||
],
|
||||
having: [],
|
||||
legend: `{{${k8sStatefulsetNameKey}}} ({{${k8sNamespaceNameKey}})`,
|
||||
legend: 'desired',
|
||||
limit: null,
|
||||
orderBy: [],
|
||||
queryName: 'B',
|
||||
reduceTo: 'avg',
|
||||
spaceAggregation: 'max',
|
||||
reduceTo: 'last',
|
||||
spaceAggregation: 'sum',
|
||||
stepInterval: 60,
|
||||
timeAggregation: 'latest',
|
||||
timeAggregation: 'max',
|
||||
},
|
||||
{
|
||||
aggregateAttribute: {
|
||||
@@ -1029,14 +1029,14 @@ export const getClusterMetricsQueryPayload = (
|
||||
key: k8sStatefulsetReadyPodsKey,
|
||||
type: 'Gauge',
|
||||
},
|
||||
aggregateOperator: 'latest',
|
||||
aggregateOperator: 'max',
|
||||
dataSource: DataSource.METRICS,
|
||||
disabled: false,
|
||||
expression: 'C',
|
||||
filters: {
|
||||
items: [
|
||||
{
|
||||
id: 'd7779183',
|
||||
id: '0bebf625',
|
||||
key: {
|
||||
dataType: DataTypes.String,
|
||||
id: 'k8s_cluster_name--string--tag--false',
|
||||
@@ -1071,14 +1071,14 @@ export const getClusterMetricsQueryPayload = (
|
||||
},
|
||||
],
|
||||
having: [],
|
||||
legend: `{{${k8sStatefulsetNameKey}}} ({{${k8sNamespaceNameKey}})`,
|
||||
legend: 'ready',
|
||||
limit: null,
|
||||
orderBy: [],
|
||||
queryName: 'C',
|
||||
reduceTo: 'avg',
|
||||
spaceAggregation: 'max',
|
||||
reduceTo: 'last',
|
||||
spaceAggregation: 'sum',
|
||||
stepInterval: 60,
|
||||
timeAggregation: 'latest',
|
||||
timeAggregation: 'max',
|
||||
},
|
||||
{
|
||||
aggregateAttribute: {
|
||||
@@ -1089,14 +1089,14 @@ export const getClusterMetricsQueryPayload = (
|
||||
key: k8sStatefulsetUpdatedPodsKey,
|
||||
type: 'Gauge',
|
||||
},
|
||||
aggregateOperator: 'latest',
|
||||
aggregateOperator: 'max',
|
||||
dataSource: DataSource.METRICS,
|
||||
disabled: false,
|
||||
expression: 'D',
|
||||
filters: {
|
||||
items: [
|
||||
{
|
||||
id: 'd7779183',
|
||||
id: '1ddacbbe',
|
||||
key: {
|
||||
dataType: DataTypes.String,
|
||||
id: 'k8s_cluster_name--string--tag--false',
|
||||
@@ -1131,14 +1131,14 @@ export const getClusterMetricsQueryPayload = (
|
||||
},
|
||||
],
|
||||
having: [],
|
||||
legend: `{{${k8sStatefulsetNameKey}}} ({{${k8sNamespaceNameKey}})`,
|
||||
legend: 'updated',
|
||||
limit: null,
|
||||
orderBy: [],
|
||||
queryName: 'D',
|
||||
reduceTo: 'avg',
|
||||
spaceAggregation: 'max',
|
||||
reduceTo: 'last',
|
||||
spaceAggregation: 'sum',
|
||||
stepInterval: 60,
|
||||
timeAggregation: 'latest',
|
||||
timeAggregation: 'max',
|
||||
},
|
||||
],
|
||||
queryFormulas: [],
|
||||
@@ -1199,13 +1199,13 @@ export const getClusterMetricsQueryPayload = (
|
||||
queryType: EQueryType.QUERY_BUILDER,
|
||||
},
|
||||
variables: {},
|
||||
formatForWeb: false,
|
||||
formatForWeb: true,
|
||||
start,
|
||||
end,
|
||||
},
|
||||
{
|
||||
selectedTime: 'GLOBAL_TIME',
|
||||
graphType: PANEL_TYPES.TIME_SERIES,
|
||||
graphType: PANEL_TYPES.TABLE,
|
||||
query: {
|
||||
builder: {
|
||||
queryData: [
|
||||
@@ -1218,14 +1218,14 @@ export const getClusterMetricsQueryPayload = (
|
||||
key: k8sDaemonsetCurrentScheduledNodesKey,
|
||||
type: 'Gauge',
|
||||
},
|
||||
aggregateOperator: 'latest',
|
||||
aggregateOperator: 'avg',
|
||||
dataSource: DataSource.METRICS,
|
||||
disabled: false,
|
||||
expression: 'A',
|
||||
filters: {
|
||||
items: [
|
||||
{
|
||||
id: 'd7779183',
|
||||
id: 'e0bea554',
|
||||
key: {
|
||||
dataType: DataTypes.String,
|
||||
id: 'k8s_cluster_name--string--tag--false',
|
||||
@@ -1250,24 +1250,16 @@ export const getClusterMetricsQueryPayload = (
|
||||
key: k8sDaemonsetNameKey,
|
||||
type: 'tag',
|
||||
},
|
||||
{
|
||||
dataType: DataTypes.String,
|
||||
id: 'k8s_namespace_name--string--tag--false',
|
||||
isColumn: false,
|
||||
isJSON: false,
|
||||
key: k8sNamespaceNameKey,
|
||||
type: 'tag',
|
||||
},
|
||||
],
|
||||
having: [],
|
||||
legend: `{{${k8sDaemonsetNameKey}} ({{${k8sNamespaceNameKey}})`,
|
||||
legend: 'current_nodes',
|
||||
limit: null,
|
||||
orderBy: [],
|
||||
queryName: 'A',
|
||||
reduceTo: 'avg',
|
||||
spaceAggregation: 'max',
|
||||
reduceTo: 'last',
|
||||
spaceAggregation: 'avg',
|
||||
stepInterval: 60,
|
||||
timeAggregation: 'latest',
|
||||
timeAggregation: 'avg',
|
||||
},
|
||||
{
|
||||
aggregateAttribute: {
|
||||
@@ -1278,14 +1270,14 @@ export const getClusterMetricsQueryPayload = (
|
||||
key: k8sDaemonsetDesiredScheduledNodesKey,
|
||||
type: 'Gauge',
|
||||
},
|
||||
aggregateOperator: 'latest',
|
||||
aggregateOperator: 'avg',
|
||||
dataSource: DataSource.METRICS,
|
||||
disabled: false,
|
||||
expression: 'B',
|
||||
filters: {
|
||||
items: [
|
||||
{
|
||||
id: 'd7779183',
|
||||
id: '741052f7',
|
||||
key: {
|
||||
dataType: DataTypes.String,
|
||||
id: 'k8s_cluster_name--string--tag--false',
|
||||
@@ -1310,24 +1302,16 @@ export const getClusterMetricsQueryPayload = (
|
||||
key: k8sDaemonsetNameKey,
|
||||
type: 'tag',
|
||||
},
|
||||
{
|
||||
dataType: DataTypes.String,
|
||||
id: 'k8s_namespace_name--string--tag--false',
|
||||
isColumn: false,
|
||||
isJSON: false,
|
||||
key: k8sNamespaceNameKey,
|
||||
type: 'tag',
|
||||
},
|
||||
],
|
||||
having: [],
|
||||
legend: `{{${k8sDaemonsetNameKey}} ({{${k8sNamespaceNameKey}})`,
|
||||
legend: 'desired_nodes',
|
||||
limit: null,
|
||||
orderBy: [],
|
||||
queryName: 'B',
|
||||
reduceTo: 'avg',
|
||||
spaceAggregation: 'max',
|
||||
reduceTo: 'last',
|
||||
spaceAggregation: 'avg',
|
||||
stepInterval: 60,
|
||||
timeAggregation: 'latest',
|
||||
timeAggregation: 'avg',
|
||||
},
|
||||
{
|
||||
aggregateAttribute: {
|
||||
@@ -1338,14 +1322,14 @@ export const getClusterMetricsQueryPayload = (
|
||||
key: k8sDaemonsetReadyNodesKey,
|
||||
type: 'Gauge',
|
||||
},
|
||||
aggregateOperator: 'latest',
|
||||
aggregateOperator: 'avg',
|
||||
dataSource: DataSource.METRICS,
|
||||
disabled: false,
|
||||
expression: 'C',
|
||||
filters: {
|
||||
items: [
|
||||
{
|
||||
id: 'd7779183',
|
||||
id: 'f23759f2',
|
||||
key: {
|
||||
dataType: DataTypes.String,
|
||||
id: 'k8s_cluster_name--string--tag--false',
|
||||
@@ -1370,24 +1354,16 @@ export const getClusterMetricsQueryPayload = (
|
||||
key: k8sDaemonsetNameKey,
|
||||
type: 'tag',
|
||||
},
|
||||
{
|
||||
dataType: DataTypes.String,
|
||||
id: 'k8s_namespace_name--string--tag--false',
|
||||
isColumn: false,
|
||||
isJSON: false,
|
||||
key: k8sNamespaceNameKey,
|
||||
type: 'tag',
|
||||
},
|
||||
],
|
||||
having: [],
|
||||
legend: `{{${k8sDaemonsetNameKey}} ({{${k8sNamespaceNameKey}})`,
|
||||
legend: 'ready_nodes',
|
||||
limit: null,
|
||||
orderBy: [],
|
||||
queryName: 'C',
|
||||
reduceTo: 'avg',
|
||||
spaceAggregation: 'max',
|
||||
reduceTo: 'last',
|
||||
spaceAggregation: 'avg',
|
||||
stepInterval: 60,
|
||||
timeAggregation: 'latest',
|
||||
timeAggregation: 'avg',
|
||||
},
|
||||
],
|
||||
queryFormulas: [],
|
||||
@@ -1436,316 +1412,7 @@ export const getClusterMetricsQueryPayload = (
|
||||
queryType: EQueryType.QUERY_BUILDER,
|
||||
},
|
||||
variables: {},
|
||||
formatForWeb: false,
|
||||
start,
|
||||
end,
|
||||
},
|
||||
{
|
||||
selectedTime: 'GLOBAL_TIME',
|
||||
graphType: PANEL_TYPES.TIME_SERIES,
|
||||
query: {
|
||||
builder: {
|
||||
queryData: [
|
||||
{
|
||||
aggregateAttribute: {
|
||||
dataType: DataTypes.Float64,
|
||||
id: 'k8s_job_active_pods--float64--Gauge--true',
|
||||
isColumn: true,
|
||||
isJSON: false,
|
||||
key: k8sJobActivePodsKey,
|
||||
type: 'Gauge',
|
||||
},
|
||||
aggregateOperator: 'latest',
|
||||
dataSource: DataSource.METRICS,
|
||||
disabled: false,
|
||||
expression: 'A',
|
||||
filters: {
|
||||
items: [
|
||||
{
|
||||
id: 'd7779183',
|
||||
key: {
|
||||
dataType: DataTypes.String,
|
||||
id: 'k8s_cluster_name--string--tag--false',
|
||||
isColumn: false,
|
||||
isJSON: false,
|
||||
key: k8sClusterNameKey,
|
||||
type: 'tag',
|
||||
},
|
||||
op: '=',
|
||||
value: cluster.meta.k8s_cluster_name,
|
||||
},
|
||||
],
|
||||
op: 'AND',
|
||||
},
|
||||
functions: [],
|
||||
groupBy: [
|
||||
{
|
||||
dataType: DataTypes.String,
|
||||
id: 'k8s_job_name--string--tag--false',
|
||||
isColumn: false,
|
||||
isJSON: false,
|
||||
key: k8sJobNameKey,
|
||||
type: 'tag',
|
||||
},
|
||||
{
|
||||
dataType: DataTypes.String,
|
||||
id: 'k8s_namespace_name--string--tag--false',
|
||||
isColumn: false,
|
||||
isJSON: false,
|
||||
key: k8sNamespaceNameKey,
|
||||
type: 'tag',
|
||||
},
|
||||
],
|
||||
having: [],
|
||||
legend: `{{${k8sJobNameKey}}} ({{${k8sNamespaceNameKey}})`,
|
||||
limit: null,
|
||||
orderBy: [],
|
||||
queryName: 'A',
|
||||
reduceTo: 'avg',
|
||||
spaceAggregation: 'max',
|
||||
stepInterval: 60,
|
||||
timeAggregation: 'latest',
|
||||
},
|
||||
{
|
||||
aggregateAttribute: {
|
||||
dataType: DataTypes.Float64,
|
||||
id: 'k8s_job_successful_pods--float64--Gauge--true',
|
||||
isColumn: true,
|
||||
isJSON: false,
|
||||
key: k8sJobSuccessfulPodsKey,
|
||||
type: 'Gauge',
|
||||
},
|
||||
aggregateOperator: 'latest',
|
||||
dataSource: DataSource.METRICS,
|
||||
disabled: false,
|
||||
expression: 'B',
|
||||
filters: {
|
||||
items: [
|
||||
{
|
||||
id: 'd7779183',
|
||||
key: {
|
||||
dataType: DataTypes.String,
|
||||
id: 'k8s_cluster_name--string--tag--false',
|
||||
isColumn: false,
|
||||
isJSON: false,
|
||||
key: k8sClusterNameKey,
|
||||
type: 'tag',
|
||||
},
|
||||
op: '=',
|
||||
value: cluster.meta.k8s_cluster_name,
|
||||
},
|
||||
],
|
||||
op: 'AND',
|
||||
},
|
||||
functions: [],
|
||||
groupBy: [
|
||||
{
|
||||
dataType: DataTypes.String,
|
||||
id: 'k8s_job_name--string--tag--false',
|
||||
isColumn: false,
|
||||
isJSON: false,
|
||||
key: k8sJobNameKey,
|
||||
type: 'tag',
|
||||
},
|
||||
{
|
||||
dataType: DataTypes.String,
|
||||
id: 'k8s_namespace_name--string--tag--false',
|
||||
isColumn: false,
|
||||
isJSON: false,
|
||||
key: k8sNamespaceNameKey,
|
||||
type: 'tag',
|
||||
},
|
||||
],
|
||||
having: [],
|
||||
legend: `{{${k8sJobNameKey}}} ({{${k8sNamespaceNameKey}})`,
|
||||
limit: null,
|
||||
orderBy: [],
|
||||
queryName: 'B',
|
||||
reduceTo: 'avg',
|
||||
spaceAggregation: 'max',
|
||||
stepInterval: 60,
|
||||
timeAggregation: 'latest',
|
||||
},
|
||||
{
|
||||
aggregateAttribute: {
|
||||
dataType: DataTypes.Float64,
|
||||
id: 'k8s_job_failed_pods--float64--Gauge--true',
|
||||
isColumn: true,
|
||||
isJSON: false,
|
||||
key: k8sJobFailedPodsKey,
|
||||
type: 'Gauge',
|
||||
},
|
||||
aggregateOperator: 'latest',
|
||||
dataSource: DataSource.METRICS,
|
||||
disabled: false,
|
||||
expression: 'C',
|
||||
filters: {
|
||||
items: [
|
||||
{
|
||||
id: 'd7779183',
|
||||
key: {
|
||||
dataType: DataTypes.String,
|
||||
id: 'k8s_cluster_name--string--tag--false',
|
||||
isColumn: false,
|
||||
isJSON: false,
|
||||
key: k8sClusterNameKey,
|
||||
type: 'tag',
|
||||
},
|
||||
op: '=',
|
||||
value: cluster.meta.k8s_cluster_name,
|
||||
},
|
||||
],
|
||||
op: 'AND',
|
||||
},
|
||||
functions: [],
|
||||
groupBy: [
|
||||
{
|
||||
dataType: DataTypes.String,
|
||||
id: 'k8s_job_name--string--tag--false',
|
||||
isColumn: false,
|
||||
isJSON: false,
|
||||
key: k8sJobNameKey,
|
||||
type: 'tag',
|
||||
},
|
||||
{
|
||||
dataType: DataTypes.String,
|
||||
id: 'k8s_namespace_name--string--tag--false',
|
||||
isColumn: false,
|
||||
isJSON: false,
|
||||
key: k8sNamespaceNameKey,
|
||||
type: 'tag',
|
||||
},
|
||||
],
|
||||
having: [],
|
||||
legend: `{{${k8sJobNameKey}}} ({{${k8sNamespaceNameKey}})`,
|
||||
limit: null,
|
||||
orderBy: [],
|
||||
queryName: 'C',
|
||||
reduceTo: 'avg',
|
||||
spaceAggregation: 'max',
|
||||
stepInterval: 60,
|
||||
timeAggregation: 'latest',
|
||||
},
|
||||
{
|
||||
aggregateAttribute: {
|
||||
dataType: DataTypes.Float64,
|
||||
id: 'k8s_job_desired_successful_pods--float64--Gauge--true',
|
||||
isColumn: true,
|
||||
isJSON: false,
|
||||
key: k8sJobDesiredSuccessfulPodsKey,
|
||||
type: 'Gauge',
|
||||
},
|
||||
aggregateOperator: 'latest',
|
||||
dataSource: DataSource.METRICS,
|
||||
disabled: false,
|
||||
expression: 'D',
|
||||
filters: {
|
||||
items: [
|
||||
{
|
||||
id: 'd7779183',
|
||||
key: {
|
||||
dataType: DataTypes.String,
|
||||
id: 'k8s_cluster_name--string--tag--false',
|
||||
isColumn: false,
|
||||
isJSON: false,
|
||||
key: k8sClusterNameKey,
|
||||
type: 'tag',
|
||||
},
|
||||
op: '=',
|
||||
value: cluster.meta.k8s_cluster_name,
|
||||
},
|
||||
],
|
||||
op: 'AND',
|
||||
},
|
||||
functions: [],
|
||||
groupBy: [
|
||||
{
|
||||
dataType: DataTypes.String,
|
||||
id: 'k8s_job_name--string--tag--false',
|
||||
isColumn: false,
|
||||
isJSON: false,
|
||||
key: k8sJobNameKey,
|
||||
type: 'tag',
|
||||
},
|
||||
{
|
||||
dataType: DataTypes.String,
|
||||
id: 'k8s_namespace_name--string--tag--false',
|
||||
isColumn: false,
|
||||
isJSON: false,
|
||||
key: k8sNamespaceNameKey,
|
||||
type: 'tag',
|
||||
},
|
||||
],
|
||||
having: [],
|
||||
legend: `{{${k8sJobNameKey}}} ({{${k8sNamespaceNameKey}})`,
|
||||
limit: null,
|
||||
orderBy: [],
|
||||
queryName: 'D',
|
||||
reduceTo: 'avg',
|
||||
spaceAggregation: 'max',
|
||||
stepInterval: 60,
|
||||
timeAggregation: 'latest',
|
||||
},
|
||||
],
|
||||
queryFormulas: [],
|
||||
},
|
||||
clickhouse_sql: [
|
||||
{
|
||||
disabled: false,
|
||||
legend: '',
|
||||
name: 'A',
|
||||
query: '',
|
||||
},
|
||||
{
|
||||
disabled: false,
|
||||
legend: '',
|
||||
name: 'B',
|
||||
query: '',
|
||||
},
|
||||
{
|
||||
disabled: false,
|
||||
legend: '',
|
||||
name: 'C',
|
||||
query: '',
|
||||
},
|
||||
{
|
||||
disabled: false,
|
||||
legend: '',
|
||||
name: 'D',
|
||||
query: '',
|
||||
},
|
||||
],
|
||||
id: v4(),
|
||||
promql: [
|
||||
{
|
||||
disabled: false,
|
||||
legend: '',
|
||||
name: 'A',
|
||||
query: '',
|
||||
},
|
||||
{
|
||||
disabled: false,
|
||||
legend: '',
|
||||
name: 'B',
|
||||
query: '',
|
||||
},
|
||||
{
|
||||
disabled: false,
|
||||
legend: '',
|
||||
name: 'C',
|
||||
query: '',
|
||||
},
|
||||
{
|
||||
disabled: false,
|
||||
legend: '',
|
||||
name: 'D',
|
||||
query: '',
|
||||
},
|
||||
],
|
||||
queryType: EQueryType.QUERY_BUILDER,
|
||||
},
|
||||
variables: {},
|
||||
formatForWeb: false,
|
||||
formatForWeb: true,
|
||||
start,
|
||||
end,
|
||||
},
|
||||
@@ -1777,7 +1444,7 @@ export const getClusterMetricsQueryPayload = (
|
||||
id: 'k8s_cluster_name--string--tag--false',
|
||||
isColumn: false,
|
||||
isJSON: false,
|
||||
key: 'k8s_cluster_name',
|
||||
key: k8sClusterNameKey,
|
||||
type: 'tag',
|
||||
},
|
||||
op: '=',
|
||||
@@ -1837,7 +1504,7 @@ export const getClusterMetricsQueryPayload = (
|
||||
id: 'k8s_cluster_name--string--tag--false',
|
||||
isColumn: false,
|
||||
isJSON: false,
|
||||
key: 'k8s_cluster_name',
|
||||
key: k8sClusterNameKey,
|
||||
type: 'tag',
|
||||
},
|
||||
op: '=',
|
||||
@@ -1897,7 +1564,7 @@ export const getClusterMetricsQueryPayload = (
|
||||
id: 'k8s_cluster_name--string--tag--false',
|
||||
isColumn: false,
|
||||
isJSON: false,
|
||||
key: 'k8s_cluster_name',
|
||||
key: k8sClusterNameKey,
|
||||
type: 'tag',
|
||||
},
|
||||
op: '=',
|
||||
@@ -1957,7 +1624,7 @@ export const getClusterMetricsQueryPayload = (
|
||||
id: 'k8s_cluster_name--string--tag--false',
|
||||
isColumn: false,
|
||||
isJSON: false,
|
||||
key: 'k8s_cluster_name',
|
||||
key: k8sClusterNameKey,
|
||||
type: 'tag',
|
||||
},
|
||||
op: '=',
|
||||
@@ -2005,6 +1672,24 @@ export const getClusterMetricsQueryPayload = (
|
||||
name: 'A',
|
||||
query: '',
|
||||
},
|
||||
{
|
||||
disabled: false,
|
||||
legend: '',
|
||||
name: 'B',
|
||||
query: '',
|
||||
},
|
||||
{
|
||||
disabled: false,
|
||||
legend: '',
|
||||
name: 'C',
|
||||
query: '',
|
||||
},
|
||||
{
|
||||
disabled: false,
|
||||
legend: '',
|
||||
name: 'D',
|
||||
query: '',
|
||||
},
|
||||
],
|
||||
id: v4(),
|
||||
promql: [
|
||||
@@ -2014,6 +1699,24 @@ export const getClusterMetricsQueryPayload = (
|
||||
name: 'A',
|
||||
query: '',
|
||||
},
|
||||
{
|
||||
disabled: false,
|
||||
legend: '',
|
||||
name: 'B',
|
||||
query: '',
|
||||
},
|
||||
{
|
||||
disabled: false,
|
||||
legend: '',
|
||||
name: 'C',
|
||||
query: '',
|
||||
},
|
||||
{
|
||||
disabled: false,
|
||||
legend: '',
|
||||
name: 'D',
|
||||
query: '',
|
||||
},
|
||||
],
|
||||
queryType: EQueryType.QUERY_BUILDER,
|
||||
},
|
||||
|
||||
@@ -189,6 +189,32 @@ function K8sClustersList({
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [minTime, maxTime, orderBy, selectedRowData, groupBy]);
|
||||
|
||||
const groupedByRowDataQueryKey = useMemo(() => {
|
||||
if (selectedClusterName) {
|
||||
return [
|
||||
'clusterList',
|
||||
JSON.stringify(queryFilters),
|
||||
JSON.stringify(orderBy),
|
||||
JSON.stringify(selectedRowData),
|
||||
];
|
||||
}
|
||||
return [
|
||||
'clusterList',
|
||||
JSON.stringify(queryFilters),
|
||||
JSON.stringify(orderBy),
|
||||
JSON.stringify(selectedRowData),
|
||||
String(minTime),
|
||||
String(maxTime),
|
||||
];
|
||||
}, [
|
||||
queryFilters,
|
||||
orderBy,
|
||||
selectedClusterName,
|
||||
minTime,
|
||||
maxTime,
|
||||
selectedRowData,
|
||||
]);
|
||||
|
||||
const {
|
||||
data: groupedByRowData,
|
||||
isFetching: isFetchingGroupedByRowData,
|
||||
@@ -198,7 +224,7 @@ function K8sClustersList({
|
||||
} = useGetK8sClustersList(
|
||||
fetchGroupedByRowDataQuery as K8sClustersListPayload,
|
||||
{
|
||||
queryKey: ['clusterList', fetchGroupedByRowDataQuery],
|
||||
queryKey: groupedByRowDataQueryKey,
|
||||
enabled: !!fetchGroupedByRowDataQuery && !!selectedRowData,
|
||||
},
|
||||
undefined,
|
||||
@@ -254,11 +280,44 @@ function K8sClustersList({
|
||||
return groupedByRowData?.payload?.data?.records || [];
|
||||
}, [groupedByRowData, selectedRowData]);
|
||||
|
||||
const queryKey = useMemo(() => {
|
||||
if (selectedClusterName) {
|
||||
return [
|
||||
'clusterList',
|
||||
String(pageSize),
|
||||
String(currentPage),
|
||||
JSON.stringify(queryFilters),
|
||||
JSON.stringify(orderBy),
|
||||
JSON.stringify(groupBy),
|
||||
];
|
||||
}
|
||||
return [
|
||||
'clusterList',
|
||||
String(pageSize),
|
||||
String(currentPage),
|
||||
JSON.stringify(queryFilters),
|
||||
JSON.stringify(orderBy),
|
||||
JSON.stringify(groupBy),
|
||||
String(minTime),
|
||||
String(maxTime),
|
||||
];
|
||||
}, [
|
||||
selectedClusterName,
|
||||
pageSize,
|
||||
currentPage,
|
||||
queryFilters,
|
||||
orderBy,
|
||||
groupBy,
|
||||
minTime,
|
||||
maxTime,
|
||||
]);
|
||||
|
||||
const { data, isFetching, isLoading, isError } = useGetK8sClustersList(
|
||||
query as K8sClustersListPayload,
|
||||
{
|
||||
queryKey: ['clusterList', query],
|
||||
queryKey,
|
||||
enabled: !!query,
|
||||
keepPreviousData: true,
|
||||
},
|
||||
undefined,
|
||||
dotMetricsEnabled,
|
||||
@@ -583,6 +642,9 @@ function K8sClustersList({
|
||||
});
|
||||
};
|
||||
|
||||
const showTableLoadingState =
|
||||
(isFetching || isLoading) && formattedClustersData.length === 0;
|
||||
|
||||
return (
|
||||
<div className="k8s-list">
|
||||
<K8sHeader
|
||||
@@ -595,12 +657,13 @@ function K8sClustersList({
|
||||
handleGroupByChange={handleGroupByChange}
|
||||
selectedGroupBy={groupBy}
|
||||
entity={K8sCategory.NODES}
|
||||
showAutoRefresh={!selectedClusterData}
|
||||
/>
|
||||
{isError && <Typography>{data?.error || 'Something went wrong'}</Typography>}
|
||||
|
||||
<Table
|
||||
className="k8s-list-table clusters-list-table"
|
||||
dataSource={isFetching || isLoading ? [] : formattedClustersData}
|
||||
dataSource={showTableLoadingState ? [] : formattedClustersData}
|
||||
columns={columns}
|
||||
pagination={{
|
||||
current: currentPage,
|
||||
@@ -612,26 +675,25 @@ function K8sClustersList({
|
||||
}}
|
||||
scroll={{ x: true }}
|
||||
loading={{
|
||||
spinning: isFetching || isLoading,
|
||||
spinning: showTableLoadingState,
|
||||
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"
|
||||
/>
|
||||
emptyText: showTableLoadingState ? 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>
|
||||
<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}
|
||||
|
||||
@@ -33,7 +33,7 @@ import {
|
||||
ScrollText,
|
||||
X,
|
||||
} from 'lucide-react';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useSearchParams } from 'react-router-dom-v5-compat';
|
||||
import { AppState } from 'store/reducers';
|
||||
@@ -84,8 +84,12 @@ function DaemonSetDetails({
|
||||
endTime: endMs,
|
||||
}));
|
||||
|
||||
const lastSelectedInterval = useRef<Time | null>(null);
|
||||
|
||||
const [selectedInterval, setSelectedInterval] = useState<Time>(
|
||||
selectedTime as Time,
|
||||
lastSelectedInterval.current
|
||||
? lastSelectedInterval.current
|
||||
: (selectedTime as Time),
|
||||
);
|
||||
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
@@ -211,10 +215,11 @@ function DaemonSetDetails({
|
||||
}, [initialFilters, initialEventsFilters]);
|
||||
|
||||
useEffect(() => {
|
||||
setSelectedInterval(selectedTime as Time);
|
||||
const currentSelectedInterval = lastSelectedInterval.current || selectedTime;
|
||||
setSelectedInterval(currentSelectedInterval as Time);
|
||||
|
||||
if (selectedTime !== 'custom') {
|
||||
const { maxTime, minTime } = GetMinMax(selectedTime);
|
||||
if (currentSelectedInterval !== 'custom') {
|
||||
const { maxTime, minTime } = GetMinMax(currentSelectedInterval);
|
||||
|
||||
setModalTimeRange({
|
||||
startTime: Math.floor(minTime / 1000000000),
|
||||
@@ -242,6 +247,7 @@ function DaemonSetDetails({
|
||||
|
||||
const handleTimeChange = useCallback(
|
||||
(interval: Time | CustomTimeType, dateTimeRange?: [number, number]): void => {
|
||||
lastSelectedInterval.current = interval as Time;
|
||||
setSelectedInterval(interval as Time);
|
||||
|
||||
if (interval === 'custom' && dateTimeRange) {
|
||||
@@ -476,6 +482,7 @@ function DaemonSetDetails({
|
||||
};
|
||||
|
||||
const handleClose = (): void => {
|
||||
lastSelectedInterval.current = null;
|
||||
setSelectedInterval(selectedTime as Time);
|
||||
|
||||
if (selectedTime !== 'custom') {
|
||||
|
||||
@@ -33,8 +33,8 @@ export const getDaemonSetMetricsQueryPayload = (
|
||||
dotMetricsEnabled: boolean,
|
||||
): GetQueryResultsProps[] => {
|
||||
const k8sPodCpuUtilizationKey = dotMetricsEnabled
|
||||
? 'k8s.pod.cpu.utilization'
|
||||
: 'k8s_pod_cpu_utilization';
|
||||
? 'k8s.pod.cpu.usage'
|
||||
: 'k8s_pod_cpu_usage';
|
||||
|
||||
const k8sContainerCpuRequestKey = dotMetricsEnabled
|
||||
? 'k8s.container.cpu_request'
|
||||
@@ -84,7 +84,7 @@ export const getDaemonSetMetricsQueryPayload = (
|
||||
{
|
||||
aggregateAttribute: {
|
||||
dataType: DataTypes.Float64,
|
||||
id: 'k8s_pod_cpu_utilization--float64--Gauge--true',
|
||||
id: 'k8s_pod_cpu_usage--float64--Gauge--true',
|
||||
isColumn: true,
|
||||
isJSON: false,
|
||||
key: k8sPodCpuUtilizationKey,
|
||||
|
||||
@@ -191,6 +191,32 @@ function K8sDaemonSetsList({
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [minTime, maxTime, orderBy, selectedRowData, groupBy]);
|
||||
|
||||
const groupedByRowDataQueryKey = useMemo(() => {
|
||||
if (selectedDaemonSetUID) {
|
||||
return [
|
||||
'daemonSetList',
|
||||
JSON.stringify(queryFilters),
|
||||
JSON.stringify(orderBy),
|
||||
JSON.stringify(selectedRowData),
|
||||
];
|
||||
}
|
||||
return [
|
||||
'daemonSetList',
|
||||
JSON.stringify(queryFilters),
|
||||
JSON.stringify(orderBy),
|
||||
JSON.stringify(selectedRowData),
|
||||
String(minTime),
|
||||
String(maxTime),
|
||||
];
|
||||
}, [
|
||||
queryFilters,
|
||||
orderBy,
|
||||
selectedDaemonSetUID,
|
||||
minTime,
|
||||
maxTime,
|
||||
selectedRowData,
|
||||
]);
|
||||
|
||||
const {
|
||||
data: groupedByRowData,
|
||||
isFetching: isFetchingGroupedByRowData,
|
||||
@@ -200,7 +226,7 @@ function K8sDaemonSetsList({
|
||||
} = useGetK8sDaemonSetsList(
|
||||
fetchGroupedByRowDataQuery as K8sDaemonSetsListPayload,
|
||||
{
|
||||
queryKey: ['daemonSetList', fetchGroupedByRowDataQuery],
|
||||
queryKey: groupedByRowDataQueryKey,
|
||||
enabled: !!fetchGroupedByRowDataQuery && !!selectedRowData,
|
||||
},
|
||||
undefined,
|
||||
@@ -251,11 +277,44 @@ function K8sDaemonSetsList({
|
||||
[groupedByRowData, groupBy],
|
||||
);
|
||||
|
||||
const queryKey = useMemo(() => {
|
||||
if (selectedDaemonSetUID) {
|
||||
return [
|
||||
'daemonSetList',
|
||||
String(pageSize),
|
||||
String(currentPage),
|
||||
JSON.stringify(queryFilters),
|
||||
JSON.stringify(orderBy),
|
||||
JSON.stringify(groupBy),
|
||||
];
|
||||
}
|
||||
return [
|
||||
'daemonSetList',
|
||||
String(pageSize),
|
||||
String(currentPage),
|
||||
JSON.stringify(queryFilters),
|
||||
JSON.stringify(orderBy),
|
||||
JSON.stringify(groupBy),
|
||||
String(minTime),
|
||||
String(maxTime),
|
||||
];
|
||||
}, [
|
||||
selectedDaemonSetUID,
|
||||
pageSize,
|
||||
currentPage,
|
||||
queryFilters,
|
||||
orderBy,
|
||||
groupBy,
|
||||
minTime,
|
||||
maxTime,
|
||||
]);
|
||||
|
||||
const { data, isFetching, isLoading, isError } = useGetK8sDaemonSetsList(
|
||||
query as K8sDaemonSetsListPayload,
|
||||
{
|
||||
queryKey: ['daemonSetList', query],
|
||||
queryKey,
|
||||
enabled: !!query,
|
||||
keepPreviousData: true,
|
||||
},
|
||||
undefined,
|
||||
dotMetricsEnabled,
|
||||
@@ -591,6 +650,9 @@ function K8sDaemonSetsList({
|
||||
});
|
||||
};
|
||||
|
||||
const showTableLoadingState =
|
||||
(isFetching || isLoading) && formattedDaemonSetsData.length === 0;
|
||||
|
||||
return (
|
||||
<div className="k8s-list">
|
||||
<K8sHeader
|
||||
@@ -603,6 +665,7 @@ function K8sDaemonSetsList({
|
||||
handleGroupByChange={handleGroupByChange}
|
||||
selectedGroupBy={groupBy}
|
||||
entity={K8sCategory.DAEMONSETS}
|
||||
showAutoRefresh={!selectedDaemonSetData}
|
||||
/>
|
||||
{isError && <Typography>{data?.error || 'Something went wrong'}</Typography>}
|
||||
|
||||
@@ -610,7 +673,7 @@ function K8sDaemonSetsList({
|
||||
className={classNames('k8s-list-table', 'daemonSets-list-table', {
|
||||
'expanded-daemonsets-list-table': isGroupedByAttribute,
|
||||
})}
|
||||
dataSource={isFetching || isLoading ? [] : formattedDaemonSetsData}
|
||||
dataSource={showTableLoadingState ? [] : formattedDaemonSetsData}
|
||||
columns={columns}
|
||||
pagination={{
|
||||
current: currentPage,
|
||||
@@ -622,26 +685,25 @@ function K8sDaemonSetsList({
|
||||
}}
|
||||
scroll={{ x: true }}
|
||||
loading={{
|
||||
spinning: isFetching || isLoading,
|
||||
spinning: showTableLoadingState,
|
||||
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"
|
||||
/>
|
||||
emptyText: showTableLoadingState ? 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>
|
||||
<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}
|
||||
|
||||
@@ -38,7 +38,7 @@ import {
|
||||
ScrollText,
|
||||
X,
|
||||
} from 'lucide-react';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useSearchParams } from 'react-router-dom-v5-compat';
|
||||
import { AppState } from 'store/reducers';
|
||||
@@ -88,8 +88,12 @@ function DeploymentDetails({
|
||||
endTime: endMs,
|
||||
}));
|
||||
|
||||
const lastSelectedInterval = useRef<Time | null>(null);
|
||||
|
||||
const [selectedInterval, setSelectedInterval] = useState<Time>(
|
||||
selectedTime as Time,
|
||||
lastSelectedInterval.current
|
||||
? lastSelectedInterval.current
|
||||
: (selectedTime as Time),
|
||||
);
|
||||
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
@@ -215,10 +219,11 @@ function DeploymentDetails({
|
||||
}, [initialFilters, initialEventsFilters]);
|
||||
|
||||
useEffect(() => {
|
||||
setSelectedInterval(selectedTime as Time);
|
||||
const currentSelectedInterval = lastSelectedInterval.current || selectedTime;
|
||||
setSelectedInterval(currentSelectedInterval as Time);
|
||||
|
||||
if (selectedTime !== 'custom') {
|
||||
const { maxTime, minTime } = GetMinMax(selectedTime);
|
||||
if (currentSelectedInterval !== 'custom') {
|
||||
const { maxTime, minTime } = GetMinMax(currentSelectedInterval);
|
||||
|
||||
setModalTimeRange({
|
||||
startTime: Math.floor(minTime / 1000000000),
|
||||
@@ -246,6 +251,7 @@ function DeploymentDetails({
|
||||
|
||||
const handleTimeChange = useCallback(
|
||||
(interval: Time | CustomTimeType, dateTimeRange?: [number, number]): void => {
|
||||
lastSelectedInterval.current = interval as Time;
|
||||
setSelectedInterval(interval as Time);
|
||||
|
||||
if (interval === 'custom' && dateTimeRange) {
|
||||
@@ -487,6 +493,7 @@ function DeploymentDetails({
|
||||
};
|
||||
|
||||
const handleClose = (): void => {
|
||||
lastSelectedInterval.current = null;
|
||||
setSelectedInterval(selectedTime as Time);
|
||||
|
||||
if (selectedTime !== 'custom') {
|
||||
|
||||
@@ -33,8 +33,8 @@ export const getDeploymentMetricsQueryPayload = (
|
||||
dotMetricsEnabled: boolean,
|
||||
): GetQueryResultsProps[] => {
|
||||
const k8sPodCpuUtilizationKey = dotMetricsEnabled
|
||||
? 'k8s.pod.cpu.utilization'
|
||||
: 'k8s_pod_cpu_utilization';
|
||||
? 'k8s.pod.cpu.usage'
|
||||
: 'k8s_pod_cpu_usage';
|
||||
|
||||
const k8sContainerCpuRequestKey = dotMetricsEnabled
|
||||
? 'k8s.container.cpu_request'
|
||||
@@ -80,7 +80,7 @@ export const getDeploymentMetricsQueryPayload = (
|
||||
{
|
||||
aggregateAttribute: {
|
||||
dataType: DataTypes.Float64,
|
||||
id: 'k8s_pod_cpu_utilization--float64--Gauge--true',
|
||||
id: 'k8s_pod_cpu_usage--float64--Gauge--true',
|
||||
isColumn: true,
|
||||
isJSON: false,
|
||||
key: k8sPodCpuUtilizationKey,
|
||||
|
||||
@@ -192,6 +192,32 @@ function K8sDeploymentsList({
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [minTime, maxTime, orderBy, selectedRowData, groupBy]);
|
||||
|
||||
const groupedByRowDataQueryKey = useMemo(() => {
|
||||
if (selectedDeploymentUID) {
|
||||
return [
|
||||
'deploymentList',
|
||||
JSON.stringify(queryFilters),
|
||||
JSON.stringify(orderBy),
|
||||
JSON.stringify(selectedRowData),
|
||||
];
|
||||
}
|
||||
return [
|
||||
'deploymentList',
|
||||
JSON.stringify(queryFilters),
|
||||
JSON.stringify(orderBy),
|
||||
JSON.stringify(selectedRowData),
|
||||
String(minTime),
|
||||
String(maxTime),
|
||||
];
|
||||
}, [
|
||||
queryFilters,
|
||||
orderBy,
|
||||
selectedDeploymentUID,
|
||||
minTime,
|
||||
maxTime,
|
||||
selectedRowData,
|
||||
]);
|
||||
|
||||
const {
|
||||
data: groupedByRowData,
|
||||
isFetching: isFetchingGroupedByRowData,
|
||||
@@ -201,7 +227,7 @@ function K8sDeploymentsList({
|
||||
} = useGetK8sDeploymentsList(
|
||||
fetchGroupedByRowDataQuery as K8sDeploymentsListPayload,
|
||||
{
|
||||
queryKey: ['deploymentList', fetchGroupedByRowDataQuery],
|
||||
queryKey: groupedByRowDataQueryKey,
|
||||
enabled: !!fetchGroupedByRowDataQuery && !!selectedRowData,
|
||||
},
|
||||
undefined,
|
||||
@@ -252,11 +278,44 @@ function K8sDeploymentsList({
|
||||
[groupedByRowData, groupBy],
|
||||
);
|
||||
|
||||
const queryKey = useMemo(() => {
|
||||
if (selectedDeploymentUID) {
|
||||
return [
|
||||
'deploymentList',
|
||||
String(pageSize),
|
||||
String(currentPage),
|
||||
JSON.stringify(queryFilters),
|
||||
JSON.stringify(orderBy),
|
||||
JSON.stringify(groupBy),
|
||||
];
|
||||
}
|
||||
return [
|
||||
'deploymentList',
|
||||
String(pageSize),
|
||||
String(currentPage),
|
||||
JSON.stringify(queryFilters),
|
||||
JSON.stringify(orderBy),
|
||||
JSON.stringify(groupBy),
|
||||
String(minTime),
|
||||
String(maxTime),
|
||||
];
|
||||
}, [
|
||||
selectedDeploymentUID,
|
||||
pageSize,
|
||||
currentPage,
|
||||
queryFilters,
|
||||
orderBy,
|
||||
groupBy,
|
||||
minTime,
|
||||
maxTime,
|
||||
]);
|
||||
|
||||
const { data, isFetching, isLoading, isError } = useGetK8sDeploymentsList(
|
||||
query as K8sDeploymentsListPayload,
|
||||
{
|
||||
queryKey: ['deploymentList', query],
|
||||
queryKey,
|
||||
enabled: !!query,
|
||||
keepPreviousData: true,
|
||||
},
|
||||
undefined,
|
||||
dotMetricsEnabled,
|
||||
@@ -596,6 +655,9 @@ function K8sDeploymentsList({
|
||||
});
|
||||
};
|
||||
|
||||
const showTableLoadingState =
|
||||
(isFetching || isLoading) && formattedDeploymentsData.length === 0;
|
||||
|
||||
return (
|
||||
<div className="k8s-list">
|
||||
<K8sHeader
|
||||
@@ -608,6 +670,7 @@ function K8sDeploymentsList({
|
||||
handleGroupByChange={handleGroupByChange}
|
||||
selectedGroupBy={groupBy}
|
||||
entity={K8sCategory.NODES}
|
||||
showAutoRefresh={!selectedDeploymentData}
|
||||
/>
|
||||
{isError && <Typography>{data?.error || 'Something went wrong'}</Typography>}
|
||||
|
||||
@@ -615,7 +678,7 @@ function K8sDeploymentsList({
|
||||
className={classNames('k8s-list-table', 'deployments-list-table', {
|
||||
'expanded-deployments-list-table': isGroupedByAttribute,
|
||||
})}
|
||||
dataSource={isFetching || isLoading ? [] : formattedDeploymentsData}
|
||||
dataSource={showTableLoadingState ? [] : formattedDeploymentsData}
|
||||
columns={columns}
|
||||
pagination={{
|
||||
current: currentPage,
|
||||
@@ -627,26 +690,25 @@ function K8sDeploymentsList({
|
||||
}}
|
||||
scroll={{ x: true }}
|
||||
loading={{
|
||||
spinning: isFetching || isLoading,
|
||||
spinning: showTableLoadingState,
|
||||
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"
|
||||
/>
|
||||
emptyText: showTableLoadingState ? 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>
|
||||
<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}
|
||||
|
||||
@@ -270,7 +270,7 @@ export default function Events({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isLoading && <LoadingContainer />}
|
||||
{isLoading && formattedEntityEvents.length === 0 && <LoadingContainer />}
|
||||
|
||||
{!isLoading && !isError && formattedEntityEvents.length === 0 && (
|
||||
<EntityDetailsEmptyContainer category={category} view="events" />
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
CustomTimeType,
|
||||
Time,
|
||||
} from 'container/TopNav/DateTimeSelectionV2/config';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import { useResizeObserver } from 'hooks/useDimensions';
|
||||
import {
|
||||
@@ -24,12 +25,13 @@ import {
|
||||
import { getUPlotChartOptions } from 'lib/uPlotLib/getUplotChartOptions';
|
||||
import { getUPlotChartData } from 'lib/uPlotLib/utils/getUplotChartData';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useQueries, UseQueryResult } from 'react-query';
|
||||
import { QueryFunctionContext, useQueries, UseQueryResult } from 'react-query';
|
||||
import { SuccessResponse } from 'types/api';
|
||||
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
|
||||
import { Options } from 'uplot';
|
||||
|
||||
import { FeatureKeys } from '../../../../constants/features';
|
||||
import { useMultiIntersectionObserver } from '../../../../hooks/useMultiIntersectionObserver';
|
||||
import { useAppContext } from '../../../../providers/App/App';
|
||||
|
||||
interface EntityMetricsProps<T> {
|
||||
@@ -73,6 +75,12 @@ function EntityMetrics<T>({
|
||||
const dotMetricsEnabled =
|
||||
featureFlags?.find((flag) => flag.name === FeatureKeys.DOT_METRICS_ENABLED)
|
||||
?.active || false;
|
||||
|
||||
const {
|
||||
visibilities,
|
||||
setElement,
|
||||
} = useMultiIntersectionObserver(entityWidgetInfo.length, { threshold: 0.1 });
|
||||
|
||||
const queryPayloads = useMemo(
|
||||
() =>
|
||||
getEntityQueryPayload(
|
||||
@@ -91,17 +99,22 @@ function EntityMetrics<T>({
|
||||
);
|
||||
|
||||
const queries = useQueries(
|
||||
queryPayloads.map((payload) => ({
|
||||
queryPayloads.map((payload, index) => ({
|
||||
queryKey: [queryKey, payload, ENTITY_VERSION_V4, category],
|
||||
queryFn: (): Promise<SuccessResponse<MetricRangePayloadProps>> =>
|
||||
GetMetricQueryRange(payload, ENTITY_VERSION_V4),
|
||||
enabled: !!payload,
|
||||
queryFn: ({
|
||||
signal,
|
||||
}: QueryFunctionContext): Promise<
|
||||
SuccessResponse<MetricRangePayloadProps>
|
||||
> => GetMetricQueryRange(payload, ENTITY_VERSION_V4, signal),
|
||||
enabled: !!payload && visibilities[index],
|
||||
keepPreviousData: true,
|
||||
})),
|
||||
);
|
||||
|
||||
const isDarkMode = useIsDarkMode();
|
||||
const graphRef = useRef<HTMLDivElement>(null);
|
||||
const dimensions = useResizeObserver(graphRef);
|
||||
const { currentQuery } = useQueryBuilder();
|
||||
|
||||
const chartData = useMemo(
|
||||
() =>
|
||||
@@ -170,6 +183,7 @@ function EntityMetrics<T>({
|
||||
minTimeScale: graphTimeIntervals[idx].start,
|
||||
maxTimeScale: graphTimeIntervals[idx].end,
|
||||
onDragSelect: (start, end) => onDragSelect(start, end, idx),
|
||||
query: currentQuery,
|
||||
});
|
||||
}),
|
||||
[
|
||||
@@ -179,6 +193,7 @@ function EntityMetrics<T>({
|
||||
entityWidgetInfo,
|
||||
graphTimeIntervals,
|
||||
onDragSelect,
|
||||
currentQuery,
|
||||
],
|
||||
);
|
||||
|
||||
@@ -186,7 +201,7 @@ function EntityMetrics<T>({
|
||||
query: UseQueryResult<SuccessResponse<MetricRangePayloadProps>, unknown>,
|
||||
idx: number,
|
||||
): JSX.Element => {
|
||||
if (query.isLoading) {
|
||||
if ((!query.data && query.isLoading) || !visibilities[idx]) {
|
||||
return <Skeleton />;
|
||||
}
|
||||
|
||||
@@ -196,7 +211,7 @@ function EntityMetrics<T>({
|
||||
return <div>{errorMessage}</div>;
|
||||
}
|
||||
|
||||
const { panelType } = (query.data?.params as any).compositeQuery;
|
||||
const panelType = (query.data?.params as any)?.compositeQuery?.panelType;
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -234,7 +249,7 @@ function EntityMetrics<T>({
|
||||
</div>
|
||||
<Row gutter={24} className="entity-metrics-container">
|
||||
{queries.map((query, idx) => (
|
||||
<Col span={12} key={entityWidgetInfo[idx].title}>
|
||||
<Col ref={setElement(idx)} span={12} key={entityWidgetInfo[idx].title}>
|
||||
<Typography.Text>{entityWidgetInfo[idx].title}</Typography.Text>
|
||||
<Card bordered className="entity-metrics-card" ref={graphRef}>
|
||||
{renderCardContent(query, idx)}
|
||||
|
||||
@@ -203,7 +203,7 @@ function EntityTraces({
|
||||
{!isError && traces.length > 0 && (
|
||||
<div className="entity-traces-table">
|
||||
<TraceExplorerControls
|
||||
isLoading={isFetching}
|
||||
isLoading={isFetching && traces.length === 0}
|
||||
totalCount={totalCount}
|
||||
perPageOptions={PER_PAGE_OPTIONS}
|
||||
showSizeChanger={false}
|
||||
@@ -212,7 +212,7 @@ function EntityTraces({
|
||||
tableLayout="fixed"
|
||||
pagination={false}
|
||||
scroll={{ x: true }}
|
||||
loading={isFetching}
|
||||
loading={isFetching && traces.length === 0}
|
||||
dataSource={traces}
|
||||
columns={traceListColumns}
|
||||
onRow={(): Record<string, unknown> => ({
|
||||
|
||||
@@ -33,7 +33,7 @@ import {
|
||||
ScrollText,
|
||||
X,
|
||||
} from 'lucide-react';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useSearchParams } from 'react-router-dom-v5-compat';
|
||||
import { AppState } from 'store/reducers';
|
||||
@@ -81,8 +81,12 @@ function JobDetails({
|
||||
endTime: endMs,
|
||||
}));
|
||||
|
||||
const lastSelectedInterval = useRef<Time | null>(null);
|
||||
|
||||
const [selectedInterval, setSelectedInterval] = useState<Time>(
|
||||
selectedTime as Time,
|
||||
lastSelectedInterval.current
|
||||
? lastSelectedInterval.current
|
||||
: (selectedTime as Time),
|
||||
);
|
||||
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
@@ -204,10 +208,11 @@ function JobDetails({
|
||||
}, [initialFilters, initialEventsFilters]);
|
||||
|
||||
useEffect(() => {
|
||||
setSelectedInterval(selectedTime as Time);
|
||||
const currentSelectedInterval = lastSelectedInterval.current || selectedTime;
|
||||
setSelectedInterval(currentSelectedInterval as Time);
|
||||
|
||||
if (selectedTime !== 'custom') {
|
||||
const { maxTime, minTime } = GetMinMax(selectedTime);
|
||||
if (currentSelectedInterval !== 'custom') {
|
||||
const { maxTime, minTime } = GetMinMax(currentSelectedInterval);
|
||||
|
||||
setModalTimeRange({
|
||||
startTime: Math.floor(minTime / 1000000000),
|
||||
@@ -235,6 +240,7 @@ function JobDetails({
|
||||
|
||||
const handleTimeChange = useCallback(
|
||||
(interval: Time | CustomTimeType, dateTimeRange?: [number, number]): void => {
|
||||
lastSelectedInterval.current = interval as Time;
|
||||
setSelectedInterval(interval as Time);
|
||||
|
||||
if (interval === 'custom' && dateTimeRange) {
|
||||
@@ -469,6 +475,7 @@ function JobDetails({
|
||||
};
|
||||
|
||||
const handleClose = (): void => {
|
||||
lastSelectedInterval.current = null;
|
||||
setSelectedInterval(selectedTime as Time);
|
||||
|
||||
if (selectedTime !== 'custom') {
|
||||
|
||||
@@ -33,8 +33,8 @@ export const getJobMetricsQueryPayload = (
|
||||
dotMetricsEnabled: boolean,
|
||||
): GetQueryResultsProps[] => {
|
||||
const k8sPodCpuUtilizationKey = dotMetricsEnabled
|
||||
? 'k8s.pod.cpu.utilization'
|
||||
: 'k8s_pod_cpu_utilization';
|
||||
? 'k8s.pod.cpu.usage'
|
||||
: 'k8s_pod_cpu_usage';
|
||||
const k8sPodMemoryUsageKey = dotMetricsEnabled
|
||||
? 'k8s.pod.memory.usage'
|
||||
: 'k8s_pod_memory_usage';
|
||||
@@ -59,7 +59,7 @@ export const getJobMetricsQueryPayload = (
|
||||
{
|
||||
aggregateAttribute: {
|
||||
dataType: DataTypes.Float64,
|
||||
id: 'k8s_pod_cpu_utilization--float64--Gauge--true',
|
||||
id: 'k8s_pod_cpu_usage--float64--Gauge--true',
|
||||
isColumn: true,
|
||||
isJSON: false,
|
||||
key: k8sPodCpuUtilizationKey,
|
||||
|
||||
@@ -186,6 +186,25 @@ function K8sJobsList({
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [minTime, maxTime, orderBy, selectedRowData, groupBy]);
|
||||
|
||||
const groupedByRowDataQueryKey = useMemo(() => {
|
||||
if (selectedJobUID) {
|
||||
return [
|
||||
'jobList',
|
||||
JSON.stringify(queryFilters),
|
||||
JSON.stringify(orderBy),
|
||||
JSON.stringify(selectedRowData),
|
||||
];
|
||||
}
|
||||
return [
|
||||
'jobList',
|
||||
JSON.stringify(queryFilters),
|
||||
JSON.stringify(orderBy),
|
||||
JSON.stringify(selectedRowData),
|
||||
String(minTime),
|
||||
String(maxTime),
|
||||
];
|
||||
}, [queryFilters, orderBy, selectedJobUID, minTime, maxTime, selectedRowData]);
|
||||
|
||||
const {
|
||||
data: groupedByRowData,
|
||||
isFetching: isFetchingGroupedByRowData,
|
||||
@@ -195,7 +214,7 @@ function K8sJobsList({
|
||||
} = useGetK8sJobsList(
|
||||
fetchGroupedByRowDataQuery as K8sJobsListPayload,
|
||||
{
|
||||
queryKey: ['jobList', fetchGroupedByRowDataQuery],
|
||||
queryKey: groupedByRowDataQueryKey,
|
||||
enabled: !!fetchGroupedByRowDataQuery && !!selectedRowData,
|
||||
},
|
||||
undefined,
|
||||
@@ -251,11 +270,44 @@ function K8sJobsList({
|
||||
return groupedByRowData?.payload?.data?.records || [];
|
||||
}, [groupedByRowData, selectedRowData]);
|
||||
|
||||
const queryKey = useMemo(() => {
|
||||
if (selectedJobUID) {
|
||||
return [
|
||||
'jobList',
|
||||
String(pageSize),
|
||||
String(currentPage),
|
||||
JSON.stringify(queryFilters),
|
||||
JSON.stringify(orderBy),
|
||||
JSON.stringify(groupBy),
|
||||
];
|
||||
}
|
||||
return [
|
||||
'jobList',
|
||||
String(pageSize),
|
||||
String(currentPage),
|
||||
JSON.stringify(queryFilters),
|
||||
JSON.stringify(orderBy),
|
||||
JSON.stringify(groupBy),
|
||||
String(minTime),
|
||||
String(maxTime),
|
||||
];
|
||||
}, [
|
||||
selectedJobUID,
|
||||
pageSize,
|
||||
currentPage,
|
||||
queryFilters,
|
||||
orderBy,
|
||||
groupBy,
|
||||
minTime,
|
||||
maxTime,
|
||||
]);
|
||||
|
||||
const { data, isFetching, isLoading, isError } = useGetK8sJobsList(
|
||||
query as K8sJobsListPayload,
|
||||
{
|
||||
queryKey: ['jobList', query],
|
||||
queryKey,
|
||||
enabled: !!query,
|
||||
keepPreviousData: true,
|
||||
},
|
||||
undefined,
|
||||
dotMetricsEnabled,
|
||||
@@ -581,6 +633,7 @@ function K8sJobsList({
|
||||
handleGroupByChange={handleGroupByChange}
|
||||
selectedGroupBy={groupBy}
|
||||
entity={K8sCategory.JOBS}
|
||||
showAutoRefresh={!selectedJobData}
|
||||
/>
|
||||
{isError && <Typography>{data?.error || 'Something went wrong'}</Typography>}
|
||||
|
||||
|
||||
@@ -30,6 +30,7 @@ interface K8sHeaderProps {
|
||||
handleFilterVisibilityChange: () => void;
|
||||
isFiltersVisible: boolean;
|
||||
entity: K8sCategory;
|
||||
showAutoRefresh: boolean;
|
||||
}
|
||||
|
||||
function K8sHeader({
|
||||
@@ -46,6 +47,7 @@ function K8sHeader({
|
||||
handleFilterVisibilityChange,
|
||||
isFiltersVisible,
|
||||
entity,
|
||||
showAutoRefresh,
|
||||
}: K8sHeaderProps): JSX.Element {
|
||||
const [isFiltersSidePanelOpen, setIsFiltersSidePanelOpen] = useState(false);
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
@@ -136,7 +138,7 @@ function K8sHeader({
|
||||
|
||||
<div className="k8s-list-controls-right">
|
||||
<DateTimeSelectionV2
|
||||
showAutoRefresh
|
||||
showAutoRefresh={showAutoRefresh}
|
||||
showRefreshText={false}
|
||||
hideShareModal
|
||||
/>
|
||||
|
||||
@@ -190,6 +190,32 @@ function K8sNamespacesList({
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [minTime, maxTime, orderBy, selectedRowData, groupBy]);
|
||||
|
||||
const groupedByRowDataQueryKey = useMemo(() => {
|
||||
if (selectedNamespaceUID) {
|
||||
return [
|
||||
'namespaceList',
|
||||
JSON.stringify(queryFilters),
|
||||
JSON.stringify(orderBy),
|
||||
JSON.stringify(selectedRowData),
|
||||
];
|
||||
}
|
||||
return [
|
||||
'namespaceList',
|
||||
JSON.stringify(queryFilters),
|
||||
JSON.stringify(orderBy),
|
||||
JSON.stringify(selectedRowData),
|
||||
String(minTime),
|
||||
String(maxTime),
|
||||
];
|
||||
}, [
|
||||
queryFilters,
|
||||
orderBy,
|
||||
selectedNamespaceUID,
|
||||
minTime,
|
||||
maxTime,
|
||||
selectedRowData,
|
||||
]);
|
||||
|
||||
const {
|
||||
data: groupedByRowData,
|
||||
isFetching: isFetchingGroupedByRowData,
|
||||
@@ -199,7 +225,7 @@ function K8sNamespacesList({
|
||||
} = useGetK8sNamespacesList(
|
||||
fetchGroupedByRowDataQuery as K8sNamespacesListPayload,
|
||||
{
|
||||
queryKey: ['namespaceList', fetchGroupedByRowDataQuery],
|
||||
queryKey: groupedByRowDataQueryKey,
|
||||
enabled: !!fetchGroupedByRowDataQuery && !!selectedRowData,
|
||||
},
|
||||
undefined,
|
||||
@@ -250,11 +276,44 @@ function K8sNamespacesList({
|
||||
[groupedByRowData, groupBy],
|
||||
);
|
||||
|
||||
const queryKey = useMemo(() => {
|
||||
if (selectedNamespaceUID) {
|
||||
return [
|
||||
'namespaceList',
|
||||
String(pageSize),
|
||||
String(currentPage),
|
||||
JSON.stringify(queryFilters),
|
||||
JSON.stringify(orderBy),
|
||||
JSON.stringify(groupBy),
|
||||
];
|
||||
}
|
||||
return [
|
||||
'namespaceList',
|
||||
String(pageSize),
|
||||
String(currentPage),
|
||||
JSON.stringify(queryFilters),
|
||||
JSON.stringify(orderBy),
|
||||
JSON.stringify(groupBy),
|
||||
String(minTime),
|
||||
String(maxTime),
|
||||
];
|
||||
}, [
|
||||
selectedNamespaceUID,
|
||||
pageSize,
|
||||
currentPage,
|
||||
queryFilters,
|
||||
orderBy,
|
||||
groupBy,
|
||||
minTime,
|
||||
maxTime,
|
||||
]);
|
||||
|
||||
const { data, isFetching, isLoading, isError } = useGetK8sNamespacesList(
|
||||
query as K8sNamespacesListPayload,
|
||||
{
|
||||
queryKey: ['namespaceList', query],
|
||||
queryKey,
|
||||
enabled: !!query,
|
||||
keepPreviousData: true,
|
||||
},
|
||||
undefined,
|
||||
dotMetricsEnabled,
|
||||
@@ -592,6 +651,9 @@ function K8sNamespacesList({
|
||||
});
|
||||
};
|
||||
|
||||
const showTableLoadingState =
|
||||
(isFetching || isLoading) && formattedNamespacesData.length === 0;
|
||||
|
||||
return (
|
||||
<div className="k8s-list">
|
||||
<K8sHeader
|
||||
@@ -604,12 +666,13 @@ function K8sNamespacesList({
|
||||
handleGroupByChange={handleGroupByChange}
|
||||
selectedGroupBy={groupBy}
|
||||
entity={K8sCategory.NODES}
|
||||
showAutoRefresh={!selectedNamespaceData}
|
||||
/>
|
||||
{isError && <Typography>{data?.error || 'Something went wrong'}</Typography>}
|
||||
|
||||
<Table
|
||||
className="k8s-list-table namespaces-list-table"
|
||||
dataSource={isFetching || isLoading ? [] : formattedNamespacesData}
|
||||
dataSource={showTableLoadingState ? [] : formattedNamespacesData}
|
||||
columns={columns}
|
||||
pagination={{
|
||||
current: currentPage,
|
||||
@@ -621,26 +684,25 @@ function K8sNamespacesList({
|
||||
}}
|
||||
scroll={{ x: true }}
|
||||
loading={{
|
||||
spinning: isFetching || isLoading,
|
||||
spinning: showTableLoadingState,
|
||||
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"
|
||||
/>
|
||||
emptyText: showTableLoadingState ? 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>
|
||||
<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}
|
||||
|
||||
@@ -35,7 +35,7 @@ import {
|
||||
ScrollText,
|
||||
X,
|
||||
} from 'lucide-react';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useSearchParams } from 'react-router-dom-v5-compat';
|
||||
import { AppState } from 'store/reducers';
|
||||
@@ -85,8 +85,12 @@ function NamespaceDetails({
|
||||
endTime: endMs,
|
||||
}));
|
||||
|
||||
const lastSelectedInterval = useRef<Time | null>(null);
|
||||
|
||||
const [selectedInterval, setSelectedInterval] = useState<Time>(
|
||||
selectedTime as Time,
|
||||
lastSelectedInterval.current
|
||||
? lastSelectedInterval.current
|
||||
: (selectedTime as Time),
|
||||
);
|
||||
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
@@ -195,10 +199,11 @@ function NamespaceDetails({
|
||||
}, [initialFilters, initialEventsFilters]);
|
||||
|
||||
useEffect(() => {
|
||||
setSelectedInterval(selectedTime as Time);
|
||||
const currentSelectedInterval = lastSelectedInterval.current || selectedTime;
|
||||
setSelectedInterval(currentSelectedInterval as Time);
|
||||
|
||||
if (selectedTime !== 'custom') {
|
||||
const { maxTime, minTime } = GetMinMax(selectedTime);
|
||||
if (currentSelectedInterval !== 'custom') {
|
||||
const { maxTime, minTime } = GetMinMax(currentSelectedInterval);
|
||||
|
||||
setModalTimeRange({
|
||||
startTime: Math.floor(minTime / 1000000000),
|
||||
@@ -226,6 +231,7 @@ function NamespaceDetails({
|
||||
|
||||
const handleTimeChange = useCallback(
|
||||
(interval: Time | CustomTimeType, dateTimeRange?: [number, number]): void => {
|
||||
lastSelectedInterval.current = interval as Time;
|
||||
setSelectedInterval(interval as Time);
|
||||
|
||||
if (interval === 'custom' && dateTimeRange) {
|
||||
@@ -461,6 +467,7 @@ function NamespaceDetails({
|
||||
};
|
||||
|
||||
const handleClose = (): void => {
|
||||
lastSelectedInterval.current = null;
|
||||
setSelectedInterval(selectedTime as Time);
|
||||
|
||||
if (selectedTime !== 'custom') {
|
||||
|
||||
@@ -59,8 +59,8 @@ export const getNamespaceMetricsQueryPayload = (
|
||||
const getKey = (dotKey: string, underscoreKey: string): string =>
|
||||
dotMetricsEnabled ? dotKey : underscoreKey;
|
||||
const k8sPodCpuUtilizationKey = getKey(
|
||||
'k8s.pod.cpu.utilization',
|
||||
'k8s_pod_cpu_utilization',
|
||||
'k8s.pod.cpu.usage',
|
||||
'k8s_pod_cpu_usage',
|
||||
);
|
||||
const k8sContainerCpuRequestKey = getKey(
|
||||
'k8s.container.cpu_request',
|
||||
|
||||
@@ -184,6 +184,32 @@ function K8sNodesList({
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [minTime, maxTime, orderBy, selectedRowData, groupBy]);
|
||||
|
||||
const groupedByRowDataQueryKey = useMemo(() => {
|
||||
if (selectedNodeUID) {
|
||||
return [
|
||||
'nodeList',
|
||||
JSON.stringify(queryFilters),
|
||||
JSON.stringify(orderBy),
|
||||
JSON.stringify(selectedRowData),
|
||||
];
|
||||
}
|
||||
return [
|
||||
'nodeList',
|
||||
JSON.stringify(queryFilters),
|
||||
JSON.stringify(orderBy),
|
||||
JSON.stringify(selectedRowData),
|
||||
String(minTime),
|
||||
String(maxTime),
|
||||
];
|
||||
}, [
|
||||
queryFilters,
|
||||
orderBy,
|
||||
selectedNodeUID,
|
||||
minTime,
|
||||
maxTime,
|
||||
selectedRowData,
|
||||
]);
|
||||
|
||||
const {
|
||||
data: groupedByRowData,
|
||||
isFetching: isFetchingGroupedByRowData,
|
||||
@@ -193,7 +219,7 @@ function K8sNodesList({
|
||||
} = useGetK8sNodesList(
|
||||
fetchGroupedByRowDataQuery as K8sNodesListPayload,
|
||||
{
|
||||
queryKey: ['nodeList', fetchGroupedByRowDataQuery],
|
||||
queryKey: groupedByRowDataQueryKey,
|
||||
enabled: !!fetchGroupedByRowDataQuery && !!selectedRowData,
|
||||
},
|
||||
undefined,
|
||||
@@ -249,11 +275,44 @@ function K8sNodesList({
|
||||
[groupedByRowData, groupBy],
|
||||
);
|
||||
|
||||
const queryKey = useMemo(() => {
|
||||
if (selectedNodeUID) {
|
||||
return [
|
||||
'nodeList',
|
||||
String(pageSize),
|
||||
String(currentPage),
|
||||
JSON.stringify(queryFilters),
|
||||
JSON.stringify(orderBy),
|
||||
JSON.stringify(groupBy),
|
||||
];
|
||||
}
|
||||
return [
|
||||
'nodeList',
|
||||
String(pageSize),
|
||||
String(currentPage),
|
||||
JSON.stringify(queryFilters),
|
||||
JSON.stringify(orderBy),
|
||||
JSON.stringify(groupBy),
|
||||
String(minTime),
|
||||
String(maxTime),
|
||||
];
|
||||
}, [
|
||||
selectedNodeUID,
|
||||
pageSize,
|
||||
currentPage,
|
||||
queryFilters,
|
||||
orderBy,
|
||||
groupBy,
|
||||
minTime,
|
||||
maxTime,
|
||||
]);
|
||||
|
||||
const { data, isFetching, isLoading, isError } = useGetK8sNodesList(
|
||||
query as K8sNodesListPayload,
|
||||
{
|
||||
queryKey: ['nodeList', query],
|
||||
queryKey,
|
||||
enabled: !!query,
|
||||
keepPreviousData: true,
|
||||
},
|
||||
undefined,
|
||||
dotMetricsEnabled,
|
||||
@@ -571,6 +630,9 @@ function K8sNodesList({
|
||||
});
|
||||
};
|
||||
|
||||
const showTableLoadingState =
|
||||
(isFetching || isLoading) && formattedNodesData.length === 0;
|
||||
|
||||
return (
|
||||
<div className="k8s-list">
|
||||
<K8sHeader
|
||||
@@ -583,12 +645,13 @@ function K8sNodesList({
|
||||
handleGroupByChange={handleGroupByChange}
|
||||
selectedGroupBy={groupBy}
|
||||
entity={K8sCategory.NODES}
|
||||
showAutoRefresh={!selectedNodeData}
|
||||
/>
|
||||
{isError && <Typography>{data?.error || 'Something went wrong'}</Typography>}
|
||||
|
||||
<Table
|
||||
className="k8s-list-table nodes-list-table"
|
||||
dataSource={isFetching || isLoading ? [] : formattedNodesData}
|
||||
dataSource={showTableLoadingState ? [] : formattedNodesData}
|
||||
columns={columns}
|
||||
pagination={{
|
||||
current: currentPage,
|
||||
@@ -600,26 +663,25 @@ function K8sNodesList({
|
||||
}}
|
||||
scroll={{ x: true }}
|
||||
loading={{
|
||||
spinning: isFetching || isLoading,
|
||||
spinning: showTableLoadingState,
|
||||
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"
|
||||
/>
|
||||
emptyText: showTableLoadingState ? 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>
|
||||
<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}
|
||||
|
||||
@@ -38,7 +38,7 @@ import {
|
||||
ScrollText,
|
||||
X,
|
||||
} from 'lucide-react';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useSearchParams } from 'react-router-dom-v5-compat';
|
||||
import { AppState } from 'store/reducers';
|
||||
@@ -85,8 +85,12 @@ function NodeDetails({
|
||||
endTime: endMs,
|
||||
}));
|
||||
|
||||
const lastSelectedInterval = useRef<Time | null>(null);
|
||||
|
||||
const [selectedInterval, setSelectedInterval] = useState<Time>(
|
||||
selectedTime as Time,
|
||||
lastSelectedInterval.current
|
||||
? lastSelectedInterval.current
|
||||
: (selectedTime as Time),
|
||||
);
|
||||
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
@@ -195,10 +199,11 @@ function NodeDetails({
|
||||
}, [initialFilters, initialEventsFilters]);
|
||||
|
||||
useEffect(() => {
|
||||
setSelectedInterval(selectedTime as Time);
|
||||
const currentSelectedInterval = lastSelectedInterval.current || selectedTime;
|
||||
setSelectedInterval(currentSelectedInterval as Time);
|
||||
|
||||
if (selectedTime !== 'custom') {
|
||||
const { maxTime, minTime } = GetMinMax(selectedTime);
|
||||
if (currentSelectedInterval !== 'custom') {
|
||||
const { maxTime, minTime } = GetMinMax(currentSelectedInterval);
|
||||
|
||||
setModalTimeRange({
|
||||
startTime: Math.floor(minTime / 1000000000),
|
||||
@@ -226,6 +231,7 @@ function NodeDetails({
|
||||
|
||||
const handleTimeChange = useCallback(
|
||||
(interval: Time | CustomTimeType, dateTimeRange?: [number, number]): void => {
|
||||
lastSelectedInterval.current = interval as Time;
|
||||
setSelectedInterval(interval as Time);
|
||||
|
||||
if (interval === 'custom' && dateTimeRange) {
|
||||
@@ -464,6 +470,7 @@ function NodeDetails({
|
||||
};
|
||||
|
||||
const handleClose = (): void => {
|
||||
lastSelectedInterval.current = null;
|
||||
setSelectedInterval(selectedTime as Time);
|
||||
|
||||
if (selectedTime !== 'custom') {
|
||||
|
||||
@@ -59,8 +59,8 @@ export const getNodeMetricsQueryPayload = (
|
||||
const getKey = (dotKey: string, underscoreKey: string): string =>
|
||||
dotMetricsEnabled ? dotKey : underscoreKey;
|
||||
const k8sNodeCpuUtilizationKey = getKey(
|
||||
'k8s.node.cpu.utilization',
|
||||
'k8s_node_cpu_utilization',
|
||||
'k8s.node.cpu.usage',
|
||||
'k8s_node_cpu_usage',
|
||||
);
|
||||
|
||||
const k8sNodeAllocatableCpuKey = getKey(
|
||||
@@ -99,8 +99,8 @@ export const getNodeMetricsQueryPayload = (
|
||||
);
|
||||
|
||||
const k8sPodCpuUtilizationKey = getKey(
|
||||
'k8s.pod.cpu.utilization',
|
||||
'k8s_pod_cpu_utilization',
|
||||
'k8s.pod.cpu.usage',
|
||||
'k8s_pod_cpu_usage',
|
||||
);
|
||||
|
||||
const k8sPodMemoryUsageKey = getKey(
|
||||
@@ -147,7 +147,7 @@ export const getNodeMetricsQueryPayload = (
|
||||
{
|
||||
aggregateAttribute: {
|
||||
dataType: DataTypes.Float64,
|
||||
id: 'k8s_node_cpu_utilization--float64--Gauge--true',
|
||||
id: 'k8s_node_cpu_usage--float64--Gauge--true',
|
||||
isColumn: true,
|
||||
isJSON: false,
|
||||
key: k8sNodeCpuUtilizationKey,
|
||||
@@ -276,7 +276,7 @@ export const getNodeMetricsQueryPayload = (
|
||||
{
|
||||
aggregateAttribute: {
|
||||
dataType: DataTypes.Float64,
|
||||
id: 'k8s_node_cpu_utilization--float64--Gauge--true',
|
||||
id: 'k8s_node_cpu_usage--float64--Gauge--true',
|
||||
isColumn: true,
|
||||
isJSON: false,
|
||||
key: k8sNodeCpuUtilizationKey,
|
||||
@@ -319,7 +319,7 @@ export const getNodeMetricsQueryPayload = (
|
||||
{
|
||||
aggregateAttribute: {
|
||||
dataType: DataTypes.Float64,
|
||||
id: 'k8s_node_cpu_utilization--float64--Gauge--true',
|
||||
id: 'k8s_node_cpu_usage--float64--Gauge--true',
|
||||
isColumn: true,
|
||||
isJSON: false,
|
||||
key: k8sNodeCpuUtilizationKey,
|
||||
@@ -729,7 +729,7 @@ export const getNodeMetricsQueryPayload = (
|
||||
{
|
||||
aggregateAttribute: {
|
||||
dataType: DataTypes.Float64,
|
||||
id: 'k8s_node_cpu_utilization--float64--Gauge--true',
|
||||
id: 'k8s_node_cpu_usage--float64--Gauge--true',
|
||||
isColumn: true,
|
||||
isJSON: false,
|
||||
key: k8sNodeCpuUtilizationKey,
|
||||
@@ -1079,7 +1079,7 @@ export const getNodeMetricsQueryPayload = (
|
||||
{
|
||||
aggregateAttribute: {
|
||||
dataType: DataTypes.Float64,
|
||||
id: 'k8s_pod_cpu_utilization--float64--Gauge--true',
|
||||
id: 'k8s_pod_cpu_usage--float64--Gauge--true',
|
||||
isColumn: true,
|
||||
isJSON: false,
|
||||
key: k8sPodCpuUtilizationKey,
|
||||
|
||||
@@ -205,11 +205,44 @@ function K8sPodsList({
|
||||
return queryPayload;
|
||||
}, [pageSize, currentPage, queryFilters, minTime, maxTime, orderBy, groupBy]);
|
||||
|
||||
const queryKey = useMemo(() => {
|
||||
if (selectedPodUID) {
|
||||
return [
|
||||
'podList',
|
||||
String(pageSize),
|
||||
String(currentPage),
|
||||
JSON.stringify(queryFilters),
|
||||
JSON.stringify(orderBy),
|
||||
JSON.stringify(groupBy),
|
||||
];
|
||||
}
|
||||
return [
|
||||
'podList',
|
||||
String(pageSize),
|
||||
String(currentPage),
|
||||
JSON.stringify(queryFilters),
|
||||
JSON.stringify(orderBy),
|
||||
JSON.stringify(groupBy),
|
||||
String(minTime),
|
||||
String(maxTime),
|
||||
];
|
||||
}, [
|
||||
selectedPodUID,
|
||||
pageSize,
|
||||
currentPage,
|
||||
queryFilters,
|
||||
orderBy,
|
||||
groupBy,
|
||||
minTime,
|
||||
maxTime,
|
||||
]);
|
||||
|
||||
const { data, isFetching, isLoading, isError } = useGetK8sPodsList(
|
||||
query as K8sPodsListPayload,
|
||||
{
|
||||
queryKey: ['hostList', query],
|
||||
queryKey,
|
||||
enabled: !!query,
|
||||
keepPreviousData: true,
|
||||
},
|
||||
undefined,
|
||||
dotMetricsEnabled,
|
||||
@@ -261,6 +294,25 @@ function K8sPodsList({
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [minTime, maxTime, orderBy, selectedRowData]);
|
||||
|
||||
const groupedByRowDataQueryKey = useMemo(() => {
|
||||
if (selectedPodUID) {
|
||||
return [
|
||||
'podList',
|
||||
JSON.stringify(queryFilters),
|
||||
JSON.stringify(orderBy),
|
||||
JSON.stringify(selectedRowData),
|
||||
];
|
||||
}
|
||||
return [
|
||||
'podList',
|
||||
JSON.stringify(queryFilters),
|
||||
JSON.stringify(orderBy),
|
||||
JSON.stringify(selectedRowData),
|
||||
String(minTime),
|
||||
String(maxTime),
|
||||
];
|
||||
}, [queryFilters, orderBy, selectedPodUID, minTime, maxTime, selectedRowData]);
|
||||
|
||||
const {
|
||||
data: groupedByRowData,
|
||||
isFetching: isFetchingGroupedByRowData,
|
||||
@@ -270,7 +322,7 @@ function K8sPodsList({
|
||||
} = useGetK8sPodsList(
|
||||
fetchGroupedByRowDataQuery as K8sPodsListPayload,
|
||||
{
|
||||
queryKey: ['hostList', fetchGroupedByRowDataQuery],
|
||||
queryKey: groupedByRowDataQueryKey,
|
||||
enabled: !!fetchGroupedByRowDataQuery && !!selectedRowData,
|
||||
},
|
||||
undefined,
|
||||
@@ -629,6 +681,9 @@ function K8sPodsList({
|
||||
});
|
||||
};
|
||||
|
||||
const showTableLoadingState =
|
||||
(isFetching || isLoading) && formattedPodsData.length === 0;
|
||||
|
||||
return (
|
||||
<div className="k8s-list">
|
||||
<K8sHeader
|
||||
@@ -645,6 +700,7 @@ function K8sPodsList({
|
||||
onAddColumn={handleAddColumn}
|
||||
onRemoveColumn={handleRemoveColumn}
|
||||
entity={K8sCategory.PODS}
|
||||
showAutoRefresh={!selectedPodData}
|
||||
/>
|
||||
{isError && <Typography>{data?.error || 'Something went wrong'}</Typography>}
|
||||
|
||||
@@ -652,7 +708,7 @@ function K8sPodsList({
|
||||
className={classNames('k8s-list-table', {
|
||||
'expanded-k8s-list-table': isGroupedByAttribute,
|
||||
})}
|
||||
dataSource={isFetching || isLoading ? [] : formattedPodsData}
|
||||
dataSource={showTableLoadingState ? [] : formattedPodsData}
|
||||
columns={columns}
|
||||
pagination={{
|
||||
current: currentPage,
|
||||
@@ -663,26 +719,25 @@ function K8sPodsList({
|
||||
onChange: onPaginationChange,
|
||||
}}
|
||||
loading={{
|
||||
spinning: isFetching || isLoading,
|
||||
spinning: showTableLoadingState,
|
||||
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"
|
||||
/>
|
||||
emptyText: showTableLoadingState ? 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>
|
||||
<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"
|
||||
|
||||
@@ -39,7 +39,7 @@ import {
|
||||
ScrollText,
|
||||
X,
|
||||
} from 'lucide-react';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useSearchParams } from 'react-router-dom-v5-compat';
|
||||
import { AppState } from 'store/reducers';
|
||||
@@ -89,8 +89,12 @@ function PodDetails({
|
||||
endTime: endMs,
|
||||
}));
|
||||
|
||||
const lastSelectedInterval = useRef<Time | null>(null);
|
||||
|
||||
const [selectedInterval, setSelectedInterval] = useState<Time>(
|
||||
selectedTime as Time,
|
||||
lastSelectedInterval.current
|
||||
? lastSelectedInterval.current
|
||||
: (selectedTime as Time),
|
||||
);
|
||||
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
@@ -212,10 +216,11 @@ function PodDetails({
|
||||
}, [initialFilters, initialEventsFilters]);
|
||||
|
||||
useEffect(() => {
|
||||
setSelectedInterval(selectedTime as Time);
|
||||
const currentSelectedInterval = lastSelectedInterval.current || selectedTime;
|
||||
setSelectedInterval(currentSelectedInterval as Time);
|
||||
|
||||
if (selectedTime !== 'custom') {
|
||||
const { maxTime, minTime } = GetMinMax(selectedTime);
|
||||
if (currentSelectedInterval !== 'custom') {
|
||||
const { maxTime, minTime } = GetMinMax(currentSelectedInterval);
|
||||
|
||||
setModalTimeRange({
|
||||
startTime: Math.floor(minTime / TimeRangeOffset),
|
||||
@@ -243,6 +248,7 @@ function PodDetails({
|
||||
|
||||
const handleTimeChange = useCallback(
|
||||
(interval: Time | CustomTimeType, dateTimeRange?: [number, number]): void => {
|
||||
lastSelectedInterval.current = interval as Time;
|
||||
setSelectedInterval(interval as Time);
|
||||
|
||||
if (interval === 'custom' && dateTimeRange) {
|
||||
@@ -485,6 +491,7 @@ function PodDetails({
|
||||
};
|
||||
|
||||
const handleClose = (): void => {
|
||||
lastSelectedInterval.current = null;
|
||||
setSelectedInterval(selectedTime as Time);
|
||||
|
||||
if (selectedTime !== 'custom') {
|
||||
|
||||
@@ -72,10 +72,7 @@ export const getPodMetricsQueryPayload = (
|
||||
dotMetricsEnabled ? dotKey : underscoreKey;
|
||||
const k8sContainerNameKey = getKey('k8s.container.name', 'k8s_container_name');
|
||||
|
||||
const k8sPodCpuUtilKey = getKey(
|
||||
'k8s.pod.cpu.utilization',
|
||||
'k8s_pod_cpu_utilization',
|
||||
);
|
||||
const k8sPodCpuUtilKey = getKey('k8s.pod.cpu.usage', 'k8s_pod_cpu_usage');
|
||||
|
||||
const k8sPodCpuReqUtilKey = getKey(
|
||||
'k8s.pod.cpu_request_utilization',
|
||||
@@ -115,8 +112,8 @@ export const getPodMetricsQueryPayload = (
|
||||
);
|
||||
|
||||
const containerCpuUtilKey = getKey(
|
||||
'container.cpu.utilization',
|
||||
'container_cpu_utilization',
|
||||
'container.cpu.usage',
|
||||
'container_cpu_usage',
|
||||
);
|
||||
|
||||
const k8sContainerCpuRequestKey = getKey(
|
||||
@@ -189,7 +186,7 @@ export const getPodMetricsQueryPayload = (
|
||||
{
|
||||
aggregateAttribute: {
|
||||
dataType: DataTypes.Float64,
|
||||
id: 'k8s_pod_cpu_utilization--float64--Gauge--true',
|
||||
id: 'k8s_pod_cpu_usage--float64--Gauge--true',
|
||||
isColumn: true,
|
||||
isJSON: false,
|
||||
key: k8sPodCpuUtilKey,
|
||||
@@ -245,7 +242,7 @@ export const getPodMetricsQueryPayload = (
|
||||
{
|
||||
aggregateAttribute: {
|
||||
dataType: DataTypes.Float64,
|
||||
id: 'k8s_pod_cpu_utilization--float64--Gauge--true',
|
||||
id: 'k8s_pod_cpu_usage--float64--Gauge--true',
|
||||
isColumn: true,
|
||||
isJSON: false,
|
||||
key: k8sPodCpuUtilKey,
|
||||
@@ -301,7 +298,7 @@ export const getPodMetricsQueryPayload = (
|
||||
{
|
||||
aggregateAttribute: {
|
||||
dataType: DataTypes.Float64,
|
||||
id: 'k8s_pod_cpu_utilization--float64--Gauge--true',
|
||||
id: 'k8s_pod_cpu_usage--float64--Gauge--true',
|
||||
isColumn: true,
|
||||
isJSON: false,
|
||||
key: k8sPodCpuUtilKey,
|
||||
@@ -1570,7 +1567,7 @@ export const getPodMetricsQueryPayload = (
|
||||
{
|
||||
aggregateAttribute: {
|
||||
dataType: DataTypes.Float64,
|
||||
id: 'container_cpu_utilization--float64--Gauge--true',
|
||||
id: 'container_cpu_usage--float64--Gauge--true',
|
||||
isColumn: true,
|
||||
isJSON: false,
|
||||
key: containerCpuUtilKey,
|
||||
@@ -1668,7 +1665,7 @@ export const getPodMetricsQueryPayload = (
|
||||
{
|
||||
aggregateAttribute: {
|
||||
dataType: DataTypes.Float64,
|
||||
id: 'container_cpu_utilization--float64--Gauge--true',
|
||||
id: 'container_cpu_usage--float64--Gauge--true',
|
||||
isColumn: true,
|
||||
isJSON: false,
|
||||
key: containerCpuUtilKey,
|
||||
|
||||
@@ -191,6 +191,32 @@ function K8sStatefulSetsList({
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [minTime, maxTime, orderBy, selectedRowData, groupBy]);
|
||||
|
||||
const groupedByRowDataQueryKey = useMemo(() => {
|
||||
if (selectedStatefulSetUID) {
|
||||
return [
|
||||
'statefulSetList',
|
||||
JSON.stringify(queryFilters),
|
||||
JSON.stringify(orderBy),
|
||||
JSON.stringify(selectedRowData),
|
||||
];
|
||||
}
|
||||
return [
|
||||
'statefulSetList',
|
||||
JSON.stringify(queryFilters),
|
||||
JSON.stringify(orderBy),
|
||||
JSON.stringify(selectedRowData),
|
||||
String(minTime),
|
||||
String(maxTime),
|
||||
];
|
||||
}, [
|
||||
queryFilters,
|
||||
orderBy,
|
||||
selectedStatefulSetUID,
|
||||
minTime,
|
||||
maxTime,
|
||||
selectedRowData,
|
||||
]);
|
||||
|
||||
const {
|
||||
data: groupedByRowData,
|
||||
isFetching: isFetchingGroupedByRowData,
|
||||
@@ -200,7 +226,7 @@ function K8sStatefulSetsList({
|
||||
} = useGetK8sStatefulSetsList(
|
||||
fetchGroupedByRowDataQuery as K8sStatefulSetsListPayload,
|
||||
{
|
||||
queryKey: ['statefulSetList', fetchGroupedByRowDataQuery],
|
||||
queryKey: groupedByRowDataQueryKey,
|
||||
enabled: !!fetchGroupedByRowDataQuery && !!selectedRowData,
|
||||
},
|
||||
undefined,
|
||||
@@ -256,11 +282,44 @@ function K8sStatefulSetsList({
|
||||
return groupedByRowData?.payload?.data?.records || [];
|
||||
}, [groupedByRowData, selectedRowData]);
|
||||
|
||||
const queryKey = useMemo(() => {
|
||||
if (selectedStatefulSetUID) {
|
||||
return [
|
||||
'statefulSetList',
|
||||
String(pageSize),
|
||||
String(currentPage),
|
||||
JSON.stringify(queryFilters),
|
||||
JSON.stringify(orderBy),
|
||||
JSON.stringify(groupBy),
|
||||
];
|
||||
}
|
||||
return [
|
||||
'statefulSetList',
|
||||
String(pageSize),
|
||||
String(currentPage),
|
||||
JSON.stringify(queryFilters),
|
||||
JSON.stringify(orderBy),
|
||||
JSON.stringify(groupBy),
|
||||
String(minTime),
|
||||
String(maxTime),
|
||||
];
|
||||
}, [
|
||||
selectedStatefulSetUID,
|
||||
pageSize,
|
||||
currentPage,
|
||||
queryFilters,
|
||||
orderBy,
|
||||
groupBy,
|
||||
minTime,
|
||||
maxTime,
|
||||
]);
|
||||
|
||||
const { data, isFetching, isLoading, isError } = useGetK8sStatefulSetsList(
|
||||
query as K8sStatefulSetsListPayload,
|
||||
{
|
||||
queryKey: ['statefulSetList', query],
|
||||
queryKey,
|
||||
enabled: !!query,
|
||||
keepPreviousData: true,
|
||||
},
|
||||
undefined,
|
||||
dotMetricsEnabled,
|
||||
@@ -592,6 +651,9 @@ function K8sStatefulSetsList({
|
||||
});
|
||||
};
|
||||
|
||||
const showTableLoadingState =
|
||||
(isFetching || isLoading) && formattedStatefulSetsData.length === 0;
|
||||
|
||||
return (
|
||||
<div className="k8s-list">
|
||||
<K8sHeader
|
||||
@@ -604,6 +666,7 @@ function K8sStatefulSetsList({
|
||||
handleGroupByChange={handleGroupByChange}
|
||||
selectedGroupBy={groupBy}
|
||||
entity={K8sCategory.STATEFULSETS}
|
||||
showAutoRefresh={!selectedStatefulSetData}
|
||||
/>
|
||||
{isError && <Typography>{data?.error || 'Something went wrong'}</Typography>}
|
||||
|
||||
@@ -611,7 +674,7 @@ function K8sStatefulSetsList({
|
||||
className={classNames('k8s-list-table', 'statefulSets-list-table', {
|
||||
'expanded-statefulsets-list-table': isGroupedByAttribute,
|
||||
})}
|
||||
dataSource={isFetching || isLoading ? [] : formattedStatefulSetsData}
|
||||
dataSource={showTableLoadingState ? [] : formattedStatefulSetsData}
|
||||
columns={columns}
|
||||
pagination={{
|
||||
current: currentPage,
|
||||
@@ -623,26 +686,25 @@ function K8sStatefulSetsList({
|
||||
}}
|
||||
scroll={{ x: true }}
|
||||
loading={{
|
||||
spinning: isFetching || isLoading,
|
||||
spinning: showTableLoadingState,
|
||||
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"
|
||||
/>
|
||||
emptyText: showTableLoadingState ? 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>
|
||||
<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}
|
||||
|
||||
@@ -38,7 +38,7 @@ import {
|
||||
ScrollText,
|
||||
X,
|
||||
} from 'lucide-react';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useSearchParams } from 'react-router-dom-v5-compat';
|
||||
import { AppState } from 'store/reducers';
|
||||
@@ -84,8 +84,12 @@ function StatefulSetDetails({
|
||||
endTime: endMs,
|
||||
}));
|
||||
|
||||
const lastSelectedInterval = useRef<Time | null>(null);
|
||||
|
||||
const [selectedInterval, setSelectedInterval] = useState<Time>(
|
||||
selectedTime as Time,
|
||||
lastSelectedInterval.current
|
||||
? lastSelectedInterval.current
|
||||
: (selectedTime as Time),
|
||||
);
|
||||
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
@@ -211,10 +215,11 @@ function StatefulSetDetails({
|
||||
}, [initialFilters, initialEventsFilters]);
|
||||
|
||||
useEffect(() => {
|
||||
setSelectedInterval(selectedTime as Time);
|
||||
const currentSelectedInterval = lastSelectedInterval.current || selectedTime;
|
||||
setSelectedInterval(currentSelectedInterval as Time);
|
||||
|
||||
if (selectedTime !== 'custom') {
|
||||
const { maxTime, minTime } = GetMinMax(selectedTime);
|
||||
if (currentSelectedInterval !== 'custom') {
|
||||
const { maxTime, minTime } = GetMinMax(currentSelectedInterval);
|
||||
|
||||
setModalTimeRange({
|
||||
startTime: Math.floor(minTime / 1000000000),
|
||||
@@ -242,6 +247,7 @@ function StatefulSetDetails({
|
||||
|
||||
const handleTimeChange = useCallback(
|
||||
(interval: Time | CustomTimeType, dateTimeRange?: [number, number]): void => {
|
||||
lastSelectedInterval.current = interval as Time;
|
||||
setSelectedInterval(interval as Time);
|
||||
|
||||
if (interval === 'custom' && dateTimeRange) {
|
||||
@@ -477,6 +483,7 @@ function StatefulSetDetails({
|
||||
};
|
||||
|
||||
const handleClose = (): void => {
|
||||
lastSelectedInterval.current = null;
|
||||
setSelectedInterval(selectedTime as Time);
|
||||
|
||||
if (selectedTime !== 'custom') {
|
||||
|
||||
@@ -49,8 +49,8 @@ export const getStatefulSetMetricsQueryPayload = (
|
||||
const k8sPodNameKey = dotMetricsEnabled ? 'k8s.pod.name' : 'k8s_pod_name';
|
||||
|
||||
const k8sPodCpuUtilKey = dotMetricsEnabled
|
||||
? 'k8s.pod.cpu.utilization'
|
||||
: 'k8s_pod_cpu_utilization';
|
||||
? 'k8s.pod.cpu.usage'
|
||||
: 'k8s_pod_cpu_usage';
|
||||
const k8sContainerCpuRequestKey = dotMetricsEnabled
|
||||
? 'k8s.container.cpu_request'
|
||||
: 'k8s_container_cpu_request';
|
||||
|
||||
@@ -574,6 +574,9 @@ function K8sVolumesList({
|
||||
});
|
||||
};
|
||||
|
||||
const showTableLoadingState =
|
||||
(isFetching || isLoading) && formattedVolumesData.length === 0;
|
||||
|
||||
return (
|
||||
<div className="k8s-list">
|
||||
<K8sHeader
|
||||
@@ -586,6 +589,7 @@ function K8sVolumesList({
|
||||
handleGroupByChange={handleGroupByChange}
|
||||
selectedGroupBy={groupBy}
|
||||
entity={K8sCategory.NODES}
|
||||
showAutoRefresh={!selectedVolumeData}
|
||||
/>
|
||||
{isError && <Typography>{data?.error || 'Something went wrong'}</Typography>}
|
||||
|
||||
@@ -593,7 +597,7 @@ function K8sVolumesList({
|
||||
className={classNames('k8s-list-table', 'volumes-list-table', {
|
||||
'expanded-volumes-list-table': isGroupedByAttribute,
|
||||
})}
|
||||
dataSource={isFetching || isLoading ? [] : formattedVolumesData}
|
||||
dataSource={showTableLoadingState ? [] : formattedVolumesData}
|
||||
columns={columns}
|
||||
pagination={{
|
||||
current: currentPage,
|
||||
@@ -605,26 +609,25 @@ function K8sVolumesList({
|
||||
}}
|
||||
scroll={{ x: true }}
|
||||
loading={{
|
||||
spinning: isFetching || isLoading,
|
||||
spinning: showTableLoadingState,
|
||||
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"
|
||||
/>
|
||||
emptyText: showTableLoadingState ? 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>
|
||||
<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}
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import GetMinMax from 'lib/getMinMax';
|
||||
import { X } from 'lucide-react';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
@@ -44,8 +44,12 @@ function VolumeDetails({
|
||||
endTime: endMs,
|
||||
}));
|
||||
|
||||
const lastSelectedInterval = useRef<Time | null>(null);
|
||||
|
||||
const [selectedInterval, setSelectedInterval] = useState<Time>(
|
||||
selectedTime as Time,
|
||||
lastSelectedInterval.current
|
||||
? lastSelectedInterval.current
|
||||
: (selectedTime as Time),
|
||||
);
|
||||
|
||||
const isDarkMode = useIsDarkMode();
|
||||
@@ -62,10 +66,11 @@ function VolumeDetails({
|
||||
}, [volume]);
|
||||
|
||||
useEffect(() => {
|
||||
setSelectedInterval(selectedTime as Time);
|
||||
const currentSelectedInterval = lastSelectedInterval.current || selectedTime;
|
||||
setSelectedInterval(currentSelectedInterval as Time);
|
||||
|
||||
if (selectedTime !== 'custom') {
|
||||
const { maxTime, minTime } = GetMinMax(selectedTime);
|
||||
if (currentSelectedInterval !== 'custom') {
|
||||
const { maxTime, minTime } = GetMinMax(currentSelectedInterval);
|
||||
|
||||
setModalTimeRange({
|
||||
startTime: Math.floor(minTime / 1000000000),
|
||||
@@ -76,6 +81,7 @@ function VolumeDetails({
|
||||
|
||||
const handleTimeChange = useCallback(
|
||||
(interval: Time | CustomTimeType, dateTimeRange?: [number, number]): void => {
|
||||
lastSelectedInterval.current = interval as Time;
|
||||
setSelectedInterval(interval as Time);
|
||||
|
||||
if (interval === 'custom' && dateTimeRange) {
|
||||
@@ -104,6 +110,7 @@ function VolumeDetails({
|
||||
);
|
||||
|
||||
const handleClose = (): void => {
|
||||
lastSelectedInterval.current = null;
|
||||
setSelectedInterval(selectedTime as Time);
|
||||
|
||||
if (selectedTime !== 'custom') {
|
||||
|
||||
@@ -38,28 +38,28 @@ export const K8sCategories = {
|
||||
|
||||
export const underscoreMap = {
|
||||
[K8sCategory.HOSTS]: 'system_cpu_load_average_15m',
|
||||
[K8sCategory.PODS]: 'k8s_pod_cpu_utilization',
|
||||
[K8sCategory.NODES]: 'k8s_node_cpu_utilization',
|
||||
[K8sCategory.NAMESPACES]: 'k8s_pod_cpu_utilization',
|
||||
[K8sCategory.CLUSTERS]: 'k8s_node_cpu_utilization',
|
||||
[K8sCategory.DEPLOYMENTS]: 'k8s_pod_cpu_utilization',
|
||||
[K8sCategory.STATEFULSETS]: 'k8s_pod_cpu_utilization',
|
||||
[K8sCategory.DAEMONSETS]: 'k8s_pod_cpu_utilization',
|
||||
[K8sCategory.CONTAINERS]: 'k8s_pod_cpu_utilization',
|
||||
[K8sCategory.PODS]: 'k8s_pod_cpu_usage',
|
||||
[K8sCategory.NODES]: 'k8s_node_cpu_usage',
|
||||
[K8sCategory.NAMESPACES]: 'k8s_pod_cpu_usage',
|
||||
[K8sCategory.CLUSTERS]: 'k8s_node_cpu_usage',
|
||||
[K8sCategory.DEPLOYMENTS]: 'k8s_pod_cpu_usage',
|
||||
[K8sCategory.STATEFULSETS]: 'k8s_pod_cpu_usage',
|
||||
[K8sCategory.DAEMONSETS]: 'k8s_pod_cpu_usage',
|
||||
[K8sCategory.CONTAINERS]: 'k8s_pod_cpu_usage',
|
||||
[K8sCategory.JOBS]: 'k8s_job_desired_successful_pods',
|
||||
[K8sCategory.VOLUMES]: 'k8s_volume_capacity',
|
||||
};
|
||||
|
||||
export const dotMap = {
|
||||
[K8sCategory.HOSTS]: 'system.cpu.load_average.15m',
|
||||
[K8sCategory.PODS]: 'k8s.pod.cpu.utilization',
|
||||
[K8sCategory.NODES]: 'k8s.node.cpu.utilization',
|
||||
[K8sCategory.NAMESPACES]: 'k8s.pod.cpu.utilization',
|
||||
[K8sCategory.CLUSTERS]: 'k8s.node.cpu.utilization',
|
||||
[K8sCategory.DEPLOYMENTS]: 'k8s.pod.cpu.utilization',
|
||||
[K8sCategory.STATEFULSETS]: 'k8s.pod.cpu.utilization',
|
||||
[K8sCategory.DAEMONSETS]: 'k8s.pod.cpu.utilization',
|
||||
[K8sCategory.CONTAINERS]: 'k8s.pod.cpu.utilization',
|
||||
[K8sCategory.PODS]: 'k8s.pod.cpu.usage',
|
||||
[K8sCategory.NODES]: 'k8s.node.cpu.usage',
|
||||
[K8sCategory.NAMESPACES]: 'k8s.pod.cpu.usage',
|
||||
[K8sCategory.CLUSTERS]: 'k8s.node.cpu.usage',
|
||||
[K8sCategory.DEPLOYMENTS]: 'k8s.pod.cpu.usage',
|
||||
[K8sCategory.STATEFULSETS]: 'k8s.pod.cpu.usage',
|
||||
[K8sCategory.DAEMONSETS]: 'k8s.pod.cpu.usage',
|
||||
[K8sCategory.CONTAINERS]: 'k8s.pod.cpu.usage',
|
||||
[K8sCategory.JOBS]: 'k8s.job.desired_successful_pods',
|
||||
[K8sCategory.VOLUMES]: 'k8s.volume.capacity',
|
||||
};
|
||||
@@ -96,8 +96,8 @@ export function GetPodsQuickFiltersConfig(
|
||||
|
||||
// Define aggregate attribute (metric) name
|
||||
const cpuUtilizationMetric = dotMetricsEnabled
|
||||
? 'k8s.pod.cpu.utilization'
|
||||
: 'k8s_pod_cpu_utilization';
|
||||
? 'k8s.pod.cpu.usage'
|
||||
: 'k8s_pod_cpu_usage';
|
||||
|
||||
return [
|
||||
{
|
||||
@@ -252,8 +252,8 @@ export function GetNodesQuickFiltersConfig(
|
||||
|
||||
// Define aggregate metric name for node CPU utilization
|
||||
const cpuUtilMetric = dotMetricsEnabled
|
||||
? 'k8s.node.cpu.utilization'
|
||||
: 'k8s_node_cpu_utilization';
|
||||
? 'k8s.node.cpu.usage'
|
||||
: 'k8s_node_cpu_usage';
|
||||
const environmentKey = dotMetricsEnabled
|
||||
? 'deployment.environment'
|
||||
: 'deployment_environment';
|
||||
@@ -314,8 +314,8 @@ export function GetNamespaceQuickFiltersConfig(
|
||||
: 'k8s_namespace_name';
|
||||
const clusterKey = dotMetricsEnabled ? 'k8s.cluster.name' : 'k8s_cluster_name';
|
||||
const cpuUtilMetric = dotMetricsEnabled
|
||||
? 'k8s.pod.cpu.utilization'
|
||||
: 'k8s_pod_cpu_utilization';
|
||||
? 'k8s.pod.cpu.usage'
|
||||
: 'k8s_pod_cpu_usage';
|
||||
const environmentKey = dotMetricsEnabled
|
||||
? 'deployment.environment'
|
||||
: 'deployment_environment';
|
||||
@@ -373,8 +373,8 @@ export function GetClustersQuickFiltersConfig(
|
||||
): IQuickFiltersConfig[] {
|
||||
const clusterKey = dotMetricsEnabled ? 'k8s.cluster.name' : 'k8s_cluster_name';
|
||||
const cpuUtilMetric = dotMetricsEnabled
|
||||
? 'k8s.node.cpu.utilization'
|
||||
: 'k8s_node_cpu_utilization';
|
||||
? 'k8s.node.cpu.usage'
|
||||
: 'k8s_node_cpu_usage';
|
||||
const environmentKey = dotMetricsEnabled
|
||||
? 'deployment.environment'
|
||||
: 'deployment_environment';
|
||||
@@ -541,9 +541,7 @@ export function GetDeploymentsQuickFiltersConfig(
|
||||
? 'k8s.namespace.name'
|
||||
: 'k8s_namespace_name';
|
||||
const clusterKey = dotMetricsEnabled ? 'k8s.cluster.name' : 'k8s_cluster_name';
|
||||
const metric = dotMetricsEnabled
|
||||
? 'k8s.pod.cpu.utilization'
|
||||
: 'k8s_pod_cpu_utilization';
|
||||
const metric = dotMetricsEnabled ? 'k8s.pod.cpu.usage' : 'k8s_pod_cpu_usage';
|
||||
const environmentKey = dotMetricsEnabled
|
||||
? 'deployment.environment'
|
||||
: 'deployment_environment';
|
||||
@@ -622,9 +620,7 @@ export function GetStatefulsetsQuickFiltersConfig(
|
||||
? 'k8s.namespace.name'
|
||||
: 'k8s_namespace_name';
|
||||
const clusterKey = dotMetricsEnabled ? 'k8s.cluster.name' : 'k8s_cluster_name';
|
||||
const metric = dotMetricsEnabled
|
||||
? 'k8s.pod.cpu.utilization'
|
||||
: 'k8s_pod_cpu_utilization';
|
||||
const metric = dotMetricsEnabled ? 'k8s.pod.cpu.usage' : 'k8s_pod_cpu_usage';
|
||||
const environmentKey = dotMetricsEnabled
|
||||
? 'deployment.environment'
|
||||
: 'deployment_environment';
|
||||
@@ -704,8 +700,8 @@ export function GetDaemonsetsQuickFiltersConfig(
|
||||
: 'k8s_namespace_name';
|
||||
const clusterKey = dotMetricsEnabled ? 'k8s.cluster.name' : 'k8s_cluster_name';
|
||||
const metricName = dotMetricsEnabled
|
||||
? 'k8s.pod.cpu.utilization'
|
||||
: 'k8s_pod_cpu_utilization';
|
||||
? 'k8s.pod.cpu.usage'
|
||||
: 'k8s_pod_cpu_usage';
|
||||
const environmentKey = dotMetricsEnabled
|
||||
? 'deployment.environment'
|
||||
: 'deployment_environment';
|
||||
@@ -781,8 +777,8 @@ export function GetJobsQuickFiltersConfig(
|
||||
: 'k8s_namespace_name';
|
||||
const clusterKey = dotMetricsEnabled ? 'k8s.cluster.name' : 'k8s_cluster_name';
|
||||
const metricName = dotMetricsEnabled
|
||||
? 'k8s.pod.cpu.utilization'
|
||||
: 'k8s_pod_cpu_utilization';
|
||||
? 'k8s.pod.cpu.usage'
|
||||
: 'k8s_pod_cpu_usage';
|
||||
const environmentKey = dotMetricsEnabled
|
||||
? 'deployment.environment'
|
||||
: 'deployment_environment';
|
||||
|
||||
@@ -32,7 +32,7 @@ function ContextLogRenderer({
|
||||
const [afterLogPage, setAfterLogPage] = useState<number>(1);
|
||||
const [logs, setLogs] = useState<ILog[]>([log]);
|
||||
|
||||
const { initialDataSource, stagedQuery } = useQueryBuilder();
|
||||
const { stagedQuery } = useQueryBuilder();
|
||||
|
||||
const listQuery = useMemo(() => {
|
||||
if (!stagedQuery || stagedQuery.builder.queryData.length < 1) return null;
|
||||
@@ -42,7 +42,7 @@ function ContextLogRenderer({
|
||||
|
||||
const { options } = useOptionsMenu({
|
||||
storageKey: LOCALSTORAGE.LOGS_LIST_OPTIONS,
|
||||
dataSource: initialDataSource || DataSource.METRICS,
|
||||
dataSource: DataSource.LOGS,
|
||||
aggregateOperator: listQuery?.aggregateOperator || StringOperators.NOOP,
|
||||
});
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import cx from 'classnames';
|
||||
import Uplot from 'components/Uplot';
|
||||
import { ENTITY_VERSION_V4 } from 'constants/app';
|
||||
import dayjs from 'dayjs';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import { useResizeObserver } from 'hooks/useDimensions';
|
||||
import { GetMetricQueryRange } from 'lib/dashboard/getQueryResults';
|
||||
@@ -89,6 +90,7 @@ function NodeMetrics({
|
||||
);
|
||||
|
||||
const { timezone } = useTimezone();
|
||||
const { currentQuery } = useQueryBuilder();
|
||||
|
||||
const options = useMemo(
|
||||
() =>
|
||||
@@ -106,6 +108,7 @@ function NodeMetrics({
|
||||
tzDate: (timestamp: number) =>
|
||||
uPlot.tzDate(new Date(timestamp * 1e3), timezone.value),
|
||||
timezone: timezone.value,
|
||||
query: currentQuery,
|
||||
}),
|
||||
),
|
||||
[
|
||||
@@ -117,6 +120,7 @@ function NodeMetrics({
|
||||
verticalLineTimestamp,
|
||||
end,
|
||||
timezone.value,
|
||||
currentQuery,
|
||||
],
|
||||
);
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import cx from 'classnames';
|
||||
import Uplot from 'components/Uplot';
|
||||
import { ENTITY_VERSION_V4 } from 'constants/app';
|
||||
import dayjs from 'dayjs';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import { useResizeObserver } from 'hooks/useDimensions';
|
||||
import { GetMetricQueryRange } from 'lib/dashboard/getQueryResults';
|
||||
@@ -71,6 +72,7 @@ function PodMetrics({
|
||||
[queries],
|
||||
);
|
||||
const { timezone } = useTimezone();
|
||||
const { currentQuery } = useQueryBuilder();
|
||||
|
||||
const options = useMemo(
|
||||
() =>
|
||||
@@ -88,6 +90,7 @@ function PodMetrics({
|
||||
tzDate: (timestamp: number) =>
|
||||
uPlot.tzDate(new Date(timestamp * 1e3), timezone.value),
|
||||
timezone: timezone.value,
|
||||
query: currentQuery,
|
||||
}),
|
||||
),
|
||||
[
|
||||
@@ -98,6 +101,7 @@ function PodMetrics({
|
||||
end,
|
||||
verticalLineTimestamp,
|
||||
timezone.value,
|
||||
currentQuery,
|
||||
],
|
||||
);
|
||||
|
||||
|
||||
@@ -17,8 +17,8 @@ export const getPodQueryPayload = (
|
||||
: 'k8s_cluster_name';
|
||||
const k8sPodNameKey = dotMetricsEnabled ? 'k8s.pod.name' : 'k8s_pod_name';
|
||||
const containerCpuUtilKey = dotMetricsEnabled
|
||||
? 'container.cpu.utilization'
|
||||
: 'container_cpu_utilization';
|
||||
? 'container.cpu.usage'
|
||||
: 'container_cpu_usage';
|
||||
const containerMemUsageKey = dotMetricsEnabled
|
||||
? 'container.memory.usage'
|
||||
: 'container_memory_usage';
|
||||
@@ -63,7 +63,7 @@ export const getPodQueryPayload = (
|
||||
{
|
||||
aggregateAttribute: {
|
||||
dataType: DataTypes.Float64,
|
||||
id: 'container_cpu_utilization--float64--Gauge--true',
|
||||
id: 'container_cpu_usage--float64--Gauge--true',
|
||||
isColumn: true,
|
||||
isJSON: false,
|
||||
key: containerCpuUtilKey,
|
||||
@@ -231,7 +231,7 @@ export const getPodQueryPayload = (
|
||||
{
|
||||
aggregateAttribute: {
|
||||
dataType: DataTypes.Float64,
|
||||
id: 'container_cpu_utilization--float64--Gauge--true',
|
||||
id: 'container_cpu_usage--float64--Gauge--true',
|
||||
isColumn: true,
|
||||
isJSON: false,
|
||||
key: containerCpuUtilKey,
|
||||
@@ -385,7 +385,7 @@ export const getPodQueryPayload = (
|
||||
{
|
||||
aggregateAttribute: {
|
||||
dataType: DataTypes.Float64,
|
||||
id: 'container_cpu_utilization--float64--Gauge--true',
|
||||
id: 'container_cpu_usage--float64--Gauge--true',
|
||||
isColumn: true,
|
||||
isJSON: false,
|
||||
key: containerCpuUtilKey,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user