Compare commits

..

1 Commits

Author SHA1 Message Date
Abhi Kumar
17dec71695 chore: updated styles for dashboard creation modal 2025-11-20 15:40:46 +05:30
273 changed files with 3866 additions and 20599 deletions

View File

@@ -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
View File

@@ -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

View File

@@ -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:

View File

@@ -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:

View File

@@ -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

View File

@@ -18,7 +18,6 @@ jobs:
- passwordauthn
- callbackauthn
- cloudintegrations
- dashboard
- querier
- ttl
sqlstore-provider:

View File

@@ -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

View File

@@ -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.

View File

@@ -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.1
image: signoz/signoz:v0.102.0
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

View File

@@ -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.1
image: signoz/signoz:v0.102.0
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

View File

@@ -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.1}
image: signoz/signoz:${VERSION:-v0.102.0}
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

View File

@@ -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.1}
image: signoz/signoz:${VERSION:-v0.102.0}
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

View File

@@ -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)

View File

@@ -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 {

View File

@@ -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(

View File

@@ -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()

View File

@@ -30,8 +30,6 @@ func (formatter Formatter) DataTypeOf(dataType string) sqlschema.DataType {
return sqlschema.DataTypeBoolean
case "VARCHAR", "CHARACTER VARYING", "CHARACTER":
return sqlschema.DataTypeText
case "BYTEA":
return sqlschema.DataTypeBytea
}
return formatter.Formatter.DataTypeOf(dataType)

View File

@@ -1,5 +1,5 @@
module.exports = {
ignorePatterns: ['src/parser/*.ts', 'scripts/update-registry.js'],
ignorePatterns: ['src/parser/*.ts'],
env: {
browser: true,
es2021: true,

View File

@@ -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"

View File

@@ -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",

Binary file not shown.

After

Width:  |  Height:  |  Size: 98 KiB

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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);
}

View File

@@ -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>

View File

@@ -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,

View File

@@ -1,30 +1,30 @@
interface ConfigureIconProps {
width?: number;
height?: number;
color?: string;
fill?: string;
}
function ConfigureIcon({
width,
height,
color,
fill,
}: ConfigureIconProps): JSX.Element {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width={width}
height={height}
fill="none"
fill={fill}
>
<path
stroke={color}
stroke="#C0C1C3"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="1.333"
d="M9.71 4.745a.576.576 0 000 .806l.922.922a.576.576 0 00.806 0l2.171-2.171a3.455 3.455 0 01-4.572 4.572l-3.98 3.98a1.222 1.222 0 11-1.727-1.728l3.98-3.98a3.455 3.455 0 014.572-4.572L9.717 4.739l-.006.006z"
/>
<path
stroke={color}
stroke="#C0C1C3"
strokeLinecap="round"
strokeWidth="1.333"
d="M4 7L2.527 5.566a1.333 1.333 0 01-.013-1.898l.81-.81a1.333 1.333 0 011.991.119L5.333 3m5.417 7.988l1.179 1.178m0 0l-.138.138a.833.833 0 00.387 1.397v0a.833.833 0 00.792-.219l.446-.446a.833.833 0 00.176-.917v0a.833.833 0 00-1.355-.261l-.308.308z"
@@ -36,6 +36,6 @@ function ConfigureIcon({
ConfigureIcon.defaultProps = {
width: 16,
height: 16,
color: 'currentColor',
fill: 'none',
};
export default ConfigureIcon;

View File

@@ -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';

View File

@@ -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),

View File

@@ -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;

View File

@@ -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;
};

View File

@@ -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({

View File

@@ -37,6 +37,7 @@
border-radius: 2px 0px 0px 2px;
border: 1px solid var(--bg-slate-400);
background: var(--bg-ink-300);
border-right: none;
border-left: none;
@@ -44,12 +45,6 @@
border-bottom-right-radius: 0px;
border-top-left-radius: 0px;
border-bottom-left-radius: 0px;
font-size: 12px !important;
line-height: 27px;
&::placeholder {
color: var(--bg-vanilla-400) !important;
font-size: 12px !important;
}
}
.close-btn {

View File

@@ -6,7 +6,6 @@ import { useCopyToClipboard } from 'react-use';
function CopyClipboardHOC({
entityKey,
textToCopy,
tooltipText = 'Copy to clipboard',
children,
}: CopyClipboardHOCProps): JSX.Element {
const [value, setCopy] = useCopyToClipboard();
@@ -32,7 +31,7 @@ function CopyClipboardHOC({
<span onClick={onClick} role="presentation" tabIndex={-1}>
<Popover
placement="top"
content={<span style={{ fontSize: '0.9rem' }}>{tooltipText}</span>}
content={<span style={{ fontSize: '0.9rem' }}>Copy to clipboard</span>}
>
{children}
</Popover>
@@ -43,11 +42,7 @@ function CopyClipboardHOC({
interface CopyClipboardHOCProps {
entityKey: string | undefined;
textToCopy: string;
tooltipText?: string;
children: ReactNode;
}
export default CopyClipboardHOC;
CopyClipboardHOC.defaultProps = {
tooltipText: 'Copy to clipboard',
};

View File

@@ -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>
);
}

View File

@@ -251,10 +251,6 @@
.ant-input-group-addon {
border-top-left-radius: 0px !important;
border-top-right-radius: 0px !important;
background: var(--bg-ink-300);
color: var(--bg-vanilla-400);
font-size: 12px;
font-weight: 300;
}
.ant-input {
@@ -300,10 +296,6 @@
}
}
.qb-trace-operator-button-container {
display: flex;
align-items: center;
gap: 8px;
&-text {
display: flex;
align-items: center;

View File

@@ -179,7 +179,6 @@ export const QueryBuilderV2 = memo(function QueryBuilderV2({
isListViewPanel={isListViewPanel}
onSignalSourceChange={onSignalSourceChange || ((): void => {})}
signalSourceChangeEnabled={signalSourceChangeEnabled}
queriesCount={1}
/>
) : (
currentQuery.builder.queryData.map((query, index) => (
@@ -201,7 +200,6 @@ export const QueryBuilderV2 = memo(function QueryBuilderV2({
signalSource={query.source as 'meter' | ''}
onSignalSourceChange={onSignalSourceChange || ((): void => {})}
signalSourceChangeEnabled={signalSourceChangeEnabled}
queriesCount={currentQuery.builder.queryData.length}
/>
))
)}

View File

@@ -98,13 +98,6 @@
border-radius: 2px;
border: 1.005px solid var(--Slate-400, #1d212d);
background: var(--Ink-300, #16181d);
color: var(--bg-vanilla-400);
font-family: 'Geist Mono';
font-size: 13px;
font-style: normal;
font-weight: 400;
line-height: 20px;
letter-spacing: -0.07px;
}
.input-with-label {

View File

@@ -6,15 +6,6 @@
gap: 8px;
width: 100%;
.ant-select-selection-search-input {
font-size: 12px !important;
line-height: 27px;
&::placeholder {
color: var(--bg-vanilla-400) !important;
font-size: 12px !important;
}
}
.source-selector {
width: 120px;
}
@@ -31,11 +22,6 @@
font-weight: 400;
line-height: 20px; /* 142.857% */
min-height: 36px;
.ant-select-selection-placeholder {
color: var(--bg-vanilla-400) !important;
font-size: 12px !important;
}
}
.ant-select-dropdown {

View File

@@ -236,10 +236,6 @@
background: var(--bg-ink-100) !important;
opacity: 0.5 !important;
}
.cm-activeLine > span {
font-size: 12px !important;
}
}
}
@@ -275,9 +271,6 @@
box-sizing: border-box;
position: relative;
.cm-placeholder {
font-size: 12px !important;
}
}
}

View File

@@ -20,8 +20,6 @@
border-radius: 2px;
flex: 1;
min-width: 0;
font-size: 12px;
color: var(--bg-vanilla-400) !important;
&.error {
.cm-editor {
@@ -233,9 +231,6 @@
.query-aggregation-interval-input {
input {
max-width: 120px;
&::placeholder {
color: var(--bg-vanilla-400);
}
}
}
}

View File

@@ -1,7 +0,0 @@
.add-trace-operator-button,
.add-new-query-button,
.add-formula-button {
border: 1px solid var(--bg-slate-400);
background: var(--bg-ink-300);
box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.1);
}

View File

@@ -1,75 +1,7 @@
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,
@@ -90,7 +22,8 @@ export default function QueryFooter({
<div className="qb-add-new-query">
<Tooltip title={<div style={{ textAlign: 'center' }}>Add New Query</div>}>
<Button
className="add-new-query-button periscope-btn "
className="add-new-query-button periscope-btn secondary"
type="text"
icon={<Plus size={16} />}
onClick={addNewBuilderQuery}
/>
@@ -116,7 +49,7 @@ export default function QueryFooter({
}
>
<Button
className="add-formula-button periscope-btn "
className="add-formula-button periscope-btn secondary"
icon={<Sigma size={16} />}
onClick={addNewFormula}
>
@@ -126,7 +59,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 secondary"
icon={<DraftingCompass size={16} />}
onClick={(): void => addTraceOperator?.()}
>
<div className="qb-trace-operator-button-container-text">
Add Trace Matching
<BetaTag />
</div>
</Button>
</Tooltip>
</div>
)}
</div>
</div>

View File

@@ -12,7 +12,6 @@ import {
startCompletion,
} from '@codemirror/autocomplete';
import { javascript } from '@codemirror/lang-javascript';
import * as Sentry from '@sentry/react';
import { Color } from '@signozhq/design-tokens';
import { copilot } from '@uiw/codemirror-theme-copilot';
import { githubLight } from '@uiw/codemirror-theme-github';
@@ -80,16 +79,6 @@ const stopEventsExtension = EditorView.domEventHandlers({
},
});
interface QuerySearchProps {
placeholder?: string;
onChange: (value: string) => void;
queryData: IBuilderQuery;
dataSource: DataSource;
signalSource?: string;
hardcodedAttributeKeys?: QueryKeyDataSuggestionsProps[];
onRun?: (query: string) => void;
}
function QuerySearch({
placeholder,
onChange,
@@ -98,8 +87,17 @@ function QuerySearch({
onRun,
signalSource,
hardcodedAttributeKeys,
}: QuerySearchProps): JSX.Element {
}: {
placeholder?: string;
onChange: (value: string) => void;
queryData: IBuilderQuery;
dataSource: DataSource;
signalSource?: string;
hardcodedAttributeKeys?: QueryKeyDataSuggestionsProps[];
onRun?: (query: string) => void;
}): JSX.Element {
const isDarkMode = useIsDarkMode();
const [query, setQuery] = useState<string>(queryData.filter?.expression || '');
const [valueSuggestions, setValueSuggestions] = useState<any[]>([]);
const [activeKey, setActiveKey] = useState<string>('');
const [isLoadingSuggestions, setIsLoadingSuggestions] = useState(false);
@@ -109,12 +107,8 @@ function QuerySearch({
message: '',
errors: [],
});
const isProgrammaticChangeRef = useRef(false);
const [isEditorReady, setIsEditorReady] = useState(false);
const [isFocused, setIsFocused] = useState(false);
const editorRef = useRef<EditorView | null>(null);
const handleQueryValidation = useCallback((newQuery: string): void => {
const handleQueryValidation = (newQuery: string): void => {
try {
const validationResponse = validateQuery(newQuery);
setValidation(validationResponse);
@@ -125,67 +119,29 @@ function QuerySearch({
errors: [error as IDetailedError],
});
}
}, []);
};
const getCurrentQuery = useCallback(
(): string => editorRef.current?.state.doc.toString() || '',
[],
);
// Track if the query was changed externally (from queryData) vs internally (user input)
const [isExternalQueryChange, setIsExternalQueryChange] = useState(false);
const [lastExternalQuery, setLastExternalQuery] = useState<string>('');
const updateEditorValue = useCallback(
(value: string, options: { skipOnChange?: boolean } = {}): void => {
const view = editorRef.current;
if (!view) return;
useEffect(() => {
const newQuery = queryData.filter?.expression || '';
// Only mark as external change if the query actually changed from external source
if (newQuery !== lastExternalQuery) {
setQuery(newQuery);
setIsExternalQueryChange(true);
setLastExternalQuery(newQuery);
}
}, [queryData.filter?.expression, lastExternalQuery]);
const currentValue = view.state.doc.toString();
if (currentValue === value) return;
if (options.skipOnChange) {
isProgrammaticChangeRef.current = true;
}
view.dispatch({
changes: {
from: 0,
to: currentValue.length,
insert: value,
},
selection: {
anchor: value.length,
},
});
},
[],
);
const handleEditorCreate = useCallback((view: EditorView): void => {
editorRef.current = view;
setIsEditorReady(true);
}, []);
useEffect(
() => {
if (!isEditorReady) return;
const newQuery = queryData.filter?.expression || '';
const currentQuery = getCurrentQuery();
/* eslint-disable-next-line sonarjs/no-collapsible-if */
if (newQuery !== currentQuery && !isFocused) {
// Prevent clearing a non-empty editor when queryData becomes empty temporarily
// Only update if newQuery has a value, or if both are empty (initial state)
if (newQuery || !currentQuery) {
updateEditorValue(newQuery, { skipOnChange: true });
if (newQuery) {
handleQueryValidation(newQuery);
}
}
}
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[isEditorReady, queryData.filter?.expression, isFocused],
);
// Validate query when it changes externally (from queryData)
useEffect(() => {
if (isExternalQueryChange && query) {
handleQueryValidation(query);
setIsExternalQueryChange(false);
}
}, [isExternalQueryChange, query]);
const [keySuggestions, setKeySuggestions] = useState<
QueryKeyDataSuggestionsProps[] | null
@@ -194,6 +150,7 @@ function QuerySearch({
const [showExamples] = useState(false);
const [cursorPos, setCursorPos] = useState({ line: 0, ch: 0 });
const [isFocused, setIsFocused] = useState(false);
const [
isFetchingCompleteValuesList,
@@ -202,6 +159,8 @@ function QuerySearch({
const lastPosRef = useRef<{ line: number; ch: number }>({ line: 0, ch: 0 });
// Reference to the editor view for programmatic autocompletion
const editorRef = useRef<EditorView | null>(null);
const lastKeyRef = useRef<string>('');
const lastFetchedKeyRef = useRef<string>('');
const lastValueRef = useRef<string>('');
@@ -547,7 +506,6 @@ function QuerySearch({
if (!editorRef.current) {
editorRef.current = viewUpdate.view;
setIsEditorReady(true);
}
const selection = viewUpdate.view.state.selection.main;
@@ -563,15 +521,7 @@ function QuerySearch({
const lastPos = lastPosRef.current;
if (newPos.line !== lastPos.line || newPos.ch !== lastPos.ch) {
setCursorPos((lastPos) => {
if (newPos.ch !== lastPos.ch && newPos.ch === 0) {
Sentry.captureEvent({
message: `Cursor jumped to start of line from ${lastPos.ch} to ${newPos.ch}`,
level: 'warning',
});
}
return newPos;
});
setCursorPos(newPos);
lastPosRef.current = newPos;
if (doc) {
@@ -604,17 +554,16 @@ function QuerySearch({
}, []);
const handleChange = (value: string): void => {
if (isProgrammaticChangeRef.current) {
isProgrammaticChangeRef.current = false;
return;
}
setQuery(value);
onChange(value);
// Mark as internal change to avoid triggering external validation
setIsExternalQueryChange(false);
// Update lastExternalQuery to prevent external validation trigger
setLastExternalQuery(value);
};
const handleBlur = (): void => {
const currentQuery = getCurrentQuery();
handleQueryValidation(currentQuery);
handleQueryValidation(query);
setIsFocused(false);
};
@@ -633,11 +582,12 @@ function QuerySearch({
const handleExampleClick = (exampleQuery: string): void => {
// If there's an existing query, append the example with AND
const currentQuery = getCurrentQuery();
const newQuery = currentQuery
? `${currentQuery} AND ${exampleQuery}`
: exampleQuery;
updateEditorValue(newQuery);
const newQuery = query ? `${query} AND ${exampleQuery}` : exampleQuery;
setQuery(newQuery);
// Mark as internal change to avoid triggering external validation
setIsExternalQueryChange(false);
// Update lastExternalQuery to prevent external validation trigger
setLastExternalQuery(newQuery);
};
// Helper function to render a badge for the current context mode
@@ -672,10 +622,8 @@ function QuerySearch({
const word = context.matchBefore(/[a-zA-Z0-9_.:/?&=#%\-\[\]]*/);
if (word?.from === word?.to && !context.explicit) return null;
// Get current query from editor
const currentQuery = editorRef.current?.state.doc.toString() || '';
// Get the query context at the cursor position
const queryContext = getQueryContextAtCursor(currentQuery, cursorPos.ch);
const queryContext = getQueryContextAtCursor(query, cursorPos.ch);
// Define autocomplete options based on the context
let options: {
@@ -1171,8 +1119,7 @@ function QuerySearch({
if (queryContext.isInParenthesis) {
// Different suggestions based on the context within parenthesis or bracket
const currentQuery = editorRef.current?.state.doc.toString() || '';
const curChar = currentQuery.charAt(cursorPos.ch - 1) || '';
const curChar = query.charAt(cursorPos.ch - 1) || '';
if (curChar === '(' || curChar === '[') {
// Right after opening parenthesis/bracket
@@ -1321,7 +1268,7 @@ function QuerySearch({
style={{
position: 'absolute',
top: 8,
right: validation.isValid === false && getCurrentQuery() ? 40 : 8, // Move left when error shown
right: validation.isValid === false && query ? 40 : 8, // Move left when error shown
cursor: 'help',
zIndex: 10,
transition: 'right 0.2s ease',
@@ -1342,10 +1289,10 @@ function QuerySearch({
</Tooltip>
<CodeMirror
value={query}
theme={isDarkMode ? copilot : githubLight}
onChange={handleChange}
onUpdate={handleUpdate}
onCreateEditor={handleEditorCreate}
className={cx('query-where-clause-editor', {
isValid: validation.isValid === true,
hasErrors: validation.errors.length > 0,
@@ -1383,7 +1330,7 @@ function QuerySearch({
// Mod-Enter is usually Ctrl-Enter or Cmd-Enter based on OS
run: (): boolean => {
if (onRun && typeof onRun === 'function') {
onRun(getCurrentQuery());
onRun(query);
} else {
handleRunQuery();
}
@@ -1409,7 +1356,7 @@ function QuerySearch({
onBlur={handleBlur}
/>
{getCurrentQuery() && validation.isValid === false && !isFocused && (
{query && validation.isValid === false && !isFocused && (
<div
className={cx('query-status-container', {
hasErrors: validation.errors.length > 0,

View File

@@ -9,13 +9,7 @@ import SpanScopeSelector from 'container/QueryBuilder/filters/QueryBuilderSearch
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useQueryOperations } from 'hooks/queryBuilder/useQueryBuilderOperations';
import { Copy, Ellipsis, Trash } from 'lucide-react';
import {
ForwardedRef,
forwardRef,
useCallback,
useMemo,
useState,
} from 'react';
import { memo, useCallback, useMemo, useState } from 'react';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import { HandleChangeQueryDataV5 } from 'types/common/operations.types';
import { DataSource } from 'types/common/queryBuilder';
@@ -26,29 +20,26 @@ import QueryAddOns from './QueryAddOns/QueryAddOns';
import QueryAggregation from './QueryAggregation/QueryAggregation';
import QuerySearch from './QuerySearch/QuerySearch';
export const QueryV2 = forwardRef(function QueryV2(
{
index,
queryVariant,
query,
filterConfigs,
isListViewPanel = false,
showTraceOperator = false,
hasTraceOperator = false,
version,
showOnlyWhereClause = false,
signalSource = '',
isMultiQueryAllowed = false,
onSignalSourceChange,
signalSourceChangeEnabled = false,
queriesCount = 1,
}: QueryProps & {
onSignalSourceChange: (value: string) => void;
signalSourceChangeEnabled: boolean;
queriesCount: number;
},
ref: ForwardedRef<HTMLDivElement>,
): JSX.Element {
export const QueryV2 = memo(function QueryV2({
ref,
index,
queryVariant,
query,
filterConfigs,
isListViewPanel = false,
showTraceOperator = false,
hasTraceOperator = false,
version,
showOnlyWhereClause = false,
signalSource = '',
isMultiQueryAllowed = false,
onSignalSourceChange,
signalSourceChangeEnabled = false,
}: QueryProps & {
ref: React.RefObject<HTMLDivElement>;
onSignalSourceChange: (value: string) => void;
signalSourceChangeEnabled: boolean;
}): JSX.Element {
const { cloneQuery, panelType } = useQueryBuilder();
const showFunctions = query?.functions?.length > 0;
@@ -201,16 +192,12 @@ export const QueryV2 = forwardRef(function QueryV2(
icon: <Copy size={14} />,
onClick: handleCloneEntity,
},
...(queriesCount && queriesCount > 1
? [
{
label: 'Delete',
key: 'delete-query',
icon: <Trash size={14} />,
onClick: handleDeleteQuery,
},
]
: []),
{
label: 'Delete',
key: 'delete-query',
icon: <Trash size={14} />,
onClick: handleDeleteQuery,
},
],
}}
placement="bottomRight"
@@ -302,5 +289,3 @@ export const QueryV2 = forwardRef(function QueryV2(
</div>
);
});
QueryV2.displayName = 'QueryV2';

View File

@@ -92,9 +92,6 @@
.qb-trace-operator-editor-container {
flex: 1;
.cm-activeLine > span {
font-size: 12px;
}
}
&.arrow-left {
@@ -116,8 +113,6 @@
text-overflow: ellipsis;
padding: 0px 8px;
border-right: 1px solid var(--bg-slate-400);
font-size: 12px;
font-weight: 300;
}
}
}

View File

@@ -68,7 +68,7 @@ export default function TraceOperator({
!isListViewPanel && 'qb-trace-operator-arrow',
)}
>
<Typography.Text className="label">Trace Operator</Typography.Text>
<Typography.Text className="label">TRACE OPERATOR</Typography.Text>
<div className="qb-trace-operator-editor-container">
<TraceOperatorEditor
value={traceOperator?.expression || ''}

View File

@@ -5,85 +5,13 @@ import { getKeySuggestions } from 'api/querySuggestions/getKeySuggestions';
import { getValueSuggestions } from 'api/querySuggestions/getValueSuggestion';
import { initialQueriesMap } from 'constants/queryBuilder';
import * as UseQBModule from 'hooks/queryBuilder/useQueryBuilder';
import { fireEvent, render, userEvent, waitFor } from 'tests/test-utils';
import React from 'react';
import { render, screen, userEvent, waitFor } from 'tests/test-utils';
import type { QueryKeyDataSuggestionsProps } from 'types/api/querySuggestions/types';
import { DataSource } from 'types/common/queryBuilder';
import QuerySearch from '../QuerySearch/QuerySearch';
const CM_EDITOR_SELECTOR = '.cm-editor .cm-content';
// Mock DOM APIs that CodeMirror needs
beforeAll(() => {
// Mock getClientRects and getBoundingClientRect for Range objects
const mockRect: DOMRect = {
width: 100,
height: 20,
top: 0,
left: 0,
right: 100,
bottom: 20,
x: 0,
y: 0,
toJSON: (): DOMRect => mockRect,
} as DOMRect;
// Create a minimal Range mock with only what CodeMirror actually uses
const createMockRange = (): Range => {
let startContainer: Node = document.createTextNode('');
let endContainer: Node = document.createTextNode('');
let startOffset = 0;
let endOffset = 0;
const mockRange = {
// CodeMirror uses these for text measurement
getClientRects: (): DOMRectList =>
(({
length: 1,
item: (index: number): DOMRect | null => (index === 0 ? mockRect : null),
0: mockRect,
*[Symbol.iterator](): Generator<DOMRect> {
yield mockRect;
},
} as unknown) as DOMRectList),
getBoundingClientRect: (): DOMRect => mockRect,
// CodeMirror calls these to set up text ranges
setStart: (node: Node, offset: number): void => {
startContainer = node;
startOffset = offset;
},
setEnd: (node: Node, offset: number): void => {
endContainer = node;
endOffset = offset;
},
// Minimal Range properties (TypeScript requires these)
get startContainer(): Node {
return startContainer;
},
get endContainer(): Node {
return endContainer;
},
get startOffset(): number {
return startOffset;
},
get endOffset(): number {
return endOffset;
},
get collapsed(): boolean {
return startContainer === endContainer && startOffset === endOffset;
},
commonAncestorContainer: document.body,
};
return (mockRange as unknown) as Range;
};
// Mock document.createRange to return a new Range instance each time
document.createRange = (): Range => createMockRange();
// Mock getBoundingClientRect for elements
Element.prototype.getBoundingClientRect = (): DOMRect => mockRect;
});
jest.mock('hooks/useDarkMode', () => ({
useIsDarkMode: (): boolean => false,
}));
@@ -103,6 +31,24 @@ jest.mock('hooks/queryBuilder/useQueryBuilder', () => {
};
});
jest.mock('@codemirror/autocomplete', () => ({
autocompletion: (): Record<string, unknown> => ({}),
closeCompletion: (): boolean => true,
completionKeymap: [] as unknown[],
startCompletion: (): boolean => true,
}));
jest.mock('@codemirror/lang-javascript', () => ({
javascript: (): Record<string, unknown> => ({}),
}));
jest.mock('@uiw/codemirror-theme-copilot', () => ({
copilot: {},
}));
jest.mock('@uiw/codemirror-theme-github', () => ({
githubLight: {},
}));
jest.mock('api/querySuggestions/getKeySuggestions', () => ({
getKeySuggestions: jest.fn().mockResolvedValue({
data: {
@@ -117,19 +63,153 @@ jest.mock('api/querySuggestions/getValueSuggestion', () => ({
}),
}));
// Note: We're NOT mocking CodeMirror here - using the real component
// This provides integration testing with the actual CodeMirror editor
// Mock CodeMirror to a simple textarea to make it testable and call onUpdate
jest.mock(
'@uiw/react-codemirror',
(): Record<string, unknown> => {
// Minimal EditorView shape used by the component
class EditorViewMock {}
(EditorViewMock as any).domEventHandlers = (): unknown => ({} as unknown);
(EditorViewMock as any).lineWrapping = {} as unknown;
(EditorViewMock as any).editable = { of: () => ({}) } as unknown;
const keymap = { of: (arr: unknown) => arr } as unknown;
const Prec = { highest: (ext: unknown) => ext } as unknown;
type CodeMirrorProps = {
value?: string;
onChange?: (v: string) => void;
onFocus?: () => void;
onBlur?: () => void;
placeholder?: string;
onCreateEditor?: (view: unknown) => unknown;
onUpdate?: (arg: {
view: {
state: {
selection: { main: { head: number } };
doc: {
toString: () => string;
lineAt: (
_pos: number,
) => { number: number; from: number; to: number; text: string };
};
};
};
}) => void;
'data-testid'?: string;
extensions?: unknown[];
};
function CodeMirrorMock({
value,
onChange,
onFocus,
onBlur,
placeholder,
onCreateEditor,
onUpdate,
'data-testid': dataTestId,
extensions,
}: CodeMirrorProps): JSX.Element {
const [localValue, setLocalValue] = React.useState<string>(value ?? '');
// Provide a fake editor instance
React.useEffect(() => {
if (onCreateEditor) {
onCreateEditor(new EditorViewMock() as any);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
// Call onUpdate whenever localValue changes to simulate cursor and doc
React.useEffect(() => {
if (onUpdate) {
const text = String(localValue ?? '');
const head = text.length;
onUpdate({
view: {
state: {
selection: { main: { head } },
doc: {
toString: (): string => text,
lineAt: () => ({
number: 1,
from: 0,
to: text.length,
text,
}),
},
},
},
});
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [localValue]);
const handleKeyDown = (
e: React.KeyboardEvent<HTMLTextAreaElement>,
): void => {
const isModEnter = e.key === 'Enter' && (e.metaKey || e.ctrlKey);
if (!isModEnter) return;
const exts: unknown[] = Array.isArray(extensions) ? extensions : [];
const flat: unknown[] = exts.flatMap((x: unknown) =>
Array.isArray(x) ? x : [x],
);
const keyBindings = flat.filter(
(x) =>
Boolean(x) &&
typeof x === 'object' &&
'key' in (x as Record<string, unknown>),
) as Array<{ key?: string; run?: () => boolean | void }>;
keyBindings
.filter((b) => b.key === 'Mod-Enter' && typeof b.run === 'function')
.forEach((b) => {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
b.run!();
});
};
return (
<textarea
data-testid={dataTestId || 'query-where-clause-editor'}
placeholder={placeholder}
value={localValue}
onChange={(e): void => {
setLocalValue(e.target.value);
if (onChange) {
onChange(e.target.value);
}
}}
onFocus={onFocus}
onBlur={onBlur}
onKeyDown={handleKeyDown}
style={{ width: '100%', minHeight: 80 }}
/>
);
}
return {
__esModule: true,
default: CodeMirrorMock,
EditorView: EditorViewMock,
keymap,
Prec,
};
},
);
const handleRunQueryMock = ((UseQBModule as unknown) as {
handleRunQuery: jest.MockedFunction<() => void>;
}).handleRunQuery;
const PLACEHOLDER_TEXT =
"Enter your filter query (e.g., http.status_code >= 500 AND service.name = 'frontend')";
const TESTID_EDITOR = 'query-where-clause-editor';
const SAMPLE_KEY_TYPING = 'http.';
const SAMPLE_VALUE_TYPING_INCOMPLETE = "service.name = '";
const SAMPLE_VALUE_TYPING_COMPLETE = "service.name = 'frontend'";
const SAMPLE_STATUS_QUERY = "http.status_code = '200'";
const SAMPLE_VALUE_TYPING_INCOMPLETE = " service.name = '";
const SAMPLE_VALUE_TYPING_COMPLETE = " service.name = 'frontend'";
const SAMPLE_STATUS_QUERY = " status_code = '200'";
describe('QuerySearch (Integration with Real CodeMirror)', () => {
describe('QuerySearch', () => {
it('renders with placeholder', () => {
render(
<QuerySearch
@@ -139,19 +219,21 @@ describe('QuerySearch (Integration with Real CodeMirror)', () => {
/>,
);
// CodeMirror renders a contenteditable div, so we check for the container
const editorContainer = document.querySelector('.query-where-clause-editor');
expect(editorContainer).toBeInTheDocument();
expect(screen.getByPlaceholderText(PLACEHOLDER_TEXT)).toBeInTheDocument();
});
it('fetches key suggestions when typing a key (debounced)', async () => {
// Use real timers for CodeMirror integration tests
jest.useFakeTimers();
const advance = (ms: number): void => {
jest.advanceTimersByTime(ms);
};
const user = userEvent.setup({
advanceTimers: advance,
pointerEventsCheck: 0,
});
const mockedGetKeys = getKeySuggestions as jest.MockedFunction<
typeof getKeySuggestions
>;
mockedGetKeys.mockClear();
const user = userEvent.setup({ pointerEventsCheck: 0 });
render(
<QuerySearch
@@ -161,33 +243,28 @@ describe('QuerySearch (Integration with Real CodeMirror)', () => {
/>,
);
// Wait for CodeMirror to initialize
await waitFor(() => {
const editor = document.querySelector(CM_EDITOR_SELECTOR);
expect(editor).toBeInTheDocument();
});
// Find the CodeMirror editor contenteditable element
const editor = document.querySelector(CM_EDITOR_SELECTOR) as HTMLElement;
// Focus and type into the editor
await user.click(editor);
const editor = screen.getByTestId(TESTID_EDITOR);
await user.type(editor, SAMPLE_KEY_TYPING);
advance(1000);
// Wait for debounced API call (300ms debounce + some buffer)
await waitFor(() => expect(mockedGetKeys).toHaveBeenCalled(), {
timeout: 2000,
timeout: 3000,
});
jest.useRealTimers();
});
it('fetches value suggestions when editing value context', async () => {
// Use real timers for CodeMirror integration tests
jest.useFakeTimers();
const advance = (ms: number): void => {
jest.advanceTimersByTime(ms);
};
const user = userEvent.setup({
advanceTimers: advance,
pointerEventsCheck: 0,
});
const mockedGetValues = getValueSuggestions as jest.MockedFunction<
typeof getValueSuggestions
>;
mockedGetValues.mockClear();
const user = userEvent.setup({ pointerEventsCheck: 0 });
render(
<QuerySearch
@@ -197,28 +274,21 @@ describe('QuerySearch (Integration with Real CodeMirror)', () => {
/>,
);
// Wait for CodeMirror to initialize
await waitFor(() => {
const editor = document.querySelector(CM_EDITOR_SELECTOR);
expect(editor).toBeInTheDocument();
});
const editor = document.querySelector(CM_EDITOR_SELECTOR) as HTMLElement;
await user.click(editor);
const editor = screen.getByTestId(TESTID_EDITOR);
await user.type(editor, SAMPLE_VALUE_TYPING_INCOMPLETE);
advance(1000);
// Wait for debounced API call (300ms debounce + some buffer)
await waitFor(() => expect(mockedGetValues).toHaveBeenCalled(), {
timeout: 2000,
timeout: 3000,
});
jest.useRealTimers();
});
it('fetches key suggestions on mount for LOGS', async () => {
// Use real timers for CodeMirror integration tests
jest.useFakeTimers();
const mockedGetKeysOnMount = getKeySuggestions as jest.MockedFunction<
typeof getKeySuggestions
>;
mockedGetKeysOnMount.mockClear();
render(
<QuerySearch
@@ -228,15 +298,17 @@ describe('QuerySearch (Integration with Real CodeMirror)', () => {
/>,
);
// Wait for debounced API call (300ms debounce + some buffer)
jest.advanceTimersByTime(1000);
await waitFor(() => expect(mockedGetKeysOnMount).toHaveBeenCalled(), {
timeout: 2000,
timeout: 3000,
});
const lastArgs = mockedGetKeysOnMount.mock.calls[
mockedGetKeysOnMount.mock.calls.length - 1
]?.[0] as { signal: unknown; searchText: string };
expect(lastArgs).toMatchObject({ signal: DataSource.LOGS, searchText: '' });
jest.useRealTimers();
});
it('calls provided onRun on Mod-Enter', async () => {
@@ -252,26 +324,12 @@ describe('QuerySearch (Integration with Real CodeMirror)', () => {
/>,
);
// Wait for CodeMirror to initialize
await waitFor(() => {
const editor = document.querySelector(CM_EDITOR_SELECTOR);
expect(editor).toBeInTheDocument();
});
const editor = document.querySelector(CM_EDITOR_SELECTOR) as HTMLElement;
const editor = screen.getByTestId(TESTID_EDITOR);
await user.click(editor);
await user.type(editor, SAMPLE_STATUS_QUERY);
await user.keyboard('{Meta>}{Enter}{/Meta}');
// Use fireEvent for keyboard shortcuts as userEvent might not work well with CodeMirror
const modKey = navigator.platform.includes('Mac') ? 'metaKey' : 'ctrlKey';
fireEvent.keyDown(editor, {
key: 'Enter',
code: 'Enter',
[modKey]: true,
keyCode: 13,
});
await waitFor(() => expect(onRun).toHaveBeenCalled(), { timeout: 2000 });
await waitFor(() => expect(onRun).toHaveBeenCalled());
});
it('calls handleRunQuery when Mod-Enter without onRun', async () => {
@@ -290,62 +348,11 @@ describe('QuerySearch (Integration with Real CodeMirror)', () => {
/>,
);
// Wait for CodeMirror to initialize
await waitFor(() => {
const editor = document.querySelector(CM_EDITOR_SELECTOR);
expect(editor).toBeInTheDocument();
});
const editor = document.querySelector(CM_EDITOR_SELECTOR) as HTMLElement;
const editor = screen.getByTestId(TESTID_EDITOR);
await user.click(editor);
await user.type(editor, SAMPLE_VALUE_TYPING_COMPLETE);
await user.keyboard('{Meta>}{Enter}{/Meta}');
// Use fireEvent for keyboard shortcuts as userEvent might not work well with CodeMirror
const modKey = navigator.platform.includes('Mac') ? 'metaKey' : 'ctrlKey';
fireEvent.keyDown(editor, {
key: 'Enter',
code: 'Enter',
[modKey]: true,
keyCode: 13,
});
await waitFor(() => expect(mockedHandleRunQuery).toHaveBeenCalled(), {
timeout: 2000,
});
});
it('initializes CodeMirror with expression from queryData.filter.expression on mount', async () => {
const testExpression =
"http.status_code >= 500 AND service.name = 'frontend'";
const queryDataWithExpression = {
...initialQueriesMap.logs.builder.queryData[0],
filter: {
expression: testExpression,
},
};
render(
<QuerySearch
onChange={jest.fn() as jest.MockedFunction<(v: string) => void>}
queryData={queryDataWithExpression}
dataSource={DataSource.LOGS}
/>,
);
// Wait for CodeMirror to initialize and the expression to be set
await waitFor(
() => {
// CodeMirror stores content in .cm-content, check the text content
const editorContent = document.querySelector(
CM_EDITOR_SELECTOR,
) as HTMLElement;
expect(editorContent).toBeInTheDocument();
// CodeMirror may render the text in multiple ways, check if it contains our expression
const textContent = editorContent.textContent || '';
expect(textContent).toContain('http.status_code');
expect(textContent).toContain('service.name');
},
{ timeout: 3000 },
);
await waitFor(() => expect(mockedHandleRunQuery).toHaveBeenCalled());
});
});

View File

@@ -13,7 +13,6 @@ import {
convertAggregationToExpression,
convertFiltersToExpression,
convertFiltersToExpressionWithExistingQuery,
formatValueForExpression,
removeKeysFromExpression,
} from '../utils';
@@ -1194,220 +1193,3 @@ describe('removeKeysFromExpression', () => {
});
});
});
describe('formatValueForExpression', () => {
beforeEach(() => {
jest.clearAllMocks();
});
describe('Variable values', () => {
it('should return variable values as-is', () => {
expect(formatValueForExpression('$variable')).toBe('$variable');
expect(formatValueForExpression('$env')).toBe('$env');
expect(formatValueForExpression(' $variable ')).toBe(' $variable ');
});
it('should return variable arrays as-is', () => {
expect(formatValueForExpression(['$var1', '$var2'])).toBe('$var1,$var2');
});
});
describe('Numeric string values', () => {
it('should return numeric strings with quotes', () => {
expect(formatValueForExpression('123')).toBe("'123'");
expect(formatValueForExpression('0')).toBe("'0'");
expect(formatValueForExpression('100000')).toBe("'100000'");
expect(formatValueForExpression('-42')).toBe("'-42'");
expect(formatValueForExpression('3.14')).toBe("'3.14'");
expect(formatValueForExpression(' 456 ')).toBe("' 456 '");
});
it('should handle numeric strings with IN operator', () => {
expect(formatValueForExpression('123', 'IN')).toBe("['123']");
expect(formatValueForExpression(['123', '456'], 'IN')).toBe(
"['123', '456']",
);
});
});
describe('Quoted string values', () => {
it('should return already quoted strings as-is', () => {
expect(formatValueForExpression("'quoted'")).toBe("'quoted'");
expect(formatValueForExpression('"double-quoted"')).toBe('"double-quoted"');
expect(formatValueForExpression('`backticked`')).toBe('`backticked`');
expect(formatValueForExpression("'100000'")).toBe("'100000'");
});
it('should preserve quoted strings in arrays', () => {
expect(formatValueForExpression(["'value1'", "'value2'"])).toBe(
"['value1', 'value2']",
);
expect(formatValueForExpression(["'100000'", "'200000'"], 'IN')).toBe(
"['100000', '200000']",
);
});
});
describe('Regular string values', () => {
it('should wrap regular strings in single quotes', () => {
expect(formatValueForExpression('hello')).toBe("'hello'");
expect(formatValueForExpression('api-gateway')).toBe("'api-gateway'");
expect(formatValueForExpression('test value')).toBe("'test value'");
});
it('should escape single quotes in strings', () => {
expect(formatValueForExpression("user's data")).toBe("'user\\'s data'");
expect(formatValueForExpression("John's")).toBe("'John\\'s'");
expect(formatValueForExpression("it's a test")).toBe("'it\\'s a test'");
});
it('should handle empty strings', () => {
expect(formatValueForExpression('')).toBe("''");
});
it('should handle strings with special characters', () => {
expect(formatValueForExpression('/api/v1/users')).toBe("'/api/v1/users'");
expect(formatValueForExpression('user@example.com')).toBe(
"'user@example.com'",
);
expect(formatValueForExpression('Contains "quotes"')).toBe(
'\'Contains "quotes"\'',
);
});
});
describe('Number values', () => {
it('should convert numbers to strings without quotes', () => {
expect(formatValueForExpression(123)).toBe('123');
expect(formatValueForExpression(0)).toBe('0');
expect(formatValueForExpression(-42)).toBe('-42');
expect(formatValueForExpression(100000)).toBe('100000');
expect(formatValueForExpression(3.14)).toBe('3.14');
});
it('should handle numbers with IN operator', () => {
expect(formatValueForExpression(123, 'IN')).toBe('[123]');
expect(formatValueForExpression([100, 200] as any, 'IN')).toBe('[100, 200]');
});
});
describe('Boolean values', () => {
it('should convert booleans to strings without quotes', () => {
expect(formatValueForExpression(true)).toBe('true');
expect(formatValueForExpression(false)).toBe('false');
});
it('should handle booleans with IN operator', () => {
expect(formatValueForExpression(true, 'IN')).toBe('[true]');
expect(formatValueForExpression([true, false] as any, 'IN')).toBe(
'[true, false]',
);
});
});
describe('Array values', () => {
it('should format array of strings', () => {
expect(formatValueForExpression(['a', 'b', 'c'])).toBe("['a', 'b', 'c']");
expect(formatValueForExpression(['service1', 'service2'])).toBe(
"['service1', 'service2']",
);
});
it('should format array of numeric strings', () => {
expect(formatValueForExpression(['123', '456', '789'])).toBe(
"['123', '456', '789']",
);
});
it('should format array of numbers', () => {
expect(formatValueForExpression([1, 2, 3] as any)).toBe('[1, 2, 3]');
expect(formatValueForExpression([100, 200, 300] as any)).toBe(
'[100, 200, 300]',
);
});
it('should format mixed array types', () => {
expect(formatValueForExpression(['hello', 123, true] as any)).toBe(
"['hello', 123, true]",
);
});
it('should format array with quoted values', () => {
expect(formatValueForExpression(["'quoted'", 'regular'])).toBe(
"['quoted', 'regular']",
);
});
it('should format array with empty strings', () => {
expect(formatValueForExpression(['', 'value'])).toBe("['', 'value']");
});
});
describe('IN and NOT IN operators', () => {
it('should format single value as array for IN operator', () => {
expect(formatValueForExpression('value', 'IN')).toBe("['value']");
expect(formatValueForExpression(123, 'IN')).toBe('[123]');
expect(formatValueForExpression('123', 'IN')).toBe("['123']");
});
it('should format array for IN operator', () => {
expect(formatValueForExpression(['a', 'b'], 'IN')).toBe("['a', 'b']");
expect(formatValueForExpression(['123', '456'], 'IN')).toBe(
"['123', '456']",
);
});
it('should format single value as array for NOT IN operator', () => {
expect(formatValueForExpression('value', 'NOT IN')).toBe("['value']");
expect(formatValueForExpression('value', 'not in')).toBe("['value']");
});
it('should format array for NOT IN operator', () => {
expect(formatValueForExpression(['a', 'b'], 'NOT IN')).toBe("['a', 'b']");
});
});
describe('Edge cases', () => {
it('should handle strings that look like numbers but have quotes', () => {
expect(formatValueForExpression("'123'")).toBe("'123'");
expect(formatValueForExpression('"456"')).toBe('"456"');
expect(formatValueForExpression('`789`')).toBe('`789`');
});
it('should handle strings with leading/trailing whitespace', () => {
expect(formatValueForExpression(' hello ')).toBe("' hello '");
expect(formatValueForExpression(' 123 ')).toBe("' 123 '");
});
it('should handle very large numbers', () => {
expect(formatValueForExpression('999999999')).toBe("'999999999'");
expect(formatValueForExpression(999999999)).toBe('999999999');
});
it('should handle decimal numbers', () => {
expect(formatValueForExpression('123.456')).toBe("'123.456'");
expect(formatValueForExpression(123.456)).toBe('123.456');
});
it('should handle negative numbers', () => {
expect(formatValueForExpression('-100')).toBe("'-100'");
expect(formatValueForExpression(-100)).toBe('-100');
});
it('should handle strings that are not valid numbers', () => {
expect(formatValueForExpression('123abc')).toBe("'123abc'");
expect(formatValueForExpression('abc123')).toBe("'abc123'");
expect(formatValueForExpression('12.34.56')).toBe("'12.34.56'");
});
it('should handle empty array', () => {
expect(formatValueForExpression([])).toBe('[]');
expect(formatValueForExpression([], 'IN')).toBe('[]');
});
it('should handle array with single element', () => {
expect(formatValueForExpression(['single'])).toBe("['single']");
expect(formatValueForExpression([123] as any)).toBe('[123]');
});
});
});

View File

@@ -24,7 +24,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 +38,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 +136,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 = (

View File

@@ -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;

View File

@@ -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();
});
});

View File

@@ -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;

View File

@@ -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;

View File

@@ -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}>

View File

@@ -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);

View File

@@ -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);
});
});

View File

@@ -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

View File

@@ -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}`;
}

View File

@@ -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',
}

View File

@@ -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);
}

View File

@@ -34,7 +34,7 @@ const themeColors = {
cyan: '#00FFFF',
},
chartcolors: {
radicalRed: '#FF1A66',
robin: '#3F5ECC',
dodgerBlue: '#2F80ED',
mediumOrchid: '#BB6BD9',
seaBuckthorn: '#F2994A',
@@ -58,7 +58,7 @@ const themeColors = {
oliveDrab: '#66991A',
lavenderRose: '#FF99E6',
electricLime: '#CCFF1A',
robin: '#3F5ECC',
radicalRed: '#FF1A66',
harleyOrange: '#E6331A',
turquoise: '#33FFCC',
gladeGreen: '#66994D',
@@ -80,7 +80,7 @@ const themeColors = {
maroon: '#800000',
navy: '#000080',
aquamarine: '#7FFFD4',
darkSeaGreen: '#8FBC8F',
gold: '#FFD700',
gray: '#808080',
skyBlue: '#87CEEB',
indigo: '#4B0082',
@@ -105,7 +105,7 @@ const themeColors = {
lawnGreen: '#7CFC00',
mediumSeaGreen: '#3CB371',
lightCoral: '#F08080',
gold: '#FFD700',
darkSeaGreen: '#8FBC8F',
sandyBrown: '#F4A460',
darkKhaki: '#BDB76B',
cornflowerBlue: '#6495ED',
@@ -113,7 +113,7 @@ const themeColors = {
paleGreen: '#98FB98',
},
lightModeColor: {
radicalRed: '#FF1A66',
robin: '#3F5ECC',
dodgerBlueDark: '#0C6EED',
steelgrey: '#2f4b7c',
steelpurple: '#665191',
@@ -143,7 +143,7 @@ const themeColors = {
oliveDrab: '#66991A',
lavenderRoseDark: '#F024BD',
electricLimeDark: '#84A800',
robin: '#3F5ECC',
radicalRed: '#FF1A66',
harleyOrange: '#E6331A',
gladeGreen: '#66994D',
hemlock: '#66664D',
@@ -181,7 +181,7 @@ const themeColors = {
darkOrchid: '#9932CC',
mediumSeaGreenDark: '#109E50',
lightCoralDark: '#F85959',
gold: '#FFD700',
darkSeaGreenDark: '#509F50',
sandyBrownDark: '#D97117',
darkKhakiDark: '#99900A',
cornflowerBlueDark: '#3371E6',

View File

@@ -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;
}
}

View File

@@ -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',
});
});
});

View File

@@ -3,6 +3,3 @@ export const THRESHOLD_TAB_TOOLTIP =
export const ANOMALY_TAB_TOOLTIP =
'An alert is triggered whenever the metric deviates from an expected pattern.';
export const ROUTING_POLICIES_ROUTE =
'/alerts?tab=Configuration&subTab=routing-policies';

View File

@@ -289,21 +289,6 @@
border: 1px solid var(--bg-robin-500);
padding: 8px 16px;
.routing-policies-info-banner-right {
display: flex;
align-items: center;
gap: 8px;
.view-routing-policies-button {
color: var(--bg-robin-500);
font-size: 12px;
font-weight: 500;
display: flex;
align-items: center;
gap: 4px;
}
}
.ant-typography {
color: var(--bg-robin-500);
}

View File

@@ -1,21 +1,18 @@
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,
AlertThresholdOperator,
} from 'container/CreateAlertV2/context/types';
import { getSelectedQueryOptions } from 'container/FormAlertRules/utils';
import { ArrowRight } from 'lucide-react';
import { IUser } from 'providers/App/types';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
import { EQueryType } from 'types/common/dashboard';
import { USER_ROLES } from 'types/roles';
import { ROUTING_POLICIES_ROUTE } from './constants';
import { RoutingPolicyBannerProps } from './types';
export function getQueryNames(currentQuery: Query): BaseOptionType[] {
@@ -40,8 +37,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 +45,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}`,
})) || []
}),
) || []
);
}
@@ -405,27 +400,16 @@ export function RoutingPolicyBanner({
<Typography.Text>
Use <strong>Routing Policies</strong> for dynamic routing
</Typography.Text>
<div className="routing-policies-info-banner-right">
<Switch
checked={notificationSettings.routingPolicies}
data-testid="routing-policies-switch"
onChange={(value): void => {
setNotificationSettings({
type: 'SET_ROUTING_POLICIES',
payload: value,
});
}}
/>
<Button
href={ROUTING_POLICIES_ROUTE}
type="link"
className="view-routing-policies-button"
data-testid="view-routing-policies-button"
>
View Routing Policies
<ArrowRight size={14} />
</Button>
</div>
<Switch
checked={notificationSettings.routingPolicies}
data-testid="routing-policies-switch"
onChange={(value): void => {
setNotificationSettings({
type: 'SET_ROUTING_POLICIES',
payload: value,
});
}}
/>
</div>
);
}

View File

@@ -137,7 +137,7 @@
font-size: 13px;
&::placeholder {
color: var(--bg-vanilla-400);
color: #888;
}
&:focus,

View File

@@ -4,7 +4,7 @@ import { toast } from '@signozhq/sonner';
import { Button, Tooltip, Typography } from 'antd';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useSafeNavigate } from 'hooks/useSafeNavigate';
import { Check, Loader, Send, X } from 'lucide-react';
import { Check, Send, X } from 'lucide-react';
import { useCallback, useMemo } from 'react';
import { useCreateAlertState } from '../context';
@@ -150,11 +150,7 @@ function Footer(): JSX.Element {
onClick={handleSaveAlert}
disabled={disableButtons || Boolean(alertValidationMessage)}
>
{isCreatingAlertRule || isUpdatingAlertRule ? (
<Loader size={14} />
) : (
<Check size={14} />
)}
<Check size={14} />
<Typography.Text>Save Alert Rule</Typography.Text>
</Button>
);
@@ -162,13 +158,7 @@ function Footer(): JSX.Element {
button = <Tooltip title={alertValidationMessage}>{button}</Tooltip>;
}
return button;
}, [
alertValidationMessage,
disableButtons,
handleSaveAlert,
isCreatingAlertRule,
isUpdatingAlertRule,
]);
}, [alertValidationMessage, disableButtons, handleSaveAlert]);
const testAlertButton = useMemo(() => {
let button = (
@@ -177,7 +167,7 @@ function Footer(): JSX.Element {
onClick={handleTestNotification}
disabled={disableButtons || Boolean(alertValidationMessage)}
>
{isTestingAlertRule ? <Loader size={14} /> : <Send size={14} />}
<Send size={14} />
<Typography.Text>Test Notification</Typography.Text>
</Button>
);
@@ -185,12 +175,7 @@ function Footer(): JSX.Element {
button = <Tooltip title={alertValidationMessage}>{button}</Tooltip>;
}
return button;
}, [
alertValidationMessage,
disableButtons,
handleTestNotification,
isTestingAlertRule,
]);
}, [alertValidationMessage, disableButtons, handleTestNotification]);
return (
<div className="create-alert-v2-footer">

View File

@@ -67,10 +67,6 @@ const SAVE_ALERT_RULE_TEXT = 'Save Alert Rule';
const TEST_NOTIFICATION_TEXT = 'Test Notification';
const DISCARD_TEXT = 'Discard';
const LOADER_ICON_SELECTOR = 'svg.lucide-loader';
const CHECK_ICON_SELECTOR = 'svg.lucide-check';
const PLAY_ICON_SELECTOR = 'svg.lucide-play';
describe('Footer', () => {
beforeEach(() => {
useQueryBuilder.mockReturnValue({
@@ -249,61 +245,4 @@ describe('Footer', () => {
).toBeEnabled();
expect(screen.getByRole('button', { name: /discard/i })).toBeEnabled();
});
it('should show loader icon on test notification button when testing alert rule', () => {
jest.spyOn(createAlertState, 'useCreateAlertState').mockReturnValueOnce({
...mockAlertContextState,
isTestingAlertRule: true,
});
const { container } = render(<Footer />);
// When testing alert rule, the play icon is replaced with a loader icon
const playIconForTestNotificationButton = container.querySelector(
PLAY_ICON_SELECTOR,
);
expect(playIconForTestNotificationButton).not.toBeInTheDocument();
const loaderIconForTestNotificationButton = container.querySelector(
LOADER_ICON_SELECTOR,
);
expect(loaderIconForTestNotificationButton).toBeInTheDocument();
});
it('should not show check icon on save alert rule button when updating alert rule', () => {
jest.spyOn(createAlertState, 'useCreateAlertState').mockReturnValueOnce({
...mockAlertContextState,
isUpdatingAlertRule: true,
});
const { container } = render(<Footer />);
// When updating alert rule, the check icon is replaced with a loader icon
const checkIconForSaveAlertRuleButton = container.querySelector(
CHECK_ICON_SELECTOR,
);
expect(checkIconForSaveAlertRuleButton).not.toBeInTheDocument();
const loaderIconForSaveAlertRuleButton = container.querySelector(
LOADER_ICON_SELECTOR,
);
expect(loaderIconForSaveAlertRuleButton).toBeInTheDocument();
});
it('should not show check icon on save alert rule button when creating alert rule', () => {
jest.spyOn(createAlertState, 'useCreateAlertState').mockReturnValueOnce({
...mockAlertContextState,
isCreatingAlertRule: true,
});
const { container } = render(<Footer />);
// When creating alert rule, the check icon is replaced with a loader icon
const checkIconForSaveAlertRuleButton = container.querySelector(
CHECK_ICON_SELECTOR,
);
expect(checkIconForSaveAlertRuleButton).not.toBeInTheDocument();
const loaderIconForSaveAlertRuleButton = container.querySelector(
LOADER_ICON_SELECTOR,
);
expect(loaderIconForSaveAlertRuleButton).toBeInTheDocument();
});
});

View File

@@ -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>
);

View File

@@ -3,7 +3,7 @@
.query-section-tabs {
display: flex;
align-items: center;
margin-left: 8px;
margin-left: 12px;
margin-top: 24px;
.query-section-query-actions {

View File

@@ -1,11 +1,9 @@
import { CaretDownFilled, CaretRightFilled } from '@ant-design/icons';
import { Col, Typography } from 'antd';
import { StyledCol, StyledRow } from 'components/Styled';
import {
IIntervalUnit,
SPAN_DETAILS_LEFT_COL_WIDTH,
} from 'container/TraceDetail/utils';
import { IIntervalUnit } from 'container/TraceDetail/utils';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { SPAN_DETAILS_LEFT_COL_WIDTH } from 'pages/TraceDetail/constants';
import {
Dispatch,
MouseEventHandler,

View File

@@ -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 {

View File

@@ -170,8 +170,7 @@ describe('MultiIngestionSettings Page', () => {
);
});
// skipping the flaky test
it.skip('navigates to create alert for logs with size threshold', async () => {
it('navigates to create alert for logs with size threshold', async () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
// Arrange API response with a logs daily size limit so the alert button is visible

View File

@@ -68,7 +68,7 @@
.template-list-item {
display: flex;
gap: 8px;
padding: 4px 12px;
padding: 8px 12px;
align-items: center;
cursor: pointer;
height: 32px;
@@ -76,8 +76,10 @@
.template-icon {
display: flex;
height: 14px;
width: 14px;
height: 20px;
width: 20px;
border-radius: 2px;
padding: 4px;
align-items: center;
justify-content: center;
}
@@ -97,6 +99,17 @@
&.active {
border-radius: 3px;
background: rgba(171, 189, 255, 0.08);
position: relative;
&::before {
content: '';
position: absolute;
top: 0;
bottom: 0;
left: 0;
width: 2px;
background: var(--bg-robin-500);
}
}
}
}
@@ -159,18 +172,38 @@
display: flex;
justify-content: center;
align-items: center;
margin: 24px;
padding: 16px;
height: calc(100% - 144px);
position: relative;
img {
&-container {
position: relative;
width: 100%;
max-width: 100%;
padding: 24px;
padding: 48px 24px;
border-radius: 4px;
border: 1px solid var(--bg-ink-50);
background: var(--bg-ink-300);
background: linear-gradient(98.66deg, #7a97fa 4.42%, #f977ff 96.6%);
max-height: 100%;
&::before {
content: '';
position: absolute;
inset: 0;
background: url('/public/Images/grains.png');
background-size: contain;
background-repeat: repeat;
opacity: 0.1;
}
}
img {
position: relative;
width: 100%;
max-width: 100%;
max-height: 540px;
object-fit: contain;
border-radius: 4px;
}
}
}

View File

@@ -4,6 +4,7 @@
/* eslint-disable @typescript-eslint/explicit-function-return-type */
import './DashboardTemplatesModal.styles.scss';
import { Color } from '@signozhq/design-tokens';
import { Button, Input, Modal, Typography } from 'antd';
import ApacheIcon from 'assets/CustomIcons/ApacheIcon';
import DockerIcon from 'assets/CustomIcons/DockerIcon';
@@ -16,7 +17,14 @@ import NginxIcon from 'assets/CustomIcons/NginxIcon';
import PostgreSQLIcon from 'assets/CustomIcons/PostgreSQLIcon';
import RedisIcon from 'assets/CustomIcons/RedisIcon';
import cx from 'classnames';
import { ConciergeBell, DraftingCompass, Drill, Plus, X } from 'lucide-react';
import {
ConciergeBell,
DraftingCompass,
Drill,
Plus,
Search,
X,
} from 'lucide-react';
import { ChangeEvent, useState } from 'react';
import { DashboardTemplate } from 'types/api/dashboard/getAll';
@@ -162,7 +170,9 @@ export default function DashboardTemplatesModal({
<div className="new-dashboard-templates-list">
<Input
className="new-dashboard-templates-search"
placeholder="🔍 Search..."
placeholder="Search..."
size="middle"
prefix={<Search size={12} color={Color.TEXT_VANILLA_400} />}
onChange={handleDashboardTemplateSearch}
/>
@@ -212,10 +222,12 @@ export default function DashboardTemplatesModal({
</div>
<div className="template-preview-image">
<img
src={selectedDashboardTemplate.previewImage}
alt={`${selectedDashboardTemplate.name}-preview`}
/>
<div className="template-preview-image-container">
<img
src={selectedDashboardTemplate.previewImage}
alt={`${selectedDashboardTemplate.name}-preview`}
/>
</div>
</div>
</div>
</div>

View File

@@ -7,10 +7,9 @@ import './DashboardList.styles.scss';
import { Color } from '@signozhq/design-tokens';
import {
Button,
Dropdown,
Flex,
Input,
MenuProps,
// MenuProps,
Modal,
Popover,
Skeleton,
@@ -47,14 +46,14 @@ import {
Ellipsis,
EllipsisVertical,
Expand,
ExternalLink,
// ExternalLink,
FileJson,
Github,
// Github,
HdmiPort,
LayoutGrid,
// LayoutGrid,
Link2,
Plus,
Radius,
// Radius,
RotateCw,
Search,
SquareArrowOutUpRight,
@@ -71,7 +70,6 @@ import {
Key,
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from 'react';
@@ -597,61 +595,61 @@ function DashboardsList(): JSX.Element {
},
];
const getCreateDashboardItems = useMemo(() => {
const menuItems: MenuProps['items'] = [
{
label: (
<div
className="create-dashboard-menu-item"
onClick={(): void => onModalHandler(false)}
>
<Radius size={14} /> Import JSON
</div>
),
key: '1',
},
{
label: (
<a
href="https://signoz.io/docs/dashboards/dashboard-templates/overview/"
target="_blank"
rel="noopener noreferrer"
>
<Flex
justify="space-between"
align="center"
style={{ width: '100%' }}
gap="small"
>
<div className="create-dashboard-menu-item">
<Github size={14} /> View templates
</div>
<ExternalLink size={14} />
</Flex>
</a>
),
key: '2',
},
];
// const getCreateDashboardItems = useMemo(() => {
// const menuItems: MenuProps['items'] = [
// {
// label: (
// <div
// className="create-dashboard-menu-item"
// onClick={(): void => onModalHandler(false)}
// >
// <Radius size={14} /> Import JSON
// </div>
// ),
// key: '1',
// },
// {
// label: (
// <a
// href="https://signoz.io/docs/dashboards/dashboard-templates/overview/"
// target="_blank"
// rel="noopener noreferrer"
// >
// <Flex
// justify="space-between"
// align="center"
// style={{ width: '100%' }}
// gap="small"
// >
// <div className="create-dashboard-menu-item">
// <Github size={14} /> View templates
// </div>
// <ExternalLink size={14} />
// </Flex>
// </a>
// ),
// key: '2',
// },
// ];
if (createNewDashboard) {
menuItems.unshift({
label: (
<div
className="create-dashboard-menu-item"
onClick={(): void => {
onNewDashboardHandler();
}}
>
<LayoutGrid size={14} /> Create dashboard
</div>
),
key: '0',
});
}
// if (createNewDashboard) {
// menuItems.unshift({
// label: (
// <div
// className="create-dashboard-menu-item"
// onClick={(): void => {
// onNewDashboardHandler();
// }}
// >
// <LayoutGrid size={14} /> Create dashboard
// </div>
// ),
// key: '0',
// });
// }
return menuItems;
}, [createNewDashboard, onNewDashboardHandler]);
// return menuItems;
// }, [createNewDashboard, onNewDashboardHandler]);
const showPaginationItem = (total: number, range: number[]): JSX.Element => (
<>
@@ -763,23 +761,16 @@ function DashboardsList(): JSX.Element {
{createNewDashboard && (
<section className="actions">
<Dropdown
overlayClassName="new-dashboard-menu"
menu={{ items: getCreateDashboardItems }}
placement="bottomRight"
trigger={['click']}
<Button
type="text"
className="new-dashboard"
icon={<Plus size={14} />}
onClick={(): void => {
logEvent('Dashboard List: New dashboard clicked', {});
}}
>
<Button
type="text"
className="new-dashboard"
icon={<Plus size={14} />}
onClick={(): void => {
logEvent('Dashboard List: New dashboard clicked', {});
}}
>
New Dashboard
</Button>
</Dropdown>
New Dashboard
</Button>
<Button
type="text"
className="learn-more"
@@ -807,23 +798,17 @@ function DashboardsList(): JSX.Element {
onChange={handleSearch}
/>
{createNewDashboard && (
<Dropdown
overlayClassName="new-dashboard-menu"
menu={{ items: getCreateDashboardItems }}
placement="bottomRight"
trigger={['click']}
<Button
type="primary"
className="periscope-btn primary btn"
icon={<Plus size={14} />}
onClick={(): void => {
logEvent('Dashboard List: New dashboard clicked', {});
setShowNewDashboardTemplatesModal(true);
}}
>
<Button
type="primary"
className="periscope-btn primary btn"
icon={<Plus size={14} />}
onClick={(): void => {
logEvent('Dashboard List: New dashboard clicked', {});
}}
>
New dashboard
</Button>
</Dropdown>
New dashboard
</Button>
)}
</div>

View File

@@ -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

View File

@@ -274,7 +274,6 @@ function Login(): JSX.Element {
autoFocus
disabled={versionLoading}
className="login-form-input"
onPressEnter={onNextHandler}
/>
</FormContainer.Item>
</ParentContainer>

View File

@@ -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(() => {

View File

@@ -124,7 +124,7 @@
.builder-units-filter-label {
margin-bottom: 0px !important;
font-size: 12px;
font-size: 13px;
}
}
}

View File

@@ -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}
/>
);
}

View File

@@ -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, {

View File

@@ -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';

View File

@@ -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,

View File

@@ -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",

View File

@@ -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',

View File

@@ -5,6 +5,7 @@ import { AxiosError } from 'axios';
import Spinner from 'components/Spinner';
import { themeColors } from 'constants/theme';
import useGetTraceFlamegraph from 'hooks/trace/useGetTraceFlamegraph';
import { useIsDarkMode } from 'hooks/useDarkMode';
import useUrlQuery from 'hooks/useUrlQuery';
import { generateColor } from 'lib/uPlotLib/utils/generateColor';
import { useEffect, useMemo, useState } from 'react';
@@ -47,6 +48,7 @@ function TraceFlamegraph(props: ITraceFlamegraphProps): JSX.Element {
traceId,
selectedSpanId: firstSpanAtFetchLevel,
});
const isDarkMode = useIsDarkMode();
// get the current state of trace flamegraph based on the API lifecycle
const traceFlamegraphState = useMemo(() => {
@@ -122,8 +124,6 @@ function TraceFlamegraph(props: ITraceFlamegraphProps): JSX.Element {
traceId,
]);
const spread = useMemo(() => endTime - startTime, [endTime, startTime]);
return (
<div className="flamegraph">
<div
@@ -132,40 +132,36 @@ function TraceFlamegraph(props: ITraceFlamegraphProps): JSX.Element {
>
<div className="exec-time-service">% exec time</div>
<div className="stats">
{Object.keys(serviceExecTime)
.sort((a, b) => {
if (spread <= 0) return 0;
const aValue = (serviceExecTime[a] * 100) / spread;
const bValue = (serviceExecTime[b] * 100) / spread;
return bValue - aValue;
})
.map((service) => {
const value =
spread <= 0 ? 0 : (serviceExecTime[service] * 100) / spread;
const color = generateColor(service, themeColors.traceDetailColors);
return (
<div key={service} className="value-row">
<section className="service-name">
<div className="square-box" style={{ backgroundColor: color }} />
<Tooltip title={service}>
<Typography.Text className="service-text" ellipsis>
{service}
</Typography.Text>
</Tooltip>
</section>
<section className="progress-service">
<Progress
percent={parseFloat(value.toFixed(2))}
className="service-progress-indicator"
showInfo={false}
/>
<Typography.Text className="percent-value">
{parseFloat(value.toFixed(2))}%
{Object.keys(serviceExecTime).map((service) => {
const spread = endTime - startTime;
const value = (serviceExecTime[service] * 100) / spread;
const color = generateColor(
service,
isDarkMode ? themeColors.chartcolors : themeColors.lightModeColor,
);
return (
<div key={service} className="value-row">
<section className="service-name">
<div className="square-box" style={{ backgroundColor: color }} />
<Tooltip title={service}>
<Typography.Text className="service-text" ellipsis>
{service}
</Typography.Text>
</section>
</div>
);
})}
</Tooltip>
</section>
<section className="progress-service">
<Progress
percent={parseFloat(value.toFixed(2))}
className="service-progress-indicator"
showInfo={false}
/>
<Typography.Text className="percent-value">
{parseFloat(value.toFixed(2))}%
</Typography.Text>
</section>
</div>
);
})}
</div>
</div>
<div

View File

@@ -11,14 +11,11 @@ import {
useGetAllDowntimeSchedules,
} from 'api/plannedDowntime/getAllDowntimeSchedules';
import dayjs from 'dayjs';
import useDebouncedFn from 'hooks/useDebouncedFunction';
import { useNotifications } from 'hooks/useNotifications';
import useUrlQuery from 'hooks/useUrlQuery';
import { Search } from 'lucide-react';
import { useAppContext } from 'providers/App/App';
import React, { ChangeEvent, useEffect, useState } from 'react';
import { useQuery } from 'react-query';
import { useHistory } from 'react-router-dom';
import { USER_ROLES } from 'types/roles';
import { PlannedDowntimeDeleteModal } from './PlannedDowntimeDeleteModal';
@@ -39,8 +36,6 @@ export function PlannedDowntime(): JSX.Element {
const [isOpen, setIsOpen] = React.useState(false);
const [form] = Form.useForm();
const { user } = useAppContext();
const history = useHistory();
const urlQuery = useUrlQuery();
const [initialValues, setInitialValues] = useState<
Partial<DowntimeSchedules & { editMode: boolean }>
@@ -62,31 +57,16 @@ export function PlannedDowntime(): JSX.Element {
}
}, [form, isOpen]);
const [searchValue, setSearchValue] = React.useState<string | number>(
urlQuery.get('search') || '',
);
const [searchValue, setSearchValue] = React.useState<string | number>('');
const [deleteData, setDeleteData] = useState<{ id: number; name: string }>();
const [isEditMode, setEditMode] = useState<boolean>(false);
const updateUrlWithSearch = useDebouncedFn((value) => {
const searchValue = value as string;
if (searchValue) {
urlQuery.set('search', searchValue);
} else {
urlQuery.delete('search');
}
const url = `/alerts?${urlQuery.toString()}`;
history.replace(url);
}, 300);
const handleSearch = (e: ChangeEvent<HTMLInputElement>): void => {
setSearchValue(e.target.value);
updateUrlWithSearch(e.target.value);
};
const clearSearch = (): void => {
setSearchValue('');
updateUrlWithSearch('');
};
// Delete Downtime Schedule

View File

@@ -1,110 +1,10 @@
import { fireEvent, screen } from '@testing-library/react';
import { PayloadProps } from 'api/plannedDowntime/getAllDowntimeSchedules';
import { AxiosError, AxiosResponse } from 'axios';
import {
mockLocation,
mockQueryParams,
} from 'container/RoutingPolicies/__tests__/testUtils';
import { UseQueryResult } from 'react-query';
import { screen } from '@testing-library/react';
import { render } from 'tests/test-utils';
import { USER_ROLES } from 'types/roles';
import { PlannedDowntime } from '../PlannedDowntime';
import { buildSchedule, createMockDowntime } from './testUtils';
const SEARCH_PLACEHOLDER = 'Search for a planned downtime...';
const MOCK_DOWNTIME_1_NAME = 'Mock Downtime 1';
const MOCK_DOWNTIME_2_NAME = 'Mock Downtime 2';
const MOCK_DOWNTIME_3_NAME = 'Mock Downtime 3';
const MOCK_DATE_1 = '2024-01-01';
const MOCK_DATE_2 = '2024-01-02';
const MOCK_DATE_3 = '2024-01-03';
const MOCK_DOWNTIME_1 = createMockDowntime({
id: 1,
name: MOCK_DOWNTIME_1_NAME,
createdAt: MOCK_DATE_1,
updatedAt: MOCK_DATE_1,
schedule: buildSchedule({ startTime: MOCK_DATE_1, timezone: 'UTC' }),
alertIds: [],
});
const MOCK_DOWNTIME_2 = createMockDowntime({
id: 2,
name: MOCK_DOWNTIME_2_NAME,
createdAt: MOCK_DATE_2,
updatedAt: MOCK_DATE_2,
schedule: buildSchedule({ startTime: MOCK_DATE_2, timezone: 'UTC' }),
alertIds: [],
});
const MOCK_DOWNTIME_3 = createMockDowntime({
id: 3,
name: MOCK_DOWNTIME_3_NAME,
createdAt: MOCK_DATE_3,
updatedAt: MOCK_DATE_3,
schedule: buildSchedule({ startTime: MOCK_DATE_3, timezone: 'UTC' }),
alertIds: [],
});
const MOCK_DOWNTIME_RESPONSE: Partial<AxiosResponse<PayloadProps>> = {
data: {
data: [MOCK_DOWNTIME_1, MOCK_DOWNTIME_2, MOCK_DOWNTIME_3],
},
};
type DowntimeQueryResult = UseQueryResult<
AxiosResponse<PayloadProps>,
AxiosError
>;
const mockDowntimeQueryResult: Partial<DowntimeQueryResult> = {
data: MOCK_DOWNTIME_RESPONSE as AxiosResponse<PayloadProps>,
isLoading: false,
isFetching: false,
isError: false,
refetch: jest.fn(),
};
const mockUseLocation = jest.fn().mockReturnValue({
pathname: '/alerts',
});
let mockUrlQuery: URLSearchParams;
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useLocation: (): void => mockUseLocation(),
}));
jest.mock('hooks/useUrlQuery', () => ({
__esModule: true,
default: (): URLSearchParams => mockUrlQuery,
}));
const mockSafeNavigate = jest.fn();
jest.mock('hooks/useSafeNavigate', () => ({
useSafeNavigate: (): { safeNavigate: jest.MockedFunction<() => void> } => ({
safeNavigate: mockSafeNavigate,
}),
}));
jest.mock('api/plannedDowntime/getAllDowntimeSchedules', () => ({
useGetAllDowntimeSchedules: (): DowntimeQueryResult =>
mockDowntimeQueryResult as DowntimeQueryResult,
}));
jest.mock('api/alerts/getAll', () => ({
__esModule: true,
default: (): Promise<{ payload: [] }> => Promise.resolve({ payload: [] }),
}));
describe('PlannedDowntime Component', () => {
beforeEach(() => {
jest.clearAllMocks();
mockUrlQuery = mockQueryParams({});
mockLocation('/alerts');
});
it('renders the PlannedDowntime component properly', () => {
render(<PlannedDowntime />, {}, { role: 'ADMIN' });
@@ -117,7 +17,9 @@ describe('PlannedDowntime Component', () => {
).toBeInTheDocument();
// Check if search input is rendered
expect(screen.getByPlaceholderText(SEARCH_PLACEHOLDER)).toBeInTheDocument();
expect(
screen.getByPlaceholderText('Search for a planned downtime...'),
).toBeInTheDocument();
// Check if "New downtime" button is enabled for ADMIN
const newDowntimeButton = screen.getByRole('button', {
@@ -139,75 +41,4 @@ describe('PlannedDowntime Component', () => {
expect(newDowntimeButton).toHaveAttribute('disabled');
});
it('should load with search term from URL query params', () => {
const searchTerm = 'existing search';
mockUrlQuery = mockQueryParams({ search: searchTerm });
render(<PlannedDowntime />, {}, { role: USER_ROLES.ADMIN });
const searchInput = screen.getByPlaceholderText(
SEARCH_PLACEHOLDER,
) as HTMLInputElement;
expect(searchInput.value).toBe(searchTerm);
});
it('should initialize with empty search when no search param is in URL', () => {
mockUrlQuery = mockQueryParams({});
render(<PlannedDowntime />, {}, { role: USER_ROLES.ADMIN });
const searchInput = screen.getByPlaceholderText(
SEARCH_PLACEHOLDER,
) as HTMLInputElement;
expect(searchInput.value).toBe('');
});
it('should display all downtime schedules when no search term is entered', async () => {
render(<PlannedDowntime />, {}, { role: USER_ROLES.ADMIN });
expect(screen.getByText(MOCK_DOWNTIME_1_NAME)).toBeInTheDocument();
expect(screen.getByText(MOCK_DOWNTIME_2_NAME)).toBeInTheDocument();
expect(screen.getByText(MOCK_DOWNTIME_3_NAME)).toBeInTheDocument();
});
it('should filter downtime schedules by name when searching', async () => {
render(<PlannedDowntime />, {}, { role: USER_ROLES.ADMIN });
expect(screen.getByText(MOCK_DOWNTIME_1_NAME)).toBeInTheDocument();
const searchInput = screen.getByPlaceholderText(SEARCH_PLACEHOLDER);
fireEvent.change(searchInput, { target: { value: MOCK_DOWNTIME_1_NAME } });
expect(screen.getByText(MOCK_DOWNTIME_1_NAME)).toBeInTheDocument();
expect(screen.queryByText(MOCK_DOWNTIME_2_NAME)).not.toBeInTheDocument();
expect(screen.queryByText(MOCK_DOWNTIME_3_NAME)).not.toBeInTheDocument();
});
it('should filter downtime schedules with partial name match', async () => {
render(<PlannedDowntime />, {}, { role: USER_ROLES.ADMIN });
expect(screen.getByText(MOCK_DOWNTIME_1_NAME)).toBeInTheDocument();
const searchInput = screen.getByPlaceholderText(SEARCH_PLACEHOLDER);
fireEvent.change(searchInput, { target: { value: '2' } });
expect(screen.getByText(MOCK_DOWNTIME_2_NAME)).toBeInTheDocument();
expect(screen.queryByText(MOCK_DOWNTIME_1_NAME)).not.toBeInTheDocument();
expect(screen.queryByText(MOCK_DOWNTIME_3_NAME)).not.toBeInTheDocument();
});
it('should show no results when search term matches nothing', async () => {
render(<PlannedDowntime />, {}, { role: USER_ROLES.ADMIN });
const searchInput = screen.getByPlaceholderText(SEARCH_PLACEHOLDER);
fireEvent.change(searchInput, { target: { value: 'NonExistentDowntime' } });
expect(screen.queryByText(MOCK_DOWNTIME_1_NAME)).not.toBeInTheDocument();
expect(screen.queryByText(MOCK_DOWNTIME_2_NAME)).not.toBeInTheDocument();
expect(screen.queryByText(MOCK_DOWNTIME_3_NAME)).not.toBeInTheDocument();
});
});

View File

@@ -1,29 +0,0 @@
import { DowntimeSchedules } from 'api/plannedDowntime/getAllDowntimeSchedules';
export const buildSchedule = (
schedule: Partial<DowntimeSchedules['schedule']>,
): DowntimeSchedules['schedule'] => ({
timezone: schedule?.timezone ?? null,
startTime: schedule?.startTime ?? null,
endTime: schedule?.endTime ?? null,
recurrence: schedule?.recurrence ?? null,
});
export const createMockDowntime = (
overrides: Partial<DowntimeSchedules>,
): DowntimeSchedules => ({
id: overrides.id ?? 0,
name: overrides.name ?? null,
description: overrides.description ?? null,
schedule: buildSchedule({
timezone: 'UTC',
startTime: '2024-01-01',
...overrides.schedule,
}),
alertIds: overrides.alertIds ?? null,
createdAt: overrides.createdAt ?? null,
createdBy: overrides.createdBy ?? null,
updatedAt: overrides.updatedAt ?? null,
updatedBy: overrides.updatedBy ?? null,
kind: overrides.kind ?? null,
});

View File

@@ -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;
}
}

View File

@@ -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}

Some files were not shown because too many files have changed in this diff Show More