Compare commits
1 Commits
feat/cmd-c
...
enh/dashbo
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
17dec71695 |
@@ -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.12
|
image: signoz/signoz-schema-migrator:v0.129.11
|
||||||
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.12
|
image: signoz/signoz-schema-migrator:v0.129.11
|
||||||
container_name: schema-migrator-async
|
container_name: schema-migrator-async
|
||||||
command:
|
command:
|
||||||
- async
|
- async
|
||||||
|
|||||||
42
.github/CODEOWNERS
vendored
@@ -3,10 +3,46 @@
|
|||||||
# 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
|
||||||
|
|
||||||
# Onboarding
|
# Dashboard, Alert, Metrics, Service Map, Services
|
||||||
/frontend/src/container/OnboardingV2Container/onboarding-configs/onboarding-config-with-links.json @makeavish
|
/frontend/src/container/ListOfDashboard/ @srikanthccv
|
||||||
/frontend/src/container/OnboardingV2Container/AddDataSource/AddDataSource.tsx @makeavish
|
/frontend/src/container/NewDashboard/ @srikanthccv
|
||||||
|
/frontend/src/pages/DashboardsListPage/ @srikanthccv
|
||||||
|
/frontend/src/pages/DashboardWidget/ @srikanthccv
|
||||||
|
/frontend/src/pages/NewDashboard/ @srikanthccv
|
||||||
|
/frontend/src/providers/Dashboard/ @srikanthccv
|
||||||
|
|
||||||
|
# Alerts
|
||||||
|
/frontend/src/container/AlertHistory/ @srikanthccv
|
||||||
|
/frontend/src/container/AllAlertChannels/ @srikanthccv
|
||||||
|
/frontend/src/container/AnomalyAlertEvaluationView/ @srikanthccv
|
||||||
|
/frontend/src/container/CreateAlertChannels/ @srikanthccv
|
||||||
|
/frontend/src/container/CreateAlertRule/ @srikanthccv
|
||||||
|
/frontend/src/container/EditAlertChannels/ @srikanthccv
|
||||||
|
/frontend/src/container/FormAlertChannels/ @srikanthccv
|
||||||
|
/frontend/src/container/FormAlertRules/ @srikanthccv
|
||||||
|
/frontend/src/container/ListAlertRules/ @srikanthccv
|
||||||
|
/frontend/src/container/TriggeredAlerts/ @srikanthccv
|
||||||
|
/frontend/src/pages/AlertChannelCreate/ @srikanthccv
|
||||||
|
/frontend/src/pages/AlertDetails/ @srikanthccv
|
||||||
|
/frontend/src/pages/AlertHistory/ @srikanthccv
|
||||||
|
/frontend/src/pages/AlertList/ @srikanthccv
|
||||||
|
/frontend/src/pages/CreateAlert/ @srikanthccv
|
||||||
|
/frontend/src/providers/Alert.tsx @srikanthccv
|
||||||
|
|
||||||
|
# Metrics
|
||||||
|
/frontend/src/container/MetricsExplorer/ @srikanthccv
|
||||||
|
/frontend/src/pages/MetricsApplication/ @srikanthccv
|
||||||
|
/frontend/src/pages/MetricsExplorer/ @srikanthccv
|
||||||
|
|
||||||
|
# Services and Service Map
|
||||||
|
/frontend/src/container/ServiceApplication/ @srikanthccv
|
||||||
|
/frontend/src/container/ServiceTable/ @srikanthccv
|
||||||
|
/frontend/src/pages/Services/ @srikanthccv
|
||||||
|
/frontend/src/pages/ServiceTopLevelOperations/ @srikanthccv
|
||||||
|
/frontend/src/container/Home/Services/ @srikanthccv
|
||||||
|
|
||||||
/deploy/ @SigNoz/devops
|
/deploy/ @SigNoz/devops
|
||||||
.github @SigNoz/devops
|
.github @SigNoz/devops
|
||||||
|
|||||||
1
.github/workflows/build-enterprise.yaml
vendored
@@ -69,7 +69,6 @@ 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,7 +68,6 @@ 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,7 +35,6 @@ 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
|
||||||
|
|||||||
1
.github/workflows/integrationci.yaml
vendored
@@ -18,7 +18,6 @@ jobs:
|
|||||||
- passwordauthn
|
- passwordauthn
|
||||||
- callbackauthn
|
- callbackauthn
|
||||||
- cloudintegrations
|
- cloudintegrations
|
||||||
- dashboard
|
|
||||||
- querier
|
- querier
|
||||||
- ttl
|
- ttl
|
||||||
sqlstore-provider:
|
sqlstore-provider:
|
||||||
|
|||||||
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 server
|
$(GO_BUILD_CONTEXT_ENTERPRISE)/*.go
|
||||||
|
|
||||||
.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:
|
||||||
# Max items for the in-memory cache (10x the entries)
|
# Time-to-live for cache entries in memory. Specify the duration in ns
|
||||||
num_counters: 100000
|
ttl: 60000000000
|
||||||
# Total cost in bytes allocated bounded cache
|
# The interval at which the cache will be cleaned up
|
||||||
max_cost: 67108864
|
cleanup_interval: 1m
|
||||||
# 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.104.0
|
image: signoz/signoz:v0.102.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.12
|
image: signoz/signoz-otel-collector:v0.129.11
|
||||||
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.12
|
image: signoz/signoz-schema-migrator:v0.129.11
|
||||||
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.104.0
|
image: signoz/signoz:v0.102.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.12
|
image: signoz/signoz-otel-collector:v0.129.11
|
||||||
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.12
|
image: signoz/signoz-schema-migrator:v0.129.11
|
||||||
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.104.0}
|
image: signoz/signoz:${VERSION:-v0.102.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.12}
|
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-v0.129.11}
|
||||||
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.12}
|
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-v0.129.11}
|
||||||
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.12}
|
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-v0.129.11}
|
||||||
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.104.0}
|
image: signoz/signoz:${VERSION:-v0.102.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.12}
|
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-v0.129.11}
|
||||||
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.12}
|
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-v0.129.11}
|
||||||
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.12}
|
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-v0.129.11}
|
||||||
container_name: schema-migrator-async
|
container_name: schema-migrator-async
|
||||||
command:
|
command:
|
||||||
- async
|
- async
|
||||||
|
|||||||
@@ -129,12 +129,6 @@ 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,14 +99,6 @@ 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,7 +19,6 @@ 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"
|
||||||
@@ -61,7 +60,6 @@ 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,7 +9,6 @@ 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"
|
||||||
@@ -75,26 +74,13 @@ 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,9 +246,7 @@ func (r *AnomalyRule) buildAndRunQuery(ctx context.Context, orgID valuer.UUID, t
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
results, err := r.Threshold.Eval(*series, r.Unit(), ruletypes.EvalData{
|
results, err := r.Threshold.ShouldAlert(*series, r.Unit())
|
||||||
ActiveAlerts: r.ActiveAlertsLabelFP(),
|
|
||||||
})
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -298,9 +296,7 @@ func (r *AnomalyRule) buildAndRunQueryV5(ctx context.Context, orgID valuer.UUID,
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
results, err := r.Threshold.Eval(*series, r.Unit(), ruletypes.EvalData{
|
results, err := r.Threshold.ShouldAlert(*series, r.Unit())
|
||||||
ActiveAlerts: r.ActiveAlertsLabelFP(),
|
|
||||||
})
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -414,7 +410,6 @@ 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,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -427,9 +422,6 @@ 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]
|
||||||
}
|
}
|
||||||
@@ -488,30 +480,6 @@ 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()
|
||||||
|
|||||||
@@ -30,8 +30,6 @@ func (formatter Formatter) DataTypeOf(dataType string) sqlschema.DataType {
|
|||||||
return sqlschema.DataTypeBoolean
|
return sqlschema.DataTypeBoolean
|
||||||
case "VARCHAR", "CHARACTER VARYING", "CHARACTER":
|
case "VARCHAR", "CHARACTER VARYING", "CHARACTER":
|
||||||
return sqlschema.DataTypeText
|
return sqlschema.DataTypeText
|
||||||
case "BYTEA":
|
|
||||||
return sqlschema.DataTypeBytea
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return formatter.Formatter.DataTypeOf(dataType)
|
return formatter.Formatter.DataTypeOf(dataType)
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
ignorePatterns: ['src/parser/*.ts', 'scripts/update-registry.js'],
|
ignorePatterns: ['src/parser/*.ts'],
|
||||||
env: {
|
env: {
|
||||||
browser: true,
|
browser: true,
|
||||||
es2021: true,
|
es2021: true,
|
||||||
|
|||||||
@@ -3,6 +3,5 @@ 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) && node scripts/update-registry.js",
|
"postinstall": "yarn i18n:generate-hash && (is-ci || yarn husky:configure)",
|
||||||
"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.55.1",
|
"@playwright/test": "1.54.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,8 +47,6 @@
|
|||||||
"@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/command": "0.0.0",
|
|
||||||
"@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",
|
||||||
@@ -85,7 +83,6 @@
|
|||||||
"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",
|
||||||
@@ -105,6 +102,7 @@
|
|||||||
"i18next-http-backend": "^1.3.2",
|
"i18next-http-backend": "^1.3.2",
|
||||||
"jest": "^27.5.1",
|
"jest": "^27.5.1",
|
||||||
"js-base64": "^3.7.2",
|
"js-base64": "^3.7.2",
|
||||||
|
"kbar": "0.1.0-beta.48",
|
||||||
"less": "^4.1.2",
|
"less": "^4.1.2",
|
||||||
"less-loader": "^10.2.0",
|
"less-loader": "^10.2.0",
|
||||||
"lodash-es": "^4.17.21",
|
"lodash-es": "^4.17.21",
|
||||||
@@ -114,7 +112,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.298.0",
|
"posthog-js": "1.215.5",
|
||||||
"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",
|
||||||
@@ -151,6 +149,7 @@
|
|||||||
"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",
|
||||||
@@ -187,7 +186,6 @@
|
|||||||
"@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",
|
||||||
|
|||||||
BIN
frontend/public/Images/grains.png
Normal file
|
After Width: | Height: | Size: 98 KiB |
@@ -1 +0,0 @@
|
|||||||
<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>
|
|
||||||
|
Before Width: | Height: | Size: 3.0 KiB |
|
Before Width: | Height: | Size: 20 KiB |
@@ -1 +0,0 @@
|
|||||||
<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>
|
|
||||||
|
Before Width: | Height: | Size: 1.3 KiB |
@@ -1 +0,0 @@
|
|||||||
<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>
|
|
||||||
|
Before Width: | Height: | Size: 1.7 KiB |
@@ -1 +0,0 @@
|
|||||||
<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>
|
|
||||||
|
Before Width: | Height: | Size: 826 B |
@@ -1 +0,0 @@
|
|||||||
<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>
|
|
||||||
|
Before Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 6.0 KiB |
|
Before Width: | Height: | Size: 5.5 KiB |
|
Before Width: | Height: | Size: 19 KiB |
@@ -1 +0,0 @@
|
|||||||
<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>
|
|
||||||
|
Before Width: | Height: | Size: 726 B |
@@ -1 +0,0 @@
|
|||||||
<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>
|
|
||||||
|
Before Width: | Height: | Size: 733 B |
@@ -1 +0,0 @@
|
|||||||
<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>
|
|
||||||
|
Before Width: | Height: | Size: 2.1 KiB |
@@ -1 +0,0 @@
|
|||||||
<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>
|
|
||||||
|
Before Width: | Height: | Size: 431 B |
@@ -1,50 +0,0 @@
|
|||||||
/* 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,14 +245,6 @@ 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;
|
||||||
|
|||||||
@@ -4,15 +4,14 @@ import getLocalStorageApi from 'api/browser/localstorage/get';
|
|||||||
import setLocalStorageApi from 'api/browser/localstorage/set';
|
import setLocalStorageApi from 'api/browser/localstorage/set';
|
||||||
import logEvent from 'api/common/logEvent';
|
import logEvent from 'api/common/logEvent';
|
||||||
import AppLoading from 'components/AppLoading/AppLoading';
|
import AppLoading from 'components/AppLoading/AppLoading';
|
||||||
import { CmdKPalette } from 'components/cmdKPalette/cmdKPalette';
|
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';
|
||||||
@@ -22,17 +21,19 @@ 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 { CmdKProvider } from 'providers/cmdKProvider';
|
|
||||||
import { DashboardProvider } from 'providers/Dashboard/Dashboard';
|
import { DashboardProvider } from 'providers/Dashboard/Dashboard';
|
||||||
import { ErrorModalProvider } from 'providers/ErrorModalProvider';
|
import { ErrorModalProvider } from 'providers/ErrorModalProvider';
|
||||||
|
import { KBarCommandPaletteProvider } from 'providers/KBarCommandPaletteProvider';
|
||||||
import { PreferenceContextProvider } from 'providers/preferences/context/PreferenceContextProvider';
|
import { PreferenceContextProvider } from 'providers/preferences/context/PreferenceContextProvider';
|
||||||
import { QueryBuilderProvider } from 'providers/QueryBuilder';
|
import { QueryBuilderProvider } from 'providers/QueryBuilder';
|
||||||
import { Suspense, useCallback, useEffect, useState } from 'react';
|
import { Suspense, useCallback, useEffect, useState } from 'react';
|
||||||
import { Route, Router, Switch } from 'react-router-dom';
|
import { Route, Router, Switch } from 'react-router-dom';
|
||||||
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';
|
||||||
@@ -83,9 +84,9 @@ function App(): JSX.Element {
|
|||||||
email,
|
email,
|
||||||
name: displayName,
|
name: displayName,
|
||||||
company_name: orgName,
|
company_name: orgName,
|
||||||
deployment_name: hostNameParts[0],
|
tenant_id: hostNameParts[0],
|
||||||
data_region: hostNameParts[1],
|
data_region: hostNameParts[1],
|
||||||
deployment_url: hostname,
|
tenant_url: hostname,
|
||||||
company_domain: domain,
|
company_domain: domain,
|
||||||
source: 'signoz-ui',
|
source: 'signoz-ui',
|
||||||
role,
|
role,
|
||||||
@@ -93,9 +94,9 @@ function App(): JSX.Element {
|
|||||||
|
|
||||||
const groupTraits = {
|
const groupTraits = {
|
||||||
name: orgName,
|
name: orgName,
|
||||||
deployment_name: hostNameParts[0],
|
tenant_id: hostNameParts[0],
|
||||||
data_region: hostNameParts[1],
|
data_region: hostNameParts[1],
|
||||||
deployment_url: hostname,
|
tenant_url: hostname,
|
||||||
company_domain: domain,
|
company_domain: domain,
|
||||||
source: 'signoz-ui',
|
source: 'signoz-ui',
|
||||||
};
|
};
|
||||||
@@ -110,23 +111,37 @@ 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],
|
||||||
deployment_url: hostname,
|
tenant_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,
|
||||||
deployment_name: hostNameParts[0],
|
tenant_id: hostNameParts[0],
|
||||||
data_region: hostNameParts[1],
|
data_region: hostNameParts[1],
|
||||||
deployment_url: hostname,
|
tenant_url: hostname,
|
||||||
company_domain: domain,
|
company_domain: domain,
|
||||||
source: 'signoz-ui',
|
source: 'signoz-ui',
|
||||||
isPaidUser: !!trialInfo?.trialConvertedToSubscription,
|
isPaidUser: !!trialInfo?.trialConvertedToSubscription,
|
||||||
@@ -134,9 +149,9 @@ function App(): JSX.Element {
|
|||||||
|
|
||||||
posthog?.group('company', orgId, {
|
posthog?.group('company', orgId, {
|
||||||
name: orgName,
|
name: orgName,
|
||||||
deployment_name: hostNameParts[0],
|
tenant_id: hostNameParts[0],
|
||||||
data_region: hostNameParts[1],
|
data_region: hostNameParts[1],
|
||||||
deployment_url: hostname,
|
tenant_url: hostname,
|
||||||
company_domain: domain,
|
company_domain: domain,
|
||||||
source: 'signoz-ui',
|
source: 'signoz-ui',
|
||||||
isPaidUser: !!trialInfo?.trialConvertedToSubscription,
|
isPaidUser: !!trialInfo?.trialConvertedToSubscription,
|
||||||
@@ -213,10 +228,7 @@ function App(): JSX.Element {
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (
|
if (pathname === ROUTES.ONBOARDING) {
|
||||||
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');
|
||||||
@@ -258,20 +270,11 @@ 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,
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -305,6 +308,10 @@ 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,
|
||||||
@@ -364,33 +371,36 @@ function App(): JSX.Element {
|
|||||||
<ConfigProvider theme={themeConfig}>
|
<ConfigProvider theme={themeConfig}>
|
||||||
<Router history={history}>
|
<Router history={history}>
|
||||||
<CompatRouter>
|
<CompatRouter>
|
||||||
<CmdKProvider>
|
<KBarCommandPaletteProvider>
|
||||||
|
<UserpilotRouteTracker />
|
||||||
|
<KBarCommandPalette />
|
||||||
<NotificationProvider>
|
<NotificationProvider>
|
||||||
<ErrorModalProvider>
|
<ErrorModalProvider>
|
||||||
{isLoggedInState && <CmdKPalette userRole={user.role} />}
|
|
||||||
<PrivateRoute>
|
<PrivateRoute>
|
||||||
<ResourceProvider>
|
<ResourceProvider>
|
||||||
<QueryBuilderProvider>
|
<QueryBuilderProvider>
|
||||||
<DashboardProvider>
|
<DashboardProvider>
|
||||||
<KeyboardHotkeysProvider>
|
<KeyboardHotkeysProvider>
|
||||||
<AppLayout>
|
<AlertRuleProvider>
|
||||||
<PreferenceContextProvider>
|
<AppLayout>
|
||||||
<Suspense fallback={<Spinner size="large" tip="Loading..." />}>
|
<PreferenceContextProvider>
|
||||||
<Switch>
|
<Suspense fallback={<Spinner size="large" tip="Loading..." />}>
|
||||||
{routes.map(({ path, component, exact }) => (
|
<Switch>
|
||||||
<Route
|
{routes.map(({ path, component, exact }) => (
|
||||||
key={`${path}`}
|
<Route
|
||||||
exact={exact}
|
key={`${path}`}
|
||||||
path={path}
|
exact={exact}
|
||||||
component={component}
|
path={path}
|
||||||
/>
|
component={component}
|
||||||
))}
|
/>
|
||||||
<Route exact path="/" component={Home} />
|
))}
|
||||||
<Route path="*" component={NotFound} />
|
<Route exact path="/" component={Home} />
|
||||||
</Switch>
|
<Route path="*" component={NotFound} />
|
||||||
</Suspense>
|
</Switch>
|
||||||
</PreferenceContextProvider>
|
</Suspense>
|
||||||
</AppLayout>
|
</PreferenceContextProvider>
|
||||||
|
</AppLayout>
|
||||||
|
</AlertRuleProvider>
|
||||||
</KeyboardHotkeysProvider>
|
</KeyboardHotkeysProvider>
|
||||||
</DashboardProvider>
|
</DashboardProvider>
|
||||||
</QueryBuilderProvider>
|
</QueryBuilderProvider>
|
||||||
@@ -398,7 +408,7 @@ function App(): JSX.Element {
|
|||||||
</PrivateRoute>
|
</PrivateRoute>
|
||||||
</ErrorModalProvider>
|
</ErrorModalProvider>
|
||||||
</NotificationProvider>
|
</NotificationProvider>
|
||||||
</CmdKProvider>
|
</KBarCommandPaletteProvider>
|
||||||
</CompatRouter>
|
</CompatRouter>
|
||||||
</Router>
|
</Router>
|
||||||
</ConfigProvider>
|
</ConfigProvider>
|
||||||
|
|||||||
@@ -295,10 +295,3 @@ 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,7 +34,6 @@ import {
|
|||||||
OrgOnboarding,
|
OrgOnboarding,
|
||||||
PasswordReset,
|
PasswordReset,
|
||||||
PipelinePage,
|
PipelinePage,
|
||||||
PublicDashboardPage,
|
|
||||||
ServiceMapPage,
|
ServiceMapPage,
|
||||||
ServiceMetricsPage,
|
ServiceMetricsPage,
|
||||||
ServicesTablePage,
|
ServicesTablePage,
|
||||||
@@ -170,13 +169,6 @@ 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,8 +1,6 @@
|
|||||||
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';
|
||||||
|
|
||||||
@@ -13,14 +11,9 @@ const logEvent = async (
|
|||||||
rateLimited?: boolean,
|
rateLimited?: boolean,
|
||||||
): Promise<SuccessResponse<EventSuccessPayloadProps> | ErrorResponse> => {
|
): Promise<SuccessResponse<EventSuccessPayloadProps> | ErrorResponse> => {
|
||||||
try {
|
try {
|
||||||
// add deployment_url and user_email to attributes
|
// add tenant_url to attributes
|
||||||
const { hostname } = window.location;
|
const { hostname } = window.location;
|
||||||
const userEmail = getLocalStorageApi(LOCALSTORAGE.LOGGED_IN_USER_EMAIL);
|
const updatedAttributes = { ...attributes, tenant_url: hostname };
|
||||||
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,
|
||||||
|
|||||||
@@ -1,28 +0,0 @@
|
|||||||
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;
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
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;
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
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;
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
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;
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
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;
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
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;
|
|
||||||
@@ -1,30 +1,30 @@
|
|||||||
interface ConfigureIconProps {
|
interface ConfigureIconProps {
|
||||||
width?: number;
|
width?: number;
|
||||||
height?: number;
|
height?: number;
|
||||||
color?: string;
|
fill?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
function ConfigureIcon({
|
function ConfigureIcon({
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
color,
|
fill,
|
||||||
}: ConfigureIconProps): JSX.Element {
|
}: ConfigureIconProps): JSX.Element {
|
||||||
return (
|
return (
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
width={width}
|
width={width}
|
||||||
height={height}
|
height={height}
|
||||||
fill="none"
|
fill={fill}
|
||||||
>
|
>
|
||||||
<path
|
<path
|
||||||
stroke={color}
|
stroke="#C0C1C3"
|
||||||
strokeLinecap="round"
|
strokeLinecap="round"
|
||||||
strokeLinejoin="round"
|
strokeLinejoin="round"
|
||||||
strokeWidth="1.333"
|
strokeWidth="1.333"
|
||||||
d="M9.71 4.745a.576.576 0 000 .806l.922.922a.576.576 0 00.806 0l2.171-2.171a3.455 3.455 0 01-4.572 4.572l-3.98 3.98a1.222 1.222 0 11-1.727-1.728l3.98-3.98a3.455 3.455 0 014.572-4.572L9.717 4.739l-.006.006z"
|
d="M9.71 4.745a.576.576 0 000 .806l.922.922a.576.576 0 00.806 0l2.171-2.171a3.455 3.455 0 01-4.572 4.572l-3.98 3.98a1.222 1.222 0 11-1.727-1.728l3.98-3.98a3.455 3.455 0 014.572-4.572L9.717 4.739l-.006.006z"
|
||||||
/>
|
/>
|
||||||
<path
|
<path
|
||||||
stroke={color}
|
stroke="#C0C1C3"
|
||||||
strokeLinecap="round"
|
strokeLinecap="round"
|
||||||
strokeWidth="1.333"
|
strokeWidth="1.333"
|
||||||
d="M4 7L2.527 5.566a1.333 1.333 0 01-.013-1.898l.81-.81a1.333 1.333 0 011.991.119L5.333 3m5.417 7.988l1.179 1.178m0 0l-.138.138a.833.833 0 00.387 1.397v0a.833.833 0 00.792-.219l.446-.446a.833.833 0 00.176-.917v0a.833.833 0 00-1.355-.261l-.308.308z"
|
d="M4 7L2.527 5.566a1.333 1.333 0 01-.013-1.898l.81-.81a1.333 1.333 0 011.991.119L5.333 3m5.417 7.988l1.179 1.178m0 0l-.138.138a.833.833 0 00.387 1.397v0a.833.833 0 00.792-.219l.446-.446a.833.833 0 00.176-.917v0a.833.833 0 00-1.355-.261l-.308.308z"
|
||||||
@@ -36,6 +36,6 @@ function ConfigureIcon({
|
|||||||
ConfigureIcon.defaultProps = {
|
ConfigureIcon.defaultProps = {
|
||||||
width: 16,
|
width: 16,
|
||||||
height: 16,
|
height: 16,
|
||||||
color: 'currentColor',
|
fill: 'none',
|
||||||
};
|
};
|
||||||
export default ConfigureIcon;
|
export default ConfigureIcon;
|
||||||
|
|||||||
25
frontend/src/auto-import-registry.d.ts
vendored
@@ -1,25 +0,0 @@
|
|||||||
// -------------------------------------------------------------------------
|
|
||||||
// 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/checkbox';
|
|
||||||
import '@signozhq/command';
|
|
||||||
import '@signozhq/design-tokens';
|
|
||||||
import '@signozhq/input';
|
|
||||||
import '@signozhq/popover';
|
|
||||||
import '@signozhq/resizable';
|
|
||||||
import '@signozhq/sonner';
|
|
||||||
import '@signozhq/table';
|
|
||||||
import '@signozhq/tooltip';
|
|
||||||
@@ -62,8 +62,6 @@ 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({
|
||||||
@@ -83,7 +81,6 @@ function CustomTimePicker({
|
|||||||
onGoLive,
|
onGoLive,
|
||||||
onExitLiveLogs,
|
onExitLiveLogs,
|
||||||
showLiveLogs,
|
showLiveLogs,
|
||||||
showRecentlyUsed = true,
|
|
||||||
}: CustomTimePickerProps): JSX.Element {
|
}: CustomTimePickerProps): JSX.Element {
|
||||||
const [
|
const [
|
||||||
selectedTimePlaceholderValue,
|
selectedTimePlaceholderValue,
|
||||||
@@ -398,7 +395,6 @@ function CustomTimePicker({
|
|||||||
setActiveView={setActiveView}
|
setActiveView={setActiveView}
|
||||||
setIsOpenedFromFooter={setIsOpenedFromFooter}
|
setIsOpenedFromFooter={setIsOpenedFromFooter}
|
||||||
isOpenedFromFooter={isOpenedFromFooter}
|
isOpenedFromFooter={isOpenedFromFooter}
|
||||||
showRecentlyUsed={showRecentlyUsed}
|
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
content
|
content
|
||||||
@@ -468,5 +464,4 @@ CustomTimePicker.defaultProps = {
|
|||||||
onCustomTimeStatusUpdate: noop,
|
onCustomTimeStatusUpdate: noop,
|
||||||
onExitLiveLogs: noop,
|
onExitLiveLogs: noop,
|
||||||
showLiveLogs: false,
|
showLiveLogs: false,
|
||||||
showRecentlyUsed: true,
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -47,7 +47,6 @@ interface CustomTimePickerPopoverContentProps {
|
|||||||
isOpenedFromFooter: boolean;
|
isOpenedFromFooter: boolean;
|
||||||
setIsOpenedFromFooter: Dispatch<SetStateAction<boolean>>;
|
setIsOpenedFromFooter: Dispatch<SetStateAction<boolean>>;
|
||||||
onExitLiveLogs: () => void;
|
onExitLiveLogs: () => void;
|
||||||
showRecentlyUsed: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface RecentlyUsedDateTimeRange {
|
interface RecentlyUsedDateTimeRange {
|
||||||
@@ -73,7 +72,6 @@ function CustomTimePickerPopoverContent({
|
|||||||
isOpenedFromFooter,
|
isOpenedFromFooter,
|
||||||
setIsOpenedFromFooter,
|
setIsOpenedFromFooter,
|
||||||
onExitLiveLogs,
|
onExitLiveLogs,
|
||||||
showRecentlyUsed = true,
|
|
||||||
}: CustomTimePickerPopoverContentProps): JSX.Element {
|
}: CustomTimePickerPopoverContentProps): JSX.Element {
|
||||||
const { pathname } = useLocation();
|
const { pathname } = useLocation();
|
||||||
|
|
||||||
@@ -226,35 +224,33 @@ function CustomTimePickerPopoverContent({
|
|||||||
<div>{getTimeChips(RelativeDurationSuggestionOptions)}</div>
|
<div>{getTimeChips(RelativeDurationSuggestionOptions)}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{showRecentlyUsed && (
|
<div className="recently-used-container">
|
||||||
<div className="recently-used-container">
|
<div className="time-heading">RECENTLY USED</div>
|
||||||
<div className="time-heading">RECENTLY USED</div>
|
<div className="recently-used-range">
|
||||||
<div className="recently-used-range">
|
{recentlyUsedTimeRanges.map((range: RecentlyUsedDateTimeRange) => (
|
||||||
{recentlyUsedTimeRanges.map((range: RecentlyUsedDateTimeRange) => (
|
<div
|
||||||
<div
|
className="recently-used-range-item"
|
||||||
className="recently-used-range-item"
|
role="button"
|
||||||
role="button"
|
tabIndex={0}
|
||||||
tabIndex={0}
|
onKeyDown={(e): void => {
|
||||||
onKeyDown={(e): void => {
|
if (e.key === 'Enter' || e.key === ' ') {
|
||||||
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);
|
||||||
}}
|
}
|
||||||
>
|
}}
|
||||||
{range.label}
|
key={range.value}
|
||||||
</div>
|
onClick={(): void => {
|
||||||
))}
|
handleExitLiveLogs();
|
||||||
</div>
|
onCustomDateHandler([dayjs(range.from), dayjs(range.to)]);
|
||||||
|
setIsOpen(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{range.label}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
/* eslint-disable sonarjs/no-duplicate-string */
|
/* eslint-disable sonarjs/no-duplicate-string */
|
||||||
import { PrecisionOptionsEnum } from '../types';
|
import { getYAxisFormattedValue, PrecisionOptionsEnum } from '../yAxisConfig';
|
||||||
import { getYAxisFormattedValue } from '../yAxisConfig';
|
|
||||||
|
|
||||||
const testFullPrecisionGetYAxisFormattedValue = (
|
const testFullPrecisionGetYAxisFormattedValue = (
|
||||||
value: string,
|
value: string,
|
||||||
@@ -233,7 +232,7 @@ describe('getYAxisFormattedValue - units (full precision legacy assertions)', ()
|
|||||||
).toBe('1%');
|
).toBe('1%');
|
||||||
expect(
|
expect(
|
||||||
testFullPrecisionGetYAxisFormattedValue('1.00555555559595876', 'percent'),
|
testFullPrecisionGetYAxisFormattedValue('1.00555555559595876', 'percent'),
|
||||||
).toBe('1.005555555595959%');
|
).toBe('1.005555555595958%');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('ratio', () => {
|
test('ratio', () => {
|
||||||
@@ -360,7 +359,7 @@ describe('getYAxisFormattedValue - precision option tests', () => {
|
|||||||
's',
|
's',
|
||||||
PrecisionOptionsEnum.FULL,
|
PrecisionOptionsEnum.FULL,
|
||||||
),
|
),
|
||||||
).toBe('26.254299141484417 µs');
|
).toBe('26254299141484417000000 µs');
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
getYAxisFormattedValue('4353.81', 'ms', PrecisionOptionsEnum.FULL),
|
getYAxisFormattedValue('4353.81', 'ms', PrecisionOptionsEnum.FULL),
|
||||||
|
|||||||
@@ -78,18 +78,3 @@ 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,12 +16,8 @@ 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';
|
||||||
@@ -153,7 +149,6 @@ export const getGraphOptions = (
|
|||||||
scales: {
|
scales: {
|
||||||
x: {
|
x: {
|
||||||
stacked: isStacked,
|
stacked: isStacked,
|
||||||
offset: false,
|
|
||||||
grid: {
|
grid: {
|
||||||
display: true,
|
display: true,
|
||||||
color: getGridColor(),
|
color: getGridColor(),
|
||||||
@@ -246,68 +241,3 @@ 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,17 +1,86 @@
|
|||||||
/* 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';
|
||||||
|
|
||||||
import { formatUniversalUnit } from '../YAxisUnitSelector/formatter';
|
const DEFAULT_SIGNIFICANT_DIGITS = 15;
|
||||||
import {
|
// max decimals to keep should not exceed 15 decimal places to avoid floating point precision issues
|
||||||
DEFAULT_SIGNIFICANT_DIGITS,
|
const MAX_DECIMALS = 15;
|
||||||
PrecisionOption,
|
|
||||||
PrecisionOptionsEnum,
|
export enum PrecisionOptionsEnum {
|
||||||
} from './types';
|
ZERO = 0,
|
||||||
import { formatDecimalWithLeadingZeros } from './utils';
|
ONE = 1,
|
||||||
|
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.
|
||||||
@@ -32,10 +101,19 @@ 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 DEFAULT_SIGNIFICANT_DIGITS;
|
return decimalPlaces && decimalPlaces >= DEFAULT_SIGNIFICANT_DIGITS
|
||||||
|
? decimalPlaces
|
||||||
|
: DEFAULT_SIGNIFICANT_DIGITS;
|
||||||
}
|
}
|
||||||
return precision;
|
return precision;
|
||||||
};
|
};
|
||||||
@@ -52,22 +130,6 @@ 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('.')) {
|
||||||
@@ -76,7 +138,6 @@ export const getYAxisFormattedValue = (
|
|||||||
precision,
|
precision,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return formattedValueToString(formattedValue);
|
return formattedValueToString(formattedValue);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
Sentry.captureEvent({
|
Sentry.captureEvent({
|
||||||
|
|||||||
@@ -37,6 +37,7 @@
|
|||||||
|
|
||||||
border-radius: 2px 0px 0px 2px;
|
border-radius: 2px 0px 0px 2px;
|
||||||
border: 1px solid var(--bg-slate-400);
|
border: 1px solid var(--bg-slate-400);
|
||||||
|
background: var(--bg-ink-300);
|
||||||
|
|
||||||
border-right: none;
|
border-right: none;
|
||||||
border-left: none;
|
border-left: none;
|
||||||
@@ -44,12 +45,6 @@
|
|||||||
border-bottom-right-radius: 0px;
|
border-bottom-right-radius: 0px;
|
||||||
border-top-left-radius: 0px;
|
border-top-left-radius: 0px;
|
||||||
border-bottom-left-radius: 0px;
|
border-bottom-left-radius: 0px;
|
||||||
font-size: 12px !important;
|
|
||||||
line-height: 27px;
|
|
||||||
&::placeholder {
|
|
||||||
color: var(--bg-vanilla-400) !important;
|
|
||||||
font-size: 12px !important;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.close-btn {
|
.close-btn {
|
||||||
|
|||||||
@@ -0,0 +1,152 @@
|
|||||||
|
.kbar-command-palette__positioner {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 1rem;
|
||||||
|
background: rgba(0, 0, 0, 0.5);
|
||||||
|
backdrop-filter: blur(6px);
|
||||||
|
z-index: 50;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kbar-command-palette__animator {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 600px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kbar-command-palette__card {
|
||||||
|
background: var(--bg-ink-500);
|
||||||
|
color: var(--text-vanilla-100);
|
||||||
|
border-radius: 3px;
|
||||||
|
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.15);
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kbar-command-palette__search {
|
||||||
|
padding: 12px 16px;
|
||||||
|
font-size: 13px;
|
||||||
|
border: none;
|
||||||
|
border-bottom: 1px solid var(--border-ink-200);
|
||||||
|
color: var(--text-vanilla-100);
|
||||||
|
outline: none;
|
||||||
|
background-color: var(--bg-ink-500);
|
||||||
|
}
|
||||||
|
|
||||||
|
.kbar-command-palette__section {
|
||||||
|
padding: 8px 16px 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-robin-500);
|
||||||
|
font-family: 'Inter', sans-serif;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.02em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kbar-command-palette__item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 10px 16px;
|
||||||
|
font-size: 13px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kbar-command-palette__item:hover,
|
||||||
|
.kbar-command-palette__item--active {
|
||||||
|
background: var(--bg-ink-400);
|
||||||
|
}
|
||||||
|
|
||||||
|
.kbar-command-palette__icon {
|
||||||
|
flex-shrink: 0;
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
color: #444;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kbar-command-palette__shortcut {
|
||||||
|
margin-left: auto;
|
||||||
|
display: flex;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kbar-command-palette__key {
|
||||||
|
padding: 2px 6px;
|
||||||
|
font-size: 12px;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: var(--bg-ink-300);
|
||||||
|
color: var(--text-vanilla-300);
|
||||||
|
text-transform: uppercase;
|
||||||
|
font-family: 'Space Mono', monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kbar-command-palette__results-container {
|
||||||
|
div {
|
||||||
|
&::-webkit-scrollbar {
|
||||||
|
width: 0.3rem;
|
||||||
|
height: 0.3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-webkit-scrollbar-track {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-webkit-scrollbar-thumb {
|
||||||
|
background: var(--bg-slate-300);
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: var(--bg-slate-200);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.lightMode {
|
||||||
|
.kbar-command-palette__positioner {
|
||||||
|
background: rgba(0, 0, 0, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.kbar-command-palette__card {
|
||||||
|
background: #fff;
|
||||||
|
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.kbar-command-palette__search {
|
||||||
|
border-bottom: 1px solid #e5e5e5;
|
||||||
|
color: var(--text-ink-500);
|
||||||
|
background-color: var(--bg-vanilla-100);
|
||||||
|
}
|
||||||
|
|
||||||
|
.kbar-command-palette__item {
|
||||||
|
color: var(--text-ink-500);
|
||||||
|
}
|
||||||
|
|
||||||
|
.kbar-command-palette__item:hover,
|
||||||
|
.kbar-command-palette__item--active {
|
||||||
|
background: #f5f5f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kbar-command-palette__icon {
|
||||||
|
color: #444;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kbar-command-palette__key {
|
||||||
|
background: #eee;
|
||||||
|
color: #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kbar-command-palette__results-container {
|
||||||
|
div {
|
||||||
|
&::-webkit-scrollbar-thumb {
|
||||||
|
background: var(--bg-vanilla-300);
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: var(--bg-vanilla-300);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
import './KBarCommandPalette.scss';
|
||||||
|
|
||||||
|
import {
|
||||||
|
KBarAnimator,
|
||||||
|
KBarPortal,
|
||||||
|
KBarPositioner,
|
||||||
|
KBarResults,
|
||||||
|
KBarSearch,
|
||||||
|
useMatches,
|
||||||
|
} from 'kbar';
|
||||||
|
|
||||||
|
function Results(): JSX.Element {
|
||||||
|
const { results } = useMatches();
|
||||||
|
|
||||||
|
const renderResults = ({
|
||||||
|
item,
|
||||||
|
active,
|
||||||
|
}: {
|
||||||
|
item: any;
|
||||||
|
active: boolean;
|
||||||
|
}): JSX.Element =>
|
||||||
|
typeof item === 'string' ? (
|
||||||
|
<div className="kbar-command-palette__section">{item}</div>
|
||||||
|
) : (
|
||||||
|
<div
|
||||||
|
className={`kbar-command-palette__item ${
|
||||||
|
active ? 'kbar-command-palette__item--active' : ''
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{item.icon}
|
||||||
|
<span>{item.name}</span>
|
||||||
|
{item.shortcut?.length ? (
|
||||||
|
<span className="kbar-command-palette__shortcut">
|
||||||
|
{item.shortcut.map((sc: string) => (
|
||||||
|
<kbd key={sc} className="kbar-command-palette__key">
|
||||||
|
{sc}
|
||||||
|
</kbd>
|
||||||
|
))}
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="kbar-command-palette__results-container">
|
||||||
|
<KBarResults items={results} onRender={renderResults} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function KBarCommandPalette(): JSX.Element {
|
||||||
|
return (
|
||||||
|
<KBarPortal>
|
||||||
|
<KBarPositioner className="kbar-command-palette__positioner">
|
||||||
|
<KBarAnimator className="kbar-command-palette__animator">
|
||||||
|
<div className="kbar-command-palette__card">
|
||||||
|
<KBarSearch
|
||||||
|
className="kbar-command-palette__search"
|
||||||
|
placeholder="Search or type a command..."
|
||||||
|
/>
|
||||||
|
<Results />
|
||||||
|
</div>
|
||||||
|
</KBarAnimator>
|
||||||
|
</KBarPositioner>
|
||||||
|
</KBarPortal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default KBarCommandPalette;
|
||||||
@@ -47,7 +47,6 @@ import { AppState } from 'store/reducers';
|
|||||||
import { Query, TagFilter } from 'types/api/queryBuilder/queryBuilderData';
|
import { Query, TagFilter } from 'types/api/queryBuilder/queryBuilderData';
|
||||||
import { DataSource, StringOperators } from 'types/common/queryBuilder';
|
import { DataSource, StringOperators } from 'types/common/queryBuilder';
|
||||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||||
import { isCtrlOrMMetaKey } from 'utils/isCtrlOrMMetaKey';
|
|
||||||
|
|
||||||
import { RESOURCE_KEYS, VIEW_TYPES, VIEWS } from './constants';
|
import { RESOURCE_KEYS, VIEW_TYPES, VIEWS } from './constants';
|
||||||
import { LogDetailInnerProps, LogDetailProps } from './LogDetail.interfaces';
|
import { LogDetailInnerProps, LogDetailProps } from './LogDetail.interfaces';
|
||||||
@@ -134,7 +133,7 @@ function LogDetailInner({
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Go to logs explorer page with the log data
|
// Go to logs explorer page with the log data
|
||||||
const handleOpenInExplorer = (event: React.MouseEvent): void => {
|
const handleOpenInExplorer = (): void => {
|
||||||
const queryParams = {
|
const queryParams = {
|
||||||
[QueryParams.activeLogId]: `"${log?.id}"`,
|
[QueryParams.activeLogId]: `"${log?.id}"`,
|
||||||
[QueryParams.startTime]: minTime?.toString() || '',
|
[QueryParams.startTime]: minTime?.toString() || '',
|
||||||
@@ -147,16 +146,7 @@ function LogDetailInner({
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
|
safeNavigate(`${ROUTES.LOGS_EXPLORER}?${createQueryParams(queryParams)}`);
|
||||||
if (isCtrlOrMMetaKey(event)) {
|
|
||||||
window.open(
|
|
||||||
`${ROUTES.LOGS_EXPLORER}?${createQueryParams(queryParams)}`,
|
|
||||||
'_blank',
|
|
||||||
'noopener,noreferrer',
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
safeNavigate(`${ROUTES.LOGS_EXPLORER}?${createQueryParams(queryParams)}`);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleQueryExpressionChange = useCallback(
|
const handleQueryExpressionChange = useCallback(
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import { useCopyToClipboard } from 'react-use';
|
|||||||
function CopyClipboardHOC({
|
function CopyClipboardHOC({
|
||||||
entityKey,
|
entityKey,
|
||||||
textToCopy,
|
textToCopy,
|
||||||
tooltipText = 'Copy to clipboard',
|
|
||||||
children,
|
children,
|
||||||
}: CopyClipboardHOCProps): JSX.Element {
|
}: CopyClipboardHOCProps): JSX.Element {
|
||||||
const [value, setCopy] = useCopyToClipboard();
|
const [value, setCopy] = useCopyToClipboard();
|
||||||
@@ -32,7 +31,7 @@ function CopyClipboardHOC({
|
|||||||
<span onClick={onClick} role="presentation" tabIndex={-1}>
|
<span onClick={onClick} role="presentation" tabIndex={-1}>
|
||||||
<Popover
|
<Popover
|
||||||
placement="top"
|
placement="top"
|
||||||
content={<span style={{ fontSize: '0.9rem' }}>{tooltipText}</span>}
|
content={<span style={{ fontSize: '0.9rem' }}>Copy to clipboard</span>}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</Popover>
|
</Popover>
|
||||||
@@ -43,11 +42,7 @@ function CopyClipboardHOC({
|
|||||||
interface CopyClipboardHOCProps {
|
interface CopyClipboardHOCProps {
|
||||||
entityKey: string | undefined;
|
entityKey: string | undefined;
|
||||||
textToCopy: string;
|
textToCopy: string;
|
||||||
tooltipText?: string;
|
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default CopyClipboardHOC;
|
export default CopyClipboardHOC;
|
||||||
CopyClipboardHOC.defaultProps = {
|
|
||||||
tooltipText: 'Copy to clipboard',
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -471,13 +471,11 @@ function LogsFormatOptionsMenu({
|
|||||||
rootClassName="format-options-popover"
|
rootClassName="format-options-popover"
|
||||||
destroyTooltipOnHide
|
destroyTooltipOnHide
|
||||||
>
|
>
|
||||||
<Tooltip title="Options">
|
<Button
|
||||||
<Button
|
className="periscope-btn ghost"
|
||||||
className="periscope-btn ghost"
|
icon={<Sliders size={14} />}
|
||||||
icon={<Sliders size={14} />}
|
data-testid="periscope-btn-format-options"
|
||||||
data-testid="periscope-btn-format-options"
|
/>
|
||||||
/>
|
|
||||||
</Tooltip>
|
|
||||||
</Popover>
|
</Popover>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,7 +28,6 @@ import React, {
|
|||||||
useState,
|
useState,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
import { Virtuoso } from 'react-virtuoso';
|
import { Virtuoso } from 'react-virtuoso';
|
||||||
import { isCtrlOrMMetaKey } from 'utils/isCtrlOrMMetaKey';
|
|
||||||
import { popupContainer } from 'utils/selectPopupContainer';
|
import { popupContainer } from 'utils/selectPopupContainer';
|
||||||
|
|
||||||
import { CustomMultiSelectProps, CustomTagProps, OptionData } from './types';
|
import { CustomMultiSelectProps, CustomTagProps, OptionData } from './types';
|
||||||
@@ -903,7 +902,7 @@ const CustomMultiSelect: React.FC<CustomMultiSelectProps> = ({
|
|||||||
const lastVisibleChipIndex = getLastVisibleChipIndex();
|
const lastVisibleChipIndex = getLastVisibleChipIndex();
|
||||||
|
|
||||||
// Handle special keyboard combinations
|
// Handle special keyboard combinations
|
||||||
const isCtrlOrCmd = isCtrlOrMMetaKey(e);
|
const isCtrlOrCmd = e.ctrlKey || e.metaKey;
|
||||||
|
|
||||||
// Handle Ctrl+A (select all)
|
// Handle Ctrl+A (select all)
|
||||||
if (isCtrlOrCmd && e.key === 'a') {
|
if (isCtrlOrCmd && e.key === 'a') {
|
||||||
|
|||||||
@@ -1,16 +0,0 @@
|
|||||||
.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;
|
|
||||||
}
|
|
||||||
@@ -1,119 +0,0 @@
|
|||||||
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:');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,73 +0,0 @@
|
|||||||
/* 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;
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
import OverflowInputToolTip from './OverflowInputToolTip';
|
|
||||||
|
|
||||||
export default OverflowInputToolTip;
|
|
||||||
@@ -251,10 +251,6 @@
|
|||||||
.ant-input-group-addon {
|
.ant-input-group-addon {
|
||||||
border-top-left-radius: 0px !important;
|
border-top-left-radius: 0px !important;
|
||||||
border-top-right-radius: 0px !important;
|
border-top-right-radius: 0px !important;
|
||||||
background: var(--bg-ink-300);
|
|
||||||
color: var(--bg-vanilla-400);
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: 300;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.ant-input {
|
.ant-input {
|
||||||
@@ -300,10 +296,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.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;
|
||||||
|
|||||||
@@ -179,7 +179,6 @@ export const QueryBuilderV2 = memo(function QueryBuilderV2({
|
|||||||
isListViewPanel={isListViewPanel}
|
isListViewPanel={isListViewPanel}
|
||||||
onSignalSourceChange={onSignalSourceChange || ((): void => {})}
|
onSignalSourceChange={onSignalSourceChange || ((): void => {})}
|
||||||
signalSourceChangeEnabled={signalSourceChangeEnabled}
|
signalSourceChangeEnabled={signalSourceChangeEnabled}
|
||||||
queriesCount={1}
|
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
currentQuery.builder.queryData.map((query, index) => (
|
currentQuery.builder.queryData.map((query, index) => (
|
||||||
@@ -201,7 +200,6 @@ export const QueryBuilderV2 = memo(function QueryBuilderV2({
|
|||||||
signalSource={query.source as 'meter' | ''}
|
signalSource={query.source as 'meter' | ''}
|
||||||
onSignalSourceChange={onSignalSourceChange || ((): void => {})}
|
onSignalSourceChange={onSignalSourceChange || ((): void => {})}
|
||||||
signalSourceChangeEnabled={signalSourceChangeEnabled}
|
signalSourceChangeEnabled={signalSourceChangeEnabled}
|
||||||
queriesCount={currentQuery.builder.queryData.length}
|
|
||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -98,13 +98,6 @@
|
|||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
border: 1.005px solid var(--Slate-400, #1d212d);
|
border: 1.005px solid var(--Slate-400, #1d212d);
|
||||||
background: var(--Ink-300, #16181d);
|
background: var(--Ink-300, #16181d);
|
||||||
color: var(--bg-vanilla-400);
|
|
||||||
font-family: 'Geist Mono';
|
|
||||||
font-size: 13px;
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 400;
|
|
||||||
line-height: 20px;
|
|
||||||
letter-spacing: -0.07px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.input-with-label {
|
.input-with-label {
|
||||||
|
|||||||
@@ -6,15 +6,6 @@
|
|||||||
gap: 8px;
|
gap: 8px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
.ant-select-selection-search-input {
|
|
||||||
font-size: 12px !important;
|
|
||||||
line-height: 27px;
|
|
||||||
&::placeholder {
|
|
||||||
color: var(--bg-vanilla-400) !important;
|
|
||||||
font-size: 12px !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.source-selector {
|
.source-selector {
|
||||||
width: 120px;
|
width: 120px;
|
||||||
}
|
}
|
||||||
@@ -31,11 +22,6 @@
|
|||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
line-height: 20px; /* 142.857% */
|
line-height: 20px; /* 142.857% */
|
||||||
min-height: 36px;
|
min-height: 36px;
|
||||||
|
|
||||||
.ant-select-selection-placeholder {
|
|
||||||
color: var(--bg-vanilla-400) !important;
|
|
||||||
font-size: 12px !important;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.ant-select-dropdown {
|
.ant-select-dropdown {
|
||||||
|
|||||||
@@ -236,10 +236,6 @@
|
|||||||
background: var(--bg-ink-100) !important;
|
background: var(--bg-ink-100) !important;
|
||||||
opacity: 0.5 !important;
|
opacity: 0.5 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cm-activeLine > span {
|
|
||||||
font-size: 12px !important;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -275,9 +271,6 @@
|
|||||||
|
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
position: relative;
|
position: relative;
|
||||||
.cm-placeholder {
|
|
||||||
font-size: 12px !important;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,8 +20,6 @@
|
|||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
font-size: 12px;
|
|
||||||
color: var(--bg-vanilla-400) !important;
|
|
||||||
|
|
||||||
&.error {
|
&.error {
|
||||||
.cm-editor {
|
.cm-editor {
|
||||||
@@ -233,9 +231,6 @@
|
|||||||
.query-aggregation-interval-input {
|
.query-aggregation-interval-input {
|
||||||
input {
|
input {
|
||||||
max-width: 120px;
|
max-width: 120px;
|
||||||
&::placeholder {
|
|
||||||
color: var(--bg-vanilla-400);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
.add-trace-operator-button,
|
|
||||||
.add-new-query-button,
|
|
||||||
.add-formula-button {
|
|
||||||
border: 1px solid var(--bg-slate-400);
|
|
||||||
background: var(--bg-ink-300);
|
|
||||||
box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.1);
|
|
||||||
}
|
|
||||||
@@ -1,75 +1,7 @@
|
|||||||
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,
|
||||||
@@ -90,7 +22,8 @@ export default function QueryFooter({
|
|||||||
<div className="qb-add-new-query">
|
<div className="qb-add-new-query">
|
||||||
<Tooltip title={<div style={{ textAlign: 'center' }}>Add New Query</div>}>
|
<Tooltip title={<div style={{ textAlign: 'center' }}>Add New Query</div>}>
|
||||||
<Button
|
<Button
|
||||||
className="add-new-query-button periscope-btn "
|
className="add-new-query-button periscope-btn secondary"
|
||||||
|
type="text"
|
||||||
icon={<Plus size={16} />}
|
icon={<Plus size={16} />}
|
||||||
onClick={addNewBuilderQuery}
|
onClick={addNewBuilderQuery}
|
||||||
/>
|
/>
|
||||||
@@ -116,7 +49,7 @@ export default function QueryFooter({
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
className="add-formula-button periscope-btn "
|
className="add-formula-button periscope-btn secondary"
|
||||||
icon={<Sigma size={16} />}
|
icon={<Sigma size={16} />}
|
||||||
onClick={addNewFormula}
|
onClick={addNewFormula}
|
||||||
>
|
>
|
||||||
@@ -126,7 +59,35 @@ export default function QueryFooter({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{showAddTraceOperator && (
|
{showAddTraceOperator && (
|
||||||
<TraceOperatorSection addTraceOperator={addTraceOperator} />
|
<div className="qb-trace-operator-button-container">
|
||||||
|
<Tooltip
|
||||||
|
title={
|
||||||
|
<div style={{ textAlign: 'center' }}>
|
||||||
|
Add Trace Matching
|
||||||
|
<Typography.Link
|
||||||
|
href="https://signoz.io/docs/userguide/query-builder-v5/#multi-query-analysis-trace-operators"
|
||||||
|
target="_blank"
|
||||||
|
style={{ textDecoration: 'underline' }}
|
||||||
|
>
|
||||||
|
{' '}
|
||||||
|
<br />
|
||||||
|
Learn more
|
||||||
|
</Typography.Link>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
className="add-trace-operator-button periscope-btn secondary"
|
||||||
|
icon={<DraftingCompass size={16} />}
|
||||||
|
onClick={(): void => addTraceOperator?.()}
|
||||||
|
>
|
||||||
|
<div className="qb-trace-operator-button-container-text">
|
||||||
|
Add Trace Matching
|
||||||
|
<BetaTag />
|
||||||
|
</div>
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ 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';
|
||||||
@@ -80,16 +79,6 @@ 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,
|
||||||
@@ -98,8 +87,17 @@ 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);
|
||||||
@@ -109,12 +107,8 @@ 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 = useCallback((newQuery: string): void => {
|
const handleQueryValidation = (newQuery: string): void => {
|
||||||
try {
|
try {
|
||||||
const validationResponse = validateQuery(newQuery);
|
const validationResponse = validateQuery(newQuery);
|
||||||
setValidation(validationResponse);
|
setValidation(validationResponse);
|
||||||
@@ -125,67 +119,29 @@ function QuerySearch({
|
|||||||
errors: [error as IDetailedError],
|
errors: [error as IDetailedError],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, []);
|
};
|
||||||
|
|
||||||
const getCurrentQuery = useCallback(
|
// Track if the query was changed externally (from queryData) vs internally (user input)
|
||||||
(): string => editorRef.current?.state.doc.toString() || '',
|
const [isExternalQueryChange, setIsExternalQueryChange] = useState(false);
|
||||||
[],
|
const [lastExternalQuery, setLastExternalQuery] = useState<string>('');
|
||||||
);
|
|
||||||
|
|
||||||
const updateEditorValue = useCallback(
|
useEffect(() => {
|
||||||
(value: string, options: { skipOnChange?: boolean } = {}): void => {
|
const newQuery = queryData.filter?.expression || '';
|
||||||
const view = editorRef.current;
|
// Only mark as external change if the query actually changed from external source
|
||||||
if (!view) return;
|
if (newQuery !== lastExternalQuery) {
|
||||||
|
setQuery(newQuery);
|
||||||
|
setIsExternalQueryChange(true);
|
||||||
|
setLastExternalQuery(newQuery);
|
||||||
|
}
|
||||||
|
}, [queryData.filter?.expression, lastExternalQuery]);
|
||||||
|
|
||||||
const currentValue = view.state.doc.toString();
|
// Validate query when it changes externally (from queryData)
|
||||||
if (currentValue === value) return;
|
useEffect(() => {
|
||||||
|
if (isExternalQueryChange && query) {
|
||||||
if (options.skipOnChange) {
|
handleQueryValidation(query);
|
||||||
isProgrammaticChangeRef.current = true;
|
setIsExternalQueryChange(false);
|
||||||
}
|
}
|
||||||
|
}, [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
|
||||||
@@ -194,6 +150,7 @@ 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,
|
||||||
@@ -202,6 +159,8 @@ 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>('');
|
||||||
@@ -547,7 +506,6 @@ 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;
|
||||||
@@ -563,15 +521,7 @@ 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((lastPos) => {
|
setCursorPos(newPos);
|
||||||
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) {
|
||||||
@@ -604,17 +554,16 @@ function QuerySearch({
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleChange = (value: string): void => {
|
const handleChange = (value: string): void => {
|
||||||
if (isProgrammaticChangeRef.current) {
|
setQuery(value);
|
||||||
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 => {
|
||||||
const currentQuery = getCurrentQuery();
|
handleQueryValidation(query);
|
||||||
handleQueryValidation(currentQuery);
|
|
||||||
setIsFocused(false);
|
setIsFocused(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -633,11 +582,12 @@ 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 currentQuery = getCurrentQuery();
|
const newQuery = query ? `${query} AND ${exampleQuery}` : exampleQuery;
|
||||||
const newQuery = currentQuery
|
setQuery(newQuery);
|
||||||
? `${currentQuery} AND ${exampleQuery}`
|
// Mark as internal change to avoid triggering external validation
|
||||||
: exampleQuery;
|
setIsExternalQueryChange(false);
|
||||||
updateEditorValue(newQuery);
|
// Update lastExternalQuery to prevent external validation trigger
|
||||||
|
setLastExternalQuery(newQuery);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Helper function to render a badge for the current context mode
|
// Helper function to render a badge for the current context mode
|
||||||
@@ -672,10 +622,8 @@ 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(currentQuery, cursorPos.ch);
|
const queryContext = getQueryContextAtCursor(query, cursorPos.ch);
|
||||||
|
|
||||||
// Define autocomplete options based on the context
|
// Define autocomplete options based on the context
|
||||||
let options: {
|
let options: {
|
||||||
@@ -1171,8 +1119,7 @@ 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 currentQuery = editorRef.current?.state.doc.toString() || '';
|
const curChar = query.charAt(cursorPos.ch - 1) || '';
|
||||||
const curChar = currentQuery.charAt(cursorPos.ch - 1) || '';
|
|
||||||
|
|
||||||
if (curChar === '(' || curChar === '[') {
|
if (curChar === '(' || curChar === '[') {
|
||||||
// Right after opening parenthesis/bracket
|
// Right after opening parenthesis/bracket
|
||||||
@@ -1321,7 +1268,7 @@ function QuerySearch({
|
|||||||
style={{
|
style={{
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
top: 8,
|
top: 8,
|
||||||
right: validation.isValid === false && getCurrentQuery() ? 40 : 8, // Move left when error shown
|
right: validation.isValid === false && query ? 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',
|
||||||
@@ -1342,10 +1289,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,
|
||||||
@@ -1383,7 +1330,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(getCurrentQuery());
|
onRun(query);
|
||||||
} else {
|
} else {
|
||||||
handleRunQuery();
|
handleRunQuery();
|
||||||
}
|
}
|
||||||
@@ -1409,7 +1356,7 @@ function QuerySearch({
|
|||||||
onBlur={handleBlur}
|
onBlur={handleBlur}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{getCurrentQuery() && validation.isValid === false && !isFocused && (
|
{query && 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,13 +9,7 @@ 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 {
|
import { memo, useCallback, useMemo, useState } from 'react';
|
||||||
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';
|
||||||
@@ -26,29 +20,26 @@ 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 = forwardRef(function QueryV2(
|
export const QueryV2 = memo(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,
|
}: QueryProps & {
|
||||||
}: QueryProps & {
|
ref: React.RefObject<HTMLDivElement>;
|
||||||
onSignalSourceChange: (value: string) => void;
|
onSignalSourceChange: (value: string) => void;
|
||||||
signalSourceChangeEnabled: boolean;
|
signalSourceChangeEnabled: boolean;
|
||||||
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;
|
||||||
@@ -201,16 +192,12 @@ export const QueryV2 = forwardRef(function QueryV2(
|
|||||||
icon: <Copy size={14} />,
|
icon: <Copy size={14} />,
|
||||||
onClick: handleCloneEntity,
|
onClick: handleCloneEntity,
|
||||||
},
|
},
|
||||||
...(queriesCount && queriesCount > 1
|
{
|
||||||
? [
|
label: 'Delete',
|
||||||
{
|
key: 'delete-query',
|
||||||
label: 'Delete',
|
icon: <Trash size={14} />,
|
||||||
key: 'delete-query',
|
onClick: handleDeleteQuery,
|
||||||
icon: <Trash size={14} />,
|
},
|
||||||
onClick: handleDeleteQuery,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
: []),
|
|
||||||
],
|
],
|
||||||
}}
|
}}
|
||||||
placement="bottomRight"
|
placement="bottomRight"
|
||||||
@@ -302,5 +289,3 @@ export const QueryV2 = forwardRef(function QueryV2(
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
QueryV2.displayName = 'QueryV2';
|
|
||||||
|
|||||||
@@ -92,9 +92,6 @@
|
|||||||
|
|
||||||
.qb-trace-operator-editor-container {
|
.qb-trace-operator-editor-container {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
.cm-activeLine > span {
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&.arrow-left {
|
&.arrow-left {
|
||||||
@@ -116,8 +113,6 @@
|
|||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
padding: 0px 8px;
|
padding: 0px 8px;
|
||||||
border-right: 1px solid var(--bg-slate-400);
|
border-right: 1px solid var(--bg-slate-400);
|
||||||
font-size: 12px;
|
|
||||||
font-weight: 300;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ export default function TraceOperator({
|
|||||||
!isListViewPanel && 'qb-trace-operator-arrow',
|
!isListViewPanel && 'qb-trace-operator-arrow',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<Typography.Text className="label">Trace Operator</Typography.Text>
|
<Typography.Text className="label">TRACE OPERATOR</Typography.Text>
|
||||||
<div className="qb-trace-operator-editor-container">
|
<div className="qb-trace-operator-editor-container">
|
||||||
<TraceOperatorEditor
|
<TraceOperatorEditor
|
||||||
value={traceOperator?.expression || ''}
|
value={traceOperator?.expression || ''}
|
||||||
|
|||||||
@@ -5,85 +5,13 @@ 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 { fireEvent, render, userEvent, waitFor } from 'tests/test-utils';
|
import React from 'react';
|
||||||
|
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,
|
||||||
}));
|
}));
|
||||||
@@ -103,6 +31,24 @@ 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: {
|
||||||
@@ -117,19 +63,153 @@ jest.mock('api/querySuggestions/getValueSuggestion', () => ({
|
|||||||
}),
|
}),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Note: We're NOT mocking CodeMirror here - using the real component
|
// Mock CodeMirror to a simple textarea to make it testable and call onUpdate
|
||||||
// This provides integration testing with the actual CodeMirror editor
|
jest.mock(
|
||||||
|
'@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 = "http.status_code = '200'";
|
const SAMPLE_STATUS_QUERY = " status_code = '200'";
|
||||||
|
|
||||||
describe('QuerySearch (Integration with Real CodeMirror)', () => {
|
describe('QuerySearch', () => {
|
||||||
it('renders with placeholder', () => {
|
it('renders with placeholder', () => {
|
||||||
render(
|
render(
|
||||||
<QuerySearch
|
<QuerySearch
|
||||||
@@ -139,19 +219,21 @@ describe('QuerySearch (Integration with Real CodeMirror)', () => {
|
|||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
|
|
||||||
// CodeMirror renders a contenteditable div, so we check for the container
|
expect(screen.getByPlaceholderText(PLACEHOLDER_TEXT)).toBeInTheDocument();
|
||||||
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 () => {
|
||||||
// Use real timers for CodeMirror integration tests
|
jest.useFakeTimers();
|
||||||
|
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
|
||||||
@@ -161,33 +243,28 @@ describe('QuerySearch (Integration with Real CodeMirror)', () => {
|
|||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Wait for CodeMirror to initialize
|
const editor = screen.getByTestId(TESTID_EDITOR);
|
||||||
await waitFor(() => {
|
|
||||||
const editor = document.querySelector(CM_EDITOR_SELECTOR);
|
|
||||||
expect(editor).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
// 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);
|
await user.type(editor, SAMPLE_KEY_TYPING);
|
||||||
|
advance(1000);
|
||||||
|
|
||||||
// Wait for debounced API call (300ms debounce + some buffer)
|
|
||||||
await waitFor(() => expect(mockedGetKeys).toHaveBeenCalled(), {
|
await waitFor(() => expect(mockedGetKeys).toHaveBeenCalled(), {
|
||||||
timeout: 2000,
|
timeout: 3000,
|
||||||
});
|
});
|
||||||
|
jest.useRealTimers();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('fetches value suggestions when editing value context', async () => {
|
it('fetches value suggestions when editing value context', async () => {
|
||||||
// Use real timers for CodeMirror integration tests
|
jest.useFakeTimers();
|
||||||
|
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
|
||||||
@@ -197,28 +274,21 @@ describe('QuerySearch (Integration with Real CodeMirror)', () => {
|
|||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Wait for CodeMirror to initialize
|
const editor = screen.getByTestId(TESTID_EDITOR);
|
||||||
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.type(editor, SAMPLE_VALUE_TYPING_INCOMPLETE);
|
await user.type(editor, SAMPLE_VALUE_TYPING_INCOMPLETE);
|
||||||
|
advance(1000);
|
||||||
|
|
||||||
// Wait for debounced API call (300ms debounce + some buffer)
|
|
||||||
await waitFor(() => expect(mockedGetValues).toHaveBeenCalled(), {
|
await waitFor(() => expect(mockedGetValues).toHaveBeenCalled(), {
|
||||||
timeout: 2000,
|
timeout: 3000,
|
||||||
});
|
});
|
||||||
|
jest.useRealTimers();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('fetches key suggestions on mount for LOGS', async () => {
|
it('fetches key suggestions on mount for LOGS', async () => {
|
||||||
// Use real timers for CodeMirror integration tests
|
jest.useFakeTimers();
|
||||||
const mockedGetKeysOnMount = getKeySuggestions as jest.MockedFunction<
|
const mockedGetKeysOnMount = getKeySuggestions as jest.MockedFunction<
|
||||||
typeof getKeySuggestions
|
typeof getKeySuggestions
|
||||||
>;
|
>;
|
||||||
mockedGetKeysOnMount.mockClear();
|
|
||||||
|
|
||||||
render(
|
render(
|
||||||
<QuerySearch
|
<QuerySearch
|
||||||
@@ -228,15 +298,17 @@ describe('QuerySearch (Integration with Real CodeMirror)', () => {
|
|||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Wait for debounced API call (300ms debounce + some buffer)
|
jest.advanceTimersByTime(1000);
|
||||||
|
|
||||||
await waitFor(() => expect(mockedGetKeysOnMount).toHaveBeenCalled(), {
|
await waitFor(() => expect(mockedGetKeysOnMount).toHaveBeenCalled(), {
|
||||||
timeout: 2000,
|
timeout: 3000,
|
||||||
});
|
});
|
||||||
|
|
||||||
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 () => {
|
||||||
@@ -252,26 +324,12 @@ describe('QuerySearch (Integration with Real CodeMirror)', () => {
|
|||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Wait for CodeMirror to initialize
|
const editor = screen.getByTestId(TESTID_EDITOR);
|
||||||
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}');
|
||||||
|
|
||||||
// Use fireEvent for keyboard shortcuts as userEvent might not work well with CodeMirror
|
await waitFor(() => expect(onRun).toHaveBeenCalled());
|
||||||
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 () => {
|
||||||
@@ -290,62 +348,11 @@ describe('QuerySearch (Integration with Real CodeMirror)', () => {
|
|||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Wait for CodeMirror to initialize
|
const editor = screen.getByTestId(TESTID_EDITOR);
|
||||||
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}');
|
||||||
|
|
||||||
// Use fireEvent for keyboard shortcuts as userEvent might not work well with CodeMirror
|
await waitFor(() => expect(mockedHandleRunQuery).toHaveBeenCalled());
|
||||||
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,7 +13,6 @@ import {
|
|||||||
convertAggregationToExpression,
|
convertAggregationToExpression,
|
||||||
convertFiltersToExpression,
|
convertFiltersToExpression,
|
||||||
convertFiltersToExpressionWithExistingQuery,
|
convertFiltersToExpressionWithExistingQuery,
|
||||||
formatValueForExpression,
|
|
||||||
removeKeysFromExpression,
|
removeKeysFromExpression,
|
||||||
} from '../utils';
|
} from '../utils';
|
||||||
|
|
||||||
@@ -1194,220 +1193,3 @@ 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 { isQuoted, unquote } from 'utils/stringUtils';
|
import { 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,57 +38,49 @@ const isArrayOperator = (operator: string): boolean => {
|
|||||||
return arrayOperators.includes(operator);
|
return arrayOperators.includes(operator);
|
||||||
};
|
};
|
||||||
|
|
||||||
const isVariable = (
|
const isVariable = (value: string | string[] | number | boolean): boolean => {
|
||||||
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
|
||||||
*/
|
*/
|
||||||
export const formatValueForExpression = (
|
const formatValueForExpression = (
|
||||||
value: (string | number | boolean)[] | string | number | boolean,
|
value: string[] | 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.map(formatSingleValue).join(', ')}]`;
|
return `[${arrayValue
|
||||||
|
.map((v) =>
|
||||||
|
typeof v === 'string' ? `'${v.replace(/'/g, "\\'")}'` : String(v),
|
||||||
|
)
|
||||||
|
.join(', ')}]`;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Array.isArray(value)) {
|
if (Array.isArray(value)) {
|
||||||
return `[${value.map(formatSingleValue).join(', ')}]`;
|
// Handle array values (e.g., for IN operations)
|
||||||
|
return `[${value
|
||||||
|
.map((v) =>
|
||||||
|
typeof v === 'string' ? `'${v.replace(/'/g, "\\'")}'` : String(v),
|
||||||
|
)
|
||||||
|
.join(', ')}]`;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof value === 'string') {
|
if (typeof value === 'string') {
|
||||||
return formatSingleValue(value);
|
// Add single quotes around all string values and escape internal single quotes
|
||||||
|
return `'${value.replace(/'/g, "\\'")}'`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return String(value);
|
return String(value);
|
||||||
@@ -144,43 +136,14 @@ export const convertFiltersToExpression = (
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
const formatValuesForFilter = (value: string | string[]): string | string[] => {
|
||||||
* Converts a string value to its appropriate type (number, boolean, or string)
|
|
||||||
* for use in filter objects. This is the inverse of formatSingleValue.
|
|
||||||
*/
|
|
||||||
function formatSingleValueForFilter(
|
|
||||||
value: string | number | boolean,
|
|
||||||
): string | number | boolean {
|
|
||||||
if (typeof value === 'string') {
|
|
||||||
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 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)) {
|
if (Array.isArray(value)) {
|
||||||
return value.map(formatSingleValueForFilter);
|
return value.map((v) => (typeof v === 'string' ? unquote(v) : String(v)));
|
||||||
}
|
}
|
||||||
|
if (typeof value === 'string') {
|
||||||
return formatSingleValueForFilter(value);
|
return unquote(value);
|
||||||
|
}
|
||||||
|
return String(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[String(val)] = true;
|
filterState[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[String(val)] = false;
|
filterState[val] = false;
|
||||||
});
|
});
|
||||||
} else if (typeof filterSync.value === 'string') {
|
} else if (typeof filterSync.value === 'string') {
|
||||||
filterState[filterSync.value] = false;
|
filterState[filterSync.value] = false;
|
||||||
|
|||||||
@@ -0,0 +1,223 @@
|
|||||||
|
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();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
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, useMemo } from 'react';
|
import { ReactNode } from 'react';
|
||||||
import { Warning } from 'types/api';
|
import { Warning } from 'types/api';
|
||||||
|
|
||||||
interface WarningContentProps {
|
interface WarningContentProps {
|
||||||
@@ -106,51 +106,19 @@ 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={content}
|
content={<WarningContent warning={warningData} />}
|
||||||
overlayStyle={{ padding: 0, maxWidth: '600px' }}
|
overlayStyle={{ padding: 0, maxWidth: '600px' }}
|
||||||
overlayInnerStyle={{ padding: 0 }}
|
overlayInnerStyle={{ padding: 0 }}
|
||||||
autoAdjustOverflow
|
autoAdjustOverflow
|
||||||
@@ -169,8 +137,6 @@ 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 } from './constants';
|
import { UniversalYAxisUnitMappings, Y_AXIS_CATEGORIES } from './constants';
|
||||||
import { UniversalYAxisUnit, YAxisUnitSelectorProps } from './types';
|
import { UniversalYAxisUnit, YAxisUnitSelectorProps } from './types';
|
||||||
import { getYAxisCategories, mapMetricUnitToUniversalUnit } from './utils';
|
import { mapMetricUnitToUniversalUnit } from './utils';
|
||||||
|
|
||||||
function YAxisUnitSelector({
|
function YAxisUnitSelector({
|
||||||
value,
|
value,
|
||||||
@@ -13,7 +13,6 @@ 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);
|
||||||
|
|
||||||
@@ -38,8 +37,6 @@ 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
|
||||||
@@ -51,7 +48,7 @@ function YAxisUnitSelector({
|
|||||||
loading={loading}
|
loading={loading}
|
||||||
data-testid={dataTestId}
|
data-testid={dataTestId}
|
||||||
>
|
>
|
||||||
{categories.map((category) => (
|
{Y_AXIS_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,6 +1,5 @@
|
|||||||
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', () => {
|
||||||
@@ -11,13 +10,7 @@ describe('YAxisUnitSelector', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('renders with default placeholder', () => {
|
it('renders with default placeholder', () => {
|
||||||
render(
|
render(<YAxisUnitSelector value="" onChange={mockOnChange} />);
|
||||||
<YAxisUnitSelector
|
|
||||||
value=""
|
|
||||||
onChange={mockOnChange}
|
|
||||||
source={YAxisSource.ALERTS}
|
|
||||||
/>,
|
|
||||||
);
|
|
||||||
expect(screen.getByText('Please select a unit')).toBeInTheDocument();
|
expect(screen.getByText('Please select a unit')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -27,20 +20,13 @@ 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(
|
render(<YAxisUnitSelector value="" onChange={mockOnChange} />);
|
||||||
<YAxisUnitSelector
|
|
||||||
value=""
|
|
||||||
onChange={mockOnChange}
|
|
||||||
source={YAxisSource.ALERTS}
|
|
||||||
/>,
|
|
||||||
);
|
|
||||||
const select = screen.getByRole('combobox');
|
const select = screen.getByRole('combobox');
|
||||||
|
|
||||||
fireEvent.mouseDown(select);
|
fireEvent.mouseDown(select);
|
||||||
@@ -55,30 +41,18 @@ describe('YAxisUnitSelector', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('filters options based on search input', () => {
|
it('filters options based on search input', () => {
|
||||||
render(
|
render(<YAxisUnitSelector value="" onChange={mockOnChange} />);
|
||||||
<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: 'bytes/sec' } });
|
fireEvent.change(input, { target: { value: 'byte' } });
|
||||||
|
|
||||||
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(
|
render(<YAxisUnitSelector value="" onChange={mockOnChange} />);
|
||||||
<YAxisUnitSelector
|
|
||||||
value=""
|
|
||||||
onChange={mockOnChange}
|
|
||||||
source={YAxisSource.ALERTS}
|
|
||||||
/>,
|
|
||||||
);
|
|
||||||
const select = screen.getByRole('combobox');
|
const select = screen.getByRole('combobox');
|
||||||
|
|
||||||
fireEvent.mouseDown(select);
|
fireEvent.mouseDown(select);
|
||||||
|
|||||||
@@ -1,951 +0,0 @@
|
|||||||
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,8 +1,6 @@
|
|||||||
import { UniversalYAxisUnit } from '../types';
|
|
||||||
import {
|
import {
|
||||||
getUniversalNameFromMetricUnit,
|
getUniversalNameFromMetricUnit,
|
||||||
mapMetricUnitToUniversalUnit,
|
mapMetricUnitToUniversalUnit,
|
||||||
mergeCategories,
|
|
||||||
} from '../utils';
|
} from '../utils';
|
||||||
|
|
||||||
describe('YAxisUnitSelector utils', () => {
|
describe('YAxisUnitSelector utils', () => {
|
||||||
@@ -38,43 +36,4 @@ 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 }],
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,90 +0,0 @@
|
|||||||
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,14 +17,6 @@ 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',
|
||||||
@@ -37,17 +29,6 @@ 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',
|
||||||
@@ -58,21 +39,9 @@ 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',
|
||||||
@@ -93,16 +62,6 @@ 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',
|
||||||
@@ -128,231 +87,7 @@ 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 {
|
||||||
@@ -558,15 +293,6 @@ 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',
|
||||||
|
|
||||||
@@ -597,24 +323,6 @@ 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',
|
||||||
|
|
||||||
@@ -656,27 +364,3 @@ 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,11 +1,5 @@
|
|||||||
import { UniversalYAxisUnitMappings, Y_AXIS_UNIT_NAMES } from './constants';
|
import { UniversalYAxisUnitMappings, Y_AXIS_UNIT_NAMES } from './constants';
|
||||||
import { ADDITIONAL_Y_AXIS_CATEGORIES, BASE_Y_AXIS_CATEGORIES } from './data';
|
import { UniversalYAxisUnit, YAxisUnit } from './types';
|
||||||
import {
|
|
||||||
UniversalYAxisUnit,
|
|
||||||
YAxisCategory,
|
|
||||||
YAxisSource,
|
|
||||||
YAxisUnit,
|
|
||||||
} from './types';
|
|
||||||
|
|
||||||
export const mapMetricUnitToUniversalUnit = (
|
export const mapMetricUnitToUniversalUnit = (
|
||||||
unit: string | undefined,
|
unit: string | undefined,
|
||||||
@@ -15,7 +9,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;
|
||||||
@@ -37,44 +31,3 @@ 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,208 +0,0 @@
|
|||||||
/**
|
|
||||||
* src/components/cmdKPalette/__test__/cmdkPalette.test.tsx
|
|
||||||
*/
|
|
||||||
import '@testing-library/jest-dom/extend-expect';
|
|
||||||
|
|
||||||
// ---- Mocks (must run BEFORE importing the component) ----
|
|
||||||
import ROUTES from 'constants/routes';
|
|
||||||
import history from 'lib/history';
|
|
||||||
import { render, screen, userEvent } from 'tests/test-utils';
|
|
||||||
|
|
||||||
import { CmdKPalette } from '../cmdKPalette';
|
|
||||||
|
|
||||||
const HOME_LABEL = 'Go to Home';
|
|
||||||
|
|
||||||
beforeAll(() => {
|
|
||||||
Object.defineProperty(HTMLElement.prototype, 'scrollIntoView', {
|
|
||||||
configurable: true,
|
|
||||||
value: jest.fn(),
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
afterAll(() => {
|
|
||||||
// restore
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
// @ts-ignore
|
|
||||||
delete (HTMLElement.prototype as any).scrollIntoView;
|
|
||||||
});
|
|
||||||
|
|
||||||
// mock history.push / replace / go / location
|
|
||||||
jest.mock('lib/history', () => {
|
|
||||||
const location = { pathname: '/', search: '', hash: '' };
|
|
||||||
|
|
||||||
const stack: { pathname: string; search: string }[] = [
|
|
||||||
{ pathname: '/', search: '' },
|
|
||||||
];
|
|
||||||
|
|
||||||
const push = jest.fn((path: string) => {
|
|
||||||
const [rawPath, rawQuery] = path.split('?');
|
|
||||||
const pathname = rawPath || '/';
|
|
||||||
const search = path.includes('?') ? `?${rawQuery || ''}` : '';
|
|
||||||
|
|
||||||
location.pathname = pathname;
|
|
||||||
location.search = search;
|
|
||||||
|
|
||||||
stack.push({ pathname, search });
|
|
||||||
return undefined;
|
|
||||||
});
|
|
||||||
|
|
||||||
const replace = jest.fn((path: string) => {
|
|
||||||
const [rawPath, rawQuery] = path.split('?');
|
|
||||||
const pathname = rawPath || '/';
|
|
||||||
const search = path.includes('?') ? `?${rawQuery || ''}` : '';
|
|
||||||
|
|
||||||
location.pathname = pathname;
|
|
||||||
location.search = search;
|
|
||||||
|
|
||||||
if (stack.length > 0) {
|
|
||||||
stack[stack.length - 1] = { pathname, search };
|
|
||||||
} else {
|
|
||||||
stack.push({ pathname, search });
|
|
||||||
}
|
|
||||||
return undefined;
|
|
||||||
});
|
|
||||||
|
|
||||||
const listen = jest.fn();
|
|
||||||
const go = jest.fn((n: number) => {
|
|
||||||
if (n < 0 && stack.length > 1) {
|
|
||||||
stack.pop();
|
|
||||||
}
|
|
||||||
const top = stack[stack.length - 1] || { pathname: '/', search: '' };
|
|
||||||
location.pathname = top.pathname;
|
|
||||||
location.search = top.search;
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
push,
|
|
||||||
replace,
|
|
||||||
listen,
|
|
||||||
go,
|
|
||||||
location,
|
|
||||||
__stack: stack,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
// Mock ResizeObserver for Jest/jsdom
|
|
||||||
class ResizeObserver {
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type, class-methods-use-this
|
|
||||||
observe() {}
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type, class-methods-use-this
|
|
||||||
unobserve() {}
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type, class-methods-use-this
|
|
||||||
disconnect() {}
|
|
||||||
}
|
|
||||||
|
|
||||||
(global as any).ResizeObserver = ResizeObserver;
|
|
||||||
|
|
||||||
// mock cmdK provider hook (open state + setter)
|
|
||||||
const mockSetOpen = jest.fn();
|
|
||||||
jest.mock('providers/cmdKProvider', (): unknown => ({
|
|
||||||
useCmdK: (): {
|
|
||||||
open: boolean;
|
|
||||||
setOpen: jest.Mock;
|
|
||||||
openCmdK: jest.Mock;
|
|
||||||
closeCmdK: jest.Mock;
|
|
||||||
} => ({
|
|
||||||
open: true,
|
|
||||||
setOpen: mockSetOpen,
|
|
||||||
openCmdK: jest.fn(),
|
|
||||||
closeCmdK: jest.fn(),
|
|
||||||
}),
|
|
||||||
}));
|
|
||||||
|
|
||||||
// mock notifications hook
|
|
||||||
jest.mock('hooks/useNotifications', (): unknown => ({
|
|
||||||
useNotifications: (): { notifications: [] } => ({ notifications: [] }),
|
|
||||||
}));
|
|
||||||
|
|
||||||
// mock theme hook
|
|
||||||
jest.mock('hooks/useDarkMode', (): unknown => ({
|
|
||||||
useThemeMode: (): {
|
|
||||||
setAutoSwitch: jest.Mock;
|
|
||||||
setTheme: jest.Mock;
|
|
||||||
theme: string;
|
|
||||||
} => ({
|
|
||||||
setAutoSwitch: jest.fn(),
|
|
||||||
setTheme: jest.fn(),
|
|
||||||
theme: 'dark',
|
|
||||||
}),
|
|
||||||
}));
|
|
||||||
|
|
||||||
// mock updateUserPreference API and react-query mutation
|
|
||||||
jest.mock('api/v1/user/preferences/name/update', (): jest.Mock => jest.fn());
|
|
||||||
jest.mock('react-query', (): unknown => {
|
|
||||||
const actual = jest.requireActual('react-query');
|
|
||||||
return {
|
|
||||||
...actual,
|
|
||||||
useMutation: (): { mutate: jest.Mock } => ({ mutate: jest.fn() }),
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
// mock other side-effecty modules
|
|
||||||
jest.mock('api/common/logEvent', () => jest.fn());
|
|
||||||
jest.mock('api/browser/localstorage/set', () => jest.fn());
|
|
||||||
jest.mock('utils/error', () => ({ showErrorNotification: jest.fn() }));
|
|
||||||
|
|
||||||
// ---- Tests ----
|
|
||||||
describe('CmdKPalette', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
jest.clearAllMocks();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('renders navigation and settings groups and items', () => {
|
|
||||||
render(<CmdKPalette userRole="ADMIN" />);
|
|
||||||
|
|
||||||
expect(screen.getByText('Navigation')).toBeInTheDocument();
|
|
||||||
expect(screen.getByText('Settings')).toBeInTheDocument();
|
|
||||||
|
|
||||||
expect(screen.getByText(HOME_LABEL)).toBeInTheDocument();
|
|
||||||
expect(screen.getByText('Go to Dashboards')).toBeInTheDocument();
|
|
||||||
expect(screen.getByText('Open Sidebar')).toBeInTheDocument();
|
|
||||||
expect(screen.getByText('Switch to Dark Mode')).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('clicking a navigation item calls history.push with correct route', async () => {
|
|
||||||
render(<CmdKPalette userRole="ADMIN" />);
|
|
||||||
|
|
||||||
const homeItem = screen.getByText(HOME_LABEL);
|
|
||||||
await userEvent.click(homeItem);
|
|
||||||
|
|
||||||
expect(history.push).toHaveBeenCalledWith(ROUTES.HOME);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('role-based filtering (basic smoke)', () => {
|
|
||||||
render(<CmdKPalette userRole="VIEWER" />);
|
|
||||||
|
|
||||||
// VIEWER still sees basic navigation items
|
|
||||||
expect(screen.getByText(HOME_LABEL)).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('keyboard shortcut opens palette via setOpen', () => {
|
|
||||||
render(<CmdKPalette userRole="ADMIN" />);
|
|
||||||
|
|
||||||
const event = new KeyboardEvent('keydown', { key: 'k', ctrlKey: true });
|
|
||||||
window.dispatchEvent(event);
|
|
||||||
|
|
||||||
expect(mockSetOpen).toHaveBeenCalledWith(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('items render with icons when provided', () => {
|
|
||||||
render(<CmdKPalette userRole="ADMIN" />);
|
|
||||||
|
|
||||||
const iconHolders = document.querySelectorAll('.cmd-item-icon');
|
|
||||||
expect(iconHolders.length).toBeGreaterThan(0);
|
|
||||||
expect(screen.getByText(HOME_LABEL)).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('closing the palette via handleInvoke sets open to false', async () => {
|
|
||||||
render(<CmdKPalette userRole="ADMIN" />);
|
|
||||||
|
|
||||||
const dashItem = screen.getByText('Go to Dashboards');
|
|
||||||
await userEvent.click(dashItem);
|
|
||||||
|
|
||||||
// last call from handleInvoke should set open to false
|
|
||||||
expect(mockSetOpen).toHaveBeenCalledWith(false);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
/* Overlay stays below content */
|
|
||||||
[data-slot='dialog-overlay'] {
|
|
||||||
z-index: 50;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Dialog content always above overlay */
|
|
||||||
[data-slot='dialog-content'] {
|
|
||||||
position: fixed;
|
|
||||||
z-index: 60;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cmdk-section-heading [cmdk-group-heading] {
|
|
||||||
text-transform: uppercase;
|
|
||||||
color: var(--bg-slate-100);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Hide scrollbar but keep scroll */
|
|
||||||
.cmdk-list-scroll {
|
|
||||||
scrollbar-width: none; /* Firefox */
|
|
||||||
}
|
|
||||||
|
|
||||||
.cmdk-list-scroll::-webkit-scrollbar {
|
|
||||||
display: none; /* Chrome, Safari, Edge */
|
|
||||||
}
|
|
||||||
|
|
||||||
.cmdk-list-scroll {
|
|
||||||
-webkit-overflow-scrolling: touch;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cmdk-input-wrapper {
|
|
||||||
margin-left: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cmdk-item-light:hover {
|
|
||||||
cursor: pointer;
|
|
||||||
background-color: var(--bg-vanilla-200) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cmdk-item-light[data-selected='true'] {
|
|
||||||
background-color: var(--bg-vanilla-300) !important;
|
|
||||||
color: var(--bg-ink-500);
|
|
||||||
}
|
|
||||||
|
|
||||||
.cmdk-item {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
[cmdk-item] svg {
|
|
||||||
width: auto;
|
|
||||||
height: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cmd-item-icon {
|
|
||||||
margin-right: 8px;
|
|
||||||
}
|
|
||||||
@@ -1,336 +0,0 @@
|
|||||||
import './cmdKPalette.scss';
|
|
||||||
|
|
||||||
import {
|
|
||||||
CommandDialog,
|
|
||||||
CommandEmpty,
|
|
||||||
CommandGroup,
|
|
||||||
CommandInput,
|
|
||||||
CommandItem,
|
|
||||||
CommandList,
|
|
||||||
CommandShortcut,
|
|
||||||
} from '@signozhq/command';
|
|
||||||
import setLocalStorageApi from 'api/browser/localstorage/set';
|
|
||||||
import logEvent from 'api/common/logEvent';
|
|
||||||
import updateUserPreference from 'api/v1/user/preferences/name/update';
|
|
||||||
import { AxiosError } from 'axios';
|
|
||||||
import ROUTES from 'constants/routes';
|
|
||||||
import { USER_PREFERENCES } from 'constants/userPreferences';
|
|
||||||
import { useThemeMode } from 'hooks/useDarkMode';
|
|
||||||
import { THEME_MODE } from 'hooks/useDarkMode/constant';
|
|
||||||
import { useNotifications } from 'hooks/useNotifications';
|
|
||||||
import history from 'lib/history';
|
|
||||||
import {
|
|
||||||
BellDot,
|
|
||||||
BugIcon,
|
|
||||||
DraftingCompass,
|
|
||||||
Expand,
|
|
||||||
HardDrive,
|
|
||||||
Home,
|
|
||||||
LayoutGrid,
|
|
||||||
ListMinus,
|
|
||||||
ScrollText,
|
|
||||||
Settings,
|
|
||||||
} from 'lucide-react';
|
|
||||||
import React, { useEffect } from 'react';
|
|
||||||
import { useMutation } from 'react-query';
|
|
||||||
import { UserPreference } from 'types/api/preferences/preference';
|
|
||||||
import { showErrorNotification } from 'utils/error';
|
|
||||||
|
|
||||||
import { useAppContext } from '../../providers/App/App';
|
|
||||||
import { useCmdK } from '../../providers/cmdKProvider';
|
|
||||||
|
|
||||||
type CmdAction = {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
shortcut?: string[];
|
|
||||||
keywords?: string;
|
|
||||||
section?: string;
|
|
||||||
icon?: React.ReactNode;
|
|
||||||
roles?: UserRole[];
|
|
||||||
perform: () => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
type UserRole = 'ADMIN' | 'EDITOR' | 'AUTHOR' | 'VIEWER';
|
|
||||||
export function CmdKPalette({
|
|
||||||
userRole,
|
|
||||||
}: {
|
|
||||||
userRole: UserRole;
|
|
||||||
}): JSX.Element | null {
|
|
||||||
const { open, setOpen } = useCmdK();
|
|
||||||
|
|
||||||
const { updateUserPreferenceInContext } = useAppContext();
|
|
||||||
const { notifications } = useNotifications();
|
|
||||||
const { setAutoSwitch, setTheme, theme } = useThemeMode();
|
|
||||||
|
|
||||||
const { mutate: updateUserPreferenceMutation } = useMutation(
|
|
||||||
updateUserPreference,
|
|
||||||
{
|
|
||||||
onError: (error) => {
|
|
||||||
showErrorNotification(notifications, error as AxiosError);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
// toggle palette with ⌘/Ctrl+K
|
|
||||||
function handleGlobalCmdK(
|
|
||||||
e: KeyboardEvent,
|
|
||||||
setOpen: React.Dispatch<React.SetStateAction<boolean>>,
|
|
||||||
): void {
|
|
||||||
if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === 'k') {
|
|
||||||
e.preventDefault();
|
|
||||||
setOpen(true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const cmdKEffect = (): void | (() => void) => {
|
|
||||||
const listener = (e: KeyboardEvent): void => {
|
|
||||||
handleGlobalCmdK(e, setOpen);
|
|
||||||
};
|
|
||||||
|
|
||||||
window.addEventListener('keydown', listener);
|
|
||||||
|
|
||||||
return (): void => {
|
|
||||||
window.removeEventListener('keydown', listener);
|
|
||||||
setOpen(false);
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(cmdKEffect, [setOpen]);
|
|
||||||
|
|
||||||
function handleThemeChange(value: string): void {
|
|
||||||
logEvent('Account Settings: Theme Changed', { theme: value });
|
|
||||||
if (value === 'auto') {
|
|
||||||
setAutoSwitch(true);
|
|
||||||
} else {
|
|
||||||
setAutoSwitch(false);
|
|
||||||
setTheme(value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function onClickHandler(key: string): void {
|
|
||||||
history.push(key);
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleOpenSidebar(): void {
|
|
||||||
setLocalStorageApi(USER_PREFERENCES.SIDENAV_PINNED, 'true');
|
|
||||||
const save = { name: USER_PREFERENCES.SIDENAV_PINNED, value: true };
|
|
||||||
updateUserPreferenceInContext(save as UserPreference);
|
|
||||||
updateUserPreferenceMutation({
|
|
||||||
name: USER_PREFERENCES.SIDENAV_PINNED,
|
|
||||||
value: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleCloseSidebar(): void {
|
|
||||||
setLocalStorageApi(USER_PREFERENCES.SIDENAV_PINNED, 'false');
|
|
||||||
const save = { name: USER_PREFERENCES.SIDENAV_PINNED, value: false };
|
|
||||||
updateUserPreferenceInContext(save as UserPreference);
|
|
||||||
updateUserPreferenceMutation({
|
|
||||||
name: USER_PREFERENCES.SIDENAV_PINNED,
|
|
||||||
value: false,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const actions: CmdAction[] = [
|
|
||||||
{
|
|
||||||
id: 'home',
|
|
||||||
name: 'Go to Home',
|
|
||||||
shortcut: ['shift + h'],
|
|
||||||
keywords: 'home',
|
|
||||||
section: 'Navigation',
|
|
||||||
icon: <Home size={14} />,
|
|
||||||
roles: ['ADMIN', 'EDITOR', 'AUTHOR', 'VIEWER'],
|
|
||||||
perform: (): void => onClickHandler(ROUTES.HOME),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'dashboards',
|
|
||||||
name: 'Go to Dashboards',
|
|
||||||
shortcut: ['shift + d'],
|
|
||||||
keywords: 'dashboards',
|
|
||||||
section: 'Navigation',
|
|
||||||
icon: <LayoutGrid size={14} />,
|
|
||||||
roles: ['ADMIN', 'EDITOR', 'AUTHOR', 'VIEWER'],
|
|
||||||
perform: (): void => onClickHandler(ROUTES.ALL_DASHBOARD),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'services',
|
|
||||||
name: 'Go to Services',
|
|
||||||
shortcut: ['shift + s'],
|
|
||||||
keywords: 'services monitoring',
|
|
||||||
section: 'Navigation',
|
|
||||||
icon: <HardDrive size={14} />,
|
|
||||||
roles: ['ADMIN', 'EDITOR', 'AUTHOR', 'VIEWER'],
|
|
||||||
perform: (): void => onClickHandler(ROUTES.APPLICATION),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'traces',
|
|
||||||
name: 'Go to Traces',
|
|
||||||
shortcut: ['shift + t'],
|
|
||||||
keywords: 'traces',
|
|
||||||
section: 'Navigation',
|
|
||||||
icon: <DraftingCompass size={14} />,
|
|
||||||
roles: ['ADMIN', 'EDITOR', 'AUTHOR', 'VIEWER'],
|
|
||||||
perform: (): void => onClickHandler(ROUTES.TRACES_EXPLORER),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'logs',
|
|
||||||
name: 'Go to Logs',
|
|
||||||
shortcut: ['shift + l'],
|
|
||||||
keywords: 'logs',
|
|
||||||
section: 'Navigation',
|
|
||||||
icon: <ScrollText size={14} />,
|
|
||||||
roles: ['ADMIN', 'EDITOR', 'AUTHOR', 'VIEWER'],
|
|
||||||
perform: (): void => onClickHandler(ROUTES.LOGS),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'alerts',
|
|
||||||
name: 'Go to Alerts',
|
|
||||||
shortcut: ['shift + a'],
|
|
||||||
keywords: 'alerts',
|
|
||||||
section: 'Navigation',
|
|
||||||
icon: <BellDot size={14} />,
|
|
||||||
roles: ['ADMIN', 'EDITOR', 'AUTHOR', 'VIEWER'],
|
|
||||||
perform: (): void => onClickHandler(ROUTES.LIST_ALL_ALERT),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'exceptions',
|
|
||||||
name: 'Go to Exceptions',
|
|
||||||
shortcut: ['shift + e'],
|
|
||||||
keywords: 'exceptions errors',
|
|
||||||
section: 'Navigation',
|
|
||||||
icon: <BugIcon size={14} />,
|
|
||||||
roles: ['ADMIN', 'EDITOR', 'AUTHOR', 'VIEWER'],
|
|
||||||
perform: (): void => onClickHandler(ROUTES.ALL_ERROR),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'messaging-queues',
|
|
||||||
name: 'Go to Messaging Queues',
|
|
||||||
shortcut: ['shift + m'],
|
|
||||||
keywords: 'messaging queues mq',
|
|
||||||
section: 'Navigation',
|
|
||||||
icon: <ListMinus size={14} />,
|
|
||||||
roles: ['ADMIN', 'EDITOR', 'AUTHOR', 'VIEWER'],
|
|
||||||
perform: (): void => onClickHandler(ROUTES.MESSAGING_QUEUES_OVERVIEW),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'my-settings',
|
|
||||||
name: 'Go to Account Settings',
|
|
||||||
keywords: 'account settings',
|
|
||||||
section: 'Navigation',
|
|
||||||
icon: <Settings size={14} />,
|
|
||||||
roles: ['ADMIN', 'EDITOR', 'AUTHOR', 'VIEWER'],
|
|
||||||
perform: (): void => onClickHandler(ROUTES.MY_SETTINGS),
|
|
||||||
},
|
|
||||||
|
|
||||||
// Settings
|
|
||||||
{
|
|
||||||
id: 'open-sidebar',
|
|
||||||
name: 'Open Sidebar',
|
|
||||||
keywords: 'sidebar navigation menu expand',
|
|
||||||
section: 'Settings',
|
|
||||||
icon: <Expand size={14} />,
|
|
||||||
roles: ['ADMIN', 'EDITOR', 'AUTHOR', 'VIEWER'],
|
|
||||||
perform: (): void => handleOpenSidebar(),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'collapse-sidebar',
|
|
||||||
name: 'Collapse Sidebar',
|
|
||||||
keywords: 'sidebar navigation menu collapse',
|
|
||||||
section: 'Settings',
|
|
||||||
icon: <Expand size={14} />,
|
|
||||||
roles: ['ADMIN', 'EDITOR', 'AUTHOR', 'VIEWER'],
|
|
||||||
perform: (): void => handleCloseSidebar(),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'dark-mode',
|
|
||||||
name: 'Switch to Dark Mode',
|
|
||||||
keywords: 'theme dark mode appearance',
|
|
||||||
section: 'Settings',
|
|
||||||
icon: <Expand size={14} />,
|
|
||||||
roles: ['ADMIN', 'EDITOR', 'AUTHOR', 'VIEWER'],
|
|
||||||
perform: (): void => handleThemeChange(THEME_MODE.DARK),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'light-mode',
|
|
||||||
name: 'Switch to Light Mode [Beta]',
|
|
||||||
keywords: 'theme light mode appearance',
|
|
||||||
section: 'Settings',
|
|
||||||
icon: <Expand size={14} />,
|
|
||||||
roles: ['ADMIN', 'EDITOR', 'AUTHOR', 'VIEWER'],
|
|
||||||
perform: (): void => handleThemeChange(THEME_MODE.LIGHT),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'system-theme',
|
|
||||||
name: 'Switch to System Theme',
|
|
||||||
keywords: 'system theme appearance',
|
|
||||||
section: 'Settings',
|
|
||||||
icon: <Expand size={14} />,
|
|
||||||
roles: ['ADMIN', 'EDITOR', 'AUTHOR', 'VIEWER'],
|
|
||||||
perform: (): void => handleThemeChange(THEME_MODE.SYSTEM),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
// RBAC filter: show action if no roles set OR current user role is included
|
|
||||||
const permitted = actions.filter(
|
|
||||||
(a) => !a.roles || a.roles.includes(userRole),
|
|
||||||
);
|
|
||||||
|
|
||||||
// group permitted actions by section
|
|
||||||
const grouped: [string, CmdAction[]][] = ((): [string, CmdAction[]][] => {
|
|
||||||
const map = new Map<string, CmdAction[]>();
|
|
||||||
|
|
||||||
permitted.forEach((a) => {
|
|
||||||
const section = a.section ?? 'Other';
|
|
||||||
const existing = map.get(section);
|
|
||||||
|
|
||||||
if (existing) {
|
|
||||||
existing.push(a);
|
|
||||||
} else {
|
|
||||||
map.set(section, [a]);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return Array.from(map.entries());
|
|
||||||
})();
|
|
||||||
|
|
||||||
const handleInvoke = (action: CmdAction): void => {
|
|
||||||
try {
|
|
||||||
action.perform();
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Error invoking action', e);
|
|
||||||
} finally {
|
|
||||||
setOpen(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<CommandDialog open={open} onOpenChange={setOpen} position="top" offset={110}>
|
|
||||||
<CommandInput placeholder="Search…" className="cmdk-input-wrapper" />
|
|
||||||
<CommandList className="cmdk-list-scroll">
|
|
||||||
<CommandEmpty>No results</CommandEmpty>
|
|
||||||
{grouped.map(([section, items]) => (
|
|
||||||
<CommandGroup
|
|
||||||
key={section}
|
|
||||||
heading={section}
|
|
||||||
className="cmdk-section-heading"
|
|
||||||
>
|
|
||||||
{items.map((it) => (
|
|
||||||
<CommandItem
|
|
||||||
key={it.id}
|
|
||||||
onSelect={(): void => handleInvoke(it)}
|
|
||||||
value={it.name}
|
|
||||||
className={theme === 'light' ? 'cmdk-item-light' : 'cmdk-item'}
|
|
||||||
>
|
|
||||||
<span className="cmd-item-icon">{it.icon}</span>
|
|
||||||
{it.name}
|
|
||||||
{it.shortcut && it.shortcut.length > 0 && (
|
|
||||||
<CommandShortcut>{it.shortcut.join(' • ')}</CommandShortcut>
|
|
||||||
)}
|
|
||||||
</CommandItem>
|
|
||||||
))}
|
|
||||||
</CommandGroup>
|
|
||||||
))}
|
|
||||||
</CommandList>
|
|
||||||
</CommandDialog>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,7 +1,4 @@
|
|||||||
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,7 +81,6 @@ 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;
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ const themeColors = {
|
|||||||
cyan: '#00FFFF',
|
cyan: '#00FFFF',
|
||||||
},
|
},
|
||||||
chartcolors: {
|
chartcolors: {
|
||||||
radicalRed: '#FF1A66',
|
robin: '#3F5ECC',
|
||||||
dodgerBlue: '#2F80ED',
|
dodgerBlue: '#2F80ED',
|
||||||
mediumOrchid: '#BB6BD9',
|
mediumOrchid: '#BB6BD9',
|
||||||
seaBuckthorn: '#F2994A',
|
seaBuckthorn: '#F2994A',
|
||||||
@@ -58,7 +58,7 @@ const themeColors = {
|
|||||||
oliveDrab: '#66991A',
|
oliveDrab: '#66991A',
|
||||||
lavenderRose: '#FF99E6',
|
lavenderRose: '#FF99E6',
|
||||||
electricLime: '#CCFF1A',
|
electricLime: '#CCFF1A',
|
||||||
robin: '#3F5ECC',
|
radicalRed: '#FF1A66',
|
||||||
harleyOrange: '#E6331A',
|
harleyOrange: '#E6331A',
|
||||||
turquoise: '#33FFCC',
|
turquoise: '#33FFCC',
|
||||||
gladeGreen: '#66994D',
|
gladeGreen: '#66994D',
|
||||||
@@ -80,7 +80,7 @@ const themeColors = {
|
|||||||
maroon: '#800000',
|
maroon: '#800000',
|
||||||
navy: '#000080',
|
navy: '#000080',
|
||||||
aquamarine: '#7FFFD4',
|
aquamarine: '#7FFFD4',
|
||||||
darkSeaGreen: '#8FBC8F',
|
gold: '#FFD700',
|
||||||
gray: '#808080',
|
gray: '#808080',
|
||||||
skyBlue: '#87CEEB',
|
skyBlue: '#87CEEB',
|
||||||
indigo: '#4B0082',
|
indigo: '#4B0082',
|
||||||
@@ -105,7 +105,7 @@ const themeColors = {
|
|||||||
lawnGreen: '#7CFC00',
|
lawnGreen: '#7CFC00',
|
||||||
mediumSeaGreen: '#3CB371',
|
mediumSeaGreen: '#3CB371',
|
||||||
lightCoral: '#F08080',
|
lightCoral: '#F08080',
|
||||||
gold: '#FFD700',
|
darkSeaGreen: '#8FBC8F',
|
||||||
sandyBrown: '#F4A460',
|
sandyBrown: '#F4A460',
|
||||||
darkKhaki: '#BDB76B',
|
darkKhaki: '#BDB76B',
|
||||||
cornflowerBlue: '#6495ED',
|
cornflowerBlue: '#6495ED',
|
||||||
@@ -113,7 +113,7 @@ const themeColors = {
|
|||||||
paleGreen: '#98FB98',
|
paleGreen: '#98FB98',
|
||||||
},
|
},
|
||||||
lightModeColor: {
|
lightModeColor: {
|
||||||
radicalRed: '#FF1A66',
|
robin: '#3F5ECC',
|
||||||
dodgerBlueDark: '#0C6EED',
|
dodgerBlueDark: '#0C6EED',
|
||||||
steelgrey: '#2f4b7c',
|
steelgrey: '#2f4b7c',
|
||||||
steelpurple: '#665191',
|
steelpurple: '#665191',
|
||||||
@@ -143,7 +143,7 @@ const themeColors = {
|
|||||||
oliveDrab: '#66991A',
|
oliveDrab: '#66991A',
|
||||||
lavenderRoseDark: '#F024BD',
|
lavenderRoseDark: '#F024BD',
|
||||||
electricLimeDark: '#84A800',
|
electricLimeDark: '#84A800',
|
||||||
robin: '#3F5ECC',
|
radicalRed: '#FF1A66',
|
||||||
harleyOrange: '#E6331A',
|
harleyOrange: '#E6331A',
|
||||||
gladeGreen: '#66994D',
|
gladeGreen: '#66994D',
|
||||||
hemlock: '#66664D',
|
hemlock: '#66664D',
|
||||||
@@ -181,7 +181,7 @@ const themeColors = {
|
|||||||
darkOrchid: '#9932CC',
|
darkOrchid: '#9932CC',
|
||||||
mediumSeaGreenDark: '#109E50',
|
mediumSeaGreenDark: '#109E50',
|
||||||
lightCoralDark: '#F85959',
|
lightCoralDark: '#F85959',
|
||||||
gold: '#FFD700',
|
darkSeaGreenDark: '#509F50',
|
||||||
sandyBrownDark: '#D97117',
|
sandyBrownDark: '#D97117',
|
||||||
darkKhakiDark: '#99900A',
|
darkKhakiDark: '#99900A',
|
||||||
cornflowerBlueDark: '#3371E6',
|
cornflowerBlueDark: '#3371E6',
|
||||||
|
|||||||