Compare commits
69 Commits
v0.102.1
...
feat/log-d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
22d1b90e2a | ||
|
|
aa9a2863af | ||
|
|
c5fddb2e09 | ||
|
|
e66bfe5961 | ||
|
|
42943f72b7 | ||
|
|
7a72a209e5 | ||
|
|
44f00943a8 | ||
|
|
8867e1ef38 | ||
|
|
c08e520941 | ||
|
|
139cc4452d | ||
|
|
2f3baeb302 | ||
|
|
3d42b0058e | ||
|
|
ed70e3c5f5 | ||
|
|
7d6918f8b6 | ||
|
|
2885bc851e | ||
|
|
857258f8c3 | ||
|
|
ece5c2b7ad | ||
|
|
1078f98388 | ||
|
|
b4e2326f38 | ||
|
|
c79b154215 | ||
|
|
a59c0188cc | ||
|
|
3df426625a | ||
|
|
646f359f33 | ||
|
|
81167c6947 | ||
|
|
bc1295b93a | ||
|
|
3db0e1f66a | ||
|
|
d52b54aeb3 | ||
|
|
c8608c18ae | ||
|
|
cde99ba1a0 | ||
|
|
a7e9d442b7 | ||
|
|
0b0d622f6b | ||
|
|
127e760b00 | ||
|
|
63e333de0d | ||
|
|
af57d11b6a | ||
|
|
8d61ee338b | ||
|
|
5d9dc17645 | ||
|
|
5288022ffd | ||
|
|
cdc18af4a2 | ||
|
|
918a90e3c1 | ||
|
|
e8ce7b22f5 | ||
|
|
b752fdd30a | ||
|
|
d73b7fadab | ||
|
|
bc4b65dbb9 | ||
|
|
e716a2a7b1 | ||
|
|
891c56b059 | ||
|
|
d01e6fc891 | ||
|
|
17f8c1040f | ||
|
|
ffa5a9725e | ||
|
|
92cab8e049 | ||
|
|
7b9e6e3cbb | ||
|
|
4837ddb601 | ||
|
|
9c818955af | ||
|
|
134a051196 | ||
|
|
c904ab5d99 | ||
|
|
d53f9a7e16 | ||
|
|
1b01b61026 | ||
|
|
95a26cecba | ||
|
|
15af828005 | ||
|
|
e5b99703ac | ||
|
|
f0941c7b2e | ||
|
|
12c9b921a7 | ||
|
|
52228bc6c4 | ||
|
|
79988b448f | ||
|
|
4bfd7ba3d7 | ||
|
|
3349158213 | ||
|
|
1c9f4efb9f | ||
|
|
fd839ff1db | ||
|
|
09cbe4aa0d | ||
|
|
096e38ee91 |
@@ -42,7 +42,7 @@ services:
|
|||||||
timeout: 5s
|
timeout: 5s
|
||||||
retries: 3
|
retries: 3
|
||||||
schema-migrator-sync:
|
schema-migrator-sync:
|
||||||
image: signoz/signoz-schema-migrator:v0.129.11
|
image: signoz/signoz-schema-migrator:v0.129.12
|
||||||
container_name: schema-migrator-sync
|
container_name: schema-migrator-sync
|
||||||
command:
|
command:
|
||||||
- sync
|
- sync
|
||||||
@@ -55,7 +55,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.11
|
image: signoz/signoz-schema-migrator:v0.129.12
|
||||||
container_name: schema-migrator-async
|
container_name: schema-migrator-async
|
||||||
command:
|
command:
|
||||||
- async
|
- async
|
||||||
|
|||||||
42
.github/CODEOWNERS
vendored
@@ -3,46 +3,10 @@
|
|||||||
# that they own.
|
# that they own.
|
||||||
|
|
||||||
/frontend/ @YounixM @aks07
|
/frontend/ @YounixM @aks07
|
||||||
/frontend/src/container/MetricsApplication @srikanthccv
|
|
||||||
/frontend/src/container/NewWidget/RightContainer/types.ts @srikanthccv
|
|
||||||
|
|
||||||
# Dashboard, Alert, Metrics, Service Map, Services
|
# Onboarding
|
||||||
/frontend/src/container/ListOfDashboard/ @srikanthccv
|
/frontend/src/container/OnboardingV2Container/onboarding-configs/onboarding-config-with-links.json @makeavish
|
||||||
/frontend/src/container/NewDashboard/ @srikanthccv
|
/frontend/src/container/OnboardingV2Container/AddDataSource/AddDataSource.tsx @makeavish
|
||||||
/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
|
||||||
|
|||||||
1
.github/workflows/build-enterprise.yaml
vendored
@@ -69,6 +69,7 @@ jobs:
|
|||||||
echo 'POSTHOG_KEY="${{ secrets.POSTHOG_KEY }}"' >> frontend/.env
|
echo 'POSTHOG_KEY="${{ secrets.POSTHOG_KEY }}"' >> frontend/.env
|
||||||
echo 'PYLON_APP_ID="${{ secrets.PYLON_APP_ID }}"' >> frontend/.env
|
echo 'PYLON_APP_ID="${{ secrets.PYLON_APP_ID }}"' >> frontend/.env
|
||||||
echo 'APPCUES_APP_ID="${{ secrets.APPCUES_APP_ID }}"' >> frontend/.env
|
echo 'APPCUES_APP_ID="${{ secrets.APPCUES_APP_ID }}"' >> frontend/.env
|
||||||
|
echo 'PYLON_IDENTITY_SECRET="${{ secrets.PYLON_IDENTITY_SECRET }}"' >> frontend/.env
|
||||||
- name: cache-dotenv
|
- name: cache-dotenv
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
|
|||||||
1
.github/workflows/build-staging.yaml
vendored
@@ -68,6 +68,7 @@ jobs:
|
|||||||
echo 'TUNNEL_DOMAIN="${{ secrets.NP_TUNNEL_DOMAIN }}"' >> frontend/.env
|
echo 'TUNNEL_DOMAIN="${{ secrets.NP_TUNNEL_DOMAIN }}"' >> frontend/.env
|
||||||
echo 'PYLON_APP_ID="${{ secrets.NP_PYLON_APP_ID }}"' >> frontend/.env
|
echo 'PYLON_APP_ID="${{ secrets.NP_PYLON_APP_ID }}"' >> frontend/.env
|
||||||
echo 'APPCUES_APP_ID="${{ secrets.NP_APPCUES_APP_ID }}"' >> frontend/.env
|
echo 'APPCUES_APP_ID="${{ secrets.NP_APPCUES_APP_ID }}"' >> frontend/.env
|
||||||
|
echo 'PYLON_IDENTITY_SECRET="${{ secrets.NP_PYLON_IDENTITY_SECRET }}"' >> frontend/.env
|
||||||
- name: cache-dotenv
|
- name: cache-dotenv
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
|
|||||||
1
.github/workflows/gor-signoz.yaml
vendored
@@ -35,6 +35,7 @@ jobs:
|
|||||||
echo 'POSTHOG_KEY="${{ secrets.POSTHOG_KEY }}"' >> .env
|
echo 'POSTHOG_KEY="${{ secrets.POSTHOG_KEY }}"' >> .env
|
||||||
echo 'PYLON_APP_ID="${{ secrets.PYLON_APP_ID }}"' >> .env
|
echo 'PYLON_APP_ID="${{ secrets.PYLON_APP_ID }}"' >> .env
|
||||||
echo 'APPCUES_APP_ID="${{ secrets.APPCUES_APP_ID }}"' >> .env
|
echo 'APPCUES_APP_ID="${{ secrets.APPCUES_APP_ID }}"' >> .env
|
||||||
|
echo 'PYLON_IDENTITY_SECRET="${{ secrets.PYLON_IDENTITY_SECRET }}"' >> .env
|
||||||
- name: build-frontend
|
- name: build-frontend
|
||||||
run: make js-build
|
run: make js-build
|
||||||
- name: upload-frontend-artifact
|
- name: upload-frontend-artifact
|
||||||
|
|||||||
2
Makefile
@@ -86,7 +86,7 @@ go-run-enterprise: ## Runs the enterprise go backend server
|
|||||||
SIGNOZ_TELEMETRYSTORE_CLICKHOUSE_DSN=tcp://127.0.0.1:9000 \
|
SIGNOZ_TELEMETRYSTORE_CLICKHOUSE_DSN=tcp://127.0.0.1:9000 \
|
||||||
SIGNOZ_TELEMETRYSTORE_CLICKHOUSE_CLUSTER=cluster \
|
SIGNOZ_TELEMETRYSTORE_CLICKHOUSE_CLUSTER=cluster \
|
||||||
go run -race \
|
go run -race \
|
||||||
$(GO_BUILD_CONTEXT_ENTERPRISE)/*.go
|
$(GO_BUILD_CONTEXT_ENTERPRISE)/*.go server
|
||||||
|
|
||||||
.PHONY: go-test
|
.PHONY: go-test
|
||||||
go-test: ## Runs go unit tests
|
go-test: ## Runs go unit tests
|
||||||
|
|||||||
@@ -47,10 +47,10 @@ cache:
|
|||||||
provider: memory
|
provider: memory
|
||||||
# memory: Uses in-memory caching.
|
# memory: Uses in-memory caching.
|
||||||
memory:
|
memory:
|
||||||
# Time-to-live for cache entries in memory. Specify the duration in ns
|
# Max items for the in-memory cache (10x the entries)
|
||||||
ttl: 60000000000
|
num_counters: 100000
|
||||||
# The interval at which the cache will be cleaned up
|
# Total cost in bytes allocated bounded cache
|
||||||
cleanup_interval: 1m
|
max_cost: 67108864
|
||||||
# redis: Uses Redis as the caching backend.
|
# redis: Uses Redis as the caching backend.
|
||||||
redis:
|
redis:
|
||||||
# The hostname or IP address of the Redis server.
|
# The hostname or IP address of the Redis server.
|
||||||
|
|||||||
@@ -176,7 +176,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.102.1
|
image: signoz/signoz:v0.104.0
|
||||||
command:
|
command:
|
||||||
- --config=/root/config/prometheus.yml
|
- --config=/root/config/prometheus.yml
|
||||||
ports:
|
ports:
|
||||||
@@ -209,7 +209,7 @@ services:
|
|||||||
retries: 3
|
retries: 3
|
||||||
otel-collector:
|
otel-collector:
|
||||||
!!merge <<: *db-depend
|
!!merge <<: *db-depend
|
||||||
image: signoz/signoz-otel-collector:v0.129.11
|
image: signoz/signoz-otel-collector:v0.129.12
|
||||||
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 +233,7 @@ services:
|
|||||||
- signoz
|
- signoz
|
||||||
schema-migrator:
|
schema-migrator:
|
||||||
!!merge <<: *common
|
!!merge <<: *common
|
||||||
image: signoz/signoz-schema-migrator:v0.129.11
|
image: signoz/signoz-schema-migrator:v0.129.12
|
||||||
deploy:
|
deploy:
|
||||||
restart_policy:
|
restart_policy:
|
||||||
condition: on-failure
|
condition: on-failure
|
||||||
|
|||||||
@@ -117,7 +117,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.102.1
|
image: signoz/signoz:v0.104.0
|
||||||
command:
|
command:
|
||||||
- --config=/root/config/prometheus.yml
|
- --config=/root/config/prometheus.yml
|
||||||
ports:
|
ports:
|
||||||
@@ -150,7 +150,7 @@ services:
|
|||||||
retries: 3
|
retries: 3
|
||||||
otel-collector:
|
otel-collector:
|
||||||
!!merge <<: *db-depend
|
!!merge <<: *db-depend
|
||||||
image: signoz/signoz-otel-collector:v0.129.11
|
image: signoz/signoz-otel-collector:v0.129.12
|
||||||
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 +176,7 @@ services:
|
|||||||
- signoz
|
- signoz
|
||||||
schema-migrator:
|
schema-migrator:
|
||||||
!!merge <<: *common
|
!!merge <<: *common
|
||||||
image: signoz/signoz-schema-migrator:v0.129.11
|
image: signoz/signoz-schema-migrator:v0.129.12
|
||||||
deploy:
|
deploy:
|
||||||
restart_policy:
|
restart_policy:
|
||||||
condition: on-failure
|
condition: on-failure
|
||||||
|
|||||||
@@ -179,7 +179,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.102.1}
|
image: signoz/signoz:${VERSION:-v0.104.0}
|
||||||
container_name: signoz
|
container_name: signoz
|
||||||
command:
|
command:
|
||||||
- --config=/root/config/prometheus.yml
|
- --config=/root/config/prometheus.yml
|
||||||
@@ -213,7 +213,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.11}
|
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-v0.129.12}
|
||||||
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 +239,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.11}
|
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-v0.129.12}
|
||||||
container_name: schema-migrator-sync
|
container_name: schema-migrator-sync
|
||||||
command:
|
command:
|
||||||
- sync
|
- sync
|
||||||
@@ -250,7 +250,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.11}
|
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-v0.129.12}
|
||||||
container_name: schema-migrator-async
|
container_name: schema-migrator-async
|
||||||
command:
|
command:
|
||||||
- async
|
- async
|
||||||
|
|||||||
@@ -111,7 +111,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.102.1}
|
image: signoz/signoz:${VERSION:-v0.104.0}
|
||||||
container_name: signoz
|
container_name: signoz
|
||||||
command:
|
command:
|
||||||
- --config=/root/config/prometheus.yml
|
- --config=/root/config/prometheus.yml
|
||||||
@@ -144,7 +144,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.11}
|
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-v0.129.12}
|
||||||
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 +166,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.11}
|
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-v0.129.12}
|
||||||
container_name: schema-migrator-sync
|
container_name: schema-migrator-sync
|
||||||
command:
|
command:
|
||||||
- sync
|
- sync
|
||||||
@@ -178,7 +178,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.11}
|
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-v0.129.12}
|
||||||
container_name: schema-migrator-async
|
container_name: schema-migrator-async
|
||||||
command:
|
command:
|
||||||
- async
|
- async
|
||||||
|
|||||||
@@ -129,6 +129,12 @@ func (a *AuthN) HandleCallback(ctx context.Context, query url.Values) (*authtype
|
|||||||
return authtypes.NewCallbackIdentity("", email, authDomain.StorableAuthDomain().OrgID, state), nil
|
return authtypes.NewCallbackIdentity("", email, authDomain.StorableAuthDomain().OrgID, state), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *AuthN) ProviderInfo(ctx context.Context, authDomain *authtypes.AuthDomain) *authtypes.AuthNProviderInfo {
|
||||||
|
return &authtypes.AuthNProviderInfo{
|
||||||
|
RelayStatePath: nil,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (a *AuthN) oidcProviderAndoauth2Config(ctx context.Context, siteURL *url.URL, authDomain *authtypes.AuthDomain) (*oidc.Provider, *oauth2.Config, error) {
|
func (a *AuthN) oidcProviderAndoauth2Config(ctx context.Context, siteURL *url.URL, authDomain *authtypes.AuthDomain) (*oidc.Provider, *oauth2.Config, error) {
|
||||||
if authDomain.AuthDomainConfig().OIDC.IssuerAlias != "" {
|
if authDomain.AuthDomainConfig().OIDC.IssuerAlias != "" {
|
||||||
ctx = oidc.InsecureIssuerURLContext(ctx, authDomain.AuthDomainConfig().OIDC.IssuerAlias)
|
ctx = oidc.InsecureIssuerURLContext(ctx, authDomain.AuthDomainConfig().OIDC.IssuerAlias)
|
||||||
|
|||||||
@@ -99,6 +99,14 @@ func (a *AuthN) HandleCallback(ctx context.Context, formValues url.Values) (*aut
|
|||||||
return authtypes.NewCallbackIdentity("", email, authDomain.StorableAuthDomain().OrgID, state), nil
|
return authtypes.NewCallbackIdentity("", email, authDomain.StorableAuthDomain().OrgID, state), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *AuthN) ProviderInfo(ctx context.Context, authDomain *authtypes.AuthDomain) *authtypes.AuthNProviderInfo {
|
||||||
|
state := authtypes.NewState(&url.URL{Path: "login"}, authDomain.StorableAuthDomain().ID).URL.String()
|
||||||
|
|
||||||
|
return &authtypes.AuthNProviderInfo{
|
||||||
|
RelayStatePath: &state,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (a *AuthN) serviceProvider(siteURL *url.URL, authDomain *authtypes.AuthDomain) (*saml2.SAMLServiceProvider, error) {
|
func (a *AuthN) serviceProvider(siteURL *url.URL, authDomain *authtypes.AuthDomain) (*saml2.SAMLServiceProvider, error) {
|
||||||
certStore, err := a.getCertificateStore(authDomain)
|
certStore, err := a.getCertificateStore(authDomain)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import (
|
|||||||
"github.com/SigNoz/signoz/pkg/query-service/interfaces"
|
"github.com/SigNoz/signoz/pkg/query-service/interfaces"
|
||||||
basemodel "github.com/SigNoz/signoz/pkg/query-service/model"
|
basemodel "github.com/SigNoz/signoz/pkg/query-service/model"
|
||||||
rules "github.com/SigNoz/signoz/pkg/query-service/rules"
|
rules "github.com/SigNoz/signoz/pkg/query-service/rules"
|
||||||
|
"github.com/SigNoz/signoz/pkg/queryparser"
|
||||||
"github.com/SigNoz/signoz/pkg/signoz"
|
"github.com/SigNoz/signoz/pkg/signoz"
|
||||||
"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"
|
||||||
@@ -60,6 +61,7 @@ func NewAPIHandler(opts APIHandlerOptions, signoz *signoz.SigNoz) (*APIHandler,
|
|||||||
FieldsAPI: fields.NewAPI(signoz.Instrumentation.ToProviderSettings(), signoz.TelemetryStore),
|
FieldsAPI: fields.NewAPI(signoz.Instrumentation.ToProviderSettings(), signoz.TelemetryStore),
|
||||||
Signoz: signoz,
|
Signoz: signoz,
|
||||||
QuerierAPI: querierAPI.NewAPI(signoz.Instrumentation.ToProviderSettings(), signoz.Querier, signoz.Analytics),
|
QuerierAPI: querierAPI.NewAPI(signoz.Instrumentation.ToProviderSettings(), signoz.Querier, signoz.Analytics),
|
||||||
|
QueryParserAPI: queryparser.NewAPI(signoz.Instrumentation.ToProviderSettings(), signoz.QueryParser),
|
||||||
})
|
})
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
_ "net/http/pprof" // http profiler
|
_ "net/http/pprof" // http profiler
|
||||||
"slices"
|
"slices"
|
||||||
|
|
||||||
|
"github.com/SigNoz/signoz/pkg/cache/memorycache"
|
||||||
"github.com/SigNoz/signoz/pkg/ruler/rulestore/sqlrulestore"
|
"github.com/SigNoz/signoz/pkg/ruler/rulestore/sqlrulestore"
|
||||||
"go.opentelemetry.io/contrib/instrumentation/github.com/gorilla/mux/otelmux"
|
"go.opentelemetry.io/contrib/instrumentation/github.com/gorilla/mux/otelmux"
|
||||||
"go.opentelemetry.io/otel/propagation"
|
"go.opentelemetry.io/otel/propagation"
|
||||||
@@ -74,13 +75,26 @@ func NewServer(config signoz.Config, signoz *signoz.SigNoz) (*Server, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cacheForTraceDetail, err := memorycache.New(context.TODO(), signoz.Instrumentation.ToProviderSettings(), cache.Config{
|
||||||
|
Provider: "memory",
|
||||||
|
Memory: cache.Memory{
|
||||||
|
NumCounters: 10 * 10000,
|
||||||
|
MaxCost: 1 << 27, // 128 MB
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
reader := clickhouseReader.NewReader(
|
reader := clickhouseReader.NewReader(
|
||||||
signoz.SQLStore,
|
signoz.SQLStore,
|
||||||
signoz.TelemetryStore,
|
signoz.TelemetryStore,
|
||||||
signoz.Prometheus,
|
signoz.Prometheus,
|
||||||
signoz.TelemetryStore.Cluster(),
|
signoz.TelemetryStore.Cluster(),
|
||||||
config.Querier.FluxInterval,
|
config.Querier.FluxInterval,
|
||||||
|
cacheForTraceDetail,
|
||||||
signoz.Cache,
|
signoz.Cache,
|
||||||
|
nil,
|
||||||
)
|
)
|
||||||
|
|
||||||
rm, err := makeRulesManager(
|
rm, err := makeRulesManager(
|
||||||
|
|||||||
@@ -246,7 +246,9 @@ func (r *AnomalyRule) buildAndRunQuery(ctx context.Context, orgID valuer.UUID, t
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
results, err := r.Threshold.ShouldAlert(*series, r.Unit())
|
results, err := r.Threshold.Eval(*series, r.Unit(), ruletypes.EvalData{
|
||||||
|
ActiveAlerts: r.ActiveAlertsLabelFP(),
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -296,7 +298,9 @@ func (r *AnomalyRule) buildAndRunQueryV5(ctx context.Context, orgID valuer.UUID,
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
results, err := r.Threshold.ShouldAlert(*series, r.Unit())
|
results, err := r.Threshold.Eval(*series, r.Unit(), ruletypes.EvalData{
|
||||||
|
ActiveAlerts: r.ActiveAlertsLabelFP(),
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -410,6 +414,7 @@ func (r *AnomalyRule) Eval(ctx context.Context, ts time.Time) (interface{}, erro
|
|||||||
GeneratorURL: r.GeneratorURL(),
|
GeneratorURL: r.GeneratorURL(),
|
||||||
Receivers: ruleReceiverMap[lbs.Map()[ruletypes.LabelThresholdName]],
|
Receivers: ruleReceiverMap[lbs.Map()[ruletypes.LabelThresholdName]],
|
||||||
Missing: smpl.IsMissing,
|
Missing: smpl.IsMissing,
|
||||||
|
IsRecovering: smpl.IsRecovering,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -422,6 +427,9 @@ func (r *AnomalyRule) Eval(ctx context.Context, ts time.Time) (interface{}, erro
|
|||||||
|
|
||||||
alert.Value = a.Value
|
alert.Value = a.Value
|
||||||
alert.Annotations = a.Annotations
|
alert.Annotations = a.Annotations
|
||||||
|
// Update the recovering and missing state of existing alert
|
||||||
|
alert.IsRecovering = a.IsRecovering
|
||||||
|
alert.Missing = a.Missing
|
||||||
if v, ok := alert.Labels.Map()[ruletypes.LabelThresholdName]; ok {
|
if v, ok := alert.Labels.Map()[ruletypes.LabelThresholdName]; ok {
|
||||||
alert.Receivers = ruleReceiverMap[v]
|
alert.Receivers = ruleReceiverMap[v]
|
||||||
}
|
}
|
||||||
@@ -480,6 +488,30 @@ func (r *AnomalyRule) Eval(ctx context.Context, ts time.Time) (interface{}, erro
|
|||||||
Value: a.Value,
|
Value: a.Value,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// We need to change firing alert to recovering if the returned sample meets recovery threshold
|
||||||
|
changeFiringToRecovering := a.State == model.StateFiring && a.IsRecovering
|
||||||
|
// We need to change recovering alerts to firing if the returned sample meets target threshold
|
||||||
|
changeRecoveringToFiring := a.State == model.StateRecovering && !a.IsRecovering && !a.Missing
|
||||||
|
// in any of the above case we need to update the status of alert
|
||||||
|
if changeFiringToRecovering || changeRecoveringToFiring {
|
||||||
|
state := model.StateRecovering
|
||||||
|
if changeRecoveringToFiring {
|
||||||
|
state = model.StateFiring
|
||||||
|
}
|
||||||
|
a.State = state
|
||||||
|
r.logger.DebugContext(ctx, "converting alert state", "name", r.Name(), "state", state)
|
||||||
|
itemsToAdd = append(itemsToAdd, model.RuleStateHistory{
|
||||||
|
RuleID: r.ID(),
|
||||||
|
RuleName: r.Name(),
|
||||||
|
State: state,
|
||||||
|
StateChanged: true,
|
||||||
|
UnixMilli: ts.UnixMilli(),
|
||||||
|
Labels: model.LabelsString(labelsJSON),
|
||||||
|
Fingerprint: a.QueryResultLables.Hash(),
|
||||||
|
Value: a.Value,
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
currentState := r.State()
|
currentState := r.State()
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
ignorePatterns: ['src/parser/*.ts'],
|
ignorePatterns: ['src/parser/*.ts', 'scripts/update-registry.js'],
|
||||||
env: {
|
env: {
|
||||||
browser: true,
|
browser: true,
|
||||||
es2021: true,
|
es2021: true,
|
||||||
|
|||||||
@@ -3,5 +3,6 @@ BUNDLE_ANALYSER="true"
|
|||||||
FRONTEND_API_ENDPOINT="http://localhost:8080/"
|
FRONTEND_API_ENDPOINT="http://localhost:8080/"
|
||||||
PYLON_APP_ID="pylon-app-id"
|
PYLON_APP_ID="pylon-app-id"
|
||||||
APPCUES_APP_ID="appcess-app-id"
|
APPCUES_APP_ID="appcess-app-id"
|
||||||
|
PYLON_IDENTITY_SECRET="pylon-identity-secret"
|
||||||
|
|
||||||
CI="1"
|
CI="1"
|
||||||
@@ -14,7 +14,7 @@
|
|||||||
"jest": "jest",
|
"jest": "jest",
|
||||||
"jest:coverage": "jest --coverage",
|
"jest:coverage": "jest --coverage",
|
||||||
"jest:watch": "jest --watch",
|
"jest:watch": "jest --watch",
|
||||||
"postinstall": "yarn i18n:generate-hash && (is-ci || yarn husky:configure)",
|
"postinstall": "yarn i18n:generate-hash && (is-ci || yarn husky:configure) && node scripts/update-registry.js",
|
||||||
"husky:configure": "cd .. && husky install frontend/.husky && cd frontend && chmod ug+x .husky/*",
|
"husky:configure": "cd .. && husky install frontend/.husky && cd frontend && chmod ug+x .husky/*",
|
||||||
"commitlint": "commitlint --edit $1",
|
"commitlint": "commitlint --edit $1",
|
||||||
"test": "jest",
|
"test": "jest",
|
||||||
@@ -38,7 +38,7 @@
|
|||||||
"@mdx-js/loader": "2.3.0",
|
"@mdx-js/loader": "2.3.0",
|
||||||
"@mdx-js/react": "2.3.0",
|
"@mdx-js/react": "2.3.0",
|
||||||
"@monaco-editor/react": "^4.3.1",
|
"@monaco-editor/react": "^4.3.1",
|
||||||
"@playwright/test": "1.54.1",
|
"@playwright/test": "1.55.1",
|
||||||
"@radix-ui/react-tabs": "1.0.4",
|
"@radix-ui/react-tabs": "1.0.4",
|
||||||
"@radix-ui/react-tooltip": "1.0.7",
|
"@radix-ui/react-tooltip": "1.0.7",
|
||||||
"@sentry/react": "8.41.0",
|
"@sentry/react": "8.41.0",
|
||||||
@@ -47,6 +47,7 @@
|
|||||||
"@signozhq/button": "0.0.2",
|
"@signozhq/button": "0.0.2",
|
||||||
"@signozhq/calendar": "0.0.0",
|
"@signozhq/calendar": "0.0.0",
|
||||||
"@signozhq/callout": "0.0.2",
|
"@signozhq/callout": "0.0.2",
|
||||||
|
"@signozhq/checkbox": "0.0.2",
|
||||||
"@signozhq/design-tokens": "1.1.4",
|
"@signozhq/design-tokens": "1.1.4",
|
||||||
"@signozhq/input": "0.0.2",
|
"@signozhq/input": "0.0.2",
|
||||||
"@signozhq/popover": "0.0.0",
|
"@signozhq/popover": "0.0.0",
|
||||||
@@ -83,6 +84,7 @@
|
|||||||
"color": "^4.2.1",
|
"color": "^4.2.1",
|
||||||
"color-alpha": "1.1.3",
|
"color-alpha": "1.1.3",
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
|
"crypto-js": "4.2.0",
|
||||||
"css-loader": "5.0.0",
|
"css-loader": "5.0.0",
|
||||||
"css-minimizer-webpack-plugin": "5.0.1",
|
"css-minimizer-webpack-plugin": "5.0.1",
|
||||||
"d3-hierarchy": "3.1.2",
|
"d3-hierarchy": "3.1.2",
|
||||||
@@ -112,7 +114,7 @@
|
|||||||
"overlayscrollbars": "^2.8.1",
|
"overlayscrollbars": "^2.8.1",
|
||||||
"overlayscrollbars-react": "^0.5.6",
|
"overlayscrollbars-react": "^0.5.6",
|
||||||
"papaparse": "5.4.1",
|
"papaparse": "5.4.1",
|
||||||
"posthog-js": "1.215.5",
|
"posthog-js": "1.298.0",
|
||||||
"rc-tween-one": "3.0.6",
|
"rc-tween-one": "3.0.6",
|
||||||
"react": "18.2.0",
|
"react": "18.2.0",
|
||||||
"react-addons-update": "15.6.3",
|
"react-addons-update": "15.6.3",
|
||||||
@@ -149,7 +151,6 @@
|
|||||||
"tsconfig-paths-webpack-plugin": "^3.5.1",
|
"tsconfig-paths-webpack-plugin": "^3.5.1",
|
||||||
"typescript": "^4.0.5",
|
"typescript": "^4.0.5",
|
||||||
"uplot": "1.6.31",
|
"uplot": "1.6.31",
|
||||||
"userpilot": "1.3.9",
|
|
||||||
"uuid": "^8.3.2",
|
"uuid": "^8.3.2",
|
||||||
"web-vitals": "^0.2.4",
|
"web-vitals": "^0.2.4",
|
||||||
"webpack": "5.94.0",
|
"webpack": "5.94.0",
|
||||||
@@ -186,6 +187,7 @@
|
|||||||
"@types/color": "^3.0.3",
|
"@types/color": "^3.0.3",
|
||||||
"@types/compression-webpack-plugin": "^9.0.0",
|
"@types/compression-webpack-plugin": "^9.0.0",
|
||||||
"@types/copy-webpack-plugin": "^8.0.1",
|
"@types/copy-webpack-plugin": "^8.0.1",
|
||||||
|
"@types/crypto-js": "4.2.2",
|
||||||
"@types/dompurify": "^2.4.0",
|
"@types/dompurify": "^2.4.0",
|
||||||
"@types/event-source-polyfill": "^1.0.0",
|
"@types/event-source-polyfill": "^1.0.0",
|
||||||
"@types/fontfaceobserver": "2.1.0",
|
"@types/fontfaceobserver": "2.1.0",
|
||||||
|
|||||||
1
frontend/public/Logos/amazon-bedrock.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" fill-rule="evenodd" style="flex:none;line-height:1" viewBox="0 0 24 24"><title>AWS</title><path d="M6.763 11.212q.002.446.088.71c.064.176.144.368.256.576.04.063.056.127.056.183q.002.12-.152.24l-.503.335a.4.4 0 0 1-.208.072q-.12-.002-.239-.112a2.5 2.5 0 0 1-.287-.375 6 6 0 0 1-.248-.471q-.934 1.101-2.347 1.101c-.67 0-1.205-.191-1.596-.574-.39-.384-.59-.894-.59-1.533 0-.678.24-1.23.726-1.644.487-.415 1.133-.623 1.955-.623.272 0 .551.024.846.064.296.04.6.104.918.176v-.583q-.001-.908-.375-1.277c-.255-.248-.686-.367-1.3-.367-.28 0-.568.031-.863.103s-.583.16-.862.272a2 2 0 0 1-.28.104.5.5 0 0 1-.127.023q-.168.002-.168-.247v-.391c0-.128.016-.224.056-.28a.6.6 0 0 1 .224-.167 4.6 4.6 0 0 1 1.005-.36 4.8 4.8 0 0 1 1.246-.151c.95 0 1.644.216 2.091.647q.661.646.662 1.963v2.586zm-3.24 1.214c.263 0 .534-.048.822-.144a1.8 1.8 0 0 0 .758-.51 1.3 1.3 0 0 0 .272-.512c.047-.191.08-.423.08-.694v-.335a7 7 0 0 0-.735-.136 6 6 0 0 0-.75-.048c-.535 0-.926.104-1.19.32-.263.215-.39.518-.39.917 0 .375.095.655.295.846.191.2.47.296.838.296m6.41.862c-.144 0-.24-.024-.304-.08-.064-.048-.12-.16-.168-.311L7.586 6.726a1.4 1.4 0 0 1-.072-.32c0-.128.064-.2.191-.2h.783q.227-.001.31.08c.065.048.113.16.16.312l1.342 5.284 1.245-5.284q.058-.24.151-.312a.55.55 0 0 1 .32-.08h.638c.152 0 .256.025.32.08.063.048.12.16.151.312l1.261 5.348 1.381-5.348q.074-.24.16-.312a.52.52 0 0 1 .311-.08h.743c.127 0 .2.065.2.2 0 .04-.009.08-.017.128a1 1 0 0 1-.056.2l-1.923 6.17q-.072.24-.168.311a.5.5 0 0 1-.303.08h-.687c-.15 0-.255-.024-.32-.08-.063-.056-.119-.16-.15-.32L12.32 7.747l-1.23 5.14c-.04.16-.087.264-.15.32-.065.056-.177.08-.32.08zm10.256.215c-.415 0-.83-.048-1.229-.143-.399-.096-.71-.2-.918-.32-.128-.071-.215-.151-.247-.223a.6.6 0 0 1-.048-.224v-.407c0-.167.064-.247.183-.247q.072 0 .144.024c.048.016.12.048.2.08q.408.181.878.279c.32.064.63.096.95.096.502 0 .894-.088 1.165-.264a.86.86 0 0 0 .415-.758.78.78 0 0 0-.215-.559c-.144-.151-.416-.287-.807-.415l-1.157-.36c-.583-.183-1.014-.454-1.277-.813a1.9 1.9 0 0 1-.4-1.158q0-.502.216-.886c.144-.255.335-.479.575-.654.24-.184.51-.32.83-.415.32-.096.655-.136 1.006-.136.175 0 .36.008.535.032.183.024.35.056.518.088q.24.058.455.127.216.072.336.144a.7.7 0 0 1 .24.2.43.43 0 0 1 .071.263v.375q-.002.254-.184.256a.8.8 0 0 1-.303-.096 3.65 3.65 0 0 0-1.532-.311c-.455 0-.815.071-1.062.223s-.375.383-.375.71c0 .224.08.416.24.567.16.152.454.304.877.44l1.134.358c.574.184.99.44 1.237.767s.367.702.367 1.117c0 .343-.072.655-.207.926a2.2 2.2 0 0 1-.583.703c-.248.2-.543.343-.886.447-.36.111-.734.167-1.142.167"/><path fill="#f90" d="M.378 15.475c3.384 1.963 7.56 3.153 11.877 3.153 2.914 0 6.114-.607 9.06-1.852.44-.2.814.287.383.607-2.626 1.94-6.442 2.969-9.722 2.969-4.598 0-8.74-1.7-11.87-4.526-.247-.223-.024-.527.272-.351m23.531-.2c.287.36-.08 2.826-1.485 4.007-.215.184-.423.088-.327-.151l.175-.439c.343-.88.802-2.198.52-2.555-.336-.43-2.22-.207-3.074-.103-.255.032-.295-.192-.063-.36 1.5-1.053 3.967-.75 4.254-.399"/></svg>
|
||||||
|
After Width: | Height: | Size: 3.0 KiB |
1
frontend/public/Logos/autogen.svg
Normal file
|
After Width: | Height: | Size: 20 KiB |
1
frontend/public/Logos/azure-openai.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24"><title>Azure</title><path fill="url(#a)" d="M7.242 1.613A1.11 1.11 0 0 1 8.295.857h6.977L8.03 22.316a1.11 1.11 0 0 1-1.052.755h-5.43a1.11 1.11 0 0 1-1.053-1.466z"/><path fill="#0078d4" d="M18.397 15.296H7.4a.51.51 0 0 0-.347.882l7.066 6.595c.206.192.477.298.758.298h6.226z"/><path fill="url(#b)" d="M15.272.857H7.497L0 23.071h7.775l1.596-4.73 5.068 4.73h6.665l-2.707-7.775h-7.998z"/><path fill="url(#c)" d="M17.193 1.613a1.11 1.11 0 0 0-1.052-.756h-7.81.035c.477 0 .9.304 1.052.756l6.748 19.992a1.11 1.11 0 0 1-1.052 1.466h-.12 7.895a1.11 1.11 0 0 0 1.052-1.466z"/><defs><linearGradient id="a" x1="8.247" x2="1.002" y1="1.626" y2="23.03" gradientUnits="userSpaceOnUse"><stop stop-color="#114a8b"/><stop offset="1" stop-color="#0669bc"/></linearGradient><linearGradient id="b" x1="14.042" x2="12.324" y1="15.302" y2="15.888" gradientUnits="userSpaceOnUse"><stop stop-opacity=".3"/><stop offset=".071" stop-opacity=".2"/><stop offset=".321" stop-opacity=".1"/><stop offset=".623" stop-opacity=".05"/><stop offset="1" stop-opacity="0"/></linearGradient><linearGradient id="c" x1="12.841" x2="20.793" y1="1.626" y2="22.814" gradientUnits="userSpaceOnUse"><stop stop-color="#3ccbf4"/><stop offset="1" stop-color="#2892df"/></linearGradient></defs></svg>
|
||||||
|
After Width: | Height: | Size: 1.3 KiB |
1
frontend/public/Logos/crew-ai.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24"><title>CrewAI</title><path fill="#461816" d="M19.41 10.783a2.75 2.75 0 0 1 2.471 1.355c.483.806.622 1.772.385 2.68l-.136.522a10 10 0 0 1-3.156 5.058c-.605.517-1.283 1.062-2.083 1.524l-.028.017c-.402.232-.884.511-1.398.756-1.19.602-2.475.997-3.798 1.167-.854.111-1.716.155-2.577.132h-.018a8.6 8.6 0 0 1-5.046-1.87l-.012-.01-.012-.01A8.02 8.02 0 0 1 1.22 17.42a10.9 10.9 0 0 1-.102-3.779A15.6 15.6 0 0 1 2.88 8.4a21.8 21.8 0 0 1 2.432-3.678 15.4 15.4 0 0 1 3.56-3.182A10 10 0 0 1 12.44.104h.004l.003-.002c2.057-.384 3.743.374 5.024 1.26a8.3 8.3 0 0 1 2.395 2.513l.024.04.023.042a5.47 5.47 0 0 1 .508 4.012c-.239.97-.577 1.914-1.01 2.814z"/><path fill="#fff" d="M18.861 13.165a.748.748 0 0 1 1.256.031c.199.332.256.73.159 1.103l-.137.522a7.94 7.94 0 0 1-2.504 4.014c-.572.49-1.138.939-1.774 1.306-.427.247-.857.496-1.303.707a9.6 9.6 0 0 1-3.155.973 14.3 14.3 0 0 1-2.257.116 6.53 6.53 0 0 1-3.837-1.422 5.97 5.97 0 0 1-2.071-3.494 8.9 8.9 0 0 1-.085-3.08 13.6 13.6 0 0 1 1.54-4.568 19.7 19.7 0 0 1 2.212-3.348 13.4 13.4 0 0 1 3.088-2.76 7.9 7.9 0 0 1 2.832-1.14c1.307-.245 2.434.207 3.481.933a6.2 6.2 0 0 1 1.806 1.892c.423.767.536 1.668.314 2.515a12.4 12.4 0 0 1-.99 2.67l-.223.497q-.48 1.07-.97 2.137a.76.76 0 0 1-.97.467 3.39 3.39 0 0 1-2.283-2.49c-.095-.83.04-1.669.39-2.426.288-.746.61-1.477.933-2.208l.248-.563a.53.53 0 0 0-.204-.742 2.35 2.35 0 0 0-1.2.702 25 25 0 0 0-1.614 1.767 21.6 21.6 0 0 0-2.619 4.184 7.6 7.6 0 0 0-.816 2.753 7 7 0 0 0 .07 2.219 2.055 2.055 0 0 0 1.934 1.715c1.801.1 3.59-.363 5.116-1.328a19 19 0 0 0 1.675-1.294c.752-.71 1.376-1.519 1.958-2.36"/></svg>
|
||||||
|
After Width: | Height: | Size: 1.7 KiB |
1
frontend/public/Logos/dashboards.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="150" height="150" fill="none" viewBox="0 0 150 150"><circle cx="75" cy="75" r="70" fill="#f3f4f6"/><g transform="translate(25 25)"><rect width="60" height="55" fill="#fcd34d" rx="6"/><path stroke="#d97706" stroke-linecap="round" stroke-linejoin="round" stroke-width="4" d="m10 40 15-15 15 8 15-23"/><rect width="35" height="55" x="65" fill="#6ee7b7" rx="6"/><rect width="20" height="6" x="73" y="10" fill="#059669" rx="3"/><rect width="15" height="6" x="73" y="22" fill="#059669" opacity=".7" rx="3"/><rect width="18" height="6" x="73" y="34" fill="#059669" opacity=".7" rx="3"/><rect width="100" height="40" y="60" fill="#a5b4fc" rx="6"/><rect width="80" height="8" x="10" y="70" fill="#4f46e5" rx="4"/><rect width="50" height="8" x="20" y="83" fill="#6366f1" rx="4"/></g></svg>
|
||||||
|
After Width: | Height: | Size: 826 B |
1
frontend/public/Logos/envoy.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128"><path fill="#b31aab" d="m33.172 61.48.176 7.325 7.797 4.785-.176-7.324Zm19.031 30.504-.176-7.18-6.84-4.206c-.085-.059-.203-.145-.289-.203l.176 7.207Zm-24.355 9.688L10.012 90.715l-.438-18.367 8.758-3.746-.172-7.352-13.969 5.969c-1.074.46-1.714 1.441-1.687 2.566l.523 22.055c.032 1.125.73 2.25 1.836 2.941l21.383 13.149c.992.605 2.215.78 3.203.46a.8.8 0 0 0 .29-.113l13.124-5.593-7.129-4.383Zm0 0"/><path fill="#d163ce" d="M85.488 61.047c-.031-1.328-.843-2.625-2.125-3.43L57.38 41.672l-.813.344.176 7.726 20.57 12.63.493 20.648 7.86 4.812.433-.172ZM54.383 97.289 30.262 82.47l-.582-24.883 11-4.7-.203-8.562-17.082 7.293c-1.25.547-2.008 1.672-1.977 3l.668 29.18c.031 1.324.844 2.625 2.125 3.402l28.281 17.387c1.164.723 2.563.894 3.754.547.117-.028.234-.086.348-.145l16.703-7.12-8.32-5.102Zm0 0"/><path fill="#e13eaf" d="M122.234 40.633 85.98 18.343c-1.335-.808-2.937-1.038-4.304-.605-.145.028-.262.086-.406.145l-35.383 15.11c-1.426.605-2.297 1.902-2.27 3.429l.903 37.371c.03 1.527.96 2.996 2.445 3.89l36.254 22.262c1.336.805 2.937 1.035 4.277.606.145-.031.262-.09.406-.145l35.383-15.11c1.426-.605 2.297-1.933 2.27-3.433l-.875-37.367c-.028-1.473-.961-2.969-2.446-3.863M85.398 91.64 53.891 72.293l-.79-32.496 30.727-13.121 31.512 19.347.785 32.497Zm0 0"/></svg>
|
||||||
|
After Width: | Height: | Size: 1.3 KiB |
1
frontend/public/Logos/fly-io.svg
Normal file
|
After Width: | Height: | Size: 6.0 KiB |
1
frontend/public/Logos/honeycomb.svg
Normal file
|
After Width: | Height: | Size: 5.5 KiB |
1
frontend/public/Logos/litellm.svg
Normal file
|
After Width: | Height: | Size: 19 KiB |
1
frontend/public/Logos/logs.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="150" height="150" fill="none" viewBox="0 0 150 150"><circle cx="75" cy="75" r="70" fill="#f3f4f6"/><g transform="translate(25 40)"><rect width="12" height="12" fill="#059669" opacity=".8" rx="3"/><rect width="80" height="12" x="20" fill="#10b981" rx="3"/><rect width="12" height="12" y="22" fill="#059669" opacity=".8" rx="3"/><rect width="65" height="12" x="20" y="22" fill="#10b981" rx="3"/><rect width="12" height="12" y="44" fill="#059669" opacity=".8" rx="3"/><rect width="75" height="12" x="20" y="44" fill="#10b981" rx="3"/><rect width="12" height="12" y="66" fill="#059669" opacity=".8" rx="3"/><rect width="50" height="12" x="20" y="66" fill="#10b981" rx="3"/></g></svg>
|
||||||
|
After Width: | Height: | Size: 726 B |
1
frontend/public/Logos/metrics.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="150" height="150" fill="none" viewBox="0 0 150 150"><circle cx="75" cy="75" r="70" fill="#f3f4f6"/><defs><linearGradient id="a" x1="0" x2="0" y1="0" y2="1"><stop offset="0%" stop-color="#f59e0b" stop-opacity=".5"/><stop offset="100%" stop-color="#f59e0b" stop-opacity=".05"/></linearGradient></defs><g transform="translate(25 35)"><path stroke="#d1d5db" stroke-linecap="round" stroke-width="2" d="M0 80h100M0 80V0"/><path fill="url(#a)" d="M2 78c18 0 28-28 48-23s30-35 48-40v63z"/><path stroke="#d97706" stroke-linecap="round" stroke-width="5" d="M2 78c18 0 28-28 48-23s30-35 48-40"/><circle cx="50" cy="55" r="4" fill="#f59e0b"/><circle cx="98" cy="15" r="4" fill="#f59e0b"/></g></svg>
|
||||||
|
After Width: | Height: | Size: 733 B |
1
frontend/public/Logos/pydantic-ai.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24"><title>PydanticAI</title><path fill="#e72564" d="M13.223 22.86c-.605.83-1.844.83-2.448 0L5.74 15.944a1.514 1.514 0 0 1 .73-2.322l5.035-1.738c.32-.11.668-.11.988 0l5.035 1.738c.962.332 1.329 1.5.73 2.322zm-1.224-1.259 4.688-6.439-4.688-1.618-4.688 1.618L12 21.602z"/><path fill="#e723a0" d="M23.71 13.463c.604.832.221 2.01-.756 2.328l-8.133 2.652a1.514 1.514 0 0 1-1.983-1.412l-.097-5.326c-.006-.338.101-.67.305-.94l3.209-4.25a1.514 1.514 0 0 1 2.434.022l5.022 6.926zm-1.574.775L17.46 7.79l-2.988 3.958.09 4.959z"/><path fill="#e520e9" d="M18.016.591a1.514 1.514 0 0 1 1.98 1.44l.009 8.554a1.514 1.514 0 0 1-1.956 1.45l-5.095-1.554a1.5 1.5 0 0 1-.8-.58l-3.05-4.366a1.514 1.514 0 0 1 .774-2.308zm.25 1.738L10.69 4.783l2.841 4.065 4.744 1.446-.008-7.965z"/><path fill="#e520e9" d="M5.99.595a1.514 1.514 0 0 0-1.98 1.44L4 10.588a1.514 1.514 0 0 0 1.956 1.45l5.095-1.554c.323-.098.605-.303.799-.58l3.052-4.366a1.514 1.514 0 0 0-.775-2.308zm-.25 1.738 7.577 2.454-2.842 4.065-4.743 1.446.007-7.965z"/><path fill="#e723a0" d="M.29 13.461a1.514 1.514 0 0 0 .756 2.329l8.133 2.651a1.514 1.514 0 0 0 1.983-1.412l.097-5.325a1.5 1.5 0 0 0-.305-.94L7.745 6.513a1.514 1.514 0 0 0-2.434.023L.289 13.461zm1.574.776L6.54 7.788l2.988 3.959-.09 4.958z"/><path fill="#ff96d1" d="m16.942 17.751 1.316-1.806q.178-.248.245-.523l-2.63.858-1.627 2.235a1.5 1.5 0 0 0 .575-.072zm-4.196-5.78.033 1.842 1.742.602-.034-1.843-1.741-.6zm7.257-3.622-1.314-1.812a1.5 1.5 0 0 0-.419-.393l.003 2.767 1.624 2.24q.107-.261.108-.566zm-5.038 2.746-1.762-.537 1.11-1.471 1.762.537zm-2.961-1.41 1.056-1.51-1.056-1.51-1.056 1.51zM9.368 3.509c.145-.122.316-.219.51-.282l2.12-.686 2.13.69c.191.062.36.157.503.276l-2.634.853zm1.433 7.053L9.691 9.09l-1.762.537 1.11 1.47 1.762-.537zm-6.696.584L5.733 8.9l.003-2.763c-.16.1-.305.232-.425.398L4.003 8.339l-.002 2.25q.002.299.104.557m7.149.824-1.741.601-.034 1.843 1.742-.601zM9.75 18.513l-1.628-2.237-2.629-.857q.068.276.247.525l1.313 1.804 2.126.693c.192.062.385.085.571.072"/></svg>
|
||||||
|
After Width: | Height: | Size: 2.1 KiB |
1
frontend/public/Logos/traces.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="150" height="150" fill="none" viewBox="0 0 150 150"><circle cx="75" cy="75" r="70" fill="#f3f4f6"/><rect width="100" height="14" x="25" y="45" fill="#4f46e5" rx="4"/><rect width="60" height="14" x="40" y="68" fill="#6366f1" rx="4"/><rect width="20" height="14" x="105" y="91" fill="#818cf8" rx="4"/><path stroke="#c7d2fe" stroke-width="2" d="M35 59v16m5 0h-5M100 59v39m5 0h-5"/></svg>
|
||||||
|
After Width: | Height: | Size: 431 B |
50
frontend/scripts/update-registry.js
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-var-requires, import/no-dynamic-require, simple-import-sort/imports, simple-import-sort/exports */
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
// 1. Define paths
|
||||||
|
const packageJsonPath = path.resolve(__dirname, '../package.json');
|
||||||
|
const registryPath = path.resolve(
|
||||||
|
__dirname,
|
||||||
|
'../src/auto-import-registry.d.ts',
|
||||||
|
);
|
||||||
|
|
||||||
|
// 2. Read package.json
|
||||||
|
const packageJson = require(packageJsonPath);
|
||||||
|
|
||||||
|
// 3. Combine dependencies and devDependencies
|
||||||
|
const allDeps = {
|
||||||
|
...packageJson.dependencies,
|
||||||
|
...packageJson.devDependencies,
|
||||||
|
};
|
||||||
|
|
||||||
|
// 4. Filter for @signozhq packages
|
||||||
|
const signozPackages = Object.keys(allDeps).filter((dep) =>
|
||||||
|
dep.startsWith('@signozhq/'),
|
||||||
|
);
|
||||||
|
|
||||||
|
// 5. Generate file content
|
||||||
|
const fileContent = `// -------------------------------------------------------------------------
|
||||||
|
// AUTO-GENERATED FILE
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
// This file is generated by scripts/update-registry.js automatically
|
||||||
|
// whenever you run 'yarn install' or 'npm install'.
|
||||||
|
//
|
||||||
|
// It forces VS Code to index these specific packages to fix auto-import
|
||||||
|
// performance issues in TypeScript 4.x.
|
||||||
|
//
|
||||||
|
// PR for reference: https://github.com/SigNoz/signoz/pull/9694
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
|
||||||
|
${signozPackages.map((pkg) => `import '${pkg}';`).join('\n')}
|
||||||
|
`;
|
||||||
|
|
||||||
|
// 6. Write the file
|
||||||
|
try {
|
||||||
|
fs.writeFileSync(registryPath, fileContent);
|
||||||
|
console.log(
|
||||||
|
`✅ Auto-import registry updated with ${signozPackages.length} @signozhq packages.`,
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('❌ Failed to update auto-import registry:', err);
|
||||||
|
}
|
||||||
@@ -245,6 +245,14 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element {
|
|||||||
history.replace(newLocation);
|
history.replace(newLocation);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// if the current route is public dashboard then don't redirect to login
|
||||||
|
const isPublicDashboard = currentRoute?.path === ROUTES.PUBLIC_DASHBOARD;
|
||||||
|
|
||||||
|
if (isPublicDashboard) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// if the current route
|
// if the current route
|
||||||
if (currentRoute) {
|
if (currentRoute) {
|
||||||
const { isPrivate, key } = currentRoute;
|
const { isPrivate, key } = currentRoute;
|
||||||
|
|||||||
@@ -7,11 +7,12 @@ import AppLoading from 'components/AppLoading/AppLoading';
|
|||||||
import KBarCommandPalette from 'components/KBarCommandPalette/KBarCommandPalette';
|
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 { FeatureKeys } from 'constants/features';
|
import { FeatureKeys } from 'constants/features';
|
||||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||||
import ROUTES from 'constants/routes';
|
import ROUTES from 'constants/routes';
|
||||||
import AppLayout from 'container/AppLayout';
|
import AppLayout from 'container/AppLayout';
|
||||||
|
import Hex from 'crypto-js/enc-hex';
|
||||||
|
import HmacSHA256 from 'crypto-js/hmac-sha256';
|
||||||
import { KeyboardHotkeysProvider } from 'hooks/hotkeys/useKeyboardHotkeys';
|
import { KeyboardHotkeysProvider } from 'hooks/hotkeys/useKeyboardHotkeys';
|
||||||
import { useThemeConfig } from 'hooks/useDarkMode';
|
import { useThemeConfig } from 'hooks/useDarkMode';
|
||||||
import { useGetTenantLicense } from 'hooks/useGetTenantLicense';
|
import { useGetTenantLicense } from 'hooks/useGetTenantLicense';
|
||||||
@@ -21,7 +22,6 @@ import { StatusCodes } from 'http-status-codes';
|
|||||||
import history from 'lib/history';
|
import history from 'lib/history';
|
||||||
import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFallback';
|
import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFallback';
|
||||||
import posthog from 'posthog-js';
|
import posthog from 'posthog-js';
|
||||||
import AlertRuleProvider from 'providers/Alert';
|
|
||||||
import { useAppContext } from 'providers/App/App';
|
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';
|
||||||
@@ -33,7 +33,6 @@ import { Suspense, useCallback, useEffect, useState } from 'react';
|
|||||||
import { Route, Router, Switch } from 'react-router-dom';
|
import { Route, Router, Switch } from 'react-router-dom';
|
||||||
import { CompatRouter } from 'react-router-dom-v5-compat';
|
import { CompatRouter } from 'react-router-dom-v5-compat';
|
||||||
import { LicenseStatus } from 'types/api/licensesV3/getActive';
|
import { LicenseStatus } from 'types/api/licensesV3/getActive';
|
||||||
import { Userpilot } from 'userpilot';
|
|
||||||
import { extractDomain } from 'utils/app';
|
import { extractDomain } from 'utils/app';
|
||||||
|
|
||||||
import { Home } from './pageComponents';
|
import { Home } from './pageComponents';
|
||||||
@@ -84,9 +83,9 @@ function App(): JSX.Element {
|
|||||||
email,
|
email,
|
||||||
name: displayName,
|
name: displayName,
|
||||||
company_name: orgName,
|
company_name: orgName,
|
||||||
tenant_id: hostNameParts[0],
|
deployment_name: hostNameParts[0],
|
||||||
data_region: hostNameParts[1],
|
data_region: hostNameParts[1],
|
||||||
tenant_url: hostname,
|
deployment_url: hostname,
|
||||||
company_domain: domain,
|
company_domain: domain,
|
||||||
source: 'signoz-ui',
|
source: 'signoz-ui',
|
||||||
role,
|
role,
|
||||||
@@ -94,9 +93,9 @@ function App(): JSX.Element {
|
|||||||
|
|
||||||
const groupTraits = {
|
const groupTraits = {
|
||||||
name: orgName,
|
name: orgName,
|
||||||
tenant_id: hostNameParts[0],
|
deployment_name: hostNameParts[0],
|
||||||
data_region: hostNameParts[1],
|
data_region: hostNameParts[1],
|
||||||
tenant_url: hostname,
|
deployment_url: hostname,
|
||||||
company_domain: domain,
|
company_domain: domain,
|
||||||
source: 'signoz-ui',
|
source: 'signoz-ui',
|
||||||
};
|
};
|
||||||
@@ -111,37 +110,23 @@ function App(): JSX.Element {
|
|||||||
if (window && window.Appcues) {
|
if (window && window.Appcues) {
|
||||||
window.Appcues.identify(id, {
|
window.Appcues.identify(id, {
|
||||||
name: displayName,
|
name: displayName,
|
||||||
|
deployment_name: hostNameParts[0],
|
||||||
tenant_id: hostNameParts[0],
|
|
||||||
data_region: hostNameParts[1],
|
data_region: hostNameParts[1],
|
||||||
tenant_url: hostname,
|
deployment_url: hostname,
|
||||||
company_domain: domain,
|
company_domain: domain,
|
||||||
|
|
||||||
companyName: orgName,
|
companyName: orgName,
|
||||||
email,
|
email,
|
||||||
paidUser: !!trialInfo?.trialConvertedToSubscription,
|
paidUser: !!trialInfo?.trialConvertedToSubscription,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Userpilot.identify(email, {
|
|
||||||
email,
|
|
||||||
name: displayName,
|
|
||||||
orgName,
|
|
||||||
tenant_id: hostNameParts[0],
|
|
||||||
data_region: hostNameParts[1],
|
|
||||||
tenant_url: hostname,
|
|
||||||
company_domain: domain,
|
|
||||||
source: 'signoz-ui',
|
|
||||||
isPaidUser: !!trialInfo?.trialConvertedToSubscription,
|
|
||||||
});
|
|
||||||
|
|
||||||
posthog?.identify(id, {
|
posthog?.identify(id, {
|
||||||
email,
|
email,
|
||||||
name: displayName,
|
name: displayName,
|
||||||
orgName,
|
orgName,
|
||||||
tenant_id: hostNameParts[0],
|
deployment_name: hostNameParts[0],
|
||||||
data_region: hostNameParts[1],
|
data_region: hostNameParts[1],
|
||||||
tenant_url: hostname,
|
deployment_url: hostname,
|
||||||
company_domain: domain,
|
company_domain: domain,
|
||||||
source: 'signoz-ui',
|
source: 'signoz-ui',
|
||||||
isPaidUser: !!trialInfo?.trialConvertedToSubscription,
|
isPaidUser: !!trialInfo?.trialConvertedToSubscription,
|
||||||
@@ -149,9 +134,9 @@ function App(): JSX.Element {
|
|||||||
|
|
||||||
posthog?.group('company', orgId, {
|
posthog?.group('company', orgId, {
|
||||||
name: orgName,
|
name: orgName,
|
||||||
tenant_id: hostNameParts[0],
|
deployment_name: hostNameParts[0],
|
||||||
data_region: hostNameParts[1],
|
data_region: hostNameParts[1],
|
||||||
tenant_url: hostname,
|
deployment_url: hostname,
|
||||||
company_domain: domain,
|
company_domain: domain,
|
||||||
source: 'signoz-ui',
|
source: 'signoz-ui',
|
||||||
isPaidUser: !!trialInfo?.trialConvertedToSubscription,
|
isPaidUser: !!trialInfo?.trialConvertedToSubscription,
|
||||||
@@ -228,7 +213,10 @@ function App(): JSX.Element {
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (pathname === ROUTES.ONBOARDING) {
|
if (
|
||||||
|
pathname === ROUTES.ONBOARDING ||
|
||||||
|
pathname.startsWith('/public/dashboard/')
|
||||||
|
) {
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
window.Pylon('hideChatBubble');
|
window.Pylon('hideChatBubble');
|
||||||
@@ -270,11 +258,20 @@ function App(): JSX.Element {
|
|||||||
!showAddCreditCardModal &&
|
!showAddCreditCardModal &&
|
||||||
(isCloudUser || isEnterpriseSelfHostedUser)
|
(isCloudUser || isEnterpriseSelfHostedUser)
|
||||||
) {
|
) {
|
||||||
|
const email = user.email || '';
|
||||||
|
const secret = process.env.PYLON_IDENTITY_SECRET || '';
|
||||||
|
let emailHash = '';
|
||||||
|
|
||||||
|
if (email && secret) {
|
||||||
|
emailHash = HmacSHA256(email, Hex.parse(secret)).toString(Hex);
|
||||||
|
}
|
||||||
|
|
||||||
window.pylon = {
|
window.pylon = {
|
||||||
chat_settings: {
|
chat_settings: {
|
||||||
app_id: process.env.PYLON_APP_ID,
|
app_id: process.env.PYLON_APP_ID,
|
||||||
email: user.email,
|
email: user.email,
|
||||||
name: user.displayName || user.email,
|
name: user.displayName || user.email,
|
||||||
|
email_hash: emailHash,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -308,10 +305,6 @@ function App(): JSX.Element {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (process.env.USERPILOT_KEY) {
|
|
||||||
Userpilot.initialize(process.env.USERPILOT_KEY);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isSentryInitialized) {
|
if (!isSentryInitialized) {
|
||||||
Sentry.init({
|
Sentry.init({
|
||||||
dsn: process.env.SENTRY_DSN,
|
dsn: process.env.SENTRY_DSN,
|
||||||
@@ -372,7 +365,6 @@ function App(): JSX.Element {
|
|||||||
<Router history={history}>
|
<Router history={history}>
|
||||||
<CompatRouter>
|
<CompatRouter>
|
||||||
<KBarCommandPaletteProvider>
|
<KBarCommandPaletteProvider>
|
||||||
<UserpilotRouteTracker />
|
|
||||||
<KBarCommandPalette />
|
<KBarCommandPalette />
|
||||||
<NotificationProvider>
|
<NotificationProvider>
|
||||||
<ErrorModalProvider>
|
<ErrorModalProvider>
|
||||||
@@ -381,26 +373,24 @@ function App(): JSX.Element {
|
|||||||
<QueryBuilderProvider>
|
<QueryBuilderProvider>
|
||||||
<DashboardProvider>
|
<DashboardProvider>
|
||||||
<KeyboardHotkeysProvider>
|
<KeyboardHotkeysProvider>
|
||||||
<AlertRuleProvider>
|
<AppLayout>
|
||||||
<AppLayout>
|
<PreferenceContextProvider>
|
||||||
<PreferenceContextProvider>
|
<Suspense fallback={<Spinner size="large" tip="Loading..." />}>
|
||||||
<Suspense fallback={<Spinner size="large" tip="Loading..." />}>
|
<Switch>
|
||||||
<Switch>
|
{routes.map(({ path, component, exact }) => (
|
||||||
{routes.map(({ path, component, exact }) => (
|
<Route
|
||||||
<Route
|
key={`${path}`}
|
||||||
key={`${path}`}
|
exact={exact}
|
||||||
exact={exact}
|
path={path}
|
||||||
path={path}
|
component={component}
|
||||||
component={component}
|
/>
|
||||||
/>
|
))}
|
||||||
))}
|
<Route exact path="/" component={Home} />
|
||||||
<Route exact path="/" component={Home} />
|
<Route path="*" component={NotFound} />
|
||||||
<Route path="*" component={NotFound} />
|
</Switch>
|
||||||
</Switch>
|
</Suspense>
|
||||||
</Suspense>
|
</PreferenceContextProvider>
|
||||||
</PreferenceContextProvider>
|
</AppLayout>
|
||||||
</AppLayout>
|
|
||||||
</AlertRuleProvider>
|
|
||||||
</KeyboardHotkeysProvider>
|
</KeyboardHotkeysProvider>
|
||||||
</DashboardProvider>
|
</DashboardProvider>
|
||||||
</QueryBuilderProvider>
|
</QueryBuilderProvider>
|
||||||
|
|||||||
@@ -295,3 +295,10 @@ export const MetricsExplorer = Loadable(
|
|||||||
export const ApiMonitoring = Loadable(
|
export const ApiMonitoring = Loadable(
|
||||||
() => import(/* webpackChunkName: "ApiMonitoring" */ 'pages/ApiMonitoring'),
|
() => import(/* webpackChunkName: "ApiMonitoring" */ 'pages/ApiMonitoring'),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const PublicDashboardPage = Loadable(
|
||||||
|
() =>
|
||||||
|
import(
|
||||||
|
/* webpackChunkName: "Public Dashboard Page" */ 'pages/PublicDashboard'
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ import {
|
|||||||
OrgOnboarding,
|
OrgOnboarding,
|
||||||
PasswordReset,
|
PasswordReset,
|
||||||
PipelinePage,
|
PipelinePage,
|
||||||
|
PublicDashboardPage,
|
||||||
ServiceMapPage,
|
ServiceMapPage,
|
||||||
ServiceMetricsPage,
|
ServiceMetricsPage,
|
||||||
ServicesTablePage,
|
ServicesTablePage,
|
||||||
@@ -169,6 +170,13 @@ const routes: AppRoutes[] = [
|
|||||||
isPrivate: true,
|
isPrivate: true,
|
||||||
key: 'DASHBOARD',
|
key: 'DASHBOARD',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: ROUTES.PUBLIC_DASHBOARD,
|
||||||
|
exact: false,
|
||||||
|
component: PublicDashboardPage,
|
||||||
|
isPrivate: false,
|
||||||
|
key: 'PUBLIC_DASHBOARD',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: ROUTES.DASHBOARD_WIDGET,
|
path: ROUTES.DASHBOARD_WIDGET,
|
||||||
exact: true,
|
exact: true,
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { LogEventAxiosInstance as axios } from 'api';
|
import { LogEventAxiosInstance as axios } from 'api';
|
||||||
|
import getLocalStorageApi from 'api/browser/localstorage/get';
|
||||||
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
||||||
import { AxiosError } from 'axios';
|
import { AxiosError } from 'axios';
|
||||||
|
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||||
import { EventSuccessPayloadProps } from 'types/api/events/types';
|
import { EventSuccessPayloadProps } from 'types/api/events/types';
|
||||||
|
|
||||||
@@ -11,9 +13,14 @@ const logEvent = async (
|
|||||||
rateLimited?: boolean,
|
rateLimited?: boolean,
|
||||||
): Promise<SuccessResponse<EventSuccessPayloadProps> | ErrorResponse> => {
|
): Promise<SuccessResponse<EventSuccessPayloadProps> | ErrorResponse> => {
|
||||||
try {
|
try {
|
||||||
// add tenant_url to attributes
|
// add deployment_url and user_email to attributes
|
||||||
const { hostname } = window.location;
|
const { hostname } = window.location;
|
||||||
const updatedAttributes = { ...attributes, tenant_url: hostname };
|
const userEmail = getLocalStorageApi(LOCALSTORAGE.LOGGED_IN_USER_EMAIL);
|
||||||
|
const updatedAttributes = {
|
||||||
|
...attributes,
|
||||||
|
deployment_url: hostname,
|
||||||
|
user_email: userEmail,
|
||||||
|
};
|
||||||
const response = await axios.post('/event', {
|
const response = await axios.post('/event', {
|
||||||
eventName,
|
eventName,
|
||||||
attributes: updatedAttributes,
|
attributes: updatedAttributes,
|
||||||
|
|||||||
28
frontend/src/api/dashboard/public/createPublicDashboard.ts
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import axios from 'api';
|
||||||
|
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
|
||||||
|
import { AxiosError } from 'axios';
|
||||||
|
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
|
||||||
|
import { CreatePublicDashboardProps } from 'types/api/dashboard/public/create';
|
||||||
|
|
||||||
|
const createPublicDashboard = async (
|
||||||
|
props: CreatePublicDashboardProps,
|
||||||
|
): Promise<SuccessResponseV2<CreatePublicDashboardProps>> => {
|
||||||
|
|
||||||
|
const { dashboardId, timeRangeEnabled = false, defaultTimeRange = '30m' } = props;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await axios.post(
|
||||||
|
`/dashboards/${dashboardId}/public`,
|
||||||
|
{ timeRangeEnabled, defaultTimeRange },
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
httpStatusCode: response.status,
|
||||||
|
data: response.data.data,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default createPublicDashboard;
|
||||||
20
frontend/src/api/dashboard/public/getPublicDashboardData.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import axios from 'api';
|
||||||
|
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
|
||||||
|
import { AxiosError } from 'axios';
|
||||||
|
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
|
||||||
|
import { GetPublicDashboardDataProps, PayloadProps,PublicDashboardDataProps } from 'types/api/dashboard/public/get';
|
||||||
|
|
||||||
|
const getPublicDashboardData = async (props: GetPublicDashboardDataProps): Promise<SuccessResponseV2<PublicDashboardDataProps>> => {
|
||||||
|
try {
|
||||||
|
const response = await axios.get<PayloadProps>(`/public/dashboards/${props.id}`);
|
||||||
|
|
||||||
|
return {
|
||||||
|
httpStatusCode: response.status,
|
||||||
|
data: response.data.data,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default getPublicDashboardData;
|
||||||
20
frontend/src/api/dashboard/public/getPublicDashboardMeta.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import axios from 'api';
|
||||||
|
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
|
||||||
|
import { AxiosError } from 'axios';
|
||||||
|
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
|
||||||
|
import { GetPublicDashboardMetaProps, PayloadProps,PublicDashboardMetaProps } from 'types/api/dashboard/public/getMeta';
|
||||||
|
|
||||||
|
const getPublicDashboardMeta = async (props: GetPublicDashboardMetaProps): Promise<SuccessResponseV2<PublicDashboardMetaProps>> => {
|
||||||
|
try {
|
||||||
|
const response = await axios.get<PayloadProps>(`/dashboards/${props.id}/public`);
|
||||||
|
|
||||||
|
return {
|
||||||
|
httpStatusCode: response.status,
|
||||||
|
data: response.data.data,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default getPublicDashboardMeta;
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import axios from 'api';
|
||||||
|
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
|
||||||
|
import { MetricRangePayloadV5 } from 'api/v5/v5';
|
||||||
|
import { AxiosError } from 'axios';
|
||||||
|
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
|
||||||
|
import { GetPublicDashboardWidgetDataProps } from 'types/api/dashboard/public/getWidgetData';
|
||||||
|
|
||||||
|
|
||||||
|
const getPublicDashboardWidgetData = async (props: GetPublicDashboardWidgetDataProps): Promise<SuccessResponseV2<MetricRangePayloadV5>> => {
|
||||||
|
try {
|
||||||
|
const response = await axios.get(`/public/dashboards/${props.id}/widgets/${props.index}/query_range`, {
|
||||||
|
params: {
|
||||||
|
startTime: props.startTime,
|
||||||
|
endTime: props.endTime,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
httpStatusCode: response.status,
|
||||||
|
data: response.data,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default getPublicDashboardWidgetData;
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import axios from 'api';
|
||||||
|
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
|
||||||
|
import { AxiosError } from 'axios';
|
||||||
|
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
|
||||||
|
import { PayloadProps,RevokePublicDashboardAccessProps } from 'types/api/dashboard/public/delete';
|
||||||
|
|
||||||
|
const revokePublicDashboardAccess = async (
|
||||||
|
props: RevokePublicDashboardAccessProps,
|
||||||
|
): Promise<SuccessResponseV2<PayloadProps>> => {
|
||||||
|
try {
|
||||||
|
const response = await axios.delete<PayloadProps>(`/dashboards/${props.id}/public`);
|
||||||
|
|
||||||
|
return {
|
||||||
|
httpStatusCode: response.status,
|
||||||
|
data: response.data,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default revokePublicDashboardAccess;
|
||||||
28
frontend/src/api/dashboard/public/updatePublicDashboard.ts
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import axios from 'api';
|
||||||
|
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
|
||||||
|
import { AxiosError } from 'axios';
|
||||||
|
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
|
||||||
|
import { UpdatePublicDashboardProps } from 'types/api/dashboard/public/update';
|
||||||
|
|
||||||
|
const updatePublicDashboard = async (
|
||||||
|
props: UpdatePublicDashboardProps,
|
||||||
|
): Promise<SuccessResponseV2<UpdatePublicDashboardProps>> => {
|
||||||
|
|
||||||
|
const { dashboardId, timeRangeEnabled = false, defaultTimeRange = '30m' } = props;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await axios.put(
|
||||||
|
`/dashboards/${dashboardId}/public`,
|
||||||
|
{ timeRangeEnabled, defaultTimeRange },
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
httpStatusCode: response.status,
|
||||||
|
data: response.data.data,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default updatePublicDashboard;
|
||||||
23
frontend/src/auto-import-registry.d.ts
vendored
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
// -------------------------------------------------------------------------
|
||||||
|
// AUTO-GENERATED FILE
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
// This file is generated by scripts/update-registry.js automatically
|
||||||
|
// whenever you run 'yarn install' or 'npm install'.
|
||||||
|
//
|
||||||
|
// It forces VS Code to index these specific packages to fix auto-import
|
||||||
|
// performance issues in TypeScript 4.x.
|
||||||
|
//
|
||||||
|
// PR for reference: https://github.com/SigNoz/signoz/pull/9694
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
|
||||||
|
import '@signozhq/badge';
|
||||||
|
import '@signozhq/button';
|
||||||
|
import '@signozhq/calendar';
|
||||||
|
import '@signozhq/callout';
|
||||||
|
import '@signozhq/design-tokens';
|
||||||
|
import '@signozhq/input';
|
||||||
|
import '@signozhq/popover';
|
||||||
|
import '@signozhq/resizable';
|
||||||
|
import '@signozhq/sonner';
|
||||||
|
import '@signozhq/table';
|
||||||
|
import '@signozhq/tooltip';
|
||||||
@@ -62,6 +62,8 @@ interface CustomTimePickerProps {
|
|||||||
showLiveLogs?: boolean;
|
showLiveLogs?: boolean;
|
||||||
onGoLive?: () => void;
|
onGoLive?: () => void;
|
||||||
onExitLiveLogs?: () => void;
|
onExitLiveLogs?: () => void;
|
||||||
|
/** When false, hides the "Recently Used" time ranges section */
|
||||||
|
showRecentlyUsed?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
function CustomTimePicker({
|
function CustomTimePicker({
|
||||||
@@ -81,6 +83,7 @@ function CustomTimePicker({
|
|||||||
onGoLive,
|
onGoLive,
|
||||||
onExitLiveLogs,
|
onExitLiveLogs,
|
||||||
showLiveLogs,
|
showLiveLogs,
|
||||||
|
showRecentlyUsed = true,
|
||||||
}: CustomTimePickerProps): JSX.Element {
|
}: CustomTimePickerProps): JSX.Element {
|
||||||
const [
|
const [
|
||||||
selectedTimePlaceholderValue,
|
selectedTimePlaceholderValue,
|
||||||
@@ -395,6 +398,7 @@ function CustomTimePicker({
|
|||||||
setActiveView={setActiveView}
|
setActiveView={setActiveView}
|
||||||
setIsOpenedFromFooter={setIsOpenedFromFooter}
|
setIsOpenedFromFooter={setIsOpenedFromFooter}
|
||||||
isOpenedFromFooter={isOpenedFromFooter}
|
isOpenedFromFooter={isOpenedFromFooter}
|
||||||
|
showRecentlyUsed={showRecentlyUsed}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
content
|
content
|
||||||
@@ -464,4 +468,5 @@ CustomTimePicker.defaultProps = {
|
|||||||
onCustomTimeStatusUpdate: noop,
|
onCustomTimeStatusUpdate: noop,
|
||||||
onExitLiveLogs: noop,
|
onExitLiveLogs: noop,
|
||||||
showLiveLogs: false,
|
showLiveLogs: false,
|
||||||
|
showRecentlyUsed: true,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ interface CustomTimePickerPopoverContentProps {
|
|||||||
isOpenedFromFooter: boolean;
|
isOpenedFromFooter: boolean;
|
||||||
setIsOpenedFromFooter: Dispatch<SetStateAction<boolean>>;
|
setIsOpenedFromFooter: Dispatch<SetStateAction<boolean>>;
|
||||||
onExitLiveLogs: () => void;
|
onExitLiveLogs: () => void;
|
||||||
|
showRecentlyUsed: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface RecentlyUsedDateTimeRange {
|
interface RecentlyUsedDateTimeRange {
|
||||||
@@ -72,6 +73,7 @@ function CustomTimePickerPopoverContent({
|
|||||||
isOpenedFromFooter,
|
isOpenedFromFooter,
|
||||||
setIsOpenedFromFooter,
|
setIsOpenedFromFooter,
|
||||||
onExitLiveLogs,
|
onExitLiveLogs,
|
||||||
|
showRecentlyUsed = true,
|
||||||
}: CustomTimePickerPopoverContentProps): JSX.Element {
|
}: CustomTimePickerPopoverContentProps): JSX.Element {
|
||||||
const { pathname } = useLocation();
|
const { pathname } = useLocation();
|
||||||
|
|
||||||
@@ -224,33 +226,35 @@ function CustomTimePickerPopoverContent({
|
|||||||
<div>{getTimeChips(RelativeDurationSuggestionOptions)}</div>
|
<div>{getTimeChips(RelativeDurationSuggestionOptions)}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="recently-used-container">
|
{showRecentlyUsed && (
|
||||||
<div className="time-heading">RECENTLY USED</div>
|
<div className="recently-used-container">
|
||||||
<div className="recently-used-range">
|
<div className="time-heading">RECENTLY USED</div>
|
||||||
{recentlyUsedTimeRanges.map((range: RecentlyUsedDateTimeRange) => (
|
<div className="recently-used-range">
|
||||||
<div
|
{recentlyUsedTimeRanges.map((range: RecentlyUsedDateTimeRange) => (
|
||||||
className="recently-used-range-item"
|
<div
|
||||||
role="button"
|
className="recently-used-range-item"
|
||||||
tabIndex={0}
|
role="button"
|
||||||
onKeyDown={(e): void => {
|
tabIndex={0}
|
||||||
if (e.key === 'Enter' || e.key === ' ') {
|
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();
|
handleExitLiveLogs();
|
||||||
onCustomDateHandler([dayjs(range.from), dayjs(range.to)]);
|
onCustomDateHandler([dayjs(range.from), dayjs(range.to)]);
|
||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
}
|
}}
|
||||||
}}
|
>
|
||||||
key={range.value}
|
{range.label}
|
||||||
onClick={(): void => {
|
</div>
|
||||||
handleExitLiveLogs();
|
))}
|
||||||
onCustomDateHandler([dayjs(range.from), dayjs(range.to)]);
|
</div>
|
||||||
setIsOpen(false);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{range.label}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
/* eslint-disable sonarjs/no-duplicate-string */
|
/* eslint-disable sonarjs/no-duplicate-string */
|
||||||
import { getYAxisFormattedValue, PrecisionOptionsEnum } from '../yAxisConfig';
|
import { PrecisionOptionsEnum } from '../types';
|
||||||
|
import { getYAxisFormattedValue } from '../yAxisConfig';
|
||||||
|
|
||||||
const testFullPrecisionGetYAxisFormattedValue = (
|
const testFullPrecisionGetYAxisFormattedValue = (
|
||||||
value: string,
|
value: string,
|
||||||
@@ -232,7 +233,7 @@ describe('getYAxisFormattedValue - units (full precision legacy assertions)', ()
|
|||||||
).toBe('1%');
|
).toBe('1%');
|
||||||
expect(
|
expect(
|
||||||
testFullPrecisionGetYAxisFormattedValue('1.00555555559595876', 'percent'),
|
testFullPrecisionGetYAxisFormattedValue('1.00555555559595876', 'percent'),
|
||||||
).toBe('1.005555555595958%');
|
).toBe('1.005555555595959%');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('ratio', () => {
|
test('ratio', () => {
|
||||||
@@ -359,7 +360,7 @@ describe('getYAxisFormattedValue - precision option tests', () => {
|
|||||||
's',
|
's',
|
||||||
PrecisionOptionsEnum.FULL,
|
PrecisionOptionsEnum.FULL,
|
||||||
),
|
),
|
||||||
).toBe('26254299141484417000000 µs');
|
).toBe('26.254299141484417 µs');
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
getYAxisFormattedValue('4353.81', 'ms', PrecisionOptionsEnum.FULL),
|
getYAxisFormattedValue('4353.81', 'ms', PrecisionOptionsEnum.FULL),
|
||||||
|
|||||||
@@ -78,3 +78,18 @@ export interface ITimeRange {
|
|||||||
minTime: number | null;
|
minTime: number | null;
|
||||||
maxTime: number | null;
|
maxTime: number | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const DEFAULT_SIGNIFICANT_DIGITS = 15;
|
||||||
|
|
||||||
|
// max decimals to keep should not exceed 15 decimal places to avoid floating point precision issues
|
||||||
|
export const MAX_DECIMALS = 15;
|
||||||
|
|
||||||
|
export enum PrecisionOptionsEnum {
|
||||||
|
ZERO = 0,
|
||||||
|
ONE = 1,
|
||||||
|
TWO = 2,
|
||||||
|
THREE = 3,
|
||||||
|
FOUR = 4,
|
||||||
|
FULL = 'full',
|
||||||
|
}
|
||||||
|
export type PrecisionOption = 0 | 1 | 2 | 3 | 4 | PrecisionOptionsEnum.FULL;
|
||||||
|
|||||||
@@ -16,8 +16,12 @@ import {
|
|||||||
} from './Plugin/IntersectionCursor';
|
} from './Plugin/IntersectionCursor';
|
||||||
import {
|
import {
|
||||||
CustomChartOptions,
|
CustomChartOptions,
|
||||||
|
DEFAULT_SIGNIFICANT_DIGITS,
|
||||||
GraphOnClickHandler,
|
GraphOnClickHandler,
|
||||||
IAxisTimeConfig,
|
IAxisTimeConfig,
|
||||||
|
MAX_DECIMALS,
|
||||||
|
PrecisionOption,
|
||||||
|
PrecisionOptionsEnum,
|
||||||
StaticLineProps,
|
StaticLineProps,
|
||||||
} from './types';
|
} from './types';
|
||||||
import { getToolTipValue, getYAxisFormattedValue } from './yAxisConfig';
|
import { getToolTipValue, getYAxisFormattedValue } from './yAxisConfig';
|
||||||
@@ -149,6 +153,7 @@ export const getGraphOptions = (
|
|||||||
scales: {
|
scales: {
|
||||||
x: {
|
x: {
|
||||||
stacked: isStacked,
|
stacked: isStacked,
|
||||||
|
offset: false,
|
||||||
grid: {
|
grid: {
|
||||||
display: true,
|
display: true,
|
||||||
color: getGridColor(),
|
color: getGridColor(),
|
||||||
@@ -241,3 +246,68 @@ declare module 'chart.js' {
|
|||||||
custom: TooltipPositionerFunction<ChartType>;
|
custom: TooltipPositionerFunction<ChartType>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Formats a number for display, preserving leading zeros after the decimal point
|
||||||
|
* and showing up to DEFAULT_SIGNIFICANT_DIGITS digits after the first non-zero decimal digit.
|
||||||
|
* It avoids scientific notation and removes unnecessary trailing zeros.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* formatDecimalWithLeadingZeros(1.2345); // "1.2345"
|
||||||
|
* formatDecimalWithLeadingZeros(0.0012345); // "0.0012345"
|
||||||
|
* formatDecimalWithLeadingZeros(5.0); // "5"
|
||||||
|
*
|
||||||
|
* @param value The number to format.
|
||||||
|
* @returns The formatted string.
|
||||||
|
*/
|
||||||
|
export const formatDecimalWithLeadingZeros = (
|
||||||
|
value: number,
|
||||||
|
precision: PrecisionOption,
|
||||||
|
): string => {
|
||||||
|
if (value === 0) {
|
||||||
|
return '0';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use toLocaleString to get a full decimal representation without scientific notation.
|
||||||
|
const numStr = value.toLocaleString('en-US', {
|
||||||
|
useGrouping: false,
|
||||||
|
maximumFractionDigits: 20,
|
||||||
|
});
|
||||||
|
|
||||||
|
const [integerPart, decimalPart = ''] = numStr.split('.');
|
||||||
|
|
||||||
|
// If there's no decimal part, the integer part is the result.
|
||||||
|
if (!decimalPart) {
|
||||||
|
return integerPart;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the index of the first non-zero digit in the decimal part.
|
||||||
|
const firstNonZeroIndex = decimalPart.search(/[^0]/);
|
||||||
|
|
||||||
|
// If the decimal part consists only of zeros, return just the integer part.
|
||||||
|
if (firstNonZeroIndex === -1) {
|
||||||
|
return integerPart;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine the number of decimals to keep: leading zeros + up to N significant digits.
|
||||||
|
const significantDigits =
|
||||||
|
precision === PrecisionOptionsEnum.FULL
|
||||||
|
? DEFAULT_SIGNIFICANT_DIGITS
|
||||||
|
: precision;
|
||||||
|
const decimalsToKeep = firstNonZeroIndex + (significantDigits || 0);
|
||||||
|
|
||||||
|
// max decimals to keep should not exceed 15 decimal places to avoid floating point precision issues
|
||||||
|
const finalDecimalsToKeep = Math.min(decimalsToKeep, MAX_DECIMALS);
|
||||||
|
const trimmedDecimalPart = decimalPart.substring(0, finalDecimalsToKeep);
|
||||||
|
|
||||||
|
// If precision is 0, we drop the decimal part entirely.
|
||||||
|
if (precision === 0) {
|
||||||
|
return integerPart;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove any trailing zeros from the result to keep it clean.
|
||||||
|
const finalDecimalPart = trimmedDecimalPart.replace(/0+$/, '');
|
||||||
|
|
||||||
|
// Return the integer part, or the integer and decimal parts combined.
|
||||||
|
return finalDecimalPart ? `${integerPart}.${finalDecimalPart}` : integerPart;
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,86 +1,17 @@
|
|||||||
/* eslint-disable sonarjs/cognitive-complexity */
|
/* eslint-disable sonarjs/cognitive-complexity */
|
||||||
import { formattedValueToString, getValueFormat } from '@grafana/data';
|
import { formattedValueToString, getValueFormat } from '@grafana/data';
|
||||||
import * as Sentry from '@sentry/react';
|
import * as Sentry from '@sentry/react';
|
||||||
|
import { UniversalYAxisUnit } from 'components/YAxisUnitSelector/types';
|
||||||
|
import { isUniversalUnit } from 'components/YAxisUnitSelector/utils';
|
||||||
import { isNaN } from 'lodash-es';
|
import { isNaN } from 'lodash-es';
|
||||||
|
|
||||||
const DEFAULT_SIGNIFICANT_DIGITS = 15;
|
import { formatUniversalUnit } from '../YAxisUnitSelector/formatter';
|
||||||
// max decimals to keep should not exceed 15 decimal places to avoid floating point precision issues
|
import {
|
||||||
const MAX_DECIMALS = 15;
|
DEFAULT_SIGNIFICANT_DIGITS,
|
||||||
|
PrecisionOption,
|
||||||
export enum PrecisionOptionsEnum {
|
PrecisionOptionsEnum,
|
||||||
ZERO = 0,
|
} from './types';
|
||||||
ONE = 1,
|
import { formatDecimalWithLeadingZeros } from './utils';
|
||||||
TWO = 2,
|
|
||||||
THREE = 3,
|
|
||||||
FOUR = 4,
|
|
||||||
FULL = 'full',
|
|
||||||
}
|
|
||||||
export type PrecisionOption = 0 | 1 | 2 | 3 | 4 | PrecisionOptionsEnum.FULL;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Formats a number for display, preserving leading zeros after the decimal point
|
|
||||||
* and showing up to DEFAULT_SIGNIFICANT_DIGITS digits after the first non-zero decimal digit.
|
|
||||||
* It avoids scientific notation and removes unnecessary trailing zeros.
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* formatDecimalWithLeadingZeros(1.2345); // "1.2345"
|
|
||||||
* formatDecimalWithLeadingZeros(0.0012345); // "0.0012345"
|
|
||||||
* formatDecimalWithLeadingZeros(5.0); // "5"
|
|
||||||
*
|
|
||||||
* @param value The number to format.
|
|
||||||
* @returns The formatted string.
|
|
||||||
*/
|
|
||||||
const formatDecimalWithLeadingZeros = (
|
|
||||||
value: number,
|
|
||||||
precision: PrecisionOption,
|
|
||||||
): string => {
|
|
||||||
if (value === 0) {
|
|
||||||
return '0';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use toLocaleString to get a full decimal representation without scientific notation.
|
|
||||||
const numStr = value.toLocaleString('en-US', {
|
|
||||||
useGrouping: false,
|
|
||||||
maximumFractionDigits: 20,
|
|
||||||
});
|
|
||||||
|
|
||||||
const [integerPart, decimalPart = ''] = numStr.split('.');
|
|
||||||
|
|
||||||
// If there's no decimal part, the integer part is the result.
|
|
||||||
if (!decimalPart) {
|
|
||||||
return integerPart;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find the index of the first non-zero digit in the decimal part.
|
|
||||||
const firstNonZeroIndex = decimalPart.search(/[^0]/);
|
|
||||||
|
|
||||||
// If the decimal part consists only of zeros, return just the integer part.
|
|
||||||
if (firstNonZeroIndex === -1) {
|
|
||||||
return integerPart;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Determine the number of decimals to keep: leading zeros + up to N significant digits.
|
|
||||||
const significantDigits =
|
|
||||||
precision === PrecisionOptionsEnum.FULL
|
|
||||||
? DEFAULT_SIGNIFICANT_DIGITS
|
|
||||||
: precision;
|
|
||||||
const decimalsToKeep = firstNonZeroIndex + (significantDigits || 0);
|
|
||||||
|
|
||||||
// max decimals to keep should not exceed 15 decimal places to avoid floating point precision issues
|
|
||||||
const finalDecimalsToKeep = Math.min(decimalsToKeep, MAX_DECIMALS);
|
|
||||||
const trimmedDecimalPart = decimalPart.substring(0, finalDecimalsToKeep);
|
|
||||||
|
|
||||||
// If precision is 0, we drop the decimal part entirely.
|
|
||||||
if (precision === 0) {
|
|
||||||
return integerPart;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove any trailing zeros from the result to keep it clean.
|
|
||||||
const finalDecimalPart = trimmedDecimalPart.replace(/0+$/, '');
|
|
||||||
|
|
||||||
// Return the integer part, or the integer and decimal parts combined.
|
|
||||||
return finalDecimalPart ? `${integerPart}.${finalDecimalPart}` : integerPart;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Formats a Y-axis value based on a given format string.
|
* Formats a Y-axis value based on a given format string.
|
||||||
@@ -101,19 +32,10 @@ export const getYAxisFormattedValue = (
|
|||||||
if (numValue === Infinity) return '∞';
|
if (numValue === Infinity) return '∞';
|
||||||
if (numValue === -Infinity) return '-∞';
|
if (numValue === -Infinity) return '-∞';
|
||||||
|
|
||||||
const decimalPlaces = value.split('.')[1]?.length || undefined;
|
|
||||||
|
|
||||||
// Use custom formatter for the 'none' format honoring precision
|
|
||||||
if (format === 'none') {
|
|
||||||
return formatDecimalWithLeadingZeros(numValue, precision);
|
|
||||||
}
|
|
||||||
|
|
||||||
// For all other standard formats, delegate to grafana/data's built-in formatter.
|
// For all other standard formats, delegate to grafana/data's built-in formatter.
|
||||||
const computeDecimals = (): number | undefined => {
|
const computeDecimals = (): number | undefined => {
|
||||||
if (precision === PrecisionOptionsEnum.FULL) {
|
if (precision === PrecisionOptionsEnum.FULL) {
|
||||||
return decimalPlaces && decimalPlaces >= DEFAULT_SIGNIFICANT_DIGITS
|
return DEFAULT_SIGNIFICANT_DIGITS;
|
||||||
? decimalPlaces
|
|
||||||
: DEFAULT_SIGNIFICANT_DIGITS;
|
|
||||||
}
|
}
|
||||||
return precision;
|
return precision;
|
||||||
};
|
};
|
||||||
@@ -130,6 +52,22 @@ export const getYAxisFormattedValue = (
|
|||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// Use custom formatter for the 'none' format honoring precision
|
||||||
|
if (format === 'none') {
|
||||||
|
return formatDecimalWithLeadingZeros(numValue, precision);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Separate logic for universal units// Separate logic for universal units
|
||||||
|
if (format && isUniversalUnit(format)) {
|
||||||
|
const decimals = computeDecimals();
|
||||||
|
return formatUniversalUnit(
|
||||||
|
numValue,
|
||||||
|
format as UniversalYAxisUnit,
|
||||||
|
precision,
|
||||||
|
decimals,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const formatter = getValueFormat(format);
|
const formatter = getValueFormat(format);
|
||||||
const formattedValue = formatter(numValue, computeDecimals(), undefined);
|
const formattedValue = formatter(numValue, computeDecimals(), undefined);
|
||||||
if (formattedValue.text && formattedValue.text.includes('.')) {
|
if (formattedValue.text && formattedValue.text.includes('.')) {
|
||||||
@@ -138,6 +76,7 @@ export const getYAxisFormattedValue = (
|
|||||||
precision,
|
precision,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return formattedValueToString(formattedValue);
|
return formattedValueToString(formattedValue);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
Sentry.captureEvent({
|
Sentry.captureEvent({
|
||||||
|
|||||||
@@ -80,12 +80,32 @@ function LogDetailInner({
|
|||||||
return stagedQuery.builder.queryData.find((item) => !item.disabled) || null;
|
return stagedQuery.builder.queryData.find((item) => !item.disabled) || null;
|
||||||
}, [stagedQuery]);
|
}, [stagedQuery]);
|
||||||
|
|
||||||
const { options } = useOptionsMenu({
|
const { options, config } = useOptionsMenu({
|
||||||
storageKey: LOCALSTORAGE.LOGS_LIST_OPTIONS,
|
storageKey: LOCALSTORAGE.LOGS_LIST_OPTIONS,
|
||||||
dataSource: DataSource.LOGS,
|
dataSource: DataSource.LOGS,
|
||||||
aggregateOperator: listQuery?.aggregateOperator || StringOperators.NOOP,
|
aggregateOperator: listQuery?.aggregateOperator || StringOperators.NOOP,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const handleAddColumn = useCallback(
|
||||||
|
(fieldName: string): void => {
|
||||||
|
if (config?.addColumn?.onSelect) {
|
||||||
|
// onSelect from SelectProps has signature (value, option), but handleSelectColumns only needs value
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
config.addColumn.onSelect(fieldName, {} as any);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[config],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleRemoveColumn = useCallback(
|
||||||
|
(fieldName: string): void => {
|
||||||
|
if (config?.addColumn?.onRemove) {
|
||||||
|
config.addColumn.onRemove(fieldName);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[config],
|
||||||
|
);
|
||||||
|
|
||||||
const isDarkMode = useIsDarkMode();
|
const isDarkMode = useIsDarkMode();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const { safeNavigate } = useSafeNavigate();
|
const { safeNavigate } = useSafeNavigate();
|
||||||
@@ -369,6 +389,8 @@ function LogDetailInner({
|
|||||||
isListViewPanel={isListViewPanel}
|
isListViewPanel={isListViewPanel}
|
||||||
selectedOptions={options}
|
selectedOptions={options}
|
||||||
listViewPanelSelectedFields={listViewPanelSelectedFields}
|
listViewPanelSelectedFields={listViewPanelSelectedFields}
|
||||||
|
onAddColumn={handleAddColumn}
|
||||||
|
onRemoveColumn={handleRemoveColumn}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{selectedView === VIEW_TYPES.JSON && <JSONView logData={log} />}
|
{selectedView === VIEW_TYPES.JSON && <JSONView logData={log} />}
|
||||||
|
|||||||
@@ -471,11 +471,13 @@ function LogsFormatOptionsMenu({
|
|||||||
rootClassName="format-options-popover"
|
rootClassName="format-options-popover"
|
||||||
destroyTooltipOnHide
|
destroyTooltipOnHide
|
||||||
>
|
>
|
||||||
<Button
|
<Tooltip title="Options">
|
||||||
className="periscope-btn ghost"
|
<Button
|
||||||
icon={<Sliders size={14} />}
|
className="periscope-btn ghost"
|
||||||
data-testid="periscope-btn-format-options"
|
icon={<Sliders size={14} />}
|
||||||
/>
|
data-testid="periscope-btn-format-options"
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
</Popover>
|
</Popover>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
.overflow-input {
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overflow-input-mirror {
|
||||||
|
position: absolute;
|
||||||
|
visibility: hidden;
|
||||||
|
white-space: pre;
|
||||||
|
pointer-events: none;
|
||||||
|
font: inherit;
|
||||||
|
letter-spacing: inherit;
|
||||||
|
height: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
import { render, screen, userEvent, waitFor, within } from 'tests/test-utils';
|
||||||
|
|
||||||
|
import OverflowInputToolTip from './OverflowInputToolTip';
|
||||||
|
|
||||||
|
const TOOLTIP_INNER_SELECTOR = '.ant-tooltip-inner';
|
||||||
|
// Utility to mock overflow behaviour on inputs / elements.
|
||||||
|
// Stubs HTMLElement.prototype.clientWidth, scrollWidth and offsetWidth used by component.
|
||||||
|
function mockOverflow(clientWidth: number, scrollWidth: number): void {
|
||||||
|
Object.defineProperty(HTMLElement.prototype, 'clientWidth', {
|
||||||
|
configurable: true,
|
||||||
|
value: clientWidth,
|
||||||
|
});
|
||||||
|
Object.defineProperty(HTMLElement.prototype, 'scrollWidth', {
|
||||||
|
configurable: true,
|
||||||
|
value: scrollWidth,
|
||||||
|
});
|
||||||
|
// mirror.offsetWidth is used to compute mirrorWidth = offsetWidth + 24.
|
||||||
|
// Use clientWidth so the mirror measurement aligns with the mocked client width in tests.
|
||||||
|
Object.defineProperty(HTMLElement.prototype, 'offsetWidth', {
|
||||||
|
configurable: true,
|
||||||
|
value: clientWidth,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function queryTooltipInner(): HTMLElement | null {
|
||||||
|
// find element that has role="tooltip" (could be the inner itself)
|
||||||
|
const tooltip = document.querySelector<HTMLElement>('[role="tooltip"]');
|
||||||
|
if (!tooltip) return document.querySelector(TOOLTIP_INNER_SELECTOR);
|
||||||
|
|
||||||
|
// if the role element is already the inner, return it; otherwise return its descendant
|
||||||
|
if (tooltip.classList.contains('ant-tooltip-inner')) return tooltip;
|
||||||
|
return (
|
||||||
|
(tooltip.querySelector(TOOLTIP_INNER_SELECTOR) as HTMLElement) ??
|
||||||
|
document.querySelector(TOOLTIP_INNER_SELECTOR)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('OverflowInputToolTip', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.restoreAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('shows tooltip when content overflows and input is clamped at maxAutoWidth', async () => {
|
||||||
|
mockOverflow(150, 250); // clientWidth >= maxAutoWidth (150), scrollWidth > clientWidth
|
||||||
|
|
||||||
|
render(<OverflowInputToolTip value="Very long overflowing text" />);
|
||||||
|
|
||||||
|
await userEvent.hover(screen.getByRole('textbox'));
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(queryTooltipInner()).not.toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
const tooltipInner = queryTooltipInner();
|
||||||
|
if (!tooltipInner) throw new Error('Tooltip inner not found');
|
||||||
|
expect(
|
||||||
|
within(tooltipInner).getByText('Very long overflowing text'),
|
||||||
|
).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('does NOT show tooltip when content does not overflow', async () => {
|
||||||
|
mockOverflow(150, 100); // content fits (scrollWidth <= clientWidth)
|
||||||
|
|
||||||
|
render(<OverflowInputToolTip value="Short text" />);
|
||||||
|
|
||||||
|
await userEvent.hover(screen.getByRole('textbox'));
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(queryTooltipInner()).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('does NOT show tooltip when content overflows but input is NOT at maxAutoWidth', async () => {
|
||||||
|
mockOverflow(100, 250); // clientWidth < maxAutoWidth (150), scrollWidth > clientWidth
|
||||||
|
|
||||||
|
render(<OverflowInputToolTip value="Long but input not clamped" />);
|
||||||
|
|
||||||
|
await userEvent.hover(screen.getByRole('textbox'));
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(queryTooltipInner()).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('uncontrolled input allows typing', async () => {
|
||||||
|
render(<OverflowInputToolTip defaultValue="Init" />);
|
||||||
|
|
||||||
|
const input = screen.getByRole('textbox') as HTMLInputElement;
|
||||||
|
await userEvent.type(input, 'ABC');
|
||||||
|
|
||||||
|
expect(input).toHaveValue('InitABC');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('disabled input never shows tooltip even if overflowing', async () => {
|
||||||
|
mockOverflow(150, 300);
|
||||||
|
|
||||||
|
render(<OverflowInputToolTip value="Overflowing!" disabled />);
|
||||||
|
|
||||||
|
await userEvent.hover(screen.getByRole('textbox'));
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(queryTooltipInner()).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('renders mirror span and input correctly (structural assertions instead of snapshot)', () => {
|
||||||
|
const { container } = render(<OverflowInputToolTip value="Snapshot" />);
|
||||||
|
const mirror = container.querySelector('.overflow-input-mirror');
|
||||||
|
const input = container.querySelector('input') as HTMLInputElement | null;
|
||||||
|
|
||||||
|
expect(mirror).toBeTruthy();
|
||||||
|
expect(mirror?.textContent).toBe('Snapshot');
|
||||||
|
expect(input).toBeTruthy();
|
||||||
|
expect(input?.value).toBe('Snapshot');
|
||||||
|
|
||||||
|
// width should be set inline (component calculates width on mount)
|
||||||
|
expect(input?.getAttribute('style')).toContain('width:');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
/* eslint-disable react/require-default-props */
|
||||||
|
/* eslint-disable react/jsx-props-no-spreading */
|
||||||
|
|
||||||
|
import './OverflowInputToolTip.scss';
|
||||||
|
|
||||||
|
import { Input, InputProps, InputRef, Tooltip } from 'antd';
|
||||||
|
import cx from 'classnames';
|
||||||
|
import { useEffect, useRef, useState } from 'react';
|
||||||
|
|
||||||
|
export interface OverflowTooltipInputProps extends InputProps {
|
||||||
|
tooltipPlacement?: 'top' | 'bottom' | 'left' | 'right';
|
||||||
|
minAutoWidth?: number;
|
||||||
|
maxAutoWidth?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
function OverflowInputToolTip({
|
||||||
|
value,
|
||||||
|
defaultValue,
|
||||||
|
onChange,
|
||||||
|
disabled = false,
|
||||||
|
tooltipPlacement = 'top',
|
||||||
|
className,
|
||||||
|
minAutoWidth = 70,
|
||||||
|
maxAutoWidth = 150,
|
||||||
|
...rest
|
||||||
|
}: OverflowTooltipInputProps): JSX.Element {
|
||||||
|
const inputRef = useRef<InputRef>(null);
|
||||||
|
const mirrorRef = useRef<HTMLSpanElement | null>(null);
|
||||||
|
const [isOverflowing, setIsOverflowing] = useState<boolean>(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const input = inputRef.current?.input;
|
||||||
|
const mirror = mirrorRef.current;
|
||||||
|
if (!input || !mirror) {
|
||||||
|
setIsOverflowing(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
mirror.textContent = String(value ?? '') || ' ';
|
||||||
|
const mirrorWidth = mirror.offsetWidth + 24;
|
||||||
|
const newWidth = Math.min(maxAutoWidth, Math.max(minAutoWidth, mirrorWidth));
|
||||||
|
input.style.width = `${newWidth}px`;
|
||||||
|
|
||||||
|
// consider clamped when mirrorWidth reaches maxAutoWidth (allow -5px tolerance)
|
||||||
|
const isClamped = mirrorWidth >= maxAutoWidth - 5;
|
||||||
|
const overflow = input.scrollWidth > input.clientWidth && isClamped;
|
||||||
|
|
||||||
|
setIsOverflowing(overflow);
|
||||||
|
}, [value, disabled, minAutoWidth, maxAutoWidth]);
|
||||||
|
|
||||||
|
const tooltipTitle = !disabled && isOverflowing ? String(value ?? '') : '';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<span ref={mirrorRef} aria-hidden className="overflow-input-mirror" />
|
||||||
|
<Tooltip title={tooltipTitle} placement={tooltipPlacement}>
|
||||||
|
<Input
|
||||||
|
{...rest}
|
||||||
|
value={value}
|
||||||
|
defaultValue={defaultValue}
|
||||||
|
onChange={onChange}
|
||||||
|
disabled={disabled}
|
||||||
|
ref={inputRef}
|
||||||
|
className={cx('overflow-input', className)}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
OverflowInputToolTip.displayName = 'OverflowInputToolTip';
|
||||||
|
|
||||||
|
export default OverflowInputToolTip;
|
||||||
3
frontend/src/components/OverflowInputToolTip/index.tsx
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
import OverflowInputToolTip from './OverflowInputToolTip';
|
||||||
|
|
||||||
|
export default OverflowInputToolTip;
|
||||||
@@ -300,6 +300,10 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.qb-trace-operator-button-container {
|
.qb-trace-operator-button-container {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
&-text {
|
&-text {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
@@ -2,8 +2,74 @@ import './QueryFooter.styles.scss';
|
|||||||
|
|
||||||
/* eslint-disable react/require-default-props */
|
/* eslint-disable react/require-default-props */
|
||||||
import { Button, Tooltip, Typography } from 'antd';
|
import { Button, Tooltip, Typography } from 'antd';
|
||||||
|
import WarningPopover from 'components/WarningPopover/WarningPopover';
|
||||||
|
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||||
|
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||||
import { DraftingCompass, Plus, Sigma } from 'lucide-react';
|
import { DraftingCompass, Plus, Sigma } from 'lucide-react';
|
||||||
import BetaTag from 'periscope/components/BetaTag/BetaTag';
|
import BetaTag from 'periscope/components/BetaTag/BetaTag';
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
|
||||||
|
function TraceOperatorSection({
|
||||||
|
addTraceOperator,
|
||||||
|
}: {
|
||||||
|
addTraceOperator?: () => void;
|
||||||
|
}): JSX.Element {
|
||||||
|
const { currentQuery, panelType } = useQueryBuilder();
|
||||||
|
|
||||||
|
const showTraceOperatorWarning = useMemo(() => {
|
||||||
|
const isListViewPanel =
|
||||||
|
panelType === PANEL_TYPES.LIST || panelType === PANEL_TYPES.TRACE;
|
||||||
|
const hasMultipleQueries = currentQuery.builder.queryData.length > 1;
|
||||||
|
const hasTraceOperator =
|
||||||
|
currentQuery.builder.queryTraceOperator &&
|
||||||
|
currentQuery.builder.queryTraceOperator.length > 0;
|
||||||
|
return isListViewPanel && hasMultipleQueries && !hasTraceOperator;
|
||||||
|
}, [
|
||||||
|
currentQuery?.builder?.queryData,
|
||||||
|
currentQuery?.builder?.queryTraceOperator,
|
||||||
|
panelType,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const traceOperatorWarning = useMemo(() => {
|
||||||
|
if (currentQuery.builder.queryData.length === 0) return '';
|
||||||
|
const firstQuery = currentQuery.builder.queryData[0];
|
||||||
|
return `Currently, you are only seeing results from query ${firstQuery.queryName}. Add a trace operator to combine results of multiple queries.`;
|
||||||
|
}, [currentQuery]);
|
||||||
|
return (
|
||||||
|
<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"
|
||||||
|
icon={<DraftingCompass size={16} />}
|
||||||
|
onClick={(): void => addTraceOperator?.()}
|
||||||
|
>
|
||||||
|
<div className="qb-trace-operator-button-container-text">
|
||||||
|
Add Trace Matching
|
||||||
|
<BetaTag />
|
||||||
|
</div>
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
{showTraceOperatorWarning && (
|
||||||
|
<WarningPopover message={traceOperatorWarning} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export default function QueryFooter({
|
export default function QueryFooter({
|
||||||
addNewBuilderQuery,
|
addNewBuilderQuery,
|
||||||
@@ -60,35 +126,7 @@ export default function QueryFooter({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{showAddTraceOperator && (
|
{showAddTraceOperator && (
|
||||||
<div className="qb-trace-operator-button-container">
|
<TraceOperatorSection addTraceOperator={addTraceOperator} />
|
||||||
<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 "
|
|
||||||
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>
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
startCompletion,
|
startCompletion,
|
||||||
} from '@codemirror/autocomplete';
|
} from '@codemirror/autocomplete';
|
||||||
import { javascript } from '@codemirror/lang-javascript';
|
import { javascript } from '@codemirror/lang-javascript';
|
||||||
|
import * as Sentry from '@sentry/react';
|
||||||
import { Color } from '@signozhq/design-tokens';
|
import { Color } from '@signozhq/design-tokens';
|
||||||
import { copilot } from '@uiw/codemirror-theme-copilot';
|
import { copilot } from '@uiw/codemirror-theme-copilot';
|
||||||
import { githubLight } from '@uiw/codemirror-theme-github';
|
import { githubLight } from '@uiw/codemirror-theme-github';
|
||||||
@@ -79,6 +80,16 @@ const stopEventsExtension = EditorView.domEventHandlers({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
interface QuerySearchProps {
|
||||||
|
placeholder?: string;
|
||||||
|
onChange: (value: string) => void;
|
||||||
|
queryData: IBuilderQuery;
|
||||||
|
dataSource: DataSource;
|
||||||
|
signalSource?: string;
|
||||||
|
hardcodedAttributeKeys?: QueryKeyDataSuggestionsProps[];
|
||||||
|
onRun?: (query: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
function QuerySearch({
|
function QuerySearch({
|
||||||
placeholder,
|
placeholder,
|
||||||
onChange,
|
onChange,
|
||||||
@@ -87,17 +98,8 @@ function QuerySearch({
|
|||||||
onRun,
|
onRun,
|
||||||
signalSource,
|
signalSource,
|
||||||
hardcodedAttributeKeys,
|
hardcodedAttributeKeys,
|
||||||
}: {
|
}: QuerySearchProps): JSX.Element {
|
||||||
placeholder?: string;
|
|
||||||
onChange: (value: string) => void;
|
|
||||||
queryData: IBuilderQuery;
|
|
||||||
dataSource: DataSource;
|
|
||||||
signalSource?: string;
|
|
||||||
hardcodedAttributeKeys?: QueryKeyDataSuggestionsProps[];
|
|
||||||
onRun?: (query: string) => void;
|
|
||||||
}): JSX.Element {
|
|
||||||
const isDarkMode = useIsDarkMode();
|
const isDarkMode = useIsDarkMode();
|
||||||
const [query, setQuery] = useState<string>(queryData.filter?.expression || '');
|
|
||||||
const [valueSuggestions, setValueSuggestions] = useState<any[]>([]);
|
const [valueSuggestions, setValueSuggestions] = useState<any[]>([]);
|
||||||
const [activeKey, setActiveKey] = useState<string>('');
|
const [activeKey, setActiveKey] = useState<string>('');
|
||||||
const [isLoadingSuggestions, setIsLoadingSuggestions] = useState(false);
|
const [isLoadingSuggestions, setIsLoadingSuggestions] = useState(false);
|
||||||
@@ -107,8 +109,12 @@ function QuerySearch({
|
|||||||
message: '',
|
message: '',
|
||||||
errors: [],
|
errors: [],
|
||||||
});
|
});
|
||||||
|
const isProgrammaticChangeRef = useRef(false);
|
||||||
|
const [isEditorReady, setIsEditorReady] = useState(false);
|
||||||
|
const [isFocused, setIsFocused] = useState(false);
|
||||||
|
const editorRef = useRef<EditorView | null>(null);
|
||||||
|
|
||||||
const handleQueryValidation = (newQuery: string): void => {
|
const handleQueryValidation = useCallback((newQuery: string): void => {
|
||||||
try {
|
try {
|
||||||
const validationResponse = validateQuery(newQuery);
|
const validationResponse = validateQuery(newQuery);
|
||||||
setValidation(validationResponse);
|
setValidation(validationResponse);
|
||||||
@@ -119,29 +125,67 @@ function QuerySearch({
|
|||||||
errors: [error as IDetailedError],
|
errors: [error as IDetailedError],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
}, []);
|
||||||
|
|
||||||
// Track if the query was changed externally (from queryData) vs internally (user input)
|
const getCurrentQuery = useCallback(
|
||||||
const [isExternalQueryChange, setIsExternalQueryChange] = useState(false);
|
(): string => editorRef.current?.state.doc.toString() || '',
|
||||||
const [lastExternalQuery, setLastExternalQuery] = useState<string>('');
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
const updateEditorValue = useCallback(
|
||||||
const newQuery = queryData.filter?.expression || '';
|
(value: string, options: { skipOnChange?: boolean } = {}): void => {
|
||||||
// Only mark as external change if the query actually changed from external source
|
const view = editorRef.current;
|
||||||
if (newQuery !== lastExternalQuery) {
|
if (!view) return;
|
||||||
setQuery(newQuery);
|
|
||||||
setIsExternalQueryChange(true);
|
|
||||||
setLastExternalQuery(newQuery);
|
|
||||||
}
|
|
||||||
}, [queryData.filter?.expression, lastExternalQuery]);
|
|
||||||
|
|
||||||
// Validate query when it changes externally (from queryData)
|
const currentValue = view.state.doc.toString();
|
||||||
useEffect(() => {
|
if (currentValue === value) return;
|
||||||
if (isExternalQueryChange && query) {
|
|
||||||
handleQueryValidation(query);
|
if (options.skipOnChange) {
|
||||||
setIsExternalQueryChange(false);
|
isProgrammaticChangeRef.current = true;
|
||||||
}
|
}
|
||||||
}, [isExternalQueryChange, query]);
|
|
||||||
|
view.dispatch({
|
||||||
|
changes: {
|
||||||
|
from: 0,
|
||||||
|
to: currentValue.length,
|
||||||
|
insert: value,
|
||||||
|
},
|
||||||
|
selection: {
|
||||||
|
anchor: value.length,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleEditorCreate = useCallback((view: EditorView): void => {
|
||||||
|
editorRef.current = view;
|
||||||
|
setIsEditorReady(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(
|
||||||
|
() => {
|
||||||
|
if (!isEditorReady) return;
|
||||||
|
|
||||||
|
const newQuery = queryData.filter?.expression || '';
|
||||||
|
const currentQuery = getCurrentQuery();
|
||||||
|
|
||||||
|
/* eslint-disable-next-line sonarjs/no-collapsible-if */
|
||||||
|
if (newQuery !== currentQuery && !isFocused) {
|
||||||
|
// Prevent clearing a non-empty editor when queryData becomes empty temporarily
|
||||||
|
// Only update if newQuery has a value, or if both are empty (initial state)
|
||||||
|
if (newQuery || !currentQuery) {
|
||||||
|
updateEditorValue(newQuery, { skipOnChange: true });
|
||||||
|
|
||||||
|
if (newQuery) {
|
||||||
|
handleQueryValidation(newQuery);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
[isEditorReady, queryData.filter?.expression, isFocused],
|
||||||
|
);
|
||||||
|
|
||||||
const [keySuggestions, setKeySuggestions] = useState<
|
const [keySuggestions, setKeySuggestions] = useState<
|
||||||
QueryKeyDataSuggestionsProps[] | null
|
QueryKeyDataSuggestionsProps[] | null
|
||||||
@@ -150,7 +194,6 @@ function QuerySearch({
|
|||||||
const [showExamples] = useState(false);
|
const [showExamples] = useState(false);
|
||||||
|
|
||||||
const [cursorPos, setCursorPos] = useState({ line: 0, ch: 0 });
|
const [cursorPos, setCursorPos] = useState({ line: 0, ch: 0 });
|
||||||
const [isFocused, setIsFocused] = useState(false);
|
|
||||||
|
|
||||||
const [
|
const [
|
||||||
isFetchingCompleteValuesList,
|
isFetchingCompleteValuesList,
|
||||||
@@ -159,8 +202,6 @@ function QuerySearch({
|
|||||||
|
|
||||||
const lastPosRef = useRef<{ line: number; ch: number }>({ line: 0, ch: 0 });
|
const lastPosRef = useRef<{ line: number; ch: number }>({ line: 0, ch: 0 });
|
||||||
|
|
||||||
// Reference to the editor view for programmatic autocompletion
|
|
||||||
const editorRef = useRef<EditorView | null>(null);
|
|
||||||
const lastKeyRef = useRef<string>('');
|
const lastKeyRef = useRef<string>('');
|
||||||
const lastFetchedKeyRef = useRef<string>('');
|
const lastFetchedKeyRef = useRef<string>('');
|
||||||
const lastValueRef = useRef<string>('');
|
const lastValueRef = useRef<string>('');
|
||||||
@@ -506,6 +547,7 @@ function QuerySearch({
|
|||||||
|
|
||||||
if (!editorRef.current) {
|
if (!editorRef.current) {
|
||||||
editorRef.current = viewUpdate.view;
|
editorRef.current = viewUpdate.view;
|
||||||
|
setIsEditorReady(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
const selection = viewUpdate.view.state.selection.main;
|
const selection = viewUpdate.view.state.selection.main;
|
||||||
@@ -521,7 +563,15 @@ function QuerySearch({
|
|||||||
const lastPos = lastPosRef.current;
|
const lastPos = lastPosRef.current;
|
||||||
|
|
||||||
if (newPos.line !== lastPos.line || newPos.ch !== lastPos.ch) {
|
if (newPos.line !== lastPos.line || newPos.ch !== lastPos.ch) {
|
||||||
setCursorPos(newPos);
|
setCursorPos((lastPos) => {
|
||||||
|
if (newPos.ch !== lastPos.ch && newPos.ch === 0) {
|
||||||
|
Sentry.captureEvent({
|
||||||
|
message: `Cursor jumped to start of line from ${lastPos.ch} to ${newPos.ch}`,
|
||||||
|
level: 'warning',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return newPos;
|
||||||
|
});
|
||||||
lastPosRef.current = newPos;
|
lastPosRef.current = newPos;
|
||||||
|
|
||||||
if (doc) {
|
if (doc) {
|
||||||
@@ -554,16 +604,17 @@ function QuerySearch({
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleChange = (value: string): void => {
|
const handleChange = (value: string): void => {
|
||||||
setQuery(value);
|
if (isProgrammaticChangeRef.current) {
|
||||||
|
isProgrammaticChangeRef.current = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
onChange(value);
|
onChange(value);
|
||||||
// Mark as internal change to avoid triggering external validation
|
|
||||||
setIsExternalQueryChange(false);
|
|
||||||
// Update lastExternalQuery to prevent external validation trigger
|
|
||||||
setLastExternalQuery(value);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleBlur = (): void => {
|
const handleBlur = (): void => {
|
||||||
handleQueryValidation(query);
|
const currentQuery = getCurrentQuery();
|
||||||
|
handleQueryValidation(currentQuery);
|
||||||
setIsFocused(false);
|
setIsFocused(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -582,12 +633,11 @@ function QuerySearch({
|
|||||||
|
|
||||||
const handleExampleClick = (exampleQuery: string): void => {
|
const handleExampleClick = (exampleQuery: string): void => {
|
||||||
// If there's an existing query, append the example with AND
|
// If there's an existing query, append the example with AND
|
||||||
const newQuery = query ? `${query} AND ${exampleQuery}` : exampleQuery;
|
const currentQuery = getCurrentQuery();
|
||||||
setQuery(newQuery);
|
const newQuery = currentQuery
|
||||||
// Mark as internal change to avoid triggering external validation
|
? `${currentQuery} AND ${exampleQuery}`
|
||||||
setIsExternalQueryChange(false);
|
: exampleQuery;
|
||||||
// Update lastExternalQuery to prevent external validation trigger
|
updateEditorValue(newQuery);
|
||||||
setLastExternalQuery(newQuery);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Helper function to render a badge for the current context mode
|
// Helper function to render a badge for the current context mode
|
||||||
@@ -622,8 +672,10 @@ function QuerySearch({
|
|||||||
const word = context.matchBefore(/[a-zA-Z0-9_.:/?&=#%\-\[\]]*/);
|
const word = context.matchBefore(/[a-zA-Z0-9_.:/?&=#%\-\[\]]*/);
|
||||||
if (word?.from === word?.to && !context.explicit) return null;
|
if (word?.from === word?.to && !context.explicit) return null;
|
||||||
|
|
||||||
|
// Get current query from editor
|
||||||
|
const currentQuery = editorRef.current?.state.doc.toString() || '';
|
||||||
// Get the query context at the cursor position
|
// Get the query context at the cursor position
|
||||||
const queryContext = getQueryContextAtCursor(query, cursorPos.ch);
|
const queryContext = getQueryContextAtCursor(currentQuery, cursorPos.ch);
|
||||||
|
|
||||||
// Define autocomplete options based on the context
|
// Define autocomplete options based on the context
|
||||||
let options: {
|
let options: {
|
||||||
@@ -1119,7 +1171,8 @@ function QuerySearch({
|
|||||||
|
|
||||||
if (queryContext.isInParenthesis) {
|
if (queryContext.isInParenthesis) {
|
||||||
// Different suggestions based on the context within parenthesis or bracket
|
// Different suggestions based on the context within parenthesis or bracket
|
||||||
const curChar = query.charAt(cursorPos.ch - 1) || '';
|
const currentQuery = editorRef.current?.state.doc.toString() || '';
|
||||||
|
const curChar = currentQuery.charAt(cursorPos.ch - 1) || '';
|
||||||
|
|
||||||
if (curChar === '(' || curChar === '[') {
|
if (curChar === '(' || curChar === '[') {
|
||||||
// Right after opening parenthesis/bracket
|
// Right after opening parenthesis/bracket
|
||||||
@@ -1268,7 +1321,7 @@ function QuerySearch({
|
|||||||
style={{
|
style={{
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
top: 8,
|
top: 8,
|
||||||
right: validation.isValid === false && query ? 40 : 8, // Move left when error shown
|
right: validation.isValid === false && getCurrentQuery() ? 40 : 8, // Move left when error shown
|
||||||
cursor: 'help',
|
cursor: 'help',
|
||||||
zIndex: 10,
|
zIndex: 10,
|
||||||
transition: 'right 0.2s ease',
|
transition: 'right 0.2s ease',
|
||||||
@@ -1289,10 +1342,10 @@ function QuerySearch({
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
<CodeMirror
|
<CodeMirror
|
||||||
value={query}
|
|
||||||
theme={isDarkMode ? copilot : githubLight}
|
theme={isDarkMode ? copilot : githubLight}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
onUpdate={handleUpdate}
|
onUpdate={handleUpdate}
|
||||||
|
onCreateEditor={handleEditorCreate}
|
||||||
className={cx('query-where-clause-editor', {
|
className={cx('query-where-clause-editor', {
|
||||||
isValid: validation.isValid === true,
|
isValid: validation.isValid === true,
|
||||||
hasErrors: validation.errors.length > 0,
|
hasErrors: validation.errors.length > 0,
|
||||||
@@ -1330,7 +1383,7 @@ function QuerySearch({
|
|||||||
// Mod-Enter is usually Ctrl-Enter or Cmd-Enter based on OS
|
// Mod-Enter is usually Ctrl-Enter or Cmd-Enter based on OS
|
||||||
run: (): boolean => {
|
run: (): boolean => {
|
||||||
if (onRun && typeof onRun === 'function') {
|
if (onRun && typeof onRun === 'function') {
|
||||||
onRun(query);
|
onRun(getCurrentQuery());
|
||||||
} else {
|
} else {
|
||||||
handleRunQuery();
|
handleRunQuery();
|
||||||
}
|
}
|
||||||
@@ -1356,7 +1409,7 @@ function QuerySearch({
|
|||||||
onBlur={handleBlur}
|
onBlur={handleBlur}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{query && validation.isValid === false && !isFocused && (
|
{getCurrentQuery() && validation.isValid === false && !isFocused && (
|
||||||
<div
|
<div
|
||||||
className={cx('query-status-container', {
|
className={cx('query-status-container', {
|
||||||
hasErrors: validation.errors.length > 0,
|
hasErrors: validation.errors.length > 0,
|
||||||
|
|||||||
@@ -9,7 +9,13 @@ import SpanScopeSelector from 'container/QueryBuilder/filters/QueryBuilderSearch
|
|||||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||||
import { useQueryOperations } from 'hooks/queryBuilder/useQueryBuilderOperations';
|
import { useQueryOperations } from 'hooks/queryBuilder/useQueryBuilderOperations';
|
||||||
import { Copy, Ellipsis, Trash } from 'lucide-react';
|
import { Copy, Ellipsis, Trash } from 'lucide-react';
|
||||||
import { memo, useCallback, useMemo, useState } from 'react';
|
import {
|
||||||
|
ForwardedRef,
|
||||||
|
forwardRef,
|
||||||
|
useCallback,
|
||||||
|
useMemo,
|
||||||
|
useState,
|
||||||
|
} from 'react';
|
||||||
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
||||||
import { HandleChangeQueryDataV5 } from 'types/common/operations.types';
|
import { HandleChangeQueryDataV5 } from 'types/common/operations.types';
|
||||||
import { DataSource } from 'types/common/queryBuilder';
|
import { DataSource } from 'types/common/queryBuilder';
|
||||||
@@ -20,28 +26,29 @@ import QueryAddOns from './QueryAddOns/QueryAddOns';
|
|||||||
import QueryAggregation from './QueryAggregation/QueryAggregation';
|
import QueryAggregation from './QueryAggregation/QueryAggregation';
|
||||||
import QuerySearch from './QuerySearch/QuerySearch';
|
import QuerySearch from './QuerySearch/QuerySearch';
|
||||||
|
|
||||||
export const QueryV2 = memo(function QueryV2({
|
export const QueryV2 = forwardRef(function QueryV2(
|
||||||
ref,
|
{
|
||||||
index,
|
index,
|
||||||
queryVariant,
|
queryVariant,
|
||||||
query,
|
query,
|
||||||
filterConfigs,
|
filterConfigs,
|
||||||
isListViewPanel = false,
|
isListViewPanel = false,
|
||||||
showTraceOperator = false,
|
showTraceOperator = false,
|
||||||
hasTraceOperator = false,
|
hasTraceOperator = false,
|
||||||
version,
|
version,
|
||||||
showOnlyWhereClause = false,
|
showOnlyWhereClause = false,
|
||||||
signalSource = '',
|
signalSource = '',
|
||||||
isMultiQueryAllowed = false,
|
isMultiQueryAllowed = false,
|
||||||
onSignalSourceChange,
|
onSignalSourceChange,
|
||||||
signalSourceChangeEnabled = false,
|
signalSourceChangeEnabled = false,
|
||||||
queriesCount = 1,
|
queriesCount = 1,
|
||||||
}: QueryProps & {
|
}: QueryProps & {
|
||||||
ref: React.RefObject<HTMLDivElement>;
|
onSignalSourceChange: (value: string) => void;
|
||||||
onSignalSourceChange: (value: string) => void;
|
signalSourceChangeEnabled: boolean;
|
||||||
signalSourceChangeEnabled: boolean;
|
queriesCount: number;
|
||||||
queriesCount: number;
|
},
|
||||||
}): JSX.Element {
|
ref: ForwardedRef<HTMLDivElement>,
|
||||||
|
): JSX.Element {
|
||||||
const { cloneQuery, panelType } = useQueryBuilder();
|
const { cloneQuery, panelType } = useQueryBuilder();
|
||||||
|
|
||||||
const showFunctions = query?.functions?.length > 0;
|
const showFunctions = query?.functions?.length > 0;
|
||||||
@@ -295,3 +302,5 @@ export const QueryV2 = memo(function QueryV2({
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
QueryV2.displayName = 'QueryV2';
|
||||||
|
|||||||
@@ -5,13 +5,85 @@ import { getKeySuggestions } from 'api/querySuggestions/getKeySuggestions';
|
|||||||
import { getValueSuggestions } from 'api/querySuggestions/getValueSuggestion';
|
import { getValueSuggestions } from 'api/querySuggestions/getValueSuggestion';
|
||||||
import { initialQueriesMap } from 'constants/queryBuilder';
|
import { initialQueriesMap } from 'constants/queryBuilder';
|
||||||
import * as UseQBModule from 'hooks/queryBuilder/useQueryBuilder';
|
import * as UseQBModule from 'hooks/queryBuilder/useQueryBuilder';
|
||||||
import React from 'react';
|
import { fireEvent, render, userEvent, waitFor } from 'tests/test-utils';
|
||||||
import { render, screen, userEvent, waitFor } from 'tests/test-utils';
|
|
||||||
import type { QueryKeyDataSuggestionsProps } from 'types/api/querySuggestions/types';
|
import type { QueryKeyDataSuggestionsProps } from 'types/api/querySuggestions/types';
|
||||||
import { DataSource } from 'types/common/queryBuilder';
|
import { DataSource } from 'types/common/queryBuilder';
|
||||||
|
|
||||||
import QuerySearch from '../QuerySearch/QuerySearch';
|
import QuerySearch from '../QuerySearch/QuerySearch';
|
||||||
|
|
||||||
|
const CM_EDITOR_SELECTOR = '.cm-editor .cm-content';
|
||||||
|
|
||||||
|
// Mock DOM APIs that CodeMirror needs
|
||||||
|
beforeAll(() => {
|
||||||
|
// Mock getClientRects and getBoundingClientRect for Range objects
|
||||||
|
const mockRect: DOMRect = {
|
||||||
|
width: 100,
|
||||||
|
height: 20,
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
right: 100,
|
||||||
|
bottom: 20,
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
toJSON: (): DOMRect => mockRect,
|
||||||
|
} as DOMRect;
|
||||||
|
|
||||||
|
// Create a minimal Range mock with only what CodeMirror actually uses
|
||||||
|
const createMockRange = (): Range => {
|
||||||
|
let startContainer: Node = document.createTextNode('');
|
||||||
|
let endContainer: Node = document.createTextNode('');
|
||||||
|
let startOffset = 0;
|
||||||
|
let endOffset = 0;
|
||||||
|
|
||||||
|
const mockRange = {
|
||||||
|
// CodeMirror uses these for text measurement
|
||||||
|
getClientRects: (): DOMRectList =>
|
||||||
|
(({
|
||||||
|
length: 1,
|
||||||
|
item: (index: number): DOMRect | null => (index === 0 ? mockRect : null),
|
||||||
|
0: mockRect,
|
||||||
|
*[Symbol.iterator](): Generator<DOMRect> {
|
||||||
|
yield mockRect;
|
||||||
|
},
|
||||||
|
} as unknown) as DOMRectList),
|
||||||
|
getBoundingClientRect: (): DOMRect => mockRect,
|
||||||
|
// CodeMirror calls these to set up text ranges
|
||||||
|
setStart: (node: Node, offset: number): void => {
|
||||||
|
startContainer = node;
|
||||||
|
startOffset = offset;
|
||||||
|
},
|
||||||
|
setEnd: (node: Node, offset: number): void => {
|
||||||
|
endContainer = node;
|
||||||
|
endOffset = offset;
|
||||||
|
},
|
||||||
|
// Minimal Range properties (TypeScript requires these)
|
||||||
|
get startContainer(): Node {
|
||||||
|
return startContainer;
|
||||||
|
},
|
||||||
|
get endContainer(): Node {
|
||||||
|
return endContainer;
|
||||||
|
},
|
||||||
|
get startOffset(): number {
|
||||||
|
return startOffset;
|
||||||
|
},
|
||||||
|
get endOffset(): number {
|
||||||
|
return endOffset;
|
||||||
|
},
|
||||||
|
get collapsed(): boolean {
|
||||||
|
return startContainer === endContainer && startOffset === endOffset;
|
||||||
|
},
|
||||||
|
commonAncestorContainer: document.body,
|
||||||
|
};
|
||||||
|
return (mockRange as unknown) as Range;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Mock document.createRange to return a new Range instance each time
|
||||||
|
document.createRange = (): Range => createMockRange();
|
||||||
|
|
||||||
|
// Mock getBoundingClientRect for elements
|
||||||
|
Element.prototype.getBoundingClientRect = (): DOMRect => mockRect;
|
||||||
|
});
|
||||||
|
|
||||||
jest.mock('hooks/useDarkMode', () => ({
|
jest.mock('hooks/useDarkMode', () => ({
|
||||||
useIsDarkMode: (): boolean => false,
|
useIsDarkMode: (): boolean => false,
|
||||||
}));
|
}));
|
||||||
@@ -31,24 +103,6 @@ jest.mock('hooks/queryBuilder/useQueryBuilder', () => {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
jest.mock('@codemirror/autocomplete', () => ({
|
|
||||||
autocompletion: (): Record<string, unknown> => ({}),
|
|
||||||
closeCompletion: (): boolean => true,
|
|
||||||
completionKeymap: [] as unknown[],
|
|
||||||
startCompletion: (): boolean => true,
|
|
||||||
}));
|
|
||||||
|
|
||||||
jest.mock('@codemirror/lang-javascript', () => ({
|
|
||||||
javascript: (): Record<string, unknown> => ({}),
|
|
||||||
}));
|
|
||||||
|
|
||||||
jest.mock('@uiw/codemirror-theme-copilot', () => ({
|
|
||||||
copilot: {},
|
|
||||||
}));
|
|
||||||
|
|
||||||
jest.mock('@uiw/codemirror-theme-github', () => ({
|
|
||||||
githubLight: {},
|
|
||||||
}));
|
|
||||||
jest.mock('api/querySuggestions/getKeySuggestions', () => ({
|
jest.mock('api/querySuggestions/getKeySuggestions', () => ({
|
||||||
getKeySuggestions: jest.fn().mockResolvedValue({
|
getKeySuggestions: jest.fn().mockResolvedValue({
|
||||||
data: {
|
data: {
|
||||||
@@ -63,153 +117,19 @@ jest.mock('api/querySuggestions/getValueSuggestion', () => ({
|
|||||||
}),
|
}),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Mock CodeMirror to a simple textarea to make it testable and call onUpdate
|
// Note: We're NOT mocking CodeMirror here - using the real component
|
||||||
jest.mock(
|
// This provides integration testing with the actual CodeMirror editor
|
||||||
'@uiw/react-codemirror',
|
|
||||||
(): Record<string, unknown> => {
|
|
||||||
// Minimal EditorView shape used by the component
|
|
||||||
class EditorViewMock {}
|
|
||||||
(EditorViewMock as any).domEventHandlers = (): unknown => ({} as unknown);
|
|
||||||
(EditorViewMock as any).lineWrapping = {} as unknown;
|
|
||||||
(EditorViewMock as any).editable = { of: () => ({}) } as unknown;
|
|
||||||
|
|
||||||
const keymap = { of: (arr: unknown) => arr } as unknown;
|
|
||||||
const Prec = { highest: (ext: unknown) => ext } as unknown;
|
|
||||||
|
|
||||||
type CodeMirrorProps = {
|
|
||||||
value?: string;
|
|
||||||
onChange?: (v: string) => void;
|
|
||||||
onFocus?: () => void;
|
|
||||||
onBlur?: () => void;
|
|
||||||
placeholder?: string;
|
|
||||||
onCreateEditor?: (view: unknown) => unknown;
|
|
||||||
onUpdate?: (arg: {
|
|
||||||
view: {
|
|
||||||
state: {
|
|
||||||
selection: { main: { head: number } };
|
|
||||||
doc: {
|
|
||||||
toString: () => string;
|
|
||||||
lineAt: (
|
|
||||||
_pos: number,
|
|
||||||
) => { number: number; from: number; to: number; text: string };
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}) => void;
|
|
||||||
'data-testid'?: string;
|
|
||||||
extensions?: unknown[];
|
|
||||||
};
|
|
||||||
|
|
||||||
function CodeMirrorMock({
|
|
||||||
value,
|
|
||||||
onChange,
|
|
||||||
onFocus,
|
|
||||||
onBlur,
|
|
||||||
placeholder,
|
|
||||||
onCreateEditor,
|
|
||||||
onUpdate,
|
|
||||||
'data-testid': dataTestId,
|
|
||||||
extensions,
|
|
||||||
}: CodeMirrorProps): JSX.Element {
|
|
||||||
const [localValue, setLocalValue] = React.useState<string>(value ?? '');
|
|
||||||
|
|
||||||
// Provide a fake editor instance
|
|
||||||
React.useEffect(() => {
|
|
||||||
if (onCreateEditor) {
|
|
||||||
onCreateEditor(new EditorViewMock() as any);
|
|
||||||
}
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// Call onUpdate whenever localValue changes to simulate cursor and doc
|
|
||||||
React.useEffect(() => {
|
|
||||||
if (onUpdate) {
|
|
||||||
const text = String(localValue ?? '');
|
|
||||||
const head = text.length;
|
|
||||||
onUpdate({
|
|
||||||
view: {
|
|
||||||
state: {
|
|
||||||
selection: { main: { head } },
|
|
||||||
doc: {
|
|
||||||
toString: (): string => text,
|
|
||||||
lineAt: () => ({
|
|
||||||
number: 1,
|
|
||||||
from: 0,
|
|
||||||
to: text.length,
|
|
||||||
text,
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [localValue]);
|
|
||||||
|
|
||||||
const handleKeyDown = (
|
|
||||||
e: React.KeyboardEvent<HTMLTextAreaElement>,
|
|
||||||
): void => {
|
|
||||||
const isModEnter = e.key === 'Enter' && (e.metaKey || e.ctrlKey);
|
|
||||||
if (!isModEnter) return;
|
|
||||||
const exts: unknown[] = Array.isArray(extensions) ? extensions : [];
|
|
||||||
const flat: unknown[] = exts.flatMap((x: unknown) =>
|
|
||||||
Array.isArray(x) ? x : [x],
|
|
||||||
);
|
|
||||||
const keyBindings = flat.filter(
|
|
||||||
(x) =>
|
|
||||||
Boolean(x) &&
|
|
||||||
typeof x === 'object' &&
|
|
||||||
'key' in (x as Record<string, unknown>),
|
|
||||||
) as Array<{ key?: string; run?: () => boolean | void }>;
|
|
||||||
keyBindings
|
|
||||||
.filter((b) => b.key === 'Mod-Enter' && typeof b.run === 'function')
|
|
||||||
.forEach((b) => {
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
||||||
b.run!();
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<textarea
|
|
||||||
data-testid={dataTestId || 'query-where-clause-editor'}
|
|
||||||
placeholder={placeholder}
|
|
||||||
value={localValue}
|
|
||||||
onChange={(e): void => {
|
|
||||||
setLocalValue(e.target.value);
|
|
||||||
if (onChange) {
|
|
||||||
onChange(e.target.value);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
onFocus={onFocus}
|
|
||||||
onBlur={onBlur}
|
|
||||||
onKeyDown={handleKeyDown}
|
|
||||||
style={{ width: '100%', minHeight: 80 }}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
__esModule: true,
|
|
||||||
default: CodeMirrorMock,
|
|
||||||
EditorView: EditorViewMock,
|
|
||||||
keymap,
|
|
||||||
Prec,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
);
|
|
||||||
const handleRunQueryMock = ((UseQBModule as unknown) as {
|
const handleRunQueryMock = ((UseQBModule as unknown) as {
|
||||||
handleRunQuery: jest.MockedFunction<() => void>;
|
handleRunQuery: jest.MockedFunction<() => void>;
|
||||||
}).handleRunQuery;
|
}).handleRunQuery;
|
||||||
|
|
||||||
const PLACEHOLDER_TEXT =
|
|
||||||
"Enter your filter query (e.g., http.status_code >= 500 AND service.name = 'frontend')";
|
|
||||||
const TESTID_EDITOR = 'query-where-clause-editor';
|
|
||||||
const SAMPLE_KEY_TYPING = 'http.';
|
const SAMPLE_KEY_TYPING = 'http.';
|
||||||
const SAMPLE_VALUE_TYPING_INCOMPLETE = " service.name = '";
|
const SAMPLE_VALUE_TYPING_INCOMPLETE = "service.name = '";
|
||||||
const SAMPLE_VALUE_TYPING_COMPLETE = " service.name = 'frontend'";
|
const SAMPLE_VALUE_TYPING_COMPLETE = "service.name = 'frontend'";
|
||||||
const SAMPLE_STATUS_QUERY = " status_code = '200'";
|
const SAMPLE_STATUS_QUERY = "http.status_code = '200'";
|
||||||
|
|
||||||
describe('QuerySearch', () => {
|
describe('QuerySearch (Integration with Real CodeMirror)', () => {
|
||||||
it('renders with placeholder', () => {
|
it('renders with placeholder', () => {
|
||||||
render(
|
render(
|
||||||
<QuerySearch
|
<QuerySearch
|
||||||
@@ -219,21 +139,19 @@ describe('QuerySearch', () => {
|
|||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(screen.getByPlaceholderText(PLACEHOLDER_TEXT)).toBeInTheDocument();
|
// CodeMirror renders a contenteditable div, so we check for the container
|
||||||
|
const editorContainer = document.querySelector('.query-where-clause-editor');
|
||||||
|
expect(editorContainer).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('fetches key suggestions when typing a key (debounced)', async () => {
|
it('fetches key suggestions when typing a key (debounced)', async () => {
|
||||||
jest.useFakeTimers();
|
// Use real timers for CodeMirror integration tests
|
||||||
const advance = (ms: number): void => {
|
|
||||||
jest.advanceTimersByTime(ms);
|
|
||||||
};
|
|
||||||
const user = userEvent.setup({
|
|
||||||
advanceTimers: advance,
|
|
||||||
pointerEventsCheck: 0,
|
|
||||||
});
|
|
||||||
const mockedGetKeys = getKeySuggestions as jest.MockedFunction<
|
const mockedGetKeys = getKeySuggestions as jest.MockedFunction<
|
||||||
typeof getKeySuggestions
|
typeof getKeySuggestions
|
||||||
>;
|
>;
|
||||||
|
mockedGetKeys.mockClear();
|
||||||
|
|
||||||
|
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||||
|
|
||||||
render(
|
render(
|
||||||
<QuerySearch
|
<QuerySearch
|
||||||
@@ -243,28 +161,33 @@ describe('QuerySearch', () => {
|
|||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
|
|
||||||
const editor = screen.getByTestId(TESTID_EDITOR);
|
// Wait for CodeMirror to initialize
|
||||||
await user.type(editor, SAMPLE_KEY_TYPING);
|
await waitFor(() => {
|
||||||
advance(1000);
|
const editor = document.querySelector(CM_EDITOR_SELECTOR);
|
||||||
|
expect(editor).toBeInTheDocument();
|
||||||
await waitFor(() => expect(mockedGetKeys).toHaveBeenCalled(), {
|
});
|
||||||
timeout: 3000,
|
|
||||||
|
// Find the CodeMirror editor contenteditable element
|
||||||
|
const editor = document.querySelector(CM_EDITOR_SELECTOR) as HTMLElement;
|
||||||
|
|
||||||
|
// Focus and type into the editor
|
||||||
|
await user.click(editor);
|
||||||
|
await user.type(editor, SAMPLE_KEY_TYPING);
|
||||||
|
|
||||||
|
// Wait for debounced API call (300ms debounce + some buffer)
|
||||||
|
await waitFor(() => expect(mockedGetKeys).toHaveBeenCalled(), {
|
||||||
|
timeout: 2000,
|
||||||
});
|
});
|
||||||
jest.useRealTimers();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('fetches value suggestions when editing value context', async () => {
|
it('fetches value suggestions when editing value context', async () => {
|
||||||
jest.useFakeTimers();
|
// Use real timers for CodeMirror integration tests
|
||||||
const advance = (ms: number): void => {
|
|
||||||
jest.advanceTimersByTime(ms);
|
|
||||||
};
|
|
||||||
const user = userEvent.setup({
|
|
||||||
advanceTimers: advance,
|
|
||||||
pointerEventsCheck: 0,
|
|
||||||
});
|
|
||||||
const mockedGetValues = getValueSuggestions as jest.MockedFunction<
|
const mockedGetValues = getValueSuggestions as jest.MockedFunction<
|
||||||
typeof getValueSuggestions
|
typeof getValueSuggestions
|
||||||
>;
|
>;
|
||||||
|
mockedGetValues.mockClear();
|
||||||
|
|
||||||
|
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||||
|
|
||||||
render(
|
render(
|
||||||
<QuerySearch
|
<QuerySearch
|
||||||
@@ -274,21 +197,28 @@ describe('QuerySearch', () => {
|
|||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
|
|
||||||
const editor = screen.getByTestId(TESTID_EDITOR);
|
// Wait for CodeMirror to initialize
|
||||||
await user.type(editor, SAMPLE_VALUE_TYPING_INCOMPLETE);
|
await waitFor(() => {
|
||||||
advance(1000);
|
const editor = document.querySelector(CM_EDITOR_SELECTOR);
|
||||||
|
expect(editor).toBeInTheDocument();
|
||||||
await waitFor(() => expect(mockedGetValues).toHaveBeenCalled(), {
|
});
|
||||||
timeout: 3000,
|
|
||||||
|
const editor = document.querySelector(CM_EDITOR_SELECTOR) as HTMLElement;
|
||||||
|
await user.click(editor);
|
||||||
|
await user.type(editor, SAMPLE_VALUE_TYPING_INCOMPLETE);
|
||||||
|
|
||||||
|
// Wait for debounced API call (300ms debounce + some buffer)
|
||||||
|
await waitFor(() => expect(mockedGetValues).toHaveBeenCalled(), {
|
||||||
|
timeout: 2000,
|
||||||
});
|
});
|
||||||
jest.useRealTimers();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('fetches key suggestions on mount for LOGS', async () => {
|
it('fetches key suggestions on mount for LOGS', async () => {
|
||||||
jest.useFakeTimers();
|
// Use real timers for CodeMirror integration tests
|
||||||
const mockedGetKeysOnMount = getKeySuggestions as jest.MockedFunction<
|
const mockedGetKeysOnMount = getKeySuggestions as jest.MockedFunction<
|
||||||
typeof getKeySuggestions
|
typeof getKeySuggestions
|
||||||
>;
|
>;
|
||||||
|
mockedGetKeysOnMount.mockClear();
|
||||||
|
|
||||||
render(
|
render(
|
||||||
<QuerySearch
|
<QuerySearch
|
||||||
@@ -298,17 +228,15 @@ describe('QuerySearch', () => {
|
|||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
|
|
||||||
jest.advanceTimersByTime(1000);
|
// Wait for debounced API call (300ms debounce + some buffer)
|
||||||
|
|
||||||
await waitFor(() => expect(mockedGetKeysOnMount).toHaveBeenCalled(), {
|
await waitFor(() => expect(mockedGetKeysOnMount).toHaveBeenCalled(), {
|
||||||
timeout: 3000,
|
timeout: 2000,
|
||||||
});
|
});
|
||||||
|
|
||||||
const lastArgs = mockedGetKeysOnMount.mock.calls[
|
const lastArgs = mockedGetKeysOnMount.mock.calls[
|
||||||
mockedGetKeysOnMount.mock.calls.length - 1
|
mockedGetKeysOnMount.mock.calls.length - 1
|
||||||
]?.[0] as { signal: unknown; searchText: string };
|
]?.[0] as { signal: unknown; searchText: string };
|
||||||
expect(lastArgs).toMatchObject({ signal: DataSource.LOGS, searchText: '' });
|
expect(lastArgs).toMatchObject({ signal: DataSource.LOGS, searchText: '' });
|
||||||
jest.useRealTimers();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('calls provided onRun on Mod-Enter', async () => {
|
it('calls provided onRun on Mod-Enter', async () => {
|
||||||
@@ -324,12 +252,26 @@ describe('QuerySearch', () => {
|
|||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
|
|
||||||
const editor = screen.getByTestId(TESTID_EDITOR);
|
// Wait for CodeMirror to initialize
|
||||||
|
await waitFor(() => {
|
||||||
|
const editor = document.querySelector(CM_EDITOR_SELECTOR);
|
||||||
|
expect(editor).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
const editor = document.querySelector(CM_EDITOR_SELECTOR) as HTMLElement;
|
||||||
await user.click(editor);
|
await user.click(editor);
|
||||||
await user.type(editor, SAMPLE_STATUS_QUERY);
|
await user.type(editor, SAMPLE_STATUS_QUERY);
|
||||||
await user.keyboard('{Meta>}{Enter}{/Meta}');
|
|
||||||
|
|
||||||
await waitFor(() => expect(onRun).toHaveBeenCalled());
|
// Use fireEvent for keyboard shortcuts as userEvent might not work well with CodeMirror
|
||||||
|
const modKey = navigator.platform.includes('Mac') ? 'metaKey' : 'ctrlKey';
|
||||||
|
fireEvent.keyDown(editor, {
|
||||||
|
key: 'Enter',
|
||||||
|
code: 'Enter',
|
||||||
|
[modKey]: true,
|
||||||
|
keyCode: 13,
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(() => expect(onRun).toHaveBeenCalled(), { timeout: 2000 });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('calls handleRunQuery when Mod-Enter without onRun', async () => {
|
it('calls handleRunQuery when Mod-Enter without onRun', async () => {
|
||||||
@@ -348,11 +290,62 @@ describe('QuerySearch', () => {
|
|||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
|
|
||||||
const editor = screen.getByTestId(TESTID_EDITOR);
|
// Wait for CodeMirror to initialize
|
||||||
|
await waitFor(() => {
|
||||||
|
const editor = document.querySelector(CM_EDITOR_SELECTOR);
|
||||||
|
expect(editor).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
const editor = document.querySelector(CM_EDITOR_SELECTOR) as HTMLElement;
|
||||||
await user.click(editor);
|
await user.click(editor);
|
||||||
await user.type(editor, SAMPLE_VALUE_TYPING_COMPLETE);
|
await user.type(editor, SAMPLE_VALUE_TYPING_COMPLETE);
|
||||||
await user.keyboard('{Meta>}{Enter}{/Meta}');
|
|
||||||
|
|
||||||
await waitFor(() => expect(mockedHandleRunQuery).toHaveBeenCalled());
|
// Use fireEvent for keyboard shortcuts as userEvent might not work well with CodeMirror
|
||||||
|
const modKey = navigator.platform.includes('Mac') ? 'metaKey' : 'ctrlKey';
|
||||||
|
fireEvent.keyDown(editor, {
|
||||||
|
key: 'Enter',
|
||||||
|
code: 'Enter',
|
||||||
|
[modKey]: true,
|
||||||
|
keyCode: 13,
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(() => expect(mockedHandleRunQuery).toHaveBeenCalled(), {
|
||||||
|
timeout: 2000,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('initializes CodeMirror with expression from queryData.filter.expression on mount', async () => {
|
||||||
|
const testExpression =
|
||||||
|
"http.status_code >= 500 AND service.name = 'frontend'";
|
||||||
|
const queryDataWithExpression = {
|
||||||
|
...initialQueriesMap.logs.builder.queryData[0],
|
||||||
|
filter: {
|
||||||
|
expression: testExpression,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
render(
|
||||||
|
<QuerySearch
|
||||||
|
onChange={jest.fn() as jest.MockedFunction<(v: string) => void>}
|
||||||
|
queryData={queryDataWithExpression}
|
||||||
|
dataSource={DataSource.LOGS}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Wait for CodeMirror to initialize and the expression to be set
|
||||||
|
await waitFor(
|
||||||
|
() => {
|
||||||
|
// CodeMirror stores content in .cm-content, check the text content
|
||||||
|
const editorContent = document.querySelector(
|
||||||
|
CM_EDITOR_SELECTOR,
|
||||||
|
) as HTMLElement;
|
||||||
|
expect(editorContent).toBeInTheDocument();
|
||||||
|
// CodeMirror may render the text in multiple ways, check if it contains our expression
|
||||||
|
const textContent = editorContent.textContent || '';
|
||||||
|
expect(textContent).toContain('http.status_code');
|
||||||
|
expect(textContent).toContain('service.name');
|
||||||
|
},
|
||||||
|
{ timeout: 3000 },
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
convertAggregationToExpression,
|
convertAggregationToExpression,
|
||||||
convertFiltersToExpression,
|
convertFiltersToExpression,
|
||||||
convertFiltersToExpressionWithExistingQuery,
|
convertFiltersToExpressionWithExistingQuery,
|
||||||
|
formatValueForExpression,
|
||||||
removeKeysFromExpression,
|
removeKeysFromExpression,
|
||||||
} from '../utils';
|
} from '../utils';
|
||||||
|
|
||||||
@@ -1193,3 +1194,220 @@ describe('removeKeysFromExpression', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('formatValueForExpression', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Variable values', () => {
|
||||||
|
it('should return variable values as-is', () => {
|
||||||
|
expect(formatValueForExpression('$variable')).toBe('$variable');
|
||||||
|
expect(formatValueForExpression('$env')).toBe('$env');
|
||||||
|
expect(formatValueForExpression(' $variable ')).toBe(' $variable ');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return variable arrays as-is', () => {
|
||||||
|
expect(formatValueForExpression(['$var1', '$var2'])).toBe('$var1,$var2');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Numeric string values', () => {
|
||||||
|
it('should return numeric strings with quotes', () => {
|
||||||
|
expect(formatValueForExpression('123')).toBe("'123'");
|
||||||
|
expect(formatValueForExpression('0')).toBe("'0'");
|
||||||
|
expect(formatValueForExpression('100000')).toBe("'100000'");
|
||||||
|
expect(formatValueForExpression('-42')).toBe("'-42'");
|
||||||
|
expect(formatValueForExpression('3.14')).toBe("'3.14'");
|
||||||
|
expect(formatValueForExpression(' 456 ')).toBe("' 456 '");
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle numeric strings with IN operator', () => {
|
||||||
|
expect(formatValueForExpression('123', 'IN')).toBe("['123']");
|
||||||
|
expect(formatValueForExpression(['123', '456'], 'IN')).toBe(
|
||||||
|
"['123', '456']",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Quoted string values', () => {
|
||||||
|
it('should return already quoted strings as-is', () => {
|
||||||
|
expect(formatValueForExpression("'quoted'")).toBe("'quoted'");
|
||||||
|
expect(formatValueForExpression('"double-quoted"')).toBe('"double-quoted"');
|
||||||
|
expect(formatValueForExpression('`backticked`')).toBe('`backticked`');
|
||||||
|
expect(formatValueForExpression("'100000'")).toBe("'100000'");
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should preserve quoted strings in arrays', () => {
|
||||||
|
expect(formatValueForExpression(["'value1'", "'value2'"])).toBe(
|
||||||
|
"['value1', 'value2']",
|
||||||
|
);
|
||||||
|
expect(formatValueForExpression(["'100000'", "'200000'"], 'IN')).toBe(
|
||||||
|
"['100000', '200000']",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Regular string values', () => {
|
||||||
|
it('should wrap regular strings in single quotes', () => {
|
||||||
|
expect(formatValueForExpression('hello')).toBe("'hello'");
|
||||||
|
expect(formatValueForExpression('api-gateway')).toBe("'api-gateway'");
|
||||||
|
expect(formatValueForExpression('test value')).toBe("'test value'");
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should escape single quotes in strings', () => {
|
||||||
|
expect(formatValueForExpression("user's data")).toBe("'user\\'s data'");
|
||||||
|
expect(formatValueForExpression("John's")).toBe("'John\\'s'");
|
||||||
|
expect(formatValueForExpression("it's a test")).toBe("'it\\'s a test'");
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle empty strings', () => {
|
||||||
|
expect(formatValueForExpression('')).toBe("''");
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle strings with special characters', () => {
|
||||||
|
expect(formatValueForExpression('/api/v1/users')).toBe("'/api/v1/users'");
|
||||||
|
expect(formatValueForExpression('user@example.com')).toBe(
|
||||||
|
"'user@example.com'",
|
||||||
|
);
|
||||||
|
expect(formatValueForExpression('Contains "quotes"')).toBe(
|
||||||
|
'\'Contains "quotes"\'',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Number values', () => {
|
||||||
|
it('should convert numbers to strings without quotes', () => {
|
||||||
|
expect(formatValueForExpression(123)).toBe('123');
|
||||||
|
expect(formatValueForExpression(0)).toBe('0');
|
||||||
|
expect(formatValueForExpression(-42)).toBe('-42');
|
||||||
|
expect(formatValueForExpression(100000)).toBe('100000');
|
||||||
|
expect(formatValueForExpression(3.14)).toBe('3.14');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle numbers with IN operator', () => {
|
||||||
|
expect(formatValueForExpression(123, 'IN')).toBe('[123]');
|
||||||
|
expect(formatValueForExpression([100, 200] as any, 'IN')).toBe('[100, 200]');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Boolean values', () => {
|
||||||
|
it('should convert booleans to strings without quotes', () => {
|
||||||
|
expect(formatValueForExpression(true)).toBe('true');
|
||||||
|
expect(formatValueForExpression(false)).toBe('false');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle booleans with IN operator', () => {
|
||||||
|
expect(formatValueForExpression(true, 'IN')).toBe('[true]');
|
||||||
|
expect(formatValueForExpression([true, false] as any, 'IN')).toBe(
|
||||||
|
'[true, false]',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Array values', () => {
|
||||||
|
it('should format array of strings', () => {
|
||||||
|
expect(formatValueForExpression(['a', 'b', 'c'])).toBe("['a', 'b', 'c']");
|
||||||
|
expect(formatValueForExpression(['service1', 'service2'])).toBe(
|
||||||
|
"['service1', 'service2']",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should format array of numeric strings', () => {
|
||||||
|
expect(formatValueForExpression(['123', '456', '789'])).toBe(
|
||||||
|
"['123', '456', '789']",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should format array of numbers', () => {
|
||||||
|
expect(formatValueForExpression([1, 2, 3] as any)).toBe('[1, 2, 3]');
|
||||||
|
expect(formatValueForExpression([100, 200, 300] as any)).toBe(
|
||||||
|
'[100, 200, 300]',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should format mixed array types', () => {
|
||||||
|
expect(formatValueForExpression(['hello', 123, true] as any)).toBe(
|
||||||
|
"['hello', 123, true]",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should format array with quoted values', () => {
|
||||||
|
expect(formatValueForExpression(["'quoted'", 'regular'])).toBe(
|
||||||
|
"['quoted', 'regular']",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should format array with empty strings', () => {
|
||||||
|
expect(formatValueForExpression(['', 'value'])).toBe("['', 'value']");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('IN and NOT IN operators', () => {
|
||||||
|
it('should format single value as array for IN operator', () => {
|
||||||
|
expect(formatValueForExpression('value', 'IN')).toBe("['value']");
|
||||||
|
expect(formatValueForExpression(123, 'IN')).toBe('[123]');
|
||||||
|
expect(formatValueForExpression('123', 'IN')).toBe("['123']");
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should format array for IN operator', () => {
|
||||||
|
expect(formatValueForExpression(['a', 'b'], 'IN')).toBe("['a', 'b']");
|
||||||
|
expect(formatValueForExpression(['123', '456'], 'IN')).toBe(
|
||||||
|
"['123', '456']",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should format single value as array for NOT IN operator', () => {
|
||||||
|
expect(formatValueForExpression('value', 'NOT IN')).toBe("['value']");
|
||||||
|
expect(formatValueForExpression('value', 'not in')).toBe("['value']");
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should format array for NOT IN operator', () => {
|
||||||
|
expect(formatValueForExpression(['a', 'b'], 'NOT IN')).toBe("['a', 'b']");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Edge cases', () => {
|
||||||
|
it('should handle strings that look like numbers but have quotes', () => {
|
||||||
|
expect(formatValueForExpression("'123'")).toBe("'123'");
|
||||||
|
expect(formatValueForExpression('"456"')).toBe('"456"');
|
||||||
|
expect(formatValueForExpression('`789`')).toBe('`789`');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle strings with leading/trailing whitespace', () => {
|
||||||
|
expect(formatValueForExpression(' hello ')).toBe("' hello '");
|
||||||
|
expect(formatValueForExpression(' 123 ')).toBe("' 123 '");
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle very large numbers', () => {
|
||||||
|
expect(formatValueForExpression('999999999')).toBe("'999999999'");
|
||||||
|
expect(formatValueForExpression(999999999)).toBe('999999999');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle decimal numbers', () => {
|
||||||
|
expect(formatValueForExpression('123.456')).toBe("'123.456'");
|
||||||
|
expect(formatValueForExpression(123.456)).toBe('123.456');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle negative numbers', () => {
|
||||||
|
expect(formatValueForExpression('-100')).toBe("'-100'");
|
||||||
|
expect(formatValueForExpression(-100)).toBe('-100');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle strings that are not valid numbers', () => {
|
||||||
|
expect(formatValueForExpression('123abc')).toBe("'123abc'");
|
||||||
|
expect(formatValueForExpression('abc123')).toBe("'abc123'");
|
||||||
|
expect(formatValueForExpression('12.34.56')).toBe("'12.34.56'");
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle empty array', () => {
|
||||||
|
expect(formatValueForExpression([])).toBe('[]');
|
||||||
|
expect(formatValueForExpression([], 'IN')).toBe('[]');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle array with single element', () => {
|
||||||
|
expect(formatValueForExpression(['single'])).toBe("['single']");
|
||||||
|
expect(formatValueForExpression([123] as any)).toBe('[123]');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ import {
|
|||||||
import { EQueryType } from 'types/common/dashboard';
|
import { EQueryType } from 'types/common/dashboard';
|
||||||
import { DataSource, ReduceOperators } from 'types/common/queryBuilder';
|
import { DataSource, ReduceOperators } from 'types/common/queryBuilder';
|
||||||
import { extractQueryPairs } from 'utils/queryContextUtils';
|
import { extractQueryPairs } from 'utils/queryContextUtils';
|
||||||
import { unquote } from 'utils/stringUtils';
|
import { isQuoted, unquote } from 'utils/stringUtils';
|
||||||
import { isFunctionOperator, isNonValueOperator } from 'utils/tokenUtils';
|
import { isFunctionOperator, isNonValueOperator } from 'utils/tokenUtils';
|
||||||
import { v4 as uuid } from 'uuid';
|
import { v4 as uuid } from 'uuid';
|
||||||
|
|
||||||
@@ -38,49 +38,57 @@ const isArrayOperator = (operator: string): boolean => {
|
|||||||
return arrayOperators.includes(operator);
|
return arrayOperators.includes(operator);
|
||||||
};
|
};
|
||||||
|
|
||||||
const isVariable = (value: string | string[] | number | boolean): boolean => {
|
const isVariable = (
|
||||||
|
value: (string | number | boolean)[] | string | number | boolean,
|
||||||
|
): boolean => {
|
||||||
if (Array.isArray(value)) {
|
if (Array.isArray(value)) {
|
||||||
return value.some((v) => typeof v === 'string' && v.trim().startsWith('$'));
|
return value.some((v) => typeof v === 'string' && v.trim().startsWith('$'));
|
||||||
}
|
}
|
||||||
return typeof value === 'string' && value.trim().startsWith('$');
|
return typeof value === 'string' && value.trim().startsWith('$');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Formats a single value for use in expression strings.
|
||||||
|
* Strings are quoted and escaped, while numbers and booleans are converted to strings.
|
||||||
|
*/
|
||||||
|
const formatSingleValue = (v: string | number | boolean): string => {
|
||||||
|
if (typeof v === 'string') {
|
||||||
|
// Preserve already-quoted strings
|
||||||
|
if (isQuoted(v)) {
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
// Quote and escape single quotes in strings
|
||||||
|
return `'${v.replace(/'/g, "\\'")}'`;
|
||||||
|
}
|
||||||
|
// Convert numbers and booleans to strings without quotes
|
||||||
|
return String(v);
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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
|
||||||
* @param operator - The operator being used (to determine if array is needed)
|
* @param operator - The operator being used (to determine if array is needed)
|
||||||
* @returns Formatted value string
|
* @returns Formatted value string
|
||||||
*/
|
*/
|
||||||
const formatValueForExpression = (
|
export const formatValueForExpression = (
|
||||||
value: string[] | string | number | boolean,
|
value: (string | number | boolean)[] | string | number | boolean,
|
||||||
operator?: string,
|
operator?: string,
|
||||||
): string => {
|
): string => {
|
||||||
if (isVariable(value)) {
|
if (isVariable(value)) {
|
||||||
return String(value);
|
return String(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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];
|
||||||
return `[${arrayValue
|
return `[${arrayValue.map(formatSingleValue).join(', ')}]`;
|
||||||
.map((v) =>
|
|
||||||
typeof v === 'string' ? `'${v.replace(/'/g, "\\'")}'` : String(v),
|
|
||||||
)
|
|
||||||
.join(', ')}]`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Array.isArray(value)) {
|
if (Array.isArray(value)) {
|
||||||
// Handle array values (e.g., for IN operations)
|
return `[${value.map(formatSingleValue).join(', ')}]`;
|
||||||
return `[${value
|
|
||||||
.map((v) =>
|
|
||||||
typeof v === 'string' ? `'${v.replace(/'/g, "\\'")}'` : String(v),
|
|
||||||
)
|
|
||||||
.join(', ')}]`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof value === 'string') {
|
if (typeof value === 'string') {
|
||||||
// Add single quotes around all string values and escape internal single quotes
|
return formatSingleValue(value);
|
||||||
return `'${value.replace(/'/g, "\\'")}'`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return String(value);
|
return String(value);
|
||||||
@@ -136,14 +144,43 @@ export const convertFiltersToExpression = (
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const formatValuesForFilter = (value: string | string[]): string | string[] => {
|
/**
|
||||||
if (Array.isArray(value)) {
|
* Converts a string value to its appropriate type (number, boolean, or string)
|
||||||
return value.map((v) => (typeof v === 'string' ? unquote(v) : String(v)));
|
* for use in filter objects. This is the inverse of formatSingleValue.
|
||||||
}
|
*/
|
||||||
|
function formatSingleValueForFilter(
|
||||||
|
value: string | number | boolean,
|
||||||
|
): string | number | boolean {
|
||||||
if (typeof value === 'string') {
|
if (typeof value === 'string') {
|
||||||
return unquote(value);
|
const trimmed = value.trim();
|
||||||
|
|
||||||
|
// Try to convert numeric strings to numbers
|
||||||
|
if (trimmed !== '' && !Number.isNaN(Number(trimmed))) {
|
||||||
|
return Number(trimmed);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert boolean strings to booleans
|
||||||
|
if (trimmed === 'true' || trimmed === 'false') {
|
||||||
|
return trimmed === 'true';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return String(value);
|
|
||||||
|
// Return non-string values as-is, or string values that couldn't be converted
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Formats values for filter objects, converting string representations
|
||||||
|
* to their proper types (numbers, booleans) when appropriate.
|
||||||
|
*/
|
||||||
|
const formatValuesForFilter = (
|
||||||
|
value: (string | number | boolean)[] | number | boolean | string,
|
||||||
|
): (string | number | boolean)[] | number | boolean | string => {
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
return value.map(formatSingleValueForFilter);
|
||||||
|
}
|
||||||
|
|
||||||
|
return formatSingleValueForFilter(value);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const convertExpressionToFilters = (
|
export const convertExpressionToFilters = (
|
||||||
|
|||||||
@@ -178,7 +178,7 @@ export default function CheckboxFilter(props: ICheckboxProps): JSX.Element {
|
|||||||
if (SELECTED_OPERATORS.includes(filterSync.op)) {
|
if (SELECTED_OPERATORS.includes(filterSync.op)) {
|
||||||
if (isArray(filterSync.value)) {
|
if (isArray(filterSync.value)) {
|
||||||
filterSync.value.forEach((val) => {
|
filterSync.value.forEach((val) => {
|
||||||
filterState[val] = true;
|
filterState[String(val)] = true;
|
||||||
});
|
});
|
||||||
} else if (typeof filterSync.value === 'string') {
|
} else if (typeof filterSync.value === 'string') {
|
||||||
filterState[filterSync.value] = true;
|
filterState[filterSync.value] = true;
|
||||||
@@ -191,7 +191,7 @@ export default function CheckboxFilter(props: ICheckboxProps): JSX.Element {
|
|||||||
filterState = setDefaultValues(attributeValues, true);
|
filterState = setDefaultValues(attributeValues, true);
|
||||||
if (isArray(filterSync.value)) {
|
if (isArray(filterSync.value)) {
|
||||||
filterSync.value.forEach((val) => {
|
filterSync.value.forEach((val) => {
|
||||||
filterState[val] = false;
|
filterState[String(val)] = false;
|
||||||
});
|
});
|
||||||
} else if (typeof filterSync.value === 'string') {
|
} else if (typeof filterSync.value === 'string') {
|
||||||
filterState[filterSync.value] = false;
|
filterState[filterSync.value] = false;
|
||||||
|
|||||||
@@ -1,223 +0,0 @@
|
|||||||
import { render } from '@testing-library/react';
|
|
||||||
import { act } from 'react-dom/test-utils';
|
|
||||||
import { MemoryRouter } from 'react-router-dom';
|
|
||||||
import { Userpilot } from 'userpilot';
|
|
||||||
|
|
||||||
import UserpilotRouteTracker from './UserpilotRouteTracker';
|
|
||||||
|
|
||||||
// Mock constants
|
|
||||||
const INITIAL_PATH = '/initial';
|
|
||||||
const TIMER_DELAY = 100;
|
|
||||||
|
|
||||||
// Mock the userpilot module
|
|
||||||
jest.mock('userpilot', () => ({
|
|
||||||
Userpilot: {
|
|
||||||
reload: jest.fn(),
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
// Mock location state
|
|
||||||
let mockLocation = {
|
|
||||||
pathname: INITIAL_PATH,
|
|
||||||
search: '',
|
|
||||||
hash: '',
|
|
||||||
state: null,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Mock react-router-dom
|
|
||||||
jest.mock('react-router-dom', () => {
|
|
||||||
const originalModule = jest.requireActual('react-router-dom');
|
|
||||||
|
|
||||||
return {
|
|
||||||
...originalModule,
|
|
||||||
useLocation: jest.fn(() => mockLocation),
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('UserpilotRouteTracker', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
jest.clearAllMocks();
|
|
||||||
// Reset timers
|
|
||||||
jest.useFakeTimers();
|
|
||||||
// Reset error mock implementation
|
|
||||||
(Userpilot.reload as jest.Mock).mockImplementation(() => {});
|
|
||||||
// Reset location to initial state
|
|
||||||
mockLocation = {
|
|
||||||
pathname: INITIAL_PATH,
|
|
||||||
search: '',
|
|
||||||
hash: '',
|
|
||||||
state: null,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
jest.useRealTimers();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('calls Userpilot.reload on initial render', () => {
|
|
||||||
render(
|
|
||||||
<MemoryRouter>
|
|
||||||
<UserpilotRouteTracker />
|
|
||||||
</MemoryRouter>,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Fast-forward timer to trigger the setTimeout in reloadUserpilot
|
|
||||||
act(() => {
|
|
||||||
jest.advanceTimersByTime(TIMER_DELAY);
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(Userpilot.reload).toHaveBeenCalledTimes(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('calls Userpilot.reload when pathname changes', () => {
|
|
||||||
const { rerender } = render(
|
|
||||||
<MemoryRouter>
|
|
||||||
<UserpilotRouteTracker />
|
|
||||||
</MemoryRouter>,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Fast-forward initial render timer
|
|
||||||
act(() => {
|
|
||||||
jest.advanceTimersByTime(TIMER_DELAY);
|
|
||||||
});
|
|
||||||
jest.clearAllMocks();
|
|
||||||
|
|
||||||
// Create a new location object with different pathname
|
|
||||||
const newLocation = {
|
|
||||||
...mockLocation,
|
|
||||||
pathname: '/new-path',
|
|
||||||
};
|
|
||||||
|
|
||||||
// Update the mock location with new path and trigger re-render
|
|
||||||
act(() => {
|
|
||||||
mockLocation = newLocation;
|
|
||||||
// Force a component update with the new location
|
|
||||||
rerender(
|
|
||||||
<MemoryRouter>
|
|
||||||
<UserpilotRouteTracker />
|
|
||||||
</MemoryRouter>,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Fast-forward timer to allow the setTimeout to execute
|
|
||||||
act(() => {
|
|
||||||
jest.advanceTimersByTime(TIMER_DELAY);
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(Userpilot.reload).toHaveBeenCalledTimes(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('calls Userpilot.reload when search parameters change', () => {
|
|
||||||
const { rerender } = render(
|
|
||||||
<MemoryRouter>
|
|
||||||
<UserpilotRouteTracker />
|
|
||||||
</MemoryRouter>,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Fast-forward initial render timer
|
|
||||||
act(() => {
|
|
||||||
jest.advanceTimersByTime(TIMER_DELAY);
|
|
||||||
});
|
|
||||||
jest.clearAllMocks();
|
|
||||||
|
|
||||||
// Create a new location object with different search params
|
|
||||||
const newLocation = {
|
|
||||||
...mockLocation,
|
|
||||||
search: '?param=value',
|
|
||||||
};
|
|
||||||
|
|
||||||
// Update the mock location with new search and trigger re-render
|
|
||||||
// eslint-disable-next-line sonarjs/no-identical-functions
|
|
||||||
act(() => {
|
|
||||||
mockLocation = newLocation;
|
|
||||||
// Force a component update with the new location
|
|
||||||
rerender(
|
|
||||||
<MemoryRouter>
|
|
||||||
<UserpilotRouteTracker />
|
|
||||||
</MemoryRouter>,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Fast-forward timer to allow the setTimeout to execute
|
|
||||||
act(() => {
|
|
||||||
jest.advanceTimersByTime(TIMER_DELAY);
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(Userpilot.reload).toHaveBeenCalledTimes(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('handles errors in Userpilot.reload gracefully', () => {
|
|
||||||
// Mock console.error to prevent test output noise and capture calls
|
|
||||||
const consoleErrorSpy = jest
|
|
||||||
.spyOn(console, 'error')
|
|
||||||
.mockImplementation(() => {});
|
|
||||||
|
|
||||||
// Instead of using the component, we test the error handling behavior directly
|
|
||||||
const errorMsg = 'Error message';
|
|
||||||
|
|
||||||
// Set up a function that has the same error handling behavior as in component
|
|
||||||
const testErrorHandler = (): void => {
|
|
||||||
try {
|
|
||||||
if (typeof Userpilot !== 'undefined' && Userpilot.reload) {
|
|
||||||
Userpilot.reload();
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('[Userpilot] Error reloading on route change:', error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Make Userpilot.reload throw an error
|
|
||||||
(Userpilot.reload as jest.Mock).mockImplementation(() => {
|
|
||||||
throw new Error(errorMsg);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Execute the function that should handle errors
|
|
||||||
testErrorHandler();
|
|
||||||
|
|
||||||
// Verify error was logged
|
|
||||||
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
|
||||||
'[Userpilot] Error reloading on route change:',
|
|
||||||
expect.any(Error),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Restore console mock
|
|
||||||
consoleErrorSpy.mockRestore();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('does not call Userpilot.reload when same route is rendered again', () => {
|
|
||||||
const { rerender } = render(
|
|
||||||
<MemoryRouter>
|
|
||||||
<UserpilotRouteTracker />
|
|
||||||
</MemoryRouter>,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Fast-forward initial render timer
|
|
||||||
act(() => {
|
|
||||||
jest.advanceTimersByTime(TIMER_DELAY);
|
|
||||||
});
|
|
||||||
jest.clearAllMocks();
|
|
||||||
|
|
||||||
act(() => {
|
|
||||||
mockLocation = {
|
|
||||||
pathname: mockLocation.pathname,
|
|
||||||
search: mockLocation.search,
|
|
||||||
hash: mockLocation.hash,
|
|
||||||
state: mockLocation.state,
|
|
||||||
};
|
|
||||||
// Force a component update with the same location
|
|
||||||
rerender(
|
|
||||||
<MemoryRouter>
|
|
||||||
<UserpilotRouteTracker />
|
|
||||||
</MemoryRouter>,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Fast-forward timer
|
|
||||||
act(() => {
|
|
||||||
jest.advanceTimersByTime(TIMER_DELAY);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Should not call reload since path and search are the same
|
|
||||||
expect(Userpilot.reload).not.toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,60 +0,0 @@
|
|||||||
import { useCallback, useEffect, useRef } from 'react';
|
|
||||||
import { useLocation } from 'react-router-dom';
|
|
||||||
import { Userpilot } from 'userpilot';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* UserpilotRouteTracker - A component that tracks route changes and calls Userpilot.reload
|
|
||||||
* on actual page changes (pathname changes or significant query parameter changes).
|
|
||||||
*
|
|
||||||
* This component renders nothing and is designed to be placed once high in the component tree.
|
|
||||||
*/
|
|
||||||
function UserpilotRouteTracker(): null {
|
|
||||||
const location = useLocation();
|
|
||||||
const prevPathRef = useRef<string>(location.pathname);
|
|
||||||
const prevSearchRef = useRef<string>(location.search);
|
|
||||||
const isFirstRenderRef = useRef<boolean>(true);
|
|
||||||
|
|
||||||
// Function to reload Userpilot safely - using useCallback to avoid dependency issues
|
|
||||||
const reloadUserpilot = useCallback((): void => {
|
|
||||||
try {
|
|
||||||
if (typeof Userpilot !== 'undefined' && Userpilot.reload) {
|
|
||||||
setTimeout(() => {
|
|
||||||
Userpilot.reload();
|
|
||||||
}, 100);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('[Userpilot] Error reloading on route change:', error);
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// Handle first render
|
|
||||||
useEffect(() => {
|
|
||||||
if (isFirstRenderRef.current) {
|
|
||||||
isFirstRenderRef.current = false;
|
|
||||||
reloadUserpilot();
|
|
||||||
}
|
|
||||||
}, [reloadUserpilot]);
|
|
||||||
|
|
||||||
// Handle route/query changes
|
|
||||||
useEffect(() => {
|
|
||||||
// Skip first render as it's handled by the effect above
|
|
||||||
if (isFirstRenderRef.current) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if the path has changed or if significant query params have changed
|
|
||||||
const pathChanged = location.pathname !== prevPathRef.current;
|
|
||||||
const searchChanged = location.search !== prevSearchRef.current;
|
|
||||||
|
|
||||||
if (pathChanged || searchChanged) {
|
|
||||||
// Update refs
|
|
||||||
prevPathRef.current = location.pathname;
|
|
||||||
prevSearchRef.current = location.search;
|
|
||||||
reloadUserpilot();
|
|
||||||
}
|
|
||||||
}, [location.pathname, location.search, reloadUserpilot]);
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default UserpilotRouteTracker;
|
|
||||||
@@ -7,7 +7,7 @@ import ErrorIcon from 'assets/Error';
|
|||||||
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
|
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
|
||||||
import { BookOpenText, ChevronsDown, TriangleAlert } from 'lucide-react';
|
import { BookOpenText, ChevronsDown, TriangleAlert } from 'lucide-react';
|
||||||
import KeyValueLabel from 'periscope/components/KeyValueLabel';
|
import KeyValueLabel from 'periscope/components/KeyValueLabel';
|
||||||
import { ReactNode } from 'react';
|
import { ReactNode, useMemo } from 'react';
|
||||||
import { Warning } from 'types/api';
|
import { Warning } from 'types/api';
|
||||||
|
|
||||||
interface WarningContentProps {
|
interface WarningContentProps {
|
||||||
@@ -106,19 +106,51 @@ export function WarningContent({ warning }: WarningContentProps): JSX.Element {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function PopoverMessage({
|
||||||
|
message,
|
||||||
|
}: {
|
||||||
|
message: string | ReactNode;
|
||||||
|
}): JSX.Element {
|
||||||
|
return (
|
||||||
|
<section className="warning-content">
|
||||||
|
<section className="warning-content__summary-section">
|
||||||
|
<header className="warning-content__summary">
|
||||||
|
<div className="warning-content__summary-left">
|
||||||
|
<div className="warning-content__summary-text">
|
||||||
|
<p className="warning-content__warning-message">{message}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
</section>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
interface WarningPopoverProps extends PopoverProps {
|
interface WarningPopoverProps extends PopoverProps {
|
||||||
children?: ReactNode;
|
children?: ReactNode;
|
||||||
warningData: Warning;
|
warningData?: Warning;
|
||||||
|
message?: string | ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
function WarningPopover({
|
function WarningPopover({
|
||||||
children,
|
children,
|
||||||
warningData,
|
warningData,
|
||||||
|
message = '',
|
||||||
...popoverProps
|
...popoverProps
|
||||||
}: WarningPopoverProps): JSX.Element {
|
}: WarningPopoverProps): JSX.Element {
|
||||||
|
const content = useMemo(() => {
|
||||||
|
if (message) {
|
||||||
|
return <PopoverMessage message={message} />;
|
||||||
|
}
|
||||||
|
if (warningData) {
|
||||||
|
return <WarningContent warning={warningData} />;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}, [message, warningData]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover
|
<Popover
|
||||||
content={<WarningContent warning={warningData} />}
|
content={content}
|
||||||
overlayStyle={{ padding: 0, maxWidth: '600px' }}
|
overlayStyle={{ padding: 0, maxWidth: '600px' }}
|
||||||
overlayInnerStyle={{ padding: 0 }}
|
overlayInnerStyle={{ padding: 0 }}
|
||||||
autoAdjustOverflow
|
autoAdjustOverflow
|
||||||
@@ -137,6 +169,8 @@ function WarningPopover({
|
|||||||
|
|
||||||
WarningPopover.defaultProps = {
|
WarningPopover.defaultProps = {
|
||||||
children: undefined,
|
children: undefined,
|
||||||
|
warningData: null,
|
||||||
|
message: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default WarningPopover;
|
export default WarningPopover;
|
||||||
|
|||||||
@@ -3,9 +3,9 @@ import './styles.scss';
|
|||||||
import { Select } from 'antd';
|
import { Select } from 'antd';
|
||||||
import { DefaultOptionType } from 'antd/es/select';
|
import { DefaultOptionType } from 'antd/es/select';
|
||||||
|
|
||||||
import { UniversalYAxisUnitMappings, Y_AXIS_CATEGORIES } from './constants';
|
import { UniversalYAxisUnitMappings } from './constants';
|
||||||
import { UniversalYAxisUnit, YAxisUnitSelectorProps } from './types';
|
import { UniversalYAxisUnit, YAxisUnitSelectorProps } from './types';
|
||||||
import { mapMetricUnitToUniversalUnit } from './utils';
|
import { getYAxisCategories, mapMetricUnitToUniversalUnit } from './utils';
|
||||||
|
|
||||||
function YAxisUnitSelector({
|
function YAxisUnitSelector({
|
||||||
value,
|
value,
|
||||||
@@ -13,6 +13,7 @@ function YAxisUnitSelector({
|
|||||||
placeholder = 'Please select a unit',
|
placeholder = 'Please select a unit',
|
||||||
loading = false,
|
loading = false,
|
||||||
'data-testid': dataTestId,
|
'data-testid': dataTestId,
|
||||||
|
source,
|
||||||
}: YAxisUnitSelectorProps): JSX.Element {
|
}: YAxisUnitSelectorProps): JSX.Element {
|
||||||
const universalUnit = mapMetricUnitToUniversalUnit(value);
|
const universalUnit = mapMetricUnitToUniversalUnit(value);
|
||||||
|
|
||||||
@@ -37,6 +38,8 @@ function YAxisUnitSelector({
|
|||||||
return aliases.some((alias) => alias.toLowerCase().includes(search));
|
return aliases.some((alias) => alias.toLowerCase().includes(search));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const categories = getYAxisCategories(source);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="y-axis-unit-selector-component">
|
<div className="y-axis-unit-selector-component">
|
||||||
<Select
|
<Select
|
||||||
@@ -48,7 +51,7 @@ function YAxisUnitSelector({
|
|||||||
loading={loading}
|
loading={loading}
|
||||||
data-testid={dataTestId}
|
data-testid={dataTestId}
|
||||||
>
|
>
|
||||||
{Y_AXIS_CATEGORIES.map((category) => (
|
{categories.map((category) => (
|
||||||
<Select.OptGroup key={category.name} label={category.name}>
|
<Select.OptGroup key={category.name} label={category.name}>
|
||||||
{category.units.map((unit) => (
|
{category.units.map((unit) => (
|
||||||
<Select.Option key={unit.id} value={unit.id}>
|
<Select.Option key={unit.id} value={unit.id}>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { fireEvent, render, screen } from '@testing-library/react';
|
import { fireEvent, render, screen } from '@testing-library/react';
|
||||||
|
|
||||||
|
import { YAxisSource } from '../types';
|
||||||
import YAxisUnitSelector from '../YAxisUnitSelector';
|
import YAxisUnitSelector from '../YAxisUnitSelector';
|
||||||
|
|
||||||
describe('YAxisUnitSelector', () => {
|
describe('YAxisUnitSelector', () => {
|
||||||
@@ -10,7 +11,13 @@ describe('YAxisUnitSelector', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('renders with default placeholder', () => {
|
it('renders with default placeholder', () => {
|
||||||
render(<YAxisUnitSelector value="" onChange={mockOnChange} />);
|
render(
|
||||||
|
<YAxisUnitSelector
|
||||||
|
value=""
|
||||||
|
onChange={mockOnChange}
|
||||||
|
source={YAxisSource.ALERTS}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
expect(screen.getByText('Please select a unit')).toBeInTheDocument();
|
expect(screen.getByText('Please select a unit')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -20,13 +27,20 @@ describe('YAxisUnitSelector', () => {
|
|||||||
value=""
|
value=""
|
||||||
onChange={mockOnChange}
|
onChange={mockOnChange}
|
||||||
placeholder="Custom placeholder"
|
placeholder="Custom placeholder"
|
||||||
|
source={YAxisSource.ALERTS}
|
||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
expect(screen.queryByText('Custom placeholder')).toBeInTheDocument();
|
expect(screen.queryByText('Custom placeholder')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('calls onChange when a value is selected', () => {
|
it('calls onChange when a value is selected', () => {
|
||||||
render(<YAxisUnitSelector value="" onChange={mockOnChange} />);
|
render(
|
||||||
|
<YAxisUnitSelector
|
||||||
|
value=""
|
||||||
|
onChange={mockOnChange}
|
||||||
|
source={YAxisSource.ALERTS}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
const select = screen.getByRole('combobox');
|
const select = screen.getByRole('combobox');
|
||||||
|
|
||||||
fireEvent.mouseDown(select);
|
fireEvent.mouseDown(select);
|
||||||
@@ -41,18 +55,30 @@ describe('YAxisUnitSelector', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('filters options based on search input', () => {
|
it('filters options based on search input', () => {
|
||||||
render(<YAxisUnitSelector value="" onChange={mockOnChange} />);
|
render(
|
||||||
|
<YAxisUnitSelector
|
||||||
|
value=""
|
||||||
|
onChange={mockOnChange}
|
||||||
|
source={YAxisSource.ALERTS}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
const select = screen.getByRole('combobox');
|
const select = screen.getByRole('combobox');
|
||||||
|
|
||||||
fireEvent.mouseDown(select);
|
fireEvent.mouseDown(select);
|
||||||
const input = screen.getByRole('combobox');
|
const input = screen.getByRole('combobox');
|
||||||
fireEvent.change(input, { target: { value: 'byte' } });
|
fireEvent.change(input, { target: { value: 'bytes/sec' } });
|
||||||
|
|
||||||
expect(screen.getByText('Bytes/sec')).toBeInTheDocument();
|
expect(screen.getByText('Bytes/sec')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('shows all categories and their units', () => {
|
it('shows all categories and their units', () => {
|
||||||
render(<YAxisUnitSelector value="" onChange={mockOnChange} />);
|
render(
|
||||||
|
<YAxisUnitSelector
|
||||||
|
value=""
|
||||||
|
onChange={mockOnChange}
|
||||||
|
source={YAxisSource.ALERTS}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
const select = screen.getByRole('combobox');
|
const select = screen.getByRole('combobox');
|
||||||
|
|
||||||
fireEvent.mouseDown(select);
|
fireEvent.mouseDown(select);
|
||||||
|
|||||||
@@ -0,0 +1,951 @@
|
|||||||
|
import { UniversalYAxisUnit } from 'components/YAxisUnitSelector/types';
|
||||||
|
|
||||||
|
import {
|
||||||
|
AdditionalLabelsMappingForGrafanaUnits,
|
||||||
|
UniversalUnitToGrafanaUnit,
|
||||||
|
} from '../constants';
|
||||||
|
import { formatUniversalUnit } from '../formatter';
|
||||||
|
|
||||||
|
describe('formatUniversalUnit', () => {
|
||||||
|
describe('Time', () => {
|
||||||
|
test.each([
|
||||||
|
// Days
|
||||||
|
[31, UniversalYAxisUnit.DAYS, '4.43 weeks'],
|
||||||
|
[7, UniversalYAxisUnit.DAYS, '1 week'],
|
||||||
|
[6, UniversalYAxisUnit.DAYS, '6 days'],
|
||||||
|
[1, UniversalYAxisUnit.DAYS, '1 day'],
|
||||||
|
// Hours
|
||||||
|
[25, UniversalYAxisUnit.HOURS, '1.04 days'],
|
||||||
|
[23, UniversalYAxisUnit.HOURS, '23 hour'],
|
||||||
|
[1, UniversalYAxisUnit.HOURS, '1 hour'],
|
||||||
|
// Minutes
|
||||||
|
[61, UniversalYAxisUnit.MINUTES, '1.02 hours'],
|
||||||
|
[60, UniversalYAxisUnit.MINUTES, '1 hour'],
|
||||||
|
[45, UniversalYAxisUnit.MINUTES, '45 min'],
|
||||||
|
[1, UniversalYAxisUnit.MINUTES, '1 min'],
|
||||||
|
// Seconds
|
||||||
|
[100000, UniversalYAxisUnit.SECONDS, '1.16 days'],
|
||||||
|
[10065, UniversalYAxisUnit.SECONDS, '2.8 hours'],
|
||||||
|
[61, UniversalYAxisUnit.SECONDS, '1.02 mins'],
|
||||||
|
[60, UniversalYAxisUnit.SECONDS, '1 min'],
|
||||||
|
[12, UniversalYAxisUnit.SECONDS, '12 s'],
|
||||||
|
[1, UniversalYAxisUnit.SECONDS, '1 s'],
|
||||||
|
// Milliseconds
|
||||||
|
[1006, UniversalYAxisUnit.MILLISECONDS, '1.01 s'],
|
||||||
|
[10000000, UniversalYAxisUnit.MILLISECONDS, '2.78 hours'],
|
||||||
|
[100006, UniversalYAxisUnit.MICROSECONDS, '100 ms'],
|
||||||
|
[1, UniversalYAxisUnit.MICROSECONDS, '1 µs'],
|
||||||
|
[12, UniversalYAxisUnit.MICROSECONDS, '12 µs'],
|
||||||
|
// Nanoseconds
|
||||||
|
[10000000000, UniversalYAxisUnit.NANOSECONDS, '10 s'],
|
||||||
|
[10000006, UniversalYAxisUnit.NANOSECONDS, '10 ms'],
|
||||||
|
[1006, UniversalYAxisUnit.NANOSECONDS, '1.01 µs'],
|
||||||
|
[1, UniversalYAxisUnit.NANOSECONDS, '1 ns'],
|
||||||
|
[12, UniversalYAxisUnit.NANOSECONDS, '12 ns'],
|
||||||
|
])('formats time value %s %s as %s', (value, unit, expected) => {
|
||||||
|
expect(formatUniversalUnit(value, unit)).toBe(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Data', () => {
|
||||||
|
test.each([
|
||||||
|
// Bytes
|
||||||
|
[864, UniversalYAxisUnit.BYTES, '864 B'],
|
||||||
|
[1000, UniversalYAxisUnit.BYTES, '1 kB'],
|
||||||
|
[1020, UniversalYAxisUnit.BYTES, '1.02 kB'],
|
||||||
|
// Kilobytes
|
||||||
|
[512, UniversalYAxisUnit.KILOBYTES, '512 kB'],
|
||||||
|
[1000, UniversalYAxisUnit.KILOBYTES, '1 MB'],
|
||||||
|
[1023, UniversalYAxisUnit.KILOBYTES, '1.02 MB'],
|
||||||
|
// Megabytes
|
||||||
|
[777, UniversalYAxisUnit.MEGABYTES, '777 MB'],
|
||||||
|
[1000, UniversalYAxisUnit.MEGABYTES, '1 GB'],
|
||||||
|
[1023, UniversalYAxisUnit.MEGABYTES, '1.02 GB'],
|
||||||
|
// Gigabytes
|
||||||
|
[432, UniversalYAxisUnit.GIGABYTES, '432 GB'],
|
||||||
|
[1000, UniversalYAxisUnit.GIGABYTES, '1 TB'],
|
||||||
|
[1023, UniversalYAxisUnit.GIGABYTES, '1.02 TB'],
|
||||||
|
// Terabytes
|
||||||
|
[678, UniversalYAxisUnit.TERABYTES, '678 TB'],
|
||||||
|
[1000, UniversalYAxisUnit.TERABYTES, '1 PB'],
|
||||||
|
[1023, UniversalYAxisUnit.TERABYTES, '1.02 PB'],
|
||||||
|
// Petabytes
|
||||||
|
[845, UniversalYAxisUnit.PETABYTES, '845 PB'],
|
||||||
|
[1000, UniversalYAxisUnit.PETABYTES, '1 EB'],
|
||||||
|
[1023, UniversalYAxisUnit.PETABYTES, '1.02 EB'],
|
||||||
|
// Exabytes
|
||||||
|
[921, UniversalYAxisUnit.EXABYTES, '921 EB'],
|
||||||
|
[1000, UniversalYAxisUnit.EXABYTES, '1 ZB'],
|
||||||
|
[1023, UniversalYAxisUnit.EXABYTES, '1.02 ZB'],
|
||||||
|
// Zettabytes
|
||||||
|
[921, UniversalYAxisUnit.ZETTABYTES, '921 ZB'],
|
||||||
|
[1000, UniversalYAxisUnit.ZETTABYTES, '1 YB'],
|
||||||
|
[1023, UniversalYAxisUnit.ZETTABYTES, '1.02 YB'],
|
||||||
|
// Yottabytes
|
||||||
|
[921, UniversalYAxisUnit.YOTTABYTES, '921 YB'],
|
||||||
|
[1000, UniversalYAxisUnit.YOTTABYTES, '1000 YB'],
|
||||||
|
[1023, UniversalYAxisUnit.YOTTABYTES, '1023 YB'],
|
||||||
|
])('formats data value %s %s as %s', (value, unit, expected) => {
|
||||||
|
expect(formatUniversalUnit(value, unit)).toBe(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Data rate', () => {
|
||||||
|
test.each([
|
||||||
|
// Bytes/second
|
||||||
|
[864, UniversalYAxisUnit.BYTES_SECOND, '864 B/s'],
|
||||||
|
[1000, UniversalYAxisUnit.BYTES_SECOND, '1 kB/s'],
|
||||||
|
[1020, UniversalYAxisUnit.BYTES_SECOND, '1.02 kB/s'],
|
||||||
|
// Kilobytes/second
|
||||||
|
[512, UniversalYAxisUnit.KILOBYTES_SECOND, '512 kB/s'],
|
||||||
|
[1000, UniversalYAxisUnit.KILOBYTES_SECOND, '1 MB/s'],
|
||||||
|
[1023, UniversalYAxisUnit.KILOBYTES_SECOND, '1.02 MB/s'],
|
||||||
|
// Megabytes/second
|
||||||
|
[777, UniversalYAxisUnit.MEGABYTES_SECOND, '777 MB/s'],
|
||||||
|
[1000, UniversalYAxisUnit.MEGABYTES_SECOND, '1 GB/s'],
|
||||||
|
[1023, UniversalYAxisUnit.MEGABYTES_SECOND, '1.02 GB/s'],
|
||||||
|
// Gigabytes/second
|
||||||
|
[432, UniversalYAxisUnit.GIGABYTES_SECOND, '432 GB/s'],
|
||||||
|
[1000, UniversalYAxisUnit.GIGABYTES_SECOND, '1 TB/s'],
|
||||||
|
[1023, UniversalYAxisUnit.GIGABYTES_SECOND, '1.02 TB/s'],
|
||||||
|
// Terabytes/second
|
||||||
|
[678, UniversalYAxisUnit.TERABYTES_SECOND, '678 TB/s'],
|
||||||
|
[1000, UniversalYAxisUnit.TERABYTES_SECOND, '1 PB/s'],
|
||||||
|
[1023, UniversalYAxisUnit.TERABYTES_SECOND, '1.02 PB/s'],
|
||||||
|
// Petabytes/second
|
||||||
|
[845, UniversalYAxisUnit.PETABYTES_SECOND, '845 PB/s'],
|
||||||
|
[1000, UniversalYAxisUnit.PETABYTES_SECOND, '1 EB/s'],
|
||||||
|
[1023, UniversalYAxisUnit.PETABYTES_SECOND, '1.02 EB/s'],
|
||||||
|
// Exabytes/second
|
||||||
|
[921, UniversalYAxisUnit.EXABYTES_SECOND, '921 EB/s'],
|
||||||
|
[1000, UniversalYAxisUnit.EXABYTES_SECOND, '1 ZB/s'],
|
||||||
|
[1023, UniversalYAxisUnit.EXABYTES_SECOND, '1.02 ZB/s'],
|
||||||
|
// Zettabytes/second
|
||||||
|
[921, UniversalYAxisUnit.ZETTABYTES_SECOND, '921 ZB/s'],
|
||||||
|
[1000, UniversalYAxisUnit.ZETTABYTES_SECOND, '1 YB/s'],
|
||||||
|
[1023, UniversalYAxisUnit.ZETTABYTES_SECOND, '1.02 YB/s'],
|
||||||
|
// Yottabytes/second
|
||||||
|
[921, UniversalYAxisUnit.YOTTABYTES_SECOND, '921 YB/s'],
|
||||||
|
[1000, UniversalYAxisUnit.YOTTABYTES_SECOND, '1000 YB/s'],
|
||||||
|
[1023, UniversalYAxisUnit.YOTTABYTES_SECOND, '1023 YB/s'],
|
||||||
|
])('formats data value %s %s as %s', (value, unit, expected) => {
|
||||||
|
expect(formatUniversalUnit(value, unit)).toBe(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Bit', () => {
|
||||||
|
test.each([
|
||||||
|
// Bits
|
||||||
|
[1, UniversalYAxisUnit.BITS, '1 b'],
|
||||||
|
[250, UniversalYAxisUnit.BITS, '250 b'],
|
||||||
|
[1000, UniversalYAxisUnit.BITS, '1 kb'],
|
||||||
|
[1023, UniversalYAxisUnit.BITS, '1.02 kb'],
|
||||||
|
// Kilobits
|
||||||
|
[0.5, UniversalYAxisUnit.KILOBITS, '500 b'],
|
||||||
|
[375, UniversalYAxisUnit.KILOBITS, '375 kb'],
|
||||||
|
[1000, UniversalYAxisUnit.KILOBITS, '1 Mb'],
|
||||||
|
[1023, UniversalYAxisUnit.KILOBITS, '1.02 Mb'],
|
||||||
|
// Megabits
|
||||||
|
[0.5, UniversalYAxisUnit.MEGABITS, '500 kb'],
|
||||||
|
[640, UniversalYAxisUnit.MEGABITS, '640 Mb'],
|
||||||
|
[1000, UniversalYAxisUnit.MEGABITS, '1 Gb'],
|
||||||
|
[1023, UniversalYAxisUnit.MEGABITS, '1.02 Gb'],
|
||||||
|
// Gigabits
|
||||||
|
[0.5, UniversalYAxisUnit.GIGABITS, '500 Mb'],
|
||||||
|
[875, UniversalYAxisUnit.GIGABITS, '875 Gb'],
|
||||||
|
[1000, UniversalYAxisUnit.GIGABITS, '1 Tb'],
|
||||||
|
[1023, UniversalYAxisUnit.GIGABITS, '1.02 Tb'],
|
||||||
|
// Terabits
|
||||||
|
[0.5, UniversalYAxisUnit.TERABITS, '500 Gb'],
|
||||||
|
[430, UniversalYAxisUnit.TERABITS, '430 Tb'],
|
||||||
|
[1000, UniversalYAxisUnit.TERABITS, '1 Pb'],
|
||||||
|
[1023, UniversalYAxisUnit.TERABITS, '1.02 Pb'],
|
||||||
|
// Petabits
|
||||||
|
[0.5, UniversalYAxisUnit.PETABITS, '500 Tb'],
|
||||||
|
[590, UniversalYAxisUnit.PETABITS, '590 Pb'],
|
||||||
|
[1000, UniversalYAxisUnit.PETABITS, '1 Eb'],
|
||||||
|
[1023, UniversalYAxisUnit.PETABITS, '1.02 Eb'],
|
||||||
|
// Exabits
|
||||||
|
[0.5, UniversalYAxisUnit.EXABITS, '500 Pb'],
|
||||||
|
[715, UniversalYAxisUnit.EXABITS, '715 Eb'],
|
||||||
|
[1000, UniversalYAxisUnit.EXABITS, '1 Zb'],
|
||||||
|
[1023, UniversalYAxisUnit.EXABITS, '1.02 Zb'],
|
||||||
|
// Zettabits
|
||||||
|
[0.5, UniversalYAxisUnit.ZETTABITS, '500 Eb'],
|
||||||
|
[840, UniversalYAxisUnit.ZETTABITS, '840 Zb'],
|
||||||
|
[1000, UniversalYAxisUnit.ZETTABITS, '1 Yb'],
|
||||||
|
[1023, UniversalYAxisUnit.ZETTABITS, '1.02 Yb'],
|
||||||
|
// Yottabits
|
||||||
|
[0.5, UniversalYAxisUnit.YOTTABITS, '500 Zb'],
|
||||||
|
[965, UniversalYAxisUnit.YOTTABITS, '965 Yb'],
|
||||||
|
[1000, UniversalYAxisUnit.YOTTABITS, '1000 Yb'],
|
||||||
|
[1023, UniversalYAxisUnit.YOTTABITS, '1023 Yb'],
|
||||||
|
])('formats bit value %s %s as %s', (value, unit, expected) => {
|
||||||
|
expect(formatUniversalUnit(value, unit)).toBe(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Bit rate', () => {
|
||||||
|
test.each([
|
||||||
|
// Bits/second
|
||||||
|
[512, UniversalYAxisUnit.BITS_SECOND, '512 b/s'],
|
||||||
|
[1000, UniversalYAxisUnit.BITS_SECOND, '1 kb/s'],
|
||||||
|
[1023, UniversalYAxisUnit.BITS_SECOND, '1.02 kb/s'],
|
||||||
|
// Kilobits/second
|
||||||
|
[0.5, UniversalYAxisUnit.KILOBITS_SECOND, '500 b/s'],
|
||||||
|
[512, UniversalYAxisUnit.KILOBITS_SECOND, '512 kb/s'],
|
||||||
|
[1000, UniversalYAxisUnit.KILOBITS_SECOND, '1 Mb/s'],
|
||||||
|
[1023, UniversalYAxisUnit.KILOBITS_SECOND, '1.02 Mb/s'],
|
||||||
|
// Megabits/second
|
||||||
|
[0.5, UniversalYAxisUnit.MEGABITS_SECOND, '500 kb/s'],
|
||||||
|
[512, UniversalYAxisUnit.MEGABITS_SECOND, '512 Mb/s'],
|
||||||
|
[1000, UniversalYAxisUnit.MEGABITS_SECOND, '1 Gb/s'],
|
||||||
|
[1023, UniversalYAxisUnit.MEGABITS_SECOND, '1.02 Gb/s'],
|
||||||
|
// Gigabits/second
|
||||||
|
[0.5, UniversalYAxisUnit.GIGABITS_SECOND, '500 Mb/s'],
|
||||||
|
[512, UniversalYAxisUnit.GIGABITS_SECOND, '512 Gb/s'],
|
||||||
|
[1000, UniversalYAxisUnit.GIGABITS_SECOND, '1 Tb/s'],
|
||||||
|
[1023, UniversalYAxisUnit.GIGABITS_SECOND, '1.02 Tb/s'],
|
||||||
|
// Terabits/second
|
||||||
|
[0.5, UniversalYAxisUnit.TERABITS_SECOND, '500 Gb/s'],
|
||||||
|
[512, UniversalYAxisUnit.TERABITS_SECOND, '512 Tb/s'],
|
||||||
|
[1000, UniversalYAxisUnit.TERABITS_SECOND, '1 Pb/s'],
|
||||||
|
[1023, UniversalYAxisUnit.TERABITS_SECOND, '1.02 Pb/s'],
|
||||||
|
// Petabits/second
|
||||||
|
[0.5, UniversalYAxisUnit.PETABITS_SECOND, '500 Tb/s'],
|
||||||
|
[512, UniversalYAxisUnit.PETABITS_SECOND, '512 Pb/s'],
|
||||||
|
[1000, UniversalYAxisUnit.PETABITS_SECOND, '1 Eb/s'],
|
||||||
|
[1023, UniversalYAxisUnit.PETABITS_SECOND, '1.02 Eb/s'],
|
||||||
|
// Exabits/second
|
||||||
|
[512, UniversalYAxisUnit.EXABITS_SECOND, '512 Eb/s'],
|
||||||
|
[1000, UniversalYAxisUnit.EXABITS_SECOND, '1 Zb/s'],
|
||||||
|
[1023, UniversalYAxisUnit.EXABITS_SECOND, '1.02 Zb/s'],
|
||||||
|
// Zettabits/second
|
||||||
|
[0.5, UniversalYAxisUnit.ZETTABITS_SECOND, '500 Eb/s'],
|
||||||
|
[512, UniversalYAxisUnit.ZETTABITS_SECOND, '512 Zb/s'],
|
||||||
|
[1000, UniversalYAxisUnit.ZETTABITS_SECOND, '1 Yb/s'],
|
||||||
|
[1023, UniversalYAxisUnit.ZETTABITS_SECOND, '1.02 Yb/s'],
|
||||||
|
// Yottabits/second
|
||||||
|
[0.5, UniversalYAxisUnit.YOTTABITS_SECOND, '500 Zb/s'],
|
||||||
|
[512, UniversalYAxisUnit.YOTTABITS_SECOND, '512 Yb/s'],
|
||||||
|
[1000, UniversalYAxisUnit.YOTTABITS_SECOND, '1000 Yb/s'],
|
||||||
|
[1023, UniversalYAxisUnit.YOTTABITS_SECOND, '1023 Yb/s'],
|
||||||
|
])('formats bit rate value %s %s as %s', (value, unit, expected) => {
|
||||||
|
expect(formatUniversalUnit(value, unit)).toBe(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Count', () => {
|
||||||
|
test.each([
|
||||||
|
[100, UniversalYAxisUnit.COUNT, '100'],
|
||||||
|
[875, UniversalYAxisUnit.COUNT, '875'],
|
||||||
|
[1000, UniversalYAxisUnit.COUNT, '1 K'],
|
||||||
|
[2500, UniversalYAxisUnit.COUNT, '2.5 K'],
|
||||||
|
[10000, UniversalYAxisUnit.COUNT, '10 K'],
|
||||||
|
[25000, UniversalYAxisUnit.COUNT, '25 K'],
|
||||||
|
[100000, UniversalYAxisUnit.COUNT, '100 K'],
|
||||||
|
[1000000, UniversalYAxisUnit.COUNT, '1 Mil'],
|
||||||
|
[10000000, UniversalYAxisUnit.COUNT, '10 Mil'],
|
||||||
|
[100000000, UniversalYAxisUnit.COUNT, '100 Mil'],
|
||||||
|
[1000000000, UniversalYAxisUnit.COUNT, '1 Bil'],
|
||||||
|
[10000000000, UniversalYAxisUnit.COUNT, '10 Bil'],
|
||||||
|
[100000000000, UniversalYAxisUnit.COUNT, '100 Bil'],
|
||||||
|
[1000000000000, UniversalYAxisUnit.COUNT, '1 Tri'],
|
||||||
|
[10000000000000, UniversalYAxisUnit.COUNT, '10 Tri'],
|
||||||
|
])('formats count value %s %s as %s', (value, unit, expected) => {
|
||||||
|
expect(formatUniversalUnit(value, unit)).toBe(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
test.each([
|
||||||
|
[100, UniversalYAxisUnit.COUNT_SECOND, '100 c/s'],
|
||||||
|
[875, UniversalYAxisUnit.COUNT_SECOND, '875 c/s'],
|
||||||
|
[1000, UniversalYAxisUnit.COUNT_SECOND, '1K c/s'],
|
||||||
|
[2500, UniversalYAxisUnit.COUNT_SECOND, '2.5K c/s'],
|
||||||
|
[10000, UniversalYAxisUnit.COUNT_SECOND, '10K c/s'],
|
||||||
|
[25000, UniversalYAxisUnit.COUNT_SECOND, '25K c/s'],
|
||||||
|
])('formats count per time value %s %s as %s', (value, unit, expected) => {
|
||||||
|
expect(formatUniversalUnit(value, unit)).toBe(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
test.each([
|
||||||
|
[100, UniversalYAxisUnit.COUNT_MINUTE, '100 c/m'],
|
||||||
|
[875, UniversalYAxisUnit.COUNT_MINUTE, '875 c/m'],
|
||||||
|
[1000, UniversalYAxisUnit.COUNT_MINUTE, '1K c/m'],
|
||||||
|
[2500, UniversalYAxisUnit.COUNT_MINUTE, '2.5K c/m'],
|
||||||
|
[10000, UniversalYAxisUnit.COUNT_MINUTE, '10K c/m'],
|
||||||
|
[25000, UniversalYAxisUnit.COUNT_MINUTE, '25K c/m'],
|
||||||
|
])('formats count per time value %s %s as %s', (value, unit, expected) => {
|
||||||
|
expect(formatUniversalUnit(value, unit)).toBe(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Operations units', () => {
|
||||||
|
test.each([
|
||||||
|
[780, UniversalYAxisUnit.OPS_SECOND, '780 ops/s'],
|
||||||
|
[1000, UniversalYAxisUnit.OPS_SECOND, '1K ops/s'],
|
||||||
|
[520, UniversalYAxisUnit.OPS_MINUTE, '520 ops/m'],
|
||||||
|
[1000, UniversalYAxisUnit.OPS_MINUTE, '1K ops/m'],
|
||||||
|
[2500, UniversalYAxisUnit.OPS_MINUTE, '2.5K ops/m'],
|
||||||
|
[10000, UniversalYAxisUnit.OPS_MINUTE, '10K ops/m'],
|
||||||
|
[25000, UniversalYAxisUnit.OPS_MINUTE, '25K ops/m'],
|
||||||
|
])(
|
||||||
|
'formats operations per time value %s %s as %s',
|
||||||
|
(value, unit, expected) => {
|
||||||
|
expect(formatUniversalUnit(value, unit)).toBe(expected);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Request units', () => {
|
||||||
|
test.each([
|
||||||
|
[615, UniversalYAxisUnit.REQUESTS_SECOND, '615 req/s'],
|
||||||
|
[1000, UniversalYAxisUnit.REQUESTS_SECOND, '1K req/s'],
|
||||||
|
[480, UniversalYAxisUnit.REQUESTS_MINUTE, '480 req/m'],
|
||||||
|
[1000, UniversalYAxisUnit.REQUESTS_MINUTE, '1K req/m'],
|
||||||
|
[2500, UniversalYAxisUnit.REQUESTS_MINUTE, '2.5K req/m'],
|
||||||
|
[10000, UniversalYAxisUnit.REQUESTS_MINUTE, '10K req/m'],
|
||||||
|
[25000, UniversalYAxisUnit.REQUESTS_MINUTE, '25K req/m'],
|
||||||
|
])('formats requests per time value %s %s as %s', (value, unit, expected) => {
|
||||||
|
expect(formatUniversalUnit(value, unit)).toBe(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Read/Write units', () => {
|
||||||
|
test.each([
|
||||||
|
[505, UniversalYAxisUnit.READS_SECOND, '505 rd/s'],
|
||||||
|
[1000, UniversalYAxisUnit.READS_SECOND, '1K rd/s'],
|
||||||
|
[610, UniversalYAxisUnit.WRITES_SECOND, '610 wr/s'],
|
||||||
|
[1000, UniversalYAxisUnit.WRITES_SECOND, '1K wr/s'],
|
||||||
|
[715, UniversalYAxisUnit.READS_MINUTE, '715 rd/m'],
|
||||||
|
[1000, UniversalYAxisUnit.READS_MINUTE, '1K rd/m'],
|
||||||
|
[2500, UniversalYAxisUnit.READS_MINUTE, '2.5K rd/m'],
|
||||||
|
[10000, UniversalYAxisUnit.READS_MINUTE, '10K rd/m'],
|
||||||
|
[25000, UniversalYAxisUnit.READS_MINUTE, '25K rd/m'],
|
||||||
|
[830, UniversalYAxisUnit.WRITES_MINUTE, '830 wr/m'],
|
||||||
|
[1000, UniversalYAxisUnit.WRITES_MINUTE, '1K wr/m'],
|
||||||
|
[2500, UniversalYAxisUnit.WRITES_MINUTE, '2.5K wr/m'],
|
||||||
|
[10000, UniversalYAxisUnit.WRITES_MINUTE, '10K wr/m'],
|
||||||
|
[25000, UniversalYAxisUnit.WRITES_MINUTE, '25K wr/m'],
|
||||||
|
])(
|
||||||
|
'formats reads and writes per time value %s %s as %s',
|
||||||
|
(value, unit, expected) => {
|
||||||
|
expect(formatUniversalUnit(value, unit)).toBe(expected);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('IO Operations units', () => {
|
||||||
|
test.each([
|
||||||
|
[777, UniversalYAxisUnit.IOOPS_SECOND, '777 io/s'],
|
||||||
|
[1000, UniversalYAxisUnit.IOOPS_SECOND, '1K io/s'],
|
||||||
|
[2500, UniversalYAxisUnit.IOOPS_SECOND, '2.5K io/s'],
|
||||||
|
[10000, UniversalYAxisUnit.IOOPS_SECOND, '10K io/s'],
|
||||||
|
[25000, UniversalYAxisUnit.IOOPS_SECOND, '25K io/s'],
|
||||||
|
])('formats IOPS value %s %s as %s', (value, unit, expected) => {
|
||||||
|
expect(formatUniversalUnit(value, unit)).toBe(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Percent units', () => {
|
||||||
|
it('formats percent as-is', () => {
|
||||||
|
expect(formatUniversalUnit(456, UniversalYAxisUnit.PERCENT)).toBe('456%');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('multiplies percent_unit by 100', () => {
|
||||||
|
expect(formatUniversalUnit(9, UniversalYAxisUnit.PERCENT_UNIT)).toBe('900%');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('None unit', () => {
|
||||||
|
it('formats as plain number', () => {
|
||||||
|
expect(formatUniversalUnit(742, UniversalYAxisUnit.NONE)).toBe('742');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Time (additional)', () => {
|
||||||
|
test.each([
|
||||||
|
[900, UniversalYAxisUnit.DURATION_MS, '900 milliseconds'],
|
||||||
|
[1000, UniversalYAxisUnit.DURATION_MS, '1 second'],
|
||||||
|
[1, UniversalYAxisUnit.DURATION_MS, '1 millisecond'],
|
||||||
|
[900, UniversalYAxisUnit.DURATION_S, '15 minutes'],
|
||||||
|
[1, UniversalYAxisUnit.DURATION_HMS, '00:00:01'],
|
||||||
|
[90005, UniversalYAxisUnit.DURATION_HMS, '25:00:05'],
|
||||||
|
[90005, UniversalYAxisUnit.DURATION_DHMS, '1 d 01:00:05'],
|
||||||
|
[900, UniversalYAxisUnit.TIMETICKS, '9 s'],
|
||||||
|
[1, UniversalYAxisUnit.TIMETICKS, '10 ms'],
|
||||||
|
[900, UniversalYAxisUnit.CLOCK_MS, '900ms'],
|
||||||
|
[1, UniversalYAxisUnit.CLOCK_MS, '001ms'],
|
||||||
|
[1, UniversalYAxisUnit.CLOCK_S, '01s:000ms'],
|
||||||
|
[900, UniversalYAxisUnit.CLOCK_S, '15m:00s:000ms'],
|
||||||
|
[900, UniversalYAxisUnit.TIME_HERTZ, '900 Hz'],
|
||||||
|
[1000, UniversalYAxisUnit.TIME_HERTZ, '1 kHz'],
|
||||||
|
[1000000, UniversalYAxisUnit.TIME_HERTZ, '1 MHz'],
|
||||||
|
[1000000000, UniversalYAxisUnit.TIME_HERTZ, '1 GHz'],
|
||||||
|
[1008, UniversalYAxisUnit.TIME_HERTZ, '1.01 kHz'],
|
||||||
|
])('formats duration value %s %s as %s', (value, unit, expected) => {
|
||||||
|
expect(formatUniversalUnit(value, unit)).toBe(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Data (IEC/Binary)', () => {
|
||||||
|
test.each([
|
||||||
|
// Bytes
|
||||||
|
[900, UniversalYAxisUnit.BYTES_IEC, '900 B'],
|
||||||
|
[1024, UniversalYAxisUnit.BYTES_IEC, '1 KiB'],
|
||||||
|
[1080, UniversalYAxisUnit.BYTES_IEC, '1.05 KiB'],
|
||||||
|
// Kibibytes
|
||||||
|
[900, UniversalYAxisUnit.KIBIBYTES, '900 KiB'],
|
||||||
|
[1024, UniversalYAxisUnit.KIBIBYTES, '1 MiB'],
|
||||||
|
[1080, UniversalYAxisUnit.KIBIBYTES, '1.05 MiB'],
|
||||||
|
// Mebibytes
|
||||||
|
[900, UniversalYAxisUnit.MEBIBYTES, '900 MiB'],
|
||||||
|
[1024, UniversalYAxisUnit.MEBIBYTES, '1 GiB'],
|
||||||
|
[1080, UniversalYAxisUnit.MEBIBYTES, '1.05 GiB'],
|
||||||
|
// Gibibytes
|
||||||
|
[900, UniversalYAxisUnit.GIBIBYTES, '900 GiB'],
|
||||||
|
[1024, UniversalYAxisUnit.GIBIBYTES, '1 TiB'],
|
||||||
|
[1080, UniversalYAxisUnit.GIBIBYTES, '1.05 TiB'],
|
||||||
|
// Tebibytes
|
||||||
|
[900, UniversalYAxisUnit.TEBIBYTES, '900 TiB'],
|
||||||
|
[1024, UniversalYAxisUnit.TEBIBYTES, '1 PiB'],
|
||||||
|
[1080, UniversalYAxisUnit.TEBIBYTES, '1.05 PiB'],
|
||||||
|
// Pebibytes
|
||||||
|
[900, UniversalYAxisUnit.PEBIBYTES, '900 PiB'],
|
||||||
|
[1024, UniversalYAxisUnit.PEBIBYTES, '1 EiB'],
|
||||||
|
[1080, UniversalYAxisUnit.PEBIBYTES, '1.05 EiB'],
|
||||||
|
// Exbibytes
|
||||||
|
[900, UniversalYAxisUnit.EXBIBYTES, '900 EiB'],
|
||||||
|
[1024, UniversalYAxisUnit.EXBIBYTES, '1 ZiB'],
|
||||||
|
[1080, UniversalYAxisUnit.EXBIBYTES, '1.05 ZiB'],
|
||||||
|
// Zebibytes
|
||||||
|
[900, UniversalYAxisUnit.ZEBIBYTES, '900 ZiB'],
|
||||||
|
[1024, UniversalYAxisUnit.ZEBIBYTES, '1 YiB'],
|
||||||
|
[1080, UniversalYAxisUnit.ZEBIBYTES, '1.05 YiB'],
|
||||||
|
// Yobibytes
|
||||||
|
[900, UniversalYAxisUnit.YOBIBYTES, '900 YiB'],
|
||||||
|
[1024, UniversalYAxisUnit.YOBIBYTES, '1024 YiB'],
|
||||||
|
])('formats IEC bytes value %s %s as %s', (value, unit, expected) => {
|
||||||
|
expect(formatUniversalUnit(value, unit)).toBe(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Data Rate (IEC/Binary)', () => {
|
||||||
|
test.each([
|
||||||
|
// Kibibytes/second
|
||||||
|
[900, UniversalYAxisUnit.KIBIBYTES_SECOND, '900 KiB/s'],
|
||||||
|
[1024, UniversalYAxisUnit.KIBIBYTES_SECOND, '1 MiB/s'],
|
||||||
|
[1080, UniversalYAxisUnit.KIBIBYTES_SECOND, '1.05 MiB/s'],
|
||||||
|
// Mebibytes/second
|
||||||
|
[900, UniversalYAxisUnit.MEBIBYTES_SECOND, '900 MiB/s'],
|
||||||
|
[1024, UniversalYAxisUnit.MEBIBYTES_SECOND, '1 GiB/s'],
|
||||||
|
[1080, UniversalYAxisUnit.MEBIBYTES_SECOND, '1.05 GiB/s'],
|
||||||
|
// Gibibytes/second
|
||||||
|
[900, UniversalYAxisUnit.GIBIBYTES_SECOND, '900 GiB/s'],
|
||||||
|
[1024, UniversalYAxisUnit.GIBIBYTES_SECOND, '1 TiB/s'],
|
||||||
|
[1080, UniversalYAxisUnit.GIBIBYTES_SECOND, '1.05 TiB/s'],
|
||||||
|
// Tebibytes/second
|
||||||
|
[900, UniversalYAxisUnit.TEBIBYTES_SECOND, '900 TiB/s'],
|
||||||
|
[1024, UniversalYAxisUnit.TEBIBYTES_SECOND, '1 PiB/s'],
|
||||||
|
[1080, UniversalYAxisUnit.TEBIBYTES_SECOND, '1.05 PiB/s'],
|
||||||
|
// Pebibytes/second
|
||||||
|
[900, UniversalYAxisUnit.PEBIBYTES_SECOND, '900 PiB/s'],
|
||||||
|
[1024, UniversalYAxisUnit.PEBIBYTES_SECOND, '1 EiB/s'],
|
||||||
|
[1080, UniversalYAxisUnit.PEBIBYTES_SECOND, '1.05 EiB/s'],
|
||||||
|
// Exbibytes/second
|
||||||
|
[900, UniversalYAxisUnit.EXBIBYTES_SECOND, '900 EiB/s'],
|
||||||
|
[1024, UniversalYAxisUnit.EXBIBYTES_SECOND, '1 ZiB/s'],
|
||||||
|
[1080, UniversalYAxisUnit.EXBIBYTES_SECOND, '1.05 ZiB/s'],
|
||||||
|
// Zebibytes/second
|
||||||
|
[900, UniversalYAxisUnit.ZEBIBYTES_SECOND, '900 ZiB/s'],
|
||||||
|
[1024, UniversalYAxisUnit.ZEBIBYTES_SECOND, '1 YiB/s'],
|
||||||
|
[1080, UniversalYAxisUnit.ZEBIBYTES_SECOND, '1.05 YiB/s'],
|
||||||
|
// Yobibytes/second
|
||||||
|
[900, UniversalYAxisUnit.YOBIBYTES_SECOND, '900 YiB/s'],
|
||||||
|
[1024, UniversalYAxisUnit.YOBIBYTES_SECOND, '1024 YiB/s'],
|
||||||
|
[1080, UniversalYAxisUnit.YOBIBYTES_SECOND, '1080 YiB/s'],
|
||||||
|
// Packets/second
|
||||||
|
[900, UniversalYAxisUnit.DATA_RATE_PACKETS_PER_SECOND, '900 p/s'],
|
||||||
|
[1000, UniversalYAxisUnit.DATA_RATE_PACKETS_PER_SECOND, '1 kp/s'],
|
||||||
|
[1080, UniversalYAxisUnit.DATA_RATE_PACKETS_PER_SECOND, '1.08 kp/s'],
|
||||||
|
])('formats IEC byte rates value %s %s as %s', (value, unit, expected) => {
|
||||||
|
expect(formatUniversalUnit(value, unit)).toBe(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Bits (IEC)', () => {
|
||||||
|
test.each([
|
||||||
|
[900, UniversalYAxisUnit.BITS_IEC, '900 b'],
|
||||||
|
[1024, UniversalYAxisUnit.BITS_IEC, '1 Kib'],
|
||||||
|
[1080, UniversalYAxisUnit.BITS_IEC, '1.05 Kib'],
|
||||||
|
])('formats IEC bits value %s %s as %s', (value, unit, expected) => {
|
||||||
|
expect(formatUniversalUnit(value, unit)).toBe(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Hash Rate', () => {
|
||||||
|
test.each([
|
||||||
|
// Hashes/second
|
||||||
|
[412, UniversalYAxisUnit.HASH_RATE_HASHES_PER_SECOND, '412 H/s'],
|
||||||
|
[1000, UniversalYAxisUnit.HASH_RATE_HASHES_PER_SECOND, '1 kH/s'],
|
||||||
|
[1023, UniversalYAxisUnit.HASH_RATE_HASHES_PER_SECOND, '1.02 kH/s'],
|
||||||
|
// Kilohashes/second
|
||||||
|
[412, UniversalYAxisUnit.HASH_RATE_KILOHASHES_PER_SECOND, '412 kH/s'],
|
||||||
|
[1000, UniversalYAxisUnit.HASH_RATE_KILOHASHES_PER_SECOND, '1 MH/s'],
|
||||||
|
[1023, UniversalYAxisUnit.HASH_RATE_KILOHASHES_PER_SECOND, '1.02 MH/s'],
|
||||||
|
// Megahashes/second
|
||||||
|
[412, UniversalYAxisUnit.HASH_RATE_MEGAHASHES_PER_SECOND, '412 MH/s'],
|
||||||
|
[1000, UniversalYAxisUnit.HASH_RATE_MEGAHASHES_PER_SECOND, '1 GH/s'],
|
||||||
|
[1023, UniversalYAxisUnit.HASH_RATE_MEGAHASHES_PER_SECOND, '1.02 GH/s'],
|
||||||
|
// Gigahashes/second
|
||||||
|
[412, UniversalYAxisUnit.HASH_RATE_GIGAHASHES_PER_SECOND, '412 GH/s'],
|
||||||
|
[1000, UniversalYAxisUnit.HASH_RATE_GIGAHASHES_PER_SECOND, '1 TH/s'],
|
||||||
|
[1023, UniversalYAxisUnit.HASH_RATE_GIGAHASHES_PER_SECOND, '1.02 TH/s'],
|
||||||
|
// Terahashes/second
|
||||||
|
[412, UniversalYAxisUnit.HASH_RATE_TERAHASHES_PER_SECOND, '412 TH/s'],
|
||||||
|
[1000, UniversalYAxisUnit.HASH_RATE_TERAHASHES_PER_SECOND, '1 PH/s'],
|
||||||
|
[1023, UniversalYAxisUnit.HASH_RATE_TERAHASHES_PER_SECOND, '1.02 PH/s'],
|
||||||
|
// Petahashes/second
|
||||||
|
[412, UniversalYAxisUnit.HASH_RATE_PETAHASHES_PER_SECOND, '412 PH/s'],
|
||||||
|
[1000, UniversalYAxisUnit.HASH_RATE_PETAHASHES_PER_SECOND, '1 EH/s'],
|
||||||
|
[1023, UniversalYAxisUnit.HASH_RATE_PETAHASHES_PER_SECOND, '1.02 EH/s'],
|
||||||
|
// Exahashes/second
|
||||||
|
[412, UniversalYAxisUnit.HASH_RATE_EXAHASHES_PER_SECOND, '412 EH/s'],
|
||||||
|
[1000, UniversalYAxisUnit.HASH_RATE_EXAHASHES_PER_SECOND, '1 ZH/s'],
|
||||||
|
[1023, UniversalYAxisUnit.HASH_RATE_EXAHASHES_PER_SECOND, '1.02 ZH/s'],
|
||||||
|
])('formats hash rate value %s %s as %s', (value, unit, expected) => {
|
||||||
|
expect(formatUniversalUnit(value, unit)).toBe(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Miscellaneous', () => {
|
||||||
|
test.each([
|
||||||
|
[742, UniversalYAxisUnit.MISC_STRING, '742'],
|
||||||
|
[688, UniversalYAxisUnit.MISC_SHORT, '688'],
|
||||||
|
[555, UniversalYAxisUnit.MISC_HUMIDITY, '555 %H'],
|
||||||
|
[812, UniversalYAxisUnit.MISC_DECIBEL, '812 dB'],
|
||||||
|
[1024, UniversalYAxisUnit.MISC_HEXADECIMAL, '400'],
|
||||||
|
[1024, UniversalYAxisUnit.MISC_HEXADECIMAL_0X, '0x400'],
|
||||||
|
[900, UniversalYAxisUnit.MISC_SCIENTIFIC_NOTATION, '9e+2'],
|
||||||
|
[678, UniversalYAxisUnit.MISC_LOCALE_FORMAT, '678'],
|
||||||
|
[444, UniversalYAxisUnit.MISC_PIXELS, '444 px'],
|
||||||
|
])('formats miscellaneous value %s %s as %s', (value, unit, expected) => {
|
||||||
|
expect(formatUniversalUnit(value, unit)).toBe(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Acceleration', () => {
|
||||||
|
test.each([
|
||||||
|
[
|
||||||
|
875,
|
||||||
|
UniversalYAxisUnit.ACCELERATION_METERS_PER_SECOND_SQUARED,
|
||||||
|
'875 m/sec²',
|
||||||
|
],
|
||||||
|
[640, UniversalYAxisUnit.ACCELERATION_FEET_PER_SECOND_SQUARED, '640 f/sec²'],
|
||||||
|
[512, UniversalYAxisUnit.ACCELERATION_G_UNIT, '512 g'],
|
||||||
|
[
|
||||||
|
2500,
|
||||||
|
UniversalYAxisUnit.ACCELERATION_METERS_PER_SECOND_SQUARED,
|
||||||
|
'2500 m/sec²',
|
||||||
|
],
|
||||||
|
])('formats acceleration value %s %s as %s', (value, unit, expected) => {
|
||||||
|
expect(formatUniversalUnit(value, unit)).toBe(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Angular', () => {
|
||||||
|
test.each([
|
||||||
|
[415, UniversalYAxisUnit.ANGULAR_DEGREE, '415 °'],
|
||||||
|
[732, UniversalYAxisUnit.ANGULAR_RADIAN, '732 rad'],
|
||||||
|
[128, UniversalYAxisUnit.ANGULAR_GRADIAN, '128 grad'],
|
||||||
|
[560, UniversalYAxisUnit.ANGULAR_ARC_MINUTE, '560 arcmin'],
|
||||||
|
[945, UniversalYAxisUnit.ANGULAR_ARC_SECOND, '945 arcsec'],
|
||||||
|
])('formats angular value %s %s as %s', (value, unit, expected) => {
|
||||||
|
expect(formatUniversalUnit(value, unit)).toBe(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Area', () => {
|
||||||
|
test.each([
|
||||||
|
[210, UniversalYAxisUnit.AREA_SQUARE_METERS, '210 m²'],
|
||||||
|
[152, UniversalYAxisUnit.AREA_SQUARE_FEET, '152 ft²'],
|
||||||
|
[64, UniversalYAxisUnit.AREA_SQUARE_MILES, '64 mi²'],
|
||||||
|
])('formats area value %s %s as %s', (value, unit, expected) => {
|
||||||
|
expect(formatUniversalUnit(value, unit)).toBe(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('FLOPs', () => {
|
||||||
|
test.each([
|
||||||
|
// FLOPS
|
||||||
|
[150, UniversalYAxisUnit.FLOPS_FLOPS, '150 FLOPS'],
|
||||||
|
[1000, UniversalYAxisUnit.FLOPS_FLOPS, '1 kFLOPS'],
|
||||||
|
[1080, UniversalYAxisUnit.FLOPS_FLOPS, '1.08 kFLOPS'],
|
||||||
|
// MFLOPS
|
||||||
|
[275, UniversalYAxisUnit.FLOPS_MFLOPS, '275 MFLOPS'],
|
||||||
|
[1000, UniversalYAxisUnit.FLOPS_MFLOPS, '1 GFLOPS'],
|
||||||
|
[1080, UniversalYAxisUnit.FLOPS_MFLOPS, '1.08 GFLOPS'],
|
||||||
|
// GFLOPS
|
||||||
|
[640, UniversalYAxisUnit.FLOPS_GFLOPS, '640 GFLOPS'],
|
||||||
|
[1000, UniversalYAxisUnit.FLOPS_GFLOPS, '1 TFLOPS'],
|
||||||
|
[1080, UniversalYAxisUnit.FLOPS_GFLOPS, '1.08 TFLOPS'],
|
||||||
|
// TFLOPS
|
||||||
|
[875, UniversalYAxisUnit.FLOPS_TFLOPS, '875 TFLOPS'],
|
||||||
|
[1000, UniversalYAxisUnit.FLOPS_TFLOPS, '1 PFLOPS'],
|
||||||
|
[1080, UniversalYAxisUnit.FLOPS_TFLOPS, '1.08 PFLOPS'],
|
||||||
|
// PFLOPS
|
||||||
|
[430, UniversalYAxisUnit.FLOPS_PFLOPS, '430 PFLOPS'],
|
||||||
|
[1000, UniversalYAxisUnit.FLOPS_PFLOPS, '1 EFLOPS'],
|
||||||
|
[1080, UniversalYAxisUnit.FLOPS_PFLOPS, '1.08 EFLOPS'],
|
||||||
|
// EFLOPS
|
||||||
|
[590, UniversalYAxisUnit.FLOPS_EFLOPS, '590 EFLOPS'],
|
||||||
|
[1000, UniversalYAxisUnit.FLOPS_EFLOPS, '1 ZFLOPS'],
|
||||||
|
[1080, UniversalYAxisUnit.FLOPS_EFLOPS, '1.08 ZFLOPS'],
|
||||||
|
// ZFLOPS
|
||||||
|
[715, UniversalYAxisUnit.FLOPS_ZFLOPS, '715 ZFLOPS'],
|
||||||
|
[1000, UniversalYAxisUnit.FLOPS_ZFLOPS, '1 YFLOPS'],
|
||||||
|
[1080, UniversalYAxisUnit.FLOPS_ZFLOPS, '1.08 YFLOPS'],
|
||||||
|
// YFLOPS
|
||||||
|
[840, UniversalYAxisUnit.FLOPS_YFLOPS, '840 YFLOPS'],
|
||||||
|
[1000, UniversalYAxisUnit.FLOPS_YFLOPS, '1000 YFLOPS'],
|
||||||
|
])('formats FLOPs value %s %s as %s', (value, unit, expected) => {
|
||||||
|
expect(formatUniversalUnit(value, unit)).toBe(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Concentration', () => {
|
||||||
|
test.each([
|
||||||
|
[415, UniversalYAxisUnit.CONCENTRATION_PPM, '415 ppm'],
|
||||||
|
[1000, UniversalYAxisUnit.CONCENTRATION_PPM, '1000 ppm'],
|
||||||
|
[732, UniversalYAxisUnit.CONCENTRATION_PPB, '732 ppb'],
|
||||||
|
[1000, UniversalYAxisUnit.CONCENTRATION_PPB, '1000 ppb'],
|
||||||
|
[128, UniversalYAxisUnit.CONCENTRATION_NG_M3, '128 ng/m³'],
|
||||||
|
[1000, UniversalYAxisUnit.CONCENTRATION_NG_M3, '1000 ng/m³'],
|
||||||
|
[560, UniversalYAxisUnit.CONCENTRATION_NG_NORMAL_CUBIC_METER, '560 ng/Nm³'],
|
||||||
|
[
|
||||||
|
1000,
|
||||||
|
UniversalYAxisUnit.CONCENTRATION_NG_NORMAL_CUBIC_METER,
|
||||||
|
'1000 ng/Nm³',
|
||||||
|
],
|
||||||
|
[945, UniversalYAxisUnit.CONCENTRATION_UG_M3, '945 μg/m³'],
|
||||||
|
[1000, UniversalYAxisUnit.CONCENTRATION_UG_M3, '1000 μg/m³'],
|
||||||
|
[210, UniversalYAxisUnit.CONCENTRATION_UG_NORMAL_CUBIC_METER, '210 μg/Nm³'],
|
||||||
|
[
|
||||||
|
1000,
|
||||||
|
UniversalYAxisUnit.CONCENTRATION_UG_NORMAL_CUBIC_METER,
|
||||||
|
'1000 μg/Nm³',
|
||||||
|
],
|
||||||
|
[152, UniversalYAxisUnit.CONCENTRATION_MG_M3, '152 mg/m³'],
|
||||||
|
[64, UniversalYAxisUnit.CONCENTRATION_MG_NORMAL_CUBIC_METER, '64 mg/Nm³'],
|
||||||
|
[508, UniversalYAxisUnit.CONCENTRATION_G_M3, '508 g/m³'],
|
||||||
|
[1000, UniversalYAxisUnit.CONCENTRATION_G_M3, '1000 g/m³'],
|
||||||
|
[377, UniversalYAxisUnit.CONCENTRATION_G_NORMAL_CUBIC_METER, '377 g/Nm³'],
|
||||||
|
[1000, UniversalYAxisUnit.CONCENTRATION_G_NORMAL_CUBIC_METER, '1000 g/Nm³'],
|
||||||
|
[286, UniversalYAxisUnit.CONCENTRATION_MG_PER_DL, '286 mg/dL'],
|
||||||
|
[1000, UniversalYAxisUnit.CONCENTRATION_MG_PER_DL, '1000 mg/dL'],
|
||||||
|
[675, UniversalYAxisUnit.CONCENTRATION_MMOL_PER_L, '675 mmol/L'],
|
||||||
|
[1000, UniversalYAxisUnit.CONCENTRATION_MMOL_PER_L, '1000 mmol/L'],
|
||||||
|
])('formats concentration value %s %s as %s', (value, unit, expected) => {
|
||||||
|
expect(formatUniversalUnit(value, unit)).toBe(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Currency', () => {
|
||||||
|
test.each([
|
||||||
|
[812, UniversalYAxisUnit.CURRENCY_USD, '$812'],
|
||||||
|
[645, UniversalYAxisUnit.CURRENCY_GBP, '£645'],
|
||||||
|
[731, UniversalYAxisUnit.CURRENCY_EUR, '€731'],
|
||||||
|
[508, UniversalYAxisUnit.CURRENCY_JPY, '¥508'],
|
||||||
|
[963, UniversalYAxisUnit.CURRENCY_RUB, '₽963'],
|
||||||
|
[447, UniversalYAxisUnit.CURRENCY_UAH, '₴447'],
|
||||||
|
[592, UniversalYAxisUnit.CURRENCY_BRL, 'R$592'],
|
||||||
|
[375, UniversalYAxisUnit.CURRENCY_DKK, '375kr'],
|
||||||
|
[418, UniversalYAxisUnit.CURRENCY_ISK, '418kr'],
|
||||||
|
[536, UniversalYAxisUnit.CURRENCY_NOK, '536kr'],
|
||||||
|
[689, UniversalYAxisUnit.CURRENCY_SEK, '689kr'],
|
||||||
|
[724, UniversalYAxisUnit.CURRENCY_CZK, 'czk724'],
|
||||||
|
[381, UniversalYAxisUnit.CURRENCY_CHF, 'CHF381'],
|
||||||
|
[267, UniversalYAxisUnit.CURRENCY_PLN, 'PLN267'],
|
||||||
|
[154, UniversalYAxisUnit.CURRENCY_BTC, '฿154'],
|
||||||
|
[999, UniversalYAxisUnit.CURRENCY_MBTC, 'mBTC999'],
|
||||||
|
[423, UniversalYAxisUnit.CURRENCY_UBTC, 'μBTC423'],
|
||||||
|
[611, UniversalYAxisUnit.CURRENCY_ZAR, 'R611'],
|
||||||
|
[782, UniversalYAxisUnit.CURRENCY_INR, '₹782'],
|
||||||
|
[834, UniversalYAxisUnit.CURRENCY_KRW, '₩834'],
|
||||||
|
[455, UniversalYAxisUnit.CURRENCY_IDR, 'Rp455'],
|
||||||
|
[978, UniversalYAxisUnit.CURRENCY_PHP, 'PHP978'],
|
||||||
|
[366, UniversalYAxisUnit.CURRENCY_VND, '366đ'],
|
||||||
|
])('formats currency value %s %s as %s', (value, unit, expected) => {
|
||||||
|
expect(formatUniversalUnit(value, unit)).toBe(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Datetime', () => {
|
||||||
|
it('formats datetime units', () => {
|
||||||
|
expect(formatUniversalUnit(900, UniversalYAxisUnit.DATETIME_FROM_NOW)).toBe(
|
||||||
|
'56 years ago',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Power/Electrical', () => {
|
||||||
|
test.each([
|
||||||
|
[715, UniversalYAxisUnit.POWER_WATT, '715 W'],
|
||||||
|
[1000, UniversalYAxisUnit.POWER_WATT, '1 kW'],
|
||||||
|
[1080, UniversalYAxisUnit.POWER_WATT, '1.08 kW'],
|
||||||
|
[438, UniversalYAxisUnit.POWER_KILOWATT, '438 kW'],
|
||||||
|
[1000, UniversalYAxisUnit.POWER_KILOWATT, '1 MW'],
|
||||||
|
[1080, UniversalYAxisUnit.POWER_KILOWATT, '1.08 MW'],
|
||||||
|
[582, UniversalYAxisUnit.POWER_MEGAWATT, '582 MW'],
|
||||||
|
[1000, UniversalYAxisUnit.POWER_MEGAWATT, '1 GW'],
|
||||||
|
[1080, UniversalYAxisUnit.POWER_MEGAWATT, '1.08 GW'],
|
||||||
|
[267, UniversalYAxisUnit.POWER_GIGAWATT, '267 GW'],
|
||||||
|
[853, UniversalYAxisUnit.POWER_MILLIWATT, '853 mW'],
|
||||||
|
[693, UniversalYAxisUnit.POWER_WATT_PER_SQUARE_METER, '693 W/m²'],
|
||||||
|
[544, UniversalYAxisUnit.POWER_VOLT_AMPERE, '544 VA'],
|
||||||
|
[812, UniversalYAxisUnit.POWER_KILOVOLT_AMPERE, '812 kVA'],
|
||||||
|
[478, UniversalYAxisUnit.POWER_VOLT_AMPERE_REACTIVE, '478 VAr'],
|
||||||
|
[365, UniversalYAxisUnit.POWER_KILOVOLT_AMPERE_REACTIVE, '365 kVAr'],
|
||||||
|
[629, UniversalYAxisUnit.POWER_WATT_HOUR, '629 Wh'],
|
||||||
|
[471, UniversalYAxisUnit.POWER_WATT_HOUR_PER_KG, '471 Wh/kg'],
|
||||||
|
[557, UniversalYAxisUnit.POWER_KILOWATT_HOUR, '557 kWh'],
|
||||||
|
[389, UniversalYAxisUnit.POWER_KILOWATT_MINUTE, '389 kW-Min'],
|
||||||
|
[642, UniversalYAxisUnit.POWER_AMPERE_HOUR, '642 Ah'],
|
||||||
|
[731, UniversalYAxisUnit.POWER_KILOAMPERE_HOUR, '731 kAh'],
|
||||||
|
[815, UniversalYAxisUnit.POWER_MILLIAMPERE_HOUR, '815 mAh'],
|
||||||
|
[963, UniversalYAxisUnit.POWER_JOULE, '963 J'],
|
||||||
|
[506, UniversalYAxisUnit.POWER_ELECTRON_VOLT, '506 eV'],
|
||||||
|
[298, UniversalYAxisUnit.POWER_AMPERE, '298 A'],
|
||||||
|
[654, UniversalYAxisUnit.POWER_KILOAMPERE, '654 kA'],
|
||||||
|
[187, UniversalYAxisUnit.POWER_MILLIAMPERE, '187 mA'],
|
||||||
|
[472, UniversalYAxisUnit.POWER_VOLT, '472 V'],
|
||||||
|
[538, UniversalYAxisUnit.POWER_KILOVOLT, '538 kV'],
|
||||||
|
[226, UniversalYAxisUnit.POWER_MILLIVOLT, '226 mV'],
|
||||||
|
[592, UniversalYAxisUnit.POWER_DECIBEL_MILLIWATT, '592 dBm'],
|
||||||
|
[333, UniversalYAxisUnit.POWER_OHM, '333 Ω'],
|
||||||
|
[447, UniversalYAxisUnit.POWER_KILOOHM, '447 kΩ'],
|
||||||
|
[781, UniversalYAxisUnit.POWER_MEGAOHM, '781 MΩ'],
|
||||||
|
[650, UniversalYAxisUnit.POWER_FARAD, '650 F'],
|
||||||
|
[512, UniversalYAxisUnit.POWER_MICROFARAD, '512 µF'],
|
||||||
|
[478, UniversalYAxisUnit.POWER_NANOFARAD, '478 nF'],
|
||||||
|
[341, UniversalYAxisUnit.POWER_PICOFARAD, '341 pF'],
|
||||||
|
[129, UniversalYAxisUnit.POWER_FEMTOFARAD, '129 fF'],
|
||||||
|
[904, UniversalYAxisUnit.POWER_HENRY, '904 H'],
|
||||||
|
[1000, UniversalYAxisUnit.POWER_HENRY, '1 kH'],
|
||||||
|
[275, UniversalYAxisUnit.POWER_MILLIHENRY, '275 mH'],
|
||||||
|
[618, UniversalYAxisUnit.POWER_MICROHENRY, '618 µH'],
|
||||||
|
[1000, UniversalYAxisUnit.POWER_MICROHENRY, '1 mH'],
|
||||||
|
[1080, UniversalYAxisUnit.POWER_MICROHENRY, '1.08 mH'],
|
||||||
|
[459, UniversalYAxisUnit.POWER_LUMENS, '459 Lm'],
|
||||||
|
[1000, UniversalYAxisUnit.POWER_LUMENS, '1 kLm'],
|
||||||
|
[1080, UniversalYAxisUnit.POWER_LUMENS, '1.08 kLm'],
|
||||||
|
])('formats power value %s %s as %s', (value, unit, expected) => {
|
||||||
|
expect(formatUniversalUnit(value, unit)).toBe(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Flow', () => {
|
||||||
|
test.each([
|
||||||
|
[512, UniversalYAxisUnit.FLOW_GALLONS_PER_MINUTE, '512 gpm'],
|
||||||
|
[1000, UniversalYAxisUnit.FLOW_GALLONS_PER_MINUTE, '1000 gpm'],
|
||||||
|
[678, UniversalYAxisUnit.FLOW_CUBIC_METERS_PER_SECOND, '678 cms'],
|
||||||
|
[1000, UniversalYAxisUnit.FLOW_CUBIC_METERS_PER_SECOND, '1000 cms'],
|
||||||
|
[245, UniversalYAxisUnit.FLOW_CUBIC_FEET_PER_SECOND, '245 cfs'],
|
||||||
|
[389, UniversalYAxisUnit.FLOW_CUBIC_FEET_PER_MINUTE, '389 cfm'],
|
||||||
|
[1000, UniversalYAxisUnit.FLOW_CUBIC_FEET_PER_MINUTE, '1000 cfm'],
|
||||||
|
[731, UniversalYAxisUnit.FLOW_LITERS_PER_HOUR, '731 L/h'],
|
||||||
|
[1000, UniversalYAxisUnit.FLOW_LITERS_PER_HOUR, '1000 L/h'],
|
||||||
|
[864, UniversalYAxisUnit.FLOW_LITERS_PER_MINUTE, '864 L/min'],
|
||||||
|
[1000, UniversalYAxisUnit.FLOW_LITERS_PER_MINUTE, '1000 L/min'],
|
||||||
|
[150, UniversalYAxisUnit.FLOW_MILLILITERS_PER_MINUTE, '150 mL/min'],
|
||||||
|
[1000, UniversalYAxisUnit.FLOW_MILLILITERS_PER_MINUTE, '1000 mL/min'],
|
||||||
|
[947, UniversalYAxisUnit.FLOW_LUX, '947 lux'],
|
||||||
|
[1000, UniversalYAxisUnit.FLOW_LUX, '1000 lux'],
|
||||||
|
])('formats flow value %s %s as %s', (value, unit, expected) => {
|
||||||
|
expect(formatUniversalUnit(value, unit)).toBe(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Force', () => {
|
||||||
|
test.each([
|
||||||
|
[845, UniversalYAxisUnit.FORCE_NEWTON_METERS, '845 Nm'],
|
||||||
|
[1000, UniversalYAxisUnit.FORCE_NEWTON_METERS, '1 kNm'],
|
||||||
|
[1080, UniversalYAxisUnit.FORCE_NEWTON_METERS, '1.08 kNm'],
|
||||||
|
[268, UniversalYAxisUnit.FORCE_KILONEWTON_METERS, '268 kNm'],
|
||||||
|
[1000, UniversalYAxisUnit.FORCE_KILONEWTON_METERS, '1 MNm'],
|
||||||
|
[1080, UniversalYAxisUnit.FORCE_KILONEWTON_METERS, '1.08 MNm'],
|
||||||
|
[593, UniversalYAxisUnit.FORCE_NEWTONS, '593 N'],
|
||||||
|
[1000, UniversalYAxisUnit.FORCE_KILONEWTONS, '1 MN'],
|
||||||
|
[1080, UniversalYAxisUnit.FORCE_KILONEWTONS, '1.08 MN'],
|
||||||
|
])('formats force value %s %s as %s', (value, unit, expected) => {
|
||||||
|
expect(formatUniversalUnit(value, unit)).toBe(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Mass', () => {
|
||||||
|
test.each([
|
||||||
|
[120, UniversalYAxisUnit.MASS_MILLIGRAM, '120 mg'],
|
||||||
|
[120000, UniversalYAxisUnit.MASS_MILLIGRAM, '120 g'],
|
||||||
|
[987, UniversalYAxisUnit.MASS_GRAM, '987 g'],
|
||||||
|
[1020, UniversalYAxisUnit.MASS_GRAM, '1.02 kg'],
|
||||||
|
[456, UniversalYAxisUnit.MASS_POUND, '456 lb'],
|
||||||
|
[321, UniversalYAxisUnit.MASS_KILOGRAM, '321 kg'],
|
||||||
|
[654, UniversalYAxisUnit.MASS_METRIC_TON, '654 t'],
|
||||||
|
])('formats mass value %s %s as %s', (value, unit, expected) => {
|
||||||
|
expect(formatUniversalUnit(value, unit)).toBe(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Length', () => {
|
||||||
|
test.each([
|
||||||
|
[88, UniversalYAxisUnit.LENGTH_MILLIMETER, '88 mm'],
|
||||||
|
[100, UniversalYAxisUnit.LENGTH_MILLIMETER, '100 mm'],
|
||||||
|
[1000, UniversalYAxisUnit.LENGTH_MILLIMETER, '1 m'],
|
||||||
|
[177, UniversalYAxisUnit.LENGTH_INCH, '177 in'],
|
||||||
|
[266, UniversalYAxisUnit.LENGTH_FOOT, '266 ft'],
|
||||||
|
[355, UniversalYAxisUnit.LENGTH_METER, '355 m'],
|
||||||
|
[355000, UniversalYAxisUnit.LENGTH_METER, '355 km'],
|
||||||
|
[444, UniversalYAxisUnit.LENGTH_KILOMETER, '444 km'],
|
||||||
|
[533, UniversalYAxisUnit.LENGTH_MILE, '533 mi'],
|
||||||
|
])('formats length value %s %s as %s', (value, unit, expected) => {
|
||||||
|
expect(formatUniversalUnit(value, unit)).toBe(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Pressure', () => {
|
||||||
|
test.each([
|
||||||
|
[45, UniversalYAxisUnit.PRESSURE_MILLIBAR, '45 mbar'],
|
||||||
|
[1013, UniversalYAxisUnit.PRESSURE_MILLIBAR, '1.01 bar'],
|
||||||
|
[27, UniversalYAxisUnit.PRESSURE_BAR, '27 bar'],
|
||||||
|
[62, UniversalYAxisUnit.PRESSURE_KILOBAR, '62 kbar'],
|
||||||
|
[845, UniversalYAxisUnit.PRESSURE_PASCAL, '845 Pa'],
|
||||||
|
[540, UniversalYAxisUnit.PRESSURE_HECTOPASCAL, '540 hPa'],
|
||||||
|
[378, UniversalYAxisUnit.PRESSURE_KILOPASCAL, '378 kPa'],
|
||||||
|
[29, UniversalYAxisUnit.PRESSURE_INCHES_HG, '29 "Hg'],
|
||||||
|
[65, UniversalYAxisUnit.PRESSURE_PSI, '65psi'],
|
||||||
|
])('formats pressure value %s %s as %s', (value, unit, expected) => {
|
||||||
|
expect(formatUniversalUnit(value, unit)).toBe(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Radiation', () => {
|
||||||
|
test.each([
|
||||||
|
[452, UniversalYAxisUnit.RADIATION_BECQUEREL, '452 Bq'],
|
||||||
|
[37, UniversalYAxisUnit.RADIATION_CURIE, '37 Ci'],
|
||||||
|
[128, UniversalYAxisUnit.RADIATION_GRAY, '128 Gy'],
|
||||||
|
[512, UniversalYAxisUnit.RADIATION_RAD, '512 rad'],
|
||||||
|
[256, UniversalYAxisUnit.RADIATION_SIEVERT, '256 Sv'],
|
||||||
|
[640, UniversalYAxisUnit.RADIATION_MILLISIEVERT, '640 mSv'],
|
||||||
|
[875, UniversalYAxisUnit.RADIATION_MICROSIEVERT, '875 µSv'],
|
||||||
|
[875000, UniversalYAxisUnit.RADIATION_MICROSIEVERT, '875 mSv'],
|
||||||
|
[92, UniversalYAxisUnit.RADIATION_REM, '92 rem'],
|
||||||
|
[715, UniversalYAxisUnit.RADIATION_EXPOSURE_C_PER_KG, '715 C/kg'],
|
||||||
|
[833, UniversalYAxisUnit.RADIATION_ROENTGEN, '833 R'],
|
||||||
|
[468, UniversalYAxisUnit.RADIATION_SIEVERT_PER_HOUR, '468 Sv/h'],
|
||||||
|
[590, UniversalYAxisUnit.RADIATION_MILLISIEVERT_PER_HOUR, '590 mSv/h'],
|
||||||
|
[712, UniversalYAxisUnit.RADIATION_MICROSIEVERT_PER_HOUR, '712 µSv/h'],
|
||||||
|
])('formats radiation value %s %s as %s', (value, unit, expected) => {
|
||||||
|
expect(formatUniversalUnit(value, unit)).toBe(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Rotation Speed', () => {
|
||||||
|
test.each([
|
||||||
|
[345, UniversalYAxisUnit.ROTATION_SPEED_REVOLUTIONS_PER_MINUTE, '345 rpm'],
|
||||||
|
[789, UniversalYAxisUnit.ROTATION_SPEED_HERTZ, '789 Hz'],
|
||||||
|
[789000, UniversalYAxisUnit.ROTATION_SPEED_HERTZ, '789 kHz'],
|
||||||
|
[213, UniversalYAxisUnit.ROTATION_SPEED_RADIANS_PER_SECOND, '213 rad/s'],
|
||||||
|
[654, UniversalYAxisUnit.ROTATION_SPEED_DEGREES_PER_SECOND, '654 °/s'],
|
||||||
|
])('formats rotation speed value %s %s as %s', (value, unit, expected) => {
|
||||||
|
expect(formatUniversalUnit(value, unit)).toBe(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Temperature', () => {
|
||||||
|
test.each([
|
||||||
|
[37, UniversalYAxisUnit.TEMPERATURE_CELSIUS, '37 °C'],
|
||||||
|
[451, UniversalYAxisUnit.TEMPERATURE_FAHRENHEIT, '451 °F'],
|
||||||
|
[310, UniversalYAxisUnit.TEMPERATURE_KELVIN, '310 K'],
|
||||||
|
])('formats temperature value %s %s as %s', (value, unit, expected) => {
|
||||||
|
expect(formatUniversalUnit(value, unit)).toBe(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Velocity', () => {
|
||||||
|
test.each([
|
||||||
|
[900, UniversalYAxisUnit.VELOCITY_METERS_PER_SECOND, '900 m/s'],
|
||||||
|
[456, UniversalYAxisUnit.VELOCITY_KILOMETERS_PER_HOUR, '456 km/h'],
|
||||||
|
[789, UniversalYAxisUnit.VELOCITY_MILES_PER_HOUR, '789 mph'],
|
||||||
|
[222, UniversalYAxisUnit.VELOCITY_KNOT, '222 kn'],
|
||||||
|
])('formats velocity value %s %s as %s', (value, unit, expected) => {
|
||||||
|
expect(formatUniversalUnit(value, unit)).toBe(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Volume', () => {
|
||||||
|
test.each([
|
||||||
|
[1200, UniversalYAxisUnit.VOLUME_MILLILITER, '1.2 L'],
|
||||||
|
[9000000, UniversalYAxisUnit.VOLUME_MILLILITER, '9 kL'],
|
||||||
|
[9, UniversalYAxisUnit.VOLUME_LITER, '9 L'],
|
||||||
|
[9000, UniversalYAxisUnit.VOLUME_LITER, '9 kL'],
|
||||||
|
[9000000, UniversalYAxisUnit.VOLUME_LITER, '9 ML'],
|
||||||
|
[9000000000, UniversalYAxisUnit.VOLUME_LITER, '9 GL'],
|
||||||
|
[9000000000000, UniversalYAxisUnit.VOLUME_LITER, '9 TL'],
|
||||||
|
[9000000000000000, UniversalYAxisUnit.VOLUME_LITER, '9 PL'],
|
||||||
|
[9010000000000000000, UniversalYAxisUnit.VOLUME_LITER, '9.01 EL'],
|
||||||
|
[9020000000000000000000, UniversalYAxisUnit.VOLUME_LITER, '9.02 ZL'],
|
||||||
|
[9030000000000000000000000, UniversalYAxisUnit.VOLUME_LITER, '9.03 YL'],
|
||||||
|
[900, UniversalYAxisUnit.VOLUME_CUBIC_METER, '900 m³'],
|
||||||
|
[
|
||||||
|
9000000000000000000000000000000,
|
||||||
|
UniversalYAxisUnit.VOLUME_CUBIC_METER,
|
||||||
|
'9e+30 m³',
|
||||||
|
],
|
||||||
|
[900, UniversalYAxisUnit.VOLUME_NORMAL_CUBIC_METER, '900 Nm³'],
|
||||||
|
[
|
||||||
|
9000000000000000000000000000000,
|
||||||
|
UniversalYAxisUnit.VOLUME_NORMAL_CUBIC_METER,
|
||||||
|
'9e+30 Nm³',
|
||||||
|
],
|
||||||
|
[900, UniversalYAxisUnit.VOLUME_CUBIC_DECIMETER, '900 dm³'],
|
||||||
|
[
|
||||||
|
9000000000000000000000000000000,
|
||||||
|
UniversalYAxisUnit.VOLUME_CUBIC_DECIMETER,
|
||||||
|
'9e+30 dm³',
|
||||||
|
],
|
||||||
|
[900, UniversalYAxisUnit.VOLUME_GALLON, '900 gal'],
|
||||||
|
[
|
||||||
|
9000000000000000000000000000000,
|
||||||
|
UniversalYAxisUnit.VOLUME_GALLON,
|
||||||
|
'9e+30 gal',
|
||||||
|
],
|
||||||
|
])('formats volume value %s %s as %s', (value, unit, expected) => {
|
||||||
|
expect(formatUniversalUnit(value, unit)).toBe(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Boolean', () => {
|
||||||
|
it('formats boolean units', () => {
|
||||||
|
expect(formatUniversalUnit(1, UniversalYAxisUnit.TRUE_FALSE)).toBe('True');
|
||||||
|
expect(formatUniversalUnit(1, UniversalYAxisUnit.YES_NO)).toBe('Yes');
|
||||||
|
expect(formatUniversalUnit(1, UniversalYAxisUnit.ON_OFF)).toBe('On');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Mapping Validator', () => {
|
||||||
|
it('validates that all units have a mapping', () => {
|
||||||
|
// Each universal unit should have a mapping to a 1:1 Grafana unit in UniversalUnitToGrafanaUnit or an additional mapping in AdditionalLabelsMappingForGrafanaUnits
|
||||||
|
const units = Object.values(UniversalYAxisUnit);
|
||||||
|
expect(
|
||||||
|
units.every((unit) => {
|
||||||
|
const hasBaseMapping = unit in UniversalUnitToGrafanaUnit;
|
||||||
|
const hasAdditionalMapping = unit in AdditionalLabelsMappingForGrafanaUnits;
|
||||||
|
const hasMapping = hasBaseMapping || hasAdditionalMapping;
|
||||||
|
if (!hasMapping) {
|
||||||
|
throw new Error(`Unit ${unit} does not have a mapping`);
|
||||||
|
}
|
||||||
|
return hasMapping;
|
||||||
|
}),
|
||||||
|
).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
|
import { UniversalYAxisUnit } from '../types';
|
||||||
import {
|
import {
|
||||||
getUniversalNameFromMetricUnit,
|
getUniversalNameFromMetricUnit,
|
||||||
mapMetricUnitToUniversalUnit,
|
mapMetricUnitToUniversalUnit,
|
||||||
|
mergeCategories,
|
||||||
} from '../utils';
|
} from '../utils';
|
||||||
|
|
||||||
describe('YAxisUnitSelector utils', () => {
|
describe('YAxisUnitSelector utils', () => {
|
||||||
@@ -36,4 +38,43 @@ describe('YAxisUnitSelector utils', () => {
|
|||||||
expect(getUniversalNameFromMetricUnit('s')).toBe('Seconds (s)');
|
expect(getUniversalNameFromMetricUnit('s')).toBe('Seconds (s)');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('mergeCategories', () => {
|
||||||
|
it('merges categories correctly', () => {
|
||||||
|
const categories1 = [
|
||||||
|
{
|
||||||
|
name: 'Data',
|
||||||
|
units: [
|
||||||
|
{ name: 'bytes', id: UniversalYAxisUnit.BYTES },
|
||||||
|
{ name: 'kilobytes', id: UniversalYAxisUnit.KILOBYTES },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const categories2 = [
|
||||||
|
{
|
||||||
|
name: 'Data',
|
||||||
|
units: [{ name: 'bits', id: UniversalYAxisUnit.BITS }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Time',
|
||||||
|
units: [{ name: 'seconds', id: UniversalYAxisUnit.SECONDS }],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const mergedCategories = mergeCategories(categories1, categories2);
|
||||||
|
expect(mergedCategories).toEqual([
|
||||||
|
{
|
||||||
|
name: 'Data',
|
||||||
|
units: [
|
||||||
|
{ name: 'bytes', id: UniversalYAxisUnit.BYTES },
|
||||||
|
{ name: 'kilobytes', id: UniversalYAxisUnit.KILOBYTES },
|
||||||
|
{ name: 'bits', id: UniversalYAxisUnit.BITS },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Time',
|
||||||
|
units: [{ name: 'seconds', id: UniversalYAxisUnit.SECONDS }],
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
1269
frontend/src/components/YAxisUnitSelector/data.ts
Normal file
90
frontend/src/components/YAxisUnitSelector/formatter.ts
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
import { formattedValueToString, getValueFormat } from '@grafana/data';
|
||||||
|
import { PrecisionOption, PrecisionOptionsEnum } from 'components/Graph/types';
|
||||||
|
import { formatDecimalWithLeadingZeros } from 'components/Graph/utils';
|
||||||
|
import {
|
||||||
|
AdditionalLabelsMappingForGrafanaUnits,
|
||||||
|
CUSTOM_SCALING_FAMILIES,
|
||||||
|
UniversalUnitToGrafanaUnit,
|
||||||
|
} from 'components/YAxisUnitSelector/constants';
|
||||||
|
import { UniversalYAxisUnit } from 'components/YAxisUnitSelector/types';
|
||||||
|
|
||||||
|
function scaleValue(
|
||||||
|
value: number,
|
||||||
|
unit: UniversalYAxisUnit,
|
||||||
|
family: UniversalYAxisUnit[],
|
||||||
|
factor: number,
|
||||||
|
): { value: number; label: string } {
|
||||||
|
let idx = family.indexOf(unit);
|
||||||
|
// If the unit is not in the family, return the unit with the additional label
|
||||||
|
if (idx === -1) {
|
||||||
|
return { value, label: AdditionalLabelsMappingForGrafanaUnits[unit] || '' };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scale the value up or down to the nearest unit in the family
|
||||||
|
let scaled = value;
|
||||||
|
// Scale up
|
||||||
|
while (scaled >= factor && idx < family.length - 1) {
|
||||||
|
scaled /= factor;
|
||||||
|
idx += 1;
|
||||||
|
}
|
||||||
|
// Scale down
|
||||||
|
while (scaled < 1 && idx > 0) {
|
||||||
|
scaled *= factor;
|
||||||
|
idx -= 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return the scaled value and the label of the nearest unit in the family
|
||||||
|
return {
|
||||||
|
value: scaled,
|
||||||
|
label: AdditionalLabelsMappingForGrafanaUnits[family[idx]] || '',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatUniversalUnit(
|
||||||
|
value: number,
|
||||||
|
unit: UniversalYAxisUnit,
|
||||||
|
precision: PrecisionOption = PrecisionOptionsEnum.FULL,
|
||||||
|
decimals: number | undefined = undefined,
|
||||||
|
): string {
|
||||||
|
// Check if this unit belongs to a family that needs custom scaling
|
||||||
|
const family = CUSTOM_SCALING_FAMILIES.find((family) =>
|
||||||
|
family.units.includes(unit),
|
||||||
|
);
|
||||||
|
if (family) {
|
||||||
|
const scaled = scaleValue(value, unit, family.units, family.scaleFactor);
|
||||||
|
const formatter = getValueFormat(scaled.label);
|
||||||
|
const formatted = formatter(scaled.value, decimals);
|
||||||
|
if (formatted.text && formatted.text.includes('.')) {
|
||||||
|
formatted.text = formatDecimalWithLeadingZeros(
|
||||||
|
parseFloat(formatted.text),
|
||||||
|
precision,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return `${formatted.text} ${scaled.label}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use Grafana formatting with custom label mappings
|
||||||
|
const grafanaFormat = UniversalUnitToGrafanaUnit[unit];
|
||||||
|
if (grafanaFormat) {
|
||||||
|
const formatter = getValueFormat(grafanaFormat);
|
||||||
|
const formatted = formatter(value, decimals);
|
||||||
|
if (formatted.text && formatted.text.includes('.')) {
|
||||||
|
formatted.text = formatDecimalWithLeadingZeros(
|
||||||
|
parseFloat(formatted.text),
|
||||||
|
precision,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return formattedValueToString(formatted);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to short format for other units
|
||||||
|
const formatter = getValueFormat('short');
|
||||||
|
const formatted = formatter(value, decimals);
|
||||||
|
if (formatted.text && formatted.text.includes('.')) {
|
||||||
|
formatted.text = formatDecimalWithLeadingZeros(
|
||||||
|
parseFloat(formatted.text),
|
||||||
|
precision,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return `${formatted.text} ${unit}`;
|
||||||
|
}
|
||||||
@@ -5,11 +5,11 @@ export interface YAxisUnitSelectorProps {
|
|||||||
loading?: boolean;
|
loading?: boolean;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
'data-testid'?: string;
|
'data-testid'?: string;
|
||||||
|
source: YAxisSource;
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum UniversalYAxisUnit {
|
export enum UniversalYAxisUnit {
|
||||||
// Time
|
// Time
|
||||||
WEEKS = 'wk',
|
|
||||||
DAYS = 'd',
|
DAYS = 'd',
|
||||||
HOURS = 'h',
|
HOURS = 'h',
|
||||||
MINUTES = 'min',
|
MINUTES = 'min',
|
||||||
@@ -17,6 +17,14 @@ export enum UniversalYAxisUnit {
|
|||||||
MICROSECONDS = 'us',
|
MICROSECONDS = 'us',
|
||||||
MILLISECONDS = 'ms',
|
MILLISECONDS = 'ms',
|
||||||
NANOSECONDS = 'ns',
|
NANOSECONDS = 'ns',
|
||||||
|
DURATION_MS = 'dtdurationms',
|
||||||
|
DURATION_S = 'dtdurations',
|
||||||
|
DURATION_HMS = 'dthms',
|
||||||
|
DURATION_DHMS = 'dtdhms',
|
||||||
|
TIMETICKS = 'timeticks',
|
||||||
|
CLOCK_MS = 'clockms',
|
||||||
|
CLOCK_S = 'clocks',
|
||||||
|
TIME_HERTZ = 'hertz',
|
||||||
|
|
||||||
// Data
|
// Data
|
||||||
BYTES = 'By',
|
BYTES = 'By',
|
||||||
@@ -29,6 +37,17 @@ export enum UniversalYAxisUnit {
|
|||||||
ZETTABYTES = 'ZBy',
|
ZETTABYTES = 'ZBy',
|
||||||
YOTTABYTES = 'YBy',
|
YOTTABYTES = 'YBy',
|
||||||
|
|
||||||
|
// Binary (IEC) Data
|
||||||
|
BYTES_IEC = 'bytes',
|
||||||
|
KIBIBYTES = 'KiBy',
|
||||||
|
MEBIBYTES = 'MiBy',
|
||||||
|
GIBIBYTES = 'GiBy',
|
||||||
|
TEBIBYTES = 'TiBy',
|
||||||
|
PEBIBYTES = 'PiBy',
|
||||||
|
EXBIBYTES = 'EiBy',
|
||||||
|
ZEBIBYTES = 'ZiBy',
|
||||||
|
YOBIBYTES = 'YiBy',
|
||||||
|
|
||||||
// Data Rate
|
// Data Rate
|
||||||
BYTES_SECOND = 'By/s',
|
BYTES_SECOND = 'By/s',
|
||||||
KILOBYTES_SECOND = 'kBy/s',
|
KILOBYTES_SECOND = 'kBy/s',
|
||||||
@@ -39,9 +58,21 @@ export enum UniversalYAxisUnit {
|
|||||||
EXABYTES_SECOND = 'EBy/s',
|
EXABYTES_SECOND = 'EBy/s',
|
||||||
ZETTABYTES_SECOND = 'ZBy/s',
|
ZETTABYTES_SECOND = 'ZBy/s',
|
||||||
YOTTABYTES_SECOND = 'YBy/s',
|
YOTTABYTES_SECOND = 'YBy/s',
|
||||||
|
DATA_RATE_PACKETS_PER_SECOND = 'pps',
|
||||||
|
|
||||||
|
// Binary (IEC) Data Rate
|
||||||
|
KIBIBYTES_SECOND = 'KiBy/s',
|
||||||
|
MEBIBYTES_SECOND = 'MiBy/s',
|
||||||
|
GIBIBYTES_SECOND = 'GiBy/s',
|
||||||
|
TEBIBYTES_SECOND = 'TiBy/s',
|
||||||
|
PEBIBYTES_SECOND = 'PiBy/s',
|
||||||
|
EXBIBYTES_SECOND = 'EiBy/s',
|
||||||
|
ZEBIBYTES_SECOND = 'ZiBy/s',
|
||||||
|
YOBIBYTES_SECOND = 'YiBy/s',
|
||||||
|
|
||||||
// Bits
|
// Bits
|
||||||
BITS = 'bit',
|
BITS = 'bit',
|
||||||
|
BITS_IEC = 'bits',
|
||||||
KILOBITS = 'kbit',
|
KILOBITS = 'kbit',
|
||||||
MEGABITS = 'Mbit',
|
MEGABITS = 'Mbit',
|
||||||
GIGABITS = 'Gbit',
|
GIGABITS = 'Gbit',
|
||||||
@@ -62,6 +93,16 @@ export enum UniversalYAxisUnit {
|
|||||||
ZETTABITS_SECOND = 'Zbit/s',
|
ZETTABITS_SECOND = 'Zbit/s',
|
||||||
YOTTABITS_SECOND = 'Ybit/s',
|
YOTTABITS_SECOND = 'Ybit/s',
|
||||||
|
|
||||||
|
// Binary (IEC) Bit Rate
|
||||||
|
KIBIBITS_SECOND = 'Kibit/s',
|
||||||
|
MEBIBITS_SECOND = 'Mibit/s',
|
||||||
|
GIBIBITS_SECOND = 'Gibit/s',
|
||||||
|
TEBIBITS_SECOND = 'Tibit/s',
|
||||||
|
PEBIBITS_SECOND = 'Pibit/s',
|
||||||
|
EXBIBITS_SECOND = 'Eibit/s',
|
||||||
|
ZEBIBITS_SECOND = 'Zibit/s',
|
||||||
|
YOBIBITS_SECOND = 'Yibit/s',
|
||||||
|
|
||||||
// Count
|
// Count
|
||||||
COUNT = '{count}',
|
COUNT = '{count}',
|
||||||
COUNT_SECOND = '{count}/s',
|
COUNT_SECOND = '{count}/s',
|
||||||
@@ -87,7 +128,231 @@ export enum UniversalYAxisUnit {
|
|||||||
// Percent
|
// Percent
|
||||||
PERCENT = '%',
|
PERCENT = '%',
|
||||||
PERCENT_UNIT = 'percentunit',
|
PERCENT_UNIT = 'percentunit',
|
||||||
|
|
||||||
|
// Boolean
|
||||||
|
TRUE_FALSE = '{bool}',
|
||||||
|
YES_NO = '{bool_yn}',
|
||||||
|
ON_OFF = 'bool_on_off',
|
||||||
|
|
||||||
|
// None
|
||||||
NONE = '1',
|
NONE = '1',
|
||||||
|
|
||||||
|
// Hash rate
|
||||||
|
HASH_RATE_HASHES_PER_SECOND = 'Hs',
|
||||||
|
HASH_RATE_KILOHASHES_PER_SECOND = 'KHs',
|
||||||
|
HASH_RATE_MEGAHASHES_PER_SECOND = 'MHs',
|
||||||
|
HASH_RATE_GIGAHASHES_PER_SECOND = 'GHs',
|
||||||
|
HASH_RATE_TERAHASHES_PER_SECOND = 'THs',
|
||||||
|
HASH_RATE_PETAHASHES_PER_SECOND = 'PHs',
|
||||||
|
HASH_RATE_EXAHASHES_PER_SECOND = 'EHs',
|
||||||
|
|
||||||
|
// Miscellaneous
|
||||||
|
MISC_STRING = 'string',
|
||||||
|
MISC_SHORT = 'short',
|
||||||
|
MISC_HUMIDITY = 'humidity',
|
||||||
|
MISC_DECIBEL = 'dB',
|
||||||
|
MISC_HEXADECIMAL = 'hex',
|
||||||
|
MISC_HEXADECIMAL_0X = 'hex0x',
|
||||||
|
MISC_SCIENTIFIC_NOTATION = 'sci',
|
||||||
|
MISC_LOCALE_FORMAT = 'locale',
|
||||||
|
MISC_PIXELS = 'pixel',
|
||||||
|
|
||||||
|
// Acceleration
|
||||||
|
ACCELERATION_METERS_PER_SECOND_SQUARED = 'accMS2',
|
||||||
|
ACCELERATION_FEET_PER_SECOND_SQUARED = 'accFS2',
|
||||||
|
ACCELERATION_G_UNIT = 'accG',
|
||||||
|
|
||||||
|
// Angular
|
||||||
|
ANGULAR_DEGREE = 'degree',
|
||||||
|
ANGULAR_RADIAN = 'radian',
|
||||||
|
ANGULAR_GRADIAN = 'grad',
|
||||||
|
ANGULAR_ARC_MINUTE = 'arcmin',
|
||||||
|
ANGULAR_ARC_SECOND = 'arcsec',
|
||||||
|
|
||||||
|
// Area
|
||||||
|
AREA_SQUARE_METERS = 'areaM2',
|
||||||
|
AREA_SQUARE_FEET = 'areaF2',
|
||||||
|
AREA_SQUARE_MILES = 'areaMI2',
|
||||||
|
|
||||||
|
// FLOPs
|
||||||
|
FLOPS_FLOPS = 'flops',
|
||||||
|
FLOPS_MFLOPS = 'mflops',
|
||||||
|
FLOPS_GFLOPS = 'gflops',
|
||||||
|
FLOPS_TFLOPS = 'tflops',
|
||||||
|
FLOPS_PFLOPS = 'pflops',
|
||||||
|
FLOPS_EFLOPS = 'eflops',
|
||||||
|
FLOPS_ZFLOPS = 'zflops',
|
||||||
|
FLOPS_YFLOPS = 'yflops',
|
||||||
|
|
||||||
|
// Concentration
|
||||||
|
CONCENTRATION_PPM = 'ppm',
|
||||||
|
CONCENTRATION_PPB = 'conppb',
|
||||||
|
CONCENTRATION_NG_M3 = 'conngm3',
|
||||||
|
CONCENTRATION_NG_NORMAL_CUBIC_METER = 'conngNm3',
|
||||||
|
CONCENTRATION_UG_M3 = 'conμgm3',
|
||||||
|
CONCENTRATION_UG_NORMAL_CUBIC_METER = 'conμgNm3',
|
||||||
|
CONCENTRATION_MG_M3 = 'conmgm3',
|
||||||
|
CONCENTRATION_MG_NORMAL_CUBIC_METER = 'conmgNm3',
|
||||||
|
CONCENTRATION_G_M3 = 'congm3',
|
||||||
|
CONCENTRATION_G_NORMAL_CUBIC_METER = 'congNm3',
|
||||||
|
CONCENTRATION_MG_PER_DL = 'conmgdL',
|
||||||
|
CONCENTRATION_MMOL_PER_L = 'conmmolL',
|
||||||
|
|
||||||
|
// Currency
|
||||||
|
CURRENCY_USD = 'currencyUSD',
|
||||||
|
CURRENCY_GBP = 'currencyGBP',
|
||||||
|
CURRENCY_EUR = 'currencyEUR',
|
||||||
|
CURRENCY_JPY = 'currencyJPY',
|
||||||
|
CURRENCY_RUB = 'currencyRUB',
|
||||||
|
CURRENCY_UAH = 'currencyUAH',
|
||||||
|
CURRENCY_BRL = 'currencyBRL',
|
||||||
|
CURRENCY_DKK = 'currencyDKK',
|
||||||
|
CURRENCY_ISK = 'currencyISK',
|
||||||
|
CURRENCY_NOK = 'currencyNOK',
|
||||||
|
CURRENCY_SEK = 'currencySEK',
|
||||||
|
CURRENCY_CZK = 'currencyCZK',
|
||||||
|
CURRENCY_CHF = 'currencyCHF',
|
||||||
|
CURRENCY_PLN = 'currencyPLN',
|
||||||
|
CURRENCY_BTC = 'currencyBTC',
|
||||||
|
CURRENCY_MBTC = 'currencymBTC',
|
||||||
|
CURRENCY_UBTC = 'currencyμBTC',
|
||||||
|
CURRENCY_ZAR = 'currencyZAR',
|
||||||
|
CURRENCY_INR = 'currencyINR',
|
||||||
|
CURRENCY_KRW = 'currencyKRW',
|
||||||
|
CURRENCY_IDR = 'currencyIDR',
|
||||||
|
CURRENCY_PHP = 'currencyPHP',
|
||||||
|
CURRENCY_VND = 'currencyVND',
|
||||||
|
|
||||||
|
// Datetime
|
||||||
|
DATETIME_ISO = 'dateTimeAsIso',
|
||||||
|
DATETIME_ISO_NO_DATE_IF_TODAY = 'dateTimeAsIsoNoDateIfToday',
|
||||||
|
DATETIME_US = 'dateTimeAsUS',
|
||||||
|
DATETIME_US_NO_DATE_IF_TODAY = 'dateTimeAsUSNoDateIfToday',
|
||||||
|
DATETIME_LOCAL = 'dateTimeAsLocal',
|
||||||
|
DATETIME_LOCAL_NO_DATE_IF_TODAY = 'dateTimeAsLocalNoDateIfToday',
|
||||||
|
DATETIME_SYSTEM = 'dateTimeAsSystem',
|
||||||
|
DATETIME_FROM_NOW = 'dateTimeFromNow',
|
||||||
|
|
||||||
|
// Power/Electrical
|
||||||
|
POWER_WATT = 'watt',
|
||||||
|
POWER_KILOWATT = 'kwatt',
|
||||||
|
POWER_MEGAWATT = 'megwatt',
|
||||||
|
POWER_GIGAWATT = 'gwatt',
|
||||||
|
POWER_MILLIWATT = 'mwatt',
|
||||||
|
POWER_WATT_PER_SQUARE_METER = 'Wm2',
|
||||||
|
POWER_VOLT_AMPERE = 'voltamp',
|
||||||
|
POWER_KILOVOLT_AMPERE = 'kvoltamp',
|
||||||
|
POWER_VOLT_AMPERE_REACTIVE = 'voltampreact',
|
||||||
|
POWER_KILOVOLT_AMPERE_REACTIVE = 'kvoltampreact',
|
||||||
|
POWER_WATT_HOUR = 'watth',
|
||||||
|
POWER_WATT_HOUR_PER_KG = 'watthperkg',
|
||||||
|
POWER_KILOWATT_HOUR = 'kwatth',
|
||||||
|
POWER_KILOWATT_MINUTE = 'kwattm',
|
||||||
|
POWER_AMPERE_HOUR = 'amph',
|
||||||
|
POWER_KILOAMPERE_HOUR = 'kamph',
|
||||||
|
POWER_MILLIAMPERE_HOUR = 'mamph',
|
||||||
|
POWER_JOULE = 'joule',
|
||||||
|
POWER_ELECTRON_VOLT = 'ev',
|
||||||
|
POWER_AMPERE = 'amp',
|
||||||
|
POWER_KILOAMPERE = 'kamp',
|
||||||
|
POWER_MILLIAMPERE = 'mamp',
|
||||||
|
POWER_VOLT = 'volt',
|
||||||
|
POWER_KILOVOLT = 'kvolt',
|
||||||
|
POWER_MILLIVOLT = 'mvolt',
|
||||||
|
POWER_DECIBEL_MILLIWATT = 'dBm',
|
||||||
|
POWER_OHM = 'ohm',
|
||||||
|
POWER_KILOOHM = 'kohm',
|
||||||
|
POWER_MEGAOHM = 'Mohm',
|
||||||
|
POWER_FARAD = 'farad',
|
||||||
|
POWER_MICROFARAD = 'µfarad',
|
||||||
|
POWER_NANOFARAD = 'nfarad',
|
||||||
|
POWER_PICOFARAD = 'pfarad',
|
||||||
|
POWER_FEMTOFARAD = 'ffarad',
|
||||||
|
POWER_HENRY = 'henry',
|
||||||
|
POWER_MILLIHENRY = 'mhenry',
|
||||||
|
POWER_MICROHENRY = 'µhenry',
|
||||||
|
POWER_LUMENS = 'lumens',
|
||||||
|
|
||||||
|
// Flow
|
||||||
|
FLOW_GALLONS_PER_MINUTE = 'flowgpm',
|
||||||
|
FLOW_CUBIC_METERS_PER_SECOND = 'flowcms',
|
||||||
|
FLOW_CUBIC_FEET_PER_SECOND = 'flowcfs',
|
||||||
|
FLOW_CUBIC_FEET_PER_MINUTE = 'flowcfm',
|
||||||
|
FLOW_LITERS_PER_HOUR = 'litreh',
|
||||||
|
FLOW_LITERS_PER_MINUTE = 'flowlpm',
|
||||||
|
FLOW_MILLILITERS_PER_MINUTE = 'flowmlpm',
|
||||||
|
FLOW_LUX = 'lux',
|
||||||
|
|
||||||
|
// Force
|
||||||
|
FORCE_NEWTON_METERS = 'forceNm',
|
||||||
|
FORCE_KILONEWTON_METERS = 'forcekNm',
|
||||||
|
FORCE_NEWTONS = 'forceN',
|
||||||
|
FORCE_KILONEWTONS = 'forcekN',
|
||||||
|
|
||||||
|
// Mass
|
||||||
|
MASS_MILLIGRAM = 'massmg',
|
||||||
|
MASS_GRAM = 'massg',
|
||||||
|
MASS_POUND = 'masslb',
|
||||||
|
MASS_KILOGRAM = 'masskg',
|
||||||
|
MASS_METRIC_TON = 'masst',
|
||||||
|
|
||||||
|
// Length
|
||||||
|
LENGTH_MILLIMETER = 'lengthmm',
|
||||||
|
LENGTH_INCH = 'lengthin',
|
||||||
|
LENGTH_FOOT = 'lengthft',
|
||||||
|
LENGTH_METER = 'lengthm',
|
||||||
|
LENGTH_KILOMETER = 'lengthkm',
|
||||||
|
LENGTH_MILE = 'lengthmi',
|
||||||
|
|
||||||
|
// Pressure
|
||||||
|
PRESSURE_MILLIBAR = 'pressurembar',
|
||||||
|
PRESSURE_BAR = 'pressurebar',
|
||||||
|
PRESSURE_KILOBAR = 'pressurekbar',
|
||||||
|
PRESSURE_PASCAL = 'pressurepa',
|
||||||
|
PRESSURE_HECTOPASCAL = 'pressurehpa',
|
||||||
|
PRESSURE_KILOPASCAL = 'pressurekpa',
|
||||||
|
PRESSURE_INCHES_HG = 'pressurehg',
|
||||||
|
PRESSURE_PSI = 'pressurepsi',
|
||||||
|
|
||||||
|
// Radiation
|
||||||
|
RADIATION_BECQUEREL = 'radbq',
|
||||||
|
RADIATION_CURIE = 'radci',
|
||||||
|
RADIATION_GRAY = 'radgy',
|
||||||
|
RADIATION_RAD = 'radrad',
|
||||||
|
RADIATION_SIEVERT = 'radsv',
|
||||||
|
RADIATION_MILLISIEVERT = 'radmsv',
|
||||||
|
RADIATION_MICROSIEVERT = 'radusv',
|
||||||
|
RADIATION_REM = 'radrem',
|
||||||
|
RADIATION_EXPOSURE_C_PER_KG = 'radexpckg',
|
||||||
|
RADIATION_ROENTGEN = 'radr',
|
||||||
|
RADIATION_SIEVERT_PER_HOUR = 'radsvh',
|
||||||
|
RADIATION_MILLISIEVERT_PER_HOUR = 'radmsvh',
|
||||||
|
RADIATION_MICROSIEVERT_PER_HOUR = 'radusvh',
|
||||||
|
|
||||||
|
// Rotation speed
|
||||||
|
ROTATION_SPEED_REVOLUTIONS_PER_MINUTE = 'rotrpm',
|
||||||
|
ROTATION_SPEED_HERTZ = 'rothz',
|
||||||
|
ROTATION_SPEED_RADIANS_PER_SECOND = 'rotrads',
|
||||||
|
ROTATION_SPEED_DEGREES_PER_SECOND = 'rotdegs',
|
||||||
|
|
||||||
|
// Temperature
|
||||||
|
TEMPERATURE_CELSIUS = 'celsius',
|
||||||
|
TEMPERATURE_FAHRENHEIT = 'fahrenheit',
|
||||||
|
TEMPERATURE_KELVIN = 'kelvin',
|
||||||
|
|
||||||
|
// Velocity
|
||||||
|
VELOCITY_METERS_PER_SECOND = 'velocityms',
|
||||||
|
VELOCITY_KILOMETERS_PER_HOUR = 'velocitykmh',
|
||||||
|
VELOCITY_MILES_PER_HOUR = 'velocitymph',
|
||||||
|
VELOCITY_KNOT = 'velocityknot',
|
||||||
|
|
||||||
|
// Volume
|
||||||
|
VOLUME_MILLILITER = 'mlitre',
|
||||||
|
VOLUME_LITER = 'litre',
|
||||||
|
VOLUME_CUBIC_METER = 'm3',
|
||||||
|
VOLUME_NORMAL_CUBIC_METER = 'Nm3',
|
||||||
|
VOLUME_CUBIC_DECIMETER = 'dm3',
|
||||||
|
VOLUME_GALLON = 'gallons',
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum YAxisUnit {
|
export enum YAxisUnit {
|
||||||
@@ -293,6 +558,15 @@ export enum YAxisUnit {
|
|||||||
UCUM_PEBIBYTES = 'PiBy',
|
UCUM_PEBIBYTES = 'PiBy',
|
||||||
OPEN_METRICS_PEBIBYTES = 'pebibytes',
|
OPEN_METRICS_PEBIBYTES = 'pebibytes',
|
||||||
|
|
||||||
|
UCUM_EXBIBYTES = 'EiBy',
|
||||||
|
OPEN_METRICS_EXBIBYTES = 'exbibytes',
|
||||||
|
|
||||||
|
UCUM_ZEBIBYTES = 'ZiBy',
|
||||||
|
OPEN_METRICS_ZEBIBYTES = 'zebibytes',
|
||||||
|
|
||||||
|
UCUM_YOBIBYTES = 'YiBy',
|
||||||
|
OPEN_METRICS_YOBIBYTES = 'yobibytes',
|
||||||
|
|
||||||
UCUM_KIBIBYTES_SECOND = 'KiBy/s',
|
UCUM_KIBIBYTES_SECOND = 'KiBy/s',
|
||||||
OPEN_METRICS_KIBIBYTES_SECOND = 'kibibytes_per_second',
|
OPEN_METRICS_KIBIBYTES_SECOND = 'kibibytes_per_second',
|
||||||
|
|
||||||
@@ -323,6 +597,24 @@ export enum YAxisUnit {
|
|||||||
UCUM_PEBIBITS_SECOND = 'Pibit/s',
|
UCUM_PEBIBITS_SECOND = 'Pibit/s',
|
||||||
OPEN_METRICS_PEBIBITS_SECOND = 'pebibits_per_second',
|
OPEN_METRICS_PEBIBITS_SECOND = 'pebibits_per_second',
|
||||||
|
|
||||||
|
UCUM_EXBIBYTES_SECOND = 'EiBy/s',
|
||||||
|
OPEN_METRICS_EXBIBYTES_SECOND = 'exbibytes_per_second',
|
||||||
|
|
||||||
|
UCUM_EXBIBITS_SECOND = 'Eibit/s',
|
||||||
|
OPEN_METRICS_EXBIBITS_SECOND = 'exbibits_per_second',
|
||||||
|
|
||||||
|
UCUM_ZEBIBYTES_SECOND = 'ZiBy/s',
|
||||||
|
OPEN_METRICS_ZEBIBYTES_SECOND = 'zebibytes_per_second',
|
||||||
|
|
||||||
|
UCUM_ZEBIBITS_SECOND = 'Zibit/s',
|
||||||
|
OPEN_METRICS_ZEBIBITS_SECOND = 'zebibits_per_second',
|
||||||
|
|
||||||
|
UCUM_YOBIBYTES_SECOND = 'YiBy/s',
|
||||||
|
OPEN_METRICS_YOBIBYTES_SECOND = 'yobibytes_per_second',
|
||||||
|
|
||||||
|
UCUM_YOBIBITS_SECOND = 'Yibit/s',
|
||||||
|
OPEN_METRICS_YOBIBITS_SECOND = 'yobibits_per_second',
|
||||||
|
|
||||||
UCUM_TRUE_FALSE = '{bool}',
|
UCUM_TRUE_FALSE = '{bool}',
|
||||||
OPEN_METRICS_TRUE_FALSE = 'boolean_true_false',
|
OPEN_METRICS_TRUE_FALSE = 'boolean_true_false',
|
||||||
|
|
||||||
@@ -364,3 +656,27 @@ export enum YAxisUnit {
|
|||||||
|
|
||||||
OPEN_METRICS_PERCENT_UNIT = 'percentunit',
|
OPEN_METRICS_PERCENT_UNIT = 'percentunit',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ScaledValue {
|
||||||
|
value: number;
|
||||||
|
label: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UnitFamilyConfig {
|
||||||
|
units: UniversalYAxisUnit[];
|
||||||
|
scaleFactor: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface YAxisCategory {
|
||||||
|
name: string;
|
||||||
|
units: {
|
||||||
|
name: string;
|
||||||
|
id: UniversalYAxisUnit;
|
||||||
|
}[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum YAxisSource {
|
||||||
|
ALERTS = 'alerts',
|
||||||
|
DASHBOARDS = 'dashboards',
|
||||||
|
EXPLORER = 'explorer',
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,11 @@
|
|||||||
import { UniversalYAxisUnitMappings, Y_AXIS_UNIT_NAMES } from './constants';
|
import { UniversalYAxisUnitMappings, Y_AXIS_UNIT_NAMES } from './constants';
|
||||||
import { UniversalYAxisUnit, YAxisUnit } from './types';
|
import { ADDITIONAL_Y_AXIS_CATEGORIES, BASE_Y_AXIS_CATEGORIES } from './data';
|
||||||
|
import {
|
||||||
|
UniversalYAxisUnit,
|
||||||
|
YAxisCategory,
|
||||||
|
YAxisSource,
|
||||||
|
YAxisUnit,
|
||||||
|
} from './types';
|
||||||
|
|
||||||
export const mapMetricUnitToUniversalUnit = (
|
export const mapMetricUnitToUniversalUnit = (
|
||||||
unit: string | undefined,
|
unit: string | undefined,
|
||||||
@@ -9,7 +15,7 @@ export const mapMetricUnitToUniversalUnit = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
const universalUnit = Object.values(UniversalYAxisUnit).find(
|
const universalUnit = Object.values(UniversalYAxisUnit).find(
|
||||||
(u) => UniversalYAxisUnitMappings[u].has(unit as YAxisUnit) || unit === u,
|
(u) => UniversalYAxisUnitMappings[u]?.has(unit as YAxisUnit) || unit === u,
|
||||||
);
|
);
|
||||||
|
|
||||||
return universalUnit || (unit as UniversalYAxisUnit) || null;
|
return universalUnit || (unit as UniversalYAxisUnit) || null;
|
||||||
@@ -31,3 +37,44 @@ export const getUniversalNameFromMetricUnit = (
|
|||||||
|
|
||||||
return universalName || unit || '-';
|
return universalName || unit || '-';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export function isUniversalUnit(format: string): boolean {
|
||||||
|
return Object.values(UniversalYAxisUnit).includes(
|
||||||
|
format as UniversalYAxisUnit,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function mergeCategories(
|
||||||
|
categories1: YAxisCategory[],
|
||||||
|
categories2: YAxisCategory[],
|
||||||
|
): YAxisCategory[] {
|
||||||
|
const mapOfCategories = new Map<string, YAxisCategory>();
|
||||||
|
|
||||||
|
categories1.forEach((category) => {
|
||||||
|
mapOfCategories.set(category.name, category);
|
||||||
|
});
|
||||||
|
|
||||||
|
categories2.forEach((category) => {
|
||||||
|
if (mapOfCategories.has(category.name)) {
|
||||||
|
mapOfCategories.set(category.name, {
|
||||||
|
name: category.name,
|
||||||
|
units: [
|
||||||
|
...(mapOfCategories.get(category.name)?.units ?? []),
|
||||||
|
...category.units,
|
||||||
|
],
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
mapOfCategories.set(category.name, category);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return Array.from(mapOfCategories.values());
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getYAxisCategories(source: YAxisSource): YAxisCategory[] {
|
||||||
|
if (source !== YAxisSource.DASHBOARDS) {
|
||||||
|
return BASE_Y_AXIS_CATEGORIES;
|
||||||
|
}
|
||||||
|
|
||||||
|
return mergeCategories(BASE_Y_AXIS_CATEGORIES, ADDITIONAL_Y_AXIS_CATEGORIES);
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
export const REACT_QUERY_KEY = {
|
export const REACT_QUERY_KEY = {
|
||||||
|
GET_PUBLIC_DASHBOARD: 'GET_PUBLIC_DASHBOARD',
|
||||||
|
GET_PUBLIC_DASHBOARD_META: 'GET_PUBLIC_DASHBOARD_META',
|
||||||
|
GET_PUBLIC_DASHBOARD_WIDGET_DATA: 'GET_PUBLIC_DASHBOARD_WIDGET_DATA',
|
||||||
GET_ALL_LICENCES: 'GET_ALL_LICENCES',
|
GET_ALL_LICENCES: 'GET_ALL_LICENCES',
|
||||||
GET_QUERY_RANGE: 'GET_QUERY_RANGE',
|
GET_QUERY_RANGE: 'GET_QUERY_RANGE',
|
||||||
GET_ALL_DASHBOARDS: 'GET_ALL_DASHBOARDS',
|
GET_ALL_DASHBOARDS: 'GET_ALL_DASHBOARDS',
|
||||||
|
|||||||
@@ -81,6 +81,7 @@ const ROUTES = {
|
|||||||
METER_EXPLORER: '/meter/explorer',
|
METER_EXPLORER: '/meter/explorer',
|
||||||
METER_EXPLORER_VIEWS: '/meter/explorer/views',
|
METER_EXPLORER_VIEWS: '/meter/explorer/views',
|
||||||
HOME_PAGE: '/',
|
HOME_PAGE: '/',
|
||||||
|
PUBLIC_DASHBOARD: '/public/dashboard/:dashboardId',
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export default ROUTES;
|
export default ROUTES;
|
||||||
|
|||||||
@@ -244,6 +244,10 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Add border-bottom to table cells when pagination is not present
|
||||||
|
.ant-spin-container:not(:has(.ant-pagination)) .ant-table-cell {
|
||||||
|
border-bottom: 1px solid var(--bg-slate-500) !important;
|
||||||
|
}
|
||||||
|
|
||||||
.endpoints-table-container {
|
.endpoints-table-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -422,30 +426,28 @@
|
|||||||
gap: 8px;
|
gap: 8px;
|
||||||
.endpoint-meta-data-pill {
|
.endpoint-meta-data-pill {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: flex-start;
|
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
border: 1px solid var(--bg-slate-300);
|
border: 1px solid var(--bg-slate-300);
|
||||||
width: fit-content;
|
overflow: hidden;
|
||||||
|
box-sizing: content-box;
|
||||||
.endpoint-meta-data-label {
|
.endpoint-meta-data-label {
|
||||||
display: flex;
|
|
||||||
padding: 6px 8px;
|
padding: 6px 8px;
|
||||||
align-items: center;
|
|
||||||
gap: 4px;
|
|
||||||
border-right: 1px solid var(--bg-slate-300);
|
border-right: 1px solid var(--bg-slate-300);
|
||||||
color: var(--text-vanilla-100);
|
color: var(--text-vanilla-100);
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 18px; /* 128.571% */
|
||||||
|
letter-spacing: -0.07px;
|
||||||
|
padding: 6px 8px;
|
||||||
background: var(--bg-slate-500);
|
background: var(--bg-slate-500);
|
||||||
height: calc(100% - 12px);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.endpoint-meta-data-value {
|
.endpoint-meta-data-value {
|
||||||
display: flex;
|
|
||||||
padding: 6px 8px;
|
padding: 6px 8px;
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
gap: 10px;
|
|
||||||
color: var(--text-vanilla-400);
|
color: var(--text-vanilla-400);
|
||||||
background: var(--bg-slate-400);
|
background: var(--bg-slate-400);
|
||||||
height: calc(100% - 12px);
|
font-size: 14px;
|
||||||
|
line-height: 18px;
|
||||||
|
letter-spacing: -0.07px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -453,9 +455,23 @@
|
|||||||
.endpoint-details-filters-container {
|
.endpoint-details-filters-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
border: 1px solid var(--bg-slate-500);
|
||||||
|
height: 36px;
|
||||||
|
box-sizing: content-box;
|
||||||
|
.ant-select-selector {
|
||||||
|
border: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
.endpoint-details-filters-container-dropdown {
|
.endpoint-details-filters-container-dropdown {
|
||||||
width: 120px;
|
width: 120px;
|
||||||
|
border-right: 1px solid var(--bg-slate-500);
|
||||||
|
height: 36px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
.ant-select-single {
|
||||||
|
height: 32px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.endpoint-details-filters-container-search {
|
.endpoint-details-filters-container-search {
|
||||||
@@ -996,7 +1012,6 @@
|
|||||||
|
|
||||||
.lightMode {
|
.lightMode {
|
||||||
.ant-drawer-header {
|
.ant-drawer-header {
|
||||||
border-bottom: 1px solid var(--bg-vanilla-400);
|
|
||||||
background: var(--bg-vanilla-100);
|
background: var(--bg-vanilla-100);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1007,6 +1022,25 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.domain-detail-drawer {
|
.domain-detail-drawer {
|
||||||
|
.endpoint-details-card,
|
||||||
|
.status-code-table-container,
|
||||||
|
.endpoint-details-filters-container,
|
||||||
|
.endpoint-details-filters-container-dropdown,
|
||||||
|
.ant-radio-button-wrapper,
|
||||||
|
.views-tabs-container,
|
||||||
|
.ant-btn-default.tab,
|
||||||
|
.tab::before,
|
||||||
|
.endpoint-meta-data-pill,
|
||||||
|
.endpoint-meta-data-label,
|
||||||
|
.endpoints-table-container,
|
||||||
|
.group-by-label,
|
||||||
|
.ant-select-selector,
|
||||||
|
.ant-drawer-header {
|
||||||
|
border-color: var(--bg-vanilla-300) !important;
|
||||||
|
}
|
||||||
|
.views-tabs .tab::before {
|
||||||
|
background: var(--bg-vanilla-300);
|
||||||
|
}
|
||||||
.title {
|
.title {
|
||||||
color: var(--text-ink-300);
|
color: var(--text-ink-300);
|
||||||
}
|
}
|
||||||
@@ -1031,7 +1065,6 @@
|
|||||||
|
|
||||||
.selected_view {
|
.selected_view {
|
||||||
background: var(--bg-vanilla-300);
|
background: var(--bg-vanilla-300);
|
||||||
border: 1px solid var(--bg-slate-300);
|
|
||||||
color: var(--text-ink-400);
|
color: var(--text-ink-400);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1160,7 +1193,11 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.top-services-content {
|
||||||
|
border-color: var(--bg-vanilla-300);
|
||||||
|
}
|
||||||
.dependent-services-container {
|
.dependent-services-container {
|
||||||
|
border: none;
|
||||||
padding: 10px 12px;
|
padding: 10px 12px;
|
||||||
.top-services-item {
|
.top-services-item {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -1187,11 +1224,31 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.top-services-item-progress-bar {
|
.top-services-item-progress-bar {
|
||||||
background-color: var(--bg-vanilla-300);
|
background-color: var(--bg-vanilla-200);
|
||||||
border: 1px solid var(--bg-slate-300);
|
border: 1px solid var(--bg-vanilla-300);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.ant-table {
|
||||||
|
.ant-table-thead > tr > th {
|
||||||
|
color: var(--text-ink-300);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-table-cell {
|
||||||
|
&,
|
||||||
|
&:has(.top-services-item-latency) {
|
||||||
|
background: var(--bg-vanilla-100);
|
||||||
|
}
|
||||||
|
color: var(--text-ink-300);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-table-tbody > tr:hover > td {
|
||||||
|
background: var(--bg-vanilla-200);
|
||||||
|
}
|
||||||
|
.table-row-dark {
|
||||||
|
background: var(--bg-vanilla-300);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.top-services-item-percentage {
|
.top-services-item-percentage {
|
||||||
color: var(--text-ink-300);
|
color: var(--text-ink-300);
|
||||||
@@ -1225,4 +1282,8 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Add border-bottom to table cells when pagination is not present
|
||||||
|
.ant-spin-container:not(:has(.ant-pagination)) .ant-table-cell {
|
||||||
|
border-bottom: 1px solid var(--bg-vanilla-300) !important;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ import { useIsDarkMode } from 'hooks/useDarkMode';
|
|||||||
import { useGetTenantLicense } from 'hooks/useGetTenantLicense';
|
import { useGetTenantLicense } from 'hooks/useGetTenantLicense';
|
||||||
import { useNotifications } from 'hooks/useNotifications';
|
import { useNotifications } from 'hooks/useNotifications';
|
||||||
import useTabVisibility from 'hooks/useTabFocus';
|
import useTabVisibility from 'hooks/useTabFocus';
|
||||||
|
import { useKBar } from 'kbar';
|
||||||
import history from 'lib/history';
|
import history from 'lib/history';
|
||||||
import { isNull } from 'lodash-es';
|
import { isNull } from 'lodash-es';
|
||||||
import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFallback';
|
import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFallback';
|
||||||
@@ -185,6 +186,19 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
|||||||
|
|
||||||
const { isCloudUser: isCloudUserVal } = useGetTenantLicense();
|
const { isCloudUser: isCloudUserVal } = useGetTenantLicense();
|
||||||
|
|
||||||
|
const { query, disabled } = useKBar((state) => ({
|
||||||
|
disabled: state.disabled,
|
||||||
|
}));
|
||||||
|
|
||||||
|
// disable the kbar command palette when not logged in
|
||||||
|
useEffect(() => {
|
||||||
|
if (isLoggedIn) {
|
||||||
|
query.disable(false);
|
||||||
|
} else {
|
||||||
|
query.disable(true);
|
||||||
|
}
|
||||||
|
}, [isLoggedIn, query, disabled]);
|
||||||
|
|
||||||
const changelogForTenant = isCloudUserVal
|
const changelogForTenant = isCloudUserVal
|
||||||
? DeploymentType.CLOUD_ONLY
|
? DeploymentType.CLOUD_ONLY
|
||||||
: DeploymentType.OSS_ONLY;
|
: DeploymentType.OSS_ONLY;
|
||||||
@@ -391,6 +405,9 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
|||||||
|
|
||||||
const routeKey = useMemo(() => getRouteKey(pathname), [pathname]);
|
const routeKey = useMemo(() => getRouteKey(pathname), [pathname]);
|
||||||
const pageTitle = t(routeKey);
|
const pageTitle = t(routeKey);
|
||||||
|
|
||||||
|
const isPublicDashboard = pathname.startsWith('/public/dashboard/');
|
||||||
|
|
||||||
const renderFullScreen =
|
const renderFullScreen =
|
||||||
pathname === ROUTES.GET_STARTED ||
|
pathname === ROUTES.GET_STARTED ||
|
||||||
pathname === ROUTES.ONBOARDING ||
|
pathname === ROUTES.ONBOARDING ||
|
||||||
@@ -399,7 +416,8 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
|||||||
pathname === ROUTES.GET_STARTED_INFRASTRUCTURE_MONITORING ||
|
pathname === ROUTES.GET_STARTED_INFRASTRUCTURE_MONITORING ||
|
||||||
pathname === ROUTES.GET_STARTED_LOGS_MANAGEMENT ||
|
pathname === ROUTES.GET_STARTED_LOGS_MANAGEMENT ||
|
||||||
pathname === ROUTES.GET_STARTED_AWS_MONITORING ||
|
pathname === ROUTES.GET_STARTED_AWS_MONITORING ||
|
||||||
pathname === ROUTES.GET_STARTED_AZURE_MONITORING;
|
pathname === ROUTES.GET_STARTED_AZURE_MONITORING ||
|
||||||
|
isPublicDashboard;
|
||||||
|
|
||||||
const [showTrialExpiryBanner, setShowTrialExpiryBanner] = useState(false);
|
const [showTrialExpiryBanner, setShowTrialExpiryBanner] = useState(false);
|
||||||
|
|
||||||
|
|||||||
@@ -57,7 +57,8 @@ describe('Request AWS integration', () => {
|
|||||||
expect(capturedPayload.attributes).toEqual({
|
expect(capturedPayload.attributes).toEqual({
|
||||||
screen: 'AWS integration details',
|
screen: 'AWS integration details',
|
||||||
integration: 's3 sync',
|
integration: 's3 sync',
|
||||||
tenant_url: 'localhost',
|
deployment_url: 'localhost',
|
||||||
|
user_email: null,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { Button, Flex, Switch, Typography } from 'antd';
|
import { Button, Flex, Switch, Typography } from 'antd';
|
||||||
import { BaseOptionType, DefaultOptionType, SelectProps } from 'antd/es/select';
|
import { BaseOptionType, DefaultOptionType, SelectProps } from 'antd/es/select';
|
||||||
import { getInvolvedQueriesInTraceOperator } from 'components/QueryBuilderV2/QueryV2/TraceOperator/utils/utils';
|
import { getInvolvedQueriesInTraceOperator } from 'components/QueryBuilderV2/QueryV2/TraceOperator/utils/utils';
|
||||||
import { Y_AXIS_CATEGORIES } from 'components/YAxisUnitSelector/constants';
|
import { YAxisSource } from 'components/YAxisUnitSelector/types';
|
||||||
|
import { getYAxisCategories } from 'components/YAxisUnitSelector/utils';
|
||||||
import ROUTES from 'constants/routes';
|
import ROUTES from 'constants/routes';
|
||||||
import {
|
import {
|
||||||
AlertThresholdMatchType,
|
AlertThresholdMatchType,
|
||||||
@@ -39,7 +40,8 @@ export function getQueryNames(currentQuery: Query): BaseOptionType[] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function getCategoryByOptionId(id: string): string | undefined {
|
export function getCategoryByOptionId(id: string): string | undefined {
|
||||||
return Y_AXIS_CATEGORIES.find((category) =>
|
const categories = getYAxisCategories(YAxisSource.ALERTS);
|
||||||
|
return categories.find((category) =>
|
||||||
category.units.some((unit) => unit.id === id),
|
category.units.some((unit) => unit.id === id),
|
||||||
)?.name;
|
)?.name;
|
||||||
}
|
}
|
||||||
@@ -47,14 +49,15 @@ export function getCategoryByOptionId(id: string): string | undefined {
|
|||||||
export function getCategorySelectOptionByName(
|
export function getCategorySelectOptionByName(
|
||||||
name: string,
|
name: string,
|
||||||
): DefaultOptionType[] {
|
): DefaultOptionType[] {
|
||||||
|
const categories = getYAxisCategories(YAxisSource.ALERTS);
|
||||||
return (
|
return (
|
||||||
Y_AXIS_CATEGORIES.find((category) => category.name === name)?.units.map(
|
categories
|
||||||
(unit) => ({
|
.find((category) => category.name === name)
|
||||||
|
?.units.map((unit) => ({
|
||||||
label: unit.name,
|
label: unit.name,
|
||||||
value: unit.id,
|
value: unit.id,
|
||||||
'data-testid': `threshold-unit-select-option-${unit.id}`,
|
'data-testid': `threshold-unit-select-option-${unit.id}`,
|
||||||
}),
|
})) || []
|
||||||
) || []
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import YAxisUnitSelector from 'components/YAxisUnitSelector';
|
import YAxisUnitSelector from 'components/YAxisUnitSelector';
|
||||||
|
import { YAxisSource } from 'components/YAxisUnitSelector/types';
|
||||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||||
import { useCreateAlertState } from 'container/CreateAlertV2/context';
|
import { useCreateAlertState } from 'container/CreateAlertV2/context';
|
||||||
import ChartPreviewComponent from 'container/FormAlertRules/ChartPreview';
|
import ChartPreviewComponent from 'container/FormAlertRules/ChartPreview';
|
||||||
@@ -37,6 +38,7 @@ function ChartPreview({ alertDef }: ChartPreviewProps): JSX.Element {
|
|||||||
onChange={(value): void => {
|
onChange={(value): void => {
|
||||||
setAlertState({ type: 'SET_Y_AXIS_UNIT', payload: value });
|
setAlertState({ type: 'SET_Y_AXIS_UNIT', payload: value });
|
||||||
}}
|
}}
|
||||||
|
source={YAxisSource.ALERTS}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -266,7 +266,10 @@ export default function CustomDomainSettings(): JSX.Element {
|
|||||||
<div className="custom-domain-settings-modal-error">
|
<div className="custom-domain-settings-modal-error">
|
||||||
{updateDomainError.status === 409 ? (
|
{updateDomainError.status === 409 ? (
|
||||||
<Alert
|
<Alert
|
||||||
message="You’ve already updated the custom domain once today. To make further changes, please contact our support team for assistance."
|
message={
|
||||||
|
(updateDomainError?.response?.data as { error?: string })?.error ||
|
||||||
|
'You’ve already updated the custom domain once today. To make further changes, please contact our support team for assistance.'
|
||||||
|
}
|
||||||
type="warning"
|
type="warning"
|
||||||
className="update-limit-reached-error"
|
className="update-limit-reached-error"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -138,9 +138,9 @@ function ErrorDetails(props: ErrorDetailsProps): JSX.Element {
|
|||||||
const { formatTimezoneAdjustedTimestamp } = useTimezone();
|
const { formatTimezoneAdjustedTimestamp } = useTimezone();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div className="error-details-container">
|
||||||
<Typography>{errorDetail.exceptionType}</Typography>
|
<Typography.Title level={4}>{errorDetail.exceptionType}</Typography.Title>
|
||||||
<Typography>{errorDetail.exceptionMessage}</Typography>
|
<Typography.Text>{errorDetail.exceptionMessage}</Typography.Text>
|
||||||
<Divider />
|
<Divider />
|
||||||
|
|
||||||
<EventContainer>
|
<EventContainer>
|
||||||
@@ -200,7 +200,7 @@ function ErrorDetails(props: ErrorDetailsProps): JSX.Element {
|
|||||||
<ResizeTable columns={columns} tableLayout="fixed" dataSource={data} />
|
<ResizeTable columns={columns} tableLayout="fixed" dataSource={data} />
|
||||||
</Space>
|
</Space>
|
||||||
</EditorContainer>
|
</EditorContainer>
|
||||||
</>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
|
.error-details-container {
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
.error-container {
|
.error-container {
|
||||||
height: 50vh;
|
height: 50vh;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -304,14 +304,19 @@ function WidgetHeader({
|
|||||||
data-testid="widget-header-search"
|
data-testid="widget-header-search"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<Dropdown menu={menu} trigger={['hover']} placement="bottomRight">
|
|
||||||
<MoreOutlined
|
{menu && Array.isArray(menu.items) && menu.items.length > 0 && (
|
||||||
data-testid="widget-header-options"
|
<Dropdown menu={menu} trigger={['hover']} placement="bottomRight">
|
||||||
className={`widget-header-more-options ${
|
<MoreOutlined
|
||||||
parentHover ? 'widget-header-hover' : ''
|
data-testid="widget-header-options"
|
||||||
} ${globalSearchAvailable ? 'widget-header-more-options-visible' : ''}`}
|
className={`widget-header-more-options ${
|
||||||
/>
|
parentHover ? 'widget-header-hover' : ''
|
||||||
</Dropdown>
|
} ${
|
||||||
|
globalSearchAvailable ? 'widget-header-more-options-visible' : ''
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
</Dropdown>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { TableProps } from 'antd';
|
import { TableProps } from 'antd';
|
||||||
import { PrecisionOption } from 'components/Graph/yAxisConfig';
|
import { PrecisionOption } from 'components/Graph/types';
|
||||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||||
import { LogsExplorerTableProps } from 'container/LogsExplorerTable/LogsExplorerTable.interfaces';
|
import { LogsExplorerTableProps } from 'container/LogsExplorerTable/LogsExplorerTable.interfaces';
|
||||||
import {
|
import {
|
||||||
|
|||||||
@@ -175,7 +175,18 @@ function LiveLogsContainer(): JSX.Element {
|
|||||||
if (isConnectionError && reconnectDueToError) {
|
if (isConnectionError && reconnectDueToError) {
|
||||||
// Small delay to prevent immediate reconnection attempts
|
// Small delay to prevent immediate reconnection attempts
|
||||||
const reconnectTimer = setTimeout(() => {
|
const reconnectTimer = setTimeout(() => {
|
||||||
handleStartNewConnection();
|
const fallbackFilterExpression =
|
||||||
|
prevFilterExpressionRef.current ||
|
||||||
|
currentQuery?.builder.queryData[0]?.filter?.expression?.trim() ||
|
||||||
|
null;
|
||||||
|
|
||||||
|
const validationResult = validateQuery(fallbackFilterExpression || '');
|
||||||
|
|
||||||
|
if (validationResult.isValid) {
|
||||||
|
handleStartNewConnection(fallbackFilterExpression);
|
||||||
|
} else {
|
||||||
|
handleStartNewConnection(null);
|
||||||
|
}
|
||||||
}, 1000);
|
}, 1000);
|
||||||
|
|
||||||
return (): void => clearTimeout(reconnectTimer);
|
return (): void => clearTimeout(reconnectTimer);
|
||||||
@@ -186,6 +197,7 @@ function LiveLogsContainer(): JSX.Element {
|
|||||||
reconnectDueToError,
|
reconnectDueToError,
|
||||||
compositeQuery,
|
compositeQuery,
|
||||||
handleStartNewConnection,
|
handleStartNewConnection,
|
||||||
|
currentQuery,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// clean up the connection when the component unmounts
|
// clean up the connection when the component unmounts
|
||||||
|
|||||||
@@ -7,6 +7,9 @@ import {
|
|||||||
QUERY_BUILDER_FUNCTIONS,
|
QUERY_BUILDER_FUNCTIONS,
|
||||||
} from 'constants/antlrQueryConstants';
|
} from 'constants/antlrQueryConstants';
|
||||||
import { useActiveLog } from 'hooks/logs/useActiveLog';
|
import { useActiveLog } from 'hooks/logs/useActiveLog';
|
||||||
|
import { useNotifications } from 'hooks/useNotifications';
|
||||||
|
import { useCallback } from 'react';
|
||||||
|
import { useCopyToClipboard } from 'react-use';
|
||||||
|
|
||||||
import { TitleWrapper } from './BodyTitleRenderer.styles';
|
import { TitleWrapper } from './BodyTitleRenderer.styles';
|
||||||
import { DROPDOWN_KEY } from './constant';
|
import { DROPDOWN_KEY } from './constant';
|
||||||
@@ -24,6 +27,8 @@ function BodyTitleRenderer({
|
|||||||
value,
|
value,
|
||||||
}: BodyTitleRendererProps): JSX.Element {
|
}: BodyTitleRendererProps): JSX.Element {
|
||||||
const { onAddToQuery } = useActiveLog();
|
const { onAddToQuery } = useActiveLog();
|
||||||
|
const [, setCopy] = useCopyToClipboard();
|
||||||
|
const { notifications } = useNotifications();
|
||||||
|
|
||||||
const filterHandler = (isFilterIn: boolean) => (): void => {
|
const filterHandler = (isFilterIn: boolean) => (): void => {
|
||||||
if (parentIsArray) {
|
if (parentIsArray) {
|
||||||
@@ -75,18 +80,53 @@ function BodyTitleRenderer({
|
|||||||
onClick: onClickHandler,
|
onClick: onClickHandler,
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleTextSelection = (e: React.MouseEvent): void => {
|
const handleNodeClick = useCallback(
|
||||||
// Prevent tree node click when user is trying to select text
|
(e: React.MouseEvent): void => {
|
||||||
e.stopPropagation();
|
// Prevent tree node expansion/collapse
|
||||||
};
|
e.stopPropagation();
|
||||||
|
const cleanedKey = removeObjectFromString(nodeKey);
|
||||||
|
let copyText: string;
|
||||||
|
|
||||||
|
// Check if value is an object or array
|
||||||
|
const isObject = typeof value === 'object' && value !== null;
|
||||||
|
|
||||||
|
if (isObject) {
|
||||||
|
// For objects/arrays, stringify the entire structure
|
||||||
|
copyText = `"${cleanedKey}": ${JSON.stringify(value, null, 2)}`;
|
||||||
|
} else if (parentIsArray) {
|
||||||
|
// For array elements, copy just the value
|
||||||
|
copyText = `"${cleanedKey}": ${value}`;
|
||||||
|
} else {
|
||||||
|
// For primitive values, format as JSON key-value pair
|
||||||
|
const valueStr = typeof value === 'string' ? `"${value}"` : String(value);
|
||||||
|
copyText = `"${cleanedKey}": ${valueStr}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
setCopy(copyText);
|
||||||
|
|
||||||
|
if (copyText) {
|
||||||
|
const notificationMessage = isObject
|
||||||
|
? `${cleanedKey} object copied to clipboard`
|
||||||
|
: `${cleanedKey} copied to clipboard`;
|
||||||
|
|
||||||
|
notifications.success({
|
||||||
|
message: notificationMessage,
|
||||||
|
key: notificationMessage,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[nodeKey, parentIsArray, setCopy, value, notifications],
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TitleWrapper onMouseDown={handleTextSelection}>
|
<TitleWrapper onClick={handleNodeClick}>
|
||||||
<Dropdown menu={menu} trigger={['click']}>
|
{typeof value !== 'object' && (
|
||||||
<SettingOutlined style={{ marginRight: 8 }} className="hover-reveal" />
|
<Dropdown menu={menu} trigger={['click']}>
|
||||||
</Dropdown>
|
<SettingOutlined style={{ marginRight: 8 }} className="hover-reveal" />
|
||||||
|
</Dropdown>
|
||||||
|
)}
|
||||||
{title.toString()}{' '}
|
{title.toString()}{' '}
|
||||||
{!parentIsArray && (
|
{!parentIsArray && typeof value !== 'object' && (
|
||||||
<span>
|
<span>
|
||||||
: <span style={{ color: orange[6] }}>{`${value}`}</span>
|
: <span style={{ color: orange[6] }}>{`${value}`}</span>
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -30,6 +30,8 @@ interface OverviewProps {
|
|||||||
selectedOptions: OptionsQuery;
|
selectedOptions: OptionsQuery;
|
||||||
listViewPanelSelectedFields?: IField[] | null;
|
listViewPanelSelectedFields?: IField[] | null;
|
||||||
onGroupByAttribute?: (fieldKey: string, dataType?: DataTypes) => Promise<void>;
|
onGroupByAttribute?: (fieldKey: string, dataType?: DataTypes) => Promise<void>;
|
||||||
|
onAddColumn?: (fieldName: string) => void;
|
||||||
|
onRemoveColumn?: (fieldName: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
type Props = OverviewProps &
|
type Props = OverviewProps &
|
||||||
@@ -44,6 +46,8 @@ function Overview({
|
|||||||
selectedOptions,
|
selectedOptions,
|
||||||
onGroupByAttribute,
|
onGroupByAttribute,
|
||||||
listViewPanelSelectedFields,
|
listViewPanelSelectedFields,
|
||||||
|
onAddColumn,
|
||||||
|
onRemoveColumn,
|
||||||
}: Props): JSX.Element {
|
}: Props): JSX.Element {
|
||||||
const [isWrapWord, setIsWrapWord] = useState<boolean>(true);
|
const [isWrapWord, setIsWrapWord] = useState<boolean>(true);
|
||||||
const [isSearchVisible, setIsSearchVisible] = useState<boolean>(false);
|
const [isSearchVisible, setIsSearchVisible] = useState<boolean>(false);
|
||||||
@@ -213,6 +217,8 @@ function Overview({
|
|||||||
isListViewPanel={isListViewPanel}
|
isListViewPanel={isListViewPanel}
|
||||||
selectedOptions={selectedOptions}
|
selectedOptions={selectedOptions}
|
||||||
listViewPanelSelectedFields={listViewPanelSelectedFields}
|
listViewPanelSelectedFields={listViewPanelSelectedFields}
|
||||||
|
onAddColumn={onAddColumn}
|
||||||
|
onRemoveColumn={onRemoveColumn}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
@@ -228,6 +234,8 @@ Overview.defaultProps = {
|
|||||||
isListViewPanel: false,
|
isListViewPanel: false,
|
||||||
listViewPanelSelectedFields: null,
|
listViewPanelSelectedFields: null,
|
||||||
onGroupByAttribute: undefined,
|
onGroupByAttribute: undefined,
|
||||||
|
onAddColumn: undefined,
|
||||||
|
onRemoveColumn: undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Overview;
|
export default Overview;
|
||||||
|
|||||||
@@ -48,6 +48,8 @@ interface TableViewProps {
|
|||||||
isListViewPanel?: boolean;
|
isListViewPanel?: boolean;
|
||||||
listViewPanelSelectedFields?: IField[] | null;
|
listViewPanelSelectedFields?: IField[] | null;
|
||||||
onGroupByAttribute?: (fieldKey: string, dataType?: DataTypes) => Promise<void>;
|
onGroupByAttribute?: (fieldKey: string, dataType?: DataTypes) => Promise<void>;
|
||||||
|
onAddColumn?: (fieldName: string) => void;
|
||||||
|
onRemoveColumn?: (fieldName: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
type Props = TableViewProps &
|
type Props = TableViewProps &
|
||||||
@@ -63,6 +65,8 @@ function TableView({
|
|||||||
selectedOptions,
|
selectedOptions,
|
||||||
onGroupByAttribute,
|
onGroupByAttribute,
|
||||||
listViewPanelSelectedFields,
|
listViewPanelSelectedFields,
|
||||||
|
onAddColumn,
|
||||||
|
onRemoveColumn,
|
||||||
}: Props): JSX.Element | null {
|
}: Props): JSX.Element | null {
|
||||||
const dispatch = useDispatch<Dispatch<AppActions>>();
|
const dispatch = useDispatch<Dispatch<AppActions>>();
|
||||||
const [isfilterInLoading, setIsFilterInLoading] = useState<boolean>(false);
|
const [isfilterInLoading, setIsFilterInLoading] = useState<boolean>(false);
|
||||||
@@ -292,6 +296,9 @@ function TableView({
|
|||||||
isfilterOutLoading={isfilterOutLoading}
|
isfilterOutLoading={isfilterOutLoading}
|
||||||
onClickHandler={onClickHandler}
|
onClickHandler={onClickHandler}
|
||||||
onGroupByAttribute={onGroupByAttribute}
|
onGroupByAttribute={onGroupByAttribute}
|
||||||
|
onAddColumn={onAddColumn}
|
||||||
|
onRemoveColumn={onRemoveColumn}
|
||||||
|
selectedOptions={selectedOptions}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
@@ -335,6 +342,8 @@ TableView.defaultProps = {
|
|||||||
isListViewPanel: false,
|
isListViewPanel: false,
|
||||||
listViewPanelSelectedFields: null,
|
listViewPanelSelectedFields: null,
|
||||||
onGroupByAttribute: undefined,
|
onGroupByAttribute: undefined,
|
||||||
|
onAddColumn: undefined,
|
||||||
|
onRemoveColumn: undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface DataType {
|
export interface DataType {
|
||||||
|
|||||||
@@ -11,7 +11,14 @@ import { OPERATORS } from 'constants/queryBuilder';
|
|||||||
import ROUTES from 'constants/routes';
|
import ROUTES from 'constants/routes';
|
||||||
import { RESTRICTED_SELECTED_FIELDS } from 'container/LogsFilters/config';
|
import { RESTRICTED_SELECTED_FIELDS } from 'container/LogsFilters/config';
|
||||||
import { MetricsType } from 'container/MetricsApplication/constant';
|
import { MetricsType } from 'container/MetricsApplication/constant';
|
||||||
import { ArrowDownToDot, ArrowUpFromDot, Ellipsis } from 'lucide-react';
|
import { OptionsQuery } from 'container/OptionsMenu/types';
|
||||||
|
import {
|
||||||
|
ArrowDownToDot,
|
||||||
|
ArrowUpFromDot,
|
||||||
|
Ellipsis,
|
||||||
|
Minus,
|
||||||
|
Plus,
|
||||||
|
} from 'lucide-react';
|
||||||
import { useTimezone } from 'providers/Timezone';
|
import { useTimezone } from 'providers/Timezone';
|
||||||
import React, { useCallback, useMemo, useState } from 'react';
|
import React, { useCallback, useMemo, useState } from 'react';
|
||||||
import { useLocation } from 'react-router-dom';
|
import { useLocation } from 'react-router-dom';
|
||||||
@@ -34,6 +41,9 @@ interface ITableViewActionsProps {
|
|||||||
isfilterInLoading: boolean;
|
isfilterInLoading: boolean;
|
||||||
isfilterOutLoading: boolean;
|
isfilterOutLoading: boolean;
|
||||||
onGroupByAttribute?: (fieldKey: string, dataType?: DataTypes) => Promise<void>;
|
onGroupByAttribute?: (fieldKey: string, dataType?: DataTypes) => Promise<void>;
|
||||||
|
onAddColumn?: (fieldName: string) => void;
|
||||||
|
onRemoveColumn?: (fieldName: string) => void;
|
||||||
|
selectedOptions?: OptionsQuery;
|
||||||
onClickHandler: (
|
onClickHandler: (
|
||||||
operator: string,
|
operator: string,
|
||||||
fieldKey: string,
|
fieldKey: string,
|
||||||
@@ -105,6 +115,7 @@ const BodyContent: React.FC<{
|
|||||||
|
|
||||||
BodyContent.displayName = 'BodyContent';
|
BodyContent.displayName = 'BodyContent';
|
||||||
|
|
||||||
|
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||||
export default function TableViewActions(
|
export default function TableViewActions(
|
||||||
props: ITableViewActionsProps,
|
props: ITableViewActionsProps,
|
||||||
): React.ReactElement {
|
): React.ReactElement {
|
||||||
@@ -116,6 +127,9 @@ export default function TableViewActions(
|
|||||||
isfilterOutLoading,
|
isfilterOutLoading,
|
||||||
onClickHandler,
|
onClickHandler,
|
||||||
onGroupByAttribute,
|
onGroupByAttribute,
|
||||||
|
onAddColumn,
|
||||||
|
onRemoveColumn,
|
||||||
|
selectedOptions,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const { pathname } = useLocation();
|
const { pathname } = useLocation();
|
||||||
@@ -142,6 +156,13 @@ export default function TableViewActions(
|
|||||||
|
|
||||||
const fieldFilterKey = filterKeyForField(fieldData.field);
|
const fieldFilterKey = filterKeyForField(fieldData.field);
|
||||||
|
|
||||||
|
const isFieldInSelectedColumns = useMemo(() => {
|
||||||
|
if (!selectedOptions?.selectColumns) return false;
|
||||||
|
return selectedOptions.selectColumns.some(
|
||||||
|
(col) => col.name === fieldFilterKey,
|
||||||
|
);
|
||||||
|
}, [selectedOptions, fieldFilterKey]);
|
||||||
|
|
||||||
// Memoize textToCopy computation
|
// Memoize textToCopy computation
|
||||||
const textToCopy = useMemo(() => {
|
const textToCopy = useMemo(() => {
|
||||||
let text = fieldData.value;
|
let text = fieldData.value;
|
||||||
@@ -202,9 +223,7 @@ export default function TableViewActions(
|
|||||||
if (record.field === 'body') {
|
if (record.field === 'body') {
|
||||||
return (
|
return (
|
||||||
<div className={cx('value-field', isOpen ? 'open-popover' : '')}>
|
<div className={cx('value-field', isOpen ? 'open-popover' : '')}>
|
||||||
<CopyClipboardHOC entityKey={fieldFilterKey} textToCopy={textToCopy}>
|
<BodyContent fieldData={fieldData} record={record} bodyHtml={bodyHtml} />
|
||||||
<BodyContent fieldData={fieldData} record={record} bodyHtml={bodyHtml} />
|
|
||||||
</CopyClipboardHOC>
|
|
||||||
{!isListViewPanel && !RESTRICTED_SELECTED_FIELDS.includes(fieldFilterKey) && (
|
{!isListViewPanel && !RESTRICTED_SELECTED_FIELDS.includes(fieldFilterKey) && (
|
||||||
<span className="action-btn">
|
<span className="action-btn">
|
||||||
<Tooltip title="Filter for value">
|
<Tooltip title="Filter for value">
|
||||||
@@ -252,6 +271,32 @@ export default function TableViewActions(
|
|||||||
arrow={false}
|
arrow={false}
|
||||||
content={
|
content={
|
||||||
<div>
|
<div>
|
||||||
|
{onAddColumn && !isFieldInSelectedColumns && (
|
||||||
|
<Button
|
||||||
|
className="group-by-clause"
|
||||||
|
type="text"
|
||||||
|
icon={<Plus size={14} />}
|
||||||
|
onClick={(): void => {
|
||||||
|
onAddColumn(fieldFilterKey);
|
||||||
|
setIsOpen(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Add to Columns
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
{onRemoveColumn && isFieldInSelectedColumns && (
|
||||||
|
<Button
|
||||||
|
className="group-by-clause"
|
||||||
|
type="text"
|
||||||
|
icon={<Minus size={14} />}
|
||||||
|
onClick={(): void => {
|
||||||
|
onRemoveColumn(fieldFilterKey);
|
||||||
|
setIsOpen(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Remove from Columns
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
<Button
|
<Button
|
||||||
className="group-by-clause"
|
className="group-by-clause"
|
||||||
type="text"
|
type="text"
|
||||||
@@ -332,6 +377,32 @@ export default function TableViewActions(
|
|||||||
arrow={false}
|
arrow={false}
|
||||||
content={
|
content={
|
||||||
<div>
|
<div>
|
||||||
|
{onAddColumn && !isFieldInSelectedColumns && (
|
||||||
|
<Button
|
||||||
|
className="group-by-clause"
|
||||||
|
type="text"
|
||||||
|
icon={<Plus size={14} />}
|
||||||
|
onClick={(): void => {
|
||||||
|
onAddColumn(fieldFilterKey);
|
||||||
|
setIsOpen(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Add to Columns
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
{onRemoveColumn && isFieldInSelectedColumns && (
|
||||||
|
<Button
|
||||||
|
className="group-by-clause"
|
||||||
|
type="text"
|
||||||
|
icon={<Minus size={14} />}
|
||||||
|
onClick={(): void => {
|
||||||
|
onRemoveColumn(fieldFilterKey);
|
||||||
|
setIsOpen(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Remove from Columns
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
<Button
|
<Button
|
||||||
className="group-by-clause"
|
className="group-by-clause"
|
||||||
type="text"
|
type="text"
|
||||||
@@ -362,4 +433,7 @@ export default function TableViewActions(
|
|||||||
|
|
||||||
TableViewActions.defaultProps = {
|
TableViewActions.defaultProps = {
|
||||||
onGroupByAttribute: undefined,
|
onGroupByAttribute: undefined,
|
||||||
|
onAddColumn: undefined,
|
||||||
|
onRemoveColumn: undefined,
|
||||||
|
selectedOptions: undefined,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import { render, screen } from '@testing-library/react';
|
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
|
||||||
import { RESTRICTED_SELECTED_FIELDS } from 'container/LogsFilters/config';
|
import { RESTRICTED_SELECTED_FIELDS } from 'container/LogsFilters/config';
|
||||||
|
import { LogViewMode } from 'container/LogsTable';
|
||||||
|
import { FontSize } from 'container/OptionsMenu/types';
|
||||||
|
|
||||||
import TableViewActions from '../TableViewActions';
|
import TableViewActions from '../TableViewActions';
|
||||||
|
|
||||||
@@ -33,17 +35,32 @@ jest.mock('react-router-dom', () => ({
|
|||||||
}),
|
}),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
jest.mock('../useAsyncJSONProcessing', () => ({
|
||||||
|
__esModule: true,
|
||||||
|
default: (): {
|
||||||
|
isLoading: boolean;
|
||||||
|
treeData: unknown[] | null;
|
||||||
|
error: string | null;
|
||||||
|
} => ({
|
||||||
|
isLoading: false,
|
||||||
|
treeData: null,
|
||||||
|
error: null,
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
describe('TableViewActions', () => {
|
describe('TableViewActions', () => {
|
||||||
const TEST_VALUE = 'test value';
|
const TEST_VALUE = 'test value';
|
||||||
const ACTION_BUTTON_TEST_ID = '.action-btn';
|
const ACTION_BUTTON_TEST_ID = '.action-btn';
|
||||||
|
const TEST_FIELD = 'test-field';
|
||||||
|
|
||||||
const defaultProps = {
|
const defaultProps = {
|
||||||
fieldData: {
|
fieldData: {
|
||||||
field: 'test-field',
|
field: TEST_FIELD,
|
||||||
value: TEST_VALUE,
|
value: TEST_VALUE,
|
||||||
},
|
},
|
||||||
record: {
|
record: {
|
||||||
key: 'test-key',
|
key: 'test-key',
|
||||||
field: 'test-field',
|
field: TEST_FIELD,
|
||||||
value: TEST_VALUE,
|
value: TEST_VALUE,
|
||||||
},
|
},
|
||||||
isListViewPanel: false,
|
isListViewPanel: false,
|
||||||
@@ -127,4 +144,134 @@ describe('TableViewActions', () => {
|
|||||||
container.querySelector(ACTION_BUTTON_TEST_ID),
|
container.querySelector(ACTION_BUTTON_TEST_ID),
|
||||||
).not.toBeInTheDocument();
|
).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('Add/Remove Column functionality', () => {
|
||||||
|
const ADD_TO_COLUMNS_TEXT = 'Add to Columns';
|
||||||
|
const REMOVE_FROM_COLUMNS_TEXT = 'Remove from Columns';
|
||||||
|
|
||||||
|
const getEllipsisButton = (container: HTMLElement): HTMLElement => {
|
||||||
|
const buttons = container.querySelectorAll('.filter-btn.periscope-btn');
|
||||||
|
return buttons[buttons.length - 1] as HTMLElement;
|
||||||
|
};
|
||||||
|
|
||||||
|
const defaultSelectedOptions = {
|
||||||
|
selectColumns: [],
|
||||||
|
maxLines: 1,
|
||||||
|
format: 'table' as LogViewMode,
|
||||||
|
fontSize: FontSize.MEDIUM,
|
||||||
|
};
|
||||||
|
|
||||||
|
it('shows Add to Columns button when field is not selected', async () => {
|
||||||
|
const onAddColumn = jest.fn();
|
||||||
|
const { container } = render(
|
||||||
|
<TableViewActions
|
||||||
|
fieldData={defaultProps.fieldData}
|
||||||
|
record={defaultProps.record}
|
||||||
|
isListViewPanel={defaultProps.isListViewPanel}
|
||||||
|
isfilterInLoading={defaultProps.isfilterInLoading}
|
||||||
|
isfilterOutLoading={defaultProps.isfilterOutLoading}
|
||||||
|
onClickHandler={defaultProps.onClickHandler}
|
||||||
|
onGroupByAttribute={defaultProps.onGroupByAttribute}
|
||||||
|
onAddColumn={onAddColumn}
|
||||||
|
selectedOptions={defaultSelectedOptions}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
const ellipsisButton = getEllipsisButton(container);
|
||||||
|
fireEvent.mouseOver(ellipsisButton);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText(ADD_TO_COLUMNS_TEXT)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it(`calls onAddColumn with correct field key when ${ADD_TO_COLUMNS_TEXT} is clicked`, async () => {
|
||||||
|
const onAddColumn = jest.fn();
|
||||||
|
const { container } = render(
|
||||||
|
<TableViewActions
|
||||||
|
fieldData={defaultProps.fieldData}
|
||||||
|
record={defaultProps.record}
|
||||||
|
isListViewPanel={defaultProps.isListViewPanel}
|
||||||
|
isfilterInLoading={defaultProps.isfilterInLoading}
|
||||||
|
isfilterOutLoading={defaultProps.isfilterOutLoading}
|
||||||
|
onClickHandler={defaultProps.onClickHandler}
|
||||||
|
onGroupByAttribute={defaultProps.onGroupByAttribute}
|
||||||
|
onAddColumn={onAddColumn}
|
||||||
|
selectedOptions={defaultSelectedOptions}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
const ellipsisButton = getEllipsisButton(container);
|
||||||
|
fireEvent.mouseOver(ellipsisButton);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText(ADD_TO_COLUMNS_TEXT)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
const addButton = screen.getByText(ADD_TO_COLUMNS_TEXT);
|
||||||
|
fireEvent.click(addButton);
|
||||||
|
|
||||||
|
expect(onAddColumn).toHaveBeenCalledWith(TEST_FIELD);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows Remove from Columns button when field is already selected', async () => {
|
||||||
|
const onRemoveColumn = jest.fn();
|
||||||
|
const { container } = render(
|
||||||
|
<TableViewActions
|
||||||
|
fieldData={defaultProps.fieldData}
|
||||||
|
record={defaultProps.record}
|
||||||
|
isListViewPanel={defaultProps.isListViewPanel}
|
||||||
|
isfilterInLoading={defaultProps.isfilterInLoading}
|
||||||
|
isfilterOutLoading={defaultProps.isfilterOutLoading}
|
||||||
|
onClickHandler={defaultProps.onClickHandler}
|
||||||
|
onGroupByAttribute={defaultProps.onGroupByAttribute}
|
||||||
|
onRemoveColumn={onRemoveColumn}
|
||||||
|
selectedOptions={{
|
||||||
|
...defaultSelectedOptions,
|
||||||
|
selectColumns: [{ name: TEST_FIELD }],
|
||||||
|
}}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
const ellipsisButton = getEllipsisButton(container);
|
||||||
|
fireEvent.mouseOver(ellipsisButton);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText(REMOVE_FROM_COLUMNS_TEXT)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
expect(screen.queryByText(ADD_TO_COLUMNS_TEXT)).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it(`calls onRemoveColumn with correct field key when ${REMOVE_FROM_COLUMNS_TEXT} is clicked`, async () => {
|
||||||
|
const onRemoveColumn = jest.fn();
|
||||||
|
const { container } = render(
|
||||||
|
<TableViewActions
|
||||||
|
fieldData={defaultProps.fieldData}
|
||||||
|
record={defaultProps.record}
|
||||||
|
isListViewPanel={defaultProps.isListViewPanel}
|
||||||
|
isfilterInLoading={defaultProps.isfilterInLoading}
|
||||||
|
isfilterOutLoading={defaultProps.isfilterOutLoading}
|
||||||
|
onClickHandler={defaultProps.onClickHandler}
|
||||||
|
onGroupByAttribute={defaultProps.onGroupByAttribute}
|
||||||
|
onRemoveColumn={onRemoveColumn}
|
||||||
|
selectedOptions={{
|
||||||
|
...defaultSelectedOptions,
|
||||||
|
selectColumns: [{ name: TEST_FIELD }],
|
||||||
|
}}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
const ellipsisButton = getEllipsisButton(container);
|
||||||
|
fireEvent.mouseOver(ellipsisButton);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText('Remove from Columns')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
const removeButton = screen.getByText(REMOVE_FROM_COLUMNS_TEXT);
|
||||||
|
fireEvent.click(removeButton);
|
||||||
|
|
||||||
|
expect(onRemoveColumn).toHaveBeenCalledWith(TEST_FIELD);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,109 @@
|
|||||||
|
import { render, screen, userEvent, waitFor } from 'tests/test-utils';
|
||||||
|
|
||||||
|
import BodyTitleRenderer from '../BodyTitleRenderer';
|
||||||
|
|
||||||
|
let mockSetCopy: jest.Mock;
|
||||||
|
const mockNotification = jest.fn();
|
||||||
|
|
||||||
|
jest.mock('hooks/logs/useActiveLog', () => ({
|
||||||
|
useActiveLog: (): any => ({
|
||||||
|
onAddToQuery: jest.fn(),
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock('react-use', () => ({
|
||||||
|
useCopyToClipboard: (): any => {
|
||||||
|
mockSetCopy = jest.fn();
|
||||||
|
return [{ value: null }, mockSetCopy];
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock('hooks/useNotifications', () => ({
|
||||||
|
useNotifications: (): any => ({
|
||||||
|
notifications: {
|
||||||
|
success: mockNotification,
|
||||||
|
error: jest.fn(),
|
||||||
|
info: jest.fn(),
|
||||||
|
warning: jest.fn(),
|
||||||
|
open: jest.fn(),
|
||||||
|
destroy: jest.fn(),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('BodyTitleRenderer', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should copy primitive value when node is clicked', async () => {
|
||||||
|
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||||
|
|
||||||
|
render(
|
||||||
|
<BodyTitleRenderer
|
||||||
|
title="name"
|
||||||
|
nodeKey="user.name"
|
||||||
|
value="John"
|
||||||
|
parentIsArray={false}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
await user.click(screen.getByText('name'));
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(mockSetCopy).toHaveBeenCalledWith('"user.name": "John"');
|
||||||
|
expect(mockNotification).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
message: expect.stringContaining('user.name'),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should copy array element value when clicked', async () => {
|
||||||
|
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||||
|
|
||||||
|
render(
|
||||||
|
<BodyTitleRenderer
|
||||||
|
title="0"
|
||||||
|
nodeKey="items[*].0"
|
||||||
|
value="arrayElement"
|
||||||
|
parentIsArray
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
await user.click(screen.getByText('0'));
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(mockSetCopy).toHaveBeenCalledWith('"items[*].0": arrayElement');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should copy entire object when object node is clicked', async () => {
|
||||||
|
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||||
|
const testObject = { id: 123, active: true };
|
||||||
|
|
||||||
|
render(
|
||||||
|
<BodyTitleRenderer
|
||||||
|
title="metadata"
|
||||||
|
nodeKey="user.metadata"
|
||||||
|
value={testObject}
|
||||||
|
parentIsArray={false}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
await user.click(screen.getByText('metadata'));
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
const callArg = mockSetCopy.mock.calls[0][0];
|
||||||
|
expect(callArg).toContain('"user.metadata":');
|
||||||
|
expect(callArg).toContain('"id": 123');
|
||||||
|
expect(callArg).toContain('"active": true');
|
||||||
|
expect(mockNotification).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
message: expect.stringContaining('object copied'),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -39,9 +39,17 @@ export const computeDataNode = (
|
|||||||
valueIsArray: boolean,
|
valueIsArray: boolean,
|
||||||
value: unknown,
|
value: unknown,
|
||||||
nodeKey: string,
|
nodeKey: string,
|
||||||
|
parentIsArray: boolean,
|
||||||
): DataNode => ({
|
): DataNode => ({
|
||||||
key: uniqueId(),
|
key: uniqueId(),
|
||||||
title: `${key} ${valueIsArray ? '[...]' : ''}`,
|
title: (
|
||||||
|
<BodyTitleRenderer
|
||||||
|
title={`${key} ${valueIsArray ? '[...]' : ''}`}
|
||||||
|
nodeKey={nodeKey}
|
||||||
|
value={value}
|
||||||
|
parentIsArray={parentIsArray}
|
||||||
|
/>
|
||||||
|
),
|
||||||
// eslint-disable-next-line @typescript-eslint/no-use-before-define
|
// eslint-disable-next-line @typescript-eslint/no-use-before-define
|
||||||
children: jsonToDataNodes(
|
children: jsonToDataNodes(
|
||||||
value as Record<string, unknown>,
|
value as Record<string, unknown>,
|
||||||
@@ -67,7 +75,7 @@ export function jsonToDataNodes(
|
|||||||
|
|
||||||
if (parentIsArray) {
|
if (parentIsArray) {
|
||||||
if (typeof value === 'object' && value !== null) {
|
if (typeof value === 'object' && value !== null) {
|
||||||
return computeDataNode(key, valueIsArray, value, nodeKey);
|
return computeDataNode(key, valueIsArray, value, nodeKey, parentIsArray);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -85,7 +93,7 @@ export function jsonToDataNodes(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (typeof value === 'object' && value !== null) {
|
if (typeof value === 'object' && value !== null) {
|
||||||
return computeDataNode(key, valueIsArray, value, nodeKey);
|
return computeDataNode(key, valueIsArray, value, nodeKey, parentIsArray);
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
key: uniqueId(),
|
key: uniqueId(),
|
||||||
|
|||||||
@@ -274,6 +274,7 @@ function Login(): JSX.Element {
|
|||||||
autoFocus
|
autoFocus
|
||||||
disabled={versionLoading}
|
disabled={versionLoading}
|
||||||
className="login-form-input"
|
className="login-form-input"
|
||||||
|
onPressEnter={onNextHandler}
|
||||||
/>
|
/>
|
||||||
</FormContainer.Item>
|
</FormContainer.Item>
|
||||||
</ParentContainer>
|
</ParentContainer>
|
||||||
|
|||||||
@@ -209,6 +209,15 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.time-series-view-container {
|
||||||
|
.time-series-view-container-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-start;
|
||||||
|
align-items: center;
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,11 +8,7 @@ import { ENTITY_VERSION_V5 } from 'constants/app';
|
|||||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||||
import { AVAILABLE_EXPORT_PANEL_TYPES } from 'constants/panelTypes';
|
import { AVAILABLE_EXPORT_PANEL_TYPES } from 'constants/panelTypes';
|
||||||
import { QueryParams } from 'constants/query';
|
import { QueryParams } from 'constants/query';
|
||||||
import {
|
import { initialFilters, PANEL_TYPES } from 'constants/queryBuilder';
|
||||||
initialFilters,
|
|
||||||
initialQueriesMap,
|
|
||||||
PANEL_TYPES,
|
|
||||||
} from 'constants/queryBuilder';
|
|
||||||
import { DEFAULT_PER_PAGE_VALUE } from 'container/Controls/config';
|
import { DEFAULT_PER_PAGE_VALUE } from 'container/Controls/config';
|
||||||
import ExplorerOptionWrapper from 'container/ExplorerOptions/ExplorerOptionWrapper';
|
import ExplorerOptionWrapper from 'container/ExplorerOptions/ExplorerOptionWrapper';
|
||||||
import { ChangeViewFunctionType } from 'container/ExplorerOptions/types';
|
import { ChangeViewFunctionType } from 'container/ExplorerOptions/types';
|
||||||
@@ -26,6 +22,7 @@ import {
|
|||||||
getListQuery,
|
getListQuery,
|
||||||
getQueryByPanelType,
|
getQueryByPanelType,
|
||||||
} from 'container/LogsExplorerViews/explorerUtils';
|
} from 'container/LogsExplorerViews/explorerUtils';
|
||||||
|
import { BuilderUnitsFilter } from 'container/QueryBuilder/filters/BuilderUnitsFilter';
|
||||||
import TimeSeriesView from 'container/TimeSeriesView/TimeSeriesView';
|
import TimeSeriesView from 'container/TimeSeriesView/TimeSeriesView';
|
||||||
import { useCopyLogLink } from 'hooks/logs/useCopyLogLink';
|
import { useCopyLogLink } from 'hooks/logs/useCopyLogLink';
|
||||||
import { useGetExplorerQueryRange } from 'hooks/queryBuilder/useGetExplorerQueryRange';
|
import { useGetExplorerQueryRange } from 'hooks/queryBuilder/useGetExplorerQueryRange';
|
||||||
@@ -101,12 +98,7 @@ function LogsExplorerViewsContainer({
|
|||||||
const currentMinTimeRef = useRef<number>(minTime);
|
const currentMinTimeRef = useRef<number>(minTime);
|
||||||
|
|
||||||
// Context
|
// Context
|
||||||
const {
|
const { stagedQuery, panelType } = useQueryBuilder();
|
||||||
currentQuery,
|
|
||||||
stagedQuery,
|
|
||||||
panelType,
|
|
||||||
updateAllQueriesOperators,
|
|
||||||
} = useQueryBuilder();
|
|
||||||
|
|
||||||
const selectedPanelType = panelType || PANEL_TYPES.LIST;
|
const selectedPanelType = panelType || PANEL_TYPES.LIST;
|
||||||
|
|
||||||
@@ -119,6 +111,8 @@ function LogsExplorerViewsContainer({
|
|||||||
|
|
||||||
const [orderBy, setOrderBy] = useState<string>('timestamp:desc');
|
const [orderBy, setOrderBy] = useState<string>('timestamp:desc');
|
||||||
|
|
||||||
|
const [yAxisUnit, setYAxisUnit] = useState<string>('');
|
||||||
|
|
||||||
const listQuery = useMemo(() => getListQuery(stagedQuery) || null, [
|
const listQuery = useMemo(() => getListQuery(stagedQuery) || null, [
|
||||||
stagedQuery,
|
stagedQuery,
|
||||||
]);
|
]);
|
||||||
@@ -136,13 +130,8 @@ function LogsExplorerViewsContainer({
|
|||||||
}, [stagedQuery, activeLogId]);
|
}, [stagedQuery, activeLogId]);
|
||||||
|
|
||||||
const exportDefaultQuery = useMemo(
|
const exportDefaultQuery = useMemo(
|
||||||
() =>
|
() => getExportQueryData(requestData, selectedPanelType),
|
||||||
updateAllQueriesOperators(
|
[selectedPanelType, requestData],
|
||||||
currentQuery || initialQueriesMap.logs,
|
|
||||||
selectedPanelType,
|
|
||||||
DataSource.LOGS,
|
|
||||||
),
|
|
||||||
[currentQuery, selectedPanelType, updateAllQueriesOperators],
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@@ -279,9 +268,7 @@ function LogsExplorerViewsContainer({
|
|||||||
|
|
||||||
const widgetId = v4();
|
const widgetId = v4();
|
||||||
|
|
||||||
const query = getExportQueryData(requestData, selectedPanelType);
|
if (!exportDefaultQuery) return;
|
||||||
|
|
||||||
if (!query) return;
|
|
||||||
|
|
||||||
logEvent('Logs Explorer: Add to dashboard successful', {
|
logEvent('Logs Explorer: Add to dashboard successful', {
|
||||||
panelType: selectedPanelType,
|
panelType: selectedPanelType,
|
||||||
@@ -290,7 +277,7 @@ function LogsExplorerViewsContainer({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const dashboardEditView = generateExportToDashboardLink({
|
const dashboardEditView = generateExportToDashboardLink({
|
||||||
query,
|
query: exportDefaultQuery,
|
||||||
panelType: panelTypeParam,
|
panelType: panelTypeParam,
|
||||||
dashboardId: dashboard.id,
|
dashboardId: dashboard.id,
|
||||||
widgetId,
|
widgetId,
|
||||||
@@ -298,7 +285,7 @@ function LogsExplorerViewsContainer({
|
|||||||
|
|
||||||
safeNavigate(dashboardEditView);
|
safeNavigate(dashboardEditView);
|
||||||
},
|
},
|
||||||
[safeNavigate, requestData, selectedPanelType],
|
[safeNavigate, exportDefaultQuery, selectedPanelType],
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -366,6 +353,10 @@ function LogsExplorerViewsContainer({
|
|||||||
orderBy,
|
orderBy,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
const onUnitChangeHandler = useCallback((value: string): void => {
|
||||||
|
setYAxisUnit(value);
|
||||||
|
}, []);
|
||||||
|
|
||||||
const chartData = useMemo(() => {
|
const chartData = useMemo(() => {
|
||||||
if (!stagedQuery) return [];
|
if (!stagedQuery) return [];
|
||||||
|
|
||||||
@@ -473,15 +464,24 @@ function LogsExplorerViewsContainer({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{selectedPanelType === PANEL_TYPES.TIME_SERIES && !showLiveLogs && (
|
{selectedPanelType === PANEL_TYPES.TIME_SERIES && !showLiveLogs && (
|
||||||
<TimeSeriesView
|
<div className="time-series-view-container">
|
||||||
isLoading={isLoading || isFetching}
|
<div className="time-series-view-container-header">
|
||||||
data={data}
|
<BuilderUnitsFilter
|
||||||
isError={isError}
|
onChange={onUnitChangeHandler}
|
||||||
error={error as APIError}
|
yAxisUnit={yAxisUnit}
|
||||||
isFilterApplied={!isEmpty(listQuery?.filters?.items)}
|
/>
|
||||||
dataSource={DataSource.LOGS}
|
</div>
|
||||||
setWarning={setWarning}
|
<TimeSeriesView
|
||||||
/>
|
isLoading={isLoading || isFetching}
|
||||||
|
data={data}
|
||||||
|
isError={isError}
|
||||||
|
error={error as APIError}
|
||||||
|
yAxisUnit={yAxisUnit}
|
||||||
|
isFilterApplied={!isEmpty(listQuery?.filters?.items)}
|
||||||
|
dataSource={DataSource.LOGS}
|
||||||
|
setWarning={setWarning}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{selectedPanelType === PANEL_TYPES.TABLE && !showLiveLogs && (
|
{selectedPanelType === PANEL_TYPES.TABLE && !showLiveLogs && (
|
||||||
|
|||||||