Compare commits
86 Commits
v0.90.1
...
fix/multi-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
070decb102 | ||
|
|
0fbe60d68f | ||
|
|
7e877790d0 | ||
|
|
3b2d3c3849 | ||
|
|
815d3b8bc0 | ||
|
|
e529ad8d57 | ||
|
|
c8f36de6fa | ||
|
|
85caefa945 | ||
|
|
62d8dd929a | ||
|
|
afceff33d6 | ||
|
|
ed77c6abd0 | ||
|
|
5575334893 | ||
|
|
137a3f6d27 | ||
|
|
c332a3c48a | ||
|
|
84ff35100a | ||
|
|
67ce050f53 | ||
|
|
ceefe50d82 | ||
|
|
04c9e852e6 | ||
|
|
97cd377fa6 | ||
|
|
157213defc | ||
|
|
8092df8961 | ||
|
|
3c895981d9 | ||
|
|
a058dac45b | ||
|
|
a18106f5d8 | ||
|
|
ea88177936 | ||
|
|
84a17dd376 | ||
|
|
f699773aec | ||
|
|
d46d1a0f24 | ||
|
|
a3b66935d8 | ||
|
|
1f8c97cd5b | ||
|
|
2659e03564 | ||
|
|
121696c1d7 | ||
|
|
20be9dd600 | ||
|
|
45e4c65c9f | ||
|
|
b7490fcf68 | ||
|
|
4bfd4e536c | ||
|
|
f7d5a26403 | ||
|
|
f87594243e | ||
|
|
dacc3d6d9e | ||
|
|
6b28ec2f7f | ||
|
|
9b757af028 | ||
|
|
ce87bcae71 | ||
|
|
25fb8b6561 | ||
|
|
428a16326a | ||
|
|
78fec2188d | ||
|
|
c5650cc131 | ||
|
|
f67213096c | ||
|
|
d71f85a8ec | ||
|
|
9335261314 | ||
|
|
0f5c54cabb | ||
|
|
0204337396 | ||
|
|
8101fef874 | ||
|
|
2d223fe9e8 | ||
|
|
de464e6042 | ||
|
|
ea42e4db6b | ||
|
|
2b5d2f0061 | ||
|
|
a013cc0fd3 | ||
|
|
e68d860adf | ||
|
|
9ed93ae5ac | ||
|
|
9989af10d6 | ||
|
|
1bc89c9d1a | ||
|
|
3fbe111bc0 | ||
|
|
c449d1da8e | ||
|
|
4635da0ee8 | ||
|
|
67453e27f7 | ||
|
|
fdcc6a6c92 | ||
|
|
62c71e6306 | ||
|
|
e2e535eaca | ||
|
|
2520718afb | ||
|
|
0ffa666903 | ||
|
|
c653e83461 | ||
|
|
b80cf96faf | ||
|
|
a2126ad22c | ||
|
|
5a75df30e2 | ||
|
|
aeca98b6aa | ||
|
|
53b31ae516 | ||
|
|
209828de01 | ||
|
|
491a0140e3 | ||
|
|
b9494a3375 | ||
|
|
d4b379ccc0 | ||
|
|
a7ff27d30c | ||
|
|
6008e8df72 | ||
|
|
27d5e16d18 | ||
|
|
24d6b48ad4 | ||
|
|
78af24b4df | ||
|
|
45fcf746b0 |
@@ -40,7 +40,7 @@ services:
|
|||||||
timeout: 5s
|
timeout: 5s
|
||||||
retries: 3
|
retries: 3
|
||||||
schema-migrator-sync:
|
schema-migrator-sync:
|
||||||
image: signoz/signoz-schema-migrator:v0.128.2
|
image: signoz/signoz-schema-migrator:v0.111.42
|
||||||
container_name: schema-migrator-sync
|
container_name: schema-migrator-sync
|
||||||
command:
|
command:
|
||||||
- sync
|
- sync
|
||||||
@@ -53,7 +53,7 @@ services:
|
|||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
restart: on-failure
|
restart: on-failure
|
||||||
schema-migrator-async:
|
schema-migrator-async:
|
||||||
image: signoz/signoz-schema-migrator:v0.128.2
|
image: signoz/signoz-schema-migrator:v0.111.42
|
||||||
container_name: schema-migrator-async
|
container_name: schema-migrator-async
|
||||||
command:
|
command:
|
||||||
- async
|
- async
|
||||||
|
|||||||
2
.github/workflows/integrationci.yaml
vendored
@@ -22,7 +22,7 @@ jobs:
|
|||||||
- 24.1.2-alpine
|
- 24.1.2-alpine
|
||||||
- 24.12-alpine
|
- 24.12-alpine
|
||||||
schema-migrator-version:
|
schema-migrator-version:
|
||||||
- v0.128.1
|
- v0.111.38
|
||||||
postgres-version:
|
postgres-version:
|
||||||
- 15
|
- 15
|
||||||
if: |
|
if: |
|
||||||
|
|||||||
@@ -231,8 +231,6 @@ Not sure how to get started? Just ping us on `#contributing` in our [slack commu
|
|||||||
- [Shaheer Kochai](https://github.com/ahmadshaheer)
|
- [Shaheer Kochai](https://github.com/ahmadshaheer)
|
||||||
- [Amlan Kumar Nandy](https://github.com/amlannandy)
|
- [Amlan Kumar Nandy](https://github.com/amlannandy)
|
||||||
- [Sahil Khan](https://github.com/sawhil)
|
- [Sahil Khan](https://github.com/sawhil)
|
||||||
- [Aditya Singh](https://github.com/aks07)
|
|
||||||
- [Abhi Kumar](https://github.com/ahrefabhi)
|
|
||||||
|
|
||||||
#### DevOps
|
#### DevOps
|
||||||
|
|
||||||
|
|||||||
@@ -174,7 +174,7 @@ services:
|
|||||||
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
|
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
|
||||||
signoz:
|
signoz:
|
||||||
!!merge <<: *db-depend
|
!!merge <<: *db-depend
|
||||||
image: signoz/signoz:v0.90.1
|
image: signoz/signoz:v0.87.0
|
||||||
command:
|
command:
|
||||||
- --config=/root/config/prometheus.yml
|
- --config=/root/config/prometheus.yml
|
||||||
ports:
|
ports:
|
||||||
@@ -194,7 +194,6 @@ services:
|
|||||||
- TELEMETRY_ENABLED=true
|
- TELEMETRY_ENABLED=true
|
||||||
- DEPLOYMENT_TYPE=docker-swarm
|
- DEPLOYMENT_TYPE=docker-swarm
|
||||||
- SIGNOZ_JWT_SECRET=secret
|
- SIGNOZ_JWT_SECRET=secret
|
||||||
- DOT_METRICS_ENABLED=true
|
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test:
|
test:
|
||||||
- CMD
|
- CMD
|
||||||
@@ -207,7 +206,7 @@ services:
|
|||||||
retries: 3
|
retries: 3
|
||||||
otel-collector:
|
otel-collector:
|
||||||
!!merge <<: *db-depend
|
!!merge <<: *db-depend
|
||||||
image: signoz/signoz-otel-collector:v0.128.2
|
image: signoz/signoz-otel-collector:v0.111.42
|
||||||
command:
|
command:
|
||||||
- --config=/etc/otel-collector-config.yaml
|
- --config=/etc/otel-collector-config.yaml
|
||||||
- --manager-config=/etc/manager-config.yaml
|
- --manager-config=/etc/manager-config.yaml
|
||||||
@@ -231,7 +230,7 @@ services:
|
|||||||
- signoz
|
- signoz
|
||||||
schema-migrator:
|
schema-migrator:
|
||||||
!!merge <<: *common
|
!!merge <<: *common
|
||||||
image: signoz/signoz-schema-migrator:v0.128.2
|
image: signoz/signoz-schema-migrator:v0.111.42
|
||||||
deploy:
|
deploy:
|
||||||
restart_policy:
|
restart_policy:
|
||||||
condition: on-failure
|
condition: on-failure
|
||||||
|
|||||||
@@ -110,12 +110,13 @@ services:
|
|||||||
target: /etc/clickhouse-server/custom-function.xml
|
target: /etc/clickhouse-server/custom-function.xml
|
||||||
- source: clickhouse-cluster
|
- source: clickhouse-cluster
|
||||||
target: /etc/clickhouse-server/config.d/cluster.xml
|
target: /etc/clickhouse-server/config.d/cluster.xml
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
- clickhouse:/var/lib/clickhouse/
|
- clickhouse:/var/lib/clickhouse/
|
||||||
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
|
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
|
||||||
signoz:
|
signoz:
|
||||||
!!merge <<: *db-depend
|
!!merge <<: *db-depend
|
||||||
image: signoz/signoz:v0.90.1
|
image: signoz/signoz:v0.87.0
|
||||||
command:
|
command:
|
||||||
- --config=/root/config/prometheus.yml
|
- --config=/root/config/prometheus.yml
|
||||||
ports:
|
ports:
|
||||||
@@ -135,7 +136,6 @@ services:
|
|||||||
- GODEBUG=netdns=go
|
- GODEBUG=netdns=go
|
||||||
- TELEMETRY_ENABLED=true
|
- TELEMETRY_ENABLED=true
|
||||||
- DEPLOYMENT_TYPE=docker-swarm
|
- DEPLOYMENT_TYPE=docker-swarm
|
||||||
- DOT_METRICS_ENABLED=true
|
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test:
|
test:
|
||||||
- CMD
|
- CMD
|
||||||
@@ -148,7 +148,7 @@ services:
|
|||||||
retries: 3
|
retries: 3
|
||||||
otel-collector:
|
otel-collector:
|
||||||
!!merge <<: *db-depend
|
!!merge <<: *db-depend
|
||||||
image: signoz/signoz-otel-collector:v0.128.2
|
image: signoz/signoz-otel-collector:v0.111.42
|
||||||
command:
|
command:
|
||||||
- --config=/etc/otel-collector-config.yaml
|
- --config=/etc/otel-collector-config.yaml
|
||||||
- --manager-config=/etc/manager-config.yaml
|
- --manager-config=/etc/manager-config.yaml
|
||||||
@@ -174,7 +174,7 @@ services:
|
|||||||
- signoz
|
- signoz
|
||||||
schema-migrator:
|
schema-migrator:
|
||||||
!!merge <<: *common
|
!!merge <<: *common
|
||||||
image: signoz/signoz-schema-migrator:v0.128.2
|
image: signoz/signoz-schema-migrator:v0.111.42
|
||||||
deploy:
|
deploy:
|
||||||
restart_policy:
|
restart_policy:
|
||||||
condition: on-failure
|
condition: on-failure
|
||||||
@@ -195,6 +195,7 @@ volumes:
|
|||||||
name: signoz-sqlite
|
name: signoz-sqlite
|
||||||
zookeeper-1:
|
zookeeper-1:
|
||||||
name: signoz-zookeeper-1
|
name: signoz-zookeeper-1
|
||||||
|
|
||||||
configs:
|
configs:
|
||||||
clickhouse-config:
|
clickhouse-config:
|
||||||
file: ../common/clickhouse/config.xml
|
file: ../common/clickhouse/config.xml
|
||||||
@@ -204,6 +205,7 @@ configs:
|
|||||||
file: ../common/clickhouse/custom-function.xml
|
file: ../common/clickhouse/custom-function.xml
|
||||||
clickhouse-cluster:
|
clickhouse-cluster:
|
||||||
file: ../common/clickhouse/cluster.xml
|
file: ../common/clickhouse/cluster.xml
|
||||||
|
|
||||||
signoz-prometheus-config:
|
signoz-prometheus-config:
|
||||||
file: ../common/signoz/prometheus.yml
|
file: ../common/signoz/prometheus.yml
|
||||||
# If you have multiple dashboard files, you can list them individually:
|
# If you have multiple dashboard files, you can list them individually:
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ processors:
|
|||||||
detectors: [env, system]
|
detectors: [env, system]
|
||||||
timeout: 2s
|
timeout: 2s
|
||||||
signozspanmetrics/delta:
|
signozspanmetrics/delta:
|
||||||
metrics_exporter: signozclickhousemetrics
|
metrics_exporter: clickhousemetricswrite, signozclickhousemetrics
|
||||||
metrics_flush_interval: 60s
|
metrics_flush_interval: 60s
|
||||||
latency_histogram_buckets: [100us, 1ms, 2ms, 6ms, 10ms, 50ms, 100ms, 250ms, 500ms, 1000ms, 1400ms, 2000ms, 5s, 10s, 20s, 40s, 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
|
dimensions_cache_size: 100000
|
||||||
@@ -60,16 +60,27 @@ exporters:
|
|||||||
datasource: tcp://clickhouse:9000/signoz_traces
|
datasource: tcp://clickhouse:9000/signoz_traces
|
||||||
low_cardinal_exception_grouping: ${env:LOW_CARDINAL_EXCEPTION_GROUPING}
|
low_cardinal_exception_grouping: ${env:LOW_CARDINAL_EXCEPTION_GROUPING}
|
||||||
use_new_schema: true
|
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:
|
signozclickhousemetrics:
|
||||||
dsn: tcp://clickhouse:9000/signoz_metrics
|
dsn: tcp://clickhouse:9000/signoz_metrics
|
||||||
clickhouselogsexporter:
|
clickhouselogsexporter:
|
||||||
dsn: tcp://clickhouse:9000/signoz_logs
|
dsn: tcp://clickhouse:9000/signoz_logs
|
||||||
timeout: 10s
|
timeout: 10s
|
||||||
use_new_schema: true
|
use_new_schema: true
|
||||||
|
# debug: {}
|
||||||
service:
|
service:
|
||||||
telemetry:
|
telemetry:
|
||||||
logs:
|
logs:
|
||||||
encoding: json
|
encoding: json
|
||||||
|
metrics:
|
||||||
|
address: 0.0.0.0:8888
|
||||||
extensions:
|
extensions:
|
||||||
- health_check
|
- health_check
|
||||||
- pprof
|
- pprof
|
||||||
@@ -81,11 +92,11 @@ service:
|
|||||||
metrics:
|
metrics:
|
||||||
receivers: [otlp]
|
receivers: [otlp]
|
||||||
processors: [batch]
|
processors: [batch]
|
||||||
exporters: [signozclickhousemetrics]
|
exporters: [clickhousemetricswrite, signozclickhousemetrics]
|
||||||
metrics/prometheus:
|
metrics/prometheus:
|
||||||
receivers: [prometheus]
|
receivers: [prometheus]
|
||||||
processors: [batch]
|
processors: [batch]
|
||||||
exporters: [signozclickhousemetrics]
|
exporters: [clickhousemetricswrite/prometheus, signozclickhousemetrics]
|
||||||
logs:
|
logs:
|
||||||
receivers: [otlp]
|
receivers: [otlp]
|
||||||
processors: [batch]
|
processors: [batch]
|
||||||
|
|||||||
@@ -177,7 +177,7 @@ services:
|
|||||||
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
|
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
|
||||||
signoz:
|
signoz:
|
||||||
!!merge <<: *db-depend
|
!!merge <<: *db-depend
|
||||||
image: signoz/signoz:${VERSION:-v0.90.1}
|
image: signoz/signoz:${VERSION:-v0.87.0}
|
||||||
container_name: signoz
|
container_name: signoz
|
||||||
command:
|
command:
|
||||||
- --config=/root/config/prometheus.yml
|
- --config=/root/config/prometheus.yml
|
||||||
@@ -197,7 +197,6 @@ services:
|
|||||||
- GODEBUG=netdns=go
|
- GODEBUG=netdns=go
|
||||||
- TELEMETRY_ENABLED=true
|
- TELEMETRY_ENABLED=true
|
||||||
- DEPLOYMENT_TYPE=docker-standalone-amd
|
- DEPLOYMENT_TYPE=docker-standalone-amd
|
||||||
- DOT_METRICS_ENABLED=true
|
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test:
|
test:
|
||||||
- CMD
|
- CMD
|
||||||
@@ -211,7 +210,7 @@ services:
|
|||||||
# TODO: support otel-collector multiple replicas. Nginx/Traefik for loadbalancing?
|
# TODO: support otel-collector multiple replicas. Nginx/Traefik for loadbalancing?
|
||||||
otel-collector:
|
otel-collector:
|
||||||
!!merge <<: *db-depend
|
!!merge <<: *db-depend
|
||||||
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-v0.128.2}
|
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-v0.111.42}
|
||||||
container_name: signoz-otel-collector
|
container_name: signoz-otel-collector
|
||||||
command:
|
command:
|
||||||
- --config=/etc/otel-collector-config.yaml
|
- --config=/etc/otel-collector-config.yaml
|
||||||
@@ -237,7 +236,7 @@ services:
|
|||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
schema-migrator-sync:
|
schema-migrator-sync:
|
||||||
!!merge <<: *common
|
!!merge <<: *common
|
||||||
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-v0.128.2}
|
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-v0.111.42}
|
||||||
container_name: schema-migrator-sync
|
container_name: schema-migrator-sync
|
||||||
command:
|
command:
|
||||||
- sync
|
- sync
|
||||||
@@ -248,7 +247,7 @@ services:
|
|||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
schema-migrator-async:
|
schema-migrator-async:
|
||||||
!!merge <<: *db-depend
|
!!merge <<: *db-depend
|
||||||
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-v0.128.2}
|
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-v0.111.42}
|
||||||
container_name: schema-migrator-async
|
container_name: schema-migrator-async
|
||||||
command:
|
command:
|
||||||
- async
|
- async
|
||||||
|
|||||||
@@ -110,7 +110,7 @@ services:
|
|||||||
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
|
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
|
||||||
signoz:
|
signoz:
|
||||||
!!merge <<: *db-depend
|
!!merge <<: *db-depend
|
||||||
image: signoz/signoz:${VERSION:-v0.90.1}
|
image: signoz/signoz:${VERSION:-v0.87.0}
|
||||||
container_name: signoz
|
container_name: signoz
|
||||||
command:
|
command:
|
||||||
- --config=/root/config/prometheus.yml
|
- --config=/root/config/prometheus.yml
|
||||||
@@ -130,7 +130,6 @@ services:
|
|||||||
- GODEBUG=netdns=go
|
- GODEBUG=netdns=go
|
||||||
- TELEMETRY_ENABLED=true
|
- TELEMETRY_ENABLED=true
|
||||||
- DEPLOYMENT_TYPE=docker-standalone-amd
|
- DEPLOYMENT_TYPE=docker-standalone-amd
|
||||||
- DOT_METRICS_ENABLED=true
|
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test:
|
test:
|
||||||
- CMD
|
- CMD
|
||||||
@@ -143,7 +142,7 @@ services:
|
|||||||
retries: 3
|
retries: 3
|
||||||
otel-collector:
|
otel-collector:
|
||||||
!!merge <<: *db-depend
|
!!merge <<: *db-depend
|
||||||
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-v0.128.2}
|
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-v0.111.42}
|
||||||
container_name: signoz-otel-collector
|
container_name: signoz-otel-collector
|
||||||
command:
|
command:
|
||||||
- --config=/etc/otel-collector-config.yaml
|
- --config=/etc/otel-collector-config.yaml
|
||||||
@@ -165,7 +164,7 @@ services:
|
|||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
schema-migrator-sync:
|
schema-migrator-sync:
|
||||||
!!merge <<: *common
|
!!merge <<: *common
|
||||||
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-v0.128.2}
|
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-v0.111.42}
|
||||||
container_name: schema-migrator-sync
|
container_name: schema-migrator-sync
|
||||||
command:
|
command:
|
||||||
- sync
|
- sync
|
||||||
@@ -177,7 +176,7 @@ services:
|
|||||||
restart: on-failure
|
restart: on-failure
|
||||||
schema-migrator-async:
|
schema-migrator-async:
|
||||||
!!merge <<: *db-depend
|
!!merge <<: *db-depend
|
||||||
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-v0.128.2}
|
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-v0.111.42}
|
||||||
container_name: schema-migrator-async
|
container_name: schema-migrator-async
|
||||||
command:
|
command:
|
||||||
- async
|
- async
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ processors:
|
|||||||
detectors: [env, system]
|
detectors: [env, system]
|
||||||
timeout: 2s
|
timeout: 2s
|
||||||
signozspanmetrics/delta:
|
signozspanmetrics/delta:
|
||||||
metrics_exporter: signozclickhousemetrics
|
metrics_exporter: clickhousemetricswrite, signozclickhousemetrics
|
||||||
metrics_flush_interval: 60s
|
metrics_flush_interval: 60s
|
||||||
latency_histogram_buckets: [100us, 1ms, 2ms, 6ms, 10ms, 50ms, 100ms, 250ms, 500ms, 1000ms, 1400ms, 2000ms, 5s, 10s, 20s, 40s, 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
|
dimensions_cache_size: 100000
|
||||||
@@ -60,16 +60,27 @@ exporters:
|
|||||||
datasource: tcp://clickhouse:9000/signoz_traces
|
datasource: tcp://clickhouse:9000/signoz_traces
|
||||||
low_cardinal_exception_grouping: ${env:LOW_CARDINAL_EXCEPTION_GROUPING}
|
low_cardinal_exception_grouping: ${env:LOW_CARDINAL_EXCEPTION_GROUPING}
|
||||||
use_new_schema: true
|
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:
|
signozclickhousemetrics:
|
||||||
dsn: tcp://clickhouse:9000/signoz_metrics
|
dsn: tcp://clickhouse:9000/signoz_metrics
|
||||||
clickhouselogsexporter:
|
clickhouselogsexporter:
|
||||||
dsn: tcp://clickhouse:9000/signoz_logs
|
dsn: tcp://clickhouse:9000/signoz_logs
|
||||||
timeout: 10s
|
timeout: 10s
|
||||||
use_new_schema: true
|
use_new_schema: true
|
||||||
|
# debug: {}
|
||||||
service:
|
service:
|
||||||
telemetry:
|
telemetry:
|
||||||
logs:
|
logs:
|
||||||
encoding: json
|
encoding: json
|
||||||
|
metrics:
|
||||||
|
address: 0.0.0.0:8888
|
||||||
extensions:
|
extensions:
|
||||||
- health_check
|
- health_check
|
||||||
- pprof
|
- pprof
|
||||||
@@ -81,11 +92,11 @@ service:
|
|||||||
metrics:
|
metrics:
|
||||||
receivers: [otlp]
|
receivers: [otlp]
|
||||||
processors: [batch]
|
processors: [batch]
|
||||||
exporters: [signozclickhousemetrics]
|
exporters: [clickhousemetricswrite, signozclickhousemetrics]
|
||||||
metrics/prometheus:
|
metrics/prometheus:
|
||||||
receivers: [prometheus]
|
receivers: [prometheus]
|
||||||
processors: [batch]
|
processors: [batch]
|
||||||
exporters: [signozclickhousemetrics]
|
exporters: [clickhousemetricswrite/prometheus, signozclickhousemetrics]
|
||||||
logs:
|
logs:
|
||||||
receivers: [otlp]
|
receivers: [otlp]
|
||||||
processors: [batch]
|
processors: [batch]
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ __Table of Contents__
|
|||||||
- [Prerequisites](#prerequisites-1)
|
- [Prerequisites](#prerequisites-1)
|
||||||
- [Install Helm Repo and Charts](#install-helm-repo-and-charts)
|
- [Install Helm Repo and Charts](#install-helm-repo-and-charts)
|
||||||
- [Start the OpenTelemetry Demo App](#start-the-opentelemetry-demo-app-1)
|
- [Start the OpenTelemetry Demo App](#start-the-opentelemetry-demo-app-1)
|
||||||
- [Monitor with SigNoz (Kubernetes)](#monitor-with-signoz-kubernetes)
|
- [Moniitor with SigNoz (Kubernetes)](#monitor-with-signoz-kubernetes)
|
||||||
- [What's next](#whats-next)
|
- [What's next](#whats-next)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -203,6 +203,17 @@ func NewServer(config signoz.Config, signoz *signoz.SigNoz, jwt *authtypes.JWT)
|
|||||||
&opAmpModel.AllAgents, agentConfMgr, signoz.Instrumentation,
|
&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
|
return s, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import (
|
|||||||
"github.com/SigNoz/signoz/ee/licensing"
|
"github.com/SigNoz/signoz/ee/licensing"
|
||||||
"github.com/SigNoz/signoz/ee/licensing/httplicensing"
|
"github.com/SigNoz/signoz/ee/licensing/httplicensing"
|
||||||
"github.com/SigNoz/signoz/ee/query-service/app"
|
"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/sqlstore/postgressqlstore"
|
||||||
"github.com/SigNoz/signoz/ee/zeus"
|
"github.com/SigNoz/signoz/ee/zeus"
|
||||||
"github.com/SigNoz/signoz/ee/zeus/httpzeus"
|
"github.com/SigNoz/signoz/ee/zeus/httpzeus"
|
||||||
@@ -22,7 +21,6 @@ import (
|
|||||||
"github.com/SigNoz/signoz/pkg/modules/organization"
|
"github.com/SigNoz/signoz/pkg/modules/organization"
|
||||||
baseconst "github.com/SigNoz/signoz/pkg/query-service/constants"
|
baseconst "github.com/SigNoz/signoz/pkg/query-service/constants"
|
||||||
"github.com/SigNoz/signoz/pkg/signoz"
|
"github.com/SigNoz/signoz/pkg/signoz"
|
||||||
"github.com/SigNoz/signoz/pkg/sqlschema"
|
|
||||||
"github.com/SigNoz/signoz/pkg/sqlstore"
|
"github.com/SigNoz/signoz/pkg/sqlstore"
|
||||||
"github.com/SigNoz/signoz/pkg/sqlstore/sqlstorehook"
|
"github.com/SigNoz/signoz/pkg/sqlstore/sqlstorehook"
|
||||||
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
||||||
@@ -147,14 +145,6 @@ func main() {
|
|||||||
signoz.NewEmailingProviderFactories(),
|
signoz.NewEmailingProviderFactories(),
|
||||||
signoz.NewCacheProviderFactories(),
|
signoz.NewCacheProviderFactories(),
|
||||||
signoz.NewWebProviderFactories(),
|
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,
|
sqlStoreFactories,
|
||||||
signoz.NewTelemetryStoreProviderFactories(),
|
signoz.NewTelemetryStoreProviderFactories(),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,36 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
@@ -1,285 +0,0 @@
|
|||||||
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,4 +1,5 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
|
ignorePatterns: ['src/parser/*.ts'],
|
||||||
env: {
|
env: {
|
||||||
browser: true,
|
browser: true,
|
||||||
es2021: true,
|
es2021: true,
|
||||||
|
|||||||
154
frontend/docs/QuerySearch.md
Normal file
@@ -0,0 +1,154 @@
|
|||||||
|
# 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
|
||||||
@@ -28,6 +28,8 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ant-design/colors": "6.0.0",
|
"@ant-design/colors": "6.0.0",
|
||||||
"@ant-design/icons": "4.8.0",
|
"@ant-design/icons": "4.8.0",
|
||||||
|
"@codemirror/autocomplete": "6.18.6",
|
||||||
|
"@codemirror/lang-javascript": "6.2.3",
|
||||||
"@dnd-kit/core": "6.1.0",
|
"@dnd-kit/core": "6.1.0",
|
||||||
"@dnd-kit/modifiers": "7.0.0",
|
"@dnd-kit/modifiers": "7.0.0",
|
||||||
"@dnd-kit/sortable": "8.0.0",
|
"@dnd-kit/sortable": "8.0.0",
|
||||||
@@ -43,6 +45,8 @@
|
|||||||
"@signozhq/design-tokens": "1.1.4",
|
"@signozhq/design-tokens": "1.1.4",
|
||||||
"@tanstack/react-table": "8.20.6",
|
"@tanstack/react-table": "8.20.6",
|
||||||
"@tanstack/react-virtual": "3.11.2",
|
"@tanstack/react-virtual": "3.11.2",
|
||||||
|
"@uiw/codemirror-theme-copilot": "4.23.11",
|
||||||
|
"@uiw/react-codemirror": "4.23.10",
|
||||||
"@uiw/react-md-editor": "3.23.5",
|
"@uiw/react-md-editor": "3.23.5",
|
||||||
"@visx/group": "3.3.0",
|
"@visx/group": "3.3.0",
|
||||||
"@visx/hierarchy": "3.12.0",
|
"@visx/hierarchy": "3.12.0",
|
||||||
@@ -53,6 +57,7 @@
|
|||||||
"antd": "5.11.0",
|
"antd": "5.11.0",
|
||||||
"antd-table-saveas-excel": "2.2.1",
|
"antd-table-saveas-excel": "2.2.1",
|
||||||
"axios": "1.8.2",
|
"axios": "1.8.2",
|
||||||
|
"antlr4": "4.13.2",
|
||||||
"babel-eslint": "^10.1.0",
|
"babel-eslint": "^10.1.0",
|
||||||
"babel-jest": "^29.6.4",
|
"babel-jest": "^29.6.4",
|
||||||
"babel-loader": "9.1.3",
|
"babel-loader": "9.1.3",
|
||||||
@@ -213,9 +218,7 @@
|
|||||||
"eslint-plugin-simple-import-sort": "^7.0.0",
|
"eslint-plugin-simple-import-sort": "^7.0.0",
|
||||||
"eslint-plugin-sonarjs": "^0.12.0",
|
"eslint-plugin-sonarjs": "^0.12.0",
|
||||||
"husky": "^7.0.4",
|
"husky": "^7.0.4",
|
||||||
"image-minimizer-webpack-plugin": "^4.0.0",
|
"image-webpack-loader": "8.1.0",
|
||||||
"imagemin": "^8.0.1",
|
|
||||||
"imagemin-svgo": "^10.0.1",
|
|
||||||
"is-ci": "^3.0.1",
|
"is-ci": "^3.0.1",
|
||||||
"jest-styled-components": "^7.0.8",
|
"jest-styled-components": "^7.0.8",
|
||||||
"lint-staged": "^12.5.0",
|
"lint-staged": "^12.5.0",
|
||||||
@@ -232,7 +235,6 @@
|
|||||||
"redux-mock-store": "1.5.4",
|
"redux-mock-store": "1.5.4",
|
||||||
"sass": "1.66.1",
|
"sass": "1.66.1",
|
||||||
"sass-loader": "13.3.2",
|
"sass-loader": "13.3.2",
|
||||||
"sharp": "^0.33.4",
|
|
||||||
"ts-jest": "^27.1.5",
|
"ts-jest": "^27.1.5",
|
||||||
"ts-node": "^10.2.1",
|
"ts-node": "^10.2.1",
|
||||||
"typescript-plugin-css-modules": "5.0.1",
|
"typescript-plugin-css-modules": "5.0.1",
|
||||||
@@ -257,7 +259,6 @@
|
|||||||
"cross-spawn": "7.0.5",
|
"cross-spawn": "7.0.5",
|
||||||
"cookie": "^0.7.1",
|
"cookie": "^0.7.1",
|
||||||
"serialize-javascript": "6.0.2",
|
"serialize-javascript": "6.0.2",
|
||||||
"prismjs": "1.30.0",
|
"prismjs": "1.30.0"
|
||||||
"got": "11.8.5"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 6.1 KiB |
@@ -1 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 18 18"><defs><linearGradient id="a" x1="2.59" y1="10.16" x2="15.41" y2="10.16" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#005ba1"/><stop offset=".07" stop-color="#0060a9"/><stop offset=".36" stop-color="#0071c8"/><stop offset=".52" stop-color="#0078d4"/><stop offset=".64" stop-color="#0074cd"/><stop offset=".82" stop-color="#006abb"/><stop offset="1" stop-color="#005ba1"/></linearGradient></defs><path d="M9 5.14c-3.54 0-6.41-1-6.41-2.32v12.36c0 1.27 2.82 2.3 6.32 2.32H9c3.54 0 6.41-1 6.41-2.32V2.82c0 1.29-2.87 2.32-6.41 2.32z" fill="url(#a)"/><path d="M15.41 2.82c0 1.29-2.87 2.32-6.41 2.32s-6.41-1-6.41-2.32S5.46.5 9 .5s6.41 1 6.41 2.32" fill="#e8e8e8"/><path d="M13.92 2.63c0 .82-2.21 1.48-4.92 1.48s-4.92-.66-4.92-1.48S6.29 1.16 9 1.16s4.92.66 4.92 1.47" fill="#50e6ff"/><path d="M9 3a11.55 11.55 0 00-3.89.57A11.42 11.42 0 009 4.11a11.15 11.15 0 003.89-.58A11.84 11.84 0 009 3z" fill="#198ab3"/><path d="M12.64 9v1.63h-1a.39.39 0 01-.29-.14V9H10v1.78a.92.92 0 001 .89h1.49l.26-.13s-.11.41-.26.43h-2.38v1h2.66A1.21 1.21 0 0014 11.7V9zM9.53 9v-.49a.7.7 0 00-.48-.77 1.74 1.74 0 00-.5-.08.94.94 0 00-.91.58l-.78 1.9-1-1.9A.93.93 0 005 7.66a1.44 1.44 0 00-.51.09c-.35.11-.43.34-.43.73v3.31h1.17V9.56l.63 1.57a1.08 1.08 0 001 .66c.44 0 .62-.26.8-.66l.67-1.51v2.15h1.18V9z" fill="#f2f2f2"/></svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.3 KiB |
@@ -1 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" width="64" height="64"><path d="M8.16 23h21.177v-5.86l-4.023-2.307-.694-.3-16.46.113z" fill="#fff"/><path d="M22.012 22.222c.197-.675.122-1.294-.206-1.754-.3-.422-.807-.666-1.416-.694l-11.545-.15c-.075 0-.14-.038-.178-.094s-.047-.13-.028-.206c.038-.113.15-.197.272-.206l11.648-.15c1.38-.066 2.88-1.182 3.404-2.55l.666-1.735a.38.38 0 0 0 .02-.225c-.75-3.395-3.78-5.927-7.4-5.927-3.34 0-6.17 2.157-7.184 5.15-.657-.488-1.5-.75-2.392-.666-1.604.16-2.9 1.444-3.048 3.048a3.58 3.58 0 0 0 .084 1.191A4.84 4.84 0 0 0 0 22.1c0 .234.02.47.047.703.02.113.113.197.225.197H21.58a.29.29 0 0 0 .272-.206l.16-.572z" fill="#f38020"/><path d="M25.688 14.803l-.32.01c-.075 0-.14.056-.17.13l-.45 1.566c-.197.675-.122 1.294.206 1.754.3.422.807.666 1.416.694l2.457.15c.075 0 .14.038.178.094s.047.14.028.206c-.038.113-.15.197-.272.206l-2.56.15c-1.388.066-2.88 1.182-3.404 2.55l-.188.478c-.038.094.028.188.13.188h8.797a.23.23 0 0 0 .225-.169A6.41 6.41 0 0 0 32 21.106a6.32 6.32 0 0 0-6.312-6.302" fill="#faae40"/></svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.0 KiB |
|
Before Width: | Height: | Size: 6.2 KiB |
@@ -1,2 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
|
||||||
<svg width="800px" height="800px" viewBox="0 0 256 256" xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMinYMin meet"><path d="M255.96 134.393c0-21.521-13.373-40.117-33.223-47.43a75.239 75.239 0 0 0 1.253-13.791c0-39.909-32.386-72.295-72.295-72.295-23.193 0-44.923 11.074-58.505 30.088-6.686-5.224-14.835-7.94-23.402-7.94-21.104 0-38.446 17.133-38.446 38.446 0 4.597.836 9.194 2.298 13.373C13.582 81.739 0 100.962 0 122.274c0 21.522 13.373 40.327 33.431 47.64-.835 4.388-1.253 8.985-1.253 13.79 0 39.7 32.386 72.087 72.086 72.087 23.402 0 44.924-11.283 58.505-30.088 6.686 5.223 15.044 8.149 23.611 8.149 21.104 0 38.446-17.134 38.446-38.446 0-4.597-.836-9.194-2.298-13.373 19.64-7.104 33.431-26.327 33.431-47.64z" fill="#FFF"/><path d="M100.085 110.364l57.043 26.119 57.669-50.565a64.312 64.312 0 0 0 1.253-12.746c0-35.52-28.834-64.355-64.355-64.355-21.313 0-41.162 10.447-53.072 27.998l-9.612 49.73 11.074 23.82z" fill="#F4BD19"/><path d="M40.953 170.75c-.835 4.179-1.253 8.567-1.253 12.955 0 35.52 29.043 64.564 64.564 64.564 21.522 0 41.372-10.656 53.49-28.208l9.403-49.729-12.746-24.238-57.251-26.118-56.207 50.774z" fill="#3CBEB1"/><path d="M40.536 71.918l39.073 9.194 8.775-44.506c-5.432-4.179-11.91-6.268-18.805-6.268-16.925 0-30.924 13.79-30.924 30.924 0 3.552.627 7.313 1.88 10.656z" fill="#E9478C"/><path d="M37.192 81.32c-17.551 5.642-29.67 22.567-29.67 40.954 0 17.97 11.074 34.059 27.79 40.327l54.953-49.73-10.03-21.52-43.043-10.03z" fill="#2C458F"/><path d="M167.784 219.852c5.432 4.18 11.91 6.478 18.596 6.478 16.925 0 30.924-13.79 30.924-30.924 0-3.761-.627-7.314-1.88-10.657l-39.073-9.193-8.567 44.296z" fill="#95C63D"/><path d="M175.724 165.317l43.043 10.03c17.551-5.85 29.67-22.566 29.67-40.954 0-17.97-11.074-33.849-27.79-40.326l-56.415 49.311 11.492 21.94z" fill="#176655"/></svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.9 KiB |
@@ -1,19 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
|
|
||||||
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
|
||||||
<svg version="1.1" baseProfile="tiny" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
|
|
||||||
width="800px" height="800px" viewBox="0 0 24 24" overflow="visible" xml:space="preserve">
|
|
||||||
<g >
|
|
||||||
<rect y="0" fill="none" width="24" height="24"/>
|
|
||||||
<g transform="translate(1.000000, 8.000000)">
|
|
||||||
<path fill-rule="evenodd" fill="#5C85DE" d="M2-1.9c-1.1,0-2.3,1.1-2.3,2.2V10H2V5.5h2.2V10h2.2V0.3c0-1.1-1.1-2.2-2.3-2.2H2
|
|
||||||
L2-1.9z M2,3.2v-3h2.2v3H2L2,3.2z"/>
|
|
||||||
<path fill-rule="evenodd" fill="#5C85DE" d="M10.3-2C9.1-2,8-0.9,8,0.2V10l2.2,0V5.5h2.2c1.1,0,2.3-1.1,2.3-2.2l0-3
|
|
||||||
c0-1.1-1.1-2.2-2.3-2.2H10.3L10.3-2z M10.2,3.2v-3h2.2v3H10.2L10.2,3.2z"/>
|
|
||||||
<polygon fill-rule="evenodd" fill="#5C85DE" points="18.5,0.3 18.5,7.8 16.2,7.8 16.2,10 23,10 23,7.8 20.8,7.8 20.8,0.3 23,0.3
|
|
||||||
23,-1.9 16.2,-1.9 16.2,0.3 "/>
|
|
||||||
<polygon fill-rule="evenodd" fill="#3367D6" points="2,5.5 2,3.2 3.5,3.2 "/>
|
|
||||||
<polygon fill-rule="evenodd" fill="#3367D6" points="10.2,5.5 10.2,3.2 11.5,3.2 "/>
|
|
||||||
<polygon fill-rule="evenodd" fill="#3367D6" points="18.5,1.8 18.5,1.8 18.5,0.3 20.8,0.3 "/>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
|
Before Width: | Height: | Size: 1.2 KiB |
@@ -1 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128"><path fill="#2088ff" d="M26.666 0C11.97 0 0 11.97 0 26.666c0 12.87 9.181 23.651 21.334 26.13v37.87c0 11.77 9.68 21.334 21.332 21.334h.195c1.302 9.023 9.1 16 18.473 16C71.612 128 80 119.612 80 109.334s-8.388-18.668-18.666-18.668c-9.372 0-17.17 6.977-18.473 16h-.195c-8.737 0-16-7.152-16-16V63.779a18.514 18.514 0 0 0 13.24 5.555h2.955c1.303 9.023 9.1 16 18.473 16 9.372 0 17.169-6.977 18.47-16h11.057c1.303 9.023 9.1 16 18.473 16 10.278 0 18.666-8.39 18.666-18.668C128 56.388 119.612 48 109.334 48c-9.373 0-17.171 6.977-18.473 16H79.805c-1.301-9.023-9.098-16-18.471-16s-17.171 6.977-18.473 16h-2.955c-6.433 0-11.793-4.589-12.988-10.672 14.58-.136 26.416-12.05 26.416-26.662C53.334 11.97 41.362 0 26.666 0zm0 5.334A21.292 21.292 0 0 1 48 26.666 21.294 21.294 0 0 1 26.666 48 21.292 21.292 0 0 1 5.334 26.666 21.29 21.29 0 0 1 26.666 5.334zm-5.215 7.541C18.67 12.889 16 15.123 16 18.166v17.043c0 4.043 4.709 6.663 8.145 4.533l13.634-8.455c3.257-2.02 3.274-7.002.032-9.045l-13.635-8.59a5.024 5.024 0 0 0-2.725-.777zm-.117 5.291 13.635 8.588-13.635 8.455V18.166zm40 35.168a13.29 13.29 0 0 1 13.332 13.332A13.293 13.293 0 0 1 61.334 80 13.294 13.294 0 0 1 48 66.666a13.293 13.293 0 0 1 13.334-13.332zm48 0a13.29 13.29 0 0 1 13.332 13.332A13.293 13.293 0 0 1 109.334 80 13.294 13.294 0 0 1 96 66.666a13.293 13.293 0 0 1 13.334-13.332zm-42.568 6.951a2.667 2.667 0 0 0-1.887.78l-6.3 6.294-2.093-2.084a2.667 2.667 0 0 0-3.771.006 2.667 2.667 0 0 0 .008 3.772l3.974 3.96a2.667 2.667 0 0 0 3.766-.001l8.185-8.174a2.667 2.667 0 0 0 .002-3.772 2.667 2.667 0 0 0-1.884-.78zm48 0a2.667 2.667 0 0 0-1.887.78l-6.3 6.294-2.093-2.084a2.667 2.667 0 0 0-3.771.006 2.667 2.667 0 0 0 .008 3.772l3.974 3.96a2.667 2.667 0 0 0 3.766-.001l8.185-8.174a2.667 2.667 0 0 0 .002-3.772 2.667 2.667 0 0 0-1.884-.78zM61.334 96a13.293 13.293 0 0 1 13.332 13.334 13.29 13.29 0 0 1-13.332 13.332A13.293 13.293 0 0 1 48 109.334 13.294 13.294 0 0 1 61.334 96zM56 105.334c-2.193 0-4 1.807-4 4 0 2.195 1.808 4 4 4s4-1.805 4-4c0-2.193-1.807-4-4-4zm10.666 0c-2.193 0-4 1.807-4 4 0 2.195 1.808 4 4 4s4-1.805 4-4c0-2.193-1.807-4-4-4zM56 108c.75 0 1.334.585 1.334 1.334 0 .753-.583 1.332-1.334 1.332-.75 0-1.334-.58-1.334-1.332 0-.75.585-1.334 1.334-1.334zm10.666 0c.75 0 1.334.585 1.334 1.334 0 .753-.583 1.332-1.334 1.332-.75 0-1.332-.58-1.332-1.332 0-.75.583-1.334 1.332-1.334z"/><path fill="#79b8ff" d="M109.334 90.666c-9.383 0-17.188 6.993-18.477 16.031a2.667 2.667 0 0 0-.265-.011l-2.7.09a2.667 2.667 0 0 0-2.578 2.751 2.667 2.667 0 0 0 2.752 2.578l2.7-.087a2.667 2.667 0 0 0 .097-.006C92.17 121.029 99.965 128 109.334 128c10.278 0 18.666-8.388 18.666-18.666s-8.388-18.668-18.666-18.668zm0 5.334a13.293 13.293 0 0 1 13.332 13.334 13.29 13.29 0 0 1-13.332 13.332A13.293 13.293 0 0 1 96 109.334 13.294 13.294 0 0 1 109.334 96z"/></svg>
|
|
||||||
|
Before Width: | Height: | Size: 2.8 KiB |
@@ -1,5 +0,0 @@
|
|||||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<g id="lucide/github">
|
|
||||||
<path id="Vector" d="M15 22V18C15.1391 16.7473 14.7799 15.4901 14 14.5C17 14.5 20 12.5 20 9C20.08 7.75 19.73 6.52 19 5.5C19.28 4.35 19.28 3.15 19 2C19 2 18 2 16 3.5C13.36 3 10.64 3 8 3.5C6 2 5 2 5 2C4.7 3.15 4.7 4.35 5 5.5C4.27187 6.51588 3.91847 7.75279 4 9C4 12.5 7 14.5 10 14.5C9.61 14.99 9.32 15.55 9.15 16.15C8.98 16.75 8.93 17.38 9 18M9 18V22M9 18C4.49 20 4 16 2 16" stroke="#C0C1C3" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 587 B |
@@ -1 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 373.71 200"><defs><style type="text/css">.cls-1{fill:#008ec7;}.cls-2{fill:#005b9b;}.cls-3{fill:#fff;}</style></defs><title>IETF-Badge-HTTP</title><g id="Layer_2"><path class="cls-1" d="M326,0H47.73L0,100,47.73,200H326l47.73-100ZM310.05,183.36H58.22L18.43,100,58.22,16.64H310.05L349.84,100Z"/><polygon class="cls-2" points="349.84 100.01 310.05 183.37 58.22 183.37 18.43 100.01 58.22 16.64 310.05 16.64 349.84 100.01"/><path class="cls-3" d="M128.05,71.89v59.53H114.27V107h-27v24.41H73.46V71.89H87.23V95.36h27V71.89Z"/><path class="cls-3" d="M154.5,83.12H135.45V71.89h51.87V83.12h-19v48.3H154.5Z"/><path class="cls-3" d="M207.9,83.12H188.85V71.89h51.87V83.12H221.67v48.3H207.9Z"/><path class="cls-3" d="M287.62,74.53a20.45,20.45,0,0,1,9,7.48,20.67,20.67,0,0,1,3.14,11.48,20.73,20.73,0,0,1-3.14,11.44,20.06,20.06,0,0,1-9,7.48A33.55,33.55,0,0,1,273.88,115h-12v16.42H248.12V71.89h25.76A33.05,33.05,0,0,1,287.62,74.53Zm-5.06,26.57a9.33,9.33,0,0,0,3.23-7.61c0-3.34-1.08-5.91-3.23-7.69s-5.3-2.68-9.44-2.68H261.89v20.66h11.23Q279.33,103.78,282.56,101.1Z"/></g></svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 13 KiB |
@@ -1 +0,0 @@
|
|||||||
<svg viewBox="0 0 832.8 959.8" xmlns="http://www.w3.org/2000/svg" width="2169" height="2500"><path d="M672.6 332.3l160.2-92.4v480L416.4 959.8V775.2l256.2-147.6z" fill="#00ac69"/><path d="M416.4 184.6L160.2 332.3 0 239.9 416.4 0l416.4 239.9-160.2 92.4z" fill="#1ce783"/><path d="M256.2 572.3L0 424.6V239.9l416.4 240v479.9l-160.2-92.2z" fill="#1d252c"/></svg>
|
|
||||||
|
Before Width: | Height: | Size: 357 B |
@@ -1 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48"><path fill="#fff" d="M44.559 19.646a11.957 11.957 0 0 0-1.028-9.822 12.094 12.094 0 0 0-13.026-5.802A11.962 11.962 0 0 0 21.485 0 12.097 12.097 0 0 0 9.95 8.373a11.964 11.964 0 0 0-7.997 5.8A12.097 12.097 0 0 0 3.44 28.356a11.957 11.957 0 0 0 1.028 9.822 12.094 12.094 0 0 0 13.026 5.802 11.953 11.953 0 0 0 9.02 4.02 12.096 12.096 0 0 0 11.54-8.379 11.964 11.964 0 0 0 7.997-5.8 12.099 12.099 0 0 0-1.491-14.177zM26.517 44.863a8.966 8.966 0 0 1-5.759-2.082 6.85 6.85 0 0 0 .284-.16L30.6 37.1c.49-.278.79-.799.786-1.361V22.265l4.04 2.332a.141.141 0 0 1 .078.111v11.16a9.006 9.006 0 0 1-8.987 8.995zM7.191 36.608a8.957 8.957 0 0 1-1.073-6.027c.071.042.195.119.284.17l9.558 5.52a1.556 1.556 0 0 0 1.57 0l11.67-6.738v4.665a.15.15 0 0 1-.057.124l-9.662 5.579a9.006 9.006 0 0 1-12.288-3.293zM4.675 15.744a8.966 8.966 0 0 1 4.682-3.943c0 .082-.005.228-.005.33v11.042a1.555 1.555 0 0 0 .785 1.359l11.669 6.736-4.04 2.333a.143.143 0 0 1-.136.012L7.967 28.03a9.006 9.006 0 0 1-3.293-12.284zm33.19 7.724L26.196 16.73l4.04-2.331a.143.143 0 0 1 .136-.012l9.664 5.579c4.302 2.485 5.776 7.989 3.29 12.29a8.991 8.991 0 0 1-4.68 3.943V24.827a1.553 1.553 0 0 0-.78-1.36zm4.02-6.051c-.07-.044-.195-.119-.283-.17l-9.558-5.52a1.556 1.556 0 0 0-1.57 0l-11.67 6.738V13.8a.15.15 0 0 1 .057-.124l9.662-5.574a8.995 8.995 0 0 1 13.36 9.315zm-25.277 8.315-4.04-2.333a.141.141 0 0 1-.079-.11v-11.16a8.997 8.997 0 0 1 14.753-6.91c-.073.04-.2.11-.283.161L17.4 10.9a1.552 1.552 0 0 0-.786 1.36l-.006 13.469zM18.803 21l5.198-3.002 5.197 3V27l-5.197 3-5.198-3z"/></svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.6 KiB |
@@ -1 +0,0 @@
|
|||||||
<svg height="2500" width="2500" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 80 80"><linearGradient id="a" x1="0%" y1="100%" y2="0%"><stop offset="0" stop-color="#1b660f"/><stop offset="1" stop-color="#6cae3e"/></linearGradient><g fill="none" fill-rule="evenodd"><path d="M0 0h80v80H0z" fill="url(#a)"/><path d="M60.836 42.893l.384-2.704c3.54 2.12 3.587 2.997 3.586 3.02-.006.006-.61.51-3.97-.316zm-1.943-.54C52.773 40.5 44.25 36.59 40.8 34.96c0-.014.004-.027.004-.041a2.406 2.406 0 0 0-2.404-2.403c-1.324 0-2.402 1.078-2.402 2.403s1.078 2.403 2.402 2.403c.582 0 1.11-.217 1.527-.562 4.058 1.92 12.515 5.774 18.68 7.594L56.17 61.56a.955.955 0 0 0-.01.14c0 1.516-6.707 4.299-17.666 4.299-11.075 0-17.853-2.783-17.853-4.298 0-.046-.003-.091-.01-.136l-5.093-37.207c4.409 3.035 13.892 4.64 22.962 4.64 9.056 0 18.523-1.6 22.94-4.625zM15 20.478C15.072 19.162 22.634 14 38.5 14c15.864 0 23.427 5.16 23.5 6.478v.449C61.13 23.877 51.33 27 38.5 27c-12.852 0-22.657-3.132-23.5-6.087zm49 .022c0-3.465-9.934-8.5-25.5-8.5S13 17.035 13 20.5l.094.754 5.548 40.524C18.775 66.31 30.86 68 38.494 68c9.472 0 19.535-2.178 19.665-6.22l2.396-16.896c1.333.319 2.43.482 3.31.482 1.184 0 1.984-.29 2.469-.867a1.95 1.95 0 0 0 .436-1.66c-.26-1.383-1.902-2.875-5.248-4.784l2.376-16.762z" fill="#fff"/></g></svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.3 KiB |
@@ -1 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" fill="#29b5e8"><path d="M9.86 15.298l13.008 7.8a3.72 3.72 0 0 0 4.589-.601 4.01 4.01 0 0 0 1.227-2.908V3.956a3.81 3.81 0 0 0-1.861-3.42 3.81 3.81 0 0 0-3.893 0 3.81 3.81 0 0 0-1.861 3.42v8.896l-7.387-4.43a3.79 3.79 0 0 0-2.922-.4c-.986.265-1.818.94-2.3 1.844-1.057 1.9-.44 4.28 1.4 5.422m31.27 7.8l13.008-7.8c1.84-1.143 2.458-3.533 1.4-5.424a3.75 3.75 0 0 0-5.22-1.452l-7.3 4.37v-8.84a3.81 3.81 0 1 0-7.615 0v15.323a4.08 4.08 0 0 0 .494 2.367c.482.903 1.314 1.57 2.3 1.844a3.71 3.71 0 0 0 2.922-.4M29.552 31.97c.013-.25.108-.5.272-.68l1.52-1.58a1.06 1.06 0 0 1 .658-.282h.057a1.05 1.05 0 0 1 .656.282l1.52 1.58a1.12 1.12 0 0 1 .272.681v.06a1.13 1.13 0 0 1-.272.683l-1.52 1.58a1.04 1.04 0 0 1-.656.284h-.057c-.246-.014-.48-.115-.658-.284l-1.52-1.58a1.13 1.13 0 0 1-.272-.683zm-4.604-.65v1.364a1.54 1.54 0 0 0 .372.93l5.16 5.357a1.42 1.42 0 0 0 .895.386h1.312a1.42 1.42 0 0 0 .895-.386l5.16-5.357a1.54 1.54 0 0 0 .372-.93V31.32a1.54 1.54 0 0 0-.372-.93l-5.16-5.357a1.42 1.42 0 0 0-.895-.386h-1.312a1.42 1.42 0 0 0-.895.386L25.32 30.4a1.55 1.55 0 0 0-.372.93M3.13 27.62l7.365 4.417L3.13 36.45a4.06 4.06 0 0 0-1.399 5.424 3.75 3.75 0 0 0 2.3 1.844c.986.274 2.042.133 2.922-.392l13.008-7.8c1.2-.762 1.9-2.078 1.9-3.492a4.16 4.16 0 0 0-1.9-3.492l-13.008-7.8a3.79 3.79 0 0 0-2.922-.4c-.986.265-1.818.94-2.3 1.844-1.057 1.9-.44 4.278 1.4 5.422m38.995 4.442a4 4 0 0 0 1.91 3.477l13 7.8c.88.524 1.934.666 2.92.392s1.817-.94 2.3-1.843a4.05 4.05 0 0 0-1.4-5.424L53.5 32.038l7.365-4.417c1.84-1.143 2.457-3.53 1.4-5.422a3.74 3.74 0 0 0-2.3-1.844c-.987-.274-2.042-.134-2.92.4l-13 7.8a4 4 0 0 0-1.91 3.507M25.48 40.508a3.7 3.7 0 0 0-2.611.464l-13.008 7.8c-1.84 1.143-2.456 3.53-1.4 5.422.483.903 1.314 1.57 2.3 1.843a3.75 3.75 0 0 0 2.922-.392l7.387-4.43v8.83a3.81 3.81 0 1 0 7.614 0V44.4a3.91 3.91 0 0 0-3.205-3.903m28.66 8.276l-13.008-7.8a3.75 3.75 0 0 0-2.922-.392 3.74 3.74 0 0 0-2.3 1.843 4.09 4.09 0 0 0-.494 2.37v15.25a3.81 3.81 0 1 0 7.614 0V51.28l7.287 4.37a3.79 3.79 0 0 0 2.922.4c.986-.265 1.818-.94 2.3-1.844 1.057-1.9.44-4.28-1.4-5.422"/></svg>
|
|
||||||
|
Before Width: | Height: | Size: 2.1 KiB |
@@ -1,18 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<svg width="80px" height="80px" viewBox="0 0 80 80" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
|
||||||
<!-- Generator: Sketch 64 (93537) - https://sketch.com -->
|
|
||||||
<title>Icon-Architecture/64/Arch_AWS-Simple-Notification-Service_64</title>
|
|
||||||
<desc>Created with Sketch.</desc>
|
|
||||||
<defs>
|
|
||||||
<linearGradient x1="0%" y1="100%" x2="100%" y2="0%" id="linearGradient-1">
|
|
||||||
<stop stop-color="#B0084D" offset="0%"></stop>
|
|
||||||
<stop stop-color="#FF4F8B" offset="100%"></stop>
|
|
||||||
</linearGradient>
|
|
||||||
</defs>
|
|
||||||
<g id="Icon-Architecture/64/Arch_AWS-Simple-Notification-Service_64" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
|
||||||
<g id="Icon-Architecture-BG/64/Application-Integration" fill="url(#linearGradient-1)">
|
|
||||||
<rect id="Rectangle" x="0" y="0" width="80" height="80"></rect>
|
|
||||||
</g>
|
|
||||||
<path d="M17,38 C18.103,38 19,38.897 19,40 C19,41.103 18.103,42 17,42 C15.897,42 15,41.103 15,40 C15,38.897 15.897,38 17,38 L17,38 Z M41,64 C29.314,64 19.289,55.466 17.194,43.98 C18.965,43.894 20.427,42.659 20.857,41 L27,41 L27,39 L20.857,39 C20.427,37.342 18.966,36.107 17.195,36.02 C19.285,24.71 29.511,16 41,16 C45.313,16 49.832,17.622 54.429,20.821 L55.571,19.179 C50.633,15.743 45.73,14 41,14 C28.27,14 16.949,23.865 15.063,36.521 C13.839,37.207 13,38.5 13,40 C13,41.5 13.839,42.793 15.063,43.478 C16.97,56.341 28.056,66 41,66 C46.407,66 51.942,64.157 56.585,60.811 L55.415,59.189 C51.11,62.292 45.991,64 41,64 L41,64 Z M30.101,36.442 C31.955,36.895 34.275,37 36,37 C37.642,37 39.823,36.905 41.629,36.506 L37.105,45.553 C37.036,45.691 37,45.845 37,46 L37,50.453 C36.199,50.964 34.833,51.812 34,51.986 L34,46 C34,45.868 33.974,45.737 33.923,45.615 L30.101,36.442 Z M36,33 C40.025,33 42.174,33.604 42.841,34 C42.174,34.396 40.025,35 36,35 C31.975,35 29.826,34.396 29.159,34 C29.826,33.604 31.975,33 36,33 L36,33 Z M33,54 L34,54 C34.043,54 34.086,53.997 34.128,53.992 C35.352,53.833 36.909,52.887 38.272,52.013 L38.535,51.845 C38.824,51.661 39,51.342 39,51 L39,46.236 L44.559,35.12 C44.833,34.801 45,34.434 45,34 C45,31.39 39.361,31 36,31 C32.639,31 27,31.39 27,34 C27,34.366 27.12,34.684 27.32,34.967 L32,46.2 L32,53 C32,53.552 32.447,54 33,54 L33,54 Z M62,53 C63.103,53 64,53.897 64,55 C64,56.103 63.103,57 62,57 C60.897,57 60,56.103 60,55 C60,53.897 60.897,53 62,53 L62,53 Z M62,23 C63.103,23 64,23.897 64,25 C64,26.103 63.103,27 62,27 C60.897,27 60,26.103 60,25 C60,23.897 60.897,23 62,23 L62,23 Z M64,38 C65.103,38 66,38.897 66,40 C66,41.103 65.103,42 64,42 C62.897,42 62,41.103 62,40 C62,38.897 62.897,38 64,38 L64,38 Z M54,41 L60.143,41 C60.589,42.72 62.142,44 64,44 C66.206,44 68,42.206 68,40 C68,37.794 66.206,36 64,36 C62.142,36 60.589,37.28 60.143,39 L54,39 L54,26 L58.143,26 C58.589,27.72 60.142,29 62,29 C64.206,29 66,27.206 66,25 C66,22.794 64.206,21 62,21 C60.142,21 58.589,22.28 58.143,24 L53,24 C52.447,24 52,24.448 52,25 L52,39 L45,39 L45,41 L52,41 L52,55 C52,55.552 52.447,56 53,56 L58.143,56 C58.589,57.72 60.142,59 62,59 C64.206,59 66,57.206 66,55 C66,52.794 64.206,51 62,51 C60.142,51 58.589,52.28 58.143,54 L54,54 L54,41 Z" id="AWS-Simple-Notification-Service_Icon_64_Squid" fill="#FFFFFF"></path>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 3.2 KiB |
|
Before Width: | Height: | Size: 5.7 KiB |
@@ -1,8 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="300" height="200">
|
|
||||||
<path fill="#fff" d="M0 0h300v200H0z"/>
|
|
||||||
<g transform="translate(30.667 -1141.475) scale(1.33333)">
|
|
||||||
<path d="M25 911.61v39h15v-6h-9v-27h9v-6zm114 0v6h9v27h-9v6h15v-39z" style="line-height:normal;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000;text-transform:none;text-orientation:mixed;white-space:normal;shape-padding:0;isolation:auto;mix-blend-mode:normal;solid-color:#000;solid-opacity:1" color="#000" font-weight="400" font-family="sans-serif" overflow="visible" fill="#201a26"/>
|
|
||||||
<path d="M92.5 931.11l27-15v30z" fill="#30d475"/>
|
|
||||||
<circle cx="70.002" cy="931.111" r="13.5" fill="#30d475"/>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 942 B |
@@ -1 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" width="64" height="64" fill="#00749a"><path d="M2.26 16c0 5.45 3.13 10.145 7.7 12.348L3.478 10.435C2.725 12.174 2.26 14.03 2.26 16zm23.015-.696c0-1.68-.638-2.9-1.16-3.768-.696-1.16-1.333-2.087-1.333-3.246 0-1.275.986-2.435 2.32-2.435h.174C22.84 3.594 19.594 2.26 16 2.26A13.95 13.95 0 0 0 4.522 8.463h.87c1.45 0 3.652-.174 3.652-.174.754-.058.812 1.043.116 1.16 0 0-.754.116-1.565.116l4.986 14.84 3.014-8.986-2.145-5.855L12 9.45c-.754-.058-.638-1.16.058-1.16 0 0 2.26.174 3.594.174 1.45 0 3.652-.174 3.652-.174.754-.058.812 1.043.116 1.16 0 0-.754.116-1.565.116L22.84 24.35l1.4-4.58c.58-1.913 1.043-3.246 1.043-4.464zm-9.043 1.913L12.116 29.16c1.217.348 2.55.58 3.884.58 1.623 0 3.13-.3 4.58-.754-.058-.058-.058-.116-.116-.174zM28.058 9.45l.116 1.4c0 1.4-.232 2.957-1.043 4.928l-4.174 12.116c4.058-2.377 6.84-6.783 6.84-11.884-.058-2.377-.696-4.58-1.74-6.55zM16 0C7.188 0 0 7.188 0 16s7.188 16 16 16 16-7.188 16-16S24.812 0 16 0zm0 31.304C7.594 31.304.754 24.464.754 16A15.27 15.27 0 0 1 16 .754 15.27 15.27 0 0 1 31.246 16c0 8.464-6.84 15.304-15.246 15.304z"/></svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.1 KiB |
@@ -14,8 +14,8 @@
|
|||||||
"remove_label_confirm": "This action will remove all the labels. Do you want to proceed?",
|
"remove_label_confirm": "This action will remove all the labels. Do you want to proceed?",
|
||||||
"remove_label_success": "Labels cleared",
|
"remove_label_success": "Labels cleared",
|
||||||
"alert_form_step1": "Step 1 - Define the metric",
|
"alert_form_step1": "Step 1 - Define the metric",
|
||||||
"alert_form_step2": "Step {{step}} - Define Alert Conditions",
|
"alert_form_step2": "Step 2 - Define Alert Conditions",
|
||||||
"alert_form_step3": "Step {{step}} - Alert Configuration",
|
"alert_form_step3": "Step 3 - Alert Configuration",
|
||||||
"metric_query_max_limit": "Can not create query. You can create maximum of 5 queries",
|
"metric_query_max_limit": "Can not create query. You can create maximum of 5 queries",
|
||||||
"confirm_save_title": "Save Changes",
|
"confirm_save_title": "Save Changes",
|
||||||
"confirm_save_content_part1": "Your alert built with",
|
"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_confirm": "This action will remove all the labels. Do you want to proceed?",
|
||||||
"remove_label_success": "Labels cleared",
|
"remove_label_success": "Labels cleared",
|
||||||
"alert_form_step1": "Step 1 - Define the metric",
|
"alert_form_step1": "Step 1 - Define the metric",
|
||||||
"alert_form_step2": "Step {{step}} - Define Alert Conditions",
|
"alert_form_step2": "Step 2 - Define Alert Conditions",
|
||||||
"alert_form_step3": "Step {{step}} - Alert Configuration",
|
"alert_form_step3": "Step 3 - Alert Configuration",
|
||||||
"metric_query_max_limit": "Can not create query. You can create maximum of 5 queries",
|
"metric_query_max_limit": "Can not create query. You can create maximum of 5 queries",
|
||||||
"confirm_save_title": "Save Changes",
|
"confirm_save_title": "Save Changes",
|
||||||
"confirm_save_content_part1": "Your alert built with",
|
"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_confirm": "This action will remove all the labels. Do you want to proceed?",
|
||||||
"remove_label_success": "Labels cleared",
|
"remove_label_success": "Labels cleared",
|
||||||
"alert_form_step1": "Step 1 - Define the metric",
|
"alert_form_step1": "Step 1 - Define the metric",
|
||||||
"alert_form_step2": "Step {{step}} - Define Alert Conditions",
|
"alert_form_step2": "Step 2 - Define Alert Conditions",
|
||||||
"alert_form_step3": "Step {{step}} - Alert Configuration",
|
"alert_form_step3": "Step 3 - Alert Configuration",
|
||||||
"metric_query_max_limit": "Can not create query. You can create maximum of 5 queries",
|
"metric_query_max_limit": "Can not create query. You can create maximum of 5 queries",
|
||||||
"confirm_save_title": "Save Changes",
|
"confirm_save_title": "Save Changes",
|
||||||
"confirm_save_content_part1": "Your alert built with",
|
"confirm_save_content_part1": "Your alert built with",
|
||||||
|
|||||||
@@ -191,8 +191,7 @@ function App(): JSX.Element {
|
|||||||
// if the user is on basic plan then remove billing
|
// if the user is on basic plan then remove billing
|
||||||
if (isOnBasicPlan) {
|
if (isOnBasicPlan) {
|
||||||
updatedRoutes = updatedRoutes.filter(
|
updatedRoutes = updatedRoutes.filter(
|
||||||
(route) =>
|
(route) => route?.path !== ROUTES.BILLING,
|
||||||
route?.path !== ROUTES.BILLING && route?.path !== ROUTES.INTEGRATIONS,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -205,8 +204,7 @@ function App(): JSX.Element {
|
|||||||
} else {
|
} else {
|
||||||
// if not a cloud user then remove billing and add list licenses route
|
// if not a cloud user then remove billing and add list licenses route
|
||||||
updatedRoutes = updatedRoutes.filter(
|
updatedRoutes = updatedRoutes.filter(
|
||||||
(route) =>
|
(route) => route?.path !== ROUTES.BILLING,
|
||||||
route?.path !== ROUTES.BILLING && route?.path !== ROUTES.INTEGRATIONS,
|
|
||||||
);
|
);
|
||||||
updatedRoutes = [...updatedRoutes, LIST_LICENSES];
|
updatedRoutes = [...updatedRoutes, LIST_LICENSES];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ const apiV1 = '/api/v1/';
|
|||||||
export const apiV2 = '/api/v2/';
|
export const apiV2 = '/api/v2/';
|
||||||
export const apiV3 = '/api/v3/';
|
export const apiV3 = '/api/v3/';
|
||||||
export const apiV4 = '/api/v4/';
|
export const apiV4 = '/api/v4/';
|
||||||
|
export const apiV5 = '/api/v5/';
|
||||||
export const gatewayApiV1 = '/api/gateway/v1/';
|
export const gatewayApiV1 = '/api/gateway/v1/';
|
||||||
export const gatewayApiV2 = '/api/gateway/v2/';
|
export const gatewayApiV2 = '/api/gateway/v2/';
|
||||||
export const apiAlertManager = '/api/alertmanager/';
|
export const apiAlertManager = '/api/alertmanager/';
|
||||||
|
|||||||
@@ -1,32 +1,14 @@
|
|||||||
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
||||||
import axios, { AxiosError } from 'axios';
|
import axios, { AxiosError } from 'axios';
|
||||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||||
import {
|
import { ChangelogSchema } from 'types/api/changelog/getChangelogByVersion';
|
||||||
ChangelogSchema,
|
|
||||||
DeploymentType,
|
|
||||||
} from 'types/api/changelog/getChangelogByVersion';
|
|
||||||
|
|
||||||
const getChangelogByVersion = async (
|
const getChangelogByVersion = async (
|
||||||
versionId: string,
|
versionId: string,
|
||||||
deployment_type?: DeploymentType,
|
|
||||||
): Promise<SuccessResponse<ChangelogSchema> | ErrorResponse> => {
|
): Promise<SuccessResponse<ChangelogSchema> | ErrorResponse> => {
|
||||||
try {
|
try {
|
||||||
let queryParams = `filters[version][$eq]=${versionId}&populate[features][sort]=sort_order:asc&populate[features][populate][media][fields]=id,ext,url,mime,alternativeText`;
|
|
||||||
|
|
||||||
if (
|
|
||||||
deployment_type &&
|
|
||||||
Object.values(DeploymentType).includes(deployment_type)
|
|
||||||
) {
|
|
||||||
const excludedDeploymentType =
|
|
||||||
deployment_type === DeploymentType.CLOUD_ONLY
|
|
||||||
? DeploymentType.OSS_ONLY
|
|
||||||
: DeploymentType.CLOUD_ONLY;
|
|
||||||
|
|
||||||
queryParams = `${queryParams}&populate[features][filters][deployment_type][$notIn]=${excludedDeploymentType}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await axios.get(`
|
const response = await axios.get(`
|
||||||
https://cms.signoz.cloud/api/release-changelogs?${queryParams}
|
https://cms.signoz.cloud/api/release-changelogs?filters[version][$eq]=${versionId}&populate[features][sort]=sort_order:asc&populate[features][populate][media][fields]=id,ext,url,mime,alternativeText
|
||||||
`);
|
`);
|
||||||
|
|
||||||
if (!Array.isArray(response.data.data) || response.data.data.length === 0) {
|
if (!Array.isArray(response.data.data) || response.data.data.length === 0) {
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import apiV1, {
|
|||||||
apiV2,
|
apiV2,
|
||||||
apiV3,
|
apiV3,
|
||||||
apiV4,
|
apiV4,
|
||||||
|
apiV5,
|
||||||
gatewayApiV1,
|
gatewayApiV1,
|
||||||
gatewayApiV2,
|
gatewayApiV2,
|
||||||
} from './apiV1';
|
} from './apiV1';
|
||||||
@@ -171,6 +172,18 @@ ApiV4Instance.interceptors.response.use(
|
|||||||
ApiV4Instance.interceptors.request.use(interceptorsRequestResponse);
|
ApiV4Instance.interceptors.request.use(interceptorsRequestResponse);
|
||||||
//
|
//
|
||||||
|
|
||||||
|
// axios V5
|
||||||
|
export const ApiV5Instance = axios.create({
|
||||||
|
baseURL: `${ENVIRONMENT.baseURL}${apiV5}`,
|
||||||
|
});
|
||||||
|
|
||||||
|
ApiV5Instance.interceptors.response.use(
|
||||||
|
interceptorsResponse,
|
||||||
|
interceptorRejected,
|
||||||
|
);
|
||||||
|
ApiV5Instance.interceptors.request.use(interceptorsRequestResponse);
|
||||||
|
//
|
||||||
|
|
||||||
// axios Base
|
// axios Base
|
||||||
export const ApiBaseInstance = axios.create({
|
export const ApiBaseInstance = axios.create({
|
||||||
baseURL: `${ENVIRONMENT.baseURL}${apiV1}`,
|
baseURL: `${ENVIRONMENT.baseURL}${apiV1}`,
|
||||||
|
|||||||
@@ -1,20 +1,24 @@
|
|||||||
import axios from 'api';
|
import axios from 'api';
|
||||||
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
|
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
||||||
import { AxiosError } from 'axios';
|
import { AxiosError } from 'axios';
|
||||||
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
|
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||||
import { Pipeline } from 'types/api/pipeline/def';
|
import { Pipeline } from 'types/api/pipeline/def';
|
||||||
import { Props } from 'types/api/pipeline/post';
|
import { Props } from 'types/api/pipeline/post';
|
||||||
|
|
||||||
const post = async (props: Props): Promise<SuccessResponseV2<Pipeline>> => {
|
const post = async (
|
||||||
|
props: Props,
|
||||||
|
): Promise<SuccessResponse<Pipeline> | ErrorResponse> => {
|
||||||
try {
|
try {
|
||||||
const response = await axios.post('/logs/pipelines', props.data);
|
const response = await axios.post('/logs/pipelines', props.data);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
httpStatusCode: response.status,
|
statusCode: 200,
|
||||||
data: response.data.data,
|
error: null,
|
||||||
|
message: response.data.status,
|
||||||
|
payload: response.data.data,
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
|
return ErrorResponseHandler(error as AxiosError);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
17
frontend/src/api/querySuggestions/getKeySuggestions.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import axios from 'api';
|
||||||
|
import { AxiosResponse } from 'axios';
|
||||||
|
import {
|
||||||
|
QueryKeyRequestProps,
|
||||||
|
QueryKeySuggestionsResponseProps,
|
||||||
|
} from 'types/api/querySuggestions/types';
|
||||||
|
|
||||||
|
export const getKeySuggestions = (
|
||||||
|
props: QueryKeyRequestProps,
|
||||||
|
): Promise<AxiosResponse<QueryKeySuggestionsResponseProps>> =>
|
||||||
|
axios.get(
|
||||||
|
`/fields/keys?signal=${props.signal}&searchText=${
|
||||||
|
props.searchText
|
||||||
|
}&metricName=${props.metricName ?? ''}&fieldContext=${
|
||||||
|
props.fieldContext ?? ''
|
||||||
|
}&fieldDataType=${props.fieldDataType ?? ''}`,
|
||||||
|
);
|
||||||
11
frontend/src/api/querySuggestions/getValueSuggestion.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import axios from 'api';
|
||||||
|
import { AxiosResponse } from 'axios';
|
||||||
|
import {
|
||||||
|
QueryKeyValueRequestProps,
|
||||||
|
QueryKeyValueSuggestionsResponseProps,
|
||||||
|
} from 'types/api/querySuggestions/types';
|
||||||
|
|
||||||
|
export const getValueSuggestions = (
|
||||||
|
props: QueryKeyValueRequestProps,
|
||||||
|
): Promise<AxiosResponse<QueryKeyValueSuggestionsResponseProps>> =>
|
||||||
|
axios.get(`/fields/values?signal=${props.signal}&name=${props.key}`);
|
||||||
168
frontend/src/api/v5/queryRange/constants.ts
Normal file
@@ -0,0 +1,168 @@
|
|||||||
|
// V5 Query Range Constants
|
||||||
|
|
||||||
|
import { ENTITY_VERSION_V5 } from 'constants/app';
|
||||||
|
import {
|
||||||
|
FunctionName,
|
||||||
|
RequestType,
|
||||||
|
SignalType,
|
||||||
|
Step,
|
||||||
|
} from 'types/api/v5/queryRange';
|
||||||
|
|
||||||
|
// ===================== Schema and Version Constants =====================
|
||||||
|
|
||||||
|
export const SCHEMA_VERSION_V5 = ENTITY_VERSION_V5;
|
||||||
|
export const API_VERSION_V5 = 'v5';
|
||||||
|
|
||||||
|
// ===================== Default Values =====================
|
||||||
|
|
||||||
|
export const DEFAULT_STEP_INTERVAL: Step = '60s';
|
||||||
|
export const DEFAULT_LIMIT = 100;
|
||||||
|
export const DEFAULT_OFFSET = 0;
|
||||||
|
|
||||||
|
// ===================== Request Type Constants =====================
|
||||||
|
|
||||||
|
export const REQUEST_TYPES: Record<string, RequestType> = {
|
||||||
|
SCALAR: 'scalar',
|
||||||
|
TIME_SERIES: 'time_series',
|
||||||
|
RAW: 'raw',
|
||||||
|
DISTRIBUTION: 'distribution',
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
// ===================== Signal Type Constants =====================
|
||||||
|
|
||||||
|
export const SIGNAL_TYPES: Record<string, SignalType> = {
|
||||||
|
TRACES: 'traces',
|
||||||
|
LOGS: 'logs',
|
||||||
|
METRICS: 'metrics',
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
// ===================== Common Aggregation Expressions =====================
|
||||||
|
|
||||||
|
export const TRACE_AGGREGATIONS = {
|
||||||
|
COUNT: 'count()',
|
||||||
|
COUNT_DISTINCT_TRACE_ID: 'count_distinct(traceID)',
|
||||||
|
AVG_DURATION: 'avg(duration_nano)',
|
||||||
|
P50_DURATION: 'p50(duration_nano)',
|
||||||
|
P95_DURATION: 'p95(duration_nano)',
|
||||||
|
P99_DURATION: 'p99(duration_nano)',
|
||||||
|
MAX_DURATION: 'max(duration_nano)',
|
||||||
|
MIN_DURATION: 'min(duration_nano)',
|
||||||
|
SUM_DURATION: 'sum(duration_nano)',
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const LOG_AGGREGATIONS = {
|
||||||
|
COUNT: 'count()',
|
||||||
|
COUNT_DISTINCT_HOST: 'count_distinct(host.name)',
|
||||||
|
COUNT_DISTINCT_SERVICE: 'count_distinct(service.name)',
|
||||||
|
COUNT_DISTINCT_CONTAINER: 'count_distinct(container.name)',
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
// ===================== Common Filter Expressions =====================
|
||||||
|
|
||||||
|
export const COMMON_FILTERS = {
|
||||||
|
// Trace filters
|
||||||
|
SERVER_SPANS: "kind_string = 'Server'",
|
||||||
|
CLIENT_SPANS: "kind_string = 'Client'",
|
||||||
|
INTERNAL_SPANS: "kind_string = 'Internal'",
|
||||||
|
ERROR_SPANS: 'http.status_code >= 400',
|
||||||
|
SUCCESS_SPANS: 'http.status_code < 400',
|
||||||
|
|
||||||
|
// Common service filters
|
||||||
|
EXCLUDE_HEALTH_CHECKS: "http.route != '/health' AND http.route != '/ping'",
|
||||||
|
HTTP_REQUESTS: "http.method != ''",
|
||||||
|
|
||||||
|
// Log filters
|
||||||
|
ERROR_LOGS: "severity_text = 'ERROR'",
|
||||||
|
WARN_LOGS: "severity_text = 'WARN'",
|
||||||
|
INFO_LOGS: "severity_text = 'INFO'",
|
||||||
|
DEBUG_LOGS: "severity_text = 'DEBUG'",
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
// ===================== Common Group By Fields =====================
|
||||||
|
|
||||||
|
export const COMMON_GROUP_BY_FIELDS = {
|
||||||
|
SERVICE_NAME: {
|
||||||
|
name: 'service.name',
|
||||||
|
fieldDataType: 'string' as const,
|
||||||
|
fieldContext: 'resource' as const,
|
||||||
|
},
|
||||||
|
HTTP_METHOD: {
|
||||||
|
name: 'http.method',
|
||||||
|
fieldDataType: 'string' as const,
|
||||||
|
fieldContext: 'attribute' as const,
|
||||||
|
},
|
||||||
|
HTTP_ROUTE: {
|
||||||
|
name: 'http.route',
|
||||||
|
fieldDataType: 'string' as const,
|
||||||
|
fieldContext: 'attribute' as const,
|
||||||
|
},
|
||||||
|
HTTP_STATUS_CODE: {
|
||||||
|
name: 'http.status_code',
|
||||||
|
fieldDataType: 'int64' as const,
|
||||||
|
fieldContext: 'attribute' as const,
|
||||||
|
},
|
||||||
|
HOST_NAME: {
|
||||||
|
name: 'host.name',
|
||||||
|
fieldDataType: 'string' as const,
|
||||||
|
fieldContext: 'resource' as const,
|
||||||
|
},
|
||||||
|
CONTAINER_NAME: {
|
||||||
|
name: 'container.name',
|
||||||
|
fieldDataType: 'string' as const,
|
||||||
|
fieldContext: 'resource' as const,
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
// ===================== Function Names =====================
|
||||||
|
|
||||||
|
export const FUNCTION_NAMES: Record<string, FunctionName> = {
|
||||||
|
CUT_OFF_MIN: 'cutOffMin',
|
||||||
|
CUT_OFF_MAX: 'cutOffMax',
|
||||||
|
CLAMP_MIN: 'clampMin',
|
||||||
|
CLAMP_MAX: 'clampMax',
|
||||||
|
ABSOLUTE: 'absolute',
|
||||||
|
RUNNING_DIFF: 'runningDiff',
|
||||||
|
LOG2: 'log2',
|
||||||
|
LOG10: 'log10',
|
||||||
|
CUM_SUM: 'cumSum',
|
||||||
|
EWMA3: 'ewma3',
|
||||||
|
EWMA5: 'ewma5',
|
||||||
|
EWMA7: 'ewma7',
|
||||||
|
MEDIAN3: 'median3',
|
||||||
|
MEDIAN5: 'median5',
|
||||||
|
MEDIAN7: 'median7',
|
||||||
|
TIME_SHIFT: 'timeShift',
|
||||||
|
ANOMALY: 'anomaly',
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
// ===================== Common Step Intervals =====================
|
||||||
|
|
||||||
|
export const STEP_INTERVALS = {
|
||||||
|
FIFTEEN_SECONDS: '15s',
|
||||||
|
THIRTY_SECONDS: '30s',
|
||||||
|
ONE_MINUTE: '60s',
|
||||||
|
FIVE_MINUTES: '300s',
|
||||||
|
TEN_MINUTES: '600s',
|
||||||
|
FIFTEEN_MINUTES: '900s',
|
||||||
|
THIRTY_MINUTES: '1800s',
|
||||||
|
ONE_HOUR: '3600s',
|
||||||
|
TWO_HOURS: '7200s',
|
||||||
|
SIX_HOURS: '21600s',
|
||||||
|
TWELVE_HOURS: '43200s',
|
||||||
|
ONE_DAY: '86400s',
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
// ===================== Time Range Presets =====================
|
||||||
|
|
||||||
|
export const TIME_RANGE_PRESETS = {
|
||||||
|
LAST_5_MINUTES: 5 * 60 * 1000,
|
||||||
|
LAST_15_MINUTES: 15 * 60 * 1000,
|
||||||
|
LAST_30_MINUTES: 30 * 60 * 1000,
|
||||||
|
LAST_HOUR: 60 * 60 * 1000,
|
||||||
|
LAST_3_HOURS: 3 * 60 * 60 * 1000,
|
||||||
|
LAST_6_HOURS: 6 * 60 * 60 * 1000,
|
||||||
|
LAST_12_HOURS: 12 * 60 * 60 * 1000,
|
||||||
|
LAST_24_HOURS: 24 * 60 * 60 * 1000,
|
||||||
|
LAST_3_DAYS: 3 * 24 * 60 * 60 * 1000,
|
||||||
|
LAST_7_DAYS: 7 * 24 * 60 * 60 * 1000,
|
||||||
|
} as const;
|
||||||
239
frontend/src/api/v5/queryRange/convertV5Response.ts
Normal file
@@ -0,0 +1,239 @@
|
|||||||
|
import { isEmpty } from 'lodash-es';
|
||||||
|
import { SuccessResponse } from 'types/api';
|
||||||
|
import { MetricRangePayloadV3 } from 'types/api/metrics/getQueryRange';
|
||||||
|
import {
|
||||||
|
DistributionData,
|
||||||
|
MetricRangePayloadV5,
|
||||||
|
RawData,
|
||||||
|
ScalarData,
|
||||||
|
TimeSeriesData,
|
||||||
|
} from 'types/api/v5/queryRange';
|
||||||
|
import { QueryDataV3 } from 'types/api/widgets/getQuery';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts V5 TimeSeriesData to legacy format
|
||||||
|
*/
|
||||||
|
function convertTimeSeriesData(
|
||||||
|
timeSeriesData: TimeSeriesData,
|
||||||
|
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) => ({
|
||||||
|
labels: series.labels
|
||||||
|
? Object.fromEntries(
|
||||||
|
series.labels.map((label) => [label.key.name, label.value]),
|
||||||
|
)
|
||||||
|
: {},
|
||||||
|
labelsArray: series.labels
|
||||||
|
? series.labels.map((label) => ({ [label.key.name]: label.value }))
|
||||||
|
: [],
|
||||||
|
values: series.values.map((value) => ({
|
||||||
|
timestamp: value.timestamp,
|
||||||
|
value: String(value.value),
|
||||||
|
})),
|
||||||
|
})),
|
||||||
|
),
|
||||||
|
list: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts V5 ScalarData array to legacy format with table structure
|
||||||
|
*/
|
||||||
|
function convertScalarDataArrayToTable(
|
||||||
|
scalarDataArray: ScalarData[],
|
||||||
|
legendMap: Record<string, string>,
|
||||||
|
): QueryDataV3[] {
|
||||||
|
// If no scalar data, return empty structure
|
||||||
|
|
||||||
|
if (!scalarDataArray || scalarDataArray.length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process each scalar data separately to maintain query separation
|
||||||
|
return scalarDataArray?.map((scalarData) => {
|
||||||
|
// Get query name from the first column
|
||||||
|
const queryName = scalarData?.columns?.[0]?.queryName || '';
|
||||||
|
|
||||||
|
// Collect columns for this specific query
|
||||||
|
const columns = scalarData?.columns?.map((col) => ({
|
||||||
|
name: col.columnType === 'aggregation' ? col.queryName : col.name,
|
||||||
|
queryName: col.queryName,
|
||||||
|
isValueColumn: col.columnType === 'aggregation',
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Process rows for this specific query
|
||||||
|
const rows = scalarData?.data?.map((dataRow) => {
|
||||||
|
const rowData: Record<string, any> = {};
|
||||||
|
|
||||||
|
scalarData?.columns?.forEach((col, colIndex) => {
|
||||||
|
const columnName =
|
||||||
|
col.columnType === 'aggregation' ? col.queryName : col.name;
|
||||||
|
rowData[columnName] = dataRow[colIndex];
|
||||||
|
});
|
||||||
|
|
||||||
|
return { data: rowData };
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
queryName,
|
||||||
|
legend: legendMap[queryName] || '',
|
||||||
|
series: null,
|
||||||
|
list: null,
|
||||||
|
table: {
|
||||||
|
columns,
|
||||||
|
rows,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts V5 RawData to legacy format
|
||||||
|
*/
|
||||||
|
function convertRawData(
|
||||||
|
rawData: RawData,
|
||||||
|
legendMap: Record<string, string>,
|
||||||
|
): QueryDataV3 {
|
||||||
|
// Convert V5 raw format to legacy QueryDataV3 format
|
||||||
|
return {
|
||||||
|
queryName: rawData.queryName,
|
||||||
|
legend: legendMap[rawData.queryName] || rawData.queryName,
|
||||||
|
series: null,
|
||||||
|
list: rawData.rows?.map((row) => ({
|
||||||
|
timestamp: row.timestamp,
|
||||||
|
data: {
|
||||||
|
// Map raw data to ILog structure - spread row.data first to include all properties
|
||||||
|
...row.data,
|
||||||
|
date: row.timestamp,
|
||||||
|
} as any,
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts V5 DistributionData to legacy format
|
||||||
|
*/
|
||||||
|
function convertDistributionData(
|
||||||
|
distributionData: DistributionData,
|
||||||
|
legendMap: Record<string, string>,
|
||||||
|
): any {
|
||||||
|
// eslint-disable-line @typescript-eslint/no-explicit-any
|
||||||
|
// Convert V5 distribution format to legacy histogram format
|
||||||
|
return {
|
||||||
|
...distributionData,
|
||||||
|
legendMap,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper function to convert V5 data based on type
|
||||||
|
*/
|
||||||
|
function convertV5DataByType(
|
||||||
|
v5Data: any,
|
||||||
|
legendMap: Record<string, string>,
|
||||||
|
): MetricRangePayloadV3['data'] {
|
||||||
|
switch (v5Data?.type) {
|
||||||
|
case 'time_series': {
|
||||||
|
const timeSeriesData = v5Data.data.results as TimeSeriesData[];
|
||||||
|
return {
|
||||||
|
resultType: 'time_series',
|
||||||
|
result: timeSeriesData.map((timeSeries) =>
|
||||||
|
convertTimeSeriesData(timeSeries, legendMap),
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
case 'scalar': {
|
||||||
|
const scalarData = v5Data.data.results as ScalarData[];
|
||||||
|
// For scalar data, combine all results into separate table entries
|
||||||
|
const combinedTables = convertScalarDataArrayToTable(scalarData, legendMap);
|
||||||
|
return {
|
||||||
|
resultType: 'scalar',
|
||||||
|
result: combinedTables,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
case 'raw': {
|
||||||
|
const rawData = v5Data.data.results as RawData[];
|
||||||
|
return {
|
||||||
|
resultType: 'raw',
|
||||||
|
result: rawData.map((raw) => convertRawData(raw, legendMap)),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
case 'distribution': {
|
||||||
|
const distributionData = v5Data.data.results as DistributionData[];
|
||||||
|
return {
|
||||||
|
resultType: 'distribution',
|
||||||
|
result: distributionData.map((distribution) =>
|
||||||
|
convertDistributionData(distribution, legendMap),
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return {
|
||||||
|
resultType: '',
|
||||||
|
result: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts V5 API response to legacy format expected by frontend components
|
||||||
|
*/
|
||||||
|
export function convertV5ResponseToLegacy(
|
||||||
|
v5Response: SuccessResponse<MetricRangePayloadV5>,
|
||||||
|
legendMap: Record<string, string>,
|
||||||
|
// formatForWeb?: boolean,
|
||||||
|
): SuccessResponse<MetricRangePayloadV3> {
|
||||||
|
const { payload } = v5Response;
|
||||||
|
const v5Data = payload?.data;
|
||||||
|
|
||||||
|
// todo - sagar
|
||||||
|
// 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;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// Convert based on V5 response type
|
||||||
|
const convertedData = convertV5DataByType(v5Data, legendMap);
|
||||||
|
|
||||||
|
// Create legacy-compatible response structure
|
||||||
|
const legacyResponse: SuccessResponse<MetricRangePayloadV3> = {
|
||||||
|
...v5Response,
|
||||||
|
payload: {
|
||||||
|
data: convertedData,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Apply legend mapping (similar to existing logic)
|
||||||
|
if (legacyResponse.payload?.data?.result) {
|
||||||
|
legacyResponse.payload.data.result = legacyResponse.payload.data.result.map(
|
||||||
|
(queryData: any) => {
|
||||||
|
// eslint-disable-line @typescript-eslint/no-explicit-any
|
||||||
|
const newQueryData = queryData;
|
||||||
|
newQueryData.legend = legendMap[queryData.queryName];
|
||||||
|
|
||||||
|
// If metric names is an empty object
|
||||||
|
if (isEmpty(queryData.metric)) {
|
||||||
|
// If metrics list is empty && the user haven't defined a legend then add the legend equal to the name of the query.
|
||||||
|
if (newQueryData.legend === undefined || newQueryData.legend === null) {
|
||||||
|
newQueryData.legend = queryData.queryName;
|
||||||
|
}
|
||||||
|
// If name of the query and the legend if inserted is same then add the same to the metrics object.
|
||||||
|
if (queryData.queryName === newQueryData.legend) {
|
||||||
|
newQueryData.metric = newQueryData.metric || {};
|
||||||
|
newQueryData.metric[queryData.queryName] = queryData.queryName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return newQueryData;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return legacyResponse;
|
||||||
|
}
|
||||||
51
frontend/src/api/v5/queryRange/getQueryRange.ts
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
import { ApiV5Instance } from 'api';
|
||||||
|
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
||||||
|
import { AxiosError } from 'axios';
|
||||||
|
import { ENTITY_VERSION_V5 } from 'constants/app';
|
||||||
|
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||||
|
import {
|
||||||
|
MetricRangePayloadV5,
|
||||||
|
QueryRangePayloadV5,
|
||||||
|
} from 'types/api/v5/queryRange';
|
||||||
|
|
||||||
|
export const getQueryRangeV5 = async (
|
||||||
|
props: QueryRangePayloadV5,
|
||||||
|
version: string,
|
||||||
|
signal: AbortSignal,
|
||||||
|
headers?: Record<string, string>,
|
||||||
|
): Promise<SuccessResponse<MetricRangePayloadV5> | ErrorResponse> => {
|
||||||
|
try {
|
||||||
|
if (version && version === ENTITY_VERSION_V5) {
|
||||||
|
const response = await ApiV5Instance.post('/query_range', props, {
|
||||||
|
signal,
|
||||||
|
headers,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
statusCode: 200,
|
||||||
|
error: null,
|
||||||
|
message: response.data.status,
|
||||||
|
payload: response.data,
|
||||||
|
params: props,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default V5 behavior
|
||||||
|
const response = await ApiV5Instance.post('/query_range', props, {
|
||||||
|
signal,
|
||||||
|
headers,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
statusCode: 200,
|
||||||
|
error: null,
|
||||||
|
message: response.data.status,
|
||||||
|
payload: response.data,
|
||||||
|
params: props,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return ErrorResponseHandler(error as AxiosError);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default getQueryRangeV5;
|
||||||
394
frontend/src/api/v5/queryRange/prepareQueryRangePayloadV5.ts
Normal file
@@ -0,0 +1,394 @@
|
|||||||
|
/* eslint-disable sonarjs/cognitive-complexity */
|
||||||
|
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||||
|
import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults';
|
||||||
|
import getStartEndRangeTime from 'lib/getStartEndRangeTime';
|
||||||
|
import { mapQueryDataToApi } from 'lib/newQueryBuilder/queryBuilderMappers/mapQueryDataToApi';
|
||||||
|
import { isEmpty } from 'lodash-es';
|
||||||
|
import {
|
||||||
|
IBuilderQuery,
|
||||||
|
QueryFunctionProps,
|
||||||
|
} from 'types/api/queryBuilder/queryBuilderData';
|
||||||
|
import {
|
||||||
|
BaseBuilderQuery,
|
||||||
|
FieldContext,
|
||||||
|
FieldDataType,
|
||||||
|
FunctionName,
|
||||||
|
GroupByKey,
|
||||||
|
LogAggregation,
|
||||||
|
MetricAggregation,
|
||||||
|
OrderBy,
|
||||||
|
QueryEnvelope,
|
||||||
|
QueryFunction,
|
||||||
|
QueryRangePayloadV5,
|
||||||
|
QueryType,
|
||||||
|
RequestType,
|
||||||
|
TelemetryFieldKey,
|
||||||
|
TraceAggregation,
|
||||||
|
VariableItem,
|
||||||
|
} from 'types/api/v5/queryRange';
|
||||||
|
import { EQueryType } from 'types/common/dashboard';
|
||||||
|
import { DataSource } from 'types/common/queryBuilder';
|
||||||
|
|
||||||
|
type PrepareQueryRangePayloadV5Result = {
|
||||||
|
queryPayload: QueryRangePayloadV5;
|
||||||
|
legendMap: Record<string, string>;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maps panel types to V5 request types
|
||||||
|
*/
|
||||||
|
function mapPanelTypeToRequestType(panelType: PANEL_TYPES): RequestType {
|
||||||
|
switch (panelType) {
|
||||||
|
case PANEL_TYPES.TIME_SERIES:
|
||||||
|
case PANEL_TYPES.BAR:
|
||||||
|
return 'time_series';
|
||||||
|
case PANEL_TYPES.TABLE:
|
||||||
|
case PANEL_TYPES.PIE:
|
||||||
|
case PANEL_TYPES.VALUE:
|
||||||
|
case PANEL_TYPES.TRACE:
|
||||||
|
return 'scalar';
|
||||||
|
case PANEL_TYPES.LIST:
|
||||||
|
return 'raw';
|
||||||
|
case PANEL_TYPES.HISTOGRAM:
|
||||||
|
return 'distribution';
|
||||||
|
default:
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets signal type from data source
|
||||||
|
*/
|
||||||
|
function getSignalType(dataSource: string): 'traces' | 'logs' | 'metrics' {
|
||||||
|
if (dataSource === 'traces') return 'traces';
|
||||||
|
if (dataSource === 'logs') return 'logs';
|
||||||
|
return 'metrics';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates base spec for builder queries
|
||||||
|
*/
|
||||||
|
function createBaseSpec(
|
||||||
|
queryData: IBuilderQuery,
|
||||||
|
requestType: RequestType,
|
||||||
|
panelType?: PANEL_TYPES,
|
||||||
|
): BaseBuilderQuery {
|
||||||
|
return {
|
||||||
|
stepInterval: queryData.stepInterval,
|
||||||
|
disabled: queryData.disabled,
|
||||||
|
filter: queryData?.filter?.expression ? queryData.filter : undefined,
|
||||||
|
groupBy:
|
||||||
|
queryData.groupBy?.length > 0
|
||||||
|
? queryData.groupBy.map(
|
||||||
|
(item: any): GroupByKey => ({
|
||||||
|
name: item.key,
|
||||||
|
fieldDataType: item?.dataType,
|
||||||
|
fieldContext: item?.type,
|
||||||
|
description: item?.description,
|
||||||
|
unit: item?.unit,
|
||||||
|
signal: item?.signal,
|
||||||
|
materialized: item?.materialized,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
: undefined,
|
||||||
|
limit:
|
||||||
|
panelType === PANEL_TYPES.TABLE || panelType === PANEL_TYPES.LIST
|
||||||
|
? queryData.limit || queryData.pageSize || undefined
|
||||||
|
: queryData.limit || undefined,
|
||||||
|
offset: requestType === 'raw' ? queryData.offset : undefined,
|
||||||
|
order:
|
||||||
|
queryData.orderBy.length > 0
|
||||||
|
? queryData.orderBy.map(
|
||||||
|
(order: any): OrderBy => ({
|
||||||
|
key: {
|
||||||
|
name: order.columnName,
|
||||||
|
},
|
||||||
|
direction: order.order,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
: undefined,
|
||||||
|
// legend: isEmpty(queryData.legend) ? undefined : queryData.legend,
|
||||||
|
having: isEmpty(queryData.havingExpression)
|
||||||
|
? undefined
|
||||||
|
: queryData?.havingExpression,
|
||||||
|
functions: isEmpty(queryData.functions)
|
||||||
|
? undefined
|
||||||
|
: queryData.functions.map(
|
||||||
|
(func: QueryFunctionProps): QueryFunction => ({
|
||||||
|
name: func.name as FunctionName,
|
||||||
|
args: func.args.map((arg) => ({
|
||||||
|
// name: arg.name,
|
||||||
|
value: arg,
|
||||||
|
})),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
selectFields: isEmpty(queryData.selectColumns)
|
||||||
|
? undefined
|
||||||
|
: queryData.selectColumns?.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,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// Utility to parse aggregation expressions with optional alias
|
||||||
|
export function parseAggregations(
|
||||||
|
expression: string,
|
||||||
|
): { expression: string; alias?: string }[] {
|
||||||
|
const result: { expression: string; alias?: string }[] = [];
|
||||||
|
const regex = /([a-zA-Z0-9_]+\([^)]*\))(?:\s*as\s+([a-zA-Z0-9_]+))?/g;
|
||||||
|
let match = regex.exec(expression);
|
||||||
|
while (match !== null) {
|
||||||
|
const expr = match[1];
|
||||||
|
const alias = match[2];
|
||||||
|
if (alias) {
|
||||||
|
result.push({ expression: expr, alias });
|
||||||
|
} else {
|
||||||
|
result.push({ expression: expr });
|
||||||
|
}
|
||||||
|
match = regex.exec(expression);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createAggregation(
|
||||||
|
queryData: any,
|
||||||
|
): TraceAggregation[] | LogAggregation[] | MetricAggregation[] {
|
||||||
|
if (queryData.dataSource === DataSource.METRICS) {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
metricName: queryData?.aggregateAttribute?.key,
|
||||||
|
temporality: queryData?.aggregateAttribute?.temporality,
|
||||||
|
timeAggregation: queryData?.timeAggregation,
|
||||||
|
spaceAggregation: queryData?.spaceAggregation,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (queryData.aggregations?.length > 0) {
|
||||||
|
return isEmpty(parseAggregations(queryData.aggregations?.[0].expression))
|
||||||
|
? [{ expression: 'count()' }]
|
||||||
|
: parseAggregations(queryData.aggregations?.[0].expression);
|
||||||
|
}
|
||||||
|
|
||||||
|
return [{ expression: 'count()' }];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts query builder data to V5 builder queries
|
||||||
|
*/
|
||||||
|
function convertBuilderQueriesToV5(
|
||||||
|
builderQueries: Record<string, any>, // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||||
|
requestType: RequestType,
|
||||||
|
panelType?: PANEL_TYPES,
|
||||||
|
): QueryEnvelope[] {
|
||||||
|
return Object.entries(builderQueries).map(
|
||||||
|
([queryName, queryData]): QueryEnvelope => {
|
||||||
|
const signal = getSignalType(queryData.dataSource);
|
||||||
|
const baseSpec = createBaseSpec(queryData, requestType, panelType);
|
||||||
|
let spec: QueryEnvelope['spec'];
|
||||||
|
|
||||||
|
const aggregations = createAggregation(queryData);
|
||||||
|
|
||||||
|
switch (signal) {
|
||||||
|
case 'traces':
|
||||||
|
spec = {
|
||||||
|
name: queryName,
|
||||||
|
signal: 'traces' as const,
|
||||||
|
...baseSpec,
|
||||||
|
aggregations: aggregations as TraceAggregation[],
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
case 'logs':
|
||||||
|
spec = {
|
||||||
|
name: queryName,
|
||||||
|
signal: 'logs' as const,
|
||||||
|
...baseSpec,
|
||||||
|
aggregations: aggregations as LogAggregation[],
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
case 'metrics':
|
||||||
|
default:
|
||||||
|
spec = {
|
||||||
|
name: queryName,
|
||||||
|
signal: 'metrics' as const,
|
||||||
|
...baseSpec,
|
||||||
|
aggregations: aggregations as MetricAggregation[],
|
||||||
|
// reduceTo: queryData.reduceTo,
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: 'builder_query' as QueryType,
|
||||||
|
spec,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts PromQL queries to V5 format
|
||||||
|
*/
|
||||||
|
function convertPromQueriesToV5(
|
||||||
|
promQueries: Record<string, any>, // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||||
|
): QueryEnvelope[] {
|
||||||
|
return Object.entries(promQueries).map(
|
||||||
|
([queryName, queryData]): QueryEnvelope => ({
|
||||||
|
type: 'promql' as QueryType,
|
||||||
|
spec: {
|
||||||
|
name: queryName,
|
||||||
|
query: queryData.query,
|
||||||
|
disabled: queryData.disabled || false,
|
||||||
|
step: queryData.stepInterval,
|
||||||
|
stats: false, // PromQL specific field
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts ClickHouse queries to V5 format
|
||||||
|
*/
|
||||||
|
function convertClickHouseQueriesToV5(
|
||||||
|
chQueries: Record<string, any>, // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||||
|
): QueryEnvelope[] {
|
||||||
|
return Object.entries(chQueries).map(
|
||||||
|
([queryName, queryData]): QueryEnvelope => ({
|
||||||
|
type: 'clickhouse_sql' as QueryType,
|
||||||
|
spec: {
|
||||||
|
name: queryName,
|
||||||
|
query: queryData.query,
|
||||||
|
disabled: queryData.disabled || false,
|
||||||
|
// ClickHouse doesn't have step or stats like PromQL
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts query formulas to V5 format
|
||||||
|
*/
|
||||||
|
function convertFormulasToV5(
|
||||||
|
formulas: Record<string, any>, // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||||
|
): QueryEnvelope[] {
|
||||||
|
return Object.entries(formulas).map(
|
||||||
|
([queryName, formulaData]): QueryEnvelope => ({
|
||||||
|
type: 'builder_formula' as QueryType,
|
||||||
|
spec: {
|
||||||
|
name: queryName,
|
||||||
|
expression: formulaData.expression || '',
|
||||||
|
functions: formulaData.functions,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper function to reduce query arrays to objects
|
||||||
|
*/
|
||||||
|
function reduceQueriesToObject(
|
||||||
|
queryArray: any[], // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||||
|
): { queries: Record<string, any>; legends: Record<string, string> } {
|
||||||
|
// eslint-disable-line @typescript-eslint/no-explicit-any
|
||||||
|
const legends: Record<string, string> = {};
|
||||||
|
const queries = queryArray.reduce((acc, queryItem) => {
|
||||||
|
if (!queryItem.query) return acc;
|
||||||
|
acc[queryItem.name] = queryItem;
|
||||||
|
legends[queryItem.name] = queryItem.legend;
|
||||||
|
return acc;
|
||||||
|
}, {} as Record<string, any>); // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||||
|
|
||||||
|
return { queries, legends };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prepares V5 query range payload from GetQueryResultsProps
|
||||||
|
*/
|
||||||
|
export const prepareQueryRangePayloadV5 = ({
|
||||||
|
query,
|
||||||
|
globalSelectedInterval,
|
||||||
|
graphType,
|
||||||
|
selectedTime,
|
||||||
|
tableParams,
|
||||||
|
variables = {},
|
||||||
|
start: startTime,
|
||||||
|
end: endTime,
|
||||||
|
formatForWeb,
|
||||||
|
}: GetQueryResultsProps): PrepareQueryRangePayloadV5Result => {
|
||||||
|
let legendMap: Record<string, string> = {};
|
||||||
|
const requestType = mapPanelTypeToRequestType(graphType);
|
||||||
|
let queries: QueryEnvelope[] = [];
|
||||||
|
|
||||||
|
switch (query.queryType) {
|
||||||
|
case EQueryType.QUERY_BUILDER: {
|
||||||
|
const { queryData: data, queryFormulas } = query.builder;
|
||||||
|
const currentQueryData = mapQueryDataToApi(data, 'queryName', tableParams);
|
||||||
|
const currentFormulas = mapQueryDataToApi(queryFormulas, 'queryName');
|
||||||
|
|
||||||
|
// Combine legend maps
|
||||||
|
legendMap = {
|
||||||
|
...currentQueryData.newLegendMap,
|
||||||
|
...currentFormulas.newLegendMap,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Convert builder queries
|
||||||
|
const builderQueries = convertBuilderQueriesToV5(
|
||||||
|
currentQueryData.data,
|
||||||
|
requestType,
|
||||||
|
graphType,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Convert formulas as separate query type
|
||||||
|
const formulaQueries = convertFormulasToV5(currentFormulas.data);
|
||||||
|
|
||||||
|
// Combine both types
|
||||||
|
queries = [...builderQueries, ...formulaQueries];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case EQueryType.PROM: {
|
||||||
|
const promQueries = reduceQueriesToObject(query[query.queryType]);
|
||||||
|
queries = convertPromQueriesToV5(promQueries.queries);
|
||||||
|
legendMap = promQueries.legends;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case EQueryType.CLICKHOUSE: {
|
||||||
|
const chQueries = reduceQueriesToObject(query[query.queryType]);
|
||||||
|
queries = convertClickHouseQueriesToV5(chQueries.queries);
|
||||||
|
legendMap = chQueries.legends;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate time range
|
||||||
|
const { start, end } = getStartEndRangeTime({
|
||||||
|
type: selectedTime,
|
||||||
|
interval: globalSelectedInterval,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create V5 payload
|
||||||
|
const queryPayload: QueryRangePayloadV5 = {
|
||||||
|
schemaVersion: 'v1',
|
||||||
|
start: startTime ? startTime * 1e3 : parseInt(start, 10) * 1e3,
|
||||||
|
end: endTime ? endTime * 1e3 : parseInt(end, 10) * 1e3,
|
||||||
|
requestType,
|
||||||
|
compositeQuery: {
|
||||||
|
queries,
|
||||||
|
},
|
||||||
|
formatOptions: {
|
||||||
|
formatTableResultForUI: !!formatForWeb,
|
||||||
|
},
|
||||||
|
variables: Object.entries(variables).reduce((acc, [key, value]) => {
|
||||||
|
acc[key] = { value };
|
||||||
|
return acc;
|
||||||
|
}, {} as Record<string, VariableItem>),
|
||||||
|
};
|
||||||
|
|
||||||
|
return { legendMap, queryPayload };
|
||||||
|
};
|
||||||
8
frontend/src/api/v5/v5.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
// V5 API exports
|
||||||
|
export * from './queryRange/constants';
|
||||||
|
export { convertV5ResponseToLegacy } from './queryRange/convertV5Response';
|
||||||
|
export { getQueryRangeV5 } from './queryRange/getQueryRange';
|
||||||
|
export { prepareQueryRangePayloadV5 } from './queryRange/prepareQueryRangePayloadV5';
|
||||||
|
|
||||||
|
// Export types from proper location
|
||||||
|
export * from 'types/api/v5/queryRange';
|
||||||
@@ -60,7 +60,6 @@
|
|||||||
|
|
||||||
&-ctas {
|
&-ctas {
|
||||||
display: flex;
|
display: flex;
|
||||||
margin-left: auto;
|
|
||||||
|
|
||||||
& svg {
|
& svg {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
@@ -111,7 +110,7 @@
|
|||||||
&-content {
|
&-content {
|
||||||
max-height: calc(100vh - 300px);
|
max-height: calc(100vh - 300px);
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
padding: 16px 16px 18px 16px;
|
padding: 16px;
|
||||||
border: 1px solid var(--bg-slate-500, #161922);
|
border: 1px solid var(--bg-slate-500, #161922);
|
||||||
border-top-width: 0;
|
border-top-width: 0;
|
||||||
border-bottom-width: 0;
|
border-bottom-width: 0;
|
||||||
|
|||||||
@@ -2,62 +2,27 @@ import './ChangelogModal.styles.scss';
|
|||||||
|
|
||||||
import { CheckOutlined, CloseOutlined } from '@ant-design/icons';
|
import { CheckOutlined, CloseOutlined } from '@ant-design/icons';
|
||||||
import { Button, Modal } from 'antd';
|
import { Button, Modal } from 'antd';
|
||||||
import updateUserPreference from 'api/v1/user/preferences/name/update';
|
|
||||||
import cx from 'classnames';
|
import cx from 'classnames';
|
||||||
import { USER_PREFERENCES } from 'constants/userPreferences';
|
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import { useGetTenantLicense } from 'hooks/useGetTenantLicense';
|
|
||||||
import { ChevronsDown, ScrollText } from 'lucide-react';
|
import { ChevronsDown, ScrollText } from 'lucide-react';
|
||||||
import { useAppContext } from 'providers/App/App';
|
import { useAppContext } from 'providers/App/App';
|
||||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
import { useMutation } from 'react-query';
|
|
||||||
import { ChangelogSchema } from 'types/api/changelog/getChangelogByVersion';
|
|
||||||
import { UserPreference } from 'types/api/preferences/preference';
|
|
||||||
|
|
||||||
import ChangelogRenderer from './components/ChangelogRenderer';
|
import ChangelogRenderer from './components/ChangelogRenderer';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
changelog: ChangelogSchema;
|
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
function ChangelogModal({ changelog, onClose }: Props): JSX.Element {
|
function ChangelogModal({ onClose }: Props): JSX.Element {
|
||||||
const [hasScroll, setHasScroll] = useState(false);
|
const [hasScroll, setHasScroll] = useState(false);
|
||||||
const changelogContentSectionRef = useRef<HTMLDivElement>(null);
|
const changelogContentSectionRef = useRef<HTMLDivElement>(null);
|
||||||
const { userPreferences, updateUserPreferenceInContext } = useAppContext();
|
const { changelog } = useAppContext();
|
||||||
|
|
||||||
const formattedReleaseDate = dayjs(changelog?.release_date).format(
|
const formattedReleaseDate = dayjs(changelog?.release_date).format(
|
||||||
'MMMM D, YYYY',
|
'MMMM D, YYYY',
|
||||||
);
|
);
|
||||||
|
|
||||||
const { isCloudUser } = useGetTenantLicense();
|
|
||||||
|
|
||||||
const seenChangelogVersion = userPreferences?.find(
|
|
||||||
(preference) =>
|
|
||||||
preference.name === USER_PREFERENCES.LAST_SEEN_CHANGELOG_VERSION,
|
|
||||||
)?.value as string;
|
|
||||||
|
|
||||||
const { mutate: updateUserPreferenceMutation } = useMutation(
|
|
||||||
updateUserPreference,
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
// Update the seen version
|
|
||||||
if (seenChangelogVersion !== changelog.version) {
|
|
||||||
const version = {
|
|
||||||
name: USER_PREFERENCES.LAST_SEEN_CHANGELOG_VERSION,
|
|
||||||
value: changelog.version,
|
|
||||||
};
|
|
||||||
updateUserPreferenceInContext(version as UserPreference);
|
|
||||||
updateUserPreferenceMutation(version);
|
|
||||||
}
|
|
||||||
}, [
|
|
||||||
seenChangelogVersion,
|
|
||||||
changelog.version,
|
|
||||||
updateUserPreferenceMutation,
|
|
||||||
updateUserPreferenceInContext,
|
|
||||||
]);
|
|
||||||
|
|
||||||
const checkScroll = useCallback((): void => {
|
const checkScroll = useCallback((): void => {
|
||||||
if (changelogContentSectionRef.current) {
|
if (changelogContentSectionRef.current) {
|
||||||
const {
|
const {
|
||||||
@@ -124,20 +89,18 @@ function ChangelogModal({ changelog, onClose }: Props): JSX.Element {
|
|||||||
{changelog.features.length > 1 ? 'features' : 'feature'}
|
{changelog.features.length > 1 ? 'features' : 'feature'}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
{!isCloudUser && (
|
<div className="changelog-modal-footer-ctas">
|
||||||
<div className="changelog-modal-footer-ctas">
|
<Button type="default" icon={<CloseOutlined />} onClick={onClose}>
|
||||||
<Button type="default" icon={<CloseOutlined />} onClick={onClose}>
|
Skip for now
|
||||||
Skip for now
|
</Button>
|
||||||
</Button>
|
<Button
|
||||||
<Button
|
type="primary"
|
||||||
type="primary"
|
icon={<CheckOutlined />}
|
||||||
icon={<CheckOutlined />}
|
onClick={onClickUpdateWorkspace}
|
||||||
onClick={onClickUpdateWorkspace}
|
>
|
||||||
>
|
Update my workspace
|
||||||
Update my workspace
|
</Button>
|
||||||
</Button>
|
</div>
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{changelog && (
|
{changelog && (
|
||||||
<div className="scroll-btn-container">
|
<div className="scroll-btn-container">
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -3,22 +3,10 @@
|
|||||||
/* eslint-disable @typescript-eslint/explicit-function-return-type */
|
/* eslint-disable @typescript-eslint/explicit-function-return-type */
|
||||||
|
|
||||||
import { fireEvent, render, screen } from '@testing-library/react';
|
import { fireEvent, render, screen } from '@testing-library/react';
|
||||||
import { USER_PREFERENCES } from 'constants/userPreferences';
|
|
||||||
import MockQueryClientProvider from 'providers/test/MockQueryClientProvider';
|
|
||||||
import {
|
|
||||||
ChangelogSchema,
|
|
||||||
DeploymentType,
|
|
||||||
} from 'types/api/changelog/getChangelogByVersion';
|
|
||||||
|
|
||||||
import ChangelogModal from '../ChangelogModal';
|
import ChangelogModal from '../ChangelogModal';
|
||||||
|
|
||||||
const mockChangelog: ChangelogSchema = {
|
const mockChangelog = {
|
||||||
id: 1,
|
|
||||||
documentId: 'doc-1',
|
|
||||||
version: 'v1.0.0',
|
|
||||||
createdAt: '2025-06-09T12:00:00Z',
|
|
||||||
updatedAt: '2025-06-09T13:00:00Z',
|
|
||||||
publishedAt: '2025-06-09T14:00:00Z',
|
|
||||||
release_date: '2025-06-10',
|
release_date: '2025-06-10',
|
||||||
features: [
|
features: [
|
||||||
{
|
{
|
||||||
@@ -26,12 +14,6 @@ const mockChangelog: ChangelogSchema = {
|
|||||||
title: 'Feature 1',
|
title: 'Feature 1',
|
||||||
description: 'Description for feature 1',
|
description: 'Description for feature 1',
|
||||||
media: null,
|
media: null,
|
||||||
documentId: 'feature-1',
|
|
||||||
sort_order: 1,
|
|
||||||
createdAt: '2025-06-09T12:00:00Z',
|
|
||||||
updatedAt: '2025-06-09T13:00:00Z',
|
|
||||||
publishedAt: '2025-06-09T14:00:00Z',
|
|
||||||
deployment_type: DeploymentType.ALL,
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
bug_fixes: 'Bug fix details',
|
bug_fixes: 'Bug fix details',
|
||||||
@@ -46,30 +28,15 @@ jest.mock(
|
|||||||
return <div>{children}</div>;
|
return <div>{children}</div>;
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
// mock useAppContext
|
// mock useAppContext
|
||||||
jest.mock('providers/App/App', () => ({
|
jest.mock('providers/App/App', () => ({
|
||||||
useAppContext: jest.fn(() => ({
|
useAppContext: jest.fn(() => ({ changelog: mockChangelog })),
|
||||||
updateUserPreferenceInContext: jest.fn(),
|
|
||||||
userPreferences: [
|
|
||||||
{
|
|
||||||
name: USER_PREFERENCES.LAST_SEEN_CHANGELOG_VERSION,
|
|
||||||
value: 'v1.0.0',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
})),
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
function renderChangelog(onClose: () => void = jest.fn()): void {
|
|
||||||
render(
|
|
||||||
<MockQueryClientProvider>
|
|
||||||
<ChangelogModal changelog={mockChangelog} onClose={onClose} />
|
|
||||||
</MockQueryClientProvider>,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
describe('ChangelogModal', () => {
|
describe('ChangelogModal', () => {
|
||||||
it('renders modal with changelog data', () => {
|
it('renders modal with changelog data', () => {
|
||||||
renderChangelog();
|
render(<ChangelogModal onClose={jest.fn()} />);
|
||||||
expect(
|
expect(
|
||||||
screen.getByText('What’s New ⎯ Changelog : June 10, 2025'),
|
screen.getByText('What’s New ⎯ Changelog : June 10, 2025'),
|
||||||
).toBeInTheDocument();
|
).toBeInTheDocument();
|
||||||
@@ -81,14 +48,14 @@ describe('ChangelogModal', () => {
|
|||||||
|
|
||||||
it('calls onClose when Skip for now is clicked', () => {
|
it('calls onClose when Skip for now is clicked', () => {
|
||||||
const onClose = jest.fn();
|
const onClose = jest.fn();
|
||||||
renderChangelog(onClose);
|
render(<ChangelogModal onClose={onClose} />);
|
||||||
fireEvent.click(screen.getByText('Skip for now'));
|
fireEvent.click(screen.getByText('Skip for now'));
|
||||||
expect(onClose).toHaveBeenCalled();
|
expect(onClose).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('opens migration docs when Update my workspace is clicked', () => {
|
it('opens migration docs when Update my workspace is clicked', () => {
|
||||||
window.open = jest.fn();
|
window.open = jest.fn();
|
||||||
renderChangelog();
|
render(<ChangelogModal onClose={jest.fn()} />);
|
||||||
fireEvent.click(screen.getByText('Update my workspace'));
|
fireEvent.click(screen.getByText('Update my workspace'));
|
||||||
expect(window.open).toHaveBeenCalledWith(
|
expect(window.open).toHaveBeenCalledWith(
|
||||||
'https://github.com/SigNoz/signoz/releases',
|
'https://github.com/SigNoz/signoz/releases',
|
||||||
@@ -98,7 +65,7 @@ describe('ChangelogModal', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('scrolls for more when Scroll for more is clicked', () => {
|
it('scrolls for more when Scroll for more is clicked', () => {
|
||||||
renderChangelog();
|
render(<ChangelogModal onClose={jest.fn()} />);
|
||||||
const scrollBtn = screen.getByTestId('scroll-more-btn');
|
const scrollBtn = screen.getByTestId('scroll-more-btn');
|
||||||
const contentDiv = screen.getByTestId('changelog-content');
|
const contentDiv = screen.getByTestId('changelog-content');
|
||||||
if (contentDiv) {
|
if (contentDiv) {
|
||||||
|
|||||||
@@ -3,10 +3,6 @@
|
|||||||
/* eslint-disable @typescript-eslint/explicit-function-return-type */
|
/* eslint-disable @typescript-eslint/explicit-function-return-type */
|
||||||
|
|
||||||
import { render, screen } from '@testing-library/react';
|
import { render, screen } from '@testing-library/react';
|
||||||
import {
|
|
||||||
ChangelogSchema,
|
|
||||||
DeploymentType,
|
|
||||||
} from 'types/api/changelog/getChangelogByVersion';
|
|
||||||
|
|
||||||
import ChangelogRenderer from '../components/ChangelogRenderer';
|
import ChangelogRenderer from '../components/ChangelogRenderer';
|
||||||
|
|
||||||
@@ -19,19 +15,23 @@ jest.mock(
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
const mockChangelog: ChangelogSchema = {
|
const mockChangelog = {
|
||||||
id: 1,
|
id: 1,
|
||||||
documentId: 'doc-1',
|
documentId: 'changelog-doc-1',
|
||||||
version: 'v1.0.0',
|
version: '1.0.0',
|
||||||
createdAt: '2025-06-09T12:00:00Z',
|
createdAt: '2025-06-09T12:00:00Z',
|
||||||
updatedAt: '2025-06-09T13:00:00Z',
|
|
||||||
publishedAt: '2025-06-09T14:00:00Z',
|
|
||||||
release_date: '2025-06-10',
|
release_date: '2025-06-10',
|
||||||
features: [
|
features: [
|
||||||
{
|
{
|
||||||
id: 1,
|
id: 1,
|
||||||
|
documentId: '1',
|
||||||
title: 'Feature 1',
|
title: 'Feature 1',
|
||||||
description: 'Description for feature 1',
|
description: 'Description for feature 1',
|
||||||
|
sort_order: 1,
|
||||||
|
createdAt: '',
|
||||||
|
updatedAt: '',
|
||||||
|
publishedAt: '',
|
||||||
|
deployment_type: 'All',
|
||||||
media: {
|
media: {
|
||||||
id: 1,
|
id: 1,
|
||||||
documentId: 'doc1',
|
documentId: 'doc1',
|
||||||
@@ -40,15 +40,11 @@ const mockChangelog: ChangelogSchema = {
|
|||||||
mime: 'image/webp',
|
mime: 'image/webp',
|
||||||
alternativeText: null,
|
alternativeText: null,
|
||||||
},
|
},
|
||||||
documentId: 'feature-1',
|
|
||||||
sort_order: 1,
|
|
||||||
createdAt: '2025-06-09T12:00:00Z',
|
|
||||||
updatedAt: '2025-06-09T13:00:00Z',
|
|
||||||
publishedAt: '2025-06-09T14:00:00Z',
|
|
||||||
deployment_type: DeploymentType.ALL,
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
bug_fixes: 'Bug fix details',
|
bug_fixes: 'Bug fix details',
|
||||||
|
updatedAt: '2025-06-09T12:00:00Z',
|
||||||
|
publishedAt: '2025-06-09T12:00:00Z',
|
||||||
maintenance: 'Maintenance details',
|
maintenance: 'Maintenance details',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -101,18 +101,13 @@
|
|||||||
line-height: 28px;
|
line-height: 28px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.changelog-media-image,
|
.changelog-media-image {
|
||||||
.changelog-media-video {
|
|
||||||
height: auto;
|
height: auto;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
border: 1px solid var(--bg-slate-400, #1d212d);
|
border: 1px solid var(--bg-slate-400, #1d212d);
|
||||||
}
|
}
|
||||||
|
|
||||||
.changelog-media-video {
|
|
||||||
margin: 12px 0;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.lightMode {
|
.lightMode {
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ function renderMedia(media: Media): JSX.Element | null {
|
|||||||
controls
|
controls
|
||||||
controlsList="nodownload noplaybackrate"
|
controlsList="nodownload noplaybackrate"
|
||||||
loop
|
loop
|
||||||
className="changelog-media-video"
|
className="my-3 h-auto w-full rounded"
|
||||||
>
|
>
|
||||||
<source src={media.url} type={media.mime} />
|
<source src={media.url} type={media.mime} />
|
||||||
<track kind="captions" src="" label="No captions available" default />
|
<track kind="captions" src="" label="No captions available" default />
|
||||||
@@ -56,7 +56,7 @@ function ChangelogRenderer({ changelog }: Props): JSX.Element {
|
|||||||
</div>
|
</div>
|
||||||
<span className="changelog-release-date">{formattedReleaseDate}</span>
|
<span className="changelog-release-date">{formattedReleaseDate}</span>
|
||||||
{changelog.features && changelog.features.length > 0 && (
|
{changelog.features && changelog.features.length > 0 && (
|
||||||
<div className="changelog-renderer-list">
|
<div className="changelog-renderer-list flex flex-col gap-7">
|
||||||
{changelog.features.map((feature) => (
|
{changelog.features.map((feature) => (
|
||||||
<div key={feature.id}>
|
<div key={feature.id}>
|
||||||
<h2>{feature.title}</h2>
|
<h2>{feature.title}</h2>
|
||||||
|
|||||||
@@ -194,7 +194,7 @@ function HostMetricTraces({
|
|||||||
{!isError && traces.length > 0 && (
|
{!isError && traces.length > 0 && (
|
||||||
<div className="host-metric-traces-table">
|
<div className="host-metric-traces-table">
|
||||||
<TraceExplorerControls
|
<TraceExplorerControls
|
||||||
isLoading={isFetching && traces.length === 0}
|
isLoading={isFetching}
|
||||||
totalCount={totalCount}
|
totalCount={totalCount}
|
||||||
perPageOptions={PER_PAGE_OPTIONS}
|
perPageOptions={PER_PAGE_OPTIONS}
|
||||||
showSizeChanger={false}
|
showSizeChanger={false}
|
||||||
@@ -203,7 +203,7 @@ function HostMetricTraces({
|
|||||||
tableLayout="fixed"
|
tableLayout="fixed"
|
||||||
pagination={false}
|
pagination={false}
|
||||||
scroll={{ x: true }}
|
scroll={{ x: true }}
|
||||||
loading={isFetching && traces.length === 0}
|
loading={isFetching}
|
||||||
dataSource={traces}
|
dataSource={traces}
|
||||||
columns={traceListColumns}
|
columns={traceListColumns}
|
||||||
onRow={(): Record<string, unknown> => ({
|
onRow={(): Record<string, unknown> => ({
|
||||||
|
|||||||
@@ -169,6 +169,7 @@
|
|||||||
box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.1);
|
box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.ant-drawer-close {
|
.ant-drawer-close {
|
||||||
padding: 0px;
|
padding: 0px;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ import {
|
|||||||
ScrollText,
|
ScrollText,
|
||||||
X,
|
X,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
import { useSearchParams } from 'react-router-dom-v5-compat';
|
import { useSearchParams } from 'react-router-dom-v5-compat';
|
||||||
import { AppState } from 'store/reducers';
|
import { AppState } from 'store/reducers';
|
||||||
@@ -86,12 +86,8 @@ function HostMetricsDetails({
|
|||||||
endTime: endMs,
|
endTime: endMs,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const lastSelectedInterval = useRef<Time | null>(null);
|
|
||||||
|
|
||||||
const [selectedInterval, setSelectedInterval] = useState<Time>(
|
const [selectedInterval, setSelectedInterval] = useState<Time>(
|
||||||
lastSelectedInterval.current
|
selectedTime as Time,
|
||||||
? lastSelectedInterval.current
|
|
||||||
: (selectedTime as Time),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const [selectedView, setSelectedView] = useState<VIEWS>(
|
const [selectedView, setSelectedView] = useState<VIEWS>(
|
||||||
@@ -154,11 +150,10 @@ function HostMetricsDetails({
|
|||||||
}, [initialFilters]);
|
}, [initialFilters]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const currentSelectedInterval = lastSelectedInterval.current || selectedTime;
|
setSelectedInterval(selectedTime as Time);
|
||||||
setSelectedInterval(currentSelectedInterval as Time);
|
|
||||||
|
|
||||||
if (currentSelectedInterval !== 'custom') {
|
if (selectedTime !== 'custom') {
|
||||||
const { maxTime, minTime } = GetMinMax(currentSelectedInterval);
|
const { maxTime, minTime } = GetMinMax(selectedTime);
|
||||||
|
|
||||||
setModalTimeRange({
|
setModalTimeRange({
|
||||||
startTime: Math.floor(minTime / 1000000000),
|
startTime: Math.floor(minTime / 1000000000),
|
||||||
@@ -186,7 +181,6 @@ function HostMetricsDetails({
|
|||||||
|
|
||||||
const handleTimeChange = useCallback(
|
const handleTimeChange = useCallback(
|
||||||
(interval: Time | CustomTimeType, dateTimeRange?: [number, number]): void => {
|
(interval: Time | CustomTimeType, dateTimeRange?: [number, number]): void => {
|
||||||
lastSelectedInterval.current = interval as Time;
|
|
||||||
setSelectedInterval(interval as Time);
|
setSelectedInterval(interval as Time);
|
||||||
|
|
||||||
if (interval === 'custom' && dateTimeRange) {
|
if (interval === 'custom' && dateTimeRange) {
|
||||||
@@ -362,7 +356,6 @@ function HostMetricsDetails({
|
|||||||
|
|
||||||
const handleClose = (): void => {
|
const handleClose = (): void => {
|
||||||
setSelectedInterval(selectedTime as Time);
|
setSelectedInterval(selectedTime as Time);
|
||||||
lastSelectedInterval.current = null;
|
|
||||||
setSearchParams({});
|
setSearchParams({});
|
||||||
|
|
||||||
if (selectedTime !== 'custom') {
|
if (selectedTime !== 'custom') {
|
||||||
|
|||||||
@@ -15,12 +15,11 @@ import {
|
|||||||
} from 'container/TopNav/DateTimeSelectionV2/config';
|
} from 'container/TopNav/DateTimeSelectionV2/config';
|
||||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||||
import { useResizeObserver } from 'hooks/useDimensions';
|
import { useResizeObserver } from 'hooks/useDimensions';
|
||||||
import { useMultiIntersectionObserver } from 'hooks/useMultiIntersectionObserver';
|
|
||||||
import { GetMetricQueryRange } from 'lib/dashboard/getQueryResults';
|
import { GetMetricQueryRange } from 'lib/dashboard/getQueryResults';
|
||||||
import { getUPlotChartOptions } from 'lib/uPlotLib/getUplotChartOptions';
|
import { getUPlotChartOptions } from 'lib/uPlotLib/getUplotChartOptions';
|
||||||
import { getUPlotChartData } from 'lib/uPlotLib/utils/getUplotChartData';
|
import { getUPlotChartData } from 'lib/uPlotLib/utils/getUplotChartData';
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import { QueryFunctionContext, useQueries, UseQueryResult } from 'react-query';
|
import { useQueries, UseQueryResult } from 'react-query';
|
||||||
import { SuccessResponse } from 'types/api';
|
import { SuccessResponse } from 'types/api';
|
||||||
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
|
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
|
||||||
|
|
||||||
@@ -54,11 +53,6 @@ function Metrics({
|
|||||||
featureFlags?.find((flag) => flag.name === FeatureKeys.DOT_METRICS_ENABLED)
|
featureFlags?.find((flag) => flag.name === FeatureKeys.DOT_METRICS_ENABLED)
|
||||||
?.active || false;
|
?.active || false;
|
||||||
|
|
||||||
const {
|
|
||||||
visibilities,
|
|
||||||
setElement,
|
|
||||||
} = useMultiIntersectionObserver(hostWidgetInfo.length, { threshold: 0.1 });
|
|
||||||
|
|
||||||
const queryPayloads = useMemo(
|
const queryPayloads = useMemo(
|
||||||
() =>
|
() =>
|
||||||
getHostQueryPayload(
|
getHostQueryPayload(
|
||||||
@@ -71,15 +65,11 @@ function Metrics({
|
|||||||
);
|
);
|
||||||
|
|
||||||
const queries = useQueries(
|
const queries = useQueries(
|
||||||
queryPayloads.map((payload, index) => ({
|
queryPayloads.map((payload) => ({
|
||||||
queryKey: ['host-metrics', payload, ENTITY_VERSION_V4, 'HOST'],
|
queryKey: ['host-metrics', payload, ENTITY_VERSION_V4, 'HOST'],
|
||||||
queryFn: ({
|
queryFn: (): Promise<SuccessResponse<MetricRangePayloadProps>> =>
|
||||||
signal,
|
GetMetricQueryRange(payload, ENTITY_VERSION_V4),
|
||||||
}: QueryFunctionContext): Promise<
|
enabled: !!payload,
|
||||||
SuccessResponse<MetricRangePayloadProps>
|
|
||||||
> => GetMetricQueryRange(payload, ENTITY_VERSION_V4, signal),
|
|
||||||
enabled: !!payload && visibilities[index],
|
|
||||||
keepPreviousData: true,
|
|
||||||
})),
|
})),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -153,7 +143,7 @@ function Metrics({
|
|||||||
query: UseQueryResult<SuccessResponse<MetricRangePayloadProps>, unknown>,
|
query: UseQueryResult<SuccessResponse<MetricRangePayloadProps>, unknown>,
|
||||||
idx: number,
|
idx: number,
|
||||||
): JSX.Element => {
|
): JSX.Element => {
|
||||||
if ((!query.data && query.isLoading) || !visibilities[idx]) {
|
if (query.isLoading) {
|
||||||
return <Skeleton />;
|
return <Skeleton />;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -191,7 +181,7 @@ function Metrics({
|
|||||||
</div>
|
</div>
|
||||||
<Row gutter={24} className="host-metrics-container">
|
<Row gutter={24} className="host-metrics-container">
|
||||||
{queries.map((query, idx) => (
|
{queries.map((query, idx) => (
|
||||||
<Col ref={setElement(idx)} span={12} key={hostWidgetInfo[idx].title}>
|
<Col span={12} key={hostWidgetInfo[idx].title}>
|
||||||
<Typography.Text>{hostWidgetInfo[idx].title}</Typography.Text>
|
<Typography.Text>{hostWidgetInfo[idx].title}</Typography.Text>
|
||||||
<Card bordered className="host-metrics-card" ref={graphRef}>
|
<Card bordered className="host-metrics-card" ref={graphRef}>
|
||||||
{renderCardContent(query, idx)}
|
{renderCardContent(query, idx)}
|
||||||
|
|||||||
@@ -0,0 +1,101 @@
|
|||||||
|
.input-with-label {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
|
||||||
|
border-radius: 2px 0px 0px 2px;
|
||||||
|
|
||||||
|
.label {
|
||||||
|
color: var(--bg-vanilla-400);
|
||||||
|
font-size: 12px;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 500;
|
||||||
|
line-height: 18px; /* 128.571% */
|
||||||
|
letter-spacing: 0.56px;
|
||||||
|
|
||||||
|
max-width: 150px;
|
||||||
|
min-width: 120px;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
|
||||||
|
padding: 0px 8px;
|
||||||
|
|
||||||
|
border-radius: 2px 0px 0px 2px;
|
||||||
|
border: 1px solid var(--bg-slate-400);
|
||||||
|
background: var(--bg-ink-300);
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-start;
|
||||||
|
align-items: center;
|
||||||
|
font-weight: var(--font-weight-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
.input {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 150px;
|
||||||
|
font-family: 'Space Mono', monospace !important;
|
||||||
|
|
||||||
|
border-radius: 2px 0px 0px 2px;
|
||||||
|
border: 1px solid var(--bg-slate-400);
|
||||||
|
background: var(--bg-ink-300);
|
||||||
|
|
||||||
|
border-right: none;
|
||||||
|
border-left: none;
|
||||||
|
border-top-right-radius: 0px;
|
||||||
|
border-bottom-right-radius: 0px;
|
||||||
|
border-top-left-radius: 0px;
|
||||||
|
border-bottom-left-radius: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.close-btn {
|
||||||
|
border-radius: 0px 2px 2px 0px;
|
||||||
|
border: 1px solid var(--bg-slate-400);
|
||||||
|
background: var(--bg-ink-300);
|
||||||
|
height: 38px;
|
||||||
|
width: 38px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.labelAfter {
|
||||||
|
.input {
|
||||||
|
border-radius: 0px 2px 2px 0px;
|
||||||
|
border: 1px solid var(--bg-slate-400);
|
||||||
|
background: var(--bg-ink-300);
|
||||||
|
border-top-right-radius: 0px;
|
||||||
|
border-bottom-right-radius: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label {
|
||||||
|
border-left: none;
|
||||||
|
border-top-left-radius: 0px;
|
||||||
|
border-bottom-left-radius: 0px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.lightMode {
|
||||||
|
.input-with-label {
|
||||||
|
.label {
|
||||||
|
color: var(--bg-ink-500) !important;
|
||||||
|
|
||||||
|
border: 1px solid var(--bg-vanilla-300) !important;
|
||||||
|
background: var(--bg-vanilla-100) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input {
|
||||||
|
border: 1px solid var(--bg-vanilla-300) !important;
|
||||||
|
background: var(--bg-vanilla-100) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.close-btn {
|
||||||
|
border: 1px solid var(--bg-vanilla-300) !important;
|
||||||
|
background: var(--bg-vanilla-100) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.labelAfter {
|
||||||
|
.input {
|
||||||
|
border: 1px solid var(--bg-vanilla-300) !important;
|
||||||
|
background: var(--bg-vanilla-100) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
71
frontend/src/components/InputWithLabel/InputWithLabel.tsx
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
import './InputWithLabel.styles.scss';
|
||||||
|
|
||||||
|
import { Button, Input, Typography } from 'antd';
|
||||||
|
import cx from 'classnames';
|
||||||
|
import { X } from 'lucide-react';
|
||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
function InputWithLabel({
|
||||||
|
label,
|
||||||
|
initialValue,
|
||||||
|
placeholder,
|
||||||
|
type,
|
||||||
|
onClose,
|
||||||
|
labelAfter,
|
||||||
|
onChange,
|
||||||
|
className,
|
||||||
|
}: {
|
||||||
|
label: string;
|
||||||
|
initialValue?: string | number;
|
||||||
|
placeholder: string;
|
||||||
|
type?: string;
|
||||||
|
onClose?: () => void;
|
||||||
|
labelAfter?: boolean;
|
||||||
|
onChange: (value: string) => void;
|
||||||
|
className?: string;
|
||||||
|
}): JSX.Element {
|
||||||
|
const [inputValue, setInputValue] = useState<string>(
|
||||||
|
initialValue ? initialValue.toString() : '',
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleChange = (e: React.ChangeEvent<HTMLInputElement>): void => {
|
||||||
|
setInputValue(e.target.value);
|
||||||
|
onChange?.(e.target.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cx('input-with-label', className, {
|
||||||
|
labelAfter,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{!labelAfter && <Typography.Text className="label">{label}</Typography.Text>}
|
||||||
|
<Input
|
||||||
|
className="input"
|
||||||
|
placeholder={placeholder}
|
||||||
|
type={type}
|
||||||
|
value={inputValue}
|
||||||
|
onChange={handleChange}
|
||||||
|
name={label.toLowerCase()}
|
||||||
|
/>
|
||||||
|
{labelAfter && <Typography.Text className="label">{label}</Typography.Text>}
|
||||||
|
{onClose && (
|
||||||
|
<Button
|
||||||
|
className="periscope-btn ghost close-btn"
|
||||||
|
icon={<X size={16} />}
|
||||||
|
onClick={onClose}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
InputWithLabel.defaultProps = {
|
||||||
|
type: 'text',
|
||||||
|
onClose: undefined,
|
||||||
|
labelAfter: false,
|
||||||
|
initialValue: undefined,
|
||||||
|
className: undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default InputWithLabel;
|
||||||
@@ -71,7 +71,7 @@ function LogDetail({
|
|||||||
const [contextQuery, setContextQuery] = useState<Query | undefined>();
|
const [contextQuery, setContextQuery] = useState<Query | undefined>();
|
||||||
const [filters, setFilters] = useState<TagFilter | null>(null);
|
const [filters, setFilters] = useState<TagFilter | null>(null);
|
||||||
const [isEdit, setIsEdit] = useState<boolean>(false);
|
const [isEdit, setIsEdit] = useState<boolean>(false);
|
||||||
const { stagedQuery } = useQueryBuilder();
|
const { initialDataSource, stagedQuery } = useQueryBuilder();
|
||||||
|
|
||||||
const listQuery = useMemo(() => {
|
const listQuery = useMemo(() => {
|
||||||
if (!stagedQuery || stagedQuery.builder.queryData.length < 1) return null;
|
if (!stagedQuery || stagedQuery.builder.queryData.length < 1) return null;
|
||||||
@@ -81,7 +81,7 @@ function LogDetail({
|
|||||||
|
|
||||||
const { options } = useOptionsMenu({
|
const { options } = useOptionsMenu({
|
||||||
storageKey: LOCALSTORAGE.LOGS_LIST_OPTIONS,
|
storageKey: LOCALSTORAGE.LOGS_LIST_OPTIONS,
|
||||||
dataSource: DataSource.LOGS,
|
dataSource: initialDataSource || DataSource.LOGS,
|
||||||
aggregateOperator: listQuery?.aggregateOperator || StringOperators.NOOP,
|
aggregateOperator: listQuery?.aggregateOperator || StringOperators.NOOP,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ export function getDefaultCellStyle(isDarkMode?: boolean): CSSProperties {
|
|||||||
letterSpacing: '-0.07px',
|
letterSpacing: '-0.07px',
|
||||||
marginBottom: '0px',
|
marginBottom: '0px',
|
||||||
minWidth: '10rem',
|
minWidth: '10rem',
|
||||||
width: '10rem',
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -47,14 +47,6 @@ export const useTableView = (props: UseTableViewProps): UseTableViewResult => {
|
|||||||
|
|
||||||
const { formatTimezoneAdjustedTimestamp } = useTimezone();
|
const { formatTimezoneAdjustedTimestamp } = useTimezone();
|
||||||
|
|
||||||
const bodyColumnStyle = useMemo(
|
|
||||||
() => ({
|
|
||||||
...defaultTableStyle,
|
|
||||||
...(fields.length > 2 ? { width: '50rem' } : {}),
|
|
||||||
}),
|
|
||||||
[fields.length],
|
|
||||||
);
|
|
||||||
|
|
||||||
const columns: ColumnsType<Record<string, unknown>> = useMemo(() => {
|
const columns: ColumnsType<Record<string, unknown>> = useMemo(() => {
|
||||||
const fieldColumns: ColumnsType<Record<string, unknown>> = fields
|
const fieldColumns: ColumnsType<Record<string, unknown>> = fields
|
||||||
.filter((e) => !['id', 'body', 'timestamp'].includes(e.name))
|
.filter((e) => !['id', 'body', 'timestamp'].includes(e.name))
|
||||||
@@ -144,7 +136,7 @@ export const useTableView = (props: UseTableViewProps): UseTableViewResult => {
|
|||||||
field: string | number,
|
field: string | number,
|
||||||
): ColumnTypeRender<Record<string, unknown>> => ({
|
): ColumnTypeRender<Record<string, unknown>> => ({
|
||||||
props: {
|
props: {
|
||||||
style: bodyColumnStyle,
|
style: defaultTableStyle,
|
||||||
},
|
},
|
||||||
children: (
|
children: (
|
||||||
<TableBodyContent
|
<TableBodyContent
|
||||||
@@ -174,7 +166,6 @@ export const useTableView = (props: UseTableViewProps): UseTableViewResult => {
|
|||||||
linesPerRow,
|
linesPerRow,
|
||||||
fontSize,
|
fontSize,
|
||||||
formatTimezoneAdjustedTimestamp,
|
formatTimezoneAdjustedTimestamp,
|
||||||
bodyColumnStyle,
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return { columns, dataSource: flattenLogData };
|
return { columns, dataSource: flattenLogData };
|
||||||
|
|||||||
@@ -410,18 +410,18 @@ export default function LogsFormatOptionsMenu({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="column-format">
|
<div className="column-format">
|
||||||
{addColumn?.value?.map(({ key, id }) => (
|
{addColumn?.value?.map(({ name }) => (
|
||||||
<div className="column-name" key={id}>
|
<div className="column-name" key={name}>
|
||||||
<div className="name">
|
<div className="name">
|
||||||
<Tooltip placement="left" title={key}>
|
<Tooltip placement="left" title={name}>
|
||||||
{key}
|
{name}
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
{addColumn?.value?.length > 1 && (
|
{addColumn?.value?.length > 1 && (
|
||||||
<X
|
<X
|
||||||
className="delete-btn"
|
className="delete-btn"
|
||||||
size={14}
|
size={14}
|
||||||
onClick={(): void => addColumn.onRemove(id as string)}
|
onClick={(): void => addColumn.onRemove(name)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,553 @@
|
|||||||
|
.query-builder-v2 {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: 4px;
|
||||||
|
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
border-bottom: 1px solid var(--bg-slate-400);
|
||||||
|
border-top: 1px solid var(--bg-slate-400);
|
||||||
|
|
||||||
|
font-family: Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto,
|
||||||
|
'Helvetica Neue', sans-serif;
|
||||||
|
|
||||||
|
border-right: none;
|
||||||
|
border-left: none;
|
||||||
|
|
||||||
|
.qb-content-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
width: calc(100% - 44px);
|
||||||
|
|
||||||
|
flex: 1;
|
||||||
|
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qb-content-section {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 8px;
|
||||||
|
|
||||||
|
flex: 1;
|
||||||
|
|
||||||
|
.qb-header-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
margin-left: 32px;
|
||||||
|
|
||||||
|
.query-actions-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: 8px;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.qb-elements-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
|
margin-left: 108px;
|
||||||
|
|
||||||
|
.code-mirror-where-clause,
|
||||||
|
.query-aggregation-container,
|
||||||
|
.query-add-ons,
|
||||||
|
.metrics-aggregation-section-content {
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
left: -10px;
|
||||||
|
top: 12px;
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
border-left: 6px dotted #1d212d;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Horizontal line pointing from vertical to the item */
|
||||||
|
&::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
left: -28px;
|
||||||
|
top: 15px;
|
||||||
|
width: 24px;
|
||||||
|
height: 1px;
|
||||||
|
background: repeating-linear-gradient(
|
||||||
|
to right,
|
||||||
|
#1d212d,
|
||||||
|
#1d212d 4px,
|
||||||
|
transparent 4px,
|
||||||
|
transparent 8px
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.where-clause-view {
|
||||||
|
.qb-content-section {
|
||||||
|
.qb-elements-container {
|
||||||
|
margin-left: 0px;
|
||||||
|
|
||||||
|
.code-mirror-where-clause,
|
||||||
|
.query-aggregation-container,
|
||||||
|
.query-add-ons,
|
||||||
|
.metrics-aggregation-section-content {
|
||||||
|
&::before {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.query-names-section {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
|
width: 44px;
|
||||||
|
padding: 8px;
|
||||||
|
|
||||||
|
border-left: 1px solid var(--bg-slate-400);
|
||||||
|
|
||||||
|
.query-name {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
|
||||||
|
padding: 4px;
|
||||||
|
|
||||||
|
border-radius: 0px 2px 2px 0px;
|
||||||
|
|
||||||
|
border-radius: 2px;
|
||||||
|
border: 1px solid rgba(242, 71, 105, 0.2);
|
||||||
|
background: rgba(242, 71, 105, 0.1);
|
||||||
|
|
||||||
|
color: var(--Sakura-400, #f56c87);
|
||||||
|
font-family: 'Space Mono';
|
||||||
|
font-size: 12px;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 16px; /* 128.571% */
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formula-name {
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
|
||||||
|
padding: 4px;
|
||||||
|
|
||||||
|
border-radius: 0px 2px 2px 0px;
|
||||||
|
|
||||||
|
border-radius: 2px;
|
||||||
|
border: 1px solid rgba(173, 127, 88, 0.2);
|
||||||
|
background: rgba(173, 127, 88, 0.1);
|
||||||
|
|
||||||
|
color: var(--Sienna-500, #ad7f58);
|
||||||
|
font-family: 'Space Mono';
|
||||||
|
font-size: 12px;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 16px; /* 128.571% */
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.qb-formulas-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
|
margin-left: 32px;
|
||||||
|
padding-bottom: 16px;
|
||||||
|
padding-left: 8px;
|
||||||
|
|
||||||
|
.qb-formula {
|
||||||
|
.ant-row {
|
||||||
|
row-gap: 0px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qb-entity-options {
|
||||||
|
margin-left: 8px;
|
||||||
|
|
||||||
|
padding-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formula-container {
|
||||||
|
margin-left: 82px;
|
||||||
|
padding: 4px 0px;
|
||||||
|
|
||||||
|
.ant-col {
|
||||||
|
&::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
left: -10px;
|
||||||
|
top: 12px;
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
border-left: 6px dotted #1d212d;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Horizontal line pointing from vertical to the item */
|
||||||
|
&::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
left: -28px;
|
||||||
|
top: 15px;
|
||||||
|
width: 24px;
|
||||||
|
height: 1px;
|
||||||
|
background: repeating-linear-gradient(
|
||||||
|
to right,
|
||||||
|
#1d212d,
|
||||||
|
#1d212d 4px,
|
||||||
|
transparent 4px,
|
||||||
|
transparent 8px
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.formula-expression {
|
||||||
|
border-bottom-left-radius: 0px !important;
|
||||||
|
border-bottom-right-radius: 0px !important;
|
||||||
|
|
||||||
|
font-family: 'Space Mono';
|
||||||
|
font-size: 12px;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 16px; /* 128.571% */
|
||||||
|
|
||||||
|
resize: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formula-legend {
|
||||||
|
border-top-left-radius: 0px !important;
|
||||||
|
border-top-right-radius: 0px !important;
|
||||||
|
|
||||||
|
.ant-input-group-addon {
|
||||||
|
border-top-left-radius: 0px !important;
|
||||||
|
border-top-right-radius: 0px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-input {
|
||||||
|
border-top-left-radius: 0px !important;
|
||||||
|
border-top-right-radius: 0px !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.qb-footer {
|
||||||
|
padding: 0 8px 16px 8px;
|
||||||
|
|
||||||
|
.qb-footer-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
|
margin-left: 32px;
|
||||||
|
|
||||||
|
.qb-add-new-query {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: '';
|
||||||
|
height: calc(100% - 82px);
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
left: 56px;
|
||||||
|
top: 31px;
|
||||||
|
bottom: 0;
|
||||||
|
width: 1px;
|
||||||
|
background: repeating-linear-gradient(
|
||||||
|
to bottom,
|
||||||
|
#1d212d,
|
||||||
|
#1d212d 4px,
|
||||||
|
transparent 4px,
|
||||||
|
transparent 8px
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.qb-entity-options {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
|
.options {
|
||||||
|
.query-name {
|
||||||
|
border-radius: 0px 2px 2px 0px !important;
|
||||||
|
border: 1px solid rgba(242, 71, 105, 0.2) !important;
|
||||||
|
background: rgba(242, 71, 105, 0.1) !important;
|
||||||
|
|
||||||
|
color: var(--Sakura-400, #f56c87) !important;
|
||||||
|
font-family: 'Space Mono';
|
||||||
|
font-size: 14px;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 18px; /* 128.571% */
|
||||||
|
text-transform: uppercase;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: '';
|
||||||
|
height: 120px;
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 31px;
|
||||||
|
bottom: 0;
|
||||||
|
width: 1px;
|
||||||
|
background: repeating-linear-gradient(
|
||||||
|
to bottom,
|
||||||
|
#1d212d,
|
||||||
|
#1d212d 4px,
|
||||||
|
transparent 4px,
|
||||||
|
transparent 8px
|
||||||
|
);
|
||||||
|
left: 15px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.formula-name {
|
||||||
|
border-radius: 0px 2px 2px 0px;
|
||||||
|
border: 1px solid rgba(173, 127, 88, 0.2);
|
||||||
|
background: rgba(173, 127, 88, 0.1);
|
||||||
|
|
||||||
|
font-family: 'Space Mono';
|
||||||
|
font-size: 14px;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 18px; /* 128.571% */
|
||||||
|
text-transform: uppercase;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: '';
|
||||||
|
height: 65px;
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 31px;
|
||||||
|
bottom: 0;
|
||||||
|
width: 1px;
|
||||||
|
background: repeating-linear-gradient(
|
||||||
|
to bottom,
|
||||||
|
#1d212d,
|
||||||
|
#1d212d 4px,
|
||||||
|
transparent 4px,
|
||||||
|
transparent 8px
|
||||||
|
);
|
||||||
|
left: 15px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.query-data-source {
|
||||||
|
margin-left: 8px;
|
||||||
|
|
||||||
|
.ant-select-selector {
|
||||||
|
min-width: 120px;
|
||||||
|
|
||||||
|
border-radius: 2px;
|
||||||
|
border: 1px solid var(--Slate-400, #1d212d);
|
||||||
|
background: var(--Ink-300, #16181d);
|
||||||
|
box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.qb-search-container {
|
||||||
|
.metrics-select-container {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.qb-search-filter-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
|
flex-wrap: wrap;
|
||||||
|
|
||||||
|
.query-search-container {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.traces-search-filter-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
|
width: 180px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-select {
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-select-selector {
|
||||||
|
border-radius: 2px;
|
||||||
|
border: 1px solid var(--Slate-400, #1d212d) !important;
|
||||||
|
background: var(--Ink-300, #16181d) !important;
|
||||||
|
height: 34px !important;
|
||||||
|
box-sizing: border-box !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-select-arrow {
|
||||||
|
color: var(--bg-vanilla-400) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.query-actions-dropdown {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.lightMode {
|
||||||
|
.query-builder-v2 {
|
||||||
|
border-bottom: 1px solid var(--bg-vanilla-300);
|
||||||
|
border-top: 1px solid var(--bg-vanilla-300);
|
||||||
|
|
||||||
|
.qb-content-section {
|
||||||
|
.qb-elements-container {
|
||||||
|
.code-mirror-where-clause,
|
||||||
|
.query-aggregation-container,
|
||||||
|
.query-add-ons,
|
||||||
|
.metrics-aggregation-section-content {
|
||||||
|
&::before {
|
||||||
|
border-left: 6px dotted var(--bg-vanilla-300);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Horizontal line pointing from vertical to the item */
|
||||||
|
&::after {
|
||||||
|
background: repeating-linear-gradient(
|
||||||
|
to right,
|
||||||
|
var(--bg-vanilla-300),
|
||||||
|
var(--bg-vanilla-300) 4px,
|
||||||
|
transparent 4px,
|
||||||
|
transparent 8px
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.query-names-section {
|
||||||
|
border-left: 1px solid var(--bg-vanilla-300);
|
||||||
|
}
|
||||||
|
|
||||||
|
.qb-formulas-container {
|
||||||
|
.qb-formula {
|
||||||
|
.formula-container {
|
||||||
|
.ant-col {
|
||||||
|
&::before {
|
||||||
|
border-left: 6px dotted var(--bg-vanilla-300);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Horizontal line pointing from vertical to the item */
|
||||||
|
&::after {
|
||||||
|
background: repeating-linear-gradient(
|
||||||
|
to right,
|
||||||
|
var(--bg-vanilla-300),
|
||||||
|
var(--bg-vanilla-300) 4px,
|
||||||
|
transparent 4px,
|
||||||
|
transparent 8px
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.qb-footer {
|
||||||
|
.qb-footer-container {
|
||||||
|
.qb-add-new-query {
|
||||||
|
&::before {
|
||||||
|
background: repeating-linear-gradient(
|
||||||
|
to bottom,
|
||||||
|
var(--bg-vanilla-300),
|
||||||
|
var(--bg-vanilla-300) 4px,
|
||||||
|
transparent 4px,
|
||||||
|
transparent 8px
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.qb-entity-options {
|
||||||
|
.options {
|
||||||
|
.query-name {
|
||||||
|
&::before {
|
||||||
|
background: repeating-linear-gradient(
|
||||||
|
to bottom,
|
||||||
|
var(--bg-vanilla-300),
|
||||||
|
var(--bg-vanilla-300) 4px,
|
||||||
|
transparent 4px,
|
||||||
|
transparent 8px
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.formula-name {
|
||||||
|
&::before {
|
||||||
|
background: repeating-linear-gradient(
|
||||||
|
to bottom,
|
||||||
|
var(--bg-vanilla-300),
|
||||||
|
var(--bg-vanilla-300) 4px,
|
||||||
|
transparent 4px,
|
||||||
|
transparent 8px
|
||||||
|
);
|
||||||
|
left: 15px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.query-data-source {
|
||||||
|
.ant-select-selector {
|
||||||
|
border: 1px solid var(--bg-vanilla-300) !important;
|
||||||
|
background: var(--bg-vanilla-100) !important;
|
||||||
|
box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.1) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.qb-search-filter-container {
|
||||||
|
.ant-select-selector {
|
||||||
|
border: 1px solid var(--bg-vanilla-300) !important;
|
||||||
|
background: var(--bg-vanilla-100) !important;
|
||||||
|
box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.1) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-select-arrow {
|
||||||
|
color: var(--bg-vanilla-400) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
185
frontend/src/components/QueryBuilderV2/QueryBuilderV2.tsx
Normal file
@@ -0,0 +1,185 @@
|
|||||||
|
import './QueryBuilderV2.styles.scss';
|
||||||
|
|
||||||
|
import { OPERATORS, PANEL_TYPES } from 'constants/queryBuilder';
|
||||||
|
import { Formula } from 'container/QueryBuilder/components/Formula';
|
||||||
|
import { QueryBuilderProps } from 'container/QueryBuilder/QueryBuilder.interfaces';
|
||||||
|
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||||
|
import { memo, useEffect, useMemo, useRef } from 'react';
|
||||||
|
import { DataSource } from 'types/common/queryBuilder';
|
||||||
|
|
||||||
|
import { QueryBuilderV2Provider } from './QueryBuilderV2Context';
|
||||||
|
import QueryFooter from './QueryV2/QueryFooter/QueryFooter';
|
||||||
|
import { QueryV2 } from './QueryV2/QueryV2';
|
||||||
|
|
||||||
|
export const QueryBuilderV2 = memo(function QueryBuilderV2({
|
||||||
|
config,
|
||||||
|
panelType: newPanelType,
|
||||||
|
filterConfigs = {},
|
||||||
|
queryComponents,
|
||||||
|
isListViewPanel = false,
|
||||||
|
showOnlyWhereClause = false,
|
||||||
|
version,
|
||||||
|
}: QueryBuilderProps): JSX.Element {
|
||||||
|
const {
|
||||||
|
currentQuery,
|
||||||
|
addNewBuilderQuery,
|
||||||
|
addNewFormula,
|
||||||
|
handleSetConfig,
|
||||||
|
panelType,
|
||||||
|
initialDataSource,
|
||||||
|
} = useQueryBuilder();
|
||||||
|
|
||||||
|
const containerRef = useRef(null);
|
||||||
|
|
||||||
|
const currentDataSource = useMemo(
|
||||||
|
() =>
|
||||||
|
(config && config.queryVariant === 'static' && config.initialDataSource) ||
|
||||||
|
null,
|
||||||
|
[config],
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (currentDataSource !== initialDataSource || newPanelType !== panelType) {
|
||||||
|
if (newPanelType === PANEL_TYPES.BAR) {
|
||||||
|
handleSetConfig(PANEL_TYPES.BAR, DataSource.METRICS);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
handleSetConfig(newPanelType, currentDataSource);
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
handleSetConfig,
|
||||||
|
panelType,
|
||||||
|
initialDataSource,
|
||||||
|
currentDataSource,
|
||||||
|
newPanelType,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const listViewLogFilterConfigs: QueryBuilderProps['filterConfigs'] = useMemo(() => {
|
||||||
|
const config: QueryBuilderProps['filterConfigs'] = {
|
||||||
|
stepInterval: { isHidden: true, isDisabled: true },
|
||||||
|
having: { isHidden: true, isDisabled: true },
|
||||||
|
filters: {
|
||||||
|
customKey: 'body',
|
||||||
|
customOp: OPERATORS.CONTAINS,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return config;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const listViewTracesFilterConfigs: QueryBuilderProps['filterConfigs'] = useMemo(() => {
|
||||||
|
const config: QueryBuilderProps['filterConfigs'] = {
|
||||||
|
stepInterval: { isHidden: true, isDisabled: true },
|
||||||
|
having: { isHidden: true, isDisabled: true },
|
||||||
|
limit: { isHidden: true, isDisabled: true },
|
||||||
|
filters: {
|
||||||
|
customKey: 'body',
|
||||||
|
customOp: OPERATORS.CONTAINS,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return config;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const queryFilterConfigs = useMemo(() => {
|
||||||
|
if (isListViewPanel) {
|
||||||
|
return currentQuery.builder.queryData[0].dataSource === DataSource.TRACES
|
||||||
|
? listViewTracesFilterConfigs
|
||||||
|
: listViewLogFilterConfigs;
|
||||||
|
}
|
||||||
|
|
||||||
|
return filterConfigs;
|
||||||
|
}, [
|
||||||
|
isListViewPanel,
|
||||||
|
filterConfigs,
|
||||||
|
currentQuery.builder.queryData,
|
||||||
|
listViewLogFilterConfigs,
|
||||||
|
listViewTracesFilterConfigs,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<QueryBuilderV2Provider>
|
||||||
|
<div className="query-builder-v2">
|
||||||
|
<div className="qb-content-container">
|
||||||
|
{isListViewPanel && (
|
||||||
|
<QueryV2
|
||||||
|
ref={containerRef}
|
||||||
|
key={currentQuery.builder.queryData[0].queryName}
|
||||||
|
index={0}
|
||||||
|
query={currentQuery.builder.queryData[0]}
|
||||||
|
filterConfigs={queryFilterConfigs}
|
||||||
|
queryComponents={queryComponents}
|
||||||
|
version={version}
|
||||||
|
isAvailableToDisable={false}
|
||||||
|
queryVariant={config?.queryVariant || 'dropdown'}
|
||||||
|
showOnlyWhereClause={showOnlyWhereClause}
|
||||||
|
isListViewPanel={isListViewPanel}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!isListViewPanel &&
|
||||||
|
currentQuery.builder.queryData.map((query, index) => (
|
||||||
|
<QueryV2
|
||||||
|
ref={containerRef}
|
||||||
|
key={query.queryName}
|
||||||
|
index={index}
|
||||||
|
query={query}
|
||||||
|
filterConfigs={queryFilterConfigs}
|
||||||
|
queryComponents={queryComponents}
|
||||||
|
version={version}
|
||||||
|
isAvailableToDisable={false}
|
||||||
|
queryVariant={config?.queryVariant || 'dropdown'}
|
||||||
|
showOnlyWhereClause={showOnlyWhereClause}
|
||||||
|
isListViewPanel={isListViewPanel}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{!showOnlyWhereClause && currentQuery.builder.queryFormulas.length > 0 && (
|
||||||
|
<div className="qb-formulas-container">
|
||||||
|
{currentQuery.builder.queryFormulas.map((formula, index) => {
|
||||||
|
const query =
|
||||||
|
currentQuery.builder.queryData[index] ||
|
||||||
|
currentQuery.builder.queryData[0];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={formula.queryName} className="qb-formula">
|
||||||
|
<Formula
|
||||||
|
filterConfigs={filterConfigs}
|
||||||
|
query={query}
|
||||||
|
formula={formula}
|
||||||
|
index={index}
|
||||||
|
isAdditionalFilterEnable={false}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!showOnlyWhereClause && !isListViewPanel && (
|
||||||
|
<QueryFooter
|
||||||
|
addNewBuilderQuery={addNewBuilderQuery}
|
||||||
|
addNewFormula={addNewFormula}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{!showOnlyWhereClause && !isListViewPanel && (
|
||||||
|
<div className="query-names-section">
|
||||||
|
{currentQuery.builder.queryData.map((query) => (
|
||||||
|
<div key={query.queryName} className="query-name">
|
||||||
|
{query.queryName}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{currentQuery.builder.queryFormulas.map((formula) => (
|
||||||
|
<div key={formula.queryName} className="formula-name">
|
||||||
|
{formula.queryName}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</QueryBuilderV2Provider>
|
||||||
|
);
|
||||||
|
});
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
import { createContext, ReactNode, useContext, useMemo, useState } from 'react';
|
||||||
|
|
||||||
|
// Types for the context state
|
||||||
|
export type AggregationOption = { func: string; arg: string };
|
||||||
|
|
||||||
|
interface QueryBuilderV2ContextType {
|
||||||
|
searchText: string;
|
||||||
|
setSearchText: (text: string) => void;
|
||||||
|
aggregationOptions: AggregationOption[];
|
||||||
|
setAggregationOptions: (options: AggregationOption[]) => void;
|
||||||
|
aggregationInterval: string;
|
||||||
|
setAggregationInterval: (interval: string) => void;
|
||||||
|
queryAddValues: any; // Replace 'any' with a more specific type if available
|
||||||
|
setQueryAddValues: (values: any) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QueryBuilderV2Context = createContext<
|
||||||
|
QueryBuilderV2ContextType | undefined
|
||||||
|
>(undefined);
|
||||||
|
|
||||||
|
export function QueryBuilderV2Provider({
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
children: ReactNode;
|
||||||
|
}): JSX.Element {
|
||||||
|
const [searchText, setSearchText] = useState('');
|
||||||
|
const [aggregationOptions, setAggregationOptions] = useState<
|
||||||
|
AggregationOption[]
|
||||||
|
>([]);
|
||||||
|
const [aggregationInterval, setAggregationInterval] = useState('');
|
||||||
|
const [queryAddValues, setQueryAddValues] = useState<any>(null); // Replace 'any' if you have a type
|
||||||
|
|
||||||
|
return (
|
||||||
|
<QueryBuilderV2Context.Provider
|
||||||
|
value={useMemo(
|
||||||
|
() => ({
|
||||||
|
searchText,
|
||||||
|
setSearchText,
|
||||||
|
aggregationOptions,
|
||||||
|
setAggregationOptions,
|
||||||
|
aggregationInterval,
|
||||||
|
setAggregationInterval,
|
||||||
|
queryAddValues,
|
||||||
|
setQueryAddValues,
|
||||||
|
}),
|
||||||
|
[searchText, aggregationOptions, aggregationInterval, queryAddValues],
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</QueryBuilderV2Context.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useQueryBuilderV2Context = (): QueryBuilderV2ContextType => {
|
||||||
|
const context = useContext(QueryBuilderV2Context);
|
||||||
|
if (!context) {
|
||||||
|
throw new Error(
|
||||||
|
'useQueryBuilderV2Context must be used within a QueryBuilderV2Provider',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
};
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
.metrics-aggregate-section {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
margin: 4px 0;
|
||||||
|
|
||||||
|
.metrics-time-aggregation-section {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
|
||||||
|
.metrics-time-aggregation-section-title {
|
||||||
|
display: flex;
|
||||||
|
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-space-aggregation-section {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
|
||||||
|
.metrics-space-aggregation-section-title {
|
||||||
|
display: flex;
|
||||||
|
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 {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
|
.metrics-aggregation-section-content-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
|
||||||
|
.metrics-aggregation-section-content-item-label {
|
||||||
|
color: var(--Vanilla-400, #c0c1c3);
|
||||||
|
font-family: 'Geist Mono';
|
||||||
|
font-size: 13px;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 20px; /* 142.857% */
|
||||||
|
letter-spacing: -0.07px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metrics-aggregation-section-content-item-value {
|
||||||
|
min-width: 320px;
|
||||||
|
|
||||||
|
.ant-select {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-select-selector {
|
||||||
|
border-radius: 2px;
|
||||||
|
border: 1.005px solid var(--Slate-400, #1d212d);
|
||||||
|
background: var(--Ink-300, #16181d);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.metrics-operators-select {
|
||||||
|
border-radius: 2px;
|
||||||
|
border: 1.005px solid var(--Slate-400, #1d212d);
|
||||||
|
background: var(--Ink-300, #16181d);
|
||||||
|
|
||||||
|
color: var(--Vanilla-400, #c0c1c3);
|
||||||
|
font-family: 'Geist Mono';
|
||||||
|
font-size: 13px;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 20px; /* 142.857% */
|
||||||
|
letter-spacing: -0.07px;
|
||||||
|
}
|
||||||
@@ -0,0 +1,168 @@
|
|||||||
|
import './MetricsAggregateSection.styles.scss';
|
||||||
|
|
||||||
|
import { Tooltip } from 'antd';
|
||||||
|
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, useEffect, useMemo } from 'react';
|
||||||
|
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
||||||
|
|
||||||
|
import { useQueryBuilderV2Context } from '../../QueryBuilderV2Context';
|
||||||
|
|
||||||
|
const MetricsAggregateSection = memo(function MetricsAggregateSection({
|
||||||
|
query,
|
||||||
|
index,
|
||||||
|
version,
|
||||||
|
panelType,
|
||||||
|
}: {
|
||||||
|
query: IBuilderQuery;
|
||||||
|
index: number;
|
||||||
|
version: string;
|
||||||
|
panelType: PANEL_TYPES | null;
|
||||||
|
}): JSX.Element {
|
||||||
|
const { setAggregationOptions } = useQueryBuilderV2Context();
|
||||||
|
const {
|
||||||
|
operators,
|
||||||
|
spaceAggregationOptions,
|
||||||
|
handleChangeQueryData,
|
||||||
|
handleChangeOperator,
|
||||||
|
handleSpaceAggregationChange,
|
||||||
|
} = useQueryOperations({
|
||||||
|
index,
|
||||||
|
query,
|
||||||
|
entityVersion: version,
|
||||||
|
});
|
||||||
|
|
||||||
|
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);
|
||||||
|
},
|
||||||
|
[handleChangeQueryData],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleChangeAggregateEvery = useCallback(
|
||||||
|
(value: string) => {
|
||||||
|
handleChangeQueryData('stepInterval', Number(value));
|
||||||
|
},
|
||||||
|
[handleChangeQueryData],
|
||||||
|
);
|
||||||
|
|
||||||
|
const showAggregationInterval = useMemo(() => {
|
||||||
|
// eslint-disable-next-line sonarjs/prefer-single-boolean-return
|
||||||
|
if (panelType === PANEL_TYPES.VALUE) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}, [panelType]);
|
||||||
|
|
||||||
|
const disableOperatorSelector =
|
||||||
|
!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="metrics-aggregation-section-content">
|
||||||
|
<div className="metrics-aggregation-section-content-item">
|
||||||
|
<div className="metrics-aggregation-section-content-item-label">
|
||||||
|
Align with
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="metrics-aggregation-section-content-item-value">
|
||||||
|
<OperatorsSelect
|
||||||
|
value={query.aggregateOperator}
|
||||||
|
onChange={handleChangeOperator}
|
||||||
|
operators={operators}
|
||||||
|
className="metrics-operators-select"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{showAggregationInterval && (
|
||||||
|
<div className="metrics-aggregation-section-content-item">
|
||||||
|
<div className="metrics-aggregation-section-content-item-label">
|
||||||
|
aggregated every
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="metrics-aggregation-section-content-item-value">
|
||||||
|
<InputWithLabel
|
||||||
|
onChange={handleChangeAggregateEvery}
|
||||||
|
label="Seconds"
|
||||||
|
placeholder="Enter a number"
|
||||||
|
labelAfter
|
||||||
|
initialValue={query?.stepInterval ?? undefined}
|
||||||
|
/>
|
||||||
|
</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>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
export default MetricsAggregateSection;
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
.metrics-select-container {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
|
||||||
|
.ant-select-selector {
|
||||||
|
width: 100%;
|
||||||
|
border-radius: 2px;
|
||||||
|
border: 1px solid #1d212d !important;
|
||||||
|
background: #16181d;
|
||||||
|
color: #fff;
|
||||||
|
font-family: 'Geist Mono';
|
||||||
|
font-size: 13px;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 20px; /* 142.857% */
|
||||||
|
min-height: 36px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-select-dropdown {
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 1px solid var(--bg-slate-400);
|
||||||
|
background: linear-gradient(
|
||||||
|
139deg,
|
||||||
|
rgba(18, 19, 23, 0.8) 0%,
|
||||||
|
rgba(18, 19, 23, 0.9) 98.68%
|
||||||
|
);
|
||||||
|
box-shadow: 4px 10px 16px 2px rgba(0, 0, 0, 0.2);
|
||||||
|
backdrop-filter: blur(20px);
|
||||||
|
|
||||||
|
.ant-select-item {
|
||||||
|
color: #fff;
|
||||||
|
font-family: 'Geist Mono';
|
||||||
|
font-size: 13px;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 20px; /* 142.857% */
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: rgba(171, 189, 255, 0.04) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import './MetricsSelect.styles.scss';
|
||||||
|
|
||||||
|
import { AggregatorFilter } from 'container/QueryBuilder/filters';
|
||||||
|
import { useQueryOperations } from 'hooks/queryBuilder/useQueryBuilderOperations';
|
||||||
|
import { memo } from 'react';
|
||||||
|
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
||||||
|
|
||||||
|
export const MetricsSelect = memo(function MetricsSelect({
|
||||||
|
query,
|
||||||
|
index,
|
||||||
|
version,
|
||||||
|
}: {
|
||||||
|
query: IBuilderQuery;
|
||||||
|
index: number;
|
||||||
|
version: string;
|
||||||
|
}): JSX.Element {
|
||||||
|
const { handleChangeAggregatorAttribute } = useQueryOperations({
|
||||||
|
index,
|
||||||
|
query,
|
||||||
|
entityVersion: version,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="metrics-select-container">
|
||||||
|
<AggregatorFilter onChange={handleChangeAggregatorAttribute} query={query} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
@@ -0,0 +1,375 @@
|
|||||||
|
/* eslint-disable import/no-extraneous-dependencies */
|
||||||
|
/* eslint-disable sonarjs/cognitive-complexity */
|
||||||
|
import {
|
||||||
|
autocompletion,
|
||||||
|
closeCompletion,
|
||||||
|
Completion,
|
||||||
|
CompletionContext,
|
||||||
|
completionKeymap,
|
||||||
|
CompletionResult,
|
||||||
|
startCompletion,
|
||||||
|
} from '@codemirror/autocomplete';
|
||||||
|
import { javascript } from '@codemirror/lang-javascript';
|
||||||
|
import { copilot } from '@uiw/codemirror-theme-copilot';
|
||||||
|
import CodeMirror, { EditorView, keymap } from '@uiw/react-codemirror';
|
||||||
|
import { Button } from 'antd';
|
||||||
|
import { useQueryBuilderV2Context } from 'components/QueryBuilderV2/QueryBuilderV2Context';
|
||||||
|
import { X } from 'lucide-react';
|
||||||
|
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||||
|
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
||||||
|
|
||||||
|
const havingOperators = [
|
||||||
|
{
|
||||||
|
label: '=',
|
||||||
|
value: '=',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '!=',
|
||||||
|
value: '!=',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '>',
|
||||||
|
value: '>',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '<',
|
||||||
|
value: '<',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '>=',
|
||||||
|
value: '>=',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '<=',
|
||||||
|
value: '<=',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'IN',
|
||||||
|
value: 'IN',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'NOT_IN',
|
||||||
|
value: 'NOT_IN',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const conjunctions = [
|
||||||
|
{ label: 'AND', value: 'AND ' },
|
||||||
|
{ label: 'OR', value: 'OR ' },
|
||||||
|
];
|
||||||
|
|
||||||
|
// Custom extension to stop events from propagating to global shortcuts
|
||||||
|
const stopEventsExtension = EditorView.domEventHandlers({
|
||||||
|
keydown: (event) => {
|
||||||
|
// Stop all keyboard events from propagating to global shortcuts
|
||||||
|
event.stopPropagation();
|
||||||
|
event.stopImmediatePropagation();
|
||||||
|
return false; // Important for CM to know you handled it
|
||||||
|
},
|
||||||
|
input: (event) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
focus: (event) => {
|
||||||
|
// Ensure focus events don't interfere with global shortcuts
|
||||||
|
event.stopPropagation();
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
blur: (event) => {
|
||||||
|
// Ensure blur events don't interfere with global shortcuts
|
||||||
|
event.stopPropagation();
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
function HavingFilter({
|
||||||
|
onClose,
|
||||||
|
onChange,
|
||||||
|
queryData,
|
||||||
|
}: {
|
||||||
|
onClose: () => void;
|
||||||
|
onChange: (value: string) => void;
|
||||||
|
queryData: IBuilderQuery;
|
||||||
|
}): JSX.Element {
|
||||||
|
const { aggregationOptions } = useQueryBuilderV2Context();
|
||||||
|
const [input, setInput] = useState(
|
||||||
|
queryData?.havingExpression?.expression || '',
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setInput(queryData?.havingExpression?.expression || '');
|
||||||
|
}, [queryData?.havingExpression?.expression]);
|
||||||
|
|
||||||
|
const [isFocused, setIsFocused] = useState(false);
|
||||||
|
|
||||||
|
const editorRef = useRef<EditorView | null>(null);
|
||||||
|
|
||||||
|
const [options, setOptions] = useState<{ label: string; value: string }[]>([]);
|
||||||
|
|
||||||
|
const handleChange = (value: string): void => {
|
||||||
|
setInput(value);
|
||||||
|
onChange(value);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isFocused && editorRef.current && options.length > 0) {
|
||||||
|
startCompletion(editorRef.current);
|
||||||
|
}
|
||||||
|
}, [isFocused, options]);
|
||||||
|
|
||||||
|
// Update options when aggregation options change
|
||||||
|
useEffect(() => {
|
||||||
|
const newOptions = [];
|
||||||
|
for (let i = 0; i < aggregationOptions.length; i++) {
|
||||||
|
const opt = aggregationOptions[i];
|
||||||
|
for (let j = 0; j < havingOperators.length; j++) {
|
||||||
|
const operator = havingOperators[j];
|
||||||
|
newOptions.push({
|
||||||
|
label: `${opt.func}(${opt.arg}) ${operator.label}`,
|
||||||
|
value: `${opt.func}(${opt.arg}) ${operator.label} `,
|
||||||
|
apply: (
|
||||||
|
view: EditorView,
|
||||||
|
completion: { label: string; value: string },
|
||||||
|
from: number,
|
||||||
|
to: number,
|
||||||
|
): void => {
|
||||||
|
view.dispatch({
|
||||||
|
changes: { from, to, insert: completion.value },
|
||||||
|
selection: { anchor: from + completion.value.length },
|
||||||
|
});
|
||||||
|
// Trigger value suggestions immediately after operator
|
||||||
|
setTimeout(() => {
|
||||||
|
startCompletion(view);
|
||||||
|
}, 0);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setOptions(newOptions);
|
||||||
|
}, [aggregationOptions]);
|
||||||
|
|
||||||
|
// Helper to check if a string is a number
|
||||||
|
const isNumber = (token: string): boolean => /^-?\d+(\.\d+)?$/.test(token);
|
||||||
|
|
||||||
|
// Helper to check if we're after an operator
|
||||||
|
const isAfterOperator = (tokens: string[]): boolean => {
|
||||||
|
if (tokens.length === 0) return false;
|
||||||
|
const lastToken = tokens[tokens.length - 1];
|
||||||
|
// Check if the last token is exactly an operator or ends with an operator and space
|
||||||
|
return havingOperators.some((op) => {
|
||||||
|
const opWithSpace = `${op.value} `;
|
||||||
|
return lastToken === op.value || lastToken.endsWith(opWithSpace);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Helper function for applying completion with space
|
||||||
|
const applyCompletionWithSpace = (
|
||||||
|
view: EditorView,
|
||||||
|
completion: Completion,
|
||||||
|
from: number,
|
||||||
|
to: number,
|
||||||
|
): void => {
|
||||||
|
const insertValue =
|
||||||
|
typeof completion.apply === 'string' ? completion.apply : completion.label;
|
||||||
|
const newText = `${insertValue} `;
|
||||||
|
const newPos = from + newText.length;
|
||||||
|
|
||||||
|
view.dispatch({
|
||||||
|
changes: { from, to, insert: newText },
|
||||||
|
selection: { anchor: newPos, head: newPos },
|
||||||
|
effects: EditorView.scrollIntoView(newPos),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const havingAutocomplete = useMemo(() => {
|
||||||
|
// Helper functions for applying completions
|
||||||
|
const forceCompletion = (view: EditorView): void => {
|
||||||
|
setTimeout(() => {
|
||||||
|
if (view) {
|
||||||
|
startCompletion(view);
|
||||||
|
}
|
||||||
|
}, 0);
|
||||||
|
};
|
||||||
|
|
||||||
|
const applyValueCompletion = (
|
||||||
|
view: EditorView,
|
||||||
|
completion: Completion,
|
||||||
|
from: number,
|
||||||
|
to: number,
|
||||||
|
): void => {
|
||||||
|
applyCompletionWithSpace(view, completion, from, to);
|
||||||
|
forceCompletion(view);
|
||||||
|
};
|
||||||
|
|
||||||
|
const applyOperatorCompletion = (
|
||||||
|
view: EditorView,
|
||||||
|
completion: Completion,
|
||||||
|
from: number,
|
||||||
|
to: number,
|
||||||
|
): void => {
|
||||||
|
const insertValue =
|
||||||
|
typeof completion.apply === 'string' ? completion.apply : completion.label;
|
||||||
|
const insertWithSpace = `${insertValue} `;
|
||||||
|
view.dispatch({
|
||||||
|
changes: { from, to, insert: insertWithSpace },
|
||||||
|
selection: { anchor: from + insertWithSpace.length },
|
||||||
|
});
|
||||||
|
forceCompletion(view);
|
||||||
|
};
|
||||||
|
|
||||||
|
return autocompletion({
|
||||||
|
override: [
|
||||||
|
(context: CompletionContext): CompletionResult | null => {
|
||||||
|
const text = context.state.sliceDoc(0, context.pos);
|
||||||
|
const trimmedText = text.trim();
|
||||||
|
const tokens = trimmedText.split(/\s+/).filter(Boolean);
|
||||||
|
|
||||||
|
// Handle empty state when no aggregation options are available
|
||||||
|
if (options.length === 0) {
|
||||||
|
return {
|
||||||
|
from: context.pos,
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
label:
|
||||||
|
'No aggregation functions available. Please add aggregation functions first.',
|
||||||
|
type: 'text',
|
||||||
|
apply: (): boolean => true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close dropdown after operator to allow custom value entry
|
||||||
|
if (isAfterOperator(tokens)) {
|
||||||
|
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
|
||||||
|
if (
|
||||||
|
tokens.length === 0 ||
|
||||||
|
conjunctions.some((c) => tokens[tokens.length - 1] === c.value.trim()) ||
|
||||||
|
tokens[tokens.length - 1] === '('
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
from: context.pos,
|
||||||
|
options: options.map((opt) => ({
|
||||||
|
...opt,
|
||||||
|
apply: applyOperatorCompletion,
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show suggestions when typing
|
||||||
|
if (tokens.length > 0) {
|
||||||
|
const lastToken = tokens[tokens.length - 1];
|
||||||
|
const filteredOptions = options.filter((opt) =>
|
||||||
|
opt.label.toLowerCase().includes(lastToken.toLowerCase()),
|
||||||
|
);
|
||||||
|
if (filteredOptions.length > 0) {
|
||||||
|
return {
|
||||||
|
from: context.pos - lastToken.length,
|
||||||
|
options: filteredOptions.map((opt) => ({
|
||||||
|
...opt,
|
||||||
|
apply: applyOperatorCompletion,
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Suggest conjunctions after a value and a space
|
||||||
|
if (
|
||||||
|
tokens.length > 0 &&
|
||||||
|
(isNumber(tokens[tokens.length - 1]) ||
|
||||||
|
tokens[tokens.length - 1] === ')') &&
|
||||||
|
text.endsWith(' ')
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
from: context.pos,
|
||||||
|
options: conjunctions.map((conj) => ({
|
||||||
|
...conj,
|
||||||
|
apply: applyValueCompletion,
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show all options if no other condition matches
|
||||||
|
return {
|
||||||
|
from: context.pos,
|
||||||
|
options: options.map((opt) => ({
|
||||||
|
...opt,
|
||||||
|
apply: applyOperatorCompletion,
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
],
|
||||||
|
defaultKeymap: true,
|
||||||
|
closeOnBlur: true,
|
||||||
|
maxRenderedOptions: 200,
|
||||||
|
activateOnTyping: true,
|
||||||
|
});
|
||||||
|
}, [options]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="having-filter-container">
|
||||||
|
<div className="having-filter-select-container">
|
||||||
|
<CodeMirror
|
||||||
|
value={input}
|
||||||
|
onChange={handleChange}
|
||||||
|
theme={copilot}
|
||||||
|
className="having-filter-select-editor"
|
||||||
|
width="100%"
|
||||||
|
extensions={[
|
||||||
|
havingAutocomplete,
|
||||||
|
javascript({ jsx: false, typescript: false }),
|
||||||
|
stopEventsExtension,
|
||||||
|
EditorView.lineWrapping,
|
||||||
|
keymap.of([
|
||||||
|
...completionKeymap,
|
||||||
|
{
|
||||||
|
key: 'Escape',
|
||||||
|
run: closeCompletion,
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
]}
|
||||||
|
placeholder="Type Having query like count() > 10 ..."
|
||||||
|
basicSetup={{
|
||||||
|
lineNumbers: false,
|
||||||
|
autocompletion: true,
|
||||||
|
completionKeymap: true,
|
||||||
|
}}
|
||||||
|
onCreateEditor={(view: EditorView): void => {
|
||||||
|
editorRef.current = view;
|
||||||
|
}}
|
||||||
|
onFocus={(): void => {
|
||||||
|
setIsFocused(true);
|
||||||
|
if (editorRef.current) {
|
||||||
|
startCompletion(editorRef.current);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onBlur={(): void => {
|
||||||
|
setIsFocused(false);
|
||||||
|
if (editorRef.current) {
|
||||||
|
closeCompletion(editorRef.current);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
className="close-btn periscope-btn ghost"
|
||||||
|
icon={<X size={16} />}
|
||||||
|
onClick={onClose}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default HavingFilter;
|
||||||
@@ -0,0 +1,377 @@
|
|||||||
|
.add-ons-list {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.add-ons-tabs {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
|
||||||
|
.add-on-tab-title {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--margin-2);
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: var(--font-size-xs);
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: var(--font-weight-normal);
|
||||||
|
|
||||||
|
color: var(--Vanilla-400, #c0c1c3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab {
|
||||||
|
border: 1px solid var(--bg-slate-400);
|
||||||
|
border-left: none;
|
||||||
|
min-width: 120px;
|
||||||
|
height: 36px;
|
||||||
|
line-height: 36px;
|
||||||
|
|
||||||
|
&:first-child {
|
||||||
|
border-left: 1px solid var(--bg-slate-400);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab::before {
|
||||||
|
background: var(--bg-slate-400);
|
||||||
|
}
|
||||||
|
|
||||||
|
.selected-view {
|
||||||
|
color: var(--text-robin-500);
|
||||||
|
border: 1px solid var(--bg-slate-400);
|
||||||
|
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selected-view::before {
|
||||||
|
background: var(--bg-slate-400);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.compass-button {
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
|
||||||
|
border-radius: 2px;
|
||||||
|
border: 1px solid var(--bg-slate-400);
|
||||||
|
background: var(--bg-ink-300);
|
||||||
|
box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.having-filter-container {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.having-filter-select-container {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.having-filter-select-editor {
|
||||||
|
border-radius: 2px;
|
||||||
|
flex: 1;
|
||||||
|
width: calc(100% - 40px);
|
||||||
|
|
||||||
|
.cm-content {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cm-editor {
|
||||||
|
border-radius: 2px;
|
||||||
|
background-color: transparent !important;
|
||||||
|
position: relative !important;
|
||||||
|
|
||||||
|
&:focus-within {
|
||||||
|
border-color: var(--bg-robin-500);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.cm-focused {
|
||||||
|
outline: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cm-content {
|
||||||
|
border-radius: 2px;
|
||||||
|
border: 1px solid var(--Slate-400, #1d212d);
|
||||||
|
border-top-right-radius: 0px;
|
||||||
|
border-bottom-right-radius: 0px;
|
||||||
|
padding: 0px !important;
|
||||||
|
background-color: #121317 !important;
|
||||||
|
|
||||||
|
&:focus-within {
|
||||||
|
border-color: var(--bg-ink-200);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.cm-tooltip-autocomplete {
|
||||||
|
background: var(--bg-ink-300) !important;
|
||||||
|
color: var(--bg-ink-500) !important;
|
||||||
|
border-radius: 2px !important;
|
||||||
|
font-size: 12px !important;
|
||||||
|
font-weight: 500 !important;
|
||||||
|
margin-top: -2px !important;
|
||||||
|
width: 100% !important;
|
||||||
|
position: absolute !important;
|
||||||
|
top: 38px !important;
|
||||||
|
left: 0px !important;
|
||||||
|
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 1px solid var(--bg-slate-200, #1d212d);
|
||||||
|
border-top: none !important;
|
||||||
|
border-top-left-radius: 0px !important;
|
||||||
|
border-top-right-radius: 0px !important;
|
||||||
|
background: linear-gradient(
|
||||||
|
139deg,
|
||||||
|
rgba(18, 19, 23, 0.8) 0%,
|
||||||
|
rgba(18, 19, 23, 0.9) 98.68%
|
||||||
|
) !important;
|
||||||
|
backdrop-filter: blur(20px);
|
||||||
|
box-sizing: border-box;
|
||||||
|
font-family: 'Space Mono', monospace !important;
|
||||||
|
|
||||||
|
ul {
|
||||||
|
width: 100% !important;
|
||||||
|
max-width: 100% !important;
|
||||||
|
font-family: 'Space Mono', monospace !important;
|
||||||
|
min-height: 200px !important;
|
||||||
|
|
||||||
|
&::-webkit-scrollbar {
|
||||||
|
width: 0.3rem;
|
||||||
|
}
|
||||||
|
&::-webkit-scrollbar-corner {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
&::-webkit-scrollbar-thumb {
|
||||||
|
background: rgb(136, 136, 136);
|
||||||
|
border-radius: 0.625rem;
|
||||||
|
}
|
||||||
|
&::-webkit-scrollbar-track {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
li {
|
||||||
|
width: 100% !important;
|
||||||
|
max-width: 100% !important;
|
||||||
|
line-height: 36px !important;
|
||||||
|
height: 36px !important;
|
||||||
|
padding: 4px 8px !important;
|
||||||
|
|
||||||
|
display: flex !important;
|
||||||
|
align-items: center !important;
|
||||||
|
gap: 8px !important;
|
||||||
|
box-sizing: border-box;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
font-family: 'Space Mono', monospace !important;
|
||||||
|
color: var(--bg-vanilla-100) !important;
|
||||||
|
|
||||||
|
.cm-completionIcon {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
&[aria-selected='true'] {
|
||||||
|
// background-color: rgba(78, 116, 248, 0.7) !important;
|
||||||
|
background: rgba(171, 189, 255, 0.04) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.cm-gutters {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cm-scroller {
|
||||||
|
scrollbar-width: none;
|
||||||
|
|
||||||
|
&::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-webkit-scrollbar-thumb {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-webkit-scrollbar-track {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-webkit-scrollbar-corner {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.cm-line {
|
||||||
|
line-height: 36px !important;
|
||||||
|
font-family: 'Space Mono', monospace !important;
|
||||||
|
background-color: #121317 !important;
|
||||||
|
|
||||||
|
::-moz-selection {
|
||||||
|
background: var(--bg-ink-100) !important;
|
||||||
|
opacity: 0.5 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
::selection {
|
||||||
|
background: var(--bg-ink-100) !important;
|
||||||
|
opacity: 0.5 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cm-function {
|
||||||
|
color: var(--bg-robin-500) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chip-decorator {
|
||||||
|
background: rgba(36, 40, 52, 1) !important;
|
||||||
|
color: var(--bg-vanilla-100) !important;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 2px 4px;
|
||||||
|
margin-right: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.cm-selectionBackground {
|
||||||
|
background: var(--bg-ink-100) !important;
|
||||||
|
opacity: 0.5 !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.close-btn {
|
||||||
|
border-radius: 0px 2px 2px 0px;
|
||||||
|
border: 1px solid var(--bg-slate-400);
|
||||||
|
background: var(--bg-ink-300);
|
||||||
|
height: 38px;
|
||||||
|
width: 38px;
|
||||||
|
|
||||||
|
border-left: transparent;
|
||||||
|
border-top-left-radius: 0px;
|
||||||
|
border-bottom-left-radius: 0px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.selected-add-ons-content {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(420px, 1fr));
|
||||||
|
gap: 8px;
|
||||||
|
padding-bottom: 8px;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
.add-on-content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
|
max-width: 100%;
|
||||||
|
min-width: 100%;
|
||||||
|
min-width: 420px;
|
||||||
|
|
||||||
|
box-sizing: border-box;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.lightMode {
|
||||||
|
.add-ons-list {
|
||||||
|
.add-ons-tabs {
|
||||||
|
.add-on-tab-title {
|
||||||
|
color: var(--bg-ink-500) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab {
|
||||||
|
border: 1px solid var(--bg-vanilla-300) !important;
|
||||||
|
background: var(--bg-vanilla-100) !important;
|
||||||
|
|
||||||
|
&:first-child {
|
||||||
|
border-left: 1px solid var(--bg-vanilla-300) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab::before {
|
||||||
|
background: var(--bg-vanilla-300) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selected-view {
|
||||||
|
color: var(--bg-robin-500) !important;
|
||||||
|
border: 1px solid var(--bg-vanilla-300) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selected-view::before {
|
||||||
|
background: var(--bg-vanilla-300) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.compass-button {
|
||||||
|
border: 1px solid var(--bg-vanilla-300) !important;
|
||||||
|
background: var(--bg-vanilla-100) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.having-filter-container {
|
||||||
|
.having-filter-select-container {
|
||||||
|
.having-filter-select-editor {
|
||||||
|
.cm-editor {
|
||||||
|
&:focus-within {
|
||||||
|
border-color: var(--bg-vanilla-300) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cm-content {
|
||||||
|
border: 1px solid var(--bg-vanilla-300) !important;
|
||||||
|
background: var(--bg-vanilla-100) !important;
|
||||||
|
|
||||||
|
&:focus-within {
|
||||||
|
border-color: var(--bg-vanilla-300) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.cm-tooltip-autocomplete {
|
||||||
|
background: var(--bg-vanilla-100) !important;
|
||||||
|
border: 1px solid var(--bg-vanilla-300) !important;
|
||||||
|
color: var(--bg-ink-500) !important;
|
||||||
|
|
||||||
|
ul {
|
||||||
|
li {
|
||||||
|
&:hover {
|
||||||
|
background: var(--bg-vanilla-300) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
&[aria-selected='true'] {
|
||||||
|
color: var(--bg-ink-500) !important;
|
||||||
|
background: var(--bg-vanilla-300) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.cm-line {
|
||||||
|
background-color: var(--bg-vanilla-100) !important;
|
||||||
|
|
||||||
|
::-moz-selection {
|
||||||
|
background: var(--bg-vanilla-100) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
::selection {
|
||||||
|
background: var(--bg-ink-100) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chip-decorator {
|
||||||
|
background: var(--bg-robin-100) !important;
|
||||||
|
color: var(--bg-ink-400) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.cm-selectionBackground {
|
||||||
|
background: var(--bg-vanilla-100) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.close-btn {
|
||||||
|
border: 1px solid var(--bg-vanilla-300) !important;
|
||||||
|
background: var(--bg-vanilla-100) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,340 @@
|
|||||||
|
import './QueryAddOns.styles.scss';
|
||||||
|
|
||||||
|
import { Button, Radio, RadioChangeEvent } from 'antd';
|
||||||
|
import InputWithLabel from 'components/InputWithLabel/InputWithLabel';
|
||||||
|
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||||
|
import { GroupByFilter } from 'container/QueryBuilder/filters/GroupByFilter/GroupByFilter';
|
||||||
|
import { OrderByFilter } from 'container/QueryBuilder/filters/OrderByFilter/OrderByFilter';
|
||||||
|
import { ReduceToFilter } from 'container/QueryBuilder/filters/ReduceToFilter/ReduceToFilter';
|
||||||
|
import { useQueryOperations } from 'hooks/queryBuilder/useQueryBuilderOperations';
|
||||||
|
import { isEmpty } from 'lodash-es';
|
||||||
|
import { BarChart2, ScrollText, X } from 'lucide-react';
|
||||||
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
|
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
||||||
|
import { DataSource } from 'types/common/queryBuilder';
|
||||||
|
|
||||||
|
import HavingFilter from './HavingFilter/HavingFilter';
|
||||||
|
|
||||||
|
interface AddOn {
|
||||||
|
icon: React.ReactNode;
|
||||||
|
label: string;
|
||||||
|
key: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ADD_ONS_KEYS = {
|
||||||
|
GROUP_BY: 'group_by',
|
||||||
|
HAVING: 'having',
|
||||||
|
ORDER_BY: 'order_by',
|
||||||
|
LIMIT: 'limit',
|
||||||
|
LEGEND_FORMAT: 'legend_format',
|
||||||
|
};
|
||||||
|
|
||||||
|
const ADD_ONS = [
|
||||||
|
{
|
||||||
|
icon: <BarChart2 size={14} />,
|
||||||
|
label: 'Group By',
|
||||||
|
key: 'group_by',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <ScrollText size={14} />,
|
||||||
|
label: 'Having',
|
||||||
|
key: 'having',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <ScrollText size={14} />,
|
||||||
|
label: 'Order By',
|
||||||
|
key: 'order_by',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <ScrollText size={14} />,
|
||||||
|
label: 'Limit',
|
||||||
|
key: 'limit',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <ScrollText size={14} />,
|
||||||
|
label: 'Legend format',
|
||||||
|
key: 'legend_format',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const REDUCE_TO = {
|
||||||
|
icon: <ScrollText size={14} />,
|
||||||
|
label: 'Reduce to',
|
||||||
|
key: 'reduce_to',
|
||||||
|
};
|
||||||
|
|
||||||
|
function QueryAddOns({
|
||||||
|
query,
|
||||||
|
version,
|
||||||
|
isListViewPanel,
|
||||||
|
showReduceTo,
|
||||||
|
panelType,
|
||||||
|
index,
|
||||||
|
}: {
|
||||||
|
query: IBuilderQuery;
|
||||||
|
version: string;
|
||||||
|
isListViewPanel: boolean;
|
||||||
|
showReduceTo: boolean;
|
||||||
|
panelType: PANEL_TYPES | null;
|
||||||
|
index: number;
|
||||||
|
}): JSX.Element {
|
||||||
|
const [addOns, setAddOns] = useState<AddOn[]>(ADD_ONS);
|
||||||
|
|
||||||
|
const [selectedViews, setSelectedViews] = useState<AddOn[]>([]);
|
||||||
|
|
||||||
|
const { handleChangeQueryData } = useQueryOperations({
|
||||||
|
index,
|
||||||
|
query,
|
||||||
|
entityVersion: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isListViewPanel) {
|
||||||
|
setAddOns([]);
|
||||||
|
|
||||||
|
setSelectedViews([
|
||||||
|
ADD_ONS.find((addOn) => addOn.key === ADD_ONS_KEYS.ORDER_BY) as AddOn,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let filteredAddOns: AddOn[];
|
||||||
|
if (panelType === PANEL_TYPES.VALUE) {
|
||||||
|
// Filter out all add-ons except legend format
|
||||||
|
filteredAddOns = ADD_ONS.filter(
|
||||||
|
(addOn) => addOn.key === ADD_ONS_KEYS.LEGEND_FORMAT,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
filteredAddOns = Object.values(ADD_ONS);
|
||||||
|
|
||||||
|
// Filter out group_by for metrics data source
|
||||||
|
if (query.dataSource === DataSource.METRICS) {
|
||||||
|
filteredAddOns = filteredAddOns.filter(
|
||||||
|
(addOn) => addOn.key !== ADD_ONS_KEYS.GROUP_BY,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// add reduce to if showReduceTo is true
|
||||||
|
if (showReduceTo) {
|
||||||
|
filteredAddOns = [...filteredAddOns, REDUCE_TO];
|
||||||
|
}
|
||||||
|
|
||||||
|
setAddOns(filteredAddOns);
|
||||||
|
|
||||||
|
// Filter selectedViews to only include add-ons present in filteredAddOns
|
||||||
|
setSelectedViews((prevSelectedViews) =>
|
||||||
|
prevSelectedViews.filter((view) =>
|
||||||
|
filteredAddOns.some((addOn) => addOn.key === view.key),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [panelType, isListViewPanel, query.dataSource]);
|
||||||
|
|
||||||
|
const handleOptionClick = (e: RadioChangeEvent): void => {
|
||||||
|
if (selectedViews.find((view) => view.key === e.target.value.key)) {
|
||||||
|
setSelectedViews(
|
||||||
|
selectedViews.filter((view) => view.key !== e.target.value.key),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
setSelectedViews([...selectedViews, e.target.value]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleChangeGroupByKeys = useCallback(
|
||||||
|
(value: IBuilderQuery['groupBy']) => {
|
||||||
|
handleChangeQueryData('groupBy', value);
|
||||||
|
},
|
||||||
|
[handleChangeQueryData],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleChangeOrderByKeys = useCallback(
|
||||||
|
(value: IBuilderQuery['orderBy']) => {
|
||||||
|
handleChangeQueryData('orderBy', value);
|
||||||
|
},
|
||||||
|
[handleChangeQueryData],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleChangeReduceTo = useCallback(
|
||||||
|
(value: IBuilderQuery['reduceTo']) => {
|
||||||
|
handleChangeQueryData('reduceTo', value);
|
||||||
|
},
|
||||||
|
[handleChangeQueryData],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleRemoveView = useCallback(
|
||||||
|
(key: string): void => {
|
||||||
|
setSelectedViews(selectedViews.filter((view) => view.key !== key));
|
||||||
|
},
|
||||||
|
[selectedViews],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleChangeQueryLegend = useCallback(
|
||||||
|
(value: string) => {
|
||||||
|
handleChangeQueryData('legend', value);
|
||||||
|
},
|
||||||
|
[handleChangeQueryData],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleChangeLimit = useCallback(
|
||||||
|
(value: string) => {
|
||||||
|
handleChangeQueryData('limit', Number(value) || null);
|
||||||
|
},
|
||||||
|
[handleChangeQueryData],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleChangeHaving = useCallback(
|
||||||
|
(value: string) => {
|
||||||
|
handleChangeQueryData('havingExpression', {
|
||||||
|
expression: value,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[handleChangeQueryData],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="query-add-ons">
|
||||||
|
{selectedViews.length > 0 && (
|
||||||
|
<div className="selected-add-ons-content">
|
||||||
|
{selectedViews.find((view) => view.key === 'group_by') && (
|
||||||
|
<div className="add-on-content">
|
||||||
|
<div className="periscope-input-with-label">
|
||||||
|
<div className="label">Group By</div>
|
||||||
|
<div className="input">
|
||||||
|
<GroupByFilter
|
||||||
|
disabled={
|
||||||
|
query.dataSource === DataSource.METRICS &&
|
||||||
|
!query.aggregateAttribute.key
|
||||||
|
}
|
||||||
|
query={query}
|
||||||
|
onChange={handleChangeGroupByKeys}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
className="close-btn periscope-btn ghost"
|
||||||
|
icon={<X size={16} />}
|
||||||
|
onClick={(): void => handleRemoveView('group_by')}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{selectedViews.find((view) => view.key === 'having') && (
|
||||||
|
<div className="add-on-content">
|
||||||
|
<div className="periscope-input-with-label">
|
||||||
|
<div className="label">Having</div>
|
||||||
|
<div className="input">
|
||||||
|
<HavingFilter
|
||||||
|
onClose={(): void => {
|
||||||
|
setSelectedViews(
|
||||||
|
selectedViews.filter((view) => view.key !== 'having'),
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
onChange={handleChangeHaving}
|
||||||
|
queryData={query}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{selectedViews.find((view) => view.key === 'limit') && (
|
||||||
|
<div className="add-on-content">
|
||||||
|
<InputWithLabel
|
||||||
|
label="Limit"
|
||||||
|
onChange={handleChangeLimit}
|
||||||
|
initialValue={query?.limit ?? undefined}
|
||||||
|
placeholder="Enter limit"
|
||||||
|
onClose={(): void => {
|
||||||
|
setSelectedViews(selectedViews.filter((view) => view.key !== 'limit'));
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{selectedViews.find((view) => view.key === 'order_by') && (
|
||||||
|
<div className="add-on-content">
|
||||||
|
<div className="periscope-input-with-label">
|
||||||
|
<div className="label">Order By</div>
|
||||||
|
<div className="input">
|
||||||
|
<OrderByFilter
|
||||||
|
entityVersion={version}
|
||||||
|
query={query}
|
||||||
|
onChange={handleChangeOrderByKeys}
|
||||||
|
isListViewPanel={isListViewPanel}
|
||||||
|
isNewQueryV2
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{!isListViewPanel && (
|
||||||
|
<Button
|
||||||
|
className="close-btn periscope-btn ghost"
|
||||||
|
icon={<X size={16} />}
|
||||||
|
onClick={(): void => handleRemoveView('order_by')}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{selectedViews.find((view) => view.key === 'reduce_to') && showReduceTo && (
|
||||||
|
<div className="add-on-content">
|
||||||
|
<div className="periscope-input-with-label">
|
||||||
|
<div className="label">Reduce to</div>
|
||||||
|
<div className="input">
|
||||||
|
<ReduceToFilter query={query} onChange={handleChangeReduceTo} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
className="close-btn periscope-btn ghost"
|
||||||
|
icon={<X size={16} />}
|
||||||
|
onClick={(): void => handleRemoveView('reduce_to')}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{selectedViews.find((view) => view.key === 'legend_format') && (
|
||||||
|
<div className="add-on-content">
|
||||||
|
<InputWithLabel
|
||||||
|
label="Legend format"
|
||||||
|
placeholder="Write legend format"
|
||||||
|
onChange={handleChangeQueryLegend}
|
||||||
|
initialValue={isEmpty(query?.legend) ? undefined : query?.legend}
|
||||||
|
onClose={(): void => {
|
||||||
|
setSelectedViews(
|
||||||
|
selectedViews.filter((view) => view.key !== 'legend_format'),
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="add-ons-list">
|
||||||
|
<Radio.Group
|
||||||
|
className="add-ons-tabs"
|
||||||
|
onChange={handleOptionClick}
|
||||||
|
value={selectedViews}
|
||||||
|
>
|
||||||
|
{addOns.map((addOn) => (
|
||||||
|
<Radio.Button
|
||||||
|
key={addOn.label}
|
||||||
|
className={
|
||||||
|
selectedViews.find((view) => view.key === addOn.key)
|
||||||
|
? 'selected-view tab'
|
||||||
|
: 'tab'
|
||||||
|
}
|
||||||
|
value={addOn}
|
||||||
|
>
|
||||||
|
<div className="add-on-tab-title">
|
||||||
|
{addOn.icon}
|
||||||
|
{addOn.label}
|
||||||
|
</div>
|
||||||
|
</Radio.Button>
|
||||||
|
))}
|
||||||
|
</Radio.Group>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default QueryAddOns;
|
||||||
@@ -0,0 +1,300 @@
|
|||||||
|
.query-aggregation-container {
|
||||||
|
display: block;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
.aggregation-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: flex-start;
|
||||||
|
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;
|
||||||
|
|
||||||
|
.query-aggregation-select-editor {
|
||||||
|
border-radius: 2px;
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
|
||||||
|
.cm-content {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cm-editor {
|
||||||
|
border-radius: 2px;
|
||||||
|
background-color: transparent !important;
|
||||||
|
position: relative !important;
|
||||||
|
|
||||||
|
&.cm-focused {
|
||||||
|
outline: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus-within {
|
||||||
|
border-color: var(--bg-robin-500);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cm-content {
|
||||||
|
border-radius: 2px;
|
||||||
|
border: 1px solid var(--Slate-400, #1d212d);
|
||||||
|
border-top-right-radius: 0px;
|
||||||
|
border-bottom-right-radius: 0px;
|
||||||
|
padding: 0px !important;
|
||||||
|
background-color: #121317 !important;
|
||||||
|
|
||||||
|
&:focus-within {
|
||||||
|
border-color: var(--bg-ink-200);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.cm-tooltip-autocomplete {
|
||||||
|
background: var(--bg-ink-300) !important;
|
||||||
|
border-radius: 2px !important;
|
||||||
|
font-size: 12px !important;
|
||||||
|
font-weight: 500 !important;
|
||||||
|
margin-top: 8px !important;
|
||||||
|
min-width: 400px !important;
|
||||||
|
position: absolute !important;
|
||||||
|
left: 0px !important;
|
||||||
|
width: 100% !important;
|
||||||
|
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 1px solid var(--bg-slate-200, #1d212d);
|
||||||
|
border-top: none !important;
|
||||||
|
border-top-left-radius: 0px !important;
|
||||||
|
border-top-right-radius: 0px !important;
|
||||||
|
background: linear-gradient(
|
||||||
|
139deg,
|
||||||
|
rgba(18, 19, 23, 0.8) 0%,
|
||||||
|
rgba(18, 19, 23, 0.9) 98.68%
|
||||||
|
) !important;
|
||||||
|
backdrop-filter: blur(20px);
|
||||||
|
box-sizing: border-box;
|
||||||
|
font-family: 'Space Mono', monospace !important;
|
||||||
|
|
||||||
|
ul {
|
||||||
|
width: 100% !important;
|
||||||
|
max-width: 100% !important;
|
||||||
|
font-family: 'Space Mono', monospace !important;
|
||||||
|
min-height: 200px !important;
|
||||||
|
|
||||||
|
&::-webkit-scrollbar {
|
||||||
|
width: 0.3rem;
|
||||||
|
}
|
||||||
|
&::-webkit-scrollbar-corner {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
&::-webkit-scrollbar-thumb {
|
||||||
|
background: rgb(136, 136, 136);
|
||||||
|
border-radius: 0.625rem;
|
||||||
|
}
|
||||||
|
&::-webkit-scrollbar-track {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
li {
|
||||||
|
width: 100% !important;
|
||||||
|
max-width: 100% !important;
|
||||||
|
line-height: 36px !important;
|
||||||
|
height: 36px !important;
|
||||||
|
padding: 4px 8px !important;
|
||||||
|
|
||||||
|
display: flex !important;
|
||||||
|
align-items: center !important;
|
||||||
|
gap: 8px !important;
|
||||||
|
box-sizing: border-box;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
font-family: 'Space Mono', monospace !important;
|
||||||
|
|
||||||
|
.cm-completionIcon {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
&[aria-selected='true'] {
|
||||||
|
// background-color: rgba(78, 116, 248, 0.7) !important;
|
||||||
|
background: rgba(171, 189, 255, 0.04) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.cm-gutters {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cm-line {
|
||||||
|
line-height: 36px !important;
|
||||||
|
font-family: 'Space Mono', monospace !important;
|
||||||
|
background-color: #121317 !important;
|
||||||
|
|
||||||
|
::-moz-selection {
|
||||||
|
background: var(--bg-ink-100) !important;
|
||||||
|
opacity: 0.5 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
::selection {
|
||||||
|
background: var(--bg-ink-100) !important;
|
||||||
|
opacity: 0.5 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cm-function {
|
||||||
|
color: var(--bg-robin-500) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chip-decorator {
|
||||||
|
background: rgba(36, 40, 52, 1) !important;
|
||||||
|
color: var(--bg-vanilla-100) !important;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 2px 4px;
|
||||||
|
margin-right: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.cm-selectionBackground {
|
||||||
|
background: var(--bg-ink-100) !important;
|
||||||
|
opacity: 0.5 !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.close-btn {
|
||||||
|
border-radius: 0px 2px 2px 0px;
|
||||||
|
border: 1px solid var(--bg-slate-400);
|
||||||
|
background: var(--bg-ink-300);
|
||||||
|
height: 38px;
|
||||||
|
width: 38px;
|
||||||
|
|
||||||
|
border-left: transparent;
|
||||||
|
border-top-left-radius: 0px;
|
||||||
|
border-bottom-left-radius: 0px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.lightMode {
|
||||||
|
.query-aggregation-container {
|
||||||
|
.aggregation-container {
|
||||||
|
.query-aggregation-options-input {
|
||||||
|
border-color: var(--bg-vanilla-300) !important;
|
||||||
|
|
||||||
|
&::placeholder {
|
||||||
|
color: var(--bg-ink-400) !important;
|
||||||
|
opacity: 0.5 !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.query-aggregation-select-container {
|
||||||
|
.query-aggregation-select-editor {
|
||||||
|
.cm-editor {
|
||||||
|
.cm-content {
|
||||||
|
border: 1px solid var(--bg-vanilla-300) !important;
|
||||||
|
background: var(--bg-vanilla-100) !important;
|
||||||
|
box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.1) !important;
|
||||||
|
|
||||||
|
&:focus-within {
|
||||||
|
border-color: var(--bg-vanilla-300) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.cm-tooltip-autocomplete {
|
||||||
|
background: var(--bg-vanilla-100) !important;
|
||||||
|
border: 1px solid var(--bg-vanilla-300) !important;
|
||||||
|
color: var(--bg-ink-500) !important;
|
||||||
|
|
||||||
|
ul {
|
||||||
|
li {
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--bg-vanilla-300) !important;
|
||||||
|
color: var(--bg-ink-500) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
&[aria-selected='true'] {
|
||||||
|
background: var(--bg-vanilla-300) !important;
|
||||||
|
color: var(--bg-ink-500) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.cm-line {
|
||||||
|
background-color: var(--bg-vanilla-100) !important;
|
||||||
|
|
||||||
|
::-moz-selection {
|
||||||
|
background: var(--bg-vanilla-100) !important;
|
||||||
|
opacity: 0.5 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
::selection {
|
||||||
|
background: var(--bg-vanilla-100) !important;
|
||||||
|
opacity: 0.5 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cm-function {
|
||||||
|
color: var(--bg-robin-500) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chip-decorator {
|
||||||
|
background: var(--bg-robin-500) !important;
|
||||||
|
color: var(--bg-ink-400) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// .cm-selectionBackground {
|
||||||
|
// background: var(--bg-vanilla-100) !important;
|
||||||
|
// opacity: 0.5 !important;
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.close-btn {
|
||||||
|
border-color: var(--bg-vanilla-300) !important;
|
||||||
|
background: var(--bg-vanilla-100) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
import './QueryAggregation.styles.scss';
|
||||||
|
|
||||||
|
import InputWithLabel from 'components/InputWithLabel/InputWithLabel';
|
||||||
|
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
||||||
|
import { DataSource } from 'types/common/queryBuilder';
|
||||||
|
|
||||||
|
import QueryAggregationSelect from './QueryAggregationSelect';
|
||||||
|
|
||||||
|
function QueryAggregationOptions({
|
||||||
|
dataSource,
|
||||||
|
panelType,
|
||||||
|
onAggregationIntervalChange,
|
||||||
|
onChange,
|
||||||
|
queryData,
|
||||||
|
}: {
|
||||||
|
dataSource: DataSource;
|
||||||
|
panelType?: string;
|
||||||
|
onAggregationIntervalChange: (value: number) => void;
|
||||||
|
onChange?: (value: string) => void;
|
||||||
|
queryData: IBuilderQuery;
|
||||||
|
}): JSX.Element {
|
||||||
|
const showAggregationInterval = useMemo(() => {
|
||||||
|
// eslint-disable-next-line sonarjs/prefer-single-boolean-return
|
||||||
|
if (panelType === PANEL_TYPES.VALUE) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dataSource === DataSource.TRACES || dataSource === DataSource.LOGS) {
|
||||||
|
return !(panelType === PANEL_TYPES.TABLE || panelType === PANEL_TYPES.PIE);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}, [dataSource, panelType]);
|
||||||
|
|
||||||
|
const handleAggregationIntervalChange = (value: string): void => {
|
||||||
|
onAggregationIntervalChange(Number(value));
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="query-aggregation-container">
|
||||||
|
<div className="aggregation-container">
|
||||||
|
<QueryAggregationSelect onChange={onChange} queryData={queryData} />
|
||||||
|
|
||||||
|
{showAggregationInterval && (
|
||||||
|
<div className="query-aggregation-interval">
|
||||||
|
<div className="query-aggregation-interval-label">every</div>
|
||||||
|
<div className="query-aggregation-interval-input-container">
|
||||||
|
<InputWithLabel
|
||||||
|
initialValue={
|
||||||
|
queryData.stepInterval ? queryData.stepInterval : undefined
|
||||||
|
}
|
||||||
|
className="query-aggregation-interval-input"
|
||||||
|
label="Seconds"
|
||||||
|
placeholder="Auto"
|
||||||
|
type="number"
|
||||||
|
onChange={handleAggregationIntervalChange}
|
||||||
|
labelAfter
|
||||||
|
onClose={(): void => {}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
QueryAggregationOptions.defaultProps = {
|
||||||
|
panelType: null,
|
||||||
|
onChange: undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default QueryAggregationOptions;
|
||||||
@@ -0,0 +1,530 @@
|
|||||||
|
/* eslint-disable import/no-extraneous-dependencies */
|
||||||
|
/* eslint-disable no-cond-assign */
|
||||||
|
/* eslint-disable no-restricted-syntax */
|
||||||
|
/* eslint-disable class-methods-use-this */
|
||||||
|
/* eslint-disable react/no-this-in-sfc */
|
||||||
|
/* eslint-disable sonarjs/cognitive-complexity */
|
||||||
|
import './QueryAggregation.styles.scss';
|
||||||
|
|
||||||
|
import {
|
||||||
|
autocompletion,
|
||||||
|
closeCompletion,
|
||||||
|
Completion,
|
||||||
|
CompletionContext,
|
||||||
|
completionKeymap,
|
||||||
|
CompletionResult,
|
||||||
|
startCompletion,
|
||||||
|
} from '@codemirror/autocomplete';
|
||||||
|
import { javascript } from '@codemirror/lang-javascript';
|
||||||
|
import { RangeSetBuilder } from '@codemirror/state';
|
||||||
|
import { copilot } from '@uiw/codemirror-theme-copilot';
|
||||||
|
import CodeMirror, {
|
||||||
|
Decoration,
|
||||||
|
EditorView,
|
||||||
|
keymap,
|
||||||
|
ViewPlugin,
|
||||||
|
ViewUpdate,
|
||||||
|
} from '@uiw/react-codemirror';
|
||||||
|
import { getAggregateAttribute } from 'api/queryBuilder/getAggregateAttribute';
|
||||||
|
import { QueryBuilderKeys } from 'constants/queryBuilder';
|
||||||
|
import { tracesAggregateOperatorOptions } from 'constants/queryBuilderOperators';
|
||||||
|
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';
|
||||||
|
|
||||||
|
import { useQueryBuilderV2Context } from '../../QueryBuilderV2Context';
|
||||||
|
|
||||||
|
const chipDecoration = Decoration.mark({
|
||||||
|
class: 'chip-decorator',
|
||||||
|
});
|
||||||
|
|
||||||
|
const operatorArgMeta: Record<
|
||||||
|
string,
|
||||||
|
{ acceptsArgs: boolean; multiple: boolean }
|
||||||
|
> = {
|
||||||
|
[TracesAggregatorOperator.NOOP]: { acceptsArgs: false, multiple: false },
|
||||||
|
[TracesAggregatorOperator.COUNT]: { acceptsArgs: false, multiple: false },
|
||||||
|
[TracesAggregatorOperator.COUNT_DISTINCT]: {
|
||||||
|
acceptsArgs: true,
|
||||||
|
multiple: true,
|
||||||
|
},
|
||||||
|
[TracesAggregatorOperator.SUM]: { acceptsArgs: true, multiple: false },
|
||||||
|
[TracesAggregatorOperator.AVG]: { acceptsArgs: true, multiple: false },
|
||||||
|
[TracesAggregatorOperator.MAX]: { acceptsArgs: true, multiple: false },
|
||||||
|
[TracesAggregatorOperator.MIN]: { acceptsArgs: true, multiple: false },
|
||||||
|
[TracesAggregatorOperator.P05]: { acceptsArgs: true, multiple: false },
|
||||||
|
[TracesAggregatorOperator.P10]: { acceptsArgs: true, multiple: false },
|
||||||
|
[TracesAggregatorOperator.P20]: { acceptsArgs: true, multiple: false },
|
||||||
|
[TracesAggregatorOperator.P25]: { acceptsArgs: true, multiple: false },
|
||||||
|
[TracesAggregatorOperator.P50]: { acceptsArgs: true, multiple: false },
|
||||||
|
[TracesAggregatorOperator.P75]: { acceptsArgs: true, multiple: false },
|
||||||
|
[TracesAggregatorOperator.P90]: { acceptsArgs: true, multiple: false },
|
||||||
|
[TracesAggregatorOperator.P95]: { acceptsArgs: true, multiple: false },
|
||||||
|
[TracesAggregatorOperator.P99]: { acceptsArgs: true, multiple: false },
|
||||||
|
[TracesAggregatorOperator.RATE]: { acceptsArgs: true, multiple: false },
|
||||||
|
[TracesAggregatorOperator.RATE_SUM]: { acceptsArgs: true, multiple: false },
|
||||||
|
[TracesAggregatorOperator.RATE_AVG]: { acceptsArgs: true, multiple: false },
|
||||||
|
[TracesAggregatorOperator.RATE_MIN]: { acceptsArgs: true, multiple: false },
|
||||||
|
[TracesAggregatorOperator.RATE_MAX]: { acceptsArgs: true, multiple: false },
|
||||||
|
};
|
||||||
|
|
||||||
|
function getFunctionContextAtCursor(
|
||||||
|
text: string,
|
||||||
|
cursorPos: number,
|
||||||
|
): string | null {
|
||||||
|
// Find the nearest function name to the left of the nearest unmatched '('
|
||||||
|
let openParenIndex = -1;
|
||||||
|
let funcName: string | null = null;
|
||||||
|
let parenStack = 0;
|
||||||
|
for (let i = cursorPos - 1; i >= 0; i--) {
|
||||||
|
if (text[i] === ')') parenStack++;
|
||||||
|
else if (text[i] === '(') {
|
||||||
|
if (parenStack === 0) {
|
||||||
|
openParenIndex = i;
|
||||||
|
const before = text.slice(0, i);
|
||||||
|
const match = before.match(/(\w+)\s*$/);
|
||||||
|
if (match) funcName = match[1].toLowerCase();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
parenStack--;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (openParenIndex === -1 || !funcName) return null;
|
||||||
|
// Scan forwards to find the matching closing parenthesis
|
||||||
|
let closeParenIndex = -1;
|
||||||
|
let depth = 1;
|
||||||
|
for (let j = openParenIndex + 1; j < text.length; j++) {
|
||||||
|
if (text[j] === '(') depth++;
|
||||||
|
else if (text[j] === ')') depth--;
|
||||||
|
if (depth === 0) {
|
||||||
|
closeParenIndex = j;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
cursorPos > openParenIndex &&
|
||||||
|
(closeParenIndex === -1 || cursorPos <= closeParenIndex)
|
||||||
|
) {
|
||||||
|
return funcName;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Custom extension to stop events from propagating to global shortcuts
|
||||||
|
const stopEventsExtension = EditorView.domEventHandlers({
|
||||||
|
keydown: (event) => {
|
||||||
|
// Stop all keyboard events from propagating to global shortcuts
|
||||||
|
event.stopPropagation();
|
||||||
|
event.stopImmediatePropagation();
|
||||||
|
return false; // Important for CM to know you handled it
|
||||||
|
},
|
||||||
|
input: (event) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
focus: (event) => {
|
||||||
|
// Ensure focus events don't interfere with global shortcuts
|
||||||
|
event.stopPropagation();
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
blur: (event) => {
|
||||||
|
// Ensure blur events don't interfere with global shortcuts
|
||||||
|
event.stopPropagation();
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// eslint-disable-next-line react/no-this-in-sfc
|
||||||
|
function QueryAggregationSelect({
|
||||||
|
onChange,
|
||||||
|
queryData,
|
||||||
|
}: {
|
||||||
|
onChange?: (value: string) => void;
|
||||||
|
queryData: IBuilderQuery;
|
||||||
|
}): JSX.Element {
|
||||||
|
const { setAggregationOptions } = useQueryBuilderV2Context();
|
||||||
|
|
||||||
|
const [input, setInput] = useState(
|
||||||
|
queryData?.aggregations?.map((i: any) => i.expression).join(' ') || '',
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setInput(
|
||||||
|
queryData?.aggregations?.map((i: any) => i.expression).join(' ') || '',
|
||||||
|
);
|
||||||
|
}, [queryData?.aggregations]);
|
||||||
|
|
||||||
|
const [cursorPos, setCursorPos] = useState(0);
|
||||||
|
const [functionArgPairs, setFunctionArgPairs] = useState<
|
||||||
|
{ func: string; arg: string }[]
|
||||||
|
>([]);
|
||||||
|
const editorRef = useRef<EditorView | null>(null);
|
||||||
|
const [isFocused, setIsFocused] = useState(false);
|
||||||
|
|
||||||
|
// Helper function to safely start completion
|
||||||
|
const safeStartCompletion = useCallback((): void => {
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
if (editorRef.current) {
|
||||||
|
startCompletion(editorRef.current);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Update cursor position on every editor update
|
||||||
|
const handleUpdate = (update: { view: EditorView }): void => {
|
||||||
|
const pos = update.view.state.selection.main.from;
|
||||||
|
setCursorPos(pos);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Effect to handle focus state and trigger suggestions
|
||||||
|
useEffect(() => {
|
||||||
|
if (isFocused) {
|
||||||
|
safeStartCompletion();
|
||||||
|
}
|
||||||
|
}, [isFocused, safeStartCompletion]);
|
||||||
|
|
||||||
|
// Extract all valid function-argument pairs from the input
|
||||||
|
useEffect(() => {
|
||||||
|
const pairs: { func: string; arg: string }[] = [];
|
||||||
|
const regex = /([a-zA-Z_][\w]*)\s*\(([^)]*)\)/g;
|
||||||
|
let match;
|
||||||
|
while ((match = regex.exec(input)) !== null) {
|
||||||
|
const func = match[1].toLowerCase();
|
||||||
|
const args = match[2]
|
||||||
|
.split(',')
|
||||||
|
.map((arg) => arg.trim())
|
||||||
|
.filter((arg) => arg.length > 0);
|
||||||
|
|
||||||
|
if (args.length === 0) {
|
||||||
|
// For functions with no arguments, add a pair with empty string as arg
|
||||||
|
pairs.push({ func, arg: '' });
|
||||||
|
} else {
|
||||||
|
args.forEach((arg) => {
|
||||||
|
pairs.push({ func, arg });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setFunctionArgPairs(pairs);
|
||||||
|
setAggregationOptions(pairs);
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [input]);
|
||||||
|
|
||||||
|
// Find function context for fetching suggestions
|
||||||
|
const functionContextForFetch = getFunctionContextAtCursor(input, cursorPos);
|
||||||
|
|
||||||
|
const { data: aggregateAttributeData, isLoading: isLoadingFields } = useQuery(
|
||||||
|
[
|
||||||
|
QueryBuilderKeys.GET_AGGREGATE_ATTRIBUTE,
|
||||||
|
functionContextForFetch,
|
||||||
|
queryData.dataSource,
|
||||||
|
],
|
||||||
|
() =>
|
||||||
|
getAggregateAttribute({
|
||||||
|
searchText: '',
|
||||||
|
aggregateOperator: functionContextForFetch as string,
|
||||||
|
dataSource: queryData.dataSource,
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
enabled:
|
||||||
|
!!functionContextForFetch &&
|
||||||
|
!!operatorArgMeta[functionContextForFetch]?.acceptsArgs,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// 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(
|
||||||
|
() =>
|
||||||
|
ViewPlugin.fromClass(
|
||||||
|
class {
|
||||||
|
decorations: import('@codemirror/view').DecorationSet;
|
||||||
|
|
||||||
|
constructor(view: EditorView) {
|
||||||
|
this.decorations = this.buildDecorations(view);
|
||||||
|
}
|
||||||
|
|
||||||
|
update(update: ViewUpdate): void {
|
||||||
|
if (update.docChanged || update.viewportChanged) {
|
||||||
|
this.decorations = this.buildDecorations(update.view);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
buildDecorations(
|
||||||
|
view: EditorView,
|
||||||
|
): import('@codemirror/view').DecorationSet {
|
||||||
|
const builder = new RangeSetBuilder<Decoration>();
|
||||||
|
for (const { from, to } of view.visibleRanges) {
|
||||||
|
const text = view.state.doc.sliceString(from, to);
|
||||||
|
|
||||||
|
const regex = /\b([a-zA-Z_][\w]*)\s*\(([^)]*)\)/g;
|
||||||
|
let match;
|
||||||
|
|
||||||
|
while ((match = regex.exec(text)) !== null) {
|
||||||
|
const func = match[1].toLowerCase();
|
||||||
|
|
||||||
|
if (validFunctions.includes(func)) {
|
||||||
|
const start = from + match.index;
|
||||||
|
const end = start + match[0].length;
|
||||||
|
builder.add(start, end, chipDecoration);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return builder.finish();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
decorations: (v: any): import('@codemirror/view').DecorationSet =>
|
||||||
|
v.decorations,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
[validFunctions],
|
||||||
|
) as any;
|
||||||
|
|
||||||
|
const operatorCompletions: Completion[] = tracesAggregateOperatorOptions.map(
|
||||||
|
(op) => ({
|
||||||
|
label: op.value,
|
||||||
|
type: 'function',
|
||||||
|
info: op.label,
|
||||||
|
apply: (
|
||||||
|
view: EditorView,
|
||||||
|
completion: Completion,
|
||||||
|
from: number,
|
||||||
|
to: number,
|
||||||
|
): void => {
|
||||||
|
const acceptsArgs = operatorArgMeta[op.value]?.acceptsArgs;
|
||||||
|
|
||||||
|
let insertText: string;
|
||||||
|
let cursorPos: number;
|
||||||
|
|
||||||
|
if (!acceptsArgs) {
|
||||||
|
insertText = `${op.value}() `;
|
||||||
|
cursorPos = from + insertText.length; // Use insertText.length instead of hardcoded values
|
||||||
|
} else {
|
||||||
|
insertText = `${op.value}(`;
|
||||||
|
cursorPos = from + insertText.length; // Use insertText.length instead of hardcoded values
|
||||||
|
}
|
||||||
|
|
||||||
|
view.dispatch({
|
||||||
|
changes: { from, to, insert: insertText },
|
||||||
|
selection: { anchor: cursorPos },
|
||||||
|
});
|
||||||
|
|
||||||
|
// Trigger suggestions after a small delay
|
||||||
|
setTimeout(() => {
|
||||||
|
safeStartCompletion();
|
||||||
|
}, 50);
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Memoize field suggestions from API (no filtering here)
|
||||||
|
const fieldSuggestions = useMemo(
|
||||||
|
() =>
|
||||||
|
aggregateAttributeData?.payload?.attributeKeys?.map(
|
||||||
|
(attributeKey: BaseAutocompleteData) => ({
|
||||||
|
label: attributeKey.key,
|
||||||
|
type: 'variable',
|
||||||
|
info: attributeKey.dataType,
|
||||||
|
apply: (
|
||||||
|
view: EditorView,
|
||||||
|
completion: Completion,
|
||||||
|
from: number,
|
||||||
|
to: number,
|
||||||
|
): void => {
|
||||||
|
const text = view.state.sliceDoc(0, from);
|
||||||
|
const funcName = getFunctionContextAtCursor(text, from);
|
||||||
|
const multiple = funcName ? operatorArgMeta[funcName]?.multiple : false;
|
||||||
|
|
||||||
|
// Insert the selected key followed by either a comma or closing parenthesis
|
||||||
|
const insertText = multiple
|
||||||
|
? `${completion.label},`
|
||||||
|
: `${completion.label}) `;
|
||||||
|
const cursorPos = from + insertText.length; // Use insertText.length instead of hardcoded values
|
||||||
|
|
||||||
|
view.dispatch({
|
||||||
|
changes: { from, to, insert: insertText },
|
||||||
|
selection: { anchor: cursorPos },
|
||||||
|
});
|
||||||
|
|
||||||
|
// Trigger next suggestions after a small delay
|
||||||
|
setTimeout(() => {
|
||||||
|
safeStartCompletion();
|
||||||
|
}, 50);
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
) || [],
|
||||||
|
[aggregateAttributeData, safeStartCompletion],
|
||||||
|
);
|
||||||
|
|
||||||
|
const aggregatorAutocomplete = useMemo(
|
||||||
|
() =>
|
||||||
|
autocompletion({
|
||||||
|
override: [
|
||||||
|
(context: CompletionContext): CompletionResult | null => {
|
||||||
|
const text = context.state.sliceDoc(0, context.state.doc.length);
|
||||||
|
const cursorPos = context.pos;
|
||||||
|
const funcName = getFunctionContextAtCursor(text, cursorPos);
|
||||||
|
|
||||||
|
// Do not show suggestions if inside count()
|
||||||
|
if (
|
||||||
|
funcName === TracesAggregatorOperator.COUNT &&
|
||||||
|
cursorPos > 0 &&
|
||||||
|
text[cursorPos - 1] !== ')'
|
||||||
|
) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If inside a function that accepts args, show field suggestions
|
||||||
|
if (funcName && operatorArgMeta[funcName]?.acceptsArgs) {
|
||||||
|
if (isLoadingFields) {
|
||||||
|
return {
|
||||||
|
from: cursorPos,
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
label: 'Loading suggestions...',
|
||||||
|
type: 'text',
|
||||||
|
apply: (): void => {},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const doc = context.state.sliceDoc(0, cursorPos);
|
||||||
|
const lastOpenParen = doc.lastIndexOf('(');
|
||||||
|
const lastComma = doc.lastIndexOf(',', cursorPos - 1);
|
||||||
|
const startOfArg =
|
||||||
|
lastComma > lastOpenParen ? lastComma + 1 : lastOpenParen + 1;
|
||||||
|
const inputText = doc.slice(startOfArg, cursorPos).trim();
|
||||||
|
|
||||||
|
// Parse arguments already present in the function call (before the cursor)
|
||||||
|
const usedArgs = new Set<string>();
|
||||||
|
if (lastOpenParen !== -1) {
|
||||||
|
const argsString = doc.slice(lastOpenParen + 1, cursorPos);
|
||||||
|
argsString.split(',').forEach((arg) => {
|
||||||
|
const trimmed = arg.trim();
|
||||||
|
if (trimmed) usedArgs.add(trimmed);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exclude arguments already paired with this function elsewhere in the input
|
||||||
|
const globalUsedArgs = new Set(
|
||||||
|
functionArgPairs
|
||||||
|
.filter((pair) => pair.func === funcName)
|
||||||
|
.map((pair) => pair.arg),
|
||||||
|
);
|
||||||
|
|
||||||
|
const availableSuggestions = fieldSuggestions.filter(
|
||||||
|
(suggestion) =>
|
||||||
|
!usedArgs.has(suggestion.label) &&
|
||||||
|
!globalUsedArgs.has(suggestion.label),
|
||||||
|
);
|
||||||
|
|
||||||
|
const filteredSuggestions =
|
||||||
|
inputText === ''
|
||||||
|
? availableSuggestions
|
||||||
|
: availableSuggestions.filter((suggestion) =>
|
||||||
|
suggestion.label.toLowerCase().includes(inputText.toLowerCase()),
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
from: startOfArg,
|
||||||
|
options: filteredSuggestions,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show operator suggestions if no function context or not accepting args
|
||||||
|
if (!funcName || !operatorArgMeta[funcName]?.acceptsArgs) {
|
||||||
|
// Check if 'count(' is present in the current input (case-insensitive)
|
||||||
|
const hasCount = text.toLowerCase().includes('count(');
|
||||||
|
const availableOperators = hasCount
|
||||||
|
? operatorCompletions.filter((op) => op.label.toLowerCase() !== 'count')
|
||||||
|
: operatorCompletions;
|
||||||
|
|
||||||
|
// Get the word before cursor if any
|
||||||
|
const word = context.matchBefore(/[\w\d_]+/);
|
||||||
|
|
||||||
|
// Show suggestions if:
|
||||||
|
// 1. There's a word match
|
||||||
|
// 2. The input is empty (cursor at start)
|
||||||
|
// 3. The user explicitly triggered completion
|
||||||
|
if (word || cursorPos === 0 || context.explicit) {
|
||||||
|
return {
|
||||||
|
from: word ? word.from : cursorPos,
|
||||||
|
options: availableOperators,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
],
|
||||||
|
defaultKeymap: true,
|
||||||
|
closeOnBlur: true,
|
||||||
|
maxRenderedOptions: 50,
|
||||||
|
activateOnTyping: true,
|
||||||
|
}),
|
||||||
|
[operatorCompletions, isLoadingFields, fieldSuggestions, functionArgPairs],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="query-aggregation-select-container">
|
||||||
|
<CodeMirror
|
||||||
|
value={input}
|
||||||
|
onChange={(value): void => {
|
||||||
|
setInput(value);
|
||||||
|
onChange?.(value);
|
||||||
|
}}
|
||||||
|
className="query-aggregation-select-editor"
|
||||||
|
theme={copilot}
|
||||||
|
extensions={[
|
||||||
|
chipPlugin,
|
||||||
|
aggregatorAutocomplete,
|
||||||
|
javascript({ jsx: false, typescript: false }),
|
||||||
|
EditorView.lineWrapping,
|
||||||
|
stopEventsExtension,
|
||||||
|
keymap.of([
|
||||||
|
...completionKeymap,
|
||||||
|
{
|
||||||
|
key: 'Escape',
|
||||||
|
run: closeCompletion,
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
]}
|
||||||
|
placeholder="Type aggregator functions like sum(), count_distinct(...), etc."
|
||||||
|
basicSetup={{
|
||||||
|
lineNumbers: false,
|
||||||
|
autocompletion: true,
|
||||||
|
completionKeymap: true,
|
||||||
|
}}
|
||||||
|
onUpdate={handleUpdate}
|
||||||
|
onCreateEditor={(view: EditorView): void => {
|
||||||
|
editorRef.current = view;
|
||||||
|
}}
|
||||||
|
onFocus={(): void => {
|
||||||
|
setIsFocused(true);
|
||||||
|
safeStartCompletion();
|
||||||
|
}}
|
||||||
|
onBlur={(): void => {
|
||||||
|
setIsFocused(false);
|
||||||
|
|
||||||
|
if (editorRef.current) {
|
||||||
|
closeCompletion(editorRef.current);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
QueryAggregationSelect.defaultProps = {
|
||||||
|
onChange: undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default QueryAggregationSelect;
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import { Button } from 'antd';
|
||||||
|
import { Plus, Sigma } from 'lucide-react';
|
||||||
|
|
||||||
|
export default function QueryFooter({
|
||||||
|
addNewBuilderQuery,
|
||||||
|
addNewFormula,
|
||||||
|
}: {
|
||||||
|
addNewBuilderQuery: () => void;
|
||||||
|
addNewFormula: () => void;
|
||||||
|
}): JSX.Element {
|
||||||
|
return (
|
||||||
|
<div className="qb-footer">
|
||||||
|
<div className="qb-footer-container">
|
||||||
|
<div className="qb-add-new-query">
|
||||||
|
<Button
|
||||||
|
className="add-new-query-button periscope-btn secondary"
|
||||||
|
type="text"
|
||||||
|
icon={<Plus size={16} />}
|
||||||
|
onClick={addNewBuilderQuery}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="qb-add-formula">
|
||||||
|
<Button
|
||||||
|
className="add-formula-button periscope-btn secondary"
|
||||||
|
icon={<Sigma size={16} />}
|
||||||
|
onClick={addNewFormula}
|
||||||
|
>
|
||||||
|
Add Formula
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,713 @@
|
|||||||
|
.code-mirror-where-clause {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
font-family: Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto,
|
||||||
|
'Helvetica Neue', sans-serif;
|
||||||
|
|
||||||
|
.query-where-clause-editor-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
|
||||||
|
.query-where-clause-editor {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 400px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.query-status-container {
|
||||||
|
width: 32px;
|
||||||
|
|
||||||
|
background-color: #121317 !important;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
border: 1px solid var(--bg-slate-200);
|
||||||
|
border-radius: 2px;
|
||||||
|
border-top-left-radius: 0px !important;
|
||||||
|
border-bottom-left-radius: 0px !important;
|
||||||
|
border-left: none !important;
|
||||||
|
|
||||||
|
&.hasErrors {
|
||||||
|
border-color: var(--bg-cherry-500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.query-where-clause-editor {
|
||||||
|
&.hasErrors {
|
||||||
|
.cm-editor {
|
||||||
|
.cm-content {
|
||||||
|
border-color: var(--bg-cherry-500);
|
||||||
|
border-top-right-radius: 0px !important;
|
||||||
|
border-bottom-right-radius: 0px !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.cm-editor {
|
||||||
|
border-radius: 2px;
|
||||||
|
overflow: hidden;
|
||||||
|
background-color: transparent !important;
|
||||||
|
|
||||||
|
&:focus-within {
|
||||||
|
border-color: var(--bg-robin-500);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cm-content {
|
||||||
|
border-radius: 2px;
|
||||||
|
border: 1px solid var(--Slate-400, #1d212d);
|
||||||
|
padding: 0px !important;
|
||||||
|
background-color: #121317 !important;
|
||||||
|
|
||||||
|
&:focus-within {
|
||||||
|
border-color: var(--bg-ink-200);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.cm-focused {
|
||||||
|
outline: 1px solid var(--bg-slate-200);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cm-tooltip-autocomplete {
|
||||||
|
background: var(--bg-ink-300) !important;
|
||||||
|
border-radius: 2px !important;
|
||||||
|
font-size: 12px !important;
|
||||||
|
font-weight: 500 !important;
|
||||||
|
margin-top: -2px !important;
|
||||||
|
min-width: 400px !important;
|
||||||
|
position: relative !important;
|
||||||
|
top: 0px !important;
|
||||||
|
left: 0px !important;
|
||||||
|
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 0px;
|
||||||
|
background: linear-gradient(
|
||||||
|
139deg,
|
||||||
|
rgba(18, 19, 23, 0.8) 0%,
|
||||||
|
rgba(18, 19, 23, 0.9) 98.68%
|
||||||
|
) !important;
|
||||||
|
backdrop-filter: blur(20px);
|
||||||
|
box-sizing: border-box;
|
||||||
|
font-family: 'Space Mono', monospace !important;
|
||||||
|
|
||||||
|
ul {
|
||||||
|
width: 100% !important;
|
||||||
|
max-width: 100% !important;
|
||||||
|
font-family: 'Space Mono', monospace !important;
|
||||||
|
min-height: 200px !important;
|
||||||
|
|
||||||
|
&::-webkit-scrollbar {
|
||||||
|
width: 0.3rem;
|
||||||
|
}
|
||||||
|
&::-webkit-scrollbar-corner {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
&::-webkit-scrollbar-thumb {
|
||||||
|
background: rgb(136, 136, 136);
|
||||||
|
border-radius: 0.625rem;
|
||||||
|
}
|
||||||
|
&::-webkit-scrollbar-track {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
li {
|
||||||
|
width: 100% !important;
|
||||||
|
max-width: 100% !important;
|
||||||
|
line-height: 36px !important;
|
||||||
|
height: 36px !important;
|
||||||
|
padding: 4px 8px !important;
|
||||||
|
|
||||||
|
display: flex !important;
|
||||||
|
align-items: center !important;
|
||||||
|
gap: 8px !important;
|
||||||
|
box-sizing: border-box;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
font-family: 'Space Mono', monospace !important;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: var(--bg-ink-100) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cm-completionIcon {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
&[aria-selected='true'] {
|
||||||
|
// background-color: rgba(78, 116, 248, 0.7) !important;
|
||||||
|
background: rgba(171, 189, 255, 0.04) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.cm-gutters {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cm-line {
|
||||||
|
line-height: 34px !important;
|
||||||
|
font-family: 'Space Mono', monospace !important;
|
||||||
|
background-color: #121317 !important;
|
||||||
|
|
||||||
|
::-moz-selection {
|
||||||
|
background: var(--bg-ink-100) !important;
|
||||||
|
opacity: 0.5 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
::selection {
|
||||||
|
background: var(--bg-ink-100) !important;
|
||||||
|
opacity: 0.5 !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.cm-selectionBackground {
|
||||||
|
background: var(--bg-ink-100) !important;
|
||||||
|
opacity: 0.5 !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.cursor-position {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--bg-ink-200);
|
||||||
|
padding: 6px;
|
||||||
|
background-color: var(--bg-vanilla-200);
|
||||||
|
border-radius: 4px;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.query-validation {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
margin-top: 16px;
|
||||||
|
|
||||||
|
.valid,
|
||||||
|
.invalid {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.valid {
|
||||||
|
background-color: rgba(39, 174, 96, 0.1);
|
||||||
|
color: #27ae60;
|
||||||
|
}
|
||||||
|
|
||||||
|
.invalid {
|
||||||
|
background-color: rgba(235, 87, 87, 0.1);
|
||||||
|
color: #eb5757;
|
||||||
|
}
|
||||||
|
|
||||||
|
.query-validation-status {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.query-validation-errors {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
|
.query-validation-error {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: 16px;
|
||||||
|
|
||||||
|
font-size: 12px;
|
||||||
|
font-family: 'Space Mono', monospace !important;
|
||||||
|
color: var(--bg-cherry-500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.query-context {
|
||||||
|
padding: 12px;
|
||||||
|
background-color: var(--bg-ink-400);
|
||||||
|
border-radius: 4px;
|
||||||
|
border-left: 3px solid var(--bg-robin-500);
|
||||||
|
color: var(--bg-ink-300) !important;
|
||||||
|
|
||||||
|
.ant-card-head {
|
||||||
|
color: var(--bg-vanilla-300) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.context-details {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 12px;
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 13px;
|
||||||
|
|
||||||
|
strong {
|
||||||
|
color: var(--bg-vanilla-300);
|
||||||
|
margin-right: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-mirror-card {
|
||||||
|
.ant-card-body {
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.query-text-preview-title {
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--bg-vanilla-100);
|
||||||
|
background-color: var(--bg-robin-500);
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-radius: 2px;
|
||||||
|
margin-right: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.query-text-preview {
|
||||||
|
font-family: 'Space Mono', monospace;
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--bg-vanilla-200);
|
||||||
|
padding: 2px 6px;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.query-examples-card {
|
||||||
|
background-color: var(--bg-ink-400);
|
||||||
|
border: 1px solid var(--bg-slate-200);
|
||||||
|
|
||||||
|
.ant-card-body {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.query-examples {
|
||||||
|
.ant-collapse-header {
|
||||||
|
padding: 8px 16px !important;
|
||||||
|
color: var(--bg-vanilla-300) !important;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-collapse-content {
|
||||||
|
background-color: transparent !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.query-examples-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: 8px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.query-example-tag {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
background-color: var(--bg-ink-400);
|
||||||
|
border: 1px solid var(--bg-slate-200);
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
outline: none;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--bg-ink-300);
|
||||||
|
border-color: var(--bg-robin-500);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus-visible {
|
||||||
|
outline: 2px solid var(--bg-robin-500);
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.query-example-content {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.query-example-label {
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--bg-vanilla-300);
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.query-example-query {
|
||||||
|
font-family: 'Space Mono', monospace;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--bg-vanilla-200);
|
||||||
|
background-color: var(--bg-ink-300);
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.query-example-description {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--bg-vanilla-200);
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.query-example-content {
|
||||||
|
display: inline-flex;
|
||||||
|
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Context indicator styles
|
||||||
|
.context-indicator {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
padding: 8px 12px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 13px;
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
border-left: 4px solid #1890ff;
|
||||||
|
|
||||||
|
display: none;
|
||||||
|
|
||||||
|
.triplet-info {
|
||||||
|
margin-left: 16px;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.query-pair-info {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
border-left: 1px solid rgba(0, 0, 0, 0.1);
|
||||||
|
padding-left: 8px;
|
||||||
|
background-color: rgba(0, 0, 0, 0.03);
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Color variations based on context
|
||||||
|
&.context-indicator-key {
|
||||||
|
border-left-color: #1890ff; // blue
|
||||||
|
background-color: rgba(24, 144, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.context-indicator-operator {
|
||||||
|
border-left-color: #722ed1; // purple
|
||||||
|
background-color: rgba(114, 46, 209, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.context-indicator-value {
|
||||||
|
border-left-color: #52c41a; // green
|
||||||
|
background-color: rgba(82, 196, 26, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.context-indicator-conjunction {
|
||||||
|
border-left-color: #fa8c16; // orange
|
||||||
|
background-color: rgba(250, 140, 22, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.context-indicator-function {
|
||||||
|
border-left-color: #13c2c2; // cyan
|
||||||
|
background-color: rgba(19, 194, 194, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.context-indicator-parenthesis {
|
||||||
|
border-left-color: #eb2f96; // magenta
|
||||||
|
background-color: rgba(235, 47, 150, 0.1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.query-status-popover {
|
||||||
|
.ant-popover-arrow {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-popover-content {
|
||||||
|
background: linear-gradient(
|
||||||
|
139deg,
|
||||||
|
rgba(18, 19, 23, 0.8) 0%,
|
||||||
|
rgba(18, 19, 23, 0.9) 98.68%
|
||||||
|
);
|
||||||
|
box-shadow: 4px 10px 16px 2px rgba(0, 0, 0, 0.2);
|
||||||
|
backdrop-filter: blur(20px);
|
||||||
|
|
||||||
|
margin-top: -6px !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// /* Dark mode support */
|
||||||
|
// :global(.darkMode) {
|
||||||
|
// .code-mirror-where-clause {
|
||||||
|
// .cm-editor {
|
||||||
|
// border-color: var(--bg-slate-500);
|
||||||
|
// background-color: var(--bg-ink-400);
|
||||||
|
// }
|
||||||
|
|
||||||
|
// .cursor-position {
|
||||||
|
// background-color: var(--bg-ink-400);
|
||||||
|
// color: var(--bg-vanilla-100);
|
||||||
|
// }
|
||||||
|
|
||||||
|
// .query-context {
|
||||||
|
// background-color: var(--bg-ink-400);
|
||||||
|
// color: var(--bg-vanilla-100);
|
||||||
|
|
||||||
|
// h3 {
|
||||||
|
// color: var(--bg-vanilla-100);
|
||||||
|
// }
|
||||||
|
|
||||||
|
// .context-details {
|
||||||
|
// p {
|
||||||
|
// strong {
|
||||||
|
// color: var(--bg-vanilla-200);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// .query-examples-card {
|
||||||
|
// background-color: var(--bg-ink-400);
|
||||||
|
// border-color: var(--bg-slate-500);
|
||||||
|
|
||||||
|
// .ant-collapse-header {
|
||||||
|
// color: var(--bg-vanilla-100) !important;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// .query-example-tag {
|
||||||
|
// background-color: var(--bg-ink-400);
|
||||||
|
// border-color: var(--bg-slate-500);
|
||||||
|
|
||||||
|
// &:hover {
|
||||||
|
// background-color: var(--bg-ink-300);
|
||||||
|
// border-color: var(--bg-robin-500);
|
||||||
|
// }
|
||||||
|
|
||||||
|
// .query-example-label {
|
||||||
|
// color: var(--bg-vanilla-100);
|
||||||
|
// }
|
||||||
|
|
||||||
|
// .query-example-query {
|
||||||
|
// color: var(--bg-vanilla-100);
|
||||||
|
// background-color: var(--bg-ink-300);
|
||||||
|
// }
|
||||||
|
|
||||||
|
// .query-example-description {
|
||||||
|
// color: var(--bg-vanilla-100);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// .context-indicator {
|
||||||
|
// background-color: var(--bg-ink-300);
|
||||||
|
// color: var(--bg-vanilla-100);
|
||||||
|
|
||||||
|
// .query-pair-info {
|
||||||
|
// border-left: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
// background-color: rgba(255, 255, 255, 0.05);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
.lightMode {
|
||||||
|
.code-mirror-where-clause {
|
||||||
|
.query-where-clause-editor-container {
|
||||||
|
.query-status-container {
|
||||||
|
background-color: var(--bg-vanilla-100) !important;
|
||||||
|
border: 1px solid var(--bg-vanilla-300);
|
||||||
|
|
||||||
|
&.hasErrors {
|
||||||
|
border-color: var(--bg-cherry-500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.query-where-clause-editor {
|
||||||
|
&.hasErrors {
|
||||||
|
.cm-editor {
|
||||||
|
.cm-content {
|
||||||
|
border-color: var(--bg-cherry-500);
|
||||||
|
border-top-right-radius: 0px !important;
|
||||||
|
border-bottom-right-radius: 0px !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.cm-editor {
|
||||||
|
&:focus-within {
|
||||||
|
border-color: var(--bg-robin-500);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.cm-focused {
|
||||||
|
outline: 1px solid var(--bg-vanilla-300);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cm-content {
|
||||||
|
border-radius: 2px;
|
||||||
|
border: 1px solid var(--bg-vanilla-300);
|
||||||
|
padding: 0px !important;
|
||||||
|
background-color: var(--bg-vanilla-100) !important;
|
||||||
|
|
||||||
|
&:focus-within {
|
||||||
|
border-color: var(--bg-vanilla-200);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.cm-tooltip-autocomplete {
|
||||||
|
background: var(--bg-vanilla-100) !important;
|
||||||
|
|
||||||
|
border: 0px;
|
||||||
|
backdrop-filter: blur(20px);
|
||||||
|
|
||||||
|
ul {
|
||||||
|
li {
|
||||||
|
background-color: var(--bg-vanilla-100) !important;
|
||||||
|
color: var(--bg-ink-300) !important;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--bg-vanilla-200) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.cm-line {
|
||||||
|
background-color: var(--bg-vanilla-100) !important;
|
||||||
|
|
||||||
|
::-moz-selection {
|
||||||
|
background: var(--bg-vanilla-100) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
::selection {
|
||||||
|
background: var(--bg-vanilla-100) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.cm-selectionBackground {
|
||||||
|
background: var(--bg-vanilla-100) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.cursor-position {
|
||||||
|
color: var(--bg-vanilla-200);
|
||||||
|
background-color: var(--bg-vanilla-100);
|
||||||
|
}
|
||||||
|
|
||||||
|
.query-context {
|
||||||
|
background-color: var(--bg-vanilla-100);
|
||||||
|
border-left: 3px solid var(--bg-vanilla-300);
|
||||||
|
color: var(--bg-vanilla-300) !important;
|
||||||
|
|
||||||
|
.ant-card-head {
|
||||||
|
color: var(--bg-ink-300) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.context-details {
|
||||||
|
p {
|
||||||
|
strong {
|
||||||
|
color: var(--bg-ink-300);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.query-examples-card {
|
||||||
|
background-color: var(--bg-vanilla-100);
|
||||||
|
border: 1px solid var(--bg-vanilla-300);
|
||||||
|
|
||||||
|
.query-examples {
|
||||||
|
.ant-collapse-header {
|
||||||
|
color: var(--bg-ink-300) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.query-example-tag {
|
||||||
|
background-color: var(--bg-vanilla-100);
|
||||||
|
border: 1px solid var(--bg-vanilla-300);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--bg-vanilla-200);
|
||||||
|
border-color: var(--bg-vanilla-300);
|
||||||
|
}
|
||||||
|
|
||||||
|
.query-example-label {
|
||||||
|
color: var(--bg-ink-300);
|
||||||
|
}
|
||||||
|
|
||||||
|
.query-example-query {
|
||||||
|
color: var(--bg-ink-300);
|
||||||
|
background-color: var(--bg-vanilla-100);
|
||||||
|
}
|
||||||
|
|
||||||
|
.query-example-description {
|
||||||
|
color: var(--bg-ink-300);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.context-indicator {
|
||||||
|
background-color: var(--bg-vanilla-100);
|
||||||
|
border-left: 4px solid var(--bg-vanilla-300);
|
||||||
|
|
||||||
|
display: none;
|
||||||
|
|
||||||
|
.query-pair-info {
|
||||||
|
border-left: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
background-color: rgba(255, 255, 255, 0.03);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Color variations based on context
|
||||||
|
&.context-indicator-key {
|
||||||
|
border-left-color: #1890ff; // blue
|
||||||
|
background-color: rgba(24, 144, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.context-indicator-operator {
|
||||||
|
border-left-color: #722ed1; // purple
|
||||||
|
background-color: rgba(114, 46, 209, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.context-indicator-value {
|
||||||
|
border-left-color: #52c41a; // green
|
||||||
|
background-color: rgba(82, 196, 26, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.context-indicator-conjunction {
|
||||||
|
border-left-color: #fa8c16; // orange
|
||||||
|
background-color: rgba(250, 140, 22, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.context-indicator-function {
|
||||||
|
border-left-color: #13c2c2; // cyan
|
||||||
|
background-color: rgba(19, 194, 194, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.context-indicator-parenthesis {
|
||||||
|
border-left-color: #eb2f96; // magenta
|
||||||
|
background-color: rgba(235, 47, 150, 0.1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.query-status-popover {
|
||||||
|
.ant-popover-content {
|
||||||
|
background: var(--bg-vanilla-100);
|
||||||
|
box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
export const queryExamples = [
|
||||||
|
{
|
||||||
|
label: 'Basic Query',
|
||||||
|
query: "status = 'error'",
|
||||||
|
description: 'Find all errors',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Multiple Conditions',
|
||||||
|
query: "status = 'error' AND service = 'frontend'",
|
||||||
|
description: 'Find errors from frontend service',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'IN Operator',
|
||||||
|
query: "status IN ['error', 'warning']",
|
||||||
|
description: 'Find items with specific statuses',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Function Usage',
|
||||||
|
query: "HAS(service, 'frontend')",
|
||||||
|
description: 'Use HAS function',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Numeric Comparison',
|
||||||
|
query: 'duration > 1000',
|
||||||
|
description: 'Find items with duration greater than 1000ms',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Range Query',
|
||||||
|
query: 'duration BETWEEN 100 AND 1000',
|
||||||
|
description: 'Find items with duration between 100ms and 1000ms',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Pattern Matching',
|
||||||
|
query: "service LIKE 'front%'",
|
||||||
|
description: 'Find services starting with "front"',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Complex Conditions',
|
||||||
|
query: "(status = 'error' OR status = 'warning') AND service = 'frontend'",
|
||||||
|
description: 'Find errors or warnings from frontend service',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Multiple Functions',
|
||||||
|
query: "HAS(service, 'frontend') AND HAS(status, 'error')",
|
||||||
|
description: 'Use multiple HAS functions',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'NOT Operator',
|
||||||
|
query: "NOT status = 'success'",
|
||||||
|
description: 'Find items that are not successful',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Array Contains',
|
||||||
|
query: "tags CONTAINS 'production'",
|
||||||
|
description: 'Find items with production tag',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Regex Pattern',
|
||||||
|
query: "service REGEXP '^prod-.*'",
|
||||||
|
description: 'Find services matching regex pattern',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Null Check',
|
||||||
|
query: 'error IS NULL',
|
||||||
|
description: 'Find items without errors',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Multiple Attributes',
|
||||||
|
query:
|
||||||
|
"service = 'frontend' AND environment = 'production' AND status = 'error'",
|
||||||
|
description: 'Find production frontend errors',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Nested Conditions',
|
||||||
|
query:
|
||||||
|
"(service = 'frontend' OR service = 'backend') AND (status = 'error' OR status = 'warning')",
|
||||||
|
description: 'Find errors or warnings from frontend or backend',
|
||||||
|
},
|
||||||
|
];
|
||||||
234
frontend/src/components/QueryBuilderV2/QueryV2/QueryV2.tsx
Normal file
@@ -0,0 +1,234 @@
|
|||||||
|
import { Dropdown } from 'antd';
|
||||||
|
import cx from 'classnames';
|
||||||
|
import { ENTITY_VERSION_V4 } from 'constants/app';
|
||||||
|
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||||
|
import QBEntityOptions from 'container/QueryBuilder/components/QBEntityOptions/QBEntityOptions';
|
||||||
|
import { QueryProps } from 'container/QueryBuilder/components/Query/Query.interfaces';
|
||||||
|
import SpanScopeSelector from 'container/QueryBuilder/filters/QueryBuilderSearchV2/SpanScopeSelector';
|
||||||
|
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||||
|
import { useQueryOperations } from 'hooks/queryBuilder/useQueryBuilderOperations';
|
||||||
|
import { Copy, Ellipsis, Trash } from 'lucide-react';
|
||||||
|
import { memo, useCallback, useMemo, useState } from 'react';
|
||||||
|
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
||||||
|
import { HandleChangeQueryDataV5 } from 'types/common/operations.types';
|
||||||
|
import { DataSource } from 'types/common/queryBuilder';
|
||||||
|
|
||||||
|
import MetricsAggregateSection from './MerticsAggregateSection/MetricsAggregateSection';
|
||||||
|
import { MetricsSelect } from './MetricsSelect/MetricsSelect';
|
||||||
|
import QueryAddOns from './QueryAddOns/QueryAddOns';
|
||||||
|
import QueryAggregation from './QueryAggregation/QueryAggregation';
|
||||||
|
import QuerySearch from './QuerySearch/QuerySearch';
|
||||||
|
|
||||||
|
export const QueryV2 = memo(function QueryV2({
|
||||||
|
ref,
|
||||||
|
index,
|
||||||
|
queryVariant,
|
||||||
|
query,
|
||||||
|
filterConfigs,
|
||||||
|
isListViewPanel = false,
|
||||||
|
version,
|
||||||
|
showOnlyWhereClause = false,
|
||||||
|
}: QueryProps & { ref: React.RefObject<HTMLDivElement> }): JSX.Element {
|
||||||
|
const { cloneQuery, panelType } = useQueryBuilder();
|
||||||
|
|
||||||
|
const showFunctions = query?.functions?.length > 0;
|
||||||
|
const { dataSource } = query;
|
||||||
|
|
||||||
|
const [isCollapsed, setIsCollapsed] = useState(false);
|
||||||
|
|
||||||
|
const {
|
||||||
|
handleChangeQueryData,
|
||||||
|
handleDeleteQuery,
|
||||||
|
handleQueryFunctionsUpdates,
|
||||||
|
handleChangeDataSource,
|
||||||
|
} = useQueryOperations({
|
||||||
|
index,
|
||||||
|
query,
|
||||||
|
filterConfigs,
|
||||||
|
isListViewPanel,
|
||||||
|
entityVersion: version,
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleToggleDisableQuery = useCallback(() => {
|
||||||
|
handleChangeQueryData('disabled', !query.disabled);
|
||||||
|
}, [handleChangeQueryData, query]);
|
||||||
|
|
||||||
|
const handleToggleCollapsQuery = (): void => {
|
||||||
|
setIsCollapsed(!isCollapsed);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCloneEntity = (): void => {
|
||||||
|
cloneQuery('query', query);
|
||||||
|
};
|
||||||
|
|
||||||
|
const showReduceTo = useMemo(
|
||||||
|
() =>
|
||||||
|
dataSource === DataSource.METRICS &&
|
||||||
|
(panelType === PANEL_TYPES.TABLE ||
|
||||||
|
panelType === PANEL_TYPES.PIE ||
|
||||||
|
panelType === PANEL_TYPES.VALUE),
|
||||||
|
[dataSource, panelType],
|
||||||
|
);
|
||||||
|
|
||||||
|
const showSpanScopeSelector = useMemo(() => dataSource === DataSource.TRACES, [
|
||||||
|
dataSource,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const handleChangeAggregateEvery = useCallback(
|
||||||
|
(value: IBuilderQuery['stepInterval']) => {
|
||||||
|
handleChangeQueryData('stepInterval', value);
|
||||||
|
},
|
||||||
|
[handleChangeQueryData],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleSearchChange = useCallback(
|
||||||
|
(value: string) => {
|
||||||
|
(handleChangeQueryData as HandleChangeQueryDataV5)('filter', {
|
||||||
|
expression: value,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[handleChangeQueryData],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleChangeAggregation = useCallback(
|
||||||
|
(value: string) => {
|
||||||
|
(handleChangeQueryData as HandleChangeQueryDataV5)('aggregations', [
|
||||||
|
{
|
||||||
|
expression: value,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
[handleChangeQueryData],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cx('query-v2', { 'where-clause-view': showOnlyWhereClause })}
|
||||||
|
ref={ref}
|
||||||
|
>
|
||||||
|
<div className="qb-content-section">
|
||||||
|
{!showOnlyWhereClause && (
|
||||||
|
<div className="qb-header-container">
|
||||||
|
<div className="query-actions-container">
|
||||||
|
<div className="query-actions-left-container">
|
||||||
|
<QBEntityOptions
|
||||||
|
isMetricsDataSource={dataSource === DataSource.METRICS}
|
||||||
|
showFunctions={
|
||||||
|
(version && version === ENTITY_VERSION_V4) ||
|
||||||
|
query.dataSource === DataSource.LOGS ||
|
||||||
|
showFunctions ||
|
||||||
|
false
|
||||||
|
}
|
||||||
|
isCollapsed={isCollapsed}
|
||||||
|
entityType="query"
|
||||||
|
entityData={query}
|
||||||
|
onToggleVisibility={handleToggleDisableQuery}
|
||||||
|
onDelete={handleDeleteQuery}
|
||||||
|
onCloneQuery={cloneQuery}
|
||||||
|
onCollapseEntity={handleToggleCollapsQuery}
|
||||||
|
query={query}
|
||||||
|
onQueryFunctionsUpdates={handleQueryFunctionsUpdates}
|
||||||
|
showDeleteButton={false}
|
||||||
|
showCloneOption={false}
|
||||||
|
isListViewPanel={isListViewPanel}
|
||||||
|
index={index}
|
||||||
|
queryVariant={queryVariant}
|
||||||
|
onChangeDataSource={handleChangeDataSource}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{!isListViewPanel && (
|
||||||
|
<Dropdown
|
||||||
|
className="query-actions-dropdown"
|
||||||
|
menu={{
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
label: 'Clone',
|
||||||
|
key: 'clone-query',
|
||||||
|
icon: <Copy size={14} />,
|
||||||
|
onClick: handleCloneEntity,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Delete',
|
||||||
|
key: 'delete-query',
|
||||||
|
icon: <Trash size={14} />,
|
||||||
|
onClick: handleDeleteQuery,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}}
|
||||||
|
placement="bottomRight"
|
||||||
|
>
|
||||||
|
<Ellipsis size={16} />
|
||||||
|
</Dropdown>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!isCollapsed && (
|
||||||
|
<div className="qb-elements-container">
|
||||||
|
<div className="qb-search-container">
|
||||||
|
{dataSource === DataSource.METRICS && (
|
||||||
|
<div className="metrics-select-container">
|
||||||
|
<MetricsSelect query={query} index={index} version="v4" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="qb-search-filter-container">
|
||||||
|
<div className="query-search-container">
|
||||||
|
<QuerySearch
|
||||||
|
key={`query-search-${query.queryName}-${query.dataSource}`}
|
||||||
|
onChange={handleSearchChange}
|
||||||
|
queryData={query}
|
||||||
|
dataSource={dataSource}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{showSpanScopeSelector && (
|
||||||
|
<div className="traces-search-filter-container">
|
||||||
|
<div className="traces-search-filter-in">in</div>
|
||||||
|
<SpanScopeSelector query={query} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{!showOnlyWhereClause &&
|
||||||
|
!isListViewPanel &&
|
||||||
|
dataSource !== DataSource.METRICS && (
|
||||||
|
<QueryAggregation
|
||||||
|
dataSource={dataSource}
|
||||||
|
key={`query-search-${query.queryName}-${query.dataSource}`}
|
||||||
|
panelType={panelType || undefined}
|
||||||
|
onAggregationIntervalChange={handleChangeAggregateEvery}
|
||||||
|
onChange={handleChangeAggregation}
|
||||||
|
queryData={query}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!showOnlyWhereClause && dataSource === DataSource.METRICS && (
|
||||||
|
<MetricsAggregateSection
|
||||||
|
panelType={panelType}
|
||||||
|
query={query}
|
||||||
|
index={index}
|
||||||
|
key={`metrics-aggregate-section-${query.queryName}-${query.dataSource}`}
|
||||||
|
version="v4"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!showOnlyWhereClause && (
|
||||||
|
<QueryAddOns
|
||||||
|
index={index}
|
||||||
|
query={query}
|
||||||
|
version="v3"
|
||||||
|
isListViewPanel={isListViewPanel}
|
||||||
|
showReduceTo={showReduceTo}
|
||||||
|
panelType={panelType}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
185
frontend/src/components/QueryBuilderV2/utils.ts
Normal file
@@ -0,0 +1,185 @@
|
|||||||
|
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||||
|
import { Having, TagFilter } from 'types/api/queryBuilder/queryBuilderData';
|
||||||
|
import {
|
||||||
|
LogAggregation,
|
||||||
|
MetricAggregation,
|
||||||
|
TraceAggregation,
|
||||||
|
} from 'types/api/v5/queryRange';
|
||||||
|
import { DataSource } from 'types/common/queryBuilder';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if an operator requires array values (like IN, NOT IN)
|
||||||
|
* @param operator - The operator to check
|
||||||
|
* @returns True if the operator requires array values
|
||||||
|
*/
|
||||||
|
const isArrayOperator = (operator: string): boolean => {
|
||||||
|
const arrayOperators = ['in', 'nin', 'IN', 'NOT IN'];
|
||||||
|
return arrayOperators.includes(operator);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format a value for the expression string
|
||||||
|
* @param value - The value to format
|
||||||
|
* @param operator - The operator being used (to determine if array is needed)
|
||||||
|
* @returns Formatted value string
|
||||||
|
*/
|
||||||
|
const formatValueForExpression = (
|
||||||
|
value: string[] | string | number | boolean,
|
||||||
|
operator?: string,
|
||||||
|
): string => {
|
||||||
|
// For IN operators, ensure value is always an array
|
||||||
|
if (isArrayOperator(operator || '')) {
|
||||||
|
const arrayValue = Array.isArray(value) ? value : [value];
|
||||||
|
return `[${arrayValue
|
||||||
|
.map((v) =>
|
||||||
|
typeof v === 'string' ? `'${v.replace(/'/g, "\\'")}'` : String(v),
|
||||||
|
)
|
||||||
|
.join(', ')}]`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
// Handle array values (e.g., for IN operations)
|
||||||
|
return `[${value
|
||||||
|
.map((v) =>
|
||||||
|
typeof v === 'string' ? `'${v.replace(/'/g, "\\'")}'` : String(v),
|
||||||
|
)
|
||||||
|
.join(', ')}]`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
// Add single quotes around all string values and escape internal single quotes
|
||||||
|
return `'${value.replace(/'/g, "\\'")}'`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return String(value);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const convertFiltersToExpression = (
|
||||||
|
filters: TagFilter,
|
||||||
|
): { expression: string } => {
|
||||||
|
if (!filters?.items || filters.items.length === 0) {
|
||||||
|
return { expression: '' };
|
||||||
|
}
|
||||||
|
|
||||||
|
const expressions = filters.items
|
||||||
|
.map((filter) => {
|
||||||
|
const { key, op, value } = filter;
|
||||||
|
|
||||||
|
// Skip if key is not defined
|
||||||
|
if (!key?.key) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
const formattedValue = formatValueForExpression(value, op);
|
||||||
|
return `${key.key} ${op} ${formattedValue}`;
|
||||||
|
})
|
||||||
|
.filter((expression) => expression !== ''); // Remove empty expressions
|
||||||
|
|
||||||
|
return {
|
||||||
|
expression: expressions.join(' AND '),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert old having format to new having format
|
||||||
|
* @param having - Array of old having objects with columnName, op, and value
|
||||||
|
* @returns New having format with expression string
|
||||||
|
*/
|
||||||
|
export const convertHavingToExpression = (
|
||||||
|
having: Having[],
|
||||||
|
): { expression: string } => {
|
||||||
|
if (!having || having.length === 0) {
|
||||||
|
return { expression: '' };
|
||||||
|
}
|
||||||
|
|
||||||
|
const expressions = having
|
||||||
|
.map((havingItem) => {
|
||||||
|
const { columnName, op, value } = havingItem;
|
||||||
|
|
||||||
|
// Skip if columnName is not defined
|
||||||
|
if (!columnName) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Format value based on its type
|
||||||
|
let formattedValue: string;
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
// For array values, format as [val1, val2, ...]
|
||||||
|
formattedValue = `[${value.join(', ')}]`;
|
||||||
|
} else {
|
||||||
|
// For single values, just convert to string
|
||||||
|
formattedValue = String(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${columnName} ${op} ${formattedValue}`;
|
||||||
|
})
|
||||||
|
.filter((expression) => expression !== ''); // Remove empty expressions
|
||||||
|
|
||||||
|
return {
|
||||||
|
expression: expressions.join(' AND '),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert old aggregation format to new aggregation format
|
||||||
|
* @param aggregateOperator - The aggregate operator (e.g., 'sum', 'count', 'avg')
|
||||||
|
* @param aggregateAttribute - The attribute to aggregate
|
||||||
|
* @param dataSource - The data source type
|
||||||
|
* @param timeAggregation - Time aggregation for metrics (optional)
|
||||||
|
* @param spaceAggregation - Space aggregation for metrics (optional)
|
||||||
|
* @param alias - Optional alias for the aggregation
|
||||||
|
* @returns New aggregation format based on data source
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export const convertAggregationToExpression = (
|
||||||
|
aggregateOperator: string,
|
||||||
|
aggregateAttribute: BaseAutocompleteData,
|
||||||
|
dataSource: DataSource,
|
||||||
|
timeAggregation?: string,
|
||||||
|
spaceAggregation?: string,
|
||||||
|
alias?: string,
|
||||||
|
): (TraceAggregation | LogAggregation | MetricAggregation)[] | undefined => {
|
||||||
|
// Skip if no operator or attribute key
|
||||||
|
if (!aggregateOperator) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Replace noop with count as default
|
||||||
|
const normalizedOperator =
|
||||||
|
aggregateOperator === 'noop' ? 'count' : aggregateOperator;
|
||||||
|
const normalizedTimeAggregation =
|
||||||
|
timeAggregation === 'noop' ? 'count' : timeAggregation;
|
||||||
|
const normalizedSpaceAggregation =
|
||||||
|
spaceAggregation === 'noop' ? 'count' : spaceAggregation;
|
||||||
|
|
||||||
|
// For metrics, use the MetricAggregation format
|
||||||
|
if (dataSource === DataSource.METRICS) {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
metricName: aggregateAttribute.key,
|
||||||
|
timeAggregation: (normalizedTimeAggregation || normalizedOperator) as any,
|
||||||
|
spaceAggregation: (normalizedSpaceAggregation || normalizedOperator) as any,
|
||||||
|
} as MetricAggregation,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// For traces and logs, use expression format
|
||||||
|
const expression = `${normalizedOperator}(${aggregateAttribute.key})`;
|
||||||
|
|
||||||
|
if (dataSource === DataSource.TRACES) {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
expression,
|
||||||
|
...(alias && { alias }),
|
||||||
|
} as TraceAggregation,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// For logs
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
expression,
|
||||||
|
...(alias && { alias }),
|
||||||
|
} as LogAggregation,
|
||||||
|
];
|
||||||
|
};
|
||||||
@@ -1,10 +1,5 @@
|
|||||||
import { Tabs, TabsProps } from 'antd';
|
import { Tabs, TabsProps } from 'antd';
|
||||||
import {
|
import { useLocation, useParams } from 'react-router-dom';
|
||||||
generatePath,
|
|
||||||
matchPath,
|
|
||||||
useLocation,
|
|
||||||
useParams,
|
|
||||||
} from 'react-router-dom';
|
|
||||||
|
|
||||||
import { RouteTabProps } from './types';
|
import { RouteTabProps } from './types';
|
||||||
|
|
||||||
@@ -22,13 +17,20 @@ function RouteTab({
|
|||||||
const params = useParams<Params>();
|
const params = useParams<Params>();
|
||||||
const location = useLocation();
|
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
|
// Find the matching route for the current pathname
|
||||||
const currentRoute = routes.find((route) => {
|
const currentRoute = routesWithParams.find((route) => {
|
||||||
const routePath = route.route.split('?')[0];
|
const routePattern = route.route.replace(/:(\w+)/g, '([^/]+)');
|
||||||
return matchPath(location.pathname, {
|
const regex = new RegExp(`^${routePattern}$`);
|
||||||
path: routePath,
|
return regex.test(location.pathname);
|
||||||
exact: true,
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const onChange = (activeRoute: string): void => {
|
const onChange = (activeRoute: string): void => {
|
||||||
@@ -36,15 +38,14 @@ function RouteTab({
|
|||||||
onChangeHandler(activeRoute);
|
onChangeHandler(activeRoute);
|
||||||
}
|
}
|
||||||
|
|
||||||
const selectedRoute = routes.find((e) => e.key === activeRoute);
|
const selectedRoute = routesWithParams.find((e) => e.key === activeRoute);
|
||||||
|
|
||||||
if (selectedRoute) {
|
if (selectedRoute) {
|
||||||
const resolvedRoute = generatePath(selectedRoute.route, params);
|
history.push(selectedRoute.route);
|
||||||
history.push(resolvedRoute);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const items = routes.map(({ Component, name, route, key }) => ({
|
const items = routesWithParams.map(({ Component, name, route, key }) => ({
|
||||||
label: name,
|
label: name,
|
||||||
key,
|
key,
|
||||||
tabKey: route,
|
tabKey: route,
|
||||||
|
|||||||
@@ -15,3 +15,4 @@ export const DASHBOARD_TIME_IN_DURATION = 'refreshInterval';
|
|||||||
|
|
||||||
export const DEFAULT_ENTITY_VERSION = 'v3';
|
export const DEFAULT_ENTITY_VERSION = 'v3';
|
||||||
export const ENTITY_VERSION_V4 = 'v4';
|
export const ENTITY_VERSION_V4 = 'v4';
|
||||||
|
export const ENTITY_VERSION_V5 = 'v5';
|
||||||
|
|||||||
@@ -46,5 +46,5 @@ export enum QueryParams {
|
|||||||
msgSystem = 'msgSystem',
|
msgSystem = 'msgSystem',
|
||||||
destination = 'destination',
|
destination = 'destination',
|
||||||
kindString = 'kindString',
|
kindString = 'kindString',
|
||||||
tab = 'tab',
|
selectedExplorerView = 'selectedExplorerView',
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -169,6 +169,9 @@ export const initialQueryBuilderFormValues: IBuilderQuery = {
|
|||||||
aggregateAttribute: initialAutocompleteData,
|
aggregateAttribute: initialAutocompleteData,
|
||||||
timeAggregation: MetricAggregateOperator.RATE,
|
timeAggregation: MetricAggregateOperator.RATE,
|
||||||
spaceAggregation: MetricAggregateOperator.SUM,
|
spaceAggregation: MetricAggregateOperator.SUM,
|
||||||
|
filter: { expression: '' },
|
||||||
|
aggregations: [{ expression: 'count() ' }],
|
||||||
|
havingExpression: { expression: '' },
|
||||||
functions: [],
|
functions: [],
|
||||||
filters: { items: [], op: 'AND' },
|
filters: { items: [], op: 'AND' },
|
||||||
expression: createNewBuilderItemName({
|
expression: createNewBuilderItemName({
|
||||||
|
|||||||
@@ -6,10 +6,6 @@ import {
|
|||||||
import { SelectOption } from 'types/common/select';
|
import { SelectOption } from 'types/common/select';
|
||||||
|
|
||||||
export const metricAggregateOperatorOptions: SelectOption<string, string>[] = [
|
export const metricAggregateOperatorOptions: SelectOption<string, string>[] = [
|
||||||
{
|
|
||||||
value: MetricAggregateOperator.NOOP,
|
|
||||||
label: 'NOOP',
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
value: MetricAggregateOperator.COUNT,
|
value: MetricAggregateOperator.COUNT,
|
||||||
label: 'Count',
|
label: 'Count',
|
||||||
@@ -130,10 +126,6 @@ export const metricAggregateOperatorOptions: SelectOption<string, string>[] = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
export const tracesAggregateOperatorOptions: SelectOption<string, string>[] = [
|
export const tracesAggregateOperatorOptions: SelectOption<string, string>[] = [
|
||||||
{
|
|
||||||
value: TracesAggregatorOperator.NOOP,
|
|
||||||
label: 'NOOP',
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
value: TracesAggregatorOperator.COUNT,
|
value: TracesAggregatorOperator.COUNT,
|
||||||
label: 'Count',
|
label: 'Count',
|
||||||
@@ -217,10 +209,6 @@ export const tracesAggregateOperatorOptions: SelectOption<string, string>[] = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
export const logsAggregateOperatorOptions: SelectOption<string, string>[] = [
|
export const logsAggregateOperatorOptions: SelectOption<string, string>[] = [
|
||||||
{
|
|
||||||
value: LogsAggregatorOperator.NOOP,
|
|
||||||
label: 'NOOP',
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
value: LogsAggregatorOperator.COUNT,
|
value: LogsAggregatorOperator.COUNT,
|
||||||
label: 'Count',
|
label: 'Count',
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
export const USER_PREFERENCES = {
|
export const USER_PREFERENCES = {
|
||||||
SIDENAV_PINNED: 'sidenav_pinned',
|
SIDENAV_PINNED: 'sidenav_pinned',
|
||||||
NAV_SHORTCUTS: 'nav_shortcuts',
|
NAV_SHORTCUTS: 'nav_shortcuts',
|
||||||
LAST_SEEN_CHANGELOG_VERSION: 'last_seen_changelog_version',
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -54,19 +54,21 @@ function DomainList(): JSX.Element {
|
|||||||
|
|
||||||
// initialise tab with default query.
|
// initialise tab with default query.
|
||||||
useShareBuilderUrl({
|
useShareBuilderUrl({
|
||||||
...initialQueriesMap.traces,
|
defaultValue: {
|
||||||
builder: {
|
...initialQueriesMap.traces,
|
||||||
...initialQueriesMap.traces.builder,
|
builder: {
|
||||||
queryData: [
|
...initialQueriesMap.traces.builder,
|
||||||
{
|
queryData: [
|
||||||
...initialQueriesMap.traces.builder.queryData[0],
|
{
|
||||||
dataSource: DataSource.TRACES,
|
...initialQueriesMap.traces.builder.queryData[0],
|
||||||
aggregateOperator: 'noop',
|
dataSource: DataSource.TRACES,
|
||||||
aggregateAttribute: {
|
aggregateOperator: 'noop',
|
||||||
...initialQueriesMap.traces.builder.queryData[0].aggregateAttribute,
|
aggregateAttribute: {
|
||||||
|
...initialQueriesMap.traces.builder.queryData[0].aggregateAttribute,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
],
|
||||||
],
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,4 @@
|
|||||||
// Earlier we were having app-banner-container class
|
.app-banner-container {
|
||||||
// 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;
|
position: relative;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ import manageCreditCardApi from 'api/v1/portal/create';
|
|||||||
import getUserLatestVersion from 'api/v1/version/getLatestVersion';
|
import getUserLatestVersion from 'api/v1/version/getLatestVersion';
|
||||||
import getUserVersion from 'api/v1/version/getVersion';
|
import getUserVersion from 'api/v1/version/getVersion';
|
||||||
import cx from 'classnames';
|
import cx from 'classnames';
|
||||||
import ChangelogModal from 'components/ChangelogModal/ChangelogModal';
|
|
||||||
import ChatSupportGateway from 'components/ChatSupportGateway/ChatSupportGateway';
|
import ChatSupportGateway from 'components/ChatSupportGateway/ChatSupportGateway';
|
||||||
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
|
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
|
||||||
import { Events } from 'constants/events';
|
import { Events } from 'constants/events';
|
||||||
@@ -27,7 +26,6 @@ import dayjs from 'dayjs';
|
|||||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||||
import { useGetTenantLicense } from 'hooks/useGetTenantLicense';
|
import { useGetTenantLicense } from 'hooks/useGetTenantLicense';
|
||||||
import { useNotifications } from 'hooks/useNotifications';
|
import { useNotifications } from 'hooks/useNotifications';
|
||||||
import useTabVisibility from 'hooks/useTabFocus';
|
|
||||||
import history from 'lib/history';
|
import history from 'lib/history';
|
||||||
import { isNull } from 'lodash-es';
|
import { isNull } from 'lodash-es';
|
||||||
import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFallback';
|
import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFallback';
|
||||||
@@ -56,10 +54,7 @@ import {
|
|||||||
} from 'types/actions/app';
|
} from 'types/actions/app';
|
||||||
import { ErrorResponse, SuccessResponse, SuccessResponseV2 } from 'types/api';
|
import { ErrorResponse, SuccessResponse, SuccessResponseV2 } from 'types/api';
|
||||||
import { CheckoutSuccessPayloadProps } from 'types/api/billing/checkout';
|
import { CheckoutSuccessPayloadProps } from 'types/api/billing/checkout';
|
||||||
import {
|
import { ChangelogSchema } from 'types/api/changelog/getChangelogByVersion';
|
||||||
ChangelogSchema,
|
|
||||||
DeploymentType,
|
|
||||||
} from 'types/api/changelog/getChangelogByVersion';
|
|
||||||
import APIError from 'types/api/error';
|
import APIError from 'types/api/error';
|
||||||
import {
|
import {
|
||||||
LicenseEvent,
|
LicenseEvent,
|
||||||
@@ -68,6 +63,7 @@ import {
|
|||||||
} from 'types/api/licensesV3/getActive';
|
} from 'types/api/licensesV3/getActive';
|
||||||
import AppReducer from 'types/reducer/app';
|
import AppReducer from 'types/reducer/app';
|
||||||
import { USER_ROLES } from 'types/roles';
|
import { USER_ROLES } from 'types/roles';
|
||||||
|
import { checkVersionState } from 'utils/app';
|
||||||
import { eventEmitter } from 'utils/getEventEmitter';
|
import { eventEmitter } from 'utils/getEventEmitter';
|
||||||
import {
|
import {
|
||||||
getFormattedDate,
|
getFormattedDate,
|
||||||
@@ -91,9 +87,6 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
|||||||
featureFlagsFetchError,
|
featureFlagsFetchError,
|
||||||
userPreferences,
|
userPreferences,
|
||||||
updateChangelog,
|
updateChangelog,
|
||||||
toggleChangelogModal,
|
|
||||||
showChangelogModal,
|
|
||||||
changelog,
|
|
||||||
} = useAppContext();
|
} = useAppContext();
|
||||||
|
|
||||||
const { notifications } = useNotifications();
|
const { notifications } = useNotifications();
|
||||||
@@ -105,11 +98,16 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
|||||||
|
|
||||||
const [showSlowApiWarning, setShowSlowApiWarning] = useState(false);
|
const [showSlowApiWarning, setShowSlowApiWarning] = useState(false);
|
||||||
const [slowApiWarningShown, setSlowApiWarningShown] = useState(false);
|
const [slowApiWarningShown, setSlowApiWarningShown] = useState(false);
|
||||||
|
const [shouldFetchChangelog, setShouldFetchChangelog] = useState<boolean>(
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
|
||||||
const { latestVersion } = useSelector<AppState, AppReducer>(
|
const { currentVersion, latestVersion } = useSelector<AppState, AppReducer>(
|
||||||
(state) => state.app,
|
(state) => state.app,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const isLatestVersion = checkVersionState(currentVersion, latestVersion);
|
||||||
|
|
||||||
const handleBillingOnSuccess = (
|
const handleBillingOnSuccess = (
|
||||||
data: SuccessResponseV2<CheckoutSuccessPayloadProps>,
|
data: SuccessResponseV2<CheckoutSuccessPayloadProps>,
|
||||||
): void => {
|
): void => {
|
||||||
@@ -146,17 +144,6 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
|||||||
|
|
||||||
const { isCloudUser: isCloudUserVal } = useGetTenantLicense();
|
const { isCloudUser: isCloudUserVal } = useGetTenantLicense();
|
||||||
|
|
||||||
const changelogForTenant = isCloudUserVal
|
|
||||||
? DeploymentType.CLOUD_ONLY
|
|
||||||
: DeploymentType.OSS_ONLY;
|
|
||||||
|
|
||||||
const seenChangelogVersion = userPreferences?.find(
|
|
||||||
(preference) =>
|
|
||||||
preference.name === USER_PREFERENCES.LAST_SEEN_CHANGELOG_VERSION,
|
|
||||||
)?.value as string;
|
|
||||||
|
|
||||||
const isVisible = useTabVisibility();
|
|
||||||
|
|
||||||
const [
|
const [
|
||||||
getUserVersionResponse,
|
getUserVersionResponse,
|
||||||
getUserLatestVersionResponse,
|
getUserLatestVersionResponse,
|
||||||
@@ -174,43 +161,12 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
queryFn: (): Promise<SuccessResponse<ChangelogSchema> | ErrorResponse> =>
|
queryFn: (): Promise<SuccessResponse<ChangelogSchema> | ErrorResponse> =>
|
||||||
getChangelogByVersion(latestVersion, changelogForTenant),
|
getChangelogByVersion(latestVersion),
|
||||||
queryKey: ['getChangelogByVersion', latestVersion, changelogForTenant],
|
queryKey: ['getChangelogByVersion', latestVersion],
|
||||||
enabled: isLoggedIn && Boolean(latestVersion),
|
enabled: isLoggedIn && !isCloudUserVal && shouldFetchChangelog,
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
// refetch the changelog only when the current tab becomes active + there isn't an active request + no changelog already available
|
|
||||||
if (!changelog && !getChangelogByVersionResponse.isLoading && isVisible) {
|
|
||||||
getChangelogByVersionResponse.refetch();
|
|
||||||
}
|
|
||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
|
||||||
}, [isVisible]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
let timer: ReturnType<typeof setTimeout>;
|
|
||||||
if (
|
|
||||||
isCloudUserVal &&
|
|
||||||
Boolean(latestVersion) &&
|
|
||||||
latestVersion !== seenChangelogVersion
|
|
||||||
) {
|
|
||||||
// Automatically open the changelog modal for cloud users after 1s, if they've not seen this version before.
|
|
||||||
timer = setTimeout(() => {
|
|
||||||
toggleChangelogModal();
|
|
||||||
}, 1000);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (): void => {
|
|
||||||
clearInterval(timer);
|
|
||||||
};
|
|
||||||
}, [
|
|
||||||
isCloudUserVal,
|
|
||||||
latestVersion,
|
|
||||||
seenChangelogVersion,
|
|
||||||
toggleChangelogModal,
|
|
||||||
]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (getUserLatestVersionResponse.status === 'idle' && isLoggedIn) {
|
if (getUserLatestVersionResponse.status === 'idle' && isLoggedIn) {
|
||||||
getUserLatestVersionResponse.refetch();
|
getUserLatestVersionResponse.refetch();
|
||||||
@@ -267,7 +223,7 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
|||||||
|
|
||||||
if (
|
if (
|
||||||
getUserVersionResponse.isFetched &&
|
getUserVersionResponse.isFetched &&
|
||||||
getUserVersionResponse.isSuccess &&
|
getUserLatestVersionResponse.isSuccess &&
|
||||||
getUserVersionResponse.data &&
|
getUserVersionResponse.data &&
|
||||||
getUserVersionResponse.data.payload
|
getUserVersionResponse.data.payload
|
||||||
) {
|
) {
|
||||||
@@ -305,13 +261,18 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
|||||||
getUserVersionResponse.isLoading,
|
getUserVersionResponse.isLoading,
|
||||||
getUserVersionResponse.isError,
|
getUserVersionResponse.isError,
|
||||||
getUserVersionResponse.data,
|
getUserVersionResponse.data,
|
||||||
getUserVersionResponse.isSuccess,
|
|
||||||
getUserLatestVersionResponse.isFetched,
|
getUserLatestVersionResponse.isFetched,
|
||||||
getUserVersionResponse.isFetched,
|
getUserVersionResponse.isFetched,
|
||||||
getUserLatestVersionResponse.isSuccess,
|
getUserLatestVersionResponse.isSuccess,
|
||||||
notifications,
|
notifications,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isLatestVersion) {
|
||||||
|
setShouldFetchChangelog(true);
|
||||||
|
}
|
||||||
|
}, [isLatestVersion]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (
|
if (
|
||||||
getChangelogByVersionResponse.isFetched &&
|
getChangelogByVersionResponse.isFetched &&
|
||||||
@@ -652,7 +613,7 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
|||||||
</Helmet>
|
</Helmet>
|
||||||
|
|
||||||
{isLoggedIn && (
|
{isLoggedIn && (
|
||||||
<div className={cx('app-banner-wrapper')}>
|
<div className={cx('app-banner-container')}>
|
||||||
{SHOW_TRIAL_EXPIRY_BANNER && (
|
{SHOW_TRIAL_EXPIRY_BANNER && (
|
||||||
<div className="trial-expiry-banner">
|
<div className="trial-expiry-banner">
|
||||||
You are in free trial period. Your free trial will end on{' '}
|
You are in free trial period. Your free trial will end on{' '}
|
||||||
@@ -733,9 +694,6 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
|||||||
</Flex>
|
</Flex>
|
||||||
|
|
||||||
{showAddCreditCardModal && <ChatSupportGateway />}
|
{showAddCreditCardModal && <ChatSupportGateway />}
|
||||||
{showChangelogModal && changelog && (
|
|
||||||
<ChangelogModal changelog={changelog} onClose={toggleChangelogModal} />
|
|
||||||
)}
|
|
||||||
</Layout>
|
</Layout>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,14 +13,3 @@
|
|||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.lightMode {
|
|
||||||
.create-alert-channels-container {
|
|
||||||
background: var(--bg-vanilla-100);
|
|
||||||
border-color: var(--bg-vanilla-300);
|
|
||||||
|
|
||||||
.form-alert-channels-title {
|
|
||||||
color: var(--bg-ink-100);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ function Download({ data, isLoading, fileName }: DownloadProps): JSX.Element {
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
className="periscope-btn"
|
className="periscope-btn ghost"
|
||||||
loading={isLoading}
|
loading={isLoading}
|
||||||
icon={<FileDown size={14} />}
|
icon={<FileDown size={14} />}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,173 +1,30 @@
|
|||||||
.empty-logs-search {
|
.empty-logs-search-container {
|
||||||
&__container {
|
display: flex;
|
||||||
display: flex;
|
flex-direction: column;
|
||||||
flex-direction: column;
|
justify-content: center;
|
||||||
justify-content: center;
|
align-items: center;
|
||||||
align-items: center;
|
height: 240px;
|
||||||
height: 240px;
|
|
||||||
}
|
.empty-logs-search-container-content {
|
||||||
&__content {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 4px;
|
gap: 4px;
|
||||||
|
|
||||||
color: var(--text-vanilla-400);
|
color: var(--text-vanilla-400);
|
||||||
font-family: Inter;
|
font-family: Inter;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
line-height: 18px;
|
line-height: 18px; /* 128.571% */
|
||||||
letter-spacing: -0.07px;
|
letter-spacing: -0.07px;
|
||||||
align-items: flex-start;
|
|
||||||
.empty-state-svg {
|
.empty-state-svg {
|
||||||
height: 50px;
|
height: 50px;
|
||||||
width: 50px;
|
width: 50px;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
&__sub-text {
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__container {
|
.sub-text {
|
||||||
&--custom-message {
|
font-weight: 600;
|
||||||
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,24 +2,16 @@ import './EmptyLogsSearch.styles.scss';
|
|||||||
|
|
||||||
import { Typography } from 'antd';
|
import { Typography } from 'antd';
|
||||||
import logEvent from 'api/common/logEvent';
|
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 { useEffect, useRef } from 'react';
|
||||||
import { DataSource, PanelTypeKeys } from 'types/common/queryBuilder';
|
import { DataSource, PanelTypeKeys } from 'types/common/queryBuilder';
|
||||||
|
|
||||||
interface EmptyLogsSearchProps {
|
|
||||||
dataSource: DataSource;
|
|
||||||
panelType: PanelTypeKeys;
|
|
||||||
customMessage?: EmptyLogsListConfig;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function EmptyLogsSearch({
|
export default function EmptyLogsSearch({
|
||||||
dataSource,
|
dataSource,
|
||||||
panelType,
|
panelType,
|
||||||
customMessage,
|
}: {
|
||||||
}: EmptyLogsSearchProps): JSX.Element {
|
dataSource: DataSource;
|
||||||
|
panelType: PanelTypeKeys;
|
||||||
|
}): JSX.Element {
|
||||||
const logEventCalledRef = useRef(false);
|
const logEventCalledRef = useRef(false);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!logEventCalledRef.current) {
|
if (!logEventCalledRef.current) {
|
||||||
@@ -38,80 +30,18 @@ export default function EmptyLogsSearch({
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div className="empty-logs-search-container">
|
||||||
className={cx('empty-logs-search__container', {
|
<div className="empty-logs-search-container-content">
|
||||||
'empty-logs-search__container--custom-message': !!customMessage,
|
<img
|
||||||
})}
|
src="/Icons/emptyState.svg"
|
||||||
>
|
alt="thinking-emoji"
|
||||||
<div className="empty-logs-search__row">
|
className="empty-state-svg"
|
||||||
<div className="empty-logs-search__content">
|
/>
|
||||||
<img
|
<Typography.Text>
|
||||||
src="/Icons/emptyState.svg"
|
<span className="sub-text">This query had no results. </span>
|
||||||
alt="thinking-emoji"
|
Edit your query and try again!
|
||||||
className="empty-state-svg"
|
</Typography.Text>
|
||||||
/>
|
|
||||||
{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>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
EmptyLogsSearch.defaultProps = {
|
|
||||||
customMessage: null,
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
.explorer-options-container {
|
.explorer-options-container {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
bottom: 24px;
|
bottom: 8px;
|
||||||
left: calc(50% + 240px);
|
left: calc(50% + 240px);
|
||||||
transform: translate(calc(-50% - 120px), 0);
|
transform: translate(calc(-50% - 120px), 0);
|
||||||
transition: left 0.2s linear;
|
transition: left 0.2s linear;
|
||||||
|
|
||||||
|
border-radius: 6px;
|
||||||
|
background: var(--Ink-300, #16181d);
|
||||||
|
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 16px;
|
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
|
|
||||||
.multi-alert-button,
|
.multi-alert-button,
|
||||||
@@ -32,19 +34,15 @@
|
|||||||
.explorer-update {
|
.explorer-update {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 12px;
|
gap: 4px;
|
||||||
padding: 10px 10px;
|
padding: 8px;
|
||||||
border-radius: 50px;
|
background: var(--Ink-300, #16181d);
|
||||||
border: 1px solid var(--bg-slate-400);
|
|
||||||
background: rgba(22, 24, 29, 0.6);
|
|
||||||
backdrop-filter: blur(20px);
|
|
||||||
|
|
||||||
.action-icon {
|
.action-icon {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 8px;
|
padding: 6px;
|
||||||
border-radius: 50px;
|
|
||||||
border: 1px solid var(--bg-slate-400);
|
border: 1px solid var(--bg-slate-400);
|
||||||
background: var(--bg-slate-500);
|
background: var(--bg-slate-500);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
@@ -64,10 +62,8 @@
|
|||||||
|
|
||||||
.explorer-options {
|
.explorer-options {
|
||||||
padding: 10px 12px;
|
padding: 10px 12px;
|
||||||
border: 1px solid var(--bg-slate-400);
|
background: var(--Ink-300, #16181d);
|
||||||
border-radius: 50px;
|
border-radius: 2px;
|
||||||
background: rgba(22, 24, 29, 0.6);
|
|
||||||
backdrop-filter: blur(20px);
|
|
||||||
|
|
||||||
cursor: default;
|
cursor: default;
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -96,27 +92,6 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
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 {
|
.ant-select-focused {
|
||||||
border-color: transparent !important;
|
border-color: transparent !important;
|
||||||
|
|
||||||
@@ -257,6 +232,10 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.lightMode {
|
.lightMode {
|
||||||
|
.explorer-options-container {
|
||||||
|
background: var(--bg-vanilla-100);
|
||||||
|
}
|
||||||
|
|
||||||
.explorer-options {
|
.explorer-options {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
|
|||||||
@@ -1,12 +1,10 @@
|
|||||||
/* eslint-disable react/jsx-props-no-spreading */
|
/* eslint-disable react/jsx-props-no-spreading */
|
||||||
import './ExplorerOptions.styles.scss';
|
import './ExplorerOptions.styles.scss';
|
||||||
|
|
||||||
import { InfoCircleOutlined } from '@ant-design/icons';
|
|
||||||
import { Color } from '@signozhq/design-tokens';
|
import { Color } from '@signozhq/design-tokens';
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
ColorPicker,
|
ColorPicker,
|
||||||
Divider,
|
|
||||||
Input,
|
Input,
|
||||||
Modal,
|
Modal,
|
||||||
RefSelectProps,
|
RefSelectProps,
|
||||||
@@ -15,6 +13,7 @@ import {
|
|||||||
Typography,
|
Typography,
|
||||||
} from 'antd';
|
} from 'antd';
|
||||||
import logEvent from 'api/common/logEvent';
|
import logEvent from 'api/common/logEvent';
|
||||||
|
import { TelemetryFieldKey } from 'api/v5/v5';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import cx from 'classnames';
|
import cx from 'classnames';
|
||||||
import { getViewDetailsUsingViewKey } from 'components/ExplorerCard/utils';
|
import { getViewDetailsUsingViewKey } from 'components/ExplorerCard/utils';
|
||||||
@@ -45,14 +44,7 @@ import { useHandleExplorerTabChange } from 'hooks/useHandleExplorerTabChange';
|
|||||||
import { useNotifications } from 'hooks/useNotifications';
|
import { useNotifications } from 'hooks/useNotifications';
|
||||||
import { mapCompositeQueryFromQuery } from 'lib/newQueryBuilder/queryBuilderMappers/mapCompositeQueryFromQuery';
|
import { mapCompositeQueryFromQuery } from 'lib/newQueryBuilder/queryBuilderMappers/mapCompositeQueryFromQuery';
|
||||||
import { cloneDeep, isEqual, omit } from 'lodash-es';
|
import { cloneDeep, isEqual, omit } from 'lodash-es';
|
||||||
import {
|
import { Check, ConciergeBell, Disc3, Plus, X } from 'lucide-react';
|
||||||
Check,
|
|
||||||
ConciergeBell,
|
|
||||||
Disc3,
|
|
||||||
PanelBottomClose,
|
|
||||||
Plus,
|
|
||||||
X,
|
|
||||||
} from 'lucide-react';
|
|
||||||
import { useAppContext } from 'providers/App/App';
|
import { useAppContext } from 'providers/App/App';
|
||||||
import { FormattingOptions } from 'providers/preferences/types';
|
import { FormattingOptions } from 'providers/preferences/types';
|
||||||
import {
|
import {
|
||||||
@@ -67,7 +59,6 @@ import {
|
|||||||
} from 'react';
|
} from 'react';
|
||||||
import { useHistory } from 'react-router-dom';
|
import { useHistory } from 'react-router-dom';
|
||||||
import { Dashboard } from 'types/api/dashboard/getAll';
|
import { Dashboard } from 'types/api/dashboard/getAll';
|
||||||
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
|
||||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||||
import { ViewProps } from 'types/api/saveViews/types';
|
import { ViewProps } from 'types/api/saveViews/types';
|
||||||
import { DataSource, StringOperators } from 'types/common/queryBuilder';
|
import { DataSource, StringOperators } from 'types/common/queryBuilder';
|
||||||
@@ -78,10 +69,8 @@ import ExplorerOptionsHideArea from './ExplorerOptionsHideArea';
|
|||||||
import { PreservedViewsInLocalStorage } from './types';
|
import { PreservedViewsInLocalStorage } from './types';
|
||||||
import {
|
import {
|
||||||
DATASOURCE_VS_ROUTES,
|
DATASOURCE_VS_ROUTES,
|
||||||
generateRGBAFromHex,
|
|
||||||
getRandomColor,
|
getRandomColor,
|
||||||
saveNewViewHandler,
|
saveNewViewHandler,
|
||||||
setExplorerToolBarVisibility,
|
|
||||||
} from './utils';
|
} from './utils';
|
||||||
|
|
||||||
const allowedRoles = [USER_ROLES.ADMIN, USER_ROLES.AUTHOR, USER_ROLES.EDITOR];
|
const allowedRoles = [USER_ROLES.ADMIN, USER_ROLES.AUTHOR, USER_ROLES.EDITOR];
|
||||||
@@ -253,12 +242,6 @@ function ExplorerOptions({
|
|||||||
const extraData = viewsData?.data?.data?.find((view) => view.id === viewKey)
|
const extraData = viewsData?.data?.data?.find((view) => view.id === viewKey)
|
||||||
?.extraData;
|
?.extraData;
|
||||||
|
|
||||||
const extraDataColor = extraData ? JSON.parse(extraData).color : '';
|
|
||||||
const rgbaColor = generateRGBAFromHex(
|
|
||||||
extraDataColor || Color.BG_SIENNA_500,
|
|
||||||
0.08,
|
|
||||||
);
|
|
||||||
|
|
||||||
const { options, handleOptionsChange } = useOptionsMenu({
|
const { options, handleOptionsChange } = useOptionsMenu({
|
||||||
storageKey:
|
storageKey:
|
||||||
sourcepage === DataSource.TRACES
|
sourcepage === DataSource.TRACES
|
||||||
@@ -270,7 +253,7 @@ function ExplorerOptions({
|
|||||||
|
|
||||||
const getUpdatedExtraData = (
|
const getUpdatedExtraData = (
|
||||||
extraData: string | undefined,
|
extraData: string | undefined,
|
||||||
newSelectedColumns: BaseAutocompleteData[],
|
newSelectedColumns: TelemetryFieldKey[],
|
||||||
formattingOptions?: FormattingOptions,
|
formattingOptions?: FormattingOptions,
|
||||||
): string => {
|
): string => {
|
||||||
let updatedExtraData;
|
let updatedExtraData;
|
||||||
@@ -354,7 +337,7 @@ function ExplorerOptions({
|
|||||||
const { handleExplorerTabChange } = useHandleExplorerTabChange();
|
const { handleExplorerTabChange } = useHandleExplorerTabChange();
|
||||||
|
|
||||||
type ExtraData = {
|
type ExtraData = {
|
||||||
selectColumns?: BaseAutocompleteData[];
|
selectColumns?: TelemetryFieldKey[];
|
||||||
version?: number;
|
version?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -589,13 +572,6 @@ function ExplorerOptions({
|
|||||||
[isDarkMode],
|
[isDarkMode],
|
||||||
);
|
);
|
||||||
|
|
||||||
const hideToolbar = (): void => {
|
|
||||||
setExplorerToolBarVisibility(false, sourcepage);
|
|
||||||
if (setIsExplorerOptionHidden) {
|
|
||||||
setIsExplorerOptionHidden(true);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const isEditDeleteSupported = allowedRoles.includes(user.role as string);
|
const isEditDeleteSupported = allowedRoles.includes(user.role as string);
|
||||||
|
|
||||||
const [
|
const [
|
||||||
@@ -647,27 +623,6 @@ function ExplorerOptions({
|
|||||||
viewsData?.data?.data,
|
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 => {
|
const getQueryName = (query: Query): string => {
|
||||||
if (query.builder.queryFormulas.length > 0) {
|
if (query.builder.queryFormulas.length > 0) {
|
||||||
return `Formula ${query.builder.queryFormulas[0].queryName}`;
|
return `Formula ${query.builder.queryFormulas[0].queryName}`;
|
||||||
@@ -680,11 +635,10 @@ function ExplorerOptions({
|
|||||||
const selectLabel = (
|
const selectLabel = (
|
||||||
<Button
|
<Button
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
shape="round"
|
className="periscope-btn ghost"
|
||||||
|
shape="default"
|
||||||
icon={<ConciergeBell size={16} />}
|
icon={<ConciergeBell size={16} />}
|
||||||
>
|
/>
|
||||||
Create an Alert
|
|
||||||
</Button>
|
|
||||||
);
|
);
|
||||||
return (
|
return (
|
||||||
<Select
|
<Select
|
||||||
@@ -713,12 +667,11 @@ function ExplorerOptions({
|
|||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
shape="round"
|
shape="default"
|
||||||
|
className="periscope-btn ghost"
|
||||||
onClick={(): void => onCreateAlertsHandler(query)}
|
onClick={(): void => onCreateAlertsHandler(query)}
|
||||||
icon={<ConciergeBell size={16} />}
|
icon={<ConciergeBell size={16} />}
|
||||||
>
|
/>
|
||||||
Create an Alert
|
|
||||||
</Button>
|
|
||||||
);
|
);
|
||||||
}, [
|
}, [
|
||||||
disabled,
|
disabled,
|
||||||
@@ -732,14 +685,11 @@ function ExplorerOptions({
|
|||||||
if (isOneChartPerQuery) {
|
if (isOneChartPerQuery) {
|
||||||
const selectLabel = (
|
const selectLabel = (
|
||||||
<Button
|
<Button
|
||||||
type="primary"
|
className="periscope-btn ghost"
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
shape="round"
|
|
||||||
onClick={onAddToDashboard}
|
onClick={onAddToDashboard}
|
||||||
icon={<Plus size={16} />}
|
icon={<Plus size={12} />}
|
||||||
>
|
/>
|
||||||
Add to Dashboard
|
|
||||||
</Button>
|
|
||||||
);
|
);
|
||||||
return (
|
return (
|
||||||
<Select
|
<Select
|
||||||
@@ -771,14 +721,11 @@ function ExplorerOptions({
|
|||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
type="primary"
|
className="periscope-btn ghost"
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
shape="round"
|
|
||||||
onClick={onAddToDashboard}
|
onClick={onAddToDashboard}
|
||||||
icon={<Plus size={16} />}
|
icon={<Plus size={16} />}
|
||||||
>
|
/>
|
||||||
Add to Dashboard
|
|
||||||
</Button>
|
|
||||||
);
|
);
|
||||||
}, [disabled, isOneChartPerQuery, onAddToDashboard, splitedQueries]);
|
}, [disabled, isOneChartPerQuery, onAddToDashboard, splitedQueries]);
|
||||||
|
|
||||||
@@ -797,41 +744,31 @@ function ExplorerOptions({
|
|||||||
>
|
>
|
||||||
<Tooltip title="Clear this view" placement="top">
|
<Tooltip title="Clear this view" placement="top">
|
||||||
<Button
|
<Button
|
||||||
className="action-icon"
|
className="periscope-btn ghost"
|
||||||
onClick={handleClearSelect}
|
onClick={handleClearSelect}
|
||||||
icon={<X size={14} />}
|
icon={<X size={16} />}
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
{
|
{
|
||||||
// only show the update view option when the query is updated
|
// only show the update view option when the query is updated
|
||||||
}
|
}
|
||||||
{isQueryUpdated && (
|
{isQueryUpdated && (
|
||||||
<>
|
<Tooltip title="Update this view" placement="top">
|
||||||
<Divider
|
<Button
|
||||||
type="vertical"
|
className={cx(
|
||||||
className={isEditDeleteSupported ? '' : 'hidden'}
|
'periscope-btn ghost',
|
||||||
|
isEditDeleteSupported ? '' : 'hidden',
|
||||||
|
)}
|
||||||
|
disabled={isViewUpdating}
|
||||||
|
onClick={onUpdateQueryHandler}
|
||||||
|
icon={<Disc3 size={16} />}
|
||||||
/>
|
/>
|
||||||
<Tooltip title="Update this view" placement="top">
|
</Tooltip>
|
||||||
<Button
|
|
||||||
className={cx('action-icon', isEditDeleteSupported ? ' ' : 'hidden')}
|
|
||||||
disabled={isViewUpdating}
|
|
||||||
onClick={onUpdateQueryHandler}
|
|
||||||
icon={<Disc3 size={14} />}
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{!isExplorerOptionHidden && (
|
{!isExplorerOptionHidden && (
|
||||||
<div
|
<div className="explorer-options">
|
||||||
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">
|
<div className="view-options">
|
||||||
<Select<string, { key: string; value: string }>
|
<Select<string, { key: string; value: string }>
|
||||||
showSearch
|
showSearch
|
||||||
@@ -872,49 +809,23 @@ function ExplorerOptions({
|
|||||||
</Select>
|
</Select>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
shape="round"
|
shape="default"
|
||||||
|
className={cx(
|
||||||
|
'periscope-btn secondary',
|
||||||
|
isEditDeleteSupported ? '' : 'hidden',
|
||||||
|
)}
|
||||||
onClick={handleSaveViewModalToggle}
|
onClick={handleSaveViewModalToggle}
|
||||||
className={isEditDeleteSupported ? '' : 'hidden'}
|
|
||||||
disabled={viewsIsLoading || isRefetching}
|
disabled={viewsIsLoading || isRefetching}
|
||||||
icon={<Disc3 size={16} />}
|
icon={<Disc3 size={12} />}
|
||||||
>
|
>
|
||||||
Save this view
|
Save this view
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<hr className={isEditDeleteSupported ? '' : 'hidden'} />
|
|
||||||
|
|
||||||
<div className={cx('actions', isEditDeleteSupported ? '' : 'hidden')}>
|
<div className={cx('actions', isEditDeleteSupported ? '' : 'hidden')}>
|
||||||
{alertButton}
|
{alertButton}
|
||||||
{dashboardButton}
|
{dashboardButton}
|
||||||
</div>
|
</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>
|
</div>
|
||||||
)}
|
)}
|
||||||
<ExplorerOptionsHideArea
|
<ExplorerOptionsHideArea
|
||||||
|
|||||||