Compare commits
182 Commits
v0.86.2-64
...
fix/query-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b0a2f64ebe | ||
|
|
e0582f6edb | ||
|
|
7c05113d8e | ||
|
|
de366c7ef6 | ||
|
|
8c5f56abd7 | ||
|
|
5820b0ba46 | ||
|
|
1e2dd240a5 | ||
|
|
b79ff25682 | ||
|
|
6d1d48e156 | ||
|
|
62c56d2150 | ||
|
|
aa544f52f3 | ||
|
|
996080aaf8 | ||
|
|
200b714306 | ||
|
|
467e8ff288 | ||
|
|
2dae184976 | ||
|
|
af7f1def55 | ||
|
|
f30d95fd5f | ||
|
|
78b4f2c698 | ||
|
|
54c1874cc2 | ||
|
|
b01f95452f | ||
|
|
04d49fceef | ||
|
|
82851e79db | ||
|
|
b1da482b2c | ||
|
|
0c9f06850a | ||
|
|
7e18087db6 | ||
|
|
b8414ad715 | ||
|
|
43f8c2dce6 | ||
|
|
50849815d5 | ||
|
|
3a38e3fff6 | ||
|
|
69d1ab3813 | ||
|
|
dc8ef8fa06 | ||
|
|
6c69dc4a0b | ||
|
|
7254772e70 | ||
|
|
3e72d3fd02 | ||
|
|
656d9a11ad | ||
|
|
6afd9258d2 | ||
|
|
d140475a80 | ||
|
|
99f5acff1d | ||
|
|
8669d94e9c | ||
|
|
5eaba94528 | ||
|
|
4a4400170f | ||
|
|
6afea4e075 | ||
|
|
b5513a2e8a | ||
|
|
307710fc5a | ||
|
|
9d94fd31b6 | ||
|
|
66e8f00749 | ||
|
|
5f28e707d1 | ||
|
|
b6f6a31ab5 | ||
|
|
f9c16b79d5 | ||
|
|
b6322fe417 | ||
|
|
8b48b955c0 | ||
|
|
a3f5f57756 | ||
|
|
cf73451020 | ||
|
|
ece8976dce | ||
|
|
d94f3a4f34 | ||
|
|
6e6e57c243 | ||
|
|
2c54354427 | ||
|
|
4c93597b6f | ||
|
|
165c14d350 | ||
|
|
bee36ee928 | ||
|
|
1eca60e9c4 | ||
|
|
2e6a6ea286 | ||
|
|
872f887646 | ||
|
|
b182bf8199 | ||
|
|
7ae62936ab | ||
|
|
0f38114b75 | ||
|
|
5aa4ae1261 | ||
|
|
a43cca9460 | ||
|
|
619146699b | ||
|
|
fcadb89f55 | ||
|
|
c43ddbc5a2 | ||
|
|
aeb0cc850f | ||
|
|
91df27861f | ||
|
|
789692953b | ||
|
|
ba2ed3ad22 | ||
|
|
eb3dfbf63b | ||
|
|
c3e048470d | ||
|
|
4563ff0e62 | ||
|
|
c9e48b6de9 | ||
|
|
06ef9ff384 | ||
|
|
26d55875f5 | ||
|
|
b1864ee328 | ||
|
|
8b62c8dced | ||
|
|
273452352d | ||
|
|
8274ebfe37 | ||
|
|
7d5e14abb6 | ||
|
|
7c17ac42b1 | ||
|
|
74ee7bb2c7 | ||
|
|
2f5640b2e6 | ||
|
|
121debcecc | ||
|
|
ff13504a74 | ||
|
|
d4e373443b | ||
|
|
3ccf822d67 | ||
|
|
0e270e6f51 | ||
|
|
749df2a979 | ||
|
|
9ee5d5d599 | ||
|
|
4940dfd46f | ||
|
|
79a31cc205 | ||
|
|
5102cf2b7b | ||
|
|
9ec5594648 | ||
|
|
b6c2ebd6d7 | ||
|
|
9a3a8c8305 | ||
|
|
2ac45b0174 | ||
|
|
2a53918ebd | ||
|
|
9daefeb881 | ||
|
|
526cf01cb7 | ||
|
|
cd4766ec2b | ||
|
|
2196b58d36 | ||
|
|
53c58b9983 | ||
|
|
d174038dce | ||
|
|
78d09e2940 | ||
|
|
6cb7f152e1 | ||
|
|
f6730d3d09 | ||
|
|
899a6ab70a | ||
|
|
a4b852bb99 | ||
|
|
92cd108c0d | ||
|
|
34c116fc7e | ||
|
|
250646a354 | ||
|
|
00191d5774 | ||
|
|
525a0d7a1a | ||
|
|
564edc7430 | ||
|
|
78f396b94a | ||
|
|
9e53c150b8 | ||
|
|
f80a6c3014 | ||
|
|
1eff6d82c9 | ||
|
|
f138eff26c | ||
|
|
50f3fc0ff9 | ||
|
|
ebcb172614 | ||
|
|
133c0deaa8 | ||
|
|
35e8165463 | ||
|
|
6d009c6607 | ||
|
|
f0994e52c0 | ||
|
|
7f5b388722 | ||
|
|
b11a4c0c21 | ||
|
|
bbb21f608f | ||
|
|
50a5b88708 | ||
|
|
5601c0886d | ||
|
|
5b342b9b5d | ||
|
|
7ec59c3c77 | ||
|
|
a12990f0bd | ||
|
|
1ee1ca7951 | ||
|
|
3b1bf34d3e | ||
|
|
fbcff29fae | ||
|
|
81fcca3bd3 | ||
|
|
4f7d84aa37 | ||
|
|
8f8dedb8b3 | ||
|
|
3f65229506 | ||
|
|
f006260719 | ||
|
|
3fc8f6c353 | ||
|
|
e02ae9a5c4 | ||
|
|
1989d07e52 | ||
|
|
78194ae955 | ||
|
|
da1b6d1ed0 | ||
|
|
d3c76ae8be | ||
|
|
bed3dbc698 | ||
|
|
66affb0ece | ||
|
|
75f62372ae | ||
|
|
a3ac307b4e | ||
|
|
7672d2f636 | ||
|
|
e3018d9529 | ||
|
|
385ee268e3 | ||
|
|
01036a8a2f | ||
|
|
1542b9d6e9 | ||
|
|
8455349459 | ||
|
|
c488a24d09 | ||
|
|
9091cf61fd | ||
|
|
eeb2ab3212 | ||
|
|
3f128f0f1d | ||
|
|
59ff7ed1e1 | ||
|
|
d236b6ce1e | ||
|
|
44b118a212 | ||
|
|
3fc6f7ee63 | ||
|
|
f1016baf03 | ||
|
|
e5c0d9e44a | ||
|
|
e51056c804 | ||
|
|
7d8dad4550 | ||
|
|
c477e0ef16 | ||
|
|
fff7f8fc76 | ||
|
|
8cfeef4521 | ||
|
|
d85a1a21ac | ||
|
|
17f48d656d | ||
|
|
2d6774da68 |
@@ -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.111.42
|
image: signoz/signoz-schema-migrator:v0.128.0
|
||||||
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.111.42
|
image: signoz/signoz-schema-migrator:v0.128.0
|
||||||
container_name: schema-migrator-async
|
container_name: schema-migrator-async
|
||||||
command:
|
command:
|
||||||
- async
|
- async
|
||||||
|
|||||||
7
.github/CODEOWNERS
vendored
7
.github/CODEOWNERS
vendored
@@ -12,4 +12,9 @@
|
|||||||
/pkg/factory/ @grandwizard28
|
/pkg/factory/ @grandwizard28
|
||||||
/pkg/types/ @grandwizard28
|
/pkg/types/ @grandwizard28
|
||||||
.golangci.yml @grandwizard28
|
.golangci.yml @grandwizard28
|
||||||
**/(zeus|licensing|sqlmigration)/ @vikrantgupta25
|
/pkg/zeus/ @vikrantgupta25
|
||||||
|
/pkg/licensing/ @vikrantgupta25
|
||||||
|
/pkg/sqlmigration/ @vikrantgupta25
|
||||||
|
/ee/zeus/ @vikrantgupta25
|
||||||
|
/ee/licensing/ @vikrantgupta25
|
||||||
|
/ee/sqlmigration/ @vikrantgupta25
|
||||||
2
.github/workflows/integrationci.yaml
vendored
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.111.38
|
- v0.128.0
|
||||||
postgres-version:
|
postgres-version:
|
||||||
- 15
|
- 15
|
||||||
if: |
|
if: |
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ linters:
|
|||||||
- sloglint
|
- sloglint
|
||||||
- depguard
|
- depguard
|
||||||
- iface
|
- iface
|
||||||
|
- unparam
|
||||||
|
|
||||||
linters-settings:
|
linters-settings:
|
||||||
sloglint:
|
sloglint:
|
||||||
|
|||||||
@@ -90,6 +90,15 @@ apiserver:
|
|||||||
- /api/v1/version
|
- /api/v1/version
|
||||||
- /
|
- /
|
||||||
|
|
||||||
|
##################### Querier #####################
|
||||||
|
querier:
|
||||||
|
# The TTL for cached query results.
|
||||||
|
cache_ttl: 168h
|
||||||
|
# The interval for recent data that should not be cached.
|
||||||
|
flux_interval: 5m
|
||||||
|
# The maximum number of concurrent queries for missing ranges.
|
||||||
|
max_concurrent_queries: 4
|
||||||
|
|
||||||
##################### TelemetryStore #####################
|
##################### TelemetryStore #####################
|
||||||
telemetrystore:
|
telemetrystore:
|
||||||
# Maximum number of idle connections in the connection pool.
|
# Maximum number of idle connections in the connection pool.
|
||||||
@@ -103,13 +112,15 @@ telemetrystore:
|
|||||||
clickhouse:
|
clickhouse:
|
||||||
# The DSN to use for clickhouse.
|
# The DSN to use for clickhouse.
|
||||||
dsn: tcp://localhost:9000
|
dsn: tcp://localhost:9000
|
||||||
|
# The cluster name to use for clickhouse.
|
||||||
|
cluster: cluster
|
||||||
# The query settings for clickhouse.
|
# The query settings for clickhouse.
|
||||||
settings:
|
settings:
|
||||||
max_execution_time: 0
|
max_execution_time: 0
|
||||||
max_execution_time_leaf: 0
|
max_execution_time_leaf: 0
|
||||||
timeout_before_checking_execution_speed: 0
|
timeout_before_checking_execution_speed: 0
|
||||||
max_bytes_to_read: 0
|
max_bytes_to_read: 0
|
||||||
max_result_rows_for_ch_query: 0
|
max_result_rows: 0
|
||||||
|
|
||||||
##################### Prometheus #####################
|
##################### Prometheus #####################
|
||||||
prometheus:
|
prometheus:
|
||||||
@@ -224,3 +235,12 @@ statsreporter:
|
|||||||
enabled: true
|
enabled: true
|
||||||
# The interval at which the stats are collected.
|
# The interval at which the stats are collected.
|
||||||
interval: 6h
|
interval: 6h
|
||||||
|
collect:
|
||||||
|
# Whether to collect identities and traits (emails).
|
||||||
|
identities: true
|
||||||
|
|
||||||
|
|
||||||
|
##################### Gateway (License only) #####################
|
||||||
|
gateway:
|
||||||
|
# The URL of the gateway's api.
|
||||||
|
url: http://localhost:8080
|
||||||
|
|||||||
@@ -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.86.2
|
image: signoz/signoz:v0.88.1
|
||||||
command:
|
command:
|
||||||
- --config=/root/config/prometheus.yml
|
- --config=/root/config/prometheus.yml
|
||||||
ports:
|
ports:
|
||||||
@@ -194,6 +194,7 @@ 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
|
||||||
@@ -206,7 +207,7 @@ services:
|
|||||||
retries: 3
|
retries: 3
|
||||||
otel-collector:
|
otel-collector:
|
||||||
!!merge <<: *db-depend
|
!!merge <<: *db-depend
|
||||||
image: signoz/signoz-otel-collector:v0.111.42
|
image: signoz/signoz-otel-collector:v0.128.0
|
||||||
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
|
||||||
@@ -230,7 +231,7 @@ services:
|
|||||||
- signoz
|
- signoz
|
||||||
schema-migrator:
|
schema-migrator:
|
||||||
!!merge <<: *common
|
!!merge <<: *common
|
||||||
image: signoz/signoz-schema-migrator:v0.111.42
|
image: signoz/signoz-schema-migrator:v0.128.0
|
||||||
deploy:
|
deploy:
|
||||||
restart_policy:
|
restart_policy:
|
||||||
condition: on-failure
|
condition: on-failure
|
||||||
|
|||||||
@@ -100,26 +100,32 @@ services:
|
|||||||
# - "9000:9000"
|
# - "9000:9000"
|
||||||
# - "8123:8123"
|
# - "8123:8123"
|
||||||
# - "9181:9181"
|
# - "9181:9181"
|
||||||
|
|
||||||
|
configs:
|
||||||
|
- source: clickhouse-config
|
||||||
|
target: /etc/clickhouse-server/config.xml
|
||||||
|
- source: clickhouse-users
|
||||||
|
target: /etc/clickhouse-server/users.xml
|
||||||
|
- source: clickhouse-custom-function
|
||||||
|
target: /etc/clickhouse-server/custom-function.xml
|
||||||
|
- source: clickhouse-cluster
|
||||||
|
target: /etc/clickhouse-server/config.d/cluster.xml
|
||||||
volumes:
|
volumes:
|
||||||
- ../common/clickhouse/config.xml:/etc/clickhouse-server/config.xml
|
|
||||||
- ../common/clickhouse/users.xml:/etc/clickhouse-server/users.xml
|
|
||||||
- ../common/clickhouse/custom-function.xml:/etc/clickhouse-server/custom-function.xml
|
|
||||||
- ../common/clickhouse/user_scripts:/var/lib/clickhouse/user_scripts/
|
|
||||||
- ../common/clickhouse/cluster.xml:/etc/clickhouse-server/config.d/cluster.xml
|
|
||||||
- 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.86.2
|
image: signoz/signoz:v0.88.1
|
||||||
command:
|
command:
|
||||||
- --config=/root/config/prometheus.yml
|
- --config=/root/config/prometheus.yml
|
||||||
ports:
|
ports:
|
||||||
- "8080:8080" # signoz port
|
- "8080:8080" # signoz port
|
||||||
# - "6060:6060" # pprof port
|
# - "6060:6060" # pprof port
|
||||||
volumes:
|
volumes:
|
||||||
- ../common/signoz/prometheus.yml:/root/config/prometheus.yml
|
|
||||||
- ../common/dashboards:/root/config/dashboards
|
|
||||||
- sqlite:/var/lib/signoz/
|
- sqlite:/var/lib/signoz/
|
||||||
|
configs:
|
||||||
|
- source: signoz-prometheus-config
|
||||||
|
target: /root/config/prometheus.yml
|
||||||
environment:
|
environment:
|
||||||
- SIGNOZ_ALERTMANAGER_PROVIDER=signoz
|
- SIGNOZ_ALERTMANAGER_PROVIDER=signoz
|
||||||
- SIGNOZ_TELEMETRYSTORE_CLICKHOUSE_DSN=tcp://clickhouse:9000
|
- SIGNOZ_TELEMETRYSTORE_CLICKHOUSE_DSN=tcp://clickhouse:9000
|
||||||
@@ -129,6 +135,7 @@ 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
|
||||||
@@ -141,15 +148,17 @@ services:
|
|||||||
retries: 3
|
retries: 3
|
||||||
otel-collector:
|
otel-collector:
|
||||||
!!merge <<: *db-depend
|
!!merge <<: *db-depend
|
||||||
image: signoz/signoz-otel-collector:v0.111.42
|
image: signoz/signoz-otel-collector:v0.128.0
|
||||||
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
|
||||||
- --copy-path=/var/tmp/collector-config.yaml
|
- --copy-path=/var/tmp/collector-config.yaml
|
||||||
- --feature-gates=-pkg.translator.prometheus.NormalizeName
|
- --feature-gates=-pkg.translator.prometheus.NormalizeName
|
||||||
volumes:
|
configs:
|
||||||
- ./otel-collector-config.yaml:/etc/otel-collector-config.yaml
|
- source: otel-collector-config
|
||||||
- ../common/signoz/otel-collector-opamp-config.yaml:/etc/manager-config.yaml
|
target: /etc/otel-collector-config.yaml
|
||||||
|
- source: otel-manager-config
|
||||||
|
target: /etc/manager-config.yaml
|
||||||
environment:
|
environment:
|
||||||
- OTEL_RESOURCE_ATTRIBUTES=host.name={{.Node.Hostname}},os.type={{.Node.Platform.OS}}
|
- OTEL_RESOURCE_ATTRIBUTES=host.name={{.Node.Hostname}},os.type={{.Node.Platform.OS}}
|
||||||
- LOW_CARDINAL_EXCEPTION_GROUPING=false
|
- LOW_CARDINAL_EXCEPTION_GROUPING=false
|
||||||
@@ -165,7 +174,7 @@ services:
|
|||||||
- signoz
|
- signoz
|
||||||
schema-migrator:
|
schema-migrator:
|
||||||
!!merge <<: *common
|
!!merge <<: *common
|
||||||
image: signoz/signoz-schema-migrator:v0.111.42
|
image: signoz/signoz-schema-migrator:v0.128.0
|
||||||
deploy:
|
deploy:
|
||||||
restart_policy:
|
restart_policy:
|
||||||
condition: on-failure
|
condition: on-failure
|
||||||
@@ -186,3 +195,24 @@ volumes:
|
|||||||
name: signoz-sqlite
|
name: signoz-sqlite
|
||||||
zookeeper-1:
|
zookeeper-1:
|
||||||
name: signoz-zookeeper-1
|
name: signoz-zookeeper-1
|
||||||
|
configs:
|
||||||
|
clickhouse-config:
|
||||||
|
file: ../common/clickhouse/config.xml
|
||||||
|
clickhouse-users:
|
||||||
|
file: ../common/clickhouse/users.xml
|
||||||
|
clickhouse-custom-function:
|
||||||
|
file: ../common/clickhouse/custom-function.xml
|
||||||
|
clickhouse-cluster:
|
||||||
|
file: ../common/clickhouse/cluster.xml
|
||||||
|
signoz-prometheus-config:
|
||||||
|
file: ../common/signoz/prometheus.yml
|
||||||
|
# If you have multiple dashboard files, you can list them individually:
|
||||||
|
# dashboard-foo:
|
||||||
|
# file: ../common/dashboards/foo.json
|
||||||
|
# dashboard-bar:
|
||||||
|
# file: ../common/dashboards/bar.json
|
||||||
|
|
||||||
|
otel-collector-config:
|
||||||
|
file: ./otel-collector-config.yaml
|
||||||
|
otel-manager-config:
|
||||||
|
file: ../common/signoz/otel-collector-opamp-config.yaml
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ processors:
|
|||||||
detectors: [env, system]
|
detectors: [env, system]
|
||||||
timeout: 2s
|
timeout: 2s
|
||||||
signozspanmetrics/delta:
|
signozspanmetrics/delta:
|
||||||
metrics_exporter: clickhousemetricswrite, signozclickhousemetrics
|
metrics_exporter: 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,27 +60,16 @@ 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
|
||||||
@@ -92,11 +81,11 @@ service:
|
|||||||
metrics:
|
metrics:
|
||||||
receivers: [otlp]
|
receivers: [otlp]
|
||||||
processors: [batch]
|
processors: [batch]
|
||||||
exporters: [clickhousemetricswrite, signozclickhousemetrics]
|
exporters: [signozclickhousemetrics]
|
||||||
metrics/prometheus:
|
metrics/prometheus:
|
||||||
receivers: [prometheus]
|
receivers: [prometheus]
|
||||||
processors: [batch]
|
processors: [batch]
|
||||||
exporters: [clickhousemetricswrite/prometheus, signozclickhousemetrics]
|
exporters: [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.86.2}
|
image: signoz/signoz:${VERSION:-v0.88.1}
|
||||||
container_name: signoz
|
container_name: signoz
|
||||||
command:
|
command:
|
||||||
- --config=/root/config/prometheus.yml
|
- --config=/root/config/prometheus.yml
|
||||||
@@ -197,6 +197,7 @@ 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
|
||||||
@@ -210,7 +211,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.111.42}
|
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-v0.128.0}
|
||||||
container_name: signoz-otel-collector
|
container_name: signoz-otel-collector
|
||||||
command:
|
command:
|
||||||
- --config=/etc/otel-collector-config.yaml
|
- --config=/etc/otel-collector-config.yaml
|
||||||
@@ -236,7 +237,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.111.42}
|
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-v0.128.0}
|
||||||
container_name: schema-migrator-sync
|
container_name: schema-migrator-sync
|
||||||
command:
|
command:
|
||||||
- sync
|
- sync
|
||||||
@@ -247,7 +248,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.111.42}
|
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-v0.128.0}
|
||||||
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.86.2}
|
image: signoz/signoz:${VERSION:-v0.88.1}
|
||||||
container_name: signoz
|
container_name: signoz
|
||||||
command:
|
command:
|
||||||
- --config=/root/config/prometheus.yml
|
- --config=/root/config/prometheus.yml
|
||||||
@@ -130,6 +130,7 @@ 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
|
||||||
@@ -142,7 +143,7 @@ services:
|
|||||||
retries: 3
|
retries: 3
|
||||||
otel-collector:
|
otel-collector:
|
||||||
!!merge <<: *db-depend
|
!!merge <<: *db-depend
|
||||||
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-v0.111.42}
|
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-v0.128.0}
|
||||||
container_name: signoz-otel-collector
|
container_name: signoz-otel-collector
|
||||||
command:
|
command:
|
||||||
- --config=/etc/otel-collector-config.yaml
|
- --config=/etc/otel-collector-config.yaml
|
||||||
@@ -164,7 +165,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.111.42}
|
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-v0.128.0}
|
||||||
container_name: schema-migrator-sync
|
container_name: schema-migrator-sync
|
||||||
command:
|
command:
|
||||||
- sync
|
- sync
|
||||||
@@ -176,7 +177,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.111.42}
|
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-v0.128.0}
|
||||||
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: clickhousemetricswrite, signozclickhousemetrics
|
metrics_exporter: 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,27 +60,16 @@ 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
|
||||||
@@ -92,11 +81,11 @@ service:
|
|||||||
metrics:
|
metrics:
|
||||||
receivers: [otlp]
|
receivers: [otlp]
|
||||||
processors: [batch]
|
processors: [batch]
|
||||||
exporters: [clickhousemetricswrite, signozclickhousemetrics]
|
exporters: [signozclickhousemetrics]
|
||||||
metrics/prometheus:
|
metrics/prometheus:
|
||||||
receivers: [prometheus]
|
receivers: [prometheus]
|
||||||
processors: [batch]
|
processors: [batch]
|
||||||
exporters: [clickhousemetricswrite/prometheus, signozclickhousemetrics]
|
exporters: [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)
|
||||||
- [Moniitor with SigNoz (Kubernetes)](#monitor-with-signoz-kubernetes)
|
- [Monitor with SigNoz (Kubernetes)](#monitor-with-signoz-kubernetes)
|
||||||
- [What's next](#whats-next)
|
- [What's next](#whats-next)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -6,11 +6,13 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/SigNoz/signoz/ee/licensing/licensingstore/sqllicensingstore"
|
"github.com/SigNoz/signoz/ee/licensing/licensingstore/sqllicensingstore"
|
||||||
|
"github.com/SigNoz/signoz/pkg/analytics"
|
||||||
"github.com/SigNoz/signoz/pkg/errors"
|
"github.com/SigNoz/signoz/pkg/errors"
|
||||||
"github.com/SigNoz/signoz/pkg/factory"
|
"github.com/SigNoz/signoz/pkg/factory"
|
||||||
"github.com/SigNoz/signoz/pkg/licensing"
|
"github.com/SigNoz/signoz/pkg/licensing"
|
||||||
"github.com/SigNoz/signoz/pkg/modules/organization"
|
"github.com/SigNoz/signoz/pkg/modules/organization"
|
||||||
"github.com/SigNoz/signoz/pkg/sqlstore"
|
"github.com/SigNoz/signoz/pkg/sqlstore"
|
||||||
|
"github.com/SigNoz/signoz/pkg/types/analyticstypes"
|
||||||
"github.com/SigNoz/signoz/pkg/types/licensetypes"
|
"github.com/SigNoz/signoz/pkg/types/licensetypes"
|
||||||
"github.com/SigNoz/signoz/pkg/valuer"
|
"github.com/SigNoz/signoz/pkg/valuer"
|
||||||
"github.com/SigNoz/signoz/pkg/zeus"
|
"github.com/SigNoz/signoz/pkg/zeus"
|
||||||
@@ -23,16 +25,17 @@ type provider struct {
|
|||||||
config licensing.Config
|
config licensing.Config
|
||||||
settings factory.ScopedProviderSettings
|
settings factory.ScopedProviderSettings
|
||||||
orgGetter organization.Getter
|
orgGetter organization.Getter
|
||||||
|
analytics analytics.Analytics
|
||||||
stopChan chan struct{}
|
stopChan chan struct{}
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewProviderFactory(store sqlstore.SQLStore, zeus zeus.Zeus, orgGetter organization.Getter) factory.ProviderFactory[licensing.Licensing, licensing.Config] {
|
func NewProviderFactory(store sqlstore.SQLStore, zeus zeus.Zeus, orgGetter organization.Getter, analytics analytics.Analytics) factory.ProviderFactory[licensing.Licensing, licensing.Config] {
|
||||||
return factory.NewProviderFactory(factory.MustNewName("http"), func(ctx context.Context, providerSettings factory.ProviderSettings, config licensing.Config) (licensing.Licensing, error) {
|
return factory.NewProviderFactory(factory.MustNewName("http"), func(ctx context.Context, providerSettings factory.ProviderSettings, config licensing.Config) (licensing.Licensing, error) {
|
||||||
return New(ctx, providerSettings, config, store, zeus, orgGetter)
|
return New(ctx, providerSettings, config, store, zeus, orgGetter, analytics)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(ctx context.Context, ps factory.ProviderSettings, config licensing.Config, sqlstore sqlstore.SQLStore, zeus zeus.Zeus, orgGetter organization.Getter) (licensing.Licensing, error) {
|
func New(ctx context.Context, ps factory.ProviderSettings, config licensing.Config, sqlstore sqlstore.SQLStore, zeus zeus.Zeus, orgGetter organization.Getter, analytics analytics.Analytics) (licensing.Licensing, error) {
|
||||||
settings := factory.NewScopedProviderSettings(ps, "github.com/SigNoz/signoz/ee/licensing/httplicensing")
|
settings := factory.NewScopedProviderSettings(ps, "github.com/SigNoz/signoz/ee/licensing/httplicensing")
|
||||||
licensestore := sqllicensingstore.New(sqlstore)
|
licensestore := sqllicensingstore.New(sqlstore)
|
||||||
return &provider{
|
return &provider{
|
||||||
@@ -42,6 +45,7 @@ func New(ctx context.Context, ps factory.ProviderSettings, config licensing.Conf
|
|||||||
settings: settings,
|
settings: settings,
|
||||||
orgGetter: orgGetter,
|
orgGetter: orgGetter,
|
||||||
stopChan: make(chan struct{}),
|
stopChan: make(chan struct{}),
|
||||||
|
analytics: analytics,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -159,6 +163,25 @@ func (provider *provider) Refresh(ctx context.Context, organizationID valuer.UUI
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
stats := licensetypes.NewStatsFromLicense(activeLicense)
|
||||||
|
provider.analytics.Send(ctx,
|
||||||
|
analyticstypes.Track{
|
||||||
|
UserId: "stats_" + organizationID.String(),
|
||||||
|
Event: "License Updated",
|
||||||
|
Properties: analyticstypes.NewPropertiesFromMap(stats),
|
||||||
|
Context: &analyticstypes.Context{
|
||||||
|
Extra: map[string]interface{}{
|
||||||
|
analyticstypes.KeyGroupID: organizationID.String(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
analyticstypes.Group{
|
||||||
|
UserId: "stats_" + organizationID.String(),
|
||||||
|
GroupId: organizationID.String(),
|
||||||
|
Traits: analyticstypes.NewTraitsFromMap(stats),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,11 +11,9 @@ RUN apk update && \
|
|||||||
|
|
||||||
|
|
||||||
COPY ./target/${OS}-${TARGETARCH}/signoz /root/signoz
|
COPY ./target/${OS}-${TARGETARCH}/signoz /root/signoz
|
||||||
COPY ./conf/prometheus.yml /root/config/prometheus.yml
|
|
||||||
COPY ./templates/email /root/templates
|
COPY ./templates/email /root/templates
|
||||||
COPY frontend/build/ /etc/signoz/web/
|
COPY frontend/build/ /etc/signoz/web/
|
||||||
|
|
||||||
RUN chmod 755 /root /root/signoz
|
RUN chmod 755 /root /root/signoz
|
||||||
|
|
||||||
ENTRYPOINT ["./signoz"]
|
ENTRYPOINT ["./signoz"]
|
||||||
CMD ["-config", "/root/config/prometheus.yml"]
|
|
||||||
@@ -12,11 +12,9 @@ RUN apk update && \
|
|||||||
rm -rf /var/cache/apk/*
|
rm -rf /var/cache/apk/*
|
||||||
|
|
||||||
COPY ./target/${OS}-${ARCH}/signoz /root/signoz
|
COPY ./target/${OS}-${ARCH}/signoz /root/signoz
|
||||||
COPY ./conf/prometheus.yml /root/config/prometheus.yml
|
|
||||||
COPY ./templates/email /root/templates
|
COPY ./templates/email /root/templates
|
||||||
COPY frontend/build/ /etc/signoz/web/
|
COPY frontend/build/ /etc/signoz/web/
|
||||||
|
|
||||||
RUN chmod 755 /root /root/signoz
|
RUN chmod 755 /root /root/signoz
|
||||||
|
|
||||||
ENTRYPOINT ["./signoz"]
|
ENTRYPOINT ["./signoz"]
|
||||||
CMD ["-config", "/root/config/prometheus.yml"]
|
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import (
|
|||||||
|
|
||||||
"github.com/SigNoz/signoz/ee/licensing/httplicensing"
|
"github.com/SigNoz/signoz/ee/licensing/httplicensing"
|
||||||
"github.com/SigNoz/signoz/ee/query-service/integrations/gateway"
|
"github.com/SigNoz/signoz/ee/query-service/integrations/gateway"
|
||||||
"github.com/SigNoz/signoz/ee/query-service/interfaces"
|
|
||||||
"github.com/SigNoz/signoz/ee/query-service/usage"
|
"github.com/SigNoz/signoz/ee/query-service/usage"
|
||||||
"github.com/SigNoz/signoz/pkg/alertmanager"
|
"github.com/SigNoz/signoz/pkg/alertmanager"
|
||||||
"github.com/SigNoz/signoz/pkg/apis/fields"
|
"github.com/SigNoz/signoz/pkg/apis/fields"
|
||||||
@@ -17,6 +16,7 @@ import (
|
|||||||
"github.com/SigNoz/signoz/pkg/query-service/app/cloudintegrations"
|
"github.com/SigNoz/signoz/pkg/query-service/app/cloudintegrations"
|
||||||
"github.com/SigNoz/signoz/pkg/query-service/app/integrations"
|
"github.com/SigNoz/signoz/pkg/query-service/app/integrations"
|
||||||
"github.com/SigNoz/signoz/pkg/query-service/app/logparsingpipeline"
|
"github.com/SigNoz/signoz/pkg/query-service/app/logparsingpipeline"
|
||||||
|
"github.com/SigNoz/signoz/pkg/query-service/interfaces"
|
||||||
basemodel "github.com/SigNoz/signoz/pkg/query-service/model"
|
basemodel "github.com/SigNoz/signoz/pkg/query-service/model"
|
||||||
rules "github.com/SigNoz/signoz/pkg/query-service/rules"
|
rules "github.com/SigNoz/signoz/pkg/query-service/rules"
|
||||||
"github.com/SigNoz/signoz/pkg/signoz"
|
"github.com/SigNoz/signoz/pkg/signoz"
|
||||||
@@ -26,8 +26,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type APIHandlerOptions struct {
|
type APIHandlerOptions struct {
|
||||||
DataConnector interfaces.DataConnector
|
DataConnector interfaces.Reader
|
||||||
PreferSpanMetrics bool
|
|
||||||
RulesManager *rules.Manager
|
RulesManager *rules.Manager
|
||||||
UsageManager *usage.Manager
|
UsageManager *usage.Manager
|
||||||
IntegrationsController *integrations.Controller
|
IntegrationsController *integrations.Controller
|
||||||
@@ -51,7 +50,6 @@ type APIHandler struct {
|
|||||||
func NewAPIHandler(opts APIHandlerOptions, signoz *signoz.SigNoz) (*APIHandler, error) {
|
func NewAPIHandler(opts APIHandlerOptions, signoz *signoz.SigNoz) (*APIHandler, error) {
|
||||||
baseHandler, err := baseapp.NewAPIHandler(baseapp.APIHandlerOpts{
|
baseHandler, err := baseapp.NewAPIHandler(baseapp.APIHandlerOpts{
|
||||||
Reader: opts.DataConnector,
|
Reader: opts.DataConnector,
|
||||||
PreferSpanMetrics: opts.PreferSpanMetrics,
|
|
||||||
RuleManager: opts.RulesManager,
|
RuleManager: opts.RulesManager,
|
||||||
IntegrationsController: opts.IntegrationsController,
|
IntegrationsController: opts.IntegrationsController,
|
||||||
CloudIntegrationsController: opts.CloudIntegrationsController,
|
CloudIntegrationsController: opts.CloudIntegrationsController,
|
||||||
@@ -61,7 +59,7 @@ func NewAPIHandler(opts APIHandlerOptions, signoz *signoz.SigNoz) (*APIHandler,
|
|||||||
LicensingAPI: httplicensing.NewLicensingAPI(signoz.Licensing),
|
LicensingAPI: httplicensing.NewLicensingAPI(signoz.Licensing),
|
||||||
FieldsAPI: fields.NewAPI(signoz.Instrumentation.ToProviderSettings(), signoz.TelemetryStore),
|
FieldsAPI: fields.NewAPI(signoz.Instrumentation.ToProviderSettings(), signoz.TelemetryStore),
|
||||||
Signoz: signoz,
|
Signoz: signoz,
|
||||||
QuerierAPI: querierAPI.NewAPI(signoz.Querier),
|
QuerierAPI: querierAPI.NewAPI(signoz.Instrumentation.ToProviderSettings(), signoz.Querier),
|
||||||
})
|
})
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -96,7 +96,7 @@ func (ah *APIHandler) receiveSAML(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
nextPage, err := ah.Signoz.Modules.User.PrepareSsoRedirect(ctx, redirectUri, email, ah.opts.JWT)
|
nextPage, err := ah.Signoz.Modules.User.PrepareSsoRedirect(ctx, redirectUri, email)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
zap.L().Error("[receiveSAML] failed to generate redirect URI after successful login ", zap.String("domain", domain.String()), zap.Error(err))
|
zap.L().Error("[receiveSAML] failed to generate redirect URI after successful login ", zap.String("domain", domain.String()), zap.Error(err))
|
||||||
handleSsoError(w, r, redirectUri)
|
handleSsoError(w, r, redirectUri)
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ func (ah *APIHandler) getFeatureFlags(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if ah.opts.PreferSpanMetrics {
|
if constants.IsPreferSpanMetrics {
|
||||||
for idx, feature := range featureSet {
|
for idx, feature := range featureSet {
|
||||||
if feature.Name == licensetypes.UseSpanMetrics {
|
if feature.Name == licensetypes.UseSpanMetrics {
|
||||||
featureSet[idx].Active = true
|
featureSet[idx].Active = true
|
||||||
|
|||||||
@@ -1,39 +0,0 @@
|
|||||||
package db
|
|
||||||
|
|
||||||
import (
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/ClickHouse/clickhouse-go/v2"
|
|
||||||
|
|
||||||
"github.com/SigNoz/signoz/pkg/cache"
|
|
||||||
"github.com/SigNoz/signoz/pkg/prometheus"
|
|
||||||
basechr "github.com/SigNoz/signoz/pkg/query-service/app/clickhouseReader"
|
|
||||||
"github.com/SigNoz/signoz/pkg/sqlstore"
|
|
||||||
"github.com/SigNoz/signoz/pkg/telemetrystore"
|
|
||||||
)
|
|
||||||
|
|
||||||
type ClickhouseReader struct {
|
|
||||||
conn clickhouse.Conn
|
|
||||||
appdb sqlstore.SQLStore
|
|
||||||
*basechr.ClickHouseReader
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewDataConnector(
|
|
||||||
sqlDB sqlstore.SQLStore,
|
|
||||||
telemetryStore telemetrystore.TelemetryStore,
|
|
||||||
prometheus prometheus.Prometheus,
|
|
||||||
cluster string,
|
|
||||||
fluxIntervalForTraceDetail time.Duration,
|
|
||||||
cache cache.Cache,
|
|
||||||
) *ClickhouseReader {
|
|
||||||
chReader := basechr.NewReader(sqlDB, telemetryStore, prometheus, cluster, fluxIntervalForTraceDetail, cache)
|
|
||||||
return &ClickhouseReader{
|
|
||||||
conn: telemetryStore.ClickhouseDB(),
|
|
||||||
appdb: sqlDB,
|
|
||||||
ClickHouseReader: chReader,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *ClickhouseReader) GetSQLStore() sqlstore.SQLStore {
|
|
||||||
return r.appdb
|
|
||||||
}
|
|
||||||
@@ -6,14 +6,10 @@ import (
|
|||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
_ "net/http/pprof" // http profiler
|
_ "net/http/pprof" // http profiler
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/gorilla/handlers"
|
"github.com/gorilla/handlers"
|
||||||
"github.com/jmoiron/sqlx"
|
|
||||||
|
|
||||||
"github.com/SigNoz/signoz/ee/query-service/app/api"
|
"github.com/SigNoz/signoz/ee/query-service/app/api"
|
||||||
"github.com/SigNoz/signoz/ee/query-service/app/db"
|
|
||||||
"github.com/SigNoz/signoz/ee/query-service/constants"
|
|
||||||
"github.com/SigNoz/signoz/ee/query-service/integrations/gateway"
|
"github.com/SigNoz/signoz/ee/query-service/integrations/gateway"
|
||||||
"github.com/SigNoz/signoz/ee/query-service/rules"
|
"github.com/SigNoz/signoz/ee/query-service/rules"
|
||||||
"github.com/SigNoz/signoz/ee/query-service/usage"
|
"github.com/SigNoz/signoz/ee/query-service/usage"
|
||||||
@@ -32,6 +28,7 @@ import (
|
|||||||
|
|
||||||
"github.com/SigNoz/signoz/pkg/query-service/agentConf"
|
"github.com/SigNoz/signoz/pkg/query-service/agentConf"
|
||||||
baseapp "github.com/SigNoz/signoz/pkg/query-service/app"
|
baseapp "github.com/SigNoz/signoz/pkg/query-service/app"
|
||||||
|
"github.com/SigNoz/signoz/pkg/query-service/app/clickhouseReader"
|
||||||
"github.com/SigNoz/signoz/pkg/query-service/app/cloudintegrations"
|
"github.com/SigNoz/signoz/pkg/query-service/app/cloudintegrations"
|
||||||
"github.com/SigNoz/signoz/pkg/query-service/app/integrations"
|
"github.com/SigNoz/signoz/pkg/query-service/app/integrations"
|
||||||
"github.com/SigNoz/signoz/pkg/query-service/app/logparsingpipeline"
|
"github.com/SigNoz/signoz/pkg/query-service/app/logparsingpipeline"
|
||||||
@@ -41,7 +38,6 @@ import (
|
|||||||
"github.com/SigNoz/signoz/pkg/query-service/healthcheck"
|
"github.com/SigNoz/signoz/pkg/query-service/healthcheck"
|
||||||
baseint "github.com/SigNoz/signoz/pkg/query-service/interfaces"
|
baseint "github.com/SigNoz/signoz/pkg/query-service/interfaces"
|
||||||
baserules "github.com/SigNoz/signoz/pkg/query-service/rules"
|
baserules "github.com/SigNoz/signoz/pkg/query-service/rules"
|
||||||
"github.com/SigNoz/signoz/pkg/query-service/telemetry"
|
|
||||||
"github.com/SigNoz/signoz/pkg/query-service/utils"
|
"github.com/SigNoz/signoz/pkg/query-service/utils"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
)
|
)
|
||||||
@@ -59,62 +55,55 @@ type ServerOptions struct {
|
|||||||
Jwt *authtypes.JWT
|
Jwt *authtypes.JWT
|
||||||
}
|
}
|
||||||
|
|
||||||
// Server runs HTTP api service
|
// Server runs HTTP, Mux and a grpc server
|
||||||
type Server struct {
|
type Server struct {
|
||||||
serverOptions *ServerOptions
|
config signoz.Config
|
||||||
ruleManager *baserules.Manager
|
signoz *signoz.SigNoz
|
||||||
|
jwt *authtypes.JWT
|
||||||
|
ruleManager *baserules.Manager
|
||||||
|
|
||||||
// public http router
|
// public http router
|
||||||
httpConn net.Listener
|
httpConn net.Listener
|
||||||
httpServer *http.Server
|
httpServer *http.Server
|
||||||
|
httpHostPort string
|
||||||
|
|
||||||
// private http
|
// private http
|
||||||
privateConn net.Listener
|
privateConn net.Listener
|
||||||
privateHTTP *http.Server
|
privateHTTP *http.Server
|
||||||
|
privateHostPort string
|
||||||
|
|
||||||
|
opampServer *opamp.Server
|
||||||
|
|
||||||
// Usage manager
|
// Usage manager
|
||||||
usageManager *usage.Manager
|
usageManager *usage.Manager
|
||||||
|
|
||||||
opampServer *opamp.Server
|
|
||||||
|
|
||||||
unavailableChannel chan healthcheck.Status
|
unavailableChannel chan healthcheck.Status
|
||||||
}
|
}
|
||||||
|
|
||||||
// HealthCheckStatus returns health check status channel a client can subscribe to
|
|
||||||
func (s Server) HealthCheckStatus() chan healthcheck.Status {
|
|
||||||
return s.unavailableChannel
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewServer creates and initializes Server
|
// NewServer creates and initializes Server
|
||||||
func NewServer(serverOptions *ServerOptions) (*Server, error) {
|
func NewServer(config signoz.Config, signoz *signoz.SigNoz, jwt *authtypes.JWT) (*Server, error) {
|
||||||
gatewayProxy, err := gateway.NewProxy(serverOptions.GatewayUrl, gateway.RoutePrefix)
|
gatewayProxy, err := gateway.NewProxy(config.Gateway.URL.String(), gateway.RoutePrefix)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
fluxIntervalForTraceDetail, err := time.ParseDuration(serverOptions.FluxIntervalForTraceDetail)
|
reader := clickhouseReader.NewReader(
|
||||||
if err != nil {
|
signoz.SQLStore,
|
||||||
return nil, err
|
signoz.TelemetryStore,
|
||||||
}
|
signoz.Prometheus,
|
||||||
|
signoz.TelemetryStore.Cluster(),
|
||||||
reader := db.NewDataConnector(
|
config.Querier.FluxInterval,
|
||||||
serverOptions.SigNoz.SQLStore,
|
signoz.Cache,
|
||||||
serverOptions.SigNoz.TelemetryStore,
|
|
||||||
serverOptions.SigNoz.Prometheus,
|
|
||||||
serverOptions.Cluster,
|
|
||||||
fluxIntervalForTraceDetail,
|
|
||||||
serverOptions.SigNoz.Cache,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
rm, err := makeRulesManager(
|
rm, err := makeRulesManager(
|
||||||
serverOptions.SigNoz.SQLStore.SQLxDB(),
|
|
||||||
reader,
|
reader,
|
||||||
serverOptions.SigNoz.Cache,
|
signoz.Cache,
|
||||||
serverOptions.SigNoz.Alertmanager,
|
signoz.Alertmanager,
|
||||||
serverOptions.SigNoz.SQLStore,
|
signoz.SQLStore,
|
||||||
serverOptions.SigNoz.TelemetryStore,
|
signoz.TelemetryStore,
|
||||||
serverOptions.SigNoz.Prometheus,
|
signoz.Prometheus,
|
||||||
serverOptions.SigNoz.Modules.OrgGetter,
|
signoz.Modules.OrgGetter,
|
||||||
)
|
)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -122,19 +111,16 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// initiate opamp
|
// initiate opamp
|
||||||
_, err = opAmpModel.InitDB(serverOptions.SigNoz.SQLStore.SQLxDB())
|
opAmpModel.Init(signoz.SQLStore, signoz.Instrumentation.Logger(), signoz.Modules.OrgGetter)
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
integrationsController, err := integrations.NewController(serverOptions.SigNoz.SQLStore)
|
integrationsController, err := integrations.NewController(signoz.SQLStore)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf(
|
return nil, fmt.Errorf(
|
||||||
"couldn't create integrations controller: %w", err,
|
"couldn't create integrations controller: %w", err,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
cloudIntegrationsController, err := cloudintegrations.NewController(serverOptions.SigNoz.SQLStore)
|
cloudIntegrationsController, err := cloudintegrations.NewController(signoz.SQLStore)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf(
|
return nil, fmt.Errorf(
|
||||||
"couldn't create cloud provider integrations controller: %w", err,
|
"couldn't create cloud provider integrations controller: %w", err,
|
||||||
@@ -143,7 +129,8 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) {
|
|||||||
|
|
||||||
// ingestion pipelines manager
|
// ingestion pipelines manager
|
||||||
logParsingPipelineController, err := logparsingpipeline.NewLogParsingPipelinesController(
|
logParsingPipelineController, err := logparsingpipeline.NewLogParsingPipelinesController(
|
||||||
serverOptions.SigNoz.SQLStore, integrationsController.GetPipelinesForInstalledIntegrations,
|
signoz.SQLStore,
|
||||||
|
integrationsController.GetPipelinesForInstalledIntegrations,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -151,7 +138,7 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) {
|
|||||||
|
|
||||||
// initiate agent config handler
|
// initiate agent config handler
|
||||||
agentConfMgr, err := agentConf.Initiate(&agentConf.ManagerOptions{
|
agentConfMgr, err := agentConf.Initiate(&agentConf.ManagerOptions{
|
||||||
DB: serverOptions.SigNoz.SQLStore.SQLxDB(),
|
Store: signoz.SQLStore,
|
||||||
AgentFeatures: []agentConf.AgentFeature{logParsingPipelineController},
|
AgentFeatures: []agentConf.AgentFeature{logParsingPipelineController},
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -159,7 +146,7 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// start the usagemanager
|
// start the usagemanager
|
||||||
usageManager, err := usage.New(serverOptions.SigNoz.Licensing, serverOptions.SigNoz.TelemetryStore.ClickhouseDB(), serverOptions.SigNoz.Zeus, serverOptions.SigNoz.Modules.OrgGetter)
|
usageManager, err := usage.New(signoz.Licensing, signoz.TelemetryStore.ClickhouseDB(), signoz.Zeus, signoz.Modules.OrgGetter)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -168,47 +155,36 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
telemetry.GetInstance().SetReader(reader)
|
|
||||||
telemetry.GetInstance().SetSqlStore(serverOptions.SigNoz.SQLStore)
|
|
||||||
telemetry.GetInstance().SetSaasOperator(constants.SaasSegmentKey)
|
|
||||||
telemetry.GetInstance().SetSavedViewsInfoCallback(telemetry.GetSavedViewsInfo)
|
|
||||||
telemetry.GetInstance().SetAlertsInfoCallback(telemetry.GetAlertsInfo)
|
|
||||||
telemetry.GetInstance().SetGetUsersCallback(telemetry.GetUsers)
|
|
||||||
telemetry.GetInstance().SetUserCountCallback(telemetry.GetUserCount)
|
|
||||||
telemetry.GetInstance().SetDashboardsInfoCallback(telemetry.GetDashboardsInfo)
|
|
||||||
|
|
||||||
fluxInterval, err := time.ParseDuration(serverOptions.FluxInterval)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
apiOpts := api.APIHandlerOptions{
|
apiOpts := api.APIHandlerOptions{
|
||||||
DataConnector: reader,
|
DataConnector: reader,
|
||||||
PreferSpanMetrics: serverOptions.PreferSpanMetrics,
|
|
||||||
RulesManager: rm,
|
RulesManager: rm,
|
||||||
UsageManager: usageManager,
|
UsageManager: usageManager,
|
||||||
IntegrationsController: integrationsController,
|
IntegrationsController: integrationsController,
|
||||||
CloudIntegrationsController: cloudIntegrationsController,
|
CloudIntegrationsController: cloudIntegrationsController,
|
||||||
LogsParsingPipelineController: logParsingPipelineController,
|
LogsParsingPipelineController: logParsingPipelineController,
|
||||||
FluxInterval: fluxInterval,
|
FluxInterval: config.Querier.FluxInterval,
|
||||||
Gateway: gatewayProxy,
|
Gateway: gatewayProxy,
|
||||||
GatewayUrl: serverOptions.GatewayUrl,
|
GatewayUrl: config.Gateway.URL.String(),
|
||||||
JWT: serverOptions.Jwt,
|
JWT: jwt,
|
||||||
}
|
}
|
||||||
|
|
||||||
apiHandler, err := api.NewAPIHandler(apiOpts, serverOptions.SigNoz)
|
apiHandler, err := api.NewAPIHandler(apiOpts, signoz)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
s := &Server{
|
s := &Server{
|
||||||
|
config: config,
|
||||||
|
signoz: signoz,
|
||||||
|
jwt: jwt,
|
||||||
ruleManager: rm,
|
ruleManager: rm,
|
||||||
serverOptions: serverOptions,
|
httpHostPort: baseconst.HTTPHostPort,
|
||||||
|
privateHostPort: baseconst.PrivateHostPort,
|
||||||
unavailableChannel: make(chan healthcheck.Status),
|
unavailableChannel: make(chan healthcheck.Status),
|
||||||
usageManager: usageManager,
|
usageManager: usageManager,
|
||||||
}
|
}
|
||||||
|
|
||||||
httpServer, err := s.createPublicServer(apiHandler, serverOptions.SigNoz.Web)
|
httpServer, err := s.createPublicServer(apiHandler, signoz.Web)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -224,35 +200,28 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) {
|
|||||||
s.privateHTTP = privateServer
|
s.privateHTTP = privateServer
|
||||||
|
|
||||||
s.opampServer = opamp.InitializeServer(
|
s.opampServer = opamp.InitializeServer(
|
||||||
&opAmpModel.AllAgents, agentConfMgr,
|
&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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// HealthCheckStatus returns health check status channel a client can subscribe to
|
||||||
|
func (s Server) HealthCheckStatus() chan healthcheck.Status {
|
||||||
|
return s.unavailableChannel
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Server) createPrivateServer(apiHandler *api.APIHandler) (*http.Server, error) {
|
func (s *Server) createPrivateServer(apiHandler *api.APIHandler) (*http.Server, error) {
|
||||||
r := baseapp.NewRouter()
|
r := baseapp.NewRouter()
|
||||||
|
|
||||||
r.Use(middleware.NewAuth(s.serverOptions.Jwt, []string{"Authorization", "Sec-WebSocket-Protocol"}, s.serverOptions.SigNoz.Sharder, s.serverOptions.SigNoz.Instrumentation.Logger()).Wrap)
|
r.Use(middleware.NewAuth(s.jwt, []string{"Authorization", "Sec-WebSocket-Protocol"}, s.signoz.Sharder, s.signoz.Instrumentation.Logger()).Wrap)
|
||||||
r.Use(middleware.NewAPIKey(s.serverOptions.SigNoz.SQLStore, []string{"SIGNOZ-API-KEY"}, s.serverOptions.SigNoz.Instrumentation.Logger(), s.serverOptions.SigNoz.Sharder).Wrap)
|
r.Use(middleware.NewAPIKey(s.signoz.SQLStore, []string{"SIGNOZ-API-KEY"}, s.signoz.Instrumentation.Logger(), s.signoz.Sharder).Wrap)
|
||||||
r.Use(middleware.NewTimeout(s.serverOptions.SigNoz.Instrumentation.Logger(),
|
r.Use(middleware.NewTimeout(s.signoz.Instrumentation.Logger(),
|
||||||
s.serverOptions.Config.APIServer.Timeout.ExcludedRoutes,
|
s.config.APIServer.Timeout.ExcludedRoutes,
|
||||||
s.serverOptions.Config.APIServer.Timeout.Default,
|
s.config.APIServer.Timeout.Default,
|
||||||
s.serverOptions.Config.APIServer.Timeout.Max,
|
s.config.APIServer.Timeout.Max,
|
||||||
).Wrap)
|
).Wrap)
|
||||||
r.Use(middleware.NewAnalytics().Wrap)
|
r.Use(middleware.NewLogging(s.signoz.Instrumentation.Logger(), s.config.APIServer.Logging.ExcludedRoutes).Wrap)
|
||||||
r.Use(middleware.NewLogging(s.serverOptions.SigNoz.Instrumentation.Logger(), s.serverOptions.Config.APIServer.Logging.ExcludedRoutes).Wrap)
|
|
||||||
|
|
||||||
apiHandler.RegisterPrivateRoutes(r)
|
apiHandler.RegisterPrivateRoutes(r)
|
||||||
|
|
||||||
@@ -274,17 +243,16 @@ func (s *Server) createPrivateServer(apiHandler *api.APIHandler) (*http.Server,
|
|||||||
|
|
||||||
func (s *Server) createPublicServer(apiHandler *api.APIHandler, web web.Web) (*http.Server, error) {
|
func (s *Server) createPublicServer(apiHandler *api.APIHandler, web web.Web) (*http.Server, error) {
|
||||||
r := baseapp.NewRouter()
|
r := baseapp.NewRouter()
|
||||||
am := middleware.NewAuthZ(s.serverOptions.SigNoz.Instrumentation.Logger())
|
am := middleware.NewAuthZ(s.signoz.Instrumentation.Logger())
|
||||||
|
|
||||||
r.Use(middleware.NewAuth(s.serverOptions.Jwt, []string{"Authorization", "Sec-WebSocket-Protocol"}, s.serverOptions.SigNoz.Sharder, s.serverOptions.SigNoz.Instrumentation.Logger()).Wrap)
|
r.Use(middleware.NewAuth(s.jwt, []string{"Authorization", "Sec-WebSocket-Protocol"}, s.signoz.Sharder, s.signoz.Instrumentation.Logger()).Wrap)
|
||||||
r.Use(middleware.NewAPIKey(s.serverOptions.SigNoz.SQLStore, []string{"SIGNOZ-API-KEY"}, s.serverOptions.SigNoz.Instrumentation.Logger(), s.serverOptions.SigNoz.Sharder).Wrap)
|
r.Use(middleware.NewAPIKey(s.signoz.SQLStore, []string{"SIGNOZ-API-KEY"}, s.signoz.Instrumentation.Logger(), s.signoz.Sharder).Wrap)
|
||||||
r.Use(middleware.NewTimeout(s.serverOptions.SigNoz.Instrumentation.Logger(),
|
r.Use(middleware.NewTimeout(s.signoz.Instrumentation.Logger(),
|
||||||
s.serverOptions.Config.APIServer.Timeout.ExcludedRoutes,
|
s.config.APIServer.Timeout.ExcludedRoutes,
|
||||||
s.serverOptions.Config.APIServer.Timeout.Default,
|
s.config.APIServer.Timeout.Default,
|
||||||
s.serverOptions.Config.APIServer.Timeout.Max,
|
s.config.APIServer.Timeout.Max,
|
||||||
).Wrap)
|
).Wrap)
|
||||||
r.Use(middleware.NewAnalytics().Wrap)
|
r.Use(middleware.NewLogging(s.signoz.Instrumentation.Logger(), s.config.APIServer.Logging.ExcludedRoutes).Wrap)
|
||||||
r.Use(middleware.NewLogging(s.serverOptions.SigNoz.Instrumentation.Logger(), s.serverOptions.Config.APIServer.Logging.ExcludedRoutes).Wrap)
|
|
||||||
|
|
||||||
apiHandler.RegisterRoutes(r, am)
|
apiHandler.RegisterRoutes(r, am)
|
||||||
apiHandler.RegisterLogsRoutes(r, am)
|
apiHandler.RegisterLogsRoutes(r, am)
|
||||||
@@ -325,7 +293,7 @@ func (s *Server) createPublicServer(apiHandler *api.APIHandler, web web.Web) (*h
|
|||||||
func (s *Server) initListeners() error {
|
func (s *Server) initListeners() error {
|
||||||
// listen on public port
|
// listen on public port
|
||||||
var err error
|
var err error
|
||||||
publicHostPort := s.serverOptions.HTTPHostPort
|
publicHostPort := s.httpHostPort
|
||||||
if publicHostPort == "" {
|
if publicHostPort == "" {
|
||||||
return fmt.Errorf("baseconst.HTTPHostPort is required")
|
return fmt.Errorf("baseconst.HTTPHostPort is required")
|
||||||
}
|
}
|
||||||
@@ -335,10 +303,10 @@ func (s *Server) initListeners() error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
zap.L().Info(fmt.Sprintf("Query server started listening on %s...", s.serverOptions.HTTPHostPort))
|
zap.L().Info(fmt.Sprintf("Query server started listening on %s...", s.httpHostPort))
|
||||||
|
|
||||||
// listen on private port to support internal services
|
// listen on private port to support internal services
|
||||||
privateHostPort := s.serverOptions.PrivateHostPort
|
privateHostPort := s.privateHostPort
|
||||||
|
|
||||||
if privateHostPort == "" {
|
if privateHostPort == "" {
|
||||||
return fmt.Errorf("baseconst.PrivateHostPort is required")
|
return fmt.Errorf("baseconst.PrivateHostPort is required")
|
||||||
@@ -348,7 +316,7 @@ func (s *Server) initListeners() error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
zap.L().Info(fmt.Sprintf("Query server started listening on private port %s...", s.serverOptions.PrivateHostPort))
|
zap.L().Info(fmt.Sprintf("Query server started listening on private port %s...", s.privateHostPort))
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -368,7 +336,7 @@ func (s *Server) Start(ctx context.Context) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
zap.L().Info("Starting HTTP server", zap.Int("port", httpPort), zap.String("addr", s.serverOptions.HTTPHostPort))
|
zap.L().Info("Starting HTTP server", zap.Int("port", httpPort), zap.String("addr", s.httpHostPort))
|
||||||
|
|
||||||
switch err := s.httpServer.Serve(s.httpConn); err {
|
switch err := s.httpServer.Serve(s.httpConn); err {
|
||||||
case nil, http.ErrServerClosed, cmux.ErrListenerClosed:
|
case nil, http.ErrServerClosed, cmux.ErrListenerClosed:
|
||||||
@@ -394,7 +362,7 @@ func (s *Server) Start(ctx context.Context) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
zap.L().Info("Starting Private HTTP server", zap.Int("port", privatePort), zap.String("addr", s.serverOptions.PrivateHostPort))
|
zap.L().Info("Starting Private HTTP server", zap.Int("port", privatePort), zap.String("addr", s.privateHostPort))
|
||||||
|
|
||||||
switch err := s.privateHTTP.Serve(s.privateConn); err {
|
switch err := s.privateHTTP.Serve(s.privateConn); err {
|
||||||
case nil, http.ErrServerClosed, cmux.ErrListenerClosed:
|
case nil, http.ErrServerClosed, cmux.ErrListenerClosed:
|
||||||
@@ -446,7 +414,6 @@ func (s *Server) Stop(ctx context.Context) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func makeRulesManager(
|
func makeRulesManager(
|
||||||
db *sqlx.DB,
|
|
||||||
ch baseint.Reader,
|
ch baseint.Reader,
|
||||||
cache cache.Cache,
|
cache cache.Cache,
|
||||||
alertmanager alertmanager.Alertmanager,
|
alertmanager alertmanager.Alertmanager,
|
||||||
@@ -459,7 +426,6 @@ func makeRulesManager(
|
|||||||
managerOpts := &baserules.ManagerOptions{
|
managerOpts := &baserules.ManagerOptions{
|
||||||
TelemetryStore: telemetryStore,
|
TelemetryStore: telemetryStore,
|
||||||
Prometheus: prometheus,
|
Prometheus: prometheus,
|
||||||
DBConn: db,
|
|
||||||
Context: context.Background(),
|
Context: context.Background(),
|
||||||
Logger: zap.L(),
|
Logger: zap.L(),
|
||||||
Reader: ch,
|
Reader: ch,
|
||||||
|
|||||||
@@ -37,9 +37,14 @@ func GetDefaultSiteURL() string {
|
|||||||
const DotMetricsEnabled = "DOT_METRICS_ENABLED"
|
const DotMetricsEnabled = "DOT_METRICS_ENABLED"
|
||||||
|
|
||||||
var IsDotMetricsEnabled = false
|
var IsDotMetricsEnabled = false
|
||||||
|
var IsPreferSpanMetrics = false
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
if GetOrDefaultEnv(DotMetricsEnabled, "false") == "true" {
|
if GetOrDefaultEnv(DotMetricsEnabled, "false") == "true" {
|
||||||
IsDotMetricsEnabled = true
|
IsDotMetricsEnabled = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if GetOrDefaultEnv("USE_SPAN_METRICS", "false") == "true" {
|
||||||
|
IsPreferSpanMetrics = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +0,0 @@
|
|||||||
package interfaces
|
|
||||||
|
|
||||||
import (
|
|
||||||
baseint "github.com/SigNoz/signoz/pkg/query-service/interfaces"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Connector defines methods for interaction
|
|
||||||
// with o11y data. for example - clickhouse
|
|
||||||
type DataConnector interface {
|
|
||||||
baseint.Reader
|
|
||||||
}
|
|
||||||
@@ -9,9 +9,11 @@ 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"
|
||||||
|
"github.com/SigNoz/signoz/pkg/analytics"
|
||||||
"github.com/SigNoz/signoz/pkg/config"
|
"github.com/SigNoz/signoz/pkg/config"
|
||||||
"github.com/SigNoz/signoz/pkg/config/envprovider"
|
"github.com/SigNoz/signoz/pkg/config/envprovider"
|
||||||
"github.com/SigNoz/signoz/pkg/config/fileprovider"
|
"github.com/SigNoz/signoz/pkg/config/fileprovider"
|
||||||
@@ -20,6 +22,7 @@ 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"
|
||||||
@@ -101,10 +104,14 @@ func main() {
|
|||||||
fileprovider.NewFactory(),
|
fileprovider.NewFactory(),
|
||||||
},
|
},
|
||||||
}, signoz.DeprecatedFlags{
|
}, signoz.DeprecatedFlags{
|
||||||
MaxIdleConns: maxIdleConns,
|
MaxIdleConns: maxIdleConns,
|
||||||
MaxOpenConns: maxOpenConns,
|
MaxOpenConns: maxOpenConns,
|
||||||
DialTimeout: dialTimeout,
|
DialTimeout: dialTimeout,
|
||||||
Config: promConfigPath,
|
Config: promConfigPath,
|
||||||
|
FluxInterval: fluxInterval,
|
||||||
|
FluxIntervalForTraceDetail: fluxIntervalForTraceDetail,
|
||||||
|
Cluster: cluster,
|
||||||
|
GatewayUrl: gatewayUrl,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
zap.L().Fatal("Failed to create config", zap.Error(err))
|
zap.L().Fatal("Failed to create config", zap.Error(err))
|
||||||
@@ -134,12 +141,20 @@ func main() {
|
|||||||
zeus.Config(),
|
zeus.Config(),
|
||||||
httpzeus.NewProviderFactory(),
|
httpzeus.NewProviderFactory(),
|
||||||
licensing.Config(24*time.Hour, 3),
|
licensing.Config(24*time.Hour, 3),
|
||||||
func(sqlstore sqlstore.SQLStore, zeus pkgzeus.Zeus, orgGetter organization.Getter) factory.ProviderFactory[pkglicensing.Licensing, pkglicensing.Config] {
|
func(sqlstore sqlstore.SQLStore, zeus pkgzeus.Zeus, orgGetter organization.Getter, analytics analytics.Analytics) factory.ProviderFactory[pkglicensing.Licensing, pkglicensing.Config] {
|
||||||
return httplicensing.NewProviderFactory(sqlstore, zeus, orgGetter)
|
return httplicensing.NewProviderFactory(sqlstore, zeus, orgGetter, analytics)
|
||||||
},
|
},
|
||||||
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(),
|
||||||
)
|
)
|
||||||
@@ -147,20 +162,7 @@ func main() {
|
|||||||
zap.L().Fatal("Failed to create signoz", zap.Error(err))
|
zap.L().Fatal("Failed to create signoz", zap.Error(err))
|
||||||
}
|
}
|
||||||
|
|
||||||
serverOptions := &app.ServerOptions{
|
server, err := app.NewServer(config, signoz, jwt)
|
||||||
Config: config,
|
|
||||||
SigNoz: signoz,
|
|
||||||
HTTPHostPort: baseconst.HTTPHostPort,
|
|
||||||
PreferSpanMetrics: preferSpanMetrics,
|
|
||||||
PrivateHostPort: baseconst.PrivateHostPort,
|
|
||||||
FluxInterval: fluxInterval,
|
|
||||||
FluxIntervalForTraceDetail: fluxIntervalForTraceDetail,
|
|
||||||
Cluster: cluster,
|
|
||||||
GatewayUrl: gatewayUrl,
|
|
||||||
Jwt: jwt,
|
|
||||||
}
|
|
||||||
|
|
||||||
server, err := app.NewServer(serverOptions)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
zap.L().Fatal("Failed to create server", zap.Error(err))
|
zap.L().Fatal("Failed to create server", zap.Error(err))
|
||||||
}
|
}
|
||||||
|
|||||||
36
ee/sqlschema/postgressqlschema/formatter.go
Normal file
36
ee/sqlschema/postgressqlschema/formatter.go
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
package postgressqlschema
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/SigNoz/signoz/pkg/sqlschema"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Formatter struct {
|
||||||
|
sqlschema.Formatter
|
||||||
|
}
|
||||||
|
|
||||||
|
func (formatter Formatter) SQLDataTypeOf(dataType sqlschema.DataType) string {
|
||||||
|
if dataType == sqlschema.DataTypeTimestamp {
|
||||||
|
return "TIMESTAMPTZ"
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.ToUpper(dataType.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (formatter Formatter) DataTypeOf(dataType string) sqlschema.DataType {
|
||||||
|
switch strings.ToUpper(dataType) {
|
||||||
|
case "TIMESTAMPTZ", "TIMESTAMP", "TIMESTAMP WITHOUT TIME ZONE", "TIMESTAMP WITH TIME ZONE":
|
||||||
|
return sqlschema.DataTypeTimestamp
|
||||||
|
case "INT8":
|
||||||
|
return sqlschema.DataTypeBigInt
|
||||||
|
case "INT2", "INT4", "SMALLINT", "INTEGER":
|
||||||
|
return sqlschema.DataTypeInteger
|
||||||
|
case "BOOL", "BOOLEAN":
|
||||||
|
return sqlschema.DataTypeBoolean
|
||||||
|
case "VARCHAR", "CHARACTER VARYING", "CHARACTER":
|
||||||
|
return sqlschema.DataTypeText
|
||||||
|
}
|
||||||
|
|
||||||
|
return formatter.Formatter.DataTypeOf(dataType)
|
||||||
|
}
|
||||||
285
ee/sqlschema/postgressqlschema/provider.go
Normal file
285
ee/sqlschema/postgressqlschema/provider.go
Normal file
@@ -0,0 +1,285 @@
|
|||||||
|
package postgressqlschema
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/SigNoz/signoz/pkg/factory"
|
||||||
|
"github.com/SigNoz/signoz/pkg/sqlschema"
|
||||||
|
"github.com/SigNoz/signoz/pkg/sqlstore"
|
||||||
|
"github.com/uptrace/bun"
|
||||||
|
)
|
||||||
|
|
||||||
|
type provider struct {
|
||||||
|
settings factory.ScopedProviderSettings
|
||||||
|
fmter sqlschema.SQLFormatter
|
||||||
|
sqlstore sqlstore.SQLStore
|
||||||
|
operator sqlschema.SQLOperator
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewFactory(sqlstore sqlstore.SQLStore) factory.ProviderFactory[sqlschema.SQLSchema, sqlschema.Config] {
|
||||||
|
return factory.NewProviderFactory(factory.MustNewName("postgres"), func(ctx context.Context, providerSettings factory.ProviderSettings, config sqlschema.Config) (sqlschema.SQLSchema, error) {
|
||||||
|
return New(ctx, providerSettings, config, sqlstore)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(ctx context.Context, providerSettings factory.ProviderSettings, config sqlschema.Config, sqlstore sqlstore.SQLStore) (sqlschema.SQLSchema, error) {
|
||||||
|
settings := factory.NewScopedProviderSettings(providerSettings, "github.com/SigNoz/signoz/pkg/sqlschema/postgressqlschema")
|
||||||
|
fmter := Formatter{Formatter: sqlschema.NewFormatter(sqlstore.BunDB().Dialect())}
|
||||||
|
|
||||||
|
return &provider{
|
||||||
|
sqlstore: sqlstore,
|
||||||
|
fmter: fmter,
|
||||||
|
settings: settings,
|
||||||
|
operator: sqlschema.NewOperator(fmter, sqlschema.OperatorSupport{
|
||||||
|
DropConstraint: true,
|
||||||
|
ColumnIfNotExistsExists: true,
|
||||||
|
AlterColumnSetNotNull: true,
|
||||||
|
}),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (provider *provider) Formatter() sqlschema.SQLFormatter {
|
||||||
|
return provider.fmter
|
||||||
|
}
|
||||||
|
|
||||||
|
func (provider *provider) Operator() sqlschema.SQLOperator {
|
||||||
|
return provider.operator
|
||||||
|
}
|
||||||
|
|
||||||
|
func (provider *provider) GetTable(ctx context.Context, tableName sqlschema.TableName) (*sqlschema.Table, []*sqlschema.UniqueConstraint, error) {
|
||||||
|
rows, err := provider.
|
||||||
|
sqlstore.
|
||||||
|
BunDB().
|
||||||
|
QueryContext(ctx, `
|
||||||
|
SELECT
|
||||||
|
c.column_name,
|
||||||
|
c.is_nullable = 'YES',
|
||||||
|
c.udt_name,
|
||||||
|
c.column_default
|
||||||
|
FROM
|
||||||
|
information_schema.columns AS c
|
||||||
|
WHERE
|
||||||
|
c.table_name = ?`, string(tableName))
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
if err := rows.Close(); err != nil {
|
||||||
|
provider.settings.Logger().ErrorContext(ctx, "error closing rows", "error", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
columns := make([]*sqlschema.Column, 0)
|
||||||
|
for rows.Next() {
|
||||||
|
var (
|
||||||
|
name string
|
||||||
|
sqlDataType string
|
||||||
|
nullable bool
|
||||||
|
defaultVal *string
|
||||||
|
)
|
||||||
|
if err := rows.Scan(&name, &nullable, &sqlDataType, &defaultVal); err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
columnDefault := ""
|
||||||
|
if defaultVal != nil {
|
||||||
|
columnDefault = *defaultVal
|
||||||
|
}
|
||||||
|
|
||||||
|
columns = append(columns, &sqlschema.Column{
|
||||||
|
Name: sqlschema.ColumnName(name),
|
||||||
|
Nullable: nullable,
|
||||||
|
DataType: provider.fmter.DataTypeOf(sqlDataType),
|
||||||
|
Default: columnDefault,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
constraintsRows, err := provider.
|
||||||
|
sqlstore.
|
||||||
|
BunDB().
|
||||||
|
QueryContext(ctx, `
|
||||||
|
SELECT
|
||||||
|
c.column_name,
|
||||||
|
constraint_name,
|
||||||
|
constraint_type
|
||||||
|
FROM
|
||||||
|
information_schema.table_constraints tc
|
||||||
|
JOIN information_schema.constraint_column_usage AS ccu USING (constraint_schema, constraint_catalog, table_name, constraint_name)
|
||||||
|
JOIN information_schema.columns AS c ON c.table_schema = tc.constraint_schema AND tc.table_name = c.table_name AND ccu.column_name = c.column_name
|
||||||
|
WHERE
|
||||||
|
c.table_name = ?`, string(tableName))
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
if err := constraintsRows.Close(); err != nil {
|
||||||
|
provider.settings.Logger().ErrorContext(ctx, "error closing rows", "error", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
var primaryKeyConstraint *sqlschema.PrimaryKeyConstraint
|
||||||
|
uniqueConstraintsMap := make(map[string]*sqlschema.UniqueConstraint)
|
||||||
|
for constraintsRows.Next() {
|
||||||
|
var (
|
||||||
|
name string
|
||||||
|
constraintName string
|
||||||
|
constraintType string
|
||||||
|
)
|
||||||
|
|
||||||
|
if err := constraintsRows.Scan(&name, &constraintName, &constraintType); err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if constraintType == "PRIMARY KEY" {
|
||||||
|
if primaryKeyConstraint == nil {
|
||||||
|
primaryKeyConstraint = (&sqlschema.PrimaryKeyConstraint{
|
||||||
|
ColumnNames: []sqlschema.ColumnName{sqlschema.ColumnName(name)},
|
||||||
|
}).Named(constraintName).(*sqlschema.PrimaryKeyConstraint)
|
||||||
|
} else {
|
||||||
|
primaryKeyConstraint.ColumnNames = append(primaryKeyConstraint.ColumnNames, sqlschema.ColumnName(name))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if constraintType == "UNIQUE" {
|
||||||
|
if _, ok := uniqueConstraintsMap[constraintName]; !ok {
|
||||||
|
uniqueConstraintsMap[constraintName] = (&sqlschema.UniqueConstraint{
|
||||||
|
ColumnNames: []sqlschema.ColumnName{sqlschema.ColumnName(name)},
|
||||||
|
}).Named(constraintName).(*sqlschema.UniqueConstraint)
|
||||||
|
} else {
|
||||||
|
uniqueConstraintsMap[constraintName].ColumnNames = append(uniqueConstraintsMap[constraintName].ColumnNames, sqlschema.ColumnName(name))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreignKeyConstraintsRows, err := provider.
|
||||||
|
sqlstore.
|
||||||
|
BunDB().
|
||||||
|
QueryContext(ctx, `
|
||||||
|
SELECT
|
||||||
|
tc.constraint_name,
|
||||||
|
kcu.table_name AS referencing_table,
|
||||||
|
kcu.column_name AS referencing_column,
|
||||||
|
ccu.table_name AS referenced_table,
|
||||||
|
ccu.column_name AS referenced_column
|
||||||
|
FROM
|
||||||
|
information_schema.key_column_usage kcu
|
||||||
|
JOIN information_schema.table_constraints tc ON kcu.constraint_name = tc.constraint_name AND kcu.table_schema = tc.table_schema
|
||||||
|
JOIN information_schema.constraint_column_usage ccu ON ccu.constraint_name = tc.constraint_name AND ccu.table_schema = tc.table_schema
|
||||||
|
WHERE
|
||||||
|
tc.constraint_type = ?
|
||||||
|
AND kcu.table_name = ?`, "FOREIGN KEY", string(tableName))
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
if err := foreignKeyConstraintsRows.Close(); err != nil {
|
||||||
|
provider.settings.Logger().ErrorContext(ctx, "error closing rows", "error", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
foreignKeyConstraints := make([]*sqlschema.ForeignKeyConstraint, 0)
|
||||||
|
for foreignKeyConstraintsRows.Next() {
|
||||||
|
var (
|
||||||
|
constraintName string
|
||||||
|
referencingTable string
|
||||||
|
referencingColumn string
|
||||||
|
referencedTable string
|
||||||
|
referencedColumn string
|
||||||
|
)
|
||||||
|
|
||||||
|
if err := foreignKeyConstraintsRows.Scan(&constraintName, &referencingTable, &referencingColumn, &referencedTable, &referencedColumn); err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
foreignKeyConstraints = append(foreignKeyConstraints, (&sqlschema.ForeignKeyConstraint{
|
||||||
|
ReferencingColumnName: sqlschema.ColumnName(referencingColumn),
|
||||||
|
ReferencedTableName: sqlschema.TableName(referencedTable),
|
||||||
|
ReferencedColumnName: sqlschema.ColumnName(referencedColumn),
|
||||||
|
}).Named(constraintName).(*sqlschema.ForeignKeyConstraint))
|
||||||
|
}
|
||||||
|
|
||||||
|
uniqueConstraints := make([]*sqlschema.UniqueConstraint, 0)
|
||||||
|
for _, uniqueConstraint := range uniqueConstraintsMap {
|
||||||
|
uniqueConstraints = append(uniqueConstraints, uniqueConstraint)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &sqlschema.Table{
|
||||||
|
Name: tableName,
|
||||||
|
Columns: columns,
|
||||||
|
PrimaryKeyConstraint: primaryKeyConstraint,
|
||||||
|
ForeignKeyConstraints: foreignKeyConstraints,
|
||||||
|
}, uniqueConstraints, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (provider *provider) GetIndices(ctx context.Context, name sqlschema.TableName) ([]sqlschema.Index, error) {
|
||||||
|
rows, err := provider.
|
||||||
|
sqlstore.
|
||||||
|
BunDB().
|
||||||
|
QueryContext(ctx, `
|
||||||
|
SELECT
|
||||||
|
ct.relname AS table_name,
|
||||||
|
ci.relname AS index_name,
|
||||||
|
i.indisunique AS unique,
|
||||||
|
i.indisprimary AS primary,
|
||||||
|
a.attname AS column_name
|
||||||
|
FROM
|
||||||
|
pg_index i
|
||||||
|
LEFT JOIN pg_class ct ON ct.oid = i.indrelid
|
||||||
|
LEFT JOIN pg_class ci ON ci.oid = i.indexrelid
|
||||||
|
LEFT JOIN pg_attribute a ON a.attrelid = ct.oid
|
||||||
|
LEFT JOIN pg_constraint con ON con.conindid = i.indexrelid
|
||||||
|
WHERE
|
||||||
|
a.attnum = ANY(i.indkey)
|
||||||
|
AND con.oid IS NULL
|
||||||
|
AND ct.relkind = 'r'
|
||||||
|
AND ct.relname = ?`, string(name))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
if err := rows.Close(); err != nil {
|
||||||
|
provider.settings.Logger().ErrorContext(ctx, "error closing rows", "error", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
uniqueIndicesMap := make(map[string]*sqlschema.UniqueIndex)
|
||||||
|
for rows.Next() {
|
||||||
|
var (
|
||||||
|
tableName string
|
||||||
|
indexName string
|
||||||
|
unique bool
|
||||||
|
primary bool
|
||||||
|
columnName string
|
||||||
|
)
|
||||||
|
|
||||||
|
if err := rows.Scan(&tableName, &indexName, &unique, &primary, &columnName); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if unique {
|
||||||
|
if _, ok := uniqueIndicesMap[indexName]; !ok {
|
||||||
|
uniqueIndicesMap[indexName] = &sqlschema.UniqueIndex{
|
||||||
|
TableName: name,
|
||||||
|
ColumnNames: []sqlschema.ColumnName{sqlschema.ColumnName(columnName)},
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
uniqueIndicesMap[indexName].ColumnNames = append(uniqueIndicesMap[indexName].ColumnNames, sqlschema.ColumnName(columnName))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
indices := make([]sqlschema.Index, 0)
|
||||||
|
for _, index := range uniqueIndicesMap {
|
||||||
|
indices = append(indices, index)
|
||||||
|
}
|
||||||
|
|
||||||
|
return indices, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (provider *provider) ToggleFKEnforcement(_ context.Context, _ bun.IDB, _ bool) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -17,19 +17,21 @@ var (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
Org = "org"
|
Org = "org"
|
||||||
User = "user"
|
User = "user"
|
||||||
UserNoCascade = "user_no_cascade"
|
UserNoCascade = "user_no_cascade"
|
||||||
FactorPassword = "factor_password"
|
FactorPassword = "factor_password"
|
||||||
CloudIntegration = "cloud_integration"
|
CloudIntegration = "cloud_integration"
|
||||||
|
AgentConfigVersion = "agent_config_version"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
OrgReference = `("org_id") REFERENCES "organizations" ("id")`
|
OrgReference = `("org_id") REFERENCES "organizations" ("id")`
|
||||||
UserReference = `("user_id") REFERENCES "users" ("id") ON DELETE CASCADE ON UPDATE CASCADE`
|
UserReference = `("user_id") REFERENCES "users" ("id") ON DELETE CASCADE ON UPDATE CASCADE`
|
||||||
UserReferenceNoCascade = `("user_id") REFERENCES "users" ("id")`
|
UserReferenceNoCascade = `("user_id") REFERENCES "users" ("id")`
|
||||||
FactorPasswordReference = `("password_id") REFERENCES "factor_password" ("id")`
|
FactorPasswordReference = `("password_id") REFERENCES "factor_password" ("id")`
|
||||||
CloudIntegrationReference = `("cloud_integration_id") REFERENCES "cloud_integration" ("id") ON DELETE CASCADE`
|
CloudIntegrationReference = `("cloud_integration_id") REFERENCES "cloud_integration" ("id") ON DELETE CASCADE`
|
||||||
|
AgentConfigVersionReference = `("version_id") REFERENCES "agent_config_version" ("id")`
|
||||||
)
|
)
|
||||||
|
|
||||||
type dialect struct{}
|
type dialect struct{}
|
||||||
@@ -274,6 +276,8 @@ func (dialect *dialect) RenameTableAndModifyModel(ctx context.Context, bun bun.I
|
|||||||
fkReferences = append(fkReferences, FactorPasswordReference)
|
fkReferences = append(fkReferences, FactorPasswordReference)
|
||||||
} else if reference == CloudIntegration && !slices.Contains(fkReferences, CloudIntegrationReference) {
|
} else if reference == CloudIntegration && !slices.Contains(fkReferences, CloudIntegrationReference) {
|
||||||
fkReferences = append(fkReferences, CloudIntegrationReference)
|
fkReferences = append(fkReferences, CloudIntegrationReference)
|
||||||
|
} else if reference == AgentConfigVersion && !slices.Contains(fkReferences, AgentConfigVersionReference) {
|
||||||
|
fkReferences = append(fkReferences, AgentConfigVersionReference)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import (
|
|||||||
"github.com/jackc/pgx/v5/pgconn"
|
"github.com/jackc/pgx/v5/pgconn"
|
||||||
"github.com/jackc/pgx/v5/pgxpool"
|
"github.com/jackc/pgx/v5/pgxpool"
|
||||||
"github.com/jackc/pgx/v5/stdlib"
|
"github.com/jackc/pgx/v5/stdlib"
|
||||||
"github.com/jmoiron/sqlx"
|
|
||||||
"github.com/uptrace/bun"
|
"github.com/uptrace/bun"
|
||||||
"github.com/uptrace/bun/dialect/pgdialect"
|
"github.com/uptrace/bun/dialect/pgdialect"
|
||||||
)
|
)
|
||||||
@@ -19,7 +18,6 @@ type provider struct {
|
|||||||
settings factory.ScopedProviderSettings
|
settings factory.ScopedProviderSettings
|
||||||
sqldb *sql.DB
|
sqldb *sql.DB
|
||||||
bundb *sqlstore.BunDB
|
bundb *sqlstore.BunDB
|
||||||
sqlxdb *sqlx.DB
|
|
||||||
dialect *dialect
|
dialect *dialect
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -61,7 +59,6 @@ func New(ctx context.Context, providerSettings factory.ProviderSettings, config
|
|||||||
settings: settings,
|
settings: settings,
|
||||||
sqldb: sqldb,
|
sqldb: sqldb,
|
||||||
bundb: sqlstore.NewBunDB(settings, sqldb, pgdialect.New(), hooks),
|
bundb: sqlstore.NewBunDB(settings, sqldb, pgdialect.New(), hooks),
|
||||||
sqlxdb: sqlx.NewDb(sqldb, "postgres"),
|
|
||||||
dialect: new(dialect),
|
dialect: new(dialect),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
@@ -74,10 +71,6 @@ func (provider *provider) SQLDB() *sql.DB {
|
|||||||
return provider.sqldb
|
return provider.sqldb
|
||||||
}
|
}
|
||||||
|
|
||||||
func (provider *provider) SQLxDB() *sqlx.DB {
|
|
||||||
return provider.sqlxdb
|
|
||||||
}
|
|
||||||
|
|
||||||
func (provider *provider) Dialect() sqlstore.SQLDialect {
|
func (provider *provider) Dialect() sqlstore.SQLDialect {
|
||||||
return provider.dialect
|
return provider.dialect
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
|
ignorePatterns: ['src/parser/*.ts'],
|
||||||
env: {
|
env: {
|
||||||
browser: true,
|
browser: true,
|
||||||
es2021: true,
|
es2021: true,
|
||||||
|
|||||||
@@ -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",
|
||||||
@@ -78,7 +83,7 @@
|
|||||||
"fontfaceobserver": "2.3.0",
|
"fontfaceobserver": "2.3.0",
|
||||||
"history": "4.10.1",
|
"history": "4.10.1",
|
||||||
"html-webpack-plugin": "5.5.0",
|
"html-webpack-plugin": "5.5.0",
|
||||||
"http-proxy-middleware": "3.0.3",
|
"http-proxy-middleware": "3.0.5",
|
||||||
"http-status-codes": "2.3.0",
|
"http-status-codes": "2.3.0",
|
||||||
"i18next": "^21.6.12",
|
"i18next": "^21.6.12",
|
||||||
"i18next-browser-languagedetector": "^6.1.3",
|
"i18next-browser-languagedetector": "^6.1.3",
|
||||||
@@ -250,7 +255,7 @@
|
|||||||
"xml2js": "0.5.0",
|
"xml2js": "0.5.0",
|
||||||
"phin": "^3.7.1",
|
"phin": "^3.7.1",
|
||||||
"body-parser": "1.20.3",
|
"body-parser": "1.20.3",
|
||||||
"http-proxy-middleware": "3.0.3",
|
"http-proxy-middleware": "3.0.5",
|
||||||
"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",
|
||||||
|
|||||||
@@ -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 2 - Define Alert Conditions",
|
"alert_form_step2": "Step {{step}} - Define Alert Conditions",
|
||||||
"alert_form_step3": "Step 3 - Alert Configuration",
|
"alert_form_step3": "Step {{step}} - 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",
|
||||||
|
|||||||
@@ -9,8 +9,8 @@
|
|||||||
"tooltip_notification_channels": "More details on how to setting notification channels",
|
"tooltip_notification_channels": "More details on how to setting notification channels",
|
||||||
"sending_channels_note": "The alerts will be sent to all the configured channels.",
|
"sending_channels_note": "The alerts will be sent to all the configured channels.",
|
||||||
"loading_channels_message": "Loading Channels..",
|
"loading_channels_message": "Loading Channels..",
|
||||||
"page_title_create": "New Notification Channels",
|
"page_title_create": "New Notification Channel",
|
||||||
"page_title_edit": "Edit Notification Channels",
|
"page_title_edit": "Edit Notification Channel",
|
||||||
"button_save_channel": "Save",
|
"button_save_channel": "Save",
|
||||||
"button_test_channel": "Test",
|
"button_test_channel": "Test",
|
||||||
"button_return": "Back",
|
"button_return": "Back",
|
||||||
@@ -62,5 +62,8 @@
|
|||||||
"channel_test_failed": "Failed to send a test message to this channel, please confirm that the parameters are set correctly",
|
"channel_test_failed": "Failed to send a test message to this channel, please confirm that the parameters are set correctly",
|
||||||
"channel_test_unexpected": "An unexpected error occurred while sending a message to this channel, please try again",
|
"channel_test_unexpected": "An unexpected error occurred while sending a message to this channel, please try again",
|
||||||
"webhook_url_required": "Webhook URL is mandatory",
|
"webhook_url_required": "Webhook URL is mandatory",
|
||||||
"slack_channel_help": "Specify channel or user, use #channel-name, @username (has to be all lowercase, no whitespace)"
|
"slack_channel_help": "Specify channel or user, use #channel-name, @username (has to be all lowercase, no whitespace)",
|
||||||
|
"api_key_required": "API Key is mandatory",
|
||||||
|
"to_required": "To field is mandatory",
|
||||||
|
"channel_name_required": "Channel name is mandatory"
|
||||||
}
|
}
|
||||||
@@ -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 2 - Define Alert Conditions",
|
"alert_form_step2": "Step {{step}} - Define Alert Conditions",
|
||||||
"alert_form_step3": "Step 3 - Alert Configuration",
|
"alert_form_step3": "Step {{step}} - 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",
|
||||||
|
|||||||
@@ -129,5 +129,6 @@
|
|||||||
"text_num_points": "data points in each result group",
|
"text_num_points": "data points in each result group",
|
||||||
"text_alert_frequency": "Run alert every",
|
"text_alert_frequency": "Run alert every",
|
||||||
"text_for": "minutes",
|
"text_for": "minutes",
|
||||||
"selected_query_placeholder": "Select query"
|
"selected_query_placeholder": "Select query",
|
||||||
|
"alert_rule_not_found": "Alert Rule not found"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,8 +9,8 @@
|
|||||||
"tooltip_notification_channels": "More details on how to setting notification channels",
|
"tooltip_notification_channels": "More details on how to setting notification channels",
|
||||||
"sending_channels_note": "The alerts will be sent to all the configured channels.",
|
"sending_channels_note": "The alerts will be sent to all the configured channels.",
|
||||||
"loading_channels_message": "Loading Channels..",
|
"loading_channels_message": "Loading Channels..",
|
||||||
"page_title_create": "New Notification Channels",
|
"page_title_create": "New Notification Channel",
|
||||||
"page_title_edit": "Edit Notification Channels",
|
"page_title_edit": "Edit Notification Channel",
|
||||||
"button_save_channel": "Save",
|
"button_save_channel": "Save",
|
||||||
"button_test_channel": "Test",
|
"button_test_channel": "Test",
|
||||||
"button_return": "Back",
|
"button_return": "Back",
|
||||||
@@ -77,5 +77,8 @@
|
|||||||
"channel_test_failed": "Failed to send a test message to this channel, please confirm that the parameters are set correctly",
|
"channel_test_failed": "Failed to send a test message to this channel, please confirm that the parameters are set correctly",
|
||||||
"channel_test_unexpected": "An unexpected error occurred while sending a message to this channel, please try again",
|
"channel_test_unexpected": "An unexpected error occurred while sending a message to this channel, please try again",
|
||||||
"webhook_url_required": "Webhook URL is mandatory",
|
"webhook_url_required": "Webhook URL is mandatory",
|
||||||
"slack_channel_help": "Specify channel or user, use #channel-name, @username (has to be all lowercase, no whitespace)"
|
"slack_channel_help": "Specify channel or user, use #channel-name, @username (has to be all lowercase, no whitespace)",
|
||||||
|
"api_key_required": "API Key is mandatory",
|
||||||
|
"to_required": "To field is mandatory",
|
||||||
|
"channel_name_required": "Channel name is mandatory"
|
||||||
}
|
}
|
||||||
@@ -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 2 - Define Alert Conditions",
|
"alert_form_step2": "Step {{step}} - Define Alert Conditions",
|
||||||
"alert_form_step3": "Step 3 - Alert Configuration",
|
"alert_form_step3": "Step {{step}} - 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",
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import setLocalStorageApi from 'api/browser/localstorage/set';
|
|||||||
import getAll from 'api/v1/user/get';
|
import getAll from 'api/v1/user/get';
|
||||||
import { FeatureKeys } from 'constants/features';
|
import { FeatureKeys } from 'constants/features';
|
||||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||||
|
import { ORG_PREFERENCES } from 'constants/orgPreferences';
|
||||||
import ROUTES from 'constants/routes';
|
import ROUTES from 'constants/routes';
|
||||||
import { useGetTenantLicense } from 'hooks/useGetTenantLicense';
|
import { useGetTenantLicense } from 'hooks/useGetTenantLicense';
|
||||||
import history from 'lib/history';
|
import history from 'lib/history';
|
||||||
@@ -14,6 +15,7 @@ import { matchPath, useLocation } from 'react-router-dom';
|
|||||||
import { SuccessResponseV2 } from 'types/api';
|
import { SuccessResponseV2 } from 'types/api';
|
||||||
import APIError from 'types/api/error';
|
import APIError from 'types/api/error';
|
||||||
import { LicensePlatform, LicenseState } from 'types/api/licensesV3/getActive';
|
import { LicensePlatform, LicenseState } from 'types/api/licensesV3/getActive';
|
||||||
|
import { OrgPreference } from 'types/api/preferences/preference';
|
||||||
import { Organization } from 'types/api/user/getOrganization';
|
import { Organization } from 'types/api/user/getOrganization';
|
||||||
import { UserResponse } from 'types/api/user/getUser';
|
import { UserResponse } from 'types/api/user/getUser';
|
||||||
import { USER_ROLES } from 'types/roles';
|
import { USER_ROLES } from 'types/roles';
|
||||||
@@ -95,7 +97,8 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element {
|
|||||||
usersData.data
|
usersData.data
|
||||||
) {
|
) {
|
||||||
const isOnboardingComplete = orgPreferences?.find(
|
const isOnboardingComplete = orgPreferences?.find(
|
||||||
(preference: Record<string, any>) => preference.name === 'org_onboarding',
|
(preference: OrgPreference) =>
|
||||||
|
preference.name === ORG_PREFERENCES.ORG_ONBOARDING,
|
||||||
)?.value;
|
)?.value;
|
||||||
|
|
||||||
const isFirstUser = checkFirstTimeUser();
|
const isFirstUser = checkFirstTimeUser();
|
||||||
@@ -123,7 +126,8 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element {
|
|||||||
|
|
||||||
const isRouteEnabledForWorkspaceBlockedState =
|
const isRouteEnabledForWorkspaceBlockedState =
|
||||||
isAdmin &&
|
isAdmin &&
|
||||||
(path === ROUTES.ORG_SETTINGS ||
|
(path === ROUTES.SETTINGS ||
|
||||||
|
path === ROUTES.ORG_SETTINGS ||
|
||||||
path === ROUTES.BILLING ||
|
path === ROUTES.BILLING ||
|
||||||
path === ROUTES.MY_SETTINGS);
|
path === ROUTES.MY_SETTINGS);
|
||||||
|
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ function App(): JSX.Element {
|
|||||||
const orgName =
|
const orgName =
|
||||||
org && Array.isArray(org) && org.length > 0 ? org[0].displayName : '';
|
org && Array.isArray(org) && org.length > 0 ? org[0].displayName : '';
|
||||||
|
|
||||||
const { displayName, email, role } = user;
|
const { displayName, email, role, id, orgId } = user;
|
||||||
|
|
||||||
const domain = extractDomain(email);
|
const domain = extractDomain(email);
|
||||||
const hostNameParts = hostname.split('.');
|
const hostNameParts = hostname.split('.');
|
||||||
@@ -105,7 +105,7 @@ function App(): JSX.Element {
|
|||||||
logEvent('Domain Identified', groupTraits, 'group');
|
logEvent('Domain Identified', groupTraits, 'group');
|
||||||
}
|
}
|
||||||
if (window && window.Appcues) {
|
if (window && window.Appcues) {
|
||||||
window.Appcues.identify(email, {
|
window.Appcues.identify(id, {
|
||||||
name: displayName,
|
name: displayName,
|
||||||
|
|
||||||
tenant_id: hostNameParts[0],
|
tenant_id: hostNameParts[0],
|
||||||
@@ -131,7 +131,7 @@ function App(): JSX.Element {
|
|||||||
isPaidUser: !!trialInfo?.trialConvertedToSubscription,
|
isPaidUser: !!trialInfo?.trialConvertedToSubscription,
|
||||||
});
|
});
|
||||||
|
|
||||||
posthog?.identify(email, {
|
posthog?.identify(id, {
|
||||||
email,
|
email,
|
||||||
name: displayName,
|
name: displayName,
|
||||||
orgName,
|
orgName,
|
||||||
@@ -143,7 +143,7 @@ function App(): JSX.Element {
|
|||||||
isPaidUser: !!trialInfo?.trialConvertedToSubscription,
|
isPaidUser: !!trialInfo?.trialConvertedToSubscription,
|
||||||
});
|
});
|
||||||
|
|
||||||
posthog?.group('company', domain, {
|
posthog?.group('company', orgId, {
|
||||||
name: orgName,
|
name: orgName,
|
||||||
tenant_id: hostNameParts[0],
|
tenant_id: hostNameParts[0],
|
||||||
data_region: hostNameParts[1],
|
data_region: hostNameParts[1],
|
||||||
@@ -191,19 +191,22 @@ 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?.path !== ROUTES.BILLING,
|
(route) =>
|
||||||
|
route?.path !== ROUTES.BILLING && route?.path !== ROUTES.INTEGRATIONS,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (isEnterpriseSelfHostedUser) {
|
|
||||||
updatedRoutes.push(LIST_LICENSES);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isEnterpriseSelfHostedUser) {
|
||||||
|
updatedRoutes.push(LIST_LICENSES);
|
||||||
|
}
|
||||||
|
|
||||||
// always add support route for cloud users
|
// always add support route for cloud users
|
||||||
updatedRoutes = [...updatedRoutes, SUPPORT_ROUTE];
|
updatedRoutes = [...updatedRoutes, SUPPORT_ROUTE];
|
||||||
} 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?.path !== ROUTES.BILLING,
|
(route) =>
|
||||||
|
route?.path !== ROUTES.BILLING && route?.path !== ROUTES.INTEGRATIONS,
|
||||||
);
|
);
|
||||||
updatedRoutes = [...updatedRoutes, LIST_LICENSES];
|
updatedRoutes = [...updatedRoutes, LIST_LICENSES];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -128,12 +128,7 @@ export const AlertOverview = Loadable(
|
|||||||
);
|
);
|
||||||
|
|
||||||
export const CreateAlertChannelAlerts = Loadable(
|
export const CreateAlertChannelAlerts = Loadable(
|
||||||
() =>
|
() => import(/* webpackChunkName: "Create Channels" */ 'pages/Settings'),
|
||||||
import(/* webpackChunkName: "Create Channels" */ 'pages/AlertChannelCreate'),
|
|
||||||
);
|
|
||||||
|
|
||||||
export const EditAlertChannelsAlerts = Loadable(
|
|
||||||
() => import(/* webpackChunkName: "Edit Channels" */ 'pages/ChannelsEdit'),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
export const AllAlertChannels = Loadable(
|
export const AllAlertChannels = Loadable(
|
||||||
@@ -165,7 +160,7 @@ export const APIKeys = Loadable(
|
|||||||
);
|
);
|
||||||
|
|
||||||
export const MySettings = Loadable(
|
export const MySettings = Loadable(
|
||||||
() => import(/* webpackChunkName: "All MySettings" */ 'pages/MySettings'),
|
() => import(/* webpackChunkName: "All MySettings" */ 'pages/Settings'),
|
||||||
);
|
);
|
||||||
|
|
||||||
export const CustomDomainSettings = Loadable(
|
export const CustomDomainSettings = Loadable(
|
||||||
@@ -222,7 +217,7 @@ export const LogsIndexToFields = Loadable(
|
|||||||
);
|
);
|
||||||
|
|
||||||
export const BillingPage = Loadable(
|
export const BillingPage = Loadable(
|
||||||
() => import(/* webpackChunkName: "BillingPage" */ 'pages/Billing'),
|
() => import(/* webpackChunkName: "BillingPage" */ 'pages/Settings'),
|
||||||
);
|
);
|
||||||
|
|
||||||
export const SupportPage = Loadable(
|
export const SupportPage = Loadable(
|
||||||
@@ -249,7 +244,7 @@ export const WorkspaceAccessRestricted = Loadable(
|
|||||||
);
|
);
|
||||||
|
|
||||||
export const ShortcutsPage = Loadable(
|
export const ShortcutsPage = Loadable(
|
||||||
() => import(/* webpackChunkName: "ShortcutsPage" */ 'pages/Shortcuts'),
|
() => import(/* webpackChunkName: "ShortcutsPage" */ 'pages/Settings'),
|
||||||
);
|
);
|
||||||
|
|
||||||
export const InstalledIntegrations = Loadable(
|
export const InstalledIntegrations = Loadable(
|
||||||
|
|||||||
@@ -7,20 +7,15 @@ import {
|
|||||||
AlertOverview,
|
AlertOverview,
|
||||||
AllAlertChannels,
|
AllAlertChannels,
|
||||||
AllErrors,
|
AllErrors,
|
||||||
APIKeys,
|
|
||||||
ApiMonitoring,
|
ApiMonitoring,
|
||||||
BillingPage,
|
|
||||||
CreateAlertChannelAlerts,
|
CreateAlertChannelAlerts,
|
||||||
CreateNewAlerts,
|
CreateNewAlerts,
|
||||||
CustomDomainSettings,
|
|
||||||
DashboardPage,
|
DashboardPage,
|
||||||
DashboardWidget,
|
DashboardWidget,
|
||||||
EditAlertChannelsAlerts,
|
|
||||||
EditRulesPage,
|
EditRulesPage,
|
||||||
ErrorDetails,
|
ErrorDetails,
|
||||||
Home,
|
Home,
|
||||||
InfrastructureMonitoring,
|
InfrastructureMonitoring,
|
||||||
IngestionSettings,
|
|
||||||
InstalledIntegrations,
|
InstalledIntegrations,
|
||||||
LicensePage,
|
LicensePage,
|
||||||
ListAllALertsPage,
|
ListAllALertsPage,
|
||||||
@@ -31,12 +26,10 @@ import {
|
|||||||
LogsIndexToFields,
|
LogsIndexToFields,
|
||||||
LogsSaveViews,
|
LogsSaveViews,
|
||||||
MetricsExplorer,
|
MetricsExplorer,
|
||||||
MySettings,
|
|
||||||
NewDashboardPage,
|
NewDashboardPage,
|
||||||
OldLogsExplorer,
|
OldLogsExplorer,
|
||||||
Onboarding,
|
Onboarding,
|
||||||
OnboardingV2,
|
OnboardingV2,
|
||||||
OrganizationSettings,
|
|
||||||
OrgOnboarding,
|
OrgOnboarding,
|
||||||
PasswordReset,
|
PasswordReset,
|
||||||
PipelinePage,
|
PipelinePage,
|
||||||
@@ -45,7 +38,6 @@ import {
|
|||||||
ServicesTablePage,
|
ServicesTablePage,
|
||||||
ServiceTopLevelOperationsPage,
|
ServiceTopLevelOperationsPage,
|
||||||
SettingsPage,
|
SettingsPage,
|
||||||
ShortcutsPage,
|
|
||||||
SignupPage,
|
SignupPage,
|
||||||
SomethingWentWrong,
|
SomethingWentWrong,
|
||||||
StatusPage,
|
StatusPage,
|
||||||
@@ -150,7 +142,7 @@ const routes: AppRoutes[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: ROUTES.SETTINGS,
|
path: ROUTES.SETTINGS,
|
||||||
exact: true,
|
exact: false,
|
||||||
component: SettingsPage,
|
component: SettingsPage,
|
||||||
isPrivate: true,
|
isPrivate: true,
|
||||||
key: 'SETTINGS',
|
key: 'SETTINGS',
|
||||||
@@ -260,13 +252,6 @@ const routes: AppRoutes[] = [
|
|||||||
isPrivate: true,
|
isPrivate: true,
|
||||||
key: 'CHANNELS_NEW',
|
key: 'CHANNELS_NEW',
|
||||||
},
|
},
|
||||||
{
|
|
||||||
path: ROUTES.CHANNELS_EDIT,
|
|
||||||
exact: true,
|
|
||||||
component: EditAlertChannelsAlerts,
|
|
||||||
isPrivate: true,
|
|
||||||
key: 'CHANNELS_EDIT',
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
path: ROUTES.ALL_CHANNELS,
|
path: ROUTES.ALL_CHANNELS,
|
||||||
exact: true,
|
exact: true,
|
||||||
@@ -295,41 +280,6 @@ const routes: AppRoutes[] = [
|
|||||||
isPrivate: true,
|
isPrivate: true,
|
||||||
key: 'VERSION',
|
key: 'VERSION',
|
||||||
},
|
},
|
||||||
{
|
|
||||||
path: ROUTES.ORG_SETTINGS,
|
|
||||||
exact: true,
|
|
||||||
component: OrganizationSettings,
|
|
||||||
isPrivate: true,
|
|
||||||
key: 'ORG_SETTINGS',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: ROUTES.INGESTION_SETTINGS,
|
|
||||||
exact: true,
|
|
||||||
component: IngestionSettings,
|
|
||||||
isPrivate: true,
|
|
||||||
key: 'INGESTION_SETTINGS',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: ROUTES.API_KEYS,
|
|
||||||
exact: true,
|
|
||||||
component: APIKeys,
|
|
||||||
isPrivate: true,
|
|
||||||
key: 'API_KEYS',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: ROUTES.MY_SETTINGS,
|
|
||||||
exact: true,
|
|
||||||
component: MySettings,
|
|
||||||
isPrivate: true,
|
|
||||||
key: 'MY_SETTINGS',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: ROUTES.CUSTOM_DOMAIN_SETTINGS,
|
|
||||||
exact: true,
|
|
||||||
component: CustomDomainSettings,
|
|
||||||
isPrivate: true,
|
|
||||||
key: 'CUSTOM_DOMAIN_SETTINGS',
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
path: ROUTES.LOGS,
|
path: ROUTES.LOGS,
|
||||||
exact: true,
|
exact: true,
|
||||||
@@ -393,13 +343,6 @@ const routes: AppRoutes[] = [
|
|||||||
key: 'SOMETHING_WENT_WRONG',
|
key: 'SOMETHING_WENT_WRONG',
|
||||||
isPrivate: false,
|
isPrivate: false,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
path: ROUTES.BILLING,
|
|
||||||
exact: true,
|
|
||||||
component: BillingPage,
|
|
||||||
key: 'BILLING',
|
|
||||||
isPrivate: true,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
path: ROUTES.WORKSPACE_LOCKED,
|
path: ROUTES.WORKSPACE_LOCKED,
|
||||||
exact: true,
|
exact: true,
|
||||||
@@ -421,13 +364,6 @@ const routes: AppRoutes[] = [
|
|||||||
isPrivate: true,
|
isPrivate: true,
|
||||||
key: 'WORKSPACE_ACCESS_RESTRICTED',
|
key: 'WORKSPACE_ACCESS_RESTRICTED',
|
||||||
},
|
},
|
||||||
{
|
|
||||||
path: ROUTES.SHORTCUTS,
|
|
||||||
exact: true,
|
|
||||||
component: ShortcutsPage,
|
|
||||||
isPrivate: true,
|
|
||||||
key: 'SHORTCUTS',
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
path: ROUTES.INTEGRATIONS,
|
path: ROUTES.INTEGRATIONS,
|
||||||
exact: true,
|
exact: true,
|
||||||
|
|||||||
@@ -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/';
|
||||||
|
|||||||
29
frontend/src/api/changelog/getChangelogByVersion.ts
Normal file
29
frontend/src/api/changelog/getChangelogByVersion.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
||||||
|
import axios, { AxiosError } from 'axios';
|
||||||
|
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||||
|
import { ChangelogSchema } from 'types/api/changelog/getChangelogByVersion';
|
||||||
|
|
||||||
|
const getChangelogByVersion = async (
|
||||||
|
versionId: string,
|
||||||
|
): Promise<SuccessResponse<ChangelogSchema> | ErrorResponse> => {
|
||||||
|
try {
|
||||||
|
const response = await axios.get(`
|
||||||
|
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) {
|
||||||
|
throw new Error('No changelog found!');
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
statusCode: 200,
|
||||||
|
error: null,
|
||||||
|
message: response.statusText,
|
||||||
|
payload: response.data.data[0],
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return ErrorResponseHandler(error as AxiosError);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default getChangelogByVersion;
|
||||||
@@ -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}`,
|
||||||
|
|||||||
22
frontend/src/api/querySuggestions/getKeySuggestions.ts
Normal file
22
frontend/src/api/querySuggestions/getKeySuggestions.ts
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import axios from 'api';
|
||||||
|
import { AxiosResponse } from 'axios';
|
||||||
|
import {
|
||||||
|
QueryKeyRequestProps,
|
||||||
|
QueryKeySuggestionsResponseProps,
|
||||||
|
} from 'types/api/querySuggestions/types';
|
||||||
|
|
||||||
|
export const getKeySuggestions = (
|
||||||
|
props: QueryKeyRequestProps,
|
||||||
|
): Promise<AxiosResponse<QueryKeySuggestionsResponseProps>> => {
|
||||||
|
const {
|
||||||
|
signal = '',
|
||||||
|
searchText = '',
|
||||||
|
metricName = '',
|
||||||
|
fieldContext = '',
|
||||||
|
fieldDataType = '',
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
return axios.get(
|
||||||
|
`/fields/keys?signal=${signal}&searchText=${searchText}&metricName=${metricName}&fieldContext=${fieldContext}&fieldDataType=${fieldDataType}`,
|
||||||
|
);
|
||||||
|
};
|
||||||
20
frontend/src/api/querySuggestions/getValueSuggestion.ts
Normal file
20
frontend/src/api/querySuggestions/getValueSuggestion.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import axios from 'api';
|
||||||
|
import { AxiosResponse } from 'axios';
|
||||||
|
import {
|
||||||
|
QueryKeyValueRequestProps,
|
||||||
|
QueryKeyValueSuggestionsResponseProps,
|
||||||
|
} from 'types/api/querySuggestions/types';
|
||||||
|
|
||||||
|
export const getValueSuggestions = (
|
||||||
|
props: QueryKeyValueRequestProps,
|
||||||
|
): Promise<AxiosResponse<QueryKeyValueSuggestionsResponseProps>> => {
|
||||||
|
const { signal, key, searchText } = props;
|
||||||
|
|
||||||
|
const encodedSignal = encodeURIComponent(signal);
|
||||||
|
const encodedKey = encodeURIComponent(key);
|
||||||
|
const encodedSearchText = encodeURIComponent(searchText);
|
||||||
|
|
||||||
|
return axios.get(
|
||||||
|
`/fields/values?signal=${encodedSignal}&name=${encodedKey}&searchText=${encodedSearchText}`,
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -119,6 +119,7 @@ export const updateFunnelSteps = async (
|
|||||||
export interface ValidateFunnelPayload {
|
export interface ValidateFunnelPayload {
|
||||||
start_time: number;
|
start_time: number;
|
||||||
end_time: number;
|
end_time: number;
|
||||||
|
steps: FunnelStepData[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ValidateFunnelResponse {
|
export interface ValidateFunnelResponse {
|
||||||
@@ -132,12 +133,11 @@ export interface ValidateFunnelResponse {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const validateFunnelSteps = async (
|
export const validateFunnelSteps = async (
|
||||||
funnelId: string,
|
|
||||||
payload: ValidateFunnelPayload,
|
payload: ValidateFunnelPayload,
|
||||||
signal?: AbortSignal,
|
signal?: AbortSignal,
|
||||||
): Promise<SuccessResponse<ValidateFunnelResponse> | ErrorResponse> => {
|
): Promise<SuccessResponse<ValidateFunnelResponse> | ErrorResponse> => {
|
||||||
const response = await axios.post(
|
const response = await axios.post(
|
||||||
`${FUNNELS_BASE_PATH}/${funnelId}/analytics/validate`,
|
`${FUNNELS_BASE_PATH}/analytics/validate`,
|
||||||
payload,
|
payload,
|
||||||
{ signal },
|
{ signal },
|
||||||
);
|
);
|
||||||
@@ -185,6 +185,7 @@ export interface FunnelOverviewPayload {
|
|||||||
end_time: number;
|
end_time: number;
|
||||||
step_start?: number;
|
step_start?: number;
|
||||||
step_end?: number;
|
step_end?: number;
|
||||||
|
steps: FunnelStepData[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface FunnelOverviewResponse {
|
export interface FunnelOverviewResponse {
|
||||||
@@ -202,12 +203,11 @@ export interface FunnelOverviewResponse {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const getFunnelOverview = async (
|
export const getFunnelOverview = async (
|
||||||
funnelId: string,
|
|
||||||
payload: FunnelOverviewPayload,
|
payload: FunnelOverviewPayload,
|
||||||
signal?: AbortSignal,
|
signal?: AbortSignal,
|
||||||
): Promise<SuccessResponse<FunnelOverviewResponse> | ErrorResponse> => {
|
): Promise<SuccessResponse<FunnelOverviewResponse> | ErrorResponse> => {
|
||||||
const response = await axios.post(
|
const response = await axios.post(
|
||||||
`${FUNNELS_BASE_PATH}/${funnelId}/analytics/overview`,
|
`${FUNNELS_BASE_PATH}/analytics/overview`,
|
||||||
payload,
|
payload,
|
||||||
{
|
{
|
||||||
signal,
|
signal,
|
||||||
@@ -235,12 +235,11 @@ export interface SlowTraceData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const getFunnelSlowTraces = async (
|
export const getFunnelSlowTraces = async (
|
||||||
funnelId: string,
|
|
||||||
payload: FunnelOverviewPayload,
|
payload: FunnelOverviewPayload,
|
||||||
signal?: AbortSignal,
|
signal?: AbortSignal,
|
||||||
): Promise<SuccessResponse<SlowTraceData> | ErrorResponse> => {
|
): Promise<SuccessResponse<SlowTraceData> | ErrorResponse> => {
|
||||||
const response = await axios.post(
|
const response = await axios.post(
|
||||||
`${FUNNELS_BASE_PATH}/${funnelId}/analytics/slow-traces`,
|
`${FUNNELS_BASE_PATH}/analytics/slow-traces`,
|
||||||
payload,
|
payload,
|
||||||
{
|
{
|
||||||
signal,
|
signal,
|
||||||
@@ -273,7 +272,7 @@ export const getFunnelErrorTraces = async (
|
|||||||
signal?: AbortSignal,
|
signal?: AbortSignal,
|
||||||
): Promise<SuccessResponse<ErrorTraceData> | ErrorResponse> => {
|
): Promise<SuccessResponse<ErrorTraceData> | ErrorResponse> => {
|
||||||
const response: AxiosResponse = await axios.post(
|
const response: AxiosResponse = await axios.post(
|
||||||
`${FUNNELS_BASE_PATH}/${funnelId}/analytics/error-traces`,
|
`${FUNNELS_BASE_PATH}/analytics/error-traces`,
|
||||||
payload,
|
payload,
|
||||||
{
|
{
|
||||||
signal,
|
signal,
|
||||||
@@ -291,6 +290,7 @@ export const getFunnelErrorTraces = async (
|
|||||||
export interface FunnelStepsPayload {
|
export interface FunnelStepsPayload {
|
||||||
start_time: number;
|
start_time: number;
|
||||||
end_time: number;
|
end_time: number;
|
||||||
|
steps: FunnelStepData[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface FunnelStepGraphMetrics {
|
export interface FunnelStepGraphMetrics {
|
||||||
@@ -307,12 +307,11 @@ export interface FunnelStepsResponse {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const getFunnelSteps = async (
|
export const getFunnelSteps = async (
|
||||||
funnelId: string,
|
|
||||||
payload: FunnelStepsPayload,
|
payload: FunnelStepsPayload,
|
||||||
signal?: AbortSignal,
|
signal?: AbortSignal,
|
||||||
): Promise<SuccessResponse<FunnelStepsResponse> | ErrorResponse> => {
|
): Promise<SuccessResponse<FunnelStepsResponse> | ErrorResponse> => {
|
||||||
const response = await axios.post(
|
const response = await axios.post(
|
||||||
`${FUNNELS_BASE_PATH}/${funnelId}/analytics/steps`,
|
`${FUNNELS_BASE_PATH}/analytics/steps`,
|
||||||
payload,
|
payload,
|
||||||
{ signal },
|
{ signal },
|
||||||
);
|
);
|
||||||
@@ -330,6 +329,7 @@ export interface FunnelStepsOverviewPayload {
|
|||||||
end_time: number;
|
end_time: number;
|
||||||
step_start?: number;
|
step_start?: number;
|
||||||
step_end?: number;
|
step_end?: number;
|
||||||
|
steps: FunnelStepData[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface FunnelStepsOverviewResponse {
|
export interface FunnelStepsOverviewResponse {
|
||||||
@@ -341,12 +341,11 @@ export interface FunnelStepsOverviewResponse {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const getFunnelStepsOverview = async (
|
export const getFunnelStepsOverview = async (
|
||||||
funnelId: string,
|
|
||||||
payload: FunnelStepsOverviewPayload,
|
payload: FunnelStepsOverviewPayload,
|
||||||
signal?: AbortSignal,
|
signal?: AbortSignal,
|
||||||
): Promise<SuccessResponse<FunnelStepsOverviewResponse> | ErrorResponse> => {
|
): Promise<SuccessResponse<FunnelStepsOverviewResponse> | ErrorResponse> => {
|
||||||
const response = await axios.post(
|
const response = await axios.post(
|
||||||
`${FUNNELS_BASE_PATH}/${funnelId}/analytics/steps/overview`,
|
`${FUNNELS_BASE_PATH}/analytics/steps/overview`,
|
||||||
payload,
|
payload,
|
||||||
{ signal },
|
{ signal },
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,21 +0,0 @@
|
|||||||
import axios from 'api';
|
|
||||||
import { Props } from 'types/api/userFeedback/sendResponse';
|
|
||||||
|
|
||||||
const sendFeedback = async (props: Props): Promise<number> => {
|
|
||||||
const response = await axios.post(
|
|
||||||
'/feedback',
|
|
||||||
{
|
|
||||||
email: props.email,
|
|
||||||
message: props.message,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/x-www-form-urlencoded',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
return response.status;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default sendFeedback;
|
|
||||||
168
frontend/src/api/v5/queryRange/constants.ts
Normal file
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;
|
||||||
367
frontend/src/api/v5/queryRange/convertV5Response.ts
Normal file
367
frontend/src/api/v5/queryRange/convertV5Response.ts
Normal file
@@ -0,0 +1,367 @@
|
|||||||
|
import { isEmpty } from 'lodash-es';
|
||||||
|
import { SuccessResponse } from 'types/api';
|
||||||
|
import { MetricRangePayloadV3 } from 'types/api/metrics/getQueryRange';
|
||||||
|
import {
|
||||||
|
DistributionData,
|
||||||
|
MetricRangePayloadV5,
|
||||||
|
QueryRangeRequestV5,
|
||||||
|
RawData,
|
||||||
|
ScalarData,
|
||||||
|
TimeSeriesData,
|
||||||
|
} from 'types/api/v5/queryRange';
|
||||||
|
import { QueryDataV3 } from 'types/api/widgets/getQuery';
|
||||||
|
|
||||||
|
function getColName(
|
||||||
|
col: ScalarData['columns'][number],
|
||||||
|
legendMap: Record<string, string>,
|
||||||
|
aggregationPerQuery: Record<string, any>,
|
||||||
|
): string {
|
||||||
|
const aggregation =
|
||||||
|
aggregationPerQuery?.[col.queryName]?.[col.aggregationIndex];
|
||||||
|
const legend = legendMap[col.queryName];
|
||||||
|
const aggregationName = aggregation?.alias || aggregation?.expression || '';
|
||||||
|
|
||||||
|
if (col.columnType === 'group') {
|
||||||
|
return col.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (aggregationName && aggregationPerQuery[col.queryName].length > 1) {
|
||||||
|
if (legend) {
|
||||||
|
return `${aggregationName}-${legend}`;
|
||||||
|
}
|
||||||
|
return `${col.queryName}.${aggregationName}`;
|
||||||
|
}
|
||||||
|
return legend || col.queryName;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts V5 TimeSeriesData to legacy format
|
||||||
|
*/
|
||||||
|
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) => {
|
||||||
|
const { index, alias, series } = aggregation;
|
||||||
|
|
||||||
|
if (!series || !series.length) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return series.map((series) => ({
|
||||||
|
labels: series.labels
|
||||||
|
? Object.fromEntries(
|
||||||
|
series.labels.map((label) => [label.key.name, label.value]),
|
||||||
|
)
|
||||||
|
: {},
|
||||||
|
labelsArray: series.labels
|
||||||
|
? series.labels.map((label) => ({ [label.key.name]: label.value }))
|
||||||
|
: [],
|
||||||
|
values: series.values.map((value) => ({
|
||||||
|
timestamp: value.timestamp,
|
||||||
|
value: String(value.value),
|
||||||
|
})),
|
||||||
|
metaData: {
|
||||||
|
alias,
|
||||||
|
index,
|
||||||
|
queryName: timeSeriesData.queryName,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
}),
|
||||||
|
list: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts V5 ScalarData array to legacy format with table structure
|
||||||
|
*/
|
||||||
|
function convertScalarDataArrayToTable(
|
||||||
|
scalarDataArray: ScalarData[],
|
||||||
|
legendMap: Record<string, string>,
|
||||||
|
aggregationPerQuery: Record<string, any>,
|
||||||
|
): 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 || '';
|
||||||
|
|
||||||
|
if ((scalarData as any)?.aggregations?.length > 0) {
|
||||||
|
return {
|
||||||
|
...convertTimeSeriesData(scalarData as any, legendMap),
|
||||||
|
table: {
|
||||||
|
columns: [],
|
||||||
|
rows: [],
|
||||||
|
},
|
||||||
|
list: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Collect columns for this specific query
|
||||||
|
const columns = scalarData?.columns?.map((col) => ({
|
||||||
|
name: getColName(col, legendMap, aggregationPerQuery),
|
||||||
|
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 = getColName(col, legendMap, aggregationPerQuery);
|
||||||
|
rowData[columnName] = dataRow[colIndex];
|
||||||
|
});
|
||||||
|
|
||||||
|
return { data: rowData };
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
queryName,
|
||||||
|
legend: legendMap[queryName] || '',
|
||||||
|
series: null,
|
||||||
|
list: null,
|
||||||
|
table: {
|
||||||
|
columns,
|
||||||
|
rows,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function convertScalerWithFormatForWeb(
|
||||||
|
scalarDataArray: ScalarData[],
|
||||||
|
legendMap: Record<string, string>,
|
||||||
|
aggregationPerQuery: Record<string, any>,
|
||||||
|
): QueryDataV3[] {
|
||||||
|
if (!scalarDataArray || scalarDataArray.length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return scalarDataArray.map((scalarData) => {
|
||||||
|
const columns =
|
||||||
|
scalarData.columns?.map((col) => {
|
||||||
|
const colName = getColName(col, legendMap, aggregationPerQuery);
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: colName,
|
||||||
|
queryName: col.queryName,
|
||||||
|
isValueColumn: col.columnType === 'aggregation',
|
||||||
|
};
|
||||||
|
}) || [];
|
||||||
|
|
||||||
|
const rows =
|
||||||
|
scalarData.data?.map((dataRow) => {
|
||||||
|
const rowData: Record<string, any> = {};
|
||||||
|
columns?.forEach((col, colIndex) => {
|
||||||
|
rowData[col.name] = dataRow[colIndex];
|
||||||
|
});
|
||||||
|
return { data: rowData };
|
||||||
|
}) || [];
|
||||||
|
|
||||||
|
const queryName = scalarData.columns?.[0]?.queryName || '';
|
||||||
|
|
||||||
|
return {
|
||||||
|
queryName,
|
||||||
|
legend: legendMap[queryName] || queryName,
|
||||||
|
series: null,
|
||||||
|
list: null,
|
||||||
|
table: {
|
||||||
|
columns,
|
||||||
|
rows,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts V5 RawData to legacy format
|
||||||
|
*/
|
||||||
|
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>,
|
||||||
|
aggregationPerQuery: Record<string, any>,
|
||||||
|
): 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,
|
||||||
|
aggregationPerQuery,
|
||||||
|
);
|
||||||
|
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
|
||||||
|
*/
|
||||||
|
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||||
|
export function convertV5ResponseToLegacy(
|
||||||
|
v5Response: SuccessResponse<MetricRangePayloadV5>,
|
||||||
|
legendMap: Record<string, string>,
|
||||||
|
formatForWeb?: boolean,
|
||||||
|
): SuccessResponse<MetricRangePayloadV3> {
|
||||||
|
const { payload, params } = v5Response;
|
||||||
|
const v5Data = payload?.data;
|
||||||
|
|
||||||
|
const aggregationPerQuery =
|
||||||
|
(params as QueryRangeRequestV5)?.compositeQuery?.queries
|
||||||
|
?.filter((query) => query.type === 'builder_query')
|
||||||
|
.reduce((acc, query) => {
|
||||||
|
if (
|
||||||
|
query.type === 'builder_query' &&
|
||||||
|
'aggregations' in query.spec &&
|
||||||
|
query.spec.name
|
||||||
|
) {
|
||||||
|
acc[query.spec.name] = query.spec.aggregations;
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
}, {} as Record<string, any>) || {};
|
||||||
|
|
||||||
|
// If formatForWeb is true, return as-is (like existing logic)
|
||||||
|
if (formatForWeb && v5Data?.type === 'scalar') {
|
||||||
|
const scalarData = v5Data.data.results as ScalarData[];
|
||||||
|
const webTables = convertScalerWithFormatForWeb(
|
||||||
|
scalarData,
|
||||||
|
legendMap,
|
||||||
|
aggregationPerQuery,
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
...v5Response,
|
||||||
|
payload: {
|
||||||
|
data: {
|
||||||
|
resultType: 'scalar',
|
||||||
|
result: webTables,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert based on V5 response type
|
||||||
|
const convertedData = convertV5DataByType(
|
||||||
|
v5Data,
|
||||||
|
legendMap,
|
||||||
|
aggregationPerQuery,
|
||||||
|
);
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
45
frontend/src/api/v5/queryRange/getQueryRange.ts
Normal file
45
frontend/src/api/v5/queryRange/getQueryRange.ts
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import { ApiV5Instance } from 'api';
|
||||||
|
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
|
||||||
|
import { AxiosError } from 'axios';
|
||||||
|
import { ENTITY_VERSION_V5 } from 'constants/app';
|
||||||
|
import { ErrorV2Resp, SuccessResponseV2 } 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<SuccessResponseV2<MetricRangePayloadV5>> => {
|
||||||
|
try {
|
||||||
|
if (version && version === ENTITY_VERSION_V5) {
|
||||||
|
const response = await ApiV5Instance.post('/query_range', props, {
|
||||||
|
signal,
|
||||||
|
headers,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
httpStatusCode: response.status,
|
||||||
|
data: response.data,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default V5 behavior
|
||||||
|
const response = await ApiV5Instance.post('/query_range', props, {
|
||||||
|
signal,
|
||||||
|
headers,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
httpStatusCode: response.status,
|
||||||
|
data: response.data.data,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default getQueryRangeV5;
|
||||||
408
frontend/src/api/v5/queryRange/prepareQueryRangePayloadV5.ts
Normal file
408
frontend/src/api/v5/queryRange/prepareQueryRangePayloadV5.ts
Normal file
@@ -0,0 +1,408 @@
|
|||||||
|
/* 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 { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||||
|
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 {
|
||||||
|
const nonEmptySelectColumns = (queryData.selectColumns as (
|
||||||
|
| BaseAutocompleteData
|
||||||
|
| TelemetryFieldKey
|
||||||
|
)[])?.filter((c) => ('key' in c ? c?.key : c?.name));
|
||||||
|
|
||||||
|
return {
|
||||||
|
stepInterval: queryData?.stepInterval || undefined,
|
||||||
|
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(nonEmptySelectColumns)
|
||||||
|
? undefined
|
||||||
|
: nonEmptySelectColumns?.map(
|
||||||
|
(column: any): TelemetryFieldKey => ({
|
||||||
|
name: column.name ?? column.key,
|
||||||
|
fieldDataType:
|
||||||
|
column?.fieldDataType ?? (column?.dataType as FieldDataType),
|
||||||
|
fieldContext: column?.fieldContext ?? (column?.type as FieldContext),
|
||||||
|
signal: column?.signal ?? undefined,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// 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) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
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,
|
||||||
|
originalGraphType,
|
||||||
|
}: 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 ||
|
||||||
|
(originalGraphType
|
||||||
|
? originalGraphType === PANEL_TYPES.TABLE
|
||||||
|
: graphType === PANEL_TYPES.TABLE),
|
||||||
|
},
|
||||||
|
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
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';
|
||||||
@@ -0,0 +1,161 @@
|
|||||||
|
.changelog-modal {
|
||||||
|
.ant-modal-content {
|
||||||
|
padding: unset;
|
||||||
|
background-color: var(--bg-ink-400, #121317);
|
||||||
|
|
||||||
|
.ant-modal-header {
|
||||||
|
margin-bottom: unset;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-modal-footer {
|
||||||
|
margin-top: unset;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&-title {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
background-color: var(--bg-ink-400, #121317);
|
||||||
|
padding: 16px;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 20px;
|
||||||
|
color: var(--text-vanilla-100, #fff);
|
||||||
|
border-bottom: 1px solid var(--bg-slate-500, #161922);
|
||||||
|
}
|
||||||
|
|
||||||
|
&-footer.scroll-available {
|
||||||
|
.scroll-btn-container {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&-footer {
|
||||||
|
position: relative;
|
||||||
|
border: 1px solid var(--bg-slate-500, #161922);
|
||||||
|
padding: 12px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
|
||||||
|
&-label {
|
||||||
|
color: var(--text-robin-400, #7190f9);
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 24px;
|
||||||
|
position: relative;
|
||||||
|
padding-left: 14px;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 0;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
border-radius: 100%;
|
||||||
|
background-color: var(--bg-robin-500, #7190f9);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&-ctas {
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
& svg {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.scroll-btn-container {
|
||||||
|
display: none;
|
||||||
|
position: absolute;
|
||||||
|
top: -40px;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
|
||||||
|
.scroll-btn {
|
||||||
|
all: unset;
|
||||||
|
padding: 4px 12px 4px 10px;
|
||||||
|
background-color: var(--bg-slate-400, #1d212d);
|
||||||
|
border-radius: 20px;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 4px;
|
||||||
|
transition: background-color 0.1s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--bg-slate-200, #2c3140);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
background-color: var(--bg-slate-600, #1c1f2a);
|
||||||
|
}
|
||||||
|
|
||||||
|
span {
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 18px;
|
||||||
|
color: var(--text-vanilla-400, #c0c1c3);
|
||||||
|
}
|
||||||
|
|
||||||
|
// add animation to the chevrons down icon
|
||||||
|
svg {
|
||||||
|
animation: pulse 1s infinite;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&-content {
|
||||||
|
max-height: calc(100vh - 300px);
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 16px;
|
||||||
|
border: 1px solid var(--bg-slate-500, #161922);
|
||||||
|
border-top-width: 0;
|
||||||
|
border-bottom-width: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// pulse for the scroll for more icon
|
||||||
|
@keyframes pulse {
|
||||||
|
0% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.lightMode {
|
||||||
|
.changelog-modal {
|
||||||
|
.ant-modal-content {
|
||||||
|
background-color: var(--bg-vanilla-100);
|
||||||
|
border-color: var(--bg-vanilla-300);
|
||||||
|
}
|
||||||
|
|
||||||
|
&-title {
|
||||||
|
background: var(--bg-vanilla-100);
|
||||||
|
color: var(--bg-ink-500);
|
||||||
|
border-color: var(--bg-vanilla-300);
|
||||||
|
}
|
||||||
|
|
||||||
|
&-content {
|
||||||
|
border-color: var(--bg-vanilla-300);
|
||||||
|
}
|
||||||
|
|
||||||
|
&-footer {
|
||||||
|
border-color: var(--bg-vanilla-300);
|
||||||
|
|
||||||
|
.scroll-btn-container {
|
||||||
|
.scroll-btn {
|
||||||
|
background-color: var(--bg-vanilla-300);
|
||||||
|
|
||||||
|
span {
|
||||||
|
color: var(--text-ink-500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
131
frontend/src/components/ChangelogModal/ChangelogModal.tsx
Normal file
131
frontend/src/components/ChangelogModal/ChangelogModal.tsx
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
import './ChangelogModal.styles.scss';
|
||||||
|
|
||||||
|
import { CheckOutlined, CloseOutlined } from '@ant-design/icons';
|
||||||
|
import { Button, Modal } from 'antd';
|
||||||
|
import cx from 'classnames';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
import { ChevronsDown, ScrollText } from 'lucide-react';
|
||||||
|
import { useAppContext } from 'providers/App/App';
|
||||||
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
|
|
||||||
|
import ChangelogRenderer from './components/ChangelogRenderer';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ChangelogModal({ onClose }: Props): JSX.Element {
|
||||||
|
const [hasScroll, setHasScroll] = useState(false);
|
||||||
|
const changelogContentSectionRef = useRef<HTMLDivElement>(null);
|
||||||
|
const { changelog } = useAppContext();
|
||||||
|
|
||||||
|
const formattedReleaseDate = dayjs(changelog?.release_date).format(
|
||||||
|
'MMMM D, YYYY',
|
||||||
|
);
|
||||||
|
|
||||||
|
const checkScroll = useCallback((): void => {
|
||||||
|
if (changelogContentSectionRef.current) {
|
||||||
|
const {
|
||||||
|
scrollHeight,
|
||||||
|
clientHeight,
|
||||||
|
scrollTop,
|
||||||
|
} = changelogContentSectionRef.current;
|
||||||
|
const isAtBottom = scrollHeight - clientHeight - scrollTop <= 8;
|
||||||
|
setHasScroll(scrollHeight > clientHeight + 24 && !isAtBottom); // 24px - buffer height to show show more
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
checkScroll();
|
||||||
|
const changelogContentSection = changelogContentSectionRef.current;
|
||||||
|
|
||||||
|
if (changelogContentSection) {
|
||||||
|
changelogContentSection.addEventListener('scroll', checkScroll);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (): void => {
|
||||||
|
if (changelogContentSection) {
|
||||||
|
changelogContentSection.removeEventListener('scroll', checkScroll);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [checkScroll]);
|
||||||
|
|
||||||
|
const onClickUpdateWorkspace = (): void => {
|
||||||
|
window.open(
|
||||||
|
'https://github.com/SigNoz/signoz/releases',
|
||||||
|
'_blank',
|
||||||
|
'noopener,noreferrer',
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onClickScrollForMore = (): void => {
|
||||||
|
if (changelogContentSectionRef.current) {
|
||||||
|
changelogContentSectionRef.current.scrollTo({
|
||||||
|
top: changelogContentSectionRef.current.scrollTop + 600, // Scroll 600px from the current position
|
||||||
|
behavior: 'smooth',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
className={cx('changelog-modal')}
|
||||||
|
title={
|
||||||
|
<div className="changelog-modal-title">
|
||||||
|
<ScrollText size={16} />
|
||||||
|
What’s New ⎯ Changelog : {formattedReleaseDate}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
width={820}
|
||||||
|
open
|
||||||
|
onCancel={onClose}
|
||||||
|
footer={
|
||||||
|
<div
|
||||||
|
className={cx('changelog-modal-footer', hasScroll && 'scroll-available')}
|
||||||
|
>
|
||||||
|
{changelog?.features && changelog.features.length > 0 && (
|
||||||
|
<span className="changelog-modal-footer-label">
|
||||||
|
{changelog.features.length} new
|
||||||
|
{changelog.features.length > 1 ? 'features' : 'feature'}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
<div className="changelog-modal-footer-ctas">
|
||||||
|
<Button type="default" icon={<CloseOutlined />} onClick={onClose}>
|
||||||
|
Skip for now
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
icon={<CheckOutlined />}
|
||||||
|
onClick={onClickUpdateWorkspace}
|
||||||
|
>
|
||||||
|
Update my workspace
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
{changelog && (
|
||||||
|
<div className="scroll-btn-container">
|
||||||
|
<button
|
||||||
|
data-testid="scroll-more-btn"
|
||||||
|
type="button"
|
||||||
|
className="scroll-btn"
|
||||||
|
onClick={onClickScrollForMore}
|
||||||
|
>
|
||||||
|
<ChevronsDown size={14} />
|
||||||
|
<span>Scroll for more</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="changelog-modal-content"
|
||||||
|
data-testid="changelog-content"
|
||||||
|
ref={changelogContentSectionRef}
|
||||||
|
>
|
||||||
|
{changelog && <ChangelogRenderer changelog={changelog} />}
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ChangelogModal;
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
/* eslint-disable sonarjs/no-duplicate-string */
|
||||||
|
/* eslint-disable sonarjs/no-identical-functions */
|
||||||
|
/* eslint-disable @typescript-eslint/explicit-function-return-type */
|
||||||
|
|
||||||
|
import { fireEvent, render, screen } from '@testing-library/react';
|
||||||
|
|
||||||
|
import ChangelogModal from '../ChangelogModal';
|
||||||
|
|
||||||
|
const mockChangelog = {
|
||||||
|
release_date: '2025-06-10',
|
||||||
|
features: [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
title: 'Feature 1',
|
||||||
|
description: 'Description for feature 1',
|
||||||
|
media: null,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
bug_fixes: 'Bug fix details',
|
||||||
|
maintenance: 'Maintenance details',
|
||||||
|
};
|
||||||
|
|
||||||
|
// Mock react-markdown to just render children as plain text
|
||||||
|
jest.mock(
|
||||||
|
'react-markdown',
|
||||||
|
() =>
|
||||||
|
function ReactMarkdown({ children }: any) {
|
||||||
|
return <div>{children}</div>;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// mock useAppContext
|
||||||
|
jest.mock('providers/App/App', () => ({
|
||||||
|
useAppContext: jest.fn(() => ({ changelog: mockChangelog })),
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('ChangelogModal', () => {
|
||||||
|
it('renders modal with changelog data', () => {
|
||||||
|
render(<ChangelogModal onClose={jest.fn()} />);
|
||||||
|
expect(
|
||||||
|
screen.getByText('What’s New ⎯ Changelog : June 10, 2025'),
|
||||||
|
).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('Feature 1')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('Description for feature 1')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('Bug fix details')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('Maintenance details')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('calls onClose when Skip for now is clicked', () => {
|
||||||
|
const onClose = jest.fn();
|
||||||
|
render(<ChangelogModal onClose={onClose} />);
|
||||||
|
fireEvent.click(screen.getByText('Skip for now'));
|
||||||
|
expect(onClose).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('opens migration docs when Update my workspace is clicked', () => {
|
||||||
|
window.open = jest.fn();
|
||||||
|
render(<ChangelogModal onClose={jest.fn()} />);
|
||||||
|
fireEvent.click(screen.getByText('Update my workspace'));
|
||||||
|
expect(window.open).toHaveBeenCalledWith(
|
||||||
|
'https://github.com/SigNoz/signoz/releases',
|
||||||
|
'_blank',
|
||||||
|
'noopener,noreferrer',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('scrolls for more when Scroll for more is clicked', () => {
|
||||||
|
render(<ChangelogModal onClose={jest.fn()} />);
|
||||||
|
const scrollBtn = screen.getByTestId('scroll-more-btn');
|
||||||
|
const contentDiv = screen.getByTestId('changelog-content');
|
||||||
|
if (contentDiv) {
|
||||||
|
contentDiv.scrollTo = jest.fn();
|
||||||
|
}
|
||||||
|
fireEvent.click(scrollBtn);
|
||||||
|
if (contentDiv) {
|
||||||
|
expect(contentDiv.scrollTo).toHaveBeenCalled();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
/* eslint-disable sonarjs/no-duplicate-string */
|
||||||
|
/* eslint-disable sonarjs/no-identical-functions */
|
||||||
|
/* eslint-disable @typescript-eslint/explicit-function-return-type */
|
||||||
|
|
||||||
|
import { render, screen } from '@testing-library/react';
|
||||||
|
|
||||||
|
import ChangelogRenderer from '../components/ChangelogRenderer';
|
||||||
|
|
||||||
|
// Mock react-markdown to just render children as plain text
|
||||||
|
jest.mock(
|
||||||
|
'react-markdown',
|
||||||
|
() =>
|
||||||
|
function ReactMarkdown({ children }: any) {
|
||||||
|
return <div>{children}</div>;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const mockChangelog = {
|
||||||
|
id: 1,
|
||||||
|
documentId: 'changelog-doc-1',
|
||||||
|
version: '1.0.0',
|
||||||
|
createdAt: '2025-06-09T12:00:00Z',
|
||||||
|
release_date: '2025-06-10',
|
||||||
|
features: [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
documentId: '1',
|
||||||
|
title: 'Feature 1',
|
||||||
|
description: 'Description for feature 1',
|
||||||
|
sort_order: 1,
|
||||||
|
createdAt: '',
|
||||||
|
updatedAt: '',
|
||||||
|
publishedAt: '',
|
||||||
|
deployment_type: 'All',
|
||||||
|
media: {
|
||||||
|
id: 1,
|
||||||
|
documentId: 'doc1',
|
||||||
|
ext: '.webp',
|
||||||
|
url: '/uploads/feature1.webp',
|
||||||
|
mime: 'image/webp',
|
||||||
|
alternativeText: null,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
bug_fixes: 'Bug fix details',
|
||||||
|
updatedAt: '2025-06-09T12:00:00Z',
|
||||||
|
publishedAt: '2025-06-09T12:00:00Z',
|
||||||
|
maintenance: 'Maintenance details',
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('ChangelogRenderer', () => {
|
||||||
|
it('renders release date', () => {
|
||||||
|
render(<ChangelogRenderer changelog={mockChangelog} />);
|
||||||
|
expect(screen.getByText('June 10, 2025')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders features, media, and description', () => {
|
||||||
|
render(<ChangelogRenderer changelog={mockChangelog} />);
|
||||||
|
expect(screen.getByText('Feature 1')).toBeInTheDocument();
|
||||||
|
expect(screen.getByAltText('Media')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('Description for feature 1')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,141 @@
|
|||||||
|
.changelog-renderer {
|
||||||
|
position: relative;
|
||||||
|
padding-left: 20px;
|
||||||
|
|
||||||
|
.changelog-release-date {
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 20px;
|
||||||
|
color: var(--text-vanilla-400, #c0c1c3);
|
||||||
|
}
|
||||||
|
|
||||||
|
&-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-line {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 6px;
|
||||||
|
bottom: -30px;
|
||||||
|
width: 1px;
|
||||||
|
background-color: var(--bg-slate-400, #1d212d);
|
||||||
|
|
||||||
|
.inner-ball {
|
||||||
|
position: absolute;
|
||||||
|
left: 50%;
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
border-radius: 100%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
background-color: var(--bg-robin-500, #7190f9);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ul,
|
||||||
|
ol {
|
||||||
|
list-style: none;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
padding-left: 30px;
|
||||||
|
|
||||||
|
li {
|
||||||
|
position: relative;
|
||||||
|
&::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
left: -10px;
|
||||||
|
top: 10px;
|
||||||
|
width: 20px;
|
||||||
|
height: 2px;
|
||||||
|
background-color: var(--bg-robin-500, #7190f9);
|
||||||
|
transform: translate(-100%, -50%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
li,
|
||||||
|
p {
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 20px;
|
||||||
|
color: var(--text-vanilla-400, #c0c1c3);
|
||||||
|
}
|
||||||
|
|
||||||
|
code {
|
||||||
|
padding: 2px 4px;
|
||||||
|
background-color: var(--bg-slate-500, #161922);
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 95%;
|
||||||
|
vertical-align: middle;
|
||||||
|
border: 1px solid var(--bg-slate-600, #1c1f2a);
|
||||||
|
}
|
||||||
|
a {
|
||||||
|
color: var(--text-robin-500, #7190f9);
|
||||||
|
font-weight: 600;
|
||||||
|
text-decoration: underline;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
h1,
|
||||||
|
h2,
|
||||||
|
h3,
|
||||||
|
h4,
|
||||||
|
h5,
|
||||||
|
h6 {
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-vanilla-100, #fff);
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 24px;
|
||||||
|
line-height: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
font-size: 20px;
|
||||||
|
line-height: 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.changelog-media-image,
|
||||||
|
.changelog-media-video {
|
||||||
|
height: auto;
|
||||||
|
width: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 1px solid var(--bg-slate-400, #1d212d);
|
||||||
|
}
|
||||||
|
|
||||||
|
.changelog-media-video {
|
||||||
|
margin: 12px 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.lightMode {
|
||||||
|
.changelog-renderer {
|
||||||
|
.changelog-release-date {
|
||||||
|
color: var(--text-ink-500);
|
||||||
|
}
|
||||||
|
|
||||||
|
&-line {
|
||||||
|
background-color: var(--bg-vanilla-300);
|
||||||
|
}
|
||||||
|
li,
|
||||||
|
p {
|
||||||
|
color: var(--text-ink-500);
|
||||||
|
}
|
||||||
|
|
||||||
|
h1,
|
||||||
|
h2,
|
||||||
|
h3,
|
||||||
|
h4,
|
||||||
|
h5,
|
||||||
|
h6 {
|
||||||
|
color: var(--text-ink-500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
import './ChangelogRenderer.styles.scss';
|
||||||
|
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
import ReactMarkdown from 'react-markdown';
|
||||||
|
import {
|
||||||
|
ChangelogSchema,
|
||||||
|
Media,
|
||||||
|
SupportedImageTypes,
|
||||||
|
SupportedVideoTypes,
|
||||||
|
} from 'types/api/changelog/getChangelogByVersion';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
changelog: ChangelogSchema;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderMedia(media: Media): JSX.Element | null {
|
||||||
|
if (SupportedImageTypes.includes(media.ext)) {
|
||||||
|
return (
|
||||||
|
<img
|
||||||
|
src={media.url}
|
||||||
|
alt={media.alternativeText || 'Media'}
|
||||||
|
width={800}
|
||||||
|
height={450}
|
||||||
|
className="changelog-media-image"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (SupportedVideoTypes.includes(media.ext)) {
|
||||||
|
return (
|
||||||
|
<video
|
||||||
|
autoPlay
|
||||||
|
controls
|
||||||
|
controlsList="nodownload noplaybackrate"
|
||||||
|
loop
|
||||||
|
className="changelog-media-video"
|
||||||
|
>
|
||||||
|
<source src={media.url} type={media.mime} />
|
||||||
|
<track kind="captions" src="" label="No captions available" default />
|
||||||
|
Your browser does not support the video tag.
|
||||||
|
</video>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ChangelogRenderer({ changelog }: Props): JSX.Element {
|
||||||
|
const formattedReleaseDate = dayjs(changelog.release_date).format(
|
||||||
|
'MMMM D, YYYY',
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="changelog-renderer">
|
||||||
|
<div className="changelog-renderer-line">
|
||||||
|
<div className="inner-ball" />
|
||||||
|
</div>
|
||||||
|
<span className="changelog-release-date">{formattedReleaseDate}</span>
|
||||||
|
{changelog.features && changelog.features.length > 0 && (
|
||||||
|
<div className="changelog-renderer-list">
|
||||||
|
{changelog.features.map((feature) => (
|
||||||
|
<div key={feature.id}>
|
||||||
|
<h2>{feature.title}</h2>
|
||||||
|
{feature.media && renderMedia(feature.media)}
|
||||||
|
<ReactMarkdown>{feature.description}</ReactMarkdown>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{changelog.bug_fixes && changelog.bug_fixes.length > 0 && (
|
||||||
|
<div>
|
||||||
|
<h2>Bug Fixes</h2>
|
||||||
|
{changelog.bug_fixes && (
|
||||||
|
<ReactMarkdown>{changelog.bug_fixes}</ReactMarkdown>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{changelog.maintenance && changelog.maintenance.length > 0 && (
|
||||||
|
<div>
|
||||||
|
<h2>Maintenance</h2>
|
||||||
|
{changelog.maintenance && (
|
||||||
|
<ReactMarkdown>{changelog.maintenance}</ReactMarkdown>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ChangelogRenderer;
|
||||||
@@ -18,7 +18,7 @@ function ErrorContent({ error }: ErrorContentProps): JSX.Element {
|
|||||||
errors: errorMessages,
|
errors: errorMessages,
|
||||||
code: errorCode,
|
code: errorCode,
|
||||||
message: errorMessage,
|
message: errorMessage,
|
||||||
} = error.error.error;
|
} = error?.error?.error || {};
|
||||||
return (
|
return (
|
||||||
<section className="error-content">
|
<section className="error-content">
|
||||||
{/* Summary Header */}
|
{/* Summary Header */}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { render, screen } from '@testing-library/react';
|
import { render, screen } from '@testing-library/react';
|
||||||
import ROUTES from 'constants/routes';
|
import ROUTES from 'constants/routes';
|
||||||
|
import { PreferenceContextProvider } from 'providers/preferences/context/PreferenceContextProvider';
|
||||||
import MockQueryClientProvider from 'providers/test/MockQueryClientProvider';
|
import MockQueryClientProvider from 'providers/test/MockQueryClientProvider';
|
||||||
import { DataSource } from 'types/common/queryBuilder';
|
import { DataSource } from 'types/common/queryBuilder';
|
||||||
|
|
||||||
@@ -52,11 +53,32 @@ jest.mock('hooks/saveViews/useDeleteView', () => ({
|
|||||||
})),
|
})),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
// Mock usePreferenceSync
|
||||||
|
jest.mock('providers/preferences/sync/usePreferenceSync', () => ({
|
||||||
|
usePreferenceSync: (): any => ({
|
||||||
|
preferences: {
|
||||||
|
columns: [],
|
||||||
|
formatting: {
|
||||||
|
maxLines: 2,
|
||||||
|
format: 'table',
|
||||||
|
fontSize: 'small',
|
||||||
|
version: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
loading: false,
|
||||||
|
error: null,
|
||||||
|
updateColumns: jest.fn(),
|
||||||
|
updateFormatting: jest.fn(),
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
describe('ExplorerCard', () => {
|
describe('ExplorerCard', () => {
|
||||||
it('renders a card with a title and a description', () => {
|
it('renders a card with a title and a description', () => {
|
||||||
render(
|
render(
|
||||||
<MockQueryClientProvider>
|
<MockQueryClientProvider>
|
||||||
<ExplorerCard sourcepage={DataSource.TRACES}>child</ExplorerCard>
|
<PreferenceContextProvider>
|
||||||
|
<ExplorerCard sourcepage={DataSource.TRACES}>child</ExplorerCard>
|
||||||
|
</PreferenceContextProvider>
|
||||||
</MockQueryClientProvider>,
|
</MockQueryClientProvider>,
|
||||||
);
|
);
|
||||||
expect(screen.queryByText('Query Builder')).not.toBeInTheDocument();
|
expect(screen.queryByText('Query Builder')).not.toBeInTheDocument();
|
||||||
@@ -65,7 +87,9 @@ describe('ExplorerCard', () => {
|
|||||||
it('renders a save view button', () => {
|
it('renders a save view button', () => {
|
||||||
render(
|
render(
|
||||||
<MockQueryClientProvider>
|
<MockQueryClientProvider>
|
||||||
<ExplorerCard sourcepage={DataSource.TRACES}>child</ExplorerCard>
|
<PreferenceContextProvider>
|
||||||
|
<ExplorerCard sourcepage={DataSource.TRACES}>child</ExplorerCard>
|
||||||
|
</PreferenceContextProvider>
|
||||||
</MockQueryClientProvider>,
|
</MockQueryClientProvider>,
|
||||||
);
|
);
|
||||||
expect(screen.queryByText('Save view')).not.toBeInTheDocument();
|
expect(screen.queryByText('Save view')).not.toBeInTheDocument();
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
|
|||||||
import { mapQueryDataFromApi } from 'lib/newQueryBuilder/queryBuilderMappers/mapQueryDataFromApi';
|
import { mapQueryDataFromApi } from 'lib/newQueryBuilder/queryBuilderMappers/mapQueryDataFromApi';
|
||||||
import isEqual from 'lodash-es/isEqual';
|
import isEqual from 'lodash-es/isEqual';
|
||||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||||
|
import { DataSource } from 'types/common/queryBuilder';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
DeleteViewHandlerProps,
|
DeleteViewHandlerProps,
|
||||||
@@ -106,7 +107,11 @@ export const isQueryUpdatedInView = ({
|
|||||||
!isEqual(
|
!isEqual(
|
||||||
options?.selectColumns,
|
options?.selectColumns,
|
||||||
extraData && JSON.parse(extraData)?.selectColumns,
|
extraData && JSON.parse(extraData)?.selectColumns,
|
||||||
)
|
) ||
|
||||||
|
(stagedQuery?.builder?.queryData?.[0]?.dataSource === DataSource.LOGS &&
|
||||||
|
(!isEqual(options?.format, extraData && JSON.parse(extraData)?.format) ||
|
||||||
|
!isEqual(options?.maxLines, extraData && JSON.parse(extraData)?.maxLines) ||
|
||||||
|
!isEqual(options?.fontSize, extraData && JSON.parse(extraData)?.fontSize)))
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -74,6 +74,7 @@ const formatMap = {
|
|||||||
'MM/dd HH:mm': DATE_TIME_FORMATS.SLASH_SHORT,
|
'MM/dd HH:mm': DATE_TIME_FORMATS.SLASH_SHORT,
|
||||||
'MM/DD': DATE_TIME_FORMATS.DATE_SHORT,
|
'MM/DD': DATE_TIME_FORMATS.DATE_SHORT,
|
||||||
'YY-MM': DATE_TIME_FORMATS.YEAR_MONTH,
|
'YY-MM': DATE_TIME_FORMATS.YEAR_MONTH,
|
||||||
|
'MMM d, yyyy, h:mm:ss aaaa': DATE_TIME_FORMATS.DASH_DATETIME,
|
||||||
YY: DATE_TIME_FORMATS.YEAR_SHORT,
|
YY: DATE_TIME_FORMATS.YEAR_SHORT,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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}
|
isLoading={isFetching && traces.length === 0}
|
||||||
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}
|
loading={isFetching && traces.length === 0}
|
||||||
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, useState } from 'react';
|
import { useCallback, useEffect, useMemo, useRef, 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,8 +86,12 @@ function HostMetricsDetails({
|
|||||||
endTime: endMs,
|
endTime: endMs,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
const lastSelectedInterval = useRef<Time | null>(null);
|
||||||
|
|
||||||
const [selectedInterval, setSelectedInterval] = useState<Time>(
|
const [selectedInterval, setSelectedInterval] = useState<Time>(
|
||||||
selectedTime as Time,
|
lastSelectedInterval.current
|
||||||
|
? lastSelectedInterval.current
|
||||||
|
: (selectedTime as Time),
|
||||||
);
|
);
|
||||||
|
|
||||||
const [selectedView, setSelectedView] = useState<VIEWS>(
|
const [selectedView, setSelectedView] = useState<VIEWS>(
|
||||||
@@ -150,10 +154,11 @@ function HostMetricsDetails({
|
|||||||
}, [initialFilters]);
|
}, [initialFilters]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setSelectedInterval(selectedTime as Time);
|
const currentSelectedInterval = lastSelectedInterval.current || selectedTime;
|
||||||
|
setSelectedInterval(currentSelectedInterval as Time);
|
||||||
|
|
||||||
if (selectedTime !== 'custom') {
|
if (currentSelectedInterval !== 'custom') {
|
||||||
const { maxTime, minTime } = GetMinMax(selectedTime);
|
const { maxTime, minTime } = GetMinMax(currentSelectedInterval);
|
||||||
|
|
||||||
setModalTimeRange({
|
setModalTimeRange({
|
||||||
startTime: Math.floor(minTime / 1000000000),
|
startTime: Math.floor(minTime / 1000000000),
|
||||||
@@ -181,6 +186,7 @@ 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) {
|
||||||
@@ -356,6 +362,7 @@ 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') {
|
||||||
@@ -430,9 +437,13 @@ function HostMetricsDetails({
|
|||||||
>
|
>
|
||||||
{host.active ? 'ACTIVE' : 'INACTIVE'}
|
{host.active ? 'ACTIVE' : 'INACTIVE'}
|
||||||
</Tag>
|
</Tag>
|
||||||
<Tag className="infra-monitoring-tags" bordered>
|
{host.os ? (
|
||||||
{host.os}
|
<Tag className="infra-monitoring-tags" bordered>
|
||||||
</Tag>
|
{host.os}
|
||||||
|
</Tag>
|
||||||
|
) : (
|
||||||
|
<Typography.Text>-</Typography.Text>
|
||||||
|
)}
|
||||||
<div className="progress-container">
|
<div className="progress-container">
|
||||||
<Progress
|
<Progress
|
||||||
percent={Number((host.cpu * 100).toFixed(1))}
|
percent={Number((host.cpu * 100).toFixed(1))}
|
||||||
|
|||||||
@@ -13,13 +13,15 @@ import {
|
|||||||
CustomTimeType,
|
CustomTimeType,
|
||||||
Time,
|
Time,
|
||||||
} from 'container/TopNav/DateTimeSelectionV2/config';
|
} from 'container/TopNav/DateTimeSelectionV2/config';
|
||||||
|
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||||
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 { useQueries, UseQueryResult } from 'react-query';
|
import { QueryFunctionContext, 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';
|
||||||
|
|
||||||
@@ -53,6 +55,11 @@ 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(
|
||||||
@@ -65,17 +72,22 @@ function Metrics({
|
|||||||
);
|
);
|
||||||
|
|
||||||
const queries = useQueries(
|
const queries = useQueries(
|
||||||
queryPayloads.map((payload) => ({
|
queryPayloads.map((payload, index) => ({
|
||||||
queryKey: ['host-metrics', payload, ENTITY_VERSION_V4, 'HOST'],
|
queryKey: ['host-metrics', payload, ENTITY_VERSION_V4, 'HOST'],
|
||||||
queryFn: (): Promise<SuccessResponse<MetricRangePayloadProps>> =>
|
queryFn: ({
|
||||||
GetMetricQueryRange(payload, ENTITY_VERSION_V4),
|
signal,
|
||||||
enabled: !!payload,
|
}: QueryFunctionContext): Promise<
|
||||||
|
SuccessResponse<MetricRangePayloadProps>
|
||||||
|
> => GetMetricQueryRange(payload, ENTITY_VERSION_V4, signal),
|
||||||
|
enabled: !!payload && visibilities[index],
|
||||||
|
keepPreviousData: true,
|
||||||
})),
|
})),
|
||||||
);
|
);
|
||||||
|
|
||||||
const isDarkMode = useIsDarkMode();
|
const isDarkMode = useIsDarkMode();
|
||||||
const graphRef = useRef<HTMLDivElement>(null);
|
const graphRef = useRef<HTMLDivElement>(null);
|
||||||
const dimensions = useResizeObserver(graphRef);
|
const dimensions = useResizeObserver(graphRef);
|
||||||
|
const { currentQuery } = useQueryBuilder();
|
||||||
|
|
||||||
const chartData = useMemo(
|
const chartData = useMemo(
|
||||||
() => queries.map(({ data }) => getUPlotChartData(data?.payload)),
|
() => queries.map(({ data }) => getUPlotChartData(data?.payload)),
|
||||||
@@ -134,16 +146,24 @@ function Metrics({
|
|||||||
minTimeScale: graphTimeIntervals[idx].start,
|
minTimeScale: graphTimeIntervals[idx].start,
|
||||||
maxTimeScale: graphTimeIntervals[idx].end,
|
maxTimeScale: graphTimeIntervals[idx].end,
|
||||||
onDragSelect: (start, end) => onDragSelect(start, end, idx),
|
onDragSelect: (start, end) => onDragSelect(start, end, idx),
|
||||||
|
query: currentQuery,
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
[queries, isDarkMode, dimensions, graphTimeIntervals, onDragSelect],
|
[
|
||||||
|
queries,
|
||||||
|
isDarkMode,
|
||||||
|
dimensions,
|
||||||
|
graphTimeIntervals,
|
||||||
|
onDragSelect,
|
||||||
|
currentQuery,
|
||||||
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
const renderCardContent = (
|
const renderCardContent = (
|
||||||
query: UseQueryResult<SuccessResponse<MetricRangePayloadProps>, unknown>,
|
query: UseQueryResult<SuccessResponse<MetricRangePayloadProps>, unknown>,
|
||||||
idx: number,
|
idx: number,
|
||||||
): JSX.Element => {
|
): JSX.Element => {
|
||||||
if (query.isLoading) {
|
if ((!query.data && query.isLoading) || !visibilities[idx]) {
|
||||||
return <Skeleton />;
|
return <Skeleton />;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -181,7 +201,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 span={12} key={hostWidgetInfo[idx].title}>
|
<Col ref={setElement(idx)} 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
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 { initialDataSource, stagedQuery } = useQueryBuilder();
|
const { 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: initialDataSource || DataSource.LOGS,
|
dataSource: DataSource.LOGS,
|
||||||
aggregateOperator: listQuery?.aggregateOperator || StringOperators.NOOP,
|
aggregateOperator: listQuery?.aggregateOperator || StringOperators.NOOP,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -5,17 +5,19 @@ import cx from 'classnames';
|
|||||||
import { OPERATORS } from 'constants/queryBuilder';
|
import { OPERATORS } from 'constants/queryBuilder';
|
||||||
import { FontSize } from 'container/OptionsMenu/types';
|
import { FontSize } from 'container/OptionsMenu/types';
|
||||||
import { memo, MouseEvent, ReactNode, useMemo } from 'react';
|
import { memo, MouseEvent, ReactNode, useMemo } from 'react';
|
||||||
|
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||||
|
|
||||||
function AddToQueryHOC({
|
function AddToQueryHOC({
|
||||||
fieldKey,
|
fieldKey,
|
||||||
fieldValue,
|
fieldValue,
|
||||||
onAddToQuery,
|
onAddToQuery,
|
||||||
fontSize,
|
fontSize,
|
||||||
|
dataType = DataTypes.EMPTY,
|
||||||
children,
|
children,
|
||||||
}: AddToQueryHOCProps): JSX.Element {
|
}: AddToQueryHOCProps): JSX.Element {
|
||||||
const handleQueryAdd = (event: MouseEvent<HTMLDivElement>): void => {
|
const handleQueryAdd = (event: MouseEvent<HTMLDivElement>): void => {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
onAddToQuery(fieldKey, fieldValue, OPERATORS['=']);
|
onAddToQuery(fieldKey, fieldValue, OPERATORS['='], undefined, dataType);
|
||||||
};
|
};
|
||||||
|
|
||||||
const popOverContent = useMemo(() => <span>Add to query: {fieldKey}</span>, [
|
const popOverContent = useMemo(() => <span>Add to query: {fieldKey}</span>, [
|
||||||
@@ -35,9 +37,20 @@ function AddToQueryHOC({
|
|||||||
export interface AddToQueryHOCProps {
|
export interface AddToQueryHOCProps {
|
||||||
fieldKey: string;
|
fieldKey: string;
|
||||||
fieldValue: string;
|
fieldValue: string;
|
||||||
onAddToQuery: (fieldKey: string, fieldValue: string, operator: string) => void;
|
onAddToQuery: (
|
||||||
|
fieldKey: string,
|
||||||
|
fieldValue: string,
|
||||||
|
operator: string,
|
||||||
|
isJSON?: boolean,
|
||||||
|
dataType?: DataTypes,
|
||||||
|
) => void;
|
||||||
fontSize: FontSize;
|
fontSize: FontSize;
|
||||||
|
dataType?: DataTypes;
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
AddToQueryHOC.defaultProps = {
|
||||||
|
dataType: DataTypes.EMPTY,
|
||||||
|
};
|
||||||
|
|
||||||
export default memo(AddToQueryHOC);
|
export default memo(AddToQueryHOC);
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ export function getDefaultCellStyle(isDarkMode?: boolean): CSSProperties {
|
|||||||
|
|
||||||
export const defaultTableStyle: CSSProperties = {
|
export const defaultTableStyle: CSSProperties = {
|
||||||
minWidth: '40rem',
|
minWidth: '40rem',
|
||||||
|
maxWidth: '60rem',
|
||||||
};
|
};
|
||||||
|
|
||||||
export const defaultListViewPanelStyle: CSSProperties = {
|
export const defaultListViewPanelStyle: CSSProperties = {
|
||||||
|
|||||||
@@ -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
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,147 @@
|
|||||||
|
.metrics-aggregate-section {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
margin: 4px 0;
|
||||||
|
|
||||||
|
.metrics-time-aggregation-section {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.non-histogram-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:not(.is-histogram) {
|
||||||
|
.metrics-time-aggregation-section,
|
||||||
|
.metrics-space-aggregation-section {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.metrics-aggregation-section-content {
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.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;
|
||||||
|
|
||||||
|
.group-by-filter-container {
|
||||||
|
min-width: 340px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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;
|
||||||
|
|
||||||
|
&.main-label {
|
||||||
|
color: var(--Slate-50, #62687c);
|
||||||
|
font-family: 'Geist Mono';
|
||||||
|
font-size: 12px;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 500;
|
||||||
|
line-height: 18px; /* 150% */
|
||||||
|
letter-spacing: 0.48px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.metrics-aggregation-section-content-item-value {
|
||||||
|
min-width: 140px;
|
||||||
|
|
||||||
|
.ant-select {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-select-selector {
|
||||||
|
border-radius: 2px;
|
||||||
|
border: 1.005px solid var(--Slate-400, #1d212d);
|
||||||
|
background: var(--Ink-300, #16181d);
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-with-label {
|
||||||
|
.label {
|
||||||
|
min-width: 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input {
|
||||||
|
flex: initial;
|
||||||
|
width: 100px !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.is-histogram {
|
||||||
|
.group-by-filter-container {
|
||||||
|
width: 420px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.histogram-every-input {
|
||||||
|
.input {
|
||||||
|
flex: initial;
|
||||||
|
width: 100px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label {
|
||||||
|
min-width: 80px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.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,226 @@
|
|||||||
|
import './MetricsAggregateSection.styles.scss';
|
||||||
|
|
||||||
|
import { Tooltip } from 'antd';
|
||||||
|
import cx from 'classnames';
|
||||||
|
import InputWithLabel from 'components/InputWithLabel/InputWithLabel';
|
||||||
|
import { ATTRIBUTE_TYPES, PANEL_TYPES } from 'constants/queryBuilder';
|
||||||
|
import SpaceAggregationOptions from 'container/QueryBuilder/components/SpaceAggregationOptions/SpaceAggregationOptions';
|
||||||
|
import { GroupByFilter, OperatorsSelect } from 'container/QueryBuilder/filters';
|
||||||
|
import { useQueryOperations } from 'hooks/queryBuilder/useQueryBuilderOperations';
|
||||||
|
import { Info } from 'lucide-react';
|
||||||
|
import { memo, useCallback, 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,
|
||||||
|
});
|
||||||
|
|
||||||
|
const isHistogram = useMemo(
|
||||||
|
() => query.aggregateAttribute.type === ATTRIBUTE_TYPES.HISTOGRAM,
|
||||||
|
[query.aggregateAttribute.type],
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setAggregationOptions([
|
||||||
|
{
|
||||||
|
func: query.spaceAggregation || 'count',
|
||||||
|
arg: query.aggregateAttribute.key || '',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
}, [
|
||||||
|
query.spaceAggregation,
|
||||||
|
query.aggregateAttribute.key,
|
||||||
|
setAggregationOptions,
|
||||||
|
query,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const handleChangeGroupByKeys = useCallback(
|
||||||
|
(value: IBuilderQuery['groupBy']) => {
|
||||||
|
handleChangeQueryData('groupBy', value);
|
||||||
|
},
|
||||||
|
[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={cx('metrics-aggregate-section', {
|
||||||
|
'is-histogram': isHistogram,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{!isHistogram && (
|
||||||
|
<div className="non-histogram-container">
|
||||||
|
<div className="metrics-time-aggregation-section">
|
||||||
|
<div className="metrics-aggregation-section-content">
|
||||||
|
<div className="metrics-aggregation-section-content-item">
|
||||||
|
<div className="metrics-aggregation-section-content-item-label main-label">
|
||||||
|
AGGREGATE BY TIME{' '}
|
||||||
|
<Tooltip title="AGGREGATE BY TIME">
|
||||||
|
<Info size={12} />
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
<div className="metrics-aggregation-section-content-item-value">
|
||||||
|
<OperatorsSelect
|
||||||
|
value={query.aggregateOperator}
|
||||||
|
onChange={handleChangeOperator}
|
||||||
|
operators={operators}
|
||||||
|
className="metrics-operators-select"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{showAggregationInterval && (
|
||||||
|
<div className="metrics-aggregation-section-content-item">
|
||||||
|
<div className="metrics-aggregation-section-content-item-label">
|
||||||
|
every
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="metrics-aggregation-section-content-item-value">
|
||||||
|
<InputWithLabel
|
||||||
|
onChange={handleChangeAggregateEvery}
|
||||||
|
label="Seconds"
|
||||||
|
placeholder="Auto"
|
||||||
|
labelAfter
|
||||||
|
initialValue={query?.stepInterval ?? undefined}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="metrics-space-aggregation-section">
|
||||||
|
<div className="metrics-aggregation-section-content">
|
||||||
|
<div className="metrics-aggregation-section-content-item">
|
||||||
|
<div className="metrics-aggregation-section-content-item-label main-label">
|
||||||
|
AGGREGATE LABELS
|
||||||
|
<Tooltip title="AGGREGATE LABELS">
|
||||||
|
<Info size={12} />
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
<div className="metrics-aggregation-section-content-item-value">
|
||||||
|
<SpaceAggregationOptions
|
||||||
|
panelType={panelType}
|
||||||
|
key={`${panelType}${query.spaceAggregation}${query.timeAggregation}`}
|
||||||
|
aggregatorAttributeType={
|
||||||
|
query?.aggregateAttribute.type as ATTRIBUTE_TYPES
|
||||||
|
}
|
||||||
|
selectedValue={query.spaceAggregation}
|
||||||
|
disabled={disableOperatorSelector}
|
||||||
|
onSelect={handleSpaceAggregationChange}
|
||||||
|
operators={spaceAggregationOptions}
|
||||||
|
qbVersion="v3"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="metrics-aggregation-section-content-item">
|
||||||
|
<div className="metrics-aggregation-section-content-item-label">by</div>
|
||||||
|
|
||||||
|
<div className="metrics-aggregation-section-content-item-value group-by-filter-container">
|
||||||
|
<GroupByFilter
|
||||||
|
disabled={!query.aggregateAttribute.key}
|
||||||
|
query={query}
|
||||||
|
onChange={handleChangeGroupByKeys}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{isHistogram && (
|
||||||
|
<div className="metrics-space-aggregation-section">
|
||||||
|
<div className="metrics-aggregation-section-content">
|
||||||
|
<div className="metrics-aggregation-section-content-item">
|
||||||
|
<div className="metrics-aggregation-section-content-item-value">
|
||||||
|
<SpaceAggregationOptions
|
||||||
|
panelType={panelType}
|
||||||
|
key={`${panelType}${query.spaceAggregation}${query.timeAggregation}`}
|
||||||
|
aggregatorAttributeType={
|
||||||
|
query?.aggregateAttribute.type as ATTRIBUTE_TYPES
|
||||||
|
}
|
||||||
|
selectedValue={query.spaceAggregation}
|
||||||
|
disabled={disableOperatorSelector}
|
||||||
|
onSelect={handleSpaceAggregationChange}
|
||||||
|
operators={spaceAggregationOptions}
|
||||||
|
qbVersion="v3"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="metrics-aggregation-section-content-item">
|
||||||
|
<div className="metrics-aggregation-section-content-item-label">by</div>
|
||||||
|
|
||||||
|
<div className="metrics-aggregation-section-content-item-value group-by-filter-container">
|
||||||
|
<GroupByFilter
|
||||||
|
disabled={!query.aggregateAttribute.key}
|
||||||
|
query={query}
|
||||||
|
onChange={handleChangeGroupByKeys}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="metrics-aggregation-section-content-item">
|
||||||
|
<div className="metrics-aggregation-section-content-item-label">
|
||||||
|
every
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="metrics-aggregation-section-content-item-value">
|
||||||
|
<InputWithLabel
|
||||||
|
onChange={handleChangeAggregateEvery}
|
||||||
|
label="Seconds"
|
||||||
|
placeholder="Auto"
|
||||||
|
labelAfter
|
||||||
|
initialValue={query?.stepInterval ?? undefined}
|
||||||
|
className="histogram-every-input"
|
||||||
|
/>
|
||||||
|
</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,334 @@
|
|||||||
|
.query-aggregation-container {
|
||||||
|
display: block;
|
||||||
|
|
||||||
|
.aggregation-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: flex-start;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
|
||||||
|
.query-aggregation-select-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
flex: 1;
|
||||||
|
min-width: 400px;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
.query-aggregation-select-editor {
|
||||||
|
border-radius: 2px;
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
|
||||||
|
&.error {
|
||||||
|
.cm-editor {
|
||||||
|
.cm-content {
|
||||||
|
border-color: var(--bg-cherry-500) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.cm-content {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.query-aggregation-error-container {
|
||||||
|
position: absolute;
|
||||||
|
right: 8px;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
z-index: 1;
|
||||||
|
|
||||||
|
.query-aggregation-error-content {
|
||||||
|
padding: 8px;
|
||||||
|
max-width: 300px;
|
||||||
|
|
||||||
|
.query-aggregation-error-message {
|
||||||
|
color: var(--bg-cherry-500);
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.query-aggregation-error-btn {
|
||||||
|
padding: 4px;
|
||||||
|
height: auto;
|
||||||
|
min-width: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.close-btn {
|
||||||
|
border-radius: 0px 2px 2px 0px;
|
||||||
|
border: 1px solid var(--bg-slate-400);
|
||||||
|
background: var(--bg-ink-300);
|
||||||
|
height: 38px;
|
||||||
|
width: 38px;
|
||||||
|
|
||||||
|
border-left: transparent;
|
||||||
|
border-top-left-radius: 0px;
|
||||||
|
border-bottom-left-radius: 0px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.query-aggregation-options-input {
|
||||||
|
width: 100%;
|
||||||
|
height: 36px;
|
||||||
|
line-height: 36px;
|
||||||
|
border-radius: 2px;
|
||||||
|
border: 1px solid var(--bg-slate-400);
|
||||||
|
box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.1);
|
||||||
|
|
||||||
|
font-family: 'Space Mono', monospace !important;
|
||||||
|
|
||||||
|
&::placeholder {
|
||||||
|
color: var(--bg-vanilla-100);
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.query-aggregation-interval {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
max-width: 360px;
|
||||||
|
|
||||||
|
.query-aggregation-interval-input-container {
|
||||||
|
.query-aggregation-interval-input {
|
||||||
|
input {
|
||||||
|
max-width: 120px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.query-aggregation-error-popover {
|
||||||
|
.ant-popover-inner {
|
||||||
|
background-color: var(--bg-slate-500);
|
||||||
|
border: 1px solid var(--bg-slate-400);
|
||||||
|
border-radius: 4px;
|
||||||
|
box-shadow: 0px 4px 12px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
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}
|
||||||
|
maxAggregations={
|
||||||
|
panelType === PANEL_TYPES.VALUE || panelType === PANEL_TYPES.PIE
|
||||||
|
? 1
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{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,667 @@
|
|||||||
|
/* 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 { EditorState, RangeSetBuilder, Transaction } from '@codemirror/state';
|
||||||
|
import { Color } from '@signozhq/design-tokens';
|
||||||
|
import { copilot } from '@uiw/codemirror-theme-copilot';
|
||||||
|
import CodeMirror, {
|
||||||
|
Decoration,
|
||||||
|
EditorView,
|
||||||
|
keymap,
|
||||||
|
ViewPlugin,
|
||||||
|
ViewUpdate,
|
||||||
|
} from '@uiw/react-codemirror';
|
||||||
|
import { Button, Popover } from 'antd';
|
||||||
|
import { getKeySuggestions } from 'api/querySuggestions/getKeySuggestions';
|
||||||
|
import { QueryBuilderKeys } from 'constants/queryBuilder';
|
||||||
|
import { tracesAggregateOperatorOptions } from 'constants/queryBuilderOperators';
|
||||||
|
import { TriangleAlert } from 'lucide-react';
|
||||||
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
|
import { useQuery } from 'react-query';
|
||||||
|
import { 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,
|
||||||
|
maxAggregations,
|
||||||
|
}: {
|
||||||
|
onChange?: (value: string) => void;
|
||||||
|
queryData: IBuilderQuery;
|
||||||
|
maxAggregations?: number;
|
||||||
|
}): 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 [validationError, setValidationError] = useState<string | null>(null);
|
||||||
|
const editorRef = useRef<EditorView | null>(null);
|
||||||
|
const [isFocused, setIsFocused] = useState(false);
|
||||||
|
|
||||||
|
// Get valid function names (lowercase)
|
||||||
|
const validFunctions = useMemo(
|
||||||
|
() => tracesAggregateOperatorOptions.map((op) => op.value.toLowerCase()),
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
// Helper function to safely start completion
|
||||||
|
const safeStartCompletion = useCallback((): void => {
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
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 });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validation logic
|
||||||
|
const validateAggregations = (): string | null => {
|
||||||
|
// Check maxAggregations limit
|
||||||
|
if (maxAggregations !== undefined && pairs.length > maxAggregations) {
|
||||||
|
return `Maximum ${maxAggregations} aggregation${
|
||||||
|
maxAggregations === 1 ? '' : 's'
|
||||||
|
} allowed`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for invalid functions
|
||||||
|
const invalidFuncs = pairs.filter(
|
||||||
|
(pair) => !validFunctions.includes(pair.func),
|
||||||
|
);
|
||||||
|
if (invalidFuncs.length > 0) {
|
||||||
|
const funcs = invalidFuncs.map((f) => f.func).join(', ');
|
||||||
|
return `Invalid function${invalidFuncs.length === 1 ? '' : 's'}: ${funcs}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for incomplete function calls
|
||||||
|
if (/([a-zA-Z_][\w]*)\s*\([^)]*$/g.test(input)) {
|
||||||
|
return 'Incomplete function call - missing closing parenthesis';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for empty function calls that require arguments
|
||||||
|
const emptyFuncs = (input.match(/([a-zA-Z_][\w]*)\s*\(\s*\)/g) || [])
|
||||||
|
.map((call) => call.match(/([a-zA-Z_][\w]*)/)?.[1])
|
||||||
|
.filter((func): func is string => Boolean(func))
|
||||||
|
.filter((func) => operatorArgMeta[func.toLowerCase()]?.acceptsArgs);
|
||||||
|
|
||||||
|
if (emptyFuncs.length > 0) {
|
||||||
|
const isPlural = emptyFuncs.length > 1;
|
||||||
|
return `Function${isPlural ? 's' : ''} ${emptyFuncs.join(', ')} require${
|
||||||
|
isPlural ? '' : 's'
|
||||||
|
} arguments`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
setValidationError(validateAggregations());
|
||||||
|
setFunctionArgPairs(pairs);
|
||||||
|
setAggregationOptions(pairs);
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [input, maxAggregations, validFunctions]);
|
||||||
|
|
||||||
|
// Transaction filter to limit aggregations
|
||||||
|
const transactionFilterExtension = useMemo(() => {
|
||||||
|
if (maxAggregations === undefined) return [];
|
||||||
|
|
||||||
|
return EditorState.transactionFilter.of((tr: Transaction) => {
|
||||||
|
if (!tr.docChanged) return tr;
|
||||||
|
|
||||||
|
const regex = /([a-zA-Z_][\w]*)\s*\(([^)]*)\)/g;
|
||||||
|
const oldMatches = [
|
||||||
|
...tr.startState.doc.toString().matchAll(regex),
|
||||||
|
].filter((match) => validFunctions.includes(match[1].toLowerCase()));
|
||||||
|
const newMatches = [
|
||||||
|
...tr.newDoc.toString().matchAll(regex),
|
||||||
|
].filter((match) => validFunctions.includes(match[1].toLowerCase()));
|
||||||
|
|
||||||
|
if (
|
||||||
|
newMatches.length > oldMatches.length &&
|
||||||
|
newMatches.length > maxAggregations
|
||||||
|
) {
|
||||||
|
return []; // Cancel transaction
|
||||||
|
}
|
||||||
|
return tr;
|
||||||
|
});
|
||||||
|
}, [maxAggregations, validFunctions]);
|
||||||
|
|
||||||
|
// Find function context for fetching suggestions
|
||||||
|
const functionContextForFetch = getFunctionContextAtCursor(input, cursorPos);
|
||||||
|
|
||||||
|
const { data: aggregateAttributeData, isLoading: isLoadingFields } = useQuery(
|
||||||
|
[
|
||||||
|
QueryBuilderKeys.GET_AGGREGATE_ATTRIBUTE,
|
||||||
|
functionContextForFetch,
|
||||||
|
queryData.dataSource,
|
||||||
|
],
|
||||||
|
() => {
|
||||||
|
const operatorsWithoutDataType: (string | undefined)[] = [
|
||||||
|
TracesAggregatorOperator.COUNT,
|
||||||
|
TracesAggregatorOperator.COUNT_DISTINCT,
|
||||||
|
TracesAggregatorOperator.RATE,
|
||||||
|
];
|
||||||
|
|
||||||
|
const fieldDataType =
|
||||||
|
functionContextForFetch &&
|
||||||
|
operatorsWithoutDataType.includes(functionContextForFetch)
|
||||||
|
? undefined
|
||||||
|
: 'number';
|
||||||
|
|
||||||
|
return getKeySuggestions({
|
||||||
|
signal: queryData.dataSource,
|
||||||
|
searchText: '',
|
||||||
|
fieldDataType,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enabled:
|
||||||
|
!!functionContextForFetch &&
|
||||||
|
!!operatorArgMeta[functionContextForFetch]?.acceptsArgs,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// 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(
|
||||||
|
() =>
|
||||||
|
Object.keys(aggregateAttributeData?.data.data.keys || {}).flatMap((key) => {
|
||||||
|
const attributeKeys = aggregateAttributeData?.data.data.keys[key];
|
||||||
|
if (!attributeKeys) return [];
|
||||||
|
|
||||||
|
return attributeKeys.map((attributeKey) => ({
|
||||||
|
label: attributeKey.name,
|
||||||
|
type: 'variable',
|
||||||
|
info: attributeKey.fieldDataType,
|
||||||
|
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);
|
||||||
|
|
||||||
|
// Check if over limit and not editing existing
|
||||||
|
if (maxAggregations !== undefined) {
|
||||||
|
const regex = /([a-zA-Z_][\w]*)\s*\(([^)]*)\)/g;
|
||||||
|
const matches = [...text.matchAll(regex)].filter((match) =>
|
||||||
|
validFunctions.includes(match[1].toLowerCase()),
|
||||||
|
);
|
||||||
|
if (matches.length >= maxAggregations) {
|
||||||
|
const isEditing = matches.some((match) => {
|
||||||
|
const start = match.index ?? 0;
|
||||||
|
return cursorPos >= start && cursorPos <= start + match[0].length;
|
||||||
|
});
|
||||||
|
if (!isEditing) return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Do not show suggestions if inside count()
|
||||||
|
if (
|
||||||
|
funcName === TracesAggregatorOperator.COUNT &&
|
||||||
|
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,
|
||||||
|
maxAggregations,
|
||||||
|
validFunctions,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="query-aggregation-select-container">
|
||||||
|
<CodeMirror
|
||||||
|
value={input}
|
||||||
|
onChange={(value): void => {
|
||||||
|
setInput(value);
|
||||||
|
onChange?.(value);
|
||||||
|
}}
|
||||||
|
className={`query-aggregation-select-editor ${
|
||||||
|
validationError ? 'error' : ''
|
||||||
|
}`}
|
||||||
|
theme={copilot}
|
||||||
|
extensions={[
|
||||||
|
chipPlugin,
|
||||||
|
aggregatorAutocomplete,
|
||||||
|
transactionFilterExtension,
|
||||||
|
javascript({ jsx: false, typescript: false }),
|
||||||
|
EditorView.lineWrapping,
|
||||||
|
stopEventsExtension,
|
||||||
|
keymap.of([
|
||||||
|
...completionKeymap,
|
||||||
|
{
|
||||||
|
key: 'Escape',
|
||||||
|
run: closeCompletion,
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
]}
|
||||||
|
placeholder={
|
||||||
|
maxAggregations !== undefined
|
||||||
|
? `Type aggregator functions (max ${maxAggregations}) like sum(), count_distinct(...), etc.`
|
||||||
|
: 'Type aggregator functions like sum(), count_distinct(...), etc.'
|
||||||
|
}
|
||||||
|
basicSetup={{
|
||||||
|
lineNumbers: false,
|
||||||
|
autocompletion: true,
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{validationError && (
|
||||||
|
<div className="query-aggregation-error-container">
|
||||||
|
<Popover
|
||||||
|
placement="bottomRight"
|
||||||
|
showArrow={false}
|
||||||
|
content={
|
||||||
|
<div className="query-aggregation-error-content">
|
||||||
|
<div className="query-aggregation-error-message">{validationError}</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
overlayClassName="query-aggregation-error-popover"
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
icon={<TriangleAlert size={14} color={Color.BG_CHERRY_500} />}
|
||||||
|
className="periscope-btn ghost query-aggregation-error-btn"
|
||||||
|
/>
|
||||||
|
</Popover>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
QueryAggregationSelect.defaultProps = {
|
||||||
|
onChange: undefined,
|
||||||
|
maxAggregations: undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default QueryAggregationSelect;
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -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
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>
|
||||||
|
);
|
||||||
|
});
|
||||||
270
frontend/src/components/QueryBuilderV2/utils.ts
Normal file
270
frontend/src/components/QueryBuilderV2/utils.ts
Normal file
@@ -0,0 +1,270 @@
|
|||||||
|
import { createAggregation } from 'api/v5/queryRange/prepareQueryRangePayloadV5';
|
||||||
|
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||||
|
import {
|
||||||
|
Having,
|
||||||
|
IBuilderQuery,
|
||||||
|
Query,
|
||||||
|
TagFilter,
|
||||||
|
} from 'types/api/queryBuilder/queryBuilderData';
|
||||||
|
import {
|
||||||
|
LogAggregation,
|
||||||
|
MetricAggregation,
|
||||||
|
TraceAggregation,
|
||||||
|
} from 'types/api/v5/queryRange';
|
||||||
|
import { EQueryType } from 'types/common/dashboard';
|
||||||
|
import { DataSource } from 'types/common/queryBuilder';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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,
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getQueryTitles = (currentQuery: Query): string[] => {
|
||||||
|
if (currentQuery.queryType === EQueryType.QUERY_BUILDER) {
|
||||||
|
const queryTitles: string[] = [];
|
||||||
|
|
||||||
|
// Handle builder queries with multiple aggregations
|
||||||
|
currentQuery.builder.queryData.forEach((q) => {
|
||||||
|
const aggregationCount = q.aggregations?.length || 1;
|
||||||
|
|
||||||
|
if (aggregationCount > 1) {
|
||||||
|
// If multiple aggregations, create titles like A.0, A.1, A.2
|
||||||
|
for (let i = 0; i < aggregationCount; i++) {
|
||||||
|
queryTitles.push(`${q.queryName}.${i}`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Single aggregation, just use query name
|
||||||
|
queryTitles.push(q.queryName);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle formulas (they don't have aggregations, so just use query name)
|
||||||
|
const formulas = currentQuery.builder.queryFormulas.map((q) => q.queryName);
|
||||||
|
|
||||||
|
return [...queryTitles, ...formulas];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentQuery.queryType === EQueryType.CLICKHOUSE) {
|
||||||
|
return currentQuery.clickhouse_sql.map((q) => q.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
return currentQuery.promql.map((q) => q.name);
|
||||||
|
};
|
||||||
|
|
||||||
|
// function to give you label value for query name taking multiaggregation into account
|
||||||
|
export function getQueryLabelWithAggregation(
|
||||||
|
queryData: IBuilderQuery[],
|
||||||
|
legendMap: Record<string, string> = {},
|
||||||
|
): { label: string; value: string }[] {
|
||||||
|
const labels: { label: string; value: string }[] = [];
|
||||||
|
|
||||||
|
const aggregationPerQuery =
|
||||||
|
queryData.reduce((acc, query) => {
|
||||||
|
if (query.queryName && query.aggregations?.length) {
|
||||||
|
acc[query.queryName] = createAggregation(query).map((a: any) => ({
|
||||||
|
alias: a.alias,
|
||||||
|
expression: a.expression,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
}, {} as Record<string, any>) || {};
|
||||||
|
|
||||||
|
Object.entries(aggregationPerQuery).forEach(([queryName, aggregations]) => {
|
||||||
|
const legend = legendMap[queryName];
|
||||||
|
|
||||||
|
if (aggregations.length > 1) {
|
||||||
|
aggregations.forEach((agg: any, index: number) => {
|
||||||
|
const aggregationName = agg.alias || agg.expression || '';
|
||||||
|
const label = `${queryName}.${index}`;
|
||||||
|
const value = legend
|
||||||
|
? `${aggregationName}-${legend}`
|
||||||
|
: `${queryName}.${aggregationName}`;
|
||||||
|
labels.push({
|
||||||
|
label,
|
||||||
|
value,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} else if (aggregations.length === 1) {
|
||||||
|
const label = legend || queryName;
|
||||||
|
const value = legend || queryName;
|
||||||
|
labels.push({
|
||||||
|
label,
|
||||||
|
value,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return labels;
|
||||||
|
}
|
||||||
@@ -1,7 +1,17 @@
|
|||||||
import { Tabs, TabsProps } from 'antd';
|
import { Tabs, TabsProps } from 'antd';
|
||||||
|
import {
|
||||||
|
generatePath,
|
||||||
|
matchPath,
|
||||||
|
useLocation,
|
||||||
|
useParams,
|
||||||
|
} from 'react-router-dom';
|
||||||
|
|
||||||
import { RouteTabProps } from './types';
|
import { RouteTabProps } from './types';
|
||||||
|
|
||||||
|
interface Params {
|
||||||
|
[key: string]: string;
|
||||||
|
}
|
||||||
|
|
||||||
function RouteTab({
|
function RouteTab({
|
||||||
routes,
|
routes,
|
||||||
activeKey,
|
activeKey,
|
||||||
@@ -9,6 +19,18 @@ function RouteTab({
|
|||||||
history,
|
history,
|
||||||
...rest
|
...rest
|
||||||
}: RouteTabProps & TabsProps): JSX.Element {
|
}: RouteTabProps & TabsProps): JSX.Element {
|
||||||
|
const params = useParams<Params>();
|
||||||
|
const location = useLocation();
|
||||||
|
|
||||||
|
// Find the matching route for the current pathname
|
||||||
|
const currentRoute = routes.find((route) => {
|
||||||
|
const routePath = route.route.split('?')[0];
|
||||||
|
return matchPath(location.pathname, {
|
||||||
|
path: routePath,
|
||||||
|
exact: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
const onChange = (activeRoute: string): void => {
|
const onChange = (activeRoute: string): void => {
|
||||||
if (onChangeHandler) {
|
if (onChangeHandler) {
|
||||||
onChangeHandler(activeRoute);
|
onChangeHandler(activeRoute);
|
||||||
@@ -17,7 +39,8 @@ function RouteTab({
|
|||||||
const selectedRoute = routes.find((e) => e.key === activeRoute);
|
const selectedRoute = routes.find((e) => e.key === activeRoute);
|
||||||
|
|
||||||
if (selectedRoute) {
|
if (selectedRoute) {
|
||||||
history.push(selectedRoute.route);
|
const resolvedRoute = generatePath(selectedRoute.route, params);
|
||||||
|
history.push(resolvedRoute);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -32,8 +55,8 @@ function RouteTab({
|
|||||||
<Tabs
|
<Tabs
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
destroyInactiveTabPane
|
destroyInactiveTabPane
|
||||||
activeKey={activeKey}
|
activeKey={currentRoute?.key || activeKey}
|
||||||
defaultActiveKey={activeKey}
|
defaultActiveKey={currentRoute?.key || activeKey}
|
||||||
animated
|
animated
|
||||||
items={items}
|
items={items}
|
||||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||||
|
|||||||
77
frontend/src/constants/antlrQueryConstants.ts
Normal file
77
frontend/src/constants/antlrQueryConstants.ts
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
export const OPERATORS = {
|
||||||
|
IN: 'IN',
|
||||||
|
LIKE: 'LIKE',
|
||||||
|
ILIKE: 'ILIKE',
|
||||||
|
REGEXP: 'REGEXP',
|
||||||
|
EXISTS: 'EXISTS',
|
||||||
|
CONTAINS: 'CONTAINS',
|
||||||
|
BETWEEN: 'BETWEEN',
|
||||||
|
NOT: 'NOT',
|
||||||
|
'=': '=',
|
||||||
|
'!=': '!=',
|
||||||
|
'>=': '>=',
|
||||||
|
'>': '>',
|
||||||
|
'<=': '<=',
|
||||||
|
'<': '<',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const NON_VALUE_OPERATORS = [OPERATORS.EXISTS];
|
||||||
|
|
||||||
|
export enum QUERY_BUILDER_KEY_TYPES {
|
||||||
|
STRING = 'string',
|
||||||
|
NUMBER = 'number',
|
||||||
|
BOOLEAN = 'boolean',
|
||||||
|
}
|
||||||
|
|
||||||
|
export const QUERY_BUILDER_OPERATORS_BY_KEY_TYPE = {
|
||||||
|
[QUERY_BUILDER_KEY_TYPES.STRING]: [
|
||||||
|
OPERATORS['='],
|
||||||
|
OPERATORS['!='],
|
||||||
|
OPERATORS.IN,
|
||||||
|
OPERATORS.LIKE,
|
||||||
|
OPERATORS.ILIKE,
|
||||||
|
OPERATORS.CONTAINS,
|
||||||
|
OPERATORS.EXISTS,
|
||||||
|
OPERATORS.REGEXP,
|
||||||
|
OPERATORS.NOT,
|
||||||
|
],
|
||||||
|
[QUERY_BUILDER_KEY_TYPES.NUMBER]: [
|
||||||
|
OPERATORS['='],
|
||||||
|
OPERATORS['!='],
|
||||||
|
OPERATORS['>='],
|
||||||
|
OPERATORS['>'],
|
||||||
|
OPERATORS['<='],
|
||||||
|
OPERATORS['<'],
|
||||||
|
OPERATORS.IN,
|
||||||
|
OPERATORS.EXISTS,
|
||||||
|
OPERATORS.BETWEEN,
|
||||||
|
OPERATORS.NOT,
|
||||||
|
],
|
||||||
|
[QUERY_BUILDER_KEY_TYPES.BOOLEAN]: [
|
||||||
|
OPERATORS['='],
|
||||||
|
OPERATORS['!='],
|
||||||
|
OPERATORS.EXISTS,
|
||||||
|
OPERATORS.NOT,
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
export const negationQueryOperatorSuggestions = [
|
||||||
|
{ label: OPERATORS['LIKE'], type: 'operator', info: 'Like' },
|
||||||
|
{ label: OPERATORS['ILIKE'], type: 'operator', info: 'Case insensitive like' },
|
||||||
|
{ label: OPERATORS['EXISTS'], type: 'operator', info: 'Exists' },
|
||||||
|
{ label: OPERATORS['BETWEEN'], type: 'operator', info: 'Between' },
|
||||||
|
{ label: OPERATORS['IN'], type: 'operator', info: 'In' },
|
||||||
|
{ label: OPERATORS['REGEXP'], type: 'operator', info: 'Regular expression' },
|
||||||
|
{ label: OPERATORS['CONTAINS'], type: 'operator', info: 'Contains' },
|
||||||
|
];
|
||||||
|
|
||||||
|
export const queryOperatorSuggestions = [
|
||||||
|
{ label: OPERATORS['='], type: 'operator', info: 'Equal to' },
|
||||||
|
{ label: OPERATORS['!='], type: 'operator', info: 'Not equal to' },
|
||||||
|
{ label: OPERATORS['>'], type: 'operator', info: 'Greater than' },
|
||||||
|
{ label: OPERATORS['<'], type: 'operator', info: 'Less than' },
|
||||||
|
{ label: OPERATORS['>='], type: 'operator', info: 'Greater than or equal to' },
|
||||||
|
{ label: OPERATORS['<='], type: 'operator', info: 'Less than or equal to' },
|
||||||
|
{ label: OPERATORS['NOT'], type: 'operator', info: 'Not' },
|
||||||
|
...negationQueryOperatorSuggestions,
|
||||||
|
];
|
||||||
@@ -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';
|
||||||
|
|||||||
@@ -30,5 +30,5 @@ export enum LOCALSTORAGE {
|
|||||||
SHOW_EXCEPTIONS_QUICK_FILTERS = 'SHOW_EXCEPTIONS_QUICK_FILTERS',
|
SHOW_EXCEPTIONS_QUICK_FILTERS = 'SHOW_EXCEPTIONS_QUICK_FILTERS',
|
||||||
BANNER_DISMISSED = 'BANNER_DISMISSED',
|
BANNER_DISMISSED = 'BANNER_DISMISSED',
|
||||||
QUICK_FILTERS_SETTINGS_ANNOUNCEMENT = 'QUICK_FILTERS_SETTINGS_ANNOUNCEMENT',
|
QUICK_FILTERS_SETTINGS_ANNOUNCEMENT = 'QUICK_FILTERS_SETTINGS_ANNOUNCEMENT',
|
||||||
UNEXECUTED_FUNNELS = 'UNEXECUTED_FUNNELS',
|
FUNNEL_STEPS = 'FUNNEL_STEPS',
|
||||||
}
|
}
|
||||||
|
|||||||
18
frontend/src/constants/orgPreferences.ts
Normal file
18
frontend/src/constants/orgPreferences.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
export const ORG_PREFERENCES = {
|
||||||
|
ORG_ONBOARDING: 'org_onboarding',
|
||||||
|
WELCOME_CHECKLIST_DO_LATER: 'welcome_checklist_do_later',
|
||||||
|
WELCOME_CHECKLIST_SEND_LOGS_SKIPPED: 'welcome_checklist_send_logs_skipped',
|
||||||
|
WELCOME_CHECKLIST_SEND_TRACES_SKIPPED: 'welcome_checklist_send_traces_skipped',
|
||||||
|
WELCOME_CHECKLIST_SETUP_ALERTS_SKIPPED:
|
||||||
|
'welcome_checklist_setup_alerts_skipped',
|
||||||
|
WELCOME_CHECKLIST_SETUP_SAVED_VIEW_SKIPPED:
|
||||||
|
'welcome_checklist_setup_saved_view_skipped',
|
||||||
|
WELCOME_CHECKLIST_SEND_INFRA_METRICS_SKIPPED:
|
||||||
|
'welcome_checklist_send_infra_metrics_skipped',
|
||||||
|
WELCOME_CHECKLIST_SETUP_DASHBOARDS_SKIPPED:
|
||||||
|
'welcome_checklist_setup_dashboards_skipped',
|
||||||
|
WELCOME_CHECKLIST_SETUP_WORKSPACE_SKIPPED:
|
||||||
|
'welcome_checklist_setup_workspace_skipped',
|
||||||
|
WELCOME_CHECKLIST_ADD_DATA_SOURCE_SKIPPED:
|
||||||
|
'welcome_checklist_add_data_source_skipped',
|
||||||
|
};
|
||||||
@@ -46,4 +46,6 @@ 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({
|
||||||
@@ -176,7 +179,7 @@ export const initialQueryBuilderFormValues: IBuilderQuery = {
|
|||||||
sourceNames: alphabet,
|
sourceNames: alphabet,
|
||||||
}),
|
}),
|
||||||
disabled: false,
|
disabled: false,
|
||||||
stepInterval: 60,
|
stepInterval: undefined,
|
||||||
having: [],
|
having: [],
|
||||||
limit: null,
|
limit: null,
|
||||||
orderBy: [],
|
orderBy: [],
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -29,12 +29,12 @@ const ROUTES = {
|
|||||||
ALERT_OVERVIEW: '/alerts/overview',
|
ALERT_OVERVIEW: '/alerts/overview',
|
||||||
ALL_CHANNELS: '/settings/channels',
|
ALL_CHANNELS: '/settings/channels',
|
||||||
CHANNELS_NEW: '/settings/channels/new',
|
CHANNELS_NEW: '/settings/channels/new',
|
||||||
CHANNELS_EDIT: '/settings/channels/:id',
|
CHANNELS_EDIT: '/settings/channels/edit/:channelId',
|
||||||
ALL_ERROR: '/exceptions',
|
ALL_ERROR: '/exceptions',
|
||||||
ERROR_DETAIL: '/error-detail',
|
ERROR_DETAIL: '/error-detail',
|
||||||
VERSION: '/status',
|
VERSION: '/status',
|
||||||
MY_SETTINGS: '/my-settings',
|
|
||||||
SETTINGS: '/settings',
|
SETTINGS: '/settings',
|
||||||
|
MY_SETTINGS: '/settings/my-settings',
|
||||||
ORG_SETTINGS: '/settings/org-settings',
|
ORG_SETTINGS: '/settings/org-settings',
|
||||||
CUSTOM_DOMAIN_SETTINGS: '/settings/custom-domain-settings',
|
CUSTOM_DOMAIN_SETTINGS: '/settings/custom-domain-settings',
|
||||||
API_KEYS: '/settings/api-keys',
|
API_KEYS: '/settings/api-keys',
|
||||||
@@ -52,7 +52,7 @@ const ROUTES = {
|
|||||||
LIST_LICENSES: '/licenses',
|
LIST_LICENSES: '/licenses',
|
||||||
LOGS_INDEX_FIELDS: '/logs-explorer/index-fields',
|
LOGS_INDEX_FIELDS: '/logs-explorer/index-fields',
|
||||||
TRACE_EXPLORER: '/trace-explorer',
|
TRACE_EXPLORER: '/trace-explorer',
|
||||||
BILLING: '/billing',
|
BILLING: '/settings/billing',
|
||||||
SUPPORT: '/support',
|
SUPPORT: '/support',
|
||||||
LOGS_SAVE_VIEWS: '/logs/saved-views',
|
LOGS_SAVE_VIEWS: '/logs/saved-views',
|
||||||
TRACES_SAVE_VIEWS: '/traces/saved-views',
|
TRACES_SAVE_VIEWS: '/traces/saved-views',
|
||||||
@@ -60,10 +60,12 @@ const ROUTES = {
|
|||||||
TRACES_FUNNELS_DETAIL: '/traces/funnels/:funnelId',
|
TRACES_FUNNELS_DETAIL: '/traces/funnels/:funnelId',
|
||||||
WORKSPACE_LOCKED: '/workspace-locked',
|
WORKSPACE_LOCKED: '/workspace-locked',
|
||||||
WORKSPACE_SUSPENDED: '/workspace-suspended',
|
WORKSPACE_SUSPENDED: '/workspace-suspended',
|
||||||
SHORTCUTS: '/shortcuts',
|
SHORTCUTS: '/settings/shortcuts',
|
||||||
INTEGRATIONS: '/integrations',
|
INTEGRATIONS: '/integrations',
|
||||||
|
MESSAGING_QUEUES_BASE: '/messaging-queues',
|
||||||
MESSAGING_QUEUES_KAFKA: '/messaging-queues/kafka',
|
MESSAGING_QUEUES_KAFKA: '/messaging-queues/kafka',
|
||||||
MESSAGING_QUEUES_KAFKA_DETAIL: '/messaging-queues/kafka/detail',
|
MESSAGING_QUEUES_KAFKA_DETAIL: '/messaging-queues/kafka/detail',
|
||||||
|
INFRASTRUCTURE_MONITORING_BASE: '/infrastructure-monitoring',
|
||||||
INFRASTRUCTURE_MONITORING_HOSTS: '/infrastructure-monitoring/hosts',
|
INFRASTRUCTURE_MONITORING_HOSTS: '/infrastructure-monitoring/hosts',
|
||||||
INFRASTRUCTURE_MONITORING_KUBERNETES: '/infrastructure-monitoring/kubernetes',
|
INFRASTRUCTURE_MONITORING_KUBERNETES: '/infrastructure-monitoring/kubernetes',
|
||||||
MESSAGING_QUEUES_CELERY_TASK: '/messaging-queues/celery-task',
|
MESSAGING_QUEUES_CELERY_TASK: '/messaging-queues/celery-task',
|
||||||
@@ -71,6 +73,7 @@ const ROUTES = {
|
|||||||
METRICS_EXPLORER: '/metrics-explorer/summary',
|
METRICS_EXPLORER: '/metrics-explorer/summary',
|
||||||
METRICS_EXPLORER_EXPLORER: '/metrics-explorer/explorer',
|
METRICS_EXPLORER_EXPLORER: '/metrics-explorer/explorer',
|
||||||
METRICS_EXPLORER_VIEWS: '/metrics-explorer/views',
|
METRICS_EXPLORER_VIEWS: '/metrics-explorer/views',
|
||||||
|
API_MONITORING_BASE: '/api-monitoring',
|
||||||
API_MONITORING: '/api-monitoring/explorer',
|
API_MONITORING: '/api-monitoring/explorer',
|
||||||
METRICS_EXPLORER_BASE: '/metrics-explorer',
|
METRICS_EXPLORER_BASE: '/metrics-explorer',
|
||||||
WORKSPACE_ACCESS_RESTRICTED: '/workspace-access-restricted',
|
WORKSPACE_ACCESS_RESTRICTED: '/workspace-access-restricted',
|
||||||
|
|||||||
4
frontend/src/constants/userPreferences.ts
Normal file
4
frontend/src/constants/userPreferences.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
export const USER_PREFERENCES = {
|
||||||
|
SIDENAV_PINNED: 'sidenav_pinned',
|
||||||
|
NAV_SHORTCUTS: 'nav_shortcuts',
|
||||||
|
};
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user