Compare commits
5 Commits
cursor/cop
...
enh/conver
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
db004aac4c | ||
|
|
0ecaa1779f | ||
|
|
4161a711de | ||
|
|
4de2783944 | ||
|
|
69f33d6fe3 |
@@ -42,7 +42,7 @@ services:
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
schema-migrator-sync:
|
||||
image: signoz/signoz-schema-migrator:v0.129.12
|
||||
image: signoz/signoz-schema-migrator:v0.129.11
|
||||
container_name: schema-migrator-sync
|
||||
command:
|
||||
- sync
|
||||
@@ -55,7 +55,7 @@ services:
|
||||
condition: service_healthy
|
||||
restart: on-failure
|
||||
schema-migrator-async:
|
||||
image: signoz/signoz-schema-migrator:v0.129.12
|
||||
image: signoz/signoz-schema-migrator:v0.129.11
|
||||
container_name: schema-migrator-async
|
||||
command:
|
||||
- async
|
||||
|
||||
4
.github/CODEOWNERS
vendored
4
.github/CODEOWNERS
vendored
@@ -6,10 +6,6 @@
|
||||
/frontend/src/container/MetricsApplication @srikanthccv
|
||||
/frontend/src/container/NewWidget/RightContainer/types.ts @srikanthccv
|
||||
|
||||
# Onboarding
|
||||
/frontend/src/container/OnboardingV2Container/onboarding-configs/onboarding-config-with-links.json @makeavish
|
||||
/frontend/src/container/OnboardingV2Container/AddDataSource/AddDataSource.tsx @makeavish
|
||||
|
||||
# Dashboard, Alert, Metrics, Service Map, Services
|
||||
/frontend/src/container/ListOfDashboard/ @srikanthccv
|
||||
/frontend/src/container/NewDashboard/ @srikanthccv
|
||||
|
||||
1
.github/workflows/build-enterprise.yaml
vendored
1
.github/workflows/build-enterprise.yaml
vendored
@@ -69,7 +69,6 @@ jobs:
|
||||
echo 'POSTHOG_KEY="${{ secrets.POSTHOG_KEY }}"' >> frontend/.env
|
||||
echo 'PYLON_APP_ID="${{ secrets.PYLON_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
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
|
||||
1
.github/workflows/build-staging.yaml
vendored
1
.github/workflows/build-staging.yaml
vendored
@@ -68,7 +68,6 @@ jobs:
|
||||
echo 'TUNNEL_DOMAIN="${{ secrets.NP_TUNNEL_DOMAIN }}"' >> 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 'PYLON_IDENTITY_SECRET="${{ secrets.NP_PYLON_IDENTITY_SECRET }}"' >> frontend/.env
|
||||
- name: cache-dotenv
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
|
||||
1
.github/workflows/gor-signoz.yaml
vendored
1
.github/workflows/gor-signoz.yaml
vendored
@@ -35,7 +35,6 @@ jobs:
|
||||
echo 'POSTHOG_KEY="${{ secrets.POSTHOG_KEY }}"' >> .env
|
||||
echo 'PYLON_APP_ID="${{ secrets.PYLON_APP_ID }}"' >> .env
|
||||
echo 'APPCUES_APP_ID="${{ secrets.APPCUES_APP_ID }}"' >> .env
|
||||
echo 'PYLON_IDENTITY_SECRET="${{ secrets.PYLON_IDENTITY_SECRET }}"' >> .env
|
||||
- name: build-frontend
|
||||
run: make js-build
|
||||
- name: upload-frontend-artifact
|
||||
|
||||
2
Makefile
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_CLUSTER=cluster \
|
||||
go run -race \
|
||||
$(GO_BUILD_CONTEXT_ENTERPRISE)/*.go server
|
||||
$(GO_BUILD_CONTEXT_ENTERPRISE)/*.go
|
||||
|
||||
.PHONY: go-test
|
||||
go-test: ## Runs go unit tests
|
||||
|
||||
@@ -47,10 +47,10 @@ cache:
|
||||
provider: memory
|
||||
# memory: Uses in-memory caching.
|
||||
memory:
|
||||
# Max items for the in-memory cache (10x the entries)
|
||||
num_counters: 100000
|
||||
# Total cost in bytes allocated bounded cache
|
||||
max_cost: 67108864
|
||||
# Time-to-live for cache entries in memory. Specify the duration in ns
|
||||
ttl: 60000000000
|
||||
# The interval at which the cache will be cleaned up
|
||||
cleanup_interval: 1m
|
||||
# redis: Uses Redis as the caching backend.
|
||||
redis:
|
||||
# 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
|
||||
signoz:
|
||||
!!merge <<: *db-depend
|
||||
image: signoz/signoz:v0.103.0
|
||||
image: signoz/signoz:v0.102.1
|
||||
command:
|
||||
- --config=/root/config/prometheus.yml
|
||||
ports:
|
||||
@@ -209,7 +209,7 @@ services:
|
||||
retries: 3
|
||||
otel-collector:
|
||||
!!merge <<: *db-depend
|
||||
image: signoz/signoz-otel-collector:v0.129.12
|
||||
image: signoz/signoz-otel-collector:v0.129.11
|
||||
command:
|
||||
- --config=/etc/otel-collector-config.yaml
|
||||
- --manager-config=/etc/manager-config.yaml
|
||||
@@ -233,7 +233,7 @@ services:
|
||||
- signoz
|
||||
schema-migrator:
|
||||
!!merge <<: *common
|
||||
image: signoz/signoz-schema-migrator:v0.129.12
|
||||
image: signoz/signoz-schema-migrator:v0.129.11
|
||||
deploy:
|
||||
restart_policy:
|
||||
condition: on-failure
|
||||
|
||||
@@ -117,7 +117,7 @@ services:
|
||||
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
|
||||
signoz:
|
||||
!!merge <<: *db-depend
|
||||
image: signoz/signoz:v0.103.0
|
||||
image: signoz/signoz:v0.102.1
|
||||
command:
|
||||
- --config=/root/config/prometheus.yml
|
||||
ports:
|
||||
@@ -150,7 +150,7 @@ services:
|
||||
retries: 3
|
||||
otel-collector:
|
||||
!!merge <<: *db-depend
|
||||
image: signoz/signoz-otel-collector:v0.129.12
|
||||
image: signoz/signoz-otel-collector:v0.129.11
|
||||
command:
|
||||
- --config=/etc/otel-collector-config.yaml
|
||||
- --manager-config=/etc/manager-config.yaml
|
||||
@@ -176,7 +176,7 @@ services:
|
||||
- signoz
|
||||
schema-migrator:
|
||||
!!merge <<: *common
|
||||
image: signoz/signoz-schema-migrator:v0.129.12
|
||||
image: signoz/signoz-schema-migrator:v0.129.11
|
||||
deploy:
|
||||
restart_policy:
|
||||
condition: on-failure
|
||||
|
||||
@@ -179,7 +179,7 @@ services:
|
||||
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
|
||||
signoz:
|
||||
!!merge <<: *db-depend
|
||||
image: signoz/signoz:${VERSION:-v0.103.0}
|
||||
image: signoz/signoz:${VERSION:-v0.102.1}
|
||||
container_name: signoz
|
||||
command:
|
||||
- --config=/root/config/prometheus.yml
|
||||
@@ -213,7 +213,7 @@ services:
|
||||
# TODO: support otel-collector multiple replicas. Nginx/Traefik for loadbalancing?
|
||||
otel-collector:
|
||||
!!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
|
||||
command:
|
||||
- --config=/etc/otel-collector-config.yaml
|
||||
@@ -239,7 +239,7 @@ services:
|
||||
condition: service_healthy
|
||||
schema-migrator-sync:
|
||||
!!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
|
||||
command:
|
||||
- sync
|
||||
@@ -250,7 +250,7 @@ services:
|
||||
condition: service_healthy
|
||||
schema-migrator-async:
|
||||
!!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
|
||||
command:
|
||||
- async
|
||||
|
||||
@@ -111,7 +111,7 @@ services:
|
||||
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
|
||||
signoz:
|
||||
!!merge <<: *db-depend
|
||||
image: signoz/signoz:${VERSION:-v0.103.0}
|
||||
image: signoz/signoz:${VERSION:-v0.102.1}
|
||||
container_name: signoz
|
||||
command:
|
||||
- --config=/root/config/prometheus.yml
|
||||
@@ -144,7 +144,7 @@ services:
|
||||
retries: 3
|
||||
otel-collector:
|
||||
!!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
|
||||
command:
|
||||
- --config=/etc/otel-collector-config.yaml
|
||||
@@ -166,7 +166,7 @@ services:
|
||||
condition: service_healthy
|
||||
schema-migrator-sync:
|
||||
!!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
|
||||
command:
|
||||
- sync
|
||||
@@ -178,7 +178,7 @@ services:
|
||||
restart: on-failure
|
||||
schema-migrator-async:
|
||||
!!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
|
||||
command:
|
||||
- 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
|
||||
}
|
||||
|
||||
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) {
|
||||
if 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
|
||||
}
|
||||
|
||||
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) {
|
||||
certStore, err := a.getCertificateStore(authDomain)
|
||||
if err != nil {
|
||||
|
||||
@@ -9,7 +9,6 @@ import (
|
||||
_ "net/http/pprof" // http profiler
|
||||
"slices"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/cache/memorycache"
|
||||
"github.com/SigNoz/signoz/pkg/ruler/rulestore/sqlrulestore"
|
||||
"go.opentelemetry.io/contrib/instrumentation/github.com/gorilla/mux/otelmux"
|
||||
"go.opentelemetry.io/otel/propagation"
|
||||
@@ -75,26 +74,13 @@ func NewServer(config signoz.Config, signoz *signoz.SigNoz) (*Server, error) {
|
||||
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(
|
||||
signoz.SQLStore,
|
||||
signoz.TelemetryStore,
|
||||
signoz.Prometheus,
|
||||
signoz.TelemetryStore.Cluster(),
|
||||
config.Querier.FluxInterval,
|
||||
cacheForTraceDetail,
|
||||
signoz.Cache,
|
||||
nil,
|
||||
)
|
||||
|
||||
rm, err := makeRulesManager(
|
||||
|
||||
@@ -246,9 +246,7 @@ func (r *AnomalyRule) buildAndRunQuery(ctx context.Context, orgID valuer.UUID, t
|
||||
continue
|
||||
}
|
||||
}
|
||||
results, err := r.Threshold.Eval(*series, r.Unit(), ruletypes.EvalData{
|
||||
ActiveAlerts: r.ActiveAlertsLabelFP(),
|
||||
})
|
||||
results, err := r.Threshold.ShouldAlert(*series, r.Unit())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -298,9 +296,7 @@ func (r *AnomalyRule) buildAndRunQueryV5(ctx context.Context, orgID valuer.UUID,
|
||||
continue
|
||||
}
|
||||
}
|
||||
results, err := r.Threshold.Eval(*series, r.Unit(), ruletypes.EvalData{
|
||||
ActiveAlerts: r.ActiveAlertsLabelFP(),
|
||||
})
|
||||
results, err := r.Threshold.ShouldAlert(*series, r.Unit())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -414,7 +410,6 @@ func (r *AnomalyRule) Eval(ctx context.Context, ts time.Time) (interface{}, erro
|
||||
GeneratorURL: r.GeneratorURL(),
|
||||
Receivers: ruleReceiverMap[lbs.Map()[ruletypes.LabelThresholdName]],
|
||||
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.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 {
|
||||
alert.Receivers = ruleReceiverMap[v]
|
||||
}
|
||||
@@ -488,30 +480,6 @@ func (r *AnomalyRule) Eval(ctx context.Context, ts time.Time) (interface{}, erro
|
||||
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()
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
module.exports = {
|
||||
ignorePatterns: ['src/parser/*.ts', 'scripts/update-registry.js'],
|
||||
ignorePatterns: ['src/parser/*.ts'],
|
||||
env: {
|
||||
browser: true,
|
||||
es2021: true,
|
||||
|
||||
@@ -3,6 +3,5 @@ BUNDLE_ANALYSER="true"
|
||||
FRONTEND_API_ENDPOINT="http://localhost:8080/"
|
||||
PYLON_APP_ID="pylon-app-id"
|
||||
APPCUES_APP_ID="appcess-app-id"
|
||||
PYLON_IDENTITY_SECRET="pylon-identity-secret"
|
||||
|
||||
CI="1"
|
||||
@@ -14,7 +14,7 @@
|
||||
"jest": "jest",
|
||||
"jest:coverage": "jest --coverage",
|
||||
"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/*",
|
||||
"commitlint": "commitlint --edit $1",
|
||||
"test": "jest",
|
||||
@@ -38,7 +38,7 @@
|
||||
"@mdx-js/loader": "2.3.0",
|
||||
"@mdx-js/react": "2.3.0",
|
||||
"@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-tooltip": "1.0.7",
|
||||
"@sentry/react": "8.41.0",
|
||||
@@ -83,7 +83,6 @@
|
||||
"color": "^4.2.1",
|
||||
"color-alpha": "1.1.3",
|
||||
"cross-env": "^7.0.3",
|
||||
"crypto-js": "4.2.0",
|
||||
"css-loader": "5.0.0",
|
||||
"css-minimizer-webpack-plugin": "5.0.1",
|
||||
"d3-hierarchy": "3.1.2",
|
||||
@@ -113,7 +112,7 @@
|
||||
"overlayscrollbars": "^2.8.1",
|
||||
"overlayscrollbars-react": "^0.5.6",
|
||||
"papaparse": "5.4.1",
|
||||
"posthog-js": "1.298.0",
|
||||
"posthog-js": "1.215.5",
|
||||
"rc-tween-one": "3.0.6",
|
||||
"react": "18.2.0",
|
||||
"react-addons-update": "15.6.3",
|
||||
@@ -150,6 +149,7 @@
|
||||
"tsconfig-paths-webpack-plugin": "^3.5.1",
|
||||
"typescript": "^4.0.5",
|
||||
"uplot": "1.6.31",
|
||||
"userpilot": "1.3.9",
|
||||
"uuid": "^8.3.2",
|
||||
"web-vitals": "^0.2.4",
|
||||
"webpack": "5.94.0",
|
||||
@@ -186,7 +186,6 @@
|
||||
"@types/color": "^3.0.3",
|
||||
"@types/compression-webpack-plugin": "^9.0.0",
|
||||
"@types/copy-webpack-plugin": "^8.0.1",
|
||||
"@types/crypto-js": "4.2.2",
|
||||
"@types/dompurify": "^2.4.0",
|
||||
"@types/event-source-polyfill": "^1.0.0",
|
||||
"@types/fontfaceobserver": "2.1.0",
|
||||
|
||||
@@ -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 |
File diff suppressed because one or more lines are too long
|
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 |
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 19 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>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,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);
|
||||
}
|
||||
@@ -7,12 +7,11 @@ import AppLoading from 'components/AppLoading/AppLoading';
|
||||
import KBarCommandPalette from 'components/KBarCommandPalette/KBarCommandPalette';
|
||||
import NotFound from 'components/NotFound';
|
||||
import Spinner from 'components/Spinner';
|
||||
import UserpilotRouteTracker from 'components/UserpilotRouteTracker/UserpilotRouteTracker';
|
||||
import { FeatureKeys } from 'constants/features';
|
||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||
import ROUTES from 'constants/routes';
|
||||
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 { useThemeConfig } from 'hooks/useDarkMode';
|
||||
import { useGetTenantLicense } from 'hooks/useGetTenantLicense';
|
||||
@@ -34,6 +33,7 @@ import { Suspense, useCallback, useEffect, useState } from 'react';
|
||||
import { Route, Router, Switch } from 'react-router-dom';
|
||||
import { CompatRouter } from 'react-router-dom-v5-compat';
|
||||
import { LicenseStatus } from 'types/api/licensesV3/getActive';
|
||||
import { Userpilot } from 'userpilot';
|
||||
import { extractDomain } from 'utils/app';
|
||||
|
||||
import { Home } from './pageComponents';
|
||||
@@ -84,9 +84,9 @@ function App(): JSX.Element {
|
||||
email,
|
||||
name: displayName,
|
||||
company_name: orgName,
|
||||
deployment_name: hostNameParts[0],
|
||||
tenant_id: hostNameParts[0],
|
||||
data_region: hostNameParts[1],
|
||||
deployment_url: hostname,
|
||||
tenant_url: hostname,
|
||||
company_domain: domain,
|
||||
source: 'signoz-ui',
|
||||
role,
|
||||
@@ -94,9 +94,9 @@ function App(): JSX.Element {
|
||||
|
||||
const groupTraits = {
|
||||
name: orgName,
|
||||
deployment_name: hostNameParts[0],
|
||||
tenant_id: hostNameParts[0],
|
||||
data_region: hostNameParts[1],
|
||||
deployment_url: hostname,
|
||||
tenant_url: hostname,
|
||||
company_domain: domain,
|
||||
source: 'signoz-ui',
|
||||
};
|
||||
@@ -111,23 +111,37 @@ function App(): JSX.Element {
|
||||
if (window && window.Appcues) {
|
||||
window.Appcues.identify(id, {
|
||||
name: displayName,
|
||||
deployment_name: hostNameParts[0],
|
||||
|
||||
tenant_id: hostNameParts[0],
|
||||
data_region: hostNameParts[1],
|
||||
deployment_url: hostname,
|
||||
tenant_url: hostname,
|
||||
company_domain: domain,
|
||||
|
||||
companyName: orgName,
|
||||
email,
|
||||
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, {
|
||||
email,
|
||||
name: displayName,
|
||||
orgName,
|
||||
deployment_name: hostNameParts[0],
|
||||
tenant_id: hostNameParts[0],
|
||||
data_region: hostNameParts[1],
|
||||
deployment_url: hostname,
|
||||
tenant_url: hostname,
|
||||
company_domain: domain,
|
||||
source: 'signoz-ui',
|
||||
isPaidUser: !!trialInfo?.trialConvertedToSubscription,
|
||||
@@ -135,9 +149,9 @@ function App(): JSX.Element {
|
||||
|
||||
posthog?.group('company', orgId, {
|
||||
name: orgName,
|
||||
deployment_name: hostNameParts[0],
|
||||
tenant_id: hostNameParts[0],
|
||||
data_region: hostNameParts[1],
|
||||
deployment_url: hostname,
|
||||
tenant_url: hostname,
|
||||
company_domain: domain,
|
||||
source: 'signoz-ui',
|
||||
isPaidUser: !!trialInfo?.trialConvertedToSubscription,
|
||||
@@ -256,20 +270,11 @@ function App(): JSX.Element {
|
||||
!showAddCreditCardModal &&
|
||||
(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 = {
|
||||
chat_settings: {
|
||||
app_id: process.env.PYLON_APP_ID,
|
||||
email: user.email,
|
||||
name: user.displayName || user.email,
|
||||
email_hash: emailHash,
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -303,6 +308,10 @@ function App(): JSX.Element {
|
||||
});
|
||||
}
|
||||
|
||||
if (process.env.USERPILOT_KEY) {
|
||||
Userpilot.initialize(process.env.USERPILOT_KEY);
|
||||
}
|
||||
|
||||
if (!isSentryInitialized) {
|
||||
Sentry.init({
|
||||
dsn: process.env.SENTRY_DSN,
|
||||
@@ -363,6 +372,7 @@ function App(): JSX.Element {
|
||||
<Router history={history}>
|
||||
<CompatRouter>
|
||||
<KBarCommandPaletteProvider>
|
||||
<UserpilotRouteTracker />
|
||||
<KBarCommandPalette />
|
||||
<NotificationProvider>
|
||||
<ErrorModalProvider>
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import { LogEventAxiosInstance as axios } from 'api';
|
||||
import getLocalStorageApi from 'api/browser/localstorage/get';
|
||||
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
||||
import { AxiosError } from 'axios';
|
||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import { EventSuccessPayloadProps } from 'types/api/events/types';
|
||||
|
||||
@@ -13,14 +11,9 @@ const logEvent = async (
|
||||
rateLimited?: boolean,
|
||||
): Promise<SuccessResponse<EventSuccessPayloadProps> | ErrorResponse> => {
|
||||
try {
|
||||
// add deployment_url and user_email to attributes
|
||||
// add tenant_url to attributes
|
||||
const { hostname } = window.location;
|
||||
const userEmail = getLocalStorageApi(LOCALSTORAGE.LOGGED_IN_USER_EMAIL);
|
||||
const updatedAttributes = {
|
||||
...attributes,
|
||||
deployment_url: hostname,
|
||||
user_email: userEmail,
|
||||
};
|
||||
const updatedAttributes = { ...attributes, tenant_url: hostname };
|
||||
const response = await axios.post('/event', {
|
||||
eventName,
|
||||
attributes: updatedAttributes,
|
||||
|
||||
23
frontend/src/auto-import-registry.d.ts
vendored
23
frontend/src/auto-import-registry.d.ts
vendored
@@ -1,23 +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/design-tokens';
|
||||
import '@signozhq/input';
|
||||
import '@signozhq/popover';
|
||||
import '@signozhq/resizable';
|
||||
import '@signozhq/sonner';
|
||||
import '@signozhq/table';
|
||||
import '@signozhq/tooltip';
|
||||
@@ -1,6 +1,5 @@
|
||||
/* eslint-disable sonarjs/no-duplicate-string */
|
||||
import { PrecisionOptionsEnum } from '../types';
|
||||
import { getYAxisFormattedValue } from '../yAxisConfig';
|
||||
import { getYAxisFormattedValue, PrecisionOptionsEnum } from '../yAxisConfig';
|
||||
|
||||
const testFullPrecisionGetYAxisFormattedValue = (
|
||||
value: string,
|
||||
@@ -233,7 +232,7 @@ describe('getYAxisFormattedValue - units (full precision legacy assertions)', ()
|
||||
).toBe('1%');
|
||||
expect(
|
||||
testFullPrecisionGetYAxisFormattedValue('1.00555555559595876', 'percent'),
|
||||
).toBe('1.005555555595959%');
|
||||
).toBe('1.005555555595958%');
|
||||
});
|
||||
|
||||
test('ratio', () => {
|
||||
@@ -360,7 +359,7 @@ describe('getYAxisFormattedValue - precision option tests', () => {
|
||||
's',
|
||||
PrecisionOptionsEnum.FULL,
|
||||
),
|
||||
).toBe('26.254299141484417 µs');
|
||||
).toBe('26254299141484417000000 µs');
|
||||
|
||||
expect(
|
||||
getYAxisFormattedValue('4353.81', 'ms', PrecisionOptionsEnum.FULL),
|
||||
|
||||
@@ -78,18 +78,3 @@ export interface ITimeRange {
|
||||
minTime: 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';
|
||||
import {
|
||||
CustomChartOptions,
|
||||
DEFAULT_SIGNIFICANT_DIGITS,
|
||||
GraphOnClickHandler,
|
||||
IAxisTimeConfig,
|
||||
MAX_DECIMALS,
|
||||
PrecisionOption,
|
||||
PrecisionOptionsEnum,
|
||||
StaticLineProps,
|
||||
} from './types';
|
||||
import { getToolTipValue, getYAxisFormattedValue } from './yAxisConfig';
|
||||
@@ -153,7 +149,6 @@ export const getGraphOptions = (
|
||||
scales: {
|
||||
x: {
|
||||
stacked: isStacked,
|
||||
offset: false,
|
||||
grid: {
|
||||
display: true,
|
||||
color: getGridColor(),
|
||||
@@ -246,68 +241,3 @@ declare module 'chart.js' {
|
||||
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 */
|
||||
import { formattedValueToString, getValueFormat } from '@grafana/data';
|
||||
import * as Sentry from '@sentry/react';
|
||||
import { UniversalYAxisUnit } from 'components/YAxisUnitSelector/types';
|
||||
import { isUniversalUnit } from 'components/YAxisUnitSelector/utils';
|
||||
import { isNaN } from 'lodash-es';
|
||||
|
||||
import { formatUniversalUnit } from '../YAxisUnitSelector/formatter';
|
||||
import {
|
||||
DEFAULT_SIGNIFICANT_DIGITS,
|
||||
PrecisionOption,
|
||||
PrecisionOptionsEnum,
|
||||
} from './types';
|
||||
import { formatDecimalWithLeadingZeros } from './utils';
|
||||
const DEFAULT_SIGNIFICANT_DIGITS = 15;
|
||||
// max decimals to keep should not exceed 15 decimal places to avoid floating point precision issues
|
||||
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;
|
||||
|
||||
/**
|
||||
* 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.
|
||||
@@ -32,10 +101,19 @@ export const getYAxisFormattedValue = (
|
||||
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.
|
||||
const computeDecimals = (): number | undefined => {
|
||||
if (precision === PrecisionOptionsEnum.FULL) {
|
||||
return DEFAULT_SIGNIFICANT_DIGITS;
|
||||
return decimalPlaces && decimalPlaces >= DEFAULT_SIGNIFICANT_DIGITS
|
||||
? decimalPlaces
|
||||
: DEFAULT_SIGNIFICANT_DIGITS;
|
||||
}
|
||||
return precision;
|
||||
};
|
||||
@@ -52,22 +130,6 @@ export const getYAxisFormattedValue = (
|
||||
};
|
||||
|
||||
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 formattedValue = formatter(numValue, computeDecimals(), undefined);
|
||||
if (formattedValue.text && formattedValue.text.includes('.')) {
|
||||
@@ -76,7 +138,6 @@ export const getYAxisFormattedValue = (
|
||||
precision,
|
||||
);
|
||||
}
|
||||
|
||||
return formattedValueToString(formattedValue);
|
||||
} catch (error) {
|
||||
Sentry.captureEvent({
|
||||
|
||||
@@ -471,13 +471,11 @@ function LogsFormatOptionsMenu({
|
||||
rootClassName="format-options-popover"
|
||||
destroyTooltipOnHide
|
||||
>
|
||||
<Tooltip title="Options">
|
||||
<Button
|
||||
className="periscope-btn ghost"
|
||||
icon={<Sliders size={14} />}
|
||||
data-testid="periscope-btn-format-options"
|
||||
/>
|
||||
</Tooltip>
|
||||
<Button
|
||||
className="periscope-btn ghost"
|
||||
icon={<Sliders size={14} />}
|
||||
data-testid="periscope-btn-format-options"
|
||||
/>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -300,10 +300,6 @@
|
||||
}
|
||||
}
|
||||
.qb-trace-operator-button-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
&-text {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -2,74 +2,8 @@ import './QueryFooter.styles.scss';
|
||||
|
||||
/* eslint-disable react/require-default-props */
|
||||
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 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({
|
||||
addNewBuilderQuery,
|
||||
@@ -126,7 +60,35 @@ export default function QueryFooter({
|
||||
</div>
|
||||
)}
|
||||
{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 "
|
||||
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>
|
||||
|
||||
@@ -0,0 +1,543 @@
|
||||
/* eslint-disable sonarjs/no-duplicate-string */
|
||||
import { negateOperator, OPERATORS } from 'constants/antlrQueryConstants';
|
||||
import { extractQueryPairs } from 'utils/queryContextUtils';
|
||||
|
||||
import {
|
||||
clearQueryPairsCache,
|
||||
convertFiltersToExpressionWithExistingQuery,
|
||||
} from '../queryProcessor';
|
||||
|
||||
describe('convertFiltersToExpressionWithExistingQuery', () => {
|
||||
beforeEach(() => {
|
||||
clearQueryPairsCache();
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should handle empty, null, and undefined inputs', () => {
|
||||
// Test null and undefined existing query
|
||||
expect(
|
||||
convertFiltersToExpressionWithExistingQuery(null as any, undefined),
|
||||
).toEqual({
|
||||
filters: null,
|
||||
filter: { expression: '' },
|
||||
});
|
||||
expect(
|
||||
convertFiltersToExpressionWithExistingQuery(undefined as any, undefined),
|
||||
).toEqual({
|
||||
filters: undefined,
|
||||
filter: { expression: '' },
|
||||
});
|
||||
|
||||
// Test empty filters
|
||||
expect(
|
||||
convertFiltersToExpressionWithExistingQuery(
|
||||
{ items: [], op: 'AND' },
|
||||
undefined,
|
||||
),
|
||||
).toEqual({
|
||||
filters: { items: [], op: 'AND' },
|
||||
filter: { expression: '' },
|
||||
});
|
||||
expect(
|
||||
convertFiltersToExpressionWithExistingQuery(
|
||||
{ items: undefined, op: 'AND' } as any,
|
||||
undefined,
|
||||
),
|
||||
).toEqual({
|
||||
filters: { items: undefined, op: 'AND' },
|
||||
filter: { expression: '' },
|
||||
});
|
||||
});
|
||||
|
||||
it('should return filters with new expression when no existing query', () => {
|
||||
const filters = {
|
||||
items: [
|
||||
{
|
||||
id: '1',
|
||||
key: { id: 'service.name', key: 'service.name', type: 'string' },
|
||||
op: OPERATORS['='],
|
||||
value: 'test-service',
|
||||
},
|
||||
],
|
||||
op: 'AND',
|
||||
};
|
||||
|
||||
const result = convertFiltersToExpressionWithExistingQuery(
|
||||
filters,
|
||||
undefined,
|
||||
);
|
||||
|
||||
expect(result.filters).toEqual(filters);
|
||||
expect(result.filter.expression).toBe("service.name = 'test-service'");
|
||||
});
|
||||
|
||||
it('should handle empty filters', () => {
|
||||
const filters = {
|
||||
items: [],
|
||||
op: 'AND',
|
||||
};
|
||||
|
||||
const result = convertFiltersToExpressionWithExistingQuery(
|
||||
filters,
|
||||
undefined,
|
||||
);
|
||||
|
||||
expect(result.filters).toEqual(filters);
|
||||
expect(result.filter.expression).toBe('');
|
||||
});
|
||||
|
||||
it('should create filters from existing query when filters array is empty', () => {
|
||||
const filters = {
|
||||
items: [],
|
||||
op: 'AND',
|
||||
};
|
||||
|
||||
const existingQuery = "service.name = 'testing'";
|
||||
|
||||
const result = convertFiltersToExpressionWithExistingQuery(
|
||||
filters,
|
||||
existingQuery,
|
||||
);
|
||||
|
||||
expect(result.filters.items).toHaveLength(1);
|
||||
expect(result.filters.items[0]).toEqual({
|
||||
id: expect.any(String),
|
||||
key: {
|
||||
id: 'service.name',
|
||||
key: 'service.name',
|
||||
type: '',
|
||||
},
|
||||
op: '=',
|
||||
value: 'testing',
|
||||
});
|
||||
expect(result.filter.expression).toBe(existingQuery);
|
||||
});
|
||||
|
||||
it('should create multiple filters from complex existing query', () => {
|
||||
const filters = {
|
||||
items: [],
|
||||
op: 'AND',
|
||||
};
|
||||
|
||||
const existingQuery =
|
||||
"service.name = 'testing' AND status = 'success' AND count > 100";
|
||||
|
||||
const result = convertFiltersToExpressionWithExistingQuery(
|
||||
filters,
|
||||
existingQuery,
|
||||
);
|
||||
|
||||
expect(result.filters.items).toHaveLength(3);
|
||||
expect(result.filters.items[0]).toEqual({
|
||||
id: expect.any(String),
|
||||
key: {
|
||||
id: 'service.name',
|
||||
key: 'service.name',
|
||||
type: '',
|
||||
},
|
||||
op: '=',
|
||||
value: 'testing',
|
||||
});
|
||||
expect(result.filters.items[1]).toEqual({
|
||||
id: expect.any(String),
|
||||
key: {
|
||||
id: 'status',
|
||||
key: 'status',
|
||||
type: '',
|
||||
},
|
||||
op: '=',
|
||||
value: 'success',
|
||||
});
|
||||
expect(result.filters.items[2]).toEqual({
|
||||
id: expect.any(String),
|
||||
key: {
|
||||
id: 'count',
|
||||
key: 'count',
|
||||
type: '',
|
||||
},
|
||||
op: '>',
|
||||
value: '100',
|
||||
});
|
||||
expect(result.filter.expression).toBe(existingQuery);
|
||||
});
|
||||
|
||||
it('should handle existing query with matching filters', () => {
|
||||
const filters = {
|
||||
items: [
|
||||
{
|
||||
id: '1',
|
||||
key: { id: 'service.name', key: 'service.name', type: 'string' },
|
||||
op: OPERATORS['='],
|
||||
value: 'updated-service',
|
||||
},
|
||||
],
|
||||
op: 'AND',
|
||||
};
|
||||
|
||||
const existingQuery = "service.name = 'old-service'";
|
||||
|
||||
const result = convertFiltersToExpressionWithExistingQuery(
|
||||
filters,
|
||||
existingQuery,
|
||||
);
|
||||
|
||||
expect(result.filters).toBeDefined();
|
||||
expect(result.filter).toBeDefined();
|
||||
expect(result.filter.expression).toBe("service.name = 'updated-service'");
|
||||
// Ensure parser can parse the existing query
|
||||
expect(extractQueryPairs(existingQuery)).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
key: 'service.name',
|
||||
operator: '=',
|
||||
value: "'old-service'",
|
||||
}),
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle IN operator with existing query', () => {
|
||||
const filters = {
|
||||
items: [
|
||||
{
|
||||
id: '1',
|
||||
key: { id: 'service.name', key: 'service.name', type: 'string' },
|
||||
op: OPERATORS.IN,
|
||||
value: ['service1', 'service2'],
|
||||
},
|
||||
],
|
||||
op: 'AND',
|
||||
};
|
||||
|
||||
const existingQuery = "service.name IN ['old-service']";
|
||||
|
||||
const result = convertFiltersToExpressionWithExistingQuery(
|
||||
filters,
|
||||
existingQuery,
|
||||
);
|
||||
|
||||
expect(result.filters).toBeDefined();
|
||||
expect(result.filter).toBeDefined();
|
||||
expect(result.filter.expression).toBe(
|
||||
"service.name IN ['service1', 'service2']",
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle IN operator conversion from equals', () => {
|
||||
const filters = {
|
||||
items: [
|
||||
{
|
||||
id: '1',
|
||||
key: { id: 'service.name', key: 'service.name', type: 'string' },
|
||||
op: OPERATORS.IN,
|
||||
value: ['service1', 'service2'],
|
||||
},
|
||||
],
|
||||
op: 'AND',
|
||||
};
|
||||
|
||||
const existingQuery = "service.name = 'old-service'";
|
||||
|
||||
const result = convertFiltersToExpressionWithExistingQuery(
|
||||
filters,
|
||||
existingQuery,
|
||||
);
|
||||
|
||||
expect(result.filters.items).toHaveLength(1);
|
||||
expect(result.filter.expression).toBe(
|
||||
"service.name IN ['service1', 'service2'] ",
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle NOT IN operator conversion from not equals', () => {
|
||||
const filters = {
|
||||
items: [
|
||||
{
|
||||
id: '1',
|
||||
key: { id: 'service.name', key: 'service.name', type: 'string' },
|
||||
op: negateOperator(OPERATORS.IN),
|
||||
value: ['service1', 'service2'],
|
||||
},
|
||||
],
|
||||
op: 'AND',
|
||||
};
|
||||
|
||||
const existingQuery = "service.name != 'old-service'";
|
||||
|
||||
const result = convertFiltersToExpressionWithExistingQuery(
|
||||
filters,
|
||||
existingQuery,
|
||||
);
|
||||
|
||||
expect(result.filters.items).toHaveLength(1);
|
||||
expect(result.filter.expression).toBe(
|
||||
"service.name NOT IN ['service1', 'service2'] ",
|
||||
);
|
||||
});
|
||||
|
||||
it('should add new filters when they do not exist in existing query', () => {
|
||||
const filters = {
|
||||
items: [
|
||||
{
|
||||
id: '1',
|
||||
key: { id: 'new.key', key: 'new.key', type: 'string' },
|
||||
op: OPERATORS['='],
|
||||
value: 'new-value',
|
||||
},
|
||||
],
|
||||
op: 'AND',
|
||||
};
|
||||
|
||||
const existingQuery = "service.name = 'old-service'";
|
||||
|
||||
const result = convertFiltersToExpressionWithExistingQuery(
|
||||
filters,
|
||||
existingQuery,
|
||||
);
|
||||
|
||||
expect(result.filters.items).toHaveLength(2); // Original + new filter
|
||||
expect(result.filter.expression).toBe(
|
||||
"service.name = 'old-service' new.key = 'new-value'",
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle simple value replacement', () => {
|
||||
const filters = {
|
||||
items: [
|
||||
{
|
||||
id: '1',
|
||||
key: { id: 'status', key: 'status', type: 'string' },
|
||||
op: OPERATORS['='],
|
||||
value: 'error',
|
||||
},
|
||||
],
|
||||
op: 'AND',
|
||||
};
|
||||
|
||||
const existingQuery = "status = 'success'";
|
||||
|
||||
const result = convertFiltersToExpressionWithExistingQuery(
|
||||
filters,
|
||||
existingQuery,
|
||||
);
|
||||
|
||||
expect(result.filters.items).toHaveLength(1);
|
||||
expect(result.filter.expression).toBe("status = 'error'");
|
||||
});
|
||||
|
||||
it('should handle filters with no key gracefully', () => {
|
||||
const filters = {
|
||||
items: [
|
||||
{
|
||||
id: '1',
|
||||
key: undefined,
|
||||
op: OPERATORS['='],
|
||||
value: 'test-value',
|
||||
},
|
||||
],
|
||||
op: 'AND',
|
||||
};
|
||||
|
||||
const existingQuery = "service.name = 'old-service'";
|
||||
|
||||
const result = convertFiltersToExpressionWithExistingQuery(
|
||||
filters,
|
||||
existingQuery,
|
||||
);
|
||||
|
||||
expect(result.filters.items).toHaveLength(2);
|
||||
expect(result.filter.expression).toBe("service.name = 'old-service'");
|
||||
});
|
||||
|
||||
it('should normalize deprecated operators', () => {
|
||||
const filters = {
|
||||
items: [
|
||||
{
|
||||
id: '1',
|
||||
key: { id: 'service', key: 'service', type: 'string' },
|
||||
op: 'regex', // deprecated operator
|
||||
value: 'api',
|
||||
},
|
||||
],
|
||||
op: 'AND',
|
||||
};
|
||||
|
||||
const result = convertFiltersToExpressionWithExistingQuery(
|
||||
filters,
|
||||
undefined,
|
||||
);
|
||||
|
||||
expect(result.filters.items[0].op).toBe('regexp');
|
||||
});
|
||||
|
||||
it('should handle complex mixed scenarios', () => {
|
||||
const filters = {
|
||||
items: [
|
||||
{
|
||||
id: '1',
|
||||
key: { id: 'service', key: 'service', type: 'string' },
|
||||
op: OPERATORS.IN,
|
||||
value: ['api-gateway', 'user-service'],
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
key: { id: 'status', key: 'status', type: 'string' },
|
||||
op: OPERATORS['='],
|
||||
value: 'success',
|
||||
},
|
||||
],
|
||||
op: 'AND',
|
||||
};
|
||||
|
||||
const existingQuery = "service = 'old-service' AND count > 100";
|
||||
|
||||
const result = convertFiltersToExpressionWithExistingQuery(
|
||||
filters,
|
||||
existingQuery,
|
||||
);
|
||||
|
||||
expect(result.filters.items).toHaveLength(3); // 2 new + 1 existing
|
||||
expect(result.filter.expression).toContain(
|
||||
"service IN ['api-gateway', 'user-service']",
|
||||
);
|
||||
expect(result.filter.expression).toContain("status = 'success'");
|
||||
expect(result.filter.expression).toContain('count > 100');
|
||||
});
|
||||
|
||||
it('should handle empty query string', () => {
|
||||
const filters = {
|
||||
items: [
|
||||
{
|
||||
id: '1',
|
||||
key: { id: 'service', key: 'service', type: 'string' },
|
||||
op: '=',
|
||||
value: 'api',
|
||||
},
|
||||
],
|
||||
op: 'AND',
|
||||
};
|
||||
|
||||
const result = convertFiltersToExpressionWithExistingQuery(filters, '');
|
||||
|
||||
expect(result.filter.expression).toBe("service = 'api'");
|
||||
});
|
||||
|
||||
it('should handle invalid query gracefully', () => {
|
||||
const filters = {
|
||||
items: [
|
||||
{
|
||||
id: '1',
|
||||
key: { id: 'service', key: 'service', type: 'string' },
|
||||
op: '=',
|
||||
value: 'api',
|
||||
},
|
||||
],
|
||||
op: 'AND',
|
||||
};
|
||||
|
||||
const result = convertFiltersToExpressionWithExistingQuery(
|
||||
filters,
|
||||
'invalid query',
|
||||
);
|
||||
|
||||
expect(result.filters).toBeDefined();
|
||||
expect(result.filter.expression).toBe("invalid query service = 'api'");
|
||||
});
|
||||
|
||||
it('should preserve existing filters when no matching query pairs', () => {
|
||||
const filters = {
|
||||
items: [
|
||||
{
|
||||
id: '1',
|
||||
key: { id: 'service', key: 'service', type: 'string' },
|
||||
op: '=',
|
||||
value: 'api',
|
||||
},
|
||||
],
|
||||
op: 'AND',
|
||||
};
|
||||
|
||||
const existingQuery = "different.field = 'value'";
|
||||
|
||||
const result = convertFiltersToExpressionWithExistingQuery(
|
||||
filters,
|
||||
existingQuery,
|
||||
);
|
||||
|
||||
expect(result.filters.items).toHaveLength(2); // Original + existing
|
||||
expect(result.filter.expression).toContain("service = 'api'");
|
||||
expect(result.filter.expression).toContain("different.field = 'value'");
|
||||
});
|
||||
|
||||
it('should handle array values in IN operators', () => {
|
||||
const filters = {
|
||||
items: [
|
||||
{
|
||||
id: '1',
|
||||
key: { id: 'services', key: 'services', type: 'string' },
|
||||
op: OPERATORS.IN,
|
||||
value: ['api', 'user', 'auth'],
|
||||
},
|
||||
],
|
||||
op: 'AND',
|
||||
};
|
||||
|
||||
const existingQuery = "services IN ['old-service']";
|
||||
|
||||
const result = convertFiltersToExpressionWithExistingQuery(
|
||||
filters,
|
||||
existingQuery,
|
||||
);
|
||||
|
||||
expect(result.filters.items).toHaveLength(1);
|
||||
expect(result.filter.expression).toBe("services IN ['api', 'user', 'auth']");
|
||||
});
|
||||
|
||||
it('should handle function operators', () => {
|
||||
const filters = {
|
||||
items: [
|
||||
{
|
||||
id: '1',
|
||||
key: { id: 'tags', key: 'tags', type: 'string' },
|
||||
op: 'has',
|
||||
value: 'production',
|
||||
},
|
||||
],
|
||||
op: 'AND',
|
||||
};
|
||||
|
||||
const existingQuery = '';
|
||||
|
||||
const result = convertFiltersToExpressionWithExistingQuery(
|
||||
filters,
|
||||
existingQuery,
|
||||
);
|
||||
|
||||
expect(result.filters.items).toHaveLength(1);
|
||||
expect(result.filter.expression).toBe("has(tags, 'production')");
|
||||
});
|
||||
|
||||
it('should handle non-value operators like EXISTS', () => {
|
||||
const filters = {
|
||||
items: [
|
||||
{
|
||||
id: '1',
|
||||
key: { id: 'user_id', key: 'user_id', type: 'string' },
|
||||
op: 'EXISTS',
|
||||
value: '',
|
||||
},
|
||||
],
|
||||
op: 'AND',
|
||||
};
|
||||
|
||||
const existingQuery = 'user_id EXISTS';
|
||||
|
||||
const result = convertFiltersToExpressionWithExistingQuery(
|
||||
filters,
|
||||
existingQuery,
|
||||
);
|
||||
|
||||
expect(result.filters.items).toHaveLength(1);
|
||||
expect(result.filter.expression).toBe('user_id EXISTS');
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,5 @@
|
||||
/* eslint-disable sonarjs/no-duplicate-string */
|
||||
/* eslint-disable import/no-unresolved */
|
||||
import { negateOperator, OPERATORS } from 'constants/antlrQueryConstants';
|
||||
import {
|
||||
BaseAutocompleteData,
|
||||
DataTypes,
|
||||
@@ -12,8 +11,6 @@ import { extractQueryPairs } from 'utils/queryContextUtils';
|
||||
import {
|
||||
convertAggregationToExpression,
|
||||
convertFiltersToExpression,
|
||||
convertFiltersToExpressionWithExistingQuery,
|
||||
formatValueForExpression,
|
||||
removeKeysFromExpression,
|
||||
} from '../utils';
|
||||
|
||||
@@ -551,231 +548,6 @@ describe('convertFiltersToExpression', () => {
|
||||
"user_id NOT EXISTS AND description NOT CONTAINS 'error' AND NOT has(tags, 'production') AND NOT hasAny(labels, ['env:prod', 'service:api'])",
|
||||
});
|
||||
});
|
||||
|
||||
it('should return filters with new expression when no existing query', () => {
|
||||
const filters = {
|
||||
items: [
|
||||
{
|
||||
id: '1',
|
||||
key: { id: 'service.name', key: 'service.name', type: 'string' },
|
||||
op: OPERATORS['='],
|
||||
value: 'test-service',
|
||||
},
|
||||
],
|
||||
op: 'AND',
|
||||
};
|
||||
|
||||
const result = convertFiltersToExpressionWithExistingQuery(
|
||||
filters,
|
||||
undefined,
|
||||
);
|
||||
|
||||
expect(result.filters).toEqual(filters);
|
||||
expect(result.filter.expression).toBe("service.name = 'test-service'");
|
||||
});
|
||||
|
||||
it('should handle empty filters', () => {
|
||||
const filters = {
|
||||
items: [],
|
||||
op: 'AND',
|
||||
};
|
||||
|
||||
const result = convertFiltersToExpressionWithExistingQuery(
|
||||
filters,
|
||||
undefined,
|
||||
);
|
||||
|
||||
expect(result.filters).toEqual(filters);
|
||||
expect(result.filter.expression).toBe('');
|
||||
});
|
||||
|
||||
it('should handle existing query with matching filters', () => {
|
||||
const filters = {
|
||||
items: [
|
||||
{
|
||||
id: '1',
|
||||
key: { id: 'service.name', key: 'service.name', type: 'string' },
|
||||
op: OPERATORS['='],
|
||||
value: 'updated-service',
|
||||
},
|
||||
],
|
||||
op: 'AND',
|
||||
};
|
||||
|
||||
const existingQuery = "service.name = 'old-service'";
|
||||
|
||||
const result = convertFiltersToExpressionWithExistingQuery(
|
||||
filters,
|
||||
existingQuery,
|
||||
);
|
||||
|
||||
expect(result.filters).toBeDefined();
|
||||
expect(result.filter).toBeDefined();
|
||||
expect(result.filter.expression).toBe("service.name = 'updated-service'");
|
||||
// Ensure parser can parse the existing query
|
||||
expect(extractQueryPairs(existingQuery)).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
key: 'service.name',
|
||||
operator: '=',
|
||||
value: "'old-service'",
|
||||
}),
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle IN operator with existing query', () => {
|
||||
const filters = {
|
||||
items: [
|
||||
{
|
||||
id: '1',
|
||||
key: { id: 'service.name', key: 'service.name', type: 'string' },
|
||||
op: OPERATORS.IN,
|
||||
value: ['service1', 'service2'],
|
||||
},
|
||||
],
|
||||
op: 'AND',
|
||||
};
|
||||
|
||||
const existingQuery = "service.name IN ['old-service']";
|
||||
|
||||
const result = convertFiltersToExpressionWithExistingQuery(
|
||||
filters,
|
||||
existingQuery,
|
||||
);
|
||||
|
||||
expect(result.filters).toBeDefined();
|
||||
expect(result.filter).toBeDefined();
|
||||
expect(result.filter.expression).toBe(
|
||||
"service.name IN ['service1', 'service2']",
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle IN operator conversion from equals', () => {
|
||||
const filters = {
|
||||
items: [
|
||||
{
|
||||
id: '1',
|
||||
key: { id: 'service.name', key: 'service.name', type: 'string' },
|
||||
op: OPERATORS.IN,
|
||||
value: ['service1', 'service2'],
|
||||
},
|
||||
],
|
||||
op: 'AND',
|
||||
};
|
||||
|
||||
const existingQuery = "service.name = 'old-service'";
|
||||
|
||||
const result = convertFiltersToExpressionWithExistingQuery(
|
||||
filters,
|
||||
existingQuery,
|
||||
);
|
||||
|
||||
expect(result.filters.items).toHaveLength(1);
|
||||
expect(result.filter.expression).toBe(
|
||||
"service.name IN ['service1', 'service2'] ",
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle NOT IN operator conversion from not equals', () => {
|
||||
const filters = {
|
||||
items: [
|
||||
{
|
||||
id: '1',
|
||||
key: { id: 'service.name', key: 'service.name', type: 'string' },
|
||||
op: negateOperator(OPERATORS.IN),
|
||||
value: ['service1', 'service2'],
|
||||
},
|
||||
],
|
||||
op: 'AND',
|
||||
};
|
||||
|
||||
const existingQuery = "service.name != 'old-service'";
|
||||
|
||||
const result = convertFiltersToExpressionWithExistingQuery(
|
||||
filters,
|
||||
existingQuery,
|
||||
);
|
||||
|
||||
expect(result.filters.items).toHaveLength(1);
|
||||
expect(result.filter.expression).toBe(
|
||||
"service.name NOT IN ['service1', 'service2'] ",
|
||||
);
|
||||
});
|
||||
|
||||
it('should add new filters when they do not exist in existing query', () => {
|
||||
const filters = {
|
||||
items: [
|
||||
{
|
||||
id: '1',
|
||||
key: { id: 'new.key', key: 'new.key', type: 'string' },
|
||||
op: OPERATORS['='],
|
||||
value: 'new-value',
|
||||
},
|
||||
],
|
||||
op: 'AND',
|
||||
};
|
||||
|
||||
const existingQuery = "service.name = 'old-service'";
|
||||
|
||||
const result = convertFiltersToExpressionWithExistingQuery(
|
||||
filters,
|
||||
existingQuery,
|
||||
);
|
||||
|
||||
expect(result.filters.items).toHaveLength(2); // Original + new filter
|
||||
expect(result.filter.expression).toBe(
|
||||
"service.name = 'old-service' new.key = 'new-value'",
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle simple value replacement', () => {
|
||||
const filters = {
|
||||
items: [
|
||||
{
|
||||
id: '1',
|
||||
key: { id: 'status', key: 'status', type: 'string' },
|
||||
op: OPERATORS['='],
|
||||
value: 'error',
|
||||
},
|
||||
],
|
||||
op: 'AND',
|
||||
};
|
||||
|
||||
const existingQuery = "status = 'success'";
|
||||
|
||||
const result = convertFiltersToExpressionWithExistingQuery(
|
||||
filters,
|
||||
existingQuery,
|
||||
);
|
||||
|
||||
expect(result.filters.items).toHaveLength(1);
|
||||
expect(result.filter.expression).toBe("status = 'error'");
|
||||
});
|
||||
|
||||
it('should handle filters with no key gracefully', () => {
|
||||
const filters = {
|
||||
items: [
|
||||
{
|
||||
id: '1',
|
||||
key: undefined,
|
||||
op: OPERATORS['='],
|
||||
value: 'test-value',
|
||||
},
|
||||
],
|
||||
op: 'AND',
|
||||
};
|
||||
|
||||
const existingQuery = "service.name = 'old-service'";
|
||||
|
||||
const result = convertFiltersToExpressionWithExistingQuery(
|
||||
filters,
|
||||
existingQuery,
|
||||
);
|
||||
|
||||
expect(result.filters.items).toHaveLength(2);
|
||||
expect(result.filter.expression).toBe("service.name = 'old-service'");
|
||||
});
|
||||
});
|
||||
|
||||
describe('convertAggregationToExpression', () => {
|
||||
@@ -1194,220 +966,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]');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
531
frontend/src/components/QueryBuilderV2/queryProcessor.ts
Normal file
531
frontend/src/components/QueryBuilderV2/queryProcessor.ts
Normal file
@@ -0,0 +1,531 @@
|
||||
/* eslint-disable sonarjs/cognitive-complexity */
|
||||
import {
|
||||
DEPRECATED_OPERATORS_MAP,
|
||||
OPERATORS,
|
||||
} from 'constants/antlrQueryConstants';
|
||||
import { getOperatorValue } from 'container/QueryBuilder/filters/QueryBuilderSearch/utils';
|
||||
import { cloneDeep } from 'lodash-es';
|
||||
import { IQueryPair } from 'types/antlrQueryTypes';
|
||||
import {
|
||||
TagFilter,
|
||||
TagFilterItem,
|
||||
} from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { extractQueryPairs } from 'utils/queryContextUtils';
|
||||
import { unquote } from 'utils/stringUtils';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
import { convertFiltersToExpression } from './utils';
|
||||
|
||||
interface ProcessingResult {
|
||||
type: 'update' | 'add' | 'skip' | 'transform';
|
||||
modifications?: QueryModification[];
|
||||
newFilters?: TagFilterItem[];
|
||||
shouldAddToNonExisting?: boolean;
|
||||
}
|
||||
|
||||
interface QueryModification {
|
||||
type: 'replace' | 'append' | 'prepend';
|
||||
startIndex?: number;
|
||||
endIndex?: number;
|
||||
newContent: string;
|
||||
}
|
||||
|
||||
interface QueryProcessingContext {
|
||||
readonly originalQuery: string;
|
||||
queryPairsMap: Map<string, IQueryPair>;
|
||||
readonly visitedPairs: Set<string>;
|
||||
modifications: QueryModification[];
|
||||
newFilters: TagFilterItem[];
|
||||
nonExistingFilters: TagFilterItem[];
|
||||
modifiedQuery: string;
|
||||
}
|
||||
|
||||
// Cache for query pairs to avoid repeated parsing
|
||||
const queryPairsCache = new Map<string, Map<string, IQueryPair>>();
|
||||
|
||||
// Validation functions
|
||||
const validateFilter = (filter: TagFilterItem): boolean =>
|
||||
!!(filter.key?.key && filter.op && filter.value !== undefined);
|
||||
|
||||
const validateQuery = (query: string): boolean =>
|
||||
typeof query === 'string' && query.trim().length > 0;
|
||||
|
||||
const areValuesEqual = (existing: unknown[], current: unknown[]): boolean => {
|
||||
if (existing.length !== current.length) return false;
|
||||
|
||||
const existingSet = new Set(existing.map((v) => String(v)));
|
||||
return current.every((v) => existingSet.has(String(v)));
|
||||
};
|
||||
|
||||
// Format a value for the expression string
|
||||
const formatValueForExpression = (
|
||||
value: string[] | string | number | boolean,
|
||||
operator?: string,
|
||||
): string => {
|
||||
// Check if it's a variable
|
||||
const isVariable = (val: string | string[] | number | boolean): boolean => {
|
||||
if (Array.isArray(val)) {
|
||||
return val.some((v) => typeof v === 'string' && v.trim().startsWith('$'));
|
||||
}
|
||||
return typeof val === 'string' && val.trim().startsWith('$');
|
||||
};
|
||||
|
||||
if (isVariable(value)) {
|
||||
return String(value);
|
||||
}
|
||||
|
||||
// Check if operator requires array values
|
||||
const isArrayOperator = (op: string): boolean => {
|
||||
const arrayOperators = ['in', 'not in', 'IN', 'NOT IN'];
|
||||
return arrayOperators.includes(op);
|
||||
};
|
||||
|
||||
// For IN operators, ensure value is always an array
|
||||
if (isArrayOperator(operator || '')) {
|
||||
const arrayValue = Array.isArray(value) ? value : [value];
|
||||
return `[${arrayValue
|
||||
.map((v) =>
|
||||
typeof v === 'string' ? `'${v.replace(/'/g, "\\'")}'` : String(v),
|
||||
)
|
||||
.join(', ')}]`;
|
||||
}
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
// Handle array values (e.g., for IN operations)
|
||||
return `[${value
|
||||
.map((v) =>
|
||||
typeof v === 'string' ? `'${v.replace(/'/g, "\\'")}'` : String(v),
|
||||
)
|
||||
.join(', ')}]`;
|
||||
}
|
||||
|
||||
if (typeof value === 'string') {
|
||||
// Add single quotes around all string values and escape internal single quotes
|
||||
return `'${value.replace(/'/g, "\\'")}'`;
|
||||
}
|
||||
|
||||
return String(value);
|
||||
};
|
||||
|
||||
// Clear cache when needed (e.g., for testing or memory management)
|
||||
export const clearQueryPairsCache = (): void => {
|
||||
queryPairsCache.clear();
|
||||
};
|
||||
|
||||
const getQueryPairsMap = (query: string): Map<string, IQueryPair> => {
|
||||
const trimmedQuery = query.trim();
|
||||
|
||||
if (!queryPairsCache.has(trimmedQuery)) {
|
||||
const queryPairs = extractQueryPairs(trimmedQuery) || [];
|
||||
const queryPairsMap: Map<string, IQueryPair> = new Map();
|
||||
|
||||
queryPairs.forEach((pair) => {
|
||||
const key = pair.hasNegation
|
||||
? `${pair.key}-not ${pair.operator}`.trim().toLowerCase()
|
||||
: `${pair.key}-${pair.operator}`.trim().toLowerCase();
|
||||
queryPairsMap.set(key, pair);
|
||||
});
|
||||
|
||||
queryPairsCache.set(trimmedQuery, queryPairsMap);
|
||||
}
|
||||
|
||||
return queryPairsCache.get(trimmedQuery) || new Map();
|
||||
};
|
||||
|
||||
// Helper function to normalize deprecated operators
|
||||
const normalizeDeprecatedOperators = (filters: TagFilter): TagFilter => {
|
||||
const updatedFilters = cloneDeep(filters);
|
||||
|
||||
if (updatedFilters?.items) {
|
||||
updatedFilters.items = updatedFilters.items.map((item) => {
|
||||
const opLower = item.op?.toLowerCase();
|
||||
if (Object.keys(DEPRECATED_OPERATORS_MAP).includes(opLower)) {
|
||||
return {
|
||||
...item,
|
||||
op: DEPRECATED_OPERATORS_MAP[
|
||||
opLower as keyof typeof DEPRECATED_OPERATORS_MAP
|
||||
].toLowerCase(),
|
||||
};
|
||||
}
|
||||
return item;
|
||||
});
|
||||
}
|
||||
|
||||
return updatedFilters;
|
||||
};
|
||||
|
||||
// ES5 compatible operator handlers using functions instead of classes
|
||||
|
||||
// Helper function to check if operator is IN or NOT IN
|
||||
function isInOperator(operator: string): boolean {
|
||||
const sanitizedOperator = operator.trim().toUpperCase();
|
||||
return (
|
||||
[OPERATORS.IN, `${OPERATORS.NOT} ${OPERATORS.IN}`].indexOf(
|
||||
sanitizedOperator,
|
||||
) !== -1
|
||||
);
|
||||
}
|
||||
|
||||
// Helper function to handle IN operator transformations
|
||||
// Generic helper function to handle operator transformations
|
||||
function handleOperatorTransformation(
|
||||
filter: TagFilterItem,
|
||||
context: QueryProcessingContext,
|
||||
formattedValue: string,
|
||||
targetOperator: string,
|
||||
transformationConfigs: Array<{
|
||||
operatorKey: string;
|
||||
positionProperty: 'operatorStart' | 'negationStart';
|
||||
}>,
|
||||
): ProcessingResult {
|
||||
const { key } = filter;
|
||||
const { op } = filter;
|
||||
|
||||
// Skip if key is not defined
|
||||
if (!key || !key.key) {
|
||||
return { type: 'add', shouldAddToNonExisting: true };
|
||||
}
|
||||
|
||||
// Check each transformation configuration
|
||||
const foundConfig = transformationConfigs.find((config) => {
|
||||
const transformationKey = `${key.key}-${config.operatorKey}`;
|
||||
const transformationKeyLower = transformationKey.trim().toLowerCase();
|
||||
return context.queryPairsMap.has(transformationKeyLower);
|
||||
});
|
||||
|
||||
if (foundConfig) {
|
||||
const transformationKey = `${key.key}-${foundConfig.operatorKey}`;
|
||||
const transformationKeyLower = transformationKey.trim().toLowerCase();
|
||||
const transformationPair = context.queryPairsMap.get(transformationKeyLower);
|
||||
context.visitedPairs.add(transformationKeyLower);
|
||||
|
||||
if (
|
||||
transformationPair &&
|
||||
transformationPair.position &&
|
||||
transformationPair.position.valueEnd
|
||||
) {
|
||||
const startPosition =
|
||||
transformationPair.position[foundConfig.positionProperty];
|
||||
context.modifiedQuery = `${
|
||||
context.modifiedQuery.slice(0, startPosition) + targetOperator
|
||||
} ${formattedValue} ${context.modifiedQuery.slice(
|
||||
transformationPair.position.valueEnd + 1,
|
||||
)}`;
|
||||
context.queryPairsMap = getQueryPairsMap(context.modifiedQuery.trim());
|
||||
}
|
||||
// Mark the current filter as visited to prevent it from being added as a new filter
|
||||
context.visitedPairs.add(`${key.key}-${op}`.trim().toLowerCase());
|
||||
return { type: 'transform', shouldAddToNonExisting: false };
|
||||
}
|
||||
|
||||
return { type: 'add', shouldAddToNonExisting: true };
|
||||
}
|
||||
|
||||
function handleInTransformations(
|
||||
filter: TagFilterItem,
|
||||
context: QueryProcessingContext,
|
||||
formattedValue: string,
|
||||
): ProcessingResult {
|
||||
const transformationConfigs = [
|
||||
{
|
||||
operatorKey: `${OPERATORS.NOT} ${filter.op}`,
|
||||
positionProperty: 'negationStart' as const,
|
||||
},
|
||||
{ operatorKey: OPERATORS['='], positionProperty: 'operatorStart' as const },
|
||||
{ operatorKey: OPERATORS['!='], positionProperty: 'operatorStart' as const },
|
||||
];
|
||||
|
||||
return handleOperatorTransformation(
|
||||
filter,
|
||||
context,
|
||||
formattedValue,
|
||||
OPERATORS.IN,
|
||||
transformationConfigs,
|
||||
);
|
||||
}
|
||||
|
||||
// Helper function to handle NOT IN operator transformations
|
||||
function handleNotInTransformations(
|
||||
filter: TagFilterItem,
|
||||
context: QueryProcessingContext,
|
||||
formattedValue: string,
|
||||
): ProcessingResult {
|
||||
const transformationConfigs = [
|
||||
{ operatorKey: OPERATORS['!='], positionProperty: 'operatorStart' as const },
|
||||
];
|
||||
|
||||
return handleOperatorTransformation(
|
||||
filter,
|
||||
context,
|
||||
formattedValue,
|
||||
`${OPERATORS.NOT} ${OPERATORS.IN}`,
|
||||
transformationConfigs,
|
||||
);
|
||||
}
|
||||
|
||||
// Helper function to handle operator transformations
|
||||
function handleOperatorTransformations(
|
||||
filter: TagFilterItem,
|
||||
context: QueryProcessingContext,
|
||||
formattedValue: string,
|
||||
): ProcessingResult {
|
||||
const { op } = filter;
|
||||
const sanitizedOperator = op.trim().toUpperCase();
|
||||
|
||||
if (sanitizedOperator === OPERATORS.IN) {
|
||||
return handleInTransformations(filter, context, formattedValue);
|
||||
}
|
||||
if (sanitizedOperator === `${OPERATORS.NOT} ${OPERATORS.IN}`) {
|
||||
return handleNotInTransformations(filter, context, formattedValue);
|
||||
}
|
||||
return { type: 'add', shouldAddToNonExisting: true };
|
||||
}
|
||||
|
||||
// ES5 compatible operator handler functions
|
||||
function processInOperator(
|
||||
filter: TagFilterItem,
|
||||
context: QueryProcessingContext,
|
||||
): ProcessingResult {
|
||||
const { key } = filter;
|
||||
const { op } = filter;
|
||||
const { value } = filter;
|
||||
|
||||
// Skip if key is not defined
|
||||
if (!key || !key.key) {
|
||||
return { type: 'skip' };
|
||||
}
|
||||
|
||||
const formattedValue = formatValueForExpression(value, op);
|
||||
const pairKey = `${key.key}-${op}`.trim().toLowerCase();
|
||||
|
||||
// Check if exact match exists
|
||||
const existingPair = context.queryPairsMap.get(pairKey);
|
||||
if (
|
||||
existingPair &&
|
||||
existingPair.position &&
|
||||
existingPair.position.valueStart &&
|
||||
existingPair.position.valueEnd
|
||||
) {
|
||||
context.visitedPairs.add(pairKey);
|
||||
|
||||
// Check if values are identical for array-based operators
|
||||
if (existingPair.valueList && filter.value && Array.isArray(filter.value)) {
|
||||
const cleanValues = (values: unknown[]): unknown[] =>
|
||||
values.map((val) => (typeof val === 'string' ? unquote(val) : val));
|
||||
|
||||
const cleanExistingValues = cleanValues(existingPair.valueList);
|
||||
const cleanFilterValues = cleanValues(filter.value);
|
||||
|
||||
if (areValuesEqual(cleanExistingValues, cleanFilterValues)) {
|
||||
// Values are identical, preserve existing formatting
|
||||
context.modifiedQuery =
|
||||
context.modifiedQuery.slice(0, existingPair.position.valueStart) +
|
||||
existingPair.value +
|
||||
context.modifiedQuery.slice(existingPair.position.valueEnd + 1);
|
||||
return { type: 'skip' };
|
||||
}
|
||||
}
|
||||
|
||||
// Update the value
|
||||
context.modifiedQuery =
|
||||
context.modifiedQuery.slice(0, existingPair.position.valueStart) +
|
||||
formattedValue +
|
||||
context.modifiedQuery.slice(existingPair.position.valueEnd + 1);
|
||||
|
||||
// Update the query pairs map
|
||||
context.queryPairsMap = getQueryPairsMap(context.modifiedQuery);
|
||||
return { type: 'update' };
|
||||
}
|
||||
|
||||
// Handle operator transformations
|
||||
return handleOperatorTransformations(filter, context, formattedValue);
|
||||
}
|
||||
|
||||
function processDefaultOperator(
|
||||
filter: TagFilterItem,
|
||||
context: QueryProcessingContext,
|
||||
): ProcessingResult {
|
||||
const { key } = filter;
|
||||
const { op } = filter;
|
||||
const { value } = filter;
|
||||
|
||||
// Skip if key is not defined
|
||||
if (!key || !key.key) {
|
||||
return { type: 'add', shouldAddToNonExisting: true };
|
||||
}
|
||||
|
||||
const pairKey = `${key.key}-${op}`.trim().toLowerCase();
|
||||
|
||||
if (context.queryPairsMap.has(pairKey)) {
|
||||
const existingPair = context.queryPairsMap.get(pairKey);
|
||||
context.visitedPairs.add(pairKey);
|
||||
|
||||
if (
|
||||
existingPair &&
|
||||
existingPair.position &&
|
||||
existingPair.position.valueStart &&
|
||||
existingPair.position.valueEnd
|
||||
) {
|
||||
const formattedValue = formatValueForExpression(value, op);
|
||||
context.modifiedQuery =
|
||||
context.modifiedQuery.slice(0, existingPair.position.valueStart) +
|
||||
formattedValue +
|
||||
context.modifiedQuery.slice(existingPair.position.valueEnd + 1);
|
||||
context.queryPairsMap = getQueryPairsMap(context.modifiedQuery);
|
||||
}
|
||||
return { type: 'update' };
|
||||
}
|
||||
|
||||
return { type: 'add', shouldAddToNonExisting: true };
|
||||
}
|
||||
|
||||
// Factory function to get appropriate handler
|
||||
function getOperatorHandler(
|
||||
operator: string,
|
||||
): (
|
||||
filter: TagFilterItem,
|
||||
context: QueryProcessingContext,
|
||||
) => ProcessingResult {
|
||||
if (isInOperator(operator)) {
|
||||
return processInOperator;
|
||||
}
|
||||
return processDefaultOperator;
|
||||
}
|
||||
|
||||
// Helper function to create new filter items from unvisited query pairs
|
||||
const createNewFilterItems = (
|
||||
context: QueryProcessingContext,
|
||||
): TagFilterItem[] => {
|
||||
const newFilterItems: TagFilterItem[] = [];
|
||||
|
||||
context.queryPairsMap.forEach((pair, key) => {
|
||||
if (!context.visitedPairs.has(key)) {
|
||||
const operator = pair.hasNegation
|
||||
? getOperatorValue(`NOT_${pair.operator}`.toUpperCase())
|
||||
: getOperatorValue(pair.operator.toUpperCase());
|
||||
|
||||
const formatValuesForFilter = (
|
||||
value: string | string[],
|
||||
): string | string[] => {
|
||||
if (Array.isArray(value)) {
|
||||
return value.map((v) => (typeof v === 'string' ? unquote(v) : String(v)));
|
||||
}
|
||||
if (typeof value === 'string') {
|
||||
return unquote(value);
|
||||
}
|
||||
return String(value);
|
||||
};
|
||||
|
||||
newFilterItems.push({
|
||||
id: uuid(),
|
||||
op: operator,
|
||||
key: {
|
||||
id: pair.key,
|
||||
key: pair.key,
|
||||
type: '',
|
||||
},
|
||||
value: pair.isMultiValue
|
||||
? formatValuesForFilter(pair.valueList as string[]) ?? ''
|
||||
: formatValuesForFilter(pair.value as string) ?? '',
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return newFilterItems;
|
||||
};
|
||||
|
||||
// Main refactored function
|
||||
export const convertFiltersToExpressionWithExistingQuery = (
|
||||
filters: TagFilter,
|
||||
existingQuery: string | undefined,
|
||||
): { filters: TagFilter; filter: { expression: string } } => {
|
||||
// Early return for no existing query
|
||||
if (!existingQuery) {
|
||||
const normalizedFilters = normalizeDeprecatedOperators(filters);
|
||||
const expression = convertFiltersToExpression(normalizedFilters);
|
||||
return {
|
||||
filters: normalizedFilters,
|
||||
filter: expression,
|
||||
};
|
||||
}
|
||||
|
||||
// Validate inputs
|
||||
if (!validateQuery(existingQuery)) {
|
||||
return { filters, filter: { expression: existingQuery || '' } };
|
||||
}
|
||||
|
||||
// Normalize deprecated operators
|
||||
const normalizedFilters = normalizeDeprecatedOperators(filters);
|
||||
|
||||
// Initialize processing context
|
||||
const context: QueryProcessingContext = {
|
||||
originalQuery: existingQuery,
|
||||
queryPairsMap: getQueryPairsMap(existingQuery.trim()),
|
||||
visitedPairs: new Set(),
|
||||
modifications: [],
|
||||
newFilters: [],
|
||||
nonExistingFilters: [],
|
||||
modifiedQuery: existingQuery,
|
||||
};
|
||||
|
||||
// Process each filter (if any exist)
|
||||
if (normalizedFilters.items?.length > 0) {
|
||||
normalizedFilters.items.filter(validateFilter).forEach((filter) => {
|
||||
const handler = getOperatorHandler(filter.op);
|
||||
const result = handler(filter, context);
|
||||
|
||||
// Apply result based on type
|
||||
switch (result.type) {
|
||||
case 'add':
|
||||
if (result.shouldAddToNonExisting) {
|
||||
context.nonExistingFilters.push(filter);
|
||||
}
|
||||
break;
|
||||
case 'update':
|
||||
case 'transform':
|
||||
case 'skip':
|
||||
// Already handled in the processor
|
||||
break;
|
||||
default:
|
||||
// Handle any other cases
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Create new filter items from unvisited query pairs
|
||||
const newFilterItems = createNewFilterItems(context);
|
||||
|
||||
// Merge new filter items with existing ones
|
||||
if (newFilterItems.length > 0) {
|
||||
if (normalizedFilters?.items?.length > 0) {
|
||||
// Add new filter items to existing ones
|
||||
normalizedFilters.items = [...normalizedFilters.items, ...newFilterItems];
|
||||
} else {
|
||||
// Use new filter items as the main filters
|
||||
normalizedFilters.items = newFilterItems;
|
||||
}
|
||||
}
|
||||
|
||||
// Build final expression
|
||||
let finalExpression = context.modifiedQuery;
|
||||
|
||||
if (context.nonExistingFilters.length > 0) {
|
||||
// Convert non-existing filters to expression and append
|
||||
const nonExistingFilterExpression = convertFiltersToExpression({
|
||||
items: context.nonExistingFilters,
|
||||
op: filters.op || 'AND',
|
||||
});
|
||||
|
||||
if (nonExistingFilterExpression.expression) {
|
||||
finalExpression = `${context.modifiedQuery.trim()} ${
|
||||
nonExistingFilterExpression.expression
|
||||
}`;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
filters: normalizedFilters,
|
||||
filter: { expression: finalExpression || '' },
|
||||
};
|
||||
};
|
||||
@@ -2,11 +2,9 @@
|
||||
import { createAggregation } from 'api/v5/queryRange/prepareQueryRangePayloadV5';
|
||||
import {
|
||||
DEPRECATED_OPERATORS_MAP,
|
||||
OPERATORS,
|
||||
QUERY_BUILDER_FUNCTIONS,
|
||||
} from 'constants/antlrQueryConstants';
|
||||
import { getOperatorValue } from 'container/QueryBuilder/filters/QueryBuilderSearch/utils';
|
||||
import { cloneDeep, isEqual, sortBy } from 'lodash-es';
|
||||
import { IQueryPair } from 'types/antlrQueryTypes';
|
||||
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import {
|
||||
@@ -24,7 +22,7 @@ import {
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
import { DataSource, ReduceOperators } from 'types/common/queryBuilder';
|
||||
import { extractQueryPairs } from 'utils/queryContextUtils';
|
||||
import { isQuoted, unquote } from 'utils/stringUtils';
|
||||
import { unquote } from 'utils/stringUtils';
|
||||
import { isFunctionOperator, isNonValueOperator } from 'utils/tokenUtils';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
@@ -38,57 +36,49 @@ const isArrayOperator = (operator: string): boolean => {
|
||||
return arrayOperators.includes(operator);
|
||||
};
|
||||
|
||||
const isVariable = (
|
||||
value: (string | number | boolean)[] | string | number | boolean,
|
||||
): boolean => {
|
||||
const isVariable = (value: string | string[] | number | boolean): boolean => {
|
||||
if (Array.isArray(value)) {
|
||||
return value.some((v) => typeof v === 'string' && v.trim().startsWith('$'));
|
||||
}
|
||||
return typeof value === 'string' && value.trim().startsWith('$');
|
||||
};
|
||||
|
||||
/**
|
||||
* 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
|
||||
* @param value - The value to format
|
||||
* @param operator - The operator being used (to determine if array is needed)
|
||||
* @returns Formatted value string
|
||||
*/
|
||||
export const formatValueForExpression = (
|
||||
value: (string | number | boolean)[] | string | number | boolean,
|
||||
const formatValueForExpression = (
|
||||
value: string[] | string | number | boolean,
|
||||
operator?: string,
|
||||
): string => {
|
||||
if (isVariable(value)) {
|
||||
return String(value);
|
||||
}
|
||||
|
||||
// For IN operators, ensure value is always an array
|
||||
if (isArrayOperator(operator || '')) {
|
||||
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)) {
|
||||
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') {
|
||||
return formatSingleValue(value);
|
||||
// Add single quotes around all string values and escape internal single quotes
|
||||
return `'${value.replace(/'/g, "\\'")}'`;
|
||||
}
|
||||
|
||||
return String(value);
|
||||
@@ -144,43 +134,14 @@ export const convertFiltersToExpression = (
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* 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 => {
|
||||
const formatValuesForFilter = (value: string | string[]): string | string[] => {
|
||||
if (Array.isArray(value)) {
|
||||
return value.map(formatSingleValueForFilter);
|
||||
return value.map((v) => (typeof v === 'string' ? unquote(v) : String(v)));
|
||||
}
|
||||
|
||||
return formatSingleValueForFilter(value);
|
||||
if (typeof value === 'string') {
|
||||
return unquote(value);
|
||||
}
|
||||
return String(value);
|
||||
};
|
||||
|
||||
export const convertExpressionToFilters = (
|
||||
@@ -211,298 +172,6 @@ export const convertExpressionToFilters = (
|
||||
|
||||
return filters;
|
||||
};
|
||||
const getQueryPairsMap = (query: string): Map<string, IQueryPair> => {
|
||||
const queryPairs = extractQueryPairs(query);
|
||||
const queryPairsMap: Map<string, IQueryPair> = new Map();
|
||||
|
||||
queryPairs.forEach((pair) => {
|
||||
const key = pair.hasNegation
|
||||
? `${pair.key}-not ${pair.operator}`.trim().toLowerCase()
|
||||
: `${pair.key}-${pair.operator}`.trim().toLowerCase();
|
||||
queryPairsMap.set(key, pair);
|
||||
});
|
||||
|
||||
return queryPairsMap;
|
||||
};
|
||||
|
||||
export const convertFiltersToExpressionWithExistingQuery = (
|
||||
filters: TagFilter,
|
||||
existingQuery: string | undefined,
|
||||
): { filters: TagFilter; filter: { expression: string } } => {
|
||||
// Check for deprecated operators and replace them with new operators
|
||||
const updatedFilters = cloneDeep(filters);
|
||||
|
||||
// Replace deprecated operators in filter items
|
||||
if (updatedFilters?.items) {
|
||||
updatedFilters.items = updatedFilters.items.map((item) => {
|
||||
const opLower = item.op?.toLowerCase();
|
||||
if (Object.keys(DEPRECATED_OPERATORS_MAP).includes(opLower)) {
|
||||
return {
|
||||
...item,
|
||||
op: DEPRECATED_OPERATORS_MAP[
|
||||
opLower as keyof typeof DEPRECATED_OPERATORS_MAP
|
||||
].toLowerCase(),
|
||||
};
|
||||
}
|
||||
return item;
|
||||
});
|
||||
}
|
||||
|
||||
if (!existingQuery) {
|
||||
// If no existing query, return filters with a newly generated expression
|
||||
return {
|
||||
filters: updatedFilters,
|
||||
filter: convertFiltersToExpression(updatedFilters),
|
||||
};
|
||||
}
|
||||
|
||||
const nonExistingFilters: TagFilterItem[] = [];
|
||||
let modifiedQuery = existingQuery; // We'll modify this query as we proceed
|
||||
const visitedPairs: Set<string> = new Set(); // Set to track visited query pairs
|
||||
|
||||
// Map extracted query pairs to key-specific pair information for faster access
|
||||
let queryPairsMap = getQueryPairsMap(existingQuery);
|
||||
|
||||
filters?.items?.forEach((filter) => {
|
||||
const { key, op, value } = filter;
|
||||
|
||||
// Skip invalid filters with no key
|
||||
if (!key) return;
|
||||
|
||||
let shouldAddToNonExisting = true; // Flag to decide if the filter should be added to non-existing filters
|
||||
const sanitizedOperator = op.trim().toUpperCase();
|
||||
|
||||
// Check if the operator is IN or NOT IN
|
||||
if (
|
||||
[OPERATORS.IN, `${OPERATORS.NOT} ${OPERATORS.IN}`].includes(
|
||||
sanitizedOperator,
|
||||
)
|
||||
) {
|
||||
const existingPair = queryPairsMap.get(
|
||||
`${key.key}-${op}`.trim().toLowerCase(),
|
||||
);
|
||||
const formattedValue = formatValueForExpression(value, op);
|
||||
|
||||
// If a matching query pair exists, modify the query
|
||||
if (
|
||||
existingPair &&
|
||||
existingPair.position?.valueStart &&
|
||||
existingPair.position?.valueEnd
|
||||
) {
|
||||
visitedPairs.add(`${key.key}-${op}`.trim().toLowerCase());
|
||||
|
||||
// Check if existing values match current filter values (for array-based operators)
|
||||
if (existingPair.valueList && filter.value && Array.isArray(filter.value)) {
|
||||
// Clean quotes from string values for comparison
|
||||
const cleanValues = (values: any[]): any[] =>
|
||||
values.map((val) => (typeof val === 'string' ? unquote(val) : val));
|
||||
|
||||
const cleanExistingValues = cleanValues(existingPair.valueList);
|
||||
const cleanFilterValues = cleanValues(filter.value);
|
||||
|
||||
// Compare arrays (order-independent) - if identical, keep existing value
|
||||
const isSameValues =
|
||||
cleanExistingValues.length === cleanFilterValues.length &&
|
||||
isEqual(sortBy(cleanExistingValues), sortBy(cleanFilterValues));
|
||||
|
||||
if (isSameValues) {
|
||||
// Values are identical, preserve existing formatting
|
||||
modifiedQuery =
|
||||
modifiedQuery.slice(0, existingPair.position.valueStart) +
|
||||
existingPair.value +
|
||||
modifiedQuery.slice(existingPair.position.valueEnd + 1);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
modifiedQuery =
|
||||
modifiedQuery.slice(0, existingPair.position.valueStart) +
|
||||
formattedValue +
|
||||
modifiedQuery.slice(existingPair.position.valueEnd + 1);
|
||||
|
||||
queryPairsMap = getQueryPairsMap(modifiedQuery);
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle the different cases for IN operator
|
||||
switch (sanitizedOperator) {
|
||||
case OPERATORS.IN:
|
||||
// If there's a NOT IN or equal operator, merge the filter
|
||||
if (
|
||||
queryPairsMap.has(
|
||||
`${key.key}-${OPERATORS.NOT} ${op}`.trim().toLowerCase(),
|
||||
)
|
||||
) {
|
||||
const notInPair = queryPairsMap.get(
|
||||
`${key.key}-${OPERATORS.NOT} ${op}`.trim().toLowerCase(),
|
||||
);
|
||||
visitedPairs.add(
|
||||
`${key.key}-${OPERATORS.NOT} ${op}`.trim().toLowerCase(),
|
||||
);
|
||||
if (notInPair?.position?.valueEnd) {
|
||||
modifiedQuery = `${modifiedQuery.slice(
|
||||
0,
|
||||
notInPair.position.negationStart,
|
||||
)}${OPERATORS.IN} ${formattedValue} ${modifiedQuery.slice(
|
||||
notInPair.position.valueEnd + 1,
|
||||
)}`;
|
||||
queryPairsMap = getQueryPairsMap(modifiedQuery);
|
||||
}
|
||||
shouldAddToNonExisting = false; // Don't add this to non-existing filters
|
||||
} else if (
|
||||
queryPairsMap.has(`${key.key}-${OPERATORS['=']}`.trim().toLowerCase())
|
||||
) {
|
||||
const equalsPair = queryPairsMap.get(
|
||||
`${key.key}-${OPERATORS['=']}`.trim().toLowerCase(),
|
||||
);
|
||||
visitedPairs.add(`${key.key}-${OPERATORS['=']}`.trim().toLowerCase());
|
||||
if (equalsPair?.position?.valueEnd) {
|
||||
modifiedQuery = `${modifiedQuery.slice(
|
||||
0,
|
||||
equalsPair.position.operatorStart,
|
||||
)}${OPERATORS.IN} ${formattedValue} ${modifiedQuery.slice(
|
||||
equalsPair.position.valueEnd + 1,
|
||||
)}`;
|
||||
queryPairsMap = getQueryPairsMap(modifiedQuery);
|
||||
}
|
||||
shouldAddToNonExisting = false; // Don't add this to non-existing filters
|
||||
} else if (
|
||||
queryPairsMap.has(`${key.key}-${OPERATORS['!=']}`.trim().toLowerCase())
|
||||
) {
|
||||
const notEqualsPair = queryPairsMap.get(
|
||||
`${key.key}-${OPERATORS['!=']}`.trim().toLowerCase(),
|
||||
);
|
||||
visitedPairs.add(`${key.key}-${OPERATORS['!=']}`.trim().toLowerCase());
|
||||
if (notEqualsPair?.position?.valueEnd) {
|
||||
modifiedQuery = `${modifiedQuery.slice(
|
||||
0,
|
||||
notEqualsPair.position.operatorStart,
|
||||
)}${OPERATORS.IN} ${formattedValue} ${modifiedQuery.slice(
|
||||
notEqualsPair.position.valueEnd + 1,
|
||||
)}`;
|
||||
queryPairsMap = getQueryPairsMap(modifiedQuery);
|
||||
}
|
||||
shouldAddToNonExisting = false; // Don't add this to non-existing filters
|
||||
}
|
||||
break;
|
||||
case `${OPERATORS.NOT} ${OPERATORS.IN}`:
|
||||
if (
|
||||
queryPairsMap.has(`${key.key}-${OPERATORS['!=']}`.trim().toLowerCase())
|
||||
) {
|
||||
const notEqualsPair = queryPairsMap.get(
|
||||
`${key.key}-${OPERATORS['!=']}`.trim().toLowerCase(),
|
||||
);
|
||||
visitedPairs.add(`${key.key}-${OPERATORS['!=']}`.trim().toLowerCase());
|
||||
if (notEqualsPair?.position?.valueEnd) {
|
||||
modifiedQuery = `${modifiedQuery.slice(
|
||||
0,
|
||||
notEqualsPair.position.operatorStart,
|
||||
)}${OPERATORS.NOT} ${
|
||||
OPERATORS.IN
|
||||
} ${formattedValue} ${modifiedQuery.slice(
|
||||
notEqualsPair.position.valueEnd + 1,
|
||||
)}`;
|
||||
queryPairsMap = getQueryPairsMap(modifiedQuery);
|
||||
}
|
||||
shouldAddToNonExisting = false; // Don't add this to non-existing filters
|
||||
}
|
||||
break; // No operation needed for NOT IN case
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
queryPairsMap.has(`${filter.key?.key}-${filter.op}`.trim().toLowerCase())
|
||||
) {
|
||||
const existingPair = queryPairsMap.get(
|
||||
`${filter.key?.key}-${filter.op}`.trim().toLowerCase(),
|
||||
);
|
||||
if (
|
||||
existingPair &&
|
||||
existingPair.position?.valueStart &&
|
||||
existingPair.position?.valueEnd
|
||||
) {
|
||||
const formattedValue = formatValueForExpression(value, op);
|
||||
// replace the value with the new value
|
||||
modifiedQuery =
|
||||
modifiedQuery.slice(0, existingPair.position.valueStart) +
|
||||
formattedValue +
|
||||
modifiedQuery.slice(existingPair.position.valueEnd + 1);
|
||||
queryPairsMap = getQueryPairsMap(modifiedQuery);
|
||||
}
|
||||
|
||||
visitedPairs.add(`${filter.key?.key}-${filter.op}`.trim().toLowerCase());
|
||||
}
|
||||
|
||||
// Add filters that don't have an existing pair to non-existing filters
|
||||
if (
|
||||
shouldAddToNonExisting &&
|
||||
!queryPairsMap.has(`${filter.key?.key}-${filter.op}`.trim().toLowerCase())
|
||||
) {
|
||||
nonExistingFilters.push(filter);
|
||||
}
|
||||
});
|
||||
|
||||
// Create new filters from non-visited query pairs
|
||||
const newFilterItems: TagFilterItem[] = [];
|
||||
queryPairsMap.forEach((pair, key) => {
|
||||
if (!visitedPairs.has(key)) {
|
||||
const operator = pair.hasNegation
|
||||
? getOperatorValue(`NOT_${pair.operator}`.toUpperCase())
|
||||
: getOperatorValue(pair.operator.toUpperCase());
|
||||
|
||||
newFilterItems.push({
|
||||
id: uuid(),
|
||||
op: operator,
|
||||
key: {
|
||||
id: pair.key,
|
||||
key: pair.key,
|
||||
type: '',
|
||||
},
|
||||
value: pair.isMultiValue
|
||||
? formatValuesForFilter(pair.valueList as string[]) ?? ''
|
||||
: formatValuesForFilter(pair.value as string) ?? '',
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Merge new filter items with existing ones
|
||||
if (newFilterItems.length > 0 && updatedFilters?.items) {
|
||||
updatedFilters.items = [...updatedFilters.items, ...newFilterItems];
|
||||
}
|
||||
|
||||
// If no non-existing filters, return the modified query directly
|
||||
if (nonExistingFilters.length === 0) {
|
||||
return {
|
||||
filters: updatedFilters,
|
||||
filter: { expression: modifiedQuery },
|
||||
};
|
||||
}
|
||||
|
||||
// Convert non-existing filters to an expression and append to the modified query
|
||||
const nonExistingFilterExpression = convertFiltersToExpression({
|
||||
items: nonExistingFilters,
|
||||
op: filters.op || 'AND',
|
||||
});
|
||||
|
||||
if (nonExistingFilterExpression.expression) {
|
||||
return {
|
||||
filters: updatedFilters,
|
||||
filter: {
|
||||
expression: `${modifiedQuery.trim()} ${
|
||||
nonExistingFilterExpression.expression
|
||||
}`,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// Return the final result with the modified query
|
||||
return {
|
||||
filters: updatedFilters,
|
||||
filter: { expression: modifiedQuery || '' },
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Removes specified key-value pairs from a logical query expression string.
|
||||
|
||||
@@ -178,7 +178,7 @@ export default function CheckboxFilter(props: ICheckboxProps): JSX.Element {
|
||||
if (SELECTED_OPERATORS.includes(filterSync.op)) {
|
||||
if (isArray(filterSync.value)) {
|
||||
filterSync.value.forEach((val) => {
|
||||
filterState[String(val)] = true;
|
||||
filterState[val] = true;
|
||||
});
|
||||
} else if (typeof filterSync.value === 'string') {
|
||||
filterState[filterSync.value] = true;
|
||||
@@ -191,7 +191,7 @@ export default function CheckboxFilter(props: ICheckboxProps): JSX.Element {
|
||||
filterState = setDefaultValues(attributeValues, true);
|
||||
if (isArray(filterSync.value)) {
|
||||
filterSync.value.forEach((val) => {
|
||||
filterState[String(val)] = false;
|
||||
filterState[val] = false;
|
||||
});
|
||||
} else if (typeof filterSync.value === 'string') {
|
||||
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 { BookOpenText, ChevronsDown, TriangleAlert } from 'lucide-react';
|
||||
import KeyValueLabel from 'periscope/components/KeyValueLabel';
|
||||
import { ReactNode, useMemo } from 'react';
|
||||
import { ReactNode } from 'react';
|
||||
import { Warning } from 'types/api';
|
||||
|
||||
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 {
|
||||
children?: ReactNode;
|
||||
warningData?: Warning;
|
||||
message?: string | ReactNode;
|
||||
warningData: Warning;
|
||||
}
|
||||
|
||||
function WarningPopover({
|
||||
children,
|
||||
warningData,
|
||||
message = '',
|
||||
...popoverProps
|
||||
}: WarningPopoverProps): JSX.Element {
|
||||
const content = useMemo(() => {
|
||||
if (message) {
|
||||
return <PopoverMessage message={message} />;
|
||||
}
|
||||
if (warningData) {
|
||||
return <WarningContent warning={warningData} />;
|
||||
}
|
||||
return null;
|
||||
}, [message, warningData]);
|
||||
|
||||
return (
|
||||
<Popover
|
||||
content={content}
|
||||
content={<WarningContent warning={warningData} />}
|
||||
overlayStyle={{ padding: 0, maxWidth: '600px' }}
|
||||
overlayInnerStyle={{ padding: 0 }}
|
||||
autoAdjustOverflow
|
||||
@@ -169,8 +137,6 @@ function WarningPopover({
|
||||
|
||||
WarningPopover.defaultProps = {
|
||||
children: undefined,
|
||||
warningData: null,
|
||||
message: null,
|
||||
};
|
||||
|
||||
export default WarningPopover;
|
||||
|
||||
@@ -3,9 +3,9 @@ import './styles.scss';
|
||||
import { Select } from 'antd';
|
||||
import { DefaultOptionType } from 'antd/es/select';
|
||||
|
||||
import { UniversalYAxisUnitMappings } from './constants';
|
||||
import { UniversalYAxisUnitMappings, Y_AXIS_CATEGORIES } from './constants';
|
||||
import { UniversalYAxisUnit, YAxisUnitSelectorProps } from './types';
|
||||
import { getYAxisCategories, mapMetricUnitToUniversalUnit } from './utils';
|
||||
import { mapMetricUnitToUniversalUnit } from './utils';
|
||||
|
||||
function YAxisUnitSelector({
|
||||
value,
|
||||
@@ -13,7 +13,6 @@ function YAxisUnitSelector({
|
||||
placeholder = 'Please select a unit',
|
||||
loading = false,
|
||||
'data-testid': dataTestId,
|
||||
source,
|
||||
}: YAxisUnitSelectorProps): JSX.Element {
|
||||
const universalUnit = mapMetricUnitToUniversalUnit(value);
|
||||
|
||||
@@ -38,8 +37,6 @@ function YAxisUnitSelector({
|
||||
return aliases.some((alias) => alias.toLowerCase().includes(search));
|
||||
};
|
||||
|
||||
const categories = getYAxisCategories(source);
|
||||
|
||||
return (
|
||||
<div className="y-axis-unit-selector-component">
|
||||
<Select
|
||||
@@ -51,7 +48,7 @@ function YAxisUnitSelector({
|
||||
loading={loading}
|
||||
data-testid={dataTestId}
|
||||
>
|
||||
{categories.map((category) => (
|
||||
{Y_AXIS_CATEGORIES.map((category) => (
|
||||
<Select.OptGroup key={category.name} label={category.name}>
|
||||
{category.units.map((unit) => (
|
||||
<Select.Option key={unit.id} value={unit.id}>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { fireEvent, render, screen } from '@testing-library/react';
|
||||
|
||||
import { YAxisSource } from '../types';
|
||||
import YAxisUnitSelector from '../YAxisUnitSelector';
|
||||
|
||||
describe('YAxisUnitSelector', () => {
|
||||
@@ -11,13 +10,7 @@ describe('YAxisUnitSelector', () => {
|
||||
});
|
||||
|
||||
it('renders with default placeholder', () => {
|
||||
render(
|
||||
<YAxisUnitSelector
|
||||
value=""
|
||||
onChange={mockOnChange}
|
||||
source={YAxisSource.ALERTS}
|
||||
/>,
|
||||
);
|
||||
render(<YAxisUnitSelector value="" onChange={mockOnChange} />);
|
||||
expect(screen.getByText('Please select a unit')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
@@ -27,20 +20,13 @@ describe('YAxisUnitSelector', () => {
|
||||
value=""
|
||||
onChange={mockOnChange}
|
||||
placeholder="Custom placeholder"
|
||||
source={YAxisSource.ALERTS}
|
||||
/>,
|
||||
);
|
||||
expect(screen.queryByText('Custom placeholder')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('calls onChange when a value is selected', () => {
|
||||
render(
|
||||
<YAxisUnitSelector
|
||||
value=""
|
||||
onChange={mockOnChange}
|
||||
source={YAxisSource.ALERTS}
|
||||
/>,
|
||||
);
|
||||
render(<YAxisUnitSelector value="" onChange={mockOnChange} />);
|
||||
const select = screen.getByRole('combobox');
|
||||
|
||||
fireEvent.mouseDown(select);
|
||||
@@ -55,30 +41,18 @@ describe('YAxisUnitSelector', () => {
|
||||
});
|
||||
|
||||
it('filters options based on search input', () => {
|
||||
render(
|
||||
<YAxisUnitSelector
|
||||
value=""
|
||||
onChange={mockOnChange}
|
||||
source={YAxisSource.ALERTS}
|
||||
/>,
|
||||
);
|
||||
render(<YAxisUnitSelector value="" onChange={mockOnChange} />);
|
||||
const select = screen.getByRole('combobox');
|
||||
|
||||
fireEvent.mouseDown(select);
|
||||
const input = screen.getByRole('combobox');
|
||||
fireEvent.change(input, { target: { value: 'bytes/sec' } });
|
||||
fireEvent.change(input, { target: { value: 'byte' } });
|
||||
|
||||
expect(screen.getByText('Bytes/sec')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows all categories and their units', () => {
|
||||
render(
|
||||
<YAxisUnitSelector
|
||||
value=""
|
||||
onChange={mockOnChange}
|
||||
source={YAxisSource.ALERTS}
|
||||
/>,
|
||||
);
|
||||
render(<YAxisUnitSelector value="" onChange={mockOnChange} />);
|
||||
const select = screen.getByRole('combobox');
|
||||
|
||||
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 {
|
||||
getUniversalNameFromMetricUnit,
|
||||
mapMetricUnitToUniversalUnit,
|
||||
mergeCategories,
|
||||
} from '../utils';
|
||||
|
||||
describe('YAxisUnitSelector utils', () => {
|
||||
@@ -38,43 +36,4 @@ describe('YAxisUnitSelector utils', () => {
|
||||
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 }],
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -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;
|
||||
disabled?: boolean;
|
||||
'data-testid'?: string;
|
||||
source: YAxisSource;
|
||||
}
|
||||
|
||||
export enum UniversalYAxisUnit {
|
||||
// Time
|
||||
WEEKS = 'wk',
|
||||
DAYS = 'd',
|
||||
HOURS = 'h',
|
||||
MINUTES = 'min',
|
||||
@@ -17,14 +17,6 @@ export enum UniversalYAxisUnit {
|
||||
MICROSECONDS = 'us',
|
||||
MILLISECONDS = 'ms',
|
||||
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
|
||||
BYTES = 'By',
|
||||
@@ -37,17 +29,6 @@ export enum UniversalYAxisUnit {
|
||||
ZETTABYTES = 'ZBy',
|
||||
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
|
||||
BYTES_SECOND = 'By/s',
|
||||
KILOBYTES_SECOND = 'kBy/s',
|
||||
@@ -58,21 +39,9 @@ export enum UniversalYAxisUnit {
|
||||
EXABYTES_SECOND = 'EBy/s',
|
||||
ZETTABYTES_SECOND = 'ZBy/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 = 'bit',
|
||||
BITS_IEC = 'bits',
|
||||
KILOBITS = 'kbit',
|
||||
MEGABITS = 'Mbit',
|
||||
GIGABITS = 'Gbit',
|
||||
@@ -93,16 +62,6 @@ export enum UniversalYAxisUnit {
|
||||
ZETTABITS_SECOND = 'Zbit/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_SECOND = '{count}/s',
|
||||
@@ -128,231 +87,7 @@ export enum UniversalYAxisUnit {
|
||||
// Percent
|
||||
PERCENT = '%',
|
||||
PERCENT_UNIT = 'percentunit',
|
||||
|
||||
// Boolean
|
||||
TRUE_FALSE = '{bool}',
|
||||
YES_NO = '{bool_yn}',
|
||||
ON_OFF = 'bool_on_off',
|
||||
|
||||
// None
|
||||
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 {
|
||||
@@ -558,15 +293,6 @@ export enum YAxisUnit {
|
||||
UCUM_PEBIBYTES = 'PiBy',
|
||||
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',
|
||||
OPEN_METRICS_KIBIBYTES_SECOND = 'kibibytes_per_second',
|
||||
|
||||
@@ -597,24 +323,6 @@ export enum YAxisUnit {
|
||||
UCUM_PEBIBITS_SECOND = 'Pibit/s',
|
||||
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}',
|
||||
OPEN_METRICS_TRUE_FALSE = 'boolean_true_false',
|
||||
|
||||
@@ -656,27 +364,3 @@ export enum YAxisUnit {
|
||||
|
||||
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 { ADDITIONAL_Y_AXIS_CATEGORIES, BASE_Y_AXIS_CATEGORIES } from './data';
|
||||
import {
|
||||
UniversalYAxisUnit,
|
||||
YAxisCategory,
|
||||
YAxisSource,
|
||||
YAxisUnit,
|
||||
} from './types';
|
||||
import { UniversalYAxisUnit, YAxisUnit } from './types';
|
||||
|
||||
export const mapMetricUnitToUniversalUnit = (
|
||||
unit: string | undefined,
|
||||
@@ -15,7 +9,7 @@ export const mapMetricUnitToUniversalUnit = (
|
||||
}
|
||||
|
||||
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;
|
||||
@@ -37,44 +31,3 @@ export const getUniversalNameFromMetricUnit = (
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -244,10 +244,6 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
// Add border-bottom to table cells when pagination is not present
|
||||
.ant-spin-container:not(:has(.ant-pagination)) .ant-table-cell {
|
||||
border-bottom: 1px solid var(--bg-slate-500) !important;
|
||||
}
|
||||
|
||||
.endpoints-table-container {
|
||||
display: flex;
|
||||
@@ -426,28 +422,30 @@
|
||||
gap: 8px;
|
||||
.endpoint-meta-data-pill {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--bg-slate-300);
|
||||
overflow: hidden;
|
||||
box-sizing: content-box;
|
||||
width: fit-content;
|
||||
.endpoint-meta-data-label {
|
||||
display: flex;
|
||||
padding: 6px 8px;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
border-right: 1px solid var(--bg-slate-300);
|
||||
color: var(--text-vanilla-100);
|
||||
font-size: 14px;
|
||||
line-height: 18px; /* 128.571% */
|
||||
letter-spacing: -0.07px;
|
||||
padding: 6px 8px;
|
||||
background: var(--bg-slate-500);
|
||||
height: calc(100% - 12px);
|
||||
}
|
||||
|
||||
.endpoint-meta-data-value {
|
||||
display: flex;
|
||||
padding: 6px 8px;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
color: var(--text-vanilla-400);
|
||||
background: var(--bg-slate-400);
|
||||
font-size: 14px;
|
||||
line-height: 18px;
|
||||
letter-spacing: -0.07px;
|
||||
height: calc(100% - 12px);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -455,23 +453,9 @@
|
||||
.endpoint-details-filters-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
border: 1px solid var(--bg-slate-500);
|
||||
height: 36px;
|
||||
box-sizing: content-box;
|
||||
.ant-select-selector {
|
||||
border: none !important;
|
||||
}
|
||||
|
||||
.endpoint-details-filters-container-dropdown {
|
||||
width: 120px;
|
||||
border-right: 1px solid var(--bg-slate-500);
|
||||
height: 36px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
.ant-select-single {
|
||||
height: 32px;
|
||||
}
|
||||
}
|
||||
|
||||
.endpoint-details-filters-container-search {
|
||||
@@ -1012,6 +996,7 @@
|
||||
|
||||
.lightMode {
|
||||
.ant-drawer-header {
|
||||
border-bottom: 1px solid var(--bg-vanilla-400);
|
||||
background: var(--bg-vanilla-100);
|
||||
}
|
||||
|
||||
@@ -1022,25 +1007,6 @@
|
||||
}
|
||||
|
||||
.domain-detail-drawer {
|
||||
.endpoint-details-card,
|
||||
.status-code-table-container,
|
||||
.endpoint-details-filters-container,
|
||||
.endpoint-details-filters-container-dropdown,
|
||||
.ant-radio-button-wrapper,
|
||||
.views-tabs-container,
|
||||
.ant-btn-default.tab,
|
||||
.tab::before,
|
||||
.endpoint-meta-data-pill,
|
||||
.endpoint-meta-data-label,
|
||||
.endpoints-table-container,
|
||||
.group-by-label,
|
||||
.ant-select-selector,
|
||||
.ant-drawer-header {
|
||||
border-color: var(--bg-vanilla-300) !important;
|
||||
}
|
||||
.views-tabs .tab::before {
|
||||
background: var(--bg-vanilla-300);
|
||||
}
|
||||
.title {
|
||||
color: var(--text-ink-300);
|
||||
}
|
||||
@@ -1065,6 +1031,7 @@
|
||||
|
||||
.selected_view {
|
||||
background: var(--bg-vanilla-300);
|
||||
border: 1px solid var(--bg-slate-300);
|
||||
color: var(--text-ink-400);
|
||||
}
|
||||
|
||||
@@ -1193,11 +1160,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
.top-services-content {
|
||||
border-color: var(--bg-vanilla-300);
|
||||
}
|
||||
.dependent-services-container {
|
||||
border: none;
|
||||
padding: 10px 12px;
|
||||
.top-services-item {
|
||||
display: flex;
|
||||
@@ -1224,31 +1187,11 @@
|
||||
}
|
||||
|
||||
.top-services-item-progress-bar {
|
||||
background-color: var(--bg-vanilla-200);
|
||||
border: 1px solid var(--bg-vanilla-300);
|
||||
background-color: var(--bg-vanilla-300);
|
||||
border: 1px solid var(--bg-slate-300);
|
||||
}
|
||||
}
|
||||
}
|
||||
.ant-table {
|
||||
.ant-table-thead > tr > th {
|
||||
color: var(--text-ink-300);
|
||||
}
|
||||
|
||||
.ant-table-cell {
|
||||
&,
|
||||
&:has(.top-services-item-latency) {
|
||||
background: var(--bg-vanilla-100);
|
||||
}
|
||||
color: var(--text-ink-300);
|
||||
}
|
||||
|
||||
.ant-table-tbody > tr:hover > td {
|
||||
background: var(--bg-vanilla-200);
|
||||
}
|
||||
.table-row-dark {
|
||||
background: var(--bg-vanilla-300);
|
||||
}
|
||||
}
|
||||
|
||||
.top-services-item-percentage {
|
||||
color: var(--text-ink-300);
|
||||
@@ -1282,8 +1225,4 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
// Add border-bottom to table cells when pagination is not present
|
||||
.ant-spin-container:not(:has(.ant-pagination)) .ant-table-cell {
|
||||
border-bottom: 1px solid var(--bg-vanilla-300) !important;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { Progress, Tag, Tooltip } from 'antd';
|
||||
import { ColumnType } from 'antd/es/table';
|
||||
import { convertFiltersToExpressionWithExistingQuery } from 'components/QueryBuilderV2/utils';
|
||||
import { convertFiltersToExpressionWithExistingQuery } from 'components/QueryBuilderV2/queryProcessor';
|
||||
import {
|
||||
FiltersType,
|
||||
IQuickFiltersConfig,
|
||||
|
||||
@@ -57,8 +57,7 @@ describe('Request AWS integration', () => {
|
||||
expect(capturedPayload.attributes).toEqual({
|
||||
screen: 'AWS integration details',
|
||||
integration: 's3 sync',
|
||||
deployment_url: 'localhost',
|
||||
user_email: null,
|
||||
tenant_url: 'localhost',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { Button, Flex, Switch, Typography } from 'antd';
|
||||
import { BaseOptionType, DefaultOptionType, SelectProps } from 'antd/es/select';
|
||||
import { getInvolvedQueriesInTraceOperator } from 'components/QueryBuilderV2/QueryV2/TraceOperator/utils/utils';
|
||||
import { YAxisSource } from 'components/YAxisUnitSelector/types';
|
||||
import { getYAxisCategories } from 'components/YAxisUnitSelector/utils';
|
||||
import { Y_AXIS_CATEGORIES } from 'components/YAxisUnitSelector/constants';
|
||||
import ROUTES from 'constants/routes';
|
||||
import {
|
||||
AlertThresholdMatchType,
|
||||
@@ -40,8 +39,7 @@ export function getQueryNames(currentQuery: Query): BaseOptionType[] {
|
||||
}
|
||||
|
||||
export function getCategoryByOptionId(id: string): string | undefined {
|
||||
const categories = getYAxisCategories(YAxisSource.ALERTS);
|
||||
return categories.find((category) =>
|
||||
return Y_AXIS_CATEGORIES.find((category) =>
|
||||
category.units.some((unit) => unit.id === id),
|
||||
)?.name;
|
||||
}
|
||||
@@ -49,15 +47,14 @@ export function getCategoryByOptionId(id: string): string | undefined {
|
||||
export function getCategorySelectOptionByName(
|
||||
name: string,
|
||||
): DefaultOptionType[] {
|
||||
const categories = getYAxisCategories(YAxisSource.ALERTS);
|
||||
return (
|
||||
categories
|
||||
.find((category) => category.name === name)
|
||||
?.units.map((unit) => ({
|
||||
Y_AXIS_CATEGORIES.find((category) => category.name === name)?.units.map(
|
||||
(unit) => ({
|
||||
label: unit.name,
|
||||
value: unit.id,
|
||||
'data-testid': `threshold-unit-select-option-${unit.id}`,
|
||||
})) || []
|
||||
}),
|
||||
) || []
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import YAxisUnitSelector from 'components/YAxisUnitSelector';
|
||||
import { YAxisSource } from 'components/YAxisUnitSelector/types';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { useCreateAlertState } from 'container/CreateAlertV2/context';
|
||||
import ChartPreviewComponent from 'container/FormAlertRules/ChartPreview';
|
||||
@@ -38,7 +37,6 @@ function ChartPreview({ alertDef }: ChartPreviewProps): JSX.Element {
|
||||
onChange={(value): void => {
|
||||
setAlertState({ type: 'SET_Y_AXIS_UNIT', payload: value });
|
||||
}}
|
||||
source={YAxisSource.ALERTS}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { TableProps } from 'antd';
|
||||
import { PrecisionOption } from 'components/Graph/types';
|
||||
import { PrecisionOption } from 'components/Graph/yAxisConfig';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { LogsExplorerTableProps } from 'container/LogsExplorerTable/LogsExplorerTable.interfaces';
|
||||
import {
|
||||
|
||||
@@ -175,18 +175,7 @@ function LiveLogsContainer(): JSX.Element {
|
||||
if (isConnectionError && reconnectDueToError) {
|
||||
// Small delay to prevent immediate reconnection attempts
|
||||
const reconnectTimer = setTimeout(() => {
|
||||
const fallbackFilterExpression =
|
||||
prevFilterExpressionRef.current ||
|
||||
currentQuery?.builder.queryData[0]?.filter?.expression?.trim() ||
|
||||
null;
|
||||
|
||||
const validationResult = validateQuery(fallbackFilterExpression || '');
|
||||
|
||||
if (validationResult.isValid) {
|
||||
handleStartNewConnection(fallbackFilterExpression);
|
||||
} else {
|
||||
handleStartNewConnection(null);
|
||||
}
|
||||
handleStartNewConnection();
|
||||
}, 1000);
|
||||
|
||||
return (): void => clearTimeout(reconnectTimer);
|
||||
@@ -197,7 +186,6 @@ function LiveLogsContainer(): JSX.Element {
|
||||
reconnectDueToError,
|
||||
compositeQuery,
|
||||
handleStartNewConnection,
|
||||
currentQuery,
|
||||
]);
|
||||
|
||||
// clean up the connection when the component unmounts
|
||||
|
||||
@@ -274,7 +274,6 @@ function Login(): JSX.Element {
|
||||
autoFocus
|
||||
disabled={versionLoading}
|
||||
className="login-form-input"
|
||||
onPressEnter={onNextHandler}
|
||||
/>
|
||||
</FormContainer.Item>
|
||||
</ParentContainer>
|
||||
|
||||
@@ -8,7 +8,11 @@ import { ENTITY_VERSION_V5 } from 'constants/app';
|
||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||
import { AVAILABLE_EXPORT_PANEL_TYPES } from 'constants/panelTypes';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import { initialFilters, PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import {
|
||||
initialFilters,
|
||||
initialQueriesMap,
|
||||
PANEL_TYPES,
|
||||
} from 'constants/queryBuilder';
|
||||
import { DEFAULT_PER_PAGE_VALUE } from 'container/Controls/config';
|
||||
import ExplorerOptionWrapper from 'container/ExplorerOptions/ExplorerOptionWrapper';
|
||||
import { ChangeViewFunctionType } from 'container/ExplorerOptions/types';
|
||||
@@ -97,7 +101,12 @@ function LogsExplorerViewsContainer({
|
||||
const currentMinTimeRef = useRef<number>(minTime);
|
||||
|
||||
// Context
|
||||
const { stagedQuery, panelType } = useQueryBuilder();
|
||||
const {
|
||||
currentQuery,
|
||||
stagedQuery,
|
||||
panelType,
|
||||
updateAllQueriesOperators,
|
||||
} = useQueryBuilder();
|
||||
|
||||
const selectedPanelType = panelType || PANEL_TYPES.LIST;
|
||||
|
||||
@@ -127,8 +136,13 @@ function LogsExplorerViewsContainer({
|
||||
}, [stagedQuery, activeLogId]);
|
||||
|
||||
const exportDefaultQuery = useMemo(
|
||||
() => getExportQueryData(requestData, selectedPanelType),
|
||||
[selectedPanelType, requestData],
|
||||
() =>
|
||||
updateAllQueriesOperators(
|
||||
currentQuery || initialQueriesMap.logs,
|
||||
selectedPanelType,
|
||||
DataSource.LOGS,
|
||||
),
|
||||
[currentQuery, selectedPanelType, updateAllQueriesOperators],
|
||||
);
|
||||
|
||||
const {
|
||||
@@ -265,7 +279,9 @@ function LogsExplorerViewsContainer({
|
||||
|
||||
const widgetId = v4();
|
||||
|
||||
if (!exportDefaultQuery) return;
|
||||
const query = getExportQueryData(requestData, selectedPanelType);
|
||||
|
||||
if (!query) return;
|
||||
|
||||
logEvent('Logs Explorer: Add to dashboard successful', {
|
||||
panelType: selectedPanelType,
|
||||
@@ -274,7 +290,7 @@ function LogsExplorerViewsContainer({
|
||||
});
|
||||
|
||||
const dashboardEditView = generateExportToDashboardLink({
|
||||
query: exportDefaultQuery,
|
||||
query,
|
||||
panelType: panelTypeParam,
|
||||
dashboardId: dashboard.id,
|
||||
widgetId,
|
||||
@@ -282,7 +298,7 @@ function LogsExplorerViewsContainer({
|
||||
|
||||
safeNavigate(dashboardEditView);
|
||||
},
|
||||
[safeNavigate, exportDefaultQuery, selectedPanelType],
|
||||
[safeNavigate, requestData, selectedPanelType],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -124,7 +124,7 @@
|
||||
|
||||
.builder-units-filter-label {
|
||||
margin-bottom: 0px !important;
|
||||
font-size: 12px;
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ import { MetricType } from 'api/metricsExplorer/getMetricsList';
|
||||
import { UpdateMetricMetadataProps } from 'api/metricsExplorer/updateMetricMetadata';
|
||||
import { ResizeTable } from 'components/ResizeTable';
|
||||
import YAxisUnitSelector from 'components/YAxisUnitSelector';
|
||||
import { YAxisSource } from 'components/YAxisUnitSelector/types';
|
||||
import { getUniversalNameFromMetricUnit } from 'components/YAxisUnitSelector/utils';
|
||||
import FieldRenderer from 'container/LogDetailedView/FieldRenderer';
|
||||
import { DataType } from 'container/LogDetailedView/TableView';
|
||||
@@ -121,7 +120,6 @@ function Metadata({
|
||||
setMetricMetadata((prev) => ({ ...prev, unit: value }));
|
||||
}}
|
||||
data-testid="unit-select"
|
||||
source={YAxisSource.EXPLORER}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
/* eslint-disable sonarjs/cognitive-complexity */
|
||||
import {
|
||||
convertFiltersToExpressionWithExistingQuery,
|
||||
removeKeysFromExpression,
|
||||
} from 'components/QueryBuilderV2/utils';
|
||||
import { convertFiltersToExpressionWithExistingQuery } from 'components/QueryBuilderV2/queryProcessor';
|
||||
import { removeKeysFromExpression } from 'components/QueryBuilderV2/utils';
|
||||
import { cloneDeep, isArray, isEmpty } from 'lodash-es';
|
||||
import { Dashboard, Widgets } from 'types/api/dashboard/getAll';
|
||||
import {
|
||||
|
||||
@@ -12,7 +12,10 @@ import {
|
||||
Switch,
|
||||
Typography,
|
||||
} from 'antd';
|
||||
import { PrecisionOption, PrecisionOptionsEnum } from 'components/Graph/types';
|
||||
import {
|
||||
PrecisionOption,
|
||||
PrecisionOptionsEnum,
|
||||
} from 'components/Graph/yAxisConfig';
|
||||
import TimePreference from 'components/TimePreferenceDropDown';
|
||||
import { PANEL_TYPES, PanelDisplay } from 'constants/queryBuilder';
|
||||
import GraphTypes, {
|
||||
|
||||
@@ -4,7 +4,10 @@ import './NewWidget.styles.scss';
|
||||
import { WarningOutlined } from '@ant-design/icons';
|
||||
import { Button, Flex, Modal, Space, Typography } from 'antd';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { PrecisionOption, PrecisionOptionsEnum } from 'components/Graph/types';
|
||||
import {
|
||||
PrecisionOption,
|
||||
PrecisionOptionsEnum,
|
||||
} from 'components/Graph/yAxisConfig';
|
||||
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
|
||||
import { adjustQueryForV5 } from 'components/QueryBuilderV2/utils';
|
||||
import { QueryParams } from 'constants/query';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { DefaultOptionType } from 'antd/es/select';
|
||||
import { omitIdFromQuery } from 'components/ExplorerCard/utils';
|
||||
import { PrecisionOptionsEnum } from 'components/Graph/types';
|
||||
import { PrecisionOptionsEnum } from 'components/Graph/yAxisConfig';
|
||||
import {
|
||||
initialQueryBuilderFormValuesMap,
|
||||
PANEL_TYPES,
|
||||
|
||||
@@ -1237,9 +1237,9 @@
|
||||
},
|
||||
{
|
||||
"dataSource": "opentelemetry-cloudflare",
|
||||
"label": "Cloudflare - Tracing",
|
||||
"label": "Cloudflare",
|
||||
"imgUrl": "/Logos/cloudflare.svg",
|
||||
"tags": ["apm/traces"],
|
||||
"tags": ["apm/traces", "logs"],
|
||||
"module": "apm",
|
||||
"relatedSearchKeywords": [
|
||||
"apm",
|
||||
@@ -1260,30 +1260,6 @@
|
||||
"id": "opentelemetry-cloudflare",
|
||||
"link": "https://signoz.io/docs/instrumentation/opentelemetry-cloudflare/"
|
||||
},
|
||||
{
|
||||
"dataSource": "opentelemetry-cloudflare-logs",
|
||||
"label": "Cloudflare Logs",
|
||||
"imgUrl": "/Logos/cloudflare.svg",
|
||||
"tags": ["logs"],
|
||||
"module": "logs",
|
||||
"relatedSearchKeywords": [
|
||||
"logs",
|
||||
"cloudflare",
|
||||
"cloudflare workers",
|
||||
"cloudflare monitoring",
|
||||
"cloudflare logging",
|
||||
"cloudflare observability",
|
||||
"opentelemetry cloudflare",
|
||||
"otel cloudflare",
|
||||
"cloudflare instrumentation",
|
||||
"monitor cloudflare workers",
|
||||
"cloudflare logs",
|
||||
"edge computing monitoring",
|
||||
"cloudflare to signoz"
|
||||
],
|
||||
"id": "opentelemetry-cloudflare-logs",
|
||||
"link": "https://signoz.io/docs/logs-management/send-logs/cloudflare-logs/"
|
||||
},
|
||||
{
|
||||
"dataSource": "kubernetes-pod-logs",
|
||||
"label": "Kubernetes Pod Logs",
|
||||
@@ -2845,133 +2821,6 @@
|
||||
],
|
||||
"link": "https://signoz.io/docs/vercel-ai-sdk-monitoring/"
|
||||
},
|
||||
{
|
||||
"dataSource": "amazon-bedrock",
|
||||
"label": "Amazon Bedrock",
|
||||
"imgUrl": "/Logos/amazon-bedrock.svg",
|
||||
"tags": ["LLM Monitoring"],
|
||||
"module": "apm",
|
||||
"relatedSearchKeywords": [
|
||||
"amazon bedrock monitoring",
|
||||
"amazon bedrock observability",
|
||||
"amazon bedrock performance tracking",
|
||||
"amazon bedrock latency tracing",
|
||||
"amazon bedrock metrics",
|
||||
"otel amazon bedrock integration",
|
||||
"amazon bedrock response time",
|
||||
"amazon bedrock logs",
|
||||
"amazon bedrock error tracking",
|
||||
"amazon bedrock debugging",
|
||||
"traces"
|
||||
],
|
||||
"link": "https://signoz.io/docs/amazon-bedrock-monitoring/"
|
||||
},
|
||||
{
|
||||
"dataSource": "autogen",
|
||||
"label": "AutoGen",
|
||||
"imgUrl": "/Logos/autogen.svg",
|
||||
"tags": ["LLM Monitoring"],
|
||||
"module": "apm",
|
||||
"relatedSearchKeywords": [
|
||||
"autogen monitoring",
|
||||
"autogen observability",
|
||||
"autogen performance tracking",
|
||||
"autogen latency tracing",
|
||||
"autogen metrics",
|
||||
"otel autogen integration",
|
||||
"autogen response time",
|
||||
"autogen logs",
|
||||
"autogen error tracking",
|
||||
"autogen debugging",
|
||||
"traces"
|
||||
],
|
||||
"link": "https://signoz.io/docs/autogen-observability/"
|
||||
},
|
||||
{
|
||||
"dataSource": "azure-openai",
|
||||
"label": "Azure OpenAI",
|
||||
"imgUrl": "/Logos/azure-openai.svg",
|
||||
"tags": ["LLM Monitoring"],
|
||||
"module": "apm",
|
||||
"relatedSearchKeywords": [
|
||||
"azure open ai monitoring",
|
||||
"azure open ai observability",
|
||||
"azure open ai performance tracking",
|
||||
"azure open ai latency tracing",
|
||||
"azure open ai metrics",
|
||||
"otel azure open ai integration",
|
||||
"azure open ai response time",
|
||||
"azure open ai logs",
|
||||
"azure open ai error tracking",
|
||||
"azure open ai debugging",
|
||||
"traces"
|
||||
],
|
||||
"link": "https://signoz.io/docs/azure-openai-monitoring/"
|
||||
},
|
||||
{
|
||||
"dataSource": "crew-ai",
|
||||
"label": "Crew AI",
|
||||
"imgUrl": "/Logos/crew-ai.svg",
|
||||
"tags": ["LLM Monitoring"],
|
||||
"module": "apm",
|
||||
"relatedSearchKeywords": [
|
||||
"crew ai monitoring",
|
||||
"crew ai observability",
|
||||
"crew ai performance tracking",
|
||||
"crew ai latency tracing",
|
||||
"crew ai metrics",
|
||||
"otel crew ai integration",
|
||||
"crew ai response time",
|
||||
"crew ai logs",
|
||||
"crew ai error tracking",
|
||||
"crew ai debugging",
|
||||
"traces"
|
||||
],
|
||||
"link": "https://signoz.io/docs/crewai-observability/"
|
||||
},
|
||||
{
|
||||
"dataSource": "litellm",
|
||||
"label": "LiteLLM",
|
||||
"imgUrl": "/Logos/litellm.svg",
|
||||
"tags": ["LLM Monitoring"],
|
||||
"module": "apm",
|
||||
"relatedSearchKeywords": [
|
||||
"litellm monitoring",
|
||||
"litellm observability",
|
||||
"litellm performance tracking",
|
||||
"litellm latency tracing",
|
||||
"litellm metrics",
|
||||
"otel litellm integration",
|
||||
"litellm response time",
|
||||
"litellm logs",
|
||||
"litellm error tracking",
|
||||
"litellm debugging",
|
||||
"traces"
|
||||
],
|
||||
"link": "https://signoz.io/docs/litellm-observability/"
|
||||
},
|
||||
{
|
||||
"dataSource": "pydantic-ai",
|
||||
"label": "Pydantic AI",
|
||||
"imgUrl": "/Logos/pydantic-ai.svg",
|
||||
"tags": ["LLM Monitoring"],
|
||||
"module": "apm",
|
||||
"relatedSearchKeywords": [
|
||||
"pydantic ai monitoring",
|
||||
"pydantic ai observability",
|
||||
"pydantic ai performance tracking",
|
||||
"pydantic ai latency tracing",
|
||||
"pydantic ai metrics",
|
||||
"otel pydantic ai integration",
|
||||
"pydantic ai response time",
|
||||
"pydantic ai logs",
|
||||
"pydantic ai error tracking",
|
||||
"pydantic ai debugging",
|
||||
"traces"
|
||||
],
|
||||
"link": "https://signoz.io/docs/pydantic-ai-observability/"
|
||||
},
|
||||
|
||||
{
|
||||
"dataSource": "mastra-monitoring",
|
||||
"label": "Mastra",
|
||||
|
||||
@@ -6,7 +6,6 @@ import { ColumnsType } from 'antd/lib/table';
|
||||
import deleteDomain from 'api/v1/domains/id/delete';
|
||||
import listAllDomain from 'api/v1/domains/list';
|
||||
import ErrorContent from 'components/ErrorModal/components/ErrorContent';
|
||||
import CopyToClipboard from 'periscope/components/CopyToClipboard';
|
||||
import { useErrorModal } from 'providers/ErrorModalProvider';
|
||||
import { useState } from 'react';
|
||||
import { useQuery } from 'react-query';
|
||||
@@ -33,23 +32,6 @@ const columns: ColumnsType<GettableAuthDomain> = [
|
||||
<Toggle isDefaultChecked={value} record={record} />
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'IDP Initiated SSO URL',
|
||||
dataIndex: 'relayState',
|
||||
key: 'relayState',
|
||||
width: 80,
|
||||
render: (_, record: GettableAuthDomain): JSX.Element => {
|
||||
const relayPath = record.authNProviderInfo.relayStatePath;
|
||||
if (!relayPath) {
|
||||
return (
|
||||
<Typography.Text style={{ paddingLeft: '6px' }}>N/A</Typography.Text>
|
||||
);
|
||||
}
|
||||
|
||||
const href = `${window.location.origin}/${relayPath}`;
|
||||
return <CopyToClipboard textToCopy={href} />;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Action',
|
||||
dataIndex: 'action',
|
||||
|
||||
@@ -116,11 +116,12 @@
|
||||
flex: 1 0 0;
|
||||
border-radius: 2px;
|
||||
background: var(--bg-cherry-500);
|
||||
border: none;
|
||||
border-color: none;
|
||||
}
|
||||
.cancel-run:hover {
|
||||
background-color: #ff7875 !important;
|
||||
color: var(--bg-vanilla-100) !important;
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { Select, SelectProps, Space, Typography } from 'antd';
|
||||
import { Select, SelectProps, Space } from 'antd';
|
||||
import { getCategorySelectOptionByName } from 'container/NewWidget/RightContainer/alertFomatCategories';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { popupContainer } from 'utils/selectPopupContainer';
|
||||
|
||||
import { categoryToSupport } from './config';
|
||||
import { selectStyles } from './styles';
|
||||
import { DefaultLabel, selectStyles } from './styles';
|
||||
import { IBuilderUnitsFilterProps } from './types';
|
||||
import { filterOption } from './utils';
|
||||
|
||||
@@ -31,9 +31,9 @@ function BuilderUnitsFilter({
|
||||
|
||||
return (
|
||||
<Space className="builder-units-filter">
|
||||
<Typography.Text className="builder-units-filter-label">
|
||||
<DefaultLabel className="builder-units-filter-label">
|
||||
Y-axis unit
|
||||
</Typography.Text>
|
||||
</DefaultLabel>
|
||||
<Select
|
||||
getPopupContainer={popupContainer}
|
||||
style={selectStyles}
|
||||
|
||||
@@ -74,7 +74,7 @@ export interface ITag {
|
||||
id?: string;
|
||||
key: BaseAutocompleteData;
|
||||
op: string;
|
||||
value: (string | number | boolean)[] | string | number | boolean;
|
||||
value: string[] | string | number | boolean;
|
||||
}
|
||||
|
||||
interface CustomTagProps {
|
||||
@@ -300,8 +300,7 @@ function QueryBuilderSearchV2(
|
||||
currentFilterItem?.key?.dataType ?? DataTypes.EMPTY,
|
||||
tagType: currentFilterItem?.key?.type ?? '',
|
||||
searchText: isArray(currentFilterItem?.value)
|
||||
? String(currentFilterItem?.value?.[currentFilterItem.value.length - 1]) ||
|
||||
''
|
||||
? currentFilterItem?.value?.[currentFilterItem.value.length - 1] || ''
|
||||
: currentFilterItem?.value?.toString() || '',
|
||||
},
|
||||
{
|
||||
|
||||
@@ -6,8 +6,6 @@
|
||||
font-weight: 400;
|
||||
line-height: 20px;
|
||||
letter-spacing: -0.07px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.dot {
|
||||
@@ -27,21 +25,16 @@
|
||||
.left-section {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
width: 90%;
|
||||
gap: 8px;
|
||||
|
||||
.value {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
.right-section {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
flex-shrink: 0;
|
||||
|
||||
.data-type {
|
||||
display: flex;
|
||||
@@ -52,7 +45,6 @@
|
||||
gap: 4px;
|
||||
border-radius: 20px;
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.type-tag {
|
||||
@@ -64,7 +56,6 @@
|
||||
gap: 4px;
|
||||
border-radius: 50px;
|
||||
text-transform: capitalize;
|
||||
white-space: nowrap;
|
||||
|
||||
&.tag {
|
||||
border-radius: 50px;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { PieArcDatum } from '@visx/shape/lib/shapes/Pie';
|
||||
import { convertFiltersToExpressionWithExistingQuery } from 'components/QueryBuilderV2/utils';
|
||||
import { convertFiltersToExpressionWithExistingQuery } from 'components/QueryBuilderV2/queryProcessor';
|
||||
import {
|
||||
initialQueryBuilderFormValuesMap,
|
||||
OPERATORS,
|
||||
|
||||
@@ -8,13 +8,4 @@
|
||||
min-height: 350px;
|
||||
padding: 0px 12px;
|
||||
}
|
||||
|
||||
.time-series-view-container {
|
||||
.time-series-view-container-header {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
padding: 12px 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,6 @@ import { LogsLoading } from 'container/LogsLoading/LogsLoading';
|
||||
import EmptyMetricsSearch from 'container/MetricsExplorer/Explorer/EmptyMetricsSearch';
|
||||
import { MetricsLoading } from 'container/MetricsExplorer/MetricsLoading/MetricsLoading';
|
||||
import NoLogs from 'container/NoLogs/NoLogs';
|
||||
import { BuilderUnitsFilter } from 'container/QueryBuilder/filters';
|
||||
import { CustomTimeType } from 'container/TopNav/DateTimeSelectionV2/config';
|
||||
import { TracesLoading } from 'container/TracesExplorer/TraceLoading/TraceLoading';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
@@ -82,14 +81,6 @@ function TimeSeriesView({
|
||||
const [minTimeScale, setMinTimeScale] = useState<number>();
|
||||
const [maxTimeScale, setMaxTimeScale] = useState<number>();
|
||||
const [graphVisibility, setGraphVisibility] = useState<boolean[]>([]);
|
||||
const [yAxisUnitInternal, setYAxisUnitInternal] = useState<string>(
|
||||
yAxisUnit || '',
|
||||
);
|
||||
|
||||
const onUnitChangeHandler = (value: string): void => {
|
||||
setYAxisUnitInternal(value);
|
||||
};
|
||||
|
||||
const legendScrollPositionRef = useRef<{
|
||||
scrollTop: number;
|
||||
scrollLeft: number;
|
||||
@@ -198,7 +189,7 @@ function TimeSeriesView({
|
||||
const chartOptions = getUPlotChartOptions({
|
||||
id: 'time-series-explorer',
|
||||
onDragSelect,
|
||||
yAxisUnit: yAxisUnitInternal || '',
|
||||
yAxisUnit: yAxisUnit || '',
|
||||
apiResponse: data?.payload,
|
||||
dimensions: {
|
||||
width: containerDimensions.width,
|
||||
@@ -270,17 +261,7 @@ function TimeSeriesView({
|
||||
!isError &&
|
||||
chartData &&
|
||||
!isEmpty(chartData?.[0]) &&
|
||||
chartOptions && (
|
||||
<div className="time-series-view-container">
|
||||
<div className="time-series-view-container-header">
|
||||
<BuilderUnitsFilter
|
||||
onChange={onUnitChangeHandler}
|
||||
yAxisUnit={yAxisUnitInternal}
|
||||
/>
|
||||
</div>
|
||||
<Uplot data={chartData} options={chartOptions} />
|
||||
</div>
|
||||
)}
|
||||
chartOptions && <Uplot data={chartData} options={chartOptions} />}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -5,13 +5,7 @@ import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import {
|
||||
Dispatch,
|
||||
MutableRefObject,
|
||||
SetStateAction,
|
||||
useEffect,
|
||||
useMemo,
|
||||
} from 'react';
|
||||
import { Dispatch, SetStateAction, useEffect, useMemo } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { Warning } from 'types/api';
|
||||
@@ -27,7 +21,6 @@ function TimeSeriesViewContainer({
|
||||
isFilterApplied,
|
||||
setWarning,
|
||||
setIsLoadingQueries,
|
||||
queryKeyRef,
|
||||
}: TimeSeriesViewProps): JSX.Element {
|
||||
const { stagedQuery, currentQuery, panelType } = useQueryBuilder();
|
||||
|
||||
@@ -55,22 +48,6 @@ function TimeSeriesViewContainer({
|
||||
return isValid.every(Boolean);
|
||||
}, [currentQuery]);
|
||||
|
||||
const queryKey = useMemo(
|
||||
() => [
|
||||
REACT_QUERY_KEY.GET_QUERY_RANGE,
|
||||
globalSelectedTime,
|
||||
maxTime,
|
||||
minTime,
|
||||
stagedQuery,
|
||||
],
|
||||
[globalSelectedTime, maxTime, minTime, stagedQuery],
|
||||
);
|
||||
|
||||
if (queryKeyRef) {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
queryKeyRef.current = queryKey;
|
||||
}
|
||||
|
||||
const { data, isLoading, isFetching, isError, error } = useGetQueryRange(
|
||||
{
|
||||
query: stagedQuery || initialQueriesMap[dataSource],
|
||||
@@ -84,7 +61,13 @@ function TimeSeriesViewContainer({
|
||||
// ENTITY_VERSION_V4,
|
||||
ENTITY_VERSION_V5,
|
||||
{
|
||||
queryKey,
|
||||
queryKey: [
|
||||
REACT_QUERY_KEY.GET_QUERY_RANGE,
|
||||
globalSelectedTime,
|
||||
maxTime,
|
||||
minTime,
|
||||
stagedQuery,
|
||||
],
|
||||
enabled: !!stagedQuery && panelType === PANEL_TYPES.TIME_SERIES,
|
||||
},
|
||||
);
|
||||
@@ -128,12 +111,10 @@ interface TimeSeriesViewProps {
|
||||
isFilterApplied: boolean;
|
||||
setWarning: Dispatch<SetStateAction<Warning | undefined>>;
|
||||
setIsLoadingQueries: Dispatch<SetStateAction<boolean>>;
|
||||
queryKeyRef?: MutableRefObject<any>;
|
||||
}
|
||||
|
||||
TimeSeriesViewContainer.defaultProps = {
|
||||
dataSource: DataSource.TRACES,
|
||||
queryKeyRef: undefined,
|
||||
};
|
||||
|
||||
export default TimeSeriesViewContainer;
|
||||
|
||||
@@ -14,7 +14,6 @@ import NoLogs from 'container/NoLogs/NoLogs';
|
||||
import { useOptionsMenu } from 'container/OptionsMenu';
|
||||
import { CustomTimeType } from 'container/TopNav/DateTimeSelectionV2/config';
|
||||
import TraceExplorerControls from 'container/TracesExplorer/Controls';
|
||||
import { getListViewQuery } from 'container/TracesExplorer/explorerUtils';
|
||||
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { Pagination } from 'hooks/queryPagination';
|
||||
@@ -23,12 +22,12 @@ import useDragColumns from 'hooks/useDragColumns';
|
||||
import { getDraggedColumns } from 'hooks/useDragColumns/utils';
|
||||
import useUrlQueryData from 'hooks/useUrlQueryData';
|
||||
import { RowData } from 'lib/query/createTableColumnsFromQuery';
|
||||
import { cloneDeep } from 'lodash-es';
|
||||
import { ArrowUp10, Minus } from 'lucide-react';
|
||||
import { useTimezone } from 'providers/Timezone';
|
||||
import {
|
||||
Dispatch,
|
||||
memo,
|
||||
MutableRefObject,
|
||||
SetStateAction,
|
||||
useCallback,
|
||||
useEffect,
|
||||
@@ -51,14 +50,12 @@ interface ListViewProps {
|
||||
isFilterApplied: boolean;
|
||||
setWarning: Dispatch<SetStateAction<Warning | undefined>>;
|
||||
setIsLoadingQueries: Dispatch<SetStateAction<boolean>>;
|
||||
queryKeyRef?: MutableRefObject<any>;
|
||||
}
|
||||
|
||||
function ListView({
|
||||
isFilterApplied,
|
||||
setWarning,
|
||||
setIsLoadingQueries,
|
||||
queryKeyRef,
|
||||
}: ListViewProps): JSX.Element {
|
||||
const {
|
||||
stagedQuery,
|
||||
@@ -95,10 +92,35 @@ function ListView({
|
||||
const paginationConfig =
|
||||
paginationQueryData ?? getDefaultPaginationConfig(PER_PAGE_OPTIONS);
|
||||
|
||||
const requestQuery = useMemo(
|
||||
() => getListViewQuery(stagedQuery || initialQueriesMap.traces, orderBy),
|
||||
[stagedQuery, orderBy],
|
||||
);
|
||||
const requestQuery = useMemo(() => {
|
||||
const query = stagedQuery
|
||||
? cloneDeep(stagedQuery)
|
||||
: cloneDeep(initialQueriesMap.traces);
|
||||
|
||||
if (query.builder.queryData[0]) {
|
||||
query.builder.queryData[0].orderBy = [
|
||||
{
|
||||
columnName: orderBy.split(':')[0],
|
||||
order: orderBy.split(':')[1] as 'asc' | 'desc',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
// add order by to trace operator
|
||||
if (
|
||||
query.builder.queryTraceOperator &&
|
||||
query.builder.queryTraceOperator.length > 0
|
||||
) {
|
||||
query.builder.queryTraceOperator[0].orderBy = [
|
||||
{
|
||||
columnName: orderBy.split(':')[0],
|
||||
order: orderBy.split(':')[1] as 'asc' | 'desc',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
return query;
|
||||
}, [stagedQuery, orderBy]);
|
||||
|
||||
const queryKey = useMemo(
|
||||
() => [
|
||||
@@ -124,11 +146,6 @@ function ListView({
|
||||
],
|
||||
);
|
||||
|
||||
if (queryKeyRef) {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
queryKeyRef.current = queryKey;
|
||||
}
|
||||
|
||||
const { data, isFetching, isLoading, isError, error } = useGetQueryRange(
|
||||
{
|
||||
query: requestQuery,
|
||||
@@ -276,8 +293,4 @@ function ListView({
|
||||
);
|
||||
}
|
||||
|
||||
ListView.defaultProps = {
|
||||
queryKeyRef: undefined,
|
||||
};
|
||||
|
||||
export default memo(ListView);
|
||||
|
||||
@@ -6,14 +6,7 @@ import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||
import { QueryTable } from 'container/QueryTable';
|
||||
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import {
|
||||
Dispatch,
|
||||
memo,
|
||||
MutableRefObject,
|
||||
SetStateAction,
|
||||
useEffect,
|
||||
useMemo,
|
||||
} from 'react';
|
||||
import { Dispatch, memo, SetStateAction, useEffect, useMemo } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { Warning } from 'types/api';
|
||||
@@ -24,11 +17,9 @@ import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
function TableView({
|
||||
setWarning,
|
||||
setIsLoadingQueries,
|
||||
queryKeyRef,
|
||||
}: {
|
||||
setWarning: Dispatch<SetStateAction<Warning | undefined>>;
|
||||
setIsLoadingQueries: Dispatch<SetStateAction<boolean>>;
|
||||
queryKeyRef?: MutableRefObject<any>;
|
||||
}): JSX.Element {
|
||||
const { stagedQuery, panelType } = useQueryBuilder();
|
||||
|
||||
@@ -37,22 +28,6 @@ function TableView({
|
||||
GlobalReducer
|
||||
>((state) => state.globalTime);
|
||||
|
||||
const queryKey = useMemo(
|
||||
() => [
|
||||
REACT_QUERY_KEY.GET_QUERY_RANGE,
|
||||
globalSelectedTime,
|
||||
maxTime,
|
||||
minTime,
|
||||
stagedQuery,
|
||||
],
|
||||
[globalSelectedTime, maxTime, minTime, stagedQuery],
|
||||
);
|
||||
|
||||
if (queryKeyRef) {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
queryKeyRef.current = queryKey;
|
||||
}
|
||||
|
||||
const { data, isLoading, isFetching, isError, error } = useGetQueryRange(
|
||||
{
|
||||
query: stagedQuery || initialQueriesMap.traces,
|
||||
@@ -65,7 +40,13 @@ function TableView({
|
||||
},
|
||||
ENTITY_VERSION_V5,
|
||||
{
|
||||
queryKey,
|
||||
queryKey: [
|
||||
REACT_QUERY_KEY.GET_QUERY_RANGE,
|
||||
globalSelectedTime,
|
||||
maxTime,
|
||||
minTime,
|
||||
stagedQuery,
|
||||
],
|
||||
enabled: !!stagedQuery && panelType === PANEL_TYPES.TABLE,
|
||||
},
|
||||
);
|
||||
@@ -108,8 +89,4 @@ function TableView({
|
||||
);
|
||||
}
|
||||
|
||||
TableView.defaultProps = {
|
||||
queryKeyRef: undefined,
|
||||
};
|
||||
|
||||
export default memo(TableView);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/* eslint-disable sonarjs/cognitive-complexity */
|
||||
import { Typography } from 'antd';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import ErrorInPlace from 'components/ErrorInPlace/ErrorInPlace';
|
||||
import ListViewOrderBy from 'components/OrderBy/ListViewOrderBy';
|
||||
import { ResizeTable } from 'components/ResizeTable';
|
||||
import { ENTITY_VERSION_V5 } from 'constants/app';
|
||||
import { QueryParams } from 'constants/query';
|
||||
@@ -9,18 +9,19 @@ import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||
import EmptyLogsSearch from 'container/EmptyLogsSearch/EmptyLogsSearch';
|
||||
import NoLogs from 'container/NoLogs/NoLogs';
|
||||
import { getListViewQuery } from 'container/TracesExplorer/explorerUtils';
|
||||
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { Pagination } from 'hooks/queryPagination';
|
||||
import useUrlQueryData from 'hooks/useUrlQueryData';
|
||||
import { ArrowUp10, Minus } from 'lucide-react';
|
||||
import {
|
||||
Dispatch,
|
||||
memo,
|
||||
MutableRefObject,
|
||||
SetStateAction,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { AppState } from 'store/reducers';
|
||||
@@ -29,6 +30,7 @@ import APIError from 'types/api/error';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
import DOCLINKS from 'utils/docLinks';
|
||||
import { transformBuilderQueryFields } from 'utils/queryTransformers';
|
||||
|
||||
import TraceExplorerControls from '../Controls';
|
||||
import { TracesLoading } from '../TraceLoading/TraceLoading';
|
||||
@@ -39,16 +41,15 @@ interface TracesViewProps {
|
||||
isFilterApplied: boolean;
|
||||
setWarning: Dispatch<SetStateAction<Warning | undefined>>;
|
||||
setIsLoadingQueries: Dispatch<SetStateAction<boolean>>;
|
||||
queryKeyRef?: MutableRefObject<any>;
|
||||
}
|
||||
|
||||
function TracesView({
|
||||
isFilterApplied,
|
||||
setWarning,
|
||||
setIsLoadingQueries,
|
||||
queryKeyRef,
|
||||
}: TracesViewProps): JSX.Element {
|
||||
const { stagedQuery, panelType } = useQueryBuilder();
|
||||
const [orderBy, setOrderBy] = useState<string>('timestamp:desc');
|
||||
|
||||
const { selectedTime: globalSelectedTime, maxTime, minTime } = useSelector<
|
||||
AppState,
|
||||
@@ -60,34 +61,21 @@ function TracesView({
|
||||
);
|
||||
|
||||
const transformedQuery = useMemo(
|
||||
() => getListViewQuery(stagedQuery || initialQueriesMap.traces),
|
||||
[stagedQuery],
|
||||
() =>
|
||||
transformBuilderQueryFields(stagedQuery || initialQueriesMap.traces, {
|
||||
orderBy: [
|
||||
{
|
||||
columnName: orderBy.split(':')[0],
|
||||
order: orderBy.split(':')[1] as 'asc' | 'desc',
|
||||
},
|
||||
],
|
||||
}),
|
||||
[stagedQuery, orderBy],
|
||||
);
|
||||
|
||||
const queryKey = useMemo(
|
||||
() => [
|
||||
REACT_QUERY_KEY.GET_QUERY_RANGE,
|
||||
globalSelectedTime,
|
||||
maxTime,
|
||||
minTime,
|
||||
stagedQuery,
|
||||
panelType,
|
||||
paginationQueryData,
|
||||
],
|
||||
[
|
||||
globalSelectedTime,
|
||||
maxTime,
|
||||
minTime,
|
||||
stagedQuery,
|
||||
panelType,
|
||||
paginationQueryData,
|
||||
],
|
||||
);
|
||||
|
||||
if (queryKeyRef) {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
queryKeyRef.current = queryKey;
|
||||
}
|
||||
const handleOrderChange = useCallback((value: string) => {
|
||||
setOrderBy(value);
|
||||
}, []);
|
||||
|
||||
const { data, isLoading, isFetching, isError, error } = useGetQueryRange(
|
||||
{
|
||||
@@ -104,7 +92,16 @@ function TracesView({
|
||||
},
|
||||
ENTITY_VERSION_V5,
|
||||
{
|
||||
queryKey,
|
||||
queryKey: [
|
||||
REACT_QUERY_KEY.GET_QUERY_RANGE,
|
||||
globalSelectedTime,
|
||||
maxTime,
|
||||
minTime,
|
||||
stagedQuery,
|
||||
panelType,
|
||||
paginationQueryData,
|
||||
orderBy,
|
||||
],
|
||||
enabled: !!stagedQuery && panelType === PANEL_TYPES.TRACE,
|
||||
},
|
||||
);
|
||||
@@ -151,6 +148,18 @@ function TracesView({
|
||||
</Typography>
|
||||
|
||||
<div className="trace-explorer-controls">
|
||||
<div className="order-by-container">
|
||||
<div className="order-by-label">
|
||||
Order by <Minus size={14} /> <ArrowUp10 size={14} />
|
||||
</div>
|
||||
|
||||
<ListViewOrderBy
|
||||
value={orderBy}
|
||||
onChange={handleOrderChange}
|
||||
dataSource={DataSource.TRACES}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<TraceExplorerControls
|
||||
isLoading={isLoading}
|
||||
totalCount={responseData?.length || 0}
|
||||
@@ -194,8 +203,4 @@ function TracesView({
|
||||
);
|
||||
}
|
||||
|
||||
TracesView.defaultProps = {
|
||||
queryKeyRef: undefined,
|
||||
};
|
||||
|
||||
export default memo(TracesView);
|
||||
|
||||
@@ -1,75 +0,0 @@
|
||||
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { OptionsQuery } from 'container/OptionsMenu/types';
|
||||
import { cloneDeep, set } from 'lodash-es';
|
||||
import { OrderByPayload, Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
export const getListViewQuery = (
|
||||
stagedQuery: Query,
|
||||
orderBy?: string,
|
||||
): Query => {
|
||||
const query = stagedQuery
|
||||
? cloneDeep(stagedQuery)
|
||||
: cloneDeep(initialQueriesMap.traces);
|
||||
|
||||
const orderByPayload: OrderByPayload[] = orderBy
|
||||
? [
|
||||
{
|
||||
columnName: orderBy.split(':')[0],
|
||||
order: orderBy.split(':')[1] as 'asc' | 'desc',
|
||||
},
|
||||
]
|
||||
: [];
|
||||
|
||||
for (let i = 0; i < query.builder.queryData.length; i++) {
|
||||
const queryData = query.builder.queryData[i];
|
||||
queryData.groupBy = [];
|
||||
queryData.having = {
|
||||
expression: '',
|
||||
};
|
||||
queryData.orderBy = orderByPayload;
|
||||
}
|
||||
|
||||
if (
|
||||
query.builder.queryTraceOperator &&
|
||||
query.builder.queryTraceOperator.length > 0
|
||||
) {
|
||||
for (let i = 0; i < query.builder.queryTraceOperator.length; i++) {
|
||||
const queryTraceOperator = query.builder.queryTraceOperator[i];
|
||||
queryTraceOperator.groupBy = [];
|
||||
queryTraceOperator.having = {
|
||||
expression: '',
|
||||
};
|
||||
queryTraceOperator.orderBy = orderByPayload;
|
||||
}
|
||||
}
|
||||
|
||||
return query;
|
||||
};
|
||||
|
||||
export const getQueryByPanelType = (
|
||||
stagedQuery: Query,
|
||||
panelType: PANEL_TYPES,
|
||||
): Query => {
|
||||
if (panelType === PANEL_TYPES.LIST || panelType === PANEL_TYPES.TRACE) {
|
||||
return getListViewQuery(stagedQuery);
|
||||
}
|
||||
return stagedQuery;
|
||||
};
|
||||
|
||||
export const getExportQueryData = (
|
||||
query: Query,
|
||||
panelType: PANEL_TYPES,
|
||||
options: OptionsQuery,
|
||||
): Query => {
|
||||
if (panelType === PANEL_TYPES.LIST) {
|
||||
const updatedQuery = cloneDeep(query);
|
||||
set(
|
||||
updatedQuery,
|
||||
'builder.queryData[0].selectColumns',
|
||||
options.selectColumns,
|
||||
);
|
||||
|
||||
return updatedQuery;
|
||||
}
|
||||
return query;
|
||||
};
|
||||
@@ -1,6 +1,6 @@
|
||||
import { convertFiltersToExpressionWithExistingQuery } from 'components/QueryBuilderV2/queryProcessor';
|
||||
import {
|
||||
convertAggregationToExpression,
|
||||
convertFiltersToExpressionWithExistingQuery,
|
||||
convertHavingToExpression,
|
||||
} from 'components/QueryBuilderV2/utils';
|
||||
import { QueryParams } from 'constants/query';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { getAggregateKeys } from 'api/queryBuilder/getAttributeKeys';
|
||||
import { convertFiltersToExpressionWithExistingQuery } from 'components/QueryBuilderV2/utils';
|
||||
import { convertFiltersToExpressionWithExistingQuery } from 'components/QueryBuilderV2/queryProcessor';
|
||||
import { SOMETHING_WENT_WRONG } from 'constants/api';
|
||||
import { QueryBuilderKeys } from 'constants/queryBuilder';
|
||||
import ROUTES from 'constants/routes';
|
||||
|
||||
@@ -63,8 +63,7 @@ export function convertOperatorLabelForExceptions(
|
||||
export function formatStringValuesForTrace(
|
||||
val: TagFilterItem['value'] = [],
|
||||
): string[] {
|
||||
// IN QB V5 we can pass array of all (boolean, number, string) values. To make this compatible with the old version, we need to convert the array to a string array.
|
||||
return !Array.isArray(val) ? [String(val)] : val.map((item) => String(item));
|
||||
return !Array.isArray(val) ? [String(val)] : val;
|
||||
}
|
||||
|
||||
export const convertCompositeQueryToTraceSelectedTags = (
|
||||
|
||||
@@ -451,12 +451,6 @@ export const getUPlotChartOptions = ({
|
||||
(self): void => {
|
||||
const selection = self.select;
|
||||
if (selection) {
|
||||
// Cleanup any visible "View Traces" buttons when drag selection occurs
|
||||
const activeButtons = document.querySelectorAll(
|
||||
'.view-onclick-show-button',
|
||||
);
|
||||
activeButtons.forEach((btn) => btn.remove());
|
||||
|
||||
const startTime = self.posToVal(selection.left, 'x');
|
||||
const endTime = self.posToVal(selection.left + selection.width, 'x');
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { PrecisionOption } from 'components/Graph/types';
|
||||
import { getToolTipValue } from 'components/Graph/yAxisConfig';
|
||||
import { getToolTipValue, PrecisionOption } from 'components/Graph/yAxisConfig';
|
||||
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
|
||||
import { themeColors } from 'constants/theme';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
/* eslint-disable @typescript-eslint/ban-ts-comment */
|
||||
// @ts-nocheck
|
||||
import { PrecisionOption } from 'components/Graph/types';
|
||||
import { getToolTipValue } from 'components/Graph/yAxisConfig';
|
||||
import { getToolTipValue, PrecisionOption } from 'components/Graph/yAxisConfig';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
|
||||
import { uPlotXAxisValuesFormat } from './constants';
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
export const TOOLBAR_VIEWS = {
|
||||
list: {
|
||||
name: 'list',
|
||||
label: 'List',
|
||||
show: true,
|
||||
key: 'list',
|
||||
},
|
||||
timeseries: {
|
||||
name: 'timeseries',
|
||||
label: 'Timeseries',
|
||||
disabled: false,
|
||||
show: true,
|
||||
key: 'timeseries',
|
||||
},
|
||||
trace: {
|
||||
name: 'trace',
|
||||
label: 'Trace',
|
||||
disabled: false,
|
||||
show: true,
|
||||
key: 'trace',
|
||||
},
|
||||
table: {
|
||||
name: 'table',
|
||||
label: 'Table',
|
||||
disabled: false,
|
||||
show: true,
|
||||
key: 'table',
|
||||
},
|
||||
clickhouse: {
|
||||
name: 'clickhouse',
|
||||
label: 'Clickhouse',
|
||||
disabled: false,
|
||||
show: false,
|
||||
key: 'clickhouse',
|
||||
},
|
||||
};
|
||||
@@ -1,6 +1,7 @@
|
||||
import './TracesExplorer.styles.scss';
|
||||
|
||||
import * as Sentry from '@sentry/react';
|
||||
import { Callout } from '@signozhq/callout';
|
||||
import { Card } from 'antd';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import cx from 'classnames';
|
||||
@@ -12,15 +13,12 @@ import { LOCALSTORAGE } from 'constants/localStorage';
|
||||
import { AVAILABLE_EXPORT_PANEL_TYPES } from 'constants/panelTypes';
|
||||
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import ExplorerOptionWrapper from 'container/ExplorerOptions/ExplorerOptionWrapper';
|
||||
import ExportPanel from 'container/ExportPanel';
|
||||
import { useOptionsMenu } from 'container/OptionsMenu';
|
||||
import LeftToolbarActions from 'container/QueryBuilder/components/ToolbarActions/LeftToolbarActions';
|
||||
import RightToolbarActions from 'container/QueryBuilder/components/ToolbarActions/RightToolbarActions';
|
||||
import TimeSeriesView from 'container/TimeSeriesView';
|
||||
import Toolbar from 'container/Toolbar/Toolbar';
|
||||
import {
|
||||
getExportQueryData,
|
||||
getQueryByPanelType,
|
||||
} from 'container/TracesExplorer/explorerUtils';
|
||||
import ListView from 'container/TracesExplorer/ListView';
|
||||
import { defaultSelectedColumns } from 'container/TracesExplorer/ListView/configs';
|
||||
import QuerySection from 'container/TracesExplorer/QuerySection';
|
||||
@@ -29,35 +27,36 @@ import TracesView from 'container/TracesExplorer/TracesView';
|
||||
import { useGetPanelTypesQueryParam } from 'hooks/queryBuilder/useGetPanelTypesQueryParam';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { useShareBuilderUrl } from 'hooks/queryBuilder/useShareBuilderUrl';
|
||||
import {
|
||||
ICurrentQueryData,
|
||||
useHandleExplorerTabChange,
|
||||
} from 'hooks/useHandleExplorerTabChange';
|
||||
import { useHandleExplorerTabChange } from 'hooks/useHandleExplorerTabChange';
|
||||
import { useSafeNavigate } from 'hooks/useSafeNavigate';
|
||||
import { isEmpty } from 'lodash-es';
|
||||
import { cloneDeep, isEmpty, set } from 'lodash-es';
|
||||
import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFallback';
|
||||
import { ExplorerViews } from 'pages/LogsExplorer/utils';
|
||||
import { TOOLBAR_VIEWS } from 'pages/TracesExplorer/constants';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useSearchParams } from 'react-router-dom-v5-compat';
|
||||
import { Warning } from 'types/api';
|
||||
import { Dashboard } from 'types/api/dashboard/getAll';
|
||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import {
|
||||
IBuilderTraceOperator,
|
||||
Query,
|
||||
} from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
import { generateExportToDashboardLink } from 'utils/dashboard/generateExportToDashboardLink';
|
||||
import {
|
||||
explorerViewToPanelType,
|
||||
getExplorerViewForPanelType,
|
||||
getExplorerViewFromUrl,
|
||||
} from 'utils/explorerUtils';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
function TracesExplorer(): JSX.Element {
|
||||
const {
|
||||
currentQuery,
|
||||
panelType,
|
||||
updateAllQueriesOperators,
|
||||
handleRunQuery,
|
||||
stagedQuery,
|
||||
handleSetConfig,
|
||||
updateQueriesData,
|
||||
} = useQueryBuilder();
|
||||
|
||||
const { options } = useOptionsMenu({
|
||||
@@ -70,7 +69,6 @@ function TracesExplorer(): JSX.Element {
|
||||
});
|
||||
|
||||
const [searchParams] = useSearchParams();
|
||||
const listQueryKeyRef = useRef<any>();
|
||||
|
||||
// Get panel type from URL
|
||||
const panelTypesFromUrl = useGetPanelTypesQueryParam(PANEL_TYPES.LIST);
|
||||
@@ -80,45 +78,103 @@ function TracesExplorer(): JSX.Element {
|
||||
getExplorerViewFromUrl(searchParams, panelTypesFromUrl),
|
||||
);
|
||||
|
||||
const [warning, setWarning] = useState<Warning | undefined>(undefined);
|
||||
const [isOpen, setOpen] = useState<boolean>(true);
|
||||
|
||||
const defaultQuery = useMemo(
|
||||
(): Query =>
|
||||
updateAllQueriesOperators(
|
||||
initialQueriesMap.traces,
|
||||
PANEL_TYPES.LIST,
|
||||
DataSource.TRACES,
|
||||
),
|
||||
[updateAllQueriesOperators],
|
||||
);
|
||||
|
||||
const { handleExplorerTabChange } = useHandleExplorerTabChange();
|
||||
const { safeNavigate } = useSafeNavigate();
|
||||
|
||||
// Update selected view when panel type from URL changes
|
||||
useEffect(() => {
|
||||
if (panelTypesFromUrl) {
|
||||
const newView = getExplorerViewForPanelType(panelTypesFromUrl);
|
||||
if (newView && newView !== selectedView) {
|
||||
setSelectedView(newView);
|
||||
}
|
||||
}
|
||||
}, [panelTypesFromUrl, selectedView]);
|
||||
|
||||
const [shouldReset, setShouldReset] = useState(false);
|
||||
|
||||
const [defaultQuery, setDefaultQuery] = useState<Query>(() =>
|
||||
updateAllQueriesOperators(
|
||||
initialQueriesMap.traces,
|
||||
PANEL_TYPES.LIST,
|
||||
DataSource.TRACES,
|
||||
),
|
||||
);
|
||||
|
||||
const handleChangeSelectedView = useCallback(
|
||||
(view: ExplorerViews, querySearchParameters?: ICurrentQueryData): void => {
|
||||
handleSetConfig(explorerViewToPanelType[view], DataSource.TRACES);
|
||||
(view: ExplorerViews): void => {
|
||||
if (selectedView === ExplorerViews.LIST) {
|
||||
handleSetConfig(PANEL_TYPES.LIST, DataSource.TRACES);
|
||||
}
|
||||
|
||||
if (
|
||||
(selectedView === ExplorerViews.TRACE ||
|
||||
selectedView === ExplorerViews.LIST) &&
|
||||
stagedQuery?.builder?.queryTraceOperator &&
|
||||
stagedQuery.builder.queryTraceOperator.length > 0
|
||||
) {
|
||||
// remove order by from trace operator
|
||||
set(stagedQuery, 'builder.queryTraceOperator[0].orderBy', []);
|
||||
}
|
||||
|
||||
if (view === ExplorerViews.LIST || view === ExplorerViews.TRACE) {
|
||||
// loop through all the queries and remove the group by
|
||||
|
||||
const updateQuery = updateQueriesData(
|
||||
currentQuery,
|
||||
'queryData',
|
||||
(item) => ({ ...item, groupBy: [], orderBy: [] }),
|
||||
);
|
||||
|
||||
setDefaultQuery(updateQuery);
|
||||
|
||||
setShouldReset(true);
|
||||
}
|
||||
|
||||
setSelectedView(view);
|
||||
|
||||
handleExplorerTabChange(
|
||||
explorerViewToPanelType[view],
|
||||
querySearchParameters,
|
||||
view === ExplorerViews.TIMESERIES ? PANEL_TYPES.TIME_SERIES : view,
|
||||
);
|
||||
},
|
||||
[handleExplorerTabChange, handleSetConfig],
|
||||
[
|
||||
selectedView,
|
||||
currentQuery,
|
||||
stagedQuery,
|
||||
handleExplorerTabChange,
|
||||
handleSetConfig,
|
||||
updateQueriesData,
|
||||
],
|
||||
);
|
||||
|
||||
const listQuery = useMemo(() => {
|
||||
if (!stagedQuery || stagedQuery.builder.queryData.length < 1) return null;
|
||||
|
||||
return stagedQuery.builder.queryData.find((item) => !item.disabled) || null;
|
||||
}, [stagedQuery]);
|
||||
|
||||
const exportDefaultQuery = useMemo(
|
||||
() =>
|
||||
getQueryByPanelType(
|
||||
stagedQuery || initialQueriesMap.traces,
|
||||
panelType || PANEL_TYPES.LIST,
|
||||
updateAllQueriesOperators(
|
||||
currentQuery || initialQueriesMap.traces,
|
||||
PANEL_TYPES.TIME_SERIES,
|
||||
DataSource.TRACES,
|
||||
),
|
||||
[stagedQuery, panelType],
|
||||
[currentQuery, updateAllQueriesOperators],
|
||||
);
|
||||
|
||||
const getUpdatedQueryForExport = useCallback((): Query => {
|
||||
const updatedQuery = cloneDeep(currentQuery);
|
||||
|
||||
set(
|
||||
updatedQuery,
|
||||
'builder.queryData[0].selectColumns',
|
||||
options.selectColumns,
|
||||
);
|
||||
|
||||
return updatedQuery;
|
||||
}, [currentQuery, options.selectColumns]);
|
||||
|
||||
const handleExport = useCallback(
|
||||
(dashboard: Dashboard | null, isNewDashboard?: boolean): void => {
|
||||
if (!dashboard || !panelType) return;
|
||||
@@ -129,11 +185,10 @@ function TracesExplorer(): JSX.Element {
|
||||
|
||||
const widgetId = v4();
|
||||
|
||||
const query = getExportQueryData(
|
||||
exportDefaultQuery,
|
||||
panelTypeParam,
|
||||
options,
|
||||
);
|
||||
const query =
|
||||
panelType === PANEL_TYPES.LIST
|
||||
? getUpdatedQueryForExport()
|
||||
: exportDefaultQuery;
|
||||
|
||||
logEvent('Traces Explorer: Add to dashboard successful', {
|
||||
panelType,
|
||||
@@ -150,11 +205,56 @@ function TracesExplorer(): JSX.Element {
|
||||
|
||||
safeNavigate(dashboardEditView);
|
||||
},
|
||||
[exportDefaultQuery, panelType, safeNavigate, options],
|
||||
[exportDefaultQuery, panelType, safeNavigate, getUpdatedQueryForExport],
|
||||
);
|
||||
|
||||
useShareBuilderUrl({ defaultValue: defaultQuery });
|
||||
useShareBuilderUrl({ defaultValue: defaultQuery, forceReset: shouldReset });
|
||||
|
||||
const hasMultipleQueries = useMemo(
|
||||
() => currentQuery?.builder?.queryData?.length > 1,
|
||||
[currentQuery],
|
||||
);
|
||||
|
||||
const traceOperator = useMemo((): IBuilderTraceOperator | undefined => {
|
||||
if (
|
||||
currentQuery.builder.queryTraceOperator &&
|
||||
currentQuery.builder.queryTraceOperator.length > 0
|
||||
) {
|
||||
return currentQuery.builder.queryTraceOperator[0];
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}, [currentQuery.builder.queryTraceOperator]);
|
||||
|
||||
const showTraceOperatorCallout = useMemo(
|
||||
() =>
|
||||
(selectedView === ExplorerViews.LIST ||
|
||||
selectedView === ExplorerViews.TRACE) &&
|
||||
hasMultipleQueries &&
|
||||
!traceOperator,
|
||||
[selectedView, hasMultipleQueries, traceOperator],
|
||||
);
|
||||
|
||||
const traceOperatorCalloutDescription = useMemo(() => {
|
||||
if (currentQuery.builder.queryData.length === 0) return '';
|
||||
const firstQuery = currentQuery.builder.queryData[0];
|
||||
return `Please use a Trace Operator to combine results of multiple span queries. Else you'd only see the results from query "${firstQuery.queryName}"`;
|
||||
}, [currentQuery]);
|
||||
|
||||
useEffect(() => {
|
||||
if (shouldReset) {
|
||||
setShouldReset(false);
|
||||
setDefaultQuery(
|
||||
updateAllQueriesOperators(
|
||||
initialQueriesMap.traces,
|
||||
PANEL_TYPES.LIST,
|
||||
DataSource.TRACES,
|
||||
),
|
||||
);
|
||||
}
|
||||
}, [shouldReset, updateAllQueriesOperators]);
|
||||
|
||||
const [isOpen, setOpen] = useState<boolean>(true);
|
||||
const logEventCalledRef = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -164,13 +264,51 @@ function TracesExplorer(): JSX.Element {
|
||||
}
|
||||
}, []);
|
||||
|
||||
const isFilterApplied = useMemo(() => {
|
||||
// if any of the non-disabled queries has filters applied, return true
|
||||
const result = stagedQuery?.builder?.queryData?.filter(
|
||||
(item) => !isEmpty(item.filters?.items) && !item.disabled,
|
||||
);
|
||||
return !!result?.length;
|
||||
}, [stagedQuery]);
|
||||
const toolbarViews = useMemo(
|
||||
() => ({
|
||||
list: {
|
||||
name: 'list',
|
||||
label: 'List',
|
||||
show: true,
|
||||
key: 'list',
|
||||
},
|
||||
timeseries: {
|
||||
name: 'timeseries',
|
||||
label: 'Timeseries',
|
||||
disabled: false,
|
||||
show: true,
|
||||
key: 'timeseries',
|
||||
},
|
||||
trace: {
|
||||
name: 'trace',
|
||||
label: 'Trace',
|
||||
disabled: false,
|
||||
show: true,
|
||||
key: 'trace',
|
||||
},
|
||||
table: {
|
||||
name: 'table',
|
||||
label: 'Table',
|
||||
disabled: false,
|
||||
show: true,
|
||||
key: 'table',
|
||||
},
|
||||
clickhouse: {
|
||||
name: 'clickhouse',
|
||||
label: 'Clickhouse',
|
||||
disabled: false,
|
||||
show: false,
|
||||
key: 'clickhouse',
|
||||
},
|
||||
}),
|
||||
[],
|
||||
);
|
||||
|
||||
const isFilterApplied = useMemo(() => !isEmpty(listQuery?.filters?.items), [
|
||||
listQuery,
|
||||
]);
|
||||
|
||||
const [warning, setWarning] = useState<Warning | undefined>(undefined);
|
||||
|
||||
return (
|
||||
<Sentry.ErrorBoundary fallback={<ErrorBoundaryFallback />}>
|
||||
@@ -197,7 +335,7 @@ function TracesExplorer(): JSX.Element {
|
||||
<LeftToolbarActions
|
||||
showFilter={isOpen}
|
||||
handleFilterVisibilityChange={(): void => setOpen(!isOpen)}
|
||||
items={TOOLBAR_VIEWS}
|
||||
items={toolbarViews}
|
||||
selectedView={selectedView}
|
||||
onChangeSelectedView={handleChangeSelectedView}
|
||||
/>
|
||||
@@ -209,7 +347,6 @@ function TracesExplorer(): JSX.Element {
|
||||
<RightToolbarActions
|
||||
onStageRunQuery={(): void => handleRunQuery()}
|
||||
isLoadingQueries={isLoadingQueries}
|
||||
listQueryKeyRef={listQueryKeyRef}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
@@ -221,13 +358,29 @@ function TracesExplorer(): JSX.Element {
|
||||
</ExplorerCard>
|
||||
|
||||
<div className="traces-explorer-views">
|
||||
<div className="traces-explorer-export-panel">
|
||||
<ExportPanel
|
||||
query={exportDefaultQuery}
|
||||
isLoading={false}
|
||||
onExport={handleExport}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{showTraceOperatorCallout && (
|
||||
<Callout
|
||||
type="info"
|
||||
size="small"
|
||||
showIcon
|
||||
description={traceOperatorCalloutDescription}
|
||||
/>
|
||||
)}
|
||||
|
||||
{selectedView === ExplorerViews.LIST && (
|
||||
<div className="trace-explorer-list-view">
|
||||
<ListView
|
||||
isFilterApplied={isFilterApplied}
|
||||
setWarning={setWarning}
|
||||
setIsLoadingQueries={setIsLoadingQueries}
|
||||
queryKeyRef={listQueryKeyRef}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
@@ -238,7 +391,6 @@ function TracesExplorer(): JSX.Element {
|
||||
isFilterApplied={isFilterApplied}
|
||||
setWarning={setWarning}
|
||||
setIsLoadingQueries={setIsLoadingQueries}
|
||||
queryKeyRef={listQueryKeyRef}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
@@ -250,7 +402,6 @@ function TracesExplorer(): JSX.Element {
|
||||
isFilterApplied={isFilterApplied}
|
||||
setWarning={setWarning}
|
||||
setIsLoadingQueries={setIsLoadingQueries}
|
||||
queryKeyRef={listQueryKeyRef}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
@@ -260,7 +411,6 @@ function TracesExplorer(): JSX.Element {
|
||||
<TableView
|
||||
setWarning={setWarning}
|
||||
setIsLoadingQueries={setIsLoadingQueries}
|
||||
queryKeyRef={listQueryKeyRef}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
@@ -271,7 +421,6 @@ function TracesExplorer(): JSX.Element {
|
||||
query={exportDefaultQuery}
|
||||
sourcepage={DataSource.TRACES}
|
||||
onExport={handleExport}
|
||||
handleChangeSelectedView={handleChangeSelectedView}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import getLocalStorageApi from 'api/browser/localstorage/get';
|
||||
import setLocalStorageApi from 'api/browser/localstorage/set';
|
||||
import listOrgPreferences from 'api/v1/org/preferences/list';
|
||||
import get from 'api/v1/user/me/get';
|
||||
import listUserPreferences from 'api/v1/user/preferences/list';
|
||||
@@ -78,7 +77,6 @@ export function AppProvider({ children }: PropsWithChildren): JSX.Element {
|
||||
|
||||
useEffect(() => {
|
||||
if (!isFetchingUser && userData && userData.data) {
|
||||
setLocalStorageApi(LOCALSTORAGE.LOGGED_IN_USER_EMAIL, userData.data.email);
|
||||
setUser((prev) => ({
|
||||
...prev,
|
||||
...userData.data,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { PrecisionOption } from 'components/Graph/types';
|
||||
import { PrecisionOption } from 'components/Graph/yAxisConfig';
|
||||
import { PANEL_GROUP_TYPES, PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { ThresholdProps } from 'container/NewWidget/RightContainer/Threshold/types';
|
||||
import { timePreferenceType } from 'container/NewWidget/RightContainer/timeItems';
|
||||
|
||||
@@ -35,7 +35,7 @@ export interface TagFilterItem {
|
||||
id: string;
|
||||
key?: BaseAutocompleteData;
|
||||
op: string;
|
||||
value: (string | number | boolean)[] | string | number | boolean;
|
||||
value: string[] | string | number | boolean;
|
||||
}
|
||||
|
||||
export interface TagFilter {
|
||||
|
||||
@@ -11,7 +11,6 @@ export interface GettableAuthDomain {
|
||||
orgId: string;
|
||||
ssoEnabled: boolean;
|
||||
ssoType: string;
|
||||
authNProviderInfo: AuthNProviderInfo;
|
||||
samlConfig?: SAMLConfig;
|
||||
googleAuthConfig?: GoogleAuthConfig;
|
||||
oidcConfig?: OIDCConfig;
|
||||
@@ -43,7 +42,3 @@ export interface OIDCConfig {
|
||||
export interface ClaimMapping {
|
||||
email: string;
|
||||
}
|
||||
|
||||
export interface AuthNProviderInfo {
|
||||
relayStatePath: string;
|
||||
}
|
||||
|
||||
28
frontend/src/utils/queryTransformers.ts
Normal file
28
frontend/src/utils/queryTransformers.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { cloneDeep } from 'lodash-es';
|
||||
import { IBuilderQuery, Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
/**
|
||||
* Transforms a query by modifying specific fields in the builder queries
|
||||
* @param query - The original query object
|
||||
* @param fieldOverrides - Partial object containing fields to override in each builder query
|
||||
* @returns A new query object with the modified fields
|
||||
*/
|
||||
export const transformBuilderQueryFields = (
|
||||
query: Query,
|
||||
fieldOverrides: Partial<IBuilderQuery>,
|
||||
): Query => {
|
||||
// Create a deep copy of the query
|
||||
const transformedQuery: Query = cloneDeep(query);
|
||||
|
||||
// Update the specified fields for each query in the builder
|
||||
if (transformedQuery.builder?.queryData) {
|
||||
transformedQuery.builder.queryData = transformedQuery.builder.queryData.map(
|
||||
(queryItem) => ({
|
||||
...queryItem,
|
||||
...fieldOverrides,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
return transformedQuery;
|
||||
};
|
||||
@@ -11,8 +11,3 @@ export function unquote(str: string): string {
|
||||
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
export function isQuoted(str: string): boolean {
|
||||
const trimmed = str.trim();
|
||||
return trimmed.length >= 2 && /^(["'`])(.*)\1$/.test(trimmed);
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ const plugins = [
|
||||
PYLON_APP_ID: process.env.PYLON_APP_ID,
|
||||
APPCUES_APP_ID: process.env.APPCUES_APP_ID,
|
||||
POSTHOG_KEY: process.env.POSTHOG_KEY,
|
||||
USERPILOT_KEY: process.env.USERPILOT_KEY,
|
||||
SENTRY_AUTH_TOKEN: process.env.SENTRY_AUTH_TOKEN,
|
||||
SENTRY_ORG: process.env.SENTRY_ORG,
|
||||
SENTRY_PROJECT_ID: process.env.SENTRY_PROJECT_ID,
|
||||
@@ -39,9 +40,9 @@ const plugins = [
|
||||
FRONTEND_API_ENDPOINT: process.env.FRONTEND_API_ENDPOINT,
|
||||
WEBSOCKET_API_ENDPOINT: process.env.WEBSOCKET_API_ENDPOINT,
|
||||
PYLON_APP_ID: process.env.PYLON_APP_ID,
|
||||
PYLON_IDENTITY_SECRET: process.env.PYLON_IDENTITY_SECRET,
|
||||
APPCUES_APP_ID: process.env.APPCUES_APP_ID,
|
||||
POSTHOG_KEY: process.env.POSTHOG_KEY,
|
||||
USERPILOT_KEY: process.env.USERPILOT_KEY,
|
||||
SENTRY_AUTH_TOKEN: process.env.SENTRY_AUTH_TOKEN,
|
||||
SENTRY_ORG: process.env.SENTRY_ORG,
|
||||
SENTRY_PROJECT_ID: process.env.SENTRY_PROJECT_ID,
|
||||
|
||||
@@ -28,6 +28,7 @@ const plugins = [
|
||||
PYLON_APP_ID: process.env.PYLON_APP_ID,
|
||||
APPCUES_APP_ID: process.env.APPCUES_APP_ID,
|
||||
POSTHOG_KEY: process.env.POSTHOG_KEY,
|
||||
USERPILOT_KEY: process.env.USERPILOT_KEY,
|
||||
SENTRY_AUTH_TOKEN: process.env.SENTRY_AUTH_TOKEN,
|
||||
SENTRY_ORG: process.env.SENTRY_ORG,
|
||||
SENTRY_PROJECT_ID: process.env.SENTRY_PROJECT_ID,
|
||||
@@ -49,9 +50,9 @@ const plugins = [
|
||||
FRONTEND_API_ENDPOINT: process.env.FRONTEND_API_ENDPOINT,
|
||||
WEBSOCKET_API_ENDPOINT: process.env.WEBSOCKET_API_ENDPOINT,
|
||||
PYLON_APP_ID: process.env.PYLON_APP_ID,
|
||||
PYLON_IDENTITY_SECRET: process.env.PYLON_IDENTITY_SECRET,
|
||||
APPCUES_APP_ID: process.env.APPCUES_APP_ID,
|
||||
POSTHOG_KEY: process.env.POSTHOG_KEY,
|
||||
USERPILOT_KEY: process.env.USERPILOT_KEY,
|
||||
SENTRY_AUTH_TOKEN: process.env.SENTRY_AUTH_TOKEN,
|
||||
SENTRY_ORG: process.env.SENTRY_ORG,
|
||||
SENTRY_PROJECT_ID: process.env.SENTRY_PROJECT_ID,
|
||||
|
||||
@@ -3385,6 +3385,30 @@
|
||||
strict-event-emitter "^0.2.4"
|
||||
web-encoding "^1.1.5"
|
||||
|
||||
"@ndhoule/each@^2.0.1":
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@ndhoule/each/-/each-2.0.1.tgz#bbed372a603e0713a3193c706a73ddebc5b426a9"
|
||||
integrity sha512-wHuJw6x+rF6Q9Skgra++KccjBozCr9ymtna0FhxmV/8xT/hZ2ExGYR8SV8prg8x4AH/7mzDYErNGIVHuzHeybw==
|
||||
dependencies:
|
||||
"@ndhoule/keys" "^2.0.0"
|
||||
|
||||
"@ndhoule/includes@^2.0.1":
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@ndhoule/includes/-/includes-2.0.1.tgz#051ff5eb042c8fa17e7158f0a8a70172e1affaa5"
|
||||
integrity sha512-Q8zN6f3yIhxgBwZ5ldLozHqJlc/fRQ5+hFFsPMFeC9SJvz0nq8vG9hoRXL1c1iaNFQd7yAZIy2igQpERoFqxqg==
|
||||
dependencies:
|
||||
"@ndhoule/each" "^2.0.1"
|
||||
|
||||
"@ndhoule/keys@^2.0.0":
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@ndhoule/keys/-/keys-2.0.0.tgz#3d64ae677c65a261747bf3a457c62eb292a4e0ce"
|
||||
integrity sha512-vtCqKBC1Av6dsBA8xpAO+cgk051nfaI+PnmTZep2Px0vYrDvpUmLxv7z40COlWH5yCpu3gzNhepk+02yiQiZNw==
|
||||
|
||||
"@ndhoule/pick@^2.0.0":
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@ndhoule/pick/-/pick-2.0.0.tgz#e1eb1a6ca3243eef56daa095c3a1612c74a52156"
|
||||
integrity sha512-xkYtpf1pRd8egwvl5tJcdGu+GBd6ZZH3S/zoIQ9txEI+pHF9oTIlxMC9G4CB3sRugAeLgu8qYJGl3tnxWq74Qw==
|
||||
|
||||
"@nodelib/fs.scandir@2.1.5":
|
||||
version "2.1.5"
|
||||
resolved "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz"
|
||||
@@ -3505,25 +3529,18 @@
|
||||
resolved "https://registry.npmjs.org/@petamoriken/float16/-/float16-3.8.0.tgz"
|
||||
integrity sha512-AhVAm6SQ+zgxIiOzwVdUcDmKlu/qU39FiYD2UD6kQQaVenrn0dGZewIghWAENGQsvC+1avLCuT+T2/3Gsp/W3w==
|
||||
|
||||
"@playwright/test@1.55.1":
|
||||
version "1.55.1"
|
||||
resolved "https://registry.yarnpkg.com/@playwright/test/-/test-1.55.1.tgz#80f775d5f948cd3ef550fcc45ef99986d3ffb36c"
|
||||
integrity sha512-IVAh/nOJaw6W9g+RJVlIQJ6gSiER+ae6mKQ5CX1bERzQgbC1VSeBlwdvczT7pxb0GWiyrxH4TGKbMfDb4Sq/ig==
|
||||
"@playwright/test@1.54.1":
|
||||
version "1.54.1"
|
||||
resolved "https://registry.yarnpkg.com/@playwright/test/-/test-1.54.1.tgz#a76333e5c2cba5f12f96a6da978bba3ab073c7e6"
|
||||
integrity sha512-FS8hQ12acieG2dYSksmLOF7BNxnVf2afRJdCuM1eMSxj6QTSE6G4InGF7oApGgDb65MX7AwMVlIkpru0yZA4Xw==
|
||||
dependencies:
|
||||
playwright "1.55.1"
|
||||
playwright "1.54.1"
|
||||
|
||||
"@polka/url@^1.0.0-next.20":
|
||||
version "1.0.0-next.21"
|
||||
resolved "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.21.tgz"
|
||||
integrity sha512-a5Sab1C4/icpTZVzZc5Ghpz88yQtGOyNqYXcZgOssB2uuAr+wF/MvN6bgtW32q7HHrvBki+BsZ0OuNv6EV3K9g==
|
||||
|
||||
"@posthog/core@1.6.0":
|
||||
version "1.6.0"
|
||||
resolved "https://registry.yarnpkg.com/@posthog/core/-/core-1.6.0.tgz#a5b63a30950a8dfe87d4bf335ab24005c7ce1278"
|
||||
integrity sha512-Tbh8UACwbb7jFdDC7wwXHtfNzO+4wKh3VbyMHmp2UBe6w1jliJixexTJNfkqdGZm+ht3M10mcKvGGPnoZ2zLBg==
|
||||
dependencies:
|
||||
cross-spawn "^7.0.6"
|
||||
|
||||
"@radix-ui/primitive@1.0.1":
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/primitive/-/primitive-1.0.1.tgz#e46f9958b35d10e9f6dc71c497305c22e3e55dbd"
|
||||
@@ -4668,11 +4685,6 @@
|
||||
tapable "^2.0.0"
|
||||
webpack "^5.1.0"
|
||||
|
||||
"@types/crypto-js@4.2.2":
|
||||
version "4.2.2"
|
||||
resolved "https://registry.yarnpkg.com/@types/crypto-js/-/crypto-js-4.2.2.tgz#771c4a768d94eb5922cc202a3009558204df0cea"
|
||||
integrity sha512-sDOLlVbHhXpAUAL0YHDUUwDZf3iN4Bwi4W6a0W0b+QcAezUbRtH4FVb+9J4h+XFPW7l/gQ9F8qC7P+Ec4k8QVQ==
|
||||
|
||||
"@types/d3-array@3.0.3":
|
||||
version "3.0.3"
|
||||
resolved "https://registry.yarnpkg.com/@types/d3-array/-/d3-array-3.0.3.tgz#87d990bf504d14ad6b16766979d04e943c046dac"
|
||||
@@ -7506,6 +7518,11 @@ compare-func@^2.0.0:
|
||||
array-ify "^1.0.0"
|
||||
dot-prop "^5.1.0"
|
||||
|
||||
component-indexof@0.0.3:
|
||||
version "0.0.3"
|
||||
resolved "https://registry.yarnpkg.com/component-indexof/-/component-indexof-0.0.3.tgz#11d091312239eb8f32c8f25ae9cb002ffe8d3c24"
|
||||
integrity sha512-puDQKvx/64HZXb4hBwIcvQLaLgux8o1CbWl39s41hrIIZDl1lJiD5jc22gj3RBeGK0ovxALDYpIbyjqDUUl0rw==
|
||||
|
||||
compressible@~2.0.16:
|
||||
version "2.0.18"
|
||||
resolved "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz"
|
||||
@@ -7716,7 +7733,7 @@ cross-fetch@3.1.5:
|
||||
dependencies:
|
||||
node-fetch "2.6.7"
|
||||
|
||||
cross-spawn@7.0.5, cross-spawn@^6.0.5, cross-spawn@^7.0.1, cross-spawn@^7.0.2, cross-spawn@^7.0.3, cross-spawn@^7.0.6:
|
||||
cross-spawn@7.0.5, cross-spawn@^6.0.5, cross-spawn@^7.0.1, cross-spawn@^7.0.2, cross-spawn@^7.0.3:
|
||||
version "7.0.5"
|
||||
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.5.tgz#910aac880ff5243da96b728bc6521a5f6c2f2f82"
|
||||
integrity sha512-ZVJrKKYunU38/76t0RMOulHOnUcbU9GbpWKAOZ0mhjr7CX6FVrH+4FrAapSOekrgFQ3f/8gwMEuIft0aKq6Hug==
|
||||
@@ -7725,11 +7742,6 @@ cross-spawn@7.0.5, cross-spawn@^6.0.5, cross-spawn@^7.0.1, cross-spawn@^7.0.2, c
|
||||
shebang-command "^2.0.0"
|
||||
which "^2.0.1"
|
||||
|
||||
crypto-js@4.2.0:
|
||||
version "4.2.0"
|
||||
resolved "https://registry.yarnpkg.com/crypto-js/-/crypto-js-4.2.0.tgz#4d931639ecdfd12ff80e8186dba6af2c2e856631"
|
||||
integrity sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==
|
||||
|
||||
css-box-model@^1.2.0:
|
||||
version "1.2.1"
|
||||
resolved "https://registry.yarnpkg.com/css-box-model/-/css-box-model-1.2.1.tgz#59951d3b81fd6b2074a62d49444415b0d2b4d7c1"
|
||||
@@ -11229,6 +11241,11 @@ is-wsl@^3.1.0:
|
||||
dependencies:
|
||||
is-inside-container "^1.0.0"
|
||||
|
||||
is@^3.1.0:
|
||||
version "3.3.0"
|
||||
resolved "https://registry.yarnpkg.com/is/-/is-3.3.0.tgz#61cff6dd3c4193db94a3d62582072b44e5645d79"
|
||||
integrity sha512-nW24QBoPcFGGHJGUwnfpI7Yc5CdqWNdsyHQszVE/z2pKHXzh7FZ5GWhJqSyaQ9wMkQnsTx+kAI8bHlCX4tKdbg==
|
||||
|
||||
isarray@0.0.1:
|
||||
version "0.0.1"
|
||||
resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf"
|
||||
@@ -13599,6 +13616,11 @@ nwsapi@^2.2.0:
|
||||
resolved "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.4.tgz"
|
||||
integrity sha512-NHj4rzRo0tQdijE9ZqAx6kYDcoRwYwSYzCA8MY3JzfxlrvEU0jhnhJT9BhqhJs7I/dKcrDm6TyulaRqZPIhN5g==
|
||||
|
||||
obj-case@^0.2.0:
|
||||
version "0.2.1"
|
||||
resolved "https://registry.yarnpkg.com/obj-case/-/obj-case-0.2.1.tgz#13a554d04e5ca32dfd9d566451fd2b0e11007f1a"
|
||||
integrity sha512-PquYBBTy+Y6Ob/O2574XHhDtHJlV1cJHMCgW+rDRc9J5hhmRelJB3k5dTK/3cVmFVtzvAKuENeuLpoyTzMzkOg==
|
||||
|
||||
object-assign@^4.0.1, object-assign@^4.1.0, object-assign@^4.1.1:
|
||||
version "4.1.1"
|
||||
resolved "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz"
|
||||
@@ -14175,17 +14197,17 @@ pkg-dir@^7.0.0:
|
||||
dependencies:
|
||||
find-up "^6.3.0"
|
||||
|
||||
playwright-core@1.55.1:
|
||||
version "1.55.1"
|
||||
resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.55.1.tgz#5d3bb1846bc4289d364ea1a9dcb33f14545802e9"
|
||||
integrity sha512-Z6Mh9mkwX+zxSlHqdr5AOcJnfp+xUWLCt9uKV18fhzA8eyxUd8NUWzAjxUh55RZKSYwDGX0cfaySdhZJGMoJ+w==
|
||||
playwright-core@1.54.1:
|
||||
version "1.54.1"
|
||||
resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.54.1.tgz#d32edcce048c9d83ceac31e294a7b60ef586960b"
|
||||
integrity sha512-Nbjs2zjj0htNhzgiy5wu+3w09YetDx5pkrpI/kZotDlDUaYk0HVA5xrBVPdow4SAUIlhgKcJeJg4GRKW6xHusA==
|
||||
|
||||
playwright@1.55.1:
|
||||
version "1.55.1"
|
||||
resolved "https://registry.yarnpkg.com/playwright/-/playwright-1.55.1.tgz#8a9954e9e61ed1ab479212af9be336888f8b3f0e"
|
||||
integrity sha512-cJW4Xd/G3v5ovXtJJ52MAOclqeac9S/aGGgRzLabuF8TnIb6xHvMzKIa6JmrRzUkeXJgfL1MhukP0NK6l39h3A==
|
||||
playwright@1.54.1:
|
||||
version "1.54.1"
|
||||
resolved "https://registry.yarnpkg.com/playwright/-/playwright-1.54.1.tgz#128d66a8d5182b5330e6440be3a72ca313362788"
|
||||
integrity sha512-peWpSwIBmSLi6aW2auvrUtf2DqY16YYcCMO8rTVx486jKmDTJg7UAhyrraP98GB8BoPURZP8+nxO7TSd4cPr5g==
|
||||
dependencies:
|
||||
playwright-core "1.55.1"
|
||||
playwright-core "1.54.1"
|
||||
optionalDependencies:
|
||||
fsevents "2.3.2"
|
||||
|
||||
@@ -14508,16 +14530,15 @@ postcss@^8.4.35:
|
||||
picocolors "^1.1.1"
|
||||
source-map-js "^1.2.1"
|
||||
|
||||
posthog-js@1.298.0:
|
||||
version "1.298.0"
|
||||
resolved "https://registry.yarnpkg.com/posthog-js/-/posthog-js-1.298.0.tgz#54730988a753220aef54af0c4e69960633917450"
|
||||
integrity sha512-Zwzsf7TO8qJ6DFLuUlQSsT/5OIOcxSBZlKOSk3satkEnwKdmnBXUuxgVXRHrvq1kj7OB2PVAPgZiQ8iHHj9DRA==
|
||||
posthog-js@1.215.5:
|
||||
version "1.215.5"
|
||||
resolved "https://registry.yarnpkg.com/posthog-js/-/posthog-js-1.215.5.tgz#0512cfdb919da960b809c5f686ca147f9c2922ba"
|
||||
integrity sha512-42lPur+xvkp51pHz2FQ7Y+KHdZ4eQSNIhUO03EECvc2UsmnM0FiVTrF1bcLwHZMaWfR26gOeuOAAjTUV9tinJg==
|
||||
dependencies:
|
||||
"@posthog/core" "1.6.0"
|
||||
core-js "^3.38.1"
|
||||
fflate "^0.4.8"
|
||||
preact "^10.19.3"
|
||||
web-vitals "^4.2.4"
|
||||
web-vitals "^4.2.0"
|
||||
|
||||
preact@^10.19.3:
|
||||
version "10.22.0"
|
||||
@@ -17886,6 +17907,17 @@ use-sync-external-store@^1.0.0:
|
||||
resolved "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz"
|
||||
integrity sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==
|
||||
|
||||
userpilot@1.3.9:
|
||||
version "1.3.9"
|
||||
resolved "https://registry.yarnpkg.com/userpilot/-/userpilot-1.3.9.tgz#6374083f3e84cbf1fc825133588b5b499054271b"
|
||||
integrity sha512-V0QIuIlAJPB8s3j+qtv7BW7NKSXthlZWuowIu+IZOMGLgUbqQTaSW5m1Ct4wJviPKUNOi8kbhCXN4c4b3zcJzg==
|
||||
dependencies:
|
||||
"@ndhoule/includes" "^2.0.1"
|
||||
"@ndhoule/pick" "^2.0.0"
|
||||
component-indexof "0.0.3"
|
||||
is "^3.1.0"
|
||||
obj-case "^0.2.0"
|
||||
|
||||
util-deprecate@^1.0.1, util-deprecate@^1.0.2, util-deprecate@~1.0.1:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz"
|
||||
@@ -18092,7 +18124,7 @@ web-vitals@^0.2.4:
|
||||
resolved "https://registry.npmjs.org/web-vitals/-/web-vitals-0.2.4.tgz"
|
||||
integrity sha512-6BjspCO9VriYy12z356nL6JBS0GYeEcA457YyRzD+dD6XYCQ75NKhcOHUMHentOE7OcVCIXXDvOm0jKFfQG2Gg==
|
||||
|
||||
web-vitals@^4.2.4:
|
||||
web-vitals@^4.2.0:
|
||||
version "4.2.4"
|
||||
resolved "https://registry.yarnpkg.com/web-vitals/-/web-vitals-4.2.4.tgz#1d20bc8590a37769bd0902b289550936069184b7"
|
||||
integrity sha512-r4DIlprAGwJ7YM11VZp4R884m0Vmgr6EAKe3P+kO0PPj3Unqyvv59rczf6UiGcb9Z8QxZVcqKNwv/g0WNdWwsw==
|
||||
|
||||
1
go.mod
1
go.mod
@@ -37,6 +37,7 @@ require (
|
||||
github.com/openfga/api/proto v0.0.0-20250909172242-b4b2a12f5c67
|
||||
github.com/openfga/language/pkg/go v0.2.0-beta.2.0.20250428093642-7aeebe78bbfe
|
||||
github.com/opentracing/opentracing-go v1.2.0
|
||||
github.com/patrickmn/go-cache v2.1.0+incompatible
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/prometheus/alertmanager v0.28.1
|
||||
github.com/prometheus/client_golang v1.23.2
|
||||
|
||||
2
go.sum
2
go.sum
@@ -786,6 +786,8 @@ github.com/ovh/go-ovh v1.7.0/go.mod h1:cTVDnl94z4tl8pP1uZ/8jlVxntjSIf09bNcQ5TJSC
|
||||
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
|
||||
github.com/pascaldekloe/goe v0.1.0 h1:cBOtyMzM9HTpWjXfbbunk26uA6nG3a8n06Wieeh0MwY=
|
||||
github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
|
||||
github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
|
||||
github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
|
||||
github.com/paulmach/orb v0.11.1 h1:3koVegMC4X/WeiXYz9iswopaTwMem53NzTJuTF20JzU=
|
||||
github.com/paulmach/orb v0.11.1/go.mod h1:5mULz1xQfs3bmQm63QEJA6lNGujuRafwA5S/EnuLaLU=
|
||||
github.com/paulmach/protoscan v0.2.1/go.mod h1:SpcSwydNLrxUGSDvXvO0P7g7AuhJ7lcKfDlhJCDw2gY=
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user