Compare commits

..

9 Commits

368 changed files with 3701 additions and 14635 deletions

View File

@@ -68,8 +68,6 @@ jobs:
echo 'TUNNEL_URL="${{ secrets.TUNNEL_URL }}"' >> frontend/.env
echo 'TUNNEL_DOMAIN="${{ secrets.TUNNEL_DOMAIN }}"' >> frontend/.env
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
- name: Setup golang
uses: actions/setup-go@v4
with:

View File

@@ -219,18 +219,12 @@ Not sure how to get started? Just ping us on `#contributing` in our [slack commu
- [Nityananda Gohain](https://github.com/nityanandagohain)
- [Srikanth Chekuri](https://github.com/srikanthccv)
- [Vishal Sharma](https://github.com/makeavish)
- [Shivanshu Raj Shrivastava](https://github.com/shivanshuraj1333)
- [Ekansh Gupta](https://github.com/eKuG)
- [Aniket Agarwal](https://github.com/aniketio-ctrl)
#### Frontend
- [Yunus M](https://github.com/YounixM)
- [Vikrant Gupta](https://github.com/vikrantgupta25)
- [Sagar Rajput](https://github.com/SagarRajput-7)
- [Shaheer Kochai](https://github.com/ahmadshaheer)
- [Amlan Kumar Nandy](https://github.com/amlannandy)
- [Sahil Khan](https://github.com/sawhil)
#### DevOps

View File

@@ -181,7 +181,7 @@ services:
- query-service
query-service:
!!merge <<: *db-depend
image: signoz/query-service:0.72.0
image: signoz/query-service:0.70.1
command:
- --config=/root/config/prometheus.yml
- --use-logs-new-schema=true
@@ -214,7 +214,7 @@ services:
retries: 3
frontend:
!!merge <<: *common
image: signoz/frontend:0.72.0
image: signoz/frontend:0.70.1
depends_on:
- alertmanager
- query-service
@@ -224,7 +224,7 @@ services:
- ../common/signoz/nginx-config.conf:/etc/nginx/conf.d/default.conf
otel-collector:
!!merge <<: *db-depend
image: signoz/signoz-otel-collector:0.111.28
image: signoz/signoz-otel-collector:0.111.25
command:
- --config=/etc/otel-collector-config.yaml
- --manager-config=/etc/manager-config.yaml
@@ -248,7 +248,7 @@ services:
- query-service
schema-migrator:
!!merge <<: *common
image: signoz/signoz-schema-migrator:0.111.28
image: signoz/signoz-schema-migrator:0.111.24
deploy:
restart_policy:
condition: on-failure

View File

@@ -117,7 +117,7 @@ services:
- query-service
query-service:
!!merge <<: *db-depend
image: signoz/query-service:0.72.0
image: signoz/query-service:0.70.1
command:
- --config=/root/config/prometheus.yml
- --use-logs-new-schema=true
@@ -150,7 +150,7 @@ services:
retries: 3
frontend:
!!merge <<: *common
image: signoz/frontend:0.72.0
image: signoz/frontend:0.70.1
depends_on:
- alertmanager
- query-service
@@ -160,7 +160,7 @@ services:
- ../common/signoz/nginx-config.conf:/etc/nginx/conf.d/default.conf
otel-collector:
!!merge <<: *db-depend
image: signoz/signoz-otel-collector:0.111.28
image: signoz/signoz-otel-collector:0.111.25
command:
- --config=/etc/otel-collector-config.yaml
- --manager-config=/etc/manager-config.yaml
@@ -184,7 +184,7 @@ services:
- query-service
schema-migrator:
!!merge <<: *common
image: signoz/signoz-schema-migrator:0.111.28
image: signoz/signoz-schema-migrator:0.111.24
deploy:
restart_policy:
condition: on-failure

View File

@@ -66,7 +66,7 @@ exporters:
enabled: true
clickhousemetricswrite/prometheus:
endpoint: tcp://clickhouse:9000/signoz_metrics
signozclickhousemetrics:
clickhousemetricswritev2:
dsn: tcp://clickhouse:9000/signoz_metrics
clickhouselogsexporter:
dsn: tcp://clickhouse:9000/signoz_logs
@@ -90,11 +90,11 @@ service:
metrics:
receivers: [otlp]
processors: [batch]
exporters: [clickhousemetricswrite, signozclickhousemetrics]
exporters: [clickhousemetricswrite, clickhousemetricswritev2]
metrics/prometheus:
receivers: [prometheus]
processors: [batch]
exporters: [clickhousemetricswrite/prometheus, signozclickhousemetrics]
exporters: [clickhousemetricswrite/prometheus, clickhousemetricswritev2]
logs:
receivers: [otlp]
processors: [batch]

View File

@@ -188,7 +188,7 @@ services:
condition: service_healthy
query-service:
!!merge <<: *db-depend
image: signoz/query-service:${DOCKER_TAG:-0.72.0}
image: signoz/query-service:${DOCKER_TAG:-0.70.1}
container_name: signoz-query-service
command:
- --config=/root/config/prometheus.yml
@@ -222,7 +222,7 @@ services:
retries: 3
frontend:
!!merge <<: *common
image: signoz/frontend:${DOCKER_TAG:-0.72.0}
image: signoz/frontend:${DOCKER_TAG:-0.70.1}
container_name: signoz-frontend
depends_on:
- alertmanager
@@ -234,7 +234,7 @@ services:
# TODO: support otel-collector multiple replicas. Nginx/Traefik for loadbalancing?
otel-collector:
!!merge <<: *db-depend
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.111.28}
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.111.25}
container_name: signoz-otel-collector
command:
- --config=/etc/otel-collector-config.yaml
@@ -260,7 +260,7 @@ services:
condition: service_healthy
schema-migrator-sync:
!!merge <<: *common
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.111.28}
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.111.24}
container_name: schema-migrator-sync
command:
- sync
@@ -271,7 +271,7 @@ services:
condition: service_healthy
schema-migrator-async:
!!merge <<: *db-depend
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.111.28}
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.111.24}
container_name: schema-migrator-async
command:
- async

View File

@@ -121,7 +121,7 @@ services:
condition: service_healthy
query-service:
!!merge <<: *db-depend
image: signoz/query-service:${DOCKER_TAG:-0.72.0}
image: signoz/query-service:${DOCKER_TAG:-0.70.1}
container_name: signoz-query-service
command:
- --config=/root/config/prometheus.yml
@@ -157,7 +157,7 @@ services:
retries: 3
frontend:
!!merge <<: *common
image: signoz/frontend:${DOCKER_TAG:-0.72.0}
image: signoz/frontend:${DOCKER_TAG:-0.70.1}
container_name: signoz-frontend
depends_on:
- alertmanager
@@ -168,7 +168,7 @@ services:
- ../common/signoz/nginx-config.conf:/etc/nginx/conf.d/default.conf
otel-collector:
!!merge <<: *db-depend
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.111.27}
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.111.25}
container_name: signoz-otel-collector
command:
- --config=/etc/otel-collector-config.yaml

View File

@@ -121,7 +121,7 @@ services:
condition: service_healthy
query-service:
!!merge <<: *db-depend
image: signoz/query-service:${DOCKER_TAG:-0.72.0}
image: signoz/query-service:${DOCKER_TAG:-0.70.1}
container_name: signoz-query-service
command:
- --config=/root/config/prometheus.yml
@@ -155,7 +155,7 @@ services:
retries: 3
frontend:
!!merge <<: *common
image: signoz/frontend:${DOCKER_TAG:-0.72.0}
image: signoz/frontend:${DOCKER_TAG:-0.70.1}
container_name: signoz-frontend
depends_on:
- alertmanager
@@ -166,7 +166,7 @@ services:
- ../common/signoz/nginx-config.conf:/etc/nginx/conf.d/default.conf
otel-collector:
!!merge <<: *db-depend
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.111.28}
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.111.25}
container_name: signoz-otel-collector
command:
- --config=/etc/otel-collector-config.yaml
@@ -188,7 +188,7 @@ services:
condition: service_healthy
schema-migrator-sync:
!!merge <<: *common
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.111.28}
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.111.24}
container_name: schema-migrator-sync
command:
- sync
@@ -199,7 +199,7 @@ services:
condition: service_healthy
schema-migrator-async:
!!merge <<: *db-depend
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.111.28}
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.111.24}
container_name: schema-migrator-async
command:
- async

View File

@@ -66,7 +66,7 @@ exporters:
enabled: true
clickhousemetricswrite/prometheus:
endpoint: tcp://clickhouse:9000/signoz_metrics
signozclickhousemetrics:
clickhousemetricswritev2:
dsn: tcp://clickhouse:9000/signoz_metrics
clickhouselogsexporter:
dsn: tcp://clickhouse:9000/signoz_logs
@@ -90,11 +90,11 @@ service:
metrics:
receivers: [otlp]
processors: [batch]
exporters: [clickhousemetricswrite, signozclickhousemetrics]
exporters: [clickhousemetricswrite, clickhousemetricswritev2]
metrics/prometheus:
receivers: [prometheus]
processors: [batch]
exporters: [clickhousemetricswrite/prometheus, signozclickhousemetrics]
exporters: [clickhousemetricswrite/prometheus, clickhousemetricswritev2]
logs:
receivers: [otlp]
processors: [batch]

View File

@@ -1,36 +0,0 @@
package middleware
import (
"net/http"
"go.signoz.io/signoz/pkg/types/authtypes"
)
type Pat struct {
uuid *authtypes.UUID
headers []string
}
func NewPat(headers []string) *Pat {
return &Pat{uuid: authtypes.NewUUID(), headers: headers}
}
func (p *Pat) Wrap(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var values []string
for _, header := range p.headers {
values = append(values, r.Header.Get(header))
}
ctx, err := p.uuid.ContextFromRequest(r.Context(), values...)
if err != nil {
next.ServeHTTP(w, r)
return
}
r = r.WithContext(ctx)
next.ServeHTTP(w, r)
})
}

View File

@@ -20,7 +20,6 @@ import (
basemodel "go.signoz.io/signoz/pkg/query-service/model"
rules "go.signoz.io/signoz/pkg/query-service/rules"
"go.signoz.io/signoz/pkg/query-service/version"
"go.signoz.io/signoz/pkg/types/authtypes"
)
type APIHandlerOptions struct {
@@ -42,7 +41,6 @@ type APIHandlerOptions struct {
FluxInterval time.Duration
UseLogsNewSchema bool
UseTraceNewSchema bool
JWT *authtypes.JWT
}
type APIHandler struct {

View File

@@ -50,7 +50,7 @@ func (ah *APIHandler) loginUser(w http.ResponseWriter, r *http.Request) {
}
// if all looks good, call auth
resp, err := baseauth.Login(ctx, &req, ah.opts.JWT)
resp, err := baseauth.Login(ctx, &req)
if ah.HandleError(w, err, http.StatusUnauthorized) {
return
}
@@ -253,7 +253,7 @@ func (ah *APIHandler) receiveGoogleAuth(w http.ResponseWriter, r *http.Request)
return
}
nextPage, err := ah.AppDao().PrepareSsoRedirect(ctx, redirectUri, identity.Email, ah.opts.JWT)
nextPage, err := ah.AppDao().PrepareSsoRedirect(ctx, redirectUri, identity.Email)
if err != nil {
zap.L().Error("[receiveGoogleAuth] failed to generate redirect URI after successful login ", zap.String("domain", domain.String()), zap.Error(err))
handleSsoError(w, r, redirectUri)
@@ -331,7 +331,7 @@ func (ah *APIHandler) receiveSAML(w http.ResponseWriter, r *http.Request) {
return
}
nextPage, err := ah.AppDao().PrepareSsoRedirect(ctx, redirectUri, email, ah.opts.JWT)
nextPage, err := ah.AppDao().PrepareSsoRedirect(ctx, redirectUri, email)
if err != nil {
zap.L().Error("[receiveSAML] failed to generate redirect URI after successful login ", zap.String("domain", domain.String()), zap.Error(err))
handleSsoError(w, r, redirectUri)

View File

@@ -37,7 +37,7 @@ func (ah *APIHandler) CloudIntegrationsGenerateConnectionParams(w http.ResponseW
return
}
currentUser, err := auth.GetUserFromReqContext(r.Context())
currentUser, err := auth.GetUserFromRequest(r)
if err != nil {
RespondError(w, basemodel.UnauthorizedError(fmt.Errorf(
"couldn't deduce current user: %w", err,
@@ -183,7 +183,7 @@ func (ah *APIHandler) getOrCreateCloudIntegrationUser(
if apiErr != nil {
return nil, basemodel.WrapApiError(apiErr, "couldn't get viewer group for creating integration user")
}
newUser.GroupId = viewerGroup.ID
newUser.GroupId = viewerGroup.Id
passwordHash, err := auth.PasswordHash(uuid.NewString())
if err != nil {

View File

@@ -34,7 +34,7 @@ func (ah *APIHandler) createPAT(w http.ResponseWriter, r *http.Request) {
RespondError(w, model.BadRequest(err), nil)
return
}
user, err := auth.GetUserFromReqContext(r.Context())
user, err := auth.GetUserFromRequest(r)
if err != nil {
RespondError(w, &model.ApiError{
Typ: model.ErrorUnauthorized,
@@ -97,7 +97,7 @@ func (ah *APIHandler) updatePAT(w http.ResponseWriter, r *http.Request) {
return
}
user, err := auth.GetUserFromReqContext(r.Context())
user, err := auth.GetUserFromRequest(r)
if err != nil {
RespondError(w, &model.ApiError{
Typ: model.ErrorUnauthorized,
@@ -127,7 +127,7 @@ func (ah *APIHandler) updatePAT(w http.ResponseWriter, r *http.Request) {
func (ah *APIHandler) getPATs(w http.ResponseWriter, r *http.Request) {
ctx := context.Background()
user, err := auth.GetUserFromReqContext(r.Context())
user, err := auth.GetUserFromRequest(r)
if err != nil {
RespondError(w, &model.ApiError{
Typ: model.ErrorUnauthorized,
@@ -147,7 +147,7 @@ func (ah *APIHandler) getPATs(w http.ResponseWriter, r *http.Request) {
func (ah *APIHandler) revokePAT(w http.ResponseWriter, r *http.Request) {
ctx := context.Background()
id := mux.Vars(r)["id"]
user, err := auth.GetUserFromReqContext(r.Context())
user, err := auth.GetUserFromRequest(r)
if err != nil {
RespondError(w, &model.ApiError{
Typ: model.ErrorUnauthorized,

View File

@@ -1,20 +1,26 @@
package app
import (
"bufio"
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net"
"net/http"
_ "net/http/pprof" // http profiler
"os"
"regexp"
"time"
"github.com/gorilla/handlers"
"github.com/gorilla/mux"
"github.com/jmoiron/sqlx"
"github.com/rs/cors"
"github.com/soheilhy/cmux"
eemiddleware "go.signoz.io/signoz/ee/http/middleware"
"go.signoz.io/signoz/ee/query-service/app/api"
"go.signoz.io/signoz/ee/query-service/app/db"
"go.signoz.io/signoz/ee/query-service/auth"
@@ -24,8 +30,9 @@ import (
"go.signoz.io/signoz/ee/query-service/interfaces"
"go.signoz.io/signoz/ee/query-service/rules"
"go.signoz.io/signoz/pkg/http/middleware"
baseauth "go.signoz.io/signoz/pkg/query-service/auth"
v3 "go.signoz.io/signoz/pkg/query-service/model/v3"
"go.signoz.io/signoz/pkg/signoz"
"go.signoz.io/signoz/pkg/types/authtypes"
"go.signoz.io/signoz/pkg/web"
licensepkg "go.signoz.io/signoz/ee/query-service/license"
@@ -74,7 +81,6 @@ type ServerOptions struct {
GatewayUrl string
UseLogsNewSchema bool
UseTraceNewSchema bool
Jwt *authtypes.JWT
}
// Server runs HTTP api service
@@ -105,7 +111,7 @@ func (s Server) HealthCheckStatus() chan healthcheck.Status {
// NewServer creates and initializes Server
func NewServer(serverOptions *ServerOptions) (*Server, error) {
modelDao, err := dao.InitDao(serverOptions.SigNoz.SQLStore)
modelDao, err := dao.InitDao(serverOptions.SigNoz.SQLStore.SQLxDB())
if err != nil {
return nil, err
}
@@ -128,7 +134,7 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) {
}
// initiate license manager
lm, err := licensepkg.StartManager(serverOptions.SigNoz.SQLStore.SQLxDB(), serverOptions.SigNoz.SQLStore.BunDB())
lm, err := licensepkg.StartManager(serverOptions.SigNoz.SQLStore.SQLxDB())
if err != nil {
return nil, err
}
@@ -143,20 +149,25 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) {
}
var reader interfaces.DataConnector
qb := db.NewDataConnector(
serverOptions.SigNoz.SQLStore.SQLxDB(),
serverOptions.SigNoz.TelemetryStore.ClickHouseDB(),
serverOptions.PromConfigPath,
lm,
serverOptions.Cluster,
serverOptions.UseLogsNewSchema,
serverOptions.UseTraceNewSchema,
fluxIntervalForTraceDetail,
serverOptions.SigNoz.Cache,
)
go qb.Start(readerReady)
reader = qb
storage := os.Getenv("STORAGE")
if storage == "clickhouse" {
zap.L().Info("Using ClickHouse as datastore ...")
qb := db.NewDataConnector(
serverOptions.SigNoz.SQLStore.SQLxDB(),
serverOptions.SigNoz.TelemetryStore.ClickHouseDB(),
serverOptions.PromConfigPath,
lm,
serverOptions.Cluster,
serverOptions.UseLogsNewSchema,
serverOptions.UseTraceNewSchema,
fluxIntervalForTraceDetail,
serverOptions.SigNoz.Cache,
)
go qb.Start(readerReady)
reader = qb
} else {
return nil, fmt.Errorf("storage type: %s is not supported in query service", storage)
}
skipConfig := &basemodel.SkipConfig{}
if serverOptions.SkipTopLvlOpsPath != "" {
// read skip config
@@ -197,14 +208,14 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) {
return nil, err
}
integrationsController, err := integrations.NewController(serverOptions.SigNoz.SQLStore)
integrationsController, err := integrations.NewController(serverOptions.SigNoz.SQLStore.SQLxDB())
if err != nil {
return nil, fmt.Errorf(
"couldn't create integrations controller: %w", err,
)
}
cloudIntegrationsController, err := cloudintegrations.NewController(serverOptions.SigNoz.SQLStore)
cloudIntegrationsController, err := cloudintegrations.NewController(serverOptions.SigNoz.SQLStore.SQLxDB())
if err != nil {
return nil, fmt.Errorf(
"couldn't create cloud provider integrations controller: %w", err,
@@ -264,7 +275,6 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) {
GatewayUrl: serverOptions.GatewayUrl,
UseLogsNewSchema: serverOptions.UseLogsNewSchema,
UseTraceNewSchema: serverOptions.UseTraceNewSchema,
JWT: serverOptions.Jwt,
}
apiHandler, err := api.NewAPIHandler(apiOpts)
@@ -307,8 +317,6 @@ func (s *Server) createPrivateServer(apiHandler *api.APIHandler) (*http.Server,
r := baseapp.NewRouter()
r.Use(middleware.NewAuth(zap.L(), s.serverOptions.Jwt, []string{"Authorization", "Sec-WebSocket-Protocol"}).Wrap)
r.Use(eemiddleware.NewPat([]string{"SIGNOZ-API-KEY"}).Wrap)
r.Use(middleware.NewTimeout(zap.L(),
s.serverOptions.Config.APIServer.Timeout.ExcludedRoutes,
s.serverOptions.Config.APIServer.Timeout.Default,
@@ -340,8 +348,8 @@ func (s *Server) createPublicServer(apiHandler *api.APIHandler, web web.Web) (*h
r := baseapp.NewRouter()
// add auth middleware
getUserFromRequest := func(ctx context.Context) (*basemodel.UserPayload, error) {
user, err := auth.GetUserFromRequestContext(ctx, apiHandler)
getUserFromRequest := func(r *http.Request) (*basemodel.UserPayload, error) {
user, err := auth.GetUserFromRequest(r, apiHandler)
if err != nil {
return nil, err
@@ -355,8 +363,6 @@ func (s *Server) createPublicServer(apiHandler *api.APIHandler, web web.Web) (*h
}
am := baseapp.NewAuthMiddleware(getUserFromRequest)
r.Use(middleware.NewAuth(zap.L(), s.serverOptions.Jwt, []string{"Authorization", "Sec-WebSocket-Protocol"}).Wrap)
r.Use(eemiddleware.NewPat([]string{"SIGNOZ-API-KEY"}).Wrap)
r.Use(middleware.NewTimeout(zap.L(),
s.serverOptions.Config.APIServer.Timeout.ExcludedRoutes,
s.serverOptions.Config.APIServer.Timeout.Default,
@@ -395,6 +401,174 @@ func (s *Server) createPublicServer(apiHandler *api.APIHandler, web web.Web) (*h
}, nil
}
// TODO(remove): Implemented at pkg/http/middleware/logging.go
type loggingResponseWriter struct {
http.ResponseWriter
statusCode int
}
// TODO(remove): Implemented at pkg/http/middleware/logging.go
func NewLoggingResponseWriter(w http.ResponseWriter) *loggingResponseWriter {
// WriteHeader(int) is not called if our response implicitly returns 200 OK, so
// we default to that status code.
return &loggingResponseWriter{w, http.StatusOK}
}
// TODO(remove): Implemented at pkg/http/middleware/logging.go
func (lrw *loggingResponseWriter) WriteHeader(code int) {
lrw.statusCode = code
lrw.ResponseWriter.WriteHeader(code)
}
// TODO(remove): Implemented at pkg/http/middleware/logging.go
// Flush implements the http.Flush interface.
func (lrw *loggingResponseWriter) Flush() {
lrw.ResponseWriter.(http.Flusher).Flush()
}
// TODO(remove): Implemented at pkg/http/middleware/logging.go
// Support websockets
func (lrw *loggingResponseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) {
h, ok := lrw.ResponseWriter.(http.Hijacker)
if !ok {
return nil, nil, errors.New("hijack not supported")
}
return h.Hijack()
}
func extractQueryRangeData(path string, r *http.Request) (map[string]interface{}, bool) {
pathToExtractBodyFromV3 := "/api/v3/query_range"
pathToExtractBodyFromV4 := "/api/v4/query_range"
data := map[string]interface{}{}
var postData *v3.QueryRangeParamsV3
if (r.Method == "POST") && ((path == pathToExtractBodyFromV3) || (path == pathToExtractBodyFromV4)) {
if r.Body != nil {
bodyBytes, err := io.ReadAll(r.Body)
if err != nil {
return nil, false
}
r.Body.Close() // must close
r.Body = io.NopCloser(bytes.NewBuffer(bodyBytes))
json.Unmarshal(bodyBytes, &postData)
} else {
return nil, false
}
} else {
return nil, false
}
referrer := r.Header.Get("Referer")
dashboardMatched, err := regexp.MatchString(`/dashboard/[a-zA-Z0-9\-]+/(new|edit)(?:\?.*)?$`, referrer)
if err != nil {
zap.L().Error("error while matching the referrer", zap.Error(err))
}
alertMatched, err := regexp.MatchString(`/alerts/(new|edit)(?:\?.*)?$`, referrer)
if err != nil {
zap.L().Error("error while matching the alert: ", zap.Error(err))
}
logsExplorerMatched, err := regexp.MatchString(`/logs/logs-explorer(?:\?.*)?$`, referrer)
if err != nil {
zap.L().Error("error while matching the logs explorer: ", zap.Error(err))
}
traceExplorerMatched, err := regexp.MatchString(`/traces-explorer(?:\?.*)?$`, referrer)
if err != nil {
zap.L().Error("error while matching the trace explorer: ", zap.Error(err))
}
queryInfoResult := telemetry.GetInstance().CheckQueryInfo(postData)
if (queryInfoResult.MetricsUsed || queryInfoResult.LogsUsed || queryInfoResult.TracesUsed) && (queryInfoResult.FilterApplied) {
if queryInfoResult.MetricsUsed {
telemetry.GetInstance().AddActiveMetricsUser()
}
if queryInfoResult.LogsUsed {
telemetry.GetInstance().AddActiveLogsUser()
}
if queryInfoResult.TracesUsed {
telemetry.GetInstance().AddActiveTracesUser()
}
data["metricsUsed"] = queryInfoResult.MetricsUsed
data["logsUsed"] = queryInfoResult.LogsUsed
data["tracesUsed"] = queryInfoResult.TracesUsed
data["filterApplied"] = queryInfoResult.FilterApplied
data["groupByApplied"] = queryInfoResult.GroupByApplied
data["aggregateOperator"] = queryInfoResult.AggregateOperator
data["aggregateAttributeKey"] = queryInfoResult.AggregateAttributeKey
data["numberOfQueries"] = queryInfoResult.NumberOfQueries
data["queryType"] = queryInfoResult.QueryType
data["panelType"] = queryInfoResult.PanelType
userEmail, err := baseauth.GetEmailFromJwt(r.Context())
if err == nil {
// switch case to set data["screen"] based on the referrer
switch {
case dashboardMatched:
data["screen"] = "panel"
case alertMatched:
data["screen"] = "alert"
case logsExplorerMatched:
data["screen"] = "logs-explorer"
case traceExplorerMatched:
data["screen"] = "traces-explorer"
default:
data["screen"] = "unknown"
return data, true
}
telemetry.GetInstance().SendEvent(telemetry.TELEMETRY_EVENT_QUERY_RANGE_API, data, userEmail, true, false)
}
}
return data, true
}
func getActiveLogs(path string, r *http.Request) {
// if path == "/api/v1/dashboards/{uuid}" {
// telemetry.GetInstance().AddActiveMetricsUser()
// }
if path == "/api/v1/logs" {
hasFilters := len(r.URL.Query().Get("q"))
if hasFilters > 0 {
telemetry.GetInstance().AddActiveLogsUser()
}
}
}
func (s *Server) analyticsMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := baseauth.AttachJwtToContext(r.Context(), r)
r = r.WithContext(ctx)
route := mux.CurrentRoute(r)
path, _ := route.GetPathTemplate()
queryRangeData, metadataExists := extractQueryRangeData(path, r)
getActiveLogs(path, r)
lrw := NewLoggingResponseWriter(w)
next.ServeHTTP(lrw, r)
data := map[string]interface{}{"path": path, "statusCode": lrw.statusCode}
if metadataExists {
for key, value := range queryRangeData {
data[key] = value
}
}
if _, ok := telemetry.EnabledPaths()[path]; ok {
userEmail, err := baseauth.GetEmailFromJwt(r.Context())
if err == nil {
telemetry.GetInstance().SendEvent(telemetry.TELEMETRY_EVENT_PATH, data, userEmail, true, false)
}
}
})
}
// initListeners initialises listeners of the server
func (s *Server) initListeners() error {
// listen on public port

View File

@@ -3,20 +3,20 @@ package auth
import (
"context"
"fmt"
"net/http"
"time"
"go.signoz.io/signoz/ee/query-service/app/api"
baseauth "go.signoz.io/signoz/pkg/query-service/auth"
basemodel "go.signoz.io/signoz/pkg/query-service/model"
"go.signoz.io/signoz/pkg/query-service/telemetry"
"go.signoz.io/signoz/pkg/types/authtypes"
"go.uber.org/zap"
)
func GetUserFromRequestContext(ctx context.Context, apiHandler *api.APIHandler) (*basemodel.UserPayload, error) {
patToken, ok := authtypes.UUIDFromContext(ctx)
if ok && patToken != "" {
func GetUserFromRequest(r *http.Request, apiHandler *api.APIHandler) (*basemodel.UserPayload, error) {
patToken := r.Header.Get("SIGNOZ-API-KEY")
if len(patToken) > 0 {
zap.L().Debug("Received a non-zero length PAT token")
ctx := context.Background()
dao := apiHandler.AppDao()
@@ -40,7 +40,7 @@ func GetUserFromRequestContext(ctx context.Context, apiHandler *api.APIHandler)
}
telemetry.GetInstance().SetPatTokenUser()
dao.UpdatePATLastUsed(ctx, patToken, time.Now().Unix())
user.User.GroupId = group.ID
user.User.GroupId = group.Id
user.User.Id = pat.Id
return &basemodel.UserPayload{
User: user.User,
@@ -52,5 +52,5 @@ func GetUserFromRequestContext(ctx context.Context, apiHandler *api.APIHandler)
return nil, err
}
}
return baseauth.GetUserFromReqContext(ctx)
return baseauth.GetUserFromRequest(r)
}

View File

@@ -1,10 +1,10 @@
package dao
import (
"github.com/jmoiron/sqlx"
"go.signoz.io/signoz/ee/query-service/dao/sqlite"
"go.signoz.io/signoz/pkg/sqlstore"
)
func InitDao(sqlStore sqlstore.SQLStore) (ModelDao, error) {
return sqlite.InitDB(sqlStore)
func InitDao(inputDB *sqlx.DB) (ModelDao, error) {
return sqlite.InitDB(inputDB)
}

View File

@@ -10,7 +10,6 @@ import (
basedao "go.signoz.io/signoz/pkg/query-service/dao"
baseint "go.signoz.io/signoz/pkg/query-service/interfaces"
basemodel "go.signoz.io/signoz/pkg/query-service/model"
"go.signoz.io/signoz/pkg/types/authtypes"
)
type ModelDao interface {
@@ -23,7 +22,7 @@ type ModelDao interface {
// auth methods
CanUsePassword(ctx context.Context, email string) (bool, basemodel.BaseApiError)
PrepareSsoRedirect(ctx context.Context, redirectUri, email string, jwt *authtypes.JWT) (redirectURL string, apierr basemodel.BaseApiError)
PrepareSsoRedirect(ctx context.Context, redirectUri, email string) (redirectURL string, apierr basemodel.BaseApiError)
GetDomainFromSsoResponse(ctx context.Context, relayState *url.URL) (*model.OrgDomain, error)
// org domain (auth domains) CRUD ops

View File

@@ -14,7 +14,6 @@ import (
baseconst "go.signoz.io/signoz/pkg/query-service/constants"
basemodel "go.signoz.io/signoz/pkg/query-service/model"
"go.signoz.io/signoz/pkg/query-service/utils"
"go.signoz.io/signoz/pkg/types/authtypes"
"go.uber.org/zap"
)
@@ -49,7 +48,7 @@ func (m *modelDao) createUserForSAMLRequest(ctx context.Context, email string) (
Password: hash,
CreatedAt: time.Now().Unix(),
ProfilePictureURL: "", // Currently unused
GroupId: group.ID,
GroupId: group.Id,
OrgId: domain.OrgId,
}
@@ -65,7 +64,7 @@ func (m *modelDao) createUserForSAMLRequest(ctx context.Context, email string) (
// PrepareSsoRedirect prepares redirect page link after SSO response
// is successfully parsed (i.e. valid email is available)
func (m *modelDao) PrepareSsoRedirect(ctx context.Context, redirectUri, email string, jwt *authtypes.JWT) (redirectURL string, apierr basemodel.BaseApiError) {
func (m *modelDao) PrepareSsoRedirect(ctx context.Context, redirectUri, email string) (redirectURL string, apierr basemodel.BaseApiError) {
userPayload, apierr := m.GetUserByEmail(ctx, email)
if !apierr.IsNil() {
@@ -86,7 +85,7 @@ func (m *modelDao) PrepareSsoRedirect(ctx context.Context, redirectUri, email st
user = &userPayload.User
}
tokenStore, err := baseauth.GenerateJWTForUser(user, jwt)
tokenStore, err := baseauth.GenerateJWTForUser(user)
if err != nil {
zap.L().Error("failed to generate token for SSO login user", zap.Error(err))
return "", model.InternalErrorStr("failed to generate token for the user")

View File

@@ -7,7 +7,7 @@ import (
basedao "go.signoz.io/signoz/pkg/query-service/dao"
basedsql "go.signoz.io/signoz/pkg/query-service/dao/sqlite"
baseint "go.signoz.io/signoz/pkg/query-service/interfaces"
"go.signoz.io/signoz/pkg/sqlstore"
"go.uber.org/zap"
)
type modelDao struct {
@@ -29,15 +29,113 @@ func (m *modelDao) checkFeature(key string) error {
return m.flags.CheckFeature(key)
}
func columnExists(db *sqlx.DB, tableName, columnName string) bool {
query := fmt.Sprintf("PRAGMA table_info(%s);", tableName)
rows, err := db.Query(query)
if err != nil {
zap.L().Error("Failed to query table info", zap.Error(err))
return false
}
defer rows.Close()
var (
cid int
name string
ctype string
notnull int
dflt_value *string
pk int
)
for rows.Next() {
err := rows.Scan(&cid, &name, &ctype, &notnull, &dflt_value, &pk)
if err != nil {
zap.L().Error("Failed to scan table info", zap.Error(err))
return false
}
if name == columnName {
return true
}
}
err = rows.Err()
if err != nil {
zap.L().Error("Failed to scan table info", zap.Error(err))
return false
}
return false
}
// InitDB creates and extends base model DB repository
func InitDB(sqlStore sqlstore.SQLStore) (*modelDao, error) {
dao, err := basedsql.InitDB(sqlStore)
func InitDB(inputDB *sqlx.DB) (*modelDao, error) {
dao, err := basedsql.InitDB(inputDB)
if err != nil {
return nil, err
}
// set package variable so dependent base methods (e.g. AuthCache) will work
basedao.SetDB(dao)
m := &modelDao{ModelDaoSqlite: dao}
table_schema := `
PRAGMA foreign_keys = ON;
CREATE TABLE IF NOT EXISTS org_domains(
id TEXT PRIMARY KEY,
org_id TEXT NOT NULL,
name VARCHAR(50) NOT NULL UNIQUE,
created_at INTEGER NOT NULL,
updated_at INTEGER,
data TEXT NOT NULL,
FOREIGN KEY(org_id) REFERENCES organizations(id)
);
CREATE TABLE IF NOT EXISTS personal_access_tokens (
id INTEGER PRIMARY KEY AUTOINCREMENT,
role TEXT NOT NULL,
user_id TEXT NOT NULL,
token TEXT NOT NULL UNIQUE,
name TEXT NOT NULL,
created_at INTEGER NOT NULL,
expires_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL,
last_used INTEGER NOT NULL,
revoked BOOLEAN NOT NULL,
updated_by_user_id TEXT NOT NULL,
FOREIGN KEY(user_id) REFERENCES users(id)
);
`
_, err = m.DB().Exec(table_schema)
if err != nil {
return nil, fmt.Errorf("error in creating tables: %v", err.Error())
}
if !columnExists(m.DB(), "personal_access_tokens", "role") {
_, err = m.DB().Exec("ALTER TABLE personal_access_tokens ADD COLUMN role TEXT NOT NULL DEFAULT 'ADMIN';")
if err != nil {
return nil, fmt.Errorf("error in adding column: %v", err.Error())
}
}
if !columnExists(m.DB(), "personal_access_tokens", "updated_at") {
_, err = m.DB().Exec("ALTER TABLE personal_access_tokens ADD COLUMN updated_at INTEGER NOT NULL DEFAULT 0;")
if err != nil {
return nil, fmt.Errorf("error in adding column: %v", err.Error())
}
}
if !columnExists(m.DB(), "personal_access_tokens", "last_used") {
_, err = m.DB().Exec("ALTER TABLE personal_access_tokens ADD COLUMN last_used INTEGER NOT NULL DEFAULT 0;")
if err != nil {
return nil, fmt.Errorf("error in adding column: %v", err.Error())
}
}
if !columnExists(m.DB(), "personal_access_tokens", "revoked") {
_, err = m.DB().Exec("ALTER TABLE personal_access_tokens ADD COLUMN revoked BOOLEAN NOT NULL DEFAULT FALSE;")
if err != nil {
return nil, fmt.Errorf("error in adding column: %v", err.Error())
}
}
if !columnExists(m.DB(), "personal_access_tokens", "updated_by_user_id") {
_, err = m.DB().Exec("ALTER TABLE personal_access_tokens ADD COLUMN updated_by_user_id TEXT NOT NULL DEFAULT '';")
if err != nil {
return nil, fmt.Errorf("error in adding column: %v", err.Error())
}
}
return m, nil
}

View File

@@ -9,28 +9,29 @@ import (
"github.com/jmoiron/sqlx"
"github.com/mattn/go-sqlite3"
"github.com/uptrace/bun"
"go.signoz.io/signoz/ee/query-service/license/sqlite"
"go.signoz.io/signoz/ee/query-service/model"
basemodel "go.signoz.io/signoz/pkg/query-service/model"
"go.signoz.io/signoz/pkg/types"
"go.uber.org/zap"
)
// Repo is license repo. stores license keys in a secured DB
type Repo struct {
db *sqlx.DB
bundb *bun.DB
db *sqlx.DB
}
// NewLicenseRepo initiates a new license repo
func NewLicenseRepo(db *sqlx.DB, bundb *bun.DB) Repo {
func NewLicenseRepo(db *sqlx.DB) Repo {
return Repo{
db: db,
bundb: bundb,
db: db,
}
}
func (r *Repo) InitDB(inputDB *sqlx.DB) error {
return sqlite.InitDB(inputDB)
}
func (r *Repo) GetLicensesV3(ctx context.Context) ([]*model.LicenseV3, error) {
licensesData := []model.LicenseDB{}
licenseV3Data := []*model.LicenseV3{}
@@ -169,25 +170,24 @@ func (r *Repo) UpdateLicenseV3(ctx context.Context, l *model.LicenseV3) error {
return nil
}
func (r *Repo) CreateFeature(req *types.FeatureStatus) *basemodel.ApiError {
func (r *Repo) CreateFeature(req *basemodel.Feature) *basemodel.ApiError {
_, err := r.bundb.NewInsert().
Model(req).
Exec(context.Background())
_, err := r.db.Exec(
`INSERT INTO feature_status (name, active, usage, usage_limit, route)
VALUES (?, ?, ?, ?, ?);`,
req.Name, req.Active, req.Usage, req.UsageLimit, req.Route)
if err != nil {
return &basemodel.ApiError{Typ: basemodel.ErrorInternal, Err: err}
}
return nil
}
func (r *Repo) GetFeature(featureName string) (types.FeatureStatus, error) {
var feature types.FeatureStatus
func (r *Repo) GetFeature(featureName string) (basemodel.Feature, error) {
err := r.bundb.NewSelect().
Model(&feature).
Where("name = ?", featureName).
Scan(context.Background())
var feature basemodel.Feature
err := r.db.Get(&feature,
`SELECT * FROM feature_status WHERE name = ?;`, featureName)
if err != nil {
return feature, err
}
@@ -210,19 +210,18 @@ func (r *Repo) GetAllFeatures() ([]basemodel.Feature, error) {
return feature, nil
}
func (r *Repo) UpdateFeature(req types.FeatureStatus) error {
func (r *Repo) UpdateFeature(req basemodel.Feature) error {
_, err := r.bundb.NewUpdate().
Model(&req).
Where("name = ?", req.Name).
Exec(context.Background())
_, err := r.db.Exec(
`UPDATE feature_status SET active = ?, usage = ?, usage_limit = ?, route = ? WHERE name = ?;`,
req.Active, req.Usage, req.UsageLimit, req.Route, req.Name)
if err != nil {
return err
}
return nil
}
func (r *Repo) InitFeatures(req []types.FeatureStatus) error {
func (r *Repo) InitFeatures(req basemodel.FeatureSet) error {
// get a feature by name, if it doesn't exist, create it. If it does exist, update it.
for _, feature := range req {
currentFeature, err := r.GetFeature(feature.Name)
@@ -235,7 +234,7 @@ func (r *Repo) InitFeatures(req []types.FeatureStatus) error {
} else if err != nil {
return err
}
feature.Usage = int(currentFeature.Usage)
feature.Usage = currentFeature.Usage
if feature.Usage >= feature.UsageLimit && feature.UsageLimit != -1 {
feature.Active = false
}

View File

@@ -2,18 +2,17 @@ package license
import (
"context"
"fmt"
"sync/atomic"
"time"
"github.com/jmoiron/sqlx"
"github.com/pkg/errors"
"github.com/uptrace/bun"
"sync"
"go.signoz.io/signoz/pkg/query-service/auth"
baseconstants "go.signoz.io/signoz/pkg/query-service/constants"
"go.signoz.io/signoz/pkg/types"
"go.signoz.io/signoz/pkg/types/authtypes"
validate "go.signoz.io/signoz/ee/query-service/integrations/signozio"
"go.signoz.io/signoz/ee/query-service/model"
@@ -45,12 +44,17 @@ type Manager struct {
activeFeatures basemodel.FeatureSet
}
func StartManager(db *sqlx.DB, bundb *bun.DB, features ...basemodel.Feature) (*Manager, error) {
func StartManager(db *sqlx.DB, features ...basemodel.Feature) (*Manager, error) {
if LM != nil {
return LM, nil
}
repo := NewLicenseRepo(db, bundb)
repo := NewLicenseRepo(db)
err := repo.InitDB(db)
if err != nil {
return nil, fmt.Errorf("failed to initiate license repo: %v", err)
}
m := &Manager{
repo: &repo,
}
@@ -239,10 +243,10 @@ func (lm *Manager) ValidateV3(ctx context.Context) (reterr error) {
func (lm *Manager) ActivateV3(ctx context.Context, licenseKey string) (licenseResponse *model.LicenseV3, errResponse *model.ApiError) {
defer func() {
if errResponse != nil {
claims, ok := authtypes.ClaimsFromContext(ctx)
if ok {
userEmail, err := auth.GetEmailFromJwt(ctx)
if err == nil {
telemetry.GetInstance().SendEvent(telemetry.TELEMETRY_LICENSE_ACT_FAILED,
map[string]interface{}{"err": errResponse.Err.Error()}, claims.Email, true, false)
map[string]interface{}{"err": errResponse.Err.Error()}, userEmail, true, false)
}
}
}()
@@ -284,41 +288,15 @@ func (lm *Manager) GetFeatureFlags() (basemodel.FeatureSet, error) {
}
func (lm *Manager) InitFeatures(features basemodel.FeatureSet) error {
featureStatus := make([]types.FeatureStatus, len(features))
for i, f := range features {
featureStatus[i] = types.FeatureStatus{
Name: f.Name,
Active: f.Active,
Usage: int(f.Usage),
UsageLimit: int(f.UsageLimit),
Route: f.Route,
}
}
return lm.repo.InitFeatures(featureStatus)
return lm.repo.InitFeatures(features)
}
func (lm *Manager) UpdateFeatureFlag(feature basemodel.Feature) error {
return lm.repo.UpdateFeature(types.FeatureStatus{
Name: feature.Name,
Active: feature.Active,
Usage: int(feature.Usage),
UsageLimit: int(feature.UsageLimit),
Route: feature.Route,
})
return lm.repo.UpdateFeature(feature)
}
func (lm *Manager) GetFeatureFlag(key string) (basemodel.Feature, error) {
featureStatus, err := lm.repo.GetFeature(key)
if err != nil {
return basemodel.Feature{}, err
}
return basemodel.Feature{
Name: featureStatus.Name,
Active: featureStatus.Active,
Usage: int64(featureStatus.Usage),
UsageLimit: int64(featureStatus.UsageLimit),
Route: featureStatus.Route,
}, nil
return lm.repo.GetFeature(key)
}
// GetRepo return the license repo

View File

@@ -0,0 +1,63 @@
package sqlite
import (
"fmt"
"github.com/jmoiron/sqlx"
)
func InitDB(db *sqlx.DB) error {
var err error
if db == nil {
return fmt.Errorf("invalid db connection")
}
table_schema := `CREATE TABLE IF NOT EXISTS licenses(
key TEXT PRIMARY KEY,
createdAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updatedAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
planDetails TEXT,
activationId TEXT,
validationMessage TEXT,
lastValidated TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS sites(
uuid TEXT PRIMARY KEY,
alias VARCHAR(180) DEFAULT 'PROD',
url VARCHAR(300),
createdAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
`
_, err = db.Exec(table_schema)
if err != nil {
return fmt.Errorf("error in creating licenses table: %s", err.Error())
}
table_schema = `CREATE TABLE IF NOT EXISTS feature_status (
name TEXT PRIMARY KEY,
active bool,
usage INTEGER DEFAULT 0,
usage_limit INTEGER DEFAULT 0,
route TEXT
);`
_, err = db.Exec(table_schema)
if err != nil {
return fmt.Errorf("error in creating feature_status table: %s", err.Error())
}
table_schema = `CREATE TABLE IF NOT EXISTS licenses_v3 (
id TEXT PRIMARY KEY,
key TEXT NOT NULL UNIQUE,
data TEXT
);`
_, err = db.Exec(table_schema)
if err != nil {
return fmt.Errorf("error in creating licenses_v3 table: %s", err.Error())
}
return nil
}

View File

@@ -18,9 +18,9 @@ import (
"go.signoz.io/signoz/pkg/config/fileprovider"
"go.signoz.io/signoz/pkg/query-service/auth"
baseconst "go.signoz.io/signoz/pkg/query-service/constants"
"go.signoz.io/signoz/pkg/query-service/migrate"
"go.signoz.io/signoz/pkg/query-service/version"
"go.signoz.io/signoz/pkg/signoz"
"go.signoz.io/signoz/pkg/types/authtypes"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
@@ -155,16 +155,6 @@ func main() {
zap.L().Fatal("Failed to create signoz struct", zap.Error(err))
}
jwtSecret := os.Getenv("SIGNOZ_JWT_SECRET")
if len(jwtSecret) == 0 {
zap.L().Warn("No JWT secret key is specified.")
} else {
zap.L().Info("JWT secret key set successfully.")
}
jwt := authtypes.NewJWT(jwtSecret, 30*time.Minute, 30*24*time.Hour)
serverOptions := &app.ServerOptions{
Config: config,
SigNoz: signoz,
@@ -182,7 +172,21 @@ func main() {
GatewayUrl: gatewayUrl,
UseLogsNewSchema: useLogsNewSchema,
UseTraceNewSchema: useTraceNewSchema,
Jwt: jwt,
}
// Read the jwt secret key
auth.JwtSecret = os.Getenv("SIGNOZ_JWT_SECRET")
if len(auth.JwtSecret) == 0 {
zap.L().Warn("No JWT secret key is specified.")
} else {
zap.L().Info("JWT secret key set successfully.")
}
if err := migrate.Migrate(signoz.SQLStore.SQLxDB()); err != nil {
zap.L().Error("Failed to migrate", zap.Error(err))
} else {
zap.L().Info("Migration successful")
}
server, err := app.NewServer(serverOptions)

View File

@@ -139,8 +139,8 @@ func NewLicenseV3(data map[string]interface{}) (*LicenseV3, error) {
if err != nil {
return nil, err
}
// if license status is invalid then default it to basic
if status == LicenseStatusInvalid {
// if license status is inactive then default it to basic
if status == LicenseStatusInactive {
planName = PlanNameBasic
}

View File

@@ -21,7 +21,7 @@ var (
)
var (
LicenseStatusInvalid = "INVALID"
LicenseStatusInactive = "INACTIVE"
)
const DisableUpsell = "DISABLE_UPSELL"

View File

@@ -92,7 +92,7 @@
"overlayscrollbars": "^2.8.1",
"overlayscrollbars-react": "^0.5.6",
"papaparse": "5.4.1",
"posthog-js": "1.215.5",
"posthog-js": "1.160.3",
"rc-tween-one": "3.0.6",
"react": "18.2.0",
"react-addons-update": "15.6.3",

View File

@@ -57,8 +57,5 @@
"ALERT_OVERVIEW": "SigNoz | Alert Rule Overview",
"MESSAGING_QUEUES": "SigNoz | Messaging Queues",
"INFRASTRUCTURE_MONITORING_HOSTS": "SigNoz | Infra Monitoring",
"INFRASTRUCTURE_MONITORING_KUBERNETES": "SigNoz | Infra Monitoring",
"METRICS_EXPLORER": "SigNoz | Metrics Explorer",
"METRICS_EXPLORER_EXPLORER": "SigNoz | Metrics Explorer",
"METRICS_EXPLORER_VIEWS": "SigNoz | Metrics Explorer"
"INFRASTRUCTURE_MONITORING_KUBERNETES": "SigNoz | Infra Monitoring"
}

View File

@@ -156,7 +156,7 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element {
const currentRoute = mapRoutes.get('current');
const shouldSuspendWorkspace =
activeLicenseV3.status === LicenseStatus.SUSPENDED &&
activeLicenseV3.state === LicenseState.DEFAULTED;
activeLicenseV3.state === LicenseState.PAYMENT_FAILED;
if (shouldSuspendWorkspace && currentRoute) {
navigateToWorkSpaceSuspended(currentRoute);

View File

@@ -110,18 +110,6 @@ function App(): JSX.Element {
source: 'signoz-ui',
isPaidUser: !!licenses?.trialConvertedToSubscription,
});
if (
window.cioanalytics &&
typeof window.cioanalytics.identify === 'function'
) {
window.cioanalytics.reset();
window.cioanalytics.identify(email, {
name: user.name,
email,
role: user.role,
});
}
}
},
[hostname, isFetchingLicenses, licenses, org],

View File

@@ -264,8 +264,3 @@ export const CeleryOverview = Loadable(
/* webpackChunkName: "CeleryOverview" */ 'pages/Celery/CeleryOverview/CeleryOverview'
),
);
export const MetricsExplorer = Loadable(
() =>
import(/* webpackChunkName: "MetricsExplorer" */ 'pages/MetricsExplorer'),
);

View File

@@ -28,7 +28,6 @@ import {
LogsExplorer,
LogsIndexToFields,
LogsSaveViews,
MetricsExplorer,
MySettings,
NewDashboardPage,
OldLogsExplorer,
@@ -436,27 +435,6 @@ const routes: AppRoutes[] = [
key: 'INFRASTRUCTURE_MONITORING_KUBERNETES',
isPrivate: true,
},
{
path: ROUTES.METRICS_EXPLORER,
exact: true,
component: MetricsExplorer,
key: 'METRICS_EXPLORER',
isPrivate: true,
},
{
path: ROUTES.METRICS_EXPLORER_EXPLORER,
exact: true,
component: MetricsExplorer,
key: 'METRICS_EXPLORER_EXPLORER',
isPrivate: true,
},
{
path: ROUTES.METRICS_EXPLORER_VIEWS,
exact: true,
component: MetricsExplorer,
key: 'METRICS_EXPLORER_VIEWS',
isPrivate: true,
},
];
export const SUPPORT_ROUTE: AppRoutes = {

View File

@@ -1,19 +0,0 @@
import axios from 'api';
import { ErrorResponse, SuccessResponse } from 'types/api';
const removeAwsIntegrationAccount = async (
accountId: string,
): Promise<SuccessResponse<Record<string, never>> | ErrorResponse> => {
const response = await axios.post(
`/cloud-integrations/aws/accounts/${accountId}/disconnect`,
);
return {
statusCode: 200,
error: null,
message: response.data.status,
payload: response.data.data,
};
};
export default removeAwsIntegrationAccount;

View File

@@ -9,22 +9,19 @@ import {
import {
AccountConfigPayload,
AccountConfigResponse,
ConnectionParams,
ConnectionUrlResponse,
} from 'types/api/integrations/aws';
export const getAwsAccounts = async (): Promise<CloudAccount[]> => {
const response = await axios.get('/cloud-integrations/aws/accounts');
return response.data.data.accounts;
return response.data.data;
};
export const getAwsServices = async (
cloudAccountId?: string,
accountId?: string,
): Promise<Service[]> => {
const params = cloudAccountId
? { cloud_account_id: cloudAccountId }
: undefined;
const params = accountId ? { account_id: accountId } : undefined;
const response = await axios.get('/cloud-integrations/aws/services', {
params,
});
@@ -34,11 +31,9 @@ export const getAwsServices = async (
export const getServiceDetails = async (
serviceId: string,
cloudAccountId?: string,
accountId?: string,
): Promise<ServiceData> => {
const params = cloudAccountId
? { cloud_account_id: cloudAccountId }
: undefined;
const params = accountId ? { account_id: accountId } : undefined;
const response = await axios.get(
`/cloud-integrations/aws/services/${serviceId}`,
{ params },
@@ -79,10 +74,3 @@ export const updateServiceConfig = async (
);
return response.data;
};
export const getConnectionParams = async (): Promise<ConnectionParams> => {
const response = await axios.get(
'/cloud-integrations/aws/accounts/generate-connection-params',
);
return response.data.data;
};

View File

@@ -1,6 +1,7 @@
import './CeleryOverviewConfigOptions.styles.scss';
import { Row, Select, Spin } from 'antd';
import { Color } from '@signozhq/design-tokens';
import { Button, Row, Select, Spin, Tooltip } from 'antd';
import {
getValuesFromQueryParams,
setQueryParamsFromOptions,
@@ -9,7 +10,10 @@ import { useCeleryFilterOptions } from 'components/CeleryTask/useCeleryFilterOpt
import { SelectMaxTagPlaceholder } from 'components/MessagingQueues/MQCommon/MQCommon';
import { QueryParams } from 'constants/query';
import useUrlQuery from 'hooks/useUrlQuery';
import { Check, Share2 } from 'lucide-react';
import { useState } from 'react';
import { useHistory, useLocation } from 'react-router-dom';
import { useCopyToClipboard } from 'react-use';
interface SelectOptionConfig {
placeholder: string;
@@ -62,6 +66,10 @@ function FilterSelect({
}
function CeleryOverviewConfigOptions(): JSX.Element {
const [isURLCopied, setIsURLCopied] = useState(false);
const [, handleCopyToClipboard] = useCopyToClipboard();
const selectConfigs: SelectOptionConfig[] = [
{
placeholder: 'Service Name',
@@ -90,6 +98,14 @@ function CeleryOverviewConfigOptions(): JSX.Element {
},
];
const handleShareURL = (): void => {
handleCopyToClipboard(window.location.href);
setIsURLCopied(true);
setTimeout(() => {
setIsURLCopied(false);
}, 1000);
};
return (
<div className="celery-overview-filters">
<Row className="celery-filters">
@@ -102,6 +118,19 @@ function CeleryOverviewConfigOptions(): JSX.Element {
/>
))}
</Row>
<Tooltip title="Share this" arrow={false}>
<Button
className="periscope-btn copy-url-btn"
onClick={handleShareURL}
icon={
isURLCopied ? (
<Check size={14} color={Color.BG_FOREST_500} />
) : (
<Share2 size={14} />
)
}
/>
</Tooltip>
</div>
);
}

View File

@@ -37,6 +37,7 @@
font-weight: 600;
line-height: 18px; /* 163.636% */
letter-spacing: 0.44px;
text-transform: uppercase;
&::before {
background-color: transparent;

View File

@@ -15,7 +15,6 @@ import {
Typography,
} from 'antd';
import { FilterDropdownProps } from 'antd/lib/table/interface';
import logEvent from 'api/common/logEvent';
import {
getQueueOverview,
QueueOverviewResponse,
@@ -219,44 +218,35 @@ function getColumns(data: RowData[]): TableColumnsType<RowData> {
showTitle: false,
},
width: 200,
sorter: (a: RowData, b: RowData): number => {
const aValue = Number(a.error_percentage);
const bValue = Number(b.error_percentage);
return aValue - bValue;
},
sorter: (a: RowData, b: RowData): number =>
String(a.error_percentage).localeCompare(String(b.error_percentage)),
render: ProgressRender,
},
{
title: 'LATENCY (P95) in ms',
title: 'LATENCY (P95)',
dataIndex: 'p95_latency',
key: 'p95_latency',
ellipsis: {
showTitle: false,
},
width: 100,
sorter: (a: RowData, b: RowData): number => {
const aValue = Number(a.p95_latency);
const bValue = Number(b.p95_latency);
return aValue - bValue;
},
sorter: (a: RowData, b: RowData): number =>
String(a.p95_latency).localeCompare(String(b.p95_latency)),
render: (value: number | string): string => {
if (!isNumber(value)) return value.toString();
return (typeof value === 'string' ? parseFloat(value) : value).toFixed(3);
},
},
{
title: 'THROUGHPUT (ops/s)',
title: 'THROUGHPUT',
dataIndex: 'throughput',
key: 'throughput',
ellipsis: {
showTitle: false,
},
width: 100,
sorter: (a: RowData, b: RowData): number => {
const aValue = Number(a.throughput);
const bValue = Number(b.throughput);
return aValue - bValue;
},
sorter: (a: RowData, b: RowData): number =>
String(a.throughput).localeCompare(String(b.throughput)),
render: (value: number | string): string => {
if (!isNumber(value)) return value.toString();
return (typeof value === 'string' ? parseFloat(value) : value).toFixed(3);
@@ -459,7 +449,6 @@ export default function CeleryOverviewTable({
const handleRowClick = (record: RowData): void => {
onRowClick(record);
logEvent('MQ Overview Page: Right Panel', { ...record });
};
const getFilteredData = useCallback(
@@ -483,22 +472,6 @@ export default function CeleryOverviewTable({
tableData,
]);
const prevTableDataRef = useRef<string>();
useEffect(() => {
if (tableData.length > 0) {
const currentTableData = JSON.stringify(tableData);
if (currentTableData !== prevTableDataRef.current) {
logEvent(`MQ Overview Page: List rendered`, {
dataRender: tableData.length,
});
prevTableDataRef.current = currentTableData;
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [JSON.stringify(tableData)]);
return (
<div className="celery-overview-table-container">
<Input.Search

View File

@@ -2,17 +2,24 @@ import './CeleryTaskDetail.style.scss';
import { Color, Spacing } from '@signozhq/design-tokens';
import { Divider, Drawer, Typography } from 'antd';
import logEvent from 'api/common/logEvent';
import { QueryParams } from 'constants/query';
import { PANEL_TYPES } from 'constants/queryBuilder';
import dayjs from 'dayjs';
import { useIsDarkMode } from 'hooks/useDarkMode';
import useUrlQuery from 'hooks/useUrlQuery';
import { X } from 'lucide-react';
import { useState } from 'react';
import { useEffect, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { useHistory, useLocation } from 'react-router-dom';
import { UpdateTimeInterval } from 'store/actions';
import { AppState } from 'store/reducers';
import { Widgets } from 'types/api/dashboard/getAll';
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { GlobalReducer } from 'types/reducer/globalTime';
import { v4 as uuidv4 } from 'uuid';
import CeleryTaskGraph from '../CeleryTaskGraph/CeleryTaskGraph';
import { createFiltersFromData } from '../CeleryUtils';
import { useNavigateToTraces } from '../useNavigateToTraces';
export type CeleryTaskData = {
@@ -32,6 +39,40 @@ export type CeleryTaskDetailProps = {
drawerOpen: boolean;
};
const createFiltersFromData = (
data: Record<string, any>,
): Array<{
id: string;
key: {
key: string;
dataType: DataTypes;
type: string;
isColumn: boolean;
isJSON: boolean;
id: string;
};
op: string;
value: string;
}> => {
const excludeKeys = ['A', 'A_without_unit'];
return Object.entries(data)
.filter(([key]) => !excludeKeys.includes(key))
.map(([key, value]) => ({
id: uuidv4(),
key: {
key,
dataType: DataTypes.String,
type: 'tag',
isColumn: false,
isJSON: false,
id: `${key}--string--tag--false`,
},
op: '=',
value: value.toString(),
}));
};
export default function CeleryTaskDetail({
widgetData,
taskData,
@@ -44,7 +85,7 @@ export default function CeleryTaskDetail({
!!taskData.entity && !!taskData.timeRange[0] && drawerOpen;
const formatTimestamp = (timestamp: number): string =>
dayjs(timestamp).format('DD-MM-YYYY hh:mm A');
dayjs(timestamp * 1000).format('MM-DD-YYYY hh:mm A');
const [totalTask, setTotalTask] = useState(0);
@@ -52,9 +93,52 @@ export default function CeleryTaskDetail({
setTotalTask((graphData?.result?.[0] as any)?.table?.rows.length);
};
// set time range
const { minTime, maxTime, selectedTime } = useSelector<
AppState,
GlobalReducer
>((state) => state.globalTime);
const startTime = taskData.timeRange[0];
const endTime = taskData.timeRange[1];
const urlQuery = useUrlQuery();
const location = useLocation();
const history = useHistory();
const dispatch = useDispatch();
useEffect(() => {
urlQuery.delete(QueryParams.relativeTime);
urlQuery.set(QueryParams.startTime, startTime.toString());
urlQuery.set(QueryParams.endTime, endTime.toString());
const generatedUrl = `${location.pathname}?${urlQuery.toString()}`;
history.replace(generatedUrl);
if (startTime !== endTime) {
dispatch(UpdateTimeInterval('custom', [startTime, endTime]));
}
return (): void => {
urlQuery.delete(QueryParams.relativeTime);
urlQuery.delete(QueryParams.startTime);
urlQuery.delete(QueryParams.endTime);
if (selectedTime !== 'custom') {
dispatch(UpdateTimeInterval(selectedTime));
urlQuery.set(QueryParams.relativeTime, selectedTime);
} else {
dispatch(UpdateTimeInterval('custom', [minTime / 1e6, maxTime / 1e6]));
urlQuery.set(QueryParams.startTime, Math.floor(minTime / 1e6).toString());
urlQuery.set(QueryParams.endTime, Math.floor(maxTime / 1e6).toString());
}
const generatedUrl = `${location.pathname}?${urlQuery.toString()}`;
history.replace(generatedUrl);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const navigateToTrace = useNavigateToTraces();
return (
@@ -65,8 +149,10 @@ export default function CeleryTaskDetail({
<Typography.Text className="title">{`Details - ${taskData.entity}`}</Typography.Text>
<div>
<Typography.Text className="subtitle">
{`${formatTimestamp(startTime)} ${
endTime ? `- ${formatTimestamp(endTime)}` : ''
{`${formatTimestamp(taskData.timeRange[0])} ${
taskData.timeRange[1]
? `- ${formatTimestamp(taskData.timeRange[1])}`
: ''
}`}
</Typography.Text>
<Divider type="vertical" />
@@ -99,16 +185,8 @@ export default function CeleryTaskDetail({
...rowData,
[taskData.entity]: taskData.value,
});
logEvent('MQ Celery: navigation to trace page', {
filters,
startTime,
endTime,
source: widgetData.title,
});
navigateToTrace(filters, startTime, endTime);
navigateToTrace(filters);
}}
start={startTime}
end={endTime}
/>
</Drawer>
);

View File

@@ -19,10 +19,6 @@ import { Widgets } from 'types/api/dashboard/getAll';
import { GlobalReducer } from 'types/reducer/globalTime';
import { CaptureDataProps } from '../CeleryTaskDetail/CeleryTaskDetail';
import {
applyCeleryFilterOnWidgetData,
getFiltersFromQueryParams,
} from '../CeleryUtils';
import { useGetGraphCustomSeries } from '../useGetGraphCustomSeries';
import {
celeryAllStateWidgetData,
@@ -42,12 +38,10 @@ import {
function CeleryTaskBar({
onClick,
queryEnabled,
checkIfDataExists,
}: {
onClick?: (task: CaptureDataProps) => void;
queryEnabled: boolean;
checkIfDataExists?: (isDataAvailable: boolean) => void;
}): JSX.Element {
const history = useHistory();
const { pathname } = useLocation();
@@ -78,60 +72,26 @@ function CeleryTaskBar({
const [barState, setBarState] = useState<CeleryTaskState>(CeleryTaskState.All);
const selectedFilters = useMemo(
() =>
getFiltersFromQueryParams(
QueryParams.taskName,
urlQuery,
'celery.task_name',
),
[urlQuery],
);
const celeryAllStateData = useMemo(
() => celeryAllStateWidgetData(minTime, maxTime),
[minTime, maxTime],
);
const celeryAllStateFilteredData = useMemo(
() =>
applyCeleryFilterOnWidgetData(selectedFilters || [], celeryAllStateData),
[selectedFilters, celeryAllStateData],
);
const celeryFailedStateData = useMemo(
() => celeryFailedStateWidgetData(minTime, maxTime),
[minTime, maxTime],
);
const celeryFailedStateFilteredData = useMemo(
() =>
applyCeleryFilterOnWidgetData(selectedFilters || [], celeryFailedStateData),
[selectedFilters, celeryFailedStateData],
);
const celeryRetryStateData = useMemo(
() => celeryRetryStateWidgetData(minTime, maxTime),
[minTime, maxTime],
);
const celeryRetryStateFilteredData = useMemo(
() =>
applyCeleryFilterOnWidgetData(selectedFilters || [], celeryRetryStateData),
[selectedFilters, celeryRetryStateData],
);
const celerySuccessStateData = useMemo(
() => celerySuccessStateWidgetData(minTime, maxTime),
[minTime, maxTime],
);
const celerySuccessStateFilteredData = useMemo(
() =>
applyCeleryFilterOnWidgetData(selectedFilters || [], celerySuccessStateData),
[selectedFilters, celerySuccessStateData],
);
const onGraphClick = (
widgetData: Widgets,
xValue: number,
@@ -181,7 +141,7 @@ function CeleryTaskBar({
<div className="celery-task-graph-grid-content">
{barState === CeleryTaskState.All && (
<GridCard
widget={celeryAllStateFilteredData}
widget={celeryAllStateData}
headerMenuList={[...ViewMenuAction]}
onDragSelect={onDragSelect}
isQueryEnabled={queryEnabled}
@@ -189,12 +149,11 @@ function CeleryTaskBar({
onGraphClick(celerySlowestTasksTableWidgetData, ...args)
}
customSeries={getCustomSeries}
dataAvailable={checkIfDataExists}
/>
)}
{barState === CeleryTaskState.Failed && (
<GridCard
widget={celeryFailedStateFilteredData}
widget={celeryFailedStateData}
headerMenuList={[...ViewMenuAction]}
onDragSelect={onDragSelect}
isQueryEnabled={queryEnabled}
@@ -206,7 +165,7 @@ function CeleryTaskBar({
)}
{barState === CeleryTaskState.Retry && (
<GridCard
widget={celeryRetryStateFilteredData}
widget={celeryRetryStateData}
headerMenuList={[...ViewMenuAction]}
onDragSelect={onDragSelect}
isQueryEnabled={queryEnabled}
@@ -218,7 +177,7 @@ function CeleryTaskBar({
)}
{barState === CeleryTaskState.Successful && (
<GridCard
widget={celerySuccessStateFilteredData}
widget={celerySuccessStateData}
headerMenuList={[...ViewMenuAction]}
onDragSelect={onDragSelect}
isQueryEnabled={queryEnabled}
@@ -235,7 +194,6 @@ function CeleryTaskBar({
CeleryTaskBar.defaultProps = {
onClick: (): void => {},
checkIfDataExists: undefined,
};
export default CeleryTaskBar;

View File

@@ -116,8 +116,9 @@
gap: 10px;
.metric-page-grid {
display: flex;
flex-direction: row;
display: grid;
grid-template-columns: 50% 50%;
align-items: flex-start;
gap: 10px;
width: 100%;
@@ -143,11 +144,6 @@
gap: 16px;
align-items: center;
border: 1px dashed var(--bg-slate-50);
border-radius: 4px;
padding: 6px 24px 6px 12px;
width: max-content;
.configure-option-Info-text {
color: var(--bg-vanilla-400);
font-size: 12px;
@@ -245,13 +241,11 @@
.lightMode {
.celery-task-graph-grid-container {
.celery-task-graph-worker-count {
border: 1px solid var(--bg-vanilla-300);
background: unset;
}
.row-panel .row-panel-section .section-title {
color: var(--bg-ink-400);
.celery-task-graph-grid {
.celery-task-graph-worker-count {
border: 1px solid var(--bg-vanilla-300);
background: unset;
}
}
}
@@ -280,8 +274,4 @@
background-color: var(--bg-ink-400);
}
}
.configure-option-Info {
border: 1px dashed var(--bg-robin-400);
}
}

View File

@@ -32,11 +32,6 @@ function CeleryTaskGraph({
openTracesButton,
onOpenTraceBtnClick,
applyCeleryTaskFilter,
customErrorMessage,
start,
end,
checkIfDataExists,
analyticsEvent,
}: {
widgetData: Widgets;
onClick?: (task: CaptureDataProps) => void;
@@ -47,11 +42,6 @@ function CeleryTaskGraph({
openTracesButton?: boolean;
onOpenTraceBtnClick?: (record: RowData) => void;
applyCeleryTaskFilter?: boolean;
customErrorMessage?: string;
start?: number;
end?: number;
checkIfDataExists?: (isDataAvailable: boolean) => void;
analyticsEvent?: string;
}): JSX.Element {
const history = useHistory();
const { pathname } = useLocation();
@@ -126,11 +116,6 @@ function CeleryTaskGraph({
openTracesButton={openTracesButton}
onOpenTraceBtnClick={onOpenTraceBtnClick}
version={ENTITY_VERSION_V4}
customErrorMessage={customErrorMessage}
start={start}
end={end}
dataAvailable={checkIfDataExists}
analyticsEvent={analyticsEvent}
/>
</Card>
);
@@ -144,11 +129,6 @@ CeleryTaskGraph.defaultProps = {
openTracesButton: false,
onOpenTraceBtnClick: undefined,
applyCeleryTaskFilter: false,
customErrorMessage: undefined,
start: undefined,
end: undefined,
checkIfDataExists: undefined,
analyticsEvent: undefined,
};
export default CeleryTaskGraph;

View File

@@ -1,7 +1,6 @@
import './CeleryTaskGraph.style.scss';
import { Card, Typography } from 'antd';
import logEvent from 'api/common/logEvent';
import { CardContainer } from 'container/GridCardLayout/styles';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { ChevronDown, ChevronUp } from 'lucide-react';
@@ -93,15 +92,6 @@ export default function CeleryTaskGraphGrid({
}));
};
const checkIfDataExists = (isDataAvailable: boolean, title: string): void => {
if (isDataAvailable) {
logEvent(`MQ Celery: ${title} data exists`, {
graph: title,
isDataAvailable,
});
}
};
return (
<div className="celery-task-graph-grid-container">
<div className="metric-based-graphs">
@@ -133,16 +123,11 @@ export default function CeleryTaskGraphGrid({
key={celeryActiveTasksData.id}
widgetData={celeryActiveTasksData}
queryEnabled={queryEnabled}
customErrorMessage="Enable Flower metrics to view this graph"
checkIfDataExists={(isDataAvailable): void =>
checkIfDataExists(isDataAvailable, 'Active Tasks by worker')
}
analyticsEvent="MQ Celery: Flower metric not enabled"
/>
<Card className="celery-task-graph-worker-count">
<div className="worker-count-header">
<Typography.Text className="worker-count-header-text">
Worker Online
Worker Count
</Typography.Text>
</div>
<div className="worker-count-text-container">
@@ -187,19 +172,8 @@ export default function CeleryTaskGraphGrid({
</div>
{!collapsedSections.traceBasedGraphs && (
<>
<CeleryTaskBar
queryEnabled={queryEnabled}
onClick={onClick}
checkIfDataExists={(isDataAvailable): void =>
checkIfDataExists(isDataAvailable, 'State Graph')
}
/>
<CeleryTaskLatencyGraph
queryEnabled={queryEnabled}
checkIfDataExists={(isDataAvailable): void =>
checkIfDataExists(isDataAvailable, 'Task Latency')
}
/>
<CeleryTaskBar queryEnabled={queryEnabled} onClick={onClick} />
<CeleryTaskLatencyGraph onClick={onClick} queryEnabled={queryEnabled} />
<div className="celery-task-graph-grid-bottom">
{bottomWidgetData.map((widgetData, index) => (
<CeleryTaskGraph
@@ -209,9 +183,6 @@ export default function CeleryTaskGraphGrid({
queryEnabled={queryEnabled}
rightPanelTitle={rightPanelTitle[index]}
applyCeleryTaskFilter
checkIfDataExists={(isDataAvailable): void =>
checkIfDataExists(isDataAvailable, rightPanelTitle[index])
}
/>
))}
</div>

View File

@@ -42,7 +42,21 @@ export const celeryAllStateWidgetData = (
disabled: false,
expression: 'A',
filters: {
items: [],
items: [
{
id: uuidv4(),
key: {
dataType: DataTypes.String,
id: 'celery.task_name--string--tag--false',
isColumn: false,
isJSON: false,
key: 'celery.task_name',
type: 'tag',
},
op: '=',
value: 'tasks.tasks.divide',
},
],
op: 'AND',
},
functions: [],
@@ -99,7 +113,7 @@ export const celeryRetryStateWidgetData = (
filters: {
items: [
{
id: uuidv4(),
id: '6d97eed3',
key: {
dataType: DataTypes.String,
id: 'celery.state--string--tag--false',
@@ -165,7 +179,7 @@ export const celeryFailedStateWidgetData = (
filters: {
items: [
{
id: uuidv4(),
id: '5983eae2',
key: {
dataType: DataTypes.String,
id: 'celery.state--string--tag--false',
@@ -231,7 +245,7 @@ export const celerySuccessStateWidgetData = (
filters: {
items: [
{
id: uuidv4(),
id: '000c5a93',
key: {
dataType: DataTypes.String,
id: 'celery.state--string--tag--false',
@@ -588,7 +602,7 @@ export const celeryTaskLatencyWidgetData = (
reduceTo: 'avg',
spaceAggregation: 'sum',
stepInterval: getStepInterval(startTime, endTime),
timeAggregation: type || 'p99',
timeAggregation: 'p99',
},
],
yAxisUnit: 'ns',
@@ -672,7 +686,7 @@ export const celeryRetryTasksTableWidgetData = getWidgetQueryBuilder(
filters: {
items: [
{
id: uuidv4(),
id: '9e09c9ed',
key: {
dataType: DataTypes.String,
id: 'celery.state--string--tag--false',
@@ -741,7 +755,7 @@ export const celeryFailedTasksTableWidgetData = getWidgetQueryBuilder(
filters: {
items: [
{
id: uuidv4(),
id: '2330f906',
key: {
dataType: DataTypes.String,
id: 'celery.state--string--tag--false',
@@ -808,7 +822,7 @@ export const celerySuccessTasksTableWidgetData = getWidgetQueryBuilder(
filters: {
items: [
{
id: uuidv4(),
id: 'ec3df7b7',
key: {
dataType: DataTypes.String,
id: 'celery.state--string--tag--false',
@@ -931,19 +945,33 @@ export const celeryAllStateCountWidgetData = getWidgetQueryBuilder(
queryData: [
{
aggregateAttribute: {
dataType: DataTypes.String,
id: 'span_id--string----true',
isColumn: true,
dataType: DataTypes.EMPTY,
id: '------false',
isColumn: false,
isJSON: false,
key: 'span_id',
key: '',
type: '',
},
aggregateOperator: 'count_distinct',
aggregateOperator: 'count',
dataSource: DataSource.TRACES,
disabled: false,
expression: 'A',
filters: {
items: [],
items: [
{
id: uuidv4(),
key: {
dataType: DataTypes.String,
id: 'celery.task_name--string--tag--false',
isColumn: false,
isJSON: false,
key: 'celery.task_name',
type: 'tag',
},
op: '=',
value: 'tasks.tasks.divide',
},
],
op: 'AND',
},
functions: [],
@@ -953,10 +981,10 @@ export const celeryAllStateCountWidgetData = getWidgetQueryBuilder(
limit: null,
orderBy: [],
queryName: 'A',
reduceTo: 'last',
reduceTo: 'avg',
spaceAggregation: 'sum',
stepInterval: 60,
timeAggregation: 'count_distinct',
timeAggregation: 'rate',
},
],
}),
@@ -970,14 +998,14 @@ export const celerySuccessStateCountWidgetData = getWidgetQueryBuilder(
queryData: [
{
aggregateAttribute: {
dataType: DataTypes.String,
id: 'span_id--string----true',
isColumn: true,
dataType: DataTypes.EMPTY,
id: '------false',
isColumn: false,
isJSON: false,
key: 'span_id',
key: '',
type: '',
},
aggregateOperator: 'count_distinct',
aggregateOperator: 'count',
dataSource: DataSource.TRACES,
disabled: false,
expression: 'A',
@@ -1006,10 +1034,10 @@ export const celerySuccessStateCountWidgetData = getWidgetQueryBuilder(
limit: null,
orderBy: [],
queryName: 'A',
reduceTo: 'last',
reduceTo: 'avg',
spaceAggregation: 'sum',
stepInterval: 60,
timeAggregation: 'count_distinct',
timeAggregation: 'rate',
},
],
}),
@@ -1023,14 +1051,14 @@ export const celeryFailedStateCountWidgetData = getWidgetQueryBuilder(
queryData: [
{
aggregateAttribute: {
dataType: DataTypes.String,
id: 'span_id--string----true',
isColumn: true,
dataType: DataTypes.EMPTY,
id: '------false',
isColumn: false,
isJSON: false,
key: 'span_id',
key: '',
type: '',
},
aggregateOperator: 'count_distinct',
aggregateOperator: 'count',
dataSource: DataSource.TRACES,
disabled: false,
expression: 'A',
@@ -1059,10 +1087,10 @@ export const celeryFailedStateCountWidgetData = getWidgetQueryBuilder(
limit: null,
orderBy: [],
queryName: 'A',
reduceTo: 'last',
reduceTo: 'avg',
spaceAggregation: 'sum',
stepInterval: 60,
timeAggregation: 'count_distinct',
timeAggregation: 'rate',
},
],
}),
@@ -1076,13 +1104,13 @@ export const celeryRetryStateCountWidgetData = getWidgetQueryBuilder(
queryData: [
{
aggregateAttribute: {
dataType: DataTypes.String,
id: 'span_id--string----true',
isColumn: true,
key: 'span_id',
dataType: DataTypes.EMPTY,
id: '------false',
isColumn: false,
key: '',
type: '',
},
aggregateOperator: 'count_distinct',
aggregateOperator: 'count',
dataSource: DataSource.TRACES,
disabled: false,
expression: 'A',
@@ -1111,10 +1139,10 @@ export const celeryRetryStateCountWidgetData = getWidgetQueryBuilder(
limit: null,
orderBy: [],
queryName: 'A',
reduceTo: 'last',
reduceTo: 'avg',
spaceAggregation: 'sum',
stepInterval: 60,
timeAggregation: 'count_distinct',
timeAggregation: 'rate',
},
],
}),

View File

@@ -1,17 +1,13 @@
import './CeleryTaskGraph.style.scss';
import { Col, Row } from 'antd';
import logEvent from 'api/common/logEvent';
import { QueryParams } from 'constants/query';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { ViewMenuAction } from 'container/GridCardLayout/config';
import GridCard from 'container/GridCardLayout/GridCard';
import { Card } from 'container/GridCardLayout/styles';
import { Button } from 'container/MetricsApplication/Tabs/styles';
import { onGraphClickHandler } from 'container/MetricsApplication/Tabs/util';
import { useIsDarkMode } from 'hooks/useDarkMode';
import useUrlQuery from 'hooks/useUrlQuery';
import { OnClickPluginOpts } from 'lib/uPlotLib/plugins/onClickPlugin';
import { getStartAndEndTimesInMilliseconds } from 'pages/MessagingQueues/MessagingQueuesUtils';
import { useCallback, useMemo, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
@@ -20,13 +16,15 @@ import { UpdateTimeInterval } from 'store/actions';
import { AppState } from 'store/reducers';
import { GlobalReducer } from 'types/reducer/globalTime';
import { CaptureDataProps } from '../CeleryTaskDetail/CeleryTaskDetail';
import {
applyCeleryFilterOnWidgetData,
createFiltersFromData,
getFiltersFromQueryParams,
} from '../CeleryUtils';
import { useNavigateToTraces } from '../useNavigateToTraces';
import { celeryTaskLatencyWidgetData } from './CeleryTaskGraphUtils';
import {
celeryTaskLatencyWidgetData,
celeryTimeSeriesTablesWidgetData,
} from './CeleryTaskGraphUtils';
interface TabData {
label: string;
@@ -40,11 +38,11 @@ export enum CeleryTaskGraphState {
}
function CeleryTaskLatencyGraph({
onClick,
queryEnabled,
checkIfDataExists,
}: {
onClick: (task: CaptureDataProps) => void;
queryEnabled: boolean;
checkIfDataExists?: (isDataAvailable: boolean) => void;
}): JSX.Element {
const history = useHistory();
const { pathname } = useLocation();
@@ -64,10 +62,6 @@ function CeleryTaskLatencyGraph({
const handleTabClick = (key: CeleryTaskGraphState): void => {
setGraphState(key as CeleryTaskGraphState);
logEvent('MQ Celery: Task latency graph tab clicked', {
taskName: urlQuery.get(QueryParams.taskName),
graphState: key,
});
};
const onDragSelect = useCallback(
@@ -112,51 +106,31 @@ function CeleryTaskLatencyGraph({
[celeryTaskLatencyData, selectedFilters],
);
const [selectedTimeStamp, setSelectedTimeStamp] = useState<number>(0);
const [entityData, setEntityData] = useState<{
entity: string;
value: string;
}>();
const handleSetTimeStamp = useCallback((selectTime: number) => {
setSelectedTimeStamp(selectTime);
}, []);
const onGraphClick = useCallback(
(type: string): OnClickPluginOpts['onClick'] => (
xValue,
yValue,
mouseX,
mouseY,
data,
): Promise<void> => {
const [firstDataPoint] = Object.entries(data || {});
const [entity, value] = firstDataPoint;
setEntityData({
entity,
value,
});
return onGraphClickHandler(handleSetTimeStamp)(
xValue,
yValue,
mouseX,
mouseY,
type,
);
const onGraphClick = (
xValue: number,
_yValue: number,
_mouseX: number,
_mouseY: number,
data?: {
[key: string]: string;
},
[handleSetTimeStamp],
);
): void => {
const { start, end } = getStartAndEndTimesInMilliseconds(xValue);
const navigateToTraces = useNavigateToTraces();
// Extract entity and value from data
const [firstDataPoint] = Object.entries(data || {});
const [entity, value] = (firstDataPoint || ([] as unknown)) as [
string,
string,
];
const goToTraces = useCallback(() => {
const { start, end } = getStartAndEndTimesInMilliseconds(selectedTimeStamp);
const filters = createFiltersFromData({
[entityData?.entity as string]: entityData?.value,
onClick?.({
entity,
value,
timeRange: [start, end],
widgetData: celeryTimeSeriesTablesWidgetData(entity, value, 'Task Latency'),
});
navigateToTraces(filters, start, end, true);
}, [entityData, navigateToTraces, selectedTimeStamp]);
};
return (
<Card
@@ -187,65 +161,32 @@ function CeleryTaskLatencyGraph({
</Row>
<div className="celery-task-graph-grid-content">
{graphState === CeleryTaskGraphState.P99 && (
<>
<Button
type="default"
size="small"
id="Celery_p99_latency_button"
onClick={goToTraces}
>
View Traces
</Button>
<GridCard
widget={updatedWidgetData}
headerMenuList={[...ViewMenuAction]}
onDragSelect={onDragSelect}
onClickHandler={onGraphClick('Celery_p99_latency')}
isQueryEnabled={queryEnabled}
dataAvailable={checkIfDataExists}
/>
</>
<GridCard
widget={updatedWidgetData}
headerMenuList={[...ViewMenuAction]}
onDragSelect={onDragSelect}
onClickHandler={onGraphClick}
isQueryEnabled={queryEnabled}
/>
)}
{graphState === CeleryTaskGraphState.P95 && (
<>
<Button
type="default"
size="small"
id="Celery_p95_latency_button"
onClick={goToTraces}
>
View Traces
</Button>
<GridCard
widget={updatedWidgetData}
headerMenuList={[...ViewMenuAction]}
onDragSelect={onDragSelect}
onClickHandler={onGraphClick('Celery_p95_latency')}
isQueryEnabled={queryEnabled}
dataAvailable={checkIfDataExists}
/>
</>
<GridCard
widget={updatedWidgetData}
headerMenuList={[...ViewMenuAction]}
onDragSelect={onDragSelect}
onClickHandler={onGraphClick}
isQueryEnabled={queryEnabled}
/>
)}
{graphState === CeleryTaskGraphState.P90 && (
<>
<Button
type="default"
size="small"
id="Celery_p90_latency_button"
onClick={goToTraces}
>
View Traces
</Button>
<GridCard
widget={updatedWidgetData}
headerMenuList={[...ViewMenuAction]}
onDragSelect={onDragSelect}
onClickHandler={onGraphClick('Celery_p90_latency')}
isQueryEnabled={queryEnabled}
dataAvailable={checkIfDataExists}
/>
</>
<GridCard
widget={updatedWidgetData}
headerMenuList={[...ViewMenuAction]}
onDragSelect={onDragSelect}
onClickHandler={onGraphClick}
isQueryEnabled={queryEnabled}
/>
)}
</div>
</Card>
@@ -253,7 +194,3 @@ function CeleryTaskLatencyGraph({
}
export default CeleryTaskLatencyGraph;
CeleryTaskLatencyGraph.defaultProps = {
checkIfDataExists: undefined,
};

View File

@@ -2,15 +2,8 @@
import './CeleryTaskGraph.style.scss';
import { Col, Row } from 'antd';
import logEvent from 'api/common/logEvent';
import { QueryParams } from 'constants/query';
import useUrlQuery from 'hooks/useUrlQuery';
import { Dispatch, SetStateAction, useMemo } from 'react';
import { Dispatch, SetStateAction } from 'react';
import {
applyCeleryFilterOnWidgetData,
getFiltersFromQueryParams,
} from '../CeleryUtils';
import {
celeryAllStateCountWidgetData,
celeryFailedStateCountWidgetData,
@@ -45,37 +38,20 @@ function CeleryTaskStateGraphConfig({
{ label: 'Successful', key: CeleryTaskState.Successful },
];
const urlQuery = useUrlQuery();
const handleTabClick = (key: CeleryTaskState): void => {
setBarState(key as CeleryTaskState);
logEvent('MQ Celery: State graph tab clicked', {
taskName: urlQuery.get(QueryParams.taskName),
graphState: key,
});
};
const selectedFilters = useMemo(
() =>
getFiltersFromQueryParams(
QueryParams.taskName,
urlQuery,
'celery.task_name',
),
[urlQuery],
const { values, isLoading, isError } = useGetValueFromWidget(
[
celeryAllStateCountWidgetData,
celeryFailedStateCountWidgetData,
celeryRetryStateCountWidgetData,
celerySuccessStateCountWidgetData,
],
['celery-task-states'],
);
const widgetData = [
celeryAllStateCountWidgetData,
celeryFailedStateCountWidgetData,
celeryRetryStateCountWidgetData,
celerySuccessStateCountWidgetData,
].map((data) => applyCeleryFilterOnWidgetData(selectedFilters || [], data));
const { values, isLoading, isError } = useGetValueFromWidget(widgetData, [
'celery-task-states',
]);
return (
<Row className="celery-task-states">
{tabs.map((tab, index) => (
@@ -90,13 +66,7 @@ function CeleryTaskStateGraphConfig({
<div className="celery-task-states__label-wrapper">
<div className="celery-task-states__label">{tab.label}</div>
<div className="celery-task-states__value">
{isLoading
? '-'
: isError
? '-'
: Number.isNaN(values[index])
? '-'
: Math.round(Number(values[index]))}
{isLoading ? '-' : isError ? '-' : values[index]}
</div>
</div>
{tab.key === barState && <div className="celery-task-states__indicator" />}

View File

@@ -90,40 +90,3 @@ export const paths = (
return renderer(u, seriesIdx, idx0, idx1, extendGap, buildClip);
};
export const createFiltersFromData = (
data: Record<string, any>,
): Array<{
id: string;
key: {
key: string;
dataType: DataTypes;
type: string;
isColumn: boolean;
isJSON: boolean;
id: string;
};
op: string;
value: string;
}> => {
const excludeKeys = ['A', 'A_without_unit'];
return (
Object.entries(data)
.filter(([key]) => !excludeKeys.includes(key))
// eslint-disable-next-line sonarjs/no-identical-functions
.map(([key, value]) => ({
id: uuidv4(),
key: {
key,
dataType: DataTypes.String,
type: 'tag',
isColumn: false,
isJSON: false,
id: `${key}--string--tag--false`,
},
op: '=',
value: value.toString(),
}))
);
};

View File

@@ -12,7 +12,6 @@ export function useNavigateToTraces(): (
filters: TagFilterItem[],
startTime?: number,
endTime?: number,
sameTab?: boolean,
) => void {
const { currentQuery } = useQueryBuilder();
const { minTime, maxTime } = useSelector<AppState, GlobalReducer>(
@@ -39,12 +38,7 @@ export function useNavigateToTraces(): (
);
return useCallback(
(
filters: TagFilterItem[],
startTime?: number,
endTime?: number,
sameTab?: boolean,
): void => {
(filters: TagFilterItem[], startTime?: number, endTime?: number): void => {
const urlParams = new URLSearchParams();
if (startTime && endTime) {
urlParams.set(QueryParams.startTime, startTime.toString());
@@ -64,7 +58,7 @@ export function useNavigateToTraces(): (
QueryParams.compositeQuery
}=${JSONCompositeQuery}`;
window.open(newTraceExplorerPath, sameTab ? '_self' : '_blank');
window.open(newTraceExplorerPath, '_blank');
},
[minTime, maxTime, prepareQuery],
);

View File

@@ -18,9 +18,7 @@ import {
import axios from 'axios';
import TextToolTip from 'components/TextToolTip';
import { SOMETHING_WENT_WRONG } from 'constants/api';
import { LOCALSTORAGE } from 'constants/localStorage';
import { QueryParams } from 'constants/query';
import { useOptionsMenu } from 'container/OptionsMenu';
import { useGetSearchQueryParam } from 'hooks/queryBuilder/useGetSearchQueryParam';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useDeleteView } from 'hooks/saveViews/useDeleteView';
@@ -31,7 +29,6 @@ import { useNotifications } from 'hooks/useNotifications';
import { mapCompositeQueryFromQuery } from 'lib/newQueryBuilder/queryBuilderMappers/mapCompositeQueryFromQuery';
import { useState } from 'react';
import { useCopyToClipboard } from 'react-use';
import { DataSource, StringOperators } from 'types/common/queryBuilder';
import { popupContainer } from 'utils/selectPopupContainer';
import { ExploreHeaderToolTip, SaveButtonText } from './constants';
@@ -86,20 +83,7 @@ function ExplorerCard({
const viewKey = useGetSearchQueryParam(QueryParams.viewKey) || '';
const { options } = useOptionsMenu({
storageKey:
sourcepage === DataSource.TRACES
? LOCALSTORAGE.TRACES_LIST_OPTIONS
: LOCALSTORAGE.LOGS_LIST_OPTIONS,
dataSource: sourcepage,
aggregateOperator: StringOperators.NOOP,
});
const isQueryUpdated = isStagedQueryUpdated(
viewsData?.data?.data,
viewKey,
options,
);
const isQueryUpdated = isStagedQueryUpdated(viewsData?.data?.data, viewKey);
const { mutateAsync: updateViewAsync } = useUpdateView({
compositeQuery: mapCompositeQueryFromQuery(currentQuery, panelType),

View File

@@ -6,24 +6,11 @@ import { DataSource } from 'types/common/queryBuilder';
import { viewMockData } from '../__mock__/viewData';
import ExplorerCard from '../ExplorerCard';
const historyReplace = jest.fn();
// eslint-disable-next-line sonarjs/no-duplicate-string
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useLocation: (): { pathname: string } => ({
pathname: `${process.env.FRONTEND_API_ENDPOINT}/${ROUTES.TRACES_EXPLORER}/`,
}),
useHistory: (): any => ({
...jest.requireActual('react-router-dom').useHistory(),
replace: historyReplace,
}),
}));
jest.mock('hooks/useSafeNavigate', () => ({
useSafeNavigate: (): any => ({
safeNavigate: jest.fn(),
}),
}));
jest.mock('hooks/queryBuilder/useGetPanelTypesQueryParam', () => ({

View File

@@ -2,7 +2,6 @@ import { FormInstance } from 'antd';
import { NotificationInstance } from 'antd/es/notification/interface';
import { AxiosResponse } from 'axios';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { OptionsQuery } from 'container/OptionsMenu/types';
import { UseMutateAsyncFunction } from 'react-query';
import { ICompositeMetricQuery } from 'types/api/alerts/compositeQuery';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
@@ -37,7 +36,6 @@ export interface IsQueryUpdatedInViewProps {
data: ViewProps[] | undefined;
stagedQuery: Query | null;
currentPanelType: PANEL_TYPES | null;
options: OptionsQuery;
}
export interface SaveViewWithNameProps {

View File

@@ -80,13 +80,12 @@ export const isQueryUpdatedInView = ({
data,
stagedQuery,
currentPanelType,
options,
}: IsQueryUpdatedInViewProps): boolean => {
const currentViewDetails = getViewDetailsUsingViewKey(viewKey, data);
if (!currentViewDetails) {
return false;
}
const { query, panelType, extraData } = currentViewDetails;
const { query, panelType } = currentViewDetails;
// Omitting id from aggregateAttribute and groupBy
const updatedCurrentQuery = omitIdFromQuery(stagedQuery);
@@ -98,15 +97,12 @@ export const isQueryUpdatedInView = ({
) {
return false;
}
return (
panelType !== currentPanelType ||
!isEqual(query.builder, updatedCurrentQuery?.builder) ||
!isEqual(query.clickhouse_sql, updatedCurrentQuery?.clickhouse_sql) ||
!isEqual(query.promql, updatedCurrentQuery?.promql) ||
!isEqual(
options?.selectColumns,
extraData && JSON.parse(extraData)?.selectColumns,
)
!isEqual(query.promql, updatedCurrentQuery?.promql)
);
};

View File

@@ -8,19 +8,13 @@ import { DEFAULT_ENTITY_VERSION } from 'constants/app';
import LogsError from 'container/LogsError/LogsError';
import { LogsLoading } from 'container/LogsLoading/LogsLoading';
import { FontSize } from 'container/OptionsMenu/types';
import { ORDERBY_FILTERS } from 'container/QueryBuilder/filters/OrderByFilter/config';
import { useHandleLogsPagination } from 'hooks/infraMonitoring/useHandleLogsPagination';
import { GetMetricQueryRange } from 'lib/dashboard/getQueryResults';
import { isEqual } from 'lodash-es';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useCallback, useEffect, useMemo } from 'react';
import { useQuery } from 'react-query';
import { Virtuoso } from 'react-virtuoso';
import { ILog } from 'types/api/logs/log';
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
import {
IBuilderQuery,
TagFilterItem,
} from 'types/api/queryBuilder/queryBuilderData';
import { v4 } from 'uuid';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import { getHostLogsQueryPayload } from './constants';
import NoLogsContainer from './NoLogsContainer';
@@ -30,51 +24,30 @@ interface Props {
startTime: number;
endTime: number;
};
handleChangeLogFilters: (filters: IBuilderQuery['filters']) => void;
filters: IBuilderQuery['filters'];
}
function HostMetricsLogs({
timeRange,
handleChangeLogFilters,
filters,
}: Props): JSX.Element {
const [logs, setLogs] = useState<ILog[]>([]);
const [hasReachedEndOfLogs, setHasReachedEndOfLogs] = useState(false);
const [restFilters, setRestFilters] = useState<TagFilterItem[]>([]);
const [resetLogsList, setResetLogsList] = useState<boolean>(false);
useEffect(() => {
const newRestFilters = filters.items.filter(
(item) => item.key?.key !== 'id' && item.key?.key !== 'host.name',
);
const areFiltersSame = isEqual(restFilters, newRestFilters);
if (!areFiltersSame) {
setResetLogsList(true);
}
setRestFilters(newRestFilters);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [filters]);
const queryPayload = useMemo(() => {
const basePayload = getHostLogsQueryPayload(
timeRange.startTime,
timeRange.endTime,
filters,
);
basePayload.query.builder.queryData[0].pageSize = 100;
basePayload.query.builder.queryData[0].orderBy = [
{ columnName: 'timestamp', order: ORDERBY_FILTERS.DESC },
];
return basePayload;
}, [timeRange.startTime, timeRange.endTime, filters]);
const [isPaginating, setIsPaginating] = useState(false);
function HostMetricsLogs({ timeRange, filters }: Props): JSX.Element {
const basePayload = getHostLogsQueryPayload(
timeRange.startTime,
timeRange.endTime,
filters,
);
const {
logs,
hasReachedEndOfLogs,
isPaginating,
currentPage,
setIsPaginating,
handleNewData,
loadMoreLogs,
queryPayload,
} = useHandleLogsPagination({
timeRange,
filters,
excludeFilterKeys: ['host.name'],
basePayload,
});
const { data, isLoading, isFetching, isError } = useQuery({
queryKey: [
@@ -82,6 +55,7 @@ function HostMetricsLogs({
timeRange.startTime,
timeRange.endTime,
filters,
currentPage,
],
queryFn: () => GetMetricQueryRange(queryPayload, DEFAULT_ENTITY_VERSION),
enabled: !!queryPayload,
@@ -90,33 +64,13 @@ function HostMetricsLogs({
useEffect(() => {
if (data?.payload?.data?.newResult?.data?.result) {
const currentData = data.payload.data.newResult.data.result;
if (resetLogsList) {
const currentLogs: ILog[] =
currentData[0].list?.map((item) => ({
...item.data,
timestamp: item.timestamp,
})) || [];
setLogs(currentLogs);
setResetLogsList(false);
}
if (currentData.length > 0 && currentData[0].list) {
const currentLogs: ILog[] =
currentData[0].list.map((item) => ({
...item.data,
timestamp: item.timestamp,
})) || [];
setLogs((prev) => [...prev, ...currentLogs]);
} else {
setHasReachedEndOfLogs(true);
}
handleNewData(data.payload.data.newResult.data.result);
}
}, [data, restFilters, isPaginating, resetLogsList]);
}, [data, handleNewData]);
useEffect(() => {
setIsPaginating(false);
}, [data, setIsPaginating]);
const getItemContent = useCallback(
(_: number, logToRender: ILog): JSX.Element => (
@@ -144,39 +98,6 @@ function HostMetricsLogs({
[],
);
const loadMoreLogs = useCallback(() => {
if (!logs.length) return;
setIsPaginating(true);
const lastLog = logs[logs.length - 1];
const newItems = [
...filters.items.filter((item) => item.key?.key !== 'id'),
{
id: v4(),
key: {
key: 'id',
type: '',
dataType: DataTypes.String,
isColumn: true,
},
op: '<',
value: lastLog.id,
},
];
const newFilters = {
op: 'AND',
items: newItems,
} as IBuilderQuery['filters'];
handleChangeLogFilters(newFilters);
}, [logs, filters, handleChangeLogFilters]);
useEffect(() => {
setIsPaginating(false);
}, [data]);
const renderFooter = useCallback(
(): JSX.Element | null => (
// eslint-disable-next-line react/jsx-no-useless-fragment

View File

@@ -133,7 +133,7 @@
.go-to-docs {
display: flex;
flex-direction: column;
gap: 44px;
gap: 64px;
&__container {
display: flex;
flex-direction: column;

View File

@@ -6,8 +6,6 @@ import { ColumnGroupType, ColumnType } from 'antd/es/table';
import { ColumnsType } from 'antd/lib/table';
import logEvent from 'api/common/logEvent';
import LaunchChatSupport from 'components/LaunchChatSupport/LaunchChatSupport';
import { useSafeNavigate } from 'hooks/useSafeNavigate';
import useUrlQuery from 'hooks/useUrlQuery';
import { SlidersHorizontal } from 'lucide-react';
import { memo, useEffect, useState } from 'react';
import { popupContainer } from 'utils/selectPopupContainer';
@@ -27,12 +25,8 @@ function DynamicColumnTable({
onDragColumn,
facingIssueBtn,
shouldSendAlertsLogEvent,
pagination,
...restProps
}: DynamicColumnTableProps): JSX.Element {
const { safeNavigate } = useSafeNavigate();
const urlQuery = useUrlQuery();
const [columnsData, setColumnsData] = useState<ColumnsType | undefined>(
columns,
);
@@ -99,28 +93,6 @@ function DynamicColumnTable({
type: 'checkbox',
})) || [];
// Get current page from URL or default to 1
const currentPage = Number(urlQuery.get('page')) || 1;
const handlePaginationChange = (page: number, pageSize?: number): void => {
// Update URL with new page number while preserving other params
urlQuery.set('page', page.toString());
const newUrl = `${window.location.pathname}?${urlQuery.toString()}`;
safeNavigate(newUrl);
// Call original pagination handler if provided
if (pagination?.onChange && !!pageSize) {
pagination.onChange(page, pageSize);
}
};
const enhancedPagination = {
...pagination,
current: currentPage, // Ensure the pagination component shows the correct page
onChange: handlePaginationChange,
};
return (
<div className="DynamicColumnTable">
<Flex justify="flex-end" align="center" gap={8}>
@@ -144,7 +116,6 @@ function DynamicColumnTable({
<ResizeTable
columns={columnsData}
onDragColumn={onDragColumn}
pagination={enhancedPagination}
{...restProps}
/>
</div>

View File

@@ -1,7 +1,6 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { TableProps } from 'antd';
import { ColumnsType } from 'antd/es/table';
import { PaginationProps } from 'antd/lib';
import { ColumnGroupType, ColumnType } from 'antd/lib/table';
import { LaunchChatSupportProps } from 'components/LaunchChatSupport/LaunchChatSupport';
@@ -16,7 +15,6 @@ export interface DynamicColumnTableProps extends TableProps<any> {
onDragColumn?: (fromIndex: number, toIndex: number) => void;
facingIssueBtn?: LaunchChatSupportProps;
shouldSendAlertsLogEvent?: boolean;
pagination?: PaginationProps;
}
export type GetVisibleColumnsFunction = (

View File

@@ -407,13 +407,3 @@ export const HAVING_OPERATORS: string[] = [
OPERATORS['<='],
OPERATORS['<'],
];
export enum PanelDisplay {
TIME_SERIES = 'Time Series',
VALUE = 'Number',
TABLE = 'Table',
LIST = 'List',
BAR = 'Bar',
PIE = 'Pie',
HISTOGRAM = 'Histogram',
}

View File

@@ -41,6 +41,5 @@ export const REACT_QUERY_KEY = {
AWS_UPDATE_ACCOUNT_CONFIG: 'AWS_UPDATE_ACCOUNT_CONFIG',
AWS_UPDATE_SERVICE_CONFIG: 'AWS_UPDATE_SERVICE_CONFIG',
AWS_GENERATE_CONNECTION_URL: 'AWS_GENERATE_CONNECTION_URL',
AWS_GET_CONNECTION_PARAMS: 'AWS_GET_CONNECTION_PARAMS',
GET_ATTRIBUTE_VALUES: 'GET_ATTRIBUTE_VALUES',
};

View File

@@ -65,9 +65,6 @@ const ROUTES = {
INFRASTRUCTURE_MONITORING_KUBERNETES: '/infrastructure-monitoring/kubernetes',
MESSAGING_QUEUES_CELERY_TASK: '/messaging-queues/celery-task',
MESSAGING_QUEUES_OVERVIEW: '/messaging-queues/overview',
METRICS_EXPLORER: '/metrics-explorer/summary',
METRICS_EXPLORER_EXPLORER: '/metrics-explorer/explorer',
METRICS_EXPLORER_VIEWS: '/metrics-explorer/views',
} as const;
export default ROUTES;

View File

@@ -250,7 +250,7 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
if (
!isFetchingActiveLicenseV3 &&
!isNull(activeLicenseV3) &&
activeLicenseV3?.event_queue?.event === LicenseEvent.DEFAULT
activeLicenseV3?.event_queue?.event === LicenseEvent.FAILED_PAYMENT
) {
setShowPaymentFailedWarning(true);
}

View File

@@ -24,9 +24,7 @@ function Header(): JSX.Element {
},
{
title: (
<div className="cloud-header__breadcrumb-title">
Amazon Web Services
</div>
<div className="cloud-header__breadcrumb-title">AWS web services</div>
),
},
]}

View File

@@ -21,7 +21,7 @@ function HeroSection(): JSX.Element {
<img src="/Logos/aws-dark.svg" alt="aws-logo" />
</div>
<div className="hero-section__details">
<div className="title">Amazon Web Services</div>
<div className="title">AWS Web Services</div>
<div className="description">
One-click setup for AWS monitoring with SigNoz
</div>

View File

@@ -1,56 +1,41 @@
.hero-section {
&__actions {
margin-top: 12px;
.hero-section__actions {
margin-top: 12px;
&-with-account {
display: flex;
flex-direction: column;
gap: 10px;
}
&-with-account {
display: flex;
flex-direction: column;
gap: 10px;
}
}
.hero-section__action-buttons {
display: flex;
align-items: center;
gap: 8px;
}
.hero-section__action-button {
font-family: 'Inter';
border-radius: 2px;
cursor: pointer;
font-size: 12px;
font-weight: 500;
line-height: 16px;
padding: 8px 17px;
&.primary {
background: var(--bg-robin-500);
border: none;
color: var(--bg-vanilla-100);
}
&__input-skeleton {
width: 300px;
margin-bottom: 16px;
}
&__new-account-button-skeleton {
width: 180px;
margin-right: 8px;
}
&__account-settings-button-skeleton {
width: 140px;
}
&__action-buttons {
&.secondary {
display: flex;
align-items: center;
gap: 8px;
}
&__action-button {
font-family: 'Inter';
border: 1px solid var(--bg-ink-300);
color: var(--bg-vanilla-100);
border-radius: 2px;
cursor: pointer;
font-size: 12px;
font-weight: 500;
line-height: 16px;
padding: 8px 17px;
&.primary {
background: var(--bg-robin-500);
border: none;
color: var(--bg-vanilla-100);
}
&.secondary {
display: flex;
align-items: center;
border: 1px solid var(--bg-ink-300);
color: var(--bg-vanilla-100);
border-radius: 2px;
background: var(--bg-slate-400);
box-shadow: none;
}
background: var(--bg-slate-400);
box-shadow: none;
}
}

View File

@@ -1,9 +1,9 @@
import './AccountActions.style.scss';
import { Color } from '@signozhq/design-tokens';
import { Button, Select, Skeleton } from 'antd';
import { Button, Select } from 'antd';
import { SelectProps } from 'antd/lib';
import { useAwsAccounts } from 'hooks/integration/aws/useAwsAccounts';
import { useAwsAccounts } from 'hooks/integrations/aws/useAwsAccounts';
import useUrlQuery from 'hooks/useUrlQuery';
import { Check, ChevronDown } from 'lucide-react';
import { useEffect, useMemo, useState } from 'react';
@@ -53,100 +53,15 @@ const getAccountById = (
): CloudAccount | null =>
accounts.find((account) => account.cloud_account_id === accountId) || null;
function AccountActionsRenderer({
accounts,
isLoading,
activeAccount,
selectOptions,
onAccountChange,
onIntegrationModalOpen,
onAccountSettingsModalOpen,
}: {
accounts: CloudAccount[] | undefined;
isLoading: boolean;
activeAccount: CloudAccount | null;
selectOptions: SelectProps['options'];
onAccountChange: (value: string) => void;
onIntegrationModalOpen: () => void;
onAccountSettingsModalOpen: () => void;
}): JSX.Element {
if (isLoading) {
return (
<div className="hero-section__actions-with-account">
<Skeleton.Input
active
size="large"
block
className="hero-section__input-skeleton"
/>
<div className="hero-section__action-buttons">
<Skeleton.Button
active
size="large"
className="hero-section__new-account-button-skeleton"
/>
<Skeleton.Button
active
size="large"
className="hero-section__account-settings-button-skeleton"
/>
</div>
</div>
);
}
if (accounts?.length) {
return (
<div className="hero-section__actions-with-account">
<Select
value={`Account: ${activeAccount?.cloud_account_id}`}
options={selectOptions}
rootClassName="cloud-account-selector"
placeholder="Select AWS Account"
suffixIcon={<ChevronDown size={16} color={Color.BG_VANILLA_400} />}
optionRender={(option): JSX.Element =>
renderOption(option, activeAccount?.cloud_account_id)
}
onChange={onAccountChange}
/>
<div className="hero-section__action-buttons">
<Button
type="primary"
className="hero-section__action-button primary"
onClick={onIntegrationModalOpen}
>
Add New AWS Account
</Button>
<Button
type="default"
className="hero-section__action-button secondary"
onClick={onAccountSettingsModalOpen}
>
Account Settings
</Button>
</div>
</div>
);
}
return (
<Button
className="hero-section__action-button primary"
onClick={onIntegrationModalOpen}
>
Integrate Now
</Button>
);
}
function AccountActions(): JSX.Element {
const urlQuery = useUrlQuery();
const navigate = useNavigate();
const { data: accounts, isLoading } = useAwsAccounts();
const { data: accounts } = useAwsAccounts();
const initialAccount = useMemo(
() =>
accounts?.length
? getAccountById(accounts, urlQuery.get('cloudAccountId') || '') ||
accounts[0]
? getAccountById(accounts, urlQuery.get('accountId') || '') || accounts[0]
: null,
[accounts, urlQuery],
);
@@ -159,9 +74,8 @@ function AccountActions(): JSX.Element {
useEffect(() => {
if (initialAccount !== null) {
setActiveAccount(initialAccount);
const latestUrlQuery = new URLSearchParams(window.location.search);
latestUrlQuery.set('cloudAccountId', initialAccount.cloud_account_id);
navigate({ search: latestUrlQuery.toString() });
urlQuery.set('accountId', initialAccount.cloud_account_id);
navigate({ search: urlQuery.toString() });
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [initialAccount]);
@@ -184,35 +98,60 @@ function AccountActions(): JSX.Element {
return (
<div className="hero-section__actions">
<AccountActionsRenderer
accounts={accounts}
isLoading={isLoading}
activeAccount={activeAccount}
selectOptions={selectOptions}
onAccountChange={(value): void => {
if (accounts) {
setActiveAccount(getAccountById(accounts, value));
urlQuery.set('cloudAccountId', value);
navigate({ search: urlQuery.toString() });
}
}}
onIntegrationModalOpen={(): void => setIsIntegrationModalOpen(true)}
onAccountSettingsModalOpen={(): void => setIsAccountSettingsModalOpen(true)}
{accounts?.length ? (
<div className="hero-section__actions-with-account">
<Select
value={`Account: ${activeAccount?.cloud_account_id}`}
options={selectOptions}
rootClassName="cloud-account-selector"
placeholder="Select AWS Account"
suffixIcon={<ChevronDown size={16} color={Color.BG_VANILLA_400} />}
optionRender={(option): JSX.Element =>
renderOption(option, activeAccount?.cloud_account_id)
}
onChange={(value): void => {
setActiveAccount(getAccountById(accounts, value));
urlQuery.set('accountId', value);
navigate({ search: urlQuery.toString() });
}}
/>
<div className="hero-section__action-buttons">
<Button
type="primary"
className="hero-section__action-button primary"
onClick={(): void => setIsIntegrationModalOpen(true)}
>
Add New AWS Account
</Button>
<Button
type="default"
className="hero-section__action-button secondary"
onClick={(): void => setIsAccountSettingsModalOpen(true)}
>
Account Settings
</Button>
</div>
</div>
) : (
<Button
className="hero-section__action-button primary"
onClick={(): void => setIsIntegrationModalOpen(true)}
>
Integrate Now
</Button>
)}
<CloudAccountSetupModal
isOpen={isIntegrationModalOpen}
onClose={(): void => setIsIntegrationModalOpen(false)}
/>
{isIntegrationModalOpen && (
<CloudAccountSetupModal
onClose={(): void => setIsIntegrationModalOpen(false)}
/>
)}
{isAccountSettingsModalOpen && (
<AccountSettingsModal
onClose={(): void => setIsAccountSettingsModalOpen(false)}
account={activeAccount as CloudAccount}
setActiveAccount={setActiveAccount}
/>
)}
<AccountSettingsModal
isOpen={isAccountSettingsModalOpen}
onClose={(): void => setIsAccountSettingsModalOpen(false)}
account={activeAccount as CloudAccount}
setActiveAccount={setActiveAccount}
/>
</div>
);
}

View File

@@ -2,27 +2,27 @@ import './AccountSettingsModal.style.scss';
import { Form, Select, Switch } from 'antd';
import SignozModal from 'components/SignozModal/SignozModal';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import {
getRegionPreviewText,
useAccountSettingsModal,
} from 'hooks/integration/aws/useAccountSettingsModal';
import useUrlQuery from 'hooks/useUrlQuery';
import history from 'lib/history';
} from 'hooks/integrations/aws/useAccountSettingsModal';
import IntergrationsUninstallBar from 'pages/Integrations/IntegrationDetailPage/IntegrationsUninstallBar';
import { ConnectionStates } from 'pages/Integrations/IntegrationDetailPage/TestConnection';
import { AWS_INTEGRATION } from 'pages/Integrations/IntegrationsList';
import { Dispatch, SetStateAction, useCallback } from 'react';
import { useQueryClient } from 'react-query';
import { CloudAccount } from '../../ServicesSection/types';
import { RegionSelector } from './RegionSelector';
import RemoveIntegrationAccount from './RemoveIntegrationAccount';
interface AccountSettingsModalProps {
isOpen: boolean;
onClose: () => void;
account: CloudAccount;
setActiveAccount: Dispatch<SetStateAction<CloudAccount | null>>;
}
function AccountSettingsModal({
isOpen,
onClose,
account,
setActiveAccount,
@@ -42,16 +42,6 @@ function AccountSettingsModal({
handleClose,
} = useAccountSettingsModal({ onClose, account, setActiveAccount });
const queryClient = useQueryClient();
const urlQuery = useUrlQuery();
const handleRemoveIntegrationAccountSuccess = (): void => {
queryClient.invalidateQueries([REACT_QUERY_KEY.AWS_ACCOUNTS]);
urlQuery.delete('cloudAccountId');
handleClose();
history.replace({ search: urlQuery.toString() });
};
const renderRegionSelector = useCallback(() => {
if (isRegionSelectOpen) {
return (
@@ -130,7 +120,7 @@ function AccountSettingsModal({
return (
<SignozModal
open
open={isOpen}
title={modalTitle}
onCancel={handleClose}
onOk={handleSubmit}
@@ -174,9 +164,12 @@ function AccountSettingsModal({
</Form.Item>
<div className="integration-detail-content">
<RemoveIntegrationAccount
accountId={account?.id}
onRemoveIntegrationAccountSuccess={handleRemoveIntegrationAccountSuccess}
<IntergrationsUninstallBar
integrationTitle={AWS_INTEGRATION.title}
integrationId={AWS_INTEGRATION.id}
onUnInstallSuccess={handleClose}
removeIntegrationTitle="Remove"
connectionStatus={ConnectionStates.Connected}
/>
</div>
</div>

View File

@@ -2,11 +2,11 @@ import './CloudAccountSetupModal.style.scss';
import { Color } from '@signozhq/design-tokens';
import SignozModal from 'components/SignozModal/SignozModal';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import { useIntegrationModal } from 'hooks/integration/aws/useIntegrationModal';
import ROUTES from 'constants/routes';
import { useIntegrationModal } from 'hooks/integrations/aws/useIntegrationModal';
import { SquareArrowOutUpRight } from 'lucide-react';
import { useCallback } from 'react';
import { useQueryClient } from 'react-query';
import { useNavigate } from 'react-router-dom-v5-compat';
import {
ActiveViewEnum,
@@ -18,9 +18,9 @@ import { RegionSelector } from './RegionSelector';
import { SuccessView } from './SuccessView';
function CloudAccountSetupModal({
isOpen,
onClose,
}: IntegrationModalProps): JSX.Element {
const queryClient = useQueryClient();
const {
form,
modalState,
@@ -41,8 +41,6 @@ function CloudAccountSetupModal({
accountId,
selectedDeploymentRegion,
handleRegionChange,
connectionParams,
isConnectionParamsLoading,
} = useIntegrationModal({ onClose });
const renderContent = useCallback(() => {
@@ -73,8 +71,6 @@ function CloudAccountSetupModal({
accountId={accountId}
selectedDeploymentRegion={selectedDeploymentRegion}
handleRegionChange={handleRegionChange}
connectionParams={connectionParams}
isConnectionParamsLoading={isConnectionParamsLoading}
/>
);
}, [
@@ -90,8 +86,6 @@ function CloudAccountSetupModal({
accountId,
selectedDeploymentRegion,
handleRegionChange,
connectionParams,
isConnectionParamsLoading,
setSelectedRegions,
setIncludeAllRegions,
]);
@@ -102,6 +96,11 @@ function CloudAccountSetupModal({
[selectedRegions, allRegions],
);
const navigate = useNavigate();
const handleGoToDashboards = useCallback((): void => {
navigate(ROUTES.ALL_DASHBOARD);
}, [navigate]);
const getModalConfig = useCallback(() => {
// Handle success state first
if (modalState === ModalStateEnum.SUCCESS) {
@@ -109,14 +108,11 @@ function CloudAccountSetupModal({
title: 'AWS Webservice Integration',
okText: (
<div className="cloud-account-setup-success-view__footer-button">
Continue
Go to Dashboards
</div>
),
block: true,
onOk: (): void => {
queryClient.invalidateQueries([REACT_QUERY_KEY.AWS_ACCOUNTS]);
handleClose();
},
onOk: handleGoToDashboards,
cancelButtonProps: { style: { display: 'none' } },
disabled: false,
};
@@ -155,16 +151,15 @@ function CloudAccountSetupModal({
isLoading,
isGeneratingUrl,
activeView,
handleClose,
handleGoToDashboards,
setActiveView,
queryClient,
]);
const modalConfig = getModalConfig();
return (
<SignozModal
open
open={isOpen}
className="cloud-account-setup-modal"
title={modalConfig.title}
onCancel={handleClose}

View File

@@ -1,6 +1,6 @@
import { Form } from 'antd';
import cx from 'classnames';
import { useAccountStatus } from 'hooks/integration/aws/useAccountStatus';
import { useAccountStatus } from 'hooks/integrations/aws/useAccountStatus';
import { useRef } from 'react';
import { AccountStatusResponse } from 'types/api/integrations/aws';
import { regions } from 'utils/regions';
@@ -12,7 +12,6 @@ import {
MonitoringRegionsSection,
RegionDeploymentSection,
} from './IntegrateNowFormSections';
import RenderConnectionFields from './RenderConnectionParams';
const allRegions = (): string[] =>
regions.flatMap((r) => r.subRegions.map((sr) => sr.name));
@@ -36,8 +35,6 @@ export function RegionForm({
accountId,
selectedDeploymentRegion,
handleRegionChange,
connectionParams,
isConnectionParamsLoading,
}: RegionFormProps): JSX.Element {
const startTimeRef = useRef(Date.now());
const refetchInterval = 10 * 1000;
@@ -91,11 +88,6 @@ export function RegionForm({
isFormDisabled={isFormDisabled}
/>
<ComplianceNote />
<RenderConnectionFields
isConnectionParamsLoading={isConnectionParamsLoading}
connectionParams={connectionParams}
isFormDisabled={isFormDisabled}
/>
</div>
</Form>
);

View File

@@ -1,7 +1,7 @@
import './RegionSelector.style.scss';
import { Checkbox } from 'antd';
import { useRegionSelection } from 'hooks/integration/aws/useRegionSelection';
import { useRegionSelection } from 'hooks/integrations/aws/useRegionSelection';
import { Dispatch, SetStateAction } from 'react';
import { regions } from 'utils/regions';

View File

@@ -1,48 +0,0 @@
.remove-integration-account {
display: flex;
justify-content: space-between;
padding: 16px;
border-radius: 4px;
border: 1px solid rgba(218, 85, 101, 0.2);
background: rgba(218, 85, 101, 0.06);
&__header {
display: flex;
flex-direction: column;
gap: 6px;
}
&__title {
color: var(--bg-cherry-500);
font-size: 14px;
letter-spacing: -0.07px;
}
&__subtitle {
color: var(--bg-cherry-300);
font-size: 14px;
line-height: 22px;
letter-spacing: -0.07px;
}
&__button {
display: flex;
align-items: center;
background: var(--bg-cherry-500);
border: none;
color: var(--bg-vanilla-100);
font-size: 12px;
font-weight: 500;
line-height: 13.3px; /* 110.833% */
padding: 9px 13px;
.ant-btn-icon {
margin-inline-end: 4px !important;
}
&:hover {
&.ant-btn-default {
color: var(--bg-vanilla-300) !important;
}
}
}
}

View File

@@ -1,94 +0,0 @@
import './RemoveIntegrationAccount.scss';
import { Button, Modal } from 'antd';
import logEvent from 'api/common/logEvent';
import removeAwsIntegrationAccount from 'api/Integrations/removeAwsIntegrationAccount';
import { SOMETHING_WENT_WRONG } from 'constants/api';
import { useNotifications } from 'hooks/useNotifications';
import { X } from 'lucide-react';
import { INTEGRATION_TELEMETRY_EVENTS } from 'pages/Integrations/utils';
import { useState } from 'react';
import { useMutation } from 'react-query';
function RemoveIntegrationAccount({
accountId,
onRemoveIntegrationAccountSuccess,
}: {
accountId: string;
onRemoveIntegrationAccountSuccess: () => void;
}): JSX.Element {
const { notifications } = useNotifications();
const [isModalOpen, setIsModalOpen] = useState(false);
const showModal = (): void => {
setIsModalOpen(true);
};
const {
mutate: removeIntegration,
isLoading: isRemoveIntegrationLoading,
} = useMutation(removeAwsIntegrationAccount, {
onSuccess: () => {
onRemoveIntegrationAccountSuccess?.();
setIsModalOpen(false);
},
onError: () => {
notifications.error({
message: SOMETHING_WENT_WRONG,
});
},
});
const handleOk = (): void => {
logEvent(INTEGRATION_TELEMETRY_EVENTS.AWS_INTEGRATION_ACCOUNT_REMOVED, {
accountId,
});
removeIntegration(accountId);
};
const handleCancel = (): void => {
setIsModalOpen(false);
};
return (
<div className="remove-integration-account">
<div className="remove-integration-account__header">
<div className="remove-integration-account__title">Remove Integration</div>
<div className="remove-integration-account__subtitle">
Removing this integration won&apos;t delete any existing data but will stop
collecting new data from AWS.
</div>
</div>
<Button
className="remove-integration-account__button"
icon={<X size={14} />}
onClick={(): void => showModal()}
>
Remove
</Button>
<Modal
className="remove-integration-modal"
open={isModalOpen}
title="Remove integration"
onOk={handleOk}
onCancel={handleCancel}
okText="Remove Integration"
okButtonProps={{
danger: true,
disabled: isRemoveIntegrationLoading,
}}
>
<div className="remove-integration-modal__text">
Removing this account will remove all components created for sending
telemetry to SigNoz in your AWS account within the next ~15 minutes
(cloudformation stacks named signoz-integration-telemetry-collection in
enabled regions). <br />
<br />
After that, you can delete the cloudformation stack that was created
manually when connecting this account.
</div>
</Modal>
</div>
);
}
export default RemoveIntegrationAccount;

View File

@@ -1,71 +0,0 @@
import { Form, Input } from 'antd';
import { ConnectionParams } from 'types/api/integrations/aws';
function RenderConnectionFields({
isConnectionParamsLoading,
connectionParams,
isFormDisabled,
}: {
isConnectionParamsLoading?: boolean;
connectionParams?: ConnectionParams | null;
isFormDisabled?: boolean;
}): JSX.Element | null {
if (
isConnectionParamsLoading ||
(!!connectionParams?.ingestion_url &&
!!connectionParams?.ingestion_key &&
!!connectionParams?.signoz_api_url &&
!!connectionParams?.signoz_api_key)
) {
return null;
}
return (
<Form.Item name="connection_params">
{!connectionParams?.ingestion_url && (
<Form.Item
name="ingestion_url"
label="Ingestion URL"
rules={[{ required: true, message: 'Please enter ingestion URL' }]}
>
<Input placeholder="Enter ingestion URL" disabled={isFormDisabled} />
</Form.Item>
)}
{!connectionParams?.ingestion_key && (
<Form.Item
name="ingestion_key"
label="Ingestion Key"
rules={[{ required: true, message: 'Please enter ingestion key' }]}
>
<Input placeholder="Enter ingestion key" disabled={isFormDisabled} />
</Form.Item>
)}
{!connectionParams?.signoz_api_url && (
<Form.Item
name="signoz_api_url"
label="SigNoz API URL"
rules={[{ required: true, message: 'Please enter SigNoz API URL' }]}
>
<Input placeholder="Enter SigNoz API URL" disabled={isFormDisabled} />
</Form.Item>
)}
{!connectionParams?.signoz_api_key && (
<Form.Item
name="signoz_api_key"
label="SigNoz API KEY"
rules={[{ required: true, message: 'Please enter SigNoz API Key' }]}
>
<Input placeholder="Enter SigNoz API Key" disabled={isFormDisabled} />
</Form.Item>
)}
</Form.Item>
);
}
RenderConnectionFields.defaultProps = {
connectionParams: null,
isFormDisabled: false,
isConnectionParamsLoading: false,
};
export default RenderConnectionFields;

View File

@@ -53,18 +53,23 @@ export function SuccessView(): JSX.Element {
WHAT NEXT
</h4>
<div className="what-next-items-wrapper">
<Alert
message={
<div className="what-next-items-wrapper__item">
<div className="what-next-item-bullet-icon"></div>
<div className="what-next-item-text">
Set up your AWS services effortlessly under your enabled account.
{[
'Understand your AWS services with SigNozs out-of-the-box dashboards',
'Set up alerts for real-time monitoring.',
'Track logs and traces.',
].map((item) => (
<Alert
key={item}
message={
<div className="what-next-items-wrapper__item">
<div className="what-next-item-bullet-icon"></div>
<div className="what-next-item-text">{item}</div>
</div>
</div>
}
type="info"
className="what-next-items-wrapper__item"
/>
}
type="info"
className="what-next-items-wrapper__item"
/>
))}
</div>
</div>
</div>

View File

@@ -1,6 +1,5 @@
import { FormInstance } from 'antd';
import { Dispatch, SetStateAction } from 'react';
import { ConnectionParams } from 'types/api/integrations/aws';
export enum ActiveViewEnum {
SELECT_REGIONS = 'select-regions',
@@ -26,10 +25,9 @@ export interface RegionFormProps {
accountId?: string;
selectedDeploymentRegion: string | undefined;
handleRegionChange: (value: string) => void;
connectionParams?: ConnectionParams;
isConnectionParamsLoading?: boolean;
}
export interface IntegrationModalProps {
isOpen: boolean;
onClose: () => void;
}

View File

@@ -1,5 +1,3 @@
import { Link } from 'react-router-dom';
import { ServiceData } from './types';
function DashboardItem({
@@ -7,8 +5,8 @@ function DashboardItem({
}: {
dashboard: ServiceData['assets']['dashboards'][number];
}): JSX.Element {
const content = (
<>
return (
<div className="cloud-service-dashboard-item">
<div className="cloud-service-dashboard-item__title">{dashboard.title}</div>
<div className="cloud-service-dashboard-item__preview">
<img
@@ -17,18 +15,6 @@ function DashboardItem({
className="cloud-service-dashboard-item__preview-image"
/>
</div>
</>
);
return (
<div className="cloud-service-dashboard-item">
{dashboard.url ? (
<Link to={dashboard.url} className="cloud-service-dashboard-item__link">
{content}
</Link>
) : (
content
)}
</div>
);
}

View File

@@ -7,7 +7,7 @@ import {
ServiceConfig,
SupportedSignals,
} from 'container/CloudIntegrationPage/ServicesSection/types';
import { useUpdateServiceConfig } from 'hooks/integration/aws/useUpdateServiceConfig';
import { useUpdateServiceConfig } from 'hooks/integrations/aws/useUpdateServiceConfig';
import { useCallback, useMemo, useState } from 'react';
import { useQueryClient } from 'react-query';
@@ -34,19 +34,10 @@ function ConfigureServiceModal({
const [isLoading, setIsLoading] = useState(false);
// Track current form values
const initialValues = {
const [currentValues, setCurrentValues] = useState({
metrics: initialConfig?.metrics?.enabled || false,
logs: initialConfig?.logs?.enabled || false,
};
const [currentValues, setCurrentValues] = useState(initialValues);
const isSaveDisabled = useMemo(
() =>
// disable only if current values are same as the initial config
currentValues.metrics === initialValues.metrics &&
currentValues.logs === initialValues.logs,
[currentValues, initialValues.metrics, initialValues.logs],
);
});
const {
mutate: updateServiceConfig,
@@ -102,6 +93,11 @@ function ConfigureServiceModal({
onClose,
]);
const isSaveDisabled = useMemo(
() => currentValues.metrics === false && currentValues.logs === false,
[currentValues],
);
const handleClose = useCallback(() => {
form.resetFields();
onClose();

View File

@@ -1,3 +1,4 @@
import { Color } from '@signozhq/design-tokens';
import { Button, Tabs, TabsProps } from 'antd';
import { MarkdownRenderer } from 'components/MarkdownRenderer/MarkdownRenderer';
import Spinner from 'components/Spinner';
@@ -5,9 +6,10 @@ import CloudServiceDashboards from 'container/CloudIntegrationPage/ServicesSecti
import CloudServiceDataCollected from 'container/CloudIntegrationPage/ServicesSection/CloudServiceDataCollected';
import { IServiceStatus } from 'container/CloudIntegrationPage/ServicesSection/types';
import dayjs from 'dayjs';
import { useServiceDetails } from 'hooks/integration/aws/useServiceDetails';
import { useServiceDetails } from 'hooks/integrations/aws/useServiceDetails';
import useUrlQuery from 'hooks/useUrlQuery';
import { useMemo, useState } from 'react';
import { Wrench } from 'lucide-react';
import { useState } from 'react';
import ConfigureServiceModal from './ConfigureServiceModal';
@@ -36,7 +38,7 @@ const getStatus = (
function ServiceStatus({
serviceStatus,
}: {
serviceStatus: IServiceStatus | undefined;
serviceStatus: IServiceStatus | null;
}): JSX.Element {
const logsLastReceivedTimestamp = serviceStatus?.logs?.last_received_ts_ms;
const metricsLastReceivedTimestamp =
@@ -52,7 +54,7 @@ function ServiceStatus({
function ServiceDetails(): JSX.Element | null {
const urlQuery = useUrlQuery();
const cloudAccountId = urlQuery.get('cloudAccountId');
const accountId = urlQuery.get('accountId');
const serviceId = urlQuery.get('service');
const [isConfigureServiceModalOpen, setIsConfigureServiceModalOpen] = useState(
false,
@@ -60,24 +62,7 @@ function ServiceDetails(): JSX.Element | null {
const { data: serviceDetailsData, isLoading } = useServiceDetails(
serviceId || '',
cloudAccountId || undefined,
);
// eslint-disable-next-line @typescript-eslint/naming-convention
const { config, supported_signals } = serviceDetailsData ?? {};
const totalSupportedSignals = Object.entries(supported_signals || {}).filter(
([, value]) => !!value,
).length;
const enabledSignals = useMemo(
() =>
Object.values(config || {}).filter((item) => item && item.enabled).length,
[config],
);
const isAnySignalConfigured = useMemo(
() => !!config?.logs?.enabled || !!config?.metrics?.enabled,
[config],
accountId || undefined,
);
if (isLoading) {
@@ -111,27 +96,18 @@ function ServiceDetails(): JSX.Element | null {
<div className="service-details__title-bar">
<div className="service-details__details-title">Details</div>
<div className="service-details__right-actions">
{isAnySignalConfigured && (
{serviceDetailsData?.status && (
<ServiceStatus serviceStatus={serviceDetailsData.status} />
)}
{!!cloudAccountId &&
(isAnySignalConfigured ? (
<Button
className="configure-button configure-button--default"
onClick={(): void => setIsConfigureServiceModalOpen(true)}
>
Configure ({enabledSignals}/{totalSupportedSignals})
</Button>
) : (
<Button
type="primary"
className="configure-button configure-button--primary"
onClick={(): void => setIsConfigureServiceModalOpen(true)}
>
Enable Service
</Button>
))}
{!!accountId && (
<Button
className="configure-button"
onClick={(): void => setIsConfigureServiceModalOpen(true)}
>
<Wrench size={12} color={Color.BG_VANILLA_400} />
Configure
</Button>
)}
</div>
</div>
<div className="service-details__overview">
@@ -143,17 +119,15 @@ function ServiceDetails(): JSX.Element | null {
<div className="service-details__tabs">
<Tabs items={tabItems} />
</div>
{isConfigureServiceModalOpen && (
<ConfigureServiceModal
isOpen
onClose={(): void => setIsConfigureServiceModalOpen(false)}
serviceName={serviceDetailsData.title}
serviceId={serviceId || ''}
cloudAccountId={cloudAccountId || ''}
initialConfig={serviceDetailsData.config}
supportedSignals={serviceDetailsData.supported_signals || {}}
/>
)}
<ConfigureServiceModal
isOpen={isConfigureServiceModalOpen}
onClose={(): void => setIsConfigureServiceModalOpen(false)}
serviceName={serviceDetailsData.title}
serviceId={serviceId || ''}
cloudAccountId={accountId || ''}
initialConfig={serviceDetailsData.config}
supportedSignals={serviceDetailsData.supported_signals || {}}
/>
</div>
);
}

View File

@@ -1,35 +1,26 @@
import Spinner from 'components/Spinner';
import { useGetAccountServices } from 'hooks/integration/aws/useGetAccountServices';
import { useGetAccountServices } from 'hooks/integrations/aws/useGetAccountServices';
import useUrlQuery from 'hooks/useUrlQuery';
import { useCallback, useEffect, useMemo } from 'react';
import { useMemo } from 'react';
import { useNavigate } from 'react-router-dom-v5-compat';
import ServiceItem from './ServiceItem';
interface ServicesListProps {
cloudAccountId: string;
accountId: string;
filter: 'all_services' | 'enabled' | 'available';
}
function ServicesList({
cloudAccountId,
filter,
}: ServicesListProps): JSX.Element {
function ServicesList({ accountId, filter }: ServicesListProps): JSX.Element {
const urlQuery = useUrlQuery();
const navigate = useNavigate();
const { data: services = [], isLoading } = useGetAccountServices(
cloudAccountId,
);
const { data: services = [], isLoading } = useGetAccountServices(accountId);
const activeService = urlQuery.get('service');
const handleActiveService = useCallback(
(serviceId: string): void => {
const latestUrlQuery = new URLSearchParams(window.location.search);
latestUrlQuery.set('service', serviceId);
navigate({ search: latestUrlQuery.toString() });
},
[navigate],
);
const handleServiceClick = (serviceId: string): void => {
urlQuery.set('service', serviceId);
navigate({ search: urlQuery.toString() });
};
const filteredServices = useMemo(() => {
if (filter === 'all_services') return services;
@@ -41,12 +32,6 @@ function ServicesList({
});
}, [services, filter]);
useEffect(() => {
if (activeService || !services?.length) return;
handleActiveService(services[0].id);
}, [services, activeService, handleActiveService]);
if (isLoading) return <Spinner size="large" height="25vh" />;
if (!services) return <div>No services found</div>;
@@ -56,7 +41,7 @@ function ServicesList({
<ServiceItem
key={service.id}
service={service}
onClick={handleActiveService}
onClick={handleServiceClick}
isActive={service.id === activeService}
/>
))}

View File

@@ -135,25 +135,18 @@
color: var(--bg-cherry-400);
}
}
.configure-button {
display: flex;
align-items: center;
gap: 6px;
color: var(--bg-vanilla-400);
background: var(--bg-ink-300);
border: 1px solid var(--bg-slate-400);
border-radius: 2px;
font-size: 12px;
font-weight: 400;
line-height: 10px; /* 83.333% */
letter-spacing: 0.12px;
font-weight: 500;
width: 116px;
box-shadow: none;
&--default {
color: var(--bg-vanilla-400);
background: var(--bg-slate-400);
border: 1px solid var(--bg-slate-400);
}
&--primary {
background-color: var(--bg-robin-500);
color: var(--bg-vanilla-100);
font-weight: 500;
color: var(--Vanilla-100, #fff);
}
}
}
}

View File

@@ -3,7 +3,7 @@ import './ServicesTabs.style.scss';
import { Color } from '@signozhq/design-tokens';
import type { SelectProps, TabsProps } from 'antd';
import { Select, Tabs } from 'antd';
import { getAwsServices } from 'api/integration/aws';
import { getAwsServices } from 'api/integrations/aws';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import useUrlQuery from 'hooks/useUrlQuery';
import { ChevronDown } from 'lucide-react';
@@ -20,17 +20,17 @@ export enum ServiceFilterType {
}
interface ServicesFilterProps {
cloudAccountId: string;
accountId: string;
onFilterChange: (value: ServiceFilterType) => void;
}
function ServicesFilter({
cloudAccountId,
accountId,
onFilterChange,
}: ServicesFilterProps): JSX.Element | null {
const { data: services, isLoading } = useQuery(
[REACT_QUERY_KEY.AWS_SERVICES, cloudAccountId],
() => getAwsServices(cloudAccountId),
[REACT_QUERY_KEY.AWS_SERVICES, accountId],
() => getAwsServices(accountId),
);
const { enabledCount, availableCount } = useMemo(() => {
@@ -77,7 +77,7 @@ function ServicesFilter({
function ServicesSection(): JSX.Element {
const urlQuery = useUrlQuery();
const cloudAccountId = urlQuery.get('cloudAccountId') || '';
const accountId = urlQuery.get('accountId') || '';
const [activeFilter, setActiveFilter] = useState<
'all_services' | 'enabled' | 'available'
@@ -86,11 +86,8 @@ function ServicesSection(): JSX.Element {
return (
<div className="services-section">
<div className="services-section__sidebar">
<ServicesFilter
cloudAccountId={cloudAccountId}
onFilterChange={setActiveFilter}
/>
<ServicesList cloudAccountId={cloudAccountId} filter={activeFilter} />
<ServicesFilter accountId={accountId} onFilterChange={setActiveFilter} />
<ServicesList accountId={accountId} filter={activeFilter} />
</div>
<div className="services-section__content">
<ServiceDetails />

View File

@@ -27,12 +27,6 @@ jest.mock('uplot', () => {
};
});
jest.mock('hooks/useSafeNavigate', () => ({
useSafeNavigate: (): any => ({
safeNavigate: jest.fn(),
}),
}));
let mockWindowOpen: jest.Mock;
window.ResizeObserver =

View File

@@ -36,11 +36,6 @@ window.ResizeObserver =
unobserve: jest.fn(),
}));
jest.mock('hooks/useSafeNavigate', () => ({
useSafeNavigate: (): any => ({
safeNavigate: jest.fn(),
}),
}));
describe('Anomaly Alert Documentation Redirection', () => {
let mockWindowOpen: jest.Mock;

View File

@@ -212,46 +212,13 @@ function ExplorerOptions({
0.08,
);
const { options, handleOptionsChange } = useOptionsMenu({
storageKey:
sourcepage === DataSource.TRACES
? LOCALSTORAGE.TRACES_LIST_OPTIONS
: LOCALSTORAGE.LOGS_LIST_OPTIONS,
dataSource: sourcepage,
aggregateOperator: StringOperators.NOOP,
});
const getUpdatedExtraData = (
extraData: string | undefined,
newSelectedColumns: BaseAutocompleteData[],
): string => {
let updatedExtraData;
if (extraData) {
const parsedExtraData = JSON.parse(extraData);
parsedExtraData.selectColumns = newSelectedColumns;
updatedExtraData = JSON.stringify(parsedExtraData);
} else {
updatedExtraData = JSON.stringify({
color: Color.BG_SIENNA_500,
selectColumns: newSelectedColumns,
});
}
return updatedExtraData;
};
const updatedExtraData = getUpdatedExtraData(
extraData,
options?.selectColumns,
);
const {
mutateAsync: updateViewAsync,
isLoading: isViewUpdating,
} = useUpdateView({
compositeQuery,
viewKey,
extraData: updatedExtraData,
extraData: extraData || JSON.stringify({ color: Color.BG_SIENNA_500 }),
sourcePage: sourcepage,
viewName,
});
@@ -263,11 +230,13 @@ function ExplorerOptions({
};
const onUpdateQueryHandler = (): void => {
const extraData = viewsData?.data?.data?.find((view) => view.uuid === viewKey)
?.extraData;
updateViewAsync(
{
compositeQuery: mapCompositeQueryFromQuery(currentQuery, panelType),
viewKey,
extraData: updatedExtraData,
extraData: extraData || JSON.stringify({ color: Color.BG_SIENNA_500 }),
sourcePage: sourcepage,
viewName,
},
@@ -289,6 +258,15 @@ function ExplorerOptions({
const { handleExplorerTabChange } = useHandleExplorerTabChange();
const { options, handleOptionsChange } = useOptionsMenu({
storageKey:
sourcepage === DataSource.TRACES
? LOCALSTORAGE.TRACES_LIST_OPTIONS
: LOCALSTORAGE.LOGS_LIST_OPTIONS,
dataSource: sourcepage,
aggregateOperator: StringOperators.NOOP,
});
type ExtraData = {
selectColumns?: BaseAutocompleteData[];
version?: number;
@@ -444,11 +422,7 @@ function ExplorerOptions({
history.replace(DATASOURCE_VS_ROUTES[sourcepage]);
};
const isQueryUpdated = isStagedQueryUpdated(
viewsData?.data?.data,
viewKey,
options,
);
const isQueryUpdated = isStagedQueryUpdated(viewsData?.data?.data, viewKey);
const {
isLoading: isSaveViewLoading,

View File

@@ -17,8 +17,8 @@ import { BuilderUnitsFilter } from 'container/QueryBuilder/filters';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useShareBuilderUrl } from 'hooks/queryBuilder/useShareBuilderUrl';
import { useNotifications } from 'hooks/useNotifications';
import { useSafeNavigate } from 'hooks/useSafeNavigate';
import useUrlQuery from 'hooks/useUrlQuery';
import history from 'lib/history';
import { mapQueryDataFromApi } from 'lib/newQueryBuilder/queryBuilderMappers/mapQueryDataFromApi';
import { mapQueryDataToApi } from 'lib/newQueryBuilder/queryBuilderMappers/mapQueryDataToApi';
import { isEqual } from 'lodash-es';
@@ -87,7 +87,7 @@ function FormAlertRules({
// init namespace for translations
const { t } = useTranslation('alerts');
const { featureFlags } = useAppContext();
const { safeNavigate } = useSafeNavigate();
const { selectedTime: globalSelectedInterval } = useSelector<
AppState,
GlobalReducer
@@ -224,7 +224,7 @@ function FormAlertRules({
const generatedUrl = `${location.pathname}?${queryParams.toString()}`;
safeNavigate(generatedUrl);
history.replace(generatedUrl);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [detectionMethod]);
@@ -295,8 +295,8 @@ function FormAlertRules({
urlQuery.delete(QueryParams.panelTypes);
urlQuery.delete(QueryParams.ruleId);
urlQuery.delete(QueryParams.relativeTime);
safeNavigate(`${ROUTES.LIST_ALL_ALERT}?${urlQuery.toString()}`);
}, [safeNavigate, urlQuery]);
history.replace(`${ROUTES.LIST_ALL_ALERT}?${urlQuery.toString()}`);
}, [urlQuery]);
// onQueryCategoryChange handles changes to query category
// in state as well as sets additional defaults
@@ -515,7 +515,7 @@ function FormAlertRules({
urlQuery.delete(QueryParams.panelTypes);
urlQuery.delete(QueryParams.ruleId);
urlQuery.delete(QueryParams.relativeTime);
safeNavigate(`${ROUTES.LIST_ALL_ALERT}?${urlQuery.toString()}`);
history.replace(`${ROUTES.LIST_ALL_ALERT}?${urlQuery.toString()}`);
}, 2000);
} else {
logData = {

View File

@@ -17,7 +17,6 @@ export default function DashboardEmptyState(): JSX.Element {
selectedDashboard,
isDashboardLocked,
handleToggleDashboardSlider,
setSelectedRowWidgetId,
} = useDashboard();
const { user } = useAppContext();
@@ -35,7 +34,6 @@ export default function DashboardEmptyState(): JSX.Element {
const [addPanelPermission] = useComponentPermission(permissions, userRole);
const onEmptyWidgetHandler = useCallback(() => {
setSelectedRowWidgetId(null);
handleToggleDashboardSlider(true);
logEvent('Dashboard Detail: Add new panel clicked', {
dashboardId: selectedDashboard?.uuid,

View File

@@ -20,11 +20,11 @@ import {
import PanelWrapper from 'container/PanelWrapper/PanelWrapper';
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
import { useChartMutable } from 'hooks/useChartMutable';
import { useSafeNavigate } from 'hooks/useSafeNavigate';
import useUrlQuery from 'hooks/useUrlQuery';
import { getDashboardVariables } from 'lib/dashbaordVariables/getDashboardVariables';
import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults';
import GetMinMax from 'lib/getMinMax';
import history from 'lib/history';
import { useDashboard } from 'providers/Dashboard/Dashboard';
import { useCallback, useEffect, useRef, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
@@ -49,7 +49,6 @@ function FullView({
isDependedDataLoaded = false,
onToggleModelHandler,
}: FullViewProps): JSX.Element {
const { safeNavigate } = useSafeNavigate();
const { selectedTime: globalSelectedTime } = useSelector<
AppState,
GlobalReducer
@@ -138,9 +137,9 @@ function FullView({
urlQuery.set(QueryParams.startTime, minTime.toString());
urlQuery.set(QueryParams.endTime, maxTime.toString());
const generatedUrl = `${location.pathname}?${urlQuery.toString()}`;
safeNavigate(generatedUrl);
history.push(generatedUrl);
},
[dispatch, location.pathname, safeNavigate, urlQuery],
[dispatch, location.pathname, urlQuery],
);
const [graphsVisibilityStates, setGraphsVisibilityStates] = useState<

View File

@@ -1,239 +0,0 @@
import { fireEvent, render, screen } from '@testing-library/react';
import { PANEL_TYPES } from 'constants/queryBuilder';
import ROUTES from 'constants/routes';
import { AppProvider } from 'providers/App/App';
import MockQueryClientProvider from 'providers/test/MockQueryClientProvider';
import { Provider } from 'react-redux';
import store from 'store';
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { EQueryType } from 'types/common/dashboard';
import { DataSource } from 'types/common/queryBuilder';
import {
MENUITEM_KEYS_VS_LABELS,
MenuItemKeys,
} from '../WidgetHeader/contants';
import { WidgetGraphComponentProps } from './types';
import WidgetGraphComponent from './WidgetGraphComponent';
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useLocation: (): { pathname: string } => ({
pathname: `${process.env.FRONTEND_API_ENDPOINT}/${ROUTES.DASHBOARD}/624652db-6097-42f5-bbca-e9012901db00`,
}),
}));
jest.mock('hooks/useSafeNavigate', () => ({
useSafeNavigate: (): any => ({
safeNavigate: jest.fn(),
}),
}));
jest.mock('uplot', () => {
const paths = {
spline: jest.fn(),
bars: jest.fn(),
};
const uplotMock = jest.fn(() => ({
paths,
}));
return {
paths,
default: uplotMock,
};
});
// Mock data
const mockProps: WidgetGraphComponentProps = {
widget: {
bucketCount: 30,
bucketWidth: 0,
columnUnits: {},
description: '',
fillSpans: false,
id: '17f905f6-d355-46bd-a78e-cbc87e6f58cc',
isStacked: false,
mergeAllActiveQueries: false,
nullZeroValues: 'zero',
opacity: '1',
panelTypes: PANEL_TYPES.VALUE,
query: {
builder: {
queryData: [
{
aggregateAttribute: {
dataType: DataTypes.String,
id: 'span_id--string----true',
isColumn: true,
isJSON: false,
key: 'span_id',
type: '',
},
aggregateOperator: 'count_distinct',
dataSource: DataSource.TRACES,
disabled: false,
expression: 'A',
filters: {
items: [],
op: 'AND',
},
functions: [],
groupBy: [],
having: [],
legend: '',
limit: null,
orderBy: [],
queryName: 'A',
reduceTo: 'last',
spaceAggregation: 'sum',
stepInterval: 60,
timeAggregation: 'count_distinct',
},
],
queryFormulas: [],
},
clickhouse_sql: [
{
disabled: false,
legend: '',
name: 'A',
query: '',
},
],
id: '47449208-2c76-4465-9c62-a37fb4f5f11f',
promql: [
{
disabled: false,
legend: '',
name: 'A',
query: '',
},
],
queryType: EQueryType.QUERY_BUILDER,
},
selectedLogFields: [
{
dataType: 'string',
name: 'body',
type: '',
},
{
dataType: 'string',
name: 'timestamp',
type: '',
},
],
selectedTracesFields: [],
softMax: 0,
softMin: 0,
stackedBarChart: false,
thresholds: [],
timePreferance: 'GLOBAL_TIME',
title: 'Test Dashboard',
yAxisUnit: 'none',
},
queryResponse: {
status: 'loading',
isLoading: true,
isSuccess: false,
isError: false,
isIdle: false,
dataUpdatedAt: 0,
error: null,
errorUpdatedAt: 0,
failureCount: 0,
errorUpdateCount: 0,
isFetched: false,
isFetchedAfterMount: false,
isFetching: true,
isRefetching: false,
isLoadingError: false,
isPlaceholderData: false,
isPreviousData: false,
isRefetchError: false,
isStale: true,
data: undefined,
refetch: jest.fn(),
remove: jest.fn(),
},
errorMessage: '',
version: 'v4',
headerMenuList: [
MenuItemKeys.View,
MenuItemKeys.Clone,
MenuItemKeys.Delete,
MenuItemKeys.Edit,
MenuItemKeys.CreateAlerts,
],
isWarning: false,
isFetchingResponse: false,
setRequestData: jest.fn(),
onClickHandler: jest.fn(),
onDragSelect: jest.fn(),
openTracesButton: false,
onOpenTraceBtnClick: jest.fn(),
};
// Mock useDashabord hook
jest.mock('providers/Dashboard/Dashboard', () => ({
useDashboard: (): any => ({
selectedDashboard: {
data: {
variables: [],
},
},
}),
}));
describe('WidgetGraphComponent', () => {
it('should show correct menu items when hovering over more options while loading', async () => {
const { getByTestId, findByRole, getByText, container } = render(
<MockQueryClientProvider>
<Provider store={store}>
<AppProvider>
<WidgetGraphComponent
widget={mockProps.widget}
queryResponse={mockProps.queryResponse}
errorMessage={mockProps.errorMessage}
version={mockProps.version}
headerMenuList={mockProps.headerMenuList}
isWarning={mockProps.isWarning}
isFetchingResponse={mockProps.isFetchingResponse}
setRequestData={mockProps.setRequestData}
onClickHandler={mockProps.onClickHandler}
onDragSelect={mockProps.onDragSelect}
openTracesButton={mockProps.openTracesButton}
onOpenTraceBtnClick={mockProps.onOpenTraceBtnClick}
/>
</AppProvider>
</Provider>
</MockQueryClientProvider>,
);
expect(getByText('Test Dashboard')).toBeInTheDocument();
// check if skeleton is rendered
const skeleton = container.querySelector('.ant-skeleton');
expect(skeleton).toBeInTheDocument();
const moreOptionsButton = getByTestId('widget-header-options');
fireEvent.mouseEnter(moreOptionsButton);
const menu = await findByRole('menu');
expect(menu).toBeInTheDocument();
// Check if all menu items are present
const expectedMenuItems = [
MENUITEM_KEYS_VS_LABELS[MenuItemKeys.View],
MENUITEM_KEYS_VS_LABELS[MenuItemKeys.Clone],
MENUITEM_KEYS_VS_LABELS[MenuItemKeys.Delete],
MENUITEM_KEYS_VS_LABELS[MenuItemKeys.Edit],
MENUITEM_KEYS_VS_LABELS[MenuItemKeys.CreateAlerts],
];
// check that menu is visible
expectedMenuItems.forEach((item) => {
expect(screen.getByText(item)).toBeInTheDocument();
});
});
});

View File

@@ -6,13 +6,12 @@ import { ToggleGraphProps } from 'components/Graph/types';
import { SOMETHING_WENT_WRONG } from 'constants/api';
import { QueryParams } from 'constants/query';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { placeWidgetAtBottom } from 'container/NewWidget/utils';
import PanelWrapper from 'container/PanelWrapper/PanelWrapper';
import { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard';
import { useNotifications } from 'hooks/useNotifications';
import { useSafeNavigate } from 'hooks/useSafeNavigate';
import useUrlQuery from 'hooks/useUrlQuery';
import createQueryParams from 'lib/createQueryParams';
import history from 'lib/history';
import { RowData } from 'lib/query/createTableColumnsFromQuery';
import { useDashboard } from 'providers/Dashboard/Dashboard';
import {
@@ -49,9 +48,7 @@ function WidgetGraphComponent({
openTracesButton,
onOpenTraceBtnClick,
customSeries,
customErrorMessage,
}: WidgetGraphComponentProps): JSX.Element {
const { safeNavigate } = useSafeNavigate();
const [deleteModal, setDeleteModal] = useState(false);
const [hovered, setHovered] = useState(false);
const { notifications } = useNotifications();
@@ -135,14 +132,18 @@ function WidgetGraphComponent({
(l) => l.i === widget.id,
);
const newLayoutItem = placeWidgetAtBottom(
uuid,
selectedDashboard?.data.layout || [],
originalPanelLayout?.w || 6,
originalPanelLayout?.h || 6,
);
const layout = [...(selectedDashboard.data.layout || []), newLayoutItem];
// added the cloned panel on the top as it is given most priority when arranging
// in the layout. React_grid_layout assigns priority from top, hence no random position for cloned panel
const layout = [
{
i: uuid,
w: originalPanelLayout?.w || 6,
x: 0,
h: originalPanelLayout?.h || 6,
y: 0,
},
...(selectedDashboard.data.layout || []),
];
updateDashboardMutation.mutateAsync(
{
@@ -174,7 +175,7 @@ function WidgetGraphComponent({
graphType: widget?.panelTypes,
widgetId: uuid,
};
safeNavigate(`${pathname}/new?${createQueryParams(queryParams)}`);
history.push(`${pathname}/new?${createQueryParams(queryParams)}`);
},
},
);
@@ -195,7 +196,7 @@ function WidgetGraphComponent({
const separator = existingSearch.toString() ? '&' : '';
const newSearch = `${existingSearch}${separator}${updatedSearch}`;
safeNavigate({
history.push({
pathname,
search: newSearch,
});
@@ -222,7 +223,7 @@ function WidgetGraphComponent({
});
setGraphVisibility(localStoredVisibilityState);
}
safeNavigate({
history.push({
pathname,
search: createQueryParams(updatedQueryParams),
});
@@ -230,6 +231,21 @@ function WidgetGraphComponent({
const [searchTerm, setSearchTerm] = useState<string>('');
const loadingState =
(queryResponse.isLoading || queryResponse.status === 'idle') &&
widget.panelTypes !== PANEL_TYPES.LIST;
if (loadingState) {
return (
<Skeleton
style={{
height: '100%',
padding: '16px',
}}
/>
);
}
return (
<div
style={{
@@ -301,13 +317,6 @@ function WidgetGraphComponent({
setSearchTerm={setSearchTerm}
/>
</div>
{queryResponse.error && customErrorMessage && (
<div className="error-message-container">
<Typography.Text type="warning">{customErrorMessage}</Typography.Text>
</div>
)}
{queryResponse.isLoading && widget.panelTypes !== PANEL_TYPES.LIST && (
<Skeleton />
)}

View File

@@ -1,4 +1,3 @@
import logEvent from 'api/common/logEvent';
import { DEFAULT_ENTITY_VERSION } from 'constants/app';
import { QueryParams } from 'constants/query';
import { PANEL_TYPES } from 'constants/queryBuilder';
@@ -42,16 +41,9 @@ function GridCardGraph({
openTracesButton,
onOpenTraceBtnClick,
customSeries,
customErrorMessage,
start,
end,
analyticsEvent,
}: GridCardGraphProps): JSX.Element {
const dispatch = useDispatch();
const [errorMessage, setErrorMessage] = useState<string>();
const [isInternalServerError, setIsInternalServerError] = useState<boolean>(
false,
);
const {
toScrollWidgetId,
setToScrollWidgetId,
@@ -186,8 +178,6 @@ function GridCardGraph({
variables: getDashboardVariables(variables),
selectedTime: widget.timePreferance || 'GLOBAL_TIME',
globalSelectedInterval,
start,
end,
},
version || DEFAULT_ENTITY_VERSION,
{
@@ -217,16 +207,6 @@ function GridCardGraph({
refetchOnMount: false,
onError: (error) => {
setErrorMessage(error.message);
if (customErrorMessage) {
setIsInternalServerError(
String(error.message).includes('API responded with 500'),
);
if (analyticsEvent) {
logEvent(analyticsEvent, {
error: error.message,
});
}
}
setDashboardQueryRangeCalled(true);
},
onSettled: (data) => {
@@ -276,7 +256,6 @@ function GridCardGraph({
openTracesButton={openTracesButton}
onOpenTraceBtnClick={onOpenTraceBtnClick}
customSeries={customSeries}
customErrorMessage={isInternalServerError ? customErrorMessage : undefined}
/>
)}
</div>
@@ -290,7 +269,6 @@ GridCardGraph.defaultProps = {
threshold: undefined,
headerMenuList: [MenuItemKeys.View],
version: 'v3',
analyticsEvent: undefined,
};
export default memo(GridCardGraph);

View File

@@ -37,7 +37,6 @@ export interface WidgetGraphComponentProps {
openTracesButton?: boolean;
onOpenTraceBtnClick?: (record: RowData) => void;
customSeries?: (data: QueryData[]) => uPlot.Series[];
customErrorMessage?: string;
}
export interface GridCardGraphProps {
@@ -55,10 +54,6 @@ export interface GridCardGraphProps {
openTracesButton?: boolean;
onOpenTraceBtnClick?: (record: RowData) => void;
customSeries?: (data: QueryData[]) => uPlot.Series[];
customErrorMessage?: string;
start?: number;
end?: number;
analyticsEvent?: string;
}
export interface GetGraphVisibilityStateOnLegendClickProps {

View File

@@ -106,16 +106,6 @@
}
}
.error-message-container {
display: flex;
width: 100%;
height: 100%;
justify-content: center;
align-items: center;
padding-top: 0;
padding-bottom: 32px;
}
.row-settings {
.ant-popover-inner {
width: 191px;
@@ -133,8 +123,7 @@
.menu-content {
.section-1 {
.rename-btn,
.new-panel-btn {
.rename-btn {
display: flex;
align-items: center;
gap: 6px;
@@ -151,10 +140,6 @@
margin-inline-end: 0px;
}
}
.rename-btn {
padding-bottom: 10px;
}
}
.section-2 {

View File

@@ -15,8 +15,8 @@ import { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard';
import useComponentPermission from 'hooks/useComponentPermission';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { useNotifications } from 'hooks/useNotifications';
import { useSafeNavigate } from 'hooks/useSafeNavigate';
import useUrlQuery from 'hooks/useUrlQuery';
import history from 'lib/history';
import { defaultTo, isUndefined } from 'lodash-es';
import isEqual from 'lodash-es/isEqual';
import {
@@ -45,7 +45,6 @@ import DashboardEmptyState from './DashboardEmptyState/DashboardEmptyState';
import GridCard from './GridCard';
import { Card, CardContainer, ReactGridLayout } from './styles';
import { removeUndefinedValuesFromLayout } from './utils';
import { MenuItemKeys } from './WidgetHeader/contants';
import { WidgetRowHeader } from './WidgetRow';
interface GraphLayoutProps {
@@ -55,7 +54,6 @@ interface GraphLayoutProps {
// eslint-disable-next-line sonarjs/cognitive-complexity
function GraphLayout(props: GraphLayoutProps): JSX.Element {
const { handle } = props;
const { safeNavigate } = useSafeNavigate();
const {
selectedDashboard,
layouts,
@@ -66,8 +64,6 @@ function GraphLayout(props: GraphLayoutProps): JSX.Element {
isDashboardLocked,
dashboardQueryRangeCalled,
setDashboardQueryRangeCalled,
setSelectedRowWidgetId,
isDashboardFetching,
} = useDashboard();
const { data } = selectedDashboard || {};
const { pathname } = useLocation();
@@ -177,7 +173,6 @@ function GraphLayout(props: GraphLayoutProps): JSX.Element {
updateDashboardMutation.mutate(updatedDashboard, {
onSuccess: (updatedDashboard) => {
setSelectedRowWidgetId(null);
if (updatedDashboard.payload) {
if (updatedDashboard.payload.data.layout)
setLayouts(sortLayout(updatedDashboard.payload.data.layout));
@@ -195,7 +190,7 @@ function GraphLayout(props: GraphLayoutProps): JSX.Element {
const widgetActions = !isDashboardLocked
? [...ViewMenuAction, ...EditMenuAction]
: [...ViewMenuAction, MenuItemKeys.CreateAlerts];
: [...ViewMenuAction];
const handleLayoutChange = (layout: Layout[]): void => {
const filterLayout = removeUndefinedValuesFromLayout(layout);
@@ -216,13 +211,13 @@ function GraphLayout(props: GraphLayoutProps): JSX.Element {
urlQuery.set(QueryParams.startTime, startTimestamp.toString());
urlQuery.set(QueryParams.endTime, endTimestamp.toString());
const generatedUrl = `${pathname}?${urlQuery.toString()}`;
safeNavigate(generatedUrl);
history.push(generatedUrl);
if (startTimestamp !== endTimestamp) {
dispatch(UpdateTimeInterval('custom', [startTimestamp, endTimestamp]));
}
},
[dispatch, pathname, safeNavigate, urlQuery],
[dispatch, pathname, urlQuery],
);
useEffect(() => {
@@ -233,8 +228,7 @@ function GraphLayout(props: GraphLayoutProps): JSX.Element {
!isEqual(layouts, dashboardLayout) &&
!isDashboardLocked &&
saveLayoutPermission &&
!updateDashboardMutation.isLoading &&
!isDashboardFetching
!updateDashboardMutation.isLoading
) {
onSaveHandler();
}

View File

@@ -18,8 +18,8 @@ import { QueryParams } from 'constants/query';
import { PANEL_TYPES } from 'constants/queryBuilder';
import useCreateAlerts from 'hooks/queryBuilder/useCreateAlerts';
import useComponentPermission from 'hooks/useComponentPermission';
import { useSafeNavigate } from 'hooks/useSafeNavigate';
import useUrlQuery from 'hooks/useUrlQuery';
import history from 'lib/history';
import { RowData } from 'lib/query/createTableColumnsFromQuery';
import { isEmpty } from 'lodash-es';
import { CircleX, X } from 'lucide-react';
@@ -72,7 +72,6 @@ function WidgetHeader({
setSearchTerm,
}: IWidgetHeaderProps): JSX.Element | null {
const urlQuery = useUrlQuery();
const { safeNavigate } = useSafeNavigate();
const onEditHandler = useCallback((): void => {
const widgetId = widget.id;
urlQuery.set(QueryParams.widgetId, widgetId);
@@ -82,8 +81,8 @@ function WidgetHeader({
encodeURIComponent(JSON.stringify(widget.query)),
);
const generatedUrl = `${window.location.pathname}/new?${urlQuery}`;
safeNavigate(generatedUrl);
}, [safeNavigate, urlQuery, widget.id, widget.panelTypes, widget.query]);
history.push(generatedUrl);
}, [urlQuery, widget.id, widget.panelTypes, widget.query]);
const onCreateAlertsHandler = useCreateAlerts(widget, 'dashboardView');

View File

@@ -1,12 +1,7 @@
import { Button, Popover } from 'antd';
import useComponentPermission from 'hooks/useComponentPermission';
import { EllipsisIcon, PenLine, Plus, X } from 'lucide-react';
import { useAppContext } from 'providers/App/App';
import { useDashboard } from 'providers/Dashboard/Dashboard';
import { EllipsisIcon, PenLine, X } from 'lucide-react';
import { useState } from 'react';
import { Layout } from 'react-grid-layout';
import { ROLES, USER_ROLES } from 'types/roles';
import { ComponentTypes } from 'utils/permission';
interface WidgetRowHeaderProps {
rowWidgetProperties: {
@@ -32,23 +27,6 @@ export function WidgetRowHeader(props: WidgetRowHeaderProps): JSX.Element {
id,
} = props;
const [isRowSettingsOpen, setIsRowSettingsOpen] = useState<boolean>(false);
const {
handleToggleDashboardSlider,
selectedDashboard,
isDashboardLocked,
setSelectedRowWidgetId,
} = useDashboard();
const permissions: ComponentTypes[] = ['add_panel'];
const { user } = useAppContext();
const userRole: ROLES | null =
selectedDashboard?.created_by === user?.email
? (USER_ROLES.AUTHOR as ROLES)
: user.role;
const [addPanelPermission] = useComponentPermission(permissions, userRole);
return (
<Popover
open={isRowSettingsOpen}
@@ -74,20 +52,6 @@ export function WidgetRowHeader(props: WidgetRowHeaderProps): JSX.Element {
Rename
</Button>
</section>
<section className="section-1">
<Button
className="new-panel-btn"
type="text"
disabled={!editWidget && addPanelPermission && !isDashboardLocked}
icon={<Plus size={14} />}
onClick={(): void => {
setSelectedRowWidgetId(id);
handleToggleDashboardSlider(true);
}}
>
New Panel
</Button>
</section>
{!rowWidgetProperties.collapsed && (
<section className="section-2">
<Button

View File

@@ -1,19 +1,18 @@
import './InfraMonitoring.styles.scss';
import { initialQueriesMap } from 'constants/queryBuilder';
import { K8sCategory } from 'container/InfraMonitoringK8s/constants';
import QueryBuilderSearch from 'container/QueryBuilder/filters/QueryBuilderSearch';
import DateTimeSelectionV2 from 'container/TopNav/DateTimeSelectionV2';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useCallback, useMemo } from 'react';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import { DataSource } from 'types/common/queryBuilder';
function HostsListControls({
handleFiltersChange,
}: {
handleFiltersChange: (value: IBuilderQuery['filters']) => void;
}): JSX.Element {
const currentQuery = initialQueriesMap[DataSource.METRICS];
const { currentQuery } = useQueryBuilder();
const updatedCurrentQuery = useMemo(
() => ({
...currentQuery,

View File

@@ -141,9 +141,13 @@ function ClusterDetails({
[cluster?.meta.k8s_cluster_name],
);
const [logsAndTracesFilters, setLogsAndTracesFilters] = useState<
IBuilderQuery['filters']
>(initialFilters);
const [logFilters, setLogFilters] = useState<IBuilderQuery['filters']>(
initialFilters,
);
const [tracesFilters, setTracesFilters] = useState<IBuilderQuery['filters']>(
initialFilters,
);
const [eventsFilters, setEventsFilters] = useState<IBuilderQuery['filters']>(
initialEventsFilters,
@@ -157,7 +161,8 @@ function ClusterDetails({
}, []);
useEffect(() => {
setLogsAndTracesFilters(initialFilters);
setLogFilters(initialFilters);
setTracesFilters(initialFilters);
setEventsFilters(initialEventsFilters);
}, [initialFilters, initialEventsFilters]);
@@ -176,10 +181,6 @@ function ClusterDetails({
const handleTabChange = (e: RadioChangeEvent): void => {
setSelectedView(e.target.value);
logEvent('Infra Monitoring: Clusters list details tab changed', {
cluster: cluster?.clusterUID,
view: e.target.value,
});
};
const handleTimeChange = useCallback(
@@ -203,7 +204,6 @@ function ClusterDetails({
logEvent('Infra Monitoring: Clusters list details time updated', {
cluster: cluster?.clusterUID,
interval,
view: selectedView,
});
},
// eslint-disable-next-line react-hooks/exhaustive-deps
@@ -212,7 +212,7 @@ function ClusterDetails({
const handleChangeLogFilters = useCallback(
(value: IBuilderQuery['filters']) => {
setLogsAndTracesFilters((prevFilters) => {
setLogFilters((prevFilters) => {
const primaryFilters = prevFilters.items.filter((item) =>
[QUERY_KEYS.K8S_CLUSTER_NAME].includes(item.key?.key ?? ''),
);
@@ -244,7 +244,7 @@ function ClusterDetails({
const handleChangeTracesFilters = useCallback(
(value: IBuilderQuery['filters']) => {
setLogsAndTracesFilters((prevFilters) => {
setTracesFilters((prevFilters) => {
const primaryFilters = prevFilters.items.filter((item) =>
[QUERY_KEYS.K8S_CLUSTER_NAME].includes(item.key?.key ?? ''),
);
@@ -320,8 +320,8 @@ function ClusterDetails({
if (selectedView === VIEW_TYPES.LOGS) {
const filtersWithoutPagination = {
...logsAndTracesFilters,
items: logsAndTracesFilters.items.filter((item) => item.key?.key !== 'id'),
...logFilters,
items: logFilters.items.filter((item) => item.key?.key !== 'id'),
};
const compositeQuery = {
@@ -355,7 +355,7 @@ function ClusterDetails({
{
...initialQueryBuilderFormValuesMap.traces,
aggregateOperator: TracesAggregatorOperator.NOOP,
filters: logsAndTracesFilters,
filters: tracesFilters,
},
],
},
@@ -519,7 +519,7 @@ function ClusterDetails({
isModalTimeSelection={isModalTimeSelection}
handleTimeChange={handleTimeChange}
handleChangeLogFilters={handleChangeLogFilters}
logFilters={logsAndTracesFilters}
logFilters={logFilters}
selectedInterval={selectedInterval}
queryKey="clusterLogs"
category={K8sCategory.CLUSTERS}
@@ -532,10 +532,9 @@ function ClusterDetails({
isModalTimeSelection={isModalTimeSelection}
handleTimeChange={handleTimeChange}
handleChangeTracesFilters={handleChangeTracesFilters}
tracesFilters={logsAndTracesFilters}
tracesFilters={tracesFilters}
selectedInterval={selectedInterval}
queryKey="clusterTraces"
queryKeyFilters={[QUERY_KEYS.K8S_CLUSTER_NAME]}
/>
)}
{selectedView === VIEW_TYPES.EVENTS && (

View File

@@ -240,11 +240,6 @@ function K8sClustersList({
}
}, [selectedRowData, fetchGroupedByRowData]);
const numberOfPages = useMemo(() => Math.ceil(totalCount / pageSize), [
totalCount,
pageSize,
]);
const handleTableChange: TableProps<K8sClustersRowData>['onChange'] = useCallback(
(
pagination: TablePaginationConfig,
@@ -255,11 +250,6 @@ function K8sClustersList({
): void => {
if (pagination.current) {
setCurrentPage(pagination.current);
logEvent('Infra Monitoring: K8s clusters list page number changed', {
page: pagination.current,
pageSize,
numberOfPages,
});
}
if ('field' in sorter && sorter.order) {
@@ -271,7 +261,7 @@ function K8sClustersList({
setOrderBy(null);
}
},
[numberOfPages, pageSize],
[],
);
const { handleChangeQueryData } = useQueryOperations({
@@ -285,13 +275,15 @@ function K8sClustersList({
handleChangeQueryData('filters', value);
setCurrentPage(1);
logEvent('Infra Monitoring: K8s clusters list filters applied', {});
logEvent('Infra Monitoring: K8s list filters applied', {
filters: value,
});
},
[handleChangeQueryData],
);
useEffect(() => {
logEvent('Infra Monitoring: K8s clusters list page visited', {});
logEvent('Infra Monitoring: K8s list page visited', {});
}, []);
const selectedClusterData = useMemo(() => {
@@ -450,7 +442,6 @@ function K8sClustersList({
setCurrentPage(1);
setGroupBy(groupBy);
setExpandedRowKeys([]);
logEvent('Infra Monitoring: K8s clusters list group by changed', {});
},
[groupByFiltersData],
);
@@ -466,16 +457,6 @@ function K8sClustersList({
}
}, [groupByFiltersData]);
const onPaginationChange = (page: number, pageSize: number): void => {
setCurrentPage(page);
setPageSize(pageSize);
logEvent('Infra Monitoring: K8s clusters list page number changed', {
page,
pageSize,
numberOfPages,
});
};
return (
<div className="k8s-list">
<K8sHeader
@@ -501,7 +482,10 @@ function K8sClustersList({
total: totalCount,
showSizeChanger: true,
hideOnSinglePage: false,
onChange: onPaginationChange,
onChange: (page, pageSize): void => {
setCurrentPage(page);
setPageSize(pageSize);
},
}}
scroll={{ x: true }}
loading={{

View File

@@ -155,9 +155,13 @@ function DaemonSetDetails({
[daemonSet?.meta.k8s_daemonset_name],
);
const [logAndTracesFilters, setLogAndTracesFilters] = useState<
IBuilderQuery['filters']
>(initialFilters);
const [logFilters, setLogFilters] = useState<IBuilderQuery['filters']>(
initialFilters,
);
const [tracesFilters, setTracesFilters] = useState<IBuilderQuery['filters']>(
initialFilters,
);
const [eventsFilters, setEventsFilters] = useState<IBuilderQuery['filters']>(
initialEventsFilters,
@@ -171,7 +175,8 @@ function DaemonSetDetails({
}, []);
useEffect(() => {
setLogAndTracesFilters(initialFilters);
setLogFilters(initialFilters);
setTracesFilters(initialFilters);
setEventsFilters(initialEventsFilters);
}, [initialFilters, initialEventsFilters]);
@@ -190,10 +195,6 @@ function DaemonSetDetails({
const handleTabChange = (e: RadioChangeEvent): void => {
setSelectedView(e.target.value);
logEvent('Infra Monitoring: DaemonSets list details tab changed', {
daemonSet: daemonSet?.daemonSetName,
view: e.target.value,
});
};
const handleTimeChange = useCallback(
@@ -217,7 +218,6 @@ function DaemonSetDetails({
logEvent('Infra Monitoring: DaemonSets list details time updated', {
daemonSet: daemonSet?.daemonSetName,
interval,
view: selectedView,
});
},
// eslint-disable-next-line react-hooks/exhaustive-deps
@@ -226,7 +226,7 @@ function DaemonSetDetails({
const handleChangeLogFilters = useCallback(
(value: IBuilderQuery['filters']) => {
setLogAndTracesFilters((prevFilters) => {
setLogFilters((prevFilters) => {
const primaryFilters = prevFilters.items.filter((item) =>
[QUERY_KEYS.K8S_DAEMON_SET_NAME, QUERY_KEYS.K8S_NAMESPACE_NAME].includes(
item.key?.key ?? '',
@@ -259,7 +259,7 @@ function DaemonSetDetails({
const handleChangeTracesFilters = useCallback(
(value: IBuilderQuery['filters']) => {
setLogAndTracesFilters((prevFilters) => {
setTracesFilters((prevFilters) => {
const primaryFilters = prevFilters.items.filter((item) =>
[QUERY_KEYS.K8S_DAEMON_SET_NAME, QUERY_KEYS.K8S_NAMESPACE_NAME].includes(
item.key?.key ?? '',
@@ -339,8 +339,8 @@ function DaemonSetDetails({
if (selectedView === VIEW_TYPES.LOGS) {
const filtersWithoutPagination = {
...logAndTracesFilters,
items: logAndTracesFilters.items.filter((item) => item.key?.key !== 'id'),
...logFilters,
items: logFilters.items.filter((item) => item.key?.key !== 'id'),
};
const compositeQuery = {
@@ -374,7 +374,7 @@ function DaemonSetDetails({
{
...initialQueryBuilderFormValuesMap.traces,
aggregateOperator: TracesAggregatorOperator.NOOP,
filters: logAndTracesFilters,
filters: tracesFilters,
},
],
},
@@ -551,7 +551,7 @@ function DaemonSetDetails({
isModalTimeSelection={isModalTimeSelection}
handleTimeChange={handleTimeChange}
handleChangeLogFilters={handleChangeLogFilters}
logFilters={logAndTracesFilters}
logFilters={logFilters}
selectedInterval={selectedInterval}
category={K8sCategory.DAEMONSETS}
queryKey="daemonsetLogs"
@@ -567,13 +567,9 @@ function DaemonSetDetails({
isModalTimeSelection={isModalTimeSelection}
handleTimeChange={handleTimeChange}
handleChangeTracesFilters={handleChangeTracesFilters}
tracesFilters={logAndTracesFilters}
tracesFilters={tracesFilters}
selectedInterval={selectedInterval}
queryKey="daemonsetTraces"
queryKeyFilters={[
QUERY_KEYS.K8S_DAEMON_SET_NAME,
QUERY_KEYS.K8S_NAMESPACE_NAME,
]}
/>
)}
{selectedView === VIEW_TYPES.EVENTS && (

View File

@@ -243,11 +243,6 @@ function K8sDaemonSetsList({
}
}, [selectedRowData, fetchGroupedByRowData]);
const numberOfPages = useMemo(() => Math.ceil(totalCount / pageSize), [
totalCount,
pageSize,
]);
const handleTableChange: TableProps<K8sDaemonSetsRowData>['onChange'] = useCallback(
(
pagination: TablePaginationConfig,
@@ -258,11 +253,6 @@ function K8sDaemonSetsList({
): void => {
if (pagination.current) {
setCurrentPage(pagination.current);
logEvent('Infra Monitoring: K8s daemonSets list page number changed', {
page: pagination.current,
pageSize,
numberOfPages,
});
}
if ('field' in sorter && sorter.order) {
@@ -274,7 +264,7 @@ function K8sDaemonSetsList({
setOrderBy(null);
}
},
[numberOfPages, pageSize],
[],
);
const { handleChangeQueryData } = useQueryOperations({
@@ -288,13 +278,15 @@ function K8sDaemonSetsList({
handleChangeQueryData('filters', value);
setCurrentPage(1);
logEvent('Infra Monitoring: K8s daemonSets list filters applied', {});
logEvent('Infra Monitoring: K8s list filters applied', {
filters: value,
});
},
[handleChangeQueryData],
);
useEffect(() => {
logEvent('Infra Monitoring: K8s daemonSets list page visited', {});
logEvent('Infra Monitoring: K8s list page visited', {});
}, []);
const selectedDaemonSetData = useMemo(() => {
@@ -456,8 +448,6 @@ function K8sDaemonSetsList({
setCurrentPage(1);
setGroupBy(groupBy);
setExpandedRowKeys([]);
logEvent('Infra Monitoring: K8s daemonSets list group by changed', {});
},
[groupByFiltersData],
);
@@ -473,16 +463,6 @@ function K8sDaemonSetsList({
}
}, [groupByFiltersData]);
const onPaginationChange = (page: number, pageSize: number): void => {
setCurrentPage(page);
setPageSize(pageSize);
logEvent('Infra Monitoring: K8s daemonSets list page number changed', {
page,
pageSize,
numberOfPages,
});
};
return (
<div className="k8s-list">
<K8sHeader
@@ -510,7 +490,10 @@ function K8sDaemonSetsList({
total: totalCount,
showSizeChanger: true,
hideOnSinglePage: false,
onChange: onPaginationChange,
onChange: (page, pageSize): void => {
setCurrentPage(page);
setPageSize(pageSize);
},
}}
scroll={{ x: true }}
loading={{

View File

@@ -157,9 +157,13 @@ function DeploymentDetails({
[deployment?.meta.k8s_deployment_name],
);
const [logAndTracesFilters, setLogAndTracesFilters] = useState<
IBuilderQuery['filters']
>(initialFilters);
const [logFilters, setLogFilters] = useState<IBuilderQuery['filters']>(
initialFilters,
);
const [tracesFilters, setTracesFilters] = useState<IBuilderQuery['filters']>(
initialFilters,
);
const [eventsFilters, setEventsFilters] = useState<IBuilderQuery['filters']>(
initialEventsFilters,
@@ -173,7 +177,8 @@ function DeploymentDetails({
}, []);
useEffect(() => {
setLogAndTracesFilters(initialFilters);
setLogFilters(initialFilters);
setTracesFilters(initialFilters);
setEventsFilters(initialEventsFilters);
}, [initialFilters, initialEventsFilters]);
@@ -192,10 +197,6 @@ function DeploymentDetails({
const handleTabChange = (e: RadioChangeEvent): void => {
setSelectedView(e.target.value);
logEvent('Infra Monitoring: Deployments list details tab changed', {
deployment: deployment?.deploymentName,
view: e.target.value,
});
};
const handleTimeChange = useCallback(
@@ -219,7 +220,6 @@ function DeploymentDetails({
logEvent('Infra Monitoring: Deployments list details time updated', {
deployment: deployment?.deploymentName,
interval,
view: selectedView,
});
},
// eslint-disable-next-line react-hooks/exhaustive-deps
@@ -228,7 +228,7 @@ function DeploymentDetails({
const handleChangeLogFilters = useCallback(
(value: IBuilderQuery['filters']) => {
setLogAndTracesFilters((prevFilters) => {
setLogFilters((prevFilters) => {
const primaryFilters = prevFilters.items.filter((item) =>
[QUERY_KEYS.K8S_DEPLOYMENT_NAME, QUERY_KEYS.K8S_NAMESPACE_NAME].includes(
item.key?.key ?? '',
@@ -266,7 +266,7 @@ function DeploymentDetails({
const handleChangeTracesFilters = useCallback(
(value: IBuilderQuery['filters']) => {
setLogAndTracesFilters((prevFilters) => {
setTracesFilters((prevFilters) => {
const primaryFilters = prevFilters.items.filter((item) =>
[QUERY_KEYS.K8S_DEPLOYMENT_NAME, QUERY_KEYS.K8S_NAMESPACE_NAME].includes(
item.key?.key ?? '',
@@ -350,8 +350,8 @@ function DeploymentDetails({
if (selectedView === VIEW_TYPES.LOGS) {
const filtersWithoutPagination = {
...logAndTracesFilters,
items: logAndTracesFilters.items.filter((item) => item.key?.key !== 'id'),
...logFilters,
items: logFilters.items.filter((item) => item.key?.key !== 'id'),
};
const compositeQuery = {
@@ -385,7 +385,7 @@ function DeploymentDetails({
{
...initialQueryBuilderFormValuesMap.traces,
aggregateOperator: TracesAggregatorOperator.NOOP,
filters: logAndTracesFilters,
filters: tracesFilters,
},
],
},
@@ -562,7 +562,7 @@ function DeploymentDetails({
isModalTimeSelection={isModalTimeSelection}
handleTimeChange={handleTimeChange}
handleChangeLogFilters={handleChangeLogFilters}
logFilters={logAndTracesFilters}
logFilters={logFilters}
selectedInterval={selectedInterval}
queryKeyFilters={[
QUERY_KEYS.K8S_DEPLOYMENT_NAME,
@@ -578,13 +578,9 @@ function DeploymentDetails({
isModalTimeSelection={isModalTimeSelection}
handleTimeChange={handleTimeChange}
handleChangeTracesFilters={handleChangeTracesFilters}
tracesFilters={logAndTracesFilters}
tracesFilters={tracesFilters}
selectedInterval={selectedInterval}
queryKey="deploymentTraces"
queryKeyFilters={[
QUERY_KEYS.K8S_DEPLOYMENT_NAME,
QUERY_KEYS.K8S_NAMESPACE_NAME,
]}
/>
)}
{selectedView === VIEW_TYPES.EVENTS && (

View File

@@ -245,11 +245,6 @@ function K8sDeploymentsList({
}
}, [selectedRowData, fetchGroupedByRowData]);
const numberOfPages = useMemo(() => Math.ceil(totalCount / pageSize), [
totalCount,
pageSize,
]);
const handleTableChange: TableProps<K8sDeploymentsRowData>['onChange'] = useCallback(
(
pagination: TablePaginationConfig,
@@ -260,11 +255,6 @@ function K8sDeploymentsList({
): void => {
if (pagination.current) {
setCurrentPage(pagination.current);
logEvent('Infra Monitoring: K8s deployments list page number changed', {
page: pagination.current,
pageSize,
numberOfPages,
});
}
if ('field' in sorter && sorter.order) {
@@ -276,7 +266,7 @@ function K8sDeploymentsList({
setOrderBy(null);
}
},
[numberOfPages, pageSize],
[],
);
const { handleChangeQueryData } = useQueryOperations({
@@ -290,13 +280,15 @@ function K8sDeploymentsList({
handleChangeQueryData('filters', value);
setCurrentPage(1);
logEvent('Infra Monitoring: K8s deployments list filters applied', {});
logEvent('Infra Monitoring: K8s list filters applied', {
filters: value,
});
},
[handleChangeQueryData],
);
useEffect(() => {
logEvent('Infra Monitoring: K8s deployments list page visited', {});
logEvent('Infra Monitoring: K8s list page visited', {});
}, []);
const selectedDeploymentData = useMemo(() => {
@@ -460,8 +452,6 @@ function K8sDeploymentsList({
setCurrentPage(1);
setGroupBy(groupBy);
setExpandedRowKeys([]);
logEvent('Infra Monitoring: K8s deployments list group by changed', {});
},
[groupByFiltersData],
);
@@ -477,16 +467,6 @@ function K8sDeploymentsList({
}
}, [groupByFiltersData]);
const onPaginationChange = (page: number, pageSize: number): void => {
setCurrentPage(page);
setPageSize(pageSize);
logEvent('Infra Monitoring: K8s deployments list page number changed', {
page,
pageSize,
numberOfPages,
});
};
return (
<div className="k8s-list">
<K8sHeader
@@ -514,7 +494,10 @@ function K8sDeploymentsList({
total: totalCount,
showSizeChanger: true,
hideOnSinglePage: false,
onChange: onPaginationChange,
onChange: (page, pageSize): void => {
setCurrentPage(page);
setPageSize(pageSize);
},
}}
scroll={{ x: true }}
loading={{

View File

@@ -7,6 +7,7 @@ import { DEFAULT_ENTITY_VERSION } from 'constants/app';
import { EventContents } from 'container/InfraMonitoringK8s/commonUtils';
import { K8sCategory } from 'container/InfraMonitoringK8s/constants';
import LoadingContainer from 'container/InfraMonitoringK8s/LoadingContainer';
import { INITIAL_PAGE_SIZE } from 'container/LogsContextList/configs';
import LogsError from 'container/LogsError/LogsError';
import { ORDERBY_FILTERS } from 'container/QueryBuilder/filters/OrderByFilter/config';
import QueryBuilderSearch from 'container/QueryBuilder/filters/QueryBuilderSearch';
@@ -21,10 +22,8 @@ import { isArray } from 'lodash-es';
import { ChevronDown, ChevronLeft, ChevronRight, Loader2 } from 'lucide-react';
import { useEffect, useMemo, useState } from 'react';
import { useQuery } from 'react-query';
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import { DataSource } from 'types/common/queryBuilder';
import { v4 } from 'uuid';
import {
EntityDetailsEmptyContainer,
@@ -123,16 +122,19 @@ export default function Events({
filters,
);
basePayload.query.builder.queryData[0].pageSize = 10;
basePayload.query.builder.queryData[0].pageSize = INITIAL_PAGE_SIZE;
basePayload.query.builder.queryData[0].offset =
(page - 1) * INITIAL_PAGE_SIZE;
basePayload.query.builder.queryData[0].orderBy = [
{ columnName: 'timestamp', order: ORDERBY_FILTERS.DESC },
{ columnName: 'id', order: ORDERBY_FILTERS.DESC },
];
return basePayload;
}, [timeRange.startTime, timeRange.endTime, filters]);
}, [timeRange.startTime, timeRange.endTime, filters, page]);
const { data: eventsData, isLoading, isFetching, isError } = useQuery({
queryKey: [queryKey, timeRange.startTime, timeRange.endTime, filters],
queryKey: [queryKey, timeRange.startTime, timeRange.endTime, filters, page],
queryFn: () => GetMetricQueryRange(queryPayload, DEFAULT_ENTITY_VERSION),
enabled: !!queryPayload,
});
@@ -189,61 +191,12 @@ export default function Events({
const handlePrev = (): void => {
if (!formattedEntityEvents.length) return;
setPage(page - 1);
const firstEvent = formattedEntityEvents[0];
const newItems = [
...filters.items.filter((item) => item.key?.key !== 'id'),
{
id: v4(),
key: {
key: 'id',
type: '',
dataType: DataTypes.String,
isColumn: true,
},
op: '>',
value: firstEvent.id,
},
];
const newFilters = {
op: 'AND',
items: newItems,
} as IBuilderQuery['filters'];
handleChangeEventFilters(newFilters);
};
const handleNext = (): void => {
if (!formattedEntityEvents.length) return;
setPage(page + 1);
const lastEvent = formattedEntityEvents[formattedEntityEvents.length - 1];
const newItems = [
...filters.items.filter((item) => item.key?.key !== 'id'),
{
id: v4(),
key: {
key: 'id',
type: '',
dataType: DataTypes.String,
isColumn: true,
},
op: '<',
value: lastEvent.id,
},
];
const newFilters = {
op: 'AND',
items: newItems,
} as IBuilderQuery['filters'];
handleChangeEventFilters(newFilters);
};
const handleExpandRowIcon = ({

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