Compare commits
13 Commits
fix-id-das
...
remove-dea
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
807aa60906 | ||
|
|
208a5603a9 | ||
|
|
0de779a866 | ||
|
|
5cc833b73f | ||
|
|
3eee3bfec1 | ||
|
|
01b308d507 | ||
|
|
dcf627a683 | ||
|
|
49c04eb9d9 | ||
|
|
c89a8cbb0c | ||
|
|
b6bb71f650 | ||
|
|
af135aa068 | ||
|
|
4a4e4d6779 | ||
|
|
fc604915ed |
@@ -1,4 +1,5 @@
|
||||
services:
|
||||
|
||||
clickhouse:
|
||||
image: clickhouse/clickhouse-server:24.1.2-alpine
|
||||
container_name: clickhouse
|
||||
@@ -23,6 +24,7 @@ services:
|
||||
retries: 3
|
||||
depends_on:
|
||||
- zookeeper
|
||||
|
||||
zookeeper:
|
||||
image: bitnami/zookeeper:3.7.1
|
||||
container_name: zookeeper
|
||||
@@ -39,8 +41,9 @@ services:
|
||||
interval: 30s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
|
||||
schema-migrator-sync:
|
||||
image: signoz/signoz-schema-migrator:v0.111.40
|
||||
image: signoz/signoz-schema-migrator:0.111.29
|
||||
container_name: schema-migrator-sync
|
||||
command:
|
||||
- sync
|
||||
@@ -52,8 +55,9 @@ services:
|
||||
clickhouse:
|
||||
condition: service_healthy
|
||||
restart: on-failure
|
||||
|
||||
schema-migrator-async:
|
||||
image: signoz/signoz-schema-migrator:v0.111.40
|
||||
image: signoz/signoz-schema-migrator:0.111.29
|
||||
container_name: schema-migrator-async
|
||||
command:
|
||||
- async
|
||||
|
||||
1
.github/CODEOWNERS
vendored
1
.github/CODEOWNERS
vendored
@@ -11,4 +11,3 @@
|
||||
/pkg/errors/ @grandwizard28
|
||||
/pkg/factory/ @grandwizard28
|
||||
/pkg/types/ @grandwizard28
|
||||
/pkg/sqlmigration/ @vikrantgupta25
|
||||
|
||||
3
.github/workflows/build-enterprise.yaml
vendored
3
.github/workflows/build-enterprise.yaml
vendored
@@ -69,7 +69,6 @@ jobs:
|
||||
echo 'POSTHOG_KEY="${{ secrets.POSTHOG_KEY }}"' >> frontend/.env
|
||||
echo 'CUSTOMERIO_ID="${{ secrets.CUSTOMERIO_ID }}"' >> frontend/.env
|
||||
echo 'CUSTOMERIO_SITE_ID="${{ secrets.CUSTOMERIO_SITE_ID }}"' >> frontend/.env
|
||||
echo 'USERPILOT_KEY="${{ secrets.USERPILOT_KEY }}"' >> frontend/.env
|
||||
- name: cache-dotenv
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
@@ -105,8 +104,6 @@ jobs:
|
||||
-X github.com/SigNoz/signoz/pkg/version.hash=${{ needs.prepare.outputs.hash }}
|
||||
-X github.com/SigNoz/signoz/pkg/version.time=${{ needs.prepare.outputs.time }}
|
||||
-X github.com/SigNoz/signoz/pkg/version.branch=${{ needs.prepare.outputs.branch }}
|
||||
-X github.com/SigNoz/signoz/ee/zeus.url=https://api.signoz.cloud
|
||||
-X github.com/SigNoz/signoz/ee/zeus.deprecatedURL=https://license.signoz.io
|
||||
-X github.com/SigNoz/signoz/ee/query-service/constants.ZeusURL=https://api.signoz.cloud
|
||||
-X github.com/SigNoz/signoz/ee/query-service/constants.LicenseSignozIo=https://license.signoz.io/api/v1'
|
||||
GO_CGO_ENABLED: 1
|
||||
|
||||
7
.github/workflows/build-staging.yaml
vendored
7
.github/workflows/build-staging.yaml
vendored
@@ -64,9 +64,8 @@ jobs:
|
||||
run: |
|
||||
mkdir -p frontend
|
||||
echo 'CI=1' > frontend/.env
|
||||
echo 'TUNNEL_URL="${{ secrets.NP_TUNNEL_URL }}"' >> frontend/.env
|
||||
echo 'TUNNEL_DOMAIN="${{ secrets.NP_TUNNEL_DOMAIN }}"' >> frontend/.env
|
||||
echo 'USERPILOT_KEY="${{ secrets.NP_USERPILOT_KEY }}"' >> frontend/.env
|
||||
echo 'TUNNEL_URL=https://telemetry.staging.signoz.cloud/tunnel' >> frontend/.env
|
||||
echo 'TUNNEL_DOMAIN=https://telemetry.staging.signoz.cloud' >> frontend/.env
|
||||
- name: cache-dotenv
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
@@ -102,8 +101,6 @@ jobs:
|
||||
-X github.com/SigNoz/signoz/pkg/version.hash=${{ needs.prepare.outputs.hash }}
|
||||
-X github.com/SigNoz/signoz/pkg/version.time=${{ needs.prepare.outputs.time }}
|
||||
-X github.com/SigNoz/signoz/pkg/version.branch=${{ needs.prepare.outputs.branch }}
|
||||
-X github.com/SigNoz/signoz/ee/zeus.url=https://api.staging.signoz.cloud
|
||||
-X github.com/SigNoz/signoz/ee/zeus.deprecatedURL=https://license.staging.signoz.cloud
|
||||
-X github.com/SigNoz/signoz/ee/query-service/constants.ZeusURL=https://api.staging.signoz.cloud
|
||||
-X github.com/SigNoz/signoz/ee/query-service/constants.LicenseSignozIo=https://license.staging.signoz.cloud/api/v1'
|
||||
GO_CGO_ENABLED: 1
|
||||
|
||||
1
.github/workflows/gor-signoz.yaml
vendored
1
.github/workflows/gor-signoz.yaml
vendored
@@ -35,7 +35,6 @@ jobs:
|
||||
echo 'POSTHOG_KEY="${{ secrets.POSTHOG_KEY }}"' >> .env
|
||||
echo 'CUSTOMERIO_ID="${{ secrets.CUSTOMERIO_ID }}"' >> .env
|
||||
echo 'CUSTOMERIO_SITE_ID="${{ secrets.CUSTOMERIO_SITE_ID }}"' >> .env
|
||||
echo 'USERPILOT_KEY="${{ secrets.USERPILOT_KEY }}"' >> .env
|
||||
- name: build-frontend
|
||||
run: make js-build
|
||||
- name: upload-frontend-artifact
|
||||
|
||||
6
Makefile
6
Makefile
@@ -14,9 +14,9 @@ ARCHS ?= amd64 arm64
|
||||
TARGET_DIR ?= $(shell pwd)/target
|
||||
|
||||
ZEUS_URL ?= https://api.signoz.cloud
|
||||
GO_BUILD_LDFLAG_ZEUS_URL = -X github.com/SigNoz/signoz/ee/zeus.url=$(ZEUS_URL)
|
||||
LICENSE_URL ?= https://license.signoz.io
|
||||
GO_BUILD_LDFLAG_LICENSE_SIGNOZ_IO = -X github.com/SigNoz/signoz/ee/zeus.deprecatedURL=$(LICENSE_URL)
|
||||
GO_BUILD_LDFLAG_ZEUS_URL = -X github.com/SigNoz/signoz/ee/query-service/constants.ZeusURL=$(ZEUS_URL)
|
||||
LICENSE_URL ?= https://license.signoz.io/api/v1
|
||||
GO_BUILD_LDFLAG_LICENSE_SIGNOZ_IO = -X github.com/SigNoz/signoz/ee/query-service/constants.LicenseSignozIo=$(LICENSE_URL)
|
||||
|
||||
GO_BUILD_VERSION_LDFLAGS = -X github.com/SigNoz/signoz/pkg/version.version=$(VERSION) -X github.com/SigNoz/signoz/pkg/version.hash=$(COMMIT_SHORT_SHA) -X github.com/SigNoz/signoz/pkg/version.time=$(TIMESTAMP) -X github.com/SigNoz/signoz/pkg/version.branch=$(BRANCH_NAME)
|
||||
GO_BUILD_ARCHS_COMMUNITY = $(addprefix go-build-community-,$(ARCHS))
|
||||
|
||||
@@ -174,7 +174,7 @@ services:
|
||||
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
|
||||
signoz:
|
||||
!!merge <<: *db-depend
|
||||
image: signoz/signoz:v0.81.0
|
||||
image: signoz/signoz:v0.80.0
|
||||
command:
|
||||
- --config=/root/config/prometheus.yml
|
||||
- --use-logs-new-schema=true
|
||||
@@ -208,7 +208,7 @@ services:
|
||||
retries: 3
|
||||
otel-collector:
|
||||
!!merge <<: *db-depend
|
||||
image: signoz/signoz-otel-collector:v0.111.40
|
||||
image: signoz/signoz-otel-collector:v0.111.39
|
||||
command:
|
||||
- --config=/etc/otel-collector-config.yaml
|
||||
- --manager-config=/etc/manager-config.yaml
|
||||
@@ -232,7 +232,7 @@ services:
|
||||
- signoz
|
||||
schema-migrator:
|
||||
!!merge <<: *common
|
||||
image: signoz/signoz-schema-migrator:v0.111.40
|
||||
image: signoz/signoz-schema-migrator:v0.111.39
|
||||
deploy:
|
||||
restart_policy:
|
||||
condition: on-failure
|
||||
|
||||
@@ -110,7 +110,7 @@ services:
|
||||
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
|
||||
signoz:
|
||||
!!merge <<: *db-depend
|
||||
image: signoz/signoz:v0.81.0
|
||||
image: signoz/signoz:v0.80.0
|
||||
command:
|
||||
- --config=/root/config/prometheus.yml
|
||||
- --use-logs-new-schema=true
|
||||
@@ -143,7 +143,7 @@ services:
|
||||
retries: 3
|
||||
otel-collector:
|
||||
!!merge <<: *db-depend
|
||||
image: signoz/signoz-otel-collector:v0.111.40
|
||||
image: signoz/signoz-otel-collector:v0.111.39
|
||||
command:
|
||||
- --config=/etc/otel-collector-config.yaml
|
||||
- --manager-config=/etc/manager-config.yaml
|
||||
@@ -167,7 +167,7 @@ services:
|
||||
- signoz
|
||||
schema-migrator:
|
||||
!!merge <<: *common
|
||||
image: signoz/signoz-schema-migrator:v0.111.40
|
||||
image: signoz/signoz-schema-migrator:v0.111.39
|
||||
deploy:
|
||||
restart_policy:
|
||||
condition: on-failure
|
||||
|
||||
@@ -177,7 +177,7 @@ services:
|
||||
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
|
||||
signoz:
|
||||
!!merge <<: *db-depend
|
||||
image: signoz/signoz:${VERSION:-v0.81.0}
|
||||
image: signoz/signoz:${VERSION:-v0.80.0}
|
||||
container_name: signoz
|
||||
command:
|
||||
- --config=/root/config/prometheus.yml
|
||||
@@ -212,7 +212,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.111.40}
|
||||
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-v0.111.39}
|
||||
container_name: signoz-otel-collector
|
||||
command:
|
||||
- --config=/etc/otel-collector-config.yaml
|
||||
@@ -238,7 +238,7 @@ services:
|
||||
condition: service_healthy
|
||||
schema-migrator-sync:
|
||||
!!merge <<: *common
|
||||
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-v0.111.40}
|
||||
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-v0.111.39}
|
||||
container_name: schema-migrator-sync
|
||||
command:
|
||||
- sync
|
||||
@@ -249,7 +249,7 @@ services:
|
||||
condition: service_healthy
|
||||
schema-migrator-async:
|
||||
!!merge <<: *db-depend
|
||||
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-v0.111.40}
|
||||
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-v0.111.39}
|
||||
container_name: schema-migrator-async
|
||||
command:
|
||||
- async
|
||||
|
||||
@@ -110,7 +110,7 @@ services:
|
||||
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
|
||||
signoz:
|
||||
!!merge <<: *db-depend
|
||||
image: signoz/signoz:${VERSION:-v0.81.0}
|
||||
image: signoz/signoz:${VERSION:-v0.80.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.111.40}
|
||||
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-v0.111.39}
|
||||
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.111.40}
|
||||
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-v0.111.39}
|
||||
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.111.40}
|
||||
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-v0.111.39}
|
||||
container_name: schema-migrator-async
|
||||
command:
|
||||
- async
|
||||
|
||||
@@ -35,8 +35,6 @@ builds:
|
||||
- -X github.com/SigNoz/signoz/pkg/version.hash={{ .ShortCommit }}
|
||||
- -X github.com/SigNoz/signoz/pkg/version.time={{ .CommitTimestamp }}
|
||||
- -X github.com/SigNoz/signoz/pkg/version.branch={{ .Branch }}
|
||||
- -X github.com/SigNoz/signoz/ee/zeus.url=https://api.signoz.cloud
|
||||
- -X github.com/SigNoz/signoz/ee/zeus.deprecatedURL=https://license.signoz.io
|
||||
- -X github.com/SigNoz/signoz/ee/query-service/constants.ZeusURL=https://api.signoz.cloud
|
||||
- -X github.com/SigNoz/signoz/ee/query-service/constants.LicenseSignozIo=https://license.signoz.io/api/v1
|
||||
- >-
|
||||
|
||||
@@ -6,8 +6,8 @@ import (
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/http/render"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/app/dashboards"
|
||||
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
@@ -41,15 +41,10 @@ func (ah *APIHandler) lockUnlockDashboard(w http.ResponseWriter, r *http.Request
|
||||
return
|
||||
}
|
||||
|
||||
// dashboard, err := dashboards.GetDashboard(r.Context(), claims.OrgID, uuid)
|
||||
// if err != nil {
|
||||
// render.Error(w, errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "failed to get dashboard"))
|
||||
// return
|
||||
// }
|
||||
|
||||
dashboard, err := ah.Signoz.Modules.Dashboard.Get(r.Context(), valuer.MustNewUUID(claims.OrgID), valuer.MustNewUUID(uuid))
|
||||
dashboard, err := dashboards.GetDashboard(r.Context(), claims.OrgID, uuid)
|
||||
if err != nil {
|
||||
render.Error(w, err)
|
||||
render.Error(w, errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "failed to get dashboard"))
|
||||
return
|
||||
}
|
||||
|
||||
if err := claims.IsAdmin(); err != nil && (dashboard.CreatedBy != claims.Email) {
|
||||
@@ -57,17 +52,12 @@ func (ah *APIHandler) lockUnlockDashboard(w http.ResponseWriter, r *http.Request
|
||||
return
|
||||
}
|
||||
|
||||
if err := ah.Signoz.Modules.Dashboard.Update(r.Context(), nil); err != nil {
|
||||
render.Error(w, err)
|
||||
// Lock/Unlock the dashboard
|
||||
err = dashboards.LockUnlockDashboard(r.Context(), claims.OrgID, uuid, lock)
|
||||
if err != nil {
|
||||
render.Error(w, errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "failed to lock/unlock dashboard"))
|
||||
return
|
||||
}
|
||||
|
||||
// Lock/Unlock the dashboard
|
||||
// err = dashboards.LockUnlockDashboard(r.Context(), claims.OrgID, uuid, lock)
|
||||
// if err != nil {
|
||||
// render.Error(w, errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "failed to lock/unlock dashboard"))
|
||||
// return
|
||||
// }
|
||||
|
||||
ah.Respond(w, "Dashboard updated successfully")
|
||||
}
|
||||
|
||||
@@ -9,8 +9,6 @@ import (
|
||||
"github.com/SigNoz/signoz/ee/query-service/integrations/signozio"
|
||||
"github.com/SigNoz/signoz/ee/query-service/model"
|
||||
"github.com/SigNoz/signoz/pkg/http/render"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/telemetry"
|
||||
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
||||
)
|
||||
|
||||
type DayWiseBreakdown struct {
|
||||
@@ -92,13 +90,8 @@ func (ah *APIHandler) getActiveLicenseV3(w http.ResponseWriter, r *http.Request)
|
||||
|
||||
// this function is called by zeus when inserting licenses in the query-service
|
||||
func (ah *APIHandler) applyLicenseV3(w http.ResponseWriter, r *http.Request) {
|
||||
claims, err := authtypes.ClaimsFromContext(r.Context())
|
||||
if err != nil {
|
||||
render.Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
var licenseKey ApplyLicenseRequest
|
||||
|
||||
if err := json.NewDecoder(r.Body).Decode(&licenseKey); err != nil {
|
||||
RespondError(w, model.BadRequest(err), nil)
|
||||
return
|
||||
@@ -109,10 +102,9 @@ func (ah *APIHandler) applyLicenseV3(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
_, err = ah.LM().ActivateV3(r.Context(), licenseKey.LicenseKey)
|
||||
if err != nil {
|
||||
telemetry.GetInstance().SendEvent(telemetry.TELEMETRY_LICENSE_ACT_FAILED, map[string]interface{}{"err": err.Error()}, claims.Email, true, false)
|
||||
render.Error(w, err)
|
||||
_, apiError := ah.LM().ActivateV3(r.Context(), licenseKey.LicenseKey)
|
||||
if apiError != nil {
|
||||
RespondError(w, apiError, nil)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -120,9 +112,10 @@ func (ah *APIHandler) applyLicenseV3(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
func (ah *APIHandler) refreshLicensesV3(w http.ResponseWriter, r *http.Request) {
|
||||
err := ah.LM().RefreshLicense(r.Context())
|
||||
if err != nil {
|
||||
render.Error(w, err)
|
||||
|
||||
apiError := ah.LM().RefreshLicense(r.Context())
|
||||
if apiError != nil {
|
||||
RespondError(w, apiError, nil)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -134,6 +127,7 @@ func getCheckoutPortalResponse(redirectURL string) *Redirect {
|
||||
}
|
||||
|
||||
func (ah *APIHandler) checkout(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
checkoutRequest := &model.CheckoutRequest{}
|
||||
if err := json.NewDecoder(r.Body).Decode(checkoutRequest); err != nil {
|
||||
RespondError(w, model.BadRequest(err), nil)
|
||||
@@ -146,9 +140,9 @@ func (ah *APIHandler) checkout(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
redirectUrl, err := signozio.CheckoutSession(r.Context(), checkoutRequest, license.Key, ah.Signoz.Zeus)
|
||||
redirectUrl, err := signozio.CheckoutSession(r.Context(), checkoutRequest, license.Key)
|
||||
if err != nil {
|
||||
render.Error(w, err)
|
||||
RespondError(w, err, nil)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -236,6 +230,7 @@ func (ah *APIHandler) listLicensesV2(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
func (ah *APIHandler) portalSession(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
portalRequest := &model.PortalRequest{}
|
||||
if err := json.NewDecoder(r.Body).Decode(portalRequest); err != nil {
|
||||
RespondError(w, model.BadRequest(err), nil)
|
||||
@@ -248,9 +243,9 @@ func (ah *APIHandler) portalSession(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
redirectUrl, err := signozio.PortalSession(r.Context(), portalRequest, license.Key, ah.Signoz.Zeus)
|
||||
redirectUrl, err := signozio.PortalSession(r.Context(), portalRequest, license.Key)
|
||||
if err != nil {
|
||||
render.Error(w, err)
|
||||
RespondError(w, err, nil)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -35,6 +35,7 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/query-service/agentConf"
|
||||
baseapp "github.com/SigNoz/signoz/pkg/query-service/app"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/app/cloudintegrations"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/app/dashboards"
|
||||
baseexplorer "github.com/SigNoz/signoz/pkg/query-service/app/explorer"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/app/integrations"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/app/logparsingpipeline"
|
||||
@@ -101,13 +102,17 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := dashboards.InitDB(serverOptions.SigNoz.SQLStore); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
gatewayProxy, err := gateway.NewProxy(serverOptions.GatewayUrl, gateway.RoutePrefix)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// initiate license manager
|
||||
lm, err := licensepkg.StartManager(serverOptions.SigNoz.SQLStore.SQLxDB(), serverOptions.SigNoz.SQLStore, serverOptions.SigNoz.Zeus)
|
||||
lm, err := licensepkg.StartManager(serverOptions.SigNoz.SQLStore.SQLxDB(), serverOptions.SigNoz.SQLStore)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -190,7 +195,7 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) {
|
||||
}
|
||||
|
||||
// start the usagemanager
|
||||
usageManager, err := usage.New(modelDao, lm.GetRepo(), serverOptions.SigNoz.TelemetryStore.ClickhouseDB(), serverOptions.SigNoz.Zeus)
|
||||
usageManager, err := usage.New(modelDao, lm.GetRepo(), serverOptions.SigNoz.TelemetryStore.ClickhouseDB(), serverOptions.Config.TelemetryStore.Clickhouse.DSN)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
16
ee/query-service/integrations/signozio/response.go
Normal file
16
ee/query-service/integrations/signozio/response.go
Normal file
@@ -0,0 +1,16 @@
|
||||
package signozio
|
||||
|
||||
type status string
|
||||
|
||||
type ValidateLicenseResponse struct {
|
||||
Status status `json:"status"`
|
||||
Data map[string]interface{} `json:"data"`
|
||||
}
|
||||
|
||||
type CheckoutSessionRedirect struct {
|
||||
RedirectURL string `json:"url"`
|
||||
}
|
||||
type CheckoutResponse struct {
|
||||
Status status `json:"status"`
|
||||
Data CheckoutSessionRedirect `json:"data"`
|
||||
}
|
||||
@@ -1,67 +1,222 @@
|
||||
package signozio
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/SigNoz/signoz/ee/query-service/constants"
|
||||
"github.com/SigNoz/signoz/ee/query-service/model"
|
||||
"github.com/SigNoz/signoz/pkg/zeus"
|
||||
"github.com/tidwall/gjson"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func ValidateLicenseV3(ctx context.Context, licenseKey string, zeus zeus.Zeus) (*model.LicenseV3, error) {
|
||||
data, err := zeus.GetLicense(ctx, licenseKey)
|
||||
var C *Client
|
||||
|
||||
const (
|
||||
POST = "POST"
|
||||
APPLICATION_JSON = "application/json"
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
Prefix string
|
||||
GatewayUrl string
|
||||
}
|
||||
|
||||
func New() *Client {
|
||||
return &Client{
|
||||
Prefix: constants.LicenseSignozIo,
|
||||
GatewayUrl: constants.ZeusURL,
|
||||
}
|
||||
}
|
||||
|
||||
func init() {
|
||||
C = New()
|
||||
}
|
||||
|
||||
func ValidateLicenseV3(licenseKey string) (*model.LicenseV3, *model.ApiError) {
|
||||
|
||||
// Creating an HTTP client with a timeout for better control
|
||||
client := &http.Client{
|
||||
Timeout: 10 * time.Second,
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("GET", C.GatewayUrl+"/v2/licenses/me", nil)
|
||||
if err != nil {
|
||||
return nil, model.BadRequest(errors.Wrap(err, "failed to create request"))
|
||||
}
|
||||
|
||||
// Setting the custom header
|
||||
req.Header.Set("X-Signoz-Cloud-Api-Key", licenseKey)
|
||||
|
||||
response, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, model.BadRequest(errors.Wrap(err, "failed to make post request"))
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(response.Body)
|
||||
if err != nil {
|
||||
return nil, model.BadRequest(errors.Wrap(err, fmt.Sprintf("failed to read validation response from %v", C.GatewayUrl)))
|
||||
}
|
||||
|
||||
defer response.Body.Close()
|
||||
|
||||
switch response.StatusCode {
|
||||
case 200:
|
||||
a := ValidateLicenseResponse{}
|
||||
err = json.Unmarshal(body, &a)
|
||||
if err != nil {
|
||||
return nil, model.BadRequest(errors.Wrap(err, "failed to marshal license validation response"))
|
||||
}
|
||||
|
||||
license, err := model.NewLicenseV3(a.Data)
|
||||
if err != nil {
|
||||
return nil, model.BadRequest(errors.Wrap(err, "failed to generate new license v3"))
|
||||
}
|
||||
|
||||
return license, nil
|
||||
case 400:
|
||||
return nil, model.BadRequest(errors.Wrap(fmt.Errorf(string(body)),
|
||||
fmt.Sprintf("bad request error received from %v", C.GatewayUrl)))
|
||||
case 401:
|
||||
return nil, model.Unauthorized(errors.Wrap(fmt.Errorf(string(body)),
|
||||
fmt.Sprintf("unauthorized request error received from %v", C.GatewayUrl)))
|
||||
default:
|
||||
return nil, model.InternalError(errors.Wrap(fmt.Errorf(string(body)),
|
||||
fmt.Sprintf("internal request error received from %v", C.GatewayUrl)))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func NewPostRequestWithCtx(ctx context.Context, url string, contentType string, body io.Reader) (*http.Request, error) {
|
||||
req, err := http.NewRequestWithContext(ctx, POST, url, body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header.Add("Content-Type", contentType)
|
||||
return req, err
|
||||
|
||||
var m map[string]any
|
||||
if err = json.Unmarshal(data, &m); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
license, err := model.NewLicenseV3(m)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return license, nil
|
||||
}
|
||||
|
||||
// SendUsage reports the usage of signoz to license server
|
||||
func SendUsage(ctx context.Context, usage model.UsagePayload, zeus zeus.Zeus) error {
|
||||
body, err := json.Marshal(usage)
|
||||
func SendUsage(ctx context.Context, usage model.UsagePayload) *model.ApiError {
|
||||
reqString, _ := json.Marshal(usage)
|
||||
req, err := NewPostRequestWithCtx(ctx, C.Prefix+"/usage", APPLICATION_JSON, bytes.NewBuffer(reqString))
|
||||
if err != nil {
|
||||
return err
|
||||
return model.BadRequest(errors.Wrap(err, "unable to create http request"))
|
||||
}
|
||||
|
||||
return zeus.PutMeters(ctx, usage.LicenseKey.String(), body)
|
||||
res, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return model.BadRequest(errors.Wrap(err, "unable to connect with license.signoz.io, please check your network connection"))
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
return model.BadRequest(errors.Wrap(err, "failed to read usage response from license.signoz.io"))
|
||||
}
|
||||
|
||||
defer res.Body.Close()
|
||||
|
||||
switch res.StatusCode {
|
||||
case 200, 201:
|
||||
return nil
|
||||
case 400, 401:
|
||||
return model.BadRequest(errors.Wrap(errors.New(string(body)),
|
||||
"bad request error received from license.signoz.io"))
|
||||
default:
|
||||
return model.InternalError(errors.Wrap(errors.New(string(body)),
|
||||
"internal error received from license.signoz.io"))
|
||||
}
|
||||
}
|
||||
|
||||
func CheckoutSession(ctx context.Context, checkoutRequest *model.CheckoutRequest, licenseKey string, zeus zeus.Zeus) (string, error) {
|
||||
body, err := json.Marshal(checkoutRequest)
|
||||
func CheckoutSession(ctx context.Context, checkoutRequest *model.CheckoutRequest, licenseKey string) (string, *model.ApiError) {
|
||||
hClient := &http.Client{}
|
||||
|
||||
reqString, err := json.Marshal(checkoutRequest)
|
||||
if err != nil {
|
||||
return "", err
|
||||
return "", model.BadRequest(err)
|
||||
}
|
||||
|
||||
response, err := zeus.GetCheckoutURL(ctx, licenseKey, body)
|
||||
req, err := http.NewRequestWithContext(ctx, "POST", C.GatewayUrl+"/v2/subscriptions/me/sessions/checkout", bytes.NewBuffer(reqString))
|
||||
if err != nil {
|
||||
return "", err
|
||||
return "", model.BadRequest(err)
|
||||
}
|
||||
req.Header.Set("X-Signoz-Cloud-Api-Key", licenseKey)
|
||||
|
||||
return gjson.GetBytes(response, "url").String(), nil
|
||||
response, err := hClient.Do(req)
|
||||
if err != nil {
|
||||
return "", model.BadRequest(err)
|
||||
}
|
||||
body, err := io.ReadAll(response.Body)
|
||||
if err != nil {
|
||||
return "", model.BadRequest(errors.Wrap(err, fmt.Sprintf("failed to read checkout response from %v", C.GatewayUrl)))
|
||||
}
|
||||
defer response.Body.Close()
|
||||
|
||||
switch response.StatusCode {
|
||||
case 201:
|
||||
a := CheckoutResponse{}
|
||||
err = json.Unmarshal(body, &a)
|
||||
if err != nil {
|
||||
return "", model.BadRequest(errors.Wrap(err, "failed to unmarshal zeus checkout response"))
|
||||
}
|
||||
return a.Data.RedirectURL, nil
|
||||
case 400:
|
||||
return "", model.BadRequest(errors.Wrap(errors.New(string(body)),
|
||||
fmt.Sprintf("bad request error received from %v", C.GatewayUrl)))
|
||||
case 401:
|
||||
return "", model.Unauthorized(errors.Wrap(errors.New(string(body)),
|
||||
fmt.Sprintf("unauthorized request error received from %v", C.GatewayUrl)))
|
||||
default:
|
||||
return "", model.InternalError(errors.Wrap(errors.New(string(body)),
|
||||
fmt.Sprintf("internal request error received from %v", C.GatewayUrl)))
|
||||
}
|
||||
}
|
||||
|
||||
func PortalSession(ctx context.Context, portalRequest *model.PortalRequest, licenseKey string, zeus zeus.Zeus) (string, error) {
|
||||
body, err := json.Marshal(portalRequest)
|
||||
func PortalSession(ctx context.Context, checkoutRequest *model.PortalRequest, licenseKey string) (string, *model.ApiError) {
|
||||
hClient := &http.Client{}
|
||||
|
||||
reqString, err := json.Marshal(checkoutRequest)
|
||||
if err != nil {
|
||||
return "", err
|
||||
return "", model.BadRequest(err)
|
||||
}
|
||||
|
||||
response, err := zeus.GetPortalURL(ctx, licenseKey, body)
|
||||
req, err := http.NewRequestWithContext(ctx, "POST", C.GatewayUrl+"/v2/subscriptions/me/sessions/portal", bytes.NewBuffer(reqString))
|
||||
if err != nil {
|
||||
return "", err
|
||||
return "", model.BadRequest(err)
|
||||
}
|
||||
req.Header.Set("X-Signoz-Cloud-Api-Key", licenseKey)
|
||||
|
||||
return gjson.GetBytes(response, "url").String(), nil
|
||||
response, err := hClient.Do(req)
|
||||
if err != nil {
|
||||
return "", model.BadRequest(err)
|
||||
}
|
||||
body, err := io.ReadAll(response.Body)
|
||||
if err != nil {
|
||||
return "", model.BadRequest(errors.Wrap(err, fmt.Sprintf("failed to read portal response from %v", C.GatewayUrl)))
|
||||
}
|
||||
defer response.Body.Close()
|
||||
|
||||
switch response.StatusCode {
|
||||
case 201:
|
||||
a := CheckoutResponse{}
|
||||
err = json.Unmarshal(body, &a)
|
||||
if err != nil {
|
||||
return "", model.BadRequest(errors.Wrap(err, "failed to unmarshal zeus portal response"))
|
||||
}
|
||||
return a.Data.RedirectURL, nil
|
||||
case 400:
|
||||
return "", model.BadRequest(errors.Wrap(errors.New(string(body)),
|
||||
fmt.Sprintf("bad request error received from %v", C.GatewayUrl)))
|
||||
case 401:
|
||||
return "", model.Unauthorized(errors.Wrap(errors.New(string(body)),
|
||||
fmt.Sprintf("unauthorized request error received from %v", C.GatewayUrl)))
|
||||
default:
|
||||
return "", model.InternalError(errors.Wrap(errors.New(string(body)),
|
||||
fmt.Sprintf("internal request error received from %v", C.GatewayUrl)))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,13 +6,14 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/jmoiron/sqlx"
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"sync"
|
||||
|
||||
baseconstants "github.com/SigNoz/signoz/pkg/query-service/constants"
|
||||
"github.com/SigNoz/signoz/pkg/sqlstore"
|
||||
"github.com/SigNoz/signoz/pkg/types"
|
||||
"github.com/SigNoz/signoz/pkg/zeus"
|
||||
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
||||
|
||||
validate "github.com/SigNoz/signoz/ee/query-service/integrations/signozio"
|
||||
"github.com/SigNoz/signoz/ee/query-service/model"
|
||||
@@ -28,7 +29,6 @@ var validationFrequency = 24 * 60 * time.Minute
|
||||
|
||||
type Manager struct {
|
||||
repo *Repo
|
||||
zeus zeus.Zeus
|
||||
mutex sync.Mutex
|
||||
validatorRunning bool
|
||||
// end the license validation, this is important to gracefully
|
||||
@@ -45,7 +45,7 @@ type Manager struct {
|
||||
activeFeatures basemodel.FeatureSet
|
||||
}
|
||||
|
||||
func StartManager(db *sqlx.DB, store sqlstore.SQLStore, zeus zeus.Zeus, features ...basemodel.Feature) (*Manager, error) {
|
||||
func StartManager(db *sqlx.DB, store sqlstore.SQLStore, features ...basemodel.Feature) (*Manager, error) {
|
||||
if LM != nil {
|
||||
return LM, nil
|
||||
}
|
||||
@@ -53,7 +53,6 @@ func StartManager(db *sqlx.DB, store sqlstore.SQLStore, zeus zeus.Zeus, features
|
||||
repo := NewLicenseRepo(db, store)
|
||||
m := &Manager{
|
||||
repo: &repo,
|
||||
zeus: zeus,
|
||||
}
|
||||
if err := m.start(features...); err != nil {
|
||||
return m, err
|
||||
@@ -173,15 +172,17 @@ func (lm *Manager) ValidatorV3(ctx context.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
func (lm *Manager) RefreshLicense(ctx context.Context) error {
|
||||
license, err := validate.ValidateLicenseV3(ctx, lm.activeLicenseV3.Key, lm.zeus)
|
||||
if err != nil {
|
||||
return err
|
||||
func (lm *Manager) RefreshLicense(ctx context.Context) *model.ApiError {
|
||||
|
||||
license, apiError := validate.ValidateLicenseV3(lm.activeLicenseV3.Key)
|
||||
if apiError != nil {
|
||||
zap.L().Error("failed to validate license", zap.Error(apiError.Err))
|
||||
return apiError
|
||||
}
|
||||
|
||||
err = lm.repo.UpdateLicenseV3(ctx, license)
|
||||
err := lm.repo.UpdateLicenseV3(ctx, license)
|
||||
if err != nil {
|
||||
return err
|
||||
return model.BadRequest(errors.Wrap(err, "failed to update the new license"))
|
||||
}
|
||||
lm.SetActiveV3(license)
|
||||
|
||||
@@ -189,6 +190,7 @@ func (lm *Manager) RefreshLicense(ctx context.Context) error {
|
||||
}
|
||||
|
||||
func (lm *Manager) ValidateV3(ctx context.Context) (reterr error) {
|
||||
zap.L().Info("License validation started")
|
||||
if lm.activeLicenseV3 == nil {
|
||||
return nil
|
||||
}
|
||||
@@ -234,17 +236,28 @@ func (lm *Manager) ValidateV3(ctx context.Context) (reterr error) {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (lm *Manager) ActivateV3(ctx context.Context, licenseKey string) (*model.LicenseV3, error) {
|
||||
license, err := validate.ValidateLicenseV3(ctx, licenseKey, lm.zeus)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
func (lm *Manager) ActivateV3(ctx context.Context, licenseKey string) (licenseResponse *model.LicenseV3, errResponse *model.ApiError) {
|
||||
defer func() {
|
||||
if errResponse != nil {
|
||||
claims, err := authtypes.ClaimsFromContext(ctx)
|
||||
if err != nil {
|
||||
telemetry.GetInstance().SendEvent(telemetry.TELEMETRY_LICENSE_ACT_FAILED,
|
||||
map[string]interface{}{"err": errResponse.Err.Error()}, claims.Email, true, false)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
license, apiError := validate.ValidateLicenseV3(licenseKey)
|
||||
if apiError != nil {
|
||||
zap.L().Error("failed to get the license", zap.Error(apiError.Err))
|
||||
return nil, apiError
|
||||
}
|
||||
|
||||
// insert the new license to the sqlite db
|
||||
modelErr := lm.repo.InsertLicenseV3(ctx, license)
|
||||
if modelErr != nil {
|
||||
zap.L().Error("failed to activate license", zap.Error(modelErr))
|
||||
return nil, modelErr
|
||||
err := lm.repo.InsertLicenseV3(ctx, license)
|
||||
if err != nil {
|
||||
zap.L().Error("failed to activate license", zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// license is valid, activate it
|
||||
|
||||
@@ -8,8 +8,6 @@ import (
|
||||
|
||||
"github.com/SigNoz/signoz/ee/query-service/app"
|
||||
"github.com/SigNoz/signoz/ee/sqlstore/postgressqlstore"
|
||||
"github.com/SigNoz/signoz/ee/zeus"
|
||||
"github.com/SigNoz/signoz/ee/zeus/httpzeus"
|
||||
"github.com/SigNoz/signoz/pkg/config"
|
||||
"github.com/SigNoz/signoz/pkg/config/envprovider"
|
||||
"github.com/SigNoz/signoz/pkg/config/fileprovider"
|
||||
@@ -111,8 +109,6 @@ func main() {
|
||||
signoz, err := signoz.New(
|
||||
context.Background(),
|
||||
config,
|
||||
zeus.Config(),
|
||||
httpzeus.NewProviderFactory(),
|
||||
signoz.NewCacheProviderFactories(),
|
||||
signoz.NewWebProviderFactories(),
|
||||
sqlStoreFactories,
|
||||
|
||||
@@ -127,7 +127,7 @@ func TestNotification(opts baserules.PrepareTestRuleOptions) (int, *basemodel.Ap
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
zap.L().Error("failed to prepare a new threshold rule for test", zap.String("name", alertname), zap.Error(err))
|
||||
zap.L().Error("failed to prepare a new threshold rule for test", zap.String("name", rule.Name()), zap.Error(err))
|
||||
return 0, basemodel.BadRequest(err)
|
||||
}
|
||||
|
||||
@@ -146,7 +146,7 @@ func TestNotification(opts baserules.PrepareTestRuleOptions) (int, *basemodel.Ap
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
zap.L().Error("failed to prepare a new promql rule for test", zap.String("name", alertname), zap.Error(err))
|
||||
zap.L().Error("failed to prepare a new promql rule for test", zap.String("name", rule.Name()), zap.Error(err))
|
||||
return 0, basemodel.BadRequest(err)
|
||||
}
|
||||
} else if parsedRule.RuleType == ruletypes.RuleTypeAnomaly {
|
||||
@@ -161,7 +161,7 @@ func TestNotification(opts baserules.PrepareTestRuleOptions) (int, *basemodel.Ap
|
||||
baserules.WithSQLStore(opts.SQLStore),
|
||||
)
|
||||
if err != nil {
|
||||
zap.L().Error("failed to prepare a new anomaly rule for test", zap.String("name", alertname), zap.Error(err))
|
||||
zap.L().Error("failed to prepare a new anomaly rule for test", zap.String("name", rule.Name()), zap.Error(err))
|
||||
return 0, basemodel.BadRequest(err)
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
@@ -15,10 +16,10 @@ import (
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/SigNoz/signoz/ee/query-service/dao"
|
||||
licenseserver "github.com/SigNoz/signoz/ee/query-service/integrations/signozio"
|
||||
"github.com/SigNoz/signoz/ee/query-service/license"
|
||||
"github.com/SigNoz/signoz/ee/query-service/model"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/utils/encryption"
|
||||
"github.com/SigNoz/signoz/pkg/zeus"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -41,16 +42,26 @@ type Manager struct {
|
||||
|
||||
modelDao dao.ModelDao
|
||||
|
||||
zeus zeus.Zeus
|
||||
tenantID string
|
||||
}
|
||||
|
||||
func New(modelDao dao.ModelDao, licenseRepo *license.Repo, clickhouseConn clickhouse.Conn, zeus zeus.Zeus) (*Manager, error) {
|
||||
func New(modelDao dao.ModelDao, licenseRepo *license.Repo, clickhouseConn clickhouse.Conn, chUrl string) (*Manager, error) {
|
||||
hostNameRegex := regexp.MustCompile(`tcp://(?P<hostname>.*):`)
|
||||
hostNameRegexMatches := hostNameRegex.FindStringSubmatch(chUrl)
|
||||
|
||||
tenantID := ""
|
||||
if len(hostNameRegexMatches) == 2 {
|
||||
tenantID = hostNameRegexMatches[1]
|
||||
tenantID = strings.TrimSuffix(tenantID, "-clickhouse")
|
||||
}
|
||||
|
||||
m := &Manager{
|
||||
// repository: repo,
|
||||
clickhouseConn: clickhouseConn,
|
||||
licenseRepo: licenseRepo,
|
||||
scheduler: gocron.NewScheduler(time.UTC).Every(1).Day().At("00:00"), // send usage every at 00:00 UTC
|
||||
modelDao: modelDao,
|
||||
zeus: zeus,
|
||||
tenantID: tenantID,
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
@@ -147,7 +158,7 @@ func (lm *Manager) UploadUsage() {
|
||||
usageData.Type = usage.Type
|
||||
usageData.Tenant = "default"
|
||||
usageData.OrgName = "default"
|
||||
usageData.TenantId = "default"
|
||||
usageData.TenantId = lm.tenantID
|
||||
usagesPayload = append(usagesPayload, usageData)
|
||||
}
|
||||
|
||||
@@ -156,18 +167,24 @@ func (lm *Manager) UploadUsage() {
|
||||
LicenseKey: key,
|
||||
Usage: usagesPayload,
|
||||
}
|
||||
lm.UploadUsageWithExponentalBackOff(ctx, payload)
|
||||
}
|
||||
|
||||
body, errv2 := json.Marshal(payload)
|
||||
if errv2 != nil {
|
||||
zap.L().Error("error while marshalling usage payload: %v", zap.Error(errv2))
|
||||
return
|
||||
}
|
||||
|
||||
errv2 = lm.zeus.PutMeters(ctx, payload.LicenseKey.String(), body)
|
||||
if errv2 != nil {
|
||||
zap.L().Error("failed to upload usage: %v", zap.Error(errv2))
|
||||
// not returning error here since it is captured in the failed count
|
||||
return
|
||||
func (lm *Manager) UploadUsageWithExponentalBackOff(ctx context.Context, payload model.UsagePayload) {
|
||||
for i := 1; i <= MaxRetries; i++ {
|
||||
apiErr := licenseserver.SendUsage(ctx, payload)
|
||||
if apiErr != nil && i == MaxRetries {
|
||||
zap.L().Error("retries stopped : %v", zap.Error(apiErr))
|
||||
// not returning error here since it is captured in the failed count
|
||||
return
|
||||
} else if apiErr != nil {
|
||||
// sleeping for exponential backoff
|
||||
sleepDuration := RetryInterval * time.Duration(i)
|
||||
zap.L().Error("failed to upload snapshot retrying after %v secs : %v", zap.Duration("sleepDuration", sleepDuration), zap.Error(apiErr.Err))
|
||||
time.Sleep(sleepDuration)
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
package zeus
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
neturl "net/url"
|
||||
"sync"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/zeus"
|
||||
)
|
||||
|
||||
// This will be set via ldflags at build time.
|
||||
var (
|
||||
url string = "<unset>"
|
||||
deprecatedURL string = "<unset>"
|
||||
)
|
||||
|
||||
var (
|
||||
config zeus.Config
|
||||
once sync.Once
|
||||
)
|
||||
|
||||
// initializes the Zeus configuration
|
||||
func Config() zeus.Config {
|
||||
once.Do(func() {
|
||||
parsedURL, err := neturl.Parse(url)
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("invalid zeus URL: %w", err))
|
||||
}
|
||||
|
||||
deprecatedParsedURL, err := neturl.Parse(deprecatedURL)
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("invalid zeus deprecated URL: %w", err))
|
||||
}
|
||||
|
||||
config = zeus.Config{URL: parsedURL, DeprecatedURL: deprecatedParsedURL}
|
||||
if err := config.Validate(); err != nil {
|
||||
panic(fmt.Errorf("invalid zeus config: %w", err))
|
||||
}
|
||||
})
|
||||
|
||||
return config
|
||||
}
|
||||
@@ -1,189 +0,0 @@
|
||||
package httpzeus
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/factory"
|
||||
"github.com/SigNoz/signoz/pkg/http/client"
|
||||
"github.com/SigNoz/signoz/pkg/zeus"
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
|
||||
type Provider struct {
|
||||
settings factory.ScopedProviderSettings
|
||||
config zeus.Config
|
||||
httpClient *client.Client
|
||||
}
|
||||
|
||||
func NewProviderFactory() factory.ProviderFactory[zeus.Zeus, zeus.Config] {
|
||||
return factory.NewProviderFactory(factory.MustNewName("http"), func(ctx context.Context, providerSettings factory.ProviderSettings, config zeus.Config) (zeus.Zeus, error) {
|
||||
return New(ctx, providerSettings, config)
|
||||
})
|
||||
}
|
||||
|
||||
func New(ctx context.Context, providerSettings factory.ProviderSettings, config zeus.Config) (zeus.Zeus, error) {
|
||||
settings := factory.NewScopedProviderSettings(providerSettings, "github.com/SigNoz/signoz/ee/zeus/httpzeus")
|
||||
|
||||
httpClient, err := client.New(
|
||||
settings.Logger(),
|
||||
providerSettings.TracerProvider,
|
||||
providerSettings.MeterProvider,
|
||||
client.WithRequestResponseLog(true),
|
||||
client.WithRetryCount(3),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Provider{
|
||||
settings: settings,
|
||||
config: config,
|
||||
httpClient: httpClient,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (provider *Provider) GetLicense(ctx context.Context, key string) ([]byte, error) {
|
||||
response, err := provider.do(
|
||||
ctx,
|
||||
provider.config.URL.JoinPath("/v2/licenses/me"),
|
||||
http.MethodGet,
|
||||
key,
|
||||
nil,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return []byte(gjson.GetBytes(response, "data").String()), nil
|
||||
}
|
||||
|
||||
func (provider *Provider) GetCheckoutURL(ctx context.Context, key string, body []byte) ([]byte, error) {
|
||||
response, err := provider.do(
|
||||
ctx,
|
||||
provider.config.URL.JoinPath("/v2/subscriptions/me/sessions/checkout"),
|
||||
http.MethodPost,
|
||||
key,
|
||||
body,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return []byte(gjson.GetBytes(response, "data").String()), nil
|
||||
}
|
||||
|
||||
func (provider *Provider) GetPortalURL(ctx context.Context, key string, body []byte) ([]byte, error) {
|
||||
response, err := provider.do(
|
||||
ctx,
|
||||
provider.config.URL.JoinPath("/v2/subscriptions/me/sessions/portal"),
|
||||
http.MethodPost,
|
||||
key,
|
||||
body,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return []byte(gjson.GetBytes(response, "data").String()), nil
|
||||
}
|
||||
|
||||
func (provider *Provider) GetDeployment(ctx context.Context, key string) ([]byte, error) {
|
||||
response, err := provider.do(
|
||||
ctx,
|
||||
provider.config.URL.JoinPath("/v2/deployments/me"),
|
||||
http.MethodGet,
|
||||
key,
|
||||
nil,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return []byte(gjson.GetBytes(response, "data").String()), nil
|
||||
}
|
||||
|
||||
func (provider *Provider) PutMeters(ctx context.Context, key string, data []byte) error {
|
||||
_, err := provider.do(
|
||||
ctx,
|
||||
provider.config.DeprecatedURL.JoinPath("/api/v1/usage"),
|
||||
http.MethodPost,
|
||||
key,
|
||||
data,
|
||||
)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (provider *Provider) PutProfile(ctx context.Context, key string, body []byte) error {
|
||||
_, err := provider.do(
|
||||
ctx,
|
||||
provider.config.URL.JoinPath("/v2/profiles/me"),
|
||||
http.MethodPut,
|
||||
key,
|
||||
body,
|
||||
)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (provider *Provider) PutHost(ctx context.Context, key string, body []byte) error {
|
||||
_, err := provider.do(
|
||||
ctx,
|
||||
provider.config.URL.JoinPath("/v2/deployments/me/hosts"),
|
||||
http.MethodPut,
|
||||
key,
|
||||
body,
|
||||
)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (provider *Provider) do(ctx context.Context, url *url.URL, method string, key string, requestBody []byte) ([]byte, error) {
|
||||
request, err := http.NewRequestWithContext(ctx, method, url.String(), bytes.NewBuffer(requestBody))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
request.Header.Set("X-Signoz-Cloud-Api-Key", key)
|
||||
request.Header.Set("Content-Type", "application/json")
|
||||
|
||||
response, err := provider.httpClient.Do(request)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
defer func() {
|
||||
_ = response.Body.Close()
|
||||
}()
|
||||
|
||||
body, err := io.ReadAll(response.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if response.StatusCode/100 == 2 {
|
||||
return body, nil
|
||||
}
|
||||
|
||||
return nil, provider.errFromStatusCode(response.StatusCode)
|
||||
}
|
||||
|
||||
// This can be taken down to the client package
|
||||
func (provider *Provider) errFromStatusCode(statusCode int) error {
|
||||
switch statusCode {
|
||||
case http.StatusBadRequest:
|
||||
return errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "bad request")
|
||||
case http.StatusUnauthorized:
|
||||
return errors.Newf(errors.TypeUnauthenticated, errors.CodeUnauthenticated, "unauthenticated")
|
||||
case http.StatusForbidden:
|
||||
return errors.Newf(errors.TypeForbidden, errors.CodeForbidden, "forbidden")
|
||||
case http.StatusNotFound:
|
||||
return errors.Newf(errors.TypeNotFound, errors.CodeNotFound, "not found")
|
||||
}
|
||||
|
||||
return errors.Newf(errors.TypeInternal, errors.CodeInternal, "internal")
|
||||
}
|
||||
@@ -132,7 +132,6 @@
|
||||
"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",
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 23.0.4, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 800.5 907.77" style="enable-background:new 0 0 800.5 907.77;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#FFFFFF;}
|
||||
</style>
|
||||
<path class="st0" d="M303.36,238.61c31.36-21.37,71.76-12.97,65-6.53c-12.89,12.28,4.26,8.65,6.11,31.31
|
||||
c1.36,16.69-4.09,25.88-8.78,31.11c-9.79,1.28-21.69,3.67-36.02,8.33c-8.48,2.76-15.85,5.82-22.31,8.9
|
||||
c-1.7-1.11-3.55-2.47-5.74-4.36C279.5,288.19,280.24,254.37,303.36,238.61 M490.68,370.72c5.69-4.41,31.55-12.72,55.49-15.55
|
||||
c12.57-1.48,30.49-2.34,34.31-0.2c7.59,4.19,7.59,17.16,2.39,29.14c-7.57,17.4-18.27,36.63-30.39,38.21
|
||||
c-19.77,2.61-38.46-8.09-59.8-24.03C485.06,392.56,480.38,378.68,490.68,370.72 M526.75,201.27c29.19,13.58,25.37,39.42,26.18,54.6
|
||||
c0.22,4.36,0.15,7.3-0.22,9.32c-4.04-2.19-10.43-3.8-20.56-3.35c-2.96,0.12-5.84,0.47-8.63,0.91c-10.77-5.77-17.21-17.06-23.1-29.06
|
||||
c-0.54-1.11-0.96-2.1-1.36-3.06c-0.17-0.44-0.35-0.91-0.52-1.31c-0.07-0.22-0.12-0.39-0.2-0.59c-3.23-10.25-1.06-12.3,0.3-15.46
|
||||
c1.41-3.23,6.68-5.89-1.11-8.58c-0.67-0.25-1.5-0.39-2.44-0.57C500.25,197.72,515.7,196.17,526.75,201.27 M367.62,510.22
|
||||
c-31.45-20.19-63.99-49.15-78.22-65.18c-2.39-1.8-2-9.79-2-9.79c12.84,9.98,66.11,48.04,122.44,65.42
|
||||
c19.87,6.14,50.36,8.46,76.81-6.53c20.21-11.46,44.54-31.43,59.06-52.01l2.66,4.61c-0.1,3.06-6.78,17.97-10.18,23.96
|
||||
c6.14,3.53,10.72,4.49,17.55,6.36l46.64-7.27c16.74-27.04,28.74-70.65,15.95-112.16c-7.3-23.81-45.36-71.22-48.09-73.83
|
||||
c-9.56-9.19,1.6-44.69-17.35-83.42C532.86,159.41,480.67,116.69,458,98.1c6.68,4.88,47.82,21.47,67,44.62
|
||||
c1.8-2.39,2.54-14.82,4.19-17.97c-16.47-21.57-17.75-59.95-17.75-70.21c0-18.81-9.56-40.13-9.56-40.13s16.47,13.04,20.73,35.5
|
||||
c5.03,26.6,15.75,47.55,29.93,65.28c26.84,33.43,51.08,50.58,63.33,38.23C630.53,138.58,601,72.2,563.28,35.15
|
||||
C519.25-8.09,507.74-2.52,481.91,6.7c-20.61,7.35-31.75,65.87-85.47,64.71c-9.1-1.06-32.54-1.63-44.13-1.53
|
||||
c6.04-8.43,11.22-14.94,11.22-14.94s-18.02,7.25-33.38,16.44l-1.18-1.77c5.18-10.92,10.75-17.82,10.75-17.82s-14.4,8.65-27.54,19.01
|
||||
c2.39-13.02,11.44-21.27,11.44-21.27s-18.19,3.28-41.36,28.77c-26.33,7.2-32.66,11.93-53.64,21.22
|
||||
c-34.12-7.44-50.21-19.45-65.55-41.56c-11.68-16.89-32.47-19.45-53.71-10.72c-30.97,12.8-70.14,30.33-70.14,30.33
|
||||
s12.77-0.52,26.08,0.05c-18.22,6.9-35.72,16.39-35.72,16.39s8.53-0.3,19.06-0.12c-7.27,6.04-11.29,8.92-18.22,13.51
|
||||
c-16.66,12.1-30.17,26.08-30.17,26.08s11.31-5.15,21.47-8.04c-7.1,16.27-21.18,28.25-18.59,48.17
|
||||
c2.49,18.19,24.82,55.66,53.64,78.66c2.49,2,41.86,38.43,71.56,23.47c29.68-14.94,41.39-28.25,46.27-48.66
|
||||
c5.74-23.44,2.47-41.17-9.79-92.05c-4.04-16.79-14.57-51.37-19.65-67.91l1.13-0.81c9.71,20.49,34.56,74.5,44.57,110.78
|
||||
c15.63,56.57,10.75,85.27,3.6,95.79c-21.57,31.73-76.84,35.92-101.98,18.34c-3.85,60.91,9.76,87.73,14.37,101.24
|
||||
c-2.29,15.53,7.77,44.37,7.77,44.37s1.13-13.11,5.74-20.02c1.23,15.41,9,33.72,9,33.72s-0.47-11.31,3.06-21.08
|
||||
c4.98,8.43,8.63,10.43,13.34,16.76c4.71,16.47,14.15,28.5,14.15,28.5s-1.53-8.83-0.69-18.02c23.05,22.14,27.02,54.45,29.31,79.28
|
||||
c6.46,68.26-107.63,122.54-129.74,165.24c-16.76,25.29-26.8,65.3,1.58,88.89c68.6,56.97,42.25,72.65,76.59,97.69
|
||||
c47.11,34.34,106.05,18.96,126.11-8.97c27.93-38.92,20.76-75.63,10.38-109.97c-8.11-26.85-30.15-71.46-57.41-88.72
|
||||
c-27.86-17.65-54.95-20.95-77.9-18.59l2.12-2.44c33.01-6.56,67.52-2.96,92.49,13.14c28.35,18.22,54.28,49.47,67.84,97.37
|
||||
c15.38-2.19,17.55-3.18,31.63-5.18l-31.7-246.76L367.62,510.22z M385.94,819.52l-3.65-34.22l71.29-108.74l80.93,23.64l69.59-116.23
|
||||
L687.52,639l63.38-132.92l22.53,242.07L385.94,819.52z M774.27,456.51l-254.72,46.17c-6.31,8.13-21.91,22.41-29.41,26.13
|
||||
c-32.17,16.2-53.91,11.51-72.7,6.63c-12.08-3.06-19.08-4.78-29.11-9.29l-62.17,8.53l37.74,314.87l436.35-78.66L774.27,456.51z"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 3.8 KiB |
@@ -1,70 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 21.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 142.5 145.6" style="enable-background:new 0 0 142.5 145.6;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#565656;}
|
||||
.st1{fill:url(#SVGID_1_);}
|
||||
</style>
|
||||
<g>
|
||||
<path class="st0" d="M28.7,131.5c-0.3,7.9-6.6,14.1-14.4,14.1C6.1,145.6,0,139,0,130.9s6.6-14.7,14.7-14.7c3.6,0,7.2,1.6,10.2,4.4
|
||||
l-2.3,2.9c-2.3-2-5.1-3.4-7.9-3.4c-5.9,0-10.8,4.8-10.8,10.8c0,6.1,4.6,10.8,10.4,10.8c5.2,0,9.3-3.8,10.2-8.8H12.6v-3.5h16.1
|
||||
V131.5z"/>
|
||||
<path class="st0" d="M42.3,129.5h-2.2c-2.4,0-4.4,2-4.4,4.4v11.4h-3.9v-19.6H35v1.6c1.1-1.1,2.7-1.6,4.6-1.6h4.2L42.3,129.5z"/>
|
||||
<path class="st0" d="M63.7,145.3h-3.4v-2.5c-2.6,2.5-6.6,3.7-10.7,1.9c-3-1.3-5.3-4.1-5.9-7.4c-1.2-6.3,3.7-11.9,9.9-11.9
|
||||
c2.6,0,5,1.1,6.7,2.8v-2.5h3.4V145.3z M59.7,137c0.9-4-2.1-7.6-6-7.6c-3.4,0-6.1,2.8-6.1,6.1c0,3.8,3.3,6.7,7.2,6.1
|
||||
C57.1,141.2,59.1,139.3,59.7,137z"/>
|
||||
<path class="st0" d="M71.5,124.7v1.1h6.2v3.4h-6.2v16.1h-3.8v-20.5c0-4.3,3.1-6.8,7-6.8h4.7l-1.6,3.7h-3.1
|
||||
C72.9,121.6,71.5,123,71.5,124.7z"/>
|
||||
<path class="st0" d="M98.5,145.3h-3.3v-2.5c-2.6,2.5-6.6,3.7-10.7,1.9c-3-1.3-5.3-4.1-5.9-7.4c-1.2-6.3,3.7-11.9,9.9-11.9
|
||||
c2.6,0,5,1.1,6.7,2.8v-2.5h3.4v19.6H98.5z M94.5,137c0.9-4-2.1-7.6-6-7.6c-3.4,0-6.1,2.8-6.1,6.1c0,3.8,3.3,6.7,7.2,6.1
|
||||
C92,141.2,93.9,139.3,94.5,137z"/>
|
||||
<path class="st0" d="M119.4,133.8v11.5h-3.9v-11.6c0-2.4-2-4.4-4.4-4.4c-2.5,0-4.4,2-4.4,4.4v11.6h-3.9v-19.6h3.2v1.7
|
||||
c1.4-1.3,3.3-2,5.2-2C115.8,125.5,119.4,129.2,119.4,133.8z"/>
|
||||
<path class="st0" d="M142.4,145.3h-3.3v-2.5c-2.6,2.5-6.6,3.7-10.7,1.9c-3-1.3-5.3-4.1-5.9-7.4c-1.2-6.3,3.7-11.9,9.9-11.9
|
||||
c2.6,0,5,1.1,6.7,2.8v-2.5h3.4v19.6H142.4z M138.4,137c0.9-4-2.1-7.6-6-7.6c-3.4,0-6.1,2.8-6.1,6.1c0,3.8,3.3,6.7,7.2,6.1
|
||||
C135.9,141.2,137.8,139.3,138.4,137z"/>
|
||||
</g>
|
||||
<linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="71.25" y1="10.4893" x2="71.25" y2="113.3415" gradientTransform="matrix(1 0 0 -1 0 148.6)">
|
||||
<stop offset="0" style="stop-color:#FCEE1F"/>
|
||||
<stop offset="1" style="stop-color:#F15B2A"/>
|
||||
</linearGradient>
|
||||
<path class="st1" d="M122.9,49.9c-0.2-1.9-0.5-4.1-1.1-6.5c-0.6-2.4-1.6-5-2.9-7.8c-1.4-2.7-3.1-5.6-5.4-8.3
|
||||
c-0.9-1.1-1.9-2.1-2.9-3.2c1.6-6.3-1.9-11.8-1.9-11.8c-6.1-0.4-9.9,1.9-11.3,2.9c-0.2-0.1-0.5-0.2-0.7-0.3c-1-0.4-2.1-0.8-3.2-1.2
|
||||
c-1.1-0.3-2.2-0.7-3.3-0.9c-1.1-0.3-2.3-0.5-3.5-0.7c-0.2,0-0.4-0.1-0.6-0.1C83.5,3.6,75.9,0,75.9,0c-8.7,5.6-10.4,13.1-10.4,13.1
|
||||
s0,0.2-0.1,0.4c-0.5,0.1-0.9,0.3-1.4,0.4c-0.6,0.2-1.3,0.4-1.9,0.7c-0.6,0.3-1.3,0.5-1.9,0.8c-1.3,0.6-2.5,1.2-3.8,1.9
|
||||
c-1.2,0.7-2.4,1.4-3.5,2.2c-0.2-0.1-0.3-0.2-0.3-0.2c-11.7-4.5-22.1,0.9-22.1,0.9c-0.9,12.5,4.7,20.3,5.8,21.7
|
||||
c-0.3,0.8-0.5,1.5-0.8,2.3c-0.9,2.8-1.5,5.7-1.9,8.7c-0.1,0.4-0.1,0.9-0.2,1.3c-10.8,5.3-14,16.3-14,16.3c9,10.4,19.6,11,19.6,11
|
||||
l0,0c1.3,2.4,2.9,4.7,4.6,6.8c0.7,0.9,1.5,1.7,2.3,2.6c-3.3,9.4,0.5,17.3,0.5,17.3c10.1,0.4,16.7-4.4,18.1-5.5c1,0.3,2,0.6,3,0.9
|
||||
c3.1,0.8,6.3,1.3,9.4,1.4c0.8,0,1.6,0,2.4,0h0.4H80h0.5H81l0,0c4.7,6.8,13.1,7.7,13.1,7.7c5.9-6.3,6.3-12.4,6.3-13.8l0,0
|
||||
c0,0,0,0,0-0.1s0-0.2,0-0.2l0,0c0-0.1,0-0.2,0-0.3c1.2-0.9,2.4-1.8,3.6-2.8c2.4-2.1,4.4-4.6,6.2-7.2c0.2-0.2,0.3-0.5,0.5-0.7
|
||||
c6.7,0.4,11.4-4.2,11.4-4.2c-1.1-7-5.1-10.4-5.9-11l0,0c0,0,0,0-0.1-0.1l-0.1-0.1l0,0l-0.1-0.1c0-0.4,0.1-0.8,0.1-1.3
|
||||
c0.1-0.8,0.1-1.5,0.1-2.3v-0.6v-0.3v-0.1c0-0.2,0-0.1,0-0.2v-0.5v-0.6c0-0.2,0-0.4,0-0.6s0-0.4-0.1-0.6l-0.1-0.6l-0.1-0.6
|
||||
c-0.1-0.8-0.3-1.5-0.4-2.3c-0.7-3-1.9-5.9-3.4-8.4c-1.6-2.6-3.5-4.8-5.7-6.8c-2.2-1.9-4.6-3.5-7.2-4.6c-2.6-1.2-5.2-1.9-7.9-2.2
|
||||
c-1.3-0.2-2.7-0.2-4-0.2h-0.5h-0.1h-0.2h-0.2h-0.5c-0.2,0-0.4,0-0.5,0c-0.7,0.1-1.4,0.2-2,0.3c-2.7,0.5-5.2,1.5-7.4,2.8
|
||||
c-2.2,1.3-4.1,3-5.7,4.9s-2.8,3.9-3.6,6.1c-0.8,2.1-1.3,4.4-1.4,6.5c0,0.5,0,1.1,0,1.6c0,0.1,0,0.3,0,0.4v0.4c0,0.3,0,0.5,0.1,0.8
|
||||
c0.1,1.1,0.3,2.1,0.6,3.1c0.6,2,1.5,3.8,2.7,5.4s2.5,2.8,4,3.8s3,1.7,4.6,2.2c1.6,0.5,3.1,0.7,4.5,0.6c0.2,0,0.4,0,0.5,0
|
||||
c0.1,0,0.2,0,0.3,0s0.2,0,0.3,0c0.2,0,0.3,0,0.5,0h0.1h0.1c0.1,0,0.2,0,0.3,0c0.2,0,0.4-0.1,0.5-0.1c0.2,0,0.3-0.1,0.5-0.1
|
||||
c0.3-0.1,0.7-0.2,1-0.3c0.6-0.2,1.2-0.5,1.8-0.7c0.6-0.3,1.1-0.6,1.5-0.9c0.1-0.1,0.3-0.2,0.4-0.3c0.5-0.4,0.6-1.1,0.2-1.6
|
||||
c-0.4-0.4-1-0.5-1.5-0.3C88,74,87.9,74,87.7,74.1c-0.4,0.2-0.9,0.4-1.3,0.5c-0.5,0.1-1,0.3-1.5,0.4c-0.3,0-0.5,0.1-0.8,0.1
|
||||
c-0.1,0-0.3,0-0.4,0c-0.1,0-0.3,0-0.4,0s-0.3,0-0.4,0c-0.2,0-0.3,0-0.5,0c0,0-0.1,0,0,0h-0.1h-0.1c-0.1,0-0.1,0-0.2,0
|
||||
s-0.3,0-0.4-0.1c-1.1-0.2-2.3-0.5-3.4-1c-1.1-0.5-2.2-1.2-3.1-2.1c-1-0.9-1.8-1.9-2.5-3.1c-0.7-1.2-1.1-2.5-1.3-3.8
|
||||
c-0.1-0.7-0.2-1.4-0.1-2.1c0-0.2,0-0.4,0-0.6c0,0.1,0,0,0,0v-0.1v-0.1c0-0.1,0-0.2,0-0.3c0-0.4,0.1-0.7,0.2-1.1c0.5-3,2-5.9,4.3-8.1
|
||||
c0.6-0.6,1.2-1.1,1.9-1.5c0.7-0.5,1.4-0.9,2.1-1.2c0.7-0.3,1.5-0.6,2.3-0.8s1.6-0.4,2.4-0.4c0.4,0,0.8-0.1,1.2-0.1
|
||||
c0.1,0,0.2,0,0.3,0h0.3h0.2c0.1,0,0,0,0,0h0.1h0.3c0.9,0.1,1.8,0.2,2.6,0.4c1.7,0.4,3.4,1,5,1.9c3.2,1.8,5.9,4.5,7.5,7.8
|
||||
c0.8,1.6,1.4,3.4,1.7,5.3c0.1,0.5,0.1,0.9,0.2,1.4v0.3V66c0,0.1,0,0.2,0,0.3c0,0.1,0,0.2,0,0.3v0.3v0.3c0,0.2,0,0.6,0,0.8
|
||||
c0,0.5-0.1,1-0.1,1.5c-0.1,0.5-0.1,1-0.2,1.5s-0.2,1-0.3,1.5c-0.2,1-0.6,1.9-0.9,2.9c-0.7,1.9-1.7,3.7-2.9,5.3
|
||||
c-2.4,3.3-5.7,6-9.4,7.7c-1.9,0.8-3.8,1.5-5.8,1.8c-1,0.2-2,0.3-3,0.3H81h-0.2h-0.3H80h-0.3c0.1,0,0,0,0,0h-0.1
|
||||
c-0.5,0-1.1,0-1.6-0.1c-2.2-0.2-4.3-0.6-6.4-1.2c-2.1-0.6-4.1-1.4-6-2.4c-3.8-2-7.2-4.9-9.9-8.2c-1.3-1.7-2.5-3.5-3.5-5.4
|
||||
s-1.7-3.9-2.3-5.9c-0.6-2-0.9-4.1-1-6.2v-0.4v-0.1v-0.1v-0.2V60v-0.1v-0.1v-0.2v-0.5V59l0,0v-0.2c0-0.3,0-0.5,0-0.8
|
||||
c0-1,0.1-2.1,0.3-3.2c0.1-1.1,0.3-2.1,0.5-3.2c0.2-1.1,0.5-2.1,0.8-3.2c0.6-2.1,1.3-4.1,2.2-6c1.8-3.8,4.1-7.2,6.8-9.9
|
||||
c0.7-0.7,1.4-1.3,2.2-1.9c0.3-0.3,1-0.9,1.8-1.4c0.8-0.5,1.6-1,2.5-1.4c0.4-0.2,0.8-0.4,1.3-0.6c0.2-0.1,0.4-0.2,0.7-0.3
|
||||
c0.2-0.1,0.4-0.2,0.7-0.3c0.9-0.4,1.8-0.7,2.7-1c0.2-0.1,0.5-0.1,0.7-0.2c0.2-0.1,0.5-0.1,0.7-0.2c0.5-0.1,0.9-0.2,1.4-0.4
|
||||
c0.2-0.1,0.5-0.1,0.7-0.2c0.2,0,0.5-0.1,0.7-0.1c0.2,0,0.5-0.1,0.7-0.1l0.4-0.1l0.4-0.1c0.2,0,0.5-0.1,0.7-0.1
|
||||
c0.3,0,0.5-0.1,0.8-0.1c0.2,0,0.6-0.1,0.8-0.1c0.2,0,0.3,0,0.5-0.1h0.3h0.2h0.2c0.3,0,0.5,0,0.8-0.1h0.4c0,0,0.1,0,0,0h0.1h0.2
|
||||
c0.2,0,0.5,0,0.7,0c0.9,0,1.8,0,2.7,0c1.8,0.1,3.6,0.3,5.3,0.6c3.4,0.6,6.7,1.7,9.6,3.2c2.9,1.4,5.6,3.2,7.8,5.1
|
||||
c0.1,0.1,0.3,0.2,0.4,0.4c0.1,0.1,0.3,0.2,0.4,0.4c0.3,0.2,0.5,0.5,0.8,0.7c0.3,0.2,0.5,0.5,0.8,0.7c0.2,0.3,0.5,0.5,0.7,0.8
|
||||
c1,1,1.9,2.1,2.7,3.1c1.6,2.1,2.9,4.2,3.9,6.2c0.1,0.1,0.1,0.2,0.2,0.4c0.1,0.1,0.1,0.2,0.2,0.4s0.2,0.5,0.4,0.7
|
||||
c0.1,0.2,0.2,0.5,0.3,0.7c0.1,0.2,0.2,0.5,0.3,0.7c0.4,0.9,0.7,1.8,1,2.7c0.5,1.4,0.8,2.6,1.1,3.6c0.1,0.4,0.5,0.7,0.9,0.7
|
||||
c0.5,0,0.8-0.4,0.8-0.9C123,52.7,123,51.4,122.9,49.9z"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 6.6 KiB |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="192" height="192" fill="none" viewBox="0 0 192 192"><rect width="192" height="192" fill="url(#paint0_linear_1452_5317)" rx="24"/><path fill="#F2F2F2" d="M123.34 68.6596C119.655 41.0484 110.327 18 96 18C81.6731 18 72.3454 41.0484 68.6596 68.6596C41.0484 72.3454 18 81.6731 18 96C18 110.327 41.0525 119.655 68.6596 123.34C72.3454 150.948 81.6731 174 96 174C110.327 174 119.655 150.948 123.34 123.34C150.952 119.655 174 110.327 174 96C174 81.6731 150.948 72.3454 123.34 68.6596ZM67.7583 115.298C41.3151 111.479 25.893 102.737 25.893 96C25.893 89.2629 41.3151 80.5212 67.7583 76.7021C67.1764 83.0674 66.8733 89.566 66.8733 96C66.8733 102.434 67.1764 108.937 67.7583 115.298ZM96 25.893C102.737 25.893 111.479 41.3151 115.298 67.7583C108.937 67.1764 102.434 66.8733 96 66.8733C89.566 66.8733 83.0633 67.1764 76.7021 67.7583C80.5212 41.3151 89.2629 25.893 96 25.893ZM124.242 115.298C122.94 115.488 117.602 116.114 116.252 116.248C116.118 117.602 115.488 122.936 115.302 124.238C111.483 150.681 102.741 166.103 96.0041 166.103C89.267 166.103 80.5253 150.681 76.7061 124.238C76.5202 122.936 75.8898 117.598 75.7564 116.248C75.1421 109.979 74.7703 103.246 74.7703 96C74.7703 88.7537 75.1421 82.0206 75.7564 75.7483C82.0247 75.134 88.7577 74.7622 96.0041 74.7622C103.25 74.7622 109.983 75.134 116.252 75.7483C117.606 75.8817 122.94 76.5121 124.242 76.698C150.685 80.5172 166.111 89.2629 166.111 95.996C166.111 102.729 150.685 111.479 124.242 115.298Z"/><defs><linearGradient id="paint0_linear_1452_5317" x1="183" x2="0" y1="192" y2="0" gradientUnits="userSpaceOnUse"><stop stop-color="#444CE7"/><stop offset="1" stop-color="#B664FF"/></linearGradient></defs></svg>
|
||||
|
Before Width: | Height: | Size: 1.7 KiB |
@@ -26,7 +26,6 @@ import { QueryBuilderProvider } from 'providers/QueryBuilder';
|
||||
import { Suspense, useCallback, useEffect, useState } from 'react';
|
||||
import { Route, Router, Switch } from 'react-router-dom';
|
||||
import { CompatRouter } from 'react-router-dom-v5-compat';
|
||||
import { Userpilot } from 'userpilot';
|
||||
import { extractDomain } from 'utils/app';
|
||||
|
||||
import { Home } from './pageComponents';
|
||||
@@ -60,8 +59,6 @@ function App(): JSX.Element {
|
||||
|
||||
const { isCloudUser, isEnterpriseSelfHostedUser } = useGetTenantLicense();
|
||||
|
||||
const [isSentryInitialized, setIsSentryInitialized] = useState(false);
|
||||
|
||||
const enableAnalytics = useCallback(
|
||||
(user: IUser): void => {
|
||||
// wait for the required data to be loaded before doing init for anything!
|
||||
@@ -103,18 +100,6 @@ function App(): JSX.Element {
|
||||
logEvent('Domain Identified', groupTraits, 'group');
|
||||
}
|
||||
|
||||
Userpilot.identify(email, {
|
||||
email,
|
||||
name,
|
||||
orgName,
|
||||
tenant_id: hostNameParts[0],
|
||||
data_region: hostNameParts[1],
|
||||
tenant_url: hostname,
|
||||
company_domain: domain,
|
||||
source: 'signoz-ui',
|
||||
isPaidUser: !!trialInfo?.trialConvertedToSubscription,
|
||||
});
|
||||
|
||||
posthog?.identify(email, {
|
||||
email,
|
||||
name,
|
||||
@@ -291,33 +276,25 @@ function App(): JSX.Element {
|
||||
});
|
||||
}
|
||||
|
||||
if (process.env.USERPILOT_KEY) {
|
||||
Userpilot.initialize(process.env.USERPILOT_KEY);
|
||||
}
|
||||
|
||||
if (!isSentryInitialized) {
|
||||
Sentry.init({
|
||||
dsn: process.env.SENTRY_DSN,
|
||||
tunnel: process.env.TUNNEL_URL,
|
||||
environment: 'production',
|
||||
integrations: [
|
||||
Sentry.browserTracingIntegration(),
|
||||
Sentry.replayIntegration({
|
||||
maskAllText: false,
|
||||
blockAllMedia: false,
|
||||
}),
|
||||
],
|
||||
// Performance Monitoring
|
||||
tracesSampleRate: 1.0, // Capture 100% of the transactions
|
||||
// Set 'tracePropagationTargets' to control for which URLs distributed tracing should be enabled
|
||||
tracePropagationTargets: [],
|
||||
// Session Replay
|
||||
replaysSessionSampleRate: 0.1, // This sets the sample rate at 10%. You may want to change it to 100% while in development and then sample at a lower rate in production.
|
||||
replaysOnErrorSampleRate: 1.0, // If you're not already sampling the entire session, change the sample rate to 100% when sampling sessions where errors occur.
|
||||
});
|
||||
|
||||
setIsSentryInitialized(true);
|
||||
}
|
||||
Sentry.init({
|
||||
dsn: process.env.SENTRY_DSN,
|
||||
tunnel: process.env.TUNNEL_URL,
|
||||
environment: 'production',
|
||||
integrations: [
|
||||
Sentry.browserTracingIntegration(),
|
||||
Sentry.replayIntegration({
|
||||
maskAllText: false,
|
||||
blockAllMedia: false,
|
||||
}),
|
||||
],
|
||||
// Performance Monitoring
|
||||
tracesSampleRate: 1.0, // Capture 100% of the transactions
|
||||
// Set 'tracePropagationTargets' to control for which URLs distributed tracing should be enabled
|
||||
tracePropagationTargets: [],
|
||||
// Session Replay
|
||||
replaysSessionSampleRate: 0.1, // This sets the sample rate at 10%. You may want to change it to 100% while in development and then sample at a lower rate in production.
|
||||
replaysOnErrorSampleRate: 1.0, // If you're not already sampling the entire session, change the sample rate to 100% when sampling sessions where errors occur.
|
||||
});
|
||||
} else {
|
||||
posthog.reset();
|
||||
Sentry.close();
|
||||
@@ -326,7 +303,6 @@ function App(): JSX.Element {
|
||||
window.cioanalytics.reset();
|
||||
}
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [isCloudUser, isEnterpriseSelfHostedUser]);
|
||||
|
||||
// if the user is in logged in state
|
||||
|
||||
@@ -64,10 +64,6 @@ export const TraceDetail = Loadable(
|
||||
),
|
||||
);
|
||||
|
||||
export const UsageExplorerPage = Loadable(
|
||||
() => import(/* webpackChunkName: "UsageExplorerPage" */ 'modules/Usage'),
|
||||
);
|
||||
|
||||
export const SignupPage = Loadable(
|
||||
() => import(/* webpackChunkName: "SignupPage" */ 'pages/SignUp'),
|
||||
);
|
||||
|
||||
@@ -57,7 +57,6 @@ import {
|
||||
TracesFunnels,
|
||||
TracesSaveViews,
|
||||
UnAuthorized,
|
||||
UsageExplorerPage,
|
||||
WorkspaceAccessRestricted,
|
||||
WorkspaceBlocked,
|
||||
WorkspaceSuspended,
|
||||
@@ -155,13 +154,6 @@ const routes: AppRoutes[] = [
|
||||
isPrivate: true,
|
||||
key: 'SETTINGS',
|
||||
},
|
||||
{
|
||||
path: ROUTES.USAGE_EXPLORER,
|
||||
exact: true,
|
||||
component: UsageExplorerPage,
|
||||
isPrivate: true,
|
||||
key: 'USAGE_EXPLORER',
|
||||
},
|
||||
{
|
||||
path: ROUTES.ALL_DASHBOARD,
|
||||
exact: true,
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
import axios from 'api';
|
||||
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
||||
import { AxiosError } from 'axios';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import { PayloadProps, Props } from 'types/api/logs/getLogs';
|
||||
|
||||
const GetLogs = async (
|
||||
props: Props,
|
||||
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
|
||||
try {
|
||||
const data = await axios.get(`/logs`, {
|
||||
params: props,
|
||||
});
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
error: null,
|
||||
message: '',
|
||||
payload: data.data.results,
|
||||
};
|
||||
} catch (error) {
|
||||
return ErrorResponseHandler(error as AxiosError);
|
||||
}
|
||||
};
|
||||
|
||||
export default GetLogs;
|
||||
@@ -1,19 +0,0 @@
|
||||
import apiV1 from 'api/apiV1';
|
||||
import getLocalStorageKey from 'api/browser/localstorage/get';
|
||||
import { ENVIRONMENT } from 'constants/env';
|
||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||
import { EventSourcePolyfill } from 'event-source-polyfill';
|
||||
|
||||
// 10 min in ms
|
||||
const TIMEOUT_IN_MS = 10 * 60 * 1000;
|
||||
|
||||
export const LiveTail = (queryParams: string): EventSourcePolyfill =>
|
||||
new EventSourcePolyfill(
|
||||
`${ENVIRONMENT.baseURL}${apiV1}logs/tail?${queryParams}`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${getLocalStorageKey(LOCALSTORAGE.AUTH_TOKEN)}`,
|
||||
},
|
||||
heartbeatTimeout: TIMEOUT_IN_MS,
|
||||
},
|
||||
);
|
||||
@@ -54,9 +54,7 @@ export const REACT_QUERY_KEY = {
|
||||
|
||||
// API Monitoring Query Keys
|
||||
GET_DOMAINS_LIST: 'GET_DOMAINS_LIST',
|
||||
GET_DOMAIN_METRICS_DATA: 'GET_DOMAIN_METRICS_DATA',
|
||||
GET_ENDPOINTS_LIST_BY_DOMAIN: 'GET_ENDPOINTS_LIST_BY_DOMAIN',
|
||||
GET_TOP_ERRORS_BY_DOMAIN: 'GET_TOP_ERRORS_BY_DOMAIN',
|
||||
GET_NESTED_ENDPOINTS_LIST: 'GET_NESTED_ENDPOINTS_LIST',
|
||||
GET_ENDPOINT_METRICS_DATA: 'GET_ENDPOINT_METRICS_DATA',
|
||||
GET_ENDPOINT_STATUS_CODE_DATA: 'GET_ENDPOINT_STATUS_CODE_DATA',
|
||||
|
||||
@@ -17,7 +17,6 @@ const ROUTES = {
|
||||
'/get-started/infrastructure-monitoring',
|
||||
GET_STARTED_AWS_MONITORING: '/get-started/aws-monitoring',
|
||||
GET_STARTED_AZURE_MONITORING: '/get-started/azure-monitoring',
|
||||
USAGE_EXPLORER: '/usage-explorer',
|
||||
APPLICATION: '/services',
|
||||
ALL_DASHBOARD: '/dashboard',
|
||||
DASHBOARD: '/dashboard/:dashboardId',
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,17 +1,29 @@
|
||||
import { Select } from 'antd';
|
||||
import { initialQueriesMap } from 'constants/queryBuilder';
|
||||
import { LoadingOutlined } from '@ant-design/icons';
|
||||
import { Select, Spin, Table, Typography } from 'antd';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { ENTITY_VERSION_V4 } from 'constants/app';
|
||||
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||
import {
|
||||
getAllEndpointsWidgetData,
|
||||
getGroupByFiltersFromGroupByValues,
|
||||
EndPointsTableRowData,
|
||||
formatEndPointsDataForTable,
|
||||
getEndPointsColumnsConfig,
|
||||
getEndPointsQueryPayload,
|
||||
} from 'container/ApiMonitoring/utils';
|
||||
import GridCard from 'container/GridCardLayout/GridCard';
|
||||
import QueryBuilderSearchV2 from 'container/QueryBuilder/filters/QueryBuilderSearchV2/QueryBuilderSearchV2';
|
||||
import { useGetAggregateKeys } from 'hooks/queryBuilder/useGetAggregateKeys';
|
||||
import { GetMetricQueryRange } from 'lib/dashboard/getQueryResults';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useQueries } from 'react-query';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { SuccessResponse } from 'types/api';
|
||||
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
|
||||
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
|
||||
import { SPAN_ATTRIBUTES, VIEWS } from './constants';
|
||||
import ErrorState from './components/ErrorState';
|
||||
import ExpandedRow from './components/ExpandedRow';
|
||||
import { VIEW_TYPES, VIEWS } from './constants';
|
||||
|
||||
function AllEndPoints({
|
||||
domainName,
|
||||
@@ -19,27 +31,13 @@ function AllEndPoints({
|
||||
setSelectedView,
|
||||
groupBy,
|
||||
setGroupBy,
|
||||
timeRange,
|
||||
initialFilters,
|
||||
setInitialFiltersEndPointStats,
|
||||
}: {
|
||||
domainName: string;
|
||||
setSelectedEndPointName: (name: string) => void;
|
||||
setSelectedView: (tab: VIEWS) => void;
|
||||
groupBy: IBuilderQuery['groupBy'];
|
||||
setGroupBy: (groupBy: IBuilderQuery['groupBy']) => void;
|
||||
timeRange: {
|
||||
startTime: number;
|
||||
endTime: number;
|
||||
};
|
||||
initialFilters: IBuilderQuery['filters'];
|
||||
setInitialFiltersEndPointStats: (filters: IBuilderQuery['filters']) => void;
|
||||
}): JSX.Element {
|
||||
const [groupBySearchValue, setGroupBySearchValue] = useState<string>('');
|
||||
const [allAvailableGroupByOptions, setAllAvailableGroupByOptions] = useState<{
|
||||
[key: string]: any;
|
||||
}>({});
|
||||
|
||||
const {
|
||||
data: groupByFiltersData,
|
||||
isLoading: isLoadingGroupByFilters,
|
||||
@@ -47,7 +45,7 @@ function AllEndPoints({
|
||||
dataSource: DataSource.TRACES,
|
||||
aggregateAttribute: '',
|
||||
aggregateOperator: 'noop',
|
||||
searchText: groupBySearchValue,
|
||||
searchText: '',
|
||||
tagType: '',
|
||||
});
|
||||
|
||||
@@ -55,144 +53,130 @@ function AllEndPoints({
|
||||
{ value: string; label: string }[]
|
||||
>([]);
|
||||
|
||||
const [expandedRowKeys, setExpandedRowKeys] = useState<React.Key[]>([]);
|
||||
|
||||
const handleGroupByChange = useCallback(
|
||||
(value: IBuilderQuery['groupBy']) => {
|
||||
const newGroupBy = [];
|
||||
const groupBy = [];
|
||||
|
||||
for (let index = 0; index < value.length; index++) {
|
||||
const element = (value[index] as unknown) as string;
|
||||
|
||||
// Check if the key exists in our cached options first
|
||||
if (allAvailableGroupByOptions[element]) {
|
||||
newGroupBy.push(allAvailableGroupByOptions[element]);
|
||||
} else {
|
||||
// If not found in cache, check the current filtered results
|
||||
const key = groupByFiltersData?.payload?.attributeKeys?.find(
|
||||
(key) => key.key === element,
|
||||
);
|
||||
const key = groupByFiltersData?.payload?.attributeKeys?.find(
|
||||
(key) => key.key === element,
|
||||
);
|
||||
|
||||
if (key) {
|
||||
newGroupBy.push(key);
|
||||
}
|
||||
if (key) {
|
||||
groupBy.push(key);
|
||||
}
|
||||
}
|
||||
|
||||
setGroupBy(newGroupBy);
|
||||
setGroupBySearchValue('');
|
||||
setGroupBy(groupBy);
|
||||
},
|
||||
[groupByFiltersData, setGroupBy, allAvailableGroupByOptions],
|
||||
[groupByFiltersData, setGroupBy],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (groupByFiltersData?.payload) {
|
||||
// Update dropdown options
|
||||
setGroupByOptions(
|
||||
groupByFiltersData?.payload?.attributeKeys?.map((filter) => ({
|
||||
value: filter.key,
|
||||
label: filter.key,
|
||||
})) || [],
|
||||
);
|
||||
|
||||
// Cache all available options to preserve selected values using functional update
|
||||
// to avoid dependency on allAvailableGroupByOptions
|
||||
setAllAvailableGroupByOptions((prevOptions) => {
|
||||
const newOptions = { ...prevOptions };
|
||||
groupByFiltersData?.payload?.attributeKeys?.forEach((filter) => {
|
||||
newOptions[filter.key] = filter;
|
||||
});
|
||||
return newOptions;
|
||||
});
|
||||
}
|
||||
}, [groupByFiltersData]); // Only depends on groupByFiltersData now
|
||||
}, [groupByFiltersData]);
|
||||
|
||||
// Cache existing selected options on component mount
|
||||
useEffect(() => {
|
||||
if (groupBy && groupBy.length > 0) {
|
||||
setAllAvailableGroupByOptions((prevOptions) => {
|
||||
const newOptions = { ...prevOptions };
|
||||
groupBy.forEach((option) => {
|
||||
newOptions[option.key] = option;
|
||||
});
|
||||
return newOptions;
|
||||
});
|
||||
const { maxTime, minTime } = useSelector<AppState, GlobalReducer>(
|
||||
(state) => state.globalTime,
|
||||
);
|
||||
|
||||
const queryPayloads = useMemo(
|
||||
() =>
|
||||
getEndPointsQueryPayload(
|
||||
groupBy,
|
||||
domainName,
|
||||
Math.floor(minTime / 1e9),
|
||||
Math.floor(maxTime / 1e9),
|
||||
),
|
||||
[groupBy, domainName, minTime, maxTime],
|
||||
);
|
||||
|
||||
// Since only one query here
|
||||
const endPointsDataQueries = useQueries(
|
||||
queryPayloads.map((payload) => ({
|
||||
queryKey: [
|
||||
REACT_QUERY_KEY.GET_ENDPOINTS_LIST_BY_DOMAIN,
|
||||
payload,
|
||||
ENTITY_VERSION_V4,
|
||||
groupBy,
|
||||
],
|
||||
queryFn: (): Promise<SuccessResponse<MetricRangePayloadProps>> =>
|
||||
GetMetricQueryRange(payload, ENTITY_VERSION_V4),
|
||||
enabled: !!payload,
|
||||
staleTime: 60 * 1000, // 1 minute stale time : optimize this part
|
||||
})),
|
||||
);
|
||||
|
||||
const endPointsDataQuery = endPointsDataQueries[0];
|
||||
const {
|
||||
data: allEndPointsData,
|
||||
isLoading,
|
||||
isRefetching,
|
||||
isError,
|
||||
refetch,
|
||||
} = endPointsDataQuery;
|
||||
|
||||
const endPointsColumnsConfig = useMemo(
|
||||
() => getEndPointsColumnsConfig(groupBy.length > 0, expandedRowKeys),
|
||||
[groupBy.length, expandedRowKeys],
|
||||
);
|
||||
|
||||
const expandedRowRender = (record: EndPointsTableRowData): JSX.Element => (
|
||||
<ExpandedRow
|
||||
domainName={domainName}
|
||||
selectedRowData={record}
|
||||
setSelectedEndPointName={setSelectedEndPointName}
|
||||
setSelectedView={setSelectedView}
|
||||
/>
|
||||
);
|
||||
|
||||
const handleGroupByRowClick = (record: EndPointsTableRowData): void => {
|
||||
if (expandedRowKeys.includes(record.key)) {
|
||||
setExpandedRowKeys(expandedRowKeys.filter((key) => key !== record.key));
|
||||
} else {
|
||||
setExpandedRowKeys((expandedRowKeys) => [...expandedRowKeys, record.key]);
|
||||
}
|
||||
}, [groupBy]); // Removed allAvailableGroupByOptions from dependencies
|
||||
};
|
||||
|
||||
const currentQuery = initialQueriesMap[DataSource.TRACES];
|
||||
const handleRowClick = (record: EndPointsTableRowData): void => {
|
||||
if (groupBy.length === 0) {
|
||||
setSelectedEndPointName(record.endpointName); // this will open up the endpoint details tab
|
||||
setSelectedView(VIEW_TYPES.ENDPOINT_DETAILS);
|
||||
logEvent('API Monitoring: Endpoint name row clicked', {});
|
||||
} else {
|
||||
handleGroupByRowClick(record); // this will prepare the nested query payload
|
||||
}
|
||||
};
|
||||
|
||||
// Local state for filters, combining endpoint filter and search filters
|
||||
const [filters, setFilters] = useState<IBuilderQuery['filters']>(() => {
|
||||
// Initialize filters based on the initial endPointName prop
|
||||
const initialItems = [...initialFilters.items];
|
||||
return { op: 'AND', items: initialItems };
|
||||
});
|
||||
|
||||
// Handler for changes from the QueryBuilderSearchV2 component
|
||||
const handleFilterChange = useCallback(
|
||||
(newFilters: IBuilderQuery['filters']): void => {
|
||||
// 1. Update local filters state immediately
|
||||
setFilters(newFilters);
|
||||
},
|
||||
[], // Dependencies for the callback
|
||||
const formattedEndPointsData = useMemo(
|
||||
() =>
|
||||
formatEndPointsDataForTable(
|
||||
allEndPointsData?.payload?.data?.result[0]?.table?.rows,
|
||||
groupBy,
|
||||
),
|
||||
[groupBy, allEndPointsData],
|
||||
);
|
||||
|
||||
const updatedCurrentQuery = useMemo(
|
||||
() => ({
|
||||
...currentQuery,
|
||||
builder: {
|
||||
...currentQuery.builder,
|
||||
queryData: [
|
||||
{
|
||||
...currentQuery.builder.queryData[0],
|
||||
dataSource: DataSource.TRACES,
|
||||
filters, // Use the local filters state
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
[filters, currentQuery],
|
||||
);
|
||||
|
||||
const query = updatedCurrentQuery?.builder?.queryData[0] || null;
|
||||
|
||||
const allEndpointsWidgetData = useMemo(
|
||||
() => getAllEndpointsWidgetData(groupBy, domainName, filters),
|
||||
[groupBy, domainName, filters],
|
||||
);
|
||||
|
||||
const onRowClick = useCallback(
|
||||
(props: any): void => {
|
||||
setSelectedEndPointName(props[SPAN_ATTRIBUTES.URL_PATH] as string);
|
||||
setSelectedView(VIEWS.ENDPOINT_STATS);
|
||||
const initialItems = [
|
||||
...filters.items,
|
||||
...getGroupByFiltersFromGroupByValues(props, groupBy).items,
|
||||
];
|
||||
setInitialFiltersEndPointStats({
|
||||
items: initialItems,
|
||||
op: 'AND',
|
||||
});
|
||||
},
|
||||
[
|
||||
filters,
|
||||
setInitialFiltersEndPointStats,
|
||||
setSelectedEndPointName,
|
||||
setSelectedView,
|
||||
groupBy,
|
||||
],
|
||||
);
|
||||
if (isError) {
|
||||
return (
|
||||
<div className="all-endpoints-error-state-wrapper">
|
||||
<ErrorState refetch={refetch} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="all-endpoints-container">
|
||||
<div className="all-endpoints-header">
|
||||
<div className="filter-container">
|
||||
<QueryBuilderSearchV2
|
||||
query={query}
|
||||
onChange={handleFilterChange}
|
||||
placeholder="Search for filters..."
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="group-by-container">
|
||||
<div className="group-by-label"> Group by </div>
|
||||
<Select
|
||||
@@ -205,17 +189,49 @@ function AllEndPoints({
|
||||
placeholder="Search for attribute"
|
||||
options={groupByOptions}
|
||||
onChange={handleGroupByChange}
|
||||
onSearch={(value: string): void => setGroupBySearchValue(value)}
|
||||
/>{' '}
|
||||
</div>
|
||||
<div className="endpoints-table-container">
|
||||
<GridCard
|
||||
widget={allEndpointsWidgetData}
|
||||
isQueryEnabled
|
||||
onDragSelect={(): void => {}}
|
||||
customOnDragSelect={(): void => {}}
|
||||
customTimeRange={timeRange}
|
||||
customOnRowClick={onRowClick}
|
||||
<div className="endpoints-table-header">Endpoint overview</div>
|
||||
<Table
|
||||
columns={endPointsColumnsConfig}
|
||||
loading={{
|
||||
spinning: isLoading || isRefetching,
|
||||
indicator: <Spin indicator={<LoadingOutlined size={14} spin />} />,
|
||||
}}
|
||||
dataSource={isLoading || isRefetching ? [] : formattedEndPointsData}
|
||||
locale={{
|
||||
emptyText:
|
||||
isLoading || isRefetching ? null : (
|
||||
<div className="no-filtered-endpoints-message-container">
|
||||
<div className="no-filtered-endpoints-message-content">
|
||||
<img
|
||||
src="/Icons/emptyState.svg"
|
||||
alt="thinking-emoji"
|
||||
className="empty-state-svg"
|
||||
/>
|
||||
|
||||
<Typography.Text className="no-filtered-endpoints-message">
|
||||
This query had no results. Edit your query and try again!
|
||||
</Typography.Text>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
}}
|
||||
scroll={{ x: true }}
|
||||
tableLayout="fixed"
|
||||
onRow={(record): { onClick: () => void; className: string } => ({
|
||||
onClick: (): void => handleRowClick(record),
|
||||
className: 'clickable-row',
|
||||
})}
|
||||
expandable={{
|
||||
expandedRowRender: groupBy.length > 0 ? expandedRowRender : undefined,
|
||||
expandedRowKeys,
|
||||
expandIconColumnIndex: -1,
|
||||
}}
|
||||
rowClassName={(_, index): string =>
|
||||
index % 2 === 0 ? 'table-row-dark' : 'table-row-light'
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -12,12 +12,6 @@
|
||||
background: var(--bg-ink-300);
|
||||
box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.domain-details-drawer-header-right-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.domain-detail-drawer {
|
||||
@@ -252,9 +246,6 @@
|
||||
border: 1px solid var(--bg-slate-500);
|
||||
|
||||
.endpoints-table-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 12px;
|
||||
color: var(--Vanilla-100, #fff);
|
||||
font-family: Inter;
|
||||
@@ -308,7 +299,6 @@
|
||||
|
||||
.ant-table-tbody > tr:hover > td {
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.ant-table-cell:first-child {
|
||||
@@ -396,21 +386,6 @@
|
||||
padding-top: 20px;
|
||||
}
|
||||
|
||||
.top-errors-dropdown-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
|
||||
.endpoint-details-filters-container-dropdown {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.endpoint-details-filters-container-search {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.endpoint-details-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -715,140 +690,30 @@
|
||||
border-radius: 3px;
|
||||
border: 1px solid var(--bg-slate-500);
|
||||
|
||||
.title-wrapper {
|
||||
display: inline-flex;
|
||||
padding: 1px 2px;
|
||||
align-items: center;
|
||||
border-radius: 2px;
|
||||
background: rgba(113, 144, 249, 0.08);
|
||||
.top-services-title {
|
||||
border-bottom: 1px solid var(--bg-slate-500);
|
||||
padding: 10px 12px;
|
||||
border-radius: 3px 3px 0px 0px;
|
||||
background: rgba(171, 189, 255, 0.04);
|
||||
|
||||
color: var(--bg-robin-400);
|
||||
font-family: Inter;
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 18px;
|
||||
letter-spacing: -0.07px;
|
||||
.title-wrapper {
|
||||
display: inline-flex;
|
||||
padding: 1px 2px;
|
||||
align-items: center;
|
||||
border-radius: 2px;
|
||||
background: rgba(113, 144, 249, 0.08);
|
||||
|
||||
color: var(--bg-robin-400);
|
||||
font-family: Inter;
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 18px;
|
||||
letter-spacing: -0.07px;
|
||||
}
|
||||
}
|
||||
.dependent-services-container {
|
||||
border-radius: 3px;
|
||||
border: 1px solid var(--bg-slate-500);
|
||||
.ant-table {
|
||||
.ant-table-thead > tr > th {
|
||||
padding: 12px;
|
||||
font-weight: 500;
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
border-bottom: none;
|
||||
|
||||
color: var(--text-vanilla-400);
|
||||
font-family: Inter;
|
||||
font-size: 11px;
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
line-height: 18px;
|
||||
/* 163.636% */
|
||||
letter-spacing: 0.44px;
|
||||
text-transform: uppercase;
|
||||
background: none;
|
||||
|
||||
&::before {
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-table-thead > tr > th:has(.status-code-header) {
|
||||
background: var(--bg-ink-300);
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.ant-table-cell {
|
||||
padding: 12px;
|
||||
font-size: 13px;
|
||||
line-height: 20px;
|
||||
color: var(--bg-vanilla-100);
|
||||
border-bottom: none;
|
||||
background: var(--bg-ink-400);
|
||||
}
|
||||
|
||||
.ant-table-cell:has(.col-title) {
|
||||
background: rgba(171, 189, 255, 0.04);
|
||||
}
|
||||
|
||||
.ant-table-cell:has(.top-services-item-latency) {
|
||||
text-align: center;
|
||||
opacity: 0.8;
|
||||
background: rgba(171, 189, 255, 0.04);
|
||||
}
|
||||
|
||||
.ant-table-cell:has(.top-services-item-latency-title) {
|
||||
text-align: center;
|
||||
opacity: 0.8;
|
||||
background: rgba(171, 189, 255, 0.04);
|
||||
}
|
||||
|
||||
.ant-table-tbody > tr:hover > td {
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
}
|
||||
|
||||
.ant-table-cell:first-child {
|
||||
text-align: justify;
|
||||
}
|
||||
|
||||
.ant-table-cell:nth-child(2) {
|
||||
padding-left: 16px;
|
||||
padding-right: 16px;
|
||||
}
|
||||
|
||||
.ant-table-cell:nth-child(n + 3) {
|
||||
padding-right: 24px;
|
||||
}
|
||||
|
||||
.ant-table-tbody > tr > td {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.ant-table-thead
|
||||
> tr
|
||||
> th:not(:last-child):not(.ant-table-selection-column):not(.ant-table-row-expand-icon-cell):not([colspan])::before {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.ant-empty-normal {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.table-row-dark {
|
||||
background: var(--bg-ink-300);
|
||||
}
|
||||
|
||||
.ant-table-content {
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
.no-status-code-data-message-container {
|
||||
height: 30vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
.no-status-code-data-message-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
justify-content: center;
|
||||
|
||||
width: fit-content;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.no-status-code-data-message {
|
||||
margin-top: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
padding: 10px 12px;
|
||||
.top-services-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
@@ -878,7 +743,6 @@
|
||||
|
||||
.top-services-item-progress-bar {
|
||||
background-color: var(--bg-slate-400);
|
||||
border-radius: 2px;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
@@ -894,7 +758,7 @@
|
||||
|
||||
.top-services-load-more {
|
||||
border-top: 1px solid var(--bg-slate-500);
|
||||
padding: 10px;
|
||||
padding-top: 10px;
|
||||
|
||||
color: var(--text-vanilla-400);
|
||||
font-family: Inter;
|
||||
|
||||
@@ -3,27 +3,15 @@ import './DomainDetails.styles.scss';
|
||||
import { Color, Spacing } from '@signozhq/design-tokens';
|
||||
import { Button, Divider, Drawer, Radio, Typography } from 'antd';
|
||||
import { RadioChangeEvent } from 'antd/lib';
|
||||
import DateTimeSelectionV2 from 'container/TopNav/DateTimeSelectionV2';
|
||||
import {
|
||||
CustomTimeType,
|
||||
Time,
|
||||
} from 'container/TopNav/DateTimeSelectionV2/config';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import GetMinMax from 'lib/getMinMax';
|
||||
import { ArrowDown, ArrowUp, X } from 'lucide-react';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { useState } from 'react';
|
||||
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
|
||||
import AllEndPoints from './AllEndPoints';
|
||||
import DomainMetrics from './components/DomainMetrics';
|
||||
import { VIEW_TYPES, VIEWS } from './constants';
|
||||
import EndPointDetails from './EndPointDetails';
|
||||
import TopErrors from './TopErrors';
|
||||
|
||||
const TimeRangeOffset = 1000000000;
|
||||
import EndPointDetailsWrapper from './EndPointDetailsWrapper';
|
||||
|
||||
function DomainDetails({
|
||||
domainData,
|
||||
@@ -45,58 +33,12 @@ function DomainDetails({
|
||||
const [endPointsGroupBy, setEndPointsGroupBy] = useState<
|
||||
IBuilderQuery['groupBy']
|
||||
>([]);
|
||||
const [initialFiltersEndPointStats, setInitialFiltersEndPointStats] = useState<
|
||||
IBuilderQuery['filters']
|
||||
>(domainListFilters);
|
||||
const isDarkMode = useIsDarkMode();
|
||||
|
||||
const handleTabChange = (e: RadioChangeEvent): void => {
|
||||
setSelectedView(e.target.value);
|
||||
};
|
||||
|
||||
const { maxTime, minTime, selectedTime } = useSelector<
|
||||
AppState,
|
||||
GlobalReducer
|
||||
>((state) => state.globalTime);
|
||||
|
||||
const startMs = useMemo(() => Math.floor(Number(minTime) / TimeRangeOffset), [
|
||||
minTime,
|
||||
]);
|
||||
const endMs = useMemo(() => Math.floor(Number(maxTime) / TimeRangeOffset), [
|
||||
maxTime,
|
||||
]);
|
||||
|
||||
const [selectedInterval, setSelectedInterval] = useState<Time>(
|
||||
selectedTime as Time,
|
||||
);
|
||||
|
||||
const [modalTimeRange, setModalTimeRange] = useState(() => ({
|
||||
startTime: startMs,
|
||||
endTime: endMs,
|
||||
}));
|
||||
|
||||
const handleTimeChange = useCallback(
|
||||
(interval: Time | CustomTimeType, dateTimeRange?: [number, number]): void => {
|
||||
setSelectedInterval(interval as Time);
|
||||
|
||||
if (interval === 'custom' && dateTimeRange) {
|
||||
setModalTimeRange({
|
||||
startTime: Math.floor(dateTimeRange[0] / 1000),
|
||||
endTime: Math.floor(dateTimeRange[1] / 1000),
|
||||
});
|
||||
} else {
|
||||
const { maxTime, minTime } = GetMinMax(interval);
|
||||
|
||||
setModalTimeRange({
|
||||
startTime: Math.floor(minTime / TimeRangeOffset),
|
||||
endTime: Math.floor(maxTime / TimeRangeOffset),
|
||||
});
|
||||
}
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[],
|
||||
);
|
||||
|
||||
return (
|
||||
<Drawer
|
||||
width="60%"
|
||||
@@ -108,44 +50,32 @@ function DomainDetails({
|
||||
{domainData.domainName}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
<div className="domain-details-drawer-header-right-container">
|
||||
<DateTimeSelectionV2
|
||||
showAutoRefresh={false}
|
||||
showRefreshText={false}
|
||||
onTimeChange={handleTimeChange}
|
||||
defaultRelativeTime="5m"
|
||||
isModalTimeSelection
|
||||
modalSelectedInterval={selectedInterval}
|
||||
modalInitialStartTime={modalTimeRange.startTime * 1000}
|
||||
modalInitialEndTime={modalTimeRange.endTime * 1000}
|
||||
<Button.Group className="domain-details-drawer-header-ctas">
|
||||
<Button
|
||||
className="domain-navigate-cta"
|
||||
onClick={(): void => {
|
||||
setSelectedDomainIndex(selectedDomainIndex - 1);
|
||||
setSelectedEndPointName('');
|
||||
setEndPointsGroupBy([]);
|
||||
setSelectedView(VIEW_TYPES.ALL_ENDPOINTS);
|
||||
}}
|
||||
icon={<ArrowUp size={16} />}
|
||||
disabled={selectedDomainIndex === 0}
|
||||
title="Previous domain"
|
||||
/>
|
||||
<Button.Group className="domain-details-drawer-header-ctas">
|
||||
<Button
|
||||
className="domain-navigate-cta"
|
||||
onClick={(): void => {
|
||||
setSelectedDomainIndex(selectedDomainIndex - 1);
|
||||
setSelectedEndPointName('');
|
||||
setEndPointsGroupBy([]);
|
||||
setSelectedView(VIEW_TYPES.ALL_ENDPOINTS);
|
||||
}}
|
||||
icon={<ArrowUp size={16} />}
|
||||
disabled={selectedDomainIndex === 0}
|
||||
title="Previous domain"
|
||||
/>
|
||||
<Button
|
||||
className="domain-navigate-cta"
|
||||
onClick={(): void => {
|
||||
setSelectedDomainIndex(selectedDomainIndex + 1);
|
||||
setSelectedEndPointName('');
|
||||
setEndPointsGroupBy([]);
|
||||
setSelectedView(VIEW_TYPES.ALL_ENDPOINTS);
|
||||
}}
|
||||
icon={<ArrowDown size={16} />}
|
||||
disabled={selectedDomainIndex === domainListLength - 1}
|
||||
title="Next domain"
|
||||
/>
|
||||
</Button.Group>
|
||||
</div>
|
||||
<Button
|
||||
className="domain-navigate-cta"
|
||||
onClick={(): void => {
|
||||
setSelectedDomainIndex(selectedDomainIndex + 1);
|
||||
setSelectedEndPointName('');
|
||||
setEndPointsGroupBy([]);
|
||||
setSelectedView(VIEW_TYPES.ALL_ENDPOINTS);
|
||||
}}
|
||||
icon={<ArrowDown size={16} />}
|
||||
disabled={selectedDomainIndex === domainListLength - 1}
|
||||
title="Next domain"
|
||||
/>
|
||||
</Button.Group>
|
||||
</div>
|
||||
}
|
||||
placement="right"
|
||||
@@ -161,11 +91,7 @@ function DomainDetails({
|
||||
>
|
||||
{domainData && (
|
||||
<>
|
||||
<DomainMetrics
|
||||
domainName={domainData.domainName}
|
||||
domainListFilters={domainListFilters}
|
||||
timeRange={modalTimeRange}
|
||||
/>
|
||||
<DomainMetrics domainData={domainData} />
|
||||
<div className="views-tabs-container">
|
||||
<Radio.Group
|
||||
className="views-tabs"
|
||||
@@ -183,21 +109,13 @@ function DomainDetails({
|
||||
</Radio.Button>
|
||||
<Radio.Button
|
||||
className={
|
||||
selectedView === VIEW_TYPES.ENDPOINT_STATS
|
||||
selectedView === VIEW_TYPES.ENDPOINT_DETAILS
|
||||
? 'tab selected_view'
|
||||
: 'tab'
|
||||
}
|
||||
value={VIEW_TYPES.ENDPOINT_STATS}
|
||||
value={VIEW_TYPES.ENDPOINT_DETAILS}
|
||||
>
|
||||
<div className="view-title">Endpoint(s) Stats</div>
|
||||
</Radio.Button>
|
||||
<Radio.Button
|
||||
className={
|
||||
selectedView === VIEW_TYPES.TOP_ERRORS ? 'tab selected_view' : 'tab'
|
||||
}
|
||||
value={VIEW_TYPES.TOP_ERRORS}
|
||||
>
|
||||
<div className="view-title">Top 10 Errors</div>
|
||||
<div className="view-title">Endpoint Details</div>
|
||||
</Radio.Button>
|
||||
</Radio.Group>
|
||||
</div>
|
||||
@@ -208,28 +126,15 @@ function DomainDetails({
|
||||
setSelectedView={setSelectedView}
|
||||
groupBy={endPointsGroupBy}
|
||||
setGroupBy={setEndPointsGroupBy}
|
||||
timeRange={modalTimeRange}
|
||||
initialFilters={domainListFilters}
|
||||
setInitialFiltersEndPointStats={setInitialFiltersEndPointStats}
|
||||
/>
|
||||
)}
|
||||
|
||||
{selectedView === VIEW_TYPES.ENDPOINT_STATS && (
|
||||
<EndPointDetails
|
||||
{selectedView === VIEW_TYPES.ENDPOINT_DETAILS && (
|
||||
<EndPointDetailsWrapper
|
||||
domainName={domainData.domainName}
|
||||
endPointName={selectedEndPointName}
|
||||
setSelectedEndPointName={setSelectedEndPointName}
|
||||
initialFilters={initialFiltersEndPointStats}
|
||||
timeRange={modalTimeRange}
|
||||
handleTimeChange={handleTimeChange}
|
||||
/>
|
||||
)}
|
||||
|
||||
{selectedView === VIEW_TYPES.TOP_ERRORS && (
|
||||
<TopErrors
|
||||
domainName={domainData.domainName}
|
||||
timeRange={modalTimeRange}
|
||||
initialFilters={domainListFilters}
|
||||
domainListFilters={domainListFilters}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -8,18 +8,16 @@ import {
|
||||
getRateOverTimeWidgetData,
|
||||
} from 'container/ApiMonitoring/utils';
|
||||
import QueryBuilderSearchV2 from 'container/QueryBuilder/filters/QueryBuilderSearchV2/QueryBuilderSearchV2';
|
||||
import {
|
||||
CustomTimeType,
|
||||
Time,
|
||||
} from 'container/TopNav/DateTimeSelectionV2/config';
|
||||
import { GetMetricQueryRange } from 'lib/dashboard/getQueryResults';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { useQueries } from 'react-query';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { SuccessResponse } from 'types/api';
|
||||
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
|
||||
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
|
||||
import DependentServices from './components/DependentServices';
|
||||
import EndPointMetrics from './components/EndPointMetrics';
|
||||
@@ -27,107 +25,33 @@ import EndPointsDropDown from './components/EndPointsDropDown';
|
||||
import MetricOverTimeGraph from './components/MetricOverTimeGraph';
|
||||
import StatusCodeBarCharts from './components/StatusCodeBarCharts';
|
||||
import StatusCodeTable from './components/StatusCodeTable';
|
||||
import { SPAN_ATTRIBUTES } from './constants';
|
||||
|
||||
const httpUrlKey = {
|
||||
dataType: DataTypes.String,
|
||||
isColumn: false,
|
||||
isJSON: false,
|
||||
key: SPAN_ATTRIBUTES.URL_PATH,
|
||||
type: 'tag',
|
||||
};
|
||||
|
||||
function EndPointDetails({
|
||||
domainName,
|
||||
endPointName,
|
||||
setSelectedEndPointName,
|
||||
initialFilters,
|
||||
timeRange,
|
||||
handleTimeChange,
|
||||
domainListFilters,
|
||||
}: {
|
||||
domainName: string;
|
||||
endPointName: string;
|
||||
setSelectedEndPointName: (value: string) => void;
|
||||
initialFilters: IBuilderQuery['filters'];
|
||||
timeRange: {
|
||||
startTime: number;
|
||||
endTime: number;
|
||||
};
|
||||
handleTimeChange: (
|
||||
interval: Time | CustomTimeType,
|
||||
dateTimeRange?: [number, number],
|
||||
) => void;
|
||||
domainListFilters: IBuilderQuery['filters'];
|
||||
}): JSX.Element {
|
||||
const { startTime: minTime, endTime: maxTime } = timeRange;
|
||||
const { maxTime, minTime } = useSelector<AppState, GlobalReducer>(
|
||||
(state) => state.globalTime,
|
||||
);
|
||||
|
||||
const currentQuery = initialQueriesMap[DataSource.TRACES];
|
||||
|
||||
// Local state for filters, combining endpoint filter and search filters
|
||||
const [filters, setFilters] = useState<IBuilderQuery['filters']>(() => {
|
||||
// Initialize filters based on the initial endPointName prop
|
||||
const initialItems = [...initialFilters.items];
|
||||
if (endPointName) {
|
||||
initialItems.push({
|
||||
id: '92b8a1c1',
|
||||
key: httpUrlKey,
|
||||
op: '=',
|
||||
value: endPointName,
|
||||
});
|
||||
}
|
||||
return { op: 'AND', items: initialItems };
|
||||
const [filters, setFilters] = useState<IBuilderQuery['filters']>({
|
||||
op: 'AND',
|
||||
items: [],
|
||||
});
|
||||
|
||||
// Effect to synchronize local filters when the endPointName prop changes (e.g., from dropdown)
|
||||
useEffect(() => {
|
||||
setFilters((currentFilters) => {
|
||||
const existingHttpUrlFilter = currentFilters.items.find(
|
||||
(item) => item.key?.key === httpUrlKey.key,
|
||||
);
|
||||
const existingHttpUrlValue = (existingHttpUrlFilter?.value as string) || '';
|
||||
|
||||
// Only update filters if the prop value is different from what's already in filters
|
||||
if (endPointName === existingHttpUrlValue) {
|
||||
return currentFilters; // No change needed, prevents loop
|
||||
}
|
||||
|
||||
// Rebuild filters: Keep non-http.url filters and add/update http.url filter based on prop
|
||||
const otherFilters = currentFilters.items.filter(
|
||||
(item) => item.key?.key !== httpUrlKey.key,
|
||||
);
|
||||
const newItems = [...otherFilters];
|
||||
if (endPointName) {
|
||||
newItems.push({
|
||||
id: '92b8a1c1',
|
||||
key: httpUrlKey,
|
||||
op: '=',
|
||||
value: endPointName,
|
||||
});
|
||||
}
|
||||
return { op: 'AND', items: newItems };
|
||||
});
|
||||
}, [endPointName]);
|
||||
|
||||
// Handler for changes from the QueryBuilderSearchV2 component
|
||||
const handleFilterChange = useCallback(
|
||||
(newFilters: IBuilderQuery['filters']): void => {
|
||||
// 1. Update local filters state immediately
|
||||
setFilters(newFilters);
|
||||
|
||||
// 2. Derive the endpoint name from the *new* filters state
|
||||
const httpUrlFilter = newFilters.items.find(
|
||||
(item) => item.key?.key === httpUrlKey.key,
|
||||
);
|
||||
const derivedEndPointName = (httpUrlFilter?.value as string) || '';
|
||||
|
||||
// 3. If the derived endpoint name is different from the current prop,
|
||||
// it means the search change modified the effective endpoint.
|
||||
// Notify the parent component.
|
||||
if (derivedEndPointName !== endPointName) {
|
||||
setSelectedEndPointName(derivedEndPointName);
|
||||
}
|
||||
},
|
||||
[endPointName, setSelectedEndPointName], // Dependencies for the callback
|
||||
);
|
||||
// Manually update the query to include the filters
|
||||
// Because using the hook is causing the global domain
|
||||
// query to be updated and causing main domain list to
|
||||
// refetch with the filters of endpoints
|
||||
|
||||
const updatedCurrentQuery = useMemo(
|
||||
() => ({
|
||||
@@ -138,7 +62,7 @@ function EndPointDetails({
|
||||
{
|
||||
...currentQuery.builder.queryData[0],
|
||||
dataSource: DataSource.TRACES,
|
||||
filters, // Use the local filters state
|
||||
filters,
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -154,8 +78,15 @@ function EndPointDetails({
|
||||
);
|
||||
|
||||
const endPointDetailsQueryPayload = useMemo(
|
||||
() => getEndPointDetailsQueryPayload(domainName, minTime, maxTime, filters),
|
||||
[domainName, filters, minTime, maxTime],
|
||||
() =>
|
||||
getEndPointDetailsQueryPayload(
|
||||
domainName,
|
||||
endPointName,
|
||||
Math.floor(minTime / 1e9),
|
||||
Math.floor(maxTime / 1e9),
|
||||
filters,
|
||||
),
|
||||
[domainName, endPointName, filters, minTime, maxTime],
|
||||
);
|
||||
|
||||
const endPointDetailsDataQueries = useQueries(
|
||||
@@ -163,7 +94,7 @@ function EndPointDetails({
|
||||
queryKey: [
|
||||
END_POINT_DETAILS_QUERY_KEYS_ARRAY[index],
|
||||
payload,
|
||||
filters.items, // Include filters.items in queryKey for better caching
|
||||
filters.items,
|
||||
ENTITY_VERSION_V4,
|
||||
],
|
||||
queryFn: (): Promise<SuccessResponse<MetricRangePayloadProps>> =>
|
||||
@@ -192,30 +123,22 @@ function EndPointDetails({
|
||||
);
|
||||
|
||||
const { endpoint, port } = useMemo(
|
||||
() => extractPortAndEndpoint(endPointName), // Derive display info from the prop
|
||||
() => extractPortAndEndpoint(endPointName),
|
||||
[endPointName],
|
||||
);
|
||||
|
||||
const [rateOverTimeWidget, latencyOverTimeWidget] = useMemo(
|
||||
() => [
|
||||
getRateOverTimeWidgetData(domainName, endPointName, filters),
|
||||
getLatencyOverTimeWidgetData(domainName, endPointName, filters),
|
||||
getRateOverTimeWidgetData(domainName, endPointName, {
|
||||
items: [...domainListFilters.items, ...filters.items],
|
||||
op: filters.op,
|
||||
}),
|
||||
getLatencyOverTimeWidgetData(domainName, endPointName, {
|
||||
items: [...domainListFilters.items, ...filters.items],
|
||||
op: filters.op,
|
||||
}),
|
||||
],
|
||||
[domainName, endPointName, filters], // Use combinedFilters
|
||||
);
|
||||
|
||||
// // [TODO] Fix this later
|
||||
const onDragSelect = useCallback(
|
||||
(start: number, end: number) => {
|
||||
const startTimestamp = Math.trunc(start);
|
||||
const endTimestamp = Math.trunc(end);
|
||||
|
||||
if (startTimestamp !== endTimestamp) {
|
||||
// update the value in local time picker
|
||||
handleTimeChange('custom', [startTimestamp, endTimestamp]);
|
||||
}
|
||||
},
|
||||
[handleTimeChange],
|
||||
[domainName, endPointName, filters, domainListFilters],
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -233,7 +156,9 @@ function EndPointDetails({
|
||||
<div className="endpoint-details-filters-container-search">
|
||||
<QueryBuilderSearchV2
|
||||
query={query}
|
||||
onChange={handleFilterChange}
|
||||
onChange={(searchFilters): void => {
|
||||
setFilters(searchFilters);
|
||||
}}
|
||||
placeholder="Search for filters..."
|
||||
/>
|
||||
</div>
|
||||
@@ -241,9 +166,7 @@ function EndPointDetails({
|
||||
<div className="endpoint-meta-data">
|
||||
<div className="endpoint-meta-data-pill">
|
||||
<div className="endpoint-meta-data-label">Endpoint</div>
|
||||
<div className="endpoint-meta-data-value">
|
||||
{endpoint || 'All Endpoints'}
|
||||
</div>
|
||||
<div className="endpoint-meta-data-value">{endpoint || '-'}</div>
|
||||
</div>
|
||||
<div className="endpoint-meta-data-pill">
|
||||
<div className="endpoint-meta-data-label">Port</div>
|
||||
@@ -254,7 +177,6 @@ function EndPointDetails({
|
||||
{!isServicesFilterApplied && (
|
||||
<DependentServices
|
||||
dependentServicesQuery={endPointDependentServicesDataQuery}
|
||||
timeRange={timeRange}
|
||||
/>
|
||||
)}
|
||||
<StatusCodeBarCharts
|
||||
@@ -264,21 +186,12 @@ function EndPointDetails({
|
||||
}
|
||||
domainName={domainName}
|
||||
endPointName={endPointName}
|
||||
domainListFilters={domainListFilters}
|
||||
filters={filters}
|
||||
timeRange={timeRange}
|
||||
onDragSelect={onDragSelect}
|
||||
/>
|
||||
<StatusCodeTable endPointStatusCodeDataQuery={endPointStatusCodeDataQuery} />
|
||||
<MetricOverTimeGraph
|
||||
widget={rateOverTimeWidget}
|
||||
timeRange={timeRange}
|
||||
onDragSelect={onDragSelect}
|
||||
/>
|
||||
<MetricOverTimeGraph
|
||||
widget={latencyOverTimeWidget}
|
||||
timeRange={timeRange}
|
||||
onDragSelect={onDragSelect}
|
||||
/>
|
||||
<MetricOverTimeGraph widget={rateOverTimeWidget} />
|
||||
<MetricOverTimeGraph widget={latencyOverTimeWidget} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
import { ENTITY_VERSION_V4 } from 'constants/app';
|
||||
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||
import { getEndPointZeroStateQueryPayload } from 'container/ApiMonitoring/utils';
|
||||
import { GetMetricQueryRange } from 'lib/dashboard/getQueryResults';
|
||||
import { useMemo } from 'react';
|
||||
import { useQueries } from 'react-query';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { SuccessResponse } from 'types/api';
|
||||
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
|
||||
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
|
||||
import EndPointDetailsZeroState from './components/EndPointDetailsZeroState';
|
||||
import EndPointDetails from './EndPointDetails';
|
||||
|
||||
function EndPointDetailsWrapper({
|
||||
domainName,
|
||||
endPointName,
|
||||
setSelectedEndPointName,
|
||||
domainListFilters,
|
||||
}: {
|
||||
domainName: string;
|
||||
endPointName: string;
|
||||
setSelectedEndPointName: (value: string) => void;
|
||||
domainListFilters: IBuilderQuery['filters'];
|
||||
}): JSX.Element {
|
||||
const { maxTime, minTime } = useSelector<AppState, GlobalReducer>(
|
||||
(state) => state.globalTime,
|
||||
);
|
||||
|
||||
const endPointZeroStateQueryPayload = useMemo(
|
||||
() =>
|
||||
getEndPointZeroStateQueryPayload(
|
||||
domainName,
|
||||
Math.floor(minTime / 1e9),
|
||||
Math.floor(maxTime / 1e9),
|
||||
),
|
||||
[domainName, minTime, maxTime],
|
||||
);
|
||||
|
||||
const endPointZeroStateDataQueries = useQueries(
|
||||
endPointZeroStateQueryPayload.map((payload) => ({
|
||||
queryKey: [
|
||||
// Since only one query here
|
||||
REACT_QUERY_KEY.GET_ENDPOINT_DROPDOWN_DATA,
|
||||
payload,
|
||||
ENTITY_VERSION_V4,
|
||||
],
|
||||
queryFn: (): Promise<SuccessResponse<MetricRangePayloadProps>> =>
|
||||
GetMetricQueryRange(payload, ENTITY_VERSION_V4),
|
||||
enabled: !!payload,
|
||||
})),
|
||||
);
|
||||
|
||||
const [endPointZeroStateDataQuery] = useMemo(
|
||||
() => [endPointZeroStateDataQueries[0]],
|
||||
[endPointZeroStateDataQueries],
|
||||
);
|
||||
|
||||
if (endPointName === '') {
|
||||
return (
|
||||
<EndPointDetailsZeroState
|
||||
setSelectedEndPointName={setSelectedEndPointName}
|
||||
endPointDropDownDataQuery={endPointZeroStateDataQuery}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<EndPointDetails
|
||||
domainName={domainName}
|
||||
endPointName={endPointName}
|
||||
setSelectedEndPointName={setSelectedEndPointName}
|
||||
domainListFilters={domainListFilters}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default EndPointDetailsWrapper;
|
||||
@@ -1,251 +0,0 @@
|
||||
import { LoadingOutlined } from '@ant-design/icons';
|
||||
import { Spin, Switch, Table, Tooltip, Typography } from 'antd';
|
||||
import { useNavigateToExplorer } from 'components/CeleryTask/useNavigateToExplorer';
|
||||
import { DEFAULT_ENTITY_VERSION, ENTITY_VERSION_V4 } from 'constants/app';
|
||||
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||
import {
|
||||
END_POINT_DETAILS_QUERY_KEYS_ARRAY,
|
||||
formatTopErrorsDataForTable,
|
||||
getEndPointDetailsQueryPayload,
|
||||
getTopErrorsColumnsConfig,
|
||||
getTopErrorsCoRelationQueryFilters,
|
||||
getTopErrorsQueryPayload,
|
||||
TopErrorsResponseRow,
|
||||
} from 'container/ApiMonitoring/utils';
|
||||
import { GetMetricQueryRange } from 'lib/dashboard/getQueryResults';
|
||||
import { Info } from 'lucide-react';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { useQueries } from 'react-query';
|
||||
import { SuccessResponse } from 'types/api';
|
||||
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
|
||||
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
import EndPointsDropDown from './components/EndPointsDropDown';
|
||||
import ErrorState from './components/ErrorState';
|
||||
import { SPAN_ATTRIBUTES } from './constants';
|
||||
|
||||
function TopErrors({
|
||||
domainName,
|
||||
timeRange,
|
||||
initialFilters,
|
||||
}: {
|
||||
domainName: string;
|
||||
timeRange: {
|
||||
startTime: number;
|
||||
endTime: number;
|
||||
};
|
||||
initialFilters: IBuilderQuery['filters'];
|
||||
}): JSX.Element {
|
||||
const { startTime: minTime, endTime: maxTime } = timeRange;
|
||||
|
||||
const [endPointName, setSelectedEndPointName] = useState<string>('');
|
||||
const [showStatusCodeErrors, setShowStatusCodeErrors] = useState<boolean>(
|
||||
true,
|
||||
);
|
||||
|
||||
const queryPayloads = useMemo(
|
||||
() =>
|
||||
getTopErrorsQueryPayload(
|
||||
domainName,
|
||||
minTime,
|
||||
maxTime,
|
||||
{
|
||||
items: endPointName
|
||||
? [
|
||||
{
|
||||
id: '92b8a1c1',
|
||||
key: {
|
||||
dataType: DataTypes.String,
|
||||
isColumn: false,
|
||||
isJSON: false,
|
||||
key: SPAN_ATTRIBUTES.URL_PATH,
|
||||
type: 'tag',
|
||||
},
|
||||
op: '=',
|
||||
value: endPointName,
|
||||
},
|
||||
...initialFilters.items,
|
||||
]
|
||||
: [...initialFilters.items],
|
||||
op: 'AND',
|
||||
},
|
||||
showStatusCodeErrors,
|
||||
),
|
||||
[
|
||||
domainName,
|
||||
endPointName,
|
||||
minTime,
|
||||
maxTime,
|
||||
initialFilters,
|
||||
showStatusCodeErrors,
|
||||
],
|
||||
);
|
||||
|
||||
const topErrorsDataQueries = useQueries(
|
||||
queryPayloads.map((payload) => ({
|
||||
queryKey: [
|
||||
REACT_QUERY_KEY.GET_TOP_ERRORS_BY_DOMAIN,
|
||||
payload,
|
||||
DEFAULT_ENTITY_VERSION,
|
||||
showStatusCodeErrors,
|
||||
],
|
||||
queryFn: (): Promise<SuccessResponse<MetricRangePayloadProps>> =>
|
||||
GetMetricQueryRange(payload, DEFAULT_ENTITY_VERSION),
|
||||
enabled: !!payload,
|
||||
staleTime: 0,
|
||||
cacheTime: 0,
|
||||
})),
|
||||
);
|
||||
|
||||
const topErrorsDataQuery = topErrorsDataQueries[0];
|
||||
const {
|
||||
data: topErrorsData,
|
||||
isLoading,
|
||||
isRefetching,
|
||||
isError,
|
||||
refetch,
|
||||
} = topErrorsDataQuery;
|
||||
|
||||
const topErrorsColumnsConfig = useMemo(() => getTopErrorsColumnsConfig(), []);
|
||||
|
||||
const formattedTopErrorsData = useMemo(
|
||||
() =>
|
||||
formatTopErrorsDataForTable(
|
||||
topErrorsData?.payload?.data?.result as TopErrorsResponseRow[],
|
||||
),
|
||||
[topErrorsData],
|
||||
);
|
||||
|
||||
const endPointDropDownQueryPayload = useMemo(
|
||||
() => [
|
||||
getEndPointDetailsQueryPayload(domainName, minTime, maxTime, {
|
||||
items: [],
|
||||
op: 'AND',
|
||||
})[2],
|
||||
],
|
||||
[domainName, minTime, maxTime],
|
||||
);
|
||||
|
||||
const endPointDropDownDataQueries = useQueries(
|
||||
endPointDropDownQueryPayload.map((payload) => ({
|
||||
queryKey: [
|
||||
END_POINT_DETAILS_QUERY_KEYS_ARRAY[4],
|
||||
payload,
|
||||
ENTITY_VERSION_V4,
|
||||
],
|
||||
queryFn: (): Promise<SuccessResponse<MetricRangePayloadProps>> =>
|
||||
GetMetricQueryRange(payload, ENTITY_VERSION_V4),
|
||||
enabled: !!payload,
|
||||
staleTime: 60 * 1000,
|
||||
})),
|
||||
);
|
||||
|
||||
const [endPointDropDownDataQuery] = useMemo(
|
||||
() => [endPointDropDownDataQueries[0]],
|
||||
[endPointDropDownDataQueries],
|
||||
);
|
||||
|
||||
const navigateToExplorer = useNavigateToExplorer();
|
||||
|
||||
if (isError) {
|
||||
return (
|
||||
<div className="all-endpoints-error-state-wrapper">
|
||||
<ErrorState refetch={refetch} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="all-endpoints-container">
|
||||
<div className="top-errors-dropdown-container">
|
||||
<div className="endpoint-details-filters-container-dropdown">
|
||||
<EndPointsDropDown
|
||||
selectedEndPointName={endPointName}
|
||||
setSelectedEndPointName={setSelectedEndPointName}
|
||||
endPointDropDownDataQuery={endPointDropDownDataQuery}
|
||||
parentContainerDiv=".endpoint-details-filters-container"
|
||||
/>
|
||||
</div>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
||||
<Switch
|
||||
checked={showStatusCodeErrors}
|
||||
onChange={setShowStatusCodeErrors}
|
||||
size="small"
|
||||
/>
|
||||
<span style={{ color: 'white', fontSize: '14px' }}>
|
||||
Status Message Exists
|
||||
</span>
|
||||
<Tooltip title="When enabled, shows errors that have a status message. When disabled, shows all errors regardless of status message">
|
||||
<Info size={16} color="white" />
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="endpoints-table-container">
|
||||
<div className="endpoints-table-header">
|
||||
{showStatusCodeErrors ? 'Errors with Status Message' : 'All Errors'}{' '}
|
||||
<Tooltip
|
||||
title={
|
||||
showStatusCodeErrors
|
||||
? 'Shows errors that have a status message'
|
||||
: 'Shows all errors regardless of status message'
|
||||
}
|
||||
>
|
||||
<Info size={16} color="white" />
|
||||
</Tooltip>
|
||||
</div>
|
||||
<Table
|
||||
columns={topErrorsColumnsConfig}
|
||||
loading={{
|
||||
spinning: isLoading || isRefetching,
|
||||
indicator: <Spin indicator={<LoadingOutlined size={14} spin />} />,
|
||||
}}
|
||||
dataSource={isLoading || isRefetching ? [] : formattedTopErrorsData}
|
||||
locale={{
|
||||
emptyText:
|
||||
isLoading || isRefetching ? null : (
|
||||
<div className="no-filtered-endpoints-message-container">
|
||||
<div className="no-filtered-endpoints-message-content">
|
||||
<img
|
||||
src="/Icons/emptyState.svg"
|
||||
alt="thinking-emoji"
|
||||
className="empty-state-svg"
|
||||
/>
|
||||
|
||||
<Typography.Text className="no-filtered-endpoints-message">
|
||||
This query had no results. Edit your query and try again!
|
||||
</Typography.Text>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
}}
|
||||
scroll={{ x: true }}
|
||||
tableLayout="fixed"
|
||||
rowClassName={(_, index): string =>
|
||||
index % 2 === 0 ? 'table-row-dark' : 'table-row-light'
|
||||
}
|
||||
onRow={(record): { onClick: () => void } => ({
|
||||
onClick: (): void => {
|
||||
const filters = getTopErrorsCoRelationQueryFilters(
|
||||
domainName,
|
||||
record.endpointName,
|
||||
record.statusCode,
|
||||
);
|
||||
navigateToExplorer({
|
||||
filters: [...filters.items],
|
||||
dataSource: DataSource.TRACES,
|
||||
startTime: minTime,
|
||||
endTime: maxTime,
|
||||
shouldResolveQuery: true,
|
||||
});
|
||||
},
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default TopErrors;
|
||||
@@ -1,13 +1,6 @@
|
||||
import '../DomainDetails.styles.scss';
|
||||
|
||||
import { Table, TablePaginationConfig, Typography } from 'antd';
|
||||
import { Typography } from 'antd';
|
||||
import Skeleton from 'antd/lib/skeleton';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import {
|
||||
dependentServicesColumns,
|
||||
DependentServicesData,
|
||||
getFormattedDependentServicesData,
|
||||
} from 'container/ApiMonitoring/utils';
|
||||
import { getFormattedDependentServicesData } from 'container/ApiMonitoring/utils';
|
||||
import { UnfoldVertical } from 'lucide-react';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { UseQueryResult } from 'react-query';
|
||||
@@ -17,15 +10,10 @@ import ErrorState from './ErrorState';
|
||||
|
||||
interface DependentServicesProps {
|
||||
dependentServicesQuery: UseQueryResult<SuccessResponse<any>, unknown>;
|
||||
timeRange: {
|
||||
startTime: number;
|
||||
endTime: number;
|
||||
};
|
||||
}
|
||||
|
||||
function DependentServices({
|
||||
dependentServicesQuery,
|
||||
timeRange,
|
||||
}: DependentServicesProps): JSX.Element {
|
||||
const {
|
||||
data,
|
||||
@@ -35,25 +23,19 @@ function DependentServices({
|
||||
isRefetching,
|
||||
} = dependentServicesQuery;
|
||||
|
||||
const [isExpanded, setIsExpanded] = useState<boolean>(false);
|
||||
const [currentRenderCount, setCurrentRenderCount] = useState(0);
|
||||
|
||||
const handleShowMoreClick = (): void => {
|
||||
setIsExpanded((prev) => !prev);
|
||||
};
|
||||
const dependentServicesData = useMemo(() => {
|
||||
const formattedDependentServicesData = getFormattedDependentServicesData(
|
||||
data?.payload?.data?.result[0].table.rows,
|
||||
);
|
||||
setCurrentRenderCount(Math.min(formattedDependentServicesData.length, 5));
|
||||
return formattedDependentServicesData;
|
||||
}, [data]);
|
||||
|
||||
const dependentServicesData = useMemo(
|
||||
(): DependentServicesData[] =>
|
||||
getFormattedDependentServicesData(data?.payload?.data?.result[0].table.rows),
|
||||
[data],
|
||||
);
|
||||
|
||||
const paginationConfig = useMemo(
|
||||
(): TablePaginationConfig => ({
|
||||
pageSize: isExpanded ? dependentServicesData.length : 5,
|
||||
hideOnSinglePage: true,
|
||||
position: ['none', 'none'],
|
||||
}),
|
||||
[isExpanded, dependentServicesData.length],
|
||||
const renderItems = useMemo(
|
||||
() => dependentServicesData.slice(0, currentRenderCount),
|
||||
[currentRenderCount, dependentServicesData],
|
||||
);
|
||||
|
||||
if (isLoading || isRefetching) {
|
||||
@@ -66,66 +48,56 @@ function DependentServices({
|
||||
|
||||
return (
|
||||
<div className="top-services-content">
|
||||
<div className="top-services-title">
|
||||
<span className="title-wrapper">Dependent Services</span>
|
||||
</div>
|
||||
<div className="dependent-services-container">
|
||||
<Table
|
||||
loading={isLoading || isRefetching}
|
||||
dataSource={dependentServicesData || []}
|
||||
columns={dependentServicesColumns}
|
||||
rowClassName="table-row-dark"
|
||||
pagination={paginationConfig}
|
||||
locale={{
|
||||
emptyText:
|
||||
isLoading || isRefetching ? null : (
|
||||
<div className="no-status-code-data-message-container">
|
||||
<div className="no-status-code-data-message-content">
|
||||
<img
|
||||
src="/Icons/emptyState.svg"
|
||||
alt="thinking-emoji"
|
||||
className="empty-state-svg"
|
||||
/>
|
||||
{renderItems.length === 0 ? (
|
||||
<div className="no-dependent-services-message-container">
|
||||
<div className="no-dependent-services-message-content">
|
||||
<img
|
||||
src="/Icons/emptyState.svg"
|
||||
alt="thinking-emoji"
|
||||
className="empty-state-svg"
|
||||
/>
|
||||
|
||||
<Typography.Text className="no-status-code-data-message">
|
||||
This query had no results. Edit your query and try again!
|
||||
</Typography.Text>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
}}
|
||||
onRow={(record): { onClick: () => void; className: string } => ({
|
||||
onClick: (): void => {
|
||||
const url = new URL(
|
||||
`/services/${
|
||||
record.serviceData.serviceName &&
|
||||
record.serviceData.serviceName !== '-'
|
||||
? record.serviceData.serviceName
|
||||
: ''
|
||||
}`,
|
||||
window.location.origin,
|
||||
);
|
||||
const urlQuery = new URLSearchParams();
|
||||
urlQuery.set(QueryParams.startTime, timeRange.startTime.toString());
|
||||
urlQuery.set(QueryParams.endTime, timeRange.endTime.toString());
|
||||
url.search = urlQuery.toString();
|
||||
window.open(url.toString(), '_blank');
|
||||
},
|
||||
className: 'clickable-row',
|
||||
})}
|
||||
/>
|
||||
<Typography.Text className="no-dependent-services-message">
|
||||
This query had no results. Edit your query and try again!
|
||||
</Typography.Text>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
renderItems.map((item) => (
|
||||
<div className="top-services-item" key={item.key}>
|
||||
<div className="top-services-item-progress">
|
||||
<div className="top-services-item-key">{item.serviceName}</div>
|
||||
<div className="top-services-item-count">{item.count}</div>
|
||||
<div
|
||||
className="top-services-item-progress-bar"
|
||||
style={{ width: `${item.percentage}%` }}
|
||||
/>
|
||||
</div>
|
||||
<div className="top-services-item-percentage">
|
||||
{item.percentage.toFixed(2)}%
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
|
||||
{dependentServicesData.length > 5 && (
|
||||
{currentRenderCount < dependentServicesData.length && (
|
||||
<div
|
||||
className="top-services-load-more"
|
||||
onClick={handleShowMoreClick}
|
||||
onClick={(): void => setCurrentRenderCount(dependentServicesData.length)}
|
||||
onKeyDown={(e): void => {
|
||||
if (e.key === 'Enter') {
|
||||
handleShowMoreClick();
|
||||
setCurrentRenderCount(dependentServicesData.length);
|
||||
}
|
||||
}}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
>
|
||||
<UnfoldVertical size={14} />
|
||||
{isExpanded ? 'Show less...' : 'Show more...'}
|
||||
Show more...
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,88 +1,8 @@
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { Progress, Skeleton, Tooltip, Typography } from 'antd';
|
||||
import { ENTITY_VERSION_V4 } from 'constants/app';
|
||||
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||
import {
|
||||
DomainMetricsResponseRow,
|
||||
formatDomainMetricsDataForTable,
|
||||
getDomainMetricsQueryPayload,
|
||||
} from 'container/ApiMonitoring/utils';
|
||||
import { GetMetricQueryRange } from 'lib/dashboard/getQueryResults';
|
||||
import { useMemo } from 'react';
|
||||
import { useQueries } from 'react-query';
|
||||
import { SuccessResponse } from 'types/api';
|
||||
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
|
||||
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
import ErrorState from './ErrorState';
|
||||
|
||||
function DomainMetrics({
|
||||
domainName,
|
||||
timeRange,
|
||||
domainListFilters,
|
||||
}: {
|
||||
domainName: string;
|
||||
timeRange: { startTime: number; endTime: number };
|
||||
domainListFilters: IBuilderQuery['filters'];
|
||||
}): JSX.Element {
|
||||
const { startTime: minTime, endTime: maxTime } = timeRange;
|
||||
|
||||
const queryPayloads = useMemo(
|
||||
() =>
|
||||
getDomainMetricsQueryPayload(
|
||||
domainName,
|
||||
minTime,
|
||||
maxTime,
|
||||
domainListFilters,
|
||||
),
|
||||
[domainName, minTime, maxTime, domainListFilters],
|
||||
);
|
||||
|
||||
// Since only one query here
|
||||
const domainMetricsDataQueries = useQueries(
|
||||
queryPayloads.map((payload) => ({
|
||||
queryKey: [
|
||||
REACT_QUERY_KEY.GET_DOMAIN_METRICS_DATA,
|
||||
payload,
|
||||
ENTITY_VERSION_V4,
|
||||
],
|
||||
queryFn: (): Promise<SuccessResponse<MetricRangePayloadProps>> =>
|
||||
GetMetricQueryRange(payload, ENTITY_VERSION_V4),
|
||||
enabled: !!payload,
|
||||
staleTime: 60 * 1000, // 1 minute stale time : optimize this part
|
||||
})),
|
||||
);
|
||||
|
||||
const domainMetricsDataQuery = domainMetricsDataQueries[0];
|
||||
// [TODO] handle the case where the data is not available
|
||||
// [TODO] Format the data properly
|
||||
const {
|
||||
data: domainMetricsData,
|
||||
isLoading,
|
||||
isRefetching,
|
||||
isError,
|
||||
refetch,
|
||||
} = domainMetricsDataQuery;
|
||||
|
||||
// [TODO] Fix type error
|
||||
const formattedDomainMetricsData = useMemo(() => {
|
||||
// Safely access the data with proper type checking
|
||||
const rowData = domainMetricsData?.payload?.data?.result[0]?.table?.rows[0];
|
||||
|
||||
// Only pass the data if it matches the expected format
|
||||
return formatDomainMetricsDataForTable(
|
||||
rowData as DomainMetricsResponseRow | undefined,
|
||||
);
|
||||
}, [domainMetricsData]);
|
||||
|
||||
if (isError) {
|
||||
return (
|
||||
<div className="all-endpoints-error-state-wrapper">
|
||||
<ErrorState refetch={refetch} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
import { Progress, Tooltip, Typography } from 'antd';
|
||||
import { getLastUsedRelativeTime } from 'container/ApiMonitoring/utils';
|
||||
|
||||
function DomainMetrics({ domainData }: { domainData: any }): JSX.Element {
|
||||
return (
|
||||
<div className="domain-detail-drawer__endpoint">
|
||||
<div className="domain-details-grid">
|
||||
@@ -103,7 +23,7 @@ function DomainMetrics({
|
||||
type="secondary"
|
||||
className="domain-details-metadata-label"
|
||||
>
|
||||
ERROR %
|
||||
ERROR RATE
|
||||
</Typography.Text>
|
||||
<Typography.Text
|
||||
type="secondary"
|
||||
@@ -115,62 +35,43 @@ function DomainMetrics({
|
||||
|
||||
<div className="values-row">
|
||||
<Typography.Text className="domain-details-metadata-value">
|
||||
{isLoading || isRefetching ? (
|
||||
<Skeleton.Button active size="small" />
|
||||
) : (
|
||||
<Tooltip title={formattedDomainMetricsData.endpointCount}>
|
||||
<span className="round-metric-tag">
|
||||
{formattedDomainMetricsData.endpointCount}
|
||||
</span>
|
||||
</Tooltip>
|
||||
)}
|
||||
<Tooltip title={domainData.endpointCount}>
|
||||
<span className="round-metric-tag">{domainData.endpointCount}</span>
|
||||
</Tooltip>
|
||||
</Typography.Text>
|
||||
{/* // update the tooltip as well */}
|
||||
<Typography.Text className="domain-details-metadata-value">
|
||||
{isLoading || isRefetching ? (
|
||||
<Skeleton.Button active size="small" />
|
||||
) : (
|
||||
<Tooltip title={formattedDomainMetricsData.latency}>
|
||||
<span className="round-metric-tag">
|
||||
{(Number(formattedDomainMetricsData.latency) / 1000).toFixed(3)}s
|
||||
</span>
|
||||
</Tooltip>
|
||||
)}
|
||||
<Tooltip title={domainData.latency}>
|
||||
<span className="round-metric-tag">
|
||||
{(domainData.latency / 1000).toFixed(3)}s
|
||||
</span>
|
||||
</Tooltip>
|
||||
</Typography.Text>
|
||||
{/* // update the tooltip as well */}
|
||||
<Typography.Text className="domain-details-metadata-value error-rate">
|
||||
{isLoading || isRefetching ? (
|
||||
<Skeleton.Button active size="small" />
|
||||
) : (
|
||||
<Tooltip title={formattedDomainMetricsData.errorRate}>
|
||||
<Progress
|
||||
status="active"
|
||||
percent={Number(
|
||||
Number(formattedDomainMetricsData.errorRate).toFixed(2),
|
||||
)}
|
||||
strokeLinecap="butt"
|
||||
size="small"
|
||||
strokeColor={((): string => {
|
||||
const errorRatePercent = Number(
|
||||
Number(formattedDomainMetricsData.errorRate).toFixed(2),
|
||||
);
|
||||
if (errorRatePercent >= 90) return Color.BG_SAKURA_500;
|
||||
if (errorRatePercent >= 60) return Color.BG_AMBER_500;
|
||||
return Color.BG_FOREST_500;
|
||||
})()}
|
||||
className="progress-bar"
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
<Tooltip title={domainData.errorRate}>
|
||||
<Progress
|
||||
status="active"
|
||||
percent={Number((domainData.errorRate * 100).toFixed(1))}
|
||||
strokeLinecap="butt"
|
||||
size="small"
|
||||
strokeColor={((): string => {
|
||||
const errorRatePercent = Number(
|
||||
(domainData.errorRate * 100).toFixed(1),
|
||||
);
|
||||
if (errorRatePercent >= 90) return Color.BG_SAKURA_500;
|
||||
if (errorRatePercent >= 60) return Color.BG_AMBER_500;
|
||||
return Color.BG_FOREST_500;
|
||||
})()}
|
||||
className="progress-bar"
|
||||
/>
|
||||
</Tooltip>
|
||||
</Typography.Text>
|
||||
{/* // update the tooltip as well */}
|
||||
<Typography.Text className="domain-details-metadata-value">
|
||||
{isLoading || isRefetching ? (
|
||||
<Skeleton.Button active size="small" />
|
||||
) : (
|
||||
<Tooltip title={formattedDomainMetricsData.lastUsed}>
|
||||
{formattedDomainMetricsData.lastUsed}
|
||||
</Tooltip>
|
||||
)}
|
||||
<Tooltip title={domainData.lastUsed}>
|
||||
{getLastUsedRelativeTime(domainData.lastUsed)}
|
||||
</Tooltip>
|
||||
</Typography.Text>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -54,7 +54,7 @@ function EndPointMetrics({
|
||||
type="secondary"
|
||||
className="domain-details-metadata-label"
|
||||
>
|
||||
ERROR %
|
||||
ERROR RATE
|
||||
</Typography.Text>
|
||||
<Typography.Text
|
||||
type="secondary"
|
||||
@@ -89,13 +89,12 @@ function EndPointMetrics({
|
||||
) : (
|
||||
<Tooltip title={metricsData?.errorRate}>
|
||||
<Progress
|
||||
status="active"
|
||||
percent={Number(Number(metricsData?.errorRate ?? 0).toFixed(2))}
|
||||
percent={Number((metricsData?.errorRate ?? 0 * 100).toFixed(1))}
|
||||
strokeLinecap="butt"
|
||||
size="small"
|
||||
strokeColor={((): string => {
|
||||
const errorRatePercent = Number(
|
||||
Number(metricsData?.errorRate ?? 0).toFixed(2),
|
||||
(metricsData?.errorRate ?? 0 * 100).toFixed(1),
|
||||
);
|
||||
if (errorRatePercent >= 90) return Color.BG_SAKURA_500;
|
||||
if (errorRatePercent >= 60) return Color.BG_AMBER_500;
|
||||
|
||||
@@ -52,10 +52,6 @@ function EndPointsDropDown({
|
||||
: (triggerNode): HTMLElement => triggerNode.parentNode as HTMLElement
|
||||
}
|
||||
dropdownStyle={dropdownStyle}
|
||||
allowClear
|
||||
onClear={(): void => {
|
||||
setSelectedEndPointName('');
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -19,7 +19,6 @@ import { useSelector } from 'react-redux';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { SuccessResponse } from 'types/api';
|
||||
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
|
||||
import { OrderByPayload } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
|
||||
import { VIEW_TYPES, VIEWS } from '../constants';
|
||||
@@ -29,13 +28,11 @@ function ExpandedRow({
|
||||
selectedRowData,
|
||||
setSelectedEndPointName,
|
||||
setSelectedView,
|
||||
orderBy,
|
||||
}: {
|
||||
domainName: string;
|
||||
selectedRowData: EndPointsTableRowData;
|
||||
setSelectedEndPointName: (name: string) => void;
|
||||
setSelectedView: (view: VIEWS) => void;
|
||||
orderBy: OrderByPayload | null;
|
||||
}): JSX.Element {
|
||||
const nestedColumns = useMemo(() => getEndPointsColumnsConfig(false, []), []);
|
||||
const { maxTime, minTime } = useSelector<AppState, GlobalReducer>(
|
||||
@@ -103,7 +100,6 @@ function ExpandedRow({
|
||||
? formatEndPointsDataForTable(
|
||||
groupedByRowQuery.data?.payload.data.result[0].table?.rows,
|
||||
[],
|
||||
orderBy,
|
||||
)
|
||||
: []
|
||||
}
|
||||
@@ -118,7 +114,7 @@ function ExpandedRow({
|
||||
onRow={(record): { onClick: () => void; className: string } => ({
|
||||
onClick: (): void => {
|
||||
setSelectedEndPointName(record.endpointName);
|
||||
setSelectedView(VIEW_TYPES.ENDPOINT_STATS);
|
||||
setSelectedView(VIEW_TYPES.ENDPOINT_DETAILS);
|
||||
logEvent('API Monitoring: Endpoint name row clicked', {});
|
||||
},
|
||||
className: 'expanded-clickable-row',
|
||||
|
||||
@@ -2,15 +2,7 @@ import { Card } from 'antd';
|
||||
import GridCard from 'container/GridCardLayout/GridCard';
|
||||
import { Widgets } from 'types/api/dashboard/getAll';
|
||||
|
||||
function MetricOverTimeGraph({
|
||||
widget,
|
||||
timeRange,
|
||||
onDragSelect,
|
||||
}: {
|
||||
widget: Widgets;
|
||||
timeRange: { startTime: number; endTime: number };
|
||||
onDragSelect: (start: number, end: number) => void;
|
||||
}): JSX.Element {
|
||||
function MetricOverTimeGraph({ widget }: { widget: Widgets }): JSX.Element {
|
||||
return (
|
||||
<div>
|
||||
<Card bordered className="endpoint-details-card">
|
||||
@@ -18,9 +10,8 @@ function MetricOverTimeGraph({
|
||||
<GridCard
|
||||
widget={widget}
|
||||
isQueryEnabled
|
||||
onDragSelect={onDragSelect}
|
||||
onDragSelect={(): void => {}}
|
||||
customOnDragSelect={(): void => {}}
|
||||
customTimeRange={timeRange}
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
@@ -21,9 +21,12 @@ import { getUPlotChartOptions } from 'lib/uPlotLib/getUplotChartOptions';
|
||||
import { getUPlotChartData } from 'lib/uPlotLib/utils/getUplotChartData';
|
||||
import { useCallback, useMemo, useRef, useState } from 'react';
|
||||
import { UseQueryResult } from 'react-query';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { SuccessResponse } from 'types/api';
|
||||
import { Widgets } from 'types/api/dashboard/getAll';
|
||||
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
import { Options } from 'uplot';
|
||||
|
||||
import ErrorState from './ErrorState';
|
||||
@@ -33,9 +36,8 @@ function StatusCodeBarCharts({
|
||||
endPointStatusCodeLatencyBarChartsDataQuery,
|
||||
domainName,
|
||||
endPointName,
|
||||
domainListFilters,
|
||||
filters,
|
||||
timeRange,
|
||||
onDragSelect,
|
||||
}: {
|
||||
endPointStatusCodeBarChartsDataQuery: UseQueryResult<
|
||||
SuccessResponse<any>,
|
||||
@@ -47,12 +49,8 @@ function StatusCodeBarCharts({
|
||||
>;
|
||||
domainName: string;
|
||||
endPointName: string;
|
||||
domainListFilters: IBuilderQuery['filters'];
|
||||
filters: IBuilderQuery['filters'];
|
||||
timeRange: {
|
||||
startTime: number;
|
||||
endTime: number;
|
||||
};
|
||||
onDragSelect: (start: number, end: number) => void;
|
||||
}): JSX.Element {
|
||||
// 0 : Status Code Count
|
||||
// 1 : Status Code Latency
|
||||
@@ -66,7 +64,9 @@ function StatusCodeBarCharts({
|
||||
data: endPointStatusCodeLatencyBarChartsData,
|
||||
} = endPointStatusCodeLatencyBarChartsDataQuery;
|
||||
|
||||
const { startTime: minTime, endTime: maxTime } = timeRange;
|
||||
const { minTime, maxTime } = useSelector<AppState, GlobalReducer>(
|
||||
(state) => state.globalTime,
|
||||
);
|
||||
|
||||
const graphRef = useRef<HTMLDivElement>(null);
|
||||
const dimensions = useResizeObserver(graphRef);
|
||||
@@ -115,30 +115,25 @@ function StatusCodeBarCharts({
|
||||
const navigateToExplorerPages = useNavigateToExplorerPages();
|
||||
const { notifications } = useNotifications();
|
||||
|
||||
const colorMapping = useMemo(
|
||||
() => ({
|
||||
const { getCustomSeries } = useGetGraphCustomSeries({
|
||||
isDarkMode,
|
||||
drawStyle: 'bars',
|
||||
colorMapping: {
|
||||
'200-299': Color.BG_FOREST_500,
|
||||
'300-399': Color.BG_AMBER_400,
|
||||
'400-499': Color.BG_CHERRY_500,
|
||||
'500-599': Color.BG_ROBIN_500,
|
||||
Other: Color.BG_SIENNA_500,
|
||||
}),
|
||||
[],
|
||||
);
|
||||
|
||||
const { getCustomSeries } = useGetGraphCustomSeries({
|
||||
isDarkMode,
|
||||
drawStyle: 'bars',
|
||||
colorMapping,
|
||||
},
|
||||
});
|
||||
|
||||
const widget = useMemo<Widgets>(
|
||||
() =>
|
||||
getStatusCodeBarChartWidgetData(domainName, endPointName, {
|
||||
items: [...filters.items],
|
||||
items: [...domainListFilters.items, ...filters.items],
|
||||
op: filters.op,
|
||||
}),
|
||||
[domainName, endPointName, filters],
|
||||
[domainName, endPointName, domainListFilters, filters],
|
||||
);
|
||||
|
||||
const graphClickHandler = useCallback(
|
||||
@@ -187,13 +182,11 @@ function StatusCodeBarCharts({
|
||||
yAxisUnit: statusCodeWidgetInfo[currentWidgetInfoIndex].yAxisUnit,
|
||||
softMax: null,
|
||||
softMin: null,
|
||||
minTimeScale: minTime,
|
||||
maxTimeScale: maxTime,
|
||||
minTimeScale: Math.floor(minTime / 1e9),
|
||||
maxTimeScale: Math.floor(maxTime / 1e9),
|
||||
panelType: PANEL_TYPES.BAR,
|
||||
onClickHandler: graphClickHandler,
|
||||
customSeries: getCustomSeries,
|
||||
onDragSelect,
|
||||
colorMapping,
|
||||
}),
|
||||
[
|
||||
minTime,
|
||||
@@ -205,8 +198,6 @@ function StatusCodeBarCharts({
|
||||
isDarkMode,
|
||||
graphClickHandler,
|
||||
getCustomSeries,
|
||||
onDragSelect,
|
||||
colorMapping,
|
||||
],
|
||||
);
|
||||
|
||||
|
||||
@@ -1,20 +1,9 @@
|
||||
export enum VIEWS {
|
||||
ALL_ENDPOINTS = 'all_endpoints',
|
||||
ENDPOINT_STATS = 'endpoint_stats',
|
||||
TOP_ERRORS = 'top_errors',
|
||||
ENDPOINT_DETAILS = 'endpoint_details',
|
||||
}
|
||||
|
||||
export const VIEW_TYPES = {
|
||||
ALL_ENDPOINTS: VIEWS.ALL_ENDPOINTS,
|
||||
ENDPOINT_STATS: VIEWS.ENDPOINT_STATS,
|
||||
TOP_ERRORS: VIEWS.TOP_ERRORS,
|
||||
ENDPOINT_DETAILS: VIEWS.ENDPOINT_DETAILS,
|
||||
};
|
||||
|
||||
// Span attribute keys - these are the source of truth for all attribute keys
|
||||
export const SPAN_ATTRIBUTES = {
|
||||
URL_PATH: 'http.url',
|
||||
STATUS_CODE: 'status_code',
|
||||
RESPONSE_STATUS_CODE: 'response_status_code',
|
||||
SERVER_NAME: 'net.peer.name',
|
||||
SERVER_PORT: 'net.peer.port',
|
||||
} as const;
|
||||
|
||||
@@ -7,22 +7,16 @@ import logEvent from 'api/common/logEvent';
|
||||
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
||||
import { AxiosError } from 'axios';
|
||||
import cx from 'classnames';
|
||||
import { initialQueriesMap } from 'constants/queryBuilder';
|
||||
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||
import RightToolbarActions from 'container/QueryBuilder/components/ToolbarActions/RightToolbarActions';
|
||||
import QueryBuilderSearchV2 from 'container/QueryBuilder/filters/QueryBuilderSearchV2/QueryBuilderSearchV2';
|
||||
import Toolbar from 'container/Toolbar/Toolbar';
|
||||
import { useGetCompositeQueryParam } from 'hooks/queryBuilder/useGetCompositeQueryParam';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { useQueryOperations } from 'hooks/queryBuilder/useQueryBuilderOperations';
|
||||
import { useShareBuilderUrl } from 'hooks/queryBuilder/useShareBuilderUrl';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import DateTimeSelectionV2 from 'container/TopNav/DateTimeSelectionV2';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { useQuery } from 'react-query';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
import { HandleChangeQueryData } from 'types/common/operations.types';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
|
||||
import {
|
||||
@@ -32,50 +26,20 @@ import {
|
||||
} from '../../utils';
|
||||
import DomainDetails from './DomainDetails/DomainDetails';
|
||||
|
||||
function DomainList({ showIP }: { showIP: boolean }): JSX.Element {
|
||||
function DomainList({
|
||||
query,
|
||||
showIP,
|
||||
handleChangeQueryData,
|
||||
}: {
|
||||
query: IBuilderQuery;
|
||||
showIP: boolean;
|
||||
handleChangeQueryData: HandleChangeQueryData;
|
||||
}): JSX.Element {
|
||||
const [selectedDomainIndex, setSelectedDomainIndex] = useState<number>(-1);
|
||||
const { maxTime, minTime } = useSelector<AppState, GlobalReducer>(
|
||||
(state) => state.globalTime,
|
||||
);
|
||||
|
||||
const { currentQuery, handleRunQuery } = useQueryBuilder();
|
||||
const query = useMemo(() => currentQuery?.builder?.queryData[0] || null, [
|
||||
currentQuery,
|
||||
]);
|
||||
|
||||
const { handleChangeQueryData } = useQueryOperations({
|
||||
index: 0,
|
||||
query,
|
||||
entityVersion: '',
|
||||
});
|
||||
|
||||
// initialise tab with default query.
|
||||
useShareBuilderUrl({
|
||||
...initialQueriesMap.traces,
|
||||
builder: {
|
||||
...initialQueriesMap.traces.builder,
|
||||
queryData: [
|
||||
{
|
||||
...initialQueriesMap.traces.builder.queryData[0],
|
||||
dataSource: DataSource.TRACES,
|
||||
aggregateOperator: 'noop',
|
||||
aggregateAttribute: {
|
||||
...initialQueriesMap.traces.builder.queryData[0].aggregateAttribute,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
const compositeData = useGetCompositeQueryParam();
|
||||
|
||||
const handleChangeTagFilters = useCallback(
|
||||
(value: IBuilderQuery['filters']) => {
|
||||
handleChangeQueryData('filters', value);
|
||||
},
|
||||
[handleChangeQueryData],
|
||||
);
|
||||
|
||||
const fetchApiOverview = async (): Promise<
|
||||
SuccessResponse<any> | ErrorResponse
|
||||
> => {
|
||||
@@ -85,21 +49,7 @@ function DomainList({ showIP }: { showIP: boolean }): JSX.Element {
|
||||
show_ip: showIP,
|
||||
filters: {
|
||||
op: 'AND',
|
||||
items: [
|
||||
{
|
||||
id: '212678b9',
|
||||
key: {
|
||||
key: 'kind_string',
|
||||
dataType: 'string',
|
||||
type: '',
|
||||
isColumn: true,
|
||||
isJSON: false,
|
||||
},
|
||||
op: '=',
|
||||
value: 'Client',
|
||||
},
|
||||
...(compositeData?.builder?.queryData[0]?.filters.items || []),
|
||||
],
|
||||
items: query?.filters.items,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -120,7 +70,7 @@ function DomainList({ showIP }: { showIP: boolean }): JSX.Element {
|
||||
};
|
||||
|
||||
const { data, isLoading, isFetching } = useQuery(
|
||||
[REACT_QUERY_KEY.GET_DOMAINS_LIST, minTime, maxTime, compositeData, showIP],
|
||||
[REACT_QUERY_KEY.GET_DOMAINS_LIST, minTime, maxTime, query, showIP],
|
||||
fetchApiOverview,
|
||||
);
|
||||
|
||||
@@ -131,18 +81,20 @@ function DomainList({ showIP }: { showIP: boolean }): JSX.Element {
|
||||
|
||||
return (
|
||||
<section className={cx('api-module-right-section')}>
|
||||
<Toolbar
|
||||
showAutoRefresh={false}
|
||||
rightActions={<RightToolbarActions onStageRunQuery={handleRunQuery} />}
|
||||
/>
|
||||
{/* add bottom border here */}
|
||||
<div className={cx('api-monitoring-list-header')}>
|
||||
<QueryBuilderSearchV2
|
||||
query={query}
|
||||
onChange={handleChangeTagFilters}
|
||||
onChange={(searchFilters): void =>
|
||||
handleChangeQueryData('filters', searchFilters)
|
||||
}
|
||||
placeholder="Search filters..."
|
||||
hardcodedAttributeKeys={hardcodedAttributeKeys}
|
||||
/>
|
||||
<DateTimeSelectionV2
|
||||
showAutoRefresh={false}
|
||||
showRefreshText={false}
|
||||
hideShareModal
|
||||
/>
|
||||
</div>
|
||||
<Table
|
||||
className={cx('api-monitoring-domain-list-table')}
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
.api-quick-filters-header {
|
||||
padding: 12px;
|
||||
border-bottom: 1px solid var(--bg-slate-400);
|
||||
border-right: 1px solid var(--bg-slate-400);
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -25,10 +24,6 @@
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
|
||||
.toolbar {
|
||||
border-bottom: 1px solid var(--bg-slate-400);
|
||||
}
|
||||
|
||||
.api-monitoring-list-header {
|
||||
width: 100%;
|
||||
padding: 8px;
|
||||
|
||||
@@ -7,8 +7,12 @@ import logEvent from 'api/common/logEvent';
|
||||
import cx from 'classnames';
|
||||
import QuickFilters from 'components/QuickFilters/QuickFilters';
|
||||
import { QuickFiltersSource } from 'components/QuickFilters/types';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { useQueryOperations } from 'hooks/queryBuilder/useQueryBuilderOperations';
|
||||
import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFallback';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
import { ApiMonitoringQuickFiltersConfig } from '../utils';
|
||||
import DomainList from './Domains/DomainList';
|
||||
@@ -16,10 +20,39 @@ import DomainList from './Domains/DomainList';
|
||||
function Explorer(): JSX.Element {
|
||||
const [showIP, setShowIP] = useState<boolean>(true);
|
||||
|
||||
const { currentQuery } = useQueryBuilder();
|
||||
|
||||
useEffect(() => {
|
||||
logEvent('API Monitoring: Landing page visited', {});
|
||||
}, []);
|
||||
|
||||
const { handleChangeQueryData } = useQueryOperations({
|
||||
index: 0,
|
||||
query: currentQuery.builder.queryData[0],
|
||||
entityVersion: '',
|
||||
});
|
||||
|
||||
const updatedCurrentQuery = useMemo(
|
||||
() => ({
|
||||
...currentQuery,
|
||||
builder: {
|
||||
...currentQuery.builder,
|
||||
queryData: [
|
||||
{
|
||||
...currentQuery.builder.queryData[0],
|
||||
dataSource: DataSource.TRACES,
|
||||
aggregateOperator: 'noop',
|
||||
aggregateAttribute: {
|
||||
...currentQuery.builder.queryData[0].aggregateAttribute,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
[currentQuery],
|
||||
);
|
||||
const query = updatedCurrentQuery?.builder?.queryData[0] || null;
|
||||
|
||||
return (
|
||||
<Sentry.ErrorBoundary fallback={<ErrorBoundaryFallback />}>
|
||||
<div className={cx('api-monitoring-page', 'filter-visible')}>
|
||||
@@ -50,9 +83,16 @@ function Explorer(): JSX.Element {
|
||||
source={QuickFiltersSource.API_MONITORING}
|
||||
config={ApiMonitoringQuickFiltersConfig}
|
||||
handleFilterVisibilityChange={(): void => {}}
|
||||
onFilterChange={(query: Query): void =>
|
||||
handleChangeQueryData('filters', query.builder.queryData[0].filters)
|
||||
}
|
||||
/>
|
||||
</section>
|
||||
<DomainList showIP={showIP} />
|
||||
<DomainList
|
||||
query={query}
|
||||
showIP={showIP}
|
||||
handleChangeQueryData={handleChangeQueryData}
|
||||
/>
|
||||
</div>
|
||||
</Sentry.ErrorBoundary>
|
||||
);
|
||||
|
||||
@@ -1,190 +0,0 @@
|
||||
import { cleanup, fireEvent, render, screen } from '@testing-library/react';
|
||||
import {
|
||||
getAllEndpointsWidgetData,
|
||||
getGroupByFiltersFromGroupByValues,
|
||||
} from 'container/ApiMonitoring/utils';
|
||||
import { useGetAggregateKeys } from 'hooks/queryBuilder/useGetAggregateKeys';
|
||||
|
||||
import AllEndPoints from '../Explorer/Domains/DomainDetails/AllEndPoints';
|
||||
import {
|
||||
SPAN_ATTRIBUTES,
|
||||
VIEWS,
|
||||
} from '../Explorer/Domains/DomainDetails/constants';
|
||||
|
||||
// Mock the dependencies
|
||||
jest.mock('container/ApiMonitoring/utils', () => ({
|
||||
getAllEndpointsWidgetData: jest.fn(),
|
||||
getGroupByFiltersFromGroupByValues: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('container/GridCardLayout/GridCard', () => ({
|
||||
__esModule: true,
|
||||
default: jest.fn().mockImplementation(({ customOnRowClick }) => (
|
||||
<div data-testid="grid-card-mock">
|
||||
<button
|
||||
type="button"
|
||||
data-testid="row-click-button"
|
||||
onClick={(): void =>
|
||||
customOnRowClick({ [SPAN_ATTRIBUTES.URL_PATH]: '/api/test' })
|
||||
}
|
||||
>
|
||||
Click Row
|
||||
</button>
|
||||
</div>
|
||||
)),
|
||||
}));
|
||||
|
||||
jest.mock(
|
||||
'container/QueryBuilder/filters/QueryBuilderSearchV2/QueryBuilderSearchV2',
|
||||
() => ({
|
||||
__esModule: true,
|
||||
default: jest.fn().mockImplementation(({ onChange }) => (
|
||||
<div data-testid="query-builder-mock">
|
||||
<button
|
||||
type="button"
|
||||
data-testid="filter-change-button"
|
||||
onClick={(): void =>
|
||||
onChange({
|
||||
items: [{ id: 'test', key: 'test', op: '=', value: 'test' }],
|
||||
op: 'AND',
|
||||
})
|
||||
}
|
||||
>
|
||||
Change Filter
|
||||
</button>
|
||||
</div>
|
||||
)),
|
||||
}),
|
||||
);
|
||||
|
||||
jest.mock('hooks/queryBuilder/useGetAggregateKeys', () => ({
|
||||
useGetAggregateKeys: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('antd', () => {
|
||||
const originalModule = jest.requireActual('antd');
|
||||
return {
|
||||
...originalModule,
|
||||
Select: jest.fn().mockImplementation(({ onChange }) => (
|
||||
<div data-testid="select-mock">
|
||||
<button
|
||||
data-testid="select-change-button"
|
||||
type="button"
|
||||
onClick={(): void => onChange(['http.status_code'])}
|
||||
>
|
||||
Change GroupBy
|
||||
</button>
|
||||
</div>
|
||||
)),
|
||||
};
|
||||
});
|
||||
|
||||
describe('AllEndPoints', () => {
|
||||
const mockProps = {
|
||||
domainName: 'test-domain',
|
||||
setSelectedEndPointName: jest.fn(),
|
||||
setSelectedView: jest.fn(),
|
||||
groupBy: [],
|
||||
setGroupBy: jest.fn(),
|
||||
timeRange: {
|
||||
startTime: 1609459200000,
|
||||
endTime: 1609545600000,
|
||||
},
|
||||
initialFilters: { op: 'AND', items: [] },
|
||||
setInitialFiltersEndPointStats: jest.fn(),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
// Setup mock implementations
|
||||
(useGetAggregateKeys as jest.Mock).mockReturnValue({
|
||||
data: {
|
||||
payload: {
|
||||
attributeKeys: [
|
||||
{
|
||||
key: 'http.status_code',
|
||||
dataType: 'string',
|
||||
isColumn: true,
|
||||
isJSON: false,
|
||||
type: '',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
isLoading: false,
|
||||
});
|
||||
|
||||
(getAllEndpointsWidgetData as jest.Mock).mockReturnValue({
|
||||
id: 'test-widget',
|
||||
title: 'Endpoint Overview',
|
||||
description: 'Endpoint Overview',
|
||||
panelTypes: 'table',
|
||||
queryData: [],
|
||||
});
|
||||
|
||||
(getGroupByFiltersFromGroupByValues as jest.Mock).mockReturnValue({
|
||||
items: [{ id: 'group-filter', key: 'status', op: '=', value: '200' }],
|
||||
op: 'AND',
|
||||
});
|
||||
});
|
||||
|
||||
// Add cleanup after each test
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
it('renders component correctly', () => {
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
render(<AllEndPoints {...mockProps} />);
|
||||
|
||||
// Verify basic component rendering
|
||||
expect(screen.getByText('Group by')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('query-builder-mock')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('select-mock')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('grid-card-mock')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('handles filter changes', () => {
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
render(<AllEndPoints {...mockProps} />);
|
||||
|
||||
// Trigger filter change
|
||||
fireEvent.click(screen.getByTestId('filter-change-button'));
|
||||
|
||||
// Check if getAllEndpointsWidgetData was called with updated filters
|
||||
expect(getAllEndpointsWidgetData).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
'test-domain',
|
||||
expect.objectContaining({
|
||||
items: expect.arrayContaining([expect.objectContaining({ id: 'test' })]),
|
||||
op: 'AND',
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('handles group by changes', () => {
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
render(<AllEndPoints {...mockProps} />);
|
||||
|
||||
// Trigger group by change
|
||||
fireEvent.click(screen.getByTestId('select-change-button'));
|
||||
|
||||
// Check if setGroupBy was called with updated group by value
|
||||
expect(mockProps.setGroupBy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('handles row click in grid card', async () => {
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
render(<AllEndPoints {...mockProps} />);
|
||||
|
||||
// Trigger row click
|
||||
fireEvent.click(screen.getByTestId('row-click-button'));
|
||||
|
||||
// Check if proper functions were called
|
||||
expect(mockProps.setSelectedEndPointName).toHaveBeenCalledWith('/api/test');
|
||||
expect(mockProps.setSelectedView).toHaveBeenCalledWith(VIEWS.ENDPOINT_STATS);
|
||||
expect(mockProps.setInitialFiltersEndPointStats).toHaveBeenCalled();
|
||||
expect(getGroupByFiltersFromGroupByValues).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -1,366 +0,0 @@
|
||||
import { fireEvent, render, screen } from '@testing-library/react';
|
||||
import { getFormattedDependentServicesData } from 'container/ApiMonitoring/utils';
|
||||
import { SuccessResponse } from 'types/api';
|
||||
|
||||
import DependentServices from '../Explorer/Domains/DomainDetails/components/DependentServices';
|
||||
import ErrorState from '../Explorer/Domains/DomainDetails/components/ErrorState';
|
||||
|
||||
// Create a partial mock of the UseQueryResult interface for testing
|
||||
interface MockQueryResult {
|
||||
isLoading: boolean;
|
||||
isRefetching: boolean;
|
||||
isError: boolean;
|
||||
data?: any;
|
||||
refetch: () => void;
|
||||
}
|
||||
|
||||
// Mock the utility function
|
||||
jest.mock('container/ApiMonitoring/utils', () => ({
|
||||
getFormattedDependentServicesData: jest.fn(),
|
||||
dependentServicesColumns: [
|
||||
{ title: 'Dependent Services', dataIndex: 'serviceData', key: 'serviceData' },
|
||||
{ title: 'AVG. LATENCY', dataIndex: 'latency', key: 'latency' },
|
||||
{ title: 'ERROR %', dataIndex: 'errorPercentage', key: 'errorPercentage' },
|
||||
{ title: 'AVG. RATE', dataIndex: 'rate', key: 'rate' },
|
||||
],
|
||||
}));
|
||||
|
||||
// Mock the ErrorState component
|
||||
jest.mock('../Explorer/Domains/DomainDetails/components/ErrorState', () => ({
|
||||
__esModule: true,
|
||||
default: jest.fn().mockImplementation(({ refetch }) => (
|
||||
<div data-testid="error-state-mock">
|
||||
<button type="button" data-testid="refetch-button" onClick={refetch}>
|
||||
Retry
|
||||
</button>
|
||||
</div>
|
||||
)),
|
||||
}));
|
||||
|
||||
// Mock antd components
|
||||
jest.mock('antd', () => {
|
||||
const originalModule = jest.requireActual('antd');
|
||||
return {
|
||||
...originalModule,
|
||||
Table: jest
|
||||
.fn()
|
||||
.mockImplementation(({ dataSource, loading, pagination, onRow }) => (
|
||||
<div data-testid="table-mock">
|
||||
<div data-testid="loading-state">
|
||||
{loading ? 'Loading' : 'Not Loading'}
|
||||
</div>
|
||||
<div data-testid="row-count">{dataSource?.length || 0}</div>
|
||||
<div data-testid="page-size">{pagination?.pageSize}</div>
|
||||
{dataSource?.map((item: any, index: number) => (
|
||||
<div
|
||||
key={`service-${item.key || index}`}
|
||||
data-testid={`table-row-${index}`}
|
||||
onClick={(): void => onRow?.(item)?.onClick?.()}
|
||||
onKeyDown={(e: React.KeyboardEvent<HTMLDivElement>): void => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
onRow?.(item)?.onClick?.();
|
||||
}
|
||||
}}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
>
|
||||
{item.serviceData.serviceName}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)),
|
||||
Skeleton: jest
|
||||
.fn()
|
||||
.mockImplementation(() => <div data-testid="skeleton-mock" />),
|
||||
Typography: {
|
||||
Text: jest
|
||||
.fn()
|
||||
.mockImplementation(({ children }) => (
|
||||
<div data-testid="typography-text">{children}</div>
|
||||
)),
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
describe('DependentServices', () => {
|
||||
// Sample mock data to use in tests
|
||||
const mockDependentServicesData = [
|
||||
{
|
||||
key: 'service1',
|
||||
serviceData: {
|
||||
// eslint-disable-next-line sonarjs/no-duplicate-string
|
||||
serviceName: 'auth-service',
|
||||
count: 500,
|
||||
percentage: 62.5,
|
||||
},
|
||||
latency: 120,
|
||||
rate: '15',
|
||||
errorPercentage: '2.5',
|
||||
},
|
||||
{
|
||||
key: 'service2',
|
||||
serviceData: {
|
||||
serviceName: 'db-service',
|
||||
count: 300,
|
||||
percentage: 37.5,
|
||||
},
|
||||
latency: 80,
|
||||
rate: '10',
|
||||
errorPercentage: '1.2',
|
||||
},
|
||||
];
|
||||
|
||||
// Default props for tests
|
||||
const mockTimeRange = {
|
||||
startTime: 1609459200000,
|
||||
endTime: 1609545600000,
|
||||
};
|
||||
|
||||
const refetchFn = jest.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
(getFormattedDependentServicesData as jest.Mock).mockReturnValue(
|
||||
mockDependentServicesData,
|
||||
);
|
||||
});
|
||||
|
||||
it('renders loading state correctly', () => {
|
||||
// Arrange
|
||||
const mockQuery: MockQueryResult = {
|
||||
isLoading: true,
|
||||
isRefetching: false,
|
||||
isError: false,
|
||||
data: undefined,
|
||||
refetch: refetchFn,
|
||||
};
|
||||
|
||||
// Act
|
||||
const { container } = render(
|
||||
<DependentServices
|
||||
dependentServicesQuery={mockQuery as any}
|
||||
timeRange={mockTimeRange}
|
||||
/>,
|
||||
);
|
||||
|
||||
// Assert
|
||||
expect(container.querySelector('.ant-skeleton')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders error state correctly', () => {
|
||||
// Arrange
|
||||
const mockQuery: MockQueryResult = {
|
||||
isLoading: false,
|
||||
isRefetching: false,
|
||||
isError: true,
|
||||
data: undefined,
|
||||
refetch: refetchFn,
|
||||
};
|
||||
|
||||
// Act
|
||||
render(
|
||||
<DependentServices
|
||||
dependentServicesQuery={mockQuery as any}
|
||||
timeRange={mockTimeRange}
|
||||
/>,
|
||||
);
|
||||
|
||||
// Assert
|
||||
expect(screen.getByTestId('error-state-mock')).toBeInTheDocument();
|
||||
expect(ErrorState).toHaveBeenCalledWith(
|
||||
{ refetch: expect.any(Function) },
|
||||
expect.anything(),
|
||||
);
|
||||
});
|
||||
|
||||
it('renders data correctly when loaded', () => {
|
||||
// Arrange
|
||||
const mockData = {
|
||||
payload: {
|
||||
data: {
|
||||
result: [
|
||||
{
|
||||
table: {
|
||||
rows: [
|
||||
{
|
||||
data: {
|
||||
'service.name': 'auth-service',
|
||||
A: '500',
|
||||
B: '120000000',
|
||||
C: '15',
|
||||
F1: '2.5',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
} as SuccessResponse<any>;
|
||||
|
||||
const mockQuery: MockQueryResult = {
|
||||
isLoading: false,
|
||||
isRefetching: false,
|
||||
isError: false,
|
||||
data: mockData,
|
||||
refetch: refetchFn,
|
||||
};
|
||||
|
||||
// Act
|
||||
render(
|
||||
<DependentServices
|
||||
dependentServicesQuery={mockQuery as any}
|
||||
timeRange={mockTimeRange}
|
||||
/>,
|
||||
);
|
||||
|
||||
// Assert
|
||||
expect(getFormattedDependentServicesData).toHaveBeenCalledWith(
|
||||
mockData.payload.data.result[0].table.rows,
|
||||
);
|
||||
|
||||
// Check the table was rendered with the correct data
|
||||
expect(screen.getByTestId('table-mock')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('loading-state')).toHaveTextContent('Not Loading');
|
||||
expect(screen.getByTestId('row-count')).toHaveTextContent('2');
|
||||
|
||||
// Default (collapsed) pagination should be 5
|
||||
expect(screen.getByTestId('page-size')).toHaveTextContent('5');
|
||||
});
|
||||
|
||||
it('handles refetching state correctly', () => {
|
||||
// Arrange
|
||||
const mockQuery: MockQueryResult = {
|
||||
isLoading: false,
|
||||
isRefetching: true,
|
||||
isError: false,
|
||||
data: undefined,
|
||||
refetch: refetchFn,
|
||||
};
|
||||
|
||||
// Act
|
||||
const { container } = render(
|
||||
<DependentServices
|
||||
dependentServicesQuery={mockQuery as any}
|
||||
timeRange={mockTimeRange}
|
||||
/>,
|
||||
);
|
||||
|
||||
// Assert
|
||||
expect(container.querySelector('.ant-skeleton')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('handles row click correctly', () => {
|
||||
// Mock window.open
|
||||
const originalOpen = window.open;
|
||||
window.open = jest.fn();
|
||||
|
||||
// Arrange
|
||||
const mockData = {
|
||||
payload: {
|
||||
data: {
|
||||
result: [
|
||||
{
|
||||
table: {
|
||||
rows: [
|
||||
{
|
||||
data: {
|
||||
'service.name': 'auth-service',
|
||||
A: '500',
|
||||
B: '120000000',
|
||||
C: '15',
|
||||
F1: '2.5',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
} as SuccessResponse<any>;
|
||||
|
||||
const mockQuery: MockQueryResult = {
|
||||
isLoading: false,
|
||||
isRefetching: false,
|
||||
isError: false,
|
||||
data: mockData,
|
||||
refetch: refetchFn,
|
||||
};
|
||||
|
||||
// Act
|
||||
render(
|
||||
<DependentServices
|
||||
dependentServicesQuery={mockQuery as any}
|
||||
timeRange={mockTimeRange}
|
||||
/>,
|
||||
);
|
||||
|
||||
// Click on the first row
|
||||
fireEvent.click(screen.getByTestId('table-row-0'));
|
||||
|
||||
// Assert
|
||||
expect(window.open).toHaveBeenCalledWith(
|
||||
expect.stringContaining('/services/auth-service'),
|
||||
'_blank',
|
||||
);
|
||||
|
||||
// Restore original window.open
|
||||
window.open = originalOpen;
|
||||
});
|
||||
|
||||
it('expands table when showing more', () => {
|
||||
// Set up more than 5 items so the "show more" button appears
|
||||
const moreItems = Array(8)
|
||||
.fill(0)
|
||||
.map((_, index) => ({
|
||||
key: `service${index}`,
|
||||
serviceData: {
|
||||
serviceName: `service-${index}`,
|
||||
count: 100,
|
||||
percentage: 12.5,
|
||||
},
|
||||
latency: 100,
|
||||
rate: '10',
|
||||
errorPercentage: '1',
|
||||
}));
|
||||
|
||||
(getFormattedDependentServicesData as jest.Mock).mockReturnValue(moreItems);
|
||||
|
||||
const mockData = {
|
||||
payload: { data: { result: [{ table: { rows: [] } }] } },
|
||||
} as SuccessResponse<any>;
|
||||
const mockQuery: MockQueryResult = {
|
||||
isLoading: false,
|
||||
isRefetching: false,
|
||||
isError: false,
|
||||
data: mockData,
|
||||
refetch: refetchFn,
|
||||
};
|
||||
|
||||
// Render the component
|
||||
render(
|
||||
<DependentServices
|
||||
dependentServicesQuery={mockQuery as any}
|
||||
timeRange={mockTimeRange}
|
||||
/>,
|
||||
);
|
||||
|
||||
// Find the "Show more" button (using container query since it might not have a testId)
|
||||
const showMoreButton = screen.getByText(/Show more/i);
|
||||
expect(showMoreButton).toBeInTheDocument();
|
||||
|
||||
// Initial page size should be 5
|
||||
expect(screen.getByTestId('page-size')).toHaveTextContent('5');
|
||||
|
||||
// Click the button to expand
|
||||
fireEvent.click(showMoreButton);
|
||||
|
||||
// Page size should now be the full data length
|
||||
expect(screen.getByTestId('page-size')).toHaveTextContent('8');
|
||||
|
||||
// Text should have changed to "Show less"
|
||||
expect(screen.getByText(/Show less/i)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -1,386 +0,0 @@
|
||||
import { fireEvent, render, screen } from '@testing-library/react';
|
||||
import {
|
||||
END_POINT_DETAILS_QUERY_KEYS_ARRAY,
|
||||
extractPortAndEndpoint,
|
||||
getEndPointDetailsQueryPayload,
|
||||
getLatencyOverTimeWidgetData,
|
||||
getRateOverTimeWidgetData,
|
||||
} from 'container/ApiMonitoring/utils';
|
||||
import {
|
||||
CustomTimeType,
|
||||
Time,
|
||||
} from 'container/TopNav/DateTimeSelectionV2/config';
|
||||
import { useQueries } from 'react-query';
|
||||
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import {
|
||||
TagFilter,
|
||||
TagFilterItem,
|
||||
} from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
import { SPAN_ATTRIBUTES } from '../Explorer/Domains/DomainDetails/constants';
|
||||
import EndPointDetails from '../Explorer/Domains/DomainDetails/EndPointDetails';
|
||||
|
||||
// Mock dependencies
|
||||
jest.mock('react-query', () => ({
|
||||
useQueries: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('container/ApiMonitoring/utils', () => ({
|
||||
END_POINT_DETAILS_QUERY_KEYS_ARRAY: [
|
||||
'endPointMetricsData',
|
||||
'endPointStatusCodeData',
|
||||
'endPointDropDownData',
|
||||
'endPointDependentServicesData',
|
||||
'endPointStatusCodeBarChartsData',
|
||||
'endPointStatusCodeLatencyBarChartsData',
|
||||
],
|
||||
extractPortAndEndpoint: jest.fn(),
|
||||
getEndPointDetailsQueryPayload: jest.fn(),
|
||||
getLatencyOverTimeWidgetData: jest.fn(),
|
||||
getRateOverTimeWidgetData: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock(
|
||||
'container/QueryBuilder/filters/QueryBuilderSearchV2/QueryBuilderSearchV2',
|
||||
() => ({
|
||||
__esModule: true,
|
||||
default: jest.fn().mockImplementation(({ onChange }) => (
|
||||
<div data-testid="query-builder-search">
|
||||
<button
|
||||
type="button"
|
||||
data-testid="filter-change-button"
|
||||
onClick={(): void =>
|
||||
onChange({
|
||||
items: [
|
||||
{
|
||||
id: 'test-filter',
|
||||
key: {
|
||||
key: 'test.key',
|
||||
dataType: DataTypes.String,
|
||||
type: 'tag',
|
||||
isColumn: false,
|
||||
isJSON: false,
|
||||
},
|
||||
op: '=',
|
||||
value: 'test-value',
|
||||
},
|
||||
],
|
||||
op: 'AND',
|
||||
})
|
||||
}
|
||||
>
|
||||
Change Filter
|
||||
</button>
|
||||
</div>
|
||||
)),
|
||||
}),
|
||||
);
|
||||
|
||||
// Mock all child components to simplify testing
|
||||
jest.mock(
|
||||
'../Explorer/Domains/DomainDetails/components/EndPointMetrics',
|
||||
() => ({
|
||||
__esModule: true,
|
||||
default: jest
|
||||
.fn()
|
||||
.mockImplementation(() => (
|
||||
<div data-testid="endpoint-metrics">EndPoint Metrics</div>
|
||||
)),
|
||||
}),
|
||||
);
|
||||
|
||||
jest.mock(
|
||||
'../Explorer/Domains/DomainDetails/components/EndPointsDropDown',
|
||||
() => ({
|
||||
__esModule: true,
|
||||
default: jest.fn().mockImplementation(({ setSelectedEndPointName }) => (
|
||||
<div data-testid="endpoints-dropdown">
|
||||
<button
|
||||
type="button"
|
||||
data-testid="select-endpoint-button"
|
||||
onClick={(): void => setSelectedEndPointName('/api/new-endpoint')}
|
||||
>
|
||||
Select Endpoint
|
||||
</button>
|
||||
</div>
|
||||
)),
|
||||
}),
|
||||
);
|
||||
|
||||
jest.mock(
|
||||
'../Explorer/Domains/DomainDetails/components/DependentServices',
|
||||
() => ({
|
||||
__esModule: true,
|
||||
default: jest
|
||||
.fn()
|
||||
.mockImplementation(() => (
|
||||
<div data-testid="dependent-services">Dependent Services</div>
|
||||
)),
|
||||
}),
|
||||
);
|
||||
|
||||
jest.mock(
|
||||
'../Explorer/Domains/DomainDetails/components/StatusCodeBarCharts',
|
||||
() => ({
|
||||
__esModule: true,
|
||||
default: jest
|
||||
.fn()
|
||||
.mockImplementation(() => (
|
||||
<div data-testid="status-code-bar-charts">Status Code Bar Charts</div>
|
||||
)),
|
||||
}),
|
||||
);
|
||||
|
||||
jest.mock(
|
||||
'../Explorer/Domains/DomainDetails/components/StatusCodeTable',
|
||||
() => ({
|
||||
__esModule: true,
|
||||
default: jest
|
||||
.fn()
|
||||
.mockImplementation(() => (
|
||||
<div data-testid="status-code-table">Status Code Table</div>
|
||||
)),
|
||||
}),
|
||||
);
|
||||
|
||||
jest.mock(
|
||||
'../Explorer/Domains/DomainDetails/components/MetricOverTimeGraph',
|
||||
() => ({
|
||||
__esModule: true,
|
||||
default: jest
|
||||
.fn()
|
||||
.mockImplementation(({ widget }) => (
|
||||
<div data-testid={`metric-graph-${widget.title}`}>{widget.title} Graph</div>
|
||||
)),
|
||||
}),
|
||||
);
|
||||
|
||||
describe('EndPointDetails Component', () => {
|
||||
const mockQueryResults = Array(6).fill({
|
||||
data: { data: [] },
|
||||
isLoading: false,
|
||||
isError: false,
|
||||
error: null,
|
||||
});
|
||||
|
||||
const mockProps = {
|
||||
// eslint-disable-next-line sonarjs/no-duplicate-string
|
||||
domainName: 'test-domain',
|
||||
endPointName: '/api/test',
|
||||
setSelectedEndPointName: jest.fn(),
|
||||
initialFilters: { items: [], op: 'AND' } as TagFilter,
|
||||
timeRange: {
|
||||
startTime: 1609459200000,
|
||||
endTime: 1609545600000,
|
||||
},
|
||||
handleTimeChange: jest.fn() as (
|
||||
interval: Time | CustomTimeType,
|
||||
dateTimeRange?: [number, number],
|
||||
) => void,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
(extractPortAndEndpoint as jest.Mock).mockReturnValue({
|
||||
port: '8080',
|
||||
endpoint: '/api/test',
|
||||
});
|
||||
|
||||
(getEndPointDetailsQueryPayload as jest.Mock).mockReturnValue([
|
||||
{ id: 'query1', label: 'Query 1' },
|
||||
{ id: 'query2', label: 'Query 2' },
|
||||
{ id: 'query3', label: 'Query 3' },
|
||||
{ id: 'query4', label: 'Query 4' },
|
||||
{ id: 'query5', label: 'Query 5' },
|
||||
{ id: 'query6', label: 'Query 6' },
|
||||
]);
|
||||
|
||||
(getRateOverTimeWidgetData as jest.Mock).mockReturnValue({
|
||||
title: 'Rate Over Time',
|
||||
id: 'rate-widget',
|
||||
});
|
||||
|
||||
(getLatencyOverTimeWidgetData as jest.Mock).mockReturnValue({
|
||||
title: 'Latency Over Time',
|
||||
id: 'latency-widget',
|
||||
});
|
||||
|
||||
(useQueries as jest.Mock).mockReturnValue(mockQueryResults);
|
||||
});
|
||||
|
||||
it('renders the component correctly', () => {
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
render(<EndPointDetails {...mockProps} />);
|
||||
|
||||
// Check all major components are rendered
|
||||
expect(screen.getByTestId('query-builder-search')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('endpoints-dropdown')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('endpoint-metrics')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('dependent-services')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('status-code-bar-charts')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('status-code-table')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('metric-graph-Rate Over Time')).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByTestId('metric-graph-Latency Over Time'),
|
||||
).toBeInTheDocument();
|
||||
|
||||
// Check endpoint metadata is displayed
|
||||
expect(screen.getByText(/8080/i)).toBeInTheDocument();
|
||||
expect(screen.getByText('/api/test')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('calls getEndPointDetailsQueryPayload with correct parameters', () => {
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
render(<EndPointDetails {...mockProps} />);
|
||||
|
||||
expect(getEndPointDetailsQueryPayload).toHaveBeenCalledWith(
|
||||
'test-domain',
|
||||
mockProps.timeRange.startTime,
|
||||
mockProps.timeRange.endTime,
|
||||
expect.objectContaining({
|
||||
items: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
key: expect.objectContaining({ key: SPAN_ATTRIBUTES.URL_PATH }),
|
||||
value: '/api/test',
|
||||
}),
|
||||
]),
|
||||
op: 'AND',
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('adds endpoint filter to initial filters', () => {
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
render(<EndPointDetails {...mockProps} />);
|
||||
|
||||
expect(getEndPointDetailsQueryPayload).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
expect.anything(),
|
||||
expect.anything(),
|
||||
expect.objectContaining({
|
||||
items: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
key: expect.objectContaining({ key: SPAN_ATTRIBUTES.URL_PATH }),
|
||||
value: '/api/test',
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('updates filters when QueryBuilderSearch changes', () => {
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
render(<EndPointDetails {...mockProps} />);
|
||||
|
||||
// Trigger filter change
|
||||
fireEvent.click(screen.getByTestId('filter-change-button'));
|
||||
|
||||
// Check that filters were updated in subsequent calls to utility functions
|
||||
expect(getEndPointDetailsQueryPayload).toHaveBeenCalledTimes(2);
|
||||
expect(getEndPointDetailsQueryPayload).toHaveBeenLastCalledWith(
|
||||
expect.anything(),
|
||||
expect.anything(),
|
||||
expect.anything(),
|
||||
expect.objectContaining({
|
||||
items: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
key: expect.objectContaining({ key: 'test.key' }),
|
||||
value: 'test-value',
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('handles endpoint dropdown selection', () => {
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
render(<EndPointDetails {...mockProps} />);
|
||||
|
||||
// Trigger endpoint selection
|
||||
fireEvent.click(screen.getByTestId('select-endpoint-button'));
|
||||
|
||||
// Check if endpoint was updated
|
||||
expect(mockProps.setSelectedEndPointName).toHaveBeenCalledWith(
|
||||
'/api/new-endpoint',
|
||||
);
|
||||
});
|
||||
|
||||
it('does not display dependent services when service filter is applied', () => {
|
||||
const propsWithServiceFilter = {
|
||||
...mockProps,
|
||||
initialFilters: {
|
||||
items: [
|
||||
{
|
||||
id: 'service-filter',
|
||||
key: {
|
||||
key: 'service.name',
|
||||
dataType: DataTypes.String,
|
||||
type: 'tag',
|
||||
isColumn: false,
|
||||
isJSON: false,
|
||||
},
|
||||
op: '=',
|
||||
value: 'test-service',
|
||||
},
|
||||
] as TagFilterItem[],
|
||||
op: 'AND',
|
||||
} as TagFilter,
|
||||
};
|
||||
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
render(<EndPointDetails {...propsWithServiceFilter} />);
|
||||
|
||||
// Dependent services should not be displayed
|
||||
expect(screen.queryByTestId('dependent-services')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('passes the correct parameters to widget data generators', () => {
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
render(<EndPointDetails {...mockProps} />);
|
||||
|
||||
expect(getRateOverTimeWidgetData).toHaveBeenCalledWith(
|
||||
'test-domain',
|
||||
'/api/test',
|
||||
expect.objectContaining({
|
||||
items: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
key: expect.objectContaining({ key: SPAN_ATTRIBUTES.URL_PATH }),
|
||||
value: '/api/test',
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
);
|
||||
|
||||
expect(getLatencyOverTimeWidgetData).toHaveBeenCalledWith(
|
||||
'test-domain',
|
||||
'/api/test',
|
||||
expect.objectContaining({
|
||||
items: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
key: expect.objectContaining({ key: SPAN_ATTRIBUTES.URL_PATH }),
|
||||
value: '/api/test',
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('generates correct query parameters for useQueries', () => {
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
render(<EndPointDetails {...mockProps} />);
|
||||
|
||||
// Check if useQueries was called with correct parameters
|
||||
expect(useQueries).toHaveBeenCalledWith(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
queryKey: expect.arrayContaining([END_POINT_DETAILS_QUERY_KEYS_ARRAY[0]]),
|
||||
}),
|
||||
expect.objectContaining({
|
||||
queryKey: expect.arrayContaining([END_POINT_DETAILS_QUERY_KEYS_ARRAY[1]]),
|
||||
}),
|
||||
// ... and so on for other queries
|
||||
]),
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -1,211 +0,0 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { getFormattedEndPointMetricsData } from 'container/ApiMonitoring/utils';
|
||||
import { SuccessResponse } from 'types/api';
|
||||
|
||||
import EndPointMetrics from '../Explorer/Domains/DomainDetails/components/EndPointMetrics';
|
||||
import ErrorState from '../Explorer/Domains/DomainDetails/components/ErrorState';
|
||||
|
||||
// Create a partial mock of the UseQueryResult interface for testing
|
||||
interface MockQueryResult {
|
||||
isLoading: boolean;
|
||||
isRefetching: boolean;
|
||||
isError: boolean;
|
||||
data?: any;
|
||||
refetch: () => void;
|
||||
}
|
||||
|
||||
// Mock the utils function
|
||||
jest.mock('container/ApiMonitoring/utils', () => ({
|
||||
getFormattedEndPointMetricsData: jest.fn(),
|
||||
}));
|
||||
|
||||
// Mock the ErrorState component
|
||||
jest.mock('../Explorer/Domains/DomainDetails/components/ErrorState', () => ({
|
||||
__esModule: true,
|
||||
default: jest.fn().mockImplementation(({ refetch }) => (
|
||||
<div data-testid="error-state-mock">
|
||||
<button type="button" data-testid="refetch-button" onClick={refetch}>
|
||||
Retry
|
||||
</button>
|
||||
</div>
|
||||
)),
|
||||
}));
|
||||
|
||||
// Mock antd components
|
||||
jest.mock('antd', () => {
|
||||
const originalModule = jest.requireActual('antd');
|
||||
return {
|
||||
...originalModule,
|
||||
Progress: jest
|
||||
.fn()
|
||||
.mockImplementation(() => <div data-testid="progress-bar-mock" />),
|
||||
Skeleton: {
|
||||
Button: jest
|
||||
.fn()
|
||||
.mockImplementation(() => <div data-testid="skeleton-button-mock" />),
|
||||
},
|
||||
Tooltip: jest
|
||||
.fn()
|
||||
.mockImplementation(({ children }) => (
|
||||
<div data-testid="tooltip-mock">{children}</div>
|
||||
)),
|
||||
Typography: {
|
||||
Text: jest.fn().mockImplementation(({ children, className }) => (
|
||||
<div data-testid={`typography-${className}`} className={className}>
|
||||
{children}
|
||||
</div>
|
||||
)),
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
describe('EndPointMetrics', () => {
|
||||
// Common metric data to use in tests
|
||||
const mockMetricsData = {
|
||||
key: 'test-key',
|
||||
rate: '42',
|
||||
latency: 99,
|
||||
errorRate: 5.5,
|
||||
lastUsed: '5 minutes ago',
|
||||
};
|
||||
|
||||
// Basic props for tests
|
||||
const refetchFn = jest.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
(getFormattedEndPointMetricsData as jest.Mock).mockReturnValue(
|
||||
mockMetricsData,
|
||||
);
|
||||
});
|
||||
|
||||
it('renders loading state correctly', () => {
|
||||
const mockQuery: MockQueryResult = {
|
||||
isLoading: true,
|
||||
isRefetching: false,
|
||||
isError: false,
|
||||
data: undefined,
|
||||
refetch: refetchFn,
|
||||
};
|
||||
|
||||
render(<EndPointMetrics endPointMetricsDataQuery={mockQuery as any} />);
|
||||
|
||||
// Verify skeleton loaders are visible
|
||||
const skeletonElements = screen.getAllByTestId('skeleton-button-mock');
|
||||
expect(skeletonElements.length).toBe(4);
|
||||
|
||||
// Verify labels are visible even during loading
|
||||
expect(screen.getByText('Rate')).toBeInTheDocument();
|
||||
expect(screen.getByText('AVERAGE LATENCY')).toBeInTheDocument();
|
||||
expect(screen.getByText('ERROR %')).toBeInTheDocument();
|
||||
expect(screen.getByText('LAST USED')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders error state correctly', () => {
|
||||
const mockQuery: MockQueryResult = {
|
||||
isLoading: false,
|
||||
isRefetching: false,
|
||||
isError: true,
|
||||
data: undefined,
|
||||
refetch: refetchFn,
|
||||
};
|
||||
|
||||
render(<EndPointMetrics endPointMetricsDataQuery={mockQuery as any} />);
|
||||
|
||||
// Verify error state is shown
|
||||
expect(screen.getByTestId('error-state-mock')).toBeInTheDocument();
|
||||
expect(ErrorState).toHaveBeenCalledWith(
|
||||
{ refetch: expect.any(Function) },
|
||||
expect.anything(),
|
||||
);
|
||||
});
|
||||
|
||||
it('renders data correctly when loaded', () => {
|
||||
const mockData = {
|
||||
payload: {
|
||||
data: {
|
||||
result: [
|
||||
{
|
||||
table: {
|
||||
rows: [
|
||||
{ data: { A: '42', B: '99000000', D: '1609459200000000', F1: '5.5' } },
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
} as SuccessResponse<any>;
|
||||
|
||||
const mockQuery: MockQueryResult = {
|
||||
isLoading: false,
|
||||
isRefetching: false,
|
||||
isError: false,
|
||||
data: mockData,
|
||||
refetch: refetchFn,
|
||||
};
|
||||
|
||||
render(<EndPointMetrics endPointMetricsDataQuery={mockQuery as any} />);
|
||||
|
||||
// Verify the utils function was called with the data
|
||||
expect(getFormattedEndPointMetricsData).toHaveBeenCalledWith(
|
||||
mockData.payload.data.result[0].table.rows,
|
||||
);
|
||||
|
||||
// Verify data is displayed
|
||||
expect(
|
||||
screen.getByText(`${mockMetricsData.rate} ops/sec`),
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getByText(`${mockMetricsData.latency}ms`)).toBeInTheDocument();
|
||||
expect(screen.getByText(mockMetricsData.lastUsed)).toBeInTheDocument();
|
||||
expect(screen.getByTestId('progress-bar-mock')).toBeInTheDocument(); // For error rate
|
||||
});
|
||||
|
||||
it('handles refetching state correctly', () => {
|
||||
const mockQuery: MockQueryResult = {
|
||||
isLoading: false,
|
||||
isRefetching: true,
|
||||
isError: false,
|
||||
data: undefined,
|
||||
refetch: refetchFn,
|
||||
};
|
||||
|
||||
render(<EndPointMetrics endPointMetricsDataQuery={mockQuery as any} />);
|
||||
|
||||
// Verify skeleton loaders are visible during refetching
|
||||
const skeletonElements = screen.getAllByTestId('skeleton-button-mock');
|
||||
expect(skeletonElements.length).toBe(4);
|
||||
});
|
||||
|
||||
it('handles null metrics data gracefully', () => {
|
||||
// Mock the utils function to return null to simulate missing data
|
||||
(getFormattedEndPointMetricsData as jest.Mock).mockReturnValue(null);
|
||||
|
||||
const mockData = {
|
||||
payload: {
|
||||
data: {
|
||||
result: [
|
||||
{
|
||||
table: {
|
||||
rows: [],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
} as SuccessResponse<any>;
|
||||
|
||||
const mockQuery: MockQueryResult = {
|
||||
isLoading: false,
|
||||
isRefetching: false,
|
||||
isError: false,
|
||||
data: mockData,
|
||||
refetch: refetchFn,
|
||||
};
|
||||
|
||||
render(<EndPointMetrics endPointMetricsDataQuery={mockQuery as any} />);
|
||||
|
||||
// Even with null data, the component should render without crashing
|
||||
expect(screen.getByText('Rate')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -1,221 +0,0 @@
|
||||
import { fireEvent, render, screen } from '@testing-library/react';
|
||||
import { getFormattedEndPointDropDownData } from 'container/ApiMonitoring/utils';
|
||||
|
||||
import EndPointsDropDown from '../Explorer/Domains/DomainDetails/components/EndPointsDropDown';
|
||||
import { SPAN_ATTRIBUTES } from '../Explorer/Domains/DomainDetails/constants';
|
||||
|
||||
// Mock the Select component from antd
|
||||
jest.mock('antd', () => {
|
||||
const originalModule = jest.requireActual('antd');
|
||||
return {
|
||||
...originalModule,
|
||||
Select: jest
|
||||
.fn()
|
||||
.mockImplementation(({ value, loading, onChange, options, onClear }) => (
|
||||
<div data-testid="mock-select">
|
||||
<div data-testid="select-value">{value}</div>
|
||||
<div data-testid="select-loading">
|
||||
{loading ? 'loading' : 'not-loading'}
|
||||
</div>
|
||||
<select
|
||||
data-testid="select-element"
|
||||
value={value || ''}
|
||||
onChange={(e): void => onChange(e.target.value)}
|
||||
>
|
||||
<option value="">Select...</option>
|
||||
{options?.map((option: { value: string; label: string; key: string }) => (
|
||||
<option key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<button data-testid="select-clear-button" type="button" onClick={onClear}>
|
||||
Clear
|
||||
</button>
|
||||
</div>
|
||||
)),
|
||||
};
|
||||
});
|
||||
|
||||
// Mock the utilities
|
||||
jest.mock('container/ApiMonitoring/utils', () => ({
|
||||
getFormattedEndPointDropDownData: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('EndPointsDropDown Component', () => {
|
||||
const mockEndPoints = [
|
||||
// eslint-disable-next-line sonarjs/no-duplicate-string
|
||||
{ key: '1', value: '/api/endpoint1', label: '/api/endpoint1' },
|
||||
// eslint-disable-next-line sonarjs/no-duplicate-string
|
||||
{ key: '2', value: '/api/endpoint2', label: '/api/endpoint2' },
|
||||
];
|
||||
|
||||
const mockSetSelectedEndPointName = jest.fn();
|
||||
|
||||
// Create a mock that satisfies the UseQueryResult interface
|
||||
const createMockQueryResult = (overrides: any = {}): any => ({
|
||||
data: {
|
||||
payload: {
|
||||
data: {
|
||||
result: [
|
||||
{
|
||||
table: {
|
||||
rows: [],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
dataUpdatedAt: 0,
|
||||
error: null,
|
||||
errorUpdatedAt: 0,
|
||||
failureCount: 0,
|
||||
isError: false,
|
||||
isFetched: true,
|
||||
isFetchedAfterMount: true,
|
||||
isFetching: false,
|
||||
isIdle: false,
|
||||
isLoading: false,
|
||||
isLoadingError: false,
|
||||
isPlaceholderData: false,
|
||||
isPreviousData: false,
|
||||
isRefetchError: false,
|
||||
isRefetching: false,
|
||||
isStale: false,
|
||||
isSuccess: true,
|
||||
refetch: jest.fn(),
|
||||
remove: jest.fn(),
|
||||
status: 'success',
|
||||
...overrides,
|
||||
});
|
||||
|
||||
const defaultProps = {
|
||||
selectedEndPointName: '',
|
||||
setSelectedEndPointName: mockSetSelectedEndPointName,
|
||||
endPointDropDownDataQuery: createMockQueryResult(),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
(getFormattedEndPointDropDownData as jest.Mock).mockReturnValue(
|
||||
mockEndPoints,
|
||||
);
|
||||
});
|
||||
|
||||
it('renders the component correctly', () => {
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
render(<EndPointsDropDown {...defaultProps} />);
|
||||
|
||||
expect(screen.getByTestId('mock-select')).toBeInTheDocument();
|
||||
// eslint-disable-next-line sonarjs/no-duplicate-string
|
||||
expect(screen.getByTestId('select-loading')).toHaveTextContent('not-loading');
|
||||
});
|
||||
|
||||
it('shows loading state when data is loading', () => {
|
||||
const loadingProps = {
|
||||
...defaultProps,
|
||||
endPointDropDownDataQuery: createMockQueryResult({
|
||||
isLoading: true,
|
||||
}),
|
||||
};
|
||||
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
render(<EndPointsDropDown {...loadingProps} />);
|
||||
|
||||
expect(screen.getByTestId('select-loading')).toHaveTextContent('loading');
|
||||
});
|
||||
|
||||
it('shows loading state when data is fetching', () => {
|
||||
const fetchingProps = {
|
||||
...defaultProps,
|
||||
endPointDropDownDataQuery: createMockQueryResult({
|
||||
isFetching: true,
|
||||
}),
|
||||
};
|
||||
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
render(<EndPointsDropDown {...fetchingProps} />);
|
||||
|
||||
expect(screen.getByTestId('select-loading')).toHaveTextContent('loading');
|
||||
});
|
||||
|
||||
it('displays the selected endpoint', () => {
|
||||
const selectedProps = {
|
||||
...defaultProps,
|
||||
selectedEndPointName: '/api/endpoint1',
|
||||
};
|
||||
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
render(<EndPointsDropDown {...selectedProps} />);
|
||||
|
||||
expect(screen.getByTestId('select-value')).toHaveTextContent(
|
||||
'/api/endpoint1',
|
||||
);
|
||||
});
|
||||
|
||||
it('calls setSelectedEndPointName when an option is selected', () => {
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
render(<EndPointsDropDown {...defaultProps} />);
|
||||
|
||||
// Get the select element and change its value
|
||||
const selectElement = screen.getByTestId('select-element');
|
||||
fireEvent.change(selectElement, { target: { value: '/api/endpoint2' } });
|
||||
|
||||
expect(mockSetSelectedEndPointName).toHaveBeenCalledWith('/api/endpoint2');
|
||||
});
|
||||
|
||||
it('calls setSelectedEndPointName with empty string when cleared', () => {
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
render(<EndPointsDropDown {...defaultProps} />);
|
||||
|
||||
// Click the clear button
|
||||
const clearButton = screen.getByTestId('select-clear-button');
|
||||
fireEvent.click(clearButton);
|
||||
|
||||
expect(mockSetSelectedEndPointName).toHaveBeenCalledWith('');
|
||||
});
|
||||
|
||||
it('passes dropdown style prop correctly', () => {
|
||||
const styleProps = {
|
||||
...defaultProps,
|
||||
dropdownStyle: { maxHeight: '200px' },
|
||||
};
|
||||
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
render(<EndPointsDropDown {...styleProps} />);
|
||||
|
||||
// We can't easily test style props in our mock, but at least ensure the component rendered
|
||||
expect(screen.getByTestId('mock-select')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('formats data using the utility function', () => {
|
||||
const mockRows = [
|
||||
{ data: { [SPAN_ATTRIBUTES.URL_PATH]: '/api/test', A: 10 } },
|
||||
];
|
||||
|
||||
const dataProps = {
|
||||
...defaultProps,
|
||||
endPointDropDownDataQuery: createMockQueryResult({
|
||||
data: {
|
||||
payload: {
|
||||
data: {
|
||||
result: [
|
||||
{
|
||||
table: {
|
||||
rows: mockRows,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
};
|
||||
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
render(<EndPointsDropDown {...dataProps} />);
|
||||
|
||||
expect(getFormattedEndPointDropDownData).toHaveBeenCalledWith(mockRows);
|
||||
});
|
||||
});
|
||||
@@ -1,493 +0,0 @@
|
||||
import { fireEvent, render, screen } from '@testing-library/react';
|
||||
import {
|
||||
getCustomFiltersForBarChart,
|
||||
getFormattedEndPointStatusCodeChartData,
|
||||
getStatusCodeBarChartWidgetData,
|
||||
} from 'container/ApiMonitoring/utils';
|
||||
import { SuccessResponse } from 'types/api';
|
||||
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
import ErrorState from '../Explorer/Domains/DomainDetails/components/ErrorState';
|
||||
import StatusCodeBarCharts from '../Explorer/Domains/DomainDetails/components/StatusCodeBarCharts';
|
||||
|
||||
// Create a partial mock of the UseQueryResult interface for testing
|
||||
interface MockQueryResult {
|
||||
isLoading: boolean;
|
||||
isRefetching: boolean;
|
||||
isError: boolean;
|
||||
error?: Error;
|
||||
data?: any;
|
||||
refetch: () => void;
|
||||
}
|
||||
|
||||
// Mocks
|
||||
jest.mock('components/Uplot', () => ({
|
||||
__esModule: true,
|
||||
default: jest.fn().mockImplementation(() => <div data-testid="uplot-mock" />),
|
||||
}));
|
||||
|
||||
jest.mock('components/CeleryTask/useGetGraphCustomSeries', () => ({
|
||||
useGetGraphCustomSeries: (): { getCustomSeries: jest.Mock } => ({
|
||||
getCustomSeries: jest.fn(),
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('components/CeleryTask/useNavigateToExplorer', () => ({
|
||||
useNavigateToExplorer: (): { navigateToExplorer: jest.Mock } => ({
|
||||
navigateToExplorer: jest.fn(),
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('container/GridCardLayout/useGraphClickToShowButton', () => ({
|
||||
useGraphClickToShowButton: (): {
|
||||
componentClick: boolean;
|
||||
htmlRef: HTMLElement | null;
|
||||
} => ({
|
||||
componentClick: false,
|
||||
htmlRef: null,
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('container/GridCardLayout/useNavigateToExplorerPages', () => ({
|
||||
__esModule: true,
|
||||
default: (): { navigateToExplorerPages: jest.Mock } => ({
|
||||
navigateToExplorerPages: jest.fn(),
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('hooks/useDarkMode', () => ({
|
||||
useIsDarkMode: (): boolean => false,
|
||||
}));
|
||||
|
||||
jest.mock('hooks/useDimensions', () => ({
|
||||
useResizeObserver: (): { width: number; height: number } => ({
|
||||
width: 800,
|
||||
height: 400,
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('hooks/useNotifications', () => ({
|
||||
useNotifications: (): { notifications: [] } => ({ notifications: [] }),
|
||||
}));
|
||||
|
||||
jest.mock('lib/uPlotLib/getUplotChartOptions', () => ({
|
||||
getUPlotChartOptions: jest.fn().mockReturnValue({}),
|
||||
}));
|
||||
|
||||
jest.mock('lib/uPlotLib/utils/getUplotChartData', () => ({
|
||||
getUPlotChartData: jest.fn().mockReturnValue([]),
|
||||
}));
|
||||
|
||||
// Mock utility functions
|
||||
jest.mock('container/ApiMonitoring/utils', () => ({
|
||||
getFormattedEndPointStatusCodeChartData: jest.fn(),
|
||||
getStatusCodeBarChartWidgetData: jest.fn(),
|
||||
getCustomFiltersForBarChart: jest.fn(),
|
||||
statusCodeWidgetInfo: [
|
||||
{ title: 'Status Code Count', yAxisUnit: 'count' },
|
||||
{ title: 'Status Code Latency', yAxisUnit: 'ms' },
|
||||
],
|
||||
}));
|
||||
|
||||
// Mock the ErrorState component
|
||||
jest.mock('../Explorer/Domains/DomainDetails/components/ErrorState', () => ({
|
||||
__esModule: true,
|
||||
default: jest.fn().mockImplementation(({ refetch }) => (
|
||||
<div data-testid="error-state-mock">
|
||||
<button type="button" data-testid="refetch-button" onClick={refetch}>
|
||||
Retry
|
||||
</button>
|
||||
</div>
|
||||
)),
|
||||
}));
|
||||
|
||||
// Mock antd components
|
||||
jest.mock('antd', () => {
|
||||
const originalModule = jest.requireActual('antd');
|
||||
return {
|
||||
...originalModule,
|
||||
Card: jest.fn().mockImplementation(({ children, className }) => (
|
||||
<div data-testid="card-mock" className={className}>
|
||||
{children}
|
||||
</div>
|
||||
)),
|
||||
Typography: {
|
||||
Text: jest
|
||||
.fn()
|
||||
.mockImplementation(({ children }) => (
|
||||
<div data-testid="typography-text">{children}</div>
|
||||
)),
|
||||
},
|
||||
Button: {
|
||||
...originalModule.Button,
|
||||
Group: jest.fn().mockImplementation(({ children, className }) => (
|
||||
<div data-testid="button-group" className={className}>
|
||||
{children}
|
||||
</div>
|
||||
)),
|
||||
},
|
||||
Skeleton: jest
|
||||
.fn()
|
||||
.mockImplementation(() => (
|
||||
<div data-testid="skeleton-mock">Loading skeleton...</div>
|
||||
)),
|
||||
};
|
||||
});
|
||||
|
||||
describe('StatusCodeBarCharts', () => {
|
||||
// Default props for tests
|
||||
const mockFilters: IBuilderQuery['filters'] = { items: [], op: 'AND' };
|
||||
const mockTimeRange = {
|
||||
startTime: 1609459200000,
|
||||
endTime: 1609545600000,
|
||||
};
|
||||
const mockDomainName = 'test-domain';
|
||||
const mockEndPointName = '/api/test';
|
||||
const onDragSelectMock = jest.fn();
|
||||
const refetchFn = jest.fn();
|
||||
|
||||
// Mock formatted data
|
||||
const mockFormattedData = {
|
||||
data: {
|
||||
result: [
|
||||
{
|
||||
values: [[1609459200, 10]],
|
||||
metric: { statusCode: '200-299' },
|
||||
queryName: 'A',
|
||||
},
|
||||
{
|
||||
values: [[1609459200, 5]],
|
||||
metric: { statusCode: '400-499' },
|
||||
queryName: 'B',
|
||||
},
|
||||
],
|
||||
resultType: 'matrix',
|
||||
},
|
||||
};
|
||||
|
||||
// Mock filter values
|
||||
const mockStatusCodeFilters = [
|
||||
{
|
||||
id: 'test-id-1',
|
||||
key: {
|
||||
dataType: 'string',
|
||||
id: 'response_status_code--string--tag--false',
|
||||
isColumn: false,
|
||||
isJSON: false,
|
||||
key: 'response_status_code',
|
||||
type: 'tag',
|
||||
},
|
||||
op: '>=',
|
||||
value: '200',
|
||||
},
|
||||
{
|
||||
id: 'test-id-2',
|
||||
key: {
|
||||
dataType: 'string',
|
||||
id: 'response_status_code--string--tag--false',
|
||||
isColumn: false,
|
||||
isJSON: false,
|
||||
key: 'response_status_code',
|
||||
type: 'tag',
|
||||
},
|
||||
op: '<=',
|
||||
value: '299',
|
||||
},
|
||||
];
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
(getFormattedEndPointStatusCodeChartData as jest.Mock).mockReturnValue(
|
||||
mockFormattedData,
|
||||
);
|
||||
(getStatusCodeBarChartWidgetData as jest.Mock).mockReturnValue({
|
||||
id: 'test-widget',
|
||||
title: 'Status Code',
|
||||
description: 'Shows status code distribution',
|
||||
query: { builder: { queryData: [] } },
|
||||
panelTypes: 'bar',
|
||||
});
|
||||
(getCustomFiltersForBarChart as jest.Mock).mockReturnValue(
|
||||
mockStatusCodeFilters,
|
||||
);
|
||||
});
|
||||
|
||||
it('renders loading state correctly', () => {
|
||||
// Arrange
|
||||
const mockStatusCodeQuery: MockQueryResult = {
|
||||
isLoading: true,
|
||||
isRefetching: false,
|
||||
isError: false,
|
||||
data: undefined,
|
||||
refetch: refetchFn,
|
||||
};
|
||||
|
||||
const mockLatencyQuery: MockQueryResult = {
|
||||
isLoading: false,
|
||||
isRefetching: false,
|
||||
isError: false,
|
||||
data: undefined,
|
||||
refetch: refetchFn,
|
||||
};
|
||||
|
||||
// Act
|
||||
render(
|
||||
<StatusCodeBarCharts
|
||||
endPointStatusCodeBarChartsDataQuery={mockStatusCodeQuery as any}
|
||||
endPointStatusCodeLatencyBarChartsDataQuery={mockLatencyQuery as any}
|
||||
domainName={mockDomainName}
|
||||
endPointName={mockEndPointName}
|
||||
filters={mockFilters}
|
||||
timeRange={mockTimeRange}
|
||||
onDragSelect={onDragSelectMock}
|
||||
/>,
|
||||
);
|
||||
|
||||
// Assert
|
||||
expect(screen.getByTestId('skeleton-mock')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders error state correctly', () => {
|
||||
// Arrange
|
||||
const mockStatusCodeQuery: MockQueryResult = {
|
||||
isLoading: false,
|
||||
isRefetching: false,
|
||||
isError: true,
|
||||
error: new Error('Test error'),
|
||||
data: undefined,
|
||||
refetch: refetchFn,
|
||||
};
|
||||
|
||||
const mockLatencyQuery: MockQueryResult = {
|
||||
isLoading: false,
|
||||
isRefetching: false,
|
||||
isError: false,
|
||||
data: undefined,
|
||||
refetch: refetchFn,
|
||||
};
|
||||
|
||||
// Act
|
||||
render(
|
||||
<StatusCodeBarCharts
|
||||
endPointStatusCodeBarChartsDataQuery={mockStatusCodeQuery as any}
|
||||
endPointStatusCodeLatencyBarChartsDataQuery={mockLatencyQuery as any}
|
||||
domainName={mockDomainName}
|
||||
endPointName={mockEndPointName}
|
||||
filters={mockFilters}
|
||||
timeRange={mockTimeRange}
|
||||
onDragSelect={onDragSelectMock}
|
||||
/>,
|
||||
);
|
||||
|
||||
// Assert
|
||||
expect(screen.getByTestId('error-state-mock')).toBeInTheDocument();
|
||||
expect(ErrorState).toHaveBeenCalledWith(
|
||||
{ refetch: expect.any(Function) },
|
||||
expect.anything(),
|
||||
);
|
||||
});
|
||||
|
||||
it('renders chart data correctly when loaded', () => {
|
||||
// Arrange
|
||||
const mockData = {
|
||||
payload: mockFormattedData,
|
||||
} as SuccessResponse<any>;
|
||||
|
||||
const mockStatusCodeQuery: MockQueryResult = {
|
||||
isLoading: false,
|
||||
isRefetching: false,
|
||||
isError: false,
|
||||
data: mockData,
|
||||
refetch: refetchFn,
|
||||
};
|
||||
|
||||
const mockLatencyQuery: MockQueryResult = {
|
||||
isLoading: false,
|
||||
isRefetching: false,
|
||||
isError: false,
|
||||
data: mockData,
|
||||
refetch: refetchFn,
|
||||
};
|
||||
|
||||
// Act
|
||||
render(
|
||||
<StatusCodeBarCharts
|
||||
endPointStatusCodeBarChartsDataQuery={mockStatusCodeQuery as any}
|
||||
endPointStatusCodeLatencyBarChartsDataQuery={mockLatencyQuery as any}
|
||||
domainName={mockDomainName}
|
||||
endPointName={mockEndPointName}
|
||||
filters={mockFilters}
|
||||
timeRange={mockTimeRange}
|
||||
onDragSelect={onDragSelectMock}
|
||||
/>,
|
||||
);
|
||||
|
||||
// Assert
|
||||
expect(getFormattedEndPointStatusCodeChartData).toHaveBeenCalledWith(
|
||||
mockData.payload,
|
||||
'sum',
|
||||
);
|
||||
expect(screen.getByTestId('uplot-mock')).toBeInTheDocument();
|
||||
expect(screen.getByText('Number of calls')).toBeInTheDocument();
|
||||
expect(screen.getByText('Latency')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('switches between number of calls and latency views', () => {
|
||||
// Arrange
|
||||
const mockData = {
|
||||
payload: mockFormattedData,
|
||||
} as SuccessResponse<any>;
|
||||
|
||||
const mockStatusCodeQuery: MockQueryResult = {
|
||||
isLoading: false,
|
||||
isRefetching: false,
|
||||
isError: false,
|
||||
data: mockData,
|
||||
refetch: refetchFn,
|
||||
};
|
||||
|
||||
const mockLatencyQuery: MockQueryResult = {
|
||||
isLoading: false,
|
||||
isRefetching: false,
|
||||
isError: false,
|
||||
data: mockData,
|
||||
refetch: refetchFn,
|
||||
};
|
||||
|
||||
// Act
|
||||
render(
|
||||
<StatusCodeBarCharts
|
||||
endPointStatusCodeBarChartsDataQuery={mockStatusCodeQuery as any}
|
||||
endPointStatusCodeLatencyBarChartsDataQuery={mockLatencyQuery as any}
|
||||
domainName={mockDomainName}
|
||||
endPointName={mockEndPointName}
|
||||
filters={mockFilters}
|
||||
timeRange={mockTimeRange}
|
||||
onDragSelect={onDragSelectMock}
|
||||
/>,
|
||||
);
|
||||
|
||||
// Initially should be showing number of calls (index 0)
|
||||
const latencyButton = screen.getByText('Latency');
|
||||
|
||||
// Click to switch to latency view
|
||||
fireEvent.click(latencyButton);
|
||||
|
||||
// Should now format with the latency data
|
||||
expect(getFormattedEndPointStatusCodeChartData).toHaveBeenCalledWith(
|
||||
mockData.payload,
|
||||
'average',
|
||||
);
|
||||
});
|
||||
|
||||
it('uses getCustomFiltersForBarChart when needed', () => {
|
||||
// Arrange
|
||||
const mockData = {
|
||||
payload: mockFormattedData,
|
||||
} as SuccessResponse<any>;
|
||||
|
||||
const mockStatusCodeQuery: MockQueryResult = {
|
||||
isLoading: false,
|
||||
isRefetching: false,
|
||||
isError: false,
|
||||
data: mockData,
|
||||
refetch: refetchFn,
|
||||
};
|
||||
|
||||
const mockLatencyQuery: MockQueryResult = {
|
||||
isLoading: false,
|
||||
isRefetching: false,
|
||||
isError: false,
|
||||
data: mockData,
|
||||
refetch: refetchFn,
|
||||
};
|
||||
|
||||
// Act
|
||||
render(
|
||||
<StatusCodeBarCharts
|
||||
endPointStatusCodeBarChartsDataQuery={mockStatusCodeQuery as any}
|
||||
endPointStatusCodeLatencyBarChartsDataQuery={mockLatencyQuery as any}
|
||||
domainName={mockDomainName}
|
||||
endPointName={mockEndPointName}
|
||||
filters={mockFilters}
|
||||
timeRange={mockTimeRange}
|
||||
onDragSelect={onDragSelectMock}
|
||||
/>,
|
||||
);
|
||||
|
||||
// Assert
|
||||
// Initially getCustomFiltersForBarChart won't be called until a graph click event
|
||||
expect(getCustomFiltersForBarChart).not.toHaveBeenCalled();
|
||||
|
||||
// We can't easily test the graph click handler directly,
|
||||
// but we've confirmed the function is mocked and ready to be tested
|
||||
expect(getStatusCodeBarChartWidgetData).toHaveBeenCalledWith(
|
||||
mockDomainName,
|
||||
mockEndPointName,
|
||||
expect.objectContaining({
|
||||
items: [],
|
||||
op: 'AND',
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('handles widget generation with current filters', () => {
|
||||
// Arrange
|
||||
const mockCustomFilters = {
|
||||
items: [
|
||||
{
|
||||
id: 'custom-filter',
|
||||
key: { key: 'test-key' },
|
||||
op: '=',
|
||||
value: 'test-value',
|
||||
},
|
||||
],
|
||||
op: 'AND',
|
||||
};
|
||||
|
||||
const mockData = {
|
||||
payload: mockFormattedData,
|
||||
} as SuccessResponse<any>;
|
||||
|
||||
const mockStatusCodeQuery: MockQueryResult = {
|
||||
isLoading: false,
|
||||
isRefetching: false,
|
||||
isError: false,
|
||||
data: mockData,
|
||||
refetch: refetchFn,
|
||||
};
|
||||
|
||||
const mockLatencyQuery: MockQueryResult = {
|
||||
isLoading: false,
|
||||
isRefetching: false,
|
||||
isError: false,
|
||||
data: mockData,
|
||||
refetch: refetchFn,
|
||||
};
|
||||
|
||||
// Act
|
||||
render(
|
||||
<StatusCodeBarCharts
|
||||
endPointStatusCodeBarChartsDataQuery={mockStatusCodeQuery as any}
|
||||
endPointStatusCodeLatencyBarChartsDataQuery={mockLatencyQuery as any}
|
||||
domainName={mockDomainName}
|
||||
endPointName={mockEndPointName}
|
||||
filters={mockCustomFilters as IBuilderQuery['filters']}
|
||||
timeRange={mockTimeRange}
|
||||
onDragSelect={onDragSelectMock}
|
||||
/>,
|
||||
);
|
||||
|
||||
// Assert widget creation was called with the correct parameters
|
||||
expect(getStatusCodeBarChartWidgetData).toHaveBeenCalledWith(
|
||||
mockDomainName,
|
||||
mockEndPointName,
|
||||
expect.objectContaining({
|
||||
items: expect.arrayContaining([
|
||||
expect.objectContaining({ id: 'custom-filter' }),
|
||||
]),
|
||||
op: 'AND',
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -1,175 +0,0 @@
|
||||
import '@testing-library/jest-dom';
|
||||
|
||||
import { render, screen } from '@testing-library/react';
|
||||
|
||||
import StatusCodeTable from '../Explorer/Domains/DomainDetails/components/StatusCodeTable';
|
||||
|
||||
// Mock the ErrorState component
|
||||
jest.mock('../Explorer/Domains/DomainDetails/components/ErrorState', () =>
|
||||
jest.fn().mockImplementation(({ refetch }) => (
|
||||
<div
|
||||
data-testid="error-state-mock"
|
||||
onClick={refetch}
|
||||
onKeyDown={(e: React.KeyboardEvent<HTMLDivElement>): void => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
refetch();
|
||||
}
|
||||
}}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
>
|
||||
Error state
|
||||
</div>
|
||||
)),
|
||||
);
|
||||
|
||||
// Mock antd components
|
||||
jest.mock('antd', () => {
|
||||
const originalModule = jest.requireActual('antd');
|
||||
return {
|
||||
...originalModule,
|
||||
Table: jest
|
||||
.fn()
|
||||
.mockImplementation(({ loading, dataSource, columns, locale }) => (
|
||||
<div data-testid="table-mock">
|
||||
{loading && <div data-testid="loading-indicator">Loading...</div>}
|
||||
{dataSource &&
|
||||
dataSource.length === 0 &&
|
||||
!loading &&
|
||||
locale?.emptyText && (
|
||||
<div data-testid="empty-table">{locale.emptyText}</div>
|
||||
)}
|
||||
{dataSource && dataSource.length > 0 && (
|
||||
<div data-testid="table-data">
|
||||
Data loaded with {dataSource.length} rows and {columns.length} columns
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)),
|
||||
Typography: {
|
||||
Text: jest.fn().mockImplementation(({ children, className }) => (
|
||||
<div data-testid="typography-text" className={className}>
|
||||
{children}
|
||||
</div>
|
||||
)),
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
// Create a mock query result type
|
||||
interface MockQueryResult {
|
||||
isLoading: boolean;
|
||||
isRefetching: boolean;
|
||||
isError: boolean;
|
||||
error?: Error;
|
||||
data?: any;
|
||||
refetch: () => void;
|
||||
}
|
||||
|
||||
describe('StatusCodeTable', () => {
|
||||
const refetchFn = jest.fn();
|
||||
|
||||
it('renders loading state correctly', () => {
|
||||
// Arrange
|
||||
const mockQuery: MockQueryResult = {
|
||||
isLoading: true,
|
||||
isRefetching: false,
|
||||
isError: false,
|
||||
data: undefined,
|
||||
refetch: refetchFn,
|
||||
};
|
||||
|
||||
// Act
|
||||
render(<StatusCodeTable endPointStatusCodeDataQuery={mockQuery as any} />);
|
||||
|
||||
// Assert
|
||||
expect(screen.getByTestId('loading-indicator')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders error state correctly', () => {
|
||||
// Arrange
|
||||
const mockQuery: MockQueryResult = {
|
||||
isLoading: false,
|
||||
isRefetching: false,
|
||||
isError: true,
|
||||
error: new Error('Test error'),
|
||||
data: undefined,
|
||||
refetch: refetchFn,
|
||||
};
|
||||
|
||||
// Act
|
||||
render(<StatusCodeTable endPointStatusCodeDataQuery={mockQuery as any} />);
|
||||
|
||||
// Assert
|
||||
expect(screen.getByTestId('error-state-mock')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders empty state when no data is available', () => {
|
||||
// Arrange
|
||||
const mockQuery: MockQueryResult = {
|
||||
isLoading: false,
|
||||
isRefetching: false,
|
||||
isError: false,
|
||||
data: {
|
||||
payload: {
|
||||
data: {
|
||||
result: [
|
||||
{
|
||||
table: {
|
||||
rows: [],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
refetch: refetchFn,
|
||||
};
|
||||
|
||||
// Act
|
||||
render(<StatusCodeTable endPointStatusCodeDataQuery={mockQuery as any} />);
|
||||
|
||||
// Assert
|
||||
expect(screen.getByTestId('empty-table')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders table data correctly when data is available', () => {
|
||||
// Arrange
|
||||
const mockData = [
|
||||
{
|
||||
data: {
|
||||
response_status_code: '200',
|
||||
A: '150', // count
|
||||
B: '10000000', // latency in nanoseconds
|
||||
C: '5', // rate
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const mockQuery: MockQueryResult = {
|
||||
isLoading: false,
|
||||
isRefetching: false,
|
||||
isError: false,
|
||||
data: {
|
||||
payload: {
|
||||
data: {
|
||||
result: [
|
||||
{
|
||||
table: {
|
||||
rows: mockData,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
refetch: refetchFn,
|
||||
};
|
||||
|
||||
// Act
|
||||
render(<StatusCodeTable endPointStatusCodeDataQuery={mockQuery as any} />);
|
||||
|
||||
// Assert
|
||||
expect(screen.getByTestId('table-data')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -1,374 +0,0 @@
|
||||
import { fireEvent, render, screen, within } from '@testing-library/react';
|
||||
import { useNavigateToExplorer } from 'components/CeleryTask/useNavigateToExplorer';
|
||||
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||
import {
|
||||
formatTopErrorsDataForTable,
|
||||
getEndPointDetailsQueryPayload,
|
||||
getTopErrorsColumnsConfig,
|
||||
getTopErrorsCoRelationQueryFilters,
|
||||
getTopErrorsQueryPayload,
|
||||
} from 'container/ApiMonitoring/utils';
|
||||
import { useQueries } from 'react-query';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
import TopErrors from '../Explorer/Domains/DomainDetails/TopErrors';
|
||||
|
||||
// Mock the EndPointsDropDown component to avoid issues
|
||||
jest.mock(
|
||||
'../Explorer/Domains/DomainDetails/components/EndPointsDropDown',
|
||||
() => ({
|
||||
__esModule: true,
|
||||
default: jest.fn().mockImplementation(
|
||||
({ setSelectedEndPointName }): JSX.Element => (
|
||||
<div data-testid="endpoints-dropdown-mock">
|
||||
<select
|
||||
data-testid="endpoints-select"
|
||||
onChange={(e): void => setSelectedEndPointName(e.target.value)}
|
||||
role="combobox"
|
||||
>
|
||||
<option value="/api/test">/api/test</option>
|
||||
<option value="/api/new-endpoint">/api/new-endpoint</option>
|
||||
</select>
|
||||
</div>
|
||||
),
|
||||
),
|
||||
}),
|
||||
);
|
||||
|
||||
// Mock dependencies
|
||||
jest.mock('react-query', () => ({
|
||||
useQueries: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('components/CeleryTask/useNavigateToExplorer', () => ({
|
||||
useNavigateToExplorer: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('container/ApiMonitoring/utils', () => ({
|
||||
END_POINT_DETAILS_QUERY_KEYS_ARRAY: ['key1', 'key2', 'key3', 'key4', 'key5'],
|
||||
formatTopErrorsDataForTable: jest.fn(),
|
||||
getEndPointDetailsQueryPayload: jest.fn(),
|
||||
getTopErrorsColumnsConfig: jest.fn(),
|
||||
getTopErrorsCoRelationQueryFilters: jest.fn(),
|
||||
getTopErrorsQueryPayload: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('TopErrors', () => {
|
||||
const mockProps = {
|
||||
// eslint-disable-next-line sonarjs/no-duplicate-string
|
||||
domainName: 'test-domain',
|
||||
timeRange: {
|
||||
startTime: 1000000000,
|
||||
endTime: 1000010000,
|
||||
},
|
||||
initialFilters: {
|
||||
items: [],
|
||||
op: 'AND',
|
||||
},
|
||||
};
|
||||
|
||||
// Setup basic mocks
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
// Mock getTopErrorsColumnsConfig
|
||||
(getTopErrorsColumnsConfig as jest.Mock).mockReturnValue([
|
||||
{
|
||||
title: 'Endpoint',
|
||||
dataIndex: 'endpointName',
|
||||
key: 'endpointName',
|
||||
},
|
||||
{
|
||||
title: 'Status Code',
|
||||
dataIndex: 'statusCode',
|
||||
key: 'statusCode',
|
||||
},
|
||||
{
|
||||
title: 'Status Message',
|
||||
dataIndex: 'statusMessage',
|
||||
key: 'statusMessage',
|
||||
},
|
||||
{
|
||||
title: 'Count',
|
||||
dataIndex: 'count',
|
||||
key: 'count',
|
||||
},
|
||||
]);
|
||||
|
||||
// Mock useQueries
|
||||
(useQueries as jest.Mock).mockImplementation((queryConfigs) => {
|
||||
// For topErrorsDataQueries
|
||||
if (
|
||||
queryConfigs.length === 1 &&
|
||||
queryConfigs[0].queryKey &&
|
||||
queryConfigs[0].queryKey[0] === REACT_QUERY_KEY.GET_TOP_ERRORS_BY_DOMAIN
|
||||
) {
|
||||
return [
|
||||
{
|
||||
data: {
|
||||
payload: {
|
||||
data: {
|
||||
result: [
|
||||
{
|
||||
metric: {
|
||||
'http.url': '/api/test',
|
||||
status_code: '500',
|
||||
// eslint-disable-next-line sonarjs/no-duplicate-string
|
||||
status_message: 'Internal Server Error',
|
||||
},
|
||||
values: [[1000000100, '10']],
|
||||
queryName: 'A',
|
||||
legend: 'Test Legend',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
isLoading: false,
|
||||
isRefetching: false,
|
||||
isError: false,
|
||||
refetch: jest.fn(),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
// For endPointDropDownDataQueries
|
||||
return [
|
||||
{
|
||||
data: {
|
||||
payload: {
|
||||
data: {
|
||||
result: [
|
||||
{
|
||||
table: {
|
||||
rows: [
|
||||
{
|
||||
'http.url': '/api/test',
|
||||
A: 100,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
isLoading: false,
|
||||
isRefetching: false,
|
||||
isError: false,
|
||||
},
|
||||
];
|
||||
});
|
||||
|
||||
// Mock formatTopErrorsDataForTable
|
||||
(formatTopErrorsDataForTable as jest.Mock).mockReturnValue([
|
||||
{
|
||||
key: '1',
|
||||
endpointName: '/api/test',
|
||||
statusCode: '500',
|
||||
statusMessage: 'Internal Server Error',
|
||||
count: 10,
|
||||
},
|
||||
]);
|
||||
|
||||
// Mock getTopErrorsQueryPayload
|
||||
(getTopErrorsQueryPayload as jest.Mock).mockReturnValue([
|
||||
{
|
||||
queryName: 'TopErrorsQuery',
|
||||
start: mockProps.timeRange.startTime,
|
||||
end: mockProps.timeRange.endTime,
|
||||
step: 60,
|
||||
},
|
||||
]);
|
||||
|
||||
// Mock getEndPointDetailsQueryPayload
|
||||
(getEndPointDetailsQueryPayload as jest.Mock).mockReturnValue([
|
||||
{},
|
||||
{},
|
||||
{
|
||||
queryName: 'EndpointDropdownQuery',
|
||||
start: mockProps.timeRange.startTime,
|
||||
end: mockProps.timeRange.endTime,
|
||||
step: 60,
|
||||
},
|
||||
]);
|
||||
|
||||
// Mock useNavigateToExplorer
|
||||
(useNavigateToExplorer as jest.Mock).mockReturnValue(jest.fn());
|
||||
|
||||
// Mock getTopErrorsCoRelationQueryFilters
|
||||
(getTopErrorsCoRelationQueryFilters as jest.Mock).mockReturnValue({
|
||||
items: [
|
||||
{ id: 'test1', key: { key: 'domain' }, op: '=', value: 'test-domain' },
|
||||
{ id: 'test2', key: { key: 'endpoint' }, op: '=', value: '/api/test' },
|
||||
{ id: 'test3', key: { key: 'status' }, op: '=', value: '500' },
|
||||
],
|
||||
op: 'AND',
|
||||
});
|
||||
});
|
||||
|
||||
it('renders component correctly', () => {
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
const { container } = render(<TopErrors {...mockProps} />);
|
||||
|
||||
// Check if the title and toggle are rendered
|
||||
expect(screen.getByText('Errors with Status Message')).toBeInTheDocument();
|
||||
expect(screen.getByText('Status Message Exists')).toBeInTheDocument();
|
||||
|
||||
// Find the table row and verify content
|
||||
const tableBody = container.querySelector('.ant-table-tbody');
|
||||
expect(tableBody).not.toBeNull();
|
||||
|
||||
if (tableBody) {
|
||||
const row = within(tableBody as HTMLElement).getByRole('row');
|
||||
expect(within(row).getByText('/api/test')).toBeInTheDocument();
|
||||
expect(within(row).getByText('500')).toBeInTheDocument();
|
||||
expect(within(row).getByText('Internal Server Error')).toBeInTheDocument();
|
||||
}
|
||||
});
|
||||
|
||||
it('renders error state when isError is true', () => {
|
||||
// Mock useQueries to return isError: true
|
||||
(useQueries as jest.Mock).mockImplementationOnce(() => [
|
||||
{
|
||||
isError: true,
|
||||
refetch: jest.fn(),
|
||||
},
|
||||
]);
|
||||
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
render(<TopErrors {...mockProps} />);
|
||||
|
||||
// Error state should be shown with the actual text displayed in the UI
|
||||
expect(
|
||||
screen.getByText('Uh-oh :/ We ran into an error.'),
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getByText('Please refresh this panel.')).toBeInTheDocument();
|
||||
expect(screen.getByText('Refresh this panel')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('handles row click correctly', () => {
|
||||
const navigateMock = jest.fn();
|
||||
(useNavigateToExplorer as jest.Mock).mockReturnValue(navigateMock);
|
||||
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
const { container } = render(<TopErrors {...mockProps} />);
|
||||
|
||||
// Find and click on the table cell containing the endpoint
|
||||
const tableBody = container.querySelector('.ant-table-tbody');
|
||||
expect(tableBody).not.toBeNull();
|
||||
|
||||
if (tableBody) {
|
||||
const row = within(tableBody as HTMLElement).getByRole('row');
|
||||
const cellWithEndpoint = within(row).getByText('/api/test');
|
||||
fireEvent.click(cellWithEndpoint);
|
||||
}
|
||||
|
||||
// Check if navigateToExplorer was called with correct params
|
||||
expect(navigateMock).toHaveBeenCalledWith({
|
||||
filters: [
|
||||
{ id: 'test1', key: { key: 'domain' }, op: '=', value: 'test-domain' },
|
||||
{ id: 'test2', key: { key: 'endpoint' }, op: '=', value: '/api/test' },
|
||||
{ id: 'test3', key: { key: 'status' }, op: '=', value: '500' },
|
||||
],
|
||||
dataSource: DataSource.TRACES,
|
||||
startTime: mockProps.timeRange.startTime,
|
||||
endTime: mockProps.timeRange.endTime,
|
||||
shouldResolveQuery: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('updates endpoint filter when dropdown value changes', () => {
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
render(<TopErrors {...mockProps} />);
|
||||
|
||||
// Find the dropdown
|
||||
const dropdown = screen.getByRole('combobox');
|
||||
|
||||
// Mock the change
|
||||
fireEvent.change(dropdown, { target: { value: '/api/new-endpoint' } });
|
||||
|
||||
// Check if getTopErrorsQueryPayload was called with updated parameters
|
||||
expect(getTopErrorsQueryPayload).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('handles status message toggle correctly', () => {
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
render(<TopErrors {...mockProps} />);
|
||||
|
||||
// Find the toggle switch
|
||||
const toggle = screen.getByRole('switch');
|
||||
expect(toggle).toBeInTheDocument();
|
||||
|
||||
// Toggle should be on by default
|
||||
expect(toggle).toHaveAttribute('aria-checked', 'true');
|
||||
|
||||
// Click the toggle to turn it off
|
||||
fireEvent.click(toggle);
|
||||
|
||||
// Check if getTopErrorsQueryPayload was called with showStatusCodeErrors=false
|
||||
expect(getTopErrorsQueryPayload).toHaveBeenCalledWith(
|
||||
mockProps.domainName,
|
||||
mockProps.timeRange.startTime,
|
||||
mockProps.timeRange.endTime,
|
||||
expect.any(Object),
|
||||
false,
|
||||
);
|
||||
|
||||
// Title should change
|
||||
expect(screen.getByText('All Errors')).toBeInTheDocument();
|
||||
|
||||
// Click the toggle to turn it back on
|
||||
fireEvent.click(toggle);
|
||||
|
||||
// Check if getTopErrorsQueryPayload was called with showStatusCodeErrors=true
|
||||
expect(getTopErrorsQueryPayload).toHaveBeenCalledWith(
|
||||
mockProps.domainName,
|
||||
mockProps.timeRange.startTime,
|
||||
mockProps.timeRange.endTime,
|
||||
expect.any(Object),
|
||||
true,
|
||||
);
|
||||
|
||||
// Title should change back
|
||||
expect(screen.getByText('Errors with Status Message')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('includes toggle state in query key for cache busting', () => {
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
render(<TopErrors {...mockProps} />);
|
||||
|
||||
const toggle = screen.getByRole('switch');
|
||||
|
||||
// Initial query should include showStatusCodeErrors=true
|
||||
expect(useQueries).toHaveBeenCalledWith(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
queryKey: expect.arrayContaining([
|
||||
REACT_QUERY_KEY.GET_TOP_ERRORS_BY_DOMAIN,
|
||||
expect.any(Object),
|
||||
expect.any(String),
|
||||
true,
|
||||
]),
|
||||
}),
|
||||
]),
|
||||
);
|
||||
|
||||
// Click toggle
|
||||
fireEvent.click(toggle);
|
||||
|
||||
// Query should be called with showStatusCodeErrors=false in key
|
||||
expect(useQueries).toHaveBeenCalledWith(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
queryKey: expect.arrayContaining([
|
||||
REACT_QUERY_KEY.GET_TOP_ERRORS_BY_DOMAIN,
|
||||
expect.any(Object),
|
||||
expect.any(String),
|
||||
false,
|
||||
]),
|
||||
}),
|
||||
]),
|
||||
);
|
||||
});
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
@@ -56,7 +56,6 @@ function WidgetGraphComponent({
|
||||
onOpenTraceBtnClick,
|
||||
customSeries,
|
||||
customErrorMessage,
|
||||
customOnRowClick,
|
||||
}: WidgetGraphComponentProps): JSX.Element {
|
||||
const { safeNavigate } = useSafeNavigate();
|
||||
const [deleteModal, setDeleteModal] = useState(false);
|
||||
@@ -381,7 +380,6 @@ function WidgetGraphComponent({
|
||||
openTracesButton={openTracesButton}
|
||||
onOpenTraceBtnClick={onOpenTraceBtnClick}
|
||||
customSeries={customSeries}
|
||||
customOnRowClick={customOnRowClick}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -47,8 +47,6 @@ function GridCardGraph({
|
||||
start,
|
||||
end,
|
||||
analyticsEvent,
|
||||
customTimeRange,
|
||||
customOnRowClick,
|
||||
}: GridCardGraphProps): JSX.Element {
|
||||
const dispatch = useDispatch();
|
||||
const [errorMessage, setErrorMessage] = useState<string>();
|
||||
@@ -132,8 +130,6 @@ function GridCardGraph({
|
||||
variables: getDashboardVariables(variables),
|
||||
fillGaps: widget.fillSpans,
|
||||
formatForWeb: widget.panelTypes === PANEL_TYPES.TABLE,
|
||||
start: customTimeRange?.startTime || start,
|
||||
end: customTimeRange?.endTime || end,
|
||||
};
|
||||
}
|
||||
updatedQuery.builder.queryData[0].pageSize = 10;
|
||||
@@ -153,8 +149,6 @@ function GridCardGraph({
|
||||
initialDataSource === DataSource.TRACES && widget.selectedTracesFields,
|
||||
},
|
||||
fillGaps: widget.fillSpans,
|
||||
start: customTimeRange?.startTime || start,
|
||||
end: customTimeRange?.endTime || end,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -193,8 +187,8 @@ function GridCardGraph({
|
||||
variables: getDashboardVariables(variables),
|
||||
selectedTime: widget.timePreferance || 'GLOBAL_TIME',
|
||||
globalSelectedInterval,
|
||||
start: customTimeRange?.startTime || start,
|
||||
end: customTimeRange?.endTime || end,
|
||||
start,
|
||||
end,
|
||||
},
|
||||
version || DEFAULT_ENTITY_VERSION,
|
||||
{
|
||||
@@ -208,9 +202,6 @@ function GridCardGraph({
|
||||
widget.timePreferance,
|
||||
widget.fillSpans,
|
||||
requestData,
|
||||
...(customTimeRange && customTimeRange.startTime && customTimeRange.endTime
|
||||
? [customTimeRange.startTime, customTimeRange.endTime]
|
||||
: []),
|
||||
],
|
||||
retry(failureCount, error): boolean {
|
||||
if (
|
||||
@@ -288,7 +279,6 @@ function GridCardGraph({
|
||||
onOpenTraceBtnClick={onOpenTraceBtnClick}
|
||||
customSeries={customSeries}
|
||||
customErrorMessage={isInternalServerError ? customErrorMessage : undefined}
|
||||
customOnRowClick={customOnRowClick}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -39,7 +39,6 @@ export interface WidgetGraphComponentProps {
|
||||
onOpenTraceBtnClick?: (record: RowData) => void;
|
||||
customSeries?: (data: QueryData[]) => uPlot.Series[];
|
||||
customErrorMessage?: string;
|
||||
customOnRowClick?: (record: RowData) => void;
|
||||
}
|
||||
|
||||
export interface GridCardGraphProps {
|
||||
@@ -62,11 +61,6 @@ export interface GridCardGraphProps {
|
||||
start?: number;
|
||||
end?: number;
|
||||
analyticsEvent?: string;
|
||||
customTimeRange?: {
|
||||
startTime: number;
|
||||
endTime: number;
|
||||
};
|
||||
customOnRowClick?: (record: RowData) => void;
|
||||
}
|
||||
|
||||
export interface GetGraphVisibilityStateOnLegendClickProps {
|
||||
|
||||
@@ -43,7 +43,6 @@ function GridTableComponent({
|
||||
sticky,
|
||||
openTracesButton,
|
||||
onOpenTraceBtnClick,
|
||||
customOnRowClick,
|
||||
widgetId,
|
||||
...props
|
||||
}: GridTableComponentProps): JSX.Element {
|
||||
@@ -215,30 +214,6 @@ function GridTableComponent({
|
||||
[newColumnData],
|
||||
);
|
||||
|
||||
const newColumnsWithRenderColumnCell = useMemo(
|
||||
() =>
|
||||
newColumnData.map((column) => ({
|
||||
...column,
|
||||
...('dataIndex' in column &&
|
||||
props.renderColumnCell?.[column.dataIndex as string]
|
||||
? { render: props.renderColumnCell[column.dataIndex as string] }
|
||||
: {}),
|
||||
})),
|
||||
[newColumnData, props.renderColumnCell],
|
||||
);
|
||||
|
||||
const newColumnsWithCustomColTitles = useMemo(
|
||||
() =>
|
||||
newColumnsWithRenderColumnCell.map((column) => ({
|
||||
...column,
|
||||
...('dataIndex' in column &&
|
||||
props.customColTitles?.[column.dataIndex as string]
|
||||
? { title: props.customColTitles[column.dataIndex as string] }
|
||||
: {}),
|
||||
})),
|
||||
[newColumnsWithRenderColumnCell, props.customColTitles],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
eventEmitter.emit(Events.TABLE_COLUMNS_DATA, {
|
||||
columns: newColumnData,
|
||||
@@ -252,22 +227,15 @@ function GridTableComponent({
|
||||
query={query}
|
||||
queryTableData={data}
|
||||
loading={false}
|
||||
columns={
|
||||
openTracesButton
|
||||
? columnDataWithOpenTracesButton
|
||||
: newColumnsWithCustomColTitles
|
||||
}
|
||||
columns={openTracesButton ? columnDataWithOpenTracesButton : newColumnData}
|
||||
dataSource={dataSource}
|
||||
sticky={sticky}
|
||||
widgetId={widgetId}
|
||||
onRow={
|
||||
openTracesButton || customOnRowClick
|
||||
openTracesButton
|
||||
? (record): React.HTMLAttributes<HTMLElement> => ({
|
||||
onClick: (): void => {
|
||||
if (openTracesButton) {
|
||||
onOpenTraceBtnClick?.(record);
|
||||
}
|
||||
customOnRowClick?.(record);
|
||||
onOpenTraceBtnClick?.(record);
|
||||
},
|
||||
})
|
||||
: undefined
|
||||
|
||||
@@ -4,7 +4,6 @@ import {
|
||||
ThresholdOperators,
|
||||
ThresholdProps,
|
||||
} from 'container/NewWidget/RightContainer/Threshold/types';
|
||||
import { QueryTableProps } from 'container/QueryTable/QueryTable.intefaces';
|
||||
import { RowData } from 'lib/query/createTableColumnsFromQuery';
|
||||
import { ColumnUnit } from 'types/api/dashboard/getAll';
|
||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
@@ -18,10 +17,7 @@ export type GridTableComponentProps = {
|
||||
searchTerm?: string;
|
||||
openTracesButton?: boolean;
|
||||
onOpenTraceBtnClick?: (record: RowData) => void;
|
||||
customOnRowClick?: (record: RowData) => void;
|
||||
widgetId?: string;
|
||||
renderColumnCell?: QueryTableProps['renderColumnCell'];
|
||||
customColTitles?: Record<string, string>;
|
||||
} & Pick<LogsExplorerTableProps, 'data'> &
|
||||
Omit<TableProps<RowData>, 'columns' | 'dataSource'>;
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import './Home.styles.scss';
|
||||
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { Button, Popover } from 'antd';
|
||||
import { Alert, Button, Popover } from 'antd';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { HostListPayload } from 'api/infraMonitoring/getHostLists';
|
||||
import { K8sPodsListPayload } from 'api/infraMonitoring/getK8sPodsList';
|
||||
@@ -644,6 +644,16 @@ export default function Home(): JSX.Element {
|
||||
</div>
|
||||
|
||||
<div className="home-right-content">
|
||||
<div className="home-notifications-container">
|
||||
<div className="notification">
|
||||
<Alert
|
||||
message="We're transitioning alert rule IDs from integers to UUIDs on April 23, 2025. Both old and new alert links will continue to work after this change - existing notifications using integer IDs will remain functional while new alerts will use the UUID format."
|
||||
type="info"
|
||||
showIcon
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{!isWelcomeChecklistSkipped && !loadingUserPreferences && (
|
||||
<AnimatePresence initial={false}>
|
||||
<Card className="checklist-card">
|
||||
|
||||
@@ -133,231 +133,3 @@ const ServicesListTable = memo(
|
||||
),
|
||||
);
|
||||
ServicesListTable.displayName = 'ServicesListTable';
|
||||
|
||||
function ServiceMetrics({
|
||||
onUpdateChecklistDoneItem,
|
||||
loadingUserPreferences,
|
||||
}: {
|
||||
onUpdateChecklistDoneItem: (itemKey: string) => void;
|
||||
loadingUserPreferences: boolean;
|
||||
}): JSX.Element {
|
||||
const { selectedTime: globalSelectedInterval } = useSelector<
|
||||
AppState,
|
||||
GlobalReducer
|
||||
>((state) => state.globalTime);
|
||||
|
||||
const { user, activeLicenseV3 } = useAppContext();
|
||||
|
||||
const [timeRange, setTimeRange] = useState(() => {
|
||||
const now = new Date().getTime();
|
||||
return {
|
||||
startTime: now - homeInterval,
|
||||
endTime: now,
|
||||
selectedInterval: homeInterval,
|
||||
};
|
||||
});
|
||||
|
||||
const { queries } = useResourceAttribute();
|
||||
const { safeNavigate } = useSafeNavigate();
|
||||
|
||||
const selectedTags = useMemo(
|
||||
() => (convertRawQueriesToTraceSelectedTags(queries) as Tags[]) || [],
|
||||
[queries],
|
||||
);
|
||||
|
||||
const [isError, setIsError] = useState(false);
|
||||
|
||||
const queryKey: QueryKey = useMemo(
|
||||
() => [
|
||||
timeRange.startTime,
|
||||
timeRange.endTime,
|
||||
selectedTags,
|
||||
globalSelectedInterval,
|
||||
],
|
||||
[
|
||||
timeRange.startTime,
|
||||
timeRange.endTime,
|
||||
selectedTags,
|
||||
globalSelectedInterval,
|
||||
],
|
||||
);
|
||||
|
||||
const {
|
||||
data,
|
||||
isLoading: isLoadingTopLevelOperations,
|
||||
isError: isErrorTopLevelOperations,
|
||||
} = useGetTopLevelOperations(queryKey, {
|
||||
start: timeRange.startTime * 1e6,
|
||||
end: timeRange.endTime * 1e6,
|
||||
});
|
||||
|
||||
const handleTimeIntervalChange = useCallback((value: number): void => {
|
||||
const timeInterval = TIME_PICKER_OPTIONS.find(
|
||||
(option) => option.value === value,
|
||||
);
|
||||
|
||||
logEvent('Homepage: Services time interval updated', {
|
||||
updatedTimeInterval: timeInterval?.label,
|
||||
});
|
||||
|
||||
const now = new Date();
|
||||
setTimeRange({
|
||||
startTime: now.getTime() - value,
|
||||
endTime: now.getTime(),
|
||||
selectedInterval: value,
|
||||
});
|
||||
}, []);
|
||||
|
||||
const topLevelOperations = useMemo(() => Object.entries(data || {}), [data]);
|
||||
|
||||
const queryRangeRequestData = useMemo(
|
||||
() =>
|
||||
getQueryRangeRequestData({
|
||||
topLevelOperations,
|
||||
minTime: timeRange.startTime * 1e6,
|
||||
maxTime: timeRange.endTime * 1e6,
|
||||
globalSelectedInterval,
|
||||
}),
|
||||
[
|
||||
globalSelectedInterval,
|
||||
timeRange.endTime,
|
||||
timeRange.startTime,
|
||||
topLevelOperations,
|
||||
],
|
||||
);
|
||||
|
||||
const dataQueries = useGetQueriesRange(
|
||||
queryRangeRequestData,
|
||||
ENTITY_VERSION_V4,
|
||||
{
|
||||
queryKey: useMemo(
|
||||
() => [
|
||||
`GetMetricsQueryRange-home-${globalSelectedInterval}`,
|
||||
timeRange.endTime,
|
||||
timeRange.startTime,
|
||||
globalSelectedInterval,
|
||||
],
|
||||
[globalSelectedInterval, timeRange.endTime, timeRange.startTime],
|
||||
),
|
||||
keepPreviousData: true,
|
||||
enabled: true,
|
||||
refetchOnMount: false,
|
||||
onError: () => {
|
||||
setIsError(true);
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const isLoading = useMemo(() => dataQueries.some((query) => query.isLoading), [
|
||||
dataQueries,
|
||||
]);
|
||||
|
||||
const services: ServicesList[] = useMemo(
|
||||
() =>
|
||||
getServiceListFromQuery({
|
||||
queries: dataQueries,
|
||||
topLevelOperations,
|
||||
isLoading,
|
||||
}),
|
||||
[dataQueries, topLevelOperations, isLoading],
|
||||
);
|
||||
|
||||
const sortedServices = useMemo(
|
||||
() =>
|
||||
services?.sort((a, b) => {
|
||||
const aUpdateAt = new Date(a.p99).getTime();
|
||||
const bUpdateAt = new Date(b.p99).getTime();
|
||||
return bUpdateAt - aUpdateAt;
|
||||
}) || [],
|
||||
[services],
|
||||
);
|
||||
|
||||
const servicesExist = sortedServices.length > 0;
|
||||
const top5Services = useMemo(() => sortedServices.slice(0, 5), [
|
||||
sortedServices,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!loadingUserPreferences && servicesExist) {
|
||||
onUpdateChecklistDoneItem('SETUP_SERVICES');
|
||||
}
|
||||
}, [onUpdateChecklistDoneItem, loadingUserPreferences, servicesExist]);
|
||||
|
||||
const handleRowClick = useCallback(
|
||||
(record: ServicesList) => {
|
||||
logEvent('Homepage: Service clicked', {
|
||||
serviceName: record.serviceName,
|
||||
});
|
||||
safeNavigate(`${ROUTES.APPLICATION}/${record.serviceName}`);
|
||||
},
|
||||
[safeNavigate],
|
||||
);
|
||||
|
||||
if (isLoadingTopLevelOperations || isLoading) {
|
||||
return (
|
||||
<Card className="services-list-card home-data-card loading-card">
|
||||
<Card.Content>
|
||||
<Skeleton active />
|
||||
</Card.Content>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
if (isErrorTopLevelOperations || isError) {
|
||||
return (
|
||||
<Card className="services-list-card home-data-card error-card">
|
||||
<Card.Content>
|
||||
<Skeleton active />
|
||||
</Card.Content>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Card className="services-list-card home-data-card">
|
||||
{servicesExist && (
|
||||
<Card.Header>
|
||||
<div className="services-header home-data-card-header">
|
||||
{' '}
|
||||
Services
|
||||
<div className="services-header-actions">
|
||||
<Select
|
||||
value={timeRange.selectedInterval}
|
||||
onChange={handleTimeIntervalChange}
|
||||
options={TIME_PICKER_OPTIONS}
|
||||
className="services-header-select"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Card.Header>
|
||||
)}
|
||||
<Card.Content>
|
||||
{servicesExist ? (
|
||||
<ServicesListTable services={top5Services} onRowClick={handleRowClick} />
|
||||
) : (
|
||||
<EmptyState user={user} activeLicenseV3={activeLicenseV3} />
|
||||
)}
|
||||
</Card.Content>
|
||||
|
||||
{servicesExist && (
|
||||
<Card.Footer>
|
||||
<div className="services-footer home-data-card-footer">
|
||||
<Link to="/services">
|
||||
<Button
|
||||
type="link"
|
||||
className="periscope-btn link learn-more-link"
|
||||
onClick={(): void => {
|
||||
logEvent('Homepage: All Services clicked', {});
|
||||
}}
|
||||
>
|
||||
All Services <ArrowRight size={12} />
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</Card.Footer>
|
||||
)}
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
export default memo(ServiceMetrics);
|
||||
|
||||
@@ -21,17 +21,10 @@ function Services({
|
||||
return (
|
||||
<Sentry.ErrorBoundary fallback={<ErrorBoundaryFallback />}>
|
||||
<div className="home-services-container">
|
||||
{isSpanMetricEnabled ? (
|
||||
<ServiceMetrics
|
||||
<ServiceTraces
|
||||
onUpdateChecklistDoneItem={onUpdateChecklistDoneItem}
|
||||
loadingUserPreferences={loadingUserPreferences}
|
||||
/>
|
||||
) : (
|
||||
<ServiceTraces
|
||||
onUpdateChecklistDoneItem={onUpdateChecklistDoneItem}
|
||||
loadingUserPreferences={loadingUserPreferences}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</Sentry.ErrorBoundary>
|
||||
);
|
||||
|
||||
@@ -481,7 +481,6 @@ export const apDexMetricsQueryBuilderQueries = ({
|
||||
export const operationPerSec = ({
|
||||
servicename,
|
||||
tagFilterItems,
|
||||
topLevelOperations,
|
||||
}: OperationPerSecProps): QueryBuilderData => {
|
||||
const autocompleteData: BaseAutocompleteData[] = [
|
||||
{
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import getTopLevelOperations, {
|
||||
ServiceDataProps,
|
||||
} from 'api/metrics/getTopLevelOperations';
|
||||
import { FeatureKeys } from 'constants/features';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
@@ -110,21 +107,6 @@ function Application(): JSX.Element {
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
const {
|
||||
data: topLevelOperations,
|
||||
error: topLevelOperationsError,
|
||||
isLoading: topLevelOperationsIsLoading,
|
||||
isError: topLevelOperationsIsError,
|
||||
} = useQuery<ServiceDataProps>({
|
||||
queryKey: [servicename, minTime, maxTime],
|
||||
queryFn: (): Promise<ServiceDataProps> =>
|
||||
getTopLevelOperations({
|
||||
service: servicename || '',
|
||||
start: minTime,
|
||||
end: maxTime,
|
||||
}),
|
||||
});
|
||||
|
||||
const selectedTraceTags: string = JSON.stringify(
|
||||
convertRawQueriesToTraceSelectedTags(queries) || [],
|
||||
);
|
||||
@@ -137,14 +119,6 @@ function Application(): JSX.Element {
|
||||
[queries],
|
||||
);
|
||||
|
||||
const topLevelOperationsRoute = useMemo(
|
||||
() =>
|
||||
topLevelOperations
|
||||
? defaultTo(topLevelOperations[servicename || ''], [])
|
||||
: [],
|
||||
[servicename, topLevelOperations],
|
||||
);
|
||||
|
||||
const operationPerSecWidget = useMemo(
|
||||
() =>
|
||||
getWidgetQueryBuilder({
|
||||
|
||||
@@ -451,9 +451,6 @@ function OnboardingAddDataSource(): JSX.Element {
|
||||
case 'integrations':
|
||||
history.push(ROUTES.INTEGRATIONS);
|
||||
break;
|
||||
case 'home':
|
||||
history.push(ROUTES.HOME);
|
||||
break;
|
||||
default:
|
||||
history.push(ROUTES.APPLICATION);
|
||||
}
|
||||
|
||||
@@ -12,34 +12,6 @@
|
||||
"imgUrl": "/Logos/quickstart.svg",
|
||||
"link": "https://signoz.io/docs/cloud/quickstart/"
|
||||
},
|
||||
{
|
||||
"dataSource": "migrate-from-datadog",
|
||||
"label": "From Datadog",
|
||||
"tags": ["migrate to SigNoz"],
|
||||
"module": "home",
|
||||
"relatedSearchKeywords": [
|
||||
"datadog",
|
||||
"opentelemetry"
|
||||
],
|
||||
"imgUrl": "/Logos/datadog.svg",
|
||||
"link": "https://signoz.io/docs/migration/migrate-from-datadog/"
|
||||
},
|
||||
{
|
||||
"dataSource": "migrate-from-lgtm",
|
||||
"label": "From Grafana",
|
||||
"tags": ["migrate to SigNoz"],
|
||||
"module": "home",
|
||||
"relatedSearchKeywords": [
|
||||
"grafana",
|
||||
"loki",
|
||||
"mirmir",
|
||||
"tempo",
|
||||
"alloy",
|
||||
"opentelemetry"
|
||||
],
|
||||
"imgUrl": "/Logos/grafana.svg",
|
||||
"link": "https://signoz.io/docs/migration/migrate-from-grafana/"
|
||||
},
|
||||
{
|
||||
"dataSource": "java",
|
||||
"entityID": "dataSource",
|
||||
@@ -1137,21 +1109,6 @@
|
||||
"id": "opentelemetry-cpp",
|
||||
"link": "https://signoz.io/docs/instrumentation/opentelemetry-cpp/"
|
||||
},
|
||||
{
|
||||
"dataSource": "nginx-tracing",
|
||||
"label": "Nginx - Tracing",
|
||||
"imgUrl": "/Logos/nginx.svg",
|
||||
"tags": ["apm"],
|
||||
"module": "apm",
|
||||
"relatedSearchKeywords": [
|
||||
"tracing",
|
||||
"nginx server",
|
||||
"nginx proxy",
|
||||
"nginx"
|
||||
],
|
||||
"id": "nginx-tracing",
|
||||
"link": "https://signoz.io/docs/instrumentation/opentelemetry-nginx/"
|
||||
},
|
||||
{
|
||||
"dataSource": "kubernetes-pod-logs",
|
||||
"label": "Kubernetes Pod Logs",
|
||||
@@ -2917,43 +2874,5 @@
|
||||
"imgUrl": "/Logos/rds.svg",
|
||||
"link": "/integrations?integration=aws-integration&service=rds",
|
||||
"internalRedirect": true
|
||||
},
|
||||
{
|
||||
"dataSource": "temporal",
|
||||
"label": "Temporal",
|
||||
"imgUrl": "/Logos/temporal.svg",
|
||||
"tags": ["integrations"],
|
||||
"module": "apm",
|
||||
"relatedSearchKeywords": [
|
||||
"temporal metrics",
|
||||
"temporal traces",
|
||||
"temporal logs",
|
||||
"temporal cloud",
|
||||
"temporal"
|
||||
],
|
||||
"question": {
|
||||
"desc": "What are you using ?",
|
||||
"type": "select",
|
||||
"options": [
|
||||
{
|
||||
"key": "temporal-cloud",
|
||||
"label": "Cloud Metrics",
|
||||
"imgUrl": "/Logos/temporal.svg",
|
||||
"link": "https://signoz.io/docs/integrations/temporal-cloud-metrics/"
|
||||
},
|
||||
{
|
||||
"key": "temporal-golang",
|
||||
"label": "Go",
|
||||
"imgUrl": "/Logos/go.svg",
|
||||
"link": "https://signoz.io/docs/integrations/temporal-golang-opentelemetry/"
|
||||
},
|
||||
{
|
||||
"key": "temporal-typescript",
|
||||
"label": "TypeScript",
|
||||
"imgUrl": "/Logos/javascript.svg",
|
||||
"link": "https://signoz.io/docs/integrations/temporal-typescript-opentelemetry/"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -20,7 +20,6 @@ function PanelWrapper({
|
||||
openTracesButton,
|
||||
onOpenTraceBtnClick,
|
||||
customSeries,
|
||||
customOnRowClick,
|
||||
}: PanelWrapperProps): JSX.Element {
|
||||
const Component = PanelTypeVsPanelWrapper[
|
||||
selectedGraph || widget.panelTypes
|
||||
@@ -47,7 +46,6 @@ function PanelWrapper({
|
||||
searchTerm={searchTerm}
|
||||
openTracesButton={openTracesButton}
|
||||
onOpenTraceBtnClick={onOpenTraceBtnClick}
|
||||
customOnRowClick={customOnRowClick}
|
||||
customSeries={customSeries}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -11,7 +11,6 @@ function TablePanelWrapper({
|
||||
searchTerm,
|
||||
openTracesButton,
|
||||
onOpenTraceBtnClick,
|
||||
customOnRowClick,
|
||||
}: PanelWrapperProps): JSX.Element {
|
||||
const panelData =
|
||||
(queryResponse.data?.payload?.data?.result?.[0] as any)?.table || [];
|
||||
@@ -27,10 +26,7 @@ function TablePanelWrapper({
|
||||
searchTerm={searchTerm}
|
||||
openTracesButton={openTracesButton}
|
||||
onOpenTraceBtnClick={onOpenTraceBtnClick}
|
||||
customOnRowClick={customOnRowClick}
|
||||
widgetId={widget.id}
|
||||
renderColumnCell={widget.renderColumnCell}
|
||||
customColTitles={widget.customColTitles}
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
{...GRID_TABLE_CONFIG}
|
||||
/>
|
||||
|
||||
@@ -28,7 +28,6 @@ export type PanelWrapperProps = {
|
||||
customTooltipElement?: HTMLDivElement;
|
||||
openTracesButton?: boolean;
|
||||
onOpenTraceBtnClick?: (record: RowData) => void;
|
||||
customOnRowClick?: (record: RowData) => void;
|
||||
customSeries?: (data: QueryData[]) => uPlot.Series[];
|
||||
};
|
||||
|
||||
|
||||
@@ -110,16 +110,9 @@
|
||||
}
|
||||
.nav-wrapper {
|
||||
height: calc(100% - 52px);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
|
||||
.primary-nav-items {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
max-height: 100%;
|
||||
max-height: 65%;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
|
||||
@@ -128,14 +121,15 @@
|
||||
}
|
||||
}
|
||||
.secondary-nav-items {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-shrink: 0;
|
||||
max-height: 35%;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
border-top: 1px solid var(--bg-slate-400);
|
||||
padding: 8px 0;
|
||||
max-width: 100%;
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 64px;
|
||||
|
||||
transition: all 0.2s, background 0s, border 0s;
|
||||
|
||||
@@ -24,11 +24,6 @@ export default function Toolbar({
|
||||
const isLogsExplorerPage = useMemo(() => pathname === ROUTES.LOGS_EXPLORER, [
|
||||
pathname,
|
||||
]);
|
||||
|
||||
const isApiMonitoringPage = useMemo(() => pathname === ROUTES.API_MONITORING, [
|
||||
pathname,
|
||||
]);
|
||||
|
||||
return (
|
||||
<div className="toolbar">
|
||||
<div className="leftActions">{leftActions}</div>
|
||||
@@ -36,7 +31,7 @@ export default function Toolbar({
|
||||
{showOldCTA && <NewExplorerCTA />}
|
||||
<DateTimeSelectionV2
|
||||
showAutoRefresh={showAutoRefresh}
|
||||
showRefreshText={!isLogsExplorerPage && !isApiMonitoringPage}
|
||||
showRefreshText={!isLogsExplorerPage}
|
||||
/>
|
||||
</div>
|
||||
<div className="rightActions">{rightActions}</div>
|
||||
|
||||
@@ -75,8 +75,6 @@ function DateTimeSelection({
|
||||
isModalTimeSelection = false,
|
||||
onTimeChange,
|
||||
modalSelectedInterval,
|
||||
modalInitialStartTime,
|
||||
modalInitialEndTime,
|
||||
}: Props): JSX.Element {
|
||||
const [formSelector] = Form.useForm();
|
||||
const { safeNavigate } = useSafeNavigate();
|
||||
@@ -96,36 +94,6 @@ function DateTimeSelection({
|
||||
const [, handleCopyToClipboard] = useCopyToClipboard();
|
||||
const [isURLCopied, setIsURLCopied] = useState(false);
|
||||
|
||||
// Prioritize props for initial modal time, fallback to URL params
|
||||
let initialModalStartTime = 0;
|
||||
if (modalInitialStartTime !== undefined) {
|
||||
initialModalStartTime = modalInitialStartTime;
|
||||
} else if (searchStartTime) {
|
||||
initialModalStartTime = parseInt(searchStartTime, 10);
|
||||
}
|
||||
|
||||
let initialModalEndTime = 0;
|
||||
if (modalInitialEndTime !== undefined) {
|
||||
initialModalEndTime = modalInitialEndTime;
|
||||
} else if (searchEndTime) {
|
||||
initialModalEndTime = parseInt(searchEndTime, 10);
|
||||
}
|
||||
|
||||
const [modalStartTime, setModalStartTime] = useState<number>(
|
||||
initialModalStartTime,
|
||||
);
|
||||
const [modalEndTime, setModalEndTime] = useState<number>(initialModalEndTime);
|
||||
|
||||
// Effect to update modal time state when props change
|
||||
useEffect(() => {
|
||||
if (modalInitialStartTime !== undefined) {
|
||||
setModalStartTime(modalInitialStartTime);
|
||||
}
|
||||
if (modalInitialEndTime !== undefined) {
|
||||
setModalEndTime(modalInitialEndTime);
|
||||
}
|
||||
}, [modalInitialStartTime, modalInitialEndTime]);
|
||||
|
||||
const {
|
||||
localstorageStartTime,
|
||||
localstorageEndTime,
|
||||
@@ -244,6 +212,7 @@ function DateTimeSelection({
|
||||
|
||||
const startString = startTime.format(format);
|
||||
const endString = endTime.format(format);
|
||||
|
||||
return `${startString} - ${endString}`;
|
||||
}
|
||||
return timeInterval;
|
||||
@@ -414,6 +383,13 @@ function DateTimeSelection({
|
||||
}
|
||||
}, [defaultRelativeTime, onSelectHandler]);
|
||||
|
||||
const [modalStartTime, setModalStartTime] = useState<number>(
|
||||
searchStartTime ? parseInt(searchStartTime, 10) : 0,
|
||||
);
|
||||
const [modalEndTime, setModalEndTime] = useState<number>(
|
||||
searchEndTime ? parseInt(searchEndTime, 10) : 0,
|
||||
);
|
||||
|
||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||
const onCustomDateHandler = (dateTimeRange: DateTimeRangeType): void => {
|
||||
if (dateTimeRange !== null) {
|
||||
@@ -888,8 +864,6 @@ interface DateTimeSelectionV2Props {
|
||||
dateTimeRange?: [number, number],
|
||||
) => void;
|
||||
modalSelectedInterval?: Time;
|
||||
modalInitialStartTime?: number;
|
||||
modalInitialEndTime?: number;
|
||||
}
|
||||
|
||||
DateTimeSelection.defaultProps = {
|
||||
@@ -901,8 +875,6 @@ DateTimeSelection.defaultProps = {
|
||||
isModalTimeSelection: false,
|
||||
onTimeChange: (): void => {},
|
||||
modalSelectedInterval: RelativeTimeMap['5m'] as Time,
|
||||
modalInitialStartTime: undefined,
|
||||
modalInitialEndTime: undefined,
|
||||
};
|
||||
interface DispatchProps {
|
||||
updateTimeInterval: (
|
||||
|
||||
@@ -59,7 +59,6 @@ export interface GetUPlotChartOptions {
|
||||
timezone?: string;
|
||||
customSeries?: (data: QueryData[]) => uPlot.Series[];
|
||||
isLogScale?: boolean;
|
||||
colorMapping?: Record<string, string>;
|
||||
}
|
||||
|
||||
/** the function converts series A , series B , series C to
|
||||
@@ -167,7 +166,6 @@ export const getUPlotChartOptions = ({
|
||||
timezone,
|
||||
customSeries,
|
||||
isLogScale,
|
||||
colorMapping,
|
||||
}: GetUPlotChartOptions): uPlot.Options => {
|
||||
const timeScaleProps = getXAxisScale(minTimeScale, maxTimeScale);
|
||||
|
||||
@@ -231,11 +229,10 @@ export const getUPlotChartOptions = ({
|
||||
tooltipPlugin({
|
||||
apiResponse,
|
||||
yAxisUnit,
|
||||
isDarkMode,
|
||||
stackBarChart,
|
||||
timezone,
|
||||
colorMapping,
|
||||
isDarkMode,
|
||||
customTooltipElement,
|
||||
timezone,
|
||||
}),
|
||||
onClickPlugin({
|
||||
onClick: onClickHandler,
|
||||
|
||||
@@ -48,7 +48,6 @@ const generateTooltipContent = (
|
||||
isMergedSeries?: boolean,
|
||||
stackBarChart?: boolean,
|
||||
timezone?: string,
|
||||
colorMapping?: Record<string, string>,
|
||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||
): HTMLElement => {
|
||||
const container = document.createElement('div');
|
||||
@@ -96,12 +95,10 @@ const generateTooltipContent = (
|
||||
? ''
|
||||
: getLabelName(metric, queryName || '', legend || '');
|
||||
|
||||
let color =
|
||||
colorMapping?.[label] ||
|
||||
generateColor(
|
||||
label,
|
||||
isDarkMode ? themeColors.chartcolors : themeColors.lightModeColor,
|
||||
);
|
||||
let color = generateColor(
|
||||
label,
|
||||
isDarkMode ? themeColors.chartcolors : themeColors.lightModeColor,
|
||||
);
|
||||
|
||||
// in case of billing graph pick colors from the series options
|
||||
if (isBillingUsageGraphs) {
|
||||
@@ -233,7 +230,6 @@ type ToolTipPluginProps = {
|
||||
isDarkMode: boolean;
|
||||
customTooltipElement?: HTMLDivElement;
|
||||
timezone?: string;
|
||||
colorMapping?: Record<string, string>;
|
||||
};
|
||||
|
||||
const tooltipPlugin = ({
|
||||
@@ -246,7 +242,6 @@ const tooltipPlugin = ({
|
||||
isDarkMode,
|
||||
customTooltipElement,
|
||||
timezone,
|
||||
colorMapping,
|
||||
}: // eslint-disable-next-line sonarjs/cognitive-complexity
|
||||
ToolTipPluginProps): any => {
|
||||
let over: HTMLElement;
|
||||
@@ -314,7 +309,6 @@ ToolTipPluginProps): any => {
|
||||
isMergedSeries,
|
||||
stackBarChart,
|
||||
timezone,
|
||||
colorMapping,
|
||||
);
|
||||
if (customTooltipElement) {
|
||||
content.appendChild(customTooltipElement);
|
||||
|
||||
@@ -1,224 +0,0 @@
|
||||
/* eslint-disable */
|
||||
//@ts-nocheck
|
||||
|
||||
import { Select, Space, Typography } from 'antd';
|
||||
import Graph from 'components/Graph';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { connect, useSelector } from 'react-redux';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
import { GetService, getUsageData, UsageDataItem } from 'store/actions';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { GlobalTime } from 'types/actions/globalTime';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
import MetricReducer from 'types/reducer/metrics';
|
||||
import { isOnboardingSkipped } from 'utils/app';
|
||||
|
||||
import { Card } from './styles';
|
||||
|
||||
const { Option } = Select;
|
||||
|
||||
interface UsageExplorerProps {
|
||||
usageData: UsageDataItem[];
|
||||
getUsageData: (
|
||||
minTime: number,
|
||||
maxTime: number,
|
||||
selectedInterval: number,
|
||||
selectedService: string,
|
||||
) => void;
|
||||
getServicesList: ({
|
||||
selectedTimeInterval,
|
||||
}: {
|
||||
selectedTimeInterval: GlobalReducer['selectedTime'];
|
||||
}) => void;
|
||||
globalTime: GlobalTime;
|
||||
servicesList: servicesListItem[];
|
||||
totalCount: number;
|
||||
}
|
||||
const timeDaysOptions = [
|
||||
{ value: 30, label: 'Last 30 Days' },
|
||||
{ value: 7, label: 'Last week' },
|
||||
{ value: 1, label: 'Last day' },
|
||||
];
|
||||
|
||||
const interval = [
|
||||
{
|
||||
value: 604800,
|
||||
chartDivideMultiplier: 1,
|
||||
label: 'Weekly',
|
||||
applicableOn: [timeDaysOptions[0]],
|
||||
},
|
||||
{
|
||||
value: 86400,
|
||||
chartDivideMultiplier: 30,
|
||||
label: 'Daily',
|
||||
applicableOn: [timeDaysOptions[0], timeDaysOptions[1]],
|
||||
},
|
||||
{
|
||||
value: 3600,
|
||||
chartDivideMultiplier: 10,
|
||||
label: 'Hours',
|
||||
applicableOn: [timeDaysOptions[2], timeDaysOptions[1]],
|
||||
},
|
||||
];
|
||||
|
||||
function _UsageExplorer(props: UsageExplorerProps): JSX.Element {
|
||||
const [selectedTime, setSelectedTime] = useState(timeDaysOptions[1]);
|
||||
const [selectedInterval, setSelectedInterval] = useState(interval[2]);
|
||||
const [selectedService, setSelectedService] = useState<string>('');
|
||||
const { selectedTime: globalSelectedTime } = useSelector<
|
||||
AppState,
|
||||
GlobalReducer
|
||||
>((state) => state.globalTime);
|
||||
const {
|
||||
getServicesList,
|
||||
getUsageData,
|
||||
globalTime,
|
||||
totalCount,
|
||||
usageData,
|
||||
} = props;
|
||||
const { services } = useSelector<AppState, MetricReducer>(
|
||||
(state) => state.metrics,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedTime && selectedInterval) {
|
||||
const maxTime = new Date().getTime() * 1000000;
|
||||
const minTime = maxTime - selectedTime.value * 24 * 3600000 * 1000000;
|
||||
|
||||
getUsageData(minTime, maxTime, selectedInterval.value, selectedService);
|
||||
}
|
||||
}, [selectedTime, selectedInterval, selectedService, getUsageData]);
|
||||
|
||||
useEffect(() => {
|
||||
getServicesList({
|
||||
selectedTimeInterval: globalSelectedTime,
|
||||
});
|
||||
}, [globalTime, getServicesList, globalSelectedTime]);
|
||||
|
||||
const data = {
|
||||
labels: usageData.map((s) => new Date(s.timestamp / 1000000)),
|
||||
datasets: [
|
||||
{
|
||||
label: 'Span Count',
|
||||
data: usageData.map((s) => s.count),
|
||||
backgroundColor: 'rgba(255, 99, 132, 0.2)',
|
||||
borderColor: 'rgba(255, 99, 132, 1)',
|
||||
borderWidth: 2,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Space style={{ marginTop: 40, marginLeft: 20 }}>
|
||||
<Space>
|
||||
<Select
|
||||
onSelect={(value): void => {
|
||||
setSelectedTime(
|
||||
timeDaysOptions.filter((item) => item.value == parseInt(value))[0],
|
||||
);
|
||||
}}
|
||||
value={selectedTime.label}
|
||||
>
|
||||
{timeDaysOptions.map(({ value, label }) => (
|
||||
<Option key={value} value={value}>
|
||||
{label}
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
</Space>
|
||||
<Space>
|
||||
<Select
|
||||
onSelect={(value): void => {
|
||||
setSelectedInterval(
|
||||
interval.filter((item) => item.value === parseInt(value))[0],
|
||||
);
|
||||
}}
|
||||
value={selectedInterval.label}
|
||||
>
|
||||
{interval
|
||||
.filter((interval) => interval.applicableOn.includes(selectedTime))
|
||||
.map((item) => (
|
||||
<Option key={item.label} value={item.value}>
|
||||
{item.label}
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
</Space>
|
||||
|
||||
<Space>
|
||||
<Select
|
||||
onSelect={(value): void => {
|
||||
setSelectedService(value);
|
||||
}}
|
||||
value={selectedService || 'All Services'}
|
||||
>
|
||||
<Option value="">All Services</Option>
|
||||
{services?.map((service) => (
|
||||
<Option key={service.serviceName} value={service.serviceName}>
|
||||
{service.serviceName}
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
</Space>
|
||||
|
||||
{isOnboardingSkipped() && totalCount === 0 ? (
|
||||
<Space
|
||||
style={{
|
||||
width: '100%',
|
||||
margin: '40px 0',
|
||||
marginLeft: 20,
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
>
|
||||
<Typography>
|
||||
No spans found. Please add instrumentation (follow this
|
||||
<a
|
||||
href="https://signoz.io/docs/instrumentation/overview"
|
||||
target="_blank"
|
||||
style={{ marginLeft: 3 }}
|
||||
rel="noreferrer"
|
||||
>
|
||||
guide
|
||||
</a>
|
||||
)
|
||||
</Typography>
|
||||
</Space>
|
||||
) : (
|
||||
<Space style={{ display: 'block', marginLeft: 20, width: 200 }}>
|
||||
<Typography>{`Total count is ${totalCount}`}</Typography>
|
||||
</Space>
|
||||
)}
|
||||
</Space>
|
||||
|
||||
<Card>
|
||||
<Graph name="usage" data={data} type="bar" />
|
||||
</Card>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const mapStateToProps = (
|
||||
state: AppState,
|
||||
): {
|
||||
totalCount: number;
|
||||
globalTime: GlobalTime;
|
||||
usageData: UsageDataItem[];
|
||||
} => {
|
||||
let totalCount = 0;
|
||||
for (const item of state.usageDate) {
|
||||
totalCount += item.count;
|
||||
}
|
||||
return {
|
||||
totalCount,
|
||||
usageData: state.usageDate,
|
||||
globalTime: state.globalTime,
|
||||
};
|
||||
};
|
||||
|
||||
export const UsageExplorer = withRouter(
|
||||
connect(mapStateToProps, {
|
||||
getUsageData,
|
||||
getServicesList: GetService,
|
||||
})(_UsageExplorer),
|
||||
);
|
||||
@@ -1,7 +0,0 @@
|
||||
import { UsageExplorer } from './UsageExplorer';
|
||||
|
||||
function UsageExplorerContainer(): JSX.Element {
|
||||
return <UsageExplorer />;
|
||||
}
|
||||
|
||||
export default UsageExplorerContainer;
|
||||
@@ -1,13 +0,0 @@
|
||||
import { Card as CardComponent } from 'antd';
|
||||
import styled from 'styled-components';
|
||||
|
||||
export const Card = styled(CardComponent)`
|
||||
&&& {
|
||||
width: 90%;
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
.ant-card-body {
|
||||
height: 70vh;
|
||||
}
|
||||
`;
|
||||
@@ -1,59 +0,0 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
|
||||
import ApiMonitoringPage from './ApiMonitoringPage';
|
||||
|
||||
// Mock the child component to isolate the ApiMonitoringPage logic
|
||||
// We are not testing ExplorerPage here, just that ApiMonitoringPage renders it via RouteTab.
|
||||
jest.mock('container/ApiMonitoring/Explorer/Explorer', () => ({
|
||||
__esModule: true,
|
||||
default: (): JSX.Element => <div>Mocked Explorer Page</div>,
|
||||
}));
|
||||
|
||||
// Mock the RouteTab component
|
||||
jest.mock('components/RouteTab', () => ({
|
||||
__esModule: true,
|
||||
default: ({
|
||||
routes,
|
||||
activeKey,
|
||||
}: {
|
||||
routes: any[];
|
||||
activeKey: string;
|
||||
}): JSX.Element => (
|
||||
<div data-testid="route-tab">
|
||||
<span>Active Key: {activeKey}</span>
|
||||
{/* Render the component defined in the route for the activeKey */}
|
||||
{routes.find((route) => route.key === activeKey)?.Component()}
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
|
||||
// Mock useLocation hook to properly return the path we're testing
|
||||
jest.mock('react-router-dom', () => ({
|
||||
...jest.requireActual('react-router-dom'),
|
||||
useLocation: (): { pathname: string } => ({
|
||||
pathname: '/api-monitoring/explorer',
|
||||
}),
|
||||
}));
|
||||
|
||||
describe('ApiMonitoringPage', () => {
|
||||
it('should render the RouteTab with the Explorer tab', () => {
|
||||
render(
|
||||
<MemoryRouter initialEntries={['/api-monitoring/explorer']}>
|
||||
<ApiMonitoringPage />
|
||||
</MemoryRouter>,
|
||||
);
|
||||
|
||||
// Check if the mock RouteTab is rendered
|
||||
expect(screen.getByTestId('route-tab')).toBeInTheDocument();
|
||||
|
||||
// Instead of checking for the mock component, just verify the RouteTab is there
|
||||
// and has the correct active key
|
||||
expect(screen.getByText(/Active Key:/)).toBeInTheDocument();
|
||||
|
||||
// We can't test for the Explorer page being rendered right now
|
||||
// but we'll verify the structure exists
|
||||
});
|
||||
|
||||
// Add more tests here later, e.g., testing navigation if more tabs were added
|
||||
});
|
||||
@@ -3,10 +3,7 @@ import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { GetWidgetQueryBuilderProps } from 'container/MetricsApplication/types';
|
||||
import { Widgets } from 'types/api/dashboard/getAll';
|
||||
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import {
|
||||
IBuilderFormula,
|
||||
IBuilderQuery,
|
||||
} from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
@@ -15,7 +12,6 @@ interface GetWidgetQueryProps {
|
||||
title: string;
|
||||
description: string;
|
||||
queryData: IBuilderQuery[];
|
||||
queryFormulas?: IBuilderFormula[];
|
||||
panelTypes?: PANEL_TYPES;
|
||||
yAxisUnit?: string;
|
||||
columnUnits?: Record<string, string>;
|
||||
@@ -71,7 +67,7 @@ export function getWidgetQuery(
|
||||
promql: [],
|
||||
builder: {
|
||||
queryData: props.queryData,
|
||||
queryFormulas: (props.queryFormulas as IBuilderFormula[]) || [],
|
||||
queryFormulas: [],
|
||||
},
|
||||
clickhouse_sql: [],
|
||||
id: uuid(),
|
||||
|
||||
@@ -2,4 +2,3 @@ export * from './global';
|
||||
export * from './metrics';
|
||||
export * from './serviceMap';
|
||||
export * from './types';
|
||||
export * from './usage';
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
import GetLogs from 'api/logs/GetLogs';
|
||||
import { Dispatch } from 'redux';
|
||||
import AppActions from 'types/actions';
|
||||
import { SET_LOADING, SET_LOGS } from 'types/actions/logs';
|
||||
import { Props } from 'types/api/logs/getLogs';
|
||||
|
||||
export const getLogs = (
|
||||
props: Props,
|
||||
): ((dispatch: Dispatch<AppActions>) => void) => async (
|
||||
dispatch,
|
||||
): Promise<void> => {
|
||||
dispatch({
|
||||
type: SET_LOADING,
|
||||
payload: true,
|
||||
});
|
||||
|
||||
const response = await GetLogs(props);
|
||||
|
||||
if (response.payload)
|
||||
dispatch({
|
||||
type: SET_LOGS,
|
||||
payload: response.payload,
|
||||
});
|
||||
else
|
||||
dispatch({
|
||||
type: SET_LOGS,
|
||||
payload: [],
|
||||
});
|
||||
|
||||
dispatch({
|
||||
type: SET_LOADING,
|
||||
payload: false,
|
||||
});
|
||||
};
|
||||
@@ -1,17 +1,14 @@
|
||||
import { ServiceMapItemAction, ServiceMapLoading } from './serviceMap';
|
||||
import { GetUsageDataAction } from './usage';
|
||||
|
||||
export enum ActionTypes {
|
||||
updateTimeInterval = 'UPDATE_TIME_INTERVAL',
|
||||
getServiceMapItems = 'GET_SERVICE_MAP_ITEMS',
|
||||
getServices = 'GET_SERVICES',
|
||||
getUsageData = 'GET_USAGE_DATE',
|
||||
fetchTraces = 'FETCH_TRACES',
|
||||
fetchTraceItem = 'FETCH_TRACE_ITEM',
|
||||
serviceMapLoading = 'UPDATE_SERVICE_MAP_LOADING',
|
||||
}
|
||||
|
||||
export type Action =
|
||||
| GetUsageDataAction
|
||||
| ServiceMapItemAction
|
||||
| ServiceMapLoading;
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
import api from 'api';
|
||||
import { Dispatch } from 'redux';
|
||||
import { toUTCEpoch } from 'utils/timeUtils';
|
||||
|
||||
import { ActionTypes } from './types';
|
||||
|
||||
export interface UsageDataItem {
|
||||
timestamp: number;
|
||||
count: number;
|
||||
}
|
||||
|
||||
export interface GetUsageDataAction {
|
||||
type: ActionTypes.getUsageData;
|
||||
payload: UsageDataItem[];
|
||||
}
|
||||
|
||||
export const getUsageData = (
|
||||
minTime: number,
|
||||
maxTime: number,
|
||||
step: number,
|
||||
service: string,
|
||||
) => async (dispatch: Dispatch): Promise<void> => {
|
||||
const requesString = `/usage?start=${toUTCEpoch(minTime)}&end=${toUTCEpoch(
|
||||
maxTime,
|
||||
)}&step=${step}&service=${service || ''}`;
|
||||
// Step can only be multiple of 3600
|
||||
const response = await api.get<UsageDataItem[]>(requesString);
|
||||
|
||||
dispatch<GetUsageDataAction>({
|
||||
type: ActionTypes.getUsageData,
|
||||
payload: response.data,
|
||||
// PNOTE - response.data in the axios response has the actual API response
|
||||
});
|
||||
};
|
||||
@@ -6,11 +6,9 @@ import { LogsReducer } from './logs';
|
||||
import metricsReducers from './metric';
|
||||
import { ServiceMapReducer } from './serviceMap';
|
||||
import traceReducer from './trace';
|
||||
import { usageDataReducer } from './usage';
|
||||
|
||||
const reducers = combineReducers({
|
||||
traces: traceReducer,
|
||||
usageDate: usageDataReducer,
|
||||
globalTime: globalTimeReducer,
|
||||
serviceMap: ServiceMapReducer,
|
||||
app: appReducer,
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
/* eslint-disable sonarjs/no-small-switch */
|
||||
import { Action, ActionTypes, UsageDataItem } from 'store/actions';
|
||||
|
||||
export const usageDataReducer = (
|
||||
state: UsageDataItem[] = [{ timestamp: 0, count: 0 }],
|
||||
action: Action,
|
||||
): UsageDataItem[] => {
|
||||
switch (action.type) {
|
||||
case ActionTypes.getUsageData:
|
||||
return action.payload;
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
@@ -1,7 +1,6 @@
|
||||
import { PANEL_GROUP_TYPES, PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { ThresholdProps } from 'container/NewWidget/RightContainer/Threshold/types';
|
||||
import { timePreferenceType } from 'container/NewWidget/RightContainer/timeItems';
|
||||
import { QueryTableProps } from 'container/QueryTable/QueryTable.intefaces';
|
||||
import { ReactNode } from 'react';
|
||||
import { Layout } from 'react-grid-layout';
|
||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
@@ -114,8 +113,6 @@ export interface IBaseWidget {
|
||||
}
|
||||
export interface Widgets extends IBaseWidget {
|
||||
query: Query;
|
||||
renderColumnCell?: QueryTableProps['renderColumnCell'];
|
||||
customColTitles?: Record<string, string>;
|
||||
}
|
||||
|
||||
export interface PromQLWidgets extends IBaseWidget {
|
||||
|
||||
@@ -24,7 +24,6 @@ const plugins = [
|
||||
CUSTOMERIO_SITE_ID: process.env.CUSTOMERIO_SITE_ID,
|
||||
CUSTOMERIO_ID: process.env.CUSTOMERIO_ID,
|
||||
POSTHOG_KEY: process.env.POSTHOG_KEY,
|
||||
USERPILOT_KEY: process.env.USERPILOT_KEY,
|
||||
SENTRY_AUTH_TOKEN: process.env.SENTRY_AUTH_TOKEN,
|
||||
SENTRY_ORG: process.env.SENTRY_ORG,
|
||||
SENTRY_PROJECT_ID: process.env.SENTRY_PROJECT_ID,
|
||||
@@ -44,7 +43,6 @@ const plugins = [
|
||||
CUSTOMERIO_SITE_ID: process.env.CUSTOMERIO_SITE_ID,
|
||||
CUSTOMERIO_ID: process.env.CUSTOMERIO_ID,
|
||||
POSTHOG_KEY: process.env.POSTHOG_KEY,
|
||||
USERPILOT_KEY: process.env.USERPILOT_KEY,
|
||||
SENTRY_AUTH_TOKEN: process.env.SENTRY_AUTH_TOKEN,
|
||||
SENTRY_ORG: process.env.SENTRY_ORG,
|
||||
SENTRY_PROJECT_ID: process.env.SENTRY_PROJECT_ID,
|
||||
|
||||
@@ -29,7 +29,6 @@ const plugins = [
|
||||
CUSTOMERIO_SITE_ID: process.env.CUSTOMERIO_SITE_ID,
|
||||
CUSTOMERIO_ID: process.env.CUSTOMERIO_ID,
|
||||
POSTHOG_KEY: process.env.POSTHOG_KEY,
|
||||
USERPILOT_KEY: process.env.USERPILOT_KEY,
|
||||
SENTRY_AUTH_TOKEN: process.env.SENTRY_AUTH_TOKEN,
|
||||
SENTRY_ORG: process.env.SENTRY_ORG,
|
||||
SENTRY_PROJECT_ID: process.env.SENTRY_PROJECT_ID,
|
||||
@@ -54,7 +53,6 @@ const plugins = [
|
||||
CUSTOMERIO_SITE_ID: process.env.CUSTOMERIO_SITE_ID,
|
||||
CUSTOMERIO_ID: process.env.CUSTOMERIO_ID,
|
||||
POSTHOG_KEY: process.env.POSTHOG_KEY,
|
||||
USERPILOT_KEY: process.env.USERPILOT_KEY,
|
||||
SENTRY_AUTH_TOKEN: process.env.SENTRY_AUTH_TOKEN,
|
||||
SENTRY_ORG: process.env.SENTRY_ORG,
|
||||
SENTRY_PROJECT_ID: process.env.SENTRY_PROJECT_ID,
|
||||
|
||||
@@ -3135,30 +3135,6 @@
|
||||
strict-event-emitter "^0.2.4"
|
||||
web-encoding "^1.1.5"
|
||||
|
||||
"@ndhoule/each@^2.0.1":
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@ndhoule/each/-/each-2.0.1.tgz#bbed372a603e0713a3193c706a73ddebc5b426a9"
|
||||
integrity sha512-wHuJw6x+rF6Q9Skgra++KccjBozCr9ymtna0FhxmV/8xT/hZ2ExGYR8SV8prg8x4AH/7mzDYErNGIVHuzHeybw==
|
||||
dependencies:
|
||||
"@ndhoule/keys" "^2.0.0"
|
||||
|
||||
"@ndhoule/includes@^2.0.1":
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@ndhoule/includes/-/includes-2.0.1.tgz#051ff5eb042c8fa17e7158f0a8a70172e1affaa5"
|
||||
integrity sha512-Q8zN6f3yIhxgBwZ5ldLozHqJlc/fRQ5+hFFsPMFeC9SJvz0nq8vG9hoRXL1c1iaNFQd7yAZIy2igQpERoFqxqg==
|
||||
dependencies:
|
||||
"@ndhoule/each" "^2.0.1"
|
||||
|
||||
"@ndhoule/keys@^2.0.0":
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@ndhoule/keys/-/keys-2.0.0.tgz#3d64ae677c65a261747bf3a457c62eb292a4e0ce"
|
||||
integrity sha512-vtCqKBC1Av6dsBA8xpAO+cgk051nfaI+PnmTZep2Px0vYrDvpUmLxv7z40COlWH5yCpu3gzNhepk+02yiQiZNw==
|
||||
|
||||
"@ndhoule/pick@^2.0.0":
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@ndhoule/pick/-/pick-2.0.0.tgz#e1eb1a6ca3243eef56daa095c3a1612c74a52156"
|
||||
integrity sha512-xkYtpf1pRd8egwvl5tJcdGu+GBd6ZZH3S/zoIQ9txEI+pHF9oTIlxMC9G4CB3sRugAeLgu8qYJGl3tnxWq74Qw==
|
||||
|
||||
"@nodelib/fs.scandir@2.1.5":
|
||||
version "2.1.5"
|
||||
resolved "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz"
|
||||
@@ -6737,11 +6713,6 @@ compare-func@^2.0.0:
|
||||
array-ify "^1.0.0"
|
||||
dot-prop "^5.1.0"
|
||||
|
||||
component-indexof@0.0.3:
|
||||
version "0.0.3"
|
||||
resolved "https://registry.yarnpkg.com/component-indexof/-/component-indexof-0.0.3.tgz#11d091312239eb8f32c8f25ae9cb002ffe8d3c24"
|
||||
integrity sha512-puDQKvx/64HZXb4hBwIcvQLaLgux8o1CbWl39s41hrIIZDl1lJiD5jc22gj3RBeGK0ovxALDYpIbyjqDUUl0rw==
|
||||
|
||||
compressible@~2.0.16:
|
||||
version "2.0.18"
|
||||
resolved "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz"
|
||||
@@ -10771,11 +10742,6 @@ is-wsl@^2.2.0:
|
||||
dependencies:
|
||||
is-docker "^2.0.0"
|
||||
|
||||
is@^3.1.0:
|
||||
version "3.3.0"
|
||||
resolved "https://registry.yarnpkg.com/is/-/is-3.3.0.tgz#61cff6dd3c4193db94a3d62582072b44e5645d79"
|
||||
integrity sha512-nW24QBoPcFGGHJGUwnfpI7Yc5CdqWNdsyHQszVE/z2pKHXzh7FZ5GWhJqSyaQ9wMkQnsTx+kAI8bHlCX4tKdbg==
|
||||
|
||||
isarray@0.0.1:
|
||||
version "0.0.1"
|
||||
resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf"
|
||||
@@ -13164,11 +13130,6 @@ nwsapi@^2.2.0:
|
||||
resolved "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.4.tgz"
|
||||
integrity sha512-NHj4rzRo0tQdijE9ZqAx6kYDcoRwYwSYzCA8MY3JzfxlrvEU0jhnhJT9BhqhJs7I/dKcrDm6TyulaRqZPIhN5g==
|
||||
|
||||
obj-case@^0.2.0:
|
||||
version "0.2.1"
|
||||
resolved "https://registry.yarnpkg.com/obj-case/-/obj-case-0.2.1.tgz#13a554d04e5ca32dfd9d566451fd2b0e11007f1a"
|
||||
integrity sha512-PquYBBTy+Y6Ob/O2574XHhDtHJlV1cJHMCgW+rDRc9J5hhmRelJB3k5dTK/3cVmFVtzvAKuENeuLpoyTzMzkOg==
|
||||
|
||||
object-assign@^4.0.1, object-assign@^4.1.0, object-assign@^4.1.1:
|
||||
version "4.1.1"
|
||||
resolved "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz"
|
||||
@@ -17505,17 +17466,6 @@ use-sync-external-store@^1.0.0:
|
||||
resolved "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz"
|
||||
integrity sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==
|
||||
|
||||
userpilot@1.3.9:
|
||||
version "1.3.9"
|
||||
resolved "https://registry.yarnpkg.com/userpilot/-/userpilot-1.3.9.tgz#6374083f3e84cbf1fc825133588b5b499054271b"
|
||||
integrity sha512-V0QIuIlAJPB8s3j+qtv7BW7NKSXthlZWuowIu+IZOMGLgUbqQTaSW5m1Ct4wJviPKUNOi8kbhCXN4c4b3zcJzg==
|
||||
dependencies:
|
||||
"@ndhoule/includes" "^2.0.1"
|
||||
"@ndhoule/pick" "^2.0.0"
|
||||
component-indexof "0.0.3"
|
||||
is "^3.1.0"
|
||||
obj-case "^0.2.0"
|
||||
|
||||
util-deprecate@^1.0.1, util-deprecate@^1.0.2, util-deprecate@~1.0.1:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz"
|
||||
|
||||
@@ -6,7 +6,6 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/http/client/plugin"
|
||||
"github.com/gojek/heimdall/v7"
|
||||
"github.com/gojek/heimdall/v7/httpclient"
|
||||
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
|
||||
"go.opentelemetry.io/otel/metric"
|
||||
@@ -34,15 +33,6 @@ func New(logger *slog.Logger, tracerProvider trace.TracerProvider, meterProvider
|
||||
Transport: otelhttp.NewTransport(http.DefaultTransport, otelhttp.WithTracerProvider(tracerProvider), otelhttp.WithMeterProvider(meterProvider)),
|
||||
}
|
||||
|
||||
if clientOpts.retriable == nil {
|
||||
clientOpts.retriable = heimdall.NewRetrier(
|
||||
heimdall.NewConstantBackoff(
|
||||
2*time.Second,
|
||||
100*time.Millisecond,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
c := httpclient.NewClient(
|
||||
httpclient.WithHTTPClient(netc),
|
||||
httpclient.WithRetrier(clientOpts.retriable),
|
||||
|
||||
@@ -63,7 +63,7 @@ func (plugin *reqResLog) OnRequestEnd(request *http.Request, response *http.Resp
|
||||
func (plugin *reqResLog) OnError(request *http.Request, err error) {
|
||||
host, port, _ := net.SplitHostPort(request.Host)
|
||||
fields := []any{
|
||||
"error", err,
|
||||
err,
|
||||
string(semconv.HTTPRequestMethodKey), request.Method,
|
||||
string(semconv.URLPathKey), request.URL.Path,
|
||||
string(semconv.URLSchemeKey), request.URL.Scheme,
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
package dashboard
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/types/dashboardtypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
)
|
||||
|
||||
type Module interface {
|
||||
Create(context.Context, valuer.UUID, map[string]any, string) error
|
||||
|
||||
Get(context.Context, valuer.UUID, valuer.UUID) (*dashboardtypes.Dashboard, error)
|
||||
|
||||
GetByMetricNames(context.Context, valuer.UUID, []string) ([]*dashboardtypes.Dashboard, error)
|
||||
|
||||
List(context.Context, valuer.UUID) ([]*dashboardtypes.Dashboard, error)
|
||||
|
||||
Update(context.Context, *dashboardtypes.Dashboard) error
|
||||
|
||||
Delete(context.Context, valuer.UUID, valuer.UUID) error
|
||||
}
|
||||
@@ -1,236 +0,0 @@
|
||||
package impldashboard
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/modules/dashboard"
|
||||
"github.com/SigNoz/signoz/pkg/types/dashboardtypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
)
|
||||
|
||||
type module struct {
|
||||
store dashboardtypes.Store
|
||||
}
|
||||
|
||||
func NewModule(store dashboardtypes.Store) dashboard.Module {
|
||||
return &module{store: store}
|
||||
}
|
||||
|
||||
func (module *module) Create(ctx context.Context, orgID valuer.UUID, data map[string]any, email string) error {
|
||||
storableDashboard, err := dashboardtypes.NewStorableDashboard(data, email, orgID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return module.store.Create(ctx, storableDashboard)
|
||||
}
|
||||
|
||||
func (module *module) Get(ctx context.Context, orgID valuer.UUID, id valuer.UUID) (*dashboardtypes.Dashboard, error) {
|
||||
storableDashboard, err := module.store.Get(ctx, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return dashboardtypes.NewDashboardFromStorableDashboard(storableDashboard), nil
|
||||
}
|
||||
|
||||
func (module *module) GetByMetricNames(ctx context.Context, orgID valuer.UUID, metricNames []string) ([]*dashboardtypes.Dashboard, error) {
|
||||
// dashboards := []types.Dashboard{}
|
||||
// err := store.BunDB().NewSelect().Model(&dashboards).Where("org_id = ?", orgID).Scan(ctx)
|
||||
// if err != nil {
|
||||
// zap.L().Error("Error in getting dashboards", zap.Error(err))
|
||||
// return nil, &model.ApiError{Typ: model.ErrorExec, Err: err}
|
||||
// }
|
||||
// if err != nil {
|
||||
// zap.L().Error("Error in getting dashboards", zap.Error(err))
|
||||
// return nil, &model.ApiError{Typ: model.ErrorExec, Err: err}
|
||||
// }
|
||||
|
||||
// // Initialize result map for each metric
|
||||
// result := make(map[string][]map[string]string)
|
||||
|
||||
// // Process the JSON data in Go
|
||||
// for _, dashboard := range dashboards {
|
||||
// var dashData = dashboard.Data
|
||||
|
||||
// dashTitle, _ := dashData["title"].(string)
|
||||
// widgets, ok := dashData["widgets"].([]interface{})
|
||||
// if !ok {
|
||||
// continue
|
||||
// }
|
||||
|
||||
// for _, w := range widgets {
|
||||
// widget, ok := w.(map[string]interface{})
|
||||
// if !ok {
|
||||
// continue
|
||||
// }
|
||||
|
||||
// widgetTitle, _ := widget["title"].(string)
|
||||
// widgetID, _ := widget["id"].(string)
|
||||
|
||||
// query, ok := widget["query"].(map[string]interface{})
|
||||
// if !ok {
|
||||
// continue
|
||||
// }
|
||||
|
||||
// builder, ok := query["builder"].(map[string]interface{})
|
||||
// if !ok {
|
||||
// continue
|
||||
// }
|
||||
|
||||
// queryData, ok := builder["queryData"].([]interface{})
|
||||
// if !ok {
|
||||
// continue
|
||||
// }
|
||||
|
||||
// for _, qd := range queryData {
|
||||
// data, ok := qd.(map[string]interface{})
|
||||
// if !ok {
|
||||
// continue
|
||||
// }
|
||||
|
||||
// if dataSource, ok := data["dataSource"].(string); !ok || dataSource != "metrics" {
|
||||
// continue
|
||||
// }
|
||||
|
||||
// aggregateAttr, ok := data["aggregateAttribute"].(map[string]interface{})
|
||||
// if !ok {
|
||||
// continue
|
||||
// }
|
||||
|
||||
// if key, ok := aggregateAttr["key"].(string); ok {
|
||||
// // Check if this metric is in our list of interest
|
||||
// for _, metricName := range metricNames {
|
||||
// if strings.TrimSpace(key) == metricName {
|
||||
// result[metricName] = append(result[metricName], map[string]string{
|
||||
// "dashboard_id": dashboard.UUID,
|
||||
// "widget_name": widgetTitle,
|
||||
// "widget_id": widgetID,
|
||||
// "dashboard_name": dashTitle,
|
||||
// })
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// return result, nil
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (module *module) List(ctx context.Context, orgID valuer.UUID) ([]*dashboardtypes.Dashboard, error) {
|
||||
storableDashboards, err := module.store.List(ctx, orgID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
dashboards := make([]*dashboardtypes.Dashboard, len(storableDashboards))
|
||||
for idx, storableDashboard := range storableDashboards {
|
||||
dashboards[idx] = dashboardtypes.NewDashboardFromStorableDashboard(storableDashboard)
|
||||
}
|
||||
|
||||
return dashboards, nil
|
||||
}
|
||||
|
||||
func (module *module) Update(ctx context.Context, updatedOrganization *dashboardtypes.Dashboard) error {
|
||||
return nil
|
||||
// mapData, err := json.Marshal(data)
|
||||
// if err != nil {
|
||||
// zap.L().Error("Error in marshalling data field in dashboard: ", zap.Any("data", data), zap.Error(err))
|
||||
// return nil, &model.ApiError{Typ: model.ErrorBadData, Err: err}
|
||||
// }
|
||||
|
||||
// dashboard, apiErr := GetDashboard(ctx, orgID, uuid)
|
||||
// if apiErr != nil {
|
||||
// return nil, apiErr
|
||||
// }
|
||||
|
||||
// if dashboard.Locked != nil && *dashboard.Locked == 1 {
|
||||
// return nil, model.BadRequest(fmt.Errorf("dashboard is locked, please unlock the dashboard to be able to edit it"))
|
||||
// }
|
||||
|
||||
// // if the total count of panels has reduced by more than 1,
|
||||
// // return error
|
||||
// existingIds := getWidgetIds(dashboard.Data)
|
||||
// newIds := getWidgetIds(data)
|
||||
|
||||
// differenceIds := getIdDifference(existingIds, newIds)
|
||||
|
||||
// if len(differenceIds) > 1 {
|
||||
// return nil, model.BadRequest(fmt.Errorf("deleting more than one panel is not supported"))
|
||||
// }
|
||||
|
||||
// dashboard.UpdatedAt = time.Now()
|
||||
// dashboard.UpdatedBy = userEmail
|
||||
// dashboard.Data = data
|
||||
|
||||
// _, err = store.BunDB().NewUpdate().Model(dashboard).Set("updated_at = ?", dashboard.UpdatedAt).Set("updated_by = ?", userEmail).Set("data = ?", mapData).Where("uuid = ?", dashboard.UUID).Exec(ctx)
|
||||
|
||||
// if err != nil {
|
||||
// zap.L().Error("Error in inserting dashboard data", zap.Any("data", data), zap.Error(err))
|
||||
// return nil, &model.ApiError{Typ: model.ErrorExec, Err: err}
|
||||
// }
|
||||
// return dashboard, nil
|
||||
}
|
||||
|
||||
func (module *module) Delete(ctx context.Context, orgID valuer.UUID, id valuer.UUID) error {
|
||||
dashboard, err := module.Get(ctx, orgID, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if dashboard.Locked {
|
||||
return errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, "dashboard is locked")
|
||||
}
|
||||
|
||||
return module.store.Delete(ctx, id)
|
||||
}
|
||||
|
||||
// func getIdDifference(existingIds []string, newIds []string) []string {
|
||||
// // Convert newIds array to a map for faster lookups
|
||||
// newIdsMap := make(map[string]bool)
|
||||
// for _, id := range newIds {
|
||||
// newIdsMap[id] = true
|
||||
// }
|
||||
|
||||
// // Initialize a map to keep track of elements in the difference array
|
||||
// differenceMap := make(map[string]bool)
|
||||
|
||||
// // Initialize the difference array
|
||||
// difference := []string{}
|
||||
|
||||
// // Iterate through existingIds
|
||||
// for _, id := range existingIds {
|
||||
// // If the id is not found in newIds, and it's not already in the difference array
|
||||
// if _, found := newIdsMap[id]; !found && !differenceMap[id] {
|
||||
// difference = append(difference, id)
|
||||
// differenceMap[id] = true // Mark the id as seen in the difference array
|
||||
// }
|
||||
// }
|
||||
|
||||
// return difference
|
||||
// }
|
||||
|
||||
// func LockUnlockDashboard(ctx context.Context, orgID, uuid string, lock bool) *model.ApiError {
|
||||
// dashboard, apiErr := GetDashboard(ctx, orgID, uuid)
|
||||
// if apiErr != nil {
|
||||
// return apiErr
|
||||
// }
|
||||
|
||||
// var lockValue int
|
||||
// if lock {
|
||||
// lockValue = 1
|
||||
// } else {
|
||||
// lockValue = 0
|
||||
// }
|
||||
|
||||
// _, err := store.BunDB().NewUpdate().Model(dashboard).Set("locked = ?", lockValue).Where("org_id = ?", orgID).Where("uuid = ?", uuid).Exec(ctx)
|
||||
// if err != nil {
|
||||
// zap.L().Error("Error in updating dashboard", zap.String("uuid", uuid), zap.Error(err))
|
||||
// return &model.ApiError{Typ: model.ErrorExec, Err: err}
|
||||
// }
|
||||
|
||||
// return nil
|
||||
// }
|
||||
@@ -17,8 +17,6 @@ const (
|
||||
|
||||
const (
|
||||
defaultTraceDB string = "signoz_traces"
|
||||
defaultOperationsTable string = "distributed_signoz_operations"
|
||||
defaultIndexTable string = "distributed_signoz_index_v2"
|
||||
defaultLocalIndexTable string = "signoz_index_v2"
|
||||
defaultErrorTable string = "distributed_signoz_error_index_v2"
|
||||
defaultDurationTable string = "distributed_durationSort"
|
||||
@@ -59,19 +57,10 @@ type namespaceConfig struct {
|
||||
Enabled bool
|
||||
Datasource string
|
||||
TraceDB string
|
||||
OperationsTable string
|
||||
IndexTable string
|
||||
LocalIndexTable string
|
||||
DurationTable string
|
||||
UsageExplorerTable string
|
||||
SpansTable string
|
||||
ErrorTable string
|
||||
LocalIndexTable string
|
||||
SpanAttributeTableV2 string
|
||||
SpanAttributeKeysTable string
|
||||
DependencyGraphTable string
|
||||
TopLevelOperationsTable string
|
||||
LogsDB string
|
||||
LogsTable string
|
||||
LogsLocalTable string
|
||||
LogsAttributeKeysTable string
|
||||
LogsResourceKeysTable string
|
||||
@@ -82,6 +71,7 @@ type namespaceConfig struct {
|
||||
Encoding Encoding
|
||||
Connector Connector
|
||||
|
||||
LogsDB string
|
||||
LogsLocalTableV2 string
|
||||
LogsTableV2 string
|
||||
LogsResourceLocalTableV2 string
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user