Compare commits
12 Commits
v0.94.1
...
fix/extrac
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
603d6d6fce | ||
|
|
339d81a240 | ||
|
|
2acee366a0 | ||
|
|
76e8672ee5 | ||
|
|
64c0bc1b97 | ||
|
|
9d3733ed72 | ||
|
|
72ec4a1b1f | ||
|
|
f5bf8f9f70 | ||
|
|
365cbdd835 | ||
|
|
30bf3a53f5 | ||
|
|
0dd085c48e | ||
|
|
531a0a12dd |
@@ -1,6 +1,6 @@
|
|||||||
services:
|
services:
|
||||||
clickhouse:
|
clickhouse:
|
||||||
image: clickhouse/clickhouse-server:25.5.6
|
image: clickhouse/clickhouse-server:24.1.2-alpine
|
||||||
container_name: clickhouse
|
container_name: clickhouse
|
||||||
volumes:
|
volumes:
|
||||||
- ${PWD}/fs/etc/clickhouse-server/config.d/config.xml:/etc/clickhouse-server/config.d/config.xml
|
- ${PWD}/fs/etc/clickhouse-server/config.d/config.xml:/etc/clickhouse-server/config.d/config.xml
|
||||||
@@ -23,8 +23,6 @@ services:
|
|||||||
retries: 3
|
retries: 3
|
||||||
depends_on:
|
depends_on:
|
||||||
- zookeeper
|
- zookeeper
|
||||||
environment:
|
|
||||||
- CLICKHOUSE_SKIP_USER_SETUP=1
|
|
||||||
zookeeper:
|
zookeeper:
|
||||||
image: signoz/zookeeper:3.7.1
|
image: signoz/zookeeper:3.7.1
|
||||||
container_name: zookeeper
|
container_name: zookeeper
|
||||||
@@ -42,7 +40,7 @@ services:
|
|||||||
timeout: 5s
|
timeout: 5s
|
||||||
retries: 3
|
retries: 3
|
||||||
schema-migrator-sync:
|
schema-migrator-sync:
|
||||||
image: signoz/signoz-schema-migrator:v0.129.4
|
image: signoz/signoz-schema-migrator:v0.129.0
|
||||||
container_name: schema-migrator-sync
|
container_name: schema-migrator-sync
|
||||||
command:
|
command:
|
||||||
- sync
|
- sync
|
||||||
@@ -55,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.129.4
|
image: signoz/signoz-schema-migrator:v0.129.0
|
||||||
container_name: schema-migrator-async
|
container_name: schema-migrator-async
|
||||||
command:
|
command:
|
||||||
- async
|
- async
|
||||||
|
|||||||
43
.github/CODEOWNERS
vendored
43
.github/CODEOWNERS
vendored
@@ -5,45 +5,6 @@
|
|||||||
/frontend/ @SigNoz/frontend @YounixM
|
/frontend/ @SigNoz/frontend @YounixM
|
||||||
/frontend/src/container/MetricsApplication @srikanthccv
|
/frontend/src/container/MetricsApplication @srikanthccv
|
||||||
/frontend/src/container/NewWidget/RightContainer/types.ts @srikanthccv
|
/frontend/src/container/NewWidget/RightContainer/types.ts @srikanthccv
|
||||||
|
|
||||||
# Dashboard, Alert, Metrics, Service Map, Services
|
|
||||||
/frontend/src/container/ListOfDashboard/ @srikanthccv
|
|
||||||
/frontend/src/container/NewDashboard/ @srikanthccv
|
|
||||||
/frontend/src/pages/DashboardsListPage/ @srikanthccv
|
|
||||||
/frontend/src/pages/DashboardWidget/ @srikanthccv
|
|
||||||
/frontend/src/pages/NewDashboard/ @srikanthccv
|
|
||||||
/frontend/src/providers/Dashboard/ @srikanthccv
|
|
||||||
|
|
||||||
# Alerts
|
|
||||||
/frontend/src/container/AlertHistory/ @srikanthccv
|
|
||||||
/frontend/src/container/AllAlertChannels/ @srikanthccv
|
|
||||||
/frontend/src/container/AnomalyAlertEvaluationView/ @srikanthccv
|
|
||||||
/frontend/src/container/CreateAlertChannels/ @srikanthccv
|
|
||||||
/frontend/src/container/CreateAlertRule/ @srikanthccv
|
|
||||||
/frontend/src/container/EditAlertChannels/ @srikanthccv
|
|
||||||
/frontend/src/container/FormAlertChannels/ @srikanthccv
|
|
||||||
/frontend/src/container/FormAlertRules/ @srikanthccv
|
|
||||||
/frontend/src/container/ListAlertRules/ @srikanthccv
|
|
||||||
/frontend/src/container/TriggeredAlerts/ @srikanthccv
|
|
||||||
/frontend/src/pages/AlertChannelCreate/ @srikanthccv
|
|
||||||
/frontend/src/pages/AlertDetails/ @srikanthccv
|
|
||||||
/frontend/src/pages/AlertHistory/ @srikanthccv
|
|
||||||
/frontend/src/pages/AlertList/ @srikanthccv
|
|
||||||
/frontend/src/pages/CreateAlert/ @srikanthccv
|
|
||||||
/frontend/src/providers/Alert.tsx @srikanthccv
|
|
||||||
|
|
||||||
# Metrics
|
|
||||||
/frontend/src/container/MetricsExplorer/ @srikanthccv
|
|
||||||
/frontend/src/pages/MetricsApplication/ @srikanthccv
|
|
||||||
/frontend/src/pages/MetricsExplorer/ @srikanthccv
|
|
||||||
|
|
||||||
# Services and Service Map
|
|
||||||
/frontend/src/container/ServiceApplication/ @srikanthccv
|
|
||||||
/frontend/src/container/ServiceTable/ @srikanthccv
|
|
||||||
/frontend/src/pages/Services/ @srikanthccv
|
|
||||||
/frontend/src/pages/ServiceTopLevelOperations/ @srikanthccv
|
|
||||||
/frontend/src/container/Home/Services/ @srikanthccv
|
|
||||||
|
|
||||||
/deploy/ @SigNoz/devops
|
/deploy/ @SigNoz/devops
|
||||||
.github @SigNoz/devops
|
.github @SigNoz/devops
|
||||||
|
|
||||||
@@ -81,7 +42,3 @@
|
|||||||
/pkg/telemetrymetadata/ @srikanthccv
|
/pkg/telemetrymetadata/ @srikanthccv
|
||||||
/pkg/telemetrymetrics/ @srikanthccv
|
/pkg/telemetrymetrics/ @srikanthccv
|
||||||
/pkg/telemetrytraces/ @srikanthccv
|
/pkg/telemetrytraces/ @srikanthccv
|
||||||
|
|
||||||
# AuthN / AuthZ Owners
|
|
||||||
|
|
||||||
/pkg/authz/ @vikrantgupta25 @grandwizard28
|
|
||||||
|
|||||||
2
.github/workflows/build-community.yaml
vendored
2
.github/workflows/build-community.yaml
vendored
@@ -62,7 +62,7 @@ jobs:
|
|||||||
secrets: inherit
|
secrets: inherit
|
||||||
with:
|
with:
|
||||||
PRIMUS_REF: main
|
PRIMUS_REF: main
|
||||||
GO_VERSION: 1.24
|
GO_VERSION: 1.23
|
||||||
GO_NAME: signoz-community
|
GO_NAME: signoz-community
|
||||||
GO_INPUT_ARTIFACT_CACHE_KEY: community-jsbuild-${{ github.sha }}
|
GO_INPUT_ARTIFACT_CACHE_KEY: community-jsbuild-${{ github.sha }}
|
||||||
GO_INPUT_ARTIFACT_PATH: frontend/build
|
GO_INPUT_ARTIFACT_PATH: frontend/build
|
||||||
|
|||||||
2
.github/workflows/build-enterprise.yaml
vendored
2
.github/workflows/build-enterprise.yaml
vendored
@@ -93,7 +93,7 @@ jobs:
|
|||||||
secrets: inherit
|
secrets: inherit
|
||||||
with:
|
with:
|
||||||
PRIMUS_REF: main
|
PRIMUS_REF: main
|
||||||
GO_VERSION: 1.24
|
GO_VERSION: 1.23
|
||||||
GO_INPUT_ARTIFACT_CACHE_KEY: enterprise-jsbuild-${{ github.sha }}
|
GO_INPUT_ARTIFACT_CACHE_KEY: enterprise-jsbuild-${{ github.sha }}
|
||||||
GO_INPUT_ARTIFACT_PATH: frontend/build
|
GO_INPUT_ARTIFACT_PATH: frontend/build
|
||||||
GO_BUILD_CONTEXT: ./cmd/enterprise
|
GO_BUILD_CONTEXT: ./cmd/enterprise
|
||||||
|
|||||||
2
.github/workflows/build-staging.yaml
vendored
2
.github/workflows/build-staging.yaml
vendored
@@ -92,7 +92,7 @@ jobs:
|
|||||||
secrets: inherit
|
secrets: inherit
|
||||||
with:
|
with:
|
||||||
PRIMUS_REF: main
|
PRIMUS_REF: main
|
||||||
GO_VERSION: 1.24
|
GO_VERSION: 1.23
|
||||||
GO_INPUT_ARTIFACT_CACHE_KEY: staging-jsbuild-${{ github.sha }}
|
GO_INPUT_ARTIFACT_CACHE_KEY: staging-jsbuild-${{ github.sha }}
|
||||||
GO_INPUT_ARTIFACT_PATH: frontend/build
|
GO_INPUT_ARTIFACT_PATH: frontend/build
|
||||||
GO_BUILD_CONTEXT: ./cmd/enterprise
|
GO_BUILD_CONTEXT: ./cmd/enterprise
|
||||||
|
|||||||
10
.github/workflows/goci.yaml
vendored
10
.github/workflows/goci.yaml
vendored
@@ -18,7 +18,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
PRIMUS_REF: main
|
PRIMUS_REF: main
|
||||||
GO_TEST_CONTEXT: ./...
|
GO_TEST_CONTEXT: ./...
|
||||||
GO_VERSION: 1.24
|
GO_VERSION: 1.23
|
||||||
fmt:
|
fmt:
|
||||||
if: |
|
if: |
|
||||||
(github.event_name == 'pull_request' && ! github.event.pull_request.head.repo.fork && github.event.pull_request.user.login != 'dependabot[bot]' && ! contains(github.event.pull_request.labels.*.name, 'safe-to-test')) ||
|
(github.event_name == 'pull_request' && ! github.event.pull_request.head.repo.fork && github.event.pull_request.user.login != 'dependabot[bot]' && ! contains(github.event.pull_request.labels.*.name, 'safe-to-test')) ||
|
||||||
@@ -27,7 +27,7 @@ jobs:
|
|||||||
secrets: inherit
|
secrets: inherit
|
||||||
with:
|
with:
|
||||||
PRIMUS_REF: main
|
PRIMUS_REF: main
|
||||||
GO_VERSION: 1.24
|
GO_VERSION: 1.23
|
||||||
lint:
|
lint:
|
||||||
if: |
|
if: |
|
||||||
(github.event_name == 'pull_request' && ! github.event.pull_request.head.repo.fork && github.event.pull_request.user.login != 'dependabot[bot]' && ! contains(github.event.pull_request.labels.*.name, 'safe-to-test')) ||
|
(github.event_name == 'pull_request' && ! github.event.pull_request.head.repo.fork && github.event.pull_request.user.login != 'dependabot[bot]' && ! contains(github.event.pull_request.labels.*.name, 'safe-to-test')) ||
|
||||||
@@ -36,7 +36,7 @@ jobs:
|
|||||||
secrets: inherit
|
secrets: inherit
|
||||||
with:
|
with:
|
||||||
PRIMUS_REF: main
|
PRIMUS_REF: main
|
||||||
GO_VERSION: 1.24
|
GO_VERSION: 1.23
|
||||||
deps:
|
deps:
|
||||||
if: |
|
if: |
|
||||||
(github.event_name == 'pull_request' && ! github.event.pull_request.head.repo.fork && github.event.pull_request.user.login != 'dependabot[bot]' && ! contains(github.event.pull_request.labels.*.name, 'safe-to-test')) ||
|
(github.event_name == 'pull_request' && ! github.event.pull_request.head.repo.fork && github.event.pull_request.user.login != 'dependabot[bot]' && ! contains(github.event.pull_request.labels.*.name, 'safe-to-test')) ||
|
||||||
@@ -45,7 +45,7 @@ jobs:
|
|||||||
secrets: inherit
|
secrets: inherit
|
||||||
with:
|
with:
|
||||||
PRIMUS_REF: main
|
PRIMUS_REF: main
|
||||||
GO_VERSION: 1.24
|
GO_VERSION: 1.23
|
||||||
build:
|
build:
|
||||||
if: |
|
if: |
|
||||||
(github.event_name == 'pull_request' && ! github.event.pull_request.head.repo.fork && github.event.pull_request.user.login != 'dependabot[bot]' && ! contains(github.event.pull_request.labels.*.name, 'safe-to-test')) ||
|
(github.event_name == 'pull_request' && ! github.event.pull_request.head.repo.fork && github.event.pull_request.user.login != 'dependabot[bot]' && ! contains(github.event.pull_request.labels.*.name, 'safe-to-test')) ||
|
||||||
@@ -57,7 +57,7 @@ jobs:
|
|||||||
- name: go-install
|
- name: go-install
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: "1.24"
|
go-version: "1.23"
|
||||||
- name: qemu-install
|
- name: qemu-install
|
||||||
uses: docker/setup-qemu-action@v3
|
uses: docker/setup-qemu-action@v3
|
||||||
- name: aarch64-install
|
- name: aarch64-install
|
||||||
|
|||||||
4
.github/workflows/gor-signoz-community.yaml
vendored
4
.github/workflows/gor-signoz-community.yaml
vendored
@@ -58,7 +58,7 @@ jobs:
|
|||||||
- name: setup-go
|
- name: setup-go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: "1.24"
|
go-version: "1.23"
|
||||||
- name: cross-compilation-tools
|
- name: cross-compilation-tools
|
||||||
if: matrix.os == 'ubuntu-latest'
|
if: matrix.os == 'ubuntu-latest'
|
||||||
run: |
|
run: |
|
||||||
@@ -122,7 +122,7 @@ jobs:
|
|||||||
- name: setup-go
|
- name: setup-go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: "1.24"
|
go-version: "1.23"
|
||||||
|
|
||||||
# copy the caches from build
|
# copy the caches from build
|
||||||
- name: get-sha
|
- name: get-sha
|
||||||
|
|||||||
4
.github/workflows/gor-signoz.yaml
vendored
4
.github/workflows/gor-signoz.yaml
vendored
@@ -72,7 +72,7 @@ jobs:
|
|||||||
- name: setup-go
|
- name: setup-go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: "1.24"
|
go-version: "1.23"
|
||||||
- name: cross-compilation-tools
|
- name: cross-compilation-tools
|
||||||
if: matrix.os == 'ubuntu-latest'
|
if: matrix.os == 'ubuntu-latest'
|
||||||
run: |
|
run: |
|
||||||
@@ -135,7 +135,7 @@ jobs:
|
|||||||
- name: setup-go
|
- name: setup-go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: "1.24"
|
go-version: "1.23"
|
||||||
|
|
||||||
# copy the caches from build
|
# copy the caches from build
|
||||||
- name: get-sha
|
- name: get-sha
|
||||||
|
|||||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -86,8 +86,6 @@ queries.active
|
|||||||
.devenv/**/tmp/**
|
.devenv/**/tmp/**
|
||||||
.qodo
|
.qodo
|
||||||
|
|
||||||
.dev
|
|
||||||
|
|
||||||
### Python ###
|
### Python ###
|
||||||
# Byte-compiled / optimized / DLL files
|
# Byte-compiled / optimized / DLL files
|
||||||
__pycache__/
|
__pycache__/
|
||||||
|
|||||||
@@ -2,11 +2,10 @@ FROM node:18-bullseye AS build
|
|||||||
|
|
||||||
WORKDIR /opt/
|
WORKDIR /opt/
|
||||||
COPY ./frontend/ ./
|
COPY ./frontend/ ./
|
||||||
ENV NODE_OPTIONS=--max-old-space-size=8192
|
|
||||||
RUN CI=1 yarn install
|
RUN CI=1 yarn install
|
||||||
RUN CI=1 yarn build
|
RUN CI=1 yarn build
|
||||||
|
|
||||||
FROM golang:1.24-bullseye
|
FROM golang:1.23-bullseye
|
||||||
|
|
||||||
ARG OS="linux"
|
ARG OS="linux"
|
||||||
ARG TARGETARCH
|
ARG TARGETARCH
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ x-common: &common
|
|||||||
max-file: "3"
|
max-file: "3"
|
||||||
x-clickhouse-defaults: &clickhouse-defaults
|
x-clickhouse-defaults: &clickhouse-defaults
|
||||||
!!merge <<: *common
|
!!merge <<: *common
|
||||||
image: clickhouse/clickhouse-server:25.5.6
|
image: clickhouse/clickhouse-server:24.1.2-alpine
|
||||||
tty: true
|
tty: true
|
||||||
deploy:
|
deploy:
|
||||||
labels:
|
labels:
|
||||||
@@ -37,8 +37,6 @@ x-clickhouse-defaults: &clickhouse-defaults
|
|||||||
nofile:
|
nofile:
|
||||||
soft: 262144
|
soft: 262144
|
||||||
hard: 262144
|
hard: 262144
|
||||||
environment:
|
|
||||||
- CLICKHOUSE_SKIP_USER_SETUP=1
|
|
||||||
x-zookeeper-defaults: &zookeeper-defaults
|
x-zookeeper-defaults: &zookeeper-defaults
|
||||||
!!merge <<: *common
|
!!merge <<: *common
|
||||||
image: signoz/zookeeper:3.7.1
|
image: signoz/zookeeper:3.7.1
|
||||||
@@ -65,7 +63,7 @@ x-db-depend: &db-depend
|
|||||||
services:
|
services:
|
||||||
init-clickhouse:
|
init-clickhouse:
|
||||||
!!merge <<: *common
|
!!merge <<: *common
|
||||||
image: clickhouse/clickhouse-server:25.5.6
|
image: clickhouse/clickhouse-server:24.1.2-alpine
|
||||||
command:
|
command:
|
||||||
- bash
|
- bash
|
||||||
- -c
|
- -c
|
||||||
@@ -176,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.94.1
|
image: signoz/signoz:v0.92.1
|
||||||
command:
|
command:
|
||||||
- --config=/root/config/prometheus.yml
|
- --config=/root/config/prometheus.yml
|
||||||
ports:
|
ports:
|
||||||
@@ -209,7 +207,7 @@ services:
|
|||||||
retries: 3
|
retries: 3
|
||||||
otel-collector:
|
otel-collector:
|
||||||
!!merge <<: *db-depend
|
!!merge <<: *db-depend
|
||||||
image: signoz/signoz-otel-collector:v0.129.4
|
image: signoz/signoz-otel-collector:v0.129.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
|
||||||
@@ -233,7 +231,7 @@ services:
|
|||||||
- signoz
|
- signoz
|
||||||
schema-migrator:
|
schema-migrator:
|
||||||
!!merge <<: *common
|
!!merge <<: *common
|
||||||
image: signoz/signoz-schema-migrator:v0.129.4
|
image: signoz/signoz-schema-migrator:v0.129.0
|
||||||
deploy:
|
deploy:
|
||||||
restart_policy:
|
restart_policy:
|
||||||
condition: on-failure
|
condition: on-failure
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ x-common: &common
|
|||||||
max-file: "3"
|
max-file: "3"
|
||||||
x-clickhouse-defaults: &clickhouse-defaults
|
x-clickhouse-defaults: &clickhouse-defaults
|
||||||
!!merge <<: *common
|
!!merge <<: *common
|
||||||
image: clickhouse/clickhouse-server:25.5.6
|
image: clickhouse/clickhouse-server:24.1.2-alpine
|
||||||
tty: true
|
tty: true
|
||||||
deploy:
|
deploy:
|
||||||
labels:
|
labels:
|
||||||
@@ -36,8 +36,6 @@ x-clickhouse-defaults: &clickhouse-defaults
|
|||||||
nofile:
|
nofile:
|
||||||
soft: 262144
|
soft: 262144
|
||||||
hard: 262144
|
hard: 262144
|
||||||
environment:
|
|
||||||
- CLICKHOUSE_SKIP_USER_SETUP=1
|
|
||||||
x-zookeeper-defaults: &zookeeper-defaults
|
x-zookeeper-defaults: &zookeeper-defaults
|
||||||
!!merge <<: *common
|
!!merge <<: *common
|
||||||
image: signoz/zookeeper:3.7.1
|
image: signoz/zookeeper:3.7.1
|
||||||
@@ -62,7 +60,7 @@ x-db-depend: &db-depend
|
|||||||
services:
|
services:
|
||||||
init-clickhouse:
|
init-clickhouse:
|
||||||
!!merge <<: *common
|
!!merge <<: *common
|
||||||
image: clickhouse/clickhouse-server:25.5.6
|
image: clickhouse/clickhouse-server:24.1.2-alpine
|
||||||
command:
|
command:
|
||||||
- bash
|
- bash
|
||||||
- -c
|
- -c
|
||||||
@@ -117,7 +115,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.94.1
|
image: signoz/signoz:v0.92.1
|
||||||
command:
|
command:
|
||||||
- --config=/root/config/prometheus.yml
|
- --config=/root/config/prometheus.yml
|
||||||
ports:
|
ports:
|
||||||
@@ -150,7 +148,7 @@ services:
|
|||||||
retries: 3
|
retries: 3
|
||||||
otel-collector:
|
otel-collector:
|
||||||
!!merge <<: *db-depend
|
!!merge <<: *db-depend
|
||||||
image: signoz/signoz-otel-collector:v0.129.4
|
image: signoz/signoz-otel-collector:v0.129.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
|
||||||
@@ -176,7 +174,7 @@ services:
|
|||||||
- signoz
|
- signoz
|
||||||
schema-migrator:
|
schema-migrator:
|
||||||
!!merge <<: *common
|
!!merge <<: *common
|
||||||
image: signoz/signoz-schema-migrator:v0.129.4
|
image: signoz/signoz-schema-migrator:v0.129.0
|
||||||
deploy:
|
deploy:
|
||||||
restart_policy:
|
restart_policy:
|
||||||
condition: on-failure
|
condition: on-failure
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ x-common: &common
|
|||||||
x-clickhouse-defaults: &clickhouse-defaults
|
x-clickhouse-defaults: &clickhouse-defaults
|
||||||
!!merge <<: *common
|
!!merge <<: *common
|
||||||
# addding non LTS version due to this fix https://github.com/ClickHouse/ClickHouse/commit/32caf8716352f45c1b617274c7508c86b7d1afab
|
# addding non LTS version due to this fix https://github.com/ClickHouse/ClickHouse/commit/32caf8716352f45c1b617274c7508c86b7d1afab
|
||||||
image: clickhouse/clickhouse-server:25.5.6
|
image: clickhouse/clickhouse-server:24.1.2-alpine
|
||||||
tty: true
|
tty: true
|
||||||
labels:
|
labels:
|
||||||
signoz.io/scrape: "true"
|
signoz.io/scrape: "true"
|
||||||
@@ -40,8 +40,6 @@ x-clickhouse-defaults: &clickhouse-defaults
|
|||||||
nofile:
|
nofile:
|
||||||
soft: 262144
|
soft: 262144
|
||||||
hard: 262144
|
hard: 262144
|
||||||
environment:
|
|
||||||
- CLICKHOUSE_SKIP_USER_SETUP=1
|
|
||||||
x-zookeeper-defaults: &zookeeper-defaults
|
x-zookeeper-defaults: &zookeeper-defaults
|
||||||
!!merge <<: *common
|
!!merge <<: *common
|
||||||
image: signoz/zookeeper:3.7.1
|
image: signoz/zookeeper:3.7.1
|
||||||
@@ -67,7 +65,7 @@ x-db-depend: &db-depend
|
|||||||
services:
|
services:
|
||||||
init-clickhouse:
|
init-clickhouse:
|
||||||
!!merge <<: *common
|
!!merge <<: *common
|
||||||
image: clickhouse/clickhouse-server:25.5.6
|
image: clickhouse/clickhouse-server:24.1.2-alpine
|
||||||
container_name: signoz-init-clickhouse
|
container_name: signoz-init-clickhouse
|
||||||
command:
|
command:
|
||||||
- bash
|
- bash
|
||||||
@@ -179,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.94.1}
|
image: signoz/signoz:${VERSION:-v0.92.1}
|
||||||
container_name: signoz
|
container_name: signoz
|
||||||
command:
|
command:
|
||||||
- --config=/root/config/prometheus.yml
|
- --config=/root/config/prometheus.yml
|
||||||
@@ -213,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.129.4}
|
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-v0.129.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
|
||||||
@@ -239,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.129.4}
|
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-v0.129.0}
|
||||||
container_name: schema-migrator-sync
|
container_name: schema-migrator-sync
|
||||||
command:
|
command:
|
||||||
- sync
|
- sync
|
||||||
@@ -250,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.129.4}
|
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-v0.129.0}
|
||||||
container_name: schema-migrator-async
|
container_name: schema-migrator-async
|
||||||
command:
|
command:
|
||||||
- async
|
- async
|
||||||
|
|||||||
@@ -9,7 +9,8 @@ x-common: &common
|
|||||||
max-file: "3"
|
max-file: "3"
|
||||||
x-clickhouse-defaults: &clickhouse-defaults
|
x-clickhouse-defaults: &clickhouse-defaults
|
||||||
!!merge <<: *common
|
!!merge <<: *common
|
||||||
image: clickhouse/clickhouse-server:25.5.6
|
# addding non LTS version due to this fix https://github.com/ClickHouse/ClickHouse/commit/32caf8716352f45c1b617274c7508c86b7d1afab
|
||||||
|
image: clickhouse/clickhouse-server:24.1.2-alpine
|
||||||
tty: true
|
tty: true
|
||||||
labels:
|
labels:
|
||||||
signoz.io/scrape: "true"
|
signoz.io/scrape: "true"
|
||||||
@@ -35,8 +36,6 @@ x-clickhouse-defaults: &clickhouse-defaults
|
|||||||
nofile:
|
nofile:
|
||||||
soft: 262144
|
soft: 262144
|
||||||
hard: 262144
|
hard: 262144
|
||||||
environment:
|
|
||||||
- CLICKHOUSE_SKIP_USER_SETUP=1
|
|
||||||
x-zookeeper-defaults: &zookeeper-defaults
|
x-zookeeper-defaults: &zookeeper-defaults
|
||||||
!!merge <<: *common
|
!!merge <<: *common
|
||||||
image: signoz/zookeeper:3.7.1
|
image: signoz/zookeeper:3.7.1
|
||||||
@@ -62,7 +61,7 @@ x-db-depend: &db-depend
|
|||||||
services:
|
services:
|
||||||
init-clickhouse:
|
init-clickhouse:
|
||||||
!!merge <<: *common
|
!!merge <<: *common
|
||||||
image: clickhouse/clickhouse-server:25.5.6
|
image: clickhouse/clickhouse-server:24.1.2-alpine
|
||||||
container_name: signoz-init-clickhouse
|
container_name: signoz-init-clickhouse
|
||||||
command:
|
command:
|
||||||
- bash
|
- bash
|
||||||
@@ -111,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.94.1}
|
image: signoz/signoz:${VERSION:-v0.92.1}
|
||||||
container_name: signoz
|
container_name: signoz
|
||||||
command:
|
command:
|
||||||
- --config=/root/config/prometheus.yml
|
- --config=/root/config/prometheus.yml
|
||||||
@@ -144,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.129.4}
|
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-v0.129.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
|
||||||
@@ -166,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.129.4}
|
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-v0.129.0}
|
||||||
container_name: schema-migrator-sync
|
container_name: schema-migrator-sync
|
||||||
command:
|
command:
|
||||||
- sync
|
- sync
|
||||||
@@ -178,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.129.4}
|
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-v0.129.0}
|
||||||
container_name: schema-migrator-async
|
container_name: schema-migrator-async
|
||||||
command:
|
command:
|
||||||
- async
|
- async
|
||||||
|
|||||||
@@ -13,11 +13,11 @@ import (
|
|||||||
"github.com/SigNoz/signoz/ee/query-service/constants"
|
"github.com/SigNoz/signoz/ee/query-service/constants"
|
||||||
"github.com/SigNoz/signoz/pkg/errors"
|
"github.com/SigNoz/signoz/pkg/errors"
|
||||||
"github.com/SigNoz/signoz/pkg/http/render"
|
"github.com/SigNoz/signoz/pkg/http/render"
|
||||||
"github.com/SigNoz/signoz/pkg/modules/user"
|
|
||||||
basemodel "github.com/SigNoz/signoz/pkg/query-service/model"
|
basemodel "github.com/SigNoz/signoz/pkg/query-service/model"
|
||||||
"github.com/SigNoz/signoz/pkg/types"
|
"github.com/SigNoz/signoz/pkg/types"
|
||||||
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
||||||
"github.com/SigNoz/signoz/pkg/valuer"
|
"github.com/SigNoz/signoz/pkg/valuer"
|
||||||
|
"github.com/google/uuid"
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
)
|
)
|
||||||
@@ -192,14 +192,14 @@ func (ah *APIHandler) getOrCreateCloudIntegrationUser(
|
|||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
password := types.MustGenerateFactorPassword(newUser.ID.StringValue())
|
password, err := types.NewFactorPassword(uuid.NewString())
|
||||||
|
|
||||||
err = ah.Signoz.Modules.User.CreateUser(ctx, newUser, user.WithFactorPassword(password))
|
integrationUser, err := ah.Signoz.Modules.User.CreateUserWithPassword(ctx, newUser, password)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, basemodel.InternalError(fmt.Errorf("couldn't create cloud integration user: %w", err))
|
return nil, basemodel.InternalError(fmt.Errorf("couldn't create cloud integration user: %w", err))
|
||||||
}
|
}
|
||||||
|
|
||||||
return newUser, nil
|
return integrationUser, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func getIngestionUrlAndSigNozAPIUrl(ctx context.Context, licenseKey string) (
|
func getIngestionUrlAndSigNozAPIUrl(ctx context.Context, licenseKey string) (
|
||||||
|
|||||||
@@ -257,7 +257,6 @@ func (s *Server) createPublicServer(apiHandler *api.APIHandler, web web.Web) (*h
|
|||||||
s.config.APIServer.Timeout.Max,
|
s.config.APIServer.Timeout.Max,
|
||||||
).Wrap)
|
).Wrap)
|
||||||
r.Use(middleware.NewLogging(s.signoz.Instrumentation.Logger(), s.config.APIServer.Logging.ExcludedRoutes).Wrap)
|
r.Use(middleware.NewLogging(s.signoz.Instrumentation.Logger(), s.config.APIServer.Logging.ExcludedRoutes).Wrap)
|
||||||
r.Use(middleware.NewComment().Wrap)
|
|
||||||
|
|
||||||
apiHandler.RegisterRoutes(r, am)
|
apiHandler.RegisterRoutes(r, am)
|
||||||
apiHandler.RegisterLogsRoutes(r, am)
|
apiHandler.RegisterLogsRoutes(r, am)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
package model
|
package model
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"fmt"
|
||||||
|
|
||||||
basemodel "github.com/SigNoz/signoz/pkg/query-service/model"
|
basemodel "github.com/SigNoz/signoz/pkg/query-service/model"
|
||||||
)
|
)
|
||||||
@@ -57,7 +57,7 @@ func Unauthorized(err error) *ApiError {
|
|||||||
func BadRequestStr(s string) *ApiError {
|
func BadRequestStr(s string) *ApiError {
|
||||||
return &ApiError{
|
return &ApiError{
|
||||||
Typ: basemodel.ErrorBadData,
|
Typ: basemodel.ErrorBadData,
|
||||||
Err: errors.New(s),
|
Err: fmt.Errorf(s),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -73,7 +73,7 @@ func InternalError(err error) *ApiError {
|
|||||||
func InternalErrorStr(s string) *ApiError {
|
func InternalErrorStr(s string) *ApiError {
|
||||||
return &ApiError{
|
return &ApiError{
|
||||||
Typ: basemodel.ErrorInternal,
|
Typ: basemodel.ErrorInternal,
|
||||||
Err: errors.New(s),
|
Err: fmt.Errorf(s),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
node_modules
|
node_modules
|
||||||
build
|
build
|
||||||
*.typegen.ts
|
*.typegen.ts
|
||||||
i18-generate-hash.js
|
i18-generate-hash.js
|
||||||
src/parser/TraceOperatorParser/**
|
|
||||||
@@ -10,6 +10,4 @@ public/
|
|||||||
**/*.json
|
**/*.json
|
||||||
|
|
||||||
# Ignore all files in parser folder:
|
# Ignore all files in parser folder:
|
||||||
src/parser/**
|
src/parser/**
|
||||||
|
|
||||||
src/TraceOperator/parser/**
|
|
||||||
@@ -16,7 +16,6 @@ const config: Config.InitialOptions = {
|
|||||||
'ts-jest': {
|
'ts-jest': {
|
||||||
useESM: true,
|
useESM: true,
|
||||||
isolatedModules: true,
|
isolatedModules: true,
|
||||||
tsconfig: '<rootDir>/tsconfig.jest.json',
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
testMatch: ['<rootDir>/src/**/*?(*.)(test).(ts|js)?(x)'],
|
testMatch: ['<rootDir>/src/**/*?(*.)(test).(ts|js)?(x)'],
|
||||||
@@ -26,7 +25,7 @@ const config: Config.InitialOptions = {
|
|||||||
'^.+\\.(js|jsx)$': 'babel-jest',
|
'^.+\\.(js|jsx)$': 'babel-jest',
|
||||||
},
|
},
|
||||||
transformIgnorePatterns: [
|
transformIgnorePatterns: [
|
||||||
'node_modules/(?!(lodash-es|react-dnd|core-dnd|@react-dnd|dnd-core|react-dnd-html5-backend|axios|@signozhq/design-tokens|@signozhq/table|@signozhq/calendar|@signozhq/input|@signozhq/popover|@signozhq/button|@signozhq/sonner|@signozhq/*|date-fns|d3-interpolate|d3-color|api|@codemirror|@lezer|@marijn)/)',
|
'node_modules/(?!(lodash-es|react-dnd|core-dnd|@react-dnd|dnd-core|react-dnd-html5-backend|axios|@signozhq/design-tokens|d3-interpolate|d3-color|api|@codemirror|@lezer|@marijn)/)',
|
||||||
],
|
],
|
||||||
setupFilesAfterEnv: ['<rootDir>jest.setup.ts'],
|
setupFilesAfterEnv: ['<rootDir>jest.setup.ts'],
|
||||||
testPathIgnorePatterns: ['/node_modules/', '/public/'],
|
testPathIgnorePatterns: ['/node_modules/', '/public/'],
|
||||||
|
|||||||
@@ -43,19 +43,11 @@
|
|||||||
"@radix-ui/react-tooltip": "1.0.7",
|
"@radix-ui/react-tooltip": "1.0.7",
|
||||||
"@sentry/react": "8.41.0",
|
"@sentry/react": "8.41.0",
|
||||||
"@sentry/webpack-plugin": "2.22.6",
|
"@sentry/webpack-plugin": "2.22.6",
|
||||||
"@signozhq/badge": "0.0.2",
|
|
||||||
"@signozhq/calendar": "0.0.0",
|
|
||||||
"@signozhq/callout": "0.0.2",
|
|
||||||
"@signozhq/design-tokens": "1.1.4",
|
"@signozhq/design-tokens": "1.1.4",
|
||||||
"@signozhq/input": "0.0.2",
|
|
||||||
"@signozhq/popover": "0.0.0",
|
|
||||||
"@signozhq/sonner": "0.1.0",
|
|
||||||
"@signozhq/table": "0.3.7",
|
|
||||||
"@signozhq/tooltip": "0.0.2",
|
|
||||||
"@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/codemirror-theme-github": "4.24.1",
|
"@uiw/codemirror-theme-github": "4.24.1",
|
||||||
|
"@uiw/codemirror-theme-copilot": "4.23.11",
|
||||||
"@uiw/react-codemirror": "4.23.10",
|
"@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",
|
||||||
@@ -100,7 +92,6 @@
|
|||||||
"i18next-http-backend": "^1.3.2",
|
"i18next-http-backend": "^1.3.2",
|
||||||
"jest": "^27.5.1",
|
"jest": "^27.5.1",
|
||||||
"js-base64": "^3.7.2",
|
"js-base64": "^3.7.2",
|
||||||
"kbar": "0.1.0-beta.48",
|
|
||||||
"less": "^4.1.2",
|
"less": "^4.1.2",
|
||||||
"less-loader": "^10.2.0",
|
"less-loader": "^10.2.0",
|
||||||
"lodash-es": "^4.17.21",
|
"lodash-es": "^4.17.21",
|
||||||
|
|||||||
@@ -48,6 +48,6 @@
|
|||||||
"INFRASTRUCTURE_MONITORING_HOSTS": "SigNoz | Infra Monitoring",
|
"INFRASTRUCTURE_MONITORING_HOSTS": "SigNoz | Infra Monitoring",
|
||||||
"INFRASTRUCTURE_MONITORING_KUBERNETES": "SigNoz | Infra Monitoring",
|
"INFRASTRUCTURE_MONITORING_KUBERNETES": "SigNoz | Infra Monitoring",
|
||||||
"METER_EXPLORER": "SigNoz | Meter Explorer",
|
"METER_EXPLORER": "SigNoz | Meter Explorer",
|
||||||
"METER_EXPLORER_VIEWS": "SigNoz | Meter Explorer Views",
|
"METER_EXPLORER_VIEWS": "SigNoz | Meter Explorer",
|
||||||
"METER": "SigNoz | Meter"
|
"METER_EXPLORER_BASE": "SigNoz | Meter Explorer"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -71,6 +71,6 @@
|
|||||||
"METRICS_EXPLORER_VIEWS": "SigNoz | Metrics Explorer",
|
"METRICS_EXPLORER_VIEWS": "SigNoz | Metrics Explorer",
|
||||||
"API_MONITORING": "SigNoz | External APIs",
|
"API_MONITORING": "SigNoz | External APIs",
|
||||||
"METER_EXPLORER": "SigNoz | Meter Explorer",
|
"METER_EXPLORER": "SigNoz | Meter Explorer",
|
||||||
"METER_EXPLORER_VIEWS": "SigNoz | Meter Explorer Views",
|
"METER_EXPLORER_VIEWS": "SigNoz | Meter Explorer",
|
||||||
"METER": "SigNoz | Meter"
|
"METER_EXPLORER_BASE": "SigNoz | Meter Explorer"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import getLocalStorageApi from 'api/browser/localstorage/get';
|
|||||||
import setLocalStorageApi from 'api/browser/localstorage/set';
|
import setLocalStorageApi from 'api/browser/localstorage/set';
|
||||||
import logEvent from 'api/common/logEvent';
|
import logEvent from 'api/common/logEvent';
|
||||||
import AppLoading from 'components/AppLoading/AppLoading';
|
import AppLoading from 'components/AppLoading/AppLoading';
|
||||||
import KBarCommandPalette from 'components/KBarCommandPalette/KBarCommandPalette';
|
|
||||||
import NotFound from 'components/NotFound';
|
import NotFound from 'components/NotFound';
|
||||||
import Spinner from 'components/Spinner';
|
import Spinner from 'components/Spinner';
|
||||||
import UserpilotRouteTracker from 'components/UserpilotRouteTracker/UserpilotRouteTracker';
|
import UserpilotRouteTracker from 'components/UserpilotRouteTracker/UserpilotRouteTracker';
|
||||||
@@ -26,7 +25,6 @@ import { useAppContext } from 'providers/App/App';
|
|||||||
import { IUser } from 'providers/App/types';
|
import { IUser } from 'providers/App/types';
|
||||||
import { DashboardProvider } from 'providers/Dashboard/Dashboard';
|
import { DashboardProvider } from 'providers/Dashboard/Dashboard';
|
||||||
import { ErrorModalProvider } from 'providers/ErrorModalProvider';
|
import { ErrorModalProvider } from 'providers/ErrorModalProvider';
|
||||||
import { KBarCommandPaletteProvider } from 'providers/KBarCommandPaletteProvider';
|
|
||||||
import { QueryBuilderProvider } from 'providers/QueryBuilder';
|
import { QueryBuilderProvider } from 'providers/QueryBuilder';
|
||||||
import { Suspense, useCallback, useEffect, useState } from 'react';
|
import { Suspense, useCallback, useEffect, useState } from 'react';
|
||||||
import { Route, Router, Switch } from 'react-router-dom';
|
import { Route, Router, Switch } from 'react-router-dom';
|
||||||
@@ -370,42 +368,39 @@ function App(): JSX.Element {
|
|||||||
<ConfigProvider theme={themeConfig}>
|
<ConfigProvider theme={themeConfig}>
|
||||||
<Router history={history}>
|
<Router history={history}>
|
||||||
<CompatRouter>
|
<CompatRouter>
|
||||||
<KBarCommandPaletteProvider>
|
<UserpilotRouteTracker />
|
||||||
<UserpilotRouteTracker />
|
<NotificationProvider>
|
||||||
<KBarCommandPalette />
|
<ErrorModalProvider>
|
||||||
<NotificationProvider>
|
<PrivateRoute>
|
||||||
<ErrorModalProvider>
|
<ResourceProvider>
|
||||||
<PrivateRoute>
|
<QueryBuilderProvider>
|
||||||
<ResourceProvider>
|
<DashboardProvider>
|
||||||
<QueryBuilderProvider>
|
<KeyboardHotkeysProvider>
|
||||||
<DashboardProvider>
|
<AlertRuleProvider>
|
||||||
<KeyboardHotkeysProvider>
|
<AppLayout>
|
||||||
<AlertRuleProvider>
|
<Suspense fallback={<Spinner size="large" tip="Loading..." />}>
|
||||||
<AppLayout>
|
<Switch>
|
||||||
<Suspense fallback={<Spinner size="large" tip="Loading..." />}>
|
{routes.map(({ path, component, exact }) => (
|
||||||
<Switch>
|
<Route
|
||||||
{routes.map(({ path, component, exact }) => (
|
key={`${path}`}
|
||||||
<Route
|
exact={exact}
|
||||||
key={`${path}`}
|
path={path}
|
||||||
exact={exact}
|
component={component}
|
||||||
path={path}
|
/>
|
||||||
component={component}
|
))}
|
||||||
/>
|
<Route exact path="/" component={Home} />
|
||||||
))}
|
<Route path="*" component={NotFound} />
|
||||||
<Route exact path="/" component={Home} />
|
</Switch>
|
||||||
<Route path="*" component={NotFound} />
|
</Suspense>
|
||||||
</Switch>
|
</AppLayout>
|
||||||
</Suspense>
|
</AlertRuleProvider>
|
||||||
</AppLayout>
|
</KeyboardHotkeysProvider>
|
||||||
</AlertRuleProvider>
|
</DashboardProvider>
|
||||||
</KeyboardHotkeysProvider>
|
</QueryBuilderProvider>
|
||||||
</DashboardProvider>
|
</ResourceProvider>
|
||||||
</QueryBuilderProvider>
|
</PrivateRoute>
|
||||||
</ResourceProvider>
|
</ErrorModalProvider>
|
||||||
</PrivateRoute>
|
</NotificationProvider>
|
||||||
</ErrorModalProvider>
|
|
||||||
</NotificationProvider>
|
|
||||||
</KBarCommandPaletteProvider>
|
|
||||||
</CompatRouter>
|
</CompatRouter>
|
||||||
</Router>
|
</Router>
|
||||||
</ConfigProvider>
|
</ConfigProvider>
|
||||||
|
|||||||
@@ -437,10 +437,10 @@ const routes: AppRoutes[] = [
|
|||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
path: ROUTES.METER,
|
path: ROUTES.METER_EXPLORER_BASE,
|
||||||
exact: true,
|
exact: true,
|
||||||
component: MeterExplorer,
|
component: MeterExplorer,
|
||||||
key: 'METER',
|
key: 'METER_EXPLORER_BASE',
|
||||||
isPrivate: true,
|
isPrivate: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,115 +0,0 @@
|
|||||||
/* eslint-disable sonarjs/no-duplicate-string */
|
|
||||||
import { ApiBaseInstance } from 'api';
|
|
||||||
|
|
||||||
import { getFieldKeys } from '../getFieldKeys';
|
|
||||||
|
|
||||||
// Mock the API instance
|
|
||||||
jest.mock('api', () => ({
|
|
||||||
ApiBaseInstance: {
|
|
||||||
get: jest.fn(),
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
describe('getFieldKeys API', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
jest.clearAllMocks();
|
|
||||||
});
|
|
||||||
|
|
||||||
const mockSuccessResponse = {
|
|
||||||
status: 200,
|
|
||||||
data: {
|
|
||||||
status: 'success',
|
|
||||||
data: {
|
|
||||||
keys: {
|
|
||||||
'service.name': [],
|
|
||||||
'http.status_code': [],
|
|
||||||
},
|
|
||||||
complete: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
it('should call API with correct parameters when no args provided', async () => {
|
|
||||||
// Mock successful API response
|
|
||||||
(ApiBaseInstance.get as jest.Mock).mockResolvedValueOnce(mockSuccessResponse);
|
|
||||||
|
|
||||||
// Call function with no parameters
|
|
||||||
await getFieldKeys();
|
|
||||||
|
|
||||||
// Verify API was called correctly with empty params object
|
|
||||||
expect(ApiBaseInstance.get).toHaveBeenCalledWith('/fields/keys', {
|
|
||||||
params: {},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should call API with signal parameter when provided', async () => {
|
|
||||||
// Mock successful API response
|
|
||||||
(ApiBaseInstance.get as jest.Mock).mockResolvedValueOnce(mockSuccessResponse);
|
|
||||||
|
|
||||||
// Call function with signal parameter
|
|
||||||
await getFieldKeys('traces');
|
|
||||||
|
|
||||||
// Verify API was called with signal parameter
|
|
||||||
expect(ApiBaseInstance.get).toHaveBeenCalledWith('/fields/keys', {
|
|
||||||
params: { signal: 'traces' },
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should call API with name parameter when provided', async () => {
|
|
||||||
// Mock successful API response
|
|
||||||
(ApiBaseInstance.get as jest.Mock).mockResolvedValueOnce({
|
|
||||||
status: 200,
|
|
||||||
data: {
|
|
||||||
status: 'success',
|
|
||||||
data: {
|
|
||||||
keys: { service: [] },
|
|
||||||
complete: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Call function with name parameter
|
|
||||||
await getFieldKeys(undefined, 'service');
|
|
||||||
|
|
||||||
// Verify API was called with name parameter
|
|
||||||
expect(ApiBaseInstance.get).toHaveBeenCalledWith('/fields/keys', {
|
|
||||||
params: { name: 'service' },
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should call API with both signal and name when provided', async () => {
|
|
||||||
// Mock successful API response
|
|
||||||
(ApiBaseInstance.get as jest.Mock).mockResolvedValueOnce({
|
|
||||||
status: 200,
|
|
||||||
data: {
|
|
||||||
status: 'success',
|
|
||||||
data: {
|
|
||||||
keys: { service: [] },
|
|
||||||
complete: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Call function with both parameters
|
|
||||||
await getFieldKeys('logs', 'service');
|
|
||||||
|
|
||||||
// Verify API was called with both parameters
|
|
||||||
expect(ApiBaseInstance.get).toHaveBeenCalledWith('/fields/keys', {
|
|
||||||
params: { signal: 'logs', name: 'service' },
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return properly formatted response', async () => {
|
|
||||||
// Mock API to return our response
|
|
||||||
(ApiBaseInstance.get as jest.Mock).mockResolvedValueOnce(mockSuccessResponse);
|
|
||||||
|
|
||||||
// Call the function
|
|
||||||
const result = await getFieldKeys('traces');
|
|
||||||
|
|
||||||
// Verify the returned structure matches SuccessResponseV2 format
|
|
||||||
expect(result).toEqual({
|
|
||||||
httpStatusCode: 200,
|
|
||||||
data: mockSuccessResponse.data.data,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,214 +0,0 @@
|
|||||||
/* eslint-disable sonarjs/no-duplicate-string */
|
|
||||||
import { ApiBaseInstance } from 'api';
|
|
||||||
|
|
||||||
import { getFieldValues } from '../getFieldValues';
|
|
||||||
|
|
||||||
// Mock the API instance
|
|
||||||
jest.mock('api', () => ({
|
|
||||||
ApiBaseInstance: {
|
|
||||||
get: jest.fn(),
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
describe('getFieldValues API', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
jest.clearAllMocks();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should call the API with correct parameters (no options)', async () => {
|
|
||||||
// Mock API response
|
|
||||||
(ApiBaseInstance.get as jest.Mock).mockResolvedValueOnce({
|
|
||||||
status: 200,
|
|
||||||
data: {
|
|
||||||
status: 'success',
|
|
||||||
data: {
|
|
||||||
values: {
|
|
||||||
stringValues: ['frontend', 'backend'],
|
|
||||||
},
|
|
||||||
complete: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Call function without parameters
|
|
||||||
await getFieldValues();
|
|
||||||
|
|
||||||
// Verify API was called correctly with empty params
|
|
||||||
expect(ApiBaseInstance.get).toHaveBeenCalledWith('/fields/values', {
|
|
||||||
params: {},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should call the API with signal parameter', async () => {
|
|
||||||
// Mock API response
|
|
||||||
(ApiBaseInstance.get as jest.Mock).mockResolvedValueOnce({
|
|
||||||
status: 200,
|
|
||||||
data: {
|
|
||||||
status: 'success',
|
|
||||||
data: {
|
|
||||||
values: {
|
|
||||||
stringValues: ['frontend', 'backend'],
|
|
||||||
},
|
|
||||||
complete: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Call function with signal parameter
|
|
||||||
await getFieldValues('traces');
|
|
||||||
|
|
||||||
// Verify API was called with signal parameter
|
|
||||||
expect(ApiBaseInstance.get).toHaveBeenCalledWith('/fields/values', {
|
|
||||||
params: { signal: 'traces' },
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should call the API with name parameter', async () => {
|
|
||||||
// Mock API response
|
|
||||||
(ApiBaseInstance.get as jest.Mock).mockResolvedValueOnce({
|
|
||||||
status: 200,
|
|
||||||
data: {
|
|
||||||
status: 'success',
|
|
||||||
data: {
|
|
||||||
values: {
|
|
||||||
stringValues: ['frontend', 'backend'],
|
|
||||||
},
|
|
||||||
complete: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Call function with name parameter
|
|
||||||
await getFieldValues(undefined, 'service.name');
|
|
||||||
|
|
||||||
// Verify API was called with name parameter
|
|
||||||
expect(ApiBaseInstance.get).toHaveBeenCalledWith('/fields/values', {
|
|
||||||
params: { name: 'service.name' },
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should call the API with value parameter', async () => {
|
|
||||||
// Mock API response
|
|
||||||
(ApiBaseInstance.get as jest.Mock).mockResolvedValueOnce({
|
|
||||||
status: 200,
|
|
||||||
data: {
|
|
||||||
status: 'success',
|
|
||||||
data: {
|
|
||||||
values: {
|
|
||||||
stringValues: ['frontend'],
|
|
||||||
},
|
|
||||||
complete: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Call function with value parameter
|
|
||||||
await getFieldValues(undefined, 'service.name', 'front');
|
|
||||||
|
|
||||||
// Verify API was called with value parameter
|
|
||||||
expect(ApiBaseInstance.get).toHaveBeenCalledWith('/fields/values', {
|
|
||||||
params: { name: 'service.name', searchText: 'front' },
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should call the API with time range parameters', async () => {
|
|
||||||
// Mock API response
|
|
||||||
(ApiBaseInstance.get as jest.Mock).mockResolvedValueOnce({
|
|
||||||
status: 200,
|
|
||||||
data: {
|
|
||||||
status: 'success',
|
|
||||||
data: {
|
|
||||||
values: {
|
|
||||||
stringValues: ['frontend', 'backend'],
|
|
||||||
},
|
|
||||||
complete: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Call function with time range parameters
|
|
||||||
const startUnixMilli = 1625097600000000; // Note: nanoseconds
|
|
||||||
const endUnixMilli = 1625184000000000;
|
|
||||||
await getFieldValues(
|
|
||||||
'logs',
|
|
||||||
'service.name',
|
|
||||||
undefined,
|
|
||||||
startUnixMilli,
|
|
||||||
endUnixMilli,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Verify API was called with time range parameters (converted to milliseconds)
|
|
||||||
expect(ApiBaseInstance.get).toHaveBeenCalledWith('/fields/values', {
|
|
||||||
params: {
|
|
||||||
signal: 'logs',
|
|
||||||
name: 'service.name',
|
|
||||||
startUnixMilli: '1625097600', // Should be converted to seconds (divided by 1000000)
|
|
||||||
endUnixMilli: '1625184000', // Should be converted to seconds (divided by 1000000)
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should normalize the response values', async () => {
|
|
||||||
// Mock API response with multiple value types
|
|
||||||
const mockResponse = {
|
|
||||||
status: 200,
|
|
||||||
data: {
|
|
||||||
status: 'success',
|
|
||||||
data: {
|
|
||||||
values: {
|
|
||||||
stringValues: ['frontend', 'backend'],
|
|
||||||
numberValues: [200, 404],
|
|
||||||
boolValues: [true, false],
|
|
||||||
},
|
|
||||||
complete: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
(ApiBaseInstance.get as jest.Mock).mockResolvedValueOnce(mockResponse);
|
|
||||||
|
|
||||||
// Call the function
|
|
||||||
const result = await getFieldValues('traces', 'mixed.values');
|
|
||||||
|
|
||||||
// Verify the response has normalized values array
|
|
||||||
expect(result.data?.normalizedValues).toContain('frontend');
|
|
||||||
expect(result.data?.normalizedValues).toContain('backend');
|
|
||||||
expect(result.data?.normalizedValues).toContain('200');
|
|
||||||
expect(result.data?.normalizedValues).toContain('404');
|
|
||||||
expect(result.data?.normalizedValues).toContain('true');
|
|
||||||
expect(result.data?.normalizedValues).toContain('false');
|
|
||||||
expect(result.data?.normalizedValues?.length).toBe(6);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return a properly formatted success response', async () => {
|
|
||||||
// Create mock response
|
|
||||||
const mockApiResponse = {
|
|
||||||
status: 200,
|
|
||||||
data: {
|
|
||||||
status: 'success',
|
|
||||||
data: {
|
|
||||||
values: {
|
|
||||||
stringValues: ['frontend', 'backend'],
|
|
||||||
},
|
|
||||||
complete: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
// Mock API to return our response
|
|
||||||
(ApiBaseInstance.get as jest.Mock).mockResolvedValueOnce(mockApiResponse);
|
|
||||||
|
|
||||||
// Call the function
|
|
||||||
const result = await getFieldValues('traces', 'service.name');
|
|
||||||
|
|
||||||
// Verify the returned structure matches SuccessResponseV2 format
|
|
||||||
expect(result).toEqual({
|
|
||||||
httpStatusCode: 200,
|
|
||||||
data: expect.objectContaining({
|
|
||||||
values: expect.any(Object),
|
|
||||||
normalizedValues: expect.any(Array),
|
|
||||||
complete: true,
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
import { ApiBaseInstance } from 'api';
|
|
||||||
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
|
|
||||||
import { AxiosError } from 'axios';
|
|
||||||
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
|
|
||||||
import { FieldKeyResponse } from 'types/api/dynamicVariables/getFieldKeys';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get field keys for a given signal type
|
|
||||||
* @param signal Type of signal (traces, logs, metrics)
|
|
||||||
* @param name Optional search text
|
|
||||||
*/
|
|
||||||
export const getFieldKeys = async (
|
|
||||||
signal?: 'traces' | 'logs' | 'metrics',
|
|
||||||
name?: string,
|
|
||||||
): Promise<SuccessResponseV2<FieldKeyResponse>> => {
|
|
||||||
const params: Record<string, string> = {};
|
|
||||||
|
|
||||||
if (signal) {
|
|
||||||
params.signal = encodeURIComponent(signal);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name) {
|
|
||||||
params.name = encodeURIComponent(name);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await ApiBaseInstance.get('/fields/keys', { params });
|
|
||||||
|
|
||||||
return {
|
|
||||||
httpStatusCode: response.status,
|
|
||||||
data: response.data.data,
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export default getFieldKeys;
|
|
||||||
@@ -1,87 +0,0 @@
|
|||||||
/* eslint-disable sonarjs/cognitive-complexity */
|
|
||||||
import { ApiBaseInstance } from 'api';
|
|
||||||
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
|
|
||||||
import { AxiosError } from 'axios';
|
|
||||||
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
|
|
||||||
import { FieldValueResponse } from 'types/api/dynamicVariables/getFieldValues';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get field values for a given signal type and field name
|
|
||||||
* @param signal Type of signal (traces, logs, metrics)
|
|
||||||
* @param name Name of the attribute for which values are being fetched
|
|
||||||
* @param value Optional search text
|
|
||||||
* @param existingQuery Optional existing query - across all present dynamic variables
|
|
||||||
*/
|
|
||||||
export const getFieldValues = async (
|
|
||||||
signal?: 'traces' | 'logs' | 'metrics',
|
|
||||||
name?: string,
|
|
||||||
searchText?: string,
|
|
||||||
startUnixMilli?: number,
|
|
||||||
endUnixMilli?: number,
|
|
||||||
existingQuery?: string,
|
|
||||||
): Promise<SuccessResponseV2<FieldValueResponse>> => {
|
|
||||||
const params: Record<string, string> = {};
|
|
||||||
|
|
||||||
if (signal) {
|
|
||||||
params.signal = encodeURIComponent(signal);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name) {
|
|
||||||
params.name = encodeURIComponent(name);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (searchText) {
|
|
||||||
params.searchText = encodeURIComponent(searchText);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (startUnixMilli) {
|
|
||||||
params.startUnixMilli = Math.floor(startUnixMilli / 1000000).toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (endUnixMilli) {
|
|
||||||
params.endUnixMilli = Math.floor(endUnixMilli / 1000000).toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (existingQuery) {
|
|
||||||
params.existingQuery = existingQuery;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await ApiBaseInstance.get('/fields/values', { params });
|
|
||||||
|
|
||||||
// Normalize values from different types (stringValues, boolValues, etc.)
|
|
||||||
if (response.data?.data?.values) {
|
|
||||||
const allValues: string[] = [];
|
|
||||||
Object.entries(response.data?.data?.values).forEach(
|
|
||||||
([key, valueArray]: [string, any]) => {
|
|
||||||
// Skip RelatedValues as they should be kept separate
|
|
||||||
if (key === 'relatedValues') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Array.isArray(valueArray)) {
|
|
||||||
allValues.push(...valueArray.map(String));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
// Add a normalized values array to the response
|
|
||||||
response.data.data.normalizedValues = allValues;
|
|
||||||
|
|
||||||
// Add relatedValues to the response as per FieldValueResponse
|
|
||||||
if (response.data?.data?.values?.relatedValues) {
|
|
||||||
response.data.data.relatedValues =
|
|
||||||
response.data?.data?.values?.relatedValues;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
httpStatusCode: response.status,
|
|
||||||
data: response.data.data,
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export default getFieldValues;
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
import { ApiV2Instance } from 'api';
|
|
||||||
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
|
|
||||||
import { AxiosError } from 'axios';
|
|
||||||
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
|
|
||||||
import { PayloadProps } from 'types/api/settings/getRetention';
|
|
||||||
|
|
||||||
// Only works for logs
|
|
||||||
const getRetentionV2 = async (): Promise<
|
|
||||||
SuccessResponseV2<PayloadProps<'logs'>>
|
|
||||||
> => {
|
|
||||||
try {
|
|
||||||
const response = await ApiV2Instance.get<PayloadProps<'logs'>>(
|
|
||||||
`/settings/ttl`,
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
|
||||||
httpStatusCode: response.status,
|
|
||||||
data: response.data,
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export default getRetentionV2;
|
|
||||||
@@ -1,14 +1,14 @@
|
|||||||
import axios from 'api';
|
import axios from 'api';
|
||||||
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
|
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
||||||
import { AxiosError } from 'axios';
|
import { AxiosError } from 'axios';
|
||||||
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
|
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||||
import { PayloadPropsV2, Props } from 'types/api/settings/setRetention';
|
import { PayloadProps, Props } from 'types/api/settings/setRetention';
|
||||||
|
|
||||||
const setRetention = async (
|
const setRetention = async (
|
||||||
props: Props,
|
props: Props,
|
||||||
): Promise<SuccessResponseV2<PayloadPropsV2>> => {
|
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
|
||||||
try {
|
try {
|
||||||
const response = await axios.post<PayloadPropsV2>(
|
const response = await axios.post<PayloadProps>(
|
||||||
`/settings/ttl?duration=${props.totalDuration}&type=${props.type}${
|
`/settings/ttl?duration=${props.totalDuration}&type=${props.type}${
|
||||||
props.coldStorage
|
props.coldStorage
|
||||||
? `&coldStorage=${props.coldStorage}&toColdDuration=${props.toColdDuration}`
|
? `&coldStorage=${props.coldStorage}&toColdDuration=${props.toColdDuration}`
|
||||||
@@ -17,11 +17,13 @@ const setRetention = async (
|
|||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
httpStatusCode: response.status,
|
statusCode: 200,
|
||||||
data: response.data,
|
error: null,
|
||||||
|
message: 'Success',
|
||||||
|
payload: response.data,
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
|
return ErrorResponseHandler(error as AxiosError);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,32 +0,0 @@
|
|||||||
import { ApiV2Instance } from 'api';
|
|
||||||
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
|
|
||||||
import { AxiosError } from 'axios';
|
|
||||||
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
|
|
||||||
import { PayloadPropsV2, PropsV2 } from 'types/api/settings/setRetention';
|
|
||||||
|
|
||||||
const setRetentionV2 = async ({
|
|
||||||
type,
|
|
||||||
defaultTTLDays,
|
|
||||||
coldStorageVolume,
|
|
||||||
coldStorageDuration,
|
|
||||||
ttlConditions,
|
|
||||||
}: PropsV2): Promise<SuccessResponseV2<PayloadPropsV2>> => {
|
|
||||||
try {
|
|
||||||
const response = await ApiV2Instance.post<PayloadPropsV2>(`/settings/ttl`, {
|
|
||||||
type,
|
|
||||||
defaultTTLDays,
|
|
||||||
coldStorageVolume,
|
|
||||||
coldStorageDuration,
|
|
||||||
ttlConditions,
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
httpStatusCode: response.status,
|
|
||||||
data: response.data,
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export default setRetentionV2;
|
|
||||||
@@ -2,7 +2,7 @@ import axios from 'api';
|
|||||||
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
||||||
import { AxiosError } from 'axios';
|
import { AxiosError } from 'axios';
|
||||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||||
import { Props, Signup as PayloadProps } from 'types/api/user/loginPrecheck';
|
import { PayloadProps, Props } from 'types/api/user/loginPrecheck';
|
||||||
|
|
||||||
const loginPrecheck = async (
|
const loginPrecheck = async (
|
||||||
props: Props,
|
props: Props,
|
||||||
|
|||||||
@@ -1,21 +1,25 @@
|
|||||||
import axios from 'api';
|
import axios from 'api';
|
||||||
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
|
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
||||||
import { AxiosError } from 'axios';
|
import { AxiosError } from 'axios';
|
||||||
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
|
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||||
import { PayloadProps, Signup } from 'types/api/user/loginPrecheck';
|
import { PayloadProps } from 'types/api/user/loginPrecheck';
|
||||||
import { Props } from 'types/api/user/signup';
|
import { Props } from 'types/api/user/signup';
|
||||||
|
|
||||||
const signup = async (props: Props): Promise<SuccessResponseV2<Signup>> => {
|
const signup = async (
|
||||||
|
props: Props,
|
||||||
|
): Promise<SuccessResponse<null | PayloadProps> | ErrorResponse> => {
|
||||||
try {
|
try {
|
||||||
const response = await axios.post<PayloadProps>(`/register`, {
|
const response = await axios.post(`/register`, {
|
||||||
...props,
|
...props,
|
||||||
});
|
});
|
||||||
return {
|
return {
|
||||||
httpStatusCode: response.status,
|
statusCode: 200,
|
||||||
data: response.data.data,
|
error: null,
|
||||||
|
message: response.data.status,
|
||||||
|
payload: response.data?.data,
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
|
return ErrorResponseHandler(error as AxiosError);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,284 +0,0 @@
|
|||||||
/* eslint-disable sonarjs/no-duplicate-string */
|
|
||||||
import { SuccessResponse } from 'types/api';
|
|
||||||
import {
|
|
||||||
MetricRangePayloadV5,
|
|
||||||
QueryBuilderFormula,
|
|
||||||
QueryRangeRequestV5,
|
|
||||||
QueryRangeResponseV5,
|
|
||||||
RequestType,
|
|
||||||
ScalarData,
|
|
||||||
TelemetryFieldKey,
|
|
||||||
TimeSeries,
|
|
||||||
TimeSeriesData,
|
|
||||||
TimeSeriesValue,
|
|
||||||
} from 'types/api/v5/queryRange';
|
|
||||||
|
|
||||||
import { convertV5ResponseToLegacy } from './convertV5Response';
|
|
||||||
|
|
||||||
describe('convertV5ResponseToLegacy', () => {
|
|
||||||
function makeBaseSuccess<T>(
|
|
||||||
payload: T,
|
|
||||||
params: QueryRangeRequestV5,
|
|
||||||
): SuccessResponse<T, QueryRangeRequestV5> {
|
|
||||||
return {
|
|
||||||
statusCode: 200,
|
|
||||||
message: 'success',
|
|
||||||
payload,
|
|
||||||
error: null,
|
|
||||||
params,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function makeBaseParams(
|
|
||||||
requestType: RequestType,
|
|
||||||
queries: QueryRangeRequestV5['compositeQuery']['queries'],
|
|
||||||
): QueryRangeRequestV5 {
|
|
||||||
return {
|
|
||||||
schemaVersion: 'v1',
|
|
||||||
start: 1,
|
|
||||||
end: 2,
|
|
||||||
requestType,
|
|
||||||
compositeQuery: { queries },
|
|
||||||
variables: {},
|
|
||||||
formatOptions: { formatTableResultForUI: false, fillGaps: false },
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
it('converts time_series response into legacy series structure', () => {
|
|
||||||
const timeSeries: TimeSeriesData = {
|
|
||||||
queryName: 'A',
|
|
||||||
aggregations: [
|
|
||||||
{
|
|
||||||
index: 0,
|
|
||||||
alias: '__result_0',
|
|
||||||
meta: {},
|
|
||||||
series: [
|
|
||||||
({
|
|
||||||
labels: [
|
|
||||||
{
|
|
||||||
key: ({ name: 'service.name' } as unknown) as TelemetryFieldKey,
|
|
||||||
value: 'adservice',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
values: [
|
|
||||||
({ timestamp: 1000, value: 10 } as unknown) as TimeSeriesValue,
|
|
||||||
({ timestamp: 2000, value: 12 } as unknown) as TimeSeriesValue,
|
|
||||||
],
|
|
||||||
} as unknown) as TimeSeries,
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
const v5Data: QueryRangeResponseV5 = {
|
|
||||||
type: 'time_series',
|
|
||||||
data: { results: [timeSeries] },
|
|
||||||
meta: { rowsScanned: 0, bytesScanned: 0, durationMs: 0 },
|
|
||||||
};
|
|
||||||
|
|
||||||
const params = makeBaseParams('time_series', [
|
|
||||||
{
|
|
||||||
type: 'builder_query',
|
|
||||||
spec: {
|
|
||||||
name: 'A',
|
|
||||||
signal: 'traces',
|
|
||||||
stepInterval: 60,
|
|
||||||
disabled: false,
|
|
||||||
aggregations: [{ expression: 'count()' }],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
const input: SuccessResponse<
|
|
||||||
MetricRangePayloadV5,
|
|
||||||
QueryRangeRequestV5
|
|
||||||
> = makeBaseSuccess({ data: v5Data }, params);
|
|
||||||
|
|
||||||
const legendMap = { A: '{{service.name}}' };
|
|
||||||
const result = convertV5ResponseToLegacy(input, legendMap, false);
|
|
||||||
|
|
||||||
expect(result.payload.data.resultType).toBe('time_series');
|
|
||||||
expect(result.payload.data.result).toHaveLength(1);
|
|
||||||
const q = result.payload.data.result[0];
|
|
||||||
expect(q.queryName).toBe('A');
|
|
||||||
expect(q.legend).toBe('{{service.name}}');
|
|
||||||
expect(q.series?.[0]).toEqual(
|
|
||||||
expect.objectContaining({
|
|
||||||
labels: { 'service.name': 'adservice' },
|
|
||||||
values: [
|
|
||||||
{ timestamp: 1000, value: '10' },
|
|
||||||
{ timestamp: 2000, value: '12' },
|
|
||||||
],
|
|
||||||
metaData: expect.objectContaining({
|
|
||||||
alias: '__result_0',
|
|
||||||
index: 0,
|
|
||||||
queryName: 'A',
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('converts scalar to legacy table (formatForWeb=false) with names/ids resolved from aggregations', () => {
|
|
||||||
const scalar: ScalarData = {
|
|
||||||
columns: [
|
|
||||||
// group column
|
|
||||||
({
|
|
||||||
name: 'service.name',
|
|
||||||
queryName: 'A',
|
|
||||||
aggregationIndex: 0,
|
|
||||||
columnType: 'group',
|
|
||||||
} as unknown) as ScalarData['columns'][number],
|
|
||||||
// aggregation 0
|
|
||||||
({
|
|
||||||
name: '__result_0',
|
|
||||||
queryName: 'A',
|
|
||||||
aggregationIndex: 0,
|
|
||||||
columnType: 'aggregation',
|
|
||||||
} as unknown) as ScalarData['columns'][number],
|
|
||||||
// aggregation 1
|
|
||||||
({
|
|
||||||
name: '__result_1',
|
|
||||||
queryName: 'A',
|
|
||||||
aggregationIndex: 1,
|
|
||||||
columnType: 'aggregation',
|
|
||||||
} as unknown) as ScalarData['columns'][number],
|
|
||||||
// formula F1
|
|
||||||
({
|
|
||||||
name: '__result',
|
|
||||||
queryName: 'F1',
|
|
||||||
aggregationIndex: 0,
|
|
||||||
columnType: 'aggregation',
|
|
||||||
} as unknown) as ScalarData['columns'][number],
|
|
||||||
],
|
|
||||||
data: [['adservice', 606, 1.452, 151.5]],
|
|
||||||
};
|
|
||||||
|
|
||||||
const v5Data: QueryRangeResponseV5 = {
|
|
||||||
type: 'scalar',
|
|
||||||
data: { results: [scalar] },
|
|
||||||
meta: { rowsScanned: 0, bytesScanned: 0, durationMs: 0 },
|
|
||||||
};
|
|
||||||
|
|
||||||
const params = makeBaseParams('scalar', [
|
|
||||||
{
|
|
||||||
type: 'builder_query',
|
|
||||||
spec: {
|
|
||||||
name: 'A',
|
|
||||||
signal: 'traces',
|
|
||||||
stepInterval: 60,
|
|
||||||
disabled: false,
|
|
||||||
aggregations: [
|
|
||||||
{ expression: 'count()' },
|
|
||||||
{ expression: 'avg(app.ads.count)', alias: 'avg' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'builder_formula',
|
|
||||||
spec: ({
|
|
||||||
name: 'F1',
|
|
||||||
expression: 'A * 0.25',
|
|
||||||
} as unknown) as QueryBuilderFormula,
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
const input: SuccessResponse<
|
|
||||||
MetricRangePayloadV5,
|
|
||||||
QueryRangeRequestV5
|
|
||||||
> = makeBaseSuccess({ data: v5Data }, params);
|
|
||||||
const legendMap = { A: '{{service.name}}', F1: '' };
|
|
||||||
const result = convertV5ResponseToLegacy(input, legendMap, false);
|
|
||||||
|
|
||||||
expect(result.payload.data.resultType).toBe('scalar');
|
|
||||||
const [tableEntry] = result.payload.data.result;
|
|
||||||
expect(tableEntry.table?.columns).toEqual([
|
|
||||||
{
|
|
||||||
name: 'service.name',
|
|
||||||
queryName: 'A',
|
|
||||||
isValueColumn: false,
|
|
||||||
id: 'service.name',
|
|
||||||
},
|
|
||||||
{ name: 'count()', queryName: 'A', isValueColumn: true, id: 'A.count()' },
|
|
||||||
{
|
|
||||||
name: 'avg',
|
|
||||||
queryName: 'A',
|
|
||||||
isValueColumn: true,
|
|
||||||
id: 'A.avg(app.ads.count)',
|
|
||||||
},
|
|
||||||
{ name: 'F1', queryName: 'F1', isValueColumn: true, id: 'F1' },
|
|
||||||
]);
|
|
||||||
expect(tableEntry.table?.rows?.[0]).toEqual({
|
|
||||||
data: {
|
|
||||||
'service.name': 'adservice',
|
|
||||||
'A.count()': 606,
|
|
||||||
'A.avg(app.ads.count)': 1.452,
|
|
||||||
F1: 151.5,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('converts scalar with formatForWeb=true to UI-friendly table', () => {
|
|
||||||
const scalar: ScalarData = {
|
|
||||||
columns: [
|
|
||||||
{
|
|
||||||
name: 'service.name',
|
|
||||||
queryName: 'A',
|
|
||||||
aggregationIndex: 0,
|
|
||||||
columnType: 'group',
|
|
||||||
} as any,
|
|
||||||
{
|
|
||||||
name: '__result_0',
|
|
||||||
queryName: 'A',
|
|
||||||
aggregationIndex: 0,
|
|
||||||
columnType: 'aggregation',
|
|
||||||
} as any,
|
|
||||||
],
|
|
||||||
data: [['adservice', 580]],
|
|
||||||
};
|
|
||||||
|
|
||||||
const v5Data: QueryRangeResponseV5 = {
|
|
||||||
type: 'scalar',
|
|
||||||
data: { results: [scalar] },
|
|
||||||
meta: { rowsScanned: 0, bytesScanned: 0, durationMs: 0 },
|
|
||||||
};
|
|
||||||
|
|
||||||
const params = makeBaseParams('scalar', [
|
|
||||||
{
|
|
||||||
type: 'builder_query',
|
|
||||||
spec: {
|
|
||||||
name: 'A',
|
|
||||||
signal: 'traces',
|
|
||||||
stepInterval: 60,
|
|
||||||
disabled: false,
|
|
||||||
aggregations: [{ expression: 'count()' }],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
const input: SuccessResponse<
|
|
||||||
MetricRangePayloadV5,
|
|
||||||
QueryRangeRequestV5
|
|
||||||
> = makeBaseSuccess({ data: v5Data }, params);
|
|
||||||
const legendMap = { A: '{{service.name}}' };
|
|
||||||
const result = convertV5ResponseToLegacy(input, legendMap, true);
|
|
||||||
|
|
||||||
expect(result.payload.data.resultType).toBe('scalar');
|
|
||||||
const [tableEntry] = result.payload.data.result;
|
|
||||||
expect(tableEntry.table?.columns).toEqual([
|
|
||||||
{
|
|
||||||
name: 'service.name',
|
|
||||||
queryName: 'A',
|
|
||||||
isValueColumn: false,
|
|
||||||
id: 'service.name',
|
|
||||||
},
|
|
||||||
// Single aggregation: name resolves to legend, id resolves to queryName
|
|
||||||
{ name: '{{service.name}}', queryName: 'A', isValueColumn: true, id: 'A' },
|
|
||||||
]);
|
|
||||||
expect(tableEntry.table?.rows?.[0]).toEqual({
|
|
||||||
data: {
|
|
||||||
'service.name': 'adservice',
|
|
||||||
A: 580,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,637 +0,0 @@
|
|||||||
/* eslint-disable sonarjs/no-duplicate-string, simple-import-sort/imports, @typescript-eslint/indent, no-mixed-spaces-and-tabs */
|
|
||||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
|
||||||
import {
|
|
||||||
IBuilderFormula,
|
|
||||||
IBuilderQuery,
|
|
||||||
} from 'types/api/queryBuilder/queryBuilderData';
|
|
||||||
import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults';
|
|
||||||
import {
|
|
||||||
ClickHouseQuery,
|
|
||||||
LogAggregation,
|
|
||||||
LogBuilderQuery,
|
|
||||||
MetricBuilderQuery,
|
|
||||||
PromQuery,
|
|
||||||
QueryBuilderFormula as V5QueryBuilderFormula,
|
|
||||||
QueryEnvelope,
|
|
||||||
QueryRangePayloadV5,
|
|
||||||
} from 'types/api/v5/queryRange';
|
|
||||||
import { EQueryType } from 'types/common/dashboard';
|
|
||||||
import { DataSource } from 'types/common/queryBuilder';
|
|
||||||
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
|
||||||
|
|
||||||
import { prepareQueryRangePayloadV5 } from './prepareQueryRangePayloadV5';
|
|
||||||
|
|
||||||
jest.mock('lib/getStartEndRangeTime', () => ({
|
|
||||||
__esModule: true,
|
|
||||||
default: jest.fn(() => ({ start: '100', end: '200' })),
|
|
||||||
}));
|
|
||||||
|
|
||||||
describe('prepareQueryRangePayloadV5', () => {
|
|
||||||
const start = 1_710_000_000; // seconds
|
|
||||||
const end = 1_710_000_600; // seconds
|
|
||||||
|
|
||||||
const baseBuilderQuery = (
|
|
||||||
overrides?: Partial<IBuilderQuery>,
|
|
||||||
): IBuilderQuery => ({
|
|
||||||
queryName: 'A',
|
|
||||||
dataSource: DataSource.METRICS,
|
|
||||||
aggregations: [
|
|
||||||
{
|
|
||||||
metricName: 'cpu_usage',
|
|
||||||
temporality: '',
|
|
||||||
timeAggregation: 'sum',
|
|
||||||
spaceAggregation: 'avg',
|
|
||||||
reduceTo: 'avg',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
timeAggregation: 'sum',
|
|
||||||
spaceAggregation: 'avg',
|
|
||||||
temporality: '',
|
|
||||||
functions: [
|
|
||||||
{
|
|
||||||
name: 'timeShift',
|
|
||||||
args: [{ value: '5m' }],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
filter: { expression: '' },
|
|
||||||
filters: { items: [], op: 'AND' },
|
|
||||||
groupBy: [],
|
|
||||||
expression: 'A',
|
|
||||||
disabled: false,
|
|
||||||
having: [],
|
|
||||||
limit: null,
|
|
||||||
stepInterval: 600,
|
|
||||||
orderBy: [],
|
|
||||||
reduceTo: 'avg',
|
|
||||||
legend: 'Legend A',
|
|
||||||
...overrides,
|
|
||||||
});
|
|
||||||
|
|
||||||
const baseFormula = (
|
|
||||||
overrides?: Partial<IBuilderFormula>,
|
|
||||||
): IBuilderFormula => ({
|
|
||||||
expression: 'A + 1',
|
|
||||||
disabled: false,
|
|
||||||
queryName: 'F1',
|
|
||||||
legend: 'Formula Legend',
|
|
||||||
limit: undefined,
|
|
||||||
having: [],
|
|
||||||
stepInterval: undefined,
|
|
||||||
orderBy: [],
|
|
||||||
...overrides,
|
|
||||||
});
|
|
||||||
|
|
||||||
it('builds payload for builder queries with formulas and variables', () => {
|
|
||||||
const props: GetQueryResultsProps = {
|
|
||||||
query: {
|
|
||||||
queryType: EQueryType.QUERY_BUILDER,
|
|
||||||
id: 'q1',
|
|
||||||
unit: undefined,
|
|
||||||
promql: [],
|
|
||||||
clickhouse_sql: [],
|
|
||||||
builder: {
|
|
||||||
queryData: [baseBuilderQuery()],
|
|
||||||
queryFormulas: [baseFormula()],
|
|
||||||
queryTraceOperator: [],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
graphType: PANEL_TYPES.TIME_SERIES,
|
|
||||||
selectedTime: 'GLOBAL_TIME',
|
|
||||||
start,
|
|
||||||
end,
|
|
||||||
variables: { svc: 'api', count: 5, flag: true },
|
|
||||||
fillGaps: true,
|
|
||||||
};
|
|
||||||
|
|
||||||
const result = prepareQueryRangePayloadV5(props);
|
|
||||||
|
|
||||||
expect(result).toEqual(
|
|
||||||
expect.objectContaining({
|
|
||||||
legendMap: { A: 'Legend A', F1: 'Formula Legend' },
|
|
||||||
queryPayload: expect.objectContaining({
|
|
||||||
compositeQuery: expect.objectContaining({
|
|
||||||
queries: expect.arrayContaining([
|
|
||||||
expect.objectContaining({
|
|
||||||
type: 'builder_query',
|
|
||||||
spec: expect.objectContaining({
|
|
||||||
name: 'A',
|
|
||||||
signal: 'metrics',
|
|
||||||
stepInterval: 600,
|
|
||||||
functions: [{ name: 'timeShift', args: [{ value: '5m' }] }],
|
|
||||||
aggregations: [
|
|
||||||
expect.objectContaining({
|
|
||||||
metricName: 'cpu_usage',
|
|
||||||
timeAggregation: 'sum',
|
|
||||||
spaceAggregation: 'avg',
|
|
||||||
reduceTo: undefined,
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
expect.objectContaining({
|
|
||||||
type: 'builder_formula',
|
|
||||||
spec: expect.objectContaining({
|
|
||||||
name: 'F1',
|
|
||||||
expression: 'A + 1',
|
|
||||||
legend: 'Formula Legend',
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
]),
|
|
||||||
}),
|
|
||||||
requestType: 'time_series',
|
|
||||||
formatOptions: expect.objectContaining({
|
|
||||||
formatTableResultForUI: false,
|
|
||||||
fillGaps: true,
|
|
||||||
}),
|
|
||||||
start: start * 1000,
|
|
||||||
end: end * 1000,
|
|
||||||
variables: expect.objectContaining({
|
|
||||||
svc: { value: 'api' },
|
|
||||||
count: { value: 5 },
|
|
||||||
flag: { value: true },
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Legend map combines builder and formulas
|
|
||||||
expect(result.legendMap).toEqual({ A: 'Legend A', F1: 'Formula Legend' });
|
|
||||||
|
|
||||||
const payload: QueryRangePayloadV5 = result.queryPayload;
|
|
||||||
|
|
||||||
expect(payload.schemaVersion).toBe('v1');
|
|
||||||
expect(payload.start).toBe(start * 1000);
|
|
||||||
expect(payload.end).toBe(end * 1000);
|
|
||||||
expect(payload.requestType).toBe('time_series');
|
|
||||||
expect(payload.formatOptions?.formatTableResultForUI).toBe(false);
|
|
||||||
expect(payload.formatOptions?.fillGaps).toBe(true);
|
|
||||||
|
|
||||||
// Variables mapped as { key: { value } }
|
|
||||||
expect(payload.variables).toEqual({
|
|
||||||
svc: { value: 'api' },
|
|
||||||
count: { value: 5 },
|
|
||||||
flag: { value: true },
|
|
||||||
});
|
|
||||||
|
|
||||||
// Queries include one builder_query and one builder_formula
|
|
||||||
expect(payload.compositeQuery.queries).toHaveLength(2);
|
|
||||||
|
|
||||||
const builderQuery = payload.compositeQuery.queries.find(
|
|
||||||
(q) => q.type === 'builder_query',
|
|
||||||
) as QueryEnvelope;
|
|
||||||
const builderSpec = builderQuery.spec as MetricBuilderQuery;
|
|
||||||
expect(builderSpec.name).toBe('A');
|
|
||||||
expect(builderSpec.signal).toBe('metrics');
|
|
||||||
expect(builderSpec.aggregations?.[0]).toMatchObject({
|
|
||||||
metricName: 'cpu_usage',
|
|
||||||
timeAggregation: 'sum',
|
|
||||||
spaceAggregation: 'avg',
|
|
||||||
});
|
|
||||||
// reduceTo should not be present for non-scalar panels
|
|
||||||
expect(builderSpec.aggregations?.[0].reduceTo).toBeUndefined();
|
|
||||||
// functions should be preserved/normalized
|
|
||||||
expect(builderSpec.functions?.[0]?.name).toBe('timeShift');
|
|
||||||
|
|
||||||
const formulaQuery = payload.compositeQuery.queries.find(
|
|
||||||
(q) => q.type === 'builder_formula',
|
|
||||||
) as QueryEnvelope;
|
|
||||||
const formulaSpec = formulaQuery.spec as V5QueryBuilderFormula;
|
|
||||||
expect(formulaSpec.name).toBe('F1');
|
|
||||||
expect(formulaSpec.expression).toBe('A + 1');
|
|
||||||
expect(formulaSpec.legend).toBe('Formula Legend');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('builds payload for PromQL queries and respects originalGraphType for formatting', () => {
|
|
||||||
const props: GetQueryResultsProps = {
|
|
||||||
query: {
|
|
||||||
queryType: EQueryType.PROM,
|
|
||||||
id: 'q2',
|
|
||||||
unit: undefined,
|
|
||||||
promql: [
|
|
||||||
{
|
|
||||||
name: 'A',
|
|
||||||
query: 'up',
|
|
||||||
disabled: false,
|
|
||||||
legend: 'LP',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
clickhouse_sql: [],
|
|
||||||
builder: { queryData: [], queryFormulas: [], queryTraceOperator: [] },
|
|
||||||
},
|
|
||||||
graphType: PANEL_TYPES.TIME_SERIES,
|
|
||||||
originalGraphType: PANEL_TYPES.TABLE,
|
|
||||||
selectedTime: 'GLOBAL_TIME',
|
|
||||||
start,
|
|
||||||
end,
|
|
||||||
};
|
|
||||||
|
|
||||||
const result = prepareQueryRangePayloadV5(props);
|
|
||||||
|
|
||||||
expect(result).toEqual(
|
|
||||||
expect.objectContaining({
|
|
||||||
legendMap: { A: 'LP' },
|
|
||||||
queryPayload: expect.objectContaining({
|
|
||||||
compositeQuery: expect.objectContaining({
|
|
||||||
queries: [
|
|
||||||
{
|
|
||||||
type: 'promql',
|
|
||||||
spec: expect.objectContaining({
|
|
||||||
name: 'A',
|
|
||||||
query: 'up',
|
|
||||||
legend: 'LP',
|
|
||||||
stats: false,
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
requestType: 'time_series',
|
|
||||||
formatOptions: expect.objectContaining({
|
|
||||||
formatTableResultForUI: true,
|
|
||||||
fillGaps: false,
|
|
||||||
}),
|
|
||||||
start: start * 1000,
|
|
||||||
end: end * 1000,
|
|
||||||
variables: {},
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(result.legendMap).toEqual({ A: 'LP' });
|
|
||||||
|
|
||||||
const payload: QueryRangePayloadV5 = result.queryPayload;
|
|
||||||
expect(payload.requestType).toBe('time_series');
|
|
||||||
expect(payload.formatOptions?.formatTableResultForUI).toBe(true);
|
|
||||||
expect(payload.compositeQuery.queries).toHaveLength(1);
|
|
||||||
|
|
||||||
const prom = payload.compositeQuery.queries[0];
|
|
||||||
expect(prom.type).toBe('promql');
|
|
||||||
const promSpec = prom.spec as PromQuery;
|
|
||||||
expect(promSpec.name).toBe('A');
|
|
||||||
expect(promSpec.query).toBe('up');
|
|
||||||
expect(promSpec.legend).toBe('LP');
|
|
||||||
expect(promSpec.stats).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('builds payload for ClickHouse queries and maps requestType from panel', () => {
|
|
||||||
const props: GetQueryResultsProps = {
|
|
||||||
query: {
|
|
||||||
queryType: EQueryType.CLICKHOUSE,
|
|
||||||
id: 'q3',
|
|
||||||
unit: undefined,
|
|
||||||
promql: [],
|
|
||||||
clickhouse_sql: [
|
|
||||||
{
|
|
||||||
name: 'Q',
|
|
||||||
query: 'SELECT 1',
|
|
||||||
disabled: false,
|
|
||||||
legend: 'LC',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
builder: { queryData: [], queryFormulas: [], queryTraceOperator: [] },
|
|
||||||
},
|
|
||||||
graphType: PANEL_TYPES.TABLE,
|
|
||||||
selectedTime: 'GLOBAL_TIME',
|
|
||||||
start,
|
|
||||||
end,
|
|
||||||
};
|
|
||||||
|
|
||||||
const result = prepareQueryRangePayloadV5(props);
|
|
||||||
|
|
||||||
expect(result).toEqual(
|
|
||||||
expect.objectContaining({
|
|
||||||
legendMap: { Q: 'LC' },
|
|
||||||
queryPayload: expect.objectContaining({
|
|
||||||
compositeQuery: expect.objectContaining({
|
|
||||||
queries: [
|
|
||||||
{
|
|
||||||
type: 'clickhouse_sql',
|
|
||||||
spec: expect.objectContaining({
|
|
||||||
name: 'Q',
|
|
||||||
query: 'SELECT 1',
|
|
||||||
legend: 'LC',
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
requestType: 'scalar',
|
|
||||||
formatOptions: expect.objectContaining({
|
|
||||||
formatTableResultForUI: true,
|
|
||||||
fillGaps: false,
|
|
||||||
}),
|
|
||||||
start: start * 1000,
|
|
||||||
end: end * 1000,
|
|
||||||
variables: {},
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(result.legendMap).toEqual({ Q: 'LC' });
|
|
||||||
|
|
||||||
const payload: QueryRangePayloadV5 = result.queryPayload;
|
|
||||||
expect(payload.requestType).toBe('scalar');
|
|
||||||
expect(payload.compositeQuery.queries).toHaveLength(1);
|
|
||||||
const ch = payload.compositeQuery.queries[0];
|
|
||||||
expect(ch.type).toBe('clickhouse_sql');
|
|
||||||
const chSpec = ch.spec as ClickHouseQuery;
|
|
||||||
expect(chSpec.name).toBe('Q');
|
|
||||||
expect(chSpec.query).toBe('SELECT 1');
|
|
||||||
expect(chSpec.legend).toBe('LC');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('uses getStartEndRangeTime when start/end are not provided', () => {
|
|
||||||
const props: GetQueryResultsProps = {
|
|
||||||
query: {
|
|
||||||
queryType: EQueryType.QUERY_BUILDER,
|
|
||||||
id: 'q4',
|
|
||||||
unit: undefined,
|
|
||||||
promql: [],
|
|
||||||
clickhouse_sql: [],
|
|
||||||
builder: { queryData: [], queryFormulas: [], queryTraceOperator: [] },
|
|
||||||
},
|
|
||||||
graphType: PANEL_TYPES.TIME_SERIES,
|
|
||||||
selectedTime: 'GLOBAL_TIME',
|
|
||||||
};
|
|
||||||
|
|
||||||
const result = prepareQueryRangePayloadV5(props);
|
|
||||||
|
|
||||||
expect(result).toEqual(
|
|
||||||
expect.objectContaining({
|
|
||||||
legendMap: {},
|
|
||||||
queryPayload: expect.objectContaining({
|
|
||||||
compositeQuery: { queries: [] },
|
|
||||||
requestType: 'time_series',
|
|
||||||
formatOptions: expect.objectContaining({
|
|
||||||
formatTableResultForUI: false,
|
|
||||||
fillGaps: false,
|
|
||||||
}),
|
|
||||||
start: 100 * 1000,
|
|
||||||
end: 200 * 1000,
|
|
||||||
variables: {},
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
const payload: QueryRangePayloadV5 = result.queryPayload;
|
|
||||||
expect(payload.start).toBe(100 * 1000);
|
|
||||||
expect(payload.end).toBe(200 * 1000);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('includes reduceTo for metrics in scalar panels (TABLE)', () => {
|
|
||||||
const props: GetQueryResultsProps = {
|
|
||||||
query: {
|
|
||||||
queryType: EQueryType.QUERY_BUILDER,
|
|
||||||
id: 'q5',
|
|
||||||
unit: undefined,
|
|
||||||
promql: [],
|
|
||||||
clickhouse_sql: [],
|
|
||||||
builder: {
|
|
||||||
queryData: [baseBuilderQuery()],
|
|
||||||
queryFormulas: [],
|
|
||||||
queryTraceOperator: [],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
graphType: PANEL_TYPES.TABLE,
|
|
||||||
selectedTime: 'GLOBAL_TIME',
|
|
||||||
start,
|
|
||||||
end,
|
|
||||||
};
|
|
||||||
|
|
||||||
const result = prepareQueryRangePayloadV5(props);
|
|
||||||
|
|
||||||
expect(result).toEqual(
|
|
||||||
expect.objectContaining({
|
|
||||||
legendMap: { A: 'Legend A' },
|
|
||||||
queryPayload: expect.objectContaining({
|
|
||||||
compositeQuery: expect.objectContaining({
|
|
||||||
queries: [
|
|
||||||
{
|
|
||||||
type: 'builder_query',
|
|
||||||
spec: expect.objectContaining({
|
|
||||||
name: 'A',
|
|
||||||
signal: 'metrics',
|
|
||||||
stepInterval: 600,
|
|
||||||
functions: [{ name: 'timeShift', args: [{ value: '5m' }] }],
|
|
||||||
aggregations: [
|
|
||||||
expect.objectContaining({
|
|
||||||
metricName: 'cpu_usage',
|
|
||||||
timeAggregation: 'sum',
|
|
||||||
spaceAggregation: 'avg',
|
|
||||||
reduceTo: 'avg',
|
|
||||||
temporality: undefined,
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
requestType: 'scalar',
|
|
||||||
formatOptions: expect.objectContaining({
|
|
||||||
formatTableResultForUI: true,
|
|
||||||
fillGaps: false,
|
|
||||||
}),
|
|
||||||
start: start * 1000,
|
|
||||||
end: end * 1000,
|
|
||||||
variables: {},
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
const payload: QueryRangePayloadV5 = result.queryPayload;
|
|
||||||
const builderQuery = payload.compositeQuery.queries.find(
|
|
||||||
(q) => q.type === 'builder_query',
|
|
||||||
) as QueryEnvelope;
|
|
||||||
const builderSpec = builderQuery.spec as MetricBuilderQuery;
|
|
||||||
expect(builderSpec.aggregations?.[0].reduceTo).toBe('avg');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('omits aggregations for raw request type (LIST panel)', () => {
|
|
||||||
const logAgg: LogAggregation[] = [{ expression: 'count()' }];
|
|
||||||
const logsQuery = baseBuilderQuery({
|
|
||||||
dataSource: DataSource.LOGS,
|
|
||||||
aggregations: logAgg,
|
|
||||||
} as Partial<IBuilderQuery>);
|
|
||||||
|
|
||||||
const props: GetQueryResultsProps = {
|
|
||||||
query: {
|
|
||||||
queryType: EQueryType.QUERY_BUILDER,
|
|
||||||
id: 'q6',
|
|
||||||
unit: undefined,
|
|
||||||
promql: [],
|
|
||||||
clickhouse_sql: [],
|
|
||||||
builder: {
|
|
||||||
queryData: [logsQuery],
|
|
||||||
queryFormulas: [],
|
|
||||||
queryTraceOperator: [],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
graphType: PANEL_TYPES.LIST,
|
|
||||||
selectedTime: 'GLOBAL_TIME',
|
|
||||||
start,
|
|
||||||
end,
|
|
||||||
};
|
|
||||||
|
|
||||||
const result = prepareQueryRangePayloadV5(props);
|
|
||||||
|
|
||||||
expect(result).toEqual(
|
|
||||||
expect.objectContaining({
|
|
||||||
legendMap: { A: 'Legend A' },
|
|
||||||
queryPayload: expect.objectContaining({
|
|
||||||
compositeQuery: expect.objectContaining({
|
|
||||||
queries: [
|
|
||||||
{
|
|
||||||
type: 'builder_query',
|
|
||||||
spec: expect.objectContaining({
|
|
||||||
name: 'A',
|
|
||||||
signal: 'logs',
|
|
||||||
stepInterval: 600,
|
|
||||||
functions: [{ name: 'timeShift', args: [{ value: '5m' }] }],
|
|
||||||
aggregations: undefined,
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
requestType: 'raw',
|
|
||||||
formatOptions: expect.objectContaining({
|
|
||||||
formatTableResultForUI: false,
|
|
||||||
fillGaps: false,
|
|
||||||
}),
|
|
||||||
start: start * 1000,
|
|
||||||
end: end * 1000,
|
|
||||||
variables: {},
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
const payload: QueryRangePayloadV5 = result.queryPayload;
|
|
||||||
expect(payload.requestType).toBe('raw');
|
|
||||||
const builderQuery = payload.compositeQuery.queries.find(
|
|
||||||
(q) => q.type === 'builder_query',
|
|
||||||
) as QueryEnvelope;
|
|
||||||
// For RAW request type, aggregations should be omitted
|
|
||||||
const logSpec = builderQuery.spec as LogBuilderQuery;
|
|
||||||
expect(logSpec.aggregations).toBeUndefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('maps groupBy, order, having, aggregations and filter for logs builder query', () => {
|
|
||||||
const getStartEndRangeTime = jest.requireMock('lib/getStartEndRangeTime')
|
|
||||||
.default as jest.Mock;
|
|
||||||
getStartEndRangeTime.mockReturnValueOnce({
|
|
||||||
start: '1754623641',
|
|
||||||
end: '1754645241',
|
|
||||||
});
|
|
||||||
|
|
||||||
const props: GetQueryResultsProps = {
|
|
||||||
query: {
|
|
||||||
queryType: EQueryType.QUERY_BUILDER,
|
|
||||||
id: 'e643e387-1996-4449-97b6-9ef4498a0573',
|
|
||||||
unit: undefined,
|
|
||||||
promql: [{ name: 'A', query: '', legend: '', disabled: false }],
|
|
||||||
clickhouse_sql: [{ name: 'A', legend: '', disabled: false, query: '' }],
|
|
||||||
builder: {
|
|
||||||
queryData: [
|
|
||||||
{
|
|
||||||
dataSource: DataSource.LOGS,
|
|
||||||
queryName: 'A',
|
|
||||||
aggregateOperator: 'count',
|
|
||||||
aggregateAttribute: {
|
|
||||||
key: '',
|
|
||||||
dataType: DataTypes.EMPTY,
|
|
||||||
type: '',
|
|
||||||
},
|
|
||||||
timeAggregation: 'rate',
|
|
||||||
spaceAggregation: 'sum',
|
|
||||||
filter: { expression: "service.name = 'adservice'" },
|
|
||||||
aggregations: [
|
|
||||||
{ expression: 'count() as cnt avg(code.lineno) ' } as LogAggregation,
|
|
||||||
],
|
|
||||||
functions: [],
|
|
||||||
filters: {
|
|
||||||
items: [
|
|
||||||
{
|
|
||||||
id: '14c790ec-54d1-42f0-a889-3b4f0fb79852',
|
|
||||||
op: '=',
|
|
||||||
key: { id: 'service.name', key: 'service.name', type: '' },
|
|
||||||
value: 'adservice',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
op: 'AND',
|
|
||||||
},
|
|
||||||
expression: 'A',
|
|
||||||
disabled: false,
|
|
||||||
stepInterval: 80,
|
|
||||||
having: { expression: 'count() > 0' },
|
|
||||||
limit: 600,
|
|
||||||
orderBy: [{ columnName: 'service.name', order: 'desc' }],
|
|
||||||
groupBy: [
|
|
||||||
{
|
|
||||||
key: 'service.name',
|
|
||||||
type: '',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
legend: '{{service.name}}',
|
|
||||||
reduceTo: 'avg',
|
|
||||||
offset: 0,
|
|
||||||
pageSize: 100,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
queryFormulas: [],
|
|
||||||
queryTraceOperator: [],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
graphType: PANEL_TYPES.TIME_SERIES,
|
|
||||||
selectedTime: 'GLOBAL_TIME',
|
|
||||||
globalSelectedInterval: 'custom' as never,
|
|
||||||
variables: {},
|
|
||||||
};
|
|
||||||
|
|
||||||
const result = prepareQueryRangePayloadV5(props);
|
|
||||||
|
|
||||||
expect(result).toEqual(
|
|
||||||
expect.objectContaining({
|
|
||||||
legendMap: { A: '{{service.name}}' },
|
|
||||||
queryPayload: expect.objectContaining({
|
|
||||||
schemaVersion: 'v1',
|
|
||||||
start: 1754623641000,
|
|
||||||
end: 1754645241000,
|
|
||||||
requestType: 'time_series',
|
|
||||||
compositeQuery: expect.objectContaining({
|
|
||||||
queries: [
|
|
||||||
{
|
|
||||||
type: 'builder_query',
|
|
||||||
spec: expect.objectContaining({
|
|
||||||
name: 'A',
|
|
||||||
signal: 'logs',
|
|
||||||
stepInterval: 80,
|
|
||||||
disabled: false,
|
|
||||||
filter: { expression: "service.name = 'adservice'" },
|
|
||||||
groupBy: [
|
|
||||||
{
|
|
||||||
name: 'service.name',
|
|
||||||
fieldDataType: '',
|
|
||||||
fieldContext: '',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
limit: 600,
|
|
||||||
order: [
|
|
||||||
{
|
|
||||||
key: { name: 'service.name' },
|
|
||||||
direction: 'desc',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
legend: '{{service.name}}',
|
|
||||||
having: { expression: 'count() > 0' },
|
|
||||||
aggregations: [
|
|
||||||
{ expression: 'count()', alias: 'cnt' },
|
|
||||||
{ expression: 'avg(code.lineno)' },
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
formatOptions: { formatTableResultForUI: false, fillGaps: false },
|
|
||||||
variables: {},
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,15 +1,11 @@
|
|||||||
/* eslint-disable sonarjs/cognitive-complexity */
|
/* eslint-disable sonarjs/cognitive-complexity */
|
||||||
/* eslint-disable sonarjs/no-identical-functions */
|
|
||||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||||
import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults';
|
import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults';
|
||||||
import getStartEndRangeTime from 'lib/getStartEndRangeTime';
|
import getStartEndRangeTime from 'lib/getStartEndRangeTime';
|
||||||
import { mapQueryDataToApi } from 'lib/newQueryBuilder/queryBuilderMappers/mapQueryDataToApi';
|
import { mapQueryDataToApi } from 'lib/newQueryBuilder/queryBuilderMappers/mapQueryDataToApi';
|
||||||
import { isEmpty } from 'lodash-es';
|
import { isEmpty } from 'lodash-es';
|
||||||
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||||
import {
|
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
||||||
IBuilderQuery,
|
|
||||||
IBuilderTraceOperator,
|
|
||||||
} from 'types/api/queryBuilder/queryBuilderData';
|
|
||||||
import {
|
import {
|
||||||
BaseBuilderQuery,
|
BaseBuilderQuery,
|
||||||
FieldContext,
|
FieldContext,
|
||||||
@@ -28,7 +24,6 @@ import {
|
|||||||
TelemetryFieldKey,
|
TelemetryFieldKey,
|
||||||
TraceAggregation,
|
TraceAggregation,
|
||||||
VariableItem,
|
VariableItem,
|
||||||
VariableType,
|
|
||||||
} from 'types/api/v5/queryRange';
|
} from 'types/api/v5/queryRange';
|
||||||
import { EQueryType } from 'types/common/dashboard';
|
import { EQueryType } from 'types/common/dashboard';
|
||||||
import { DataSource } from 'types/common/queryBuilder';
|
import { DataSource } from 'types/common/queryBuilder';
|
||||||
@@ -71,46 +66,9 @@ function getSignalType(dataSource: string): 'traces' | 'logs' | 'metrics' {
|
|||||||
return 'metrics';
|
return 'metrics';
|
||||||
}
|
}
|
||||||
|
|
||||||
function isDeprecatedField(fieldName: string): boolean {
|
/**
|
||||||
const deprecatedIntrinsicFields = [
|
* Creates base spec for builder queries
|
||||||
'traceID',
|
*/
|
||||||
'spanID',
|
|
||||||
'parentSpanID',
|
|
||||||
'spanKind',
|
|
||||||
'durationNano',
|
|
||||||
'statusCode',
|
|
||||||
'statusMessage',
|
|
||||||
'statusCodeString',
|
|
||||||
];
|
|
||||||
|
|
||||||
const deprecatedCalculatedFields = [
|
|
||||||
'responseStatusCode',
|
|
||||||
'externalHttpUrl',
|
|
||||||
'httpUrl',
|
|
||||||
'externalHttpMethod',
|
|
||||||
'httpMethod',
|
|
||||||
'httpHost',
|
|
||||||
'dbName',
|
|
||||||
'dbOperation',
|
|
||||||
'hasError',
|
|
||||||
'isRemote',
|
|
||||||
'serviceName',
|
|
||||||
'httpRoute',
|
|
||||||
'msgSystem',
|
|
||||||
'msgOperation',
|
|
||||||
'dbSystem',
|
|
||||||
'rpcSystem',
|
|
||||||
'rpcService',
|
|
||||||
'rpcMethod',
|
|
||||||
'peerService',
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
|
||||||
deprecatedIntrinsicFields.includes(fieldName) ||
|
|
||||||
deprecatedCalculatedFields.includes(fieldName)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function createBaseSpec(
|
function createBaseSpec(
|
||||||
queryData: IBuilderQuery,
|
queryData: IBuilderQuery,
|
||||||
requestType: RequestType,
|
requestType: RequestType,
|
||||||
@@ -122,7 +80,7 @@ function createBaseSpec(
|
|||||||
)[])?.filter((c) => ('key' in c ? c?.key : c?.name));
|
)[])?.filter((c) => ('key' in c ? c?.key : c?.name));
|
||||||
|
|
||||||
return {
|
return {
|
||||||
stepInterval: queryData?.stepInterval || null,
|
stepInterval: queryData?.stepInterval || undefined,
|
||||||
disabled: queryData.disabled,
|
disabled: queryData.disabled,
|
||||||
filter: queryData?.filter?.expression ? queryData.filter : undefined,
|
filter: queryData?.filter?.expression ? queryData.filter : undefined,
|
||||||
groupBy:
|
groupBy:
|
||||||
@@ -130,8 +88,8 @@ function createBaseSpec(
|
|||||||
? queryData.groupBy.map(
|
? queryData.groupBy.map(
|
||||||
(item: any): GroupByKey => ({
|
(item: any): GroupByKey => ({
|
||||||
name: item.key,
|
name: item.key,
|
||||||
fieldDataType: item?.dataType || '',
|
fieldDataType: item?.dataType,
|
||||||
fieldContext: item?.type || '',
|
fieldContext: item?.type,
|
||||||
description: item?.description,
|
description: item?.description,
|
||||||
unit: item?.unit,
|
unit: item?.unit,
|
||||||
signal: item?.signal,
|
signal: item?.signal,
|
||||||
@@ -182,33 +140,19 @@ function createBaseSpec(
|
|||||||
selectFields: isEmpty(nonEmptySelectColumns)
|
selectFields: isEmpty(nonEmptySelectColumns)
|
||||||
? undefined
|
? undefined
|
||||||
: nonEmptySelectColumns?.map(
|
: nonEmptySelectColumns?.map(
|
||||||
(column: any): TelemetryFieldKey => {
|
(column: any): TelemetryFieldKey => ({
|
||||||
const fieldName = column.name ?? column.key;
|
name: column.name ?? column.key,
|
||||||
const isDeprecated = isDeprecatedField(fieldName);
|
fieldDataType:
|
||||||
|
column?.fieldDataType ?? (column?.dataType as FieldDataType),
|
||||||
const fieldObj: TelemetryFieldKey = {
|
fieldContext: column?.fieldContext ?? (column?.type as FieldContext),
|
||||||
name: fieldName,
|
signal: column?.signal ?? undefined,
|
||||||
fieldDataType:
|
}),
|
||||||
column?.fieldDataType ?? (column?.dataType as FieldDataType),
|
|
||||||
signal: column?.signal ?? undefined,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Only add fieldContext if the field is NOT deprecated
|
|
||||||
if (!isDeprecated && fieldName !== 'name') {
|
|
||||||
fieldObj.fieldContext =
|
|
||||||
column?.fieldContext ?? (column?.type as FieldContext);
|
|
||||||
}
|
|
||||||
|
|
||||||
return fieldObj;
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Utility to parse aggregation expressions with optional alias
|
// Utility to parse aggregation expressions with optional alias
|
||||||
export function parseAggregations(
|
export function parseAggregations(
|
||||||
expression: string,
|
expression: string,
|
||||||
availableAlias?: string,
|
|
||||||
): { expression: string; alias?: string }[] {
|
): { expression: string; alias?: string }[] {
|
||||||
const result: { expression: string; alias?: string }[] = [];
|
const result: { expression: string; alias?: string }[] = [];
|
||||||
// Matches function calls like "count()" or "sum(field)" with optional alias like "as 'alias'"
|
// Matches function calls like "count()" or "sum(field)" with optional alias like "as 'alias'"
|
||||||
@@ -217,7 +161,7 @@ export function parseAggregations(
|
|||||||
let match = regex.exec(expression);
|
let match = regex.exec(expression);
|
||||||
while (match !== null) {
|
while (match !== null) {
|
||||||
const expr = match[1];
|
const expr = match[1];
|
||||||
let alias = match[2] || availableAlias; // Use provided alias or availableAlias if not matched
|
let alias = match[2];
|
||||||
if (alias) {
|
if (alias) {
|
||||||
// Remove quotes if present
|
// Remove quotes if present
|
||||||
alias = alias.replace(/^['"]|['"]$/g, '');
|
alias = alias.replace(/^['"]|['"]$/g, '');
|
||||||
@@ -268,14 +212,9 @@ export function createAggregation(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (queryData.aggregations?.length > 0) {
|
if (queryData.aggregations?.length > 0) {
|
||||||
return queryData.aggregations.flatMap(
|
return isEmpty(parseAggregations(queryData.aggregations?.[0].expression))
|
||||||
(agg: { expression: string; alias?: string }) => {
|
? [{ expression: 'count()' }]
|
||||||
const parsedAggregations = parseAggregations(agg.expression, agg?.alias);
|
: parseAggregations(queryData.aggregations?.[0].expression);
|
||||||
return isEmpty(parsedAggregations)
|
|
||||||
? [{ expression: 'count()' }]
|
|
||||||
: parsedAggregations;
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return [{ expression: 'count()' }];
|
return [{ expression: 'count()' }];
|
||||||
@@ -337,109 +276,6 @@ export function convertBuilderQueriesToV5(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function createTraceOperatorBaseSpec(
|
|
||||||
queryData: IBuilderTraceOperator,
|
|
||||||
requestType: RequestType,
|
|
||||||
panelType?: PANEL_TYPES,
|
|
||||||
): BaseBuilderQuery {
|
|
||||||
const nonEmptySelectColumns = (queryData.selectColumns as (
|
|
||||||
| BaseAutocompleteData
|
|
||||||
| TelemetryFieldKey
|
|
||||||
)[])?.filter((c) => ('key' in c ? c?.key : c?.name));
|
|
||||||
|
|
||||||
const {
|
|
||||||
stepInterval,
|
|
||||||
groupBy,
|
|
||||||
limit,
|
|
||||||
offset,
|
|
||||||
legend,
|
|
||||||
having,
|
|
||||||
orderBy,
|
|
||||||
pageSize,
|
|
||||||
} = queryData;
|
|
||||||
|
|
||||||
return {
|
|
||||||
stepInterval: stepInterval || undefined,
|
|
||||||
groupBy:
|
|
||||||
groupBy?.length > 0
|
|
||||||
? 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
|
|
||||||
? limit || pageSize || undefined
|
|
||||||
: limit || undefined,
|
|
||||||
offset: requestType === 'raw' || requestType === 'trace' ? offset : undefined,
|
|
||||||
order:
|
|
||||||
orderBy?.length > 0
|
|
||||||
? orderBy.map(
|
|
||||||
(order: any): OrderBy => ({
|
|
||||||
key: {
|
|
||||||
name: order.columnName,
|
|
||||||
},
|
|
||||||
direction: order.order,
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
: undefined,
|
|
||||||
legend: isEmpty(legend) ? undefined : legend,
|
|
||||||
having: isEmpty(having) ? undefined : (having as Having),
|
|
||||||
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,
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function convertTraceOperatorToV5(
|
|
||||||
traceOperator: Record<string, IBuilderTraceOperator>,
|
|
||||||
requestType: RequestType,
|
|
||||||
panelType?: PANEL_TYPES,
|
|
||||||
): QueryEnvelope[] {
|
|
||||||
return Object.entries(traceOperator).map(
|
|
||||||
([queryName, traceOperatorData]): QueryEnvelope => {
|
|
||||||
const baseSpec = createTraceOperatorBaseSpec(
|
|
||||||
traceOperatorData,
|
|
||||||
requestType,
|
|
||||||
panelType,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Skip aggregation for raw request type
|
|
||||||
const aggregations =
|
|
||||||
requestType === 'raw'
|
|
||||||
? undefined
|
|
||||||
: createAggregation(traceOperatorData, panelType);
|
|
||||||
|
|
||||||
const spec: QueryEnvelope['spec'] = {
|
|
||||||
name: queryName,
|
|
||||||
...baseSpec,
|
|
||||||
expression: traceOperatorData.expression || '',
|
|
||||||
aggregations: aggregations as TraceAggregation[],
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
type: 'builder_trace_operator' as QueryType,
|
|
||||||
spec,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Converts PromQL queries to V5 format
|
* Converts PromQL queries to V5 format
|
||||||
*/
|
*/
|
||||||
@@ -514,7 +350,6 @@ export const prepareQueryRangePayloadV5 = ({
|
|||||||
formatForWeb,
|
formatForWeb,
|
||||||
originalGraphType,
|
originalGraphType,
|
||||||
fillGaps,
|
fillGaps,
|
||||||
dynamicVariables,
|
|
||||||
}: GetQueryResultsProps): PrepareQueryRangePayloadV5Result => {
|
}: GetQueryResultsProps): PrepareQueryRangePayloadV5Result => {
|
||||||
let legendMap: Record<string, string> = {};
|
let legendMap: Record<string, string> = {};
|
||||||
const requestType = mapPanelTypeToRequestType(graphType);
|
const requestType = mapPanelTypeToRequestType(graphType);
|
||||||
@@ -522,28 +357,14 @@ export const prepareQueryRangePayloadV5 = ({
|
|||||||
|
|
||||||
switch (query.queryType) {
|
switch (query.queryType) {
|
||||||
case EQueryType.QUERY_BUILDER: {
|
case EQueryType.QUERY_BUILDER: {
|
||||||
const { queryData: data, queryFormulas, queryTraceOperator } = query.builder;
|
const { queryData: data, queryFormulas } = query.builder;
|
||||||
const currentQueryData = mapQueryDataToApi(data, 'queryName', tableParams);
|
const currentQueryData = mapQueryDataToApi(data, 'queryName', tableParams);
|
||||||
const currentFormulas = mapQueryDataToApi(queryFormulas, 'queryName');
|
const currentFormulas = mapQueryDataToApi(queryFormulas, 'queryName');
|
||||||
|
|
||||||
const filteredTraceOperator =
|
|
||||||
queryTraceOperator && queryTraceOperator.length > 0
|
|
||||||
? queryTraceOperator.filter((traceOperator) =>
|
|
||||||
Boolean(traceOperator.expression.trim()),
|
|
||||||
)
|
|
||||||
: [];
|
|
||||||
|
|
||||||
const currentTraceOperator = mapQueryDataToApi(
|
|
||||||
filteredTraceOperator,
|
|
||||||
'queryName',
|
|
||||||
tableParams,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Combine legend maps
|
// Combine legend maps
|
||||||
legendMap = {
|
legendMap = {
|
||||||
...currentQueryData.newLegendMap,
|
...currentQueryData.newLegendMap,
|
||||||
...currentFormulas.newLegendMap,
|
...currentFormulas.newLegendMap,
|
||||||
...currentTraceOperator.newLegendMap,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Convert builder queries
|
// Convert builder queries
|
||||||
@@ -576,14 +397,8 @@ export const prepareQueryRangePayloadV5 = ({
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
const traceOperatorQueries = convertTraceOperatorToV5(
|
// Combine both types
|
||||||
currentTraceOperator.data,
|
queries = [...builderQueries, ...formulaQueries];
|
||||||
requestType,
|
|
||||||
graphType,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Combine all query types
|
|
||||||
queries = [...builderQueries, ...formulaQueries, ...traceOperatorQueries];
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case EQueryType.PROM: {
|
case EQueryType.PROM: {
|
||||||
@@ -626,12 +441,7 @@ export const prepareQueryRangePayloadV5 = ({
|
|||||||
fillGaps: fillGaps || false,
|
fillGaps: fillGaps || false,
|
||||||
},
|
},
|
||||||
variables: Object.entries(variables).reduce((acc, [key, value]) => {
|
variables: Object.entries(variables).reduce((acc, [key, value]) => {
|
||||||
acc[key] = {
|
acc[key] = { value };
|
||||||
value,
|
|
||||||
type: dynamicVariables
|
|
||||||
?.find((v) => v.name === key)
|
|
||||||
?.type?.toLowerCase() as VariableType,
|
|
||||||
};
|
|
||||||
return acc;
|
return acc;
|
||||||
}, {} as Record<string, VariableItem>),
|
}, {} as Record<string, VariableItem>),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,16 +1,14 @@
|
|||||||
import { render, screen } from '@testing-library/react';
|
import { render, screen } from '@testing-library/react';
|
||||||
|
|
||||||
import getLocal from '../../../api/browser/localstorage/get';
|
|
||||||
import AppLoading from '../AppLoading';
|
import AppLoading from '../AppLoading';
|
||||||
|
|
||||||
jest.mock('../../../api/browser/localstorage/get', () => ({
|
// Mock the localStorage API
|
||||||
|
const mockGet = jest.fn();
|
||||||
|
jest.mock('api/browser/localstorage/get', () => ({
|
||||||
__esModule: true,
|
__esModule: true,
|
||||||
default: jest.fn(),
|
default: mockGet,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Access the mocked function
|
|
||||||
const mockGet = (getLocal as unknown) as jest.Mock;
|
|
||||||
|
|
||||||
describe('AppLoading', () => {
|
describe('AppLoading', () => {
|
||||||
const SIGNOZ_TEXT = 'SigNoz';
|
const SIGNOZ_TEXT = 'SigNoz';
|
||||||
const TAGLINE_TEXT =
|
const TAGLINE_TEXT =
|
||||||
|
|||||||
@@ -20,15 +20,13 @@
|
|||||||
.ant-card-body {
|
.ant-card-body {
|
||||||
height: calc(100% - 18px);
|
height: calc(100% - 18px);
|
||||||
|
|
||||||
.widget-graph-component-container {
|
.widget-graph-container {
|
||||||
.widget-graph-container {
|
&.bar {
|
||||||
&.bar-panel-container {
|
height: calc(100% - 110px);
|
||||||
height: calc(100% - 110px);
|
}
|
||||||
}
|
|
||||||
|
|
||||||
&.graph-panel-container {
|
&.graph {
|
||||||
height: calc(100% - 80px);
|
height: calc(100% - 80px);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -84,11 +82,9 @@
|
|||||||
.ant-card-body {
|
.ant-card-body {
|
||||||
height: calc(100% - 18px);
|
height: calc(100% - 18px);
|
||||||
|
|
||||||
.widget-graph-component-container {
|
.widget-graph-container {
|
||||||
.widget-graph-container {
|
&.bar {
|
||||||
&.bar-panel-container {
|
height: calc(100% - 110px);
|
||||||
height: calc(100% - 110px);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,6 +32,8 @@ export const celeryAllStateWidgetData = (
|
|||||||
aggregateAttribute: {
|
aggregateAttribute: {
|
||||||
dataType: DataTypes.String,
|
dataType: DataTypes.String,
|
||||||
id: '------false',
|
id: '------false',
|
||||||
|
isColumn: false,
|
||||||
|
isJSON: false,
|
||||||
key: '',
|
key: '',
|
||||||
type: '',
|
type: '',
|
||||||
},
|
},
|
||||||
@@ -48,6 +50,8 @@ export const celeryAllStateWidgetData = (
|
|||||||
{
|
{
|
||||||
dataType: DataTypes.String,
|
dataType: DataTypes.String,
|
||||||
id: 'celery.state--string--tag--false',
|
id: 'celery.state--string--tag--false',
|
||||||
|
isColumn: false,
|
||||||
|
isJSON: false,
|
||||||
key: 'celery.state',
|
key: 'celery.state',
|
||||||
type: 'tag',
|
type: 'tag',
|
||||||
},
|
},
|
||||||
@@ -84,6 +88,7 @@ export const celeryRetryStateWidgetData = (
|
|||||||
aggregateAttribute: {
|
aggregateAttribute: {
|
||||||
dataType: DataTypes.String,
|
dataType: DataTypes.String,
|
||||||
id: '------false',
|
id: '------false',
|
||||||
|
isColumn: false,
|
||||||
key: '',
|
key: '',
|
||||||
type: '',
|
type: '',
|
||||||
},
|
},
|
||||||
@@ -98,6 +103,8 @@ export const celeryRetryStateWidgetData = (
|
|||||||
key: {
|
key: {
|
||||||
dataType: DataTypes.String,
|
dataType: DataTypes.String,
|
||||||
id: 'celery.state--string--tag--false',
|
id: 'celery.state--string--tag--false',
|
||||||
|
isColumn: false,
|
||||||
|
isJSON: false,
|
||||||
key: 'celery.state',
|
key: 'celery.state',
|
||||||
type: 'tag',
|
type: 'tag',
|
||||||
},
|
},
|
||||||
@@ -112,6 +119,8 @@ export const celeryRetryStateWidgetData = (
|
|||||||
{
|
{
|
||||||
dataType: DataTypes.String,
|
dataType: DataTypes.String,
|
||||||
id: 'celery.hostname--string--tag--false',
|
id: 'celery.hostname--string--tag--false',
|
||||||
|
isColumn: false,
|
||||||
|
isJSON: false,
|
||||||
key: 'celery.hostname',
|
key: 'celery.hostname',
|
||||||
type: 'tag',
|
type: 'tag',
|
||||||
},
|
},
|
||||||
@@ -144,6 +153,8 @@ export const celeryFailedStateWidgetData = (
|
|||||||
aggregateAttribute: {
|
aggregateAttribute: {
|
||||||
dataType: DataTypes.String,
|
dataType: DataTypes.String,
|
||||||
id: '------false',
|
id: '------false',
|
||||||
|
isColumn: false,
|
||||||
|
isJSON: false,
|
||||||
key: '',
|
key: '',
|
||||||
type: '',
|
type: '',
|
||||||
},
|
},
|
||||||
@@ -158,6 +169,8 @@ export const celeryFailedStateWidgetData = (
|
|||||||
key: {
|
key: {
|
||||||
dataType: DataTypes.String,
|
dataType: DataTypes.String,
|
||||||
id: 'celery.state--string--tag--false',
|
id: 'celery.state--string--tag--false',
|
||||||
|
isColumn: false,
|
||||||
|
isJSON: false,
|
||||||
key: 'celery.state',
|
key: 'celery.state',
|
||||||
type: 'tag',
|
type: 'tag',
|
||||||
},
|
},
|
||||||
@@ -172,6 +185,8 @@ export const celeryFailedStateWidgetData = (
|
|||||||
{
|
{
|
||||||
dataType: DataTypes.String,
|
dataType: DataTypes.String,
|
||||||
id: 'celery.hostname--string--tag--false',
|
id: 'celery.hostname--string--tag--false',
|
||||||
|
isColumn: false,
|
||||||
|
isJSON: false,
|
||||||
key: 'celery.hostname',
|
key: 'celery.hostname',
|
||||||
type: 'tag',
|
type: 'tag',
|
||||||
},
|
},
|
||||||
@@ -204,6 +219,8 @@ export const celerySuccessStateWidgetData = (
|
|||||||
aggregateAttribute: {
|
aggregateAttribute: {
|
||||||
dataType: DataTypes.String,
|
dataType: DataTypes.String,
|
||||||
id: '------false',
|
id: '------false',
|
||||||
|
isColumn: false,
|
||||||
|
isJSON: false,
|
||||||
key: '',
|
key: '',
|
||||||
type: '',
|
type: '',
|
||||||
},
|
},
|
||||||
@@ -218,6 +235,8 @@ export const celerySuccessStateWidgetData = (
|
|||||||
key: {
|
key: {
|
||||||
dataType: DataTypes.String,
|
dataType: DataTypes.String,
|
||||||
id: 'celery.state--string--tag--false',
|
id: 'celery.state--string--tag--false',
|
||||||
|
isColumn: false,
|
||||||
|
isJSON: false,
|
||||||
key: 'celery.state',
|
key: 'celery.state',
|
||||||
type: 'tag',
|
type: 'tag',
|
||||||
},
|
},
|
||||||
@@ -232,6 +251,8 @@ export const celerySuccessStateWidgetData = (
|
|||||||
{
|
{
|
||||||
dataType: DataTypes.String,
|
dataType: DataTypes.String,
|
||||||
id: 'celery.hostname--string--tag--false',
|
id: 'celery.hostname--string--tag--false',
|
||||||
|
isColumn: false,
|
||||||
|
isJSON: false,
|
||||||
key: 'celery.hostname',
|
key: 'celery.hostname',
|
||||||
type: 'tag',
|
type: 'tag',
|
||||||
},
|
},
|
||||||
@@ -263,6 +284,7 @@ export const celeryTasksByWorkerWidgetData = (
|
|||||||
aggregateAttribute: {
|
aggregateAttribute: {
|
||||||
dataType: DataTypes.String,
|
dataType: DataTypes.String,
|
||||||
id: '------false',
|
id: '------false',
|
||||||
|
isColumn: false,
|
||||||
key: '',
|
key: '',
|
||||||
type: '',
|
type: '',
|
||||||
},
|
},
|
||||||
@@ -279,6 +301,8 @@ export const celeryTasksByWorkerWidgetData = (
|
|||||||
{
|
{
|
||||||
dataType: DataTypes.String,
|
dataType: DataTypes.String,
|
||||||
id: 'celery.hostname--string--tag--false',
|
id: 'celery.hostname--string--tag--false',
|
||||||
|
isColumn: false,
|
||||||
|
isJSON: false,
|
||||||
key: 'celery.hostname',
|
key: 'celery.hostname',
|
||||||
type: 'tag',
|
type: 'tag',
|
||||||
},
|
},
|
||||||
@@ -314,6 +338,8 @@ export const celeryErrorByWorkerWidgetData = (
|
|||||||
aggregateAttribute: {
|
aggregateAttribute: {
|
||||||
dataType: 'string',
|
dataType: 'string',
|
||||||
id: 'span_id--string----true',
|
id: 'span_id--string----true',
|
||||||
|
isColumn: true,
|
||||||
|
isJSON: false,
|
||||||
key: 'span_id',
|
key: 'span_id',
|
||||||
type: '',
|
type: '',
|
||||||
},
|
},
|
||||||
@@ -327,6 +353,8 @@ export const celeryErrorByWorkerWidgetData = (
|
|||||||
key: {
|
key: {
|
||||||
dataType: DataTypes.bool,
|
dataType: DataTypes.bool,
|
||||||
id: 'has_error--bool----true',
|
id: 'has_error--bool----true',
|
||||||
|
isColumn: true,
|
||||||
|
isJSON: false,
|
||||||
key: 'has_error',
|
key: 'has_error',
|
||||||
type: '',
|
type: '',
|
||||||
},
|
},
|
||||||
@@ -345,6 +373,8 @@ export const celeryErrorByWorkerWidgetData = (
|
|||||||
groupBy: [
|
groupBy: [
|
||||||
{
|
{
|
||||||
dataType: DataTypes.String,
|
dataType: DataTypes.String,
|
||||||
|
isColumn: false,
|
||||||
|
isJSON: false,
|
||||||
key: 'celery.hostname',
|
key: 'celery.hostname',
|
||||||
type: 'tag',
|
type: 'tag',
|
||||||
id: 'celery.hostname--string--tag--false',
|
id: 'celery.hostname--string--tag--false',
|
||||||
@@ -360,6 +390,8 @@ export const celeryErrorByWorkerWidgetData = (
|
|||||||
aggregateAttribute: {
|
aggregateAttribute: {
|
||||||
dataType: 'string',
|
dataType: 'string',
|
||||||
id: 'span_id--string----true',
|
id: 'span_id--string----true',
|
||||||
|
isColumn: true,
|
||||||
|
isJSON: false,
|
||||||
key: 'span_id',
|
key: 'span_id',
|
||||||
type: '',
|
type: '',
|
||||||
},
|
},
|
||||||
@@ -379,6 +411,8 @@ export const celeryErrorByWorkerWidgetData = (
|
|||||||
groupBy: [
|
groupBy: [
|
||||||
{
|
{
|
||||||
dataType: DataTypes.String,
|
dataType: DataTypes.String,
|
||||||
|
isColumn: false,
|
||||||
|
isJSON: false,
|
||||||
key: 'celery.hostname',
|
key: 'celery.hostname',
|
||||||
type: 'tag',
|
type: 'tag',
|
||||||
id: 'celery.hostname--string--tag--false',
|
id: 'celery.hostname--string--tag--false',
|
||||||
@@ -411,6 +445,8 @@ export const celeryLatencyByWorkerWidgetData = (
|
|||||||
aggregateAttribute: {
|
aggregateAttribute: {
|
||||||
dataType: DataTypes.Float64,
|
dataType: DataTypes.Float64,
|
||||||
id: 'duration_nano--float64----true',
|
id: 'duration_nano--float64----true',
|
||||||
|
isColumn: true,
|
||||||
|
isJSON: false,
|
||||||
key: 'duration_nano',
|
key: 'duration_nano',
|
||||||
type: '',
|
type: '',
|
||||||
},
|
},
|
||||||
@@ -427,6 +463,8 @@ export const celeryLatencyByWorkerWidgetData = (
|
|||||||
{
|
{
|
||||||
dataType: DataTypes.String,
|
dataType: DataTypes.String,
|
||||||
id: 'celery.hostname--string--tag--false',
|
id: 'celery.hostname--string--tag--false',
|
||||||
|
isColumn: false,
|
||||||
|
isJSON: false,
|
||||||
key: 'celery.hostname',
|
key: 'celery.hostname',
|
||||||
type: 'tag',
|
type: 'tag',
|
||||||
},
|
},
|
||||||
@@ -460,6 +498,8 @@ export const celeryActiveTasksWidgetData = (
|
|||||||
dataType: DataTypes.Float64,
|
dataType: DataTypes.Float64,
|
||||||
id:
|
id:
|
||||||
'flower_worker_number_of_currently_executing_tasks--float64--Gauge--true',
|
'flower_worker_number_of_currently_executing_tasks--float64--Gauge--true',
|
||||||
|
isColumn: true,
|
||||||
|
isJSON: false,
|
||||||
key: 'flower_worker_number_of_currently_executing_tasks',
|
key: 'flower_worker_number_of_currently_executing_tasks',
|
||||||
type: 'Gauge',
|
type: 'Gauge',
|
||||||
},
|
},
|
||||||
@@ -476,6 +516,8 @@ export const celeryActiveTasksWidgetData = (
|
|||||||
{
|
{
|
||||||
dataType: DataTypes.String,
|
dataType: DataTypes.String,
|
||||||
id: 'worker--string--tag--false',
|
id: 'worker--string--tag--false',
|
||||||
|
isColumn: false,
|
||||||
|
isJSON: false,
|
||||||
key: 'worker',
|
key: 'worker',
|
||||||
type: 'tag',
|
type: 'tag',
|
||||||
},
|
},
|
||||||
@@ -509,6 +551,8 @@ export const celeryTaskLatencyWidgetData = (
|
|||||||
aggregateAttribute: {
|
aggregateAttribute: {
|
||||||
dataType: DataTypes.Float64,
|
dataType: DataTypes.Float64,
|
||||||
id: 'duration_nano--float64----true',
|
id: 'duration_nano--float64----true',
|
||||||
|
isColumn: true,
|
||||||
|
isJSON: false,
|
||||||
key: 'duration_nano',
|
key: 'duration_nano',
|
||||||
type: '',
|
type: '',
|
||||||
},
|
},
|
||||||
@@ -525,6 +569,8 @@ export const celeryTaskLatencyWidgetData = (
|
|||||||
{
|
{
|
||||||
dataType: DataTypes.String,
|
dataType: DataTypes.String,
|
||||||
id: 'celery.task_name--string--tag--false',
|
id: 'celery.task_name--string--tag--false',
|
||||||
|
isColumn: false,
|
||||||
|
isJSON: false,
|
||||||
key: 'celery.task_name',
|
key: 'celery.task_name',
|
||||||
type: 'tag',
|
type: 'tag',
|
||||||
},
|
},
|
||||||
@@ -560,6 +606,8 @@ export const celerySlowestTasksTableWidgetData = getWidgetQueryBuilder(
|
|||||||
aggregateAttribute: {
|
aggregateAttribute: {
|
||||||
dataType: DataTypes.Float64,
|
dataType: DataTypes.Float64,
|
||||||
id: 'duration_nano--float64----true',
|
id: 'duration_nano--float64----true',
|
||||||
|
isColumn: true,
|
||||||
|
isJSON: false,
|
||||||
key: 'duration_nano',
|
key: 'duration_nano',
|
||||||
type: '',
|
type: '',
|
||||||
},
|
},
|
||||||
@@ -576,6 +624,8 @@ export const celerySlowestTasksTableWidgetData = getWidgetQueryBuilder(
|
|||||||
{
|
{
|
||||||
dataType: DataTypes.String,
|
dataType: DataTypes.String,
|
||||||
id: 'celery.task_name--string--tag--false',
|
id: 'celery.task_name--string--tag--false',
|
||||||
|
isColumn: false,
|
||||||
|
isJSON: false,
|
||||||
key: 'celery.task_name',
|
key: 'celery.task_name',
|
||||||
type: 'tag',
|
type: 'tag',
|
||||||
},
|
},
|
||||||
@@ -610,6 +660,8 @@ export const celeryRetryTasksTableWidgetData = getWidgetQueryBuilder(
|
|||||||
aggregateAttribute: {
|
aggregateAttribute: {
|
||||||
dataType: DataTypes.Float64,
|
dataType: DataTypes.Float64,
|
||||||
id: 'duration_nano--float64----true',
|
id: 'duration_nano--float64----true',
|
||||||
|
isColumn: true,
|
||||||
|
isJSON: false,
|
||||||
key: 'duration_nano',
|
key: 'duration_nano',
|
||||||
type: '',
|
type: '',
|
||||||
},
|
},
|
||||||
@@ -624,6 +676,8 @@ export const celeryRetryTasksTableWidgetData = getWidgetQueryBuilder(
|
|||||||
key: {
|
key: {
|
||||||
dataType: DataTypes.String,
|
dataType: DataTypes.String,
|
||||||
id: 'celery.state--string--tag--false',
|
id: 'celery.state--string--tag--false',
|
||||||
|
isColumn: false,
|
||||||
|
isJSON: false,
|
||||||
key: 'celery.state',
|
key: 'celery.state',
|
||||||
type: 'tag',
|
type: 'tag',
|
||||||
},
|
},
|
||||||
@@ -638,6 +692,8 @@ export const celeryRetryTasksTableWidgetData = getWidgetQueryBuilder(
|
|||||||
{
|
{
|
||||||
dataType: DataTypes.String,
|
dataType: DataTypes.String,
|
||||||
id: 'celery.task_name--string--tag--false',
|
id: 'celery.task_name--string--tag--false',
|
||||||
|
isColumn: false,
|
||||||
|
isJSON: false,
|
||||||
key: 'celery.task_name',
|
key: 'celery.task_name',
|
||||||
type: 'tag',
|
type: 'tag',
|
||||||
},
|
},
|
||||||
@@ -673,6 +729,8 @@ export const celeryFailedTasksTableWidgetData = getWidgetQueryBuilder(
|
|||||||
aggregateAttribute: {
|
aggregateAttribute: {
|
||||||
dataType: DataTypes.Float64,
|
dataType: DataTypes.Float64,
|
||||||
id: 'duration_nano--float64----true',
|
id: 'duration_nano--float64----true',
|
||||||
|
isColumn: true,
|
||||||
|
isJSON: false,
|
||||||
key: 'duration_nano',
|
key: 'duration_nano',
|
||||||
type: '',
|
type: '',
|
||||||
},
|
},
|
||||||
@@ -687,6 +745,8 @@ export const celeryFailedTasksTableWidgetData = getWidgetQueryBuilder(
|
|||||||
key: {
|
key: {
|
||||||
dataType: DataTypes.String,
|
dataType: DataTypes.String,
|
||||||
id: 'celery.state--string--tag--false',
|
id: 'celery.state--string--tag--false',
|
||||||
|
isColumn: false,
|
||||||
|
isJSON: false,
|
||||||
key: 'celery.state',
|
key: 'celery.state',
|
||||||
type: 'tag',
|
type: 'tag',
|
||||||
},
|
},
|
||||||
@@ -701,6 +761,8 @@ export const celeryFailedTasksTableWidgetData = getWidgetQueryBuilder(
|
|||||||
{
|
{
|
||||||
dataType: DataTypes.String,
|
dataType: DataTypes.String,
|
||||||
id: 'celery.task_name--string--tag--false',
|
id: 'celery.task_name--string--tag--false',
|
||||||
|
isColumn: false,
|
||||||
|
isJSON: false,
|
||||||
key: 'celery.task_name',
|
key: 'celery.task_name',
|
||||||
type: 'tag',
|
type: 'tag',
|
||||||
},
|
},
|
||||||
@@ -734,6 +796,8 @@ export const celerySuccessTasksTableWidgetData = getWidgetQueryBuilder(
|
|||||||
aggregateAttribute: {
|
aggregateAttribute: {
|
||||||
dataType: DataTypes.Float64,
|
dataType: DataTypes.Float64,
|
||||||
id: 'duration_nano--float64----true',
|
id: 'duration_nano--float64----true',
|
||||||
|
isColumn: true,
|
||||||
|
isJSON: false,
|
||||||
key: 'duration_nano',
|
key: 'duration_nano',
|
||||||
type: '',
|
type: '',
|
||||||
},
|
},
|
||||||
@@ -748,6 +812,8 @@ export const celerySuccessTasksTableWidgetData = getWidgetQueryBuilder(
|
|||||||
key: {
|
key: {
|
||||||
dataType: DataTypes.String,
|
dataType: DataTypes.String,
|
||||||
id: 'celery.state--string--tag--false',
|
id: 'celery.state--string--tag--false',
|
||||||
|
isColumn: false,
|
||||||
|
isJSON: false,
|
||||||
key: 'celery.state',
|
key: 'celery.state',
|
||||||
type: 'tag',
|
type: 'tag',
|
||||||
},
|
},
|
||||||
@@ -762,6 +828,8 @@ export const celerySuccessTasksTableWidgetData = getWidgetQueryBuilder(
|
|||||||
{
|
{
|
||||||
dataType: DataTypes.String,
|
dataType: DataTypes.String,
|
||||||
id: 'celery.task_name--string--tag--false',
|
id: 'celery.task_name--string--tag--false',
|
||||||
|
isColumn: false,
|
||||||
|
isJSON: false,
|
||||||
key: 'celery.task_name',
|
key: 'celery.task_name',
|
||||||
type: 'tag',
|
type: 'tag',
|
||||||
},
|
},
|
||||||
@@ -801,6 +869,8 @@ export const celeryTimeSeriesTablesWidgetData = (
|
|||||||
aggregateAttribute: {
|
aggregateAttribute: {
|
||||||
dataType: DataTypes.Float64,
|
dataType: DataTypes.Float64,
|
||||||
id: 'duration_nano--float64----true',
|
id: 'duration_nano--float64----true',
|
||||||
|
isColumn: true,
|
||||||
|
isJSON: false,
|
||||||
key: 'duration_nano',
|
key: 'duration_nano',
|
||||||
type: '',
|
type: '',
|
||||||
},
|
},
|
||||||
@@ -815,6 +885,8 @@ export const celeryTimeSeriesTablesWidgetData = (
|
|||||||
key: {
|
key: {
|
||||||
dataType: DataTypes.String,
|
dataType: DataTypes.String,
|
||||||
id: `${entity}--string--tag--false`,
|
id: `${entity}--string--tag--false`,
|
||||||
|
isColumn: false,
|
||||||
|
isJSON: false,
|
||||||
key: `${entity}`,
|
key: `${entity}`,
|
||||||
type: 'tag',
|
type: 'tag',
|
||||||
},
|
},
|
||||||
@@ -829,6 +901,8 @@ export const celeryTimeSeriesTablesWidgetData = (
|
|||||||
{
|
{
|
||||||
dataType: DataTypes.String,
|
dataType: DataTypes.String,
|
||||||
id: 'celery.task_name--string--tag--false',
|
id: 'celery.task_name--string--tag--false',
|
||||||
|
isColumn: false,
|
||||||
|
isJSON: false,
|
||||||
key: 'celery.task_name',
|
key: 'celery.task_name',
|
||||||
type: 'tag',
|
type: 'tag',
|
||||||
},
|
},
|
||||||
@@ -859,6 +933,8 @@ export const celeryAllStateCountWidgetData = getWidgetQueryBuilder(
|
|||||||
aggregateAttribute: {
|
aggregateAttribute: {
|
||||||
dataType: DataTypes.String,
|
dataType: DataTypes.String,
|
||||||
id: 'span_id--string----true',
|
id: 'span_id--string----true',
|
||||||
|
isColumn: true,
|
||||||
|
isJSON: false,
|
||||||
key: 'span_id',
|
key: 'span_id',
|
||||||
type: '',
|
type: '',
|
||||||
},
|
},
|
||||||
@@ -896,6 +972,8 @@ export const celerySuccessStateCountWidgetData = getWidgetQueryBuilder(
|
|||||||
aggregateAttribute: {
|
aggregateAttribute: {
|
||||||
dataType: DataTypes.String,
|
dataType: DataTypes.String,
|
||||||
id: 'span_id--string----true',
|
id: 'span_id--string----true',
|
||||||
|
isColumn: true,
|
||||||
|
isJSON: false,
|
||||||
key: 'span_id',
|
key: 'span_id',
|
||||||
type: '',
|
type: '',
|
||||||
},
|
},
|
||||||
@@ -910,6 +988,8 @@ export const celerySuccessStateCountWidgetData = getWidgetQueryBuilder(
|
|||||||
key: {
|
key: {
|
||||||
dataType: DataTypes.String,
|
dataType: DataTypes.String,
|
||||||
id: 'celery.state--string--tag--false',
|
id: 'celery.state--string--tag--false',
|
||||||
|
isColumn: false,
|
||||||
|
isJSON: false,
|
||||||
key: 'celery.state',
|
key: 'celery.state',
|
||||||
type: 'tag',
|
type: 'tag',
|
||||||
},
|
},
|
||||||
@@ -945,6 +1025,8 @@ export const celeryFailedStateCountWidgetData = getWidgetQueryBuilder(
|
|||||||
aggregateAttribute: {
|
aggregateAttribute: {
|
||||||
dataType: DataTypes.String,
|
dataType: DataTypes.String,
|
||||||
id: 'span_id--string----true',
|
id: 'span_id--string----true',
|
||||||
|
isColumn: true,
|
||||||
|
isJSON: false,
|
||||||
key: 'span_id',
|
key: 'span_id',
|
||||||
type: '',
|
type: '',
|
||||||
},
|
},
|
||||||
@@ -959,6 +1041,8 @@ export const celeryFailedStateCountWidgetData = getWidgetQueryBuilder(
|
|||||||
key: {
|
key: {
|
||||||
dataType: DataTypes.String,
|
dataType: DataTypes.String,
|
||||||
id: 'celery.state--string--tag--false',
|
id: 'celery.state--string--tag--false',
|
||||||
|
isColumn: false,
|
||||||
|
isJSON: false,
|
||||||
key: 'celery.state',
|
key: 'celery.state',
|
||||||
type: 'tag',
|
type: 'tag',
|
||||||
},
|
},
|
||||||
@@ -994,6 +1078,7 @@ export const celeryRetryStateCountWidgetData = getWidgetQueryBuilder(
|
|||||||
aggregateAttribute: {
|
aggregateAttribute: {
|
||||||
dataType: DataTypes.String,
|
dataType: DataTypes.String,
|
||||||
id: 'span_id--string----true',
|
id: 'span_id--string----true',
|
||||||
|
isColumn: true,
|
||||||
key: 'span_id',
|
key: 'span_id',
|
||||||
type: '',
|
type: '',
|
||||||
},
|
},
|
||||||
@@ -1008,6 +1093,8 @@ export const celeryRetryStateCountWidgetData = getWidgetQueryBuilder(
|
|||||||
key: {
|
key: {
|
||||||
dataType: DataTypes.String,
|
dataType: DataTypes.String,
|
||||||
id: 'celery.state--string--tag--false',
|
id: 'celery.state--string--tag--false',
|
||||||
|
isColumn: false,
|
||||||
|
isJSON: false,
|
||||||
key: 'celery.state',
|
key: 'celery.state',
|
||||||
type: 'tag',
|
type: 'tag',
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -39,6 +39,8 @@ export function getFiltersFromQueryParams(
|
|||||||
key,
|
key,
|
||||||
dataType: DataTypes.String,
|
dataType: DataTypes.String,
|
||||||
type: 'tag',
|
type: 'tag',
|
||||||
|
isColumn: false,
|
||||||
|
isJSON: false,
|
||||||
id: `${key}--string--tag--false`,
|
id: `${key}--string--tag--false`,
|
||||||
},
|
},
|
||||||
op: '=',
|
op: '=',
|
||||||
@@ -98,7 +100,8 @@ export const createFiltersFromData = (
|
|||||||
key: string;
|
key: string;
|
||||||
dataType: DataTypes;
|
dataType: DataTypes;
|
||||||
type: string;
|
type: string;
|
||||||
|
isColumn: boolean;
|
||||||
|
isJSON: boolean;
|
||||||
id: string;
|
id: string;
|
||||||
};
|
};
|
||||||
op: string;
|
op: string;
|
||||||
@@ -116,6 +119,8 @@ export const createFiltersFromData = (
|
|||||||
key,
|
key,
|
||||||
dataType: DataTypes.String,
|
dataType: DataTypes.String,
|
||||||
type: 'tag',
|
type: 'tag',
|
||||||
|
isColumn: false,
|
||||||
|
isJSON: false,
|
||||||
id: `${key}--string--tag--false`,
|
id: `${key}--string--tag--false`,
|
||||||
},
|
},
|
||||||
op: '=',
|
op: '=',
|
||||||
|
|||||||
@@ -137,11 +137,5 @@
|
|||||||
h6 {
|
h6 {
|
||||||
color: var(--text-ink-500);
|
color: var(--text-ink-500);
|
||||||
}
|
}
|
||||||
|
|
||||||
code {
|
|
||||||
background-color: var(--bg-vanilla-300);
|
|
||||||
border: 1px solid var(--bg-vanilla-300);
|
|
||||||
color: var(--text-ink-500);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -241,6 +241,8 @@ function ClientSideQBSearch(
|
|||||||
key: 'body',
|
key: 'body',
|
||||||
dataType: DataTypes.String,
|
dataType: DataTypes.String,
|
||||||
type: '',
|
type: '',
|
||||||
|
isColumn: true,
|
||||||
|
isJSON: false,
|
||||||
id: 'body--string----true',
|
id: 'body--string----true',
|
||||||
},
|
},
|
||||||
op: OPERATORS.CONTAINS,
|
op: OPERATORS.CONTAINS,
|
||||||
|
|||||||
@@ -1,16 +1,6 @@
|
|||||||
.custom-time-picker {
|
.custom-time-picker {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
||||||
.timeSelection-input {
|
|
||||||
&:hover {
|
|
||||||
border-color: #1d212d !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.time-input-suffix {
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.time-options-container {
|
.time-options-container {
|
||||||
@@ -145,7 +135,6 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
color: var(--bg-vanilla-400);
|
color: var(--bg-vanilla-400);
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
|
|
||||||
.timezone {
|
.timezone {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -174,52 +163,6 @@
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.time-input-prefix {
|
|
||||||
.live-dot-icon {
|
|
||||||
width: 6px;
|
|
||||||
height: 6px;
|
|
||||||
border-radius: 50%;
|
|
||||||
background-color: var(--bg-forest-500);
|
|
||||||
animation: ripple 1s infinite;
|
|
||||||
|
|
||||||
margin-right: 4px;
|
|
||||||
margin-left: 4px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes ripple {
|
|
||||||
0% {
|
|
||||||
box-shadow: 0 0 0 0 rgba(245, 158, 11, 0.4);
|
|
||||||
}
|
|
||||||
70% {
|
|
||||||
box-shadow: 0 0 0 6px rgba(245, 158, 11, 0);
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
box-shadow: 0 0 0 0 rgba(245, 158, 11, 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.time-input-suffix-icon-badge {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
padding: 0 4px;
|
|
||||||
border-radius: 2px;
|
|
||||||
background: rgba(171, 189, 255, 0.04);
|
|
||||||
color: var(--bg-vanilla-100);
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: 400;
|
|
||||||
line-height: 16px;
|
|
||||||
letter-spacing: -0.06px;
|
|
||||||
cursor: pointer;
|
|
||||||
height: 20px;
|
|
||||||
width: 20px;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background: rgba(171, 189, 255, 0.08);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.lightMode {
|
.lightMode {
|
||||||
.date-time-popover__footer {
|
.date-time-popover__footer {
|
||||||
border-color: var(--bg-vanilla-400);
|
border-color: var(--bg-vanilla-400);
|
||||||
@@ -237,26 +180,8 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.custom-time-picker {
|
|
||||||
.timeSelection-input {
|
|
||||||
&:hover {
|
|
||||||
border-color: var(--bg-vanilla-300) !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.timezone-badge {
|
.timezone-badge {
|
||||||
color: var(--bg-ink-100);
|
color: var(--bg-ink-100);
|
||||||
background: rgb(179 179 179 / 15%);
|
background: rgb(179 179 179 / 15%);
|
||||||
}
|
}
|
||||||
|
|
||||||
.time-input-suffix-icon-badge {
|
|
||||||
color: var(--bg-ink-100);
|
|
||||||
background: rgb(179 179 179 / 15%);
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background: rgb(179 179 179 / 20%);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,12 +5,13 @@ import './CustomTimePicker.styles.scss';
|
|||||||
import { Input, Popover, Tooltip, Typography } from 'antd';
|
import { Input, Popover, Tooltip, Typography } from 'antd';
|
||||||
import logEvent from 'api/common/logEvent';
|
import logEvent from 'api/common/logEvent';
|
||||||
import cx from 'classnames';
|
import cx from 'classnames';
|
||||||
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
|
|
||||||
import { DateTimeRangeType } from 'container/TopNav/CustomDateTimeModal';
|
import { DateTimeRangeType } from 'container/TopNav/CustomDateTimeModal';
|
||||||
import {
|
import {
|
||||||
|
CustomTimeType,
|
||||||
FixedDurationSuggestionOptions,
|
FixedDurationSuggestionOptions,
|
||||||
Options,
|
Options,
|
||||||
RelativeDurationSuggestionOptions,
|
RelativeDurationSuggestionOptions,
|
||||||
|
Time,
|
||||||
} from 'container/TopNav/DateTimeSelectionV2/config';
|
} from 'container/TopNav/DateTimeSelectionV2/config';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import { isValidTimeFormat } from 'lib/getMinMax';
|
import { isValidTimeFormat } from 'lib/getMinMax';
|
||||||
@@ -27,10 +28,7 @@ import {
|
|||||||
useMemo,
|
useMemo,
|
||||||
useState,
|
useState,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
import { useSelector } from 'react-redux';
|
|
||||||
import { useLocation } from 'react-router-dom';
|
import { useLocation } from 'react-router-dom';
|
||||||
import { AppState } from 'store/reducers';
|
|
||||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
|
||||||
import { popupContainer } from 'utils/selectPopupContainer';
|
import { popupContainer } from 'utils/selectPopupContainer';
|
||||||
|
|
||||||
import CustomTimePickerPopoverContent from './CustomTimePickerPopoverContent';
|
import CustomTimePickerPopoverContent from './CustomTimePickerPopoverContent';
|
||||||
@@ -59,9 +57,11 @@ interface CustomTimePickerProps {
|
|||||||
customDateTimeVisible?: boolean;
|
customDateTimeVisible?: boolean;
|
||||||
setCustomDTPickerVisible?: Dispatch<SetStateAction<boolean>>;
|
setCustomDTPickerVisible?: Dispatch<SetStateAction<boolean>>;
|
||||||
onCustomDateHandler?: (dateTimeRange: DateTimeRangeType) => void;
|
onCustomDateHandler?: (dateTimeRange: DateTimeRangeType) => void;
|
||||||
showLiveLogs?: boolean;
|
handleGoLive?: () => void;
|
||||||
onGoLive?: () => void;
|
onTimeChange?: (
|
||||||
onExitLiveLogs?: () => void;
|
interval: Time | CustomTimeType,
|
||||||
|
dateTimeRange?: [number, number],
|
||||||
|
) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
function CustomTimePicker({
|
function CustomTimePicker({
|
||||||
@@ -78,19 +78,14 @@ function CustomTimePicker({
|
|||||||
customDateTimeVisible,
|
customDateTimeVisible,
|
||||||
setCustomDTPickerVisible,
|
setCustomDTPickerVisible,
|
||||||
onCustomDateHandler,
|
onCustomDateHandler,
|
||||||
onGoLive,
|
handleGoLive,
|
||||||
onExitLiveLogs,
|
onTimeChange,
|
||||||
showLiveLogs,
|
|
||||||
}: CustomTimePickerProps): JSX.Element {
|
}: CustomTimePickerProps): JSX.Element {
|
||||||
const [
|
const [
|
||||||
selectedTimePlaceholderValue,
|
selectedTimePlaceholderValue,
|
||||||
setSelectedTimePlaceholderValue,
|
setSelectedTimePlaceholderValue,
|
||||||
] = useState('Select / Enter Time Range');
|
] = useState('Select / Enter Time Range');
|
||||||
|
|
||||||
const { maxTime, minTime } = useSelector<AppState, GlobalReducer>(
|
|
||||||
(state) => state.globalTime,
|
|
||||||
);
|
|
||||||
|
|
||||||
const [inputValue, setInputValue] = useState('');
|
const [inputValue, setInputValue] = useState('');
|
||||||
const [inputStatus, setInputStatus] = useState<'' | 'error' | 'success'>('');
|
const [inputStatus, setInputStatus] = useState<'' | 'error' | 'success'>('');
|
||||||
const [inputErrorMessage, setInputErrorMessage] = useState<string | null>(
|
const [inputErrorMessage, setInputErrorMessage] = useState<string | null>(
|
||||||
@@ -169,13 +164,9 @@ function CustomTimePicker({
|
|||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (showLiveLogs) {
|
const value = getSelectedTimeRangeLabel(selectedTime, selectedValue);
|
||||||
setSelectedTimePlaceholderValue('Live');
|
setSelectedTimePlaceholderValue(value);
|
||||||
} else {
|
}, [selectedTime, selectedValue]);
|
||||||
const value = getSelectedTimeRangeLabel(selectedTime, selectedValue);
|
|
||||||
setSelectedTimePlaceholderValue(value);
|
|
||||||
}
|
|
||||||
}, [selectedTime, selectedValue, showLiveLogs]);
|
|
||||||
|
|
||||||
const hide = (): void => {
|
const hide = (): void => {
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
@@ -265,11 +256,6 @@ function CustomTimePicker({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleSelect = (label: string, value: string): void => {
|
const handleSelect = (label: string, value: string): void => {
|
||||||
if (label === 'Custom') {
|
|
||||||
setCustomDTPickerVisible?.(true);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
onSelect(value);
|
onSelect(value);
|
||||||
setSelectedTimePlaceholderValue(label);
|
setSelectedTimePlaceholderValue(label);
|
||||||
setInputStatus('');
|
setInputStatus('');
|
||||||
@@ -332,118 +318,84 @@ function CustomTimePicker({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const getTooltipTitle = (): string => {
|
|
||||||
if (selectedTime === 'custom' && inputValue === '' && !open) {
|
|
||||||
return `${dayjs(minTime / 1000_000)
|
|
||||||
.tz(timezone.value)
|
|
||||||
.format(DATE_TIME_FORMATS.DD_MMM_YYYY_HH_MM_SS)} - ${dayjs(
|
|
||||||
maxTime / 1000_000,
|
|
||||||
)
|
|
||||||
.tz(timezone.value)
|
|
||||||
.format(DATE_TIME_FORMATS.DD_MMM_YYYY_HH_MM_SS)}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return '';
|
|
||||||
};
|
|
||||||
|
|
||||||
const getInputPrefix = (): JSX.Element => {
|
|
||||||
if (showLiveLogs) {
|
|
||||||
return (
|
|
||||||
<div className="time-input-prefix">
|
|
||||||
<div className="live-dot-icon" />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="time-input-prefix">
|
|
||||||
{inputValue && inputStatus === 'success' ? (
|
|
||||||
<CheckCircle size={14} color="#51E7A8" />
|
|
||||||
) : (
|
|
||||||
<Tooltip title="Enter time in format (e.g., 1m, 2h, 3d, 4w)">
|
|
||||||
<Clock size={14} className="cursor-pointer" />
|
|
||||||
</Tooltip>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="custom-time-picker">
|
<div className="custom-time-picker">
|
||||||
<Tooltip title={getTooltipTitle()} placement="top">
|
<Popover
|
||||||
<Popover
|
className={cx(
|
||||||
className={cx(
|
'timeSelection-input-container',
|
||||||
'timeSelection-input-container',
|
selectedTime === 'custom' && inputValue === '' ? 'custom-time' : '',
|
||||||
selectedTime === 'custom' && inputValue === '' ? 'custom-time' : '',
|
)}
|
||||||
)}
|
placement="bottomRight"
|
||||||
placement="bottomRight"
|
getPopupContainer={popupContainer}
|
||||||
getPopupContainer={popupContainer}
|
rootClassName="date-time-root"
|
||||||
rootClassName="date-time-root"
|
content={
|
||||||
content={
|
newPopover ? (
|
||||||
newPopover ? (
|
<CustomTimePickerPopoverContent
|
||||||
<CustomTimePickerPopoverContent
|
setIsOpen={setOpen}
|
||||||
setIsOpen={setOpen}
|
customDateTimeVisible={defaultTo(customDateTimeVisible, false)}
|
||||||
customDateTimeVisible={defaultTo(customDateTimeVisible, false)}
|
setCustomDTPickerVisible={defaultTo(setCustomDTPickerVisible, noop)}
|
||||||
setCustomDTPickerVisible={defaultTo(setCustomDTPickerVisible, noop)}
|
onCustomDateHandler={defaultTo(onCustomDateHandler, noop)}
|
||||||
onCustomDateHandler={defaultTo(onCustomDateHandler, noop)}
|
onSelectHandler={handleSelect}
|
||||||
onSelectHandler={handleSelect}
|
handleGoLive={defaultTo(handleGoLive, noop)}
|
||||||
onGoLive={defaultTo(onGoLive, noop)}
|
options={items}
|
||||||
onExitLiveLogs={defaultTo(onExitLiveLogs, noop)}
|
selectedTime={selectedTime}
|
||||||
options={items}
|
activeView={activeView}
|
||||||
selectedTime={selectedTime}
|
setActiveView={setActiveView}
|
||||||
activeView={activeView}
|
setIsOpenedFromFooter={setIsOpenedFromFooter}
|
||||||
setActiveView={setActiveView}
|
isOpenedFromFooter={isOpenedFromFooter}
|
||||||
setIsOpenedFromFooter={setIsOpenedFromFooter}
|
onTimeChange={onTimeChange}
|
||||||
isOpenedFromFooter={isOpenedFromFooter}
|
/>
|
||||||
/>
|
) : (
|
||||||
|
content
|
||||||
|
)
|
||||||
|
}
|
||||||
|
arrow={false}
|
||||||
|
trigger="click"
|
||||||
|
open={open}
|
||||||
|
onOpenChange={handleOpenChange}
|
||||||
|
style={{
|
||||||
|
padding: 0,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
className="timeSelection-input"
|
||||||
|
type="text"
|
||||||
|
status={inputValue && inputStatus === 'error' ? 'error' : ''}
|
||||||
|
placeholder={
|
||||||
|
isInputFocused
|
||||||
|
? 'Time Format (1m or 2h or 3d or 4w)'
|
||||||
|
: selectedTimePlaceholderValue
|
||||||
|
}
|
||||||
|
value={inputValue}
|
||||||
|
onFocus={handleFocus}
|
||||||
|
onBlur={handleBlur}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
data-1p-ignore
|
||||||
|
prefix={
|
||||||
|
inputValue && inputStatus === 'success' ? (
|
||||||
|
<CheckCircle size={14} color="#51E7A8" />
|
||||||
) : (
|
) : (
|
||||||
content
|
<Tooltip title="Enter time in format (e.g., 1m, 2h, 3d, 4w)">
|
||||||
|
<Clock size={14} />
|
||||||
|
</Tooltip>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
arrow={false}
|
suffix={
|
||||||
trigger="click"
|
<>
|
||||||
open={open}
|
{!!isTimezoneOverridden && activeTimezoneOffset && (
|
||||||
onOpenChange={handleOpenChange}
|
<div className="timezone-badge" onClick={handleTimezoneHintClick}>
|
||||||
style={{
|
<span>{activeTimezoneOffset}</span>
|
||||||
padding: 0,
|
</div>
|
||||||
}}
|
)}
|
||||||
>
|
<ChevronDown
|
||||||
<Input
|
size={14}
|
||||||
className="timeSelection-input"
|
onClick={(): void => handleViewChange('datetime')}
|
||||||
type="text"
|
/>
|
||||||
status={inputValue && inputStatus === 'error' ? 'error' : ''}
|
</>
|
||||||
placeholder={
|
}
|
||||||
isInputFocused
|
/>
|
||||||
? 'Time Format (1m or 2h or 3d or 4w)'
|
</Popover>
|
||||||
: selectedTimePlaceholderValue
|
|
||||||
}
|
|
||||||
value={inputValue}
|
|
||||||
onFocus={handleFocus}
|
|
||||||
onClick={handleFocus}
|
|
||||||
onBlur={handleBlur}
|
|
||||||
onChange={handleInputChange}
|
|
||||||
data-1p-ignore
|
|
||||||
prefix={getInputPrefix()}
|
|
||||||
suffix={
|
|
||||||
<div className="time-input-suffix">
|
|
||||||
{!!isTimezoneOverridden && activeTimezoneOffset && (
|
|
||||||
<div className="timezone-badge" onClick={handleTimezoneHintClick}>
|
|
||||||
<span>{activeTimezoneOffset}</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<ChevronDown
|
|
||||||
size={14}
|
|
||||||
className="cursor-pointer time-input-suffix-icon-badge"
|
|
||||||
onClick={(e): void => {
|
|
||||||
e.stopPropagation();
|
|
||||||
handleViewChange('datetime');
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</Popover>
|
|
||||||
</Tooltip>
|
|
||||||
{inputStatus === 'error' && inputErrorMessage && (
|
{inputStatus === 'error' && inputErrorMessage && (
|
||||||
<Typography.Title level={5} className="valid-format-error">
|
<Typography.Title level={5} className="valid-format-error">
|
||||||
{inputErrorMessage}
|
{inputErrorMessage}
|
||||||
@@ -460,8 +412,7 @@ CustomTimePicker.defaultProps = {
|
|||||||
customDateTimeVisible: false,
|
customDateTimeVisible: false,
|
||||||
setCustomDTPickerVisible: noop,
|
setCustomDTPickerVisible: noop,
|
||||||
onCustomDateHandler: noop,
|
onCustomDateHandler: noop,
|
||||||
onGoLive: noop,
|
handleGoLive: noop,
|
||||||
onCustomTimeStatusUpdate: noop,
|
onCustomTimeStatusUpdate: noop,
|
||||||
onExitLiveLogs: noop,
|
onTimeChange: undefined,
|
||||||
showLiveLogs: false,
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -4,30 +4,21 @@ import { Color } from '@signozhq/design-tokens';
|
|||||||
import { Button } from 'antd';
|
import { Button } from 'antd';
|
||||||
import logEvent from 'api/common/logEvent';
|
import logEvent from 'api/common/logEvent';
|
||||||
import cx from 'classnames';
|
import cx from 'classnames';
|
||||||
import DatePickerV2 from 'components/DatePickerV2/DatePickerV2';
|
|
||||||
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
|
|
||||||
import { QueryParams } from 'constants/query';
|
|
||||||
import ROUTES from 'constants/routes';
|
import ROUTES from 'constants/routes';
|
||||||
import { DateTimeRangeType } from 'container/TopNav/CustomDateTimeModal';
|
import { DateTimeRangeType } from 'container/TopNav/CustomDateTimeModal';
|
||||||
import {
|
import {
|
||||||
|
CustomTimeType,
|
||||||
LexicalContext,
|
LexicalContext,
|
||||||
Option,
|
Option,
|
||||||
RelativeDurationSuggestionOptions,
|
RelativeDurationSuggestionOptions,
|
||||||
|
Time,
|
||||||
} from 'container/TopNav/DateTimeSelectionV2/config';
|
} from 'container/TopNav/DateTimeSelectionV2/config';
|
||||||
import dayjs from 'dayjs';
|
|
||||||
import { Clock, PenLine } from 'lucide-react';
|
import { Clock, PenLine } from 'lucide-react';
|
||||||
import { useTimezone } from 'providers/Timezone';
|
import { useTimezone } from 'providers/Timezone';
|
||||||
import {
|
import { Dispatch, SetStateAction, useMemo } from 'react';
|
||||||
Dispatch,
|
|
||||||
SetStateAction,
|
|
||||||
useCallback,
|
|
||||||
useEffect,
|
|
||||||
useMemo,
|
|
||||||
useState,
|
|
||||||
} from 'react';
|
|
||||||
import { useLocation } from 'react-router-dom';
|
import { useLocation } from 'react-router-dom';
|
||||||
import { getCustomTimeRanges } from 'utils/customTimeRangeUtils';
|
|
||||||
|
|
||||||
|
import RangePickerModal from './RangePickerModal';
|
||||||
import TimezonePicker from './TimezonePicker';
|
import TimezonePicker from './TimezonePicker';
|
||||||
|
|
||||||
interface CustomTimePickerPopoverContentProps {
|
interface CustomTimePickerPopoverContentProps {
|
||||||
@@ -40,21 +31,16 @@ interface CustomTimePickerPopoverContentProps {
|
|||||||
lexicalContext?: LexicalContext,
|
lexicalContext?: LexicalContext,
|
||||||
) => void;
|
) => void;
|
||||||
onSelectHandler: (label: string, value: string) => void;
|
onSelectHandler: (label: string, value: string) => void;
|
||||||
onGoLive: () => void;
|
handleGoLive: () => void;
|
||||||
selectedTime: string;
|
selectedTime: string;
|
||||||
activeView: 'datetime' | 'timezone';
|
activeView: 'datetime' | 'timezone';
|
||||||
setActiveView: Dispatch<SetStateAction<'datetime' | 'timezone'>>;
|
setActiveView: Dispatch<SetStateAction<'datetime' | 'timezone'>>;
|
||||||
isOpenedFromFooter: boolean;
|
isOpenedFromFooter: boolean;
|
||||||
setIsOpenedFromFooter: Dispatch<SetStateAction<boolean>>;
|
setIsOpenedFromFooter: Dispatch<SetStateAction<boolean>>;
|
||||||
onExitLiveLogs: () => void;
|
onTimeChange?: (
|
||||||
}
|
interval: Time | CustomTimeType,
|
||||||
|
dateTimeRange?: [number, number],
|
||||||
interface RecentlyUsedDateTimeRange {
|
) => void;
|
||||||
label: string;
|
|
||||||
value: number;
|
|
||||||
timestamp: number;
|
|
||||||
from: string;
|
|
||||||
to: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||||
@@ -65,68 +51,22 @@ function CustomTimePickerPopoverContent({
|
|||||||
setCustomDTPickerVisible,
|
setCustomDTPickerVisible,
|
||||||
onCustomDateHandler,
|
onCustomDateHandler,
|
||||||
onSelectHandler,
|
onSelectHandler,
|
||||||
onGoLive,
|
handleGoLive,
|
||||||
selectedTime,
|
selectedTime,
|
||||||
activeView,
|
activeView,
|
||||||
setActiveView,
|
setActiveView,
|
||||||
isOpenedFromFooter,
|
isOpenedFromFooter,
|
||||||
setIsOpenedFromFooter,
|
setIsOpenedFromFooter,
|
||||||
onExitLiveLogs,
|
onTimeChange,
|
||||||
}: CustomTimePickerPopoverContentProps): JSX.Element {
|
}: CustomTimePickerPopoverContentProps): JSX.Element {
|
||||||
const { pathname } = useLocation();
|
const { pathname } = useLocation();
|
||||||
|
|
||||||
const isLogsExplorerPage = useMemo(() => pathname === ROUTES.LOGS_EXPLORER, [
|
const isLogsExplorerPage = useMemo(() => pathname === ROUTES.LOGS_EXPLORER, [
|
||||||
pathname,
|
pathname,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const url = new URLSearchParams(window.location.search);
|
|
||||||
|
|
||||||
let panelTypeFromURL = url.get(QueryParams.panelTypes);
|
|
||||||
|
|
||||||
try {
|
|
||||||
panelTypeFromURL = JSON.parse(panelTypeFromURL as string);
|
|
||||||
} catch {
|
|
||||||
// fallback → leave as-is
|
|
||||||
}
|
|
||||||
|
|
||||||
const isLogsListView =
|
|
||||||
panelTypeFromURL !== 'table' && panelTypeFromURL !== 'graph'; // we do not select list view in the url
|
|
||||||
|
|
||||||
const { timezone } = useTimezone();
|
const { timezone } = useTimezone();
|
||||||
const activeTimezoneOffset = timezone.offset;
|
const activeTimezoneOffset = timezone.offset;
|
||||||
|
|
||||||
const [recentlyUsedTimeRanges, setRecentlyUsedTimeRanges] = useState<
|
|
||||||
RecentlyUsedDateTimeRange[]
|
|
||||||
>([]);
|
|
||||||
|
|
||||||
const handleExitLiveLogs = useCallback((): void => {
|
|
||||||
if (isLogsExplorerPage) {
|
|
||||||
onExitLiveLogs();
|
|
||||||
}
|
|
||||||
}, [isLogsExplorerPage, onExitLiveLogs]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!customDateTimeVisible) {
|
|
||||||
const customTimeRanges = getCustomTimeRanges();
|
|
||||||
|
|
||||||
const formattedCustomTimeRanges: RecentlyUsedDateTimeRange[] = customTimeRanges.map(
|
|
||||||
(range) => ({
|
|
||||||
label: `${dayjs(range.from)
|
|
||||||
.tz(timezone.value)
|
|
||||||
.format(DATE_TIME_FORMATS.DD_MMM_YYYY_HH_MM_SS)} - ${dayjs(range.to)
|
|
||||||
.tz(timezone.value)
|
|
||||||
.format(DATE_TIME_FORMATS.DD_MMM_YYYY_HH_MM_SS)}`,
|
|
||||||
from: range.from,
|
|
||||||
to: range.to,
|
|
||||||
value: range.timestamp,
|
|
||||||
timestamp: range.timestamp,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
setRecentlyUsedTimeRanges(formattedCustomTimeRanges);
|
|
||||||
}
|
|
||||||
}, [customDateTimeVisible, timezone.value]);
|
|
||||||
|
|
||||||
function getTimeChips(options: Option[]): JSX.Element {
|
function getTimeChips(options: Option[]): JSX.Element {
|
||||||
return (
|
return (
|
||||||
<div className="relative-date-time-section">
|
<div className="relative-date-time-section">
|
||||||
@@ -136,7 +76,6 @@ function CustomTimePickerPopoverContent({
|
|||||||
className="time-btns"
|
className="time-btns"
|
||||||
key={option.label + option.value}
|
key={option.label + option.value}
|
||||||
onClick={(): void => {
|
onClick={(): void => {
|
||||||
handleExitLiveLogs();
|
|
||||||
onSelectHandler(option.label, option.value);
|
onSelectHandler(option.label, option.value);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -170,87 +109,53 @@ function CustomTimePickerPopoverContent({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleGoLive = (): void => {
|
|
||||||
onGoLive();
|
|
||||||
setIsOpen(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="date-time-popover">
|
<div className="date-time-popover">
|
||||||
{!customDateTimeVisible && (
|
<div className="date-time-options">
|
||||||
<div className="date-time-options">
|
{isLogsExplorerPage && (
|
||||||
{isLogsExplorerPage && isLogsListView && (
|
<Button className="data-time-live" type="text" onClick={handleGoLive}>
|
||||||
<Button className="data-time-live" type="text" onClick={handleGoLive}>
|
Live
|
||||||
Live
|
</Button>
|
||||||
</Button>
|
)}
|
||||||
)}
|
{options.map((option) => (
|
||||||
{options.map((option) => (
|
<Button
|
||||||
<Button
|
type="text"
|
||||||
type="text"
|
key={option.label + option.value}
|
||||||
key={option.label + option.value}
|
onClick={(): void => {
|
||||||
onClick={(): void => {
|
onSelectHandler(option.label, option.value);
|
||||||
handleExitLiveLogs();
|
}}
|
||||||
onSelectHandler(option.label, option.value);
|
className={cx(
|
||||||
}}
|
'date-time-options-btn',
|
||||||
className={cx(
|
customDateTimeVisible
|
||||||
'date-time-options-btn',
|
? option.value === 'custom' && 'active'
|
||||||
customDateTimeVisible
|
: selectedTime === option.value && 'active',
|
||||||
? option.value === 'custom' && 'active'
|
)}
|
||||||
: selectedTime === option.value && 'active',
|
>
|
||||||
)}
|
{option.label}
|
||||||
>
|
</Button>
|
||||||
{option.label}
|
))}
|
||||||
</Button>
|
</div>
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div
|
<div
|
||||||
className={cx(
|
className={cx(
|
||||||
'relative-date-time',
|
'relative-date-time',
|
||||||
customDateTimeVisible ? 'date-picker' : 'relative-times',
|
selectedTime === 'custom' || customDateTimeVisible
|
||||||
|
? 'date-picker'
|
||||||
|
: 'relative-times',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{customDateTimeVisible ? (
|
{selectedTime === 'custom' || customDateTimeVisible ? (
|
||||||
<DatePickerV2
|
<RangePickerModal
|
||||||
onSetCustomDTPickerVisible={setCustomDTPickerVisible}
|
setCustomDTPickerVisible={setCustomDTPickerVisible}
|
||||||
setIsOpen={setIsOpen}
|
setIsOpen={setIsOpen}
|
||||||
onCustomDateHandler={onCustomDateHandler}
|
onCustomDateHandler={onCustomDateHandler}
|
||||||
|
selectedTime={selectedTime}
|
||||||
|
onTimeChange={onTimeChange}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div className="time-selector-container">
|
<div className="relative-times-container">
|
||||||
<div className="relative-times-container">
|
<div className="time-heading">RELATIVE TIMES</div>
|
||||||
<div className="time-heading">RELATIVE TIMES</div>
|
<div>{getTimeChips(RelativeDurationSuggestionOptions)}</div>
|
||||||
<div>{getTimeChips(RelativeDurationSuggestionOptions)}</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="recently-used-container">
|
|
||||||
<div className="time-heading">RECENTLY USED</div>
|
|
||||||
<div className="recently-used-range">
|
|
||||||
{recentlyUsedTimeRanges.map((range: RecentlyUsedDateTimeRange) => (
|
|
||||||
<div
|
|
||||||
className="recently-used-range-item"
|
|
||||||
role="button"
|
|
||||||
tabIndex={0}
|
|
||||||
onKeyDown={(e): void => {
|
|
||||||
if (e.key === 'Enter' || e.key === ' ') {
|
|
||||||
handleExitLiveLogs();
|
|
||||||
onCustomDateHandler([dayjs(range.from), dayjs(range.to)]);
|
|
||||||
setIsOpen(false);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
key={range.value}
|
|
||||||
onClick={(): void => {
|
|
||||||
handleExitLiveLogs();
|
|
||||||
onCustomDateHandler([dayjs(range.from), dayjs(range.to)]);
|
|
||||||
setIsOpen(false);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{range.label}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -284,4 +189,8 @@ function CustomTimePickerPopoverContent({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
CustomTimePickerPopoverContent.defaultProps = {
|
||||||
|
onTimeChange: undefined,
|
||||||
|
};
|
||||||
|
|
||||||
export default CustomTimePickerPopoverContent;
|
export default CustomTimePickerPopoverContent;
|
||||||
|
|||||||
@@ -1,114 +0,0 @@
|
|||||||
.date-picker-v2-container {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
}
|
|
||||||
|
|
||||||
.custom-date-time-picker-v2 {
|
|
||||||
padding: 12px;
|
|
||||||
|
|
||||||
.periscope-calendar {
|
|
||||||
border-radius: 4px;
|
|
||||||
border: none !important;
|
|
||||||
background: none !important;
|
|
||||||
padding: 8px 0 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.periscope-calendar-day {
|
|
||||||
background: none !important;
|
|
||||||
|
|
||||||
&.periscope-calendar-today {
|
|
||||||
&.text-accent-foreground {
|
|
||||||
color: var(--bg-vanilla-100) !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
button {
|
|
||||||
&:hover {
|
|
||||||
background-color: var(--bg-robin-500) !important;
|
|
||||||
color: var(--bg-vanilla-100) !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.custom-time-selector {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
gap: 16px;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
|
|
||||||
.time-input {
|
|
||||||
border-radius: 4px;
|
|
||||||
border: none !important;
|
|
||||||
background: none !important;
|
|
||||||
padding: 8px 4px !important;
|
|
||||||
color: var(--bg-vanilla-100) !important;
|
|
||||||
|
|
||||||
&::-webkit-calendar-picker-indicator {
|
|
||||||
display: none !important;
|
|
||||||
-webkit-appearance: none;
|
|
||||||
appearance: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:focus {
|
|
||||||
border: none !important;
|
|
||||||
outline: none !important;
|
|
||||||
box-shadow: none !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:focus-visible {
|
|
||||||
border: none !important;
|
|
||||||
outline: none !important;
|
|
||||||
box-shadow: none !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.custom-date-time-picker-footer {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
gap: 8px;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: flex-end;
|
|
||||||
margin-top: 16px;
|
|
||||||
|
|
||||||
.next-btn {
|
|
||||||
width: 80px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.clear-btn {
|
|
||||||
width: 80px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.invalid-date-range-tooltip {
|
|
||||||
.ant-tooltip-inner {
|
|
||||||
color: var(--bg-sakura-500) !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.lightMode {
|
|
||||||
.custom-date-time-picker-v2 {
|
|
||||||
.periscope-calendar-day {
|
|
||||||
&.periscope-calendar-today {
|
|
||||||
&.text-accent-foreground {
|
|
||||||
color: var(--bg-ink-500) !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
button {
|
|
||||||
&:hover {
|
|
||||||
background-color: var(--bg-robin-500) !important;
|
|
||||||
color: var(--bg-ink-500) !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.custom-time-selector {
|
|
||||||
.time-input {
|
|
||||||
color: var(--bg-ink-500) !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,311 +0,0 @@
|
|||||||
import './DatePickerV2.styles.scss';
|
|
||||||
|
|
||||||
import { Calendar } from '@signozhq/calendar';
|
|
||||||
import { Input } from '@signozhq/input';
|
|
||||||
import { Button, Tooltip } from 'antd';
|
|
||||||
import cx from 'classnames';
|
|
||||||
import { DateTimeRangeType } from 'container/TopNav/CustomDateTimeModal';
|
|
||||||
import { LexicalContext } from 'container/TopNav/DateTimeSelectionV2/config';
|
|
||||||
import dayjs, { Dayjs } from 'dayjs';
|
|
||||||
import { CornerUpLeft, MoveRight } from 'lucide-react';
|
|
||||||
import { useTimezone } from 'providers/Timezone';
|
|
||||||
import { useRef, useState } from 'react';
|
|
||||||
import { useSelector } from 'react-redux';
|
|
||||||
import { AppState } from 'store/reducers';
|
|
||||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
|
||||||
import { addCustomTimeRange } from 'utils/customTimeRangeUtils';
|
|
||||||
|
|
||||||
function DatePickerV2({
|
|
||||||
onSetCustomDTPickerVisible,
|
|
||||||
setIsOpen,
|
|
||||||
onCustomDateHandler,
|
|
||||||
}: {
|
|
||||||
onSetCustomDTPickerVisible: (visible: boolean) => void;
|
|
||||||
setIsOpen: (isOpen: boolean) => void;
|
|
||||||
onCustomDateHandler: (
|
|
||||||
dateTimeRange: DateTimeRangeType,
|
|
||||||
lexicalContext?: LexicalContext,
|
|
||||||
) => void;
|
|
||||||
}): JSX.Element {
|
|
||||||
const { maxTime, minTime } = useSelector<AppState, GlobalReducer>(
|
|
||||||
(state) => state.globalTime,
|
|
||||||
);
|
|
||||||
|
|
||||||
const timeInputRef = useRef<HTMLInputElement>(null);
|
|
||||||
|
|
||||||
const { timezone } = useTimezone();
|
|
||||||
|
|
||||||
const [selectedDateTimeFor, setSelectedDateTimeFor] = useState<'to' | 'from'>(
|
|
||||||
'from',
|
|
||||||
);
|
|
||||||
|
|
||||||
const [selectedFromDateTime, setSelectedFromDateTime] = useState<Dayjs | null>(
|
|
||||||
dayjs(minTime / 1000_000).tz(timezone.value),
|
|
||||||
);
|
|
||||||
|
|
||||||
const [selectedToDateTime, setSelectedToDateTime] = useState<Dayjs | null>(
|
|
||||||
dayjs(maxTime / 1000_000).tz(timezone.value),
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleNext = (): void => {
|
|
||||||
if (selectedDateTimeFor === 'to') {
|
|
||||||
onCustomDateHandler([selectedFromDateTime, selectedToDateTime]);
|
|
||||||
|
|
||||||
addCustomTimeRange([selectedFromDateTime, selectedToDateTime]);
|
|
||||||
|
|
||||||
setIsOpen(false);
|
|
||||||
onSetCustomDTPickerVisible(false);
|
|
||||||
setSelectedDateTimeFor('from');
|
|
||||||
} else {
|
|
||||||
setSelectedDateTimeFor('to');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDateChange = (date: Date | undefined): void => {
|
|
||||||
if (!date) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (selectedDateTimeFor === 'from') {
|
|
||||||
const prevFromDateTime = selectedFromDateTime;
|
|
||||||
|
|
||||||
const newDate = dayjs(date);
|
|
||||||
|
|
||||||
const updatedFromDateTime = prevFromDateTime
|
|
||||||
? prevFromDateTime
|
|
||||||
.year(newDate.year())
|
|
||||||
.month(newDate.month())
|
|
||||||
.date(newDate.date())
|
|
||||||
: dayjs(date).tz(timezone.value);
|
|
||||||
|
|
||||||
setSelectedFromDateTime(updatedFromDateTime);
|
|
||||||
} else {
|
|
||||||
// eslint-disable-next-line sonarjs/no-identical-functions
|
|
||||||
setSelectedToDateTime((prev) => {
|
|
||||||
const newDate = dayjs(date);
|
|
||||||
|
|
||||||
// Update only the date part, keeping time from existing state
|
|
||||||
return prev
|
|
||||||
? prev.year(newDate.year()).month(newDate.month()).date(newDate.date())
|
|
||||||
: dayjs(date).tz(timezone.value);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// focus the time input
|
|
||||||
timeInputRef?.current?.focus();
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleTimeChange = (time: string): void => {
|
|
||||||
// time should have format HH:mm:ss
|
|
||||||
if (!/^\d{2}:\d{2}:\d{2}$/.test(time)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (selectedDateTimeFor === 'from') {
|
|
||||||
setSelectedFromDateTime((prev) => {
|
|
||||||
if (prev) {
|
|
||||||
return prev
|
|
||||||
.set('hour', parseInt(time.split(':')[0], 10))
|
|
||||||
.set('minute', parseInt(time.split(':')[1], 10))
|
|
||||||
.set('second', parseInt(time.split(':')[2], 10));
|
|
||||||
}
|
|
||||||
|
|
||||||
return prev;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (selectedDateTimeFor === 'to') {
|
|
||||||
// eslint-disable-next-line sonarjs/no-identical-functions
|
|
||||||
setSelectedToDateTime((prev) => {
|
|
||||||
if (prev) {
|
|
||||||
return prev
|
|
||||||
.set('hour', parseInt(time.split(':')[0], 10))
|
|
||||||
.set('minute', parseInt(time.split(':')[1], 10))
|
|
||||||
.set('second', parseInt(time.split(':')[2], 10));
|
|
||||||
}
|
|
||||||
|
|
||||||
return prev;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const getDefaultMonth = (): Date => {
|
|
||||||
let defaultDate = null;
|
|
||||||
|
|
||||||
if (selectedDateTimeFor === 'from') {
|
|
||||||
defaultDate = selectedFromDateTime?.toDate();
|
|
||||||
} else if (selectedDateTimeFor === 'to') {
|
|
||||||
defaultDate = selectedToDateTime?.toDate();
|
|
||||||
}
|
|
||||||
|
|
||||||
return defaultDate ?? new Date();
|
|
||||||
};
|
|
||||||
|
|
||||||
const isValidRange = (): boolean => {
|
|
||||||
if (selectedDateTimeFor === 'to') {
|
|
||||||
return selectedToDateTime?.isAfter(selectedFromDateTime) ?? false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleBack = (): void => {
|
|
||||||
setSelectedDateTimeFor('from');
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleHideCustomDTPicker = (): void => {
|
|
||||||
onSetCustomDTPickerVisible(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSelectDateTimeFor = (selectedDateTimeFor: 'to' | 'from'): void => {
|
|
||||||
setSelectedDateTimeFor(selectedDateTimeFor);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="date-picker-v2-container">
|
|
||||||
<div className="date-time-custom-options-container">
|
|
||||||
<div
|
|
||||||
className="back-btn"
|
|
||||||
onClick={handleHideCustomDTPicker}
|
|
||||||
role="button"
|
|
||||||
tabIndex={0}
|
|
||||||
onKeyDown={(e): void => {
|
|
||||||
if (e.key === 'Enter') {
|
|
||||||
handleHideCustomDTPicker();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<CornerUpLeft size={16} />
|
|
||||||
<span>Back</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="date-time-custom-options">
|
|
||||||
<div
|
|
||||||
role="button"
|
|
||||||
tabIndex={0}
|
|
||||||
onKeyDown={(e): void => {
|
|
||||||
if (e.key === 'Enter') {
|
|
||||||
handleSelectDateTimeFor('from');
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
className={cx(
|
|
||||||
'date-time-custom-option-from',
|
|
||||||
selectedDateTimeFor === 'from' && 'active',
|
|
||||||
)}
|
|
||||||
onClick={(): void => {
|
|
||||||
handleSelectDateTimeFor('from');
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div className="date-time-custom-option-from-title">FROM</div>
|
|
||||||
<div className="date-time-custom-option-from-value">
|
|
||||||
{selectedFromDateTime?.format('YYYY-MM-DD HH:mm:ss')}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
role="button"
|
|
||||||
tabIndex={0}
|
|
||||||
onKeyDown={(e): void => {
|
|
||||||
if (e.key === 'Enter') {
|
|
||||||
handleSelectDateTimeFor('to');
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
className={cx(
|
|
||||||
'date-time-custom-option-to',
|
|
||||||
selectedDateTimeFor === 'to' && 'active',
|
|
||||||
)}
|
|
||||||
onClick={(): void => {
|
|
||||||
handleSelectDateTimeFor('to');
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div className="date-time-custom-option-to-title">TO</div>
|
|
||||||
<div className="date-time-custom-option-to-value">
|
|
||||||
{selectedToDateTime?.format('YYYY-MM-DD HH:mm:ss')}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="custom-date-time-picker-v2">
|
|
||||||
<Calendar
|
|
||||||
mode="single"
|
|
||||||
required
|
|
||||||
selected={
|
|
||||||
selectedDateTimeFor === 'from'
|
|
||||||
? selectedFromDateTime?.toDate()
|
|
||||||
: selectedToDateTime?.toDate()
|
|
||||||
}
|
|
||||||
key={selectedDateTimeFor + selectedDateTimeFor}
|
|
||||||
onSelect={handleDateChange}
|
|
||||||
defaultMonth={getDefaultMonth()}
|
|
||||||
disabled={(current): boolean => {
|
|
||||||
if (selectedDateTimeFor === 'to') {
|
|
||||||
// disable dates after today and before selectedFromDateTime
|
|
||||||
const currentDay = dayjs(current);
|
|
||||||
return currentDay.isAfter(dayjs()) || false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (selectedDateTimeFor === 'from') {
|
|
||||||
// disable dates after selectedToDateTime
|
|
||||||
|
|
||||||
return dayjs(current).isAfter(dayjs()) || false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}}
|
|
||||||
className="rounded-md border"
|
|
||||||
navLayout="after"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div className="custom-time-selector">
|
|
||||||
<label className="text-xs font-normal block" htmlFor="time-picker">
|
|
||||||
Timestamp
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<MoveRight size={16} />
|
|
||||||
|
|
||||||
<div className="time-input-container">
|
|
||||||
<Input
|
|
||||||
type="time"
|
|
||||||
ref={timeInputRef}
|
|
||||||
className="time-input"
|
|
||||||
value={
|
|
||||||
selectedDateTimeFor === 'from'
|
|
||||||
? selectedFromDateTime?.format('HH:mm:ss')
|
|
||||||
: selectedToDateTime?.format('HH:mm:ss')
|
|
||||||
}
|
|
||||||
onChange={(e): void => handleTimeChange(e.target.value)}
|
|
||||||
step="1"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="custom-date-time-picker-footer">
|
|
||||||
{selectedDateTimeFor === 'to' && (
|
|
||||||
<Button
|
|
||||||
className="periscope-btn secondary clear-btn"
|
|
||||||
type="default"
|
|
||||||
onClick={handleBack}
|
|
||||||
>
|
|
||||||
Back
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
<Tooltip
|
|
||||||
title={
|
|
||||||
!isValidRange() ? 'Invalid range: TO date should be after FROM date' : ''
|
|
||||||
}
|
|
||||||
overlayClassName="invalid-date-range-tooltip"
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
className="periscope-btn primary next-btn"
|
|
||||||
type="primary"
|
|
||||||
onClick={handleNext}
|
|
||||||
disabled={!isValidRange()}
|
|
||||||
>
|
|
||||||
{selectedDateTimeFor === 'from' ? 'Next' : 'Apply'}
|
|
||||||
</Button>
|
|
||||||
</Tooltip>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default DatePickerV2;
|
|
||||||
@@ -55,31 +55,37 @@ export const selectedColumns: BaseAutocompleteData[] = [
|
|||||||
key: 'timestamp',
|
key: 'timestamp',
|
||||||
dataType: DataTypes.String,
|
dataType: DataTypes.String,
|
||||||
type: 'tag',
|
type: 'tag',
|
||||||
|
isColumn: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'serviceName',
|
key: 'serviceName',
|
||||||
dataType: DataTypes.String,
|
dataType: DataTypes.String,
|
||||||
type: 'tag',
|
type: 'tag',
|
||||||
|
isColumn: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'name',
|
key: 'name',
|
||||||
dataType: DataTypes.String,
|
dataType: DataTypes.String,
|
||||||
type: 'tag',
|
type: 'tag',
|
||||||
|
isColumn: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'durationNano',
|
key: 'durationNano',
|
||||||
dataType: DataTypes.Float64,
|
dataType: DataTypes.Float64,
|
||||||
type: 'tag',
|
type: 'tag',
|
||||||
|
isColumn: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'httpMethod',
|
key: 'httpMethod',
|
||||||
dataType: DataTypes.String,
|
dataType: DataTypes.String,
|
||||||
type: 'tag',
|
type: 'tag',
|
||||||
|
isColumn: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'responseStatusCode',
|
key: 'responseStatusCode',
|
||||||
dataType: DataTypes.String,
|
dataType: DataTypes.String,
|
||||||
type: 'tag',
|
type: 'tag',
|
||||||
|
isColumn: true,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -102,7 +108,9 @@ export const getHostTracesQueryPayload = (
|
|||||||
id: '------false',
|
id: '------false',
|
||||||
dataType: DataTypes.EMPTY,
|
dataType: DataTypes.EMPTY,
|
||||||
key: '',
|
key: '',
|
||||||
|
isColumn: false,
|
||||||
type: '',
|
type: '',
|
||||||
|
isJSON: false,
|
||||||
},
|
},
|
||||||
timeAggregation: 'rate',
|
timeAggregation: 'rate',
|
||||||
spaceAggregation: 'sum',
|
spaceAggregation: 'sum',
|
||||||
@@ -125,7 +133,6 @@ export const getHostTracesQueryPayload = (
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
queryFormulas: [],
|
queryFormulas: [],
|
||||||
queryTraceOperator: [],
|
|
||||||
},
|
},
|
||||||
id: '572f1d91-6ac0-46c0-b726-c21488b34434',
|
id: '572f1d91-6ac0-46c0-b726-c21488b34434',
|
||||||
queryType: EQueryType.QUERY_BUILDER,
|
queryType: EQueryType.QUERY_BUILDER,
|
||||||
@@ -147,6 +154,8 @@ export const getHostTracesQueryPayload = (
|
|||||||
key: 'serviceName',
|
key: 'serviceName',
|
||||||
dataType: 'string',
|
dataType: 'string',
|
||||||
type: 'tag',
|
type: 'tag',
|
||||||
|
isColumn: true,
|
||||||
|
isJSON: false,
|
||||||
id: 'serviceName--string--tag--true',
|
id: 'serviceName--string--tag--true',
|
||||||
isIndexed: false,
|
isIndexed: false,
|
||||||
},
|
},
|
||||||
@@ -154,6 +163,8 @@ export const getHostTracesQueryPayload = (
|
|||||||
key: 'name',
|
key: 'name',
|
||||||
dataType: 'string',
|
dataType: 'string',
|
||||||
type: 'tag',
|
type: 'tag',
|
||||||
|
isColumn: true,
|
||||||
|
isJSON: false,
|
||||||
id: 'name--string--tag--true',
|
id: 'name--string--tag--true',
|
||||||
isIndexed: false,
|
isIndexed: false,
|
||||||
},
|
},
|
||||||
@@ -161,6 +172,8 @@ export const getHostTracesQueryPayload = (
|
|||||||
key: 'durationNano',
|
key: 'durationNano',
|
||||||
dataType: 'float64',
|
dataType: 'float64',
|
||||||
type: 'tag',
|
type: 'tag',
|
||||||
|
isColumn: true,
|
||||||
|
isJSON: false,
|
||||||
id: 'durationNano--float64--tag--true',
|
id: 'durationNano--float64--tag--true',
|
||||||
isIndexed: false,
|
isIndexed: false,
|
||||||
},
|
},
|
||||||
@@ -168,6 +181,8 @@ export const getHostTracesQueryPayload = (
|
|||||||
key: 'httpMethod',
|
key: 'httpMethod',
|
||||||
dataType: 'string',
|
dataType: 'string',
|
||||||
type: 'tag',
|
type: 'tag',
|
||||||
|
isColumn: true,
|
||||||
|
isJSON: false,
|
||||||
id: 'httpMethod--string--tag--true',
|
id: 'httpMethod--string--tag--true',
|
||||||
isIndexed: false,
|
isIndexed: false,
|
||||||
},
|
},
|
||||||
@@ -175,6 +190,8 @@ export const getHostTracesQueryPayload = (
|
|||||||
key: 'responseStatusCode',
|
key: 'responseStatusCode',
|
||||||
dataType: 'string',
|
dataType: 'string',
|
||||||
type: 'tag',
|
type: 'tag',
|
||||||
|
isColumn: true,
|
||||||
|
isJSON: false,
|
||||||
id: 'responseStatusCode--string--tag--true',
|
id: 'responseStatusCode--string--tag--true',
|
||||||
isIndexed: false,
|
isIndexed: false,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -169,7 +169,6 @@
|
|||||||
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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -119,6 +119,8 @@ function HostMetricsDetails({
|
|||||||
key: 'host.name',
|
key: 'host.name',
|
||||||
dataType: DataTypes.String,
|
dataType: DataTypes.String,
|
||||||
type: 'resource',
|
type: 'resource',
|
||||||
|
isColumn: false,
|
||||||
|
isJSON: false,
|
||||||
id: 'host.name--string--resource--false',
|
id: 'host.name--string--resource--false',
|
||||||
},
|
},
|
||||||
op: '=',
|
op: '=',
|
||||||
|
|||||||
@@ -26,7 +26,9 @@ export const getHostLogsQueryPayload = (
|
|||||||
id: '------false',
|
id: '------false',
|
||||||
dataType: DataTypes.String,
|
dataType: DataTypes.String,
|
||||||
key: '',
|
key: '',
|
||||||
|
isColumn: false,
|
||||||
type: '',
|
type: '',
|
||||||
|
isJSON: false,
|
||||||
},
|
},
|
||||||
timeAggregation: 'rate',
|
timeAggregation: 'rate',
|
||||||
spaceAggregation: 'sum',
|
spaceAggregation: 'sum',
|
||||||
@@ -51,7 +53,6 @@ export const getHostLogsQueryPayload = (
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
queryFormulas: [],
|
queryFormulas: [],
|
||||||
queryTraceOperator: [],
|
|
||||||
},
|
},
|
||||||
id: uuidv4(),
|
id: uuidv4(),
|
||||||
queryType: EQueryType.QUERY_BUILDER,
|
queryType: EQueryType.QUERY_BUILDER,
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ function Metrics({
|
|||||||
signal,
|
signal,
|
||||||
}: QueryFunctionContext): Promise<
|
}: QueryFunctionContext): Promise<
|
||||||
SuccessResponse<MetricRangePayloadProps>
|
SuccessResponse<MetricRangePayloadProps>
|
||||||
> => GetMetricQueryRange(payload, ENTITY_VERSION_V4, undefined, signal),
|
> => GetMetricQueryRange(payload, ENTITY_VERSION_V4, signal),
|
||||||
enabled: !!payload && visibilities[index],
|
enabled: !!payload && visibilities[index],
|
||||||
keepPreviousData: true,
|
keepPreviousData: true,
|
||||||
})),
|
})),
|
||||||
|
|||||||
@@ -1,50 +0,0 @@
|
|||||||
import { Badge } from '@signozhq/badge';
|
|
||||||
|
|
||||||
type BadgeColor =
|
|
||||||
| 'vanilla'
|
|
||||||
| 'robin'
|
|
||||||
| 'forest'
|
|
||||||
| 'amber'
|
|
||||||
| 'sienna'
|
|
||||||
| 'cherry'
|
|
||||||
| 'sakura'
|
|
||||||
| 'aqua';
|
|
||||||
|
|
||||||
interface HttpStatusBadgeProps {
|
|
||||||
statusCode: string | number;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getStatusCodeColor(statusCode: number): BadgeColor {
|
|
||||||
if (statusCode >= 200 && statusCode < 300) {
|
|
||||||
return 'forest'; // Success - green
|
|
||||||
}
|
|
||||||
if (statusCode >= 300 && statusCode < 400) {
|
|
||||||
return 'robin'; // Redirect - blue
|
|
||||||
}
|
|
||||||
if (statusCode >= 400 && statusCode < 500) {
|
|
||||||
return 'amber'; // Client error - amber
|
|
||||||
}
|
|
||||||
if (statusCode >= 500) {
|
|
||||||
return 'cherry'; // Server error - red
|
|
||||||
}
|
|
||||||
if (statusCode >= 100 && statusCode < 200) {
|
|
||||||
return 'vanilla'; // Informational - neutral
|
|
||||||
}
|
|
||||||
return 'robin'; // Default fallback
|
|
||||||
}
|
|
||||||
|
|
||||||
function HttpStatusBadge({
|
|
||||||
statusCode,
|
|
||||||
}: HttpStatusBadgeProps): JSX.Element | null {
|
|
||||||
const numericStatusCode = Number(statusCode);
|
|
||||||
|
|
||||||
if (!numericStatusCode || numericStatusCode <= 0) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const color = getStatusCodeColor(numericStatusCode);
|
|
||||||
|
|
||||||
return <Badge color={color}>{statusCode}</Badge>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default HttpStatusBadge;
|
|
||||||
@@ -17,7 +17,7 @@ function InputWithLabel({
|
|||||||
closeIcon,
|
closeIcon,
|
||||||
}: {
|
}: {
|
||||||
label: string;
|
label: string;
|
||||||
initialValue?: string | number | null;
|
initialValue?: string | number;
|
||||||
placeholder: string;
|
placeholder: string;
|
||||||
type?: string;
|
type?: string;
|
||||||
onClose?: () => void;
|
onClose?: () => void;
|
||||||
|
|||||||
@@ -1,152 +0,0 @@
|
|||||||
.kbar-command-palette__positioner {
|
|
||||||
position: fixed;
|
|
||||||
inset: 0;
|
|
||||||
display: flex;
|
|
||||||
align-items: flex-start;
|
|
||||||
justify-content: center;
|
|
||||||
padding: 1rem;
|
|
||||||
background: rgba(0, 0, 0, 0.5);
|
|
||||||
backdrop-filter: blur(6px);
|
|
||||||
z-index: 50;
|
|
||||||
}
|
|
||||||
|
|
||||||
.kbar-command-palette__animator {
|
|
||||||
width: 100%;
|
|
||||||
max-width: 600px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.kbar-command-palette__card {
|
|
||||||
background: var(--bg-ink-500);
|
|
||||||
color: var(--text-vanilla-100);
|
|
||||||
border-radius: 3px;
|
|
||||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.15);
|
|
||||||
overflow: hidden;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.kbar-command-palette__search {
|
|
||||||
padding: 12px 16px;
|
|
||||||
font-size: 13px;
|
|
||||||
border: none;
|
|
||||||
border-bottom: 1px solid var(--border-ink-200);
|
|
||||||
color: var(--text-vanilla-100);
|
|
||||||
outline: none;
|
|
||||||
background-color: var(--bg-ink-500);
|
|
||||||
}
|
|
||||||
|
|
||||||
.kbar-command-palette__section {
|
|
||||||
padding: 8px 16px 4px;
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: var(--text-robin-500);
|
|
||||||
font-family: 'Inter', sans-serif;
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 0.02em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.kbar-command-palette__item {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
padding: 10px 16px;
|
|
||||||
font-size: 13px;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: background 0.15s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.kbar-command-palette__item:hover,
|
|
||||||
.kbar-command-palette__item--active {
|
|
||||||
background: var(--bg-ink-400);
|
|
||||||
}
|
|
||||||
|
|
||||||
.kbar-command-palette__icon {
|
|
||||||
flex-shrink: 0;
|
|
||||||
width: 18px;
|
|
||||||
height: 18px;
|
|
||||||
color: #444;
|
|
||||||
}
|
|
||||||
|
|
||||||
.kbar-command-palette__shortcut {
|
|
||||||
margin-left: auto;
|
|
||||||
display: flex;
|
|
||||||
gap: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.kbar-command-palette__key {
|
|
||||||
padding: 2px 6px;
|
|
||||||
font-size: 12px;
|
|
||||||
border-radius: 4px;
|
|
||||||
background: var(--bg-ink-300);
|
|
||||||
color: var(--text-vanilla-300);
|
|
||||||
text-transform: uppercase;
|
|
||||||
font-family: 'Space Mono', monospace;
|
|
||||||
}
|
|
||||||
|
|
||||||
.kbar-command-palette__results-container {
|
|
||||||
div {
|
|
||||||
&::-webkit-scrollbar {
|
|
||||||
width: 0.3rem;
|
|
||||||
height: 0.3rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
&::-webkit-scrollbar-track {
|
|
||||||
background: transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
&::-webkit-scrollbar-thumb {
|
|
||||||
background: var(--bg-slate-300);
|
|
||||||
}
|
|
||||||
|
|
||||||
&::-webkit-scrollbar-thumb:hover {
|
|
||||||
background: var(--bg-slate-200);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.lightMode {
|
|
||||||
.kbar-command-palette__positioner {
|
|
||||||
background: rgba(0, 0, 0, 0.5);
|
|
||||||
}
|
|
||||||
|
|
||||||
.kbar-command-palette__card {
|
|
||||||
background: #fff;
|
|
||||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.15);
|
|
||||||
}
|
|
||||||
|
|
||||||
.kbar-command-palette__search {
|
|
||||||
border-bottom: 1px solid #e5e5e5;
|
|
||||||
color: var(--text-ink-500);
|
|
||||||
background-color: var(--bg-vanilla-100);
|
|
||||||
}
|
|
||||||
|
|
||||||
.kbar-command-palette__item {
|
|
||||||
color: var(--text-ink-500);
|
|
||||||
}
|
|
||||||
|
|
||||||
.kbar-command-palette__item:hover,
|
|
||||||
.kbar-command-palette__item--active {
|
|
||||||
background: #f5f5f5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.kbar-command-palette__icon {
|
|
||||||
color: #444;
|
|
||||||
}
|
|
||||||
|
|
||||||
.kbar-command-palette__key {
|
|
||||||
background: #eee;
|
|
||||||
color: #555;
|
|
||||||
}
|
|
||||||
|
|
||||||
.kbar-command-palette__results-container {
|
|
||||||
div {
|
|
||||||
&::-webkit-scrollbar-thumb {
|
|
||||||
background: var(--bg-vanilla-300);
|
|
||||||
}
|
|
||||||
|
|
||||||
&::-webkit-scrollbar-thumb:hover {
|
|
||||||
background: var(--bg-vanilla-300);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
import './KBarCommandPalette.scss';
|
|
||||||
|
|
||||||
import {
|
|
||||||
KBarAnimator,
|
|
||||||
KBarPortal,
|
|
||||||
KBarPositioner,
|
|
||||||
KBarResults,
|
|
||||||
KBarSearch,
|
|
||||||
useMatches,
|
|
||||||
} from 'kbar';
|
|
||||||
|
|
||||||
function Results(): JSX.Element {
|
|
||||||
const { results } = useMatches();
|
|
||||||
|
|
||||||
const renderResults = ({
|
|
||||||
item,
|
|
||||||
active,
|
|
||||||
}: {
|
|
||||||
item: any;
|
|
||||||
active: boolean;
|
|
||||||
}): JSX.Element =>
|
|
||||||
typeof item === 'string' ? (
|
|
||||||
<div className="kbar-command-palette__section">{item}</div>
|
|
||||||
) : (
|
|
||||||
<div
|
|
||||||
className={`kbar-command-palette__item ${
|
|
||||||
active ? 'kbar-command-palette__item--active' : ''
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{item.icon}
|
|
||||||
<span>{item.name}</span>
|
|
||||||
{item.shortcut?.length ? (
|
|
||||||
<span className="kbar-command-palette__shortcut">
|
|
||||||
{item.shortcut.map((sc: string) => (
|
|
||||||
<kbd key={sc} className="kbar-command-palette__key">
|
|
||||||
{sc}
|
|
||||||
</kbd>
|
|
||||||
))}
|
|
||||||
</span>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="kbar-command-palette__results-container">
|
|
||||||
<KBarResults items={results} onRender={renderResults} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function KBarCommandPalette(): JSX.Element {
|
|
||||||
return (
|
|
||||||
<KBarPortal>
|
|
||||||
<KBarPositioner className="kbar-command-palette__positioner">
|
|
||||||
<KBarAnimator className="kbar-command-palette__animator">
|
|
||||||
<div className="kbar-command-palette__card">
|
|
||||||
<KBarSearch
|
|
||||||
className="kbar-command-palette__search"
|
|
||||||
placeholder="Search or type a command..."
|
|
||||||
/>
|
|
||||||
<Results />
|
|
||||||
</div>
|
|
||||||
</KBarAnimator>
|
|
||||||
</KBarPositioner>
|
|
||||||
</KBarPortal>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default KBarCommandPalette;
|
|
||||||
@@ -10,7 +10,11 @@ import { VIEWS } from './constants';
|
|||||||
export type LogDetailProps = {
|
export type LogDetailProps = {
|
||||||
log: ILog | null;
|
log: ILog | null;
|
||||||
selectedTab: VIEWS;
|
selectedTab: VIEWS;
|
||||||
onGroupByAttribute?: (fieldKey: string, dataType?: DataTypes) => Promise<void>;
|
onGroupByAttribute?: (
|
||||||
|
fieldKey: string,
|
||||||
|
isJSON?: boolean,
|
||||||
|
dataType?: DataTypes,
|
||||||
|
) => Promise<void>;
|
||||||
isListViewPanel?: boolean;
|
isListViewPanel?: boolean;
|
||||||
listViewPanelSelectedFields?: IField[] | null;
|
listViewPanelSelectedFields?: IField[] | null;
|
||||||
} & Pick<AddToQueryHOCProps, 'onAddToQuery'> &
|
} & Pick<AddToQueryHOCProps, 'onAddToQuery'> &
|
||||||
|
|||||||
@@ -23,7 +23,6 @@ import {
|
|||||||
} from 'container/LogDetailedView/utils';
|
} from 'container/LogDetailedView/utils';
|
||||||
import useInitialQuery from 'container/LogsExplorerContext/useInitialQuery';
|
import useInitialQuery from 'container/LogsExplorerContext/useInitialQuery';
|
||||||
import { useOptionsMenu } from 'container/OptionsMenu';
|
import { useOptionsMenu } from 'container/OptionsMenu';
|
||||||
import { useCopyLogLink } from 'hooks/logs/useCopyLogLink';
|
|
||||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||||
import { useNotifications } from 'hooks/useNotifications';
|
import { useNotifications } from 'hooks/useNotifications';
|
||||||
@@ -40,7 +39,7 @@ import {
|
|||||||
TextSelect,
|
TextSelect,
|
||||||
X,
|
X,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { useCallback, useMemo, useState } from 'react';
|
import { useMemo, useState } from 'react';
|
||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
import { useCopyToClipboard, useLocation } from 'react-use';
|
import { useCopyToClipboard, useLocation } from 'react-use';
|
||||||
import { AppState } from 'store/reducers';
|
import { AppState } from 'store/reducers';
|
||||||
@@ -95,8 +94,6 @@ function LogDetailInner({
|
|||||||
|
|
||||||
const { notifications } = useNotifications();
|
const { notifications } = useNotifications();
|
||||||
|
|
||||||
const { onLogCopy } = useCopyLogLink(log?.id);
|
|
||||||
|
|
||||||
const LogJsonData = log ? aggregateAttributesResourcesToString(log) : '';
|
const LogJsonData = log ? aggregateAttributesResourcesToString(log) : '';
|
||||||
|
|
||||||
const handleModeChange = (e: RadioChangeEvent): void => {
|
const handleModeChange = (e: RadioChangeEvent): void => {
|
||||||
@@ -149,34 +146,6 @@ function LogDetailInner({
|
|||||||
safeNavigate(`${ROUTES.LOGS_EXPLORER}?${createQueryParams(queryParams)}`);
|
safeNavigate(`${ROUTES.LOGS_EXPLORER}?${createQueryParams(queryParams)}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleQueryExpressionChange = useCallback(
|
|
||||||
(value: string, queryIndex: number) => {
|
|
||||||
// update the query at the given index
|
|
||||||
setContextQuery((prev) => {
|
|
||||||
if (!prev) return prev;
|
|
||||||
|
|
||||||
return {
|
|
||||||
...prev,
|
|
||||||
builder: {
|
|
||||||
...prev.builder,
|
|
||||||
queryData: prev.builder.queryData.map((query, idx) =>
|
|
||||||
idx === queryIndex
|
|
||||||
? {
|
|
||||||
...query,
|
|
||||||
filter: {
|
|
||||||
...query.filter,
|
|
||||||
expression: value,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
: query,
|
|
||||||
),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
});
|
|
||||||
},
|
|
||||||
[],
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleRunQuery = (expression: string): void => {
|
const handleRunQuery = (expression: string): void => {
|
||||||
let updatedContextQuery = cloneDeep(contextQuery);
|
let updatedContextQuery = cloneDeep(contextQuery);
|
||||||
|
|
||||||
@@ -336,19 +305,11 @@ function LogDetailInner({
|
|||||||
onClick={handleFilterVisible}
|
onClick={handleFilterVisible}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Tooltip title="Copy Log Link" placement="left" aria-label="Copy Log Link">
|
|
||||||
<Button
|
|
||||||
className="action-btn"
|
|
||||||
icon={<Copy size={16} />}
|
|
||||||
onClick={onLogCopy}
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
|
||||||
</div>
|
</div>
|
||||||
{isFilterVisible && contextQuery?.builder.queryData[0] && (
|
{isFilterVisible && contextQuery?.builder.queryData[0] && (
|
||||||
<div className="log-detail-drawer-query-container">
|
<div className="log-detail-drawer-query-container">
|
||||||
<QuerySearch
|
<QuerySearch
|
||||||
onChange={(value): void => handleQueryExpressionChange(value, 0)}
|
onChange={(): void => {}}
|
||||||
dataSource={DataSource.LOGS}
|
dataSource={DataSource.LOGS}
|
||||||
queryData={contextQuery?.builder.queryData[0]}
|
queryData={contextQuery?.builder.queryData[0]}
|
||||||
onRun={handleRunQuery}
|
onRun={handleRunQuery}
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ function AddToQueryHOC({
|
|||||||
}: 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['='], dataType);
|
onAddToQuery(fieldKey, fieldValue, OPERATORS['='], undefined, dataType);
|
||||||
};
|
};
|
||||||
|
|
||||||
const popOverContent = useMemo(() => <span>Add to query: {fieldKey}</span>, [
|
const popOverContent = useMemo(() => <span>Add to query: {fieldKey}</span>, [
|
||||||
@@ -41,6 +41,7 @@ export interface AddToQueryHOCProps {
|
|||||||
fieldKey: string,
|
fieldKey: string,
|
||||||
fieldValue: string,
|
fieldValue: string,
|
||||||
operator: string,
|
operator: string,
|
||||||
|
isJSON?: boolean,
|
||||||
dataType?: DataTypes,
|
dataType?: DataTypes,
|
||||||
) => void;
|
) => void;
|
||||||
fontSize: FontSize;
|
fontSize: FontSize;
|
||||||
|
|||||||
@@ -56,8 +56,6 @@ export const useTableView = (props: UseTableViewProps): UseTableViewResult => {
|
|||||||
.map(({ name }) => ({
|
.map(({ name }) => ({
|
||||||
title: name,
|
title: name,
|
||||||
dataIndex: name,
|
dataIndex: name,
|
||||||
accessorKey: name,
|
|
||||||
id: name.toLowerCase().replace(/\./g, '_'),
|
|
||||||
key: name,
|
key: name,
|
||||||
render: (field): ColumnTypeRender<Record<string, unknown>> => ({
|
render: (field): ColumnTypeRender<Record<string, unknown>> => ({
|
||||||
props: {
|
props: {
|
||||||
@@ -85,10 +83,7 @@ export const useTableView = (props: UseTableViewProps): UseTableViewResult => {
|
|||||||
// We do not need any title and data index for the log state indicator
|
// We do not need any title and data index for the log state indicator
|
||||||
title: '',
|
title: '',
|
||||||
dataIndex: '',
|
dataIndex: '',
|
||||||
// eslint-disable-next-line sonarjs/no-duplicate-string
|
|
||||||
key: 'state-indicator',
|
key: 'state-indicator',
|
||||||
accessorKey: 'state-indicator',
|
|
||||||
id: 'state-indicator',
|
|
||||||
render: (_, item): ColumnTypeRender<Record<string, unknown>> => ({
|
render: (_, item): ColumnTypeRender<Record<string, unknown>> => ({
|
||||||
children: (
|
children: (
|
||||||
<div className={cx('state-indicator', fontSize)}>
|
<div className={cx('state-indicator', fontSize)}>
|
||||||
@@ -106,8 +101,6 @@ export const useTableView = (props: UseTableViewProps): UseTableViewResult => {
|
|||||||
title: 'timestamp',
|
title: 'timestamp',
|
||||||
dataIndex: 'timestamp',
|
dataIndex: 'timestamp',
|
||||||
key: 'timestamp',
|
key: 'timestamp',
|
||||||
accessorKey: 'timestamp',
|
|
||||||
id: 'timestamp',
|
|
||||||
// https://github.com/ant-design/ant-design/discussions/36886
|
// https://github.com/ant-design/ant-design/discussions/36886
|
||||||
render: (
|
render: (
|
||||||
field: string | number,
|
field: string | number,
|
||||||
@@ -142,8 +135,6 @@ export const useTableView = (props: UseTableViewProps): UseTableViewResult => {
|
|||||||
title: 'body',
|
title: 'body',
|
||||||
dataIndex: 'body',
|
dataIndex: 'body',
|
||||||
key: 'body',
|
key: 'body',
|
||||||
accessorKey: 'body',
|
|
||||||
id: 'body',
|
|
||||||
render: (
|
render: (
|
||||||
field: string | number,
|
field: string | number,
|
||||||
): ColumnTypeRender<Record<string, unknown>> => ({
|
): ColumnTypeRender<Record<string, unknown>> => ({
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
/* eslint-disable jsx-a11y/no-static-element-interactions */
|
|
||||||
/* eslint-disable jsx-a11y/click-events-have-key-events */
|
|
||||||
/* eslint-disable sonarjs/cognitive-complexity */
|
/* eslint-disable sonarjs/cognitive-complexity */
|
||||||
/* eslint-disable react/jsx-props-no-spreading */
|
/* eslint-disable react/jsx-props-no-spreading */
|
||||||
/* eslint-disable no-nested-ternary */
|
/* eslint-disable no-nested-ternary */
|
||||||
@@ -14,11 +12,9 @@ import {
|
|||||||
import { Color } from '@signozhq/design-tokens';
|
import { Color } from '@signozhq/design-tokens';
|
||||||
import { Button, Checkbox, Select, Typography } from 'antd';
|
import { Button, Checkbox, Select, Typography } from 'antd';
|
||||||
import cx from 'classnames';
|
import cx from 'classnames';
|
||||||
import TextToolTip from 'components/TextToolTip/TextToolTip';
|
|
||||||
import { SOMETHING_WENT_WRONG } from 'constants/api';
|
import { SOMETHING_WENT_WRONG } from 'constants/api';
|
||||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
|
||||||
import { capitalize, isEmpty } from 'lodash-es';
|
import { capitalize, isEmpty } from 'lodash-es';
|
||||||
import { ArrowDown, ArrowLeft, ArrowRight, ArrowUp, Info } from 'lucide-react';
|
import { ArrowDown, ArrowLeft, ArrowRight, ArrowUp } from 'lucide-react';
|
||||||
import type { BaseSelectRef } from 'rc-select';
|
import type { BaseSelectRef } from 'rc-select';
|
||||||
import React, {
|
import React, {
|
||||||
useCallback,
|
useCallback,
|
||||||
@@ -27,13 +23,11 @@ import React, {
|
|||||||
useRef,
|
useRef,
|
||||||
useState,
|
useState,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
import { Virtuoso } from 'react-virtuoso';
|
|
||||||
import { popupContainer } from 'utils/selectPopupContainer';
|
import { popupContainer } from 'utils/selectPopupContainer';
|
||||||
|
|
||||||
import { CustomMultiSelectProps, CustomTagProps, OptionData } from './types';
|
import { CustomMultiSelectProps, CustomTagProps, OptionData } from './types';
|
||||||
import {
|
import {
|
||||||
filterOptionsBySearch,
|
filterOptionsBySearch,
|
||||||
handleScrollToBottom,
|
|
||||||
prioritizeOrAddOptionForMultiSelect,
|
prioritizeOrAddOptionForMultiSelect,
|
||||||
SPACEKEY,
|
SPACEKEY,
|
||||||
} from './utils';
|
} from './utils';
|
||||||
@@ -43,7 +37,7 @@ enum ToggleTagValue {
|
|||||||
All = 'All',
|
All = 'All',
|
||||||
}
|
}
|
||||||
|
|
||||||
const ALL_SELECTED_VALUE = '__ALL__'; // Constant for the special value
|
const ALL_SELECTED_VALUE = '__all__'; // Constant for the special value
|
||||||
|
|
||||||
const CustomMultiSelect: React.FC<CustomMultiSelectProps> = ({
|
const CustomMultiSelect: React.FC<CustomMultiSelectProps> = ({
|
||||||
placeholder = 'Search...',
|
placeholder = 'Search...',
|
||||||
@@ -68,12 +62,6 @@ const CustomMultiSelect: React.FC<CustomMultiSelectProps> = ({
|
|||||||
allowClear = false,
|
allowClear = false,
|
||||||
onRetry,
|
onRetry,
|
||||||
maxTagTextLength,
|
maxTagTextLength,
|
||||||
onDropdownVisibleChange,
|
|
||||||
showIncompleteDataMessage = false,
|
|
||||||
showLabels = false,
|
|
||||||
enableRegexOption = false,
|
|
||||||
isDynamicVariable = false,
|
|
||||||
showRetryButton = true,
|
|
||||||
...rest
|
...rest
|
||||||
}) => {
|
}) => {
|
||||||
// ===== State & Refs =====
|
// ===== State & Refs =====
|
||||||
@@ -90,10 +78,6 @@ const CustomMultiSelect: React.FC<CustomMultiSelectProps> = ({
|
|||||||
const optionRefs = useRef<Record<number, HTMLDivElement | null>>({});
|
const optionRefs = useRef<Record<number, HTMLDivElement | null>>({});
|
||||||
const [visibleOptions, setVisibleOptions] = useState<OptionData[]>([]);
|
const [visibleOptions, setVisibleOptions] = useState<OptionData[]>([]);
|
||||||
const isClickInsideDropdownRef = useRef(false);
|
const isClickInsideDropdownRef = useRef(false);
|
||||||
const justOpenedRef = useRef<boolean>(false);
|
|
||||||
const [isScrolledToBottom, setIsScrolledToBottom] = useState(false);
|
|
||||||
|
|
||||||
const isDarkMode = useIsDarkMode();
|
|
||||||
|
|
||||||
// Convert single string value to array for consistency
|
// Convert single string value to array for consistency
|
||||||
const selectedValues = useMemo(
|
const selectedValues = useMemo(
|
||||||
@@ -140,12 +124,6 @@ const CustomMultiSelect: React.FC<CustomMultiSelectProps> = ({
|
|||||||
return allAvailableValues.every((val) => selectedValues.includes(val));
|
return allAvailableValues.every((val) => selectedValues.includes(val));
|
||||||
}, [selectedValues, allAvailableValues, enableAllSelection]);
|
}, [selectedValues, allAvailableValues, enableAllSelection]);
|
||||||
|
|
||||||
// Define allOptionShown earlier in the code
|
|
||||||
const allOptionShown = useMemo(
|
|
||||||
() => value === ALL_SELECTED_VALUE || value === 'ALL',
|
|
||||||
[value],
|
|
||||||
);
|
|
||||||
|
|
||||||
// Value passed to the underlying Ant Select component
|
// Value passed to the underlying Ant Select component
|
||||||
const displayValue = useMemo(
|
const displayValue = useMemo(
|
||||||
() => (isAllSelected ? [ALL_SELECTED_VALUE] : selectedValues),
|
() => (isAllSelected ? [ALL_SELECTED_VALUE] : selectedValues),
|
||||||
@@ -154,18 +132,10 @@ const CustomMultiSelect: React.FC<CustomMultiSelectProps> = ({
|
|||||||
|
|
||||||
// ===== Internal onChange Handler =====
|
// ===== Internal onChange Handler =====
|
||||||
const handleInternalChange = useCallback(
|
const handleInternalChange = useCallback(
|
||||||
(newValue: string | string[], directCaller?: boolean): void => {
|
(newValue: string | string[]): void => {
|
||||||
// Ensure newValue is an array
|
// Ensure newValue is an array
|
||||||
const currentNewValue = Array.isArray(newValue) ? newValue : [];
|
const currentNewValue = Array.isArray(newValue) ? newValue : [];
|
||||||
|
|
||||||
if (
|
|
||||||
(allOptionShown || isAllSelected) &&
|
|
||||||
!directCaller &&
|
|
||||||
currentNewValue.length === 0
|
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!onChange) return;
|
if (!onChange) return;
|
||||||
|
|
||||||
// Case 1: Cleared (empty array or undefined)
|
// Case 1: Cleared (empty array or undefined)
|
||||||
@@ -174,7 +144,7 @@ const CustomMultiSelect: React.FC<CustomMultiSelectProps> = ({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Case 2: "__ALL__" is selected (means select all actual values)
|
// Case 2: "__all__" is selected (means select all actual values)
|
||||||
if (currentNewValue.includes(ALL_SELECTED_VALUE)) {
|
if (currentNewValue.includes(ALL_SELECTED_VALUE)) {
|
||||||
const allActualOptions = allAvailableValues.map(
|
const allActualOptions = allAvailableValues.map(
|
||||||
(v) => options.flat().find((o) => o.value === v) || { label: v, value: v },
|
(v) => options.flat().find((o) => o.value === v) || { label: v, value: v },
|
||||||
@@ -205,14 +175,7 @@ const CustomMultiSelect: React.FC<CustomMultiSelectProps> = ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[
|
[onChange, allAvailableValues, options, enableAllSelection],
|
||||||
allOptionShown,
|
|
||||||
isAllSelected,
|
|
||||||
onChange,
|
|
||||||
allAvailableValues,
|
|
||||||
options,
|
|
||||||
enableAllSelection,
|
|
||||||
],
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// ===== Existing Callbacks (potentially needing adjustment later) =====
|
// ===== Existing Callbacks (potentially needing adjustment later) =====
|
||||||
@@ -309,8 +272,7 @@ const CustomMultiSelect: React.FC<CustomMultiSelectProps> = ({
|
|||||||
: filteredOptions,
|
: filteredOptions,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
}, [filteredOptions, searchText, options, selectedValues]);
|
||||||
}, [filteredOptions, searchText, options]);
|
|
||||||
|
|
||||||
// ===== Text Selection Utilities =====
|
// ===== Text Selection Utilities =====
|
||||||
|
|
||||||
@@ -548,46 +510,13 @@ const CustomMultiSelect: React.FC<CustomMultiSelectProps> = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Normal single value handling
|
// Normal single value handling
|
||||||
const trimmedValue = value.trim();
|
setSearchText(value.trim());
|
||||||
setSearchText(trimmedValue);
|
|
||||||
if (!isOpen) {
|
if (!isOpen) {
|
||||||
setIsOpen(true);
|
setIsOpen(true);
|
||||||
justOpenedRef.current = true;
|
|
||||||
}
|
}
|
||||||
|
if (onSearch) onSearch(value.trim());
|
||||||
// Reset active index when search changes if dropdown is open
|
|
||||||
if (isOpen && trimmedValue) {
|
|
||||||
setActiveIndex(-1);
|
|
||||||
// see if the trimmed value matched any option and set that active index
|
|
||||||
const matchedOption = filteredOptions.find(
|
|
||||||
(option) =>
|
|
||||||
option.label.toLowerCase() === trimmedValue.toLowerCase() ||
|
|
||||||
option.value?.toLowerCase() === trimmedValue.toLowerCase(),
|
|
||||||
);
|
|
||||||
if (matchedOption) {
|
|
||||||
setActiveIndex(1);
|
|
||||||
} else {
|
|
||||||
// check if the trimmed value is a regex pattern and set that active index
|
|
||||||
const isRegex =
|
|
||||||
trimmedValue.startsWith('.*') && trimmedValue.endsWith('.*');
|
|
||||||
if (isRegex && enableRegexOption) {
|
|
||||||
setActiveIndex(0);
|
|
||||||
} else {
|
|
||||||
setActiveIndex(enableRegexOption ? 1 : 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (onSearch) onSearch(trimmedValue);
|
|
||||||
},
|
},
|
||||||
[
|
[onSearch, isOpen, selectedValues, onChange],
|
||||||
onSearch,
|
|
||||||
isOpen,
|
|
||||||
selectedValues,
|
|
||||||
onChange,
|
|
||||||
filteredOptions,
|
|
||||||
enableRegexOption,
|
|
||||||
],
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// ===== UI & Rendering Functions =====
|
// ===== UI & Rendering Functions =====
|
||||||
@@ -599,34 +528,28 @@ const CustomMultiSelect: React.FC<CustomMultiSelectProps> = ({
|
|||||||
(text: string, searchQuery: string): React.ReactNode => {
|
(text: string, searchQuery: string): React.ReactNode => {
|
||||||
if (!searchQuery || !highlightSearch) return text;
|
if (!searchQuery || !highlightSearch) return text;
|
||||||
|
|
||||||
try {
|
const parts = text.split(
|
||||||
const parts = text.split(
|
new RegExp(
|
||||||
new RegExp(
|
`(${searchQuery.replace(/[.*+?^${}()|[\\]\\]/g, '\\$&')})`,
|
||||||
`(${searchQuery.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&')})`,
|
'gi',
|
||||||
'gi',
|
),
|
||||||
),
|
);
|
||||||
);
|
return (
|
||||||
return (
|
<>
|
||||||
<>
|
{parts.map((part, i) => {
|
||||||
{parts.map((part, i) => {
|
// Create a unique key that doesn't rely on array index
|
||||||
// Create a unique key that doesn't rely on array index
|
const uniqueKey = `${text.substring(0, 3)}-${part.substring(0, 3)}-${i}`;
|
||||||
const uniqueKey = `${text.substring(0, 3)}-${part.substring(0, 3)}-${i}`;
|
|
||||||
|
|
||||||
return part.toLowerCase() === searchQuery.toLowerCase() ? (
|
return part.toLowerCase() === searchQuery.toLowerCase() ? (
|
||||||
<span key={uniqueKey} className="highlight-text">
|
<span key={uniqueKey} className="highlight-text">
|
||||||
{part}
|
{part}
|
||||||
</span>
|
</span>
|
||||||
) : (
|
) : (
|
||||||
part
|
part
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
} catch (error) {
|
|
||||||
// If regex fails, return the original text without highlighting
|
|
||||||
console.error('Error in text highlighting:', error);
|
|
||||||
return text;
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
[highlightSearch],
|
[highlightSearch],
|
||||||
);
|
);
|
||||||
@@ -637,10 +560,10 @@ const CustomMultiSelect: React.FC<CustomMultiSelectProps> = ({
|
|||||||
|
|
||||||
if (isAllSelected) {
|
if (isAllSelected) {
|
||||||
// If all are selected, deselect all
|
// If all are selected, deselect all
|
||||||
handleInternalChange([], true);
|
handleInternalChange([]);
|
||||||
} else {
|
} else {
|
||||||
// Otherwise, select all
|
// Otherwise, select all
|
||||||
handleInternalChange([ALL_SELECTED_VALUE], true);
|
handleInternalChange([ALL_SELECTED_VALUE]);
|
||||||
}
|
}
|
||||||
}, [options, isAllSelected, handleInternalChange]);
|
}, [options, isAllSelected, handleInternalChange]);
|
||||||
|
|
||||||
@@ -815,26 +738,6 @@ const CustomMultiSelect: React.FC<CustomMultiSelectProps> = ({
|
|||||||
// Enhanced keyboard navigation with support for maxTagCount
|
// Enhanced keyboard navigation with support for maxTagCount
|
||||||
const handleKeyDown = useCallback(
|
const handleKeyDown = useCallback(
|
||||||
(e: React.KeyboardEvent<HTMLElement>): void => {
|
(e: React.KeyboardEvent<HTMLElement>): void => {
|
||||||
// Simple early return if ALL is selected - block all possible keyboard interactions
|
|
||||||
// that could remove the ALL tag, but still allow dropdown navigation and search
|
|
||||||
if (
|
|
||||||
(allOptionShown || isAllSelected) &&
|
|
||||||
(e.key === 'Backspace' || e.key === 'Delete')
|
|
||||||
) {
|
|
||||||
// Only prevent default if the input is empty or cursor is at start position
|
|
||||||
const activeElement = document.activeElement as HTMLInputElement;
|
|
||||||
const isInputActive = activeElement?.tagName === 'INPUT';
|
|
||||||
const isInputEmpty = isInputActive && !activeElement?.value;
|
|
||||||
const isCursorAtStart =
|
|
||||||
isInputActive && activeElement?.selectionStart === 0;
|
|
||||||
|
|
||||||
if (isInputEmpty || isCursorAtStart) {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get flattened list of all selectable options
|
// Get flattened list of all selectable options
|
||||||
const getFlatOptions = (): OptionData[] => {
|
const getFlatOptions = (): OptionData[] => {
|
||||||
if (!visibleOptions) return [];
|
if (!visibleOptions) return [];
|
||||||
@@ -849,13 +752,13 @@ const CustomMultiSelect: React.FC<CustomMultiSelectProps> = ({
|
|||||||
if (hasAll) {
|
if (hasAll) {
|
||||||
flatList.push({
|
flatList.push({
|
||||||
label: 'ALL',
|
label: 'ALL',
|
||||||
value: ALL_SELECTED_VALUE, // Special value for the ALL option
|
value: '__all__', // Special value for the ALL option
|
||||||
type: 'defined',
|
type: 'defined',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add Regex to flat list
|
// Add Regex to flat list
|
||||||
if (!isEmpty(searchText) && enableRegexOption) {
|
if (!isEmpty(searchText)) {
|
||||||
// Only add regex wrapper if it doesn't already look like a regex pattern
|
// Only add regex wrapper if it doesn't already look like a regex pattern
|
||||||
const isAlreadyRegex =
|
const isAlreadyRegex =
|
||||||
searchText.startsWith('.*') && searchText.endsWith('.*');
|
searchText.startsWith('.*') && searchText.endsWith('.*');
|
||||||
@@ -881,17 +784,6 @@ const CustomMultiSelect: React.FC<CustomMultiSelectProps> = ({
|
|||||||
|
|
||||||
const flatOptions = getFlatOptions();
|
const flatOptions = getFlatOptions();
|
||||||
|
|
||||||
// If we just opened the dropdown and have options, set first option as active
|
|
||||||
if (justOpenedRef.current && flatOptions.length > 0) {
|
|
||||||
setActiveIndex(0);
|
|
||||||
justOpenedRef.current = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If no option is active but we have options and dropdown is open, activate the first one
|
|
||||||
if (isOpen && activeIndex === -1 && flatOptions.length > 0) {
|
|
||||||
setActiveIndex(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the active input element to check cursor position
|
// Get the active input element to check cursor position
|
||||||
const activeElement = document.activeElement as HTMLInputElement;
|
const activeElement = document.activeElement as HTMLInputElement;
|
||||||
const isInputActive = activeElement?.tagName === 'INPUT';
|
const isInputActive = activeElement?.tagName === 'INPUT';
|
||||||
@@ -1237,7 +1129,7 @@ const CustomMultiSelect: React.FC<CustomMultiSelectProps> = ({
|
|||||||
// If there's an active option in the dropdown, prioritize selecting it
|
// If there's an active option in the dropdown, prioritize selecting it
|
||||||
if (activeIndex >= 0 && activeIndex < flatOptions.length) {
|
if (activeIndex >= 0 && activeIndex < flatOptions.length) {
|
||||||
const selectedOption = flatOptions[activeIndex];
|
const selectedOption = flatOptions[activeIndex];
|
||||||
if (selectedOption.value === ALL_SELECTED_VALUE) {
|
if (selectedOption.value === '__all__') {
|
||||||
handleSelectAll();
|
handleSelectAll();
|
||||||
} else if (selectedOption.value && onChange) {
|
} else if (selectedOption.value && onChange) {
|
||||||
const newValues = selectedValues.includes(selectedOption.value)
|
const newValues = selectedValues.includes(selectedOption.value)
|
||||||
@@ -1267,10 +1159,6 @@ const CustomMultiSelect: React.FC<CustomMultiSelectProps> = ({
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
setActiveIndex(-1);
|
setActiveIndex(-1);
|
||||||
// Call onDropdownVisibleChange when Escape is pressed to close dropdown
|
|
||||||
if (onDropdownVisibleChange) {
|
|
||||||
onDropdownVisibleChange(false);
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case SPACEKEY:
|
case SPACEKEY:
|
||||||
@@ -1280,7 +1168,7 @@ const CustomMultiSelect: React.FC<CustomMultiSelectProps> = ({
|
|||||||
const selectedOption = flatOptions[activeIndex];
|
const selectedOption = flatOptions[activeIndex];
|
||||||
|
|
||||||
// Check if it's the ALL option
|
// Check if it's the ALL option
|
||||||
if (selectedOption.value === ALL_SELECTED_VALUE) {
|
if (selectedOption.value === '__all__') {
|
||||||
handleSelectAll();
|
handleSelectAll();
|
||||||
} else if (selectedOption.value && onChange) {
|
} else if (selectedOption.value && onChange) {
|
||||||
const newValues = selectedValues.includes(selectedOption.value)
|
const newValues = selectedValues.includes(selectedOption.value)
|
||||||
@@ -1326,7 +1214,7 @@ const CustomMultiSelect: React.FC<CustomMultiSelectProps> = ({
|
|||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setIsOpen(true);
|
setIsOpen(true);
|
||||||
justOpenedRef.current = true; // Set flag to initialize active option on next render
|
setActiveIndex(0);
|
||||||
setActiveChipIndex(-1);
|
setActiveChipIndex(-1);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
@@ -1372,14 +1260,9 @@ const CustomMultiSelect: React.FC<CustomMultiSelectProps> = ({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
[
|
[
|
||||||
allOptionShown,
|
|
||||||
isAllSelected,
|
|
||||||
isOpen,
|
|
||||||
activeIndex,
|
|
||||||
getVisibleChipIndices,
|
|
||||||
getLastVisibleChipIndex,
|
|
||||||
selectedChips,
|
selectedChips,
|
||||||
isSelectionMode,
|
isSelectionMode,
|
||||||
|
isOpen,
|
||||||
activeChipIndex,
|
activeChipIndex,
|
||||||
selectedValues,
|
selectedValues,
|
||||||
visibleOptions,
|
visibleOptions,
|
||||||
@@ -1395,9 +1278,10 @@ const CustomMultiSelect: React.FC<CustomMultiSelectProps> = ({
|
|||||||
startSelection,
|
startSelection,
|
||||||
selectionEnd,
|
selectionEnd,
|
||||||
extendSelection,
|
extendSelection,
|
||||||
onDropdownVisibleChange,
|
activeIndex,
|
||||||
handleSelectAll,
|
handleSelectAll,
|
||||||
enableRegexOption,
|
getVisibleChipIndices,
|
||||||
|
getLastVisibleChipIndex,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -1422,14 +1306,6 @@ const CustomMultiSelect: React.FC<CustomMultiSelectProps> = ({
|
|||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Add a scroll handler for the dropdown
|
|
||||||
const handleDropdownScroll = useCallback(
|
|
||||||
(e: React.UIEvent<HTMLDivElement>): void => {
|
|
||||||
setIsScrolledToBottom(handleScrollToBottom(e));
|
|
||||||
},
|
|
||||||
[],
|
|
||||||
);
|
|
||||||
|
|
||||||
// Custom dropdown render with sections support
|
// Custom dropdown render with sections support
|
||||||
const customDropdownRender = useCallback((): React.ReactElement => {
|
const customDropdownRender = useCallback((): React.ReactElement => {
|
||||||
// Process options based on current search
|
// Process options based on current search
|
||||||
@@ -1448,7 +1324,7 @@ const CustomMultiSelect: React.FC<CustomMultiSelectProps> = ({
|
|||||||
const customOptions: OptionData[] = [];
|
const customOptions: OptionData[] = [];
|
||||||
|
|
||||||
// add regex options first since they appear first in the UI
|
// add regex options first since they appear first in the UI
|
||||||
if (!isEmpty(searchText) && enableRegexOption) {
|
if (!isEmpty(searchText)) {
|
||||||
// Only add regex wrapper if it doesn't already look like a regex pattern
|
// Only add regex wrapper if it doesn't already look like a regex pattern
|
||||||
const isAlreadyRegex =
|
const isAlreadyRegex =
|
||||||
searchText.startsWith('.*') && searchText.endsWith('.*');
|
searchText.startsWith('.*') && searchText.endsWith('.*');
|
||||||
@@ -1471,17 +1347,8 @@ const CustomMultiSelect: React.FC<CustomMultiSelectProps> = ({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Now add all custom options at the beginning, removing duplicates based on value
|
// Now add all custom options at the beginning
|
||||||
const allOptions = [...customOptions, ...nonSectionOptions];
|
const enhancedNonSectionOptions = [...customOptions, ...nonSectionOptions];
|
||||||
const seenValues = new Set<string>();
|
|
||||||
const enhancedNonSectionOptions = allOptions.filter((option) => {
|
|
||||||
const value = option.value || '';
|
|
||||||
if (seenValues.has(value)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
seenValues.add(value);
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
|
|
||||||
const allOptionValues = getAllAvailableValues(processedOptions);
|
const allOptionValues = getAllAvailableValues(processedOptions);
|
||||||
const allOptionsSelected =
|
const allOptionsSelected =
|
||||||
@@ -1515,7 +1382,6 @@ const CustomMultiSelect: React.FC<CustomMultiSelectProps> = ({
|
|||||||
onMouseDown={handleDropdownMouseDown}
|
onMouseDown={handleDropdownMouseDown}
|
||||||
onClick={handleDropdownClick}
|
onClick={handleDropdownClick}
|
||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
onScroll={handleDropdownScroll}
|
|
||||||
onBlur={handleBlur}
|
onBlur={handleBlur}
|
||||||
role="listbox"
|
role="listbox"
|
||||||
aria-multiselectable="true"
|
aria-multiselectable="true"
|
||||||
@@ -1557,39 +1423,14 @@ const CustomMultiSelect: React.FC<CustomMultiSelectProps> = ({
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div style={{ display: 'flex', alignItems: 'center', width: '100%' }}>
|
<Checkbox
|
||||||
<Checkbox checked={allOptionsSelected} className="option-checkbox">
|
checked={allOptionsSelected}
|
||||||
<div className="option-content">
|
style={{ width: '100%', height: '100%' }}
|
||||||
<div className="all-option-text">ALL</div>
|
>
|
||||||
</div>
|
<div className="option-content">
|
||||||
</Checkbox>
|
<div>ALL</div>
|
||||||
<div
|
|
||||||
onClick={(e): void => {
|
|
||||||
e.stopPropagation();
|
|
||||||
}}
|
|
||||||
onMouseDown={(e): void => {
|
|
||||||
e.stopPropagation();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{isDynamicVariable && (
|
|
||||||
<TextToolTip
|
|
||||||
text="ALL in dynamic variable = No filter applied (unlike other variable types where ALL sends all selected values). Learn more"
|
|
||||||
url="https://signoz.io/docs/userguide/manage-variables/#note-about-all"
|
|
||||||
urlText="here"
|
|
||||||
useFilledIcon={false}
|
|
||||||
outlinedIcon={
|
|
||||||
<Info
|
|
||||||
size={14}
|
|
||||||
style={{
|
|
||||||
color: isDarkMode ? Color.BG_VANILLA_100 : Color.BG_INK_500,
|
|
||||||
marginLeft: 5,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Checkbox>
|
||||||
</div>
|
</div>
|
||||||
<div className="divider" />
|
<div className="divider" />
|
||||||
</>
|
</>
|
||||||
@@ -1598,19 +1439,7 @@ const CustomMultiSelect: React.FC<CustomMultiSelectProps> = ({
|
|||||||
{/* Non-section options when not searching */}
|
{/* Non-section options when not searching */}
|
||||||
{enhancedNonSectionOptions.length > 0 && (
|
{enhancedNonSectionOptions.length > 0 && (
|
||||||
<div className="no-section-options">
|
<div className="no-section-options">
|
||||||
<Virtuoso
|
{mapOptions(enhancedNonSectionOptions)}
|
||||||
style={{
|
|
||||||
minHeight: Math.min(300, enhancedNonSectionOptions.length * 40),
|
|
||||||
maxHeight: enhancedNonSectionOptions.length * 40,
|
|
||||||
}}
|
|
||||||
data={enhancedNonSectionOptions}
|
|
||||||
itemContent={(index, item): React.ReactNode =>
|
|
||||||
(mapOptions([item]) as unknown) as React.ReactElement
|
|
||||||
}
|
|
||||||
totalCount={enhancedNonSectionOptions.length}
|
|
||||||
itemSize={(): number => 40}
|
|
||||||
overscan={5}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -1621,65 +1450,31 @@ const CustomMultiSelect: React.FC<CustomMultiSelectProps> = ({
|
|||||||
<div className="select-group" key={section.label}>
|
<div className="select-group" key={section.label}>
|
||||||
<div className="group-label" role="heading" aria-level={2}>
|
<div className="group-label" role="heading" aria-level={2}>
|
||||||
{section.label}
|
{section.label}
|
||||||
{isDynamicVariable && (
|
|
||||||
<TextToolTip
|
|
||||||
text="Related values: Filtered by other variable selections. All values: Unfiltered complete list. Learn more"
|
|
||||||
url="https://signoz.io/docs/userguide/manage-variables/#dynamic-variable-dropdowns-display-values-in-two-sections"
|
|
||||||
urlText="here"
|
|
||||||
useFilledIcon={false}
|
|
||||||
outlinedIcon={
|
|
||||||
<Info
|
|
||||||
size={14}
|
|
||||||
style={{
|
|
||||||
color: isDarkMode ? Color.BG_VANILLA_100 : Color.BG_INK_500,
|
|
||||||
marginTop: 1,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
<div role="group" aria-label={`${section.label} options`}>
|
<div role="group" aria-label={`${section.label} options`}>
|
||||||
<Virtuoso
|
{section.options && mapOptions(section.options)}
|
||||||
style={{
|
|
||||||
minHeight: Math.min(300, (section.options?.length || 0) * 40),
|
|
||||||
maxHeight: (section.options?.length || 0) * 40,
|
|
||||||
}}
|
|
||||||
data={section.options || []}
|
|
||||||
itemContent={(index, item): React.ReactNode =>
|
|
||||||
(mapOptions([item]) as unknown) as React.ReactElement
|
|
||||||
}
|
|
||||||
totalCount={section.options?.length || 0}
|
|
||||||
itemSize={(): number => 40}
|
|
||||||
overscan={5}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : null,
|
||||||
<div key={section.label} />
|
|
||||||
),
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Navigation help footer */}
|
{/* Navigation help footer */}
|
||||||
<div className="navigation-footer" role="note">
|
<div className="navigation-footer" role="note">
|
||||||
{!loading &&
|
{!loading && !errorMessage && !noDataMessage && (
|
||||||
!errorMessage &&
|
<section className="navigate">
|
||||||
!noDataMessage &&
|
<ArrowDown size={8} className="icons" />
|
||||||
!(showIncompleteDataMessage && isScrolledToBottom) && (
|
<ArrowUp size={8} className="icons" />
|
||||||
<section className="navigate">
|
<ArrowLeft size={8} className="icons" />
|
||||||
<ArrowDown size={8} className="icons" />
|
<ArrowRight size={8} className="icons" />
|
||||||
<ArrowUp size={8} className="icons" />
|
<span className="keyboard-text">to navigate</span>
|
||||||
<ArrowLeft size={8} className="icons" />
|
</section>
|
||||||
<ArrowRight size={8} className="icons" />
|
)}
|
||||||
<span className="keyboard-text">to navigate</span>
|
|
||||||
</section>
|
|
||||||
)}
|
|
||||||
{loading && (
|
{loading && (
|
||||||
<div className="navigation-loading">
|
<div className="navigation-loading">
|
||||||
<div className="navigation-icons">
|
<div className="navigation-icons">
|
||||||
<LoadingOutlined />
|
<LoadingOutlined />
|
||||||
</div>
|
</div>
|
||||||
<div className="navigation-text">Refreshing values...</div>
|
<div className="navigation-text">We are updating the values...</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{errorMessage && !loading && (
|
{errorMessage && !loading && (
|
||||||
@@ -1687,33 +1482,21 @@ const CustomMultiSelect: React.FC<CustomMultiSelectProps> = ({
|
|||||||
<div className="navigation-text">
|
<div className="navigation-text">
|
||||||
{errorMessage || SOMETHING_WENT_WRONG}
|
{errorMessage || SOMETHING_WENT_WRONG}
|
||||||
</div>
|
</div>
|
||||||
{onRetry && showRetryButton && (
|
<div className="navigation-icons">
|
||||||
<div className="navigation-icons">
|
<ReloadOutlined
|
||||||
<ReloadOutlined
|
twoToneColor={Color.BG_CHERRY_400}
|
||||||
twoToneColor={Color.BG_CHERRY_400}
|
onClick={(e): void => {
|
||||||
onClick={(e): void => {
|
e.stopPropagation();
|
||||||
e.stopPropagation();
|
if (onRetry) onRetry();
|
||||||
onRetry();
|
}}
|
||||||
}}
|
/>
|
||||||
/>
|
</div>
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{showIncompleteDataMessage &&
|
{noDataMessage && !loading && (
|
||||||
isScrolledToBottom &&
|
<div className="navigation-text">{noDataMessage}</div>
|
||||||
!loading &&
|
)}
|
||||||
!errorMessage && (
|
|
||||||
<div className="navigation-text-incomplete">
|
|
||||||
Don't see the value? Use search
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{noDataMessage &&
|
|
||||||
!loading &&
|
|
||||||
!(showIncompleteDataMessage && isScrolledToBottom) &&
|
|
||||||
!errorMessage && <div className="navigation-text">{noDataMessage}</div>}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -1730,7 +1513,6 @@ const CustomMultiSelect: React.FC<CustomMultiSelectProps> = ({
|
|||||||
handleDropdownMouseDown,
|
handleDropdownMouseDown,
|
||||||
handleDropdownClick,
|
handleDropdownClick,
|
||||||
handleKeyDown,
|
handleKeyDown,
|
||||||
handleDropdownScroll,
|
|
||||||
handleBlur,
|
handleBlur,
|
||||||
activeIndex,
|
activeIndex,
|
||||||
loading,
|
loading,
|
||||||
@@ -1740,35 +1522,8 @@ const CustomMultiSelect: React.FC<CustomMultiSelectProps> = ({
|
|||||||
renderOptionWithIndex,
|
renderOptionWithIndex,
|
||||||
handleSelectAll,
|
handleSelectAll,
|
||||||
onRetry,
|
onRetry,
|
||||||
showIncompleteDataMessage,
|
|
||||||
isScrolledToBottom,
|
|
||||||
enableRegexOption,
|
|
||||||
isDarkMode,
|
|
||||||
isDynamicVariable,
|
|
||||||
showRetryButton,
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Custom handler for dropdown visibility changes
|
|
||||||
const handleDropdownVisibleChange = useCallback(
|
|
||||||
(visible: boolean): void => {
|
|
||||||
setIsOpen(visible);
|
|
||||||
if (visible) {
|
|
||||||
justOpenedRef.current = true;
|
|
||||||
setActiveIndex(0);
|
|
||||||
setActiveChipIndex(-1);
|
|
||||||
} else {
|
|
||||||
setSearchText('');
|
|
||||||
setActiveIndex(-1);
|
|
||||||
// Don't clear activeChipIndex when dropdown closes to maintain tag focus
|
|
||||||
}
|
|
||||||
// Pass through to the parent component's handler if provided
|
|
||||||
if (onDropdownVisibleChange) {
|
|
||||||
onDropdownVisibleChange(visible);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[onDropdownVisibleChange],
|
|
||||||
);
|
|
||||||
|
|
||||||
// ===== Side Effects =====
|
// ===== Side Effects =====
|
||||||
|
|
||||||
// Clear search when dropdown closes
|
// Clear search when dropdown closes
|
||||||
@@ -1830,16 +1585,55 @@ const CustomMultiSelect: React.FC<CustomMultiSelectProps> = ({
|
|||||||
// Custom Tag Render (needs significant updates)
|
// Custom Tag Render (needs significant updates)
|
||||||
const tagRender = useCallback(
|
const tagRender = useCallback(
|
||||||
(props: CustomTagProps): React.ReactElement => {
|
(props: CustomTagProps): React.ReactElement => {
|
||||||
const { label: labelProp, value, closable, onClose } = props;
|
const { label, value, closable, onClose } = props;
|
||||||
|
|
||||||
const label = showLabels
|
|
||||||
? options.find((option) => option.value === value)?.label || labelProp
|
|
||||||
: labelProp;
|
|
||||||
|
|
||||||
// If the display value is the special ALL value, render the ALL tag
|
// If the display value is the special ALL value, render the ALL tag
|
||||||
if (allOptionShown) {
|
if (value === ALL_SELECTED_VALUE && isAllSelected) {
|
||||||
// Don't render a visible tag - will be shown as placeholder
|
const handleAllTagClose = (
|
||||||
return <div style={{ display: 'none' }} />;
|
e: React.MouseEvent | React.KeyboardEvent,
|
||||||
|
): void => {
|
||||||
|
e.stopPropagation();
|
||||||
|
e.preventDefault();
|
||||||
|
handleInternalChange([]); // Clear selection when ALL tag is closed
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAllTagKeyDown = (e: React.KeyboardEvent): void => {
|
||||||
|
if (e.key === 'Enter' || e.key === SPACEKEY) {
|
||||||
|
handleAllTagClose(e);
|
||||||
|
}
|
||||||
|
// Prevent Backspace/Delete propagation if needed, handle in main keydown handler
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cx('ant-select-selection-item', {
|
||||||
|
'ant-select-selection-item-active': activeChipIndex === 0, // Treat ALL tag as index 0 when active
|
||||||
|
'ant-select-selection-item-selected': selectedChips.includes(0),
|
||||||
|
})}
|
||||||
|
style={
|
||||||
|
activeChipIndex === 0 || selectedChips.includes(0)
|
||||||
|
? {
|
||||||
|
borderColor: Color.BG_ROBIN_500,
|
||||||
|
backgroundColor: Color.BG_SLATE_400,
|
||||||
|
}
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<span className="ant-select-selection-item-content">ALL</span>
|
||||||
|
{closable && (
|
||||||
|
<span
|
||||||
|
className="ant-select-selection-item-remove"
|
||||||
|
onClick={handleAllTagClose}
|
||||||
|
onKeyDown={handleAllTagKeyDown}
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
aria-label="Remove ALL tag (deselect all)"
|
||||||
|
>
|
||||||
|
×
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// If not isAllSelected, render individual tags using previous logic
|
// If not isAllSelected, render individual tags using previous logic
|
||||||
@@ -1919,69 +1713,52 @@ const CustomMultiSelect: React.FC<CustomMultiSelectProps> = ({
|
|||||||
// Fallback for safety, should not be reached
|
// Fallback for safety, should not be reached
|
||||||
return <div />;
|
return <div />;
|
||||||
},
|
},
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
[
|
||||||
[isAllSelected, activeChipIndex, selectedChips, selectedValues, maxTagCount],
|
isAllSelected,
|
||||||
|
handleInternalChange,
|
||||||
|
activeChipIndex,
|
||||||
|
selectedChips,
|
||||||
|
selectedValues,
|
||||||
|
maxTagCount,
|
||||||
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
// Simple onClear handler to prevent clearing ALL
|
|
||||||
const onClearHandler = useCallback((): void => {
|
|
||||||
// Skip clearing if ALL is selected
|
|
||||||
if (allOptionShown || isAllSelected) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Normal clear behavior
|
|
||||||
handleInternalChange([], true);
|
|
||||||
if (onClear) onClear();
|
|
||||||
}, [onClear, handleInternalChange, allOptionShown, isAllSelected]);
|
|
||||||
|
|
||||||
// ===== Component Rendering =====
|
// ===== Component Rendering =====
|
||||||
return (
|
return (
|
||||||
<div
|
<Select
|
||||||
className={cx('custom-multiselect-wrapper', {
|
ref={selectRef}
|
||||||
'all-selected': allOptionShown || isAllSelected,
|
className={cx('custom-multiselect', className, {
|
||||||
|
'has-selection': selectedChips.length > 0 && !isAllSelected,
|
||||||
|
'is-all-selected': isAllSelected,
|
||||||
})}
|
})}
|
||||||
>
|
placeholder={placeholder}
|
||||||
{(allOptionShown || isAllSelected) && !searchText && (
|
mode="multiple"
|
||||||
<div className="all-text">ALL</div>
|
showSearch
|
||||||
)}
|
filterOption={false}
|
||||||
<Select
|
onSearch={handleSearch}
|
||||||
ref={selectRef}
|
value={displayValue}
|
||||||
className={cx('custom-multiselect', className, {
|
onChange={handleInternalChange}
|
||||||
'has-selection': selectedChips.length > 0 && !isAllSelected,
|
onClear={(): void => handleInternalChange([])}
|
||||||
'is-all-selected': isAllSelected,
|
onDropdownVisibleChange={setIsOpen}
|
||||||
})}
|
open={isOpen}
|
||||||
placeholder={placeholder}
|
defaultActiveFirstOption={defaultActiveFirstOption}
|
||||||
mode="multiple"
|
popupMatchSelectWidth={dropdownMatchSelectWidth}
|
||||||
showSearch
|
allowClear={allowClear}
|
||||||
filterOption={false}
|
getPopupContainer={getPopupContainer ?? popupContainer}
|
||||||
onSearch={handleSearch}
|
suffixIcon={<DownOutlined style={{ cursor: 'default' }} />}
|
||||||
value={displayValue}
|
dropdownRender={customDropdownRender}
|
||||||
onChange={(newValue): void => {
|
menuItemSelectedIcon={null}
|
||||||
handleInternalChange(newValue, false);
|
popupClassName={cx('custom-multiselect-dropdown-container', popupClassName)}
|
||||||
}}
|
notFoundContent={<div className="empty-message">{noDataMessage}</div>}
|
||||||
onClear={onClearHandler}
|
onKeyDown={handleKeyDown}
|
||||||
onDropdownVisibleChange={handleDropdownVisibleChange}
|
tagRender={tagRender as any}
|
||||||
open={isOpen}
|
placement={placement}
|
||||||
defaultActiveFirstOption={defaultActiveFirstOption}
|
listHeight={300}
|
||||||
popupMatchSelectWidth={dropdownMatchSelectWidth}
|
searchValue={searchText}
|
||||||
allowClear={allowClear}
|
maxTagTextLength={maxTagTextLength}
|
||||||
getPopupContainer={getPopupContainer ?? popupContainer}
|
maxTagCount={isAllSelected ? 1 : maxTagCount}
|
||||||
suffixIcon={<DownOutlined style={{ cursor: 'default' }} />}
|
{...rest}
|
||||||
dropdownRender={customDropdownRender}
|
/>
|
||||||
menuItemSelectedIcon={null}
|
|
||||||
popupClassName={cx('custom-multiselect-dropdown-container', popupClassName)}
|
|
||||||
notFoundContent={<div className="empty-message">{noDataMessage}</div>}
|
|
||||||
onKeyDown={handleKeyDown}
|
|
||||||
tagRender={tagRender as any}
|
|
||||||
placement={placement}
|
|
||||||
listHeight={300}
|
|
||||||
searchValue={searchText}
|
|
||||||
maxTagTextLength={maxTagTextLength}
|
|
||||||
maxTagCount={isAllSelected ? undefined : maxTagCount}
|
|
||||||
{...rest}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -13,11 +13,9 @@ import {
|
|||||||
import { Color } from '@signozhq/design-tokens';
|
import { Color } from '@signozhq/design-tokens';
|
||||||
import { Select } from 'antd';
|
import { Select } from 'antd';
|
||||||
import cx from 'classnames';
|
import cx from 'classnames';
|
||||||
import TextToolTip from 'components/TextToolTip';
|
|
||||||
import { SOMETHING_WENT_WRONG } from 'constants/api';
|
import { SOMETHING_WENT_WRONG } from 'constants/api';
|
||||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
|
||||||
import { capitalize, isEmpty } from 'lodash-es';
|
import { capitalize, isEmpty } from 'lodash-es';
|
||||||
import { ArrowDown, ArrowUp, Info } from 'lucide-react';
|
import { ArrowDown, ArrowUp } from 'lucide-react';
|
||||||
import type { BaseSelectRef } from 'rc-select';
|
import type { BaseSelectRef } from 'rc-select';
|
||||||
import React, {
|
import React, {
|
||||||
useCallback,
|
useCallback,
|
||||||
@@ -31,7 +29,6 @@ import { popupContainer } from 'utils/selectPopupContainer';
|
|||||||
import { CustomSelectProps, OptionData } from './types';
|
import { CustomSelectProps, OptionData } from './types';
|
||||||
import {
|
import {
|
||||||
filterOptionsBySearch,
|
filterOptionsBySearch,
|
||||||
handleScrollToBottom,
|
|
||||||
prioritizeOrAddOptionForSingleSelect,
|
prioritizeOrAddOptionForSingleSelect,
|
||||||
SPACEKEY,
|
SPACEKEY,
|
||||||
} from './utils';
|
} from './utils';
|
||||||
@@ -60,33 +57,17 @@ const CustomSelect: React.FC<CustomSelectProps> = ({
|
|||||||
errorMessage,
|
errorMessage,
|
||||||
allowClear = false,
|
allowClear = false,
|
||||||
onRetry,
|
onRetry,
|
||||||
showIncompleteDataMessage = false,
|
|
||||||
showRetryButton = true,
|
|
||||||
isDynamicVariable = false,
|
|
||||||
...rest
|
...rest
|
||||||
}) => {
|
}) => {
|
||||||
// ===== State & Refs =====
|
// ===== State & Refs =====
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const [searchText, setSearchText] = useState('');
|
const [searchText, setSearchText] = useState('');
|
||||||
const [activeOptionIndex, setActiveOptionIndex] = useState<number>(-1);
|
const [activeOptionIndex, setActiveOptionIndex] = useState<number>(-1);
|
||||||
const [isScrolledToBottom, setIsScrolledToBottom] = useState(false);
|
|
||||||
|
|
||||||
const isDarkMode = useIsDarkMode();
|
|
||||||
|
|
||||||
// Refs for element access and scroll behavior
|
// Refs for element access and scroll behavior
|
||||||
const selectRef = useRef<BaseSelectRef>(null);
|
const selectRef = useRef<BaseSelectRef>(null);
|
||||||
const dropdownRef = useRef<HTMLDivElement>(null);
|
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||||
const optionRefs = useRef<Record<number, HTMLDivElement | null>>({});
|
const optionRefs = useRef<Record<number, HTMLDivElement | null>>({});
|
||||||
// Flag to track if dropdown just opened
|
|
||||||
const justOpenedRef = useRef<boolean>(false);
|
|
||||||
|
|
||||||
// Add a scroll handler for the dropdown
|
|
||||||
const handleDropdownScroll = useCallback(
|
|
||||||
(e: React.UIEvent<HTMLDivElement>): void => {
|
|
||||||
setIsScrolledToBottom(handleScrollToBottom(e));
|
|
||||||
},
|
|
||||||
[],
|
|
||||||
);
|
|
||||||
|
|
||||||
// ===== Option Filtering & Processing Utilities =====
|
// ===== Option Filtering & Processing Utilities =====
|
||||||
|
|
||||||
@@ -149,33 +130,23 @@ const CustomSelect: React.FC<CustomSelectProps> = ({
|
|||||||
(text: string, searchQuery: string): React.ReactNode => {
|
(text: string, searchQuery: string): React.ReactNode => {
|
||||||
if (!searchQuery || !highlightSearch) return text;
|
if (!searchQuery || !highlightSearch) return text;
|
||||||
|
|
||||||
try {
|
const parts = text.split(new RegExp(`(${searchQuery})`, 'gi'));
|
||||||
const parts = text.split(
|
return (
|
||||||
new RegExp(
|
<>
|
||||||
`(${searchQuery.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&')})`,
|
{parts.map((part, i) => {
|
||||||
'gi',
|
// Create a deterministic but unique key
|
||||||
),
|
const uniqueKey = `${text.substring(0, 3)}-${part.substring(0, 3)}-${i}`;
|
||||||
);
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{parts.map((part, i) => {
|
|
||||||
// Create a deterministic but unique key
|
|
||||||
const uniqueKey = `${text.substring(0, 3)}-${part.substring(0, 3)}-${i}`;
|
|
||||||
|
|
||||||
return part.toLowerCase() === searchQuery.toLowerCase() ? (
|
return part.toLowerCase() === searchQuery.toLowerCase() ? (
|
||||||
<span key={uniqueKey} className="highlight-text">
|
<span key={uniqueKey} className="highlight-text">
|
||||||
{part}
|
{part}
|
||||||
</span>
|
</span>
|
||||||
) : (
|
) : (
|
||||||
part
|
part
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
} catch (error) {
|
|
||||||
console.error('Error in text highlighting:', error);
|
|
||||||
return text;
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
[highlightSearch],
|
[highlightSearch],
|
||||||
);
|
);
|
||||||
@@ -275,14 +246,9 @@ const CustomSelect: React.FC<CustomSelectProps> = ({
|
|||||||
const trimmedValue = value.trim();
|
const trimmedValue = value.trim();
|
||||||
setSearchText(trimmedValue);
|
setSearchText(trimmedValue);
|
||||||
|
|
||||||
// Reset active option index when search changes
|
|
||||||
if (isOpen) {
|
|
||||||
setActiveOptionIndex(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (onSearch) onSearch(trimmedValue);
|
if (onSearch) onSearch(trimmedValue);
|
||||||
},
|
},
|
||||||
[onSearch, isOpen],
|
[onSearch],
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -306,23 +272,14 @@ const CustomSelect: React.FC<CustomSelectProps> = ({
|
|||||||
const flatList: OptionData[] = [];
|
const flatList: OptionData[] = [];
|
||||||
|
|
||||||
// Process options
|
// Process options
|
||||||
let processedOptions = isEmpty(value)
|
|
||||||
? filteredOptions
|
|
||||||
: prioritizeOrAddOptionForSingleSelect(filteredOptions, value);
|
|
||||||
|
|
||||||
if (!isEmpty(searchText)) {
|
|
||||||
processedOptions = filterOptionsBySearch(processedOptions, searchText);
|
|
||||||
}
|
|
||||||
|
|
||||||
const { sectionOptions, nonSectionOptions } = splitOptions(
|
const { sectionOptions, nonSectionOptions } = splitOptions(
|
||||||
processedOptions,
|
isEmpty(value)
|
||||||
|
? filteredOptions
|
||||||
|
: prioritizeOrAddOptionForSingleSelect(filteredOptions, value),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Add custom option if needed
|
// Add custom option if needed
|
||||||
if (
|
if (!isEmpty(searchText) && !isLabelPresent(filteredOptions, searchText)) {
|
||||||
!isEmpty(searchText) &&
|
|
||||||
!isLabelPresent(processedOptions, searchText)
|
|
||||||
) {
|
|
||||||
flatList.push({
|
flatList.push({
|
||||||
label: searchText,
|
label: searchText,
|
||||||
value: searchText,
|
value: searchText,
|
||||||
@@ -343,52 +300,33 @@ const CustomSelect: React.FC<CustomSelectProps> = ({
|
|||||||
|
|
||||||
const options = getFlatOptions();
|
const options = getFlatOptions();
|
||||||
|
|
||||||
// If we just opened the dropdown and have options, set first option as active
|
|
||||||
if (justOpenedRef.current && options.length > 0) {
|
|
||||||
setActiveOptionIndex(0);
|
|
||||||
justOpenedRef.current = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If no option is active but we have options, activate the first one
|
|
||||||
if (activeOptionIndex === -1 && options.length > 0) {
|
|
||||||
setActiveOptionIndex(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (e.key) {
|
switch (e.key) {
|
||||||
case 'ArrowDown':
|
case 'ArrowDown':
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (options.length > 0) {
|
setActiveOptionIndex((prev) =>
|
||||||
setActiveOptionIndex((prev) =>
|
prev < options.length - 1 ? prev + 1 : 0,
|
||||||
prev < options.length - 1 ? prev + 1 : 0,
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'ArrowUp':
|
case 'ArrowUp':
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (options.length > 0) {
|
setActiveOptionIndex((prev) =>
|
||||||
setActiveOptionIndex((prev) =>
|
prev > 0 ? prev - 1 : options.length - 1,
|
||||||
prev > 0 ? prev - 1 : options.length - 1,
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'Tab':
|
case 'Tab':
|
||||||
// Tab navigation with Shift key support
|
// Tab navigation with Shift key support
|
||||||
if (e.shiftKey) {
|
if (e.shiftKey) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (options.length > 0) {
|
setActiveOptionIndex((prev) =>
|
||||||
setActiveOptionIndex((prev) =>
|
prev > 0 ? prev - 1 : options.length - 1,
|
||||||
prev > 0 ? prev - 1 : options.length - 1,
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (options.length > 0) {
|
setActiveOptionIndex((prev) =>
|
||||||
setActiveOptionIndex((prev) =>
|
prev < options.length - 1 ? prev + 1 : 0,
|
||||||
prev < options.length - 1 ? prev + 1 : 0,
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
@@ -401,7 +339,6 @@ const CustomSelect: React.FC<CustomSelectProps> = ({
|
|||||||
onChange(selectedOption.value, selectedOption);
|
onChange(selectedOption.value, selectedOption);
|
||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
setActiveOptionIndex(-1);
|
setActiveOptionIndex(-1);
|
||||||
setSearchText('');
|
|
||||||
}
|
}
|
||||||
} else if (!isEmpty(searchText)) {
|
} else if (!isEmpty(searchText)) {
|
||||||
// Add custom value when no option is focused
|
// Add custom value when no option is focused
|
||||||
@@ -414,7 +351,6 @@ const CustomSelect: React.FC<CustomSelectProps> = ({
|
|||||||
onChange(customOption.value, customOption);
|
onChange(customOption.value, customOption);
|
||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
setActiveOptionIndex(-1);
|
setActiveOptionIndex(-1);
|
||||||
setSearchText('');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@@ -423,7 +359,6 @@ const CustomSelect: React.FC<CustomSelectProps> = ({
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
setActiveOptionIndex(-1);
|
setActiveOptionIndex(-1);
|
||||||
setSearchText('');
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case ' ': // Space key
|
case ' ': // Space key
|
||||||
@@ -434,7 +369,6 @@ const CustomSelect: React.FC<CustomSelectProps> = ({
|
|||||||
onChange(selectedOption.value, selectedOption);
|
onChange(selectedOption.value, selectedOption);
|
||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
setActiveOptionIndex(-1);
|
setActiveOptionIndex(-1);
|
||||||
setSearchText('');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@@ -445,7 +379,7 @@ const CustomSelect: React.FC<CustomSelectProps> = ({
|
|||||||
// Open dropdown when Down or Tab is pressed while closed
|
// Open dropdown when Down or Tab is pressed while closed
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setIsOpen(true);
|
setIsOpen(true);
|
||||||
justOpenedRef.current = true; // Set flag to initialize active option on next render
|
setActiveOptionIndex(0);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[
|
[
|
||||||
@@ -510,7 +444,6 @@ const CustomSelect: React.FC<CustomSelectProps> = ({
|
|||||||
className="custom-select-dropdown"
|
className="custom-select-dropdown"
|
||||||
onClick={handleDropdownClick}
|
onClick={handleDropdownClick}
|
||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
onScroll={handleDropdownScroll}
|
|
||||||
role="listbox"
|
role="listbox"
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
aria-activedescendant={
|
aria-activedescendant={
|
||||||
@@ -521,6 +454,7 @@ const CustomSelect: React.FC<CustomSelectProps> = ({
|
|||||||
<div className="no-section-options">
|
<div className="no-section-options">
|
||||||
{nonSectionOptions.length > 0 && mapOptions(nonSectionOptions)}
|
{nonSectionOptions.length > 0 && mapOptions(nonSectionOptions)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Section options */}
|
{/* Section options */}
|
||||||
{sectionOptions.length > 0 &&
|
{sectionOptions.length > 0 &&
|
||||||
sectionOptions.map((section) =>
|
sectionOptions.map((section) =>
|
||||||
@@ -528,23 +462,6 @@ const CustomSelect: React.FC<CustomSelectProps> = ({
|
|||||||
<div className="select-group" key={section.label}>
|
<div className="select-group" key={section.label}>
|
||||||
<div className="group-label" role="heading" aria-level={2}>
|
<div className="group-label" role="heading" aria-level={2}>
|
||||||
{section.label}
|
{section.label}
|
||||||
{isDynamicVariable && (
|
|
||||||
<TextToolTip
|
|
||||||
text="Related values: Filtered by other variable selections. All values: Unfiltered complete list. Learn more"
|
|
||||||
url="https://signoz.io/docs/userguide/manage-variables/#dynamic-variable-dropdowns-display-values-in-two-sections"
|
|
||||||
urlText="here"
|
|
||||||
useFilledIcon={false}
|
|
||||||
outlinedIcon={
|
|
||||||
<Info
|
|
||||||
size={14}
|
|
||||||
style={{
|
|
||||||
color: isDarkMode ? Color.BG_VANILLA_100 : Color.BG_INK_500,
|
|
||||||
marginTop: 1,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
<div role="group" aria-label={`${section.label} options`}>
|
<div role="group" aria-label={`${section.label} options`}>
|
||||||
{section.options && mapOptions(section.options)}
|
{section.options && mapOptions(section.options)}
|
||||||
@@ -555,22 +472,19 @@ const CustomSelect: React.FC<CustomSelectProps> = ({
|
|||||||
|
|
||||||
{/* Navigation help footer */}
|
{/* Navigation help footer */}
|
||||||
<div className="navigation-footer" role="note">
|
<div className="navigation-footer" role="note">
|
||||||
{!loading &&
|
{!loading && !errorMessage && !noDataMessage && (
|
||||||
!errorMessage &&
|
<section className="navigate">
|
||||||
!noDataMessage &&
|
<ArrowDown size={8} className="icons" />
|
||||||
!(showIncompleteDataMessage && isScrolledToBottom) && (
|
<ArrowUp size={8} className="icons" />
|
||||||
<section className="navigate">
|
<span className="keyboard-text">to navigate</span>
|
||||||
<ArrowDown size={8} className="icons" />
|
</section>
|
||||||
<ArrowUp size={8} className="icons" />
|
)}
|
||||||
<span className="keyboard-text">to navigate</span>
|
|
||||||
</section>
|
|
||||||
)}
|
|
||||||
{loading && (
|
{loading && (
|
||||||
<div className="navigation-loading">
|
<div className="navigation-loading">
|
||||||
<div className="navigation-icons">
|
<div className="navigation-icons">
|
||||||
<LoadingOutlined />
|
<LoadingOutlined />
|
||||||
</div>
|
</div>
|
||||||
<div className="navigation-text">Refreshing values...</div>
|
<div className="navigation-text">We are updating the values...</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{errorMessage && !loading && (
|
{errorMessage && !loading && (
|
||||||
@@ -578,33 +492,21 @@ const CustomSelect: React.FC<CustomSelectProps> = ({
|
|||||||
<div className="navigation-text">
|
<div className="navigation-text">
|
||||||
{errorMessage || SOMETHING_WENT_WRONG}
|
{errorMessage || SOMETHING_WENT_WRONG}
|
||||||
</div>
|
</div>
|
||||||
{onRetry && showRetryButton && (
|
<div className="navigation-icons">
|
||||||
<div className="navigation-icons">
|
<ReloadOutlined
|
||||||
<ReloadOutlined
|
twoToneColor={Color.BG_CHERRY_400}
|
||||||
twoToneColor={Color.BG_CHERRY_400}
|
onClick={(e): void => {
|
||||||
onClick={(e): void => {
|
e.stopPropagation();
|
||||||
e.stopPropagation();
|
if (onRetry) onRetry();
|
||||||
onRetry();
|
}}
|
||||||
}}
|
/>
|
||||||
/>
|
</div>
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{showIncompleteDataMessage &&
|
{noDataMessage && !loading && (
|
||||||
isScrolledToBottom &&
|
<div className="navigation-text">{noDataMessage}</div>
|
||||||
!loading &&
|
)}
|
||||||
!errorMessage && (
|
|
||||||
<div className="navigation-text-incomplete">
|
|
||||||
Don't see the value? Use search
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{noDataMessage &&
|
|
||||||
!loading &&
|
|
||||||
!(showIncompleteDataMessage && isScrolledToBottom) &&
|
|
||||||
!errorMessage && <div className="navigation-text">{noDataMessage}</div>}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -618,7 +520,6 @@ const CustomSelect: React.FC<CustomSelectProps> = ({
|
|||||||
isLabelPresent,
|
isLabelPresent,
|
||||||
handleDropdownClick,
|
handleDropdownClick,
|
||||||
handleKeyDown,
|
handleKeyDown,
|
||||||
handleDropdownScroll,
|
|
||||||
activeOptionIndex,
|
activeOptionIndex,
|
||||||
loading,
|
loading,
|
||||||
errorMessage,
|
errorMessage,
|
||||||
@@ -626,25 +527,8 @@ const CustomSelect: React.FC<CustomSelectProps> = ({
|
|||||||
dropdownRender,
|
dropdownRender,
|
||||||
renderOptionWithIndex,
|
renderOptionWithIndex,
|
||||||
onRetry,
|
onRetry,
|
||||||
showIncompleteDataMessage,
|
|
||||||
isScrolledToBottom,
|
|
||||||
showRetryButton,
|
|
||||||
isDarkMode,
|
|
||||||
isDynamicVariable,
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Handle dropdown visibility changes
|
|
||||||
const handleDropdownVisibleChange = useCallback((visible: boolean): void => {
|
|
||||||
setIsOpen(visible);
|
|
||||||
if (visible) {
|
|
||||||
justOpenedRef.current = true;
|
|
||||||
setActiveOptionIndex(0);
|
|
||||||
} else {
|
|
||||||
setSearchText('');
|
|
||||||
setActiveOptionIndex(-1);
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// ===== Side Effects =====
|
// ===== Side Effects =====
|
||||||
|
|
||||||
// Clear search text when dropdown closes
|
// Clear search text when dropdown closes
|
||||||
@@ -698,7 +582,7 @@ const CustomSelect: React.FC<CustomSelectProps> = ({
|
|||||||
onSearch={handleSearch}
|
onSearch={handleSearch}
|
||||||
value={value}
|
value={value}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
onDropdownVisibleChange={handleDropdownVisibleChange}
|
onDropdownVisibleChange={setIsOpen}
|
||||||
open={isOpen}
|
open={isOpen}
|
||||||
options={optionsWithHighlight}
|
options={optionsWithHighlight}
|
||||||
defaultActiveFirstOption={defaultActiveFirstOption}
|
defaultActiveFirstOption={defaultActiveFirstOption}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,127 +0,0 @@
|
|||||||
import {
|
|
||||||
fireEvent,
|
|
||||||
render,
|
|
||||||
RenderResult,
|
|
||||||
screen,
|
|
||||||
waitFor,
|
|
||||||
} from '@testing-library/react';
|
|
||||||
import { VirtuosoMockContext } from 'react-virtuoso';
|
|
||||||
|
|
||||||
import CustomMultiSelect from '../CustomMultiSelect';
|
|
||||||
|
|
||||||
// Mock scrollIntoView which isn't available in JSDOM
|
|
||||||
window.HTMLElement.prototype.scrollIntoView = jest.fn();
|
|
||||||
|
|
||||||
// Helper function to render with VirtuosoMockContext
|
|
||||||
const renderWithVirtuoso = (component: React.ReactElement): RenderResult =>
|
|
||||||
render(
|
|
||||||
<VirtuosoMockContext.Provider
|
|
||||||
value={{ viewportHeight: 300, itemHeight: 100 }}
|
|
||||||
>
|
|
||||||
{component}
|
|
||||||
</VirtuosoMockContext.Provider>,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Mock options data
|
|
||||||
const mockOptions = [
|
|
||||||
{ label: 'Option 1', value: 'option1' },
|
|
||||||
{ label: 'Option 2', value: 'option2' },
|
|
||||||
{ label: 'Option 3', value: 'option3' },
|
|
||||||
];
|
|
||||||
|
|
||||||
// CSS selector for retry button
|
|
||||||
const RETRY_BUTTON_SELECTOR = '.navigation-icons .anticon-reload';
|
|
||||||
|
|
||||||
describe('CustomMultiSelect - Retry Functionality', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
jest.clearAllMocks();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should show retry button when 5xx error occurs and error message is displayed', async () => {
|
|
||||||
const mockOnRetry = jest.fn();
|
|
||||||
const errorMessage = 'Internal Server Error (500)';
|
|
||||||
|
|
||||||
renderWithVirtuoso(
|
|
||||||
<CustomMultiSelect
|
|
||||||
options={mockOptions}
|
|
||||||
errorMessage={errorMessage}
|
|
||||||
onRetry={mockOnRetry}
|
|
||||||
showRetryButton
|
|
||||||
loading={false}
|
|
||||||
/>,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Open dropdown to see error state
|
|
||||||
const selectElement = screen.getByRole('combobox');
|
|
||||||
fireEvent.mouseDown(selectElement);
|
|
||||||
|
|
||||||
// Wait for dropdown to appear with error message
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(screen.getByText(errorMessage)).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Check that retry button (ReloadOutlined icon) is present
|
|
||||||
const retryButton = document.querySelector(RETRY_BUTTON_SELECTOR);
|
|
||||||
expect(retryButton).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should show retry button when 4xx error occurs and error message is displayed (current behavior)', async () => {
|
|
||||||
const mockOnRetry = jest.fn();
|
|
||||||
const errorMessage = 'Bad Request (400)';
|
|
||||||
|
|
||||||
renderWithVirtuoso(
|
|
||||||
<CustomMultiSelect
|
|
||||||
options={mockOptions}
|
|
||||||
errorMessage={errorMessage}
|
|
||||||
onRetry={mockOnRetry}
|
|
||||||
showRetryButton={false}
|
|
||||||
loading={false}
|
|
||||||
/>,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Open dropdown
|
|
||||||
const selectElement = screen.getByRole('combobox');
|
|
||||||
fireEvent.mouseDown(selectElement);
|
|
||||||
|
|
||||||
// Wait for dropdown to appear with error message
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(screen.getByText(errorMessage)).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
const retryButton = document.querySelector(RETRY_BUTTON_SELECTOR);
|
|
||||||
expect(retryButton).not.toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should call onRetry function when retry button is clicked', async () => {
|
|
||||||
const mockOnRetry = jest.fn();
|
|
||||||
const errorMessage = 'Internal Server Error (500)';
|
|
||||||
|
|
||||||
renderWithVirtuoso(
|
|
||||||
<CustomMultiSelect
|
|
||||||
options={mockOptions}
|
|
||||||
errorMessage={errorMessage}
|
|
||||||
onRetry={mockOnRetry}
|
|
||||||
showRetryButton
|
|
||||||
loading={false}
|
|
||||||
/>,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Open dropdown
|
|
||||||
const selectElement = screen.getByRole('combobox');
|
|
||||||
fireEvent.mouseDown(selectElement);
|
|
||||||
|
|
||||||
// Wait for dropdown to appear
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(screen.getByText(errorMessage)).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Find and click the retry button
|
|
||||||
const retryButton = document.querySelector(RETRY_BUTTON_SELECTOR);
|
|
||||||
expect(retryButton).toBeInTheDocument();
|
|
||||||
|
|
||||||
fireEvent.click(retryButton as Element);
|
|
||||||
|
|
||||||
// Verify onRetry was called
|
|
||||||
expect(mockOnRetry).toHaveBeenCalledTimes(1);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,27 +1,10 @@
|
|||||||
import {
|
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
|
||||||
fireEvent,
|
|
||||||
render,
|
|
||||||
RenderResult,
|
|
||||||
screen,
|
|
||||||
waitFor,
|
|
||||||
} from '@testing-library/react';
|
|
||||||
import { VirtuosoMockContext } from 'react-virtuoso';
|
|
||||||
|
|
||||||
import CustomMultiSelect from '../CustomMultiSelect';
|
import CustomMultiSelect from '../CustomMultiSelect';
|
||||||
|
|
||||||
// Mock scrollIntoView which isn't available in JSDOM
|
// Mock scrollIntoView which isn't available in JSDOM
|
||||||
window.HTMLElement.prototype.scrollIntoView = jest.fn();
|
window.HTMLElement.prototype.scrollIntoView = jest.fn();
|
||||||
|
|
||||||
// Helper function to render with VirtuosoMockContext
|
|
||||||
const renderWithVirtuoso = (component: React.ReactElement): RenderResult =>
|
|
||||||
render(
|
|
||||||
<VirtuosoMockContext.Provider
|
|
||||||
value={{ viewportHeight: 300, itemHeight: 100 }}
|
|
||||||
>
|
|
||||||
{component}
|
|
||||||
</VirtuosoMockContext.Provider>,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Mock options data
|
// Mock options data
|
||||||
const mockOptions = [
|
const mockOptions = [
|
||||||
{ label: 'Option 1', value: 'option1' },
|
{ label: 'Option 1', value: 'option1' },
|
||||||
@@ -49,7 +32,7 @@ const mockGroupedOptions = [
|
|||||||
describe('CustomMultiSelect Component', () => {
|
describe('CustomMultiSelect Component', () => {
|
||||||
it('renders with placeholder', () => {
|
it('renders with placeholder', () => {
|
||||||
const handleChange = jest.fn();
|
const handleChange = jest.fn();
|
||||||
renderWithVirtuoso(
|
render(
|
||||||
<CustomMultiSelect
|
<CustomMultiSelect
|
||||||
placeholder="Select multiple options"
|
placeholder="Select multiple options"
|
||||||
options={mockOptions}
|
options={mockOptions}
|
||||||
@@ -64,9 +47,7 @@ describe('CustomMultiSelect Component', () => {
|
|||||||
|
|
||||||
it('opens dropdown when clicked', async () => {
|
it('opens dropdown when clicked', async () => {
|
||||||
const handleChange = jest.fn();
|
const handleChange = jest.fn();
|
||||||
renderWithVirtuoso(
|
render(<CustomMultiSelect options={mockOptions} onChange={handleChange} />);
|
||||||
<CustomMultiSelect options={mockOptions} onChange={handleChange} />,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Click to open the dropdown
|
// Click to open the dropdown
|
||||||
const selectElement = screen.getByRole('combobox');
|
const selectElement = screen.getByRole('combobox');
|
||||||
@@ -85,7 +66,7 @@ describe('CustomMultiSelect Component', () => {
|
|||||||
const handleChange = jest.fn();
|
const handleChange = jest.fn();
|
||||||
|
|
||||||
// Start with option1 already selected
|
// Start with option1 already selected
|
||||||
renderWithVirtuoso(
|
render(
|
||||||
<CustomMultiSelect
|
<CustomMultiSelect
|
||||||
options={mockOptions}
|
options={mockOptions}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
@@ -112,7 +93,7 @@ describe('CustomMultiSelect Component', () => {
|
|||||||
|
|
||||||
it('selects ALL options when ALL is clicked', async () => {
|
it('selects ALL options when ALL is clicked', async () => {
|
||||||
const handleChange = jest.fn();
|
const handleChange = jest.fn();
|
||||||
renderWithVirtuoso(
|
render(
|
||||||
<CustomMultiSelect
|
<CustomMultiSelect
|
||||||
options={mockOptions}
|
options={mockOptions}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
@@ -145,7 +126,7 @@ describe('CustomMultiSelect Component', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('displays selected options as tags', async () => {
|
it('displays selected options as tags', async () => {
|
||||||
renderWithVirtuoso(
|
render(
|
||||||
<CustomMultiSelect options={mockOptions} value={['option1', 'option2']} />,
|
<CustomMultiSelect options={mockOptions} value={['option1', 'option2']} />,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -156,7 +137,7 @@ describe('CustomMultiSelect Component', () => {
|
|||||||
|
|
||||||
it('removes a tag when clicked', async () => {
|
it('removes a tag when clicked', async () => {
|
||||||
const handleChange = jest.fn();
|
const handleChange = jest.fn();
|
||||||
renderWithVirtuoso(
|
render(
|
||||||
<CustomMultiSelect
|
<CustomMultiSelect
|
||||||
options={mockOptions}
|
options={mockOptions}
|
||||||
value={['option1', 'option2']}
|
value={['option1', 'option2']}
|
||||||
@@ -178,7 +159,7 @@ describe('CustomMultiSelect Component', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('filters options when searching', async () => {
|
it('filters options when searching', async () => {
|
||||||
renderWithVirtuoso(<CustomMultiSelect options={mockOptions} />);
|
render(<CustomMultiSelect options={mockOptions} />);
|
||||||
|
|
||||||
// Open dropdown
|
// Open dropdown
|
||||||
const selectElement = screen.getByRole('combobox');
|
const selectElement = screen.getByRole('combobox');
|
||||||
@@ -212,7 +193,7 @@ describe('CustomMultiSelect Component', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('renders grouped options correctly', async () => {
|
it('renders grouped options correctly', async () => {
|
||||||
renderWithVirtuoso(<CustomMultiSelect options={mockGroupedOptions} />);
|
render(<CustomMultiSelect options={mockGroupedOptions} />);
|
||||||
|
|
||||||
// Open dropdown
|
// Open dropdown
|
||||||
const selectElement = screen.getByRole('combobox');
|
const selectElement = screen.getByRole('combobox');
|
||||||
@@ -230,18 +211,18 @@ describe('CustomMultiSelect Component', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('shows loading state', () => {
|
it('shows loading state', () => {
|
||||||
renderWithVirtuoso(<CustomMultiSelect options={mockOptions} loading />);
|
render(<CustomMultiSelect options={mockOptions} loading />);
|
||||||
|
|
||||||
// Open dropdown
|
// Open dropdown
|
||||||
const selectElement = screen.getByRole('combobox');
|
const selectElement = screen.getByRole('combobox');
|
||||||
fireEvent.mouseDown(selectElement);
|
fireEvent.mouseDown(selectElement);
|
||||||
|
|
||||||
// Check loading text is displayed
|
// Check loading text is displayed
|
||||||
expect(screen.getByText('Refreshing values...')).toBeInTheDocument();
|
expect(screen.getByText('We are updating the values...')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('shows error message', () => {
|
it('shows error message', () => {
|
||||||
renderWithVirtuoso(
|
render(
|
||||||
<CustomMultiSelect
|
<CustomMultiSelect
|
||||||
options={mockOptions}
|
options={mockOptions}
|
||||||
errorMessage="Test error message"
|
errorMessage="Test error message"
|
||||||
@@ -257,9 +238,7 @@ describe('CustomMultiSelect Component', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('shows no data message', () => {
|
it('shows no data message', () => {
|
||||||
renderWithVirtuoso(
|
render(<CustomMultiSelect options={[]} noDataMessage="No data available" />);
|
||||||
<CustomMultiSelect options={[]} noDataMessage="No data available" />,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Open dropdown
|
// Open dropdown
|
||||||
const selectElement = screen.getByRole('combobox');
|
const selectElement = screen.getByRole('combobox');
|
||||||
@@ -270,7 +249,7 @@ describe('CustomMultiSelect Component', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('shows "ALL" tag when all options are selected', () => {
|
it('shows "ALL" tag when all options are selected', () => {
|
||||||
renderWithVirtuoso(
|
render(
|
||||||
<CustomMultiSelect
|
<CustomMultiSelect
|
||||||
options={mockOptions}
|
options={mockOptions}
|
||||||
value={['option1', 'option2', 'option3']}
|
value={['option1', 'option2', 'option3']}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -140,7 +140,7 @@ describe('CustomSelect Component', () => {
|
|||||||
fireEvent.mouseDown(selectElement);
|
fireEvent.mouseDown(selectElement);
|
||||||
|
|
||||||
// Check loading text is displayed
|
// Check loading text is displayed
|
||||||
expect(screen.getByText('Refreshing values...')).toBeInTheDocument();
|
expect(screen.getByText('We are updating the values...')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('shows error message', () => {
|
it('shows error message', () => {
|
||||||
|
|||||||
@@ -1,624 +0,0 @@
|
|||||||
/* eslint-disable sonarjs/no-duplicate-string */
|
|
||||||
import { render, screen, waitFor } from '@testing-library/react';
|
|
||||||
import userEvent from '@testing-library/user-event';
|
|
||||||
import { QueryClient, QueryClientProvider } from 'react-query';
|
|
||||||
import { Provider } from 'react-redux';
|
|
||||||
import { VirtuosoMockContext } from 'react-virtuoso';
|
|
||||||
import configureStore from 'redux-mock-store';
|
|
||||||
import { IDashboardVariable } from 'types/api/dashboard/getAll';
|
|
||||||
|
|
||||||
import VariableItem from '../../../container/NewDashboard/DashboardVariablesSelection/VariableItem';
|
|
||||||
|
|
||||||
// Mock the dashboard variables query
|
|
||||||
jest.mock('api/dashboard/variables/dashboardVariablesQuery', () => ({
|
|
||||||
__esModule: true,
|
|
||||||
default: jest.fn(() =>
|
|
||||||
Promise.resolve({
|
|
||||||
payload: {
|
|
||||||
variableValues: ['option1', 'option2', 'option3', 'option4'],
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
}));
|
|
||||||
|
|
||||||
// Mock scrollIntoView which isn't available in JSDOM
|
|
||||||
window.HTMLElement.prototype.scrollIntoView = jest.fn();
|
|
||||||
|
|
||||||
// Constants
|
|
||||||
const TEST_VARIABLE_NAME = 'test_variable';
|
|
||||||
const TEST_VARIABLE_ID = 'test-var-id';
|
|
||||||
|
|
||||||
// Create a mock store
|
|
||||||
const mockStore = configureStore([])({
|
|
||||||
globalTime: {
|
|
||||||
minTime: Date.now() - 3600000, // 1 hour ago
|
|
||||||
maxTime: Date.now(),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Test data
|
|
||||||
const createMockVariable = (
|
|
||||||
overrides: Partial<IDashboardVariable> = {},
|
|
||||||
): IDashboardVariable => ({
|
|
||||||
id: TEST_VARIABLE_ID,
|
|
||||||
name: TEST_VARIABLE_NAME,
|
|
||||||
description: 'Test variable description',
|
|
||||||
type: 'QUERY',
|
|
||||||
queryValue: 'SELECT DISTINCT value FROM table',
|
|
||||||
customValue: '',
|
|
||||||
sort: 'ASC',
|
|
||||||
multiSelect: false,
|
|
||||||
showALLOption: true,
|
|
||||||
selectedValue: [],
|
|
||||||
allSelected: false,
|
|
||||||
...overrides,
|
|
||||||
});
|
|
||||||
|
|
||||||
function TestWrapper({ children }: { children: React.ReactNode }): JSX.Element {
|
|
||||||
const queryClient = new QueryClient({
|
|
||||||
defaultOptions: {
|
|
||||||
queries: { retry: false },
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Provider store={mockStore}>
|
|
||||||
<QueryClientProvider client={queryClient}>
|
|
||||||
<VirtuosoMockContext.Provider
|
|
||||||
// eslint-disable-next-line react/jsx-no-constructed-context-values
|
|
||||||
value={{ viewportHeight: 300, itemHeight: 40 }}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</VirtuosoMockContext.Provider>
|
|
||||||
</QueryClientProvider>
|
|
||||||
</Provider>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
describe('VariableItem Integration Tests', () => {
|
|
||||||
let user: ReturnType<typeof userEvent.setup>;
|
|
||||||
let mockOnValueUpdate: jest.Mock;
|
|
||||||
let mockSetVariablesToGetUpdated: jest.Mock;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
user = userEvent.setup();
|
|
||||||
mockOnValueUpdate = jest.fn();
|
|
||||||
mockSetVariablesToGetUpdated = jest.fn();
|
|
||||||
jest.clearAllMocks();
|
|
||||||
});
|
|
||||||
|
|
||||||
// ===== 1. INTEGRATION WITH CUSTOMSELECT =====
|
|
||||||
describe('CustomSelect Integration (VI)', () => {
|
|
||||||
test('VI-01: Single select variable integration', async () => {
|
|
||||||
const variable = createMockVariable({
|
|
||||||
multiSelect: false,
|
|
||||||
type: 'CUSTOM',
|
|
||||||
customValue: 'option1,option2,option3',
|
|
||||||
});
|
|
||||||
|
|
||||||
render(
|
|
||||||
<TestWrapper>
|
|
||||||
<VariableItem
|
|
||||||
variableData={variable}
|
|
||||||
existingVariables={{}}
|
|
||||||
onValueUpdate={mockOnValueUpdate}
|
|
||||||
variablesToGetUpdated={[]}
|
|
||||||
setVariablesToGetUpdated={mockSetVariablesToGetUpdated}
|
|
||||||
dependencyData={null}
|
|
||||||
/>
|
|
||||||
</TestWrapper>,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Should render with CustomSelect
|
|
||||||
const combobox = screen.getByRole('combobox');
|
|
||||||
expect(combobox).toBeInTheDocument();
|
|
||||||
|
|
||||||
await user.click(combobox);
|
|
||||||
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(screen.getByText('option1')).toBeInTheDocument();
|
|
||||||
expect(screen.getByText('option2')).toBeInTheDocument();
|
|
||||||
expect(screen.getByText('option3')).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Select an option
|
|
||||||
const option1 = screen.getByText('option1');
|
|
||||||
await user.click(option1);
|
|
||||||
|
|
||||||
expect(mockOnValueUpdate).toHaveBeenCalledWith(
|
|
||||||
TEST_VARIABLE_NAME,
|
|
||||||
TEST_VARIABLE_ID,
|
|
||||||
'option1',
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// ===== 2. INTEGRATION WITH CUSTOMMULTISELECT =====
|
|
||||||
describe('CustomMultiSelect Integration (VI)', () => {
|
|
||||||
test('VI-02: Multi select variable integration', async () => {
|
|
||||||
const variable = createMockVariable({
|
|
||||||
multiSelect: true,
|
|
||||||
type: 'CUSTOM',
|
|
||||||
customValue: 'option1,option2,option3,option4',
|
|
||||||
showALLOption: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
render(
|
|
||||||
<TestWrapper>
|
|
||||||
<VariableItem
|
|
||||||
variableData={variable}
|
|
||||||
existingVariables={{}}
|
|
||||||
onValueUpdate={mockOnValueUpdate}
|
|
||||||
variablesToGetUpdated={[]}
|
|
||||||
setVariablesToGetUpdated={mockSetVariablesToGetUpdated}
|
|
||||||
dependencyData={null}
|
|
||||||
/>
|
|
||||||
</TestWrapper>,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Should render with CustomMultiSelect
|
|
||||||
const combobox = screen.getByRole('combobox');
|
|
||||||
expect(combobox).toBeInTheDocument();
|
|
||||||
|
|
||||||
await user.click(combobox);
|
|
||||||
|
|
||||||
await waitFor(() => {
|
|
||||||
// Should show ALL option
|
|
||||||
expect(screen.getByText('ALL')).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Wait for Virtuoso to render the custom options
|
|
||||||
await waitFor(
|
|
||||||
() => {
|
|
||||||
expect(screen.getByText('option1')).toBeInTheDocument();
|
|
||||||
expect(screen.getByText('option2')).toBeInTheDocument();
|
|
||||||
expect(screen.getByText('option3')).toBeInTheDocument();
|
|
||||||
expect(screen.getByText('option4')).toBeInTheDocument();
|
|
||||||
},
|
|
||||||
{ timeout: 5000 },
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// ===== 3. TEXTBOX VARIABLE TYPE =====
|
|
||||||
describe('Textbox Variable Integration', () => {
|
|
||||||
test('VI-03: Textbox variable handling', async () => {
|
|
||||||
const variable = createMockVariable({
|
|
||||||
type: 'TEXTBOX',
|
|
||||||
selectedValue: 'initial-value',
|
|
||||||
});
|
|
||||||
|
|
||||||
render(
|
|
||||||
<TestWrapper>
|
|
||||||
<VariableItem
|
|
||||||
variableData={variable}
|
|
||||||
existingVariables={{}}
|
|
||||||
onValueUpdate={mockOnValueUpdate}
|
|
||||||
variablesToGetUpdated={[]}
|
|
||||||
setVariablesToGetUpdated={mockSetVariablesToGetUpdated}
|
|
||||||
dependencyData={null}
|
|
||||||
/>
|
|
||||||
</TestWrapper>,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Should render a regular input
|
|
||||||
const textInput = screen.getByDisplayValue('initial-value');
|
|
||||||
expect(textInput).toBeInTheDocument();
|
|
||||||
expect(textInput.tagName).toBe('INPUT');
|
|
||||||
|
|
||||||
// Clear and type new value
|
|
||||||
await user.clear(textInput);
|
|
||||||
await user.type(textInput, 'new-text-value');
|
|
||||||
|
|
||||||
// Should call onValueUpdate after debounce
|
|
||||||
await waitFor(
|
|
||||||
() => {
|
|
||||||
expect(mockOnValueUpdate).toHaveBeenCalledWith(
|
|
||||||
TEST_VARIABLE_NAME,
|
|
||||||
TEST_VARIABLE_ID,
|
|
||||||
'new-text-value',
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
{ timeout: 1000 },
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// ===== 4. VALUE PERSISTENCE AND STATE MANAGEMENT =====
|
|
||||||
describe('Value Persistence and State Management', () => {
|
|
||||||
test('VI-04: All selected state handling', () => {
|
|
||||||
const variable = createMockVariable({
|
|
||||||
multiSelect: true,
|
|
||||||
type: 'CUSTOM',
|
|
||||||
customValue: 'service1,service2,service3',
|
|
||||||
selectedValue: ['service1', 'service2', 'service3'],
|
|
||||||
allSelected: true,
|
|
||||||
showALLOption: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
render(
|
|
||||||
<TestWrapper>
|
|
||||||
<VariableItem
|
|
||||||
variableData={variable}
|
|
||||||
existingVariables={{}}
|
|
||||||
onValueUpdate={mockOnValueUpdate}
|
|
||||||
variablesToGetUpdated={[]}
|
|
||||||
setVariablesToGetUpdated={mockSetVariablesToGetUpdated}
|
|
||||||
dependencyData={null}
|
|
||||||
/>
|
|
||||||
</TestWrapper>,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Should show "ALL" instead of individual values
|
|
||||||
expect(screen.getByText('ALL')).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('VI-05: Dropdown behavior with temporary selections', async () => {
|
|
||||||
const variable = createMockVariable({
|
|
||||||
multiSelect: true,
|
|
||||||
type: 'CUSTOM',
|
|
||||||
customValue: 'item1,item2,item3',
|
|
||||||
selectedValue: ['item1'],
|
|
||||||
});
|
|
||||||
|
|
||||||
render(
|
|
||||||
<TestWrapper>
|
|
||||||
<VariableItem
|
|
||||||
variableData={variable}
|
|
||||||
existingVariables={{}}
|
|
||||||
onValueUpdate={mockOnValueUpdate}
|
|
||||||
variablesToGetUpdated={[]}
|
|
||||||
setVariablesToGetUpdated={mockSetVariablesToGetUpdated}
|
|
||||||
dependencyData={null}
|
|
||||||
/>
|
|
||||||
</TestWrapper>,
|
|
||||||
);
|
|
||||||
|
|
||||||
const combobox = screen.getByRole('combobox');
|
|
||||||
await user.click(combobox);
|
|
||||||
|
|
||||||
// Wait for dropdown to open
|
|
||||||
await waitFor(() => {
|
|
||||||
const dropdown = document.querySelector('.ant-select-dropdown');
|
|
||||||
expect(dropdown).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Verify the component renders without crashing
|
|
||||||
expect(combobox).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// ===== 6. ACCESSIBILITY AND USER EXPERIENCE =====
|
|
||||||
describe('Accessibility and User Experience', () => {
|
|
||||||
test('VI-06: Variable description tooltip', async () => {
|
|
||||||
const variable = createMockVariable({
|
|
||||||
description: 'This variable controls the service selection',
|
|
||||||
type: 'CUSTOM',
|
|
||||||
customValue: 'service1,service2',
|
|
||||||
});
|
|
||||||
|
|
||||||
render(
|
|
||||||
<TestWrapper>
|
|
||||||
<VariableItem
|
|
||||||
variableData={variable}
|
|
||||||
existingVariables={{}}
|
|
||||||
onValueUpdate={mockOnValueUpdate}
|
|
||||||
variablesToGetUpdated={[]}
|
|
||||||
setVariablesToGetUpdated={mockSetVariablesToGetUpdated}
|
|
||||||
dependencyData={null}
|
|
||||||
/>
|
|
||||||
</TestWrapper>,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Should show info icon
|
|
||||||
const infoIcon = document.querySelector('.info-icon');
|
|
||||||
expect(infoIcon).toBeInTheDocument();
|
|
||||||
|
|
||||||
// Hover to show tooltip
|
|
||||||
if (infoIcon) {
|
|
||||||
await user.hover(infoIcon);
|
|
||||||
}
|
|
||||||
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(
|
|
||||||
screen.getByText('This variable controls the service selection'),
|
|
||||||
).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test('VI-07: Variable name display', () => {
|
|
||||||
const variable = createMockVariable({
|
|
||||||
name: 'service_name',
|
|
||||||
type: 'CUSTOM',
|
|
||||||
customValue: 'service1,service2',
|
|
||||||
});
|
|
||||||
|
|
||||||
render(
|
|
||||||
<TestWrapper>
|
|
||||||
<VariableItem
|
|
||||||
variableData={variable}
|
|
||||||
existingVariables={{}}
|
|
||||||
onValueUpdate={mockOnValueUpdate}
|
|
||||||
variablesToGetUpdated={[]}
|
|
||||||
setVariablesToGetUpdated={mockSetVariablesToGetUpdated}
|
|
||||||
dependencyData={null}
|
|
||||||
/>
|
|
||||||
</TestWrapper>,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Should show variable name with $ prefix
|
|
||||||
expect(screen.getByText('$service_name')).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('VI-08: Max tag count behavior', async () => {
|
|
||||||
const variable = createMockVariable({
|
|
||||||
multiSelect: true,
|
|
||||||
type: 'CUSTOM',
|
|
||||||
customValue: 'tag1,tag2,tag3,tag4,tag5,tag6,tag7',
|
|
||||||
selectedValue: ['tag1', 'tag2', 'tag3', 'tag4', 'tag5', 'tag6', 'tag7'],
|
|
||||||
});
|
|
||||||
|
|
||||||
render(
|
|
||||||
<TestWrapper>
|
|
||||||
<VariableItem
|
|
||||||
variableData={variable}
|
|
||||||
existingVariables={{}}
|
|
||||||
onValueUpdate={mockOnValueUpdate}
|
|
||||||
variablesToGetUpdated={[]}
|
|
||||||
setVariablesToGetUpdated={mockSetVariablesToGetUpdated}
|
|
||||||
dependencyData={null}
|
|
||||||
/>
|
|
||||||
</TestWrapper>,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Wait for component to render
|
|
||||||
await waitFor(() => {
|
|
||||||
const combobox = screen.getByRole('combobox');
|
|
||||||
expect(combobox).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Should show limited number of tags with "+ X more"
|
|
||||||
const tags = document.querySelectorAll('.ant-select-selection-item');
|
|
||||||
|
|
||||||
// The component should render without crashing
|
|
||||||
expect(tags.length).toBeGreaterThanOrEqual(0);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// ===== 8. SEARCH INTERACTION TESTS =====
|
|
||||||
describe('Search Interaction Tests', () => {
|
|
||||||
test('VI-14: Search persistence across dropdown open/close', async () => {
|
|
||||||
const variable = createMockVariable({
|
|
||||||
type: 'CUSTOM',
|
|
||||||
customValue: 'option1,option2,option3',
|
|
||||||
multiSelect: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
render(
|
|
||||||
<TestWrapper>
|
|
||||||
<VariableItem
|
|
||||||
variableData={variable}
|
|
||||||
existingVariables={{}}
|
|
||||||
onValueUpdate={mockOnValueUpdate}
|
|
||||||
variablesToGetUpdated={[]}
|
|
||||||
setVariablesToGetUpdated={mockSetVariablesToGetUpdated}
|
|
||||||
dependencyData={null}
|
|
||||||
/>
|
|
||||||
</TestWrapper>,
|
|
||||||
);
|
|
||||||
|
|
||||||
const combobox = screen.getByRole('combobox');
|
|
||||||
await user.click(combobox);
|
|
||||||
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(screen.getByText('ALL')).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
const searchInput = document.querySelector(
|
|
||||||
'.ant-select-selection-search-input',
|
|
||||||
);
|
|
||||||
expect(searchInput).toBeInTheDocument();
|
|
||||||
|
|
||||||
if (searchInput) {
|
|
||||||
await user.type(searchInput, 'search-text');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify search text is in input
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(searchInput).toHaveValue('search-text');
|
|
||||||
});
|
|
||||||
|
|
||||||
// Press Escape to close dropdown
|
|
||||||
await user.keyboard('{Escape}');
|
|
||||||
|
|
||||||
// Dropdown should close and search text should be cleared
|
|
||||||
await waitFor(() => {
|
|
||||||
const dropdown = document.querySelector('.ant-select-dropdown');
|
|
||||||
expect(dropdown).toHaveClass('ant-select-dropdown-hidden');
|
|
||||||
expect(searchInput).toHaveValue('');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// ===== 9. ADVANCED KEYBOARD NAVIGATION =====
|
|
||||||
describe('Advanced Keyboard Navigation (VI)', () => {
|
|
||||||
test('VI-15: Shift + Arrow + Del chip deletion in multiselect', async () => {
|
|
||||||
const variable = createMockVariable({
|
|
||||||
type: 'CUSTOM',
|
|
||||||
customValue: 'option1,option2,option3',
|
|
||||||
multiSelect: true,
|
|
||||||
selectedValue: ['option1', 'option2', 'option3'],
|
|
||||||
});
|
|
||||||
|
|
||||||
render(
|
|
||||||
<TestWrapper>
|
|
||||||
<VariableItem
|
|
||||||
variableData={variable}
|
|
||||||
existingVariables={{}}
|
|
||||||
onValueUpdate={mockOnValueUpdate}
|
|
||||||
variablesToGetUpdated={[]}
|
|
||||||
setVariablesToGetUpdated={mockSetVariablesToGetUpdated}
|
|
||||||
dependencyData={null}
|
|
||||||
/>
|
|
||||||
</TestWrapper>,
|
|
||||||
);
|
|
||||||
|
|
||||||
const combobox = screen.getByRole('combobox');
|
|
||||||
await user.click(combobox);
|
|
||||||
|
|
||||||
// Navigate to chips using arrow keys
|
|
||||||
await user.keyboard('{ArrowLeft}');
|
|
||||||
|
|
||||||
// Use Shift + Arrow to navigate between chips
|
|
||||||
await user.keyboard('{Shift>}{ArrowLeft}{/Shift}');
|
|
||||||
|
|
||||||
// Use Del to delete the active chip
|
|
||||||
await user.keyboard('{Delete}');
|
|
||||||
|
|
||||||
// Note: The component may not immediately call onValueUpdate
|
|
||||||
// This test verifies the chip deletion behavior
|
|
||||||
await waitFor(() => {
|
|
||||||
// Check if a chip was removed from the selection
|
|
||||||
const selectionItems = document.querySelectorAll(
|
|
||||||
'.ant-select-selection-item',
|
|
||||||
);
|
|
||||||
expect(selectionItems.length).toBeLessThan(3);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// ===== 11. ADVANCED UI STATES =====
|
|
||||||
describe('Advanced UI States (VI)', () => {
|
|
||||||
test('VI-19: No data with previous value selected in variable', async () => {
|
|
||||||
const variable = createMockVariable({
|
|
||||||
type: 'CUSTOM',
|
|
||||||
customValue: '',
|
|
||||||
multiSelect: true,
|
|
||||||
selectedValue: ['previous-value'],
|
|
||||||
});
|
|
||||||
|
|
||||||
render(
|
|
||||||
<TestWrapper>
|
|
||||||
<VariableItem
|
|
||||||
variableData={variable}
|
|
||||||
existingVariables={{}}
|
|
||||||
onValueUpdate={mockOnValueUpdate}
|
|
||||||
variablesToGetUpdated={[]}
|
|
||||||
setVariablesToGetUpdated={mockSetVariablesToGetUpdated}
|
|
||||||
dependencyData={null}
|
|
||||||
/>
|
|
||||||
</TestWrapper>,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Wait for component to initialize
|
|
||||||
await waitFor(() => {
|
|
||||||
const combobox = screen.getByRole('combobox');
|
|
||||||
expect(combobox).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
const combobox = screen.getByRole('combobox');
|
|
||||||
await user.click(combobox);
|
|
||||||
|
|
||||||
// Should show no data message (the component may not show this exact text)
|
|
||||||
await waitFor(() => {
|
|
||||||
// Check if dropdown is empty or shows no data indication
|
|
||||||
const dropdown = document.querySelector('.ant-select-dropdown');
|
|
||||||
expect(dropdown).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Verify the component renders without crashing
|
|
||||||
expect(combobox).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('VI-20: Always editable accessibility in variable', async () => {
|
|
||||||
const variable = createMockVariable({
|
|
||||||
type: 'CUSTOM',
|
|
||||||
customValue: 'option1,option2',
|
|
||||||
multiSelect: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
render(
|
|
||||||
<TestWrapper>
|
|
||||||
<VariableItem
|
|
||||||
variableData={variable}
|
|
||||||
existingVariables={{}}
|
|
||||||
onValueUpdate={mockOnValueUpdate}
|
|
||||||
variablesToGetUpdated={[]}
|
|
||||||
setVariablesToGetUpdated={mockSetVariablesToGetUpdated}
|
|
||||||
dependencyData={null}
|
|
||||||
/>
|
|
||||||
</TestWrapper>,
|
|
||||||
);
|
|
||||||
|
|
||||||
const combobox = screen.getByRole('combobox');
|
|
||||||
|
|
||||||
// Should be editable
|
|
||||||
expect(combobox).not.toBeDisabled();
|
|
||||||
await user.click(combobox);
|
|
||||||
expect(combobox).toHaveFocus();
|
|
||||||
|
|
||||||
// Should still be interactive
|
|
||||||
expect(combobox).not.toBeDisabled();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// ===== 13. DROPDOWN PERSISTENCE =====
|
|
||||||
describe('Dropdown Persistence (VI)', () => {
|
|
||||||
test('VI-24: Dropdown stays open for non-save actions in variable', async () => {
|
|
||||||
const variable = createMockVariable({
|
|
||||||
type: 'CUSTOM',
|
|
||||||
customValue: 'option1,option2,option3',
|
|
||||||
multiSelect: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
render(
|
|
||||||
<TestWrapper>
|
|
||||||
<VariableItem
|
|
||||||
variableData={variable}
|
|
||||||
existingVariables={{}}
|
|
||||||
onValueUpdate={mockOnValueUpdate}
|
|
||||||
variablesToGetUpdated={[]}
|
|
||||||
setVariablesToGetUpdated={mockSetVariablesToGetUpdated}
|
|
||||||
dependencyData={null}
|
|
||||||
/>
|
|
||||||
</TestWrapper>,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Wait for component to initialize
|
|
||||||
await waitFor(() => {
|
|
||||||
const combobox = screen.getByRole('combobox');
|
|
||||||
expect(combobox).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
const combobox = screen.getByRole('combobox');
|
|
||||||
await user.click(combobox);
|
|
||||||
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(screen.getByText('ALL')).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Navigate with arrow keys (non-save action)
|
|
||||||
await user.keyboard('{ArrowDown}');
|
|
||||||
await user.keyboard('{ArrowDown}');
|
|
||||||
|
|
||||||
// Dropdown should still be open
|
|
||||||
await waitFor(() => {
|
|
||||||
const dropdown = document.querySelector('.ant-select-dropdown');
|
|
||||||
expect(dropdown).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Verify the component renders without crashing
|
|
||||||
expect(combobox).toBeInTheDocument();
|
|
||||||
|
|
||||||
// Only ESC should close the dropdown
|
|
||||||
await user.keyboard('{Escape}');
|
|
||||||
|
|
||||||
await waitFor(() => {
|
|
||||||
const dropdown = document.querySelector('.ant-select-dropdown');
|
|
||||||
expect(dropdown).toHaveClass('ant-select-dropdown-hidden');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -35,50 +35,12 @@ $custom-border-color: #2c3044;
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
&.is-all-selected {
|
|
||||||
.ant-select-selection-search-input {
|
|
||||||
caret-color: transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ant-select-selection-placeholder {
|
|
||||||
opacity: 1 !important;
|
|
||||||
color: var(--bg-vanilla-400) !important;
|
|
||||||
font-weight: 500;
|
|
||||||
visibility: visible !important;
|
|
||||||
pointer-events: none;
|
|
||||||
z-index: 2;
|
|
||||||
|
|
||||||
.lightMode & {
|
|
||||||
color: rgba(0, 0, 0, 0.85) !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.ant-select-focused .ant-select-selection-placeholder {
|
|
||||||
opacity: 0.45 !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.all-selected-text {
|
|
||||||
position: absolute;
|
|
||||||
left: 12px;
|
|
||||||
top: 50%;
|
|
||||||
transform: translateY(-50%);
|
|
||||||
color: var(--bg-vanilla-400);
|
|
||||||
z-index: 1;
|
|
||||||
pointer-events: none;
|
|
||||||
|
|
||||||
.lightMode & {
|
|
||||||
color: rgba(0, 0, 0, 0.85);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.ant-select-selector {
|
.ant-select-selector {
|
||||||
max-height: 200px;
|
max-height: 200px;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
scrollbar-width: thin;
|
scrollbar-width: thin;
|
||||||
background-color: var(--bg-ink-400);
|
background-color: var(--bg-ink-400);
|
||||||
border-color: var(--bg-slate-400);
|
border-color: var(--bg-slate-400);
|
||||||
cursor: text;
|
|
||||||
|
|
||||||
&::-webkit-scrollbar {
|
&::-webkit-scrollbar {
|
||||||
width: 6px;
|
width: 6px;
|
||||||
@@ -94,16 +56,6 @@ $custom-border-color: #2c3044;
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure adequate space for input area
|
|
||||||
.ant-select-selection-search {
|
|
||||||
min-width: 60px !important;
|
|
||||||
flex: 1 1 auto;
|
|
||||||
.ant-select-selection-search-input {
|
|
||||||
min-width: 60px !important;
|
|
||||||
cursor: text;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.ant-select-focused {
|
&.ant-select-focused {
|
||||||
.ant-select-selector {
|
.ant-select-selector {
|
||||||
border-color: var(--bg-robin-500);
|
border-color: var(--bg-robin-500);
|
||||||
@@ -206,7 +158,7 @@ $custom-border-color: #2c3044;
|
|||||||
// Custom dropdown styles for single select
|
// Custom dropdown styles for single select
|
||||||
.custom-select-dropdown {
|
.custom-select-dropdown {
|
||||||
padding: 8px 0 0 0;
|
padding: 8px 0 0 0;
|
||||||
max-height: 300px;
|
max-height: 500px;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
scrollbar-width: thin;
|
scrollbar-width: thin;
|
||||||
@@ -324,10 +276,6 @@ $custom-border-color: #2c3044;
|
|||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.navigation-text-incomplete {
|
|
||||||
color: var(--bg-amber-600) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.navigation-error {
|
.navigation-error {
|
||||||
.navigation-text,
|
.navigation-text,
|
||||||
.navigation-icons {
|
.navigation-icons {
|
||||||
@@ -374,7 +322,7 @@ $custom-border-color: #2c3044;
|
|||||||
// Custom dropdown styles for multi-select
|
// Custom dropdown styles for multi-select
|
||||||
.custom-multiselect-dropdown {
|
.custom-multiselect-dropdown {
|
||||||
padding: 8px 0 0 0;
|
padding: 8px 0 0 0;
|
||||||
max-height: 350px;
|
max-height: 500px;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
scrollbar-width: thin;
|
scrollbar-width: thin;
|
||||||
@@ -407,13 +355,8 @@ $custom-border-color: #2c3044;
|
|||||||
.select-group {
|
.select-group {
|
||||||
margin-bottom: 12px;
|
margin-bottom: 12px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
margin-top: 4px;
|
|
||||||
|
|
||||||
.group-label {
|
.group-label {
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
|
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
padding: 4px 12px;
|
padding: 4px 12px;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
@@ -461,13 +404,6 @@ $custom-border-color: #2c3044;
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.all-option-text {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.option-content {
|
.option-content {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
@@ -701,7 +637,6 @@ $custom-border-color: #2c3044;
|
|||||||
.ant-select-selector {
|
.ant-select-selector {
|
||||||
background-color: var(--bg-vanilla-100);
|
background-color: var(--bg-vanilla-100);
|
||||||
border-color: #e9e9e9;
|
border-color: #e9e9e9;
|
||||||
cursor: text; // Make entire selector clickable for input focus
|
|
||||||
|
|
||||||
&::-webkit-scrollbar-thumb {
|
&::-webkit-scrollbar-thumb {
|
||||||
background-color: #ccc;
|
background-color: #ccc;
|
||||||
@@ -712,20 +647,6 @@ $custom-border-color: #2c3044;
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.ant-select-selection-search {
|
|
||||||
min-width: 60px !important;
|
|
||||||
flex: 1 1 auto;
|
|
||||||
|
|
||||||
.ant-select-selection-search-input {
|
|
||||||
min-width: 60px !important;
|
|
||||||
cursor: text;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.ant-select-selector {
|
|
||||||
cursor: text;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ant-select-selection-placeholder {
|
.ant-select-selection-placeholder {
|
||||||
color: rgba(0, 0, 0, 0.45);
|
color: rgba(0, 0, 0, 0.45);
|
||||||
}
|
}
|
||||||
@@ -735,10 +656,6 @@ $custom-border-color: #2c3044;
|
|||||||
border: 1px solid #e8e8e8;
|
border: 1px solid #e8e8e8;
|
||||||
color: rgba(0, 0, 0, 0.85);
|
color: rgba(0, 0, 0, 0.85);
|
||||||
|
|
||||||
font-size: 12px !important;
|
|
||||||
height: 20px;
|
|
||||||
line-height: 18px;
|
|
||||||
|
|
||||||
.ant-select-selection-item-content {
|
.ant-select-selection-item-content {
|
||||||
color: rgba(0, 0, 0, 0.85);
|
color: rgba(0, 0, 0, 0.85);
|
||||||
}
|
}
|
||||||
@@ -801,10 +718,6 @@ $custom-border-color: #2c3044;
|
|||||||
|
|
||||||
.select-group {
|
.select-group {
|
||||||
.group-label {
|
.group-label {
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
|
|
||||||
color: rgba(0, 0, 0, 0.85);
|
color: rgba(0, 0, 0, 0.85);
|
||||||
background-color: #fafafa;
|
background-color: #fafafa;
|
||||||
border-bottom: 1px solid #f0f0f0;
|
border-bottom: 1px solid #f0f0f0;
|
||||||
@@ -923,38 +836,3 @@ $custom-border-color: #2c3044;
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.custom-multiselect-wrapper {
|
|
||||||
position: relative;
|
|
||||||
width: 100%;
|
|
||||||
|
|
||||||
&.all-selected {
|
|
||||||
.all-text {
|
|
||||||
position: absolute;
|
|
||||||
left: 12px;
|
|
||||||
top: 50%;
|
|
||||||
transform: translateY(-50%);
|
|
||||||
color: var(--bg-vanilla-400);
|
|
||||||
font-weight: 500;
|
|
||||||
z-index: 2;
|
|
||||||
pointer-events: none;
|
|
||||||
transition: opacity 0.2s ease, visibility 0.2s ease;
|
|
||||||
|
|
||||||
.lightMode & {
|
|
||||||
color: rgba(0, 0, 0, 0.85);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&:focus-within .all-text {
|
|
||||||
opacity: 0.45;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ant-select-selection-search-input {
|
|
||||||
caret-color: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ant-select-selection-placeholder {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -24,12 +24,9 @@ export interface CustomSelectProps extends Omit<SelectProps, 'options'> {
|
|||||||
highlightSearch?: boolean;
|
highlightSearch?: boolean;
|
||||||
placement?: 'topLeft' | 'topRight' | 'bottomLeft' | 'bottomRight';
|
placement?: 'topLeft' | 'topRight' | 'bottomLeft' | 'bottomRight';
|
||||||
popupMatchSelectWidth?: boolean;
|
popupMatchSelectWidth?: boolean;
|
||||||
errorMessage?: string | null;
|
errorMessage?: string;
|
||||||
allowClear?: SelectProps['allowClear'];
|
allowClear?: SelectProps['allowClear'];
|
||||||
onRetry?: () => void;
|
onRetry?: () => void;
|
||||||
showIncompleteDataMessage?: boolean;
|
|
||||||
showRetryButton?: boolean;
|
|
||||||
isDynamicVariable?: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CustomTagProps {
|
export interface CustomTagProps {
|
||||||
@@ -54,16 +51,10 @@ export interface CustomMultiSelectProps
|
|||||||
getPopupContainer?: (triggerNode: HTMLElement) => HTMLElement;
|
getPopupContainer?: (triggerNode: HTMLElement) => HTMLElement;
|
||||||
dropdownRender?: (menu: React.ReactElement) => React.ReactElement;
|
dropdownRender?: (menu: React.ReactElement) => React.ReactElement;
|
||||||
highlightSearch?: boolean;
|
highlightSearch?: boolean;
|
||||||
errorMessage?: string | null;
|
errorMessage?: string;
|
||||||
popupClassName?: string;
|
popupClassName?: string;
|
||||||
placement?: 'topLeft' | 'topRight' | 'bottomLeft' | 'bottomRight';
|
placement?: 'topLeft' | 'topRight' | 'bottomLeft' | 'bottomRight';
|
||||||
maxTagCount?: number;
|
maxTagCount?: number;
|
||||||
allowClear?: SelectProps['allowClear'];
|
allowClear?: SelectProps['allowClear'];
|
||||||
onRetry?: () => void;
|
onRetry?: () => void;
|
||||||
maxTagTextLength?: number;
|
|
||||||
showIncompleteDataMessage?: boolean;
|
|
||||||
showLabels?: boolean;
|
|
||||||
enableRegexOption?: boolean;
|
|
||||||
isDynamicVariable?: boolean;
|
|
||||||
showRetryButton?: boolean;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
/* eslint-disable sonarjs/cognitive-complexity */
|
/* eslint-disable sonarjs/cognitive-complexity */
|
||||||
import { uniqueOptions } from 'container/NewDashboard/DashboardVariablesSelection/util';
|
|
||||||
|
|
||||||
import { OptionData } from './types';
|
import { OptionData } from './types';
|
||||||
|
|
||||||
export const SPACEKEY = ' ';
|
export const SPACEKEY = ' ';
|
||||||
@@ -100,10 +98,8 @@ export const prioritizeOrAddOptionForMultiSelect = (
|
|||||||
label: labels?.[value] ?? value, // Use provided label or default to value
|
label: labels?.[value] ?? value, // Use provided label or default to value
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const flatOutSelectedOptions = uniqueOptions([...newOptions, ...foundOptions]);
|
|
||||||
|
|
||||||
// Add found & new options to the top
|
// Add found & new options to the top
|
||||||
return [...flatOutSelectedOptions, ...filteredOptions];
|
return [...newOptions, ...foundOptions, ...filteredOptions];
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -137,15 +133,3 @@ export const filterOptionsBySearch = (
|
|||||||
})
|
})
|
||||||
.filter(Boolean) as OptionData[];
|
.filter(Boolean) as OptionData[];
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* Utility function to handle dropdown scroll and detect when scrolled to bottom
|
|
||||||
* Returns true when scrolled to within 20px of the bottom
|
|
||||||
*/
|
|
||||||
export const handleScrollToBottom = (
|
|
||||||
e: React.UIEvent<HTMLDivElement>,
|
|
||||||
): boolean => {
|
|
||||||
const { scrollTop, scrollHeight, clientHeight } = e.currentTarget;
|
|
||||||
// Consider "scrolled to bottom" when within 20px of the bottom or at the bottom
|
|
||||||
return scrollHeight - scrollTop - clientHeight < 20;
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -102,7 +102,7 @@ function ListViewOrderBy({
|
|||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
onSearch={handleSearch}
|
onSearch={handleSearch}
|
||||||
notFoundContent={<Loader isLoading={isLoading} />}
|
notFoundContent={<Loader isLoading={isLoading} />}
|
||||||
placeholder="Select a field"
|
placeholder="Select an attribute"
|
||||||
style={{ width: 200 }}
|
style={{ width: 200 }}
|
||||||
options={selectOptions}
|
options={selectOptions}
|
||||||
filterOption={(input, option): boolean =>
|
filterOption={(input, option): boolean =>
|
||||||
|
|||||||
@@ -22,10 +22,6 @@
|
|||||||
flex: 1;
|
flex: 1;
|
||||||
|
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
.qb-trace-view-selector-container {
|
|
||||||
padding: 12px 8px 8px 8px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.qb-content-section {
|
.qb-content-section {
|
||||||
@@ -183,7 +179,7 @@
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
|
|
||||||
margin-left: 26px;
|
margin-left: 32px;
|
||||||
padding-bottom: 16px;
|
padding-bottom: 16px;
|
||||||
padding-left: 8px;
|
padding-left: 8px;
|
||||||
|
|
||||||
@@ -199,8 +195,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.formula-container {
|
.formula-container {
|
||||||
padding: 8px;
|
margin-left: 82px;
|
||||||
margin-left: 74px;
|
padding: 4px 0px;
|
||||||
|
|
||||||
.ant-col {
|
.ant-col {
|
||||||
&::before {
|
&::before {
|
||||||
@@ -295,13 +291,6 @@
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.qb-trace-operator-button-container {
|
|
||||||
&-text {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -342,12 +331,6 @@
|
|||||||
);
|
);
|
||||||
left: 15px;
|
left: 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.has-trace-operator {
|
|
||||||
&::before {
|
|
||||||
height: 0px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.formula-name {
|
.formula-name {
|
||||||
@@ -364,7 +347,7 @@
|
|||||||
|
|
||||||
&::before {
|
&::before {
|
||||||
content: '';
|
content: '';
|
||||||
height: 128px;
|
height: 65px;
|
||||||
content: '';
|
content: '';
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 0;
|
left: 0;
|
||||||
@@ -404,7 +387,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.qb-search-filter-container {
|
.qb-search-filter-container {
|
||||||
flex: 1;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
|
|||||||
@@ -5,13 +5,11 @@ import { Formula } from 'container/QueryBuilder/components/Formula';
|
|||||||
import { QueryBuilderProps } from 'container/QueryBuilder/QueryBuilder.interfaces';
|
import { QueryBuilderProps } from 'container/QueryBuilder/QueryBuilder.interfaces';
|
||||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||||
import { memo, useEffect, useMemo, useRef } from 'react';
|
import { memo, useEffect, useMemo, useRef } from 'react';
|
||||||
import { IBuilderTraceOperator } from 'types/api/queryBuilder/queryBuilderData';
|
|
||||||
import { DataSource } from 'types/common/queryBuilder';
|
import { DataSource } from 'types/common/queryBuilder';
|
||||||
|
|
||||||
import { QueryBuilderV2Provider } from './QueryBuilderV2Context';
|
import { QueryBuilderV2Provider } from './QueryBuilderV2Context';
|
||||||
import QueryFooter from './QueryV2/QueryFooter/QueryFooter';
|
import QueryFooter from './QueryV2/QueryFooter/QueryFooter';
|
||||||
import { QueryV2 } from './QueryV2/QueryV2';
|
import { QueryV2 } from './QueryV2/QueryV2';
|
||||||
import TraceOperator from './QueryV2/TraceOperator/TraceOperator';
|
|
||||||
|
|
||||||
export const QueryBuilderV2 = memo(function QueryBuilderV2({
|
export const QueryBuilderV2 = memo(function QueryBuilderV2({
|
||||||
config,
|
config,
|
||||||
@@ -20,7 +18,6 @@ export const QueryBuilderV2 = memo(function QueryBuilderV2({
|
|||||||
queryComponents,
|
queryComponents,
|
||||||
isListViewPanel = false,
|
isListViewPanel = false,
|
||||||
showOnlyWhereClause = false,
|
showOnlyWhereClause = false,
|
||||||
showTraceOperator = false,
|
|
||||||
version,
|
version,
|
||||||
}: QueryBuilderProps): JSX.Element {
|
}: QueryBuilderProps): JSX.Element {
|
||||||
const {
|
const {
|
||||||
@@ -28,7 +25,6 @@ export const QueryBuilderV2 = memo(function QueryBuilderV2({
|
|||||||
addNewBuilderQuery,
|
addNewBuilderQuery,
|
||||||
addNewFormula,
|
addNewFormula,
|
||||||
handleSetConfig,
|
handleSetConfig,
|
||||||
addTraceOperator,
|
|
||||||
panelType,
|
panelType,
|
||||||
initialDataSource,
|
initialDataSource,
|
||||||
} = useQueryBuilder();
|
} = useQueryBuilder();
|
||||||
@@ -58,11 +54,6 @@ export const QueryBuilderV2 = memo(function QueryBuilderV2({
|
|||||||
newPanelType,
|
newPanelType,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const isMultiQueryAllowed = useMemo(
|
|
||||||
() => !isListViewPanel || showTraceOperator,
|
|
||||||
[showTraceOperator, isListViewPanel],
|
|
||||||
);
|
|
||||||
|
|
||||||
const listViewLogFilterConfigs: QueryBuilderProps['filterConfigs'] = useMemo(() => {
|
const listViewLogFilterConfigs: QueryBuilderProps['filterConfigs'] = useMemo(() => {
|
||||||
const config: QueryBuilderProps['filterConfigs'] = {
|
const config: QueryBuilderProps['filterConfigs'] = {
|
||||||
stepInterval: { isHidden: true, isDisabled: true },
|
stepInterval: { isHidden: true, isDisabled: true },
|
||||||
@@ -106,60 +97,11 @@ export const QueryBuilderV2 = memo(function QueryBuilderV2({
|
|||||||
listViewTracesFilterConfigs,
|
listViewTracesFilterConfigs,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const traceOperator = useMemo((): IBuilderTraceOperator | undefined => {
|
|
||||||
if (
|
|
||||||
currentQuery.builder.queryTraceOperator &&
|
|
||||||
currentQuery.builder.queryTraceOperator.length > 0
|
|
||||||
) {
|
|
||||||
return currentQuery.builder.queryTraceOperator[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
return undefined;
|
|
||||||
}, [currentQuery.builder.queryTraceOperator]);
|
|
||||||
|
|
||||||
const hasAtLeastOneTraceQuery = useMemo(
|
|
||||||
() =>
|
|
||||||
currentQuery.builder.queryData.some(
|
|
||||||
(query) => query.dataSource === DataSource.TRACES,
|
|
||||||
),
|
|
||||||
[currentQuery.builder.queryData],
|
|
||||||
);
|
|
||||||
|
|
||||||
const hasTraceOperator = useMemo(
|
|
||||||
() => showTraceOperator && hasAtLeastOneTraceQuery && Boolean(traceOperator),
|
|
||||||
[showTraceOperator, traceOperator, hasAtLeastOneTraceQuery],
|
|
||||||
);
|
|
||||||
|
|
||||||
const shouldShowFooter = useMemo(
|
|
||||||
() =>
|
|
||||||
(!showOnlyWhereClause && !isListViewPanel) ||
|
|
||||||
(currentDataSource === DataSource.TRACES && showTraceOperator),
|
|
||||||
[isListViewPanel, showTraceOperator, showOnlyWhereClause, currentDataSource],
|
|
||||||
);
|
|
||||||
|
|
||||||
const showQueryList = useMemo(
|
|
||||||
() => (!showOnlyWhereClause && !isListViewPanel) || showTraceOperator,
|
|
||||||
[isListViewPanel, showOnlyWhereClause, showTraceOperator],
|
|
||||||
);
|
|
||||||
|
|
||||||
const showFormula = useMemo(() => {
|
|
||||||
if (currentDataSource === DataSource.TRACES) {
|
|
||||||
return !isListViewPanel;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}, [isListViewPanel, currentDataSource]);
|
|
||||||
|
|
||||||
const showAddTraceOperator = useMemo(
|
|
||||||
() => showTraceOperator && !traceOperator && hasAtLeastOneTraceQuery,
|
|
||||||
[showTraceOperator, traceOperator, hasAtLeastOneTraceQuery],
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<QueryBuilderV2Provider>
|
<QueryBuilderV2Provider>
|
||||||
<div className="query-builder-v2">
|
<div className="query-builder-v2">
|
||||||
<div className="qb-content-container">
|
<div className="qb-content-container">
|
||||||
{!isMultiQueryAllowed ? (
|
{isListViewPanel && (
|
||||||
<QueryV2
|
<QueryV2
|
||||||
ref={containerRef}
|
ref={containerRef}
|
||||||
key={currentQuery.builder.queryData[0].queryName}
|
key={currentQuery.builder.queryData[0].queryName}
|
||||||
@@ -167,16 +109,15 @@ export const QueryBuilderV2 = memo(function QueryBuilderV2({
|
|||||||
query={currentQuery.builder.queryData[0]}
|
query={currentQuery.builder.queryData[0]}
|
||||||
filterConfigs={queryFilterConfigs}
|
filterConfigs={queryFilterConfigs}
|
||||||
queryComponents={queryComponents}
|
queryComponents={queryComponents}
|
||||||
isMultiQueryAllowed={isMultiQueryAllowed}
|
|
||||||
showTraceOperator={showTraceOperator}
|
|
||||||
hasTraceOperator={hasTraceOperator}
|
|
||||||
version={version}
|
version={version}
|
||||||
isAvailableToDisable={false}
|
isAvailableToDisable={false}
|
||||||
queryVariant={config?.queryVariant || 'dropdown'}
|
queryVariant={config?.queryVariant || 'dropdown'}
|
||||||
showOnlyWhereClause={showOnlyWhereClause}
|
showOnlyWhereClause={showOnlyWhereClause}
|
||||||
isListViewPanel={isListViewPanel}
|
isListViewPanel={isListViewPanel}
|
||||||
/>
|
/>
|
||||||
) : (
|
)}
|
||||||
|
|
||||||
|
{!isListViewPanel &&
|
||||||
currentQuery.builder.queryData.map((query, index) => (
|
currentQuery.builder.queryData.map((query, index) => (
|
||||||
<QueryV2
|
<QueryV2
|
||||||
ref={containerRef}
|
ref={containerRef}
|
||||||
@@ -186,17 +127,13 @@ export const QueryBuilderV2 = memo(function QueryBuilderV2({
|
|||||||
filterConfigs={queryFilterConfigs}
|
filterConfigs={queryFilterConfigs}
|
||||||
queryComponents={queryComponents}
|
queryComponents={queryComponents}
|
||||||
version={version}
|
version={version}
|
||||||
isMultiQueryAllowed={isMultiQueryAllowed}
|
|
||||||
isAvailableToDisable={false}
|
isAvailableToDisable={false}
|
||||||
showTraceOperator={showTraceOperator}
|
|
||||||
hasTraceOperator={hasTraceOperator}
|
|
||||||
queryVariant={config?.queryVariant || 'dropdown'}
|
queryVariant={config?.queryVariant || 'dropdown'}
|
||||||
showOnlyWhereClause={showOnlyWhereClause}
|
showOnlyWhereClause={showOnlyWhereClause}
|
||||||
isListViewPanel={isListViewPanel}
|
isListViewPanel={isListViewPanel}
|
||||||
signalSource={config?.signalSource || ''}
|
signalSource={config?.signalSource || ''}
|
||||||
/>
|
/>
|
||||||
))
|
))}
|
||||||
)}
|
|
||||||
|
|
||||||
{!showOnlyWhereClause && currentQuery.builder.queryFormulas.length > 0 && (
|
{!showOnlyWhereClause && currentQuery.builder.queryFormulas.length > 0 && (
|
||||||
<div className="qb-formulas-container">
|
<div className="qb-formulas-container">
|
||||||
@@ -221,25 +158,15 @@ export const QueryBuilderV2 = memo(function QueryBuilderV2({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{shouldShowFooter && (
|
{!showOnlyWhereClause && !isListViewPanel && (
|
||||||
<QueryFooter
|
<QueryFooter
|
||||||
showAddFormula={showFormula}
|
|
||||||
addNewBuilderQuery={addNewBuilderQuery}
|
addNewBuilderQuery={addNewBuilderQuery}
|
||||||
addNewFormula={addNewFormula}
|
addNewFormula={addNewFormula}
|
||||||
addTraceOperator={addTraceOperator}
|
|
||||||
showAddTraceOperator={showAddTraceOperator}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{hasTraceOperator && (
|
|
||||||
<TraceOperator
|
|
||||||
isListViewPanel={isListViewPanel}
|
|
||||||
traceOperator={traceOperator as IBuilderTraceOperator}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{showQueryList && (
|
{!showOnlyWhereClause && !isListViewPanel && (
|
||||||
<div className="query-names-section">
|
<div className="query-names-section">
|
||||||
{currentQuery.builder.queryData.map((query) => (
|
{currentQuery.builder.queryData.map((query) => (
|
||||||
<div key={query.queryName} className="query-name">
|
<div key={query.queryName} className="query-name">
|
||||||
|
|||||||
@@ -160,7 +160,7 @@ const MetricsAggregateSection = memo(function MetricsAggregateSection({
|
|||||||
label="Seconds"
|
label="Seconds"
|
||||||
placeholder="Auto"
|
placeholder="Auto"
|
||||||
labelAfter
|
labelAfter
|
||||||
initialValue={query?.stepInterval ?? null}
|
initialValue={query?.stepInterval ?? undefined}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -283,7 +283,7 @@ const MetricsAggregateSection = memo(function MetricsAggregateSection({
|
|||||||
label="Seconds"
|
label="Seconds"
|
||||||
placeholder="Auto"
|
placeholder="Auto"
|
||||||
labelAfter
|
labelAfter
|
||||||
initialValue={query?.stepInterval ?? null}
|
initialValue={query?.stepInterval ?? undefined}
|
||||||
className="histogram-every-input"
|
className="histogram-every-input"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -44,14 +44,13 @@
|
|||||||
.lightMode {
|
.lightMode {
|
||||||
.metrics-select-container {
|
.metrics-select-container {
|
||||||
.ant-select-selector {
|
.ant-select-selector {
|
||||||
border: 1px solid var(--bg-vanilla-300) !important;
|
border: 1px solid var(--bg-slate-300) !important;
|
||||||
background: var(--bg-vanilla-100);
|
background: var(--bg-vanilla-100);
|
||||||
color: var(--text-ink-100);
|
color: var(--text-ink-100);
|
||||||
}
|
}
|
||||||
|
|
||||||
.ant-select-dropdown {
|
.ant-select-dropdown {
|
||||||
background: var(--bg-vanilla-100);
|
background: var(--bg-vanilla-100);
|
||||||
border: 1px solid var(--bg-vanilla-300) !important;
|
|
||||||
box-shadow: 0 3px 6px -4px rgba(0, 0, 0, 0.12),
|
box-shadow: 0 3px 6px -4px rgba(0, 0, 0, 0.12),
|
||||||
0 6px 16px 0 rgba(0, 0, 0, 0.08), 0 9px 28px 8px rgba(0, 0, 0, 0.05);
|
0 6px 16px 0 rgba(0, 0, 0, 0.08), 0 9px 28px 8px rgba(0, 0, 0, 0.05);
|
||||||
backdrop-filter: none;
|
backdrop-filter: none;
|
||||||
|
|||||||
@@ -1,11 +1,7 @@
|
|||||||
.query-add-ons {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.add-ons-list {
|
.add-ons-list {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 16px;
|
|
||||||
|
|
||||||
.add-ons-tabs {
|
.add-ons-tabs {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -144,7 +144,6 @@ function QueryAddOns({
|
|||||||
showReduceTo,
|
showReduceTo,
|
||||||
panelType,
|
panelType,
|
||||||
index,
|
index,
|
||||||
isForTraceOperator = false,
|
|
||||||
}: {
|
}: {
|
||||||
query: IBuilderQuery;
|
query: IBuilderQuery;
|
||||||
version: string;
|
version: string;
|
||||||
@@ -152,7 +151,6 @@ function QueryAddOns({
|
|||||||
showReduceTo: boolean;
|
showReduceTo: boolean;
|
||||||
panelType: PANEL_TYPES | null;
|
panelType: PANEL_TYPES | null;
|
||||||
index: number;
|
index: number;
|
||||||
isForTraceOperator?: boolean;
|
|
||||||
}): JSX.Element {
|
}): JSX.Element {
|
||||||
const [addOns, setAddOns] = useState<AddOn[]>(ADD_ONS);
|
const [addOns, setAddOns] = useState<AddOn[]>(ADD_ONS);
|
||||||
|
|
||||||
@@ -162,7 +160,6 @@ function QueryAddOns({
|
|||||||
index,
|
index,
|
||||||
query,
|
query,
|
||||||
entityVersion: '',
|
entityVersion: '',
|
||||||
isForTraceOperator,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const { handleSetQueryData } = useQueryBuilder();
|
const { handleSetQueryData } = useQueryBuilder();
|
||||||
|
|||||||
@@ -4,10 +4,7 @@ import { Tooltip } from 'antd';
|
|||||||
import InputWithLabel from 'components/InputWithLabel/InputWithLabel';
|
import InputWithLabel from 'components/InputWithLabel/InputWithLabel';
|
||||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import {
|
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
||||||
IBuilderQuery,
|
|
||||||
IBuilderTraceOperator,
|
|
||||||
} from 'types/api/queryBuilder/queryBuilderData';
|
|
||||||
import { DataSource } from 'types/common/queryBuilder';
|
import { DataSource } from 'types/common/queryBuilder';
|
||||||
|
|
||||||
import QueryAggregationSelect from './QueryAggregationSelect';
|
import QueryAggregationSelect from './QueryAggregationSelect';
|
||||||
@@ -23,7 +20,7 @@ function QueryAggregationOptions({
|
|||||||
panelType?: string;
|
panelType?: string;
|
||||||
onAggregationIntervalChange: (value: number) => void;
|
onAggregationIntervalChange: (value: number) => void;
|
||||||
onChange?: (value: string) => void;
|
onChange?: (value: string) => void;
|
||||||
queryData: IBuilderQuery | IBuilderTraceOperator;
|
queryData: IBuilderQuery;
|
||||||
}): JSX.Element {
|
}): JSX.Element {
|
||||||
const showAggregationInterval = useMemo(() => {
|
const showAggregationInterval = useMemo(() => {
|
||||||
// eslint-disable-next-line sonarjs/prefer-single-boolean-return
|
// eslint-disable-next-line sonarjs/prefer-single-boolean-return
|
||||||
@@ -84,7 +81,9 @@ function QueryAggregationOptions({
|
|||||||
|
|
||||||
<div className="query-aggregation-interval-input-container">
|
<div className="query-aggregation-interval-input-container">
|
||||||
<InputWithLabel
|
<InputWithLabel
|
||||||
initialValue={queryData?.stepInterval ? queryData?.stepInterval : null}
|
initialValue={
|
||||||
|
queryData?.stepInterval ? queryData?.stepInterval : undefined
|
||||||
|
}
|
||||||
className="query-aggregation-interval-input"
|
className="query-aggregation-interval-input"
|
||||||
label="Seconds"
|
label="Seconds"
|
||||||
placeholder="Auto"
|
placeholder="Auto"
|
||||||
|
|||||||
@@ -154,23 +154,15 @@ function QueryAggregationSelect({
|
|||||||
const isDarkMode = useIsDarkMode();
|
const isDarkMode = useIsDarkMode();
|
||||||
const { setAggregationOptions } = useQueryBuilderV2Context();
|
const { setAggregationOptions } = useQueryBuilderV2Context();
|
||||||
|
|
||||||
const formatAggregations = useCallback(
|
|
||||||
(aggregations: any[] | undefined): string =>
|
|
||||||
aggregations
|
|
||||||
?.map(({ expression, alias }: any) =>
|
|
||||||
alias ? `${expression} as ${alias}` : expression,
|
|
||||||
)
|
|
||||||
.join(' ') || '',
|
|
||||||
[],
|
|
||||||
);
|
|
||||||
|
|
||||||
const [input, setInput] = useState(
|
const [input, setInput] = useState(
|
||||||
formatAggregations(queryData?.aggregations),
|
queryData?.aggregations?.map((i: any) => i.expression).join(' ') || '',
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setInput(formatAggregations(queryData?.aggregations));
|
setInput(
|
||||||
}, [queryData?.aggregations, formatAggregations]);
|
queryData?.aggregations?.map((i: any) => i.expression).join(' ') || '',
|
||||||
|
);
|
||||||
|
}, [queryData?.aggregations]);
|
||||||
|
|
||||||
const [cursorPos, setCursorPos] = useState(0);
|
const [cursorPos, setCursorPos] = useState(0);
|
||||||
const [functionArgPairs, setFunctionArgPairs] = useState<
|
const [functionArgPairs, setFunctionArgPairs] = useState<
|
||||||
@@ -686,10 +678,7 @@ function QueryAggregationSelect({
|
|||||||
>
|
>
|
||||||
<Info
|
<Info
|
||||||
size={14}
|
size={14}
|
||||||
style={{
|
style={{ opacity: 0.9, color: isDarkMode ? '#ffffff' : '#000000' }}
|
||||||
opacity: 0.9,
|
|
||||||
color: isDarkMode ? Color.BG_VANILLA_100 : Color.BG_INK_500,
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|||||||
@@ -1,20 +1,12 @@
|
|||||||
/* eslint-disable react/require-default-props */
|
|
||||||
import { Button, Tooltip, Typography } from 'antd';
|
import { Button, Tooltip, Typography } from 'antd';
|
||||||
import { DraftingCompass, Plus, Sigma } from 'lucide-react';
|
import { Plus, Sigma } from 'lucide-react';
|
||||||
import BetaTag from 'periscope/components/BetaTag/BetaTag';
|
|
||||||
|
|
||||||
export default function QueryFooter({
|
export default function QueryFooter({
|
||||||
addNewBuilderQuery,
|
addNewBuilderQuery,
|
||||||
addNewFormula,
|
addNewFormula,
|
||||||
addTraceOperator,
|
|
||||||
showAddFormula = true,
|
|
||||||
showAddTraceOperator = false,
|
|
||||||
}: {
|
}: {
|
||||||
addNewBuilderQuery: () => void;
|
addNewBuilderQuery: () => void;
|
||||||
addNewFormula: () => void;
|
addNewFormula: () => void;
|
||||||
addTraceOperator?: () => void;
|
|
||||||
showAddTraceOperator: boolean;
|
|
||||||
showAddFormula?: boolean;
|
|
||||||
}): JSX.Element {
|
}): JSX.Element {
|
||||||
return (
|
return (
|
||||||
<div className="qb-footer">
|
<div className="qb-footer">
|
||||||
@@ -30,65 +22,32 @@ export default function QueryFooter({
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{showAddFormula && (
|
<div className="qb-add-formula">
|
||||||
<div className="qb-add-formula">
|
<Tooltip
|
||||||
<Tooltip
|
title={
|
||||||
title={
|
<div style={{ textAlign: 'center' }}>
|
||||||
<div style={{ textAlign: 'center' }}>
|
Add New Formula
|
||||||
Add New Formula
|
<Typography.Link
|
||||||
<Typography.Link
|
href="https://signoz.io/docs/userguide/query-builder-v5/#multi-query-analysis-advanced-comparisons"
|
||||||
href="https://signoz.io/docs/userguide/query-builder-v5/#multi-query-analysis-advanced-comparisons"
|
target="_blank"
|
||||||
target="_blank"
|
style={{ textDecoration: 'underline' }}
|
||||||
style={{ textDecoration: 'underline' }}
|
>
|
||||||
>
|
{' '}
|
||||||
{' '}
|
<br />
|
||||||
<br />
|
Learn more
|
||||||
Learn more
|
</Typography.Link>
|
||||||
</Typography.Link>
|
</div>
|
||||||
</div>
|
}
|
||||||
}
|
>
|
||||||
|
<Button
|
||||||
|
className="add-formula-button periscope-btn secondary"
|
||||||
|
icon={<Sigma size={16} />}
|
||||||
|
onClick={addNewFormula}
|
||||||
>
|
>
|
||||||
<Button
|
Add Formula
|
||||||
className="add-formula-button periscope-btn secondary"
|
</Button>
|
||||||
icon={<Sigma size={16} />}
|
</Tooltip>
|
||||||
onClick={addNewFormula}
|
</div>
|
||||||
>
|
|
||||||
Add Formula
|
|
||||||
</Button>
|
|
||||||
</Tooltip>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{showAddTraceOperator && (
|
|
||||||
<div className="qb-trace-operator-button-container">
|
|
||||||
<Tooltip
|
|
||||||
title={
|
|
||||||
<div style={{ textAlign: 'center' }}>
|
|
||||||
Add Trace Matching
|
|
||||||
<Typography.Link
|
|
||||||
href="https://signoz.io/docs/userguide/query-builder-v5/#multi-query-analysis-trace-operators"
|
|
||||||
target="_blank"
|
|
||||||
style={{ textDecoration: 'underline' }}
|
|
||||||
>
|
|
||||||
{' '}
|
|
||||||
<br />
|
|
||||||
Learn more
|
|
||||||
</Typography.Link>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
className="add-trace-operator-button periscope-btn secondary"
|
|
||||||
icon={<DraftingCompass size={16} />}
|
|
||||||
onClick={(): void => addTraceOperator?.()}
|
|
||||||
>
|
|
||||||
<div className="qb-trace-operator-button-container-text">
|
|
||||||
Add Trace Matching
|
|
||||||
<BetaTag />
|
|
||||||
</div>
|
|
||||||
</Button>
|
|
||||||
</Tooltip>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -7,7 +7,6 @@
|
|||||||
'Helvetica Neue', sans-serif;
|
'Helvetica Neue', sans-serif;
|
||||||
|
|
||||||
.query-where-clause-editor-container {
|
.query-where-clause-editor-container {
|
||||||
position: relative;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
|
||||||
|
|||||||
@@ -32,14 +32,12 @@ import { useIsDarkMode } from 'hooks/useDarkMode';
|
|||||||
import useDebounce from 'hooks/useDebounce';
|
import useDebounce from 'hooks/useDebounce';
|
||||||
import { debounce, isNull } from 'lodash-es';
|
import { debounce, isNull } from 'lodash-es';
|
||||||
import { Info, TriangleAlert } from 'lucide-react';
|
import { Info, TriangleAlert } from 'lucide-react';
|
||||||
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import {
|
import {
|
||||||
IDetailedError,
|
IDetailedError,
|
||||||
IQueryContext,
|
IQueryContext,
|
||||||
IValidationResult,
|
IValidationResult,
|
||||||
} from 'types/antlrQueryTypes';
|
} from 'types/antlrQueryTypes';
|
||||||
import { IDashboardVariable } from 'types/api/dashboard/getAll';
|
|
||||||
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
||||||
import { QueryKeyDataSuggestionsProps } from 'types/api/querySuggestions/types';
|
import { QueryKeyDataSuggestionsProps } from 'types/api/querySuggestions/types';
|
||||||
import { DataSource } from 'types/common/queryBuilder';
|
import { DataSource } from 'types/common/queryBuilder';
|
||||||
@@ -163,15 +161,13 @@ function QuerySearch({
|
|||||||
|
|
||||||
const { handleRunQuery } = useQueryBuilder();
|
const { handleRunQuery } = useQueryBuilder();
|
||||||
|
|
||||||
const { selectedDashboard } = useDashboard();
|
// const {
|
||||||
|
// data: queryKeySuggestions,
|
||||||
const dynamicVariables = useMemo(
|
// refetch: refetchQueryKeySuggestions,
|
||||||
() =>
|
// } = useGetQueryKeySuggestions({
|
||||||
Object.values(selectedDashboard?.data?.variables || {})?.filter(
|
// signal: dataSource,
|
||||||
(variable: IDashboardVariable) => variable.type === 'DYNAMIC',
|
// name: searchText || '',
|
||||||
),
|
// });
|
||||||
[selectedDashboard],
|
|
||||||
);
|
|
||||||
|
|
||||||
// Add back the generateOptions function and useEffect
|
// Add back the generateOptions function and useEffect
|
||||||
const generateOptions = (keys: {
|
const generateOptions = (keys: {
|
||||||
@@ -986,25 +982,6 @@ function QuerySearch({
|
|||||||
option.label.toLowerCase().includes(searchText),
|
option.label.toLowerCase().includes(searchText),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Add dynamic variables suggestions for the current key
|
|
||||||
const variableName = dynamicVariables?.find(
|
|
||||||
(variable) => variable?.dynamicVariablesAttribute === keyName,
|
|
||||||
)?.name;
|
|
||||||
|
|
||||||
if (variableName) {
|
|
||||||
const variableValue = `$${variableName}`;
|
|
||||||
const variableOption = {
|
|
||||||
label: variableValue,
|
|
||||||
type: 'variable',
|
|
||||||
apply: variableValue,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Add variable suggestion at the beginning if it matches the search text
|
|
||||||
if (variableValue.toLowerCase().includes(searchText.toLowerCase())) {
|
|
||||||
options = [variableOption, ...options];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Trigger fetch only if needed
|
// Trigger fetch only if needed
|
||||||
const shouldFetch =
|
const shouldFetch =
|
||||||
// Fetch only if key is available
|
// Fetch only if key is available
|
||||||
@@ -1057,9 +1034,6 @@ function QuerySearch({
|
|||||||
} else if (option.type === 'array') {
|
} else if (option.type === 'array') {
|
||||||
// Arrays are already formatted as arrays
|
// Arrays are already formatted as arrays
|
||||||
processedOption.apply = option.label;
|
processedOption.apply = option.label;
|
||||||
} else if (option.type === 'variable') {
|
|
||||||
// Variables should be used as-is (they already have the $ prefix)
|
|
||||||
processedOption.apply = option.label;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return processedOption;
|
return processedOption;
|
||||||
@@ -1269,10 +1243,7 @@ function QuerySearch({
|
|||||||
>
|
>
|
||||||
<Info
|
<Info
|
||||||
size={14}
|
size={14}
|
||||||
style={{
|
style={{ opacity: 0.9, color: isDarkMode ? '#ffffff' : '#000000' }}
|
||||||
opacity: 0.9,
|
|
||||||
color: isDarkMode ? Color.BG_VANILLA_100 : Color.BG_INK_500,
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</a>
|
</a>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
@@ -1321,7 +1292,7 @@ function QuerySearch({
|
|||||||
if (onRun && typeof onRun === 'function') {
|
if (onRun && typeof onRun === 'function') {
|
||||||
onRun(query);
|
onRun(query);
|
||||||
} else {
|
} else {
|
||||||
handleRunQuery();
|
handleRunQuery(true, true);
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
/* eslint-disable sonarjs/cognitive-complexity */
|
|
||||||
import { Dropdown } from 'antd';
|
import { Dropdown } from 'antd';
|
||||||
import cx from 'classnames';
|
import cx from 'classnames';
|
||||||
import { ENTITY_VERSION_V4, ENTITY_VERSION_V5 } from 'constants/app';
|
import { ENTITY_VERSION_V4, ENTITY_VERSION_V5 } from 'constants/app';
|
||||||
@@ -27,12 +26,9 @@ export const QueryV2 = memo(function QueryV2({
|
|||||||
query,
|
query,
|
||||||
filterConfigs,
|
filterConfigs,
|
||||||
isListViewPanel = false,
|
isListViewPanel = false,
|
||||||
showTraceOperator = false,
|
|
||||||
hasTraceOperator = false,
|
|
||||||
version,
|
version,
|
||||||
showOnlyWhereClause = false,
|
showOnlyWhereClause = false,
|
||||||
signalSource = '',
|
signalSource = '',
|
||||||
isMultiQueryAllowed = false,
|
|
||||||
}: QueryProps & { ref: React.RefObject<HTMLDivElement> }): JSX.Element {
|
}: QueryProps & { ref: React.RefObject<HTMLDivElement> }): JSX.Element {
|
||||||
const { cloneQuery, panelType } = useQueryBuilder();
|
const { cloneQuery, panelType } = useQueryBuilder();
|
||||||
|
|
||||||
@@ -79,15 +75,6 @@ export const QueryV2 = memo(function QueryV2({
|
|||||||
dataSource,
|
dataSource,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const showInlineQuerySearch = useMemo(() => {
|
|
||||||
if (!showTraceOperator) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
dataSource === DataSource.TRACES && (hasTraceOperator || isListViewPanel)
|
|
||||||
);
|
|
||||||
}, [hasTraceOperator, isListViewPanel, showTraceOperator, dataSource]);
|
|
||||||
|
|
||||||
const handleChangeAggregateEvery = useCallback(
|
const handleChangeAggregateEvery = useCallback(
|
||||||
(value: IBuilderQuery['stepInterval']) => {
|
(value: IBuilderQuery['stepInterval']) => {
|
||||||
handleChangeQueryData('stepInterval', value);
|
handleChangeQueryData('stepInterval', value);
|
||||||
@@ -121,12 +108,11 @@ export const QueryV2 = memo(function QueryV2({
|
|||||||
ref={ref}
|
ref={ref}
|
||||||
>
|
>
|
||||||
<div className="qb-content-section">
|
<div className="qb-content-section">
|
||||||
{(!showOnlyWhereClause || showTraceOperator) && (
|
{!showOnlyWhereClause && (
|
||||||
<div className="qb-header-container">
|
<div className="qb-header-container">
|
||||||
<div className="query-actions-container">
|
<div className="query-actions-container">
|
||||||
<div className="query-actions-left-container">
|
<div className="query-actions-left-container">
|
||||||
<QBEntityOptions
|
<QBEntityOptions
|
||||||
hasTraceOperator={hasTraceOperator}
|
|
||||||
isMetricsDataSource={dataSource === DataSource.METRICS}
|
isMetricsDataSource={dataSource === DataSource.METRICS}
|
||||||
showFunctions={
|
showFunctions={
|
||||||
(version && version === ENTITY_VERSION_V4) ||
|
(version && version === ENTITY_VERSION_V4) ||
|
||||||
@@ -136,7 +122,6 @@ export const QueryV2 = memo(function QueryV2({
|
|||||||
false
|
false
|
||||||
}
|
}
|
||||||
isCollapsed={isCollapsed}
|
isCollapsed={isCollapsed}
|
||||||
showTraceOperator={showTraceOperator}
|
|
||||||
entityType="query"
|
entityType="query"
|
||||||
entityData={query}
|
entityData={query}
|
||||||
onToggleVisibility={handleToggleDisableQuery}
|
onToggleVisibility={handleToggleDisableQuery}
|
||||||
@@ -154,28 +139,7 @@ export const QueryV2 = memo(function QueryV2({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{!isCollapsed && showInlineQuerySearch && (
|
{!isListViewPanel && (
|
||||||
<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}
|
|
||||||
signalSource={signalSource}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{showSpanScopeSelector && (
|
|
||||||
<div className="traces-search-filter-container">
|
|
||||||
<div className="traces-search-filter-in">in</div>
|
|
||||||
<SpanScopeSelector query={query} />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{isMultiQueryAllowed && (
|
|
||||||
<Dropdown
|
<Dropdown
|
||||||
className="query-actions-dropdown"
|
className="query-actions-dropdown"
|
||||||
menu={{
|
menu={{
|
||||||
@@ -217,31 +181,28 @@ export const QueryV2 = memo(function QueryV2({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{!showInlineQuerySearch && (
|
<div className="qb-search-filter-container">
|
||||||
<div className="qb-search-filter-container">
|
<div className="query-search-container">
|
||||||
<div className="query-search-container">
|
<QuerySearch
|
||||||
<QuerySearch
|
key={`query-search-${query.queryName}-${query.dataSource}`}
|
||||||
key={`query-search-${query.queryName}-${query.dataSource}`}
|
onChange={handleSearchChange}
|
||||||
onChange={handleSearchChange}
|
queryData={query}
|
||||||
queryData={query}
|
dataSource={dataSource}
|
||||||
dataSource={dataSource}
|
signalSource={signalSource}
|
||||||
signalSource={signalSource}
|
/>
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{showSpanScopeSelector && (
|
|
||||||
<div className="traces-search-filter-container">
|
|
||||||
<div className="traces-search-filter-in">in</div>
|
|
||||||
<SpanScopeSelector query={query} />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
|
{showSpanScopeSelector && (
|
||||||
|
<div className="traces-search-filter-container">
|
||||||
|
<div className="traces-search-filter-in">in</div>
|
||||||
|
<SpanScopeSelector query={query} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{!showOnlyWhereClause &&
|
{!showOnlyWhereClause &&
|
||||||
!isListViewPanel &&
|
!isListViewPanel &&
|
||||||
!(hasTraceOperator && dataSource === DataSource.TRACES) &&
|
|
||||||
dataSource !== DataSource.METRICS && (
|
dataSource !== DataSource.METRICS && (
|
||||||
<QueryAggregation
|
<QueryAggregation
|
||||||
dataSource={dataSource}
|
dataSource={dataSource}
|
||||||
@@ -264,17 +225,16 @@ export const QueryV2 = memo(function QueryV2({
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{!showOnlyWhereClause &&
|
{!showOnlyWhereClause && (
|
||||||
!(hasTraceOperator && query.dataSource === DataSource.TRACES) && (
|
<QueryAddOns
|
||||||
<QueryAddOns
|
index={index}
|
||||||
index={index}
|
query={query}
|
||||||
query={query}
|
version="v3"
|
||||||
version="v3"
|
isListViewPanel={isListViewPanel}
|
||||||
isListViewPanel={isListViewPanel}
|
showReduceTo={showReduceTo}
|
||||||
showReduceTo={showReduceTo}
|
panelType={panelType}
|
||||||
panelType={panelType}
|
/>
|
||||||
/>
|
)}
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,159 +0,0 @@
|
|||||||
.qb-trace-operator {
|
|
||||||
padding: 8px;
|
|
||||||
display: flex;
|
|
||||||
gap: 8px;
|
|
||||||
|
|
||||||
&.non-list-view {
|
|
||||||
padding-left: 40px;
|
|
||||||
position: relative;
|
|
||||||
|
|
||||||
&::before {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
top: 24px;
|
|
||||||
left: 12px;
|
|
||||||
height: 88px;
|
|
||||||
width: 1px;
|
|
||||||
background: repeating-linear-gradient(
|
|
||||||
to bottom,
|
|
||||||
var(--bg-slate-400),
|
|
||||||
var(--bg-slate-400) 4px,
|
|
||||||
transparent 4px,
|
|
||||||
transparent 8px
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&-arrow {
|
|
||||||
position: relative;
|
|
||||||
&::before {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
top: 16px;
|
|
||||||
transform: translateY(-50%);
|
|
||||||
left: -26px;
|
|
||||||
height: 1px;
|
|
||||||
width: 20px;
|
|
||||||
background: repeating-linear-gradient(
|
|
||||||
to right,
|
|
||||||
var(--bg-slate-400),
|
|
||||||
var(--bg-slate-400) 4px,
|
|
||||||
transparent 4px,
|
|
||||||
transparent 8px
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
&::after {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
top: 16px;
|
|
||||||
left: -10px;
|
|
||||||
transform: translateY(-50%);
|
|
||||||
height: 4px;
|
|
||||||
width: 4px;
|
|
||||||
border-radius: 50%;
|
|
||||||
background-color: var(--bg-slate-400);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&-input {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
&-container {
|
|
||||||
flex: 1;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&-aggregation-container {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&-add-ons-container {
|
|
||||||
width: 100%;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
gap: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&-label-with-input {
|
|
||||||
position: relative;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
flex-direction: row;
|
|
||||||
|
|
||||||
border-radius: 2px;
|
|
||||||
border: 1px solid var(--bg-slate-400);
|
|
||||||
background: var(--bg-ink-300);
|
|
||||||
|
|
||||||
.qb-trace-operator-editor-container {
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.arrow-left {
|
|
||||||
&::before {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
left: -16px;
|
|
||||||
top: 50%;
|
|
||||||
height: 1px;
|
|
||||||
width: 16px;
|
|
||||||
background-color: var(--bg-slate-400);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.label {
|
|
||||||
color: var(--bg-vanilla-400);
|
|
||||||
white-space: nowrap;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
padding: 0px 8px;
|
|
||||||
border-right: 1px solid var(--bg-slate-400);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.lightMode {
|
|
||||||
.qb-trace-operator {
|
|
||||||
&-arrow {
|
|
||||||
&::before {
|
|
||||||
background: repeating-linear-gradient(
|
|
||||||
to right,
|
|
||||||
var(--bg-vanilla-300),
|
|
||||||
var(--bg-vanilla-300) 4px,
|
|
||||||
transparent 4px,
|
|
||||||
transparent 8px
|
|
||||||
);
|
|
||||||
}
|
|
||||||
&::after {
|
|
||||||
background-color: var(--bg-vanilla-300);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
&.non-list-view {
|
|
||||||
&::before {
|
|
||||||
background: repeating-linear-gradient(
|
|
||||||
to bottom,
|
|
||||||
var(--bg-vanilla-300),
|
|
||||||
var(--bg-vanilla-300) 4px,
|
|
||||||
transparent 4px,
|
|
||||||
transparent 8px
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&-label-with-input {
|
|
||||||
border: 1px solid var(--bg-vanilla-300) !important;
|
|
||||||
background: var(--bg-vanilla-100) !important;
|
|
||||||
|
|
||||||
.label {
|
|
||||||
color: var(--bg-ink-500) !important;
|
|
||||||
border-right: 1px solid var(--bg-vanilla-300) !important;
|
|
||||||
background: var(--bg-vanilla-100) !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,119 +0,0 @@
|
|||||||
/* eslint-disable react/require-default-props */
|
|
||||||
/* eslint-disable sonarjs/no-duplicate-string */
|
|
||||||
|
|
||||||
import './TraceOperator.styles.scss';
|
|
||||||
|
|
||||||
import { Button, Tooltip, Typography } from 'antd';
|
|
||||||
import cx from 'classnames';
|
|
||||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
|
||||||
import { useQueryOperations } from 'hooks/queryBuilder/useQueryBuilderOperations';
|
|
||||||
import { Trash2 } from 'lucide-react';
|
|
||||||
import { useCallback } from 'react';
|
|
||||||
import {
|
|
||||||
IBuilderQuery,
|
|
||||||
IBuilderTraceOperator,
|
|
||||||
} from 'types/api/queryBuilder/queryBuilderData';
|
|
||||||
import { DataSource } from 'types/common/queryBuilder';
|
|
||||||
|
|
||||||
import QueryAddOns from '../QueryAddOns/QueryAddOns';
|
|
||||||
import QueryAggregation from '../QueryAggregation/QueryAggregation';
|
|
||||||
import TraceOperatorEditor from './TraceOperatorEditor';
|
|
||||||
|
|
||||||
export default function TraceOperator({
|
|
||||||
traceOperator,
|
|
||||||
isListViewPanel = false,
|
|
||||||
}: {
|
|
||||||
traceOperator: IBuilderTraceOperator;
|
|
||||||
isListViewPanel?: boolean;
|
|
||||||
}): JSX.Element {
|
|
||||||
const { panelType, removeTraceOperator } = useQueryBuilder();
|
|
||||||
const { handleChangeQueryData } = useQueryOperations({
|
|
||||||
index: 0,
|
|
||||||
query: traceOperator,
|
|
||||||
entityVersion: '',
|
|
||||||
isForTraceOperator: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
const handleTraceOperatorChange = useCallback(
|
|
||||||
(traceOperatorExpression: string) => {
|
|
||||||
handleChangeQueryData('expression', traceOperatorExpression);
|
|
||||||
},
|
|
||||||
[handleChangeQueryData],
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleChangeAggregateEvery = useCallback(
|
|
||||||
(value: IBuilderQuery['stepInterval']) => {
|
|
||||||
handleChangeQueryData('stepInterval', value);
|
|
||||||
},
|
|
||||||
[handleChangeQueryData],
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleChangeAggregation = useCallback(
|
|
||||||
(value: string) => {
|
|
||||||
handleChangeQueryData('aggregations', [
|
|
||||||
{
|
|
||||||
expression: value,
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
},
|
|
||||||
[handleChangeQueryData],
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={cx('qb-trace-operator', !isListViewPanel && 'non-list-view')}>
|
|
||||||
<div className="qb-trace-operator-container">
|
|
||||||
<div
|
|
||||||
className={cx(
|
|
||||||
'qb-trace-operator-label-with-input',
|
|
||||||
!isListViewPanel && 'qb-trace-operator-arrow',
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<Typography.Text className="label">TRACE OPERATOR</Typography.Text>
|
|
||||||
<div className="qb-trace-operator-editor-container">
|
|
||||||
<TraceOperatorEditor
|
|
||||||
value={traceOperator?.expression || ''}
|
|
||||||
traceOperator={traceOperator}
|
|
||||||
onChange={handleTraceOperatorChange}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{!isListViewPanel && (
|
|
||||||
<div className="qb-trace-operator-aggregation-container">
|
|
||||||
<div className={cx(!isListViewPanel && 'qb-trace-operator-arrow')}>
|
|
||||||
<QueryAggregation
|
|
||||||
dataSource={DataSource.TRACES}
|
|
||||||
key={`query-search-${traceOperator.queryName}`}
|
|
||||||
panelType={panelType || undefined}
|
|
||||||
onAggregationIntervalChange={handleChangeAggregateEvery}
|
|
||||||
onChange={handleChangeAggregation}
|
|
||||||
queryData={traceOperator}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className={cx(
|
|
||||||
'qb-trace-operator-add-ons-container',
|
|
||||||
!isListViewPanel && 'qb-trace-operator-arrow',
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<QueryAddOns
|
|
||||||
index={0}
|
|
||||||
query={traceOperator}
|
|
||||||
version="v3"
|
|
||||||
isForTraceOperator
|
|
||||||
isListViewPanel={false}
|
|
||||||
showReduceTo={false}
|
|
||||||
panelType={panelType}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<Tooltip title="Remove Trace Operator" placement="topLeft">
|
|
||||||
<Button className="periscope-btn ghost" onClick={removeTraceOperator}>
|
|
||||||
<Trash2 size={14} />
|
|
||||||
</Button>
|
|
||||||
</Tooltip>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,491 +0,0 @@
|
|||||||
/* eslint-disable sonarjs/cognitive-complexity */
|
|
||||||
/* eslint-disable sonarjs/no-identical-functions */
|
|
||||||
|
|
||||||
import '../QuerySearch/QuerySearch.styles.scss';
|
|
||||||
|
|
||||||
import { CheckCircleFilled } from '@ant-design/icons';
|
|
||||||
import {
|
|
||||||
autocompletion,
|
|
||||||
closeCompletion,
|
|
||||||
CompletionContext,
|
|
||||||
completionKeymap,
|
|
||||||
CompletionResult,
|
|
||||||
startCompletion,
|
|
||||||
} from '@codemirror/autocomplete';
|
|
||||||
import { javascript } from '@codemirror/lang-javascript';
|
|
||||||
import { Color } from '@signozhq/design-tokens';
|
|
||||||
import { copilot } from '@uiw/codemirror-theme-copilot';
|
|
||||||
import { githubLight } from '@uiw/codemirror-theme-github';
|
|
||||||
import CodeMirror, { EditorView, keymap, Prec } from '@uiw/react-codemirror';
|
|
||||||
import { Button, Popover } from 'antd';
|
|
||||||
import cx from 'classnames';
|
|
||||||
import {
|
|
||||||
TRACE_OPERATOR_OPERATORS,
|
|
||||||
TRACE_OPERATOR_OPERATORS_LABELS,
|
|
||||||
TRACE_OPERATOR_OPERATORS_WITH_PRIORITY,
|
|
||||||
} from 'constants/antlrQueryConstants';
|
|
||||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
|
||||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
|
||||||
import { TriangleAlert } from 'lucide-react';
|
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
||||||
import { IDetailedError, IValidationResult } from 'types/antlrQueryTypes';
|
|
||||||
import { IBuilderTraceOperator } from 'types/api/queryBuilder/queryBuilderData';
|
|
||||||
import { DataSource } from 'types/common/queryBuilder';
|
|
||||||
import { validateTraceOperatorQuery } from 'utils/queryValidationUtils';
|
|
||||||
|
|
||||||
import { getTraceOperatorContextAtCursor } from './utils/traceOperatorContextUtils';
|
|
||||||
import { getInvolvedQueriesInTraceOperator } from './utils/utils';
|
|
||||||
|
|
||||||
// Custom extension to stop events
|
|
||||||
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;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
interface TraceOperatorEditorProps {
|
|
||||||
value: string;
|
|
||||||
traceOperator: IBuilderTraceOperator;
|
|
||||||
onChange: (value: string) => void;
|
|
||||||
placeholder?: string;
|
|
||||||
onRun?: (query: string) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
function TraceOperatorEditor({
|
|
||||||
value,
|
|
||||||
onChange,
|
|
||||||
traceOperator,
|
|
||||||
placeholder = 'Enter your trace operator query',
|
|
||||||
onRun,
|
|
||||||
}: TraceOperatorEditorProps): JSX.Element {
|
|
||||||
const isDarkMode = useIsDarkMode();
|
|
||||||
const [isFocused, setIsFocused] = useState(false);
|
|
||||||
const [cursorPos, setCursorPos] = useState({ line: 0, ch: 0 });
|
|
||||||
const editorRef = useRef<EditorView | null>(null);
|
|
||||||
const [validation, setValidation] = useState<IValidationResult>({
|
|
||||||
isValid: false,
|
|
||||||
message: '',
|
|
||||||
errors: [],
|
|
||||||
});
|
|
||||||
// Track if the query was changed externally (from props) vs internally (user input)
|
|
||||||
const [isExternalQueryChange, setIsExternalQueryChange] = useState(false);
|
|
||||||
const [lastExternalValue, setLastExternalValue] = useState<string>('');
|
|
||||||
const { currentQuery, handleRunQuery } = useQueryBuilder();
|
|
||||||
|
|
||||||
const queryOptions = useMemo(
|
|
||||||
() =>
|
|
||||||
currentQuery.builder.queryData
|
|
||||||
.filter((query) => query.dataSource === DataSource.TRACES) // Only show trace queries
|
|
||||||
.map((query) => ({
|
|
||||||
label: query.queryName,
|
|
||||||
type: 'atom',
|
|
||||||
apply: query.queryName,
|
|
||||||
})),
|
|
||||||
[currentQuery.builder.queryData],
|
|
||||||
);
|
|
||||||
|
|
||||||
const toggleSuggestions = useCallback(
|
|
||||||
(timeout?: number) => {
|
|
||||||
const timeoutId = setTimeout(() => {
|
|
||||||
if (!editorRef.current) return;
|
|
||||||
if (isFocused) {
|
|
||||||
startCompletion(editorRef.current);
|
|
||||||
} else {
|
|
||||||
closeCompletion(editorRef.current);
|
|
||||||
}
|
|
||||||
}, timeout);
|
|
||||||
|
|
||||||
return (): void => clearTimeout(timeoutId);
|
|
||||||
},
|
|
||||||
[isFocused],
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleQueryValidation = (newQuery: string): void => {
|
|
||||||
try {
|
|
||||||
const validationResponse = validateTraceOperatorQuery(newQuery);
|
|
||||||
setValidation(validationResponse);
|
|
||||||
} catch (error) {
|
|
||||||
setValidation({
|
|
||||||
isValid: false,
|
|
||||||
message: 'Failed to process trace operator',
|
|
||||||
errors: [error as IDetailedError],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Detect external value changes and mark for validation
|
|
||||||
useEffect(() => {
|
|
||||||
const newValue = value || '';
|
|
||||||
if (newValue !== lastExternalValue) {
|
|
||||||
setIsExternalQueryChange(true);
|
|
||||||
setLastExternalValue(newValue);
|
|
||||||
}
|
|
||||||
}, [value, lastExternalValue]);
|
|
||||||
|
|
||||||
// Validate when the value changes externally (including on mount)
|
|
||||||
useEffect(() => {
|
|
||||||
if (isExternalQueryChange && value) {
|
|
||||||
handleQueryValidation(value);
|
|
||||||
setIsExternalQueryChange(false);
|
|
||||||
}
|
|
||||||
}, [isExternalQueryChange, value]);
|
|
||||||
|
|
||||||
// Enhanced autosuggestion function with context awareness
|
|
||||||
function autoSuggestions(context: CompletionContext): CompletionResult | null {
|
|
||||||
// This matches words before the cursor position
|
|
||||||
// eslint-disable-next-line no-useless-escape
|
|
||||||
const word = context.matchBefore(/[a-zA-Z0-9_.:/?&=#%\-\[\]]*/);
|
|
||||||
if (word?.from === word?.to && !context.explicit) return null;
|
|
||||||
|
|
||||||
// Get the trace operator context at the cursor position
|
|
||||||
const queryContext = getTraceOperatorContextAtCursor(value, cursorPos.ch);
|
|
||||||
|
|
||||||
// Define autocomplete options based on the context
|
|
||||||
let options: {
|
|
||||||
label: string;
|
|
||||||
type: string;
|
|
||||||
info?: string;
|
|
||||||
apply:
|
|
||||||
| string
|
|
||||||
| ((view: EditorView, completion: any, from: number, to: number) => void);
|
|
||||||
detail?: string;
|
|
||||||
boost?: number;
|
|
||||||
}[] = [];
|
|
||||||
|
|
||||||
// Helper function to add space after selection
|
|
||||||
const addSpaceAfterSelection = (
|
|
||||||
view: EditorView,
|
|
||||||
completion: any,
|
|
||||||
from: number,
|
|
||||||
to: number,
|
|
||||||
shouldAddSpace = true,
|
|
||||||
): void => {
|
|
||||||
view.dispatch({
|
|
||||||
changes: {
|
|
||||||
from,
|
|
||||||
to,
|
|
||||||
insert: shouldAddSpace ? `${completion.apply} ` : `${completion.apply}`,
|
|
||||||
},
|
|
||||||
selection: {
|
|
||||||
anchor:
|
|
||||||
from +
|
|
||||||
(shouldAddSpace ? completion.apply.length + 1 : completion.apply.length),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
// Do not reopen here; onUpdate will handle reopening via toggleSuggestions
|
|
||||||
};
|
|
||||||
|
|
||||||
// Helper function to add space after selection to options
|
|
||||||
const addSpaceToOptions = (opts: typeof options): typeof options =>
|
|
||||||
opts.map((option) => {
|
|
||||||
const originalApply = option.apply || option.label;
|
|
||||||
return {
|
|
||||||
...option,
|
|
||||||
apply: (
|
|
||||||
view: EditorView,
|
|
||||||
completion: any,
|
|
||||||
from: number,
|
|
||||||
to: number,
|
|
||||||
): void => {
|
|
||||||
addSpaceAfterSelection(view, { apply: originalApply }, from, to);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
if (queryContext.isInAtom) {
|
|
||||||
// Suggest atoms (identifiers) for trace operators
|
|
||||||
|
|
||||||
const involvedQueries = getInvolvedQueriesInTraceOperator([traceOperator]);
|
|
||||||
|
|
||||||
options = queryOptions.map((option) => ({
|
|
||||||
...option,
|
|
||||||
boost: !involvedQueries.includes(option.apply as string) ? 100 : -99,
|
|
||||||
}));
|
|
||||||
|
|
||||||
// Filter options based on what user is typing
|
|
||||||
const searchText = word?.text.toLowerCase().trim() ?? '';
|
|
||||||
options = options.filter((option) =>
|
|
||||||
option.label.toLowerCase().includes(searchText),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Add space after selection for atoms
|
|
||||||
const optionsWithSpace = addSpaceToOptions(options);
|
|
||||||
|
|
||||||
return {
|
|
||||||
from: word?.from ?? 0,
|
|
||||||
to: word?.to ?? cursorPos.ch,
|
|
||||||
options: optionsWithSpace,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (queryContext.isInOperator) {
|
|
||||||
// Suggest operators for trace operators
|
|
||||||
const operators = Object.values(TRACE_OPERATOR_OPERATORS);
|
|
||||||
options = operators.map((operator) => ({
|
|
||||||
label: TRACE_OPERATOR_OPERATORS_LABELS[operator]
|
|
||||||
? `${operator} (${TRACE_OPERATOR_OPERATORS_LABELS[operator]})`
|
|
||||||
: operator,
|
|
||||||
type: 'operator',
|
|
||||||
apply: operator,
|
|
||||||
boost: TRACE_OPERATOR_OPERATORS_WITH_PRIORITY[operator] * -10,
|
|
||||||
}));
|
|
||||||
|
|
||||||
// Add space after selection for operators
|
|
||||||
const optionsWithSpace = addSpaceToOptions(options);
|
|
||||||
|
|
||||||
return {
|
|
||||||
from: word?.from ?? 0,
|
|
||||||
to: word?.to ?? cursorPos.ch,
|
|
||||||
options: optionsWithSpace,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (queryContext.isInParenthesis) {
|
|
||||||
// Different suggestions based on the context within parenthesis
|
|
||||||
const curChar = value.charAt(cursorPos.ch - 1) || '';
|
|
||||||
|
|
||||||
if (curChar === '(') {
|
|
||||||
// Right after opening parenthesis, suggest atoms or nested expressions
|
|
||||||
options = [
|
|
||||||
{ label: '(', type: 'parenthesis', apply: '(' },
|
|
||||||
...queryOptions,
|
|
||||||
];
|
|
||||||
|
|
||||||
// Add space after selection for opening parenthesis context
|
|
||||||
const optionsWithSpace = addSpaceToOptions(options);
|
|
||||||
|
|
||||||
return {
|
|
||||||
from: word?.from ?? 0,
|
|
||||||
options: optionsWithSpace,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (curChar === ')') {
|
|
||||||
// After closing parenthesis, suggest operators
|
|
||||||
const operators = Object.values(TRACE_OPERATOR_OPERATORS);
|
|
||||||
options = operators.map((operator) => ({
|
|
||||||
label: TRACE_OPERATOR_OPERATORS_LABELS[operator]
|
|
||||||
? `${operator} (${TRACE_OPERATOR_OPERATORS_LABELS[operator]})`
|
|
||||||
: operator,
|
|
||||||
type: 'operator',
|
|
||||||
apply: operator,
|
|
||||||
boost: TRACE_OPERATOR_OPERATORS_WITH_PRIORITY[operator] * -10,
|
|
||||||
}));
|
|
||||||
|
|
||||||
// Add space after selection for closing parenthesis context
|
|
||||||
const optionsWithSpace = addSpaceToOptions(options);
|
|
||||||
|
|
||||||
return {
|
|
||||||
from: word?.from ?? 0,
|
|
||||||
options: optionsWithSpace,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Default: suggest atoms if no specific context
|
|
||||||
options = [
|
|
||||||
...queryOptions,
|
|
||||||
{
|
|
||||||
label: '(',
|
|
||||||
type: 'parenthesis',
|
|
||||||
apply: '(',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
// Filter options based on what user is typing
|
|
||||||
const searchText = word?.text.toLowerCase().trim() ?? '';
|
|
||||||
options = options.filter((option) =>
|
|
||||||
option.label.toLowerCase().includes(searchText),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Add space after selection
|
|
||||||
const optionsWithSpace = addSpaceToOptions(options);
|
|
||||||
|
|
||||||
return {
|
|
||||||
from: word?.from ?? 0,
|
|
||||||
to: word?.to ?? context.pos,
|
|
||||||
options: optionsWithSpace,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleUpdate = useCallback(
|
|
||||||
(viewUpdate: { view: EditorView }): void => {
|
|
||||||
if (!editorRef.current) {
|
|
||||||
editorRef.current = viewUpdate.view;
|
|
||||||
}
|
|
||||||
|
|
||||||
const selection = viewUpdate.view.state.selection.main;
|
|
||||||
const pos = selection.head;
|
|
||||||
|
|
||||||
const lineInfo = viewUpdate.view.state.doc.lineAt(pos);
|
|
||||||
const newPos = {
|
|
||||||
line: lineInfo.number,
|
|
||||||
ch: pos - lineInfo.from,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (newPos.line !== cursorPos.line || newPos.ch !== cursorPos.ch) {
|
|
||||||
setCursorPos(newPos);
|
|
||||||
// Trigger suggestions on context update
|
|
||||||
toggleSuggestions(10);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[cursorPos, toggleSuggestions],
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleChange = (newValue: string): void => {
|
|
||||||
// Mark as internal change to avoid triggering external validation
|
|
||||||
setIsExternalQueryChange(false);
|
|
||||||
setLastExternalValue(newValue);
|
|
||||||
onChange(newValue);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleBlur = (): void => {
|
|
||||||
handleQueryValidation(value);
|
|
||||||
setIsFocused(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Effect to handle focus state and trigger suggestions on focus
|
|
||||||
useEffect(() => {
|
|
||||||
const clearTimeout = toggleSuggestions(10);
|
|
||||||
return (): void => clearTimeout();
|
|
||||||
}, [isFocused, toggleSuggestions]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="code-mirror-where-clause">
|
|
||||||
<div className="query-where-clause-editor-container">
|
|
||||||
<CodeMirror
|
|
||||||
value={value}
|
|
||||||
theme={isDarkMode ? copilot : githubLight}
|
|
||||||
onChange={handleChange}
|
|
||||||
onUpdate={handleUpdate}
|
|
||||||
className={cx('query-where-clause-editor', {
|
|
||||||
isValid: validation.isValid === true,
|
|
||||||
hasErrors: validation.errors.length > 0,
|
|
||||||
})}
|
|
||||||
extensions={[
|
|
||||||
autocompletion({
|
|
||||||
override: [autoSuggestions],
|
|
||||||
defaultKeymap: true,
|
|
||||||
closeOnBlur: true,
|
|
||||||
activateOnTyping: true,
|
|
||||||
maxRenderedOptions: 50,
|
|
||||||
}),
|
|
||||||
javascript({ jsx: false, typescript: false }),
|
|
||||||
EditorView.lineWrapping,
|
|
||||||
stopEventsExtension,
|
|
||||||
Prec.highest(
|
|
||||||
keymap.of([
|
|
||||||
...completionKeymap,
|
|
||||||
{
|
|
||||||
key: 'Escape',
|
|
||||||
run: closeCompletion,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'Enter',
|
|
||||||
preventDefault: true,
|
|
||||||
// Prevent default behavior of Enter to add new line
|
|
||||||
// and instead run a custom action
|
|
||||||
run: (): boolean => true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'Mod-Enter',
|
|
||||||
preventDefault: true,
|
|
||||||
run: (): boolean => {
|
|
||||||
if (onRun && typeof onRun === 'function') {
|
|
||||||
onRun(value);
|
|
||||||
} else {
|
|
||||||
handleRunQuery();
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'Shift-Enter',
|
|
||||||
preventDefault: true,
|
|
||||||
// Prevent default behavior of Shift-Enter to add new line
|
|
||||||
run: (): boolean => true,
|
|
||||||
},
|
|
||||||
]),
|
|
||||||
),
|
|
||||||
]}
|
|
||||||
placeholder={placeholder}
|
|
||||||
basicSetup={{
|
|
||||||
lineNumbers: false,
|
|
||||||
}}
|
|
||||||
onFocus={(): void => {
|
|
||||||
setIsFocused(true);
|
|
||||||
}}
|
|
||||||
onBlur={handleBlur}
|
|
||||||
/>
|
|
||||||
{value && validation.isValid === false && !isFocused && (
|
|
||||||
<div
|
|
||||||
className={cx('query-status-container', {
|
|
||||||
hasErrors: validation.errors.length > 0,
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
<Popover
|
|
||||||
placement="bottomRight"
|
|
||||||
showArrow={false}
|
|
||||||
content={
|
|
||||||
<div className="query-status-content">
|
|
||||||
<div className="query-status-content-header">
|
|
||||||
<div className="query-validation">
|
|
||||||
<div className="query-validation-errors">
|
|
||||||
{validation.errors.map((error) => (
|
|
||||||
<div key={error.message} className="query-validation-error">
|
|
||||||
<div className="query-validation-error">
|
|
||||||
{error.line}:{error.column} - {error.message}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
overlayClassName="query-status-popover"
|
|
||||||
>
|
|
||||||
{validation.isValid ? (
|
|
||||||
<Button
|
|
||||||
type="text"
|
|
||||||
icon={<CheckCircleFilled />}
|
|
||||||
className="periscope-btn ghost"
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<Button
|
|
||||||
type="text"
|
|
||||||
icon={<TriangleAlert size={14} color={Color.BG_CHERRY_500} />}
|
|
||||||
className="periscope-btn ghost"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Popover>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
TraceOperatorEditor.defaultProps = {
|
|
||||||
onRun: undefined,
|
|
||||||
placeholder: 'Enter your trace operator query',
|
|
||||||
};
|
|
||||||
|
|
||||||
export default TraceOperatorEditor;
|
|
||||||
@@ -1,425 +0,0 @@
|
|||||||
/* eslint-disable sonarjs/no-duplicate-string */
|
|
||||||
/* eslint-disable sonarjs/cognitive-complexity */
|
|
||||||
|
|
||||||
import { Token } from 'antlr4';
|
|
||||||
import TraceOperatorGrammarLexer from 'parser/TraceOperatorParser/TraceOperatorGrammarLexer';
|
|
||||||
|
|
||||||
import {
|
|
||||||
createTraceOperatorContext,
|
|
||||||
extractTraceExpressionPairs,
|
|
||||||
getTraceOperatorContextAtCursor,
|
|
||||||
} from '../utils/traceOperatorContextUtils';
|
|
||||||
|
|
||||||
describe('traceOperatorContextUtils', () => {
|
|
||||||
describe('createTraceOperatorContext', () => {
|
|
||||||
it('should create a context object with all required properties', () => {
|
|
||||||
const mockToken = {
|
|
||||||
type: TraceOperatorGrammarLexer.IDENTIFIER,
|
|
||||||
text: 'test',
|
|
||||||
start: 0,
|
|
||||||
stop: 3,
|
|
||||||
} as Token;
|
|
||||||
|
|
||||||
const context = createTraceOperatorContext(
|
|
||||||
mockToken,
|
|
||||||
true,
|
|
||||||
false,
|
|
||||||
false,
|
|
||||||
false,
|
|
||||||
'atom',
|
|
||||||
'operator',
|
|
||||||
[],
|
|
||||||
null,
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(context).toEqual({
|
|
||||||
tokenType: TraceOperatorGrammarLexer.IDENTIFIER,
|
|
||||||
text: 'test',
|
|
||||||
start: 0,
|
|
||||||
stop: 3,
|
|
||||||
currentToken: 'test',
|
|
||||||
isInAtom: true,
|
|
||||||
isInOperator: false,
|
|
||||||
isInParenthesis: false,
|
|
||||||
isInExpression: false,
|
|
||||||
atomToken: 'atom',
|
|
||||||
operatorToken: 'operator',
|
|
||||||
expressionPairs: [],
|
|
||||||
currentPair: null,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should create a context object with default values', () => {
|
|
||||||
const mockToken = {
|
|
||||||
type: TraceOperatorGrammarLexer.IDENTIFIER,
|
|
||||||
text: 'test',
|
|
||||||
start: 0,
|
|
||||||
stop: 3,
|
|
||||||
} as Token;
|
|
||||||
|
|
||||||
const context = createTraceOperatorContext(
|
|
||||||
mockToken,
|
|
||||||
false,
|
|
||||||
true,
|
|
||||||
false,
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(context).toEqual({
|
|
||||||
tokenType: TraceOperatorGrammarLexer.IDENTIFIER,
|
|
||||||
text: 'test',
|
|
||||||
start: 0,
|
|
||||||
stop: 3,
|
|
||||||
currentToken: 'test',
|
|
||||||
isInAtom: false,
|
|
||||||
isInOperator: true,
|
|
||||||
isInParenthesis: false,
|
|
||||||
isInExpression: false,
|
|
||||||
atomToken: undefined,
|
|
||||||
operatorToken: undefined,
|
|
||||||
expressionPairs: [],
|
|
||||||
currentPair: undefined,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('extractTraceExpressionPairs', () => {
|
|
||||||
it('should extract simple expression pair', () => {
|
|
||||||
const query = 'A => B';
|
|
||||||
const result = extractTraceExpressionPairs(query);
|
|
||||||
|
|
||||||
expect(result).toHaveLength(1);
|
|
||||||
expect(result[0].leftAtom).toBe('A');
|
|
||||||
expect(result[0].position.leftStart).toBe(0);
|
|
||||||
expect(result[0].position.leftEnd).toBe(0);
|
|
||||||
expect(result[0].operator).toBe('=>');
|
|
||||||
expect(result[0].position.operatorStart).toBe(2);
|
|
||||||
expect(result[0].position.operatorEnd).toBe(3);
|
|
||||||
expect(result[0].rightAtom).toBe('B');
|
|
||||||
expect(result[0].position.rightStart).toBe(5);
|
|
||||||
expect(result[0].position.rightEnd).toBe(5);
|
|
||||||
expect(result[0].isComplete).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should extract multiple expression pairs', () => {
|
|
||||||
const query = 'A => B && C => D';
|
|
||||||
const result = extractTraceExpressionPairs(query);
|
|
||||||
|
|
||||||
expect(result).toHaveLength(2);
|
|
||||||
|
|
||||||
// First pair: A => B
|
|
||||||
expect(result[0].leftAtom).toBe('A');
|
|
||||||
expect(result[0].operator).toBe('=>');
|
|
||||||
expect(result[0].rightAtom).toBe('B');
|
|
||||||
|
|
||||||
// Second pair: C => D
|
|
||||||
expect(result[1].leftAtom).toBe('C');
|
|
||||||
expect(result[1].operator).toBe('=>');
|
|
||||||
expect(result[1].rightAtom).toBe('D');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should handle NOT operator', () => {
|
|
||||||
const query = 'NOT A => B';
|
|
||||||
const result = extractTraceExpressionPairs(query);
|
|
||||||
|
|
||||||
expect(result).toHaveLength(1);
|
|
||||||
expect(result[0].leftAtom).toBe('A');
|
|
||||||
expect(result[0].operator).toBe('=>');
|
|
||||||
expect(result[0].rightAtom).toBe('B');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should handle parentheses', () => {
|
|
||||||
const query = '(A => B) && (C => D)';
|
|
||||||
const result = extractTraceExpressionPairs(query);
|
|
||||||
|
|
||||||
expect(result).toHaveLength(2);
|
|
||||||
expect(result[0].leftAtom).toBe('A');
|
|
||||||
expect(result[0].rightAtom).toBe('B');
|
|
||||||
expect(result[1].leftAtom).toBe('C');
|
|
||||||
expect(result[1].rightAtom).toBe('D');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should handle incomplete expressions', () => {
|
|
||||||
const query = 'A =>';
|
|
||||||
const result = extractTraceExpressionPairs(query);
|
|
||||||
|
|
||||||
expect(result).toHaveLength(1);
|
|
||||||
expect(result[0].leftAtom).toBe('A');
|
|
||||||
expect(result[0].operator).toBe('=>');
|
|
||||||
expect(result[0].rightAtom).toBeUndefined();
|
|
||||||
expect(result[0].isComplete).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should handle complex nested expressions', () => {
|
|
||||||
const query = 'A => B && (C => D || E => F)';
|
|
||||||
const result = extractTraceExpressionPairs(query);
|
|
||||||
|
|
||||||
expect(result).toHaveLength(3);
|
|
||||||
expect(result[0].leftAtom).toBe('A');
|
|
||||||
expect(result[0].rightAtom).toBe('B');
|
|
||||||
expect(result[1].leftAtom).toBe('C');
|
|
||||||
expect(result[1].rightAtom).toBe('D');
|
|
||||||
expect(result[2].leftAtom).toBe('E');
|
|
||||||
expect(result[2].rightAtom).toBe('F');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should handle whitespace variations', () => {
|
|
||||||
const query = 'A=>B';
|
|
||||||
const result = extractTraceExpressionPairs(query);
|
|
||||||
|
|
||||||
expect(result).toHaveLength(1);
|
|
||||||
expect(result[0].leftAtom).toBe('A');
|
|
||||||
expect(result[0].operator).toBe('=>');
|
|
||||||
expect(result[0].rightAtom).toBe('B');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should handle error cases gracefully', () => {
|
|
||||||
const query = 'invalid syntax @#$%';
|
|
||||||
const result = extractTraceExpressionPairs(query);
|
|
||||||
|
|
||||||
// Should return an array (even if empty or with partial results)
|
|
||||||
expect(Array.isArray(result)).toBe(true);
|
|
||||||
expect(result.length).toBeGreaterThanOrEqual(0);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('getTraceOperatorContextAtCursor', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
// Reset console.error mock
|
|
||||||
jest.spyOn(console, 'error').mockImplementation(() => {});
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
jest.restoreAllMocks();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return default context for empty query', () => {
|
|
||||||
const result = getTraceOperatorContextAtCursor('', 0);
|
|
||||||
|
|
||||||
expect(result).toEqual({
|
|
||||||
tokenType: -1,
|
|
||||||
text: '',
|
|
||||||
start: 0,
|
|
||||||
stop: 0,
|
|
||||||
currentToken: '',
|
|
||||||
isInAtom: true,
|
|
||||||
isInOperator: false,
|
|
||||||
isInParenthesis: false,
|
|
||||||
isInExpression: false,
|
|
||||||
expressionPairs: [],
|
|
||||||
currentPair: null,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return default context for null query', () => {
|
|
||||||
const result = getTraceOperatorContextAtCursor(null as any, 0);
|
|
||||||
|
|
||||||
expect(result).toEqual({
|
|
||||||
tokenType: -1,
|
|
||||||
text: '',
|
|
||||||
start: 0,
|
|
||||||
stop: 0,
|
|
||||||
currentToken: '',
|
|
||||||
isInAtom: true,
|
|
||||||
isInOperator: false,
|
|
||||||
isInParenthesis: false,
|
|
||||||
isInExpression: false,
|
|
||||||
expressionPairs: [],
|
|
||||||
currentPair: null,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return default context for undefined query', () => {
|
|
||||||
const result = getTraceOperatorContextAtCursor(undefined as any, 0);
|
|
||||||
|
|
||||||
expect(result).toEqual({
|
|
||||||
tokenType: -1,
|
|
||||||
text: '',
|
|
||||||
start: 0,
|
|
||||||
stop: 0,
|
|
||||||
currentToken: '',
|
|
||||||
isInAtom: true,
|
|
||||||
isInOperator: false,
|
|
||||||
isInParenthesis: false,
|
|
||||||
isInExpression: false,
|
|
||||||
expressionPairs: [],
|
|
||||||
currentPair: null,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should identify atom context', () => {
|
|
||||||
const query = 'A => B';
|
|
||||||
const result = getTraceOperatorContextAtCursor(query, 0); // cursor at 'A'
|
|
||||||
|
|
||||||
expect(result.atomToken).toBe('A');
|
|
||||||
expect(result.operatorToken).toBe('=>');
|
|
||||||
expect(result.isInAtom).toBe(true);
|
|
||||||
expect(result.isInOperator).toBe(false);
|
|
||||||
expect(result.isInParenthesis).toBe(false);
|
|
||||||
expect(result.start).toBe(0);
|
|
||||||
expect(result.stop).toBe(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should identify operator context', () => {
|
|
||||||
const query = 'A => B';
|
|
||||||
const result = getTraceOperatorContextAtCursor(query, 2); // cursor at '='
|
|
||||||
|
|
||||||
expect(result.atomToken).toBe('A');
|
|
||||||
expect(result.operatorToken).toBeUndefined();
|
|
||||||
expect(result.isInAtom).toBe(false);
|
|
||||||
expect(result.isInOperator).toBe(true);
|
|
||||||
expect(result.isInParenthesis).toBe(false);
|
|
||||||
expect(result.start).toBe(2);
|
|
||||||
expect(result.stop).toBe(2);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should identify parenthesis context', () => {
|
|
||||||
const query = '(A => B)';
|
|
||||||
const result = getTraceOperatorContextAtCursor(query, 0); // cursor at '('
|
|
||||||
|
|
||||||
expect(result.atomToken).toBeUndefined();
|
|
||||||
expect(result.operatorToken).toBeUndefined();
|
|
||||||
expect(result.isInAtom).toBe(false);
|
|
||||||
expect(result.isInOperator).toBe(false);
|
|
||||||
expect(result.isInParenthesis).toBe(true);
|
|
||||||
expect(result.start).toBe(0);
|
|
||||||
expect(result.stop).toBe(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should handle cursor at space', () => {
|
|
||||||
const query = 'A => B';
|
|
||||||
const result = getTraceOperatorContextAtCursor(query, 1); // cursor at space
|
|
||||||
|
|
||||||
expect(result.atomToken).toBe('A');
|
|
||||||
expect(result.operatorToken).toBeUndefined();
|
|
||||||
expect(result.isInAtom).toBe(false);
|
|
||||||
expect(result.isInOperator).toBe(true);
|
|
||||||
expect(result.isInParenthesis).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should handle cursor at end of query', () => {
|
|
||||||
const query = 'A => B';
|
|
||||||
const result = getTraceOperatorContextAtCursor(query, 5); // cursor at end
|
|
||||||
|
|
||||||
expect(result.atomToken).toBe('A');
|
|
||||||
expect(result.operatorToken).toBe('=>');
|
|
||||||
expect(result.isInAtom).toBe(true);
|
|
||||||
expect(result.isInOperator).toBe(false);
|
|
||||||
expect(result.isInParenthesis).toBe(false);
|
|
||||||
expect(result.start).toBe(5);
|
|
||||||
expect(result.stop).toBe(5);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should handle complex query', () => {
|
|
||||||
const query = 'A => B && C => D';
|
|
||||||
const result = getTraceOperatorContextAtCursor(query, 8); // cursor at '&'
|
|
||||||
|
|
||||||
expect(result.atomToken).toBeUndefined();
|
|
||||||
expect(result.operatorToken).toBe('&&');
|
|
||||||
expect(result.isInAtom).toBe(false);
|
|
||||||
expect(result.isInOperator).toBe(true);
|
|
||||||
expect(result.isInParenthesis).toBe(false);
|
|
||||||
expect(result.start).toBe(7);
|
|
||||||
expect(result.stop).toBe(8);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should identify operator position in complex query', () => {
|
|
||||||
const query = 'A => B && C => D';
|
|
||||||
const result = getTraceOperatorContextAtCursor(query, 10); // cursor at 'C'
|
|
||||||
|
|
||||||
expect(result.atomToken).toBe('C');
|
|
||||||
expect(result.operatorToken).toBe('&&');
|
|
||||||
expect(result.isInAtom).toBe(true);
|
|
||||||
expect(result.isInOperator).toBe(false);
|
|
||||||
expect(result.isInParenthesis).toBe(false);
|
|
||||||
expect(result.start).toBe(10);
|
|
||||||
expect(result.stop).toBe(10);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should identify atom position in complex query', () => {
|
|
||||||
const query = 'A => B && C => D';
|
|
||||||
const result = getTraceOperatorContextAtCursor(query, 13); // cursor at '>'
|
|
||||||
|
|
||||||
expect(result.atomToken).toBe('C');
|
|
||||||
expect(result.operatorToken).toBe('=>');
|
|
||||||
expect(result.isInAtom).toBe(false);
|
|
||||||
expect(result.isInOperator).toBe(true);
|
|
||||||
expect(result.isInParenthesis).toBe(false);
|
|
||||||
expect(result.start).toBe(12);
|
|
||||||
expect(result.stop).toBe(13);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should handle transition points', () => {
|
|
||||||
const query = 'A => B';
|
|
||||||
const result = getTraceOperatorContextAtCursor(query, 4); // cursor at 'B'
|
|
||||||
|
|
||||||
expect(result.atomToken).toBe('A');
|
|
||||||
expect(result.operatorToken).toBe('=>');
|
|
||||||
expect(result.isInAtom).toBe(true);
|
|
||||||
expect(result.isInOperator).toBe(false);
|
|
||||||
expect(result.isInParenthesis).toBe(false);
|
|
||||||
expect(result.start).toBe(4);
|
|
||||||
expect(result.stop).toBe(4);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should handle whitespace in complex queries', () => {
|
|
||||||
const query = 'A=>B && C=>D';
|
|
||||||
const result = getTraceOperatorContextAtCursor(query, 6); // cursor at '&'
|
|
||||||
|
|
||||||
expect(result.atomToken).toBeUndefined();
|
|
||||||
expect(result.operatorToken).toBe('&&');
|
|
||||||
expect(result.isInAtom).toBe(false);
|
|
||||||
expect(result.isInOperator).toBe(true);
|
|
||||||
expect(result.isInParenthesis).toBe(false);
|
|
||||||
expect(result.start).toBe(5);
|
|
||||||
expect(result.stop).toBe(6);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should handle NOT operator context', () => {
|
|
||||||
const query = 'NOT A => B';
|
|
||||||
const result = getTraceOperatorContextAtCursor(query, 0); // cursor at 'N'
|
|
||||||
|
|
||||||
expect(result.atomToken).toBeUndefined();
|
|
||||||
expect(result.operatorToken).toBeUndefined();
|
|
||||||
expect(result.isInAtom).toBe(false);
|
|
||||||
expect(result.isInOperator).toBe(false);
|
|
||||||
expect(result.isInParenthesis).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should handle parentheses context', () => {
|
|
||||||
const query = '(A => B)';
|
|
||||||
const result = getTraceOperatorContextAtCursor(query, 1); // cursor at 'A'
|
|
||||||
|
|
||||||
expect(result.atomToken).toBe('A');
|
|
||||||
expect(result.operatorToken).toBe('=>');
|
|
||||||
expect(result.isInAtom).toBe(false);
|
|
||||||
expect(result.isInOperator).toBe(false);
|
|
||||||
expect(result.isInParenthesis).toBe(true);
|
|
||||||
expect(result.start).toBe(0);
|
|
||||||
expect(result.stop).toBe(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should handle expression pairs context', () => {
|
|
||||||
const query = 'A => B && C => D';
|
|
||||||
const result = getTraceOperatorContextAtCursor(query, 5); // cursor at 'A' in "&&"
|
|
||||||
|
|
||||||
expect(result.atomToken).toBe('A');
|
|
||||||
expect(result.operatorToken).toBe('=>');
|
|
||||||
expect(result.isInAtom).toBe(true);
|
|
||||||
expect(result.isInOperator).toBe(false);
|
|
||||||
expect(result.isInParenthesis).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should handle various cursor positions', () => {
|
|
||||||
const query = 'A => B';
|
|
||||||
|
|
||||||
// Test cursor at each position
|
|
||||||
for (let i = 0; i < query.length; i++) {
|
|
||||||
const result = getTraceOperatorContextAtCursor(query, i);
|
|
||||||
expect(result).toBeDefined();
|
|
||||||
expect(typeof result.start).toBe('number');
|
|
||||||
expect(typeof result.stop).toBe('number');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
import { IBuilderTraceOperator } from 'types/api/queryBuilder/queryBuilderData';
|
|
||||||
|
|
||||||
import { getInvolvedQueriesInTraceOperator } from '../utils/utils';
|
|
||||||
|
|
||||||
const makeTraceOperator = (expression: string): IBuilderTraceOperator =>
|
|
||||||
(({ expression } as unknown) as IBuilderTraceOperator);
|
|
||||||
|
|
||||||
describe('getInvolvedQueriesInTraceOperator', () => {
|
|
||||||
it('returns empty array for empty input', () => {
|
|
||||||
const result = getInvolvedQueriesInTraceOperator([]);
|
|
||||||
expect(result).toEqual([]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('extracts identifiers from expression', () => {
|
|
||||||
const result = getInvolvedQueriesInTraceOperator([
|
|
||||||
makeTraceOperator('A => B'),
|
|
||||||
]);
|
|
||||||
expect(result).toEqual(['A', 'B']);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('extracts identifiers from complex expression', () => {
|
|
||||||
const result = getInvolvedQueriesInTraceOperator([
|
|
||||||
makeTraceOperator('A => (NOT B || C)'),
|
|
||||||
]);
|
|
||||||
expect(result).toEqual(['A', 'B', 'C']);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('filters out querynames from complex expression', () => {
|
|
||||||
const result = getInvolvedQueriesInTraceOperator([
|
|
||||||
makeTraceOperator(
|
|
||||||
'(A1 && (NOT B2 || (C3 -> (D4 && E5)))) => ((F6 || G7) && (NOT (H8 -> I9)))',
|
|
||||||
),
|
|
||||||
]);
|
|
||||||
expect(result).toEqual([
|
|
||||||
'A1',
|
|
||||||
'B2',
|
|
||||||
'C3',
|
|
||||||
'D4',
|
|
||||||
'E5',
|
|
||||||
'F6',
|
|
||||||
'G7',
|
|
||||||
'H8',
|
|
||||||
'I9',
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,562 +0,0 @@
|
|||||||
/* eslint-disable sonarjs/cognitive-complexity */
|
|
||||||
/* eslint-disable no-continue */
|
|
||||||
|
|
||||||
import { CharStreams, CommonTokenStream, Token } from 'antlr4';
|
|
||||||
import TraceOperatorGrammarLexer from 'parser/TraceOperatorParser/TraceOperatorGrammarLexer';
|
|
||||||
import { IToken } from 'types/antlrQueryTypes';
|
|
||||||
|
|
||||||
// Trace Operator Context Interface
|
|
||||||
export interface ITraceOperatorContext {
|
|
||||||
tokenType: number;
|
|
||||||
text: string;
|
|
||||||
start: number;
|
|
||||||
stop: number;
|
|
||||||
currentToken: string;
|
|
||||||
isInAtom: boolean;
|
|
||||||
isInOperator: boolean;
|
|
||||||
isInParenthesis: boolean;
|
|
||||||
isInExpression: boolean;
|
|
||||||
atomToken?: string;
|
|
||||||
operatorToken?: string;
|
|
||||||
expressionPairs: ITraceExpressionPair[];
|
|
||||||
currentPair?: ITraceExpressionPair | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Trace Expression Pair Interface
|
|
||||||
export interface ITraceExpressionPair {
|
|
||||||
leftAtom: string;
|
|
||||||
operator: string;
|
|
||||||
rightAtom?: string;
|
|
||||||
rightExpression?: string;
|
|
||||||
position: {
|
|
||||||
leftStart: number;
|
|
||||||
leftEnd: number;
|
|
||||||
operatorStart: number;
|
|
||||||
operatorEnd: number;
|
|
||||||
rightStart?: number;
|
|
||||||
rightEnd?: number;
|
|
||||||
};
|
|
||||||
isComplete: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper functions to determine token types
|
|
||||||
function isAtomToken(tokenType: number): boolean {
|
|
||||||
return tokenType === TraceOperatorGrammarLexer.IDENTIFIER;
|
|
||||||
}
|
|
||||||
|
|
||||||
function isOperatorToken(tokenType: number): boolean {
|
|
||||||
return [
|
|
||||||
TraceOperatorGrammarLexer.T__2, // '=>'
|
|
||||||
TraceOperatorGrammarLexer.T__3, // '&&'
|
|
||||||
TraceOperatorGrammarLexer.T__4, // '||'
|
|
||||||
TraceOperatorGrammarLexer.T__5, // 'NOT'
|
|
||||||
TraceOperatorGrammarLexer.T__6, // '->'
|
|
||||||
].includes(tokenType);
|
|
||||||
}
|
|
||||||
|
|
||||||
function isParenthesisToken(tokenType: number): boolean {
|
|
||||||
return (
|
|
||||||
tokenType === TraceOperatorGrammarLexer.T__0 ||
|
|
||||||
tokenType === TraceOperatorGrammarLexer.T__1
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function isOpeningParenthesis(tokenType: number): boolean {
|
|
||||||
return tokenType === TraceOperatorGrammarLexer.T__0;
|
|
||||||
}
|
|
||||||
|
|
||||||
function isClosingParenthesis(tokenType: number): boolean {
|
|
||||||
return tokenType === TraceOperatorGrammarLexer.T__1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Function to create a context object
|
|
||||||
export function createTraceOperatorContext(
|
|
||||||
token: Token,
|
|
||||||
isInAtom: boolean,
|
|
||||||
isInOperator: boolean,
|
|
||||||
isInParenthesis: boolean,
|
|
||||||
isInExpression: boolean,
|
|
||||||
atomToken?: string,
|
|
||||||
operatorToken?: string,
|
|
||||||
expressionPairs?: ITraceExpressionPair[],
|
|
||||||
currentPair?: ITraceExpressionPair | null,
|
|
||||||
): ITraceOperatorContext {
|
|
||||||
return {
|
|
||||||
tokenType: token.type,
|
|
||||||
text: token.text || '',
|
|
||||||
start: token.start,
|
|
||||||
stop: token.stop,
|
|
||||||
currentToken: token.text || '',
|
|
||||||
isInAtom,
|
|
||||||
isInOperator,
|
|
||||||
isInParenthesis,
|
|
||||||
isInExpression,
|
|
||||||
atomToken,
|
|
||||||
operatorToken,
|
|
||||||
expressionPairs: expressionPairs || [],
|
|
||||||
currentPair,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper to determine token context
|
|
||||||
function determineTraceTokenContext(
|
|
||||||
token: IToken,
|
|
||||||
): {
|
|
||||||
isInAtom: boolean;
|
|
||||||
isInOperator: boolean;
|
|
||||||
isInParenthesis: boolean;
|
|
||||||
isInExpression: boolean;
|
|
||||||
} {
|
|
||||||
const tokenType = token.type;
|
|
||||||
|
|
||||||
return {
|
|
||||||
isInAtom: isAtomToken(tokenType),
|
|
||||||
isInOperator: isOperatorToken(tokenType),
|
|
||||||
isInParenthesis: isParenthesisToken(tokenType),
|
|
||||||
isInExpression: false, // Will be determined by broader context
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Extracts all expression pairs from a trace operator query string
|
|
||||||
* This parses the query according to the TraceOperatorGrammar.g4 grammar
|
|
||||||
*
|
|
||||||
* @param query The trace operator query string to parse
|
|
||||||
* @returns An array of ITraceExpressionPair objects representing the expression pairs
|
|
||||||
*/
|
|
||||||
export function extractTraceExpressionPairs(
|
|
||||||
query: string,
|
|
||||||
): ITraceExpressionPair[] {
|
|
||||||
try {
|
|
||||||
const input = query || '';
|
|
||||||
const chars = CharStreams.fromString(input);
|
|
||||||
const lexer = new TraceOperatorGrammarLexer(chars);
|
|
||||||
|
|
||||||
const tokenStream = new CommonTokenStream(lexer);
|
|
||||||
tokenStream.fill();
|
|
||||||
|
|
||||||
const allTokens = tokenStream.tokens as IToken[];
|
|
||||||
const expressionPairs: ITraceExpressionPair[] = [];
|
|
||||||
let currentPair: Partial<ITraceExpressionPair> | null = null;
|
|
||||||
|
|
||||||
let i = 0;
|
|
||||||
while (i < allTokens.length) {
|
|
||||||
const token = allTokens[i];
|
|
||||||
i++;
|
|
||||||
|
|
||||||
// Skip EOF and whitespace tokens
|
|
||||||
if (token.type === TraceOperatorGrammarLexer.EOF || token.channel !== 0) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If token is an IDENTIFIER (atom), start or continue a pair
|
|
||||||
if (isAtomToken(token.type)) {
|
|
||||||
// If we don't have a current pair, start one
|
|
||||||
if (!currentPair) {
|
|
||||||
currentPair = {
|
|
||||||
leftAtom: token.text,
|
|
||||||
position: {
|
|
||||||
leftStart: token.start,
|
|
||||||
leftEnd: token.stop,
|
|
||||||
operatorStart: 0,
|
|
||||||
operatorEnd: 0,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
// If we have a current pair but no operator yet, this is still the left atom
|
|
||||||
else if (!currentPair.operator && currentPair.position) {
|
|
||||||
currentPair.leftAtom = token.text;
|
|
||||||
currentPair.position.leftStart = token.start;
|
|
||||||
currentPair.position.leftEnd = token.stop;
|
|
||||||
}
|
|
||||||
// If we have an operator, this is the right atom
|
|
||||||
else if (
|
|
||||||
currentPair.operator &&
|
|
||||||
!currentPair.rightAtom &&
|
|
||||||
currentPair.position
|
|
||||||
) {
|
|
||||||
currentPair.rightAtom = token.text;
|
|
||||||
currentPair.position.rightStart = token.start;
|
|
||||||
currentPair.position.rightEnd = token.stop;
|
|
||||||
currentPair.isComplete = true;
|
|
||||||
|
|
||||||
// Add the completed pair to the result
|
|
||||||
expressionPairs.push(currentPair as ITraceExpressionPair);
|
|
||||||
currentPair = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// If token is an operator and we have a left atom
|
|
||||||
else if (
|
|
||||||
isOperatorToken(token.type) &&
|
|
||||||
currentPair &&
|
|
||||||
currentPair.leftAtom &&
|
|
||||||
currentPair.position
|
|
||||||
) {
|
|
||||||
currentPair.operator = token.text;
|
|
||||||
currentPair.position.operatorStart = token.start;
|
|
||||||
currentPair.position.operatorEnd = token.stop;
|
|
||||||
|
|
||||||
// If this is a NOT operator, it might be followed by another operator
|
|
||||||
if (token.type === TraceOperatorGrammarLexer.T__5 && i < allTokens.length) {
|
|
||||||
// Look ahead for the next operator
|
|
||||||
const nextToken = allTokens[i];
|
|
||||||
if (isOperatorToken(nextToken.type) && nextToken.channel === 0) {
|
|
||||||
currentPair.operator = `${token.text} ${nextToken.text}`;
|
|
||||||
currentPair.position.operatorEnd = nextToken.stop;
|
|
||||||
i++; // Skip the next token since we've consumed it
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// If token is an opening parenthesis after an operator, this is a right expression
|
|
||||||
else if (
|
|
||||||
isOpeningParenthesis(token.type) &&
|
|
||||||
currentPair &&
|
|
||||||
currentPair.operator &&
|
|
||||||
!currentPair.rightAtom &&
|
|
||||||
currentPair.position
|
|
||||||
) {
|
|
||||||
// Find the matching closing parenthesis
|
|
||||||
let parenCount = 1;
|
|
||||||
let j = i;
|
|
||||||
let rightExpression = '';
|
|
||||||
const rightStart = token.start;
|
|
||||||
let rightEnd = token.stop;
|
|
||||||
|
|
||||||
while (j < allTokens.length && parenCount > 0) {
|
|
||||||
const parenToken = allTokens[j];
|
|
||||||
if (parenToken.channel === 0) {
|
|
||||||
if (isOpeningParenthesis(parenToken.type)) {
|
|
||||||
parenCount++;
|
|
||||||
} else if (isClosingParenthesis(parenToken.type)) {
|
|
||||||
parenCount--;
|
|
||||||
if (parenCount === 0) {
|
|
||||||
rightEnd = parenToken.stop;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
rightExpression += parenToken.text;
|
|
||||||
j++;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (parenCount === 0) {
|
|
||||||
currentPair.rightExpression = rightExpression;
|
|
||||||
currentPair.position.rightStart = rightStart;
|
|
||||||
currentPair.position.rightEnd = rightEnd;
|
|
||||||
currentPair.isComplete = true;
|
|
||||||
|
|
||||||
// Add the completed pair to the result
|
|
||||||
expressionPairs.push(currentPair as ITraceExpressionPair);
|
|
||||||
currentPair = null;
|
|
||||||
|
|
||||||
// Skip to the end of the expression
|
|
||||||
i = j;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add any remaining incomplete pair
|
|
||||||
if (currentPair && currentPair.leftAtom && currentPair.position) {
|
|
||||||
expressionPairs.push({
|
|
||||||
...currentPair,
|
|
||||||
isComplete: !!(currentPair.leftAtom && currentPair.operator),
|
|
||||||
} as ITraceExpressionPair);
|
|
||||||
}
|
|
||||||
|
|
||||||
return expressionPairs;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error in extractTraceExpressionPairs:', error);
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the current expression pair at the cursor position
|
|
||||||
*
|
|
||||||
* @param expressionPairs An array of ITraceExpressionPair objects
|
|
||||||
* @param query The full query string
|
|
||||||
* @param cursorIndex The position of the cursor in the query
|
|
||||||
* @returns The expression pair at the cursor position, or null if not found
|
|
||||||
*/
|
|
||||||
export function getCurrentTraceExpressionPair(
|
|
||||||
expressionPairs: ITraceExpressionPair[],
|
|
||||||
cursorIndex: number,
|
|
||||||
): ITraceExpressionPair | null {
|
|
||||||
try {
|
|
||||||
if (expressionPairs.length === 0) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find the rightmost pair whose end position is before or at the cursor
|
|
||||||
let bestMatch: ITraceExpressionPair | null = null;
|
|
||||||
|
|
||||||
// eslint-disable-next-line no-restricted-syntax
|
|
||||||
for (const pair of expressionPairs) {
|
|
||||||
const { position } = pair;
|
|
||||||
const pairEnd =
|
|
||||||
position.rightEnd || position.operatorEnd || position.leftEnd;
|
|
||||||
const pairStart = position.leftStart;
|
|
||||||
|
|
||||||
// If this pair ends at or before the cursor, and it's further right than our previous best match
|
|
||||||
if (
|
|
||||||
pairStart <= cursorIndex &&
|
|
||||||
cursorIndex <= pairEnd + 1 &&
|
|
||||||
(!bestMatch ||
|
|
||||||
pairEnd >
|
|
||||||
(bestMatch.position.rightEnd ||
|
|
||||||
bestMatch.position.operatorEnd ||
|
|
||||||
bestMatch.position.leftEnd))
|
|
||||||
) {
|
|
||||||
bestMatch = pair;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return bestMatch;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error in getCurrentTraceExpressionPair:', error);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the current trace operator context at the cursor position
|
|
||||||
* This is useful for determining what kind of suggestions to show
|
|
||||||
*
|
|
||||||
* @param query The trace operator query string
|
|
||||||
* @param cursorIndex The position of the cursor in the query
|
|
||||||
* @returns The trace operator context at the cursor position
|
|
||||||
*/
|
|
||||||
export function getTraceOperatorContextAtCursor(
|
|
||||||
query: string,
|
|
||||||
cursorIndex: number,
|
|
||||||
): ITraceOperatorContext {
|
|
||||||
try {
|
|
||||||
// Guard against infinite recursion
|
|
||||||
const stackTrace = new Error().stack || '';
|
|
||||||
const callCount = (stackTrace.match(/getTraceOperatorContextAtCursor/g) || [])
|
|
||||||
.length;
|
|
||||||
if (callCount > 3) {
|
|
||||||
console.warn(
|
|
||||||
'Potential infinite recursion detected in getTraceOperatorContextAtCursor',
|
|
||||||
);
|
|
||||||
return {
|
|
||||||
tokenType: -1,
|
|
||||||
text: '',
|
|
||||||
start: cursorIndex,
|
|
||||||
stop: cursorIndex,
|
|
||||||
currentToken: '',
|
|
||||||
isInAtom: true,
|
|
||||||
isInOperator: false,
|
|
||||||
isInParenthesis: false,
|
|
||||||
isInExpression: false,
|
|
||||||
expressionPairs: [],
|
|
||||||
currentPair: null,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create input stream and lexer
|
|
||||||
const input = query || '';
|
|
||||||
const chars = CharStreams.fromString(input);
|
|
||||||
const lexer = new TraceOperatorGrammarLexer(chars);
|
|
||||||
|
|
||||||
const tokenStream = new CommonTokenStream(lexer);
|
|
||||||
tokenStream.fill();
|
|
||||||
|
|
||||||
const allTokens = tokenStream.tokens as IToken[];
|
|
||||||
|
|
||||||
// Get expression pairs information
|
|
||||||
const expressionPairs = extractTraceExpressionPairs(query);
|
|
||||||
const currentPair = getCurrentTraceExpressionPair(
|
|
||||||
expressionPairs,
|
|
||||||
cursorIndex,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Find the token at or just before the cursor
|
|
||||||
let lastTokenBeforeCursor: IToken | null = null;
|
|
||||||
for (let i = 0; i < allTokens.length; i++) {
|
|
||||||
const token = allTokens[i];
|
|
||||||
if (token.type === TraceOperatorGrammarLexer.EOF) continue;
|
|
||||||
|
|
||||||
if (token.stop < cursorIndex || token.stop + 1 === cursorIndex) {
|
|
||||||
lastTokenBeforeCursor = token;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (token.start > cursorIndex) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find exact token at cursor
|
|
||||||
let exactToken: IToken | null = null;
|
|
||||||
for (let i = 0; i < allTokens.length; i++) {
|
|
||||||
const token = allTokens[i];
|
|
||||||
if (token.type === TraceOperatorGrammarLexer.EOF) continue;
|
|
||||||
|
|
||||||
if (token.start <= cursorIndex && cursorIndex <= token.stop + 1) {
|
|
||||||
exactToken = token;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we don't have any tokens, return default context
|
|
||||||
if (!lastTokenBeforeCursor && !exactToken) {
|
|
||||||
return {
|
|
||||||
tokenType: -1,
|
|
||||||
text: '',
|
|
||||||
start: cursorIndex,
|
|
||||||
stop: cursorIndex,
|
|
||||||
currentToken: '',
|
|
||||||
isInAtom: true, // Default to atom context when input is empty
|
|
||||||
isInOperator: false,
|
|
||||||
isInParenthesis: false,
|
|
||||||
isInExpression: false,
|
|
||||||
expressionPairs,
|
|
||||||
currentPair: null,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if cursor is at a space after a token (transition point)
|
|
||||||
const isAtSpace = cursorIndex < query.length && query[cursorIndex] === ' ';
|
|
||||||
const isAfterSpace = cursorIndex > 0 && query[cursorIndex - 1] === ' ';
|
|
||||||
const isAfterToken = cursorIndex > 0 && query[cursorIndex - 1] !== ' ';
|
|
||||||
const isTransitionPoint =
|
|
||||||
(isAtSpace && isAfterToken) ||
|
|
||||||
(cursorIndex === query.length && isAfterToken);
|
|
||||||
|
|
||||||
// If we're at a transition point after a token, progress the context
|
|
||||||
if (
|
|
||||||
lastTokenBeforeCursor &&
|
|
||||||
(isAtSpace || isAfterSpace || isTransitionPoint)
|
|
||||||
) {
|
|
||||||
const lastTokenContext = determineTraceTokenContext(lastTokenBeforeCursor);
|
|
||||||
|
|
||||||
// Apply context progression: atom → operator → atom/expression → operator → atom
|
|
||||||
if (lastTokenContext.isInAtom) {
|
|
||||||
// After atom + space, move to operator context
|
|
||||||
return {
|
|
||||||
tokenType: lastTokenBeforeCursor.type,
|
|
||||||
text: lastTokenBeforeCursor.text,
|
|
||||||
start: cursorIndex,
|
|
||||||
stop: cursorIndex,
|
|
||||||
currentToken: lastTokenBeforeCursor.text,
|
|
||||||
isInAtom: false,
|
|
||||||
isInOperator: true,
|
|
||||||
isInParenthesis: false,
|
|
||||||
isInExpression: false,
|
|
||||||
atomToken: lastTokenBeforeCursor.text,
|
|
||||||
expressionPairs,
|
|
||||||
currentPair,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (lastTokenContext.isInOperator) {
|
|
||||||
// After operator + space, move to atom/expression context
|
|
||||||
return {
|
|
||||||
tokenType: lastTokenBeforeCursor.type,
|
|
||||||
text: lastTokenBeforeCursor.text,
|
|
||||||
start: cursorIndex,
|
|
||||||
stop: cursorIndex,
|
|
||||||
currentToken: lastTokenBeforeCursor.text,
|
|
||||||
isInAtom: true, // Expecting an atom or expression after operator
|
|
||||||
isInOperator: false,
|
|
||||||
isInParenthesis: false,
|
|
||||||
isInExpression: false,
|
|
||||||
operatorToken: lastTokenBeforeCursor.text,
|
|
||||||
atomToken: currentPair?.leftAtom,
|
|
||||||
expressionPairs,
|
|
||||||
currentPair,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
lastTokenContext.isInParenthesis &&
|
|
||||||
isClosingParenthesis(lastTokenBeforeCursor.type)
|
|
||||||
) {
|
|
||||||
// After closing parenthesis, move to operator context
|
|
||||||
return {
|
|
||||||
tokenType: lastTokenBeforeCursor.type,
|
|
||||||
text: lastTokenBeforeCursor.text,
|
|
||||||
start: cursorIndex,
|
|
||||||
stop: cursorIndex,
|
|
||||||
currentToken: lastTokenBeforeCursor.text,
|
|
||||||
isInAtom: false,
|
|
||||||
isInOperator: true,
|
|
||||||
isInParenthesis: false,
|
|
||||||
isInExpression: false,
|
|
||||||
expressionPairs,
|
|
||||||
currentPair,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If cursor is at the end of a token, return the current token context
|
|
||||||
if (exactToken && cursorIndex === exactToken.stop + 1) {
|
|
||||||
const tokenContext = determineTraceTokenContext(exactToken);
|
|
||||||
|
|
||||||
return {
|
|
||||||
tokenType: exactToken.type,
|
|
||||||
text: exactToken.text,
|
|
||||||
start: exactToken.start,
|
|
||||||
stop: exactToken.stop,
|
|
||||||
currentToken: exactToken.text,
|
|
||||||
...tokenContext,
|
|
||||||
atomToken: tokenContext.isInAtom ? exactToken.text : currentPair?.leftAtom,
|
|
||||||
operatorToken: tokenContext.isInOperator
|
|
||||||
? exactToken.text
|
|
||||||
: currentPair?.operator,
|
|
||||||
expressionPairs,
|
|
||||||
currentPair,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Regular token-based context detection
|
|
||||||
if (exactToken?.channel === 0) {
|
|
||||||
const tokenContext = determineTraceTokenContext(exactToken);
|
|
||||||
|
|
||||||
return {
|
|
||||||
tokenType: exactToken.type,
|
|
||||||
text: exactToken.text,
|
|
||||||
start: exactToken.start,
|
|
||||||
stop: exactToken.stop,
|
|
||||||
currentToken: exactToken.text,
|
|
||||||
...tokenContext,
|
|
||||||
atomToken: tokenContext.isInAtom ? exactToken.text : currentPair?.leftAtom,
|
|
||||||
operatorToken: tokenContext.isInOperator
|
|
||||||
? exactToken.text
|
|
||||||
: currentPair?.operator,
|
|
||||||
expressionPairs,
|
|
||||||
currentPair,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Default fallback to atom context
|
|
||||||
return {
|
|
||||||
tokenType: -1,
|
|
||||||
text: '',
|
|
||||||
start: cursorIndex,
|
|
||||||
stop: cursorIndex,
|
|
||||||
currentToken: '',
|
|
||||||
isInAtom: true,
|
|
||||||
isInOperator: false,
|
|
||||||
isInParenthesis: false,
|
|
||||||
isInExpression: false,
|
|
||||||
expressionPairs,
|
|
||||||
currentPair,
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error in getTraceOperatorContextAtCursor:', error);
|
|
||||||
return {
|
|
||||||
tokenType: -1,
|
|
||||||
text: '',
|
|
||||||
start: cursorIndex,
|
|
||||||
stop: cursorIndex,
|
|
||||||
currentToken: '',
|
|
||||||
isInAtom: true,
|
|
||||||
isInOperator: false,
|
|
||||||
isInParenthesis: false,
|
|
||||||
isInExpression: false,
|
|
||||||
expressionPairs: [],
|
|
||||||
currentPair: null,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
import { IBuilderTraceOperator } from 'types/api/queryBuilder/queryBuilderData';
|
|
||||||
|
|
||||||
export const getInvolvedQueriesInTraceOperator = (
|
|
||||||
traceOperators: IBuilderTraceOperator[],
|
|
||||||
): string[] => {
|
|
||||||
if (
|
|
||||||
!traceOperators ||
|
|
||||||
traceOperators.length === 0 ||
|
|
||||||
traceOperators.length > 1
|
|
||||||
)
|
|
||||||
return [];
|
|
||||||
|
|
||||||
const currentTraceOperator = traceOperators[0];
|
|
||||||
|
|
||||||
// Match any word starting with letter or underscore
|
|
||||||
const tokens =
|
|
||||||
currentTraceOperator.expression.match(/\b[A-Za-z_][A-Za-z0-9_]*\b/g) || [];
|
|
||||||
|
|
||||||
// Filter out operator keywords
|
|
||||||
const operators = new Set(['NOT']);
|
|
||||||
return tokens.filter((t) => !operators.has(t));
|
|
||||||
};
|
|
||||||
File diff suppressed because it is too large
Load Diff
386
frontend/src/components/QueryBuilderV2/utils.test.ts
Normal file
386
frontend/src/components/QueryBuilderV2/utils.test.ts
Normal file
@@ -0,0 +1,386 @@
|
|||||||
|
/* eslint-disable sonarjs/no-duplicate-string */
|
||||||
|
import { negateOperator, OPERATORS } from 'constants/antlrQueryConstants';
|
||||||
|
import { extractQueryPairs } from 'utils/queryContextUtils';
|
||||||
|
|
||||||
|
// Now import the function after all mocks are set up
|
||||||
|
import { convertFiltersToExpressionWithExistingQuery } from './utils';
|
||||||
|
|
||||||
|
jest.mock('utils/queryContextUtils', () => ({
|
||||||
|
extractQueryPairs: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Type the mocked functions
|
||||||
|
const mockExtractQueryPairs = extractQueryPairs as jest.MockedFunction<
|
||||||
|
typeof extractQueryPairs
|
||||||
|
>;
|
||||||
|
|
||||||
|
describe('convertFiltersToExpressionWithExistingQuery', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return filters with new expression when no existing query', () => {
|
||||||
|
const filters = {
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
id: '1',
|
||||||
|
key: { id: 'service.name', key: 'service.name', type: 'string' },
|
||||||
|
op: OPERATORS['='],
|
||||||
|
value: 'test-service',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
op: 'AND',
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = convertFiltersToExpressionWithExistingQuery(
|
||||||
|
filters,
|
||||||
|
undefined,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result.filters).toEqual(filters);
|
||||||
|
expect(result.filter.expression).toBe("service.name = 'test-service'");
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should handle empty filters', () => {
|
||||||
|
const filters = {
|
||||||
|
items: [],
|
||||||
|
op: 'AND',
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = convertFiltersToExpressionWithExistingQuery(
|
||||||
|
filters,
|
||||||
|
undefined,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result.filters).toEqual(filters);
|
||||||
|
expect(result.filter.expression).toBe('');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should handle existing query with matching filters', () => {
|
||||||
|
const filters = {
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
id: '1',
|
||||||
|
key: { id: 'service.name', key: 'service.name', type: 'string' },
|
||||||
|
op: OPERATORS['='],
|
||||||
|
value: 'updated-service',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
op: 'AND',
|
||||||
|
};
|
||||||
|
|
||||||
|
const existingQuery = "service.name = 'old-service'";
|
||||||
|
|
||||||
|
// Mock extractQueryPairs to return query pairs with position information
|
||||||
|
mockExtractQueryPairs.mockReturnValue([
|
||||||
|
{
|
||||||
|
key: 'service.name',
|
||||||
|
operator: OPERATORS['='],
|
||||||
|
value: "'old-service'",
|
||||||
|
hasNegation: false,
|
||||||
|
isMultiValue: false,
|
||||||
|
isComplete: true,
|
||||||
|
position: {
|
||||||
|
keyStart: 0,
|
||||||
|
keyEnd: 11,
|
||||||
|
operatorStart: 13,
|
||||||
|
operatorEnd: 13,
|
||||||
|
valueStart: 15,
|
||||||
|
valueEnd: 28,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const result = convertFiltersToExpressionWithExistingQuery(
|
||||||
|
filters,
|
||||||
|
existingQuery,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result.filters).toBeDefined();
|
||||||
|
expect(result.filter).toBeDefined();
|
||||||
|
expect(result.filter.expression).toBe("service.name = 'old-service'");
|
||||||
|
expect(mockExtractQueryPairs).toHaveBeenCalledWith(
|
||||||
|
"service.name = 'old-service'",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should handle IN operator with existing query', () => {
|
||||||
|
const filters = {
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
id: '1',
|
||||||
|
key: { id: 'service.name', key: 'service.name', type: 'string' },
|
||||||
|
op: OPERATORS.IN,
|
||||||
|
value: ['service1', 'service2'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
op: 'AND',
|
||||||
|
};
|
||||||
|
|
||||||
|
const existingQuery = "service.name IN ['old-service']";
|
||||||
|
|
||||||
|
mockExtractQueryPairs.mockReturnValue([
|
||||||
|
{
|
||||||
|
key: 'service.name',
|
||||||
|
operator: 'IN',
|
||||||
|
value: "['old-service']",
|
||||||
|
valueList: ["'old-service'"],
|
||||||
|
valuesPosition: [
|
||||||
|
{
|
||||||
|
start: 17,
|
||||||
|
end: 29,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
hasNegation: false,
|
||||||
|
isMultiValue: true,
|
||||||
|
position: {
|
||||||
|
keyStart: 0,
|
||||||
|
keyEnd: 11,
|
||||||
|
operatorStart: 13,
|
||||||
|
operatorEnd: 14,
|
||||||
|
valueStart: 16,
|
||||||
|
valueEnd: 30,
|
||||||
|
negationStart: 0,
|
||||||
|
negationEnd: 0,
|
||||||
|
},
|
||||||
|
isComplete: true,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const result = convertFiltersToExpressionWithExistingQuery(
|
||||||
|
filters,
|
||||||
|
existingQuery,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result.filters).toBeDefined();
|
||||||
|
expect(result.filter).toBeDefined();
|
||||||
|
// The function is currently returning the new value but with extra characters
|
||||||
|
expect(result.filter.expression).toBe(
|
||||||
|
"service.name IN ['service1', 'service2']",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should handle IN operator conversion from equals', () => {
|
||||||
|
const filters = {
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
id: '1',
|
||||||
|
key: { id: 'service.name', key: 'service.name', type: 'string' },
|
||||||
|
op: OPERATORS.IN,
|
||||||
|
value: ['service1', 'service2'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
op: 'AND',
|
||||||
|
};
|
||||||
|
|
||||||
|
const existingQuery = "service.name = 'old-service'";
|
||||||
|
|
||||||
|
mockExtractQueryPairs.mockReturnValue([
|
||||||
|
{
|
||||||
|
key: 'service.name',
|
||||||
|
operator: OPERATORS['='],
|
||||||
|
value: "'old-service'",
|
||||||
|
hasNegation: false,
|
||||||
|
isMultiValue: false,
|
||||||
|
isComplete: true,
|
||||||
|
position: {
|
||||||
|
keyStart: 0,
|
||||||
|
keyEnd: 11,
|
||||||
|
operatorStart: 13,
|
||||||
|
operatorEnd: 13,
|
||||||
|
valueStart: 15,
|
||||||
|
valueEnd: 28,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const result = convertFiltersToExpressionWithExistingQuery(
|
||||||
|
filters,
|
||||||
|
existingQuery,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result.filters.items).toHaveLength(1);
|
||||||
|
// The function is currently returning the new value but with extra characters
|
||||||
|
expect(result.filter.expression).toBe(
|
||||||
|
"service.name IN ['service1', 'service2'] ",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should handle NOT IN operator conversion from not equals', () => {
|
||||||
|
const filters = {
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
id: '1',
|
||||||
|
key: { id: 'service.name', key: 'service.name', type: 'string' },
|
||||||
|
op: negateOperator(OPERATORS.IN),
|
||||||
|
value: ['service1', 'service2'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
op: 'AND',
|
||||||
|
};
|
||||||
|
|
||||||
|
const existingQuery = "service.name != 'old-service'";
|
||||||
|
|
||||||
|
mockExtractQueryPairs.mockReturnValue([
|
||||||
|
{
|
||||||
|
key: 'service.name',
|
||||||
|
operator: OPERATORS['!='],
|
||||||
|
value: "'old-service'",
|
||||||
|
hasNegation: false,
|
||||||
|
isMultiValue: false,
|
||||||
|
isComplete: true,
|
||||||
|
position: {
|
||||||
|
keyStart: 0,
|
||||||
|
keyEnd: 11,
|
||||||
|
operatorStart: 13,
|
||||||
|
operatorEnd: 14,
|
||||||
|
valueStart: 16,
|
||||||
|
valueEnd: 28,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const result = convertFiltersToExpressionWithExistingQuery(
|
||||||
|
filters,
|
||||||
|
existingQuery,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result.filters.items).toHaveLength(1);
|
||||||
|
// The function is currently returning the new value but with extra characters
|
||||||
|
expect(result.filter.expression).toBe(
|
||||||
|
"service.name NOT IN ['service1', 'service2'] ",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should add new filters when they do not exist in existing query', () => {
|
||||||
|
const filters = {
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
id: '1',
|
||||||
|
key: { id: 'new.key', key: 'new.key', type: 'string' },
|
||||||
|
op: OPERATORS['='],
|
||||||
|
value: 'new-value',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
op: 'AND',
|
||||||
|
};
|
||||||
|
|
||||||
|
const existingQuery = "service.name = 'old-service'";
|
||||||
|
|
||||||
|
mockExtractQueryPairs.mockReturnValue([
|
||||||
|
{
|
||||||
|
key: 'service.name',
|
||||||
|
operator: OPERATORS['='],
|
||||||
|
value: "'old-service'",
|
||||||
|
hasNegation: false,
|
||||||
|
isMultiValue: false,
|
||||||
|
isComplete: true,
|
||||||
|
position: {
|
||||||
|
keyStart: 0,
|
||||||
|
keyEnd: 11,
|
||||||
|
operatorStart: 13,
|
||||||
|
operatorEnd: 13,
|
||||||
|
valueStart: 15,
|
||||||
|
valueEnd: 28,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const result = convertFiltersToExpressionWithExistingQuery(
|
||||||
|
filters,
|
||||||
|
existingQuery,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result.filters.items).toHaveLength(2); // Original + new filter
|
||||||
|
expect(result.filter.expression).toBe(
|
||||||
|
"service.name = 'old-service' new.key = 'new-value'",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should handle simple value replacement', () => {
|
||||||
|
const filters = {
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
id: '1',
|
||||||
|
key: { id: 'status', key: 'status', type: 'string' },
|
||||||
|
op: OPERATORS['='],
|
||||||
|
value: 'error',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
op: 'AND',
|
||||||
|
};
|
||||||
|
|
||||||
|
const existingQuery = "status = 'success'";
|
||||||
|
|
||||||
|
mockExtractQueryPairs.mockReturnValue([
|
||||||
|
{
|
||||||
|
key: 'status',
|
||||||
|
operator: OPERATORS['='],
|
||||||
|
value: "'success'",
|
||||||
|
hasNegation: false,
|
||||||
|
isMultiValue: false,
|
||||||
|
isComplete: true,
|
||||||
|
position: {
|
||||||
|
keyStart: 0,
|
||||||
|
keyEnd: 6,
|
||||||
|
operatorStart: 8,
|
||||||
|
operatorEnd: 8,
|
||||||
|
valueStart: 10,
|
||||||
|
valueEnd: 19,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const result = convertFiltersToExpressionWithExistingQuery(
|
||||||
|
filters,
|
||||||
|
existingQuery,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result.filters.items).toHaveLength(1);
|
||||||
|
// The function is currently returning the original expression (until we fix the replacement logic)
|
||||||
|
expect(result.filter.expression).toBe("status = 'success'");
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should handle filters with no key gracefully', () => {
|
||||||
|
const filters = {
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
id: '1',
|
||||||
|
key: undefined,
|
||||||
|
op: OPERATORS['='],
|
||||||
|
value: 'test-value',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
op: 'AND',
|
||||||
|
};
|
||||||
|
|
||||||
|
const existingQuery = "service.name = 'old-service'";
|
||||||
|
|
||||||
|
mockExtractQueryPairs.mockReturnValue([
|
||||||
|
{
|
||||||
|
key: 'service.name',
|
||||||
|
operator: OPERATORS['='],
|
||||||
|
value: "'old-service'",
|
||||||
|
hasNegation: false,
|
||||||
|
isMultiValue: false,
|
||||||
|
isComplete: true,
|
||||||
|
position: {
|
||||||
|
keyStart: 0,
|
||||||
|
keyEnd: 11,
|
||||||
|
operatorStart: 13,
|
||||||
|
operatorEnd: 13,
|
||||||
|
valueStart: 15,
|
||||||
|
valueEnd: 28,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const result = convertFiltersToExpressionWithExistingQuery(
|
||||||
|
filters,
|
||||||
|
existingQuery,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result.filters.items).toHaveLength(2); // Original + new filter (even though it has no key)
|
||||||
|
expect(result.filter.expression).toBe("service.name = 'old-service'");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,10 +1,6 @@
|
|||||||
/* eslint-disable sonarjs/cognitive-complexity */
|
/* eslint-disable sonarjs/cognitive-complexity */
|
||||||
import { createAggregation } from 'api/v5/queryRange/prepareQueryRangePayloadV5';
|
import { createAggregation } from 'api/v5/queryRange/prepareQueryRangePayloadV5';
|
||||||
import {
|
import { NON_VALUE_OPERATORS, OPERATORS } from 'constants/antlrQueryConstants';
|
||||||
DEPRECATED_OPERATORS_MAP,
|
|
||||||
OPERATORS,
|
|
||||||
QUERY_BUILDER_FUNCTIONS,
|
|
||||||
} from 'constants/antlrQueryConstants';
|
|
||||||
import { getOperatorValue } from 'container/QueryBuilder/filters/QueryBuilderSearch/utils';
|
import { getOperatorValue } from 'container/QueryBuilder/filters/QueryBuilderSearch/utils';
|
||||||
import { cloneDeep, isEqual, sortBy } from 'lodash-es';
|
import { cloneDeep, isEqual, sortBy } from 'lodash-es';
|
||||||
import { IQueryPair } from 'types/antlrQueryTypes';
|
import { IQueryPair } from 'types/antlrQueryTypes';
|
||||||
@@ -22,10 +18,10 @@ import {
|
|||||||
TraceAggregation,
|
TraceAggregation,
|
||||||
} from 'types/api/v5/queryRange';
|
} from 'types/api/v5/queryRange';
|
||||||
import { EQueryType } from 'types/common/dashboard';
|
import { EQueryType } from 'types/common/dashboard';
|
||||||
import { DataSource, ReduceOperators } from 'types/common/queryBuilder';
|
import { DataSource } from 'types/common/queryBuilder';
|
||||||
import { extractQueryPairs } from 'utils/queryContextUtils';
|
import { extractQueryPairs } from 'utils/queryContextUtils';
|
||||||
import { unquote } from 'utils/stringUtils';
|
import { unquote } from 'utils/stringUtils';
|
||||||
import { isFunctionOperator, isNonValueOperator } from 'utils/tokenUtils';
|
import { isFunctionOperator } from 'utils/tokenUtils';
|
||||||
import { v4 as uuid } from 'uuid';
|
import { v4 as uuid } from 'uuid';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -38,13 +34,6 @@ const isArrayOperator = (operator: string): boolean => {
|
|||||||
return arrayOperators.includes(operator);
|
return arrayOperators.includes(operator);
|
||||||
};
|
};
|
||||||
|
|
||||||
const isVariable = (value: string | string[] | number | boolean): boolean => {
|
|
||||||
if (Array.isArray(value)) {
|
|
||||||
return value.some((v) => typeof v === 'string' && v.trim().startsWith('$'));
|
|
||||||
}
|
|
||||||
return typeof value === 'string' && value.trim().startsWith('$');
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Format a value for the expression string
|
* Format a value for the expression string
|
||||||
* @param value - The value to format
|
* @param value - The value to format
|
||||||
@@ -55,10 +44,6 @@ const formatValueForExpression = (
|
|||||||
value: string[] | string | number | boolean,
|
value: string[] | string | number | boolean,
|
||||||
operator?: string,
|
operator?: string,
|
||||||
): string => {
|
): string => {
|
||||||
if (isVariable(value)) {
|
|
||||||
return String(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
// For IN operators, ensure value is always an array
|
// For IN operators, ensure value is always an array
|
||||||
if (isArrayOperator(operator || '')) {
|
if (isArrayOperator(operator || '')) {
|
||||||
const arrayValue = Array.isArray(value) ? value : [value];
|
const arrayValue = Array.isArray(value) ? value : [value];
|
||||||
@@ -102,32 +87,17 @@ export const convertFiltersToExpression = (
|
|||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
let operator = op.trim().toLowerCase();
|
const sanitizedOperator = op.trim().toUpperCase();
|
||||||
if (Object.keys(DEPRECATED_OPERATORS_MAP).includes(operator)) {
|
if (isFunctionOperator(op)) {
|
||||||
operator =
|
return `${op}(${key.key}, ${value})`;
|
||||||
DEPRECATED_OPERATORS_MAP[
|
|
||||||
operator as keyof typeof DEPRECATED_OPERATORS_MAP
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isNonValueOperator(operator)) {
|
if (NON_VALUE_OPERATORS.includes(sanitizedOperator)) {
|
||||||
return `${key.key} ${operator}`;
|
return `${key.key} ${op}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isFunctionOperator(operator)) {
|
const formattedValue = formatValueForExpression(value, op);
|
||||||
// Get the proper function name from QUERY_BUILDER_FUNCTIONS
|
return `${key.key} ${op} ${formattedValue}`;
|
||||||
const functionOperators = Object.values(QUERY_BUILDER_FUNCTIONS);
|
|
||||||
const properFunctionName =
|
|
||||||
functionOperators.find(
|
|
||||||
(func: string) => func.toLowerCase() === operator.toLowerCase(),
|
|
||||||
) || operator;
|
|
||||||
|
|
||||||
const formattedValue = formatValueForExpression(value, operator);
|
|
||||||
return `${properFunctionName}(${key.key}, ${formattedValue})`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const formattedValue = formatValueForExpression(value, operator);
|
|
||||||
return `${key.key} ${operator} ${formattedValue}`;
|
|
||||||
})
|
})
|
||||||
.filter((expression) => expression !== ''); // Remove empty expressions
|
.filter((expression) => expression !== ''); // Remove empty expressions
|
||||||
|
|
||||||
@@ -152,6 +122,7 @@ export const convertExpressionToFilters = (
|
|||||||
if (!expression) return [];
|
if (!expression) return [];
|
||||||
|
|
||||||
const queryPairs = extractQueryPairs(expression);
|
const queryPairs = extractQueryPairs(expression);
|
||||||
|
|
||||||
const filters: TagFilterItem[] = [];
|
const filters: TagFilterItem[] = [];
|
||||||
|
|
||||||
queryPairs.forEach((pair) => {
|
queryPairs.forEach((pair) => {
|
||||||
@@ -174,57 +145,39 @@ export const convertExpressionToFilters = (
|
|||||||
|
|
||||||
return filters;
|
return filters;
|
||||||
};
|
};
|
||||||
const getQueryPairsMap = (query: string): Map<string, IQueryPair> => {
|
|
||||||
const queryPairs = extractQueryPairs(query);
|
|
||||||
const queryPairsMap: Map<string, IQueryPair> = new Map();
|
|
||||||
|
|
||||||
queryPairs.forEach((pair) => {
|
|
||||||
const key = pair.hasNegation
|
|
||||||
? `${pair.key}-not ${pair.operator}`.trim().toLowerCase()
|
|
||||||
: `${pair.key}-${pair.operator}`.trim().toLowerCase();
|
|
||||||
queryPairsMap.set(key, pair);
|
|
||||||
});
|
|
||||||
|
|
||||||
return queryPairsMap;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const convertFiltersToExpressionWithExistingQuery = (
|
export const convertFiltersToExpressionWithExistingQuery = (
|
||||||
filters: TagFilter,
|
filters: TagFilter,
|
||||||
existingQuery: string | undefined,
|
existingQuery: string | undefined,
|
||||||
): { filters: TagFilter; filter: { expression: string } } => {
|
): { filters: TagFilter; filter: { expression: string } } => {
|
||||||
// Check for deprecated operators and replace them with new operators
|
|
||||||
const updatedFilters = cloneDeep(filters);
|
|
||||||
|
|
||||||
// Replace deprecated operators in filter items
|
|
||||||
if (updatedFilters?.items) {
|
|
||||||
updatedFilters.items = updatedFilters.items.map((item) => {
|
|
||||||
const opLower = item.op?.toLowerCase();
|
|
||||||
if (Object.keys(DEPRECATED_OPERATORS_MAP).includes(opLower)) {
|
|
||||||
return {
|
|
||||||
...item,
|
|
||||||
op: DEPRECATED_OPERATORS_MAP[
|
|
||||||
opLower as keyof typeof DEPRECATED_OPERATORS_MAP
|
|
||||||
].toLowerCase(),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return item;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!existingQuery) {
|
if (!existingQuery) {
|
||||||
// If no existing query, return filters with a newly generated expression
|
// If no existing query, return filters with a newly generated expression
|
||||||
return {
|
return {
|
||||||
filters: updatedFilters,
|
filters,
|
||||||
filter: convertFiltersToExpression(updatedFilters),
|
filter: convertFiltersToExpression(filters),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Extract query pairs from the existing query
|
||||||
|
const queryPairs = extractQueryPairs(existingQuery.trim());
|
||||||
|
let queryPairsMap: Map<string, IQueryPair> = new Map();
|
||||||
|
|
||||||
|
const updatedFilters = cloneDeep(filters); // Clone filters to avoid direct mutation
|
||||||
const nonExistingFilters: TagFilterItem[] = [];
|
const nonExistingFilters: TagFilterItem[] = [];
|
||||||
let modifiedQuery = existingQuery; // We'll modify this query as we proceed
|
let modifiedQuery = existingQuery; // We'll modify this query as we proceed
|
||||||
const visitedPairs: Set<string> = new Set(); // Set to track visited query pairs
|
const visitedPairs: Set<string> = new Set(); // Set to track visited query pairs
|
||||||
|
|
||||||
// Map extracted query pairs to key-specific pair information for faster access
|
// Map extracted query pairs to key-specific pair information for faster access
|
||||||
let queryPairsMap = getQueryPairsMap(existingQuery.trim());
|
if (queryPairs.length > 0) {
|
||||||
|
queryPairsMap = new Map(
|
||||||
|
queryPairs.map((pair) => {
|
||||||
|
const key = pair.hasNegation
|
||||||
|
? `${pair.key}-not ${pair.operator}`.trim().toLowerCase()
|
||||||
|
: `${pair.key}-${pair.operator}`.trim().toLowerCase();
|
||||||
|
return [key, pair];
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
filters?.items?.forEach((filter) => {
|
filters?.items?.forEach((filter) => {
|
||||||
const { key, op, value } = filter;
|
const { key, op, value } = filter;
|
||||||
@@ -282,8 +235,6 @@ export const convertFiltersToExpressionWithExistingQuery = (
|
|||||||
modifiedQuery.slice(0, existingPair.position.valueStart) +
|
modifiedQuery.slice(0, existingPair.position.valueStart) +
|
||||||
formattedValue +
|
formattedValue +
|
||||||
modifiedQuery.slice(existingPair.position.valueEnd + 1);
|
modifiedQuery.slice(existingPair.position.valueEnd + 1);
|
||||||
|
|
||||||
queryPairsMap = getQueryPairsMap(modifiedQuery);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -309,7 +260,6 @@ export const convertFiltersToExpressionWithExistingQuery = (
|
|||||||
)}${OPERATORS.IN} ${formattedValue} ${modifiedQuery.slice(
|
)}${OPERATORS.IN} ${formattedValue} ${modifiedQuery.slice(
|
||||||
notInPair.position.valueEnd + 1,
|
notInPair.position.valueEnd + 1,
|
||||||
)}`;
|
)}`;
|
||||||
queryPairsMap = getQueryPairsMap(modifiedQuery.trim());
|
|
||||||
}
|
}
|
||||||
shouldAddToNonExisting = false; // Don't add this to non-existing filters
|
shouldAddToNonExisting = false; // Don't add this to non-existing filters
|
||||||
} else if (
|
} else if (
|
||||||
@@ -326,7 +276,6 @@ export const convertFiltersToExpressionWithExistingQuery = (
|
|||||||
)}${OPERATORS.IN} ${formattedValue} ${modifiedQuery.slice(
|
)}${OPERATORS.IN} ${formattedValue} ${modifiedQuery.slice(
|
||||||
equalsPair.position.valueEnd + 1,
|
equalsPair.position.valueEnd + 1,
|
||||||
)}`;
|
)}`;
|
||||||
queryPairsMap = getQueryPairsMap(modifiedQuery);
|
|
||||||
}
|
}
|
||||||
shouldAddToNonExisting = false; // Don't add this to non-existing filters
|
shouldAddToNonExisting = false; // Don't add this to non-existing filters
|
||||||
} else if (
|
} else if (
|
||||||
@@ -343,7 +292,6 @@ export const convertFiltersToExpressionWithExistingQuery = (
|
|||||||
)}${OPERATORS.IN} ${formattedValue} ${modifiedQuery.slice(
|
)}${OPERATORS.IN} ${formattedValue} ${modifiedQuery.slice(
|
||||||
notEqualsPair.position.valueEnd + 1,
|
notEqualsPair.position.valueEnd + 1,
|
||||||
)}`;
|
)}`;
|
||||||
queryPairsMap = getQueryPairsMap(modifiedQuery);
|
|
||||||
}
|
}
|
||||||
shouldAddToNonExisting = false; // Don't add this to non-existing filters
|
shouldAddToNonExisting = false; // Don't add this to non-existing filters
|
||||||
}
|
}
|
||||||
@@ -365,7 +313,6 @@ export const convertFiltersToExpressionWithExistingQuery = (
|
|||||||
} ${formattedValue} ${modifiedQuery.slice(
|
} ${formattedValue} ${modifiedQuery.slice(
|
||||||
notEqualsPair.position.valueEnd + 1,
|
notEqualsPair.position.valueEnd + 1,
|
||||||
)}`;
|
)}`;
|
||||||
queryPairsMap = getQueryPairsMap(modifiedQuery);
|
|
||||||
}
|
}
|
||||||
shouldAddToNonExisting = false; // Don't add this to non-existing filters
|
shouldAddToNonExisting = false; // Don't add this to non-existing filters
|
||||||
}
|
}
|
||||||
@@ -378,23 +325,6 @@ export const convertFiltersToExpressionWithExistingQuery = (
|
|||||||
if (
|
if (
|
||||||
queryPairsMap.has(`${filter.key?.key}-${filter.op}`.trim().toLowerCase())
|
queryPairsMap.has(`${filter.key?.key}-${filter.op}`.trim().toLowerCase())
|
||||||
) {
|
) {
|
||||||
const existingPair = queryPairsMap.get(
|
|
||||||
`${filter.key?.key}-${filter.op}`.trim().toLowerCase(),
|
|
||||||
);
|
|
||||||
if (
|
|
||||||
existingPair &&
|
|
||||||
existingPair.position?.valueStart &&
|
|
||||||
existingPair.position?.valueEnd
|
|
||||||
) {
|
|
||||||
const formattedValue = formatValueForExpression(value, op);
|
|
||||||
// replace the value with the new value
|
|
||||||
modifiedQuery =
|
|
||||||
modifiedQuery.slice(0, existingPair.position.valueStart) +
|
|
||||||
formattedValue +
|
|
||||||
modifiedQuery.slice(existingPair.position.valueEnd + 1);
|
|
||||||
queryPairsMap = getQueryPairsMap(modifiedQuery);
|
|
||||||
}
|
|
||||||
|
|
||||||
visitedPairs.add(`${filter.key?.key}-${filter.op}`.trim().toLowerCase());
|
visitedPairs.add(`${filter.key?.key}-${filter.op}`.trim().toLowerCase());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -477,13 +407,11 @@ export const convertFiltersToExpressionWithExistingQuery = (
|
|||||||
*
|
*
|
||||||
* @param expression - The full query string.
|
* @param expression - The full query string.
|
||||||
* @param keysToRemove - An array of keys (case-insensitive) that should be removed from the expression.
|
* @param keysToRemove - An array of keys (case-insensitive) that should be removed from the expression.
|
||||||
* @param removeOnlyVariableExpressions - When true, only removes key-value pairs where the value is a variable (starts with $). When false, uses the original behavior.
|
|
||||||
* @returns A new expression string with the specified keys and their associated clauses removed.
|
* @returns A new expression string with the specified keys and their associated clauses removed.
|
||||||
*/
|
*/
|
||||||
export const removeKeysFromExpression = (
|
export const removeKeysFromExpression = (
|
||||||
expression: string,
|
expression: string,
|
||||||
keysToRemove: string[],
|
keysToRemove: string[],
|
||||||
removeOnlyVariableExpressions = false,
|
|
||||||
): string => {
|
): string => {
|
||||||
if (!keysToRemove || keysToRemove.length === 0) {
|
if (!keysToRemove || keysToRemove.length === 0) {
|
||||||
return expression;
|
return expression;
|
||||||
@@ -499,20 +427,9 @@ export const removeKeysFromExpression = (
|
|||||||
let queryPairsMap: Map<string, IQueryPair>;
|
let queryPairsMap: Map<string, IQueryPair>;
|
||||||
|
|
||||||
if (existingQueryPairs.length > 0) {
|
if (existingQueryPairs.length > 0) {
|
||||||
// Filter query pairs based on the removeOnlyVariableExpressions flag
|
|
||||||
const filteredQueryPairs = removeOnlyVariableExpressions
|
|
||||||
? existingQueryPairs.filter((pair) => {
|
|
||||||
const pairKey = pair.key?.trim().toLowerCase();
|
|
||||||
const matchesKey = pairKey === `${key}`.trim().toLowerCase();
|
|
||||||
if (!matchesKey) return false;
|
|
||||||
const value = pair.value?.toString().trim();
|
|
||||||
return value && value.includes('$');
|
|
||||||
})
|
|
||||||
: existingQueryPairs;
|
|
||||||
|
|
||||||
// Build a map for quick lookup of query pairs by their lowercase trimmed keys
|
// Build a map for quick lookup of query pairs by their lowercase trimmed keys
|
||||||
queryPairsMap = new Map(
|
queryPairsMap = new Map(
|
||||||
filteredQueryPairs.map((pair) => {
|
existingQueryPairs.map((pair) => {
|
||||||
const key = pair.key.trim().toLowerCase();
|
const key = pair.key.trim().toLowerCase();
|
||||||
return [key, pair];
|
return [key, pair];
|
||||||
}),
|
}),
|
||||||
@@ -548,12 +465,6 @@ export const removeKeysFromExpression = (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Clean up any remaining trailing AND/OR operators and extra whitespace
|
|
||||||
updatedExpression = updatedExpression
|
|
||||||
.replace(/\s+(AND|OR)\s*$/i, '') // Remove trailing AND/OR
|
|
||||||
.replace(/^(AND|OR)\s+/i, '') // Remove leading AND/OR
|
|
||||||
.trim();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return updatedExpression;
|
return updatedExpression;
|
||||||
@@ -610,25 +521,14 @@ export const convertHavingToExpression = (
|
|||||||
* @returns New aggregation format based on data source
|
* @returns New aggregation format based on data source
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
export const convertAggregationToExpression = ({
|
export const convertAggregationToExpression = (
|
||||||
aggregateOperator,
|
aggregateOperator: string,
|
||||||
aggregateAttribute,
|
aggregateAttribute: BaseAutocompleteData,
|
||||||
dataSource,
|
dataSource: DataSource,
|
||||||
timeAggregation,
|
timeAggregation?: string,
|
||||||
spaceAggregation,
|
spaceAggregation?: string,
|
||||||
alias,
|
alias?: string,
|
||||||
reduceTo,
|
): (TraceAggregation | LogAggregation | MetricAggregation)[] | undefined => {
|
||||||
temporality,
|
|
||||||
}: {
|
|
||||||
aggregateOperator: string;
|
|
||||||
aggregateAttribute: BaseAutocompleteData;
|
|
||||||
dataSource: DataSource;
|
|
||||||
timeAggregation?: string;
|
|
||||||
spaceAggregation?: string;
|
|
||||||
alias?: string;
|
|
||||||
reduceTo?: ReduceOperators;
|
|
||||||
temporality?: string;
|
|
||||||
}): (TraceAggregation | LogAggregation | MetricAggregation)[] | undefined => {
|
|
||||||
// Skip if no operator or attribute key
|
// Skip if no operator or attribute key
|
||||||
if (!aggregateOperator) {
|
if (!aggregateOperator) {
|
||||||
return undefined;
|
return undefined;
|
||||||
@@ -646,9 +546,7 @@ export const convertAggregationToExpression = ({
|
|||||||
if (dataSource === DataSource.METRICS) {
|
if (dataSource === DataSource.METRICS) {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
metricName: aggregateAttribute?.key || '',
|
metricName: aggregateAttribute.key,
|
||||||
reduceTo,
|
|
||||||
temporality,
|
|
||||||
timeAggregation: (normalizedTimeAggregation || normalizedOperator) as any,
|
timeAggregation: (normalizedTimeAggregation || normalizedOperator) as any,
|
||||||
spaceAggregation: (normalizedSpaceAggregation || normalizedOperator) as any,
|
spaceAggregation: (normalizedSpaceAggregation || normalizedOperator) as any,
|
||||||
} as MetricAggregation,
|
} as MetricAggregation,
|
||||||
@@ -656,9 +554,7 @@ export const convertAggregationToExpression = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
// For traces and logs, use expression format
|
// For traces and logs, use expression format
|
||||||
const expression = aggregateAttribute?.key
|
const expression = `${normalizedOperator}(${aggregateAttribute.key})`;
|
||||||
? `${normalizedOperator}(${aggregateAttribute?.key})`
|
|
||||||
: `${normalizedOperator}()`;
|
|
||||||
|
|
||||||
if (dataSource === DataSource.TRACES) {
|
if (dataSource === DataSource.TRACES) {
|
||||||
return [
|
return [
|
||||||
|
|||||||
@@ -5,11 +5,8 @@ import { SignalType } from 'components/QuickFilters/types';
|
|||||||
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||||
import { useGetAggregateKeys } from 'hooks/queryBuilder/useGetAggregateKeys';
|
import { useGetAggregateKeys } from 'hooks/queryBuilder/useGetAggregateKeys';
|
||||||
import { useGetAttributeSuggestions } from 'hooks/queryBuilder/useGetAttributeSuggestions';
|
import { useGetAttributeSuggestions } from 'hooks/queryBuilder/useGetAttributeSuggestions';
|
||||||
import { useGetQueryKeySuggestions } from 'hooks/querySuggestions/useGetQueryKeySuggestions';
|
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
|
||||||
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
|
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
|
||||||
import { QueryKeyDataSuggestionsProps } from 'types/api/querySuggestions/types';
|
|
||||||
import { Filter as FilterType } from 'types/api/quickFilters/getCustomFilters';
|
import { Filter as FilterType } from 'types/api/quickFilters/getCustomFilters';
|
||||||
import { DataSource } from 'types/common/queryBuilder';
|
import { DataSource } from 'types/common/queryBuilder';
|
||||||
|
|
||||||
@@ -43,10 +40,6 @@ function OtherFilters({
|
|||||||
() => SIGNAL_DATA_SOURCE_MAP[signal as SignalType] === DataSource.LOGS,
|
() => SIGNAL_DATA_SOURCE_MAP[signal as SignalType] === DataSource.LOGS,
|
||||||
[signal],
|
[signal],
|
||||||
);
|
);
|
||||||
const isMeterDataSource = useMemo(
|
|
||||||
() => signal && signal === SignalType.METER_EXPLORER,
|
|
||||||
[signal],
|
|
||||||
);
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data: suggestionsData,
|
data: suggestionsData,
|
||||||
@@ -76,22 +69,7 @@ function OtherFilters({
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
queryKey: [REACT_QUERY_KEY.GET_OTHER_FILTERS, inputValue],
|
queryKey: [REACT_QUERY_KEY.GET_OTHER_FILTERS, inputValue],
|
||||||
enabled: !!signal && !isLogDataSource && !isMeterDataSource,
|
enabled: !!signal && !isLogDataSource,
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const {
|
|
||||||
data: fieldKeysData,
|
|
||||||
isLoading: isLoadingFieldKeys,
|
|
||||||
} = useGetQueryKeySuggestions(
|
|
||||||
{
|
|
||||||
searchText: inputValue,
|
|
||||||
signal: SIGNAL_DATA_SOURCE_MAP[signal as SignalType],
|
|
||||||
signalSource: 'meter',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
queryKey: [REACT_QUERY_KEY.GET_OTHER_FILTERS, inputValue],
|
|
||||||
enabled: !!signal && isMeterDataSource,
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -99,33 +77,13 @@ function OtherFilters({
|
|||||||
let filterAttributes;
|
let filterAttributes;
|
||||||
if (isLogDataSource) {
|
if (isLogDataSource) {
|
||||||
filterAttributes = suggestionsData?.payload?.attributes || [];
|
filterAttributes = suggestionsData?.payload?.attributes || [];
|
||||||
} else if (isMeterDataSource) {
|
|
||||||
const fieldKeys: QueryKeyDataSuggestionsProps[] = Object.values(
|
|
||||||
fieldKeysData?.data?.data?.keys || {},
|
|
||||||
)?.flat();
|
|
||||||
filterAttributes = fieldKeys.map(
|
|
||||||
(attr) =>
|
|
||||||
({
|
|
||||||
key: attr.name,
|
|
||||||
dataType: attr.fieldDataType,
|
|
||||||
type: attr.fieldContext,
|
|
||||||
signal: attr.signal,
|
|
||||||
} as BaseAutocompleteData),
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
filterAttributes = aggregateKeysData?.payload?.attributeKeys || [];
|
filterAttributes = aggregateKeysData?.payload?.attributeKeys || [];
|
||||||
}
|
}
|
||||||
return filterAttributes?.filter(
|
return filterAttributes?.filter(
|
||||||
(attr) => !addedFilters.some((filter) => filter.key === attr.key),
|
(attr) => !addedFilters.some((filter) => filter.key === attr.key),
|
||||||
);
|
);
|
||||||
}, [
|
}, [suggestionsData, aggregateKeysData, addedFilters, isLogDataSource]);
|
||||||
suggestionsData,
|
|
||||||
aggregateKeysData,
|
|
||||||
addedFilters,
|
|
||||||
isLogDataSource,
|
|
||||||
fieldKeysData,
|
|
||||||
isMeterDataSource,
|
|
||||||
]);
|
|
||||||
|
|
||||||
const handleAddFilter = (filter: FilterType): void => {
|
const handleAddFilter = (filter: FilterType): void => {
|
||||||
setAddedFilters((prev) => [
|
setAddedFilters((prev) => [
|
||||||
@@ -133,14 +91,15 @@ function OtherFilters({
|
|||||||
{
|
{
|
||||||
key: filter.key,
|
key: filter.key,
|
||||||
dataType: filter.dataType,
|
dataType: filter.dataType,
|
||||||
|
isColumn: filter.isColumn,
|
||||||
|
isJSON: filter.isJSON,
|
||||||
type: filter.type,
|
type: filter.type,
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderFilters = (): React.ReactNode => {
|
const renderFilters = (): React.ReactNode => {
|
||||||
const isLoading =
|
const isLoading = isFetchingSuggestions || isFetchingAggregateKeys;
|
||||||
isFetchingSuggestions || isFetchingAggregateKeys || isLoadingFieldKeys;
|
|
||||||
if (isLoading) return <OtherFiltersSkeleton />;
|
if (isLoading) return <OtherFiltersSkeleton />;
|
||||||
if (!otherFilters?.length)
|
if (!otherFilters?.length)
|
||||||
return <div className="no-values-found">No values found</div>;
|
return <div className="no-values-found">No values found</div>;
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ export const QuickFiltersConfig = [
|
|||||||
key: 'deployment.environment',
|
key: 'deployment.environment',
|
||||||
dataType: DataTypes.String,
|
dataType: DataTypes.String,
|
||||||
type: 'resource',
|
type: 'resource',
|
||||||
|
isColumn: false,
|
||||||
|
isJSON: false,
|
||||||
},
|
},
|
||||||
defaultOpen: true,
|
defaultOpen: true,
|
||||||
},
|
},
|
||||||
@@ -20,6 +22,8 @@ export const QuickFiltersConfig = [
|
|||||||
key: 'service.name',
|
key: 'service.name',
|
||||||
dataType: DataTypes.String,
|
dataType: DataTypes.String,
|
||||||
type: 'resource',
|
type: 'resource',
|
||||||
|
isColumn: false,
|
||||||
|
isJSON: false,
|
||||||
},
|
},
|
||||||
defaultOpen: false,
|
defaultOpen: false,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -53,6 +53,8 @@ export const getFilterConfig = (
|
|||||||
key: att.key,
|
key: att.key,
|
||||||
dataType: att.dataType,
|
dataType: att.dataType,
|
||||||
type: att.type,
|
type: att.type,
|
||||||
|
isColumn: att.isColumn,
|
||||||
|
isJSON: att.isJSON,
|
||||||
},
|
},
|
||||||
defaultOpen: index < 2,
|
defaultOpen: index < 2,
|
||||||
} as IQuickFiltersConfig),
|
} as IQuickFiltersConfig),
|
||||||
|
|||||||
@@ -17,19 +17,6 @@
|
|||||||
font-weight: var(--font-weight-normal);
|
font-weight: var(--font-weight-normal);
|
||||||
}
|
}
|
||||||
|
|
||||||
.view-title-container {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 6px;
|
|
||||||
justify-content: center;
|
|
||||||
|
|
||||||
.icon-container {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.tab {
|
.tab {
|
||||||
border: 1px solid var(--bg-slate-400);
|
border: 1px solid var(--bg-slate-400);
|
||||||
&:hover {
|
&:hover {
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import { RadioChangeEvent } from 'antd/es/radio';
|
|||||||
interface Option {
|
interface Option {
|
||||||
value: string;
|
value: string;
|
||||||
label: string;
|
label: string;
|
||||||
icon?: React.ReactNode;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SignozRadioGroupProps {
|
interface SignozRadioGroupProps {
|
||||||
@@ -38,10 +37,7 @@ function SignozRadioGroup({
|
|||||||
value={option.value}
|
value={option.value}
|
||||||
className={value === option.value ? 'selected_view tab' : 'tab'}
|
className={value === option.value ? 'selected_view tab' : 'tab'}
|
||||||
>
|
>
|
||||||
<div className="view-title-container">
|
{option.label}
|
||||||
{option.icon && <div className="icon-container">{option.icon}</div>}
|
|
||||||
{option.label}
|
|
||||||
</div>
|
|
||||||
</Radio.Button>
|
</Radio.Button>
|
||||||
))}
|
))}
|
||||||
</Radio.Group>
|
</Radio.Group>
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user