diff --git a/deploy/docker-swarm/clickhouse-setup/docker-compose.yaml b/deploy/docker-swarm/clickhouse-setup/docker-compose.yaml index edc5ead22c..38581568dc 100644 --- a/deploy/docker-swarm/clickhouse-setup/docker-compose.yaml +++ b/deploy/docker-swarm/clickhouse-setup/docker-compose.yaml @@ -146,7 +146,7 @@ services: condition: on-failure query-service: - image: signoz/query-service:0.39.0 + image: signoz/query-service:0.39.1 command: [ "-config=/root/config/prometheus.yml", @@ -186,7 +186,7 @@ services: <<: *db-depend frontend: - image: signoz/frontend:0.39.0 + image: signoz/frontend:0.39.1 deploy: restart_policy: condition: on-failure @@ -199,7 +199,7 @@ services: - ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf otel-collector: - image: signoz/signoz-otel-collector:0.88.12 + image: signoz/signoz-otel-collector:0.88.13 command: [ "--config=/etc/otel-collector-config.yaml", @@ -237,7 +237,7 @@ services: - query-service otel-collector-migrator: - image: signoz/signoz-schema-migrator:0.88.12 + image: signoz/signoz-schema-migrator:0.88.13 deploy: restart_policy: condition: on-failure diff --git a/deploy/docker-swarm/clickhouse-setup/otel-collector-config.yaml b/deploy/docker-swarm/clickhouse-setup/otel-collector-config.yaml index 8f13b6506f..f8a710d535 100644 --- a/deploy/docker-swarm/clickhouse-setup/otel-collector-config.yaml +++ b/deploy/docker-swarm/clickhouse-setup/otel-collector-config.yaml @@ -98,6 +98,7 @@ processors: latency_histogram_buckets: [100us, 1ms, 2ms, 6ms, 10ms, 50ms, 100ms, 250ms, 500ms, 1000ms, 1400ms, 2000ms, 5s, 10s, 20s, 40s, 60s ] dimensions_cache_size: 100000 aggregation_temporality: AGGREGATION_TEMPORALITY_DELTA + enable_exp_histogram: true dimensions: - name: service.namespace default: default diff --git a/deploy/docker/clickhouse-setup/docker-compose-core.yaml b/deploy/docker/clickhouse-setup/docker-compose-core.yaml index 61e03804f4..525fa5175d 100644 --- a/deploy/docker/clickhouse-setup/docker-compose-core.yaml +++ b/deploy/docker/clickhouse-setup/docker-compose-core.yaml @@ -66,7 +66,7 @@ services: - --storage.path=/data otel-collector-migrator: - image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.88.12} + image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.88.13} container_name: otel-migrator command: - "--dsn=tcp://clickhouse:9000" @@ -81,7 +81,7 @@ services: # Notes for Maintainers/Contributors who will change Line Numbers of Frontend & Query-Section. Please Update Line Numbers in `./scripts/commentLinesForSetup.sh` & `./CONTRIBUTING.md` otel-collector: container_name: signoz-otel-collector - image: signoz/signoz-otel-collector:0.88.12 + image: signoz/signoz-otel-collector:0.88.13 command: [ "--config=/etc/otel-collector-config.yaml", diff --git a/deploy/docker/clickhouse-setup/docker-compose.yaml b/deploy/docker/clickhouse-setup/docker-compose.yaml index cb77c4c024..b0d11fbaf5 100644 --- a/deploy/docker/clickhouse-setup/docker-compose.yaml +++ b/deploy/docker/clickhouse-setup/docker-compose.yaml @@ -164,7 +164,7 @@ services: # Notes for Maintainers/Contributors who will change Line Numbers of Frontend & Query-Section. Please Update Line Numbers in `./scripts/commentLinesForSetup.sh` & `./CONTRIBUTING.md` query-service: - image: signoz/query-service:${DOCKER_TAG:-0.39.0} + image: signoz/query-service:${DOCKER_TAG:-0.39.1} container_name: signoz-query-service command: [ @@ -203,7 +203,7 @@ services: <<: *db-depend frontend: - image: signoz/frontend:${DOCKER_TAG:-0.39.0} + image: signoz/frontend:${DOCKER_TAG:-0.39.1} container_name: signoz-frontend restart: on-failure depends_on: @@ -215,7 +215,7 @@ services: - ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf otel-collector-migrator: - image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.88.12} + image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.88.13} container_name: otel-migrator command: - "--dsn=tcp://clickhouse:9000" @@ -229,7 +229,7 @@ services: otel-collector: - image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.88.12} + image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.88.13} container_name: signoz-otel-collector command: [ diff --git a/deploy/docker/clickhouse-setup/otel-collector-config.yaml b/deploy/docker/clickhouse-setup/otel-collector-config.yaml index 8211364efb..d382a252e5 100644 --- a/deploy/docker/clickhouse-setup/otel-collector-config.yaml +++ b/deploy/docker/clickhouse-setup/otel-collector-config.yaml @@ -101,6 +101,7 @@ processors: latency_histogram_buckets: [100us, 1ms, 2ms, 6ms, 10ms, 50ms, 100ms, 250ms, 500ms, 1000ms, 1400ms, 2000ms, 5s, 10s, 20s, 40s, 60s ] dimensions_cache_size: 100000 aggregation_temporality: AGGREGATION_TEMPORALITY_DELTA + enable_exp_histogram: true dimensions: - name: service.namespace default: default diff --git a/ee/query-service/app/api/api.go b/ee/query-service/app/api/api.go index 22f3ee67c2..32bb22435f 100644 --- a/ee/query-service/app/api/api.go +++ b/ee/query-service/app/api/api.go @@ -152,9 +152,10 @@ func (ah *APIHandler) RegisterRoutes(router *mux.Router, am *baseapp.AuthMiddlew router.HandleFunc("/api/v2/metrics/query_range", am.ViewAccess(ah.queryRangeMetricsV2)).Methods(http.MethodPost) // PAT APIs - router.HandleFunc("/api/v1/pat", am.AdminAccess(ah.createPAT)).Methods(http.MethodPost) - router.HandleFunc("/api/v1/pat", am.AdminAccess(ah.getPATs)).Methods(http.MethodGet) - router.HandleFunc("/api/v1/pat/{id}", am.AdminAccess(ah.deletePAT)).Methods(http.MethodDelete) + router.HandleFunc("/api/v1/pats", am.AdminAccess(ah.createPAT)).Methods(http.MethodPost) + router.HandleFunc("/api/v1/pats", am.AdminAccess(ah.getPATs)).Methods(http.MethodGet) + router.HandleFunc("/api/v1/pats/{id}", am.AdminAccess(ah.updatePAT)).Methods(http.MethodPut) + router.HandleFunc("/api/v1/pats/{id}", am.AdminAccess(ah.revokePAT)).Methods(http.MethodDelete) router.HandleFunc("/api/v1/checkout", am.AdminAccess(ah.checkout)).Methods(http.MethodPost) router.HandleFunc("/api/v1/billing", am.AdminAccess(ah.getBilling)).Methods(http.MethodGet) diff --git a/ee/query-service/app/api/pat.go b/ee/query-service/app/api/pat.go index b0fcf073a4..49ed36f092 100644 --- a/ee/query-service/app/api/pat.go +++ b/ee/query-service/app/api/pat.go @@ -12,6 +12,7 @@ import ( "github.com/gorilla/mux" "go.signoz.io/signoz/ee/query-service/model" "go.signoz.io/signoz/pkg/query-service/auth" + baseconstants "go.signoz.io/signoz/pkg/query-service/constants" basemodel "go.signoz.io/signoz/pkg/query-service/model" "go.uber.org/zap" ) @@ -28,7 +29,7 @@ func generatePATToken() string { func (ah *APIHandler) createPAT(w http.ResponseWriter, r *http.Request) { ctx := context.Background() - req := model.PAT{} + req := model.CreatePATRequestBody{} if err := json.NewDecoder(r.Body).Decode(&req); err != nil { RespondError(w, model.BadRequest(err), nil) return @@ -41,30 +42,87 @@ func (ah *APIHandler) createPAT(w http.ResponseWriter, r *http.Request) { }, nil) return } - - // All the PATs are associated with the user creating the PAT. Hence, the permissions - // associated with the PAT is also equivalent to that of the user. - req.UserID = user.Id - req.CreatedAt = time.Now().Unix() - req.Token = generatePATToken() - - // default expiry is 30 days - if req.ExpiresAt == 0 { - req.ExpiresAt = time.Now().AddDate(0, 0, 30).Unix() + pat := model.PAT{ + Name: req.Name, + Role: req.Role, + ExpiresAt: req.ExpiresInDays, } - // max expiry is 1 year - if req.ExpiresAt > time.Now().AddDate(1, 0, 0).Unix() { - req.ExpiresAt = time.Now().AddDate(1, 0, 0).Unix() + err = validatePATRequest(pat) + if err != nil { + RespondError(w, model.BadRequest(err), nil) + return } - zap.S().Debugf("Got PAT request: %+v", req) + // All the PATs are associated with the user creating the PAT. + pat.UserID = user.Id + pat.CreatedAt = time.Now().Unix() + pat.UpdatedAt = time.Now().Unix() + pat.LastUsed = 0 + pat.Token = generatePATToken() + + if pat.ExpiresAt != 0 { + // convert expiresAt to unix timestamp from days + pat.ExpiresAt = time.Now().Unix() + (pat.ExpiresAt * 24 * 60 * 60) + } + + zap.S().Debugf("Got Create PAT request: %+v", pat) var apierr basemodel.BaseApiError - if req, apierr = ah.AppDao().CreatePAT(ctx, req); apierr != nil { + if pat, apierr = ah.AppDao().CreatePAT(ctx, pat); apierr != nil { RespondError(w, apierr, nil) return } - ah.Respond(w, &req) + ah.Respond(w, &pat) +} + +func validatePATRequest(req model.PAT) error { + if req.Role == "" || (req.Role != baseconstants.ViewerGroup && req.Role != baseconstants.EditorGroup && req.Role != baseconstants.AdminGroup) { + return fmt.Errorf("valid role is required") + } + if req.ExpiresAt < 0 { + return fmt.Errorf("valid expiresAt is required") + } + if req.Name == "" { + return fmt.Errorf("valid name is required") + } + return nil +} + +func (ah *APIHandler) updatePAT(w http.ResponseWriter, r *http.Request) { + ctx := context.Background() + + req := model.PAT{} + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + RespondError(w, model.BadRequest(err), nil) + return + } + + user, err := auth.GetUserFromRequest(r) + if err != nil { + RespondError(w, &model.ApiError{ + Typ: model.ErrorUnauthorized, + Err: err, + }, nil) + return + } + + err = validatePATRequest(req) + if err != nil { + RespondError(w, model.BadRequest(err), nil) + return + } + + req.UpdatedByUserID = user.Id + id := mux.Vars(r)["id"] + req.UpdatedAt = time.Now().Unix() + zap.S().Debugf("Got Update PAT request: %+v", req) + var apierr basemodel.BaseApiError + if apierr = ah.AppDao().UpdatePAT(ctx, req, id); apierr != nil { + RespondError(w, apierr, nil) + return + } + + ah.Respond(w, map[string]string{"data": "pat updated successfully"}) } func (ah *APIHandler) getPATs(w http.ResponseWriter, r *http.Request) { @@ -86,7 +144,7 @@ func (ah *APIHandler) getPATs(w http.ResponseWriter, r *http.Request) { ah.Respond(w, pats) } -func (ah *APIHandler) deletePAT(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.GetUserFromRequest(r) @@ -105,14 +163,14 @@ func (ah *APIHandler) deletePAT(w http.ResponseWriter, r *http.Request) { if pat.UserID != user.Id { RespondError(w, &model.ApiError{ Typ: model.ErrorUnauthorized, - Err: fmt.Errorf("unauthorized PAT delete request"), + Err: fmt.Errorf("unauthorized PAT revoke request"), }, nil) return } - zap.S().Debugf("Delete PAT with id: %+v", id) - if apierr := ah.AppDao().DeletePAT(ctx, id); apierr != nil { + zap.S().Debugf("Revoke PAT with id: %+v", id) + if apierr := ah.AppDao().RevokePAT(ctx, id, user.Id); apierr != nil { RespondError(w, apierr, nil) return } - ah.Respond(w, map[string]string{"data": "pat deleted successfully"}) + ah.Respond(w, map[string]string{"data": "pat revoked successfully"}) } diff --git a/ee/query-service/app/server.go b/ee/query-service/app/server.go index ea0b0344ad..f8c7633417 100644 --- a/ee/query-service/app/server.go +++ b/ee/query-service/app/server.go @@ -20,10 +20,11 @@ import ( "github.com/soheilhy/cmux" "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" "go.signoz.io/signoz/ee/query-service/constants" "go.signoz.io/signoz/ee/query-service/dao" "go.signoz.io/signoz/ee/query-service/interfaces" - "go.signoz.io/signoz/pkg/query-service/auth" + baseauth "go.signoz.io/signoz/pkg/query-service/auth" baseInterface "go.signoz.io/signoz/pkg/query-service/interfaces" v3 "go.signoz.io/signoz/pkg/query-service/model/v3" @@ -37,7 +38,6 @@ import ( "go.signoz.io/signoz/pkg/query-service/app/logparsingpipeline" "go.signoz.io/signoz/pkg/query-service/app/opamp" opAmpModel "go.signoz.io/signoz/pkg/query-service/app/opamp/model" - baseauth "go.signoz.io/signoz/pkg/query-service/auth" "go.signoz.io/signoz/pkg/query-service/cache" baseconst "go.signoz.io/signoz/pkg/query-service/constants" "go.signoz.io/signoz/pkg/query-service/healthcheck" @@ -304,25 +304,12 @@ func (s *Server) createPublicServer(apiHandler *api.APIHandler) (*http.Server, e r := mux.NewRouter() + // add auth middleware getUserFromRequest := func(r *http.Request) (*basemodel.UserPayload, error) { - patToken := r.Header.Get("SIGNOZ-API-KEY") - if len(patToken) > 0 { - zap.S().Debugf("Received a non-zero length PAT token") - ctx := context.Background() - dao := apiHandler.AppDao() - - user, err := dao.GetUserByPAT(ctx, patToken) - if err == nil && user != nil { - zap.S().Debugf("Found valid PAT user: %+v", user) - return user, nil - } - if err != nil { - zap.S().Debugf("Error while getting user for PAT: %+v", err) - } - } - return baseauth.GetUserFromRequest(r) + return auth.GetUserFromRequest(r, apiHandler) } am := baseapp.NewAuthMiddleware(getUserFromRequest) + r.Use(setTimeoutMiddleware) r.Use(s.analyticsMiddleware) r.Use(loggingMiddleware) @@ -439,7 +426,7 @@ func extractQueryRangeV3Data(path string, r *http.Request) (map[string]interface telemetry.GetInstance().AddActiveLogsUser() } data["dataSources"] = dataSources - userEmail, err := auth.GetEmailFromJwt(r.Context()) + userEmail, err := baseauth.GetEmailFromJwt(r.Context()) if err == nil { telemetry.GetInstance().SendEvent(telemetry.TELEMETRY_EVENT_QUERY_RANGE_V3, data, userEmail, true) } @@ -463,7 +450,7 @@ func getActiveLogs(path string, r *http.Request) { func (s *Server) analyticsMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - ctx := auth.AttachJwtToContext(r.Context(), r) + ctx := baseauth.AttachJwtToContext(r.Context(), r) r = r.WithContext(ctx) route := mux.CurrentRoute(r) path, _ := route.GetPathTemplate() @@ -482,7 +469,7 @@ func (s *Server) analyticsMiddleware(next http.Handler) http.Handler { } if _, ok := telemetry.EnabledPaths()[path]; ok { - userEmail, err := auth.GetEmailFromJwt(r.Context()) + userEmail, err := baseauth.GetEmailFromJwt(r.Context()) if err == nil { telemetry.GetInstance().SendEvent(telemetry.TELEMETRY_EVENT_PATH, data, userEmail) } diff --git a/ee/query-service/auth/auth.go b/ee/query-service/auth/auth.go new file mode 100644 index 0000000000..8c06384549 --- /dev/null +++ b/ee/query-service/auth/auth.go @@ -0,0 +1,56 @@ +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.uber.org/zap" +) + +func GetUserFromRequest(r *http.Request, apiHandler *api.APIHandler) (*basemodel.UserPayload, error) { + patToken := r.Header.Get("SIGNOZ-API-KEY") + if len(patToken) > 0 { + zap.S().Debugf("Received a non-zero length PAT token") + ctx := context.Background() + dao := apiHandler.AppDao() + + pat, err := dao.GetPAT(ctx, patToken) + if err == nil && pat != nil { + zap.S().Debugf("Found valid PAT: %+v", pat) + if pat.ExpiresAt < time.Now().Unix() && pat.ExpiresAt != 0 { + zap.S().Debugf("PAT has expired: %+v", pat) + return nil, fmt.Errorf("PAT has expired") + } + group, apiErr := dao.GetGroupByName(ctx, pat.Role) + if apiErr != nil { + zap.S().Debugf("Error while getting group for PAT: %+v", apiErr) + return nil, apiErr + } + user, err := dao.GetUser(ctx, pat.UserID) + if err != nil { + zap.S().Debugf("Error while getting user for PAT: %+v", err) + return nil, err + } + telemetry.GetInstance().SetPatTokenUser() + dao.UpdatePATLastUsed(ctx, patToken, time.Now().Unix()) + user.User.GroupId = group.Id + user.User.Id = pat.Id + return &basemodel.UserPayload{ + User: user.User, + Role: pat.Role, + }, nil + } + if err != nil { + zap.S().Debugf("Error while getting user for PAT: %+v", err) + return nil, err + } + } + return baseauth.GetUserFromRequest(r) +} diff --git a/ee/query-service/dao/interface.go b/ee/query-service/dao/interface.go index 479ca56edc..78155bc23a 100644 --- a/ee/query-service/dao/interface.go +++ b/ee/query-service/dao/interface.go @@ -34,9 +34,11 @@ type ModelDao interface { GetDomainByEmail(ctx context.Context, email string) (*model.OrgDomain, basemodel.BaseApiError) CreatePAT(ctx context.Context, p model.PAT) (model.PAT, basemodel.BaseApiError) + UpdatePAT(ctx context.Context, p model.PAT, id string) (basemodel.BaseApiError) GetPAT(ctx context.Context, pat string) (*model.PAT, basemodel.BaseApiError) + UpdatePATLastUsed(ctx context.Context, pat string, lastUsed int64) basemodel.BaseApiError GetPATByID(ctx context.Context, id string) (*model.PAT, basemodel.BaseApiError) GetUserByPAT(ctx context.Context, token string) (*basemodel.UserPayload, basemodel.BaseApiError) ListPATs(ctx context.Context, userID string) ([]model.PAT, basemodel.BaseApiError) - DeletePAT(ctx context.Context, id string) basemodel.BaseApiError + RevokePAT(ctx context.Context, id string, userID string) basemodel.BaseApiError } diff --git a/ee/query-service/dao/sqlite/modelDao.go b/ee/query-service/dao/sqlite/modelDao.go index 3c195ea9bf..02b4367da0 100644 --- a/ee/query-service/dao/sqlite/modelDao.go +++ b/ee/query-service/dao/sqlite/modelDao.go @@ -7,6 +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.uber.org/zap" ) type modelDao struct { @@ -28,6 +29,41 @@ 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, ¬null, &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(dataSourceName string) (*modelDao, error) { dao, err := basedsql.InitDB(dataSourceName) @@ -51,11 +87,16 @@ func InitDB(dataSourceName string) (*modelDao, error) { ); 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) ); ` @@ -65,6 +106,36 @@ func InitDB(dataSourceName string) (*modelDao, error) { 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 } diff --git a/ee/query-service/dao/sqlite/pat.go b/ee/query-service/dao/sqlite/pat.go index 5bd1b78a62..a1752ea238 100644 --- a/ee/query-service/dao/sqlite/pat.go +++ b/ee/query-service/dao/sqlite/pat.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "strconv" + "time" "go.signoz.io/signoz/ee/query-service/model" basemodel "go.signoz.io/signoz/pkg/query-service/model" @@ -12,12 +13,16 @@ import ( func (m *modelDao) CreatePAT(ctx context.Context, p model.PAT) (model.PAT, basemodel.BaseApiError) { result, err := m.DB().ExecContext(ctx, - "INSERT INTO personal_access_tokens (user_id, token, name, created_at, expires_at) VALUES ($1, $2, $3, $4, $5)", + "INSERT INTO personal_access_tokens (user_id, token, role, name, created_at, expires_at, updated_at, updated_by_user_id) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)", p.UserID, p.Token, + p.Role, p.Name, p.CreatedAt, - p.ExpiresAt) + p.ExpiresAt, + p.UpdatedAt, + p.UpdatedByUserID, + ) if err != nil { zap.S().Errorf("Failed to insert PAT in db, err: %v", zap.Error(err)) return model.PAT{}, model.InternalError(fmt.Errorf("PAT insertion failed")) @@ -28,24 +33,102 @@ func (m *modelDao) CreatePAT(ctx context.Context, p model.PAT) (model.PAT, basem return model.PAT{}, model.InternalError(fmt.Errorf("PAT insertion failed")) } p.Id = strconv.Itoa(int(id)) + createdByUser, _ := m.GetUser(ctx, p.UserID) + if createdByUser == nil { + p.CreatedByUser = model.User{ + NotFound: true, + } + } else { + p.CreatedByUser = model.User{ + Id: createdByUser.Id, + Name: createdByUser.Name, + Email: createdByUser.Email, + CreatedAt: createdByUser.CreatedAt, + ProfilePictureURL: createdByUser.ProfilePictureURL, + NotFound: false, + } + } return p, nil } +func (m *modelDao) UpdatePAT(ctx context.Context, p model.PAT, id string) basemodel.BaseApiError { + _, err := m.DB().ExecContext(ctx, + "UPDATE personal_access_tokens SET role=$1, name=$2, updated_at=$3, updated_by_user_id=$4 WHERE id=$5 and revoked=false;", + p.Role, + p.Name, + p.UpdatedAt, + p.UpdatedByUserID, + id) + if err != nil { + zap.S().Errorf("Failed to update PAT in db, err: %v", zap.Error(err)) + return model.InternalError(fmt.Errorf("PAT update failed")) + } + return nil +} + +func (m *modelDao) UpdatePATLastUsed(ctx context.Context, token string, lastUsed int64) basemodel.BaseApiError { + _, err := m.DB().ExecContext(ctx, + "UPDATE personal_access_tokens SET last_used=$1 WHERE token=$2 and revoked=false;", + lastUsed, + token) + if err != nil { + zap.S().Errorf("Failed to update PAT last used in db, err: %v", zap.Error(err)) + return model.InternalError(fmt.Errorf("PAT last used update failed")) + } + return nil +} + func (m *modelDao) ListPATs(ctx context.Context, userID string) ([]model.PAT, basemodel.BaseApiError) { pats := []model.PAT{} - if err := m.DB().Select(&pats, `SELECT * FROM personal_access_tokens WHERE user_id=?;`, userID); err != nil { + if err := m.DB().Select(&pats, `SELECT * FROM personal_access_tokens WHERE user_id=? and revoked=false ORDER by updated_at DESC;`, userID); err != nil { zap.S().Errorf("Failed to fetch PATs for user: %s, err: %v", userID, zap.Error(err)) return nil, model.InternalError(fmt.Errorf("failed to fetch PATs")) } + for i := range pats { + createdByUser, _ := m.GetUser(ctx, pats[i].UserID) + if createdByUser == nil { + pats[i].CreatedByUser = model.User{ + NotFound: true, + } + } else { + pats[i].CreatedByUser = model.User{ + Id: createdByUser.Id, + Name: createdByUser.Name, + Email: createdByUser.Email, + CreatedAt: createdByUser.CreatedAt, + ProfilePictureURL: createdByUser.ProfilePictureURL, + NotFound: false, + } + } + + updatedByUser, _ := m.GetUser(ctx, pats[i].UpdatedByUserID) + if updatedByUser == nil { + pats[i].UpdatedByUser = model.User{ + NotFound: true, + } + } else { + pats[i].UpdatedByUser = model.User{ + Id: updatedByUser.Id, + Name: updatedByUser.Name, + Email: updatedByUser.Email, + CreatedAt: updatedByUser.CreatedAt, + ProfilePictureURL: updatedByUser.ProfilePictureURL, + NotFound: false, + } + } + } return pats, nil } -func (m *modelDao) DeletePAT(ctx context.Context, id string) basemodel.BaseApiError { - _, err := m.DB().ExecContext(ctx, `DELETE from personal_access_tokens where id=?;`, id) +func (m *modelDao) RevokePAT(ctx context.Context, id string, userID string) basemodel.BaseApiError { + updatedAt := time.Now().Unix() + _, err := m.DB().ExecContext(ctx, + "UPDATE personal_access_tokens SET revoked=true, updated_by_user_id = $1, updated_at=$2 WHERE id=$3", + userID, updatedAt, id) if err != nil { - zap.S().Errorf("Failed to delete PAT, err: %v", zap.Error(err)) - return model.InternalError(fmt.Errorf("failed to delete PAT")) + zap.S().Errorf("Failed to revoke PAT in db, err: %v", zap.Error(err)) + return model.InternalError(fmt.Errorf("PAT revoke failed")) } return nil } @@ -53,7 +136,7 @@ func (m *modelDao) DeletePAT(ctx context.Context, id string) basemodel.BaseApiEr func (m *modelDao) GetPAT(ctx context.Context, token string) (*model.PAT, basemodel.BaseApiError) { pats := []model.PAT{} - if err := m.DB().Select(&pats, `SELECT * FROM personal_access_tokens WHERE token=?;`, token); err != nil { + if err := m.DB().Select(&pats, `SELECT * FROM personal_access_tokens WHERE token=? and revoked=false;`, token); err != nil { return nil, model.InternalError(fmt.Errorf("failed to fetch PAT")) } @@ -70,7 +153,7 @@ func (m *modelDao) GetPAT(ctx context.Context, token string) (*model.PAT, basemo func (m *modelDao) GetPATByID(ctx context.Context, id string) (*model.PAT, basemodel.BaseApiError) { pats := []model.PAT{} - if err := m.DB().Select(&pats, `SELECT * FROM personal_access_tokens WHERE id=?;`, id); err != nil { + if err := m.DB().Select(&pats, `SELECT * FROM personal_access_tokens WHERE id=? and revoked=false;`, id); err != nil { return nil, model.InternalError(fmt.Errorf("failed to fetch PAT")) } @@ -84,6 +167,7 @@ func (m *modelDao) GetPATByID(ctx context.Context, id string) (*model.PAT, basem return &pats[0], nil } +// deprecated func (m *modelDao) GetUserByPAT(ctx context.Context, token string) (*basemodel.UserPayload, basemodel.BaseApiError) { users := []basemodel.UserPayload{} diff --git a/ee/query-service/model/pat.go b/ee/query-service/model/pat.go index f320d0be7c..ef683a08bf 100644 --- a/ee/query-service/model/pat.go +++ b/ee/query-service/model/pat.go @@ -1,10 +1,32 @@ package model -type PAT struct { - Id string `json:"id" db:"id"` - UserID string `json:"userId" db:"user_id"` - Token string `json:"token" db:"token"` - Name string `json:"name" db:"name"` - CreatedAt int64 `json:"createdAt" db:"created_at"` - ExpiresAt int64 `json:"expiresAt" db:"expires_at"` +type User struct { + Id string `json:"id" db:"id"` + Name string `json:"name" db:"name"` + Email string `json:"email" db:"email"` + CreatedAt int64 `json:"createdAt" db:"created_at"` + ProfilePictureURL string `json:"profilePictureURL" db:"profile_picture_url"` + NotFound bool `json:"notFound"` +} + +type CreatePATRequestBody struct { + Name string `json:"name"` + Role string `json:"role"` + ExpiresInDays int64 `json:"expiresInDays"` +} + +type PAT struct { + Id string `json:"id" db:"id"` + UserID string `json:"userId" db:"user_id"` + CreatedByUser User `json:"createdByUser"` + UpdatedByUser User `json:"updatedByUser"` + Token string `json:"token" db:"token"` + Role string `json:"role" db:"role"` + Name string `json:"name" db:"name"` + CreatedAt int64 `json:"createdAt" db:"created_at"` + ExpiresAt int64 `json:"expiresAt" db:"expires_at"` + UpdatedAt int64 `json:"updatedAt" db:"updated_at"` + LastUsed int64 `json:"lastUsed" db:"last_used"` + Revoked bool `json:"revoked" db:"revoked"` + UpdatedByUserID string `json:"updatedByUserId" db:"updated_by_user_id"` } diff --git a/frontend/jest.config.ts b/frontend/jest.config.ts index 20bf44bf96..0493353115 100644 --- a/frontend/jest.config.ts +++ b/frontend/jest.config.ts @@ -20,6 +20,8 @@ const config: Config.InitialOptions = { transform: { '^.+\\.(ts|tsx)?$': 'ts-jest', '^.+\\.(js|jsx)$': 'babel-jest', + '^.+\\.(css|scss|sass|less)$': 'jest-preview/transforms/css', + '^(?!.*\\.(js|jsx|mjs|cjs|ts|tsx|css|json)$)': 'jest-preview/transforms/file', }, transformIgnorePatterns: [ 'node_modules/(?!(lodash-es|react-dnd|core-dnd|@react-dnd|dnd-core|react-dnd-html5-backend|axios|@signozhq/design-tokens)/)', diff --git a/frontend/jest.setup.ts b/frontend/jest.setup.ts index debe6128e2..4c3aad294f 100644 --- a/frontend/jest.setup.ts +++ b/frontend/jest.setup.ts @@ -7,6 +7,7 @@ */ import '@testing-library/jest-dom'; import 'jest-styled-components'; +import './src/styles.scss'; import { server } from './src/mocks-server/server'; // Establish API mocking before all tests. diff --git a/frontend/package.json b/frontend/package.json index 1a7acae5ad..7d1a0855e3 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -13,6 +13,8 @@ "jest": "jest", "jest:coverage": "jest --coverage", "jest:watch": "jest --watch", + "jest-preview": "jest-preview", + "test:debug": "npm-run-all -p test jest-preview", "postinstall": "is-ci || yarn husky:configure", "playwright": "npm run i18n:generate-hash && NODE_ENV=testing playwright test --config=./playwright.config.ts", "playwright:local:debug": "PWDEBUG=console yarn playwright --headed --browser=chromium", @@ -77,11 +79,12 @@ "less": "^4.1.2", "less-loader": "^10.2.0", "lodash-es": "^4.17.21", - "lucide-react": "0.288.0", + "lucide-react": "0.321.0", "mini-css-extract-plugin": "2.4.5", "papaparse": "5.4.1", "react": "18.2.0", "react-addons-update": "15.6.3", + "react-beautiful-dnd": "13.1.1", "react-dnd": "16.0.1", "react-dnd-html5-backend": "16.0.1", "react-dom": "18.2.0", @@ -155,6 +158,7 @@ "@types/papaparse": "5.3.7", "@types/react": "18.0.26", "@types/react-addons-update": "0.14.21", + "@types/react-beautiful-dnd": "13.1.8", "@types/react-dom": "18.0.10", "@types/react-grid-layout": "^1.1.2", "@types/react-helmet-async": "1.0.3", @@ -192,6 +196,7 @@ "husky": "^7.0.4", "is-ci": "^3.0.1", "jest-playwright-preset": "^1.7.2", + "jest-preview": "0.3.1", "jest-styled-components": "^7.0.8", "lint-staged": "^12.5.0", "msw": "1.3.2", @@ -208,7 +213,8 @@ "ts-node": "^10.2.1", "typescript-plugin-css-modules": "5.0.1", "webpack-bundle-analyzer": "^4.5.0", - "webpack-cli": "^4.9.2" + "webpack-cli": "^4.9.2", + "npm-run-all": "latest" }, "lint-staged": { "*.(js|jsx|ts|tsx)": [ diff --git a/frontend/public/locales/en-GB/routes.json b/frontend/public/locales/en-GB/routes.json index a3357435dd..c88baa096a 100644 --- a/frontend/public/locales/en-GB/routes.json +++ b/frontend/public/locales/en-GB/routes.json @@ -3,6 +3,7 @@ "alert_channels": "Alert Channels", "organization_settings": "Organization Settings", "ingestion_settings": "Ingestion Settings", + "api_keys": "API Keys", "my_settings": "My Settings", "overview_metrics": "Overview Metrics", "dbcall_metrics": "Database Calls", diff --git a/frontend/public/locales/en-GB/titles.json b/frontend/public/locales/en-GB/titles.json index a457360245..d8ed6ff0ef 100644 --- a/frontend/public/locales/en-GB/titles.json +++ b/frontend/public/locales/en-GB/titles.json @@ -26,6 +26,7 @@ "MY_SETTINGS": "SigNoz | My Settings", "ORG_SETTINGS": "SigNoz | Organization Settings", "INGESTION_SETTINGS": "SigNoz | Ingestion Settings", + "API_KEYS": "SigNoz | API Keys", "SOMETHING_WENT_WRONG": "SigNoz | Something Went Wrong", "UN_AUTHORIZED": "SigNoz | Unauthorized", "NOT_FOUND": "SigNoz | Page Not Found", diff --git a/frontend/public/locales/en/apiKeys.json b/frontend/public/locales/en/apiKeys.json new file mode 100644 index 0000000000..5cc51fa92e --- /dev/null +++ b/frontend/public/locales/en/apiKeys.json @@ -0,0 +1,3 @@ +{ + "delete_confirm_message": "Are you sure you want to delete {{keyName}} key? Deleting a key is irreversible and cannot be undone." +} diff --git a/frontend/public/locales/en/routes.json b/frontend/public/locales/en/routes.json index a3357435dd..c88baa096a 100644 --- a/frontend/public/locales/en/routes.json +++ b/frontend/public/locales/en/routes.json @@ -3,6 +3,7 @@ "alert_channels": "Alert Channels", "organization_settings": "Organization Settings", "ingestion_settings": "Ingestion Settings", + "api_keys": "API Keys", "my_settings": "My Settings", "overview_metrics": "Overview Metrics", "dbcall_metrics": "Database Calls", diff --git a/frontend/public/locales/en/titles.json b/frontend/public/locales/en/titles.json index 82fad7f472..cebb3151d9 100644 --- a/frontend/public/locales/en/titles.json +++ b/frontend/public/locales/en/titles.json @@ -26,6 +26,7 @@ "MY_SETTINGS": "SigNoz | My Settings", "ORG_SETTINGS": "SigNoz | Organization Settings", "INGESTION_SETTINGS": "SigNoz | Ingestion Settings", + "API_KEYS": "SigNoz | API Keys", "SOMETHING_WENT_WRONG": "SigNoz | Something Went Wrong", "UN_AUTHORIZED": "SigNoz | Unauthorized", "NOT_FOUND": "SigNoz | Page Not Found", diff --git a/frontend/src/AppRoutes/Private.tsx b/frontend/src/AppRoutes/Private.tsx index aafaa932af..f0bfa62d6e 100644 --- a/frontend/src/AppRoutes/Private.tsx +++ b/frontend/src/AppRoutes/Private.tsx @@ -20,7 +20,7 @@ import { UPDATE_USER_IS_FETCH } from 'types/actions/app'; import AppReducer from 'types/reducer/app'; import { routePermission } from 'utils/permission'; -import routes from './routes'; +import routes, { LIST_LICENSES } from './routes'; import afterLogin from './utils'; function PrivateRoute({ children }: PrivateRouteProps): JSX.Element { @@ -29,7 +29,7 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element { const mapRoutes = useMemo( () => new Map( - routes.map((e) => { + [...routes, LIST_LICENSES].map((e) => { const currentPath = matchPath(pathname, { path: e.path, }); @@ -98,6 +98,7 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element { if ( userResponse && + route && route.find((e) => e === userResponse.payload.role) === undefined ) { history.push(ROUTES.UN_AUTHORIZED); diff --git a/frontend/src/AppRoutes/pageComponents.ts b/frontend/src/AppRoutes/pageComponents.ts index 0a2e0e59f2..6d26f9b55a 100644 --- a/frontend/src/AppRoutes/pageComponents.ts +++ b/frontend/src/AppRoutes/pageComponents.ts @@ -118,6 +118,10 @@ export const IngestionSettings = Loadable( () => import(/* webpackChunkName: "Ingestion Settings" */ 'pages/Settings'), ); +export const APIKeys = Loadable( + () => import(/* webpackChunkName: "All Settings" */ 'pages/Settings'), +); + export const MySettings = Loadable( () => import(/* webpackChunkName: "All MySettings" */ 'pages/MySettings'), ); diff --git a/frontend/src/AppRoutes/routes.ts b/frontend/src/AppRoutes/routes.ts index e9d202c420..a6543ad01d 100644 --- a/frontend/src/AppRoutes/routes.ts +++ b/frontend/src/AppRoutes/routes.ts @@ -6,6 +6,7 @@ import { RouteProps } from 'react-router-dom'; import { AllAlertChannels, AllErrors, + APIKeys, BillingPage, CreateAlertChannelAlerts, CreateNewAlerts, @@ -236,6 +237,13 @@ const routes: AppRoutes[] = [ isPrivate: true, key: 'INGESTION_SETTINGS', }, + { + path: ROUTES.API_KEYS, + exact: true, + component: APIKeys, + isPrivate: true, + key: 'API_KEYS', + }, { path: ROUTES.MY_SETTINGS, exact: true, diff --git a/frontend/src/api/APIKeys/createAPIKey.ts b/frontend/src/api/APIKeys/createAPIKey.ts new file mode 100644 index 0000000000..2b219a0166 --- /dev/null +++ b/frontend/src/api/APIKeys/createAPIKey.ts @@ -0,0 +1,26 @@ +import axios from 'api'; +import { ErrorResponseHandler } from 'api/ErrorResponseHandler'; +import { AxiosError } from 'axios'; +import { ErrorResponse, SuccessResponse } from 'types/api'; +import { APIKeyProps, CreateAPIKeyProps } from 'types/api/pat/types'; + +const createAPIKey = async ( + props: CreateAPIKeyProps, +): Promise | ErrorResponse> => { + try { + const response = await axios.post('/pats', { + ...props, + }); + + return { + statusCode: 200, + error: null, + message: response.data.status, + payload: response.data.data, + }; + } catch (error) { + return ErrorResponseHandler(error as AxiosError); + } +}; + +export default createAPIKey; diff --git a/frontend/src/api/APIKeys/deleteAPIKey.ts b/frontend/src/api/APIKeys/deleteAPIKey.ts new file mode 100644 index 0000000000..03b8d595da --- /dev/null +++ b/frontend/src/api/APIKeys/deleteAPIKey.ts @@ -0,0 +1,24 @@ +import axios from 'api'; +import { ErrorResponseHandler } from 'api/ErrorResponseHandler'; +import { AxiosError } from 'axios'; +import { ErrorResponse, SuccessResponse } from 'types/api'; +import { AllAPIKeyProps } from 'types/api/pat/types'; + +const deleteAPIKey = async ( + id: string, +): Promise | ErrorResponse> => { + try { + const response = await axios.delete(`/pats/${id}`); + + return { + statusCode: 200, + error: null, + message: response.data.status, + payload: response.data.data, + }; + } catch (error) { + return ErrorResponseHandler(error as AxiosError); + } +}; + +export default deleteAPIKey; diff --git a/frontend/src/api/APIKeys/getAPIKey.ts b/frontend/src/api/APIKeys/getAPIKey.ts new file mode 100644 index 0000000000..c0410d873f --- /dev/null +++ b/frontend/src/api/APIKeys/getAPIKey.ts @@ -0,0 +1,24 @@ +import axios from 'api'; +import { ErrorResponseHandler } from 'api/ErrorResponseHandler'; +import { AxiosError } from 'axios'; +import { ErrorResponse, SuccessResponse } from 'types/api'; +import { PayloadProps, Props } from 'types/api/alerts/get'; + +const get = async ( + props: Props, +): Promise | ErrorResponse> => { + try { + const response = await axios.get(`/pats/${props.id}`); + + return { + statusCode: 200, + error: null, + message: response.data.status, + payload: response.data, + }; + } catch (error) { + return ErrorResponseHandler(error as AxiosError); + } +}; + +export default get; diff --git a/frontend/src/api/APIKeys/getAllAPIKeys.ts b/frontend/src/api/APIKeys/getAllAPIKeys.ts new file mode 100644 index 0000000000..488d9dc5cf --- /dev/null +++ b/frontend/src/api/APIKeys/getAllAPIKeys.ts @@ -0,0 +1,6 @@ +import axios from 'api'; +import { AxiosResponse } from 'axios'; +import { AllAPIKeyProps } from 'types/api/pat/types'; + +export const getAllAPIKeys = (): Promise> => + axios.get(`/pats`); diff --git a/frontend/src/api/APIKeys/updateAPIKey.ts b/frontend/src/api/APIKeys/updateAPIKey.ts new file mode 100644 index 0000000000..38d20227a3 --- /dev/null +++ b/frontend/src/api/APIKeys/updateAPIKey.ts @@ -0,0 +1,26 @@ +import axios from 'api'; +import { ErrorResponseHandler } from 'api/ErrorResponseHandler'; +import { AxiosError } from 'axios'; +import { ErrorResponse, SuccessResponse } from 'types/api'; +import { PayloadProps, UpdateAPIKeyProps } from 'types/api/pat/types'; + +const updateAPIKey = async ( + props: UpdateAPIKeyProps, +): Promise | ErrorResponse> => { + try { + const response = await axios.put(`/pats/${props.id}`, { + ...props.data, + }); + + return { + statusCode: 200, + error: null, + message: response.data.status, + payload: response.data.data, + }; + } catch (error) { + return ErrorResponseHandler(error as AxiosError); + } +}; + +export default updateAPIKey; diff --git a/frontend/src/assets/Dashboard/List.tsx b/frontend/src/assets/Dashboard/List.tsx new file mode 100644 index 0000000000..1c4d1d04a9 --- /dev/null +++ b/frontend/src/assets/Dashboard/List.tsx @@ -0,0 +1,30 @@ +import { CSSProperties } from 'react'; + +function ListIcon({ + fillColor, +}: { + fillColor: CSSProperties['color']; +}): JSX.Element { + return ( + + + + + + + + + ); +} + +export default ListIcon; diff --git a/frontend/src/assets/Dashboard/Table.tsx b/frontend/src/assets/Dashboard/Table.tsx index e1ade19994..60effdbfc0 100644 --- a/frontend/src/assets/Dashboard/Table.tsx +++ b/frontend/src/assets/Dashboard/Table.tsx @@ -1,18 +1,48 @@ -function Table(): JSX.Element { +import { CSSProperties } from 'react'; + +function TableIcon({ + fillColor, +}: { + fillColor: CSSProperties['color']; +}): JSX.Element { return ( + + + ); } -export default Table; +export default TableIcon; diff --git a/frontend/src/assets/Dashboard/TimeSeries.tsx b/frontend/src/assets/Dashboard/TimeSeries.tsx index a9ddbc5717..afa9b5f095 100644 --- a/frontend/src/assets/Dashboard/TimeSeries.tsx +++ b/frontend/src/assets/Dashboard/TimeSeries.tsx @@ -1,15 +1,66 @@ -function TimeSeries(): JSX.Element { +import { CSSProperties } from 'react'; + +function TimeSeries({ + fillColor, +}: { + fillColor: CSSProperties['color']; +}): JSX.Element { return ( + + + + + + ); diff --git a/frontend/src/assets/Dashboard/Value.tsx b/frontend/src/assets/Dashboard/Value.tsx index 708e16d6b9..39ef8d9f44 100644 --- a/frontend/src/assets/Dashboard/Value.tsx +++ b/frontend/src/assets/Dashboard/Value.tsx @@ -1,19 +1,29 @@ -function Value(): JSX.Element { +import { CSSProperties } from 'react'; + +function Value({ + fillColor, +}: { + fillColor: CSSProperties['color']; +}): JSX.Element { return ( ); diff --git a/frontend/src/components/LogDetail/LogDetail.interfaces.ts b/frontend/src/components/LogDetail/LogDetail.interfaces.ts index 991fb4488e..399e1dffb2 100644 --- a/frontend/src/components/LogDetail/LogDetail.interfaces.ts +++ b/frontend/src/components/LogDetail/LogDetail.interfaces.ts @@ -8,6 +8,7 @@ import { VIEWS } from './constants'; export type LogDetailProps = { log: ILog | null; selectedTab: VIEWS; + isListViewPanel?: boolean; } & Pick & Partial> & Pick; diff --git a/frontend/src/components/LogDetail/index.tsx b/frontend/src/components/LogDetail/index.tsx index ffa1c07e9c..0794ead980 100644 --- a/frontend/src/components/LogDetail/index.tsx +++ b/frontend/src/components/LogDetail/index.tsx @@ -35,6 +35,7 @@ function LogDetail({ onAddToQuery, onClickActionItem, selectedTab, + isListViewPanel = false, }: LogDetailProps): JSX.Element { const [, copyToClipboard] = useCopyToClipboard(); const [selectedView, setSelectedView] = useState(selectedTab); @@ -190,6 +191,7 @@ function LogDetail({ logData={log} onAddToQuery={onAddToQuery} onClickActionItem={onClickActionItem} + isListViewPanel={isListViewPanel} /> )} {selectedView === VIEW_TYPES.JSON && } diff --git a/frontend/src/components/Logs/TableView/config.ts b/frontend/src/components/Logs/TableView/config.ts index c532981ad7..73b5f9a4c3 100644 --- a/frontend/src/components/Logs/TableView/config.ts +++ b/frontend/src/components/Logs/TableView/config.ts @@ -22,6 +22,10 @@ export const defaultTableStyle: CSSProperties = { maxWidth: '40rem', }; +export const defaultListViewPanelStyle: CSSProperties = { + maxWidth: '40rem', +}; + export const tableScroll: TableProps>['scroll'] = { x: true, }; diff --git a/frontend/src/components/Logs/TableView/types.ts b/frontend/src/components/Logs/TableView/types.ts index 35ef198ac6..3176101d9d 100644 --- a/frontend/src/components/Logs/TableView/types.ts +++ b/frontend/src/components/Logs/TableView/types.ts @@ -24,6 +24,7 @@ export type UseTableViewProps = { onClickExpand?: (log: ILog) => void; activeLog?: ILog | null; activeContextLog?: ILog | null; + isListViewPanel?: boolean; } & LogsTableViewProps; export type ActionsColumnProps = { diff --git a/frontend/src/components/Logs/TableView/useTableView.tsx b/frontend/src/components/Logs/TableView/useTableView.tsx index 5fad6e3896..9db2332635 100644 --- a/frontend/src/components/Logs/TableView/useTableView.tsx +++ b/frontend/src/components/Logs/TableView/useTableView.tsx @@ -13,7 +13,11 @@ import { useMemo } from 'react'; import LogStateIndicator, { LogType, } from '../LogStateIndicator/LogStateIndicator'; -import { defaultTableStyle, getDefaultCellStyle } from './config'; +import { + defaultListViewPanelStyle, + defaultTableStyle, + getDefaultCellStyle, +} from './config'; import { TableBodyContent } from './styles'; import { ColumnTypeRender, @@ -31,6 +35,7 @@ export const useTableView = (props: UseTableViewProps): UseTableViewResult => { appendTo = 'center', activeContextLog, activeLog, + isListViewPanel, } = props; const isDarkMode = useIsDarkMode(); @@ -48,7 +53,9 @@ export const useTableView = (props: UseTableViewProps): UseTableViewResult => { key: name, render: (field): ColumnTypeRender> => ({ props: { - style: getDefaultCellStyle(isDarkMode), + style: isListViewPanel + ? defaultListViewPanelStyle + : getDefaultCellStyle(isDarkMode), }, children: ( @@ -58,6 +65,10 @@ export const useTableView = (props: UseTableViewProps): UseTableViewResult => { }), })); + if (isListViewPanel) { + return [...fieldColumns]; + } + return [ { title: 'timestamp', @@ -110,6 +121,7 @@ export const useTableView = (props: UseTableViewProps): UseTableViewResult => { ]; }, [ fields, + isListViewPanel, appendTo, isDarkMode, linesPerRow, diff --git a/frontend/src/constants/panelTypes.ts b/frontend/src/constants/panelTypes.ts index 87cc62eae3..d16e5bf92d 100644 --- a/frontend/src/constants/panelTypes.ts +++ b/frontend/src/constants/panelTypes.ts @@ -1,6 +1,9 @@ import Uplot from 'components/Uplot'; import GridTableComponent from 'container/GridTableComponent'; import GridValueComponent from 'container/GridValueComponent'; +import LogsPanelComponent from 'container/LogsPanelTable/LogsPanelComponent'; +import TracesTableComponent from 'container/TracesTableComponent/TracesTableComponent'; +import { DataSource } from 'types/common/queryBuilder'; import { PANEL_TYPES } from './queryBuilder'; @@ -9,10 +12,27 @@ export const PANEL_TYPES_COMPONENT_MAP = { [PANEL_TYPES.VALUE]: GridValueComponent, [PANEL_TYPES.TABLE]: GridTableComponent, [PANEL_TYPES.TRACE]: null, - [PANEL_TYPES.LIST]: null, + [PANEL_TYPES.LIST]: LogsPanelComponent, [PANEL_TYPES.EMPTY_WIDGET]: null, } as const; +export const getComponentForPanelType = ( + panelType: PANEL_TYPES, + dataSource?: DataSource, +): React.ComponentType | null => { + const componentsMap = { + [PANEL_TYPES.TIME_SERIES]: Uplot, + [PANEL_TYPES.VALUE]: GridValueComponent, + [PANEL_TYPES.TABLE]: GridTableComponent, + [PANEL_TYPES.TRACE]: null, + [PANEL_TYPES.LIST]: + dataSource === DataSource.LOGS ? LogsPanelComponent : TracesTableComponent, + [PANEL_TYPES.EMPTY_WIDGET]: null, + }; + + return componentsMap[panelType]; +}; + export const AVAILABLE_EXPORT_PANEL_TYPES = [ PANEL_TYPES.TIME_SERIES, PANEL_TYPES.TABLE, diff --git a/frontend/src/constants/routes.ts b/frontend/src/constants/routes.ts index 0dae80950d..0715ebf787 100644 --- a/frontend/src/constants/routes.ts +++ b/frontend/src/constants/routes.ts @@ -24,6 +24,7 @@ const ROUTES = { MY_SETTINGS: '/my-settings', SETTINGS: '/settings', ORG_SETTINGS: '/settings/org-settings', + API_KEYS: '/settings/api-keys', INGESTION_SETTINGS: '/settings/ingestion-settings', SOMETHING_WENT_WRONG: '/something-went-wrong', UN_AUTHORIZED: '/un-authorized', diff --git a/frontend/src/container/APIKeys/APIKeys.styles.scss b/frontend/src/container/APIKeys/APIKeys.styles.scss new file mode 100644 index 0000000000..c0aeec0cac --- /dev/null +++ b/frontend/src/container/APIKeys/APIKeys.styles.scss @@ -0,0 +1,685 @@ +.api-key-container { + margin-top: 24px; + display: flex; + justify-content: center; + width: 100%; + + .api-key-content { + width: calc(100% - 30px); + max-width: 736px; + + .title { + color: var(--bg-vanilla-100); + font-size: var(--font-size-lg); + font-style: normal; + font-weight: var(--font-weight-normal); + line-height: 28px; /* 155.556% */ + letter-spacing: -0.09px; + } + + .subtitle { + color: var(--bg-vanilla-400); + font-size: var(--font-size-sm); + font-style: normal; + font-weight: var(--font-weight-normal); + line-height: 20px; /* 142.857% */ + letter-spacing: -0.07px; + } + + .api-keys-search-add-new { + display: flex; + align-items: center; + gap: 12px; + + padding: 16px 0; + + .add-new-api-key-btn { + display: flex; + align-items: center; + gap: 8px; + } + } + + .ant-table-row { + .ant-table-cell { + padding: 0; + border: none; + background: var(--bg-ink-500); + } + .column-render { + margin: 8px 0 !important; + border-radius: 6px; + border: 1px solid var(--bg-slate-500); + background: var(--bg-ink-400); + + .title-with-action { + display: flex; + justify-content: space-between; + + align-items: center; + padding: 8px; + + .api-key-data { + display: flex; + gap: 8px; + align-items: center; + + .api-key-title { + display: flex; + align-items: center; + gap: 6px; + + .ant-typography { + color: var(--bg-vanilla-400); + font-size: var(--font-size-sm); + font-style: normal; + font-weight: var(--font-weight-medium); + line-height: 20px; + letter-spacing: -0.07px; + } + } + + .api-key-value { + display: flex; + align-items: center; + gap: 12px; + + border-radius: 20px; + padding: 0px 12px; + + background: var(--bg-ink-200); + + .ant-typography { + color: var(--bg-vanilla-400); + font-size: var(--font-size-xs); + font-family: 'Space Mono', monospace; + font-style: normal; + font-weight: var(--font-weight-medium); + line-height: 20px; + letter-spacing: -0.07px; + } + + .copy-key-btn { + cursor: pointer; + } + } + } + + .action-btn { + display: flex; + align-items: center; + gap: 4px; + cursor: pointer; + } + + .visibility-btn { + border: 1px solid rgba(113, 144, 249, 0.2); + background: rgba(113, 144, 249, 0.1); + } + } + + .ant-collapse { + border: none; + + .ant-collapse-header { + padding: 0px 8px; + + display: flex; + align-items: center; + background-color: #121317; + } + + .ant-collapse-content { + border-top: 1px solid var(--bg-slate-500); + } + + .ant-collapse-item { + border-bottom: none; + } + + .ant-collapse-expand-icon { + padding-inline-end: 0px; + } + } + + .api-key-details { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; + border-top: 1px solid var(--bg-slate-500); + padding: 8px; + + .api-key-tag { + width: 14px; + height: 14px; + border-radius: 50px; + background: var(--bg-slate-300); + display: flex; + justify-content: center; + align-items: center; + + .tag-text { + color: var(--bg-vanilla-400); + leading-trim: both; + text-edge: cap; + font-size: 10px; + font-style: normal; + font-weight: var(--font-weight-normal); + line-height: normal; + letter-spacing: -0.05px; + } + } + + .api-key-created-by { + margin-left: 8px; + } + + .api-key-last-used-at { + display: flex; + align-items: center; + gap: 8px; + + .ant-typography { + color: var(--bg-vanilla-400); + font-size: var(--font-size-sm); + font-style: normal; + font-weight: var(--font-weight-normal); + line-height: 18px; /* 128.571% */ + letter-spacing: -0.07px; + font-variant-numeric: lining-nums tabular-nums stacked-fractions + slashed-zero; + font-feature-settings: 'dlig' on, 'salt' on, 'cpsp' on, 'case' on; + } + } + + .api-key-expires-in { + font-style: normal; + font-weight: 400; + line-height: 18px; + + display: flex; + align-items: center; + gap: 8px; + + .dot { + height: 6px; + width: 6px; + border-radius: 50%; + } + + &.warning { + color: var(--bg-amber-400); + + .dot { + background: var(--bg-amber-400); + box-shadow: 0px 0px 6px 0px var(--bg-amber-400); + } + } + + &.danger { + color: var(--bg-cherry-400); + + .dot { + background: var(--bg-cherry-400); + box-shadow: 0px 0px 6px 0px var(--bg-cherry-400); + } + } + } + } + } + } + + .ant-pagination-item { + display: flex; + justify-content: center; + align-items: center; + + > a { + color: var(--bg-vanilla-400); + font-variant-numeric: lining-nums tabular-nums slashed-zero; + font-feature-settings: 'dlig' on, 'salt' on, 'case' on, 'cpsp' on; + font-size: var(--font-size-sm); + font-style: normal; + font-weight: var(--font-weight-normal); + line-height: 20px; /* 142.857% */ + } + } + + .ant-pagination-item-active { + background-color: var(--bg-robin-500); + > a { + color: var(--bg-ink-500) !important; + font-size: var(--font-size-sm); + font-style: normal; + font-weight: var(--font-weight-medium); + line-height: 20px; + } + } + } +} + +.api-key-info-container { + display: flex; + gap: 12px; + flex-direction: column; + + .user-info { + display: flex; + gap: 8px; + align-items: center; + flex-wrap: wrap; + + .user-avatar { + background-color: lightslategray; + vertical-align: middle; + } + } + + .user-email { + display: inline-flex; + align-items: center; + gap: 12px; + border-radius: 20px; + padding: 0px 12px; + background: var(--bg-ink-200); + + font-family: 'Space Mono', monospace; + } + + .role { + display: flex; + align-items: center; + gap: 12px; + } +} + +.api-key-modal { + .ant-modal-content { + border-radius: 4px; + border: 1px solid var(--bg-slate-500); + background: var(--bg-ink-400); + box-shadow: 0px -4px 16px 2px rgba(0, 0, 0, 0.2); + padding: 0; + + .ant-modal-header { + background: none; + border-bottom: 1px solid var(--bg-slate-500); + padding: 16px; + } + + .ant-modal-close-x { + font-size: 12px; + } + + .ant-modal-body { + padding: 12px 16px; + } + + .ant-modal-footer { + padding: 16px; + margin-top: 0; + + display: flex; + justify-content: flex-end; + } + } +} + +.api-key-access-role { + display: flex; + + .ant-radio-button-wrapper { + font-size: 12px; + text-transform: capitalize; + + &.ant-radio-button-wrapper-checked { + color: #fff; + background: var(--bg-slate-400, #1d212d); + border-color: var(--bg-slate-400, #1d212d); + + &:hover { + color: #fff; + background: var(--bg-slate-400, #1d212d); + border-color: var(--bg-slate-400, #1d212d); + + &::before { + background-color: var(--bg-slate-400, #1d212d); + } + } + + &:focus { + color: #fff; + background: var(--bg-slate-400, #1d212d); + border-color: var(--bg-slate-400, #1d212d); + } + } + } + + .tab { + border: 1px solid var(--bg-slate-400); + + flex: 1; + + display: flex; + justify-content: center; + + &::before { + background: var(--bg-slate-400); + } + + &.selected { + background: var(--bg-slate-400, #1d212d); + } + } + + .role { + display: flex; + align-items: center; + gap: 8px; + } +} + +.delete-api-key-modal { + width: calc(100% - 30px) !important; /* Adjust the 20px as needed */ + max-width: 384px; + .ant-modal-content { + padding: 0; + border-radius: 4px; + border: 1px solid var(--bg-slate-500); + background: var(--bg-ink-400); + box-shadow: 0px -4px 16px 2px rgba(0, 0, 0, 0.2); + + .ant-modal-header { + padding: 16px; + background: var(--bg-ink-400); + } + + .ant-modal-body { + padding: 0px 16px 28px 16px; + + .ant-typography { + color: var(--bg-vanilla-400); + font-size: var(--font-size-sm); + font-style: normal; + font-weight: var(--font-weight-normal); + line-height: 20px; + letter-spacing: -0.07px; + } + + .api-key-input { + margin-top: 8px; + display: flex; + gap: 8px; + } + + .ant-color-picker-trigger { + padding: 6px; + border-radius: 2px; + border: 1px solid var(--bg-slate-400); + background: var(--bg-ink-300); + width: 32px; + height: 32px; + + .ant-color-picker-color-block { + border-radius: 50px; + width: 16px; + height: 16px; + flex-shrink: 0; + + .ant-color-picker-color-block-inner { + display: flex; + justify-content: center; + align-items: center; + } + } + } + } + + .ant-modal-footer { + display: flex; + justify-content: flex-end; + padding: 16px 16px; + margin: 0; + + .cancel-btn { + display: flex; + align-items: center; + border: none; + border-radius: 2px; + background: var(--bg-slate-500); + } + + .delete-btn { + display: flex; + align-items: center; + border: none; + border-radius: 2px; + background: var(--bg-cherry-500); + margin-left: 12px; + } + + .delete-btn:hover { + color: var(--bg-vanilla-100); + background: var(--bg-cherry-600); + } + } + } + + .title { + color: var(--bg-vanilla-100); + font-size: var(--font-size-sm); + font-style: normal; + font-weight: var(--font-weight-medium); + line-height: 20px; /* 142.857% */ + } +} + +.expiration-selector { + .ant-select-selector { + border: 1px solid var(--bg-slate-400) !important; + } +} + +.newAPIKeyDetails { + display: flex; + flex-direction: column; + gap: 8px; +} + +.copyable-text { + display: inline-flex; + align-items: center; + gap: 12px; + border-radius: 20px; + padding: 0px 12px; + background: var(--bg-ink-200, #23262e); + + .copy-key-btn { + cursor: pointer; + } +} + +.lightMode { + .api-key-container { + .api-key-content { + .title { + color: var(--bg-ink-500); + } + + .ant-table-row { + .ant-table-cell { + background: var(--bg-vanilla-200); + } + + &:hover { + .ant-table-cell { + background: var(--bg-vanilla-200) !important; + } + } + + .column-render { + border: 1px solid var(--bg-vanilla-200); + background: var(--bg-vanilla-100); + + .ant-collapse { + border: none; + + .ant-collapse-header { + background: var(--bg-vanilla-100); + } + + .ant-collapse-content { + border-top: 1px solid var(--bg-vanilla-300); + } + } + + .title-with-action { + .api-key-title { + .ant-typography { + color: var(--bg-ink-500); + } + } + + .api-key-value { + background: var(--bg-vanilla-200); + + .ant-typography { + color: var(--bg-slate-400); + } + + .copy-key-btn { + cursor: pointer; + } + } + + .action-btn { + .ant-typography { + color: var(--bg-ink-500); + } + } + } + + .api-key-details { + border-top: 1px solid var(--bg-vanilla-200); + .api-key-tag { + background: var(--bg-vanilla-200); + .tag-text { + color: var(--bg-ink-500); + } + } + + .api-key-created-by { + color: var(--bg-ink-500); + } + + .api-key-last-used-at { + .ant-typography { + color: var(--bg-ink-500); + } + } + } + } + } + } + } + + .delete-api-key-modal { + .ant-modal-content { + border: 1px solid var(--bg-vanilla-200); + background: var(--bg-vanilla-100); + + .ant-modal-header { + background: var(--bg-vanilla-100); + + .title { + color: var(--bg-ink-500); + } + } + + .ant-modal-body { + .ant-typography { + color: var(--bg-ink-500); + } + + .api-key-input { + .ant-input { + background: var(--bg-vanilla-200); + color: var(--bg-ink-500); + } + } + } + + .ant-modal-footer { + .cancel-btn { + background: var(--bg-vanilla-300); + color: var(--bg-ink-400); + } + } + } + } + + .api-key-info-container { + .user-email { + background: var(--bg-vanilla-200); + } + } + + .api-key-modal { + .ant-modal-content { + border-radius: 4px; + border: 1px solid var(--bg-vanilla-200); + background: var(--bg-vanilla-100); + box-shadow: 0px -4px 16px 2px rgba(0, 0, 0, 0.2); + padding: 0; + + .ant-modal-header { + background: none; + border-bottom: 1px solid var(--bg-vanilla-200); + padding: 16px; + } + } + } + + .api-key-access-role { + .ant-radio-button-wrapper { + &.ant-radio-button-wrapper-checked { + color: var(--bg-ink-400); + background: var(--bg-vanilla-300); + border-color: var(--bg-vanilla-300); + + &:hover { + color: var(--bg-ink-400); + background: var(--bg-vanilla-300); + border-color: var(--bg-vanilla-300); + + &::before { + background-color: var(--bg-vanilla-300); + } + } + + &:focus { + color: var(--bg-ink-400); + background: var(--bg-vanilla-300); + border-color: var(--bg-vanilla-300); + } + } + } + + .tab { + border: 1px solid var(--bg-vanilla-300); + + &::before { + background: var(--bg-vanilla-300); + } + + &.selected { + background: var(--bg-vanilla-300); + } + } + } + + .copyable-text { + background: var(--bg-vanilla-200); + } +} diff --git a/frontend/src/container/APIKeys/APIKeys.test.tsx b/frontend/src/container/APIKeys/APIKeys.test.tsx new file mode 100644 index 0000000000..cfc2239236 --- /dev/null +++ b/frontend/src/container/APIKeys/APIKeys.test.tsx @@ -0,0 +1,99 @@ +import { + createAPIKeyResponse, + getAPIKeysResponse, +} from 'mocks-server/__mockdata__/apiKeys'; +import { server } from 'mocks-server/server'; +import { rest } from 'msw'; +import { act, fireEvent, render, screen, waitFor } from 'tests/test-utils'; + +import APIKeys from './APIKeys'; + +const apiKeysURL = 'http://localhost/api/v1/pats'; + +describe('APIKeys component', () => { + beforeEach(() => { + server.use( + rest.get(apiKeysURL, (req, res, ctx) => + res(ctx.status(200), ctx.json(getAPIKeysResponse)), + ), + ); + + render(); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('renders APIKeys component without crashing', () => { + expect(screen.getByText('API Keys')).toBeInTheDocument(); + expect( + screen.getByText('Create and manage access keys for the SigNoz API'), + ).toBeInTheDocument(); + }); + + it('render list of API Keys', async () => { + server.use( + rest.get(apiKeysURL, (req, res, ctx) => + res(ctx.status(200), ctx.json(getAPIKeysResponse)), + ), + ); + + await waitFor(() => { + expect(screen.getByText('No Expiry Token')).toBeInTheDocument(); + expect(screen.getByText('1-5 of 18 API Keys')).toBeInTheDocument(); + }); + }); + + it('opens add new key modal on button click', async () => { + fireEvent.click(screen.getByText('New Key')); + await waitFor(() => { + const createNewKeyBtn = screen.getByRole('button', { + name: /Create new key/i, + }); + + expect(createNewKeyBtn).toBeInTheDocument(); + }); + }); + + it('closes add new key modal on cancel button click', async () => { + fireEvent.click(screen.getByText('New Key')); + + const createNewKeyBtn = screen.getByRole('button', { + name: /Create new key/i, + }); + + await waitFor(() => { + expect(createNewKeyBtn).toBeInTheDocument(); + }); + fireEvent.click(screen.getByText('Cancel')); + await waitFor(() => { + expect(createNewKeyBtn).not.toBeInTheDocument(); + }); + }); + + it('creates a new key on form submission', async () => { + server.use( + rest.post(apiKeysURL, (req, res, ctx) => + res(ctx.status(200), ctx.json(createAPIKeyResponse)), + ), + ); + + fireEvent.click(screen.getByText('New Key')); + + const createNewKeyBtn = screen.getByRole('button', { + name: /Create new key/i, + }); + + await waitFor(() => { + expect(createNewKeyBtn).toBeInTheDocument(); + }); + + act(() => { + const inputElement = screen.getByPlaceholderText('Enter Key Name'); + fireEvent.change(inputElement, { target: { value: 'Top Secret' } }); + fireEvent.click(screen.getByTestId('create-form-admin-role-btn')); + fireEvent.click(createNewKeyBtn); + }); + }); +}); diff --git a/frontend/src/container/APIKeys/APIKeys.tsx b/frontend/src/container/APIKeys/APIKeys.tsx new file mode 100644 index 0000000000..c24bad7009 --- /dev/null +++ b/frontend/src/container/APIKeys/APIKeys.tsx @@ -0,0 +1,864 @@ +import './APIKeys.styles.scss'; + +import { Color } from '@signozhq/design-tokens'; +import { + Avatar, + Button, + Col, + Collapse, + Flex, + Form, + Input, + Modal, + Radio, + Row, + Select, + Table, + TableProps, + Tooltip, + Typography, +} from 'antd'; +import { NotificationInstance } from 'antd/es/notification/interface'; +import { CollapseProps } from 'antd/lib'; +import createAPIKeyApi from 'api/APIKeys/createAPIKey'; +import deleteAPIKeyApi from 'api/APIKeys/deleteAPIKey'; +import updateAPIKeyApi from 'api/APIKeys/updateAPIKey'; +import axios, { AxiosError } from 'axios'; +import cx from 'classnames'; +import { SOMETHING_WENT_WRONG } from 'constants/api'; +import dayjs from 'dayjs'; +import { useGetAllAPIKeys } from 'hooks/APIKeys/useGetAllAPIKeys'; +import { useNotifications } from 'hooks/useNotifications'; +import { + CalendarClock, + Check, + ClipboardEdit, + Contact2, + Copy, + Eye, + Minus, + PenLine, + Plus, + Search, + Trash2, + View, + X, +} from 'lucide-react'; +import { ChangeEvent, useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useMutation } from 'react-query'; +import { useSelector } from 'react-redux'; +import { useCopyToClipboard } from 'react-use'; +import { AppState } from 'store/reducers'; +import { APIKeyProps } from 'types/api/pat/types'; +import AppReducer from 'types/reducer/app'; +import { USER_ROLES } from 'types/roles'; + +export const showErrorNotification = ( + notifications: NotificationInstance, + err: Error, +): void => { + notifications.error({ + message: axios.isAxiosError(err) ? err.message : SOMETHING_WENT_WRONG, + }); +}; + +type ExpiryOption = { + value: string; + label: string; +}; + +const EXPIRATION_WITHIN_SEVEN_DAYS = 7; + +const API_KEY_EXPIRY_OPTIONS: ExpiryOption[] = [ + { value: '1', label: '1 day' }, + { value: '7', label: '1 week' }, + { value: '30', label: '1 month' }, + { value: '90', label: '3 months' }, + { value: '365', label: '1 year' }, + { value: '0', label: 'No Expiry' }, +]; + +function APIKeys(): JSX.Element { + const { user } = useSelector((state) => state.app); + const { notifications } = useNotifications(); + const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); + const [isAddModalOpen, setIsAddModalOpen] = useState(false); + const [showNewAPIKeyDetails, setShowNewAPIKeyDetails] = useState(false); + const [, handleCopyToClipboard] = useCopyToClipboard(); + + const [isEditModalOpen, setIsEditModalOpen] = useState(false); + const [activeAPIKey, setActiveAPIKey] = useState(); + + const [searchValue, setSearchValue] = useState(''); + const [dataSource, setDataSource] = useState([]); + const { t } = useTranslation(['apiKeys']); + + const [editForm] = Form.useForm(); + const [createForm] = Form.useForm(); + + const handleFormReset = (): void => { + editForm.resetFields(); + createForm.resetFields(); + }; + + const hideDeleteViewModal = (): void => { + handleFormReset(); + setActiveAPIKey(null); + setIsDeleteModalOpen(false); + }; + + const showDeleteModal = (apiKey: APIKeyProps): void => { + setActiveAPIKey(apiKey); + setIsDeleteModalOpen(true); + }; + + const hideEditViewModal = (): void => { + handleFormReset(); + setActiveAPIKey(null); + setIsEditModalOpen(false); + }; + + const hideAddViewModal = (): void => { + handleFormReset(); + setShowNewAPIKeyDetails(false); + setActiveAPIKey(null); + setIsAddModalOpen(false); + }; + + const showEditModal = (apiKey: APIKeyProps): void => { + handleFormReset(); + setActiveAPIKey(apiKey); + + editForm.setFieldsValue({ + name: apiKey.name, + role: apiKey.role || USER_ROLES.VIEWER, + }); + + setIsEditModalOpen(true); + }; + + const showAddModal = (): void => { + setActiveAPIKey(null); + setIsAddModalOpen(true); + }; + + const handleModalClose = (): void => { + setActiveAPIKey(null); + }; + + const { + data: APIKeys, + isLoading, + isRefetching, + refetch: refetchAPIKeys, + error, + isError, + } = useGetAllAPIKeys(); + + useEffect(() => { + setActiveAPIKey(APIKeys?.data.data[0]); + }, [APIKeys]); + + useEffect(() => { + setDataSource(APIKeys?.data.data || []); + }, [APIKeys?.data.data]); + + useEffect(() => { + if (isError) { + showErrorNotification(notifications, error as AxiosError); + } + }, [error, isError, notifications]); + + const handleSearch = (e: ChangeEvent): void => { + setSearchValue(e.target.value); + const filteredData = APIKeys?.data?.data?.filter( + (key: APIKeyProps) => + key && + key.name && + key.name.toLowerCase().includes(e.target.value.toLowerCase()), + ); + setDataSource(filteredData || []); + }; + + const clearSearch = (): void => { + setSearchValue(''); + }; + + const { mutate: createAPIKey, isLoading: isLoadingCreateAPIKey } = useMutation( + createAPIKeyApi, + { + onSuccess: (data) => { + setShowNewAPIKeyDetails(true); + setActiveAPIKey(data.payload); + + refetchAPIKeys(); + }, + onError: (error) => { + showErrorNotification(notifications, error as AxiosError); + }, + }, + ); + + const { mutate: updateAPIKey, isLoading: isLoadingUpdateAPIKey } = useMutation( + updateAPIKeyApi, + { + onSuccess: () => { + refetchAPIKeys(); + setIsEditModalOpen(false); + }, + onError: (error) => { + showErrorNotification(notifications, error as AxiosError); + }, + }, + ); + + const { mutate: deleteAPIKey, isLoading: isDeleteingAPIKey } = useMutation( + deleteAPIKeyApi, + { + onSuccess: () => { + refetchAPIKeys(); + setIsDeleteModalOpen(false); + }, + onError: (error) => { + showErrorNotification(notifications, error as AxiosError); + }, + }, + ); + + const onDeleteHandler = (): void => { + clearSearch(); + + if (activeAPIKey) { + deleteAPIKey(activeAPIKey.id); + } + }; + + const onUpdateApiKey = (): void => { + editForm + .validateFields() + .then((values) => { + if (activeAPIKey) { + updateAPIKey({ + id: activeAPIKey.id, + data: { + name: values.name, + role: values.role, + }, + }); + } + }) + .catch((errorInfo) => { + console.error('error info', errorInfo); + }); + }; + + const onCreateAPIKey = (): void => { + createForm + .validateFields() + .then((values) => { + if (user) { + createAPIKey({ + name: values.name, + expiresInDays: parseInt(values.expiration, 10), + role: values.role, + }); + } + }) + .catch((errorInfo) => { + console.error('error info', errorInfo); + }); + }; + + const handleCopyKey = (text: string): void => { + handleCopyToClipboard(text); + notifications.success({ + message: 'Copied to clipboard', + }); + }; + + const getFormattedTime = (epochTime: number): string => { + const timeOptions: Intl.DateTimeFormatOptions = { + hour: '2-digit', + minute: '2-digit', + second: '2-digit', + hour12: false, + }; + const formattedTime = new Date(epochTime * 1000).toLocaleTimeString( + 'en-US', + timeOptions, + ); + + const dateOptions: Intl.DateTimeFormatOptions = { + month: 'short', + day: 'numeric', + year: 'numeric', + }; + + const formattedDate = new Date(epochTime * 1000).toLocaleDateString( + 'en-US', + dateOptions, + ); + + return `${formattedDate} ${formattedTime}`; + }; + + const handleCopyClose = (): void => { + if (activeAPIKey) { + handleCopyKey(activeAPIKey?.token); + } + + hideAddViewModal(); + }; + + const getDateDifference = ( + createdTimestamp: number, + expiryTimestamp: number, + ): number => { + const differenceInSeconds = Math.abs(expiryTimestamp - createdTimestamp); + + // Convert seconds to days + return differenceInSeconds / (60 * 60 * 24); + }; + + const isExpiredToken = (expiryTimestamp: number): boolean => { + if (expiryTimestamp === 0) { + return false; + } + const currentTime = dayjs(); + const tokenExpiresAt = dayjs.unix(expiryTimestamp); + return tokenExpiresAt.isBefore(currentTime); + }; + + const columns: TableProps['columns'] = [ + { + title: 'API Key', + key: 'api-key', + // eslint-disable-next-line sonarjs/cognitive-complexity + render: (APIKey: APIKeyProps): JSX.Element => { + const formattedDateAndTime = + APIKey && APIKey?.lastUsed && APIKey?.lastUsed !== 0 + ? getFormattedTime(APIKey?.lastUsed) + : 'Never'; + + const createdOn = getFormattedTime(APIKey.createdAt); + + const expiresIn = + APIKey.expiresAt === 0 + ? Number.POSITIVE_INFINITY + : getDateDifference(APIKey?.createdAt, APIKey?.expiresAt); + + const isExpired = isExpiredToken(APIKey.expiresAt); + + const expiresOn = + !APIKey.expiresAt || APIKey.expiresAt === 0 + ? 'No Expiry' + : getFormattedTime(APIKey.expiresAt); + + const updatedOn = + !APIKey.updatedAt || APIKey.updatedAt === 0 + ? null + : getFormattedTime(APIKey?.updatedAt); + + const items: CollapseProps['items'] = [ + { + key: '1', + label: ( +
+
+
+ {APIKey?.name} +
+ +
+ + {APIKey?.token.substring(0, 2)}******** + {APIKey?.token.substring(APIKey.token.length - 2).trim()} + + + { + e.stopPropagation(); + e.preventDefault(); + handleCopyKey(APIKey.token); + }} + /> +
+ + {APIKey.role === USER_ROLES.ADMIN && ( + + + + )} + + {APIKey.role === USER_ROLES.EDITOR && ( + + + + )} + + {APIKey.role === USER_ROLES.VIEWER && ( + + + + )} + + {!APIKey.role && ( + + + + )} +
+
+
+
+ ), + children: ( +
+ {APIKey?.createdByUser && ( + + Creator + + + {APIKey?.createdByUser?.name?.substring(0, 1)} + + + {APIKey.createdByUser?.name} + +
{APIKey.createdByUser?.email}
+ +
+ )} + + Created on + + {createdOn} + + + {updatedOn && ( + + Updated on + + {updatedOn} + + + )} + + + Expires on + + {expiresOn} + + +
+ ), + }, + ]; + + return ( +
+ + +
+
+ + Last used + {formattedDateAndTime} +
+ + {!isExpired && expiresIn <= EXPIRATION_WITHIN_SEVEN_DAYS && ( +
+ Expires in {expiresIn} Days +
+ )} + + {isExpired && ( +
+ Expired +
+ )} +
+
+ ); + }, + }, + ]; + + return ( +
+
+
+ API Keys + + Create and manage access keys for the SigNoz API + +
+ +
+ } + value={searchValue} + onChange={handleSearch} + /> + + +
+ + + `${range[0]}-${range[1]} of ${total} API Keys`, + }} + /> + + + {/* Delete Key Modal */} + Delete key} + open={isDeleteModalOpen} + closable + afterClose={handleModalClose} + onCancel={hideDeleteViewModal} + destroyOnClose + footer={[ + , + , + ]} + > + + {t('delete_confirm_message', { + keyName: activeAPIKey?.name, + })} + + + + {/* Edit Key Modal */} + } + > + Cancel + , + , + ]} + > +
+ + + + + + + + +
+ Admin +
+
+ +
+ Editor +
+
+ +
+ Viewer +
+
+
+
+
+ +
+ + {/* Create New Key Modal */} + } + > + Copy key and close + , + ] + : [ + , + , + ] + } + > + {!showNewAPIKeyDetails && ( +
+ + + + + + + + +
+ Admin +
+
+ +
+ Editor +
+
+ +
+ Viewer +
+
+
+
+
+ +
+ + {!query.builder.queryData[0].limit && ( +
+ +
+ )} + + + + ); +} + +export type LogsPanelComponentProps = { + selectedLogsFields: Widgets['selectedLogFields']; + query: Query; + selectedTime?: timePreferance; +}; + +LogsPanelComponent.defaultProps = { + selectedTime: undefined, +}; + +export default LogsPanelComponent; diff --git a/frontend/src/container/LogsPanelTable/utils.tsx b/frontend/src/container/LogsPanelTable/utils.tsx new file mode 100644 index 0000000000..46701e763b --- /dev/null +++ b/frontend/src/container/LogsPanelTable/utils.tsx @@ -0,0 +1,38 @@ +import { ColumnsType } from 'antd/es/table'; +import { Typography } from 'antd/lib'; +// import Typography from 'antd/es/typography/Typography'; +import { RowData } from 'lib/query/createTableColumnsFromQuery'; +import { ReactNode } from 'react'; +import { Widgets } from 'types/api/dashboard/getAll'; +import { IField } from 'types/api/logs/fields'; + +export const getLogPanelColumnsList = ( + selectedLogFields: Widgets['selectedLogFields'], +): ColumnsType => { + const initialColumns: ColumnsType = []; + + const columns: ColumnsType = + selectedLogFields?.map((field: IField) => { + const { name } = field; + return { + title: name, + dataIndex: name, + key: name, + width: name === 'body' ? 350 : 100, + render: (value: ReactNode): JSX.Element => { + if (name === 'body') { + return ( + + {value} + + ); + } + + return {value}; + }, + responsive: ['md'], + }; + }) || []; + + return [...initialColumns, ...columns]; +}; diff --git a/frontend/src/container/MetricsApplication/MetricsApplication.factory.ts b/frontend/src/container/MetricsApplication/MetricsApplication.factory.ts index 20becbb810..b974972d70 100644 --- a/frontend/src/container/MetricsApplication/MetricsApplication.factory.ts +++ b/frontend/src/container/MetricsApplication/MetricsApplication.factory.ts @@ -22,4 +22,6 @@ export const getWidgetQueryBuilder = ({ yAxisUnit, softMax: null, softMin: null, + selectedLogFields: [], + selectedTracesFields: [], }); diff --git a/frontend/src/container/NewDashboard/ComponentsSlider/constants.ts b/frontend/src/container/NewDashboard/ComponentsSlider/constants.ts new file mode 100644 index 0000000000..44512e3a00 --- /dev/null +++ b/frontend/src/container/NewDashboard/ComponentsSlider/constants.ts @@ -0,0 +1,88 @@ +import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder'; +import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse'; +import { Query } from 'types/api/queryBuilder/queryBuilderData'; +import { LogsAggregatorOperator } from 'types/common/queryBuilder'; + +export const PANEL_TYPES_INITIAL_QUERY = { + [PANEL_TYPES.TIME_SERIES]: initialQueriesMap.metrics, + [PANEL_TYPES.VALUE]: initialQueriesMap.metrics, + [PANEL_TYPES.TABLE]: initialQueriesMap.metrics, + [PANEL_TYPES.LIST]: initialQueriesMap.logs, + [PANEL_TYPES.TRACE]: initialQueriesMap.traces, + [PANEL_TYPES.EMPTY_WIDGET]: initialQueriesMap.metrics, +}; + +export const listViewInitialLogQuery: Query = { + ...initialQueriesMap.logs, + builder: { + ...initialQueriesMap.logs.builder, + queryData: [ + { + ...initialQueriesMap.logs.builder.queryData[0], + aggregateOperator: LogsAggregatorOperator.NOOP, + orderBy: [{ columnName: 'timestamp', order: 'desc' }], + offset: 0, + pageSize: 100, + }, + ], + }, +}; + +export const listViewInitialTraceQuery = { + // it should be the above commented query + ...initialQueriesMap.traces, + builder: { + ...initialQueriesMap.traces.builder, + queryData: [ + { + ...initialQueriesMap.traces.builder.queryData[0], + aggregateOperator: LogsAggregatorOperator.NOOP, + orderBy: [{ columnName: 'timestamp', order: 'desc' }], + offset: 0, + pageSize: 10, + selectColumns: [ + { + key: 'serviceName', + dataType: 'string', + type: 'tag', + isColumn: true, + isJSON: false, + id: 'serviceName--string--tag--true', + }, + { + key: 'name', + dataType: 'string', + type: 'tag', + isColumn: true, + isJSON: false, + id: 'name--string--tag--true', + }, + { + key: 'durationNano', + dataType: 'float64', + type: 'tag', + isColumn: true, + isJSON: false, + id: 'durationNano--float64--tag--true', + }, + { + key: 'httpMethod', + dataType: 'string', + type: 'tag', + isColumn: true, + isJSON: false, + id: 'httpMethod--string--tag--true', + }, + { + key: 'responseStatusCode', + dataType: 'string', + type: 'tag', + isColumn: true, + isJSON: false, + id: 'responseStatusCode--string--tag--true', + }, + ] as BaseAutocompleteData[], + }, + ], + }, +}; diff --git a/frontend/src/container/NewDashboard/ComponentsSlider/index.tsx b/frontend/src/container/NewDashboard/ComponentsSlider/index.tsx index d355edfd1a..80f1e745da 100644 --- a/frontend/src/container/NewDashboard/ComponentsSlider/index.tsx +++ b/frontend/src/container/NewDashboard/ComponentsSlider/index.tsx @@ -1,6 +1,6 @@ import { SOMETHING_WENT_WRONG } from 'constants/api'; import { QueryParams } from 'constants/query'; -import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder'; +import { PANEL_TYPES } from 'constants/queryBuilder'; import { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard'; import { useIsDarkMode } from 'hooks/useDarkMode'; import { useNotifications } from 'hooks/useNotifications'; @@ -8,8 +8,14 @@ import createQueryParams from 'lib/createQueryParams'; import history from 'lib/history'; import { useDashboard } from 'providers/Dashboard/Dashboard'; import { CSSProperties } from 'react'; +import { LogsAggregatorOperator } from 'types/common/queryBuilder'; import { v4 as uuid } from 'uuid'; +import { + listViewInitialLogQuery, + listViewInitialTraceQuery, + PANEL_TYPES_INITIAL_QUERY, +} from './constants'; import menuItems from './menuItems'; import { Card, Container, Text } from './styles'; @@ -28,6 +34,7 @@ function DashboardGraphSlider(): JSX.Element { const updateDashboardMutation = useUpdateDashboard(); + // eslint-disable-next-line sonarjs/cognitive-complexity const onClickHandler = (name: PANEL_TYPES) => (): void => { const id = uuid(); @@ -61,10 +68,28 @@ function DashboardGraphSlider(): JSX.Element { nullZeroValues: '', opacity: '', panelTypes: name, - query: initialQueriesMap.metrics, + query: + name === PANEL_TYPES.LIST + ? listViewInitialLogQuery + : PANEL_TYPES_INITIAL_QUERY[name], timePreferance: 'GLOBAL_TIME', softMax: null, softMin: null, + selectedLogFields: [ + { + dataType: 'string', + type: '', + name: 'body', + }, + { + dataType: 'string', + type: '', + name: 'timestamp', + }, + ], + selectedTracesFields: [ + ...listViewInitialTraceQuery.builder.queryData[0].selectColumns, + ], }, ], }, @@ -73,16 +98,43 @@ function DashboardGraphSlider(): JSX.Element { onSuccess: (data) => { if (data.payload) { handleToggleDashboardSlider(false); + const queryParamsLog = { + graphType: name, + widgetId: id, + [QueryParams.compositeQuery]: JSON.stringify({ + ...PANEL_TYPES_INITIAL_QUERY[name], + builder: { + ...PANEL_TYPES_INITIAL_QUERY[name].builder, + queryData: [ + { + ...PANEL_TYPES_INITIAL_QUERY[name].builder.queryData[0], + aggregateOperator: LogsAggregatorOperator.NOOP, + orderBy: [{ columnName: 'timestamp', order: 'desc' }], + offset: 0, + pageSize: 100, + }, + ], + }, + }), + }; const queryParams = { graphType: name, widgetId: id, - [QueryParams.compositeQuery]: JSON.stringify(initialQueriesMap.metrics), + [QueryParams.compositeQuery]: JSON.stringify( + PANEL_TYPES_INITIAL_QUERY[name], + ), }; - history.push( - `${history.location.pathname}/new?${createQueryParams(queryParams)}`, - ); + if (name === PANEL_TYPES.LIST) { + history.push( + `${history.location.pathname}/new?${createQueryParams(queryParamsLog)}`, + ); + } else { + history.push( + `${history.location.pathname}/new?${createQueryParams(queryParams)}`, + ); + } } }, onError: () => { diff --git a/frontend/src/container/NewDashboard/ComponentsSlider/menuItems.ts b/frontend/src/container/NewDashboard/ComponentsSlider/menuItems.ts index 560da8deef..1aaa3a71ea 100644 --- a/frontend/src/container/NewDashboard/ComponentsSlider/menuItems.ts +++ b/frontend/src/container/NewDashboard/ComponentsSlider/menuItems.ts @@ -1,3 +1,4 @@ +import List from 'assets/Dashboard/List'; import TableIcon from 'assets/Dashboard/Table'; import TimeSeriesIcon from 'assets/Dashboard/TimeSeries'; import ValueIcon from 'assets/Dashboard/Value'; @@ -16,6 +17,7 @@ const Items: ItemsProps[] = [ display: 'Value', }, { name: PANEL_TYPES.TABLE, Icon: TableIcon, display: 'Table' }, + { name: PANEL_TYPES.LIST, Icon: List, display: 'List' }, ]; interface ItemsProps { diff --git a/frontend/src/container/NewWidget/LeftContainer/ExplorerColumnsRenderer.styles.scss b/frontend/src/container/NewWidget/LeftContainer/ExplorerColumnsRenderer.styles.scss new file mode 100644 index 0000000000..f9652e859a --- /dev/null +++ b/frontend/src/container/NewWidget/LeftContainer/ExplorerColumnsRenderer.styles.scss @@ -0,0 +1,184 @@ +.explorer-columns-renderer { + margin-top: 10px; + + .title { + display: flex; + align-items: center; + gap: 4px; + } + + .ant-typography { + color: var(rgba(255, 255, 255, 0.85)); + font-family: "Inter"; + font-size: 13px; + font-style: normal; + font-weight: 400; + line-height: 22px; + letter-spacing: 0.5px; + } + + .ant-divider { + margin: 8px 0 !important; + border: 0.5px solid var(--bg-slate-400); + } + + .explorer-columns-contents { + display: flex; + justify-content: space-between; + align-items: center; + + .explorer-columns { + display: flex; + align-items: center; + gap: 12px; + overflow-x: scroll; + min-width: 90%; + + .explorer-columns-list { + display: flex !important; + } + + .explorer-column-card { + display: flex; + align-items: center; + justify-content: space-between; + padding: 4px; + min-width: 200px; + border-radius: 2px; + border: 1px solid var(--colorBorder, rgba(118, 136, 201, 0.12)); + background: var(--bg-slate-500); + cursor: unset; + + .explorer-column-title { + display: flex; + align-items: center; + gap: 8px; + font-family: Inter; + font-size: 12px; + cursor: grab; + } + + .lucide-trash2 { + cursor: pointer !important; + } + + } + } + + .explorer-columns::-webkit-scrollbar { + height: 0px; /* Height of the scrollbar */ + } + + .action-btn { + display: flex; + align-items: center; + justify-content: center; + width: 32px; + height: 32px; + padding: 0px 16px; + border-radius: 2px; + background: var(--bg-robin-400); + } + } +} + +.explorer-columns-search { + border: 1px solid rgba(118, 136, 201, 0.12); + border-radius: 6px; + padding: 0px; + background:#141414; + > input { + height: 32px; + padding: 0 6px; + } + + +} + +.explorer-columns-dropdown { + height: 200px; + background-color: var(--bg-slate-500); + overflow: hidden !important; + .ant-dropdown-menu { + padding: 0; + + .ant-dropdown-menu-item { + padding: 4px; + .ant-checkbox-wrapper { + padding: 2px 8px !important; + } + + .attribute-columns { + display: flex; + flex-direction: column; + height: 160px; + overflow: scroll; + } + + .attribute-columns::-webkit-scrollbar { + width: 3px; /* Width of the scrollbar */ + } + + .attribute-columns::-webkit-scrollbar-track { + background: var(--bg-slate-500); /* Color of the track */ + } + + .attribute-columns::-webkit-scrollbar-thumb { + background: var(--bg-vanilla-400); /* Color of the thumb */ + border-radius: 4px; /* Roundness of the thumb */ + } + + .attribute-columns::-webkit-scrollbar-thumb:hover { + background: var(--bg-vanilla-300); /* Color of the thumb on hover */ + } + } + } +} + +.lightMode { + .explorer-columns-renderer { + + .ant-divider { + border: 0.5px solid var(--bg-vanilla-300); + } + + .explorer-columns { + .explorer-column-card { + border: 1px solid var(--colorBorder, rgba(118, 136, 201, 0.12)); + background: var(--bg-vanilla-200); + } + } + + .explorer-columns-search { + border: 1px solid rgba(118, 136, 201, 0.12); + } + } + + .explorer-columns-dropdown { + background-color: var(--bg-vanilla-100); + + .ant-dropdown-menu-item { + .attribute-columns { + &::-webkit-scrollbar { + width: 3px; /* Width of the scrollbar */ + } + + &::-webkit-scrollbar-track { + background: var(--bg-vanilla-200); /* Color of the track */ + } + + &::-webkit-scrollbar-thumb { + background: var(--bg-vanilla-400); /* Color of the thumb */ + } + + &::-webkit-scrollbar-thumb:hover { + background: var(--bg-vanilla-300); /* Color of the thumb on hover */ + } + } + } + } + + .explorer-columns-search { + background: var(--bg-vanilla-100); + } +} diff --git a/frontend/src/container/NewWidget/LeftContainer/ExplorerColumnsRenderer.tsx b/frontend/src/container/NewWidget/LeftContainer/ExplorerColumnsRenderer.tsx new file mode 100644 index 0000000000..a9a8d9ceb2 --- /dev/null +++ b/frontend/src/container/NewWidget/LeftContainer/ExplorerColumnsRenderer.tsx @@ -0,0 +1,328 @@ +/* eslint-disable sonarjs/cognitive-complexity */ +/* eslint-disable react/jsx-props-no-spreading */ +import './ExplorerColumnsRenderer.styles.scss'; + +import { Color } from '@signozhq/design-tokens'; +import { + Button, + Checkbox, + Divider, + Dropdown, + Input, + Tooltip, + Typography, +} from 'antd'; +import { MenuProps } from 'antd/lib'; +import Spinner from 'components/Spinner'; +import { SOMETHING_WENT_WRONG } from 'constants/api'; +import { useGetAggregateKeys } from 'hooks/queryBuilder/useGetAggregateKeys'; +import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder'; +import { useIsDarkMode } from 'hooks/useDarkMode'; +import { + AlertCircle, + GripVertical, + PlusCircle, + Search, + Trash2, +} from 'lucide-react'; +import { useState } from 'react'; +import { + DragDropContext, + Draggable, + Droppable, + DropResult, +} from 'react-beautiful-dnd'; +import { DataSource } from 'types/common/queryBuilder'; + +import { WidgetGraphProps } from '../types'; + +type LogColumnsRendererProps = { + setSelectedLogFields: WidgetGraphProps['setSelectedLogFields']; + selectedLogFields: WidgetGraphProps['selectedLogFields']; + selectedTracesFields: WidgetGraphProps['selectedTracesFields']; + setSelectedTracesFields: WidgetGraphProps['setSelectedTracesFields']; +}; + +function ExplorerColumnsRenderer({ + selectedLogFields, + setSelectedLogFields, + selectedTracesFields, + setSelectedTracesFields, +}: LogColumnsRendererProps): JSX.Element { + const { currentQuery } = useQueryBuilder(); + const [searchText, setSearchText] = useState(''); + const [open, setOpen] = useState(false); + + const initialDataSource = currentQuery.builder.queryData[0].dataSource; + + const { data, isLoading, isError } = useGetAggregateKeys( + { + aggregateAttribute: '', + dataSource: currentQuery.builder.queryData[0].dataSource, + aggregateOperator: currentQuery.builder.queryData[0].aggregateOperator, + searchText: '', + tagType: '', + }, + { + queryKey: [ + currentQuery.builder.queryData[0].dataSource, + currentQuery.builder.queryData[0].aggregateOperator, + ], + }, + ); + + const isAttributeKeySelected = (key: string): boolean => { + if (initialDataSource === DataSource.LOGS && selectedLogFields) { + return selectedLogFields.some((field) => field.name === key); + } + if (initialDataSource === DataSource.TRACES && selectedTracesFields) { + return selectedTracesFields.some((field) => field.key === key); + } + return false; + }; + + const handleCheckboxChange = (key: string): void => { + if ( + initialDataSource === DataSource.LOGS && + setSelectedLogFields !== undefined + ) { + if (selectedLogFields) { + if (isAttributeKeySelected(key)) { + setSelectedLogFields( + selectedLogFields.filter((field) => field.name !== key), + ); + } else { + setSelectedLogFields([ + ...selectedLogFields, + { dataType: 'string', name: key, type: '' }, + ]); + } + } else { + setSelectedLogFields([{ dataType: 'string', name: key, type: '' }]); + } + } else if ( + initialDataSource === DataSource.TRACES && + setSelectedTracesFields !== undefined + ) { + const selectedField = data?.payload?.attributeKeys?.find( + (attributeKey) => attributeKey.key === key, + ); + if (selectedTracesFields) { + if (isAttributeKeySelected(key)) { + setSelectedTracesFields( + selectedTracesFields.filter((field) => field.key !== key), + ); + } else if (selectedField) { + setSelectedTracesFields([...selectedTracesFields, selectedField]); + } + } else if (selectedField) setSelectedTracesFields([selectedField]); + } + setOpen(false); + }; + + const handleSearchChange = (e: React.ChangeEvent): void => { + setSearchText(e.target.value); + }; + + const items: MenuProps['items'] = [ + { + key: 'search', + label: ( + } + /> + ), + }, + { + key: 'columns', + label: ( +
+ {data?.payload?.attributeKeys + ?.filter((attributeKey) => + attributeKey.key.toLowerCase().includes(searchText.toLowerCase()), + ) + ?.map((attributeKey) => ( + handleCheckboxChange(attributeKey.key)} + style={{ padding: 0 }} + key={attributeKey.key} + > + {attributeKey.key} + + ))} +
+ ), + }, + ]; + + const removeSelectedLogField = (name: string): void => { + if ( + initialDataSource === DataSource.LOGS && + setSelectedLogFields && + selectedLogFields + ) { + setSelectedLogFields( + selectedLogFields.filter((field) => field.name !== name), + ); + } + if ( + initialDataSource === DataSource.TRACES && + setSelectedTracesFields && + selectedTracesFields + ) { + setSelectedTracesFields( + selectedTracesFields.filter((field) => field.key !== name), + ); + } + }; + + const onDragEnd = (result: DropResult): void => { + if (!result.destination) { + return; + } + + if ( + initialDataSource === DataSource.LOGS && + selectedLogFields && + setSelectedLogFields + ) { + const items = [...selectedLogFields]; + const [reorderedItem] = items.splice(result.source.index, 1); + items.splice(result.destination.index, 0, reorderedItem); + + setSelectedLogFields(items); + } + if ( + initialDataSource === DataSource.TRACES && + selectedTracesFields && + setSelectedTracesFields + ) { + const items = [...selectedTracesFields]; + const [reorderedItem] = items.splice(result.source.index, 1); + items.splice(result.destination.index, 0, reorderedItem); + + setSelectedTracesFields(items); + } + }; + + const toggleDropdown = (): void => { + setOpen(!open); + if (!open) { + setSearchText(''); + } + }; + + const isDarkMode = useIsDarkMode(); + + if (isLoading) { + return ; + } + + return ( +
+
+ Columns + {isError && ( + + + + )} +
+ + {!isError && ( +
+ + + {(provided): JSX.Element => ( +
+ {initialDataSource === DataSource.LOGS && + selectedLogFields && + selectedLogFields.map((field, index) => ( + // eslint-disable-next-line react/no-array-index-key + + {(dragProvided): JSX.Element => ( +
+
+ + {field.name} +
+ removeSelectedLogField(field.name)} + /> +
+ )} +
+ ))} + {initialDataSource === DataSource.TRACES && + selectedTracesFields && + selectedTracesFields.map((field, index) => ( + // eslint-disable-next-line react/no-array-index-key + + {(dragProvided): JSX.Element => ( +
+
+ + {field.key} +
+ removeSelectedLogField(field.key)} + /> +
+ )} +
+ ))} +
+ )} +
+
+
+ +
+
+ )} +
+ ); +} + +export default ExplorerColumnsRenderer; diff --git a/frontend/src/container/NewWidget/LeftContainer/QuerySection/index.tsx b/frontend/src/container/NewWidget/LeftContainer/QuerySection/index.tsx index 9fe20bd8bc..4c66c70b83 100644 --- a/frontend/src/container/NewWidget/LeftContainer/QuerySection/index.tsx +++ b/frontend/src/container/NewWidget/LeftContainer/QuerySection/index.tsx @@ -18,7 +18,7 @@ import { getPreviousWidgets, getSelectedWidgetIndex, } from 'providers/Dashboard/util'; -import { useCallback, useMemo, useState } from 'react'; +import { useCallback, useMemo } from 'react'; import { useSelector } from 'react-redux'; import { AppState } from 'store/reducers'; import { Widgets } from 'types/api/dashboard/getAll'; @@ -35,7 +35,6 @@ function QuerySection({ selectedTime, }: QueryProps): JSX.Element { const { currentQuery, redirectWithQueryBuilderData } = useQueryBuilder(); - const [currentTab, setCurrentTab] = useState(currentQuery.queryType); const urlQuery = useUrlQuery(); const { minTime, maxTime } = useSelector( @@ -100,7 +99,6 @@ function QuerySection({ ], }, }); - redirectWithQueryBuilderData(updatedQuery); }, [ @@ -114,11 +112,13 @@ function QuerySection({ ); const handleQueryCategoryChange = (qCategory: string): void => { - const currentQueryType = qCategory as EQueryType; - setCurrentTab(qCategory as EQueryType); + const currentQueryType = qCategory; featureResponse.refetch().then(() => { - handleStageQuery({ ...currentQuery, queryType: currentQueryType }); + handleStageQuery({ + ...currentQuery, + queryType: currentQueryType as EQueryType, + }); }); }; @@ -134,6 +134,27 @@ function QuerySection({ return config; }, []); + const listItems = [ + { + key: EQueryType.QUERY_BUILDER, + label: ( + + + + ), + tab: Query Builder, + children: ( + + ), + }, + ]; + const items = [ { key: EQueryType.QUERY_BUILDER, @@ -180,8 +201,12 @@ function QuerySection({ @@ -197,7 +222,7 @@ function QuerySection({ } - items={items} + items={selectedGraph === PANEL_TYPES.LIST ? listItems : items} /> ); diff --git a/frontend/src/container/NewWidget/LeftContainer/WidgetGraph/PlotTag.tsx b/frontend/src/container/NewWidget/LeftContainer/WidgetGraph/PlotTag.tsx index 219d4e011b..99da2e517e 100644 --- a/frontend/src/container/NewWidget/LeftContainer/WidgetGraph/PlotTag.tsx +++ b/frontend/src/container/NewWidget/LeftContainer/WidgetGraph/PlotTag.tsx @@ -10,7 +10,7 @@ interface IPlotTagProps { } function PlotTag({ queryType, panelType }: IPlotTagProps): JSX.Element | null { - if (queryType === undefined) { + if (queryType === undefined || panelType === PANEL_TYPES.LIST) { return null; } diff --git a/frontend/src/container/NewWidget/LeftContainer/WidgetGraph/WidgetGraphContainer.tsx b/frontend/src/container/NewWidget/LeftContainer/WidgetGraph/WidgetGraphContainer.tsx index c3d47880d3..2472d94092 100644 --- a/frontend/src/container/NewWidget/LeftContainer/WidgetGraph/WidgetGraphContainer.tsx +++ b/frontend/src/container/NewWidget/LeftContainer/WidgetGraph/WidgetGraphContainer.tsx @@ -1,5 +1,6 @@ import { Card, Typography } from 'antd'; import Spinner from 'components/Spinner'; +import { PANEL_TYPES } from 'constants/queryBuilder'; import { WidgetGraphProps } from 'container/NewWidget/types'; import { useGetWidgetQueryRange } from 'hooks/queryBuilder/useGetWidgetQueryRange'; import useUrlQuery from 'hooks/useUrlQuery'; @@ -16,6 +17,8 @@ function WidgetGraphContainer({ fillSpans = false, softMax, softMin, + selectedLogFields, + selectedTracesFields, }: WidgetGraphProps): JSX.Element { const { selectedDashboard } = useDashboard(); @@ -46,7 +49,21 @@ function WidgetGraphContainer({ if (getWidgetQueryRange.isLoading) { return ; } - if (getWidgetQueryRange.data?.payload.data.result.length === 0) { + + if ( + selectedGraph !== PANEL_TYPES.LIST && + getWidgetQueryRange.data?.payload.data.result.length === 0 + ) { + return ( + + No Data + + ); + } + if ( + selectedGraph === PANEL_TYPES.LIST && + getWidgetQueryRange.data?.payload.data.newResult.data.result.length === 0 + ) { return ( No Data @@ -63,6 +80,9 @@ function WidgetGraphContainer({ fillSpans={fillSpans} softMax={softMax} softMin={softMin} + selectedLogFields={selectedLogFields} + selectedTracesFields={selectedTracesFields} + selectedTime={selectedTime} /> ); } diff --git a/frontend/src/container/NewWidget/LeftContainer/WidgetGraph/WidgetGraphs.tsx b/frontend/src/container/NewWidget/LeftContainer/WidgetGraph/WidgetGraphs.tsx index f4a8ed1b22..ccd0a91ea3 100644 --- a/frontend/src/container/NewWidget/LeftContainer/WidgetGraph/WidgetGraphs.tsx +++ b/frontend/src/container/NewWidget/LeftContainer/WidgetGraph/WidgetGraphs.tsx @@ -1,6 +1,7 @@ import { QueryParams } from 'constants/query'; import GridPanelSwitch from 'container/GridPanelSwitch'; import { ThresholdProps } from 'container/NewWidget/RightContainer/Threshold/types'; +import { timePreferance } from 'container/NewWidget/RightContainer/timeItems'; import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder'; import { useIsDarkMode } from 'hooks/useDarkMode'; import { useResizeObserver } from 'hooks/useDimensions'; @@ -30,8 +31,11 @@ function WidgetGraph({ fillSpans, softMax, softMin, + selectedLogFields, + selectedTracesFields, + selectedTime, }: WidgetGraphProps): JSX.Element { - const { stagedQuery } = useQueryBuilder(); + const { stagedQuery, currentQuery } = useQueryBuilder(); const { minTime, maxTime, selectedTime: globalSelectedInterval } = useSelector< AppState, @@ -156,6 +160,10 @@ function WidgetGraph({ } query={stagedQuery || selectedWidget.query} thresholds={thresholds} + selectedLogFields={selectedLogFields} + selectedTracesFields={selectedTracesFields} + dataSource={currentQuery.builder.queryData[0].dataSource} + selectedTime={selectedTime} /> ); @@ -172,6 +180,9 @@ interface WidgetGraphProps { >; softMax: number | null; softMin: number | null; + selectedLogFields: Widgets['selectedLogFields']; + selectedTracesFields: Widgets['selectedTracesFields']; + selectedTime: timePreferance; } export default WidgetGraph; diff --git a/frontend/src/container/NewWidget/LeftContainer/WidgetGraph/index.tsx b/frontend/src/container/NewWidget/LeftContainer/WidgetGraph/index.tsx index 56846c8dec..1f306a41a2 100644 --- a/frontend/src/container/NewWidget/LeftContainer/WidgetGraph/index.tsx +++ b/frontend/src/container/NewWidget/LeftContainer/WidgetGraph/index.tsx @@ -19,6 +19,8 @@ function WidgetGraph({ fillSpans, softMax, softMin, + selectedLogFields, + selectedTracesFields, }: WidgetGraphProps): JSX.Element { const { currentQuery } = useQueryBuilder(); const { selectedDashboard } = useDashboard(); @@ -57,6 +59,8 @@ function WidgetGraph({ fillSpans={fillSpans} softMax={softMax} softMin={softMin} + selectedLogFields={selectedLogFields} + selectedTracesFields={selectedTracesFields} /> ); diff --git a/frontend/src/container/NewWidget/LeftContainer/WidgetGraph/styles.ts b/frontend/src/container/NewWidget/LeftContainer/WidgetGraph/styles.ts index b1bf36b588..a5d030e27c 100644 --- a/frontend/src/container/NewWidget/LeftContainer/WidgetGraph/styles.ts +++ b/frontend/src/container/NewWidget/LeftContainer/WidgetGraph/styles.ts @@ -13,8 +13,10 @@ export const Container = styled(Card)` .ant-card-body { padding: ${({ $panelType }): string => - $panelType === PANEL_TYPES.TABLE ? '0 0' : '1.5rem 0'}; - height: 57vh; + $panelType === PANEL_TYPES.TABLE || $panelType === PANEL_TYPES.LIST + ? '0 0' + : '1.5rem 0'}; + height: 60vh; display: flex; flex-direction: column; } diff --git a/frontend/src/container/NewWidget/LeftContainer/index.tsx b/frontend/src/container/NewWidget/LeftContainer/index.tsx index 0a78a084d8..5dd429d83f 100644 --- a/frontend/src/container/NewWidget/LeftContainer/index.tsx +++ b/frontend/src/container/NewWidget/LeftContainer/index.tsx @@ -1,6 +1,8 @@ +import { PANEL_TYPES } from 'constants/queryBuilder'; import { memo } from 'react'; import { WidgetGraphProps } from '../types'; +import ExplorerColumnsRenderer from './ExplorerColumnsRenderer'; import QuerySection from './QuerySection'; import { QueryContainer } from './styles'; import WidgetGraph from './WidgetGraph'; @@ -13,6 +15,10 @@ function LeftContainer({ fillSpans, softMax, softMin, + selectedLogFields, + setSelectedLogFields, + selectedTracesFields, + setSelectedTracesFields, }: WidgetGraphProps): JSX.Element { return ( <> @@ -24,9 +30,19 @@ function LeftContainer({ fillSpans={fillSpans} softMax={softMax} softMin={softMin} + selectedLogFields={selectedLogFields} + selectedTracesFields={selectedTracesFields} /> + {selectedGraph === PANEL_TYPES.LIST && ( + + )} ); diff --git a/frontend/src/container/NewWidget/RightContainer/constants.ts b/frontend/src/container/NewWidget/RightContainer/constants.ts index 0030f6927b..a6663ac75c 100644 --- a/frontend/src/container/NewWidget/RightContainer/constants.ts +++ b/frontend/src/container/NewWidget/RightContainer/constants.ts @@ -47,3 +47,41 @@ export const panelTypeVsDragAndDrop: { [key in PANEL_TYPES]: boolean } = { [PANEL_TYPES.TRACE]: false, [PANEL_TYPES.EMPTY_WIDGET]: false, } as const; + +export const panelTypeVsFillSpan: { [key in PANEL_TYPES]: boolean } = { + [PANEL_TYPES.TIME_SERIES]: true, + [PANEL_TYPES.VALUE]: false, + [PANEL_TYPES.TABLE]: false, + [PANEL_TYPES.LIST]: false, + [PANEL_TYPES.TRACE]: false, + [PANEL_TYPES.EMPTY_WIDGET]: false, +} as const; + +export const panelTypeVsYAxisUnit: { [key in PANEL_TYPES]: boolean } = { + [PANEL_TYPES.TIME_SERIES]: true, + [PANEL_TYPES.VALUE]: true, + [PANEL_TYPES.TABLE]: true, + [PANEL_TYPES.LIST]: false, + [PANEL_TYPES.TRACE]: false, + [PANEL_TYPES.EMPTY_WIDGET]: false, +} as const; + +export const panelTypeVsCreateAlert: { [key in PANEL_TYPES]: boolean } = { + [PANEL_TYPES.TIME_SERIES]: true, + [PANEL_TYPES.VALUE]: true, + [PANEL_TYPES.TABLE]: false, + [PANEL_TYPES.LIST]: false, + [PANEL_TYPES.TRACE]: false, + [PANEL_TYPES.EMPTY_WIDGET]: false, +} as const; + +export const panelTypeVsPanelTimePreferences: { + [key in PANEL_TYPES]: boolean; +} = { + [PANEL_TYPES.TIME_SERIES]: true, + [PANEL_TYPES.VALUE]: true, + [PANEL_TYPES.TABLE]: true, + [PANEL_TYPES.LIST]: false, + [PANEL_TYPES.TRACE]: false, + [PANEL_TYPES.EMPTY_WIDGET]: false, +} as const; diff --git a/frontend/src/container/NewWidget/RightContainer/index.tsx b/frontend/src/container/NewWidget/RightContainer/index.tsx index 5ab1a965fc..60e90c866e 100644 --- a/frontend/src/container/NewWidget/RightContainer/index.tsx +++ b/frontend/src/container/NewWidget/RightContainer/index.tsx @@ -17,7 +17,14 @@ import useCreateAlerts from 'hooks/queryBuilder/useCreateAlerts'; import { Dispatch, SetStateAction, useCallback } from 'react'; import { Widgets } from 'types/api/dashboard/getAll'; -import { panelTypeVsSoftMinMax, panelTypeVsThreshold } from './constants'; +import { + panelTypeVsCreateAlert, + panelTypeVsFillSpan, + panelTypeVsPanelTimePreferences, + panelTypeVsSoftMinMax, + panelTypeVsThreshold, + panelTypeVsYAxisUnit, +} from './constants'; import { Container, Title } from './styles'; import ThresholdSelector from './Threshold/ThresholdSelector'; import { ThresholdProps } from './Threshold/types'; @@ -62,6 +69,11 @@ function RightContainer({ const allowThreshold = panelTypeVsThreshold[selectedGraph]; const allowSoftMinMax = panelTypeVsSoftMinMax[selectedGraph]; + const allowFillSpans = panelTypeVsFillSpan[selectedGraph]; + const allowYAxisUnit = panelTypeVsYAxisUnit[selectedGraph]; + const allowCreateAlerts = panelTypeVsCreateAlert[selectedGraph]; + const allowPanelTimePreference = + panelTypeVsPanelTimePreferences[selectedGraph]; const softMinHandler = useCallback( (value: number | null) => { @@ -117,32 +129,40 @@ function RightContainer({ } /> - - Fill gaps + {allowFillSpans && ( + + Fill gaps - setIsFillSpans(checked)} - /> - + setIsFillSpans(checked)} + /> + + )} - Panel Time Preference + {allowPanelTimePreference && ( + Panel Time Preference + )} - + {allowPanelTimePreference && ( + + )} - + {allowYAxisUnit && ( + + )} - {selectedWidget?.panelTypes !== PANEL_TYPES.TABLE && ( + {allowCreateAlerts && ( diff --git a/frontend/src/container/NewWidget/index.tsx b/frontend/src/container/NewWidget/index.tsx index 5192644a5e..f4c62ae601 100644 --- a/frontend/src/container/NewWidget/index.tsx +++ b/frontend/src/container/NewWidget/index.tsx @@ -24,6 +24,7 @@ import { useSelector } from 'react-redux'; import { generatePath, useLocation, useParams } from 'react-router-dom'; import { AppState } from 'store/reducers'; import { Dashboard, Widgets } from 'types/api/dashboard/getAll'; +import { IField } from 'types/api/logs/fields'; import { EQueryType } from 'types/common/dashboard'; import { DataSource } from 'types/common/queryBuilder'; import AppReducer from 'types/reducer/app'; @@ -110,6 +111,14 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element { : selectedWidget?.softMin || 0, ); + const [selectedLogFields, setSelectedLogFields] = useState( + selectedWidget?.selectedLogFields || null, + ); + + const [selectedTracesFields, setSelectedTracesFields] = useState( + selectedWidget?.selectedTracesFields || null, + ); + const [softMax, setSoftMax] = useState( selectedWidget?.softMax === null || selectedWidget?.softMax === undefined ? null @@ -189,10 +198,13 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element { title, yAxisUnit, panelTypes: graphType, + query: currentQuery, thresholds, softMin, softMax, fillSpans: isFillSpans, + selectedLogFields, + selectedTracesFields, }, ...afterWidgets, ], @@ -226,10 +238,13 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element { title, yAxisUnit, graphType, + currentQuery, thresholds, softMin, softMax, isFillSpans, + selectedLogFields, + selectedTracesFields, afterWidgets, updateDashboardMutation, setSelectedDashboard, @@ -336,6 +351,10 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element { fillSpans={isFillSpans} softMax={softMax} softMin={softMin} + selectedLogFields={selectedLogFields} + setSelectedLogFields={setSelectedLogFields} + selectedTracesFields={selectedTracesFields} + setSelectedTracesFields={setSelectedTracesFields} /> diff --git a/frontend/src/container/NewWidget/types.ts b/frontend/src/container/NewWidget/types.ts index 21d2268d76..31cdf0c87c 100644 --- a/frontend/src/container/NewWidget/types.ts +++ b/frontend/src/container/NewWidget/types.ts @@ -1,4 +1,5 @@ import { PANEL_TYPES } from 'constants/queryBuilder'; +import { Dispatch, SetStateAction } from 'react'; import { Widgets } from 'types/api/dashboard/getAll'; import { ThresholdProps } from './RightContainer/Threshold/types'; @@ -15,4 +16,10 @@ export interface WidgetGraphProps extends NewWidgetProps { thresholds: ThresholdProps[]; softMin: number | null; softMax: number | null; + selectedLogFields: Widgets['selectedLogFields']; + setSelectedLogFields?: Dispatch>; + selectedTracesFields: Widgets['selectedTracesFields']; + setSelectedTracesFields?: Dispatch< + SetStateAction + >; } diff --git a/frontend/src/container/NoLogs/NoLogs.tsx b/frontend/src/container/NoLogs/NoLogs.tsx index df934b7bcc..1274bdb20a 100644 --- a/frontend/src/container/NoLogs/NoLogs.tsx +++ b/frontend/src/container/NoLogs/NoLogs.tsx @@ -9,13 +9,17 @@ export default function NoLogs(): JSX.Element {
eyes emoji - No logs yet.{' '} + No logs yet. When we receive logs, they would show up here - + Sending Logs to SigNoz
diff --git a/frontend/src/container/OptionsMenu/AddColumnField/index.tsx b/frontend/src/container/OptionsMenu/AddColumnField/index.tsx index 5b5382a016..e6f082e500 100644 --- a/frontend/src/container/OptionsMenu/AddColumnField/index.tsx +++ b/frontend/src/container/OptionsMenu/AddColumnField/index.tsx @@ -1,6 +1,5 @@ import { SearchOutlined } from '@ant-design/icons'; -import { Input, Spin } from 'antd'; -import Typography from 'antd/es/typography/Typography'; +import { Input, Spin, Typography } from 'antd'; import { useIsDarkMode } from 'hooks/useDarkMode'; import { useTranslation } from 'react-i18next'; diff --git a/frontend/src/container/QueryBuilder/QueryBuilder.interfaces.ts b/frontend/src/container/QueryBuilder/QueryBuilder.interfaces.ts index 0923395296..ef18d8ce39 100644 --- a/frontend/src/container/QueryBuilder/QueryBuilder.interfaces.ts +++ b/frontend/src/container/QueryBuilder/QueryBuilder.interfaces.ts @@ -26,4 +26,5 @@ export type QueryBuilderProps = { actions?: ReactNode; filterConfigs?: Partial; queryComponents?: { renderOrderBy?: (props: OrderByFilterProps) => ReactNode }; + isListViewPanel?: boolean; }; diff --git a/frontend/src/container/QueryBuilder/QueryBuilder.tsx b/frontend/src/container/QueryBuilder/QueryBuilder.tsx index 39875a7c90..f67533e427 100644 --- a/frontend/src/container/QueryBuilder/QueryBuilder.tsx +++ b/frontend/src/container/QueryBuilder/QueryBuilder.tsx @@ -1,7 +1,12 @@ import './QueryBuilder.styles.scss'; import { Button, Col, Divider, Row, Tooltip } from 'antd'; -import { MAX_FORMULAS, MAX_QUERIES } from 'constants/queryBuilder'; +import { + MAX_FORMULAS, + MAX_QUERIES, + OPERATORS, + PANEL_TYPES, +} from 'constants/queryBuilder'; // ** Hooks import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder'; import { DatabaseZap, Sigma } from 'lucide-react'; @@ -19,6 +24,7 @@ export const QueryBuilder = memo(function QueryBuilder({ panelType: newPanelType, filterConfigs = {}, queryComponents, + isListViewPanel = false, }: QueryBuilderProps): JSX.Element { const { currentQuery, @@ -84,6 +90,33 @@ export const QueryBuilder = memo(function QueryBuilder({ } }; + const listViewLogFilterConfigs: QueryBuilderProps['filterConfigs'] = useMemo(() => { + const config: QueryBuilderProps['filterConfigs'] = { + stepInterval: { isHidden: true, isDisabled: true }, + having: { isHidden: true, isDisabled: true }, + filters: { + customKey: 'body', + customOp: OPERATORS.CONTAINS, + }, + }; + + return config; + }, []); + + const listViewTracesFilterConfigs: QueryBuilderProps['filterConfigs'] = useMemo(() => { + const config: QueryBuilderProps['filterConfigs'] = { + stepInterval: { isHidden: true, isDisabled: true }, + having: { isHidden: true, isDisabled: true }, + limit: { isHidden: true, isDisabled: true }, + filters: { + customKey: 'body', + customOp: OPERATORS.CONTAINS, + }, + }; + + return config; + }, []); + return ( -
- - - - + {!isListViewPanel && ( +
+ + + + - - - - -
+ + + +
+
+ )}
@@ -119,49 +154,66 @@ export const QueryBuilder = memo(function QueryBuilder({ className="query-builder-queries-formula-container" ref={containerRef} > - {currentQuery.builder.queryData.map((query, index) => ( - - - - ))} - {currentQuery.builder.queryFormulas.map((formula, index) => { - const isAllMetricDataSource = currentQuery.builder.queryData.every( - (query) => query.dataSource === DataSource.METRICS, - ); - - const query = - currentQuery.builder.queryData[index] || - currentQuery.builder.queryData[0]; - - return ( + {panelType === PANEL_TYPES.LIST && isListViewPanel && ( + + )} + {!isListViewPanel && + currentQuery.builder.queryData.map((query, index) => ( - - ); - })} + ))} + {!isListViewPanel && + currentQuery.builder.queryFormulas.map((formula, index) => { + const isAllMetricDataSource = currentQuery.builder.queryData.every( + (query) => query.dataSource === DataSource.METRICS, + ); + + const query = + currentQuery.builder.queryData[index] || + currentQuery.builder.queryData[0]; + + return ( + + + + ); + })} @@ -171,29 +223,31 @@ export const QueryBuilder = memo(function QueryBuilder({ - - {currentQuery.builder.queryData.map((query) => ( - - ))} + {!isListViewPanel && ( + + {currentQuery.builder.queryData.map((query) => ( + + ))} - {currentQuery.builder.queryFormulas.map((formula) => ( - - ))} - + {currentQuery.builder.queryFormulas.map((formula) => ( + + ))} + + )} ); }); diff --git a/frontend/src/container/QueryBuilder/components/DataSourceDropdown/DataSourceDropdown.interfaces.ts b/frontend/src/container/QueryBuilder/components/DataSourceDropdown/DataSourceDropdown.interfaces.ts index 0a34d52c6f..acbe586220 100644 --- a/frontend/src/container/QueryBuilder/components/DataSourceDropdown/DataSourceDropdown.interfaces.ts +++ b/frontend/src/container/QueryBuilder/components/DataSourceDropdown/DataSourceDropdown.interfaces.ts @@ -3,4 +3,5 @@ import { DataSource } from 'types/common/queryBuilder'; export type QueryLabelProps = { onChange: (value: DataSource) => void; + isListViewPanel?: boolean; } & Omit; diff --git a/frontend/src/container/QueryBuilder/components/DataSourceDropdown/DataSourceDropdown.tsx b/frontend/src/container/QueryBuilder/components/DataSourceDropdown/DataSourceDropdown.tsx index 52b3d030cb..81efc3b9d7 100644 --- a/frontend/src/container/QueryBuilder/components/DataSourceDropdown/DataSourceDropdown.tsx +++ b/frontend/src/container/QueryBuilder/components/DataSourceDropdown/DataSourceDropdown.tsx @@ -10,18 +10,22 @@ import { QueryLabelProps } from './DataSourceDropdown.interfaces'; const dataSourceMap = [DataSource.LOGS, DataSource.METRICS, DataSource.TRACES]; +const exploreDataSourceMap = [DataSource.LOGS, DataSource.TRACES]; + export const DataSourceDropdown = memo(function DataSourceDropdown( props: QueryLabelProps, ): JSX.Element { - const { onChange, value, style } = props; + const { onChange, value, style, isListViewPanel = false } = props; - const dataSourceOptions: SelectOption< - DataSource, - string - >[] = dataSourceMap.map((source) => ({ - label: transformToUpperCase(source), - value: source, - })); + const dataSourceOptions: SelectOption[] = isListViewPanel + ? exploreDataSourceMap.map((source) => ({ + label: transformToUpperCase(source), + value: source, + })) + : dataSourceMap.map((source) => ({ + label: transformToUpperCase(source), + value: source, + })); return ( onChangeSelectedView(SELECTED_VIEWS.SEARCH)} > - + @@ -52,7 +52,7 @@ export default function LeftToolbarActions({ )} onClick={(): void => onChangeSelectedView(SELECTED_VIEWS.QUERY_BUILDER)} > - + @@ -66,7 +66,7 @@ export default function LeftToolbarActions({ )} onClick={(): void => onChangeSelectedView(SELECTED_VIEWS.CLICKHOUSE)} > - + )} diff --git a/frontend/src/container/QueryBuilder/filters/OrderByFilter/OrderByFilter.interfaces.ts b/frontend/src/container/QueryBuilder/filters/OrderByFilter/OrderByFilter.interfaces.ts index 7a1f9b53ae..154fdb8b10 100644 --- a/frontend/src/container/QueryBuilder/filters/OrderByFilter/OrderByFilter.interfaces.ts +++ b/frontend/src/container/QueryBuilder/filters/OrderByFilter/OrderByFilter.interfaces.ts @@ -6,6 +6,7 @@ import { export type OrderByFilterProps = { query: IBuilderQuery; onChange: (values: OrderByPayload[]) => void; + isListViewPanel?: boolean; }; export type OrderByFilterValue = { diff --git a/frontend/src/container/QueryBuilder/filters/OrderByFilter/OrderByFilter.tsx b/frontend/src/container/QueryBuilder/filters/OrderByFilter/OrderByFilter.tsx index 2f707ec7a1..dc6fb964fa 100644 --- a/frontend/src/container/QueryBuilder/filters/OrderByFilter/OrderByFilter.tsx +++ b/frontend/src/container/QueryBuilder/filters/OrderByFilter/OrderByFilter.tsx @@ -11,6 +11,7 @@ import { useOrderByFilter } from './useOrderByFilter'; export function OrderByFilter({ query, onChange, + isListViewPanel = false, }: OrderByFilterProps): JSX.Element { const { debouncedSearchText, @@ -30,7 +31,7 @@ export function OrderByFilter({ searchText: debouncedSearchText, }, { - enabled: !!query.aggregateAttribute.key, + enabled: !!query.aggregateAttribute.key || isListViewPanel, keepPreviousData: true, }, ); diff --git a/frontend/src/container/SideNav/NavItem/NavItem.styles.scss b/frontend/src/container/SideNav/NavItem/NavItem.styles.scss index f182a1df6d..fe5952d400 100644 --- a/frontend/src/container/SideNav/NavItem/NavItem.styles.scss +++ b/frontend/src/container/SideNav/NavItem/NavItem.styles.scss @@ -7,6 +7,7 @@ height: 36px; margin-bottom: 4px; + cursor: pointer; &.active { .nav-item-active-marker { diff --git a/frontend/src/container/SideNav/NavItem/NavItem.tsx b/frontend/src/container/SideNav/NavItem/NavItem.tsx index 0ec6127da1..4171f2f4f5 100644 --- a/frontend/src/container/SideNav/NavItem/NavItem.tsx +++ b/frontend/src/container/SideNav/NavItem/NavItem.tsx @@ -16,13 +16,16 @@ export default function NavItem({ isCollapsed: boolean; item: SidebarItem; isActive: boolean; - onClick: () => void; + onClick: (event: React.MouseEvent) => void; }): JSX.Element { const { label, icon } = item; return ( -
+
onClick(event)} + >
{icon}
diff --git a/frontend/src/container/SideNav/SideNav.tsx b/frontend/src/container/SideNav/SideNav.tsx index a98d9eacd9..665a406710 100644 --- a/frontend/src/container/SideNav/SideNav.tsx +++ b/frontend/src/container/SideNav/SideNav.tsx @@ -19,7 +19,7 @@ import { RocketIcon, UserCircle, } from 'lucide-react'; -import { useCallback, useEffect, useMemo, useState } from 'react'; +import { MouseEvent, useCallback, useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useSelector } from 'react-redux'; import { useLocation } from 'react-router-dom'; @@ -40,7 +40,7 @@ import defaultMenuItems, { trySignozCloudMenuItem, } from './menuItems'; import NavItem from './NavItem/NavItem'; -import { SecondaryMenuItemKey } from './sideNav.types'; +import { SecondaryMenuItemKey, SidebarItem } from './sideNav.types'; import { getActiveMenuKeyFromPath } from './sideNav.utils'; interface UserManagementMenuItems { @@ -88,10 +88,6 @@ function SideNav({ window.open('https://signoz.io/slack', '_blank'); }; - const onClickVersionHandler = (): void => { - history.push(ROUTES.VERSION); - }; - const isLatestVersion = checkVersionState(currentVersion, latestVersion); const [inviteMembers] = useComponentPermission(['invite_members'], role); @@ -164,23 +160,49 @@ function SideNav({ ); }; - const onClickShortcuts = (): void => { - history.push(`/shortcuts`); + const isCtrlMetaKey = (e: MouseEvent): boolean => e.ctrlKey || e.metaKey; + + const openInNewTab = (path: string): void => { + window.open(path, '_blank'); }; - const onClickGetStarted = (): void => { - history.push(`/get-started`); + const onClickShortcuts = (e: MouseEvent): void => { + if (isCtrlMetaKey(e)) { + openInNewTab('/shortcuts'); + } else { + history.push(`/shortcuts`); + } + }; + + const onClickGetStarted = (event: MouseEvent): void => { + if (isCtrlMetaKey(event)) { + openInNewTab('/get-started'); + } else { + history.push(`/get-started`); + } + }; + + const onClickVersionHandler = (event: MouseEvent): void => { + if (isCtrlMetaKey(event)) { + openInNewTab(ROUTES.VERSION); + } else { + history.push(ROUTES.VERSION); + } }; const onClickHandler = useCallback( - (key: string) => { + (key: string, event: MouseEvent | null) => { const params = new URLSearchParams(search); const availableParams = routeConfig[key]; const queryString = getQueryString(availableParams || [], params); if (pathname !== key) { - history.push(`${key}?${queryString.join('&')}`); + if (event && isCtrlMetaKey(event)) { + openInNewTab(`${key}?${queryString.join('&')}`); + } else { + history.push(`${key}?${queryString.join('&')}`); + } } }, [pathname, search], @@ -220,16 +242,19 @@ function SideNav({ // eslint-disable-next-line react-hooks/exhaustive-deps }, [currentVersion, latestVersion]); - const handleUserManagentMenuItemClick = (key: string): void => { + const handleUserManagentMenuItemClick = ( + key: string, + event: MouseEvent, + ): void => { switch (key) { case SecondaryMenuItemKey.Slack: onClickSlackHandler(); break; case SecondaryMenuItemKey.Version: - onClickVersionHandler(); + onClickVersionHandler(event); break; default: - onClickHandler(key); + onClickHandler(key, event); break; } }; @@ -255,29 +280,41 @@ function SideNav({ ? ROUTES.ORG_SETTINGS : ROUTES.SETTINGS; + const handleMenuItemClick = (event: MouseEvent, item: SidebarItem): void => { + if (item.key === ROUTES.SETTINGS) { + if (isCtrlMetaKey(event)) { + openInNewTab(settingsRoute); + } else { + history.push(settingsRoute); + } + } else if (item) { + onClickHandler(item?.key as string, event); + } + }; + useEffect(() => { registerShortcut(GlobalShortcuts.SidebarCollapse, onCollapse); registerShortcut(GlobalShortcuts.NavigateToServices, () => - onClickHandler(ROUTES.APPLICATION), + onClickHandler(ROUTES.APPLICATION, null), ); registerShortcut(GlobalShortcuts.NavigateToTraces, () => - onClickHandler(ROUTES.TRACE), + onClickHandler(ROUTES.TRACE, null), ); registerShortcut(GlobalShortcuts.NavigateToLogs, () => - onClickHandler(ROUTES.LOGS), + onClickHandler(ROUTES.LOGS, null), ); registerShortcut(GlobalShortcuts.NavigateToDashboards, () => - onClickHandler(ROUTES.ALL_DASHBOARD), + onClickHandler(ROUTES.ALL_DASHBOARD, null), ); registerShortcut(GlobalShortcuts.NavigateToAlerts, () => - onClickHandler(ROUTES.LIST_ALL_ALERT), + onClickHandler(ROUTES.LIST_ALL_ALERT, null), ); registerShortcut(GlobalShortcuts.NavigateToExceptions, () => - onClickHandler(ROUTES.ALL_ERROR), + onClickHandler(ROUTES.ALL_ERROR, null), ); return (): void => { @@ -297,9 +334,9 @@ function SideNav({
{ + onClick={(event: MouseEvent): void => { // Current home page - onClickHandler(ROUTES.APPLICATION); + onClickHandler(ROUTES.APPLICATION, event); }} > SigNoz @@ -314,7 +351,12 @@ function SideNav({ {isCloudUserVal && (
-
+ +
+ { + setPagination({ + ...pagination, + offset: pagination.offset - pagination.limit, + }); + }} + handleNavigateNext={(): void => { + setPagination({ + ...pagination, + offset: pagination.offset + pagination.limit, + }); + }} + handleCountItemsPerPageChange={(value): void => { + setPagination({ + ...pagination, + limit: value, + offset: 0, + }); + }} + /> +
+ + ); +} + +export type TracesTableComponentProps = { + selectedTracesFields: Widgets['selectedTracesFields']; + query: Query; + selectedTime?: timePreferance; +}; + +TracesTableComponent.defaultProps = { + selectedTime: undefined, +}; + +export default TracesTableComponent; diff --git a/frontend/src/hooks/APIKeys/useGetAllAPIKeys.ts b/frontend/src/hooks/APIKeys/useGetAllAPIKeys.ts new file mode 100644 index 0000000000..c4a0d290a2 --- /dev/null +++ b/frontend/src/hooks/APIKeys/useGetAllAPIKeys.ts @@ -0,0 +1,13 @@ +import { getAllAPIKeys } from 'api/APIKeys/getAllAPIKeys'; +import { AxiosError, AxiosResponse } from 'axios'; +import { useQuery, UseQueryResult } from 'react-query'; +import { AllAPIKeyProps } from 'types/api/pat/types'; + +export const useGetAllAPIKeys = (): UseQueryResult< + AxiosResponse, + AxiosError +> => + useQuery, AxiosError>({ + queryKey: ['APIKeys'], + queryFn: () => getAllAPIKeys(), + }); diff --git a/frontend/src/hooks/dashboard/utils.ts b/frontend/src/hooks/dashboard/utils.ts index 4dfb8ce9c3..a66204ae62 100644 --- a/frontend/src/hooks/dashboard/utils.ts +++ b/frontend/src/hooks/dashboard/utils.ts @@ -35,6 +35,8 @@ export const addEmptyWidgetInDashboardJSONWithQuery = ( panelTypes: panelTypes || PANEL_TYPES.TIME_SERIES, softMax: null, softMin: null, + selectedLogFields: [], + selectedTracesFields: [], }, ], }, diff --git a/frontend/src/hooks/queryBuilder/useQueryBuilderOperations.ts b/frontend/src/hooks/queryBuilder/useQueryBuilderOperations.ts index 8e883852cb..799640da4e 100644 --- a/frontend/src/hooks/queryBuilder/useQueryBuilderOperations.ts +++ b/frontend/src/hooks/queryBuilder/useQueryBuilderOperations.ts @@ -6,6 +6,10 @@ import { mapOfQueryFilters, PANEL_TYPES, } from 'constants/queryBuilder'; +import { + listViewInitialLogQuery, + listViewInitialTraceQuery, +} from 'container/NewDashboard/ComponentsSlider/constants'; import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder'; import { getOperatorsBySourceAndPanelType } from 'lib/newQueryBuilder/getOperatorsBySourceAndPanelType'; import { findDataTypeOfOperator } from 'lib/query/findDataTypeOfOperator'; @@ -29,6 +33,7 @@ export const useQueryOperations: UseQueryOperations = ({ index, filterConfigs, formula, + isListViewPanel = false, }) => { const { handleSetQueryData, @@ -37,6 +42,7 @@ export const useQueryOperations: UseQueryOperations = ({ panelType, initialDataSource, currentQuery, + redirectWithQueryBuilderData, } = useQueryBuilder(); const [operators, setOperators] = useState[]>([]); @@ -125,6 +131,14 @@ export const useQueryOperations: UseQueryOperations = ({ const handleChangeDataSource = useCallback( (nextSource: DataSource): void => { + if (isListViewPanel) { + if (nextSource === DataSource.LOGS) { + redirectWithQueryBuilderData(listViewInitialLogQuery); + } else if (nextSource === DataSource.TRACES) { + redirectWithQueryBuilderData(listViewInitialTraceQuery); + } + } + const newOperators = getOperatorsBySourceAndPanelType({ dataSource: nextSource, panelType: panelType || PANEL_TYPES.TIME_SERIES, @@ -146,7 +160,14 @@ export const useQueryOperations: UseQueryOperations = ({ setOperators(newOperators); handleSetQueryData(index, newQuery); }, - [index, query, panelType, handleSetQueryData], + [ + isListViewPanel, + panelType, + query, + handleSetQueryData, + index, + redirectWithQueryBuilderData, + ], ); const handleDeleteQuery = useCallback(() => { diff --git a/frontend/src/hooks/useLogsData.ts b/frontend/src/hooks/useLogsData.ts new file mode 100644 index 0000000000..6105c03cbf --- /dev/null +++ b/frontend/src/hooks/useLogsData.ts @@ -0,0 +1,196 @@ +import { QueryParams } from 'constants/query'; +import { + initialQueryBuilderFormValues, + PANEL_TYPES, +} from 'constants/queryBuilder'; +import { DEFAULT_PER_PAGE_VALUE } from 'container/Controls/config'; +import { getPaginationQueryData } from 'lib/newQueryBuilder/getPaginationQueryData'; +import { useEffect, useMemo, useState } from 'react'; +import { ILog } from 'types/api/logs/log'; +import { + IBuilderQuery, + OrderByPayload, + Query, + TagFilter, +} from 'types/api/queryBuilder/queryBuilderData'; +import { QueryDataV3 } from 'types/api/widgets/getQuery'; + +import { LogTimeRange } from './logs/types'; +import { useCopyLogLink } from './logs/useCopyLogLink'; +import { useGetExplorerQueryRange } from './queryBuilder/useGetExplorerQueryRange'; +import useUrlQueryData from './useUrlQueryData'; + +export const useLogsData = ({ + result, + panelType, + stagedQuery, +}: { + result: QueryDataV3[] | undefined; + panelType: PANEL_TYPES; + stagedQuery: Query | null; +}): { + logs: ILog[]; + handleEndReached: (index: number) => void; + isFetching: boolean; +} => { + const [logs, setLogs] = useState([]); + const [page, setPage] = useState(1); + const [requestData, setRequestData] = useState(null); + const [shouldLoadMoreLogs, setShouldLoadMoreLogs] = useState(false); + + const { queryData: pageSize } = useUrlQueryData( + QueryParams.pageSize, + DEFAULT_PER_PAGE_VALUE, + ); + + const listQuery = useMemo(() => { + if (!stagedQuery || stagedQuery?.builder?.queryData?.length < 1) return null; + + return stagedQuery.builder?.queryData.find((item) => !item.disabled) || null; + }, [stagedQuery]); + + const isLimit: boolean = useMemo(() => { + if (!listQuery) return false; + if (!listQuery.limit) return false; + + return logs.length >= listQuery.limit; + }, [logs.length, listQuery]); + + const orderByTimestamp: OrderByPayload | null = useMemo(() => { + const timestampOrderBy = listQuery?.orderBy.find( + (item) => item.columnName === 'timestamp', + ); + + return timestampOrderBy || null; + }, [listQuery]); + + useEffect(() => { + if (panelType !== PANEL_TYPES.LIST) return; + const currentData = result || []; + if (currentData.length > 0 && currentData[0].list) { + const currentLogs: ILog[] = currentData[0].list.map((item) => ({ + ...item.data, + timestamp: item.timestamp, + })); + const newLogs = [...currentLogs]; + + setLogs(newLogs); + } else { + setLogs([]); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [result]); + + const getRequestData = ( + query: Query | null, + params: { + page: number; + log: ILog | null; + pageSize: number; + filters: TagFilter; + }, + ): Query | null => { + if (!query) return null; + + const paginateData = getPaginationQueryData({ + filters: params.filters, + listItemId: params.log ? params.log.id : null, + orderByTimestamp, + page: params.page, + pageSize: params.pageSize, + }); + + const queryData: IBuilderQuery[] = + query.builder.queryData.length > 1 + ? query.builder.queryData + : [ + { + ...(listQuery || initialQueryBuilderFormValues), + ...paginateData, + }, + ]; + + const data: Query = { + ...query, + builder: { + ...query.builder, + queryData, + }, + }; + + return data; + }; + + const { activeLogId, timeRange, onTimeRangeChange } = useCopyLogLink(); + + const { data, isFetching } = useGetExplorerQueryRange( + requestData, + panelType, + { + keepPreviousData: true, + enabled: !isLimit && !!requestData, + }, + { + ...(timeRange && + activeLogId && + !logs.length && { + start: timeRange.start, + end: timeRange.end, + }), + }, + shouldLoadMoreLogs, + ); + + useEffect(() => { + const currentParams = data?.params as Omit; + const currentData = data?.payload.data.newResult.data.result || []; + if (currentData.length > 0 && currentData[0].list) { + const currentLogs: ILog[] = currentData[0].list.map((item) => ({ + ...item.data, + timestamp: item.timestamp, + })); + const newLogs = [...logs, ...currentLogs]; + + setLogs(newLogs); + onTimeRangeChange({ + start: currentParams?.start, + end: timeRange?.end || currentParams?.end, + pageSize: newLogs.length, + }); + } + + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [data]); + + const handleEndReached = (index: number): void => { + if (!listQuery) return; + + if (isLimit) return; + if (logs.length < pageSize) return; + + const { limit, filters } = listQuery; + + const lastLog = logs[index]; + + const nextLogsLength = logs.length + pageSize; + + const nextPageSize = + limit && nextLogsLength >= limit ? limit - logs.length : pageSize; + + if (!stagedQuery) return; + + const newRequestData = getRequestData(stagedQuery, { + filters, + page: page + 1, + log: orderByTimestamp ? lastLog : null, + pageSize: nextPageSize, + }); + + setPage((prevPage) => prevPage + 1); + + setRequestData(newRequestData); + setShouldLoadMoreLogs(true); + }; + + return { logs, handleEndReached, isFetching }; +}; diff --git a/frontend/src/lib/logs/flatLogData.ts b/frontend/src/lib/logs/flatLogData.ts index d0bf6cf5dd..dfb59b9c68 100644 --- a/frontend/src/lib/logs/flatLogData.ts +++ b/frontend/src/lib/logs/flatLogData.ts @@ -1,3 +1,4 @@ +import { defaultTo } from 'lodash-es'; import { ILog } from 'types/api/logs/log'; export function FlatLogData(log: ILog): Record { @@ -7,7 +8,7 @@ export function FlatLogData(log: ILog): Record { if (typeof log[key as never] !== 'object') { flattenLogObject[key] = log[key as never]; } else { - Object.keys(log[key as never]).forEach((childKey) => { + Object.keys(defaultTo(log[key as never], {})).forEach((childKey) => { flattenLogObject[childKey] = log[key as never][childKey]; }); } diff --git a/frontend/src/mocks-server/__mockdata__/apiKeys.ts b/frontend/src/mocks-server/__mockdata__/apiKeys.ts new file mode 100644 index 0000000000..6bc2d8cbfc --- /dev/null +++ b/frontend/src/mocks-server/__mockdata__/apiKeys.ts @@ -0,0 +1,541 @@ +const createdByEmail = 'mando@signoz.io'; + +export const getAPIKeysResponse = { + status: 'success', + data: [ + { + id: '26', + userId: 'mandalorian', + createdByUser: { + id: '001', + name: 'Mando', + email: createdByEmail, + createdAt: 1707974098, + profilePictureURL: '', + notFound: false, + }, + updatedByUser: { + id: '', + name: '', + email: '', + createdAt: 0, + profilePictureURL: '', + notFound: true, + }, + token: 'T2DuASwpuUx3wlYraFl5r7N9G1ikBhzGuy2ihcIKDMs=', + role: 'ADMIN', + name: '1 Day Old', + createdAt: 1708010258, + expiresAt: 1708096658, + updatedAt: 1708010258, + lastUsed: 0, + revoked: false, + updatedByUserId: '', + }, + { + id: '24', + userId: 'mandalorian', + createdByUser: { + id: '001', + name: 'Mando', + email: createdByEmail, + createdAt: 1707974098, + profilePictureURL: '', + notFound: false, + }, + updatedByUser: { + id: '001', + name: 'Mando', + email: createdByEmail, + createdAt: 1707974098, + profilePictureURL: '', + notFound: false, + }, + token: 'EteVs77BA4FFLJD/TsFE9c+CLX4kXVmlx+0GGK7dpXY=', + role: 'ADMIN', + name: '1 year expiry - updated', + createdAt: 1708008146, + expiresAt: 1739544146, + updatedAt: 1708008239, + lastUsed: 0, + revoked: false, + updatedByUserId: 'mandalorian', + }, + { + id: '25', + userId: 'mandalorian', + createdByUser: { + id: '001', + name: 'Mando', + email: createdByEmail, + createdAt: 1707974098, + profilePictureURL: '', + notFound: false, + }, + updatedByUser: { + id: '001', + name: 'Mando', + email: createdByEmail, + createdAt: 1707974098, + profilePictureURL: '', + notFound: false, + }, + token: '1udrUFmRI6gdb8r/hLabS7zRlgfMQlUw/tz9sac82pE=', + role: 'ADMIN', + name: 'No Expiry Token', + createdAt: 1708008178, + expiresAt: 0, + updatedAt: 1708008190, + lastUsed: 0, + revoked: false, + updatedByUserId: 'mandalorian', + }, + { + id: '22', + userId: 'mandalorian', + createdByUser: { + id: '001', + name: 'Mando', + email: createdByEmail, + createdAt: 1707974098, + profilePictureURL: '', + notFound: false, + }, + updatedByUser: { + id: '001', + name: 'Mando', + email: createdByEmail, + createdAt: 1707974098, + profilePictureURL: '', + notFound: false, + }, + token: 'gtqKF7g7avoe+Yu2+WhyDDLQSr6IsVaR5xpby2XhLAY=', + role: 'VIEWER', + name: 'No Expiry', + createdAt: 1708007395, + expiresAt: 0, + updatedAt: 1708007936, + lastUsed: 0, + revoked: false, + updatedByUserId: 'mandalorian', + }, + { + id: '23', + userId: 'mandalorian', + createdByUser: { + id: '001', + name: 'Mando', + email: createdByEmail, + createdAt: 1707974098, + profilePictureURL: '', + notFound: false, + }, + updatedByUser: { + id: '001', + name: 'Mando', + email: createdByEmail, + createdAt: 1707974098, + profilePictureURL: '', + notFound: false, + }, + token: 'GM/TqEID8N4ynlvQHK38ITEvRAcn5XkJZpmd11xT3OQ=', + role: 'VIEWER', + name: 'No Expiry - 2', + createdAt: 1708007685, + expiresAt: 0, + updatedAt: 1708007786, + lastUsed: 0, + revoked: false, + updatedByUserId: 'mandalorian', + }, + { + id: '19', + userId: 'mandalorian', + createdByUser: { + id: '001', + name: 'Mando', + email: createdByEmail, + createdAt: 1707974098, + profilePictureURL: '', + notFound: false, + }, + updatedByUser: { + id: '001', + name: 'Mando', + email: createdByEmail, + createdAt: 1707974098, + profilePictureURL: '', + notFound: false, + }, + token: 'Oj75e6Zr7JmjFcWIo0UK/Nl06RdC2BKOr/QVHoBA0gM=', + role: 'ADMIN', + name: '7 Days', + createdAt: 1708003326, + expiresAt: 1708608126, + updatedAt: 1708007380, + lastUsed: 0, + revoked: false, + updatedByUserId: 'mandalorian', + }, + { + id: '20', + userId: 'mandalorian', + createdByUser: { + id: '001', + name: 'Mando', + email: createdByEmail, + createdAt: 1707974098, + profilePictureURL: '', + notFound: false, + }, + updatedByUser: { + id: '001', + name: 'Mando', + email: createdByEmail, + createdAt: 1707974098, + profilePictureURL: '', + notFound: false, + }, + token: 'T+sNdYe6I74ya/9mEKqB3UTrFm8+jwI0DiirqEx3bsM=', + role: 'EDITOR', + name: '1 month', + createdAt: 1708004012, + expiresAt: 1710596012, + updatedAt: 1708005206, + lastUsed: 0, + revoked: false, + updatedByUserId: 'mandalorian', + }, + { + id: '21', + userId: 'mandalorian', + createdByUser: { + id: '001', + name: 'Mando', + email: createdByEmail, + createdAt: 1707974098, + profilePictureURL: '', + notFound: false, + }, + updatedByUser: { + id: '001', + name: 'Mando', + email: createdByEmail, + createdAt: 1707974098, + profilePictureURL: '', + notFound: false, + }, + token: 'JWw26FuymeHq+fsfFcb+2+Ls/MdokmeXxXdZisuaVeI=', + role: 'ADMIN', + name: '3 Months', + createdAt: 1708004755, + expiresAt: 1715780755, + updatedAt: 1708005197, + lastUsed: 0, + revoked: false, + updatedByUserId: 'mandalorian', + }, + { + id: '17', + userId: 'mandalorian', + createdByUser: { + id: '001', + name: 'Mando', + email: createdByEmail, + createdAt: 1707974098, + profilePictureURL: '', + notFound: false, + }, + updatedByUser: { + id: '', + name: '', + email: '', + createdAt: 0, + profilePictureURL: '', + notFound: true, + }, + token: '2zDrYNr+IWXUyA14+afVvO6GI9dcHfEsOYxjA9mrprg=', + role: 'ADMIN', + name: 'New No Expiry', + createdAt: 1708000444, + expiresAt: 0, + updatedAt: 1708000444, + lastUsed: 0, + revoked: false, + updatedByUserId: '', + }, + { + id: '14', + userId: 'mandalorian', + createdByUser: { + id: '001', + name: 'Mando', + email: createdByEmail, + createdAt: 1707974098, + profilePictureURL: '', + notFound: false, + }, + updatedByUser: { + id: '', + name: '', + email: '', + createdAt: 0, + profilePictureURL: '', + notFound: true, + }, + token: 'Q+/+UB2OrDPcS9b0+5A1dDXYmWHz0abbVVidF48QCso=', + role: 'EDITOR', + name: 'Editor Token for user 1', + createdAt: 1707997720, + expiresAt: 1708170520, + updatedAt: 1707997720, + lastUsed: 0, + revoked: false, + updatedByUserId: '', + }, + { + id: '13', + userId: 'mandalorian', + createdByUser: { + id: '001', + name: 'Mando', + email: createdByEmail, + createdAt: 1707974098, + profilePictureURL: '', + notFound: false, + }, + updatedByUser: { + id: '', + name: '', + email: '', + createdAt: 0, + profilePictureURL: '', + notFound: true, + }, + token: '/X3OEaSOLrrJImvzIB3g5WGg+5831X89fZZQT1JaxvQ=', + role: 'EDITOR', + name: 'Editor Token for user 2', + createdAt: 1707997603, + expiresAt: 1708170403, + updatedAt: 1707997603, + lastUsed: 0, + revoked: false, + updatedByUserId: '', + }, + { + id: '12', + userId: 'mandalorian', + createdByUser: { + id: '001', + name: 'Mando', + email: createdByEmail, + createdAt: 1707974098, + profilePictureURL: '', + notFound: false, + }, + updatedByUser: { + id: '', + name: '', + email: '', + createdAt: 0, + profilePictureURL: '', + notFound: true, + }, + token: 'bTs+Q6waIiP4KJ8L5N58EQonuapWMXsfEra/cmMwmbE=', + role: 'EDITOR', + name: 'Editor Token for user 3', + createdAt: 1707997539, + expiresAt: 1708170339, + updatedAt: 1707997539, + lastUsed: 0, + revoked: false, + updatedByUserId: '', + }, + { + id: '11', + userId: 'mandalorian', + createdByUser: { + id: '001', + name: 'Mando', + email: createdByEmail, + createdAt: 1707974098, + profilePictureURL: '', + notFound: false, + }, + updatedByUser: { + id: '', + name: '', + email: '', + createdAt: 0, + profilePictureURL: '', + notFound: true, + }, + token: 'YaEqQHrH8KOnYFllor/8Tq653TgxPU1Z7ZDzY3+ETmI=', + role: 'EDITOR', + name: 'Editor Token for user', + createdAt: 1707997537, + expiresAt: 1708170337, + updatedAt: 1707997537, + lastUsed: 0, + revoked: false, + updatedByUserId: '', + }, + { + id: '10', + userId: 'mandalorian', + createdByUser: { + id: '001', + name: 'Mando', + email: createdByEmail, + createdAt: 1707974098, + profilePictureURL: '', + notFound: false, + }, + updatedByUser: { + id: '', + name: '', + email: '', + createdAt: 0, + profilePictureURL: '', + notFound: true, + }, + token: 'Hg/QpMU9VQyqIuzSh9ND2454IN5uOHzVkv7owEtBcPo=', + role: 'EDITOR', + name: 'test123', + createdAt: 1707997288, + expiresAt: 1708083688, + updatedAt: 1707997288, + lastUsed: 0, + revoked: false, + updatedByUserId: '', + }, + { + id: '9', + userId: 'mandalorian', + createdByUser: { + id: '001', + name: 'Mando', + email: createdByEmail, + createdAt: 1707974098, + profilePictureURL: '', + notFound: false, + }, + updatedByUser: { + id: '', + name: '', + email: '', + createdAt: 0, + profilePictureURL: '', + notFound: true, + }, + token: 'M5gMsccDthPTibquB7kR7ZSEI76y4endOxZPESZ9/po=', + role: 'VIEWER', + name: 'Viewer Token for user', + createdAt: 1707996747, + expiresAt: 1708255947, + updatedAt: 1707996747, + lastUsed: 0, + revoked: false, + updatedByUserId: '', + }, + { + id: '8', + userId: 'mandalorian', + createdByUser: { + id: '001', + name: 'Mando', + email: createdByEmail, + createdAt: 1707974098, + profilePictureURL: '', + notFound: false, + }, + updatedByUser: { + id: '', + name: '', + email: '', + createdAt: 0, + profilePictureURL: '', + notFound: true, + }, + token: 'H8NVlOD09IcMgQ/rzfVucb+4+jEcqZ4ZRx6n7QztMSc=', + role: 'EDITOR', + name: 'Editor Token for user', + createdAt: 1707996736, + expiresAt: 1708169536, + updatedAt: 1707996736, + lastUsed: 0, + revoked: false, + updatedByUserId: '', + }, + { + id: '7', + userId: 'mandalorian', + createdByUser: { + id: '001', + name: 'Mando', + email: createdByEmail, + createdAt: 1707974098, + profilePictureURL: '', + notFound: false, + }, + updatedByUser: { + id: '', + name: '', + email: '', + createdAt: 0, + profilePictureURL: '', + notFound: true, + }, + token: 'z24SswLmNlPVUgb1j6rfc2u4Kb4xSUolwb11cI8kbrs=', + role: 'ADMIN', + name: 'Admin Token for user', + createdAt: 1707996719, + expiresAt: 0, + updatedAt: 1707996719, + lastUsed: 0, + revoked: false, + updatedByUserId: '', + }, + { + id: '5', + userId: 'mandalorian', + createdByUser: { + id: '001', + name: 'Mando', + email: createdByEmail, + createdAt: 1707974098, + profilePictureURL: '', + notFound: false, + }, + updatedByUser: { + id: '001', + name: 'Mando', + email: createdByEmail, + createdAt: 1707974098, + profilePictureURL: '', + notFound: false, + }, + token: 'SWuNSF08EB6+VN05312QaAsPum2wkqIm+ujiWZKnm2Q=', + role: 'EDITOR', + name: 'Editor Token', + createdAt: 1707992270, + expiresAt: 1708165070, + updatedAt: 1707995424, + lastUsed: 1707992517, + revoked: false, + updatedByUserId: 'mandalorian', + }, + ], +}; + +export const createAPIKeyResponse = { + status: 'success', + data: { + id: '57', + userId: 'mandalorian', + token: 'pQ5kiHjcbQ2FbKlS14LQjA2RzXEBi/KvBfM7BRSwltI=', + name: 'test1233', + createdAt: 1707818550, + expiresAt: 0, + }, +}; diff --git a/frontend/src/mocks-server/__mockdata__/logs_query_range.ts b/frontend/src/mocks-server/__mockdata__/logs_query_range.ts new file mode 100644 index 0000000000..3b67e48945 --- /dev/null +++ b/frontend/src/mocks-server/__mockdata__/logs_query_range.ts @@ -0,0 +1,45 @@ +export const logsQueryRangeSuccessResponse = { + status: 'success', + data: { + resultType: '', + result: [ + { + queryName: 'A', + series: null, + list: [ + { + timestamp: '2024-02-15T21:20:22Z', + data: { + attributes_bool: {}, + attributes_float64: {}, + attributes_int64: {}, + attributes_string: { + container_id: 'container_id', + container_name: 'container_name', + driver: 'driver', + eta: '2m0s', + location: 'frontend', + log_level: 'INFO', + message: 'Dispatch successful', + service: 'frontend', + span_id: 'span_id', + trace_id: 'span_id', + }, + body: + '2024-02-15T21:20:22.035Z\tINFO\tfrontend\tDispatch successful\t{"service": "frontend", "trace_id": "span_id", "span_id": "span_id", "driver": "driver", "eta": "2m0s"}', + id: 'id', + resources_string: { + 'container.name': 'container_name', + }, + severity_number: 0, + severity_text: '', + span_id: '', + trace_flags: 0, + trace_id: '', + }, + }, + ], + }, + ], + }, +}; diff --git a/frontend/src/pages/LogsExplorer/__tests__/LogsExplorer.test.tsx b/frontend/src/pages/LogsExplorer/__tests__/LogsExplorer.test.tsx new file mode 100644 index 0000000000..072bfe98ca --- /dev/null +++ b/frontend/src/pages/LogsExplorer/__tests__/LogsExplorer.test.tsx @@ -0,0 +1,147 @@ +import { render, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { logsQueryRangeSuccessResponse } from 'mocks-server/__mockdata__/logs_query_range'; +import { server } from 'mocks-server/server'; +import { rest } from 'msw'; +import { QueryBuilderProvider } from 'providers/QueryBuilder'; +import MockQueryClientProvider from 'providers/test/MockQueryClientProvider'; +import { I18nextProvider } from 'react-i18next'; +import { Provider } from 'react-redux'; +import { MemoryRouter } from 'react-router-dom'; +// https://virtuoso.dev/mocking-in-tests/ +import { VirtuosoMockContext } from 'react-virtuoso'; +import i18n from 'ReactI18'; +import store from 'store'; + +import LogsExplorer from '..'; + +const queryRangeURL = 'http://localhost/api/v3/query_range'; +// mocking the graph components in this test as this should be handled separately +jest.mock( + 'container/TimeSeriesView/TimeSeriesView', + () => + // eslint-disable-next-line func-names, @typescript-eslint/explicit-function-return-type, react/display-name + function () { + return
Time Series Chart
; + }, +); +jest.mock( + 'container/LogsExplorerChart', + () => + // eslint-disable-next-line func-names, @typescript-eslint/explicit-function-return-type, react/display-name + function () { + return
Histogram Chart
; + }, +); + +jest.mock('constants/panelTypes', () => ({ + AVAILABLE_EXPORT_PANEL_TYPES: ['graph', 'table'], +})); + +jest.mock('d3-interpolate', () => ({ + interpolate: jest.fn(), +})); + +describe('Logs Explorer Tests', () => { + test('Logs Explorer default view test without data', async () => { + const { + getByText, + getByRole, + queryByText, + getByTestId, + queryByTestId, + } = render( + + + + + + + + + + + , + ); + + // check the presence of histogram chart + expect(getByText('Histogram Chart')).toBeInTheDocument(); + + // toggle the chart and check it gets removed from the DOM + const histogramToggle = getByRole('switch'); + await userEvent.click(histogramToggle); + expect(queryByText('Histogram Chart')).not.toBeInTheDocument(); + + // check the presence of search bar and query builder and absence of clickhouse + const searchView = getByTestId('search-view'); + expect(searchView).toBeInTheDocument(); + const queryBuilderView = getByTestId('query-builder-view'); + expect(queryBuilderView).toBeInTheDocument(); + const clickhouseView = queryByTestId('clickhouse-view'); + expect(clickhouseView).not.toBeInTheDocument(); + + // check the presence of List View / Time Series View / Table View + const listView = getByTestId('logs-list-view'); + const timeSeriesView = getByTestId('time-series-view'); + const tableView = getByTestId('table-view'); + expect(listView).toBeInTheDocument(); + expect(timeSeriesView).toBeInTheDocument(); + expect(tableView).toBeInTheDocument(); + + // check the presence of old logs explorer CTA + const oldLogsCTA = getByText('Switch to Old Logs Explorer'); + expect(oldLogsCTA).toBeInTheDocument(); + }); + + test('Logs Explorer Page should render with data', async () => { + // mocking the query range API to return the logs + server.use( + rest.post(queryRangeURL, (req, res, ctx) => + res(ctx.status(200), ctx.json(logsQueryRangeSuccessResponse)), + ), + ); + const { queryByText, queryByTestId } = render( + + + + + + + + + + + + + , + ); + + // check for loading state to be not present + await waitFor(() => + expect( + queryByText( + `Just a bit of patience, just a little bit’s enough ⎯ we’re getting your logs!`, + ), + ).not.toBeInTheDocument(), + ); + + // check for no data state to not be present + await waitFor(() => + expect(queryByText('No logs yet.')).not.toBeInTheDocument(), + ); + + // check for the data container loaded + await waitFor(() => + expect(queryByTestId('logs-list-virtuoso')).toBeInTheDocument(), + ); + + // check for data being present in the UI + expect( + queryByText( + '2024-02-15T21:20:22.035Z INFO frontend Dispatch successful {"service": "frontend", "trace_id": "span_id", "span_id": "span_id", "driver": "driver", "eta": "2m0s"}', + ), + ).toBeInTheDocument(); + }); +}); diff --git a/frontend/src/pages/Settings/config.ts b/frontend/src/pages/Settings/config.tsx similarity index 56% rename from frontend/src/pages/Settings/config.ts rename to frontend/src/pages/Settings/config.tsx index ca18559ec5..c6073ce322 100644 --- a/frontend/src/pages/Settings/config.ts +++ b/frontend/src/pages/Settings/config.tsx @@ -1,16 +1,22 @@ import { RouteTabProps } from 'components/RouteTab/types'; import ROUTES from 'constants/routes'; import AlertChannels from 'container/AllAlertChannels'; +import APIKeys from 'container/APIKeys/APIKeys'; import GeneralSettings from 'container/GeneralSettings'; import GeneralSettingsCloud from 'container/GeneralSettingsCloud'; import IngestionSettings from 'container/IngestionSettings/IngestionSettings'; import OrganizationSettings from 'container/OrganizationSettings'; import { TFunction } from 'i18next'; +import { Backpack, BellDot, Building, Cpu, KeySquare } from 'lucide-react'; export const organizationSettings = (t: TFunction): RouteTabProps['routes'] => [ { Component: OrganizationSettings, - name: t('routes:organization_settings').toString(), + name: ( +
+ {t('routes:organization_settings').toString()} +
+ ), route: ROUTES.ORG_SETTINGS, key: ROUTES.ORG_SETTINGS, }, @@ -19,7 +25,11 @@ export const organizationSettings = (t: TFunction): RouteTabProps['routes'] => [ export const alertChannels = (t: TFunction): RouteTabProps['routes'] => [ { Component: AlertChannels, - name: t('routes:alert_channels').toString(), + name: ( +
+ {t('routes:alert_channels').toString()} +
+ ), route: ROUTES.ALL_CHANNELS, key: ROUTES.ALL_CHANNELS, }, @@ -28,7 +38,11 @@ export const alertChannels = (t: TFunction): RouteTabProps['routes'] => [ export const ingestionSettings = (t: TFunction): RouteTabProps['routes'] => [ { Component: IngestionSettings, - name: t('routes:ingestion_settings').toString(), + name: ( +
+ {t('routes:ingestion_settings').toString()} +
+ ), route: ROUTES.INGESTION_SETTINGS, key: ROUTES.INGESTION_SETTINGS, }, @@ -37,7 +51,11 @@ export const ingestionSettings = (t: TFunction): RouteTabProps['routes'] => [ export const generalSettings = (t: TFunction): RouteTabProps['routes'] => [ { Component: GeneralSettings, - name: t('routes:general').toString(), + name: ( +
+ {t('routes:general').toString()} +
+ ), route: ROUTES.SETTINGS, key: ROUTES.SETTINGS, }, @@ -46,8 +64,25 @@ export const generalSettings = (t: TFunction): RouteTabProps['routes'] => [ export const generalSettingsCloud = (t: TFunction): RouteTabProps['routes'] => [ { Component: GeneralSettingsCloud, - name: t('routes:general').toString(), + name: ( +
+ {t('routes:general').toString()} +
+ ), route: ROUTES.SETTINGS, key: ROUTES.SETTINGS, }, ]; + +export const apiKeys = (t: TFunction): RouteTabProps['routes'] => [ + { + Component: APIKeys, + name: ( +
+ {t('routes:api_keys').toString()} +
+ ), + route: ROUTES.API_KEYS, + key: ROUTES.API_KEYS, + }, +]; diff --git a/frontend/src/pages/Settings/index.tsx b/frontend/src/pages/Settings/index.tsx index 6e98edf774..c2bdcef5e6 100644 --- a/frontend/src/pages/Settings/index.tsx +++ b/frontend/src/pages/Settings/index.tsx @@ -19,7 +19,8 @@ function SettingsPage(): JSX.Element { ); const { t } = useTranslation(['routes']); - const routes = useMemo(() => getRoutes(isCurrentOrgSettings, t), [ + const routes = useMemo(() => getRoutes(role, isCurrentOrgSettings, t), [ + role, isCurrentOrgSettings, t, ]); diff --git a/frontend/src/pages/Settings/utils.ts b/frontend/src/pages/Settings/utils.ts index e453ee56a5..6078ef7621 100644 --- a/frontend/src/pages/Settings/utils.ts +++ b/frontend/src/pages/Settings/utils.ts @@ -1,20 +1,25 @@ import { RouteTabProps } from 'components/RouteTab/types'; import { TFunction } from 'i18next'; -import { isCloudUser } from 'utils/app'; +import { ROLES, USER_ROLES } from 'types/roles'; +import { isCloudUser, isEECloudUser } from 'utils/app'; import { alertChannels, + apiKeys, generalSettings, ingestionSettings, organizationSettings, } from './config'; export const getRoutes = ( + userRole: ROLES | null, isCurrentOrgSettings: boolean, t: TFunction, ): RouteTabProps['routes'] => { const settings = []; + settings.push(...generalSettings(t)); + if (isCurrentOrgSettings) { settings.push(...organizationSettings(t)); } @@ -24,10 +29,11 @@ export const getRoutes = ( settings.push(...alertChannels(t)); } else { settings.push(...alertChannels(t)); - settings.push(...generalSettings(t)); } - settings.push(...generalSettings(t)); + if ((isCloudUser() || isEECloudUser()) && userRole === USER_ROLES.ADMIN) { + settings.push(...apiKeys(t)); + } return settings; }; diff --git a/frontend/src/pages/TracesExplorer/TracesExplorer.styles.scss b/frontend/src/pages/TracesExplorer/TracesExplorer.styles.scss index ae985deb74..5ab2bd07b2 100644 --- a/frontend/src/pages/TracesExplorer/TracesExplorer.styles.scss +++ b/frontend/src/pages/TracesExplorer/TracesExplorer.styles.scss @@ -2,6 +2,12 @@ display: flex; flex-direction: row-reverse; align-items: center; - margin: 1rem 0 0.5rem 0; + margin: 8px 16px; gap: 8px; } + +.traces-explorer-views { + .ant-tabs-tabpane { + padding: 0 8px; + } +} diff --git a/frontend/src/pages/TracesExplorer/index.tsx b/frontend/src/pages/TracesExplorer/index.tsx index 22d4b71d2e..3d39aab2cb 100644 --- a/frontend/src/pages/TracesExplorer/index.tsx +++ b/frontend/src/pages/TracesExplorer/index.tsx @@ -192,7 +192,7 @@ function TracesExplorer(): JSX.Element { - + - ; + ); } diff --git a/frontend/src/periscope.scss b/frontend/src/periscope.scss index 026301e2f4..d32ac10973 100644 --- a/frontend/src/periscope.scss +++ b/frontend/src/periscope.scss @@ -24,6 +24,18 @@ } cursor: pointer; + + &.primary { + color: #fff; + background-color: #4566d6; + box-shadow: 0 2px 0 rgba(62, 86, 245, 0.09); + } +} + +.periscope-tab { + display: flex; + align-items: center; + gap: 8px; } .lightMode { diff --git a/frontend/src/styles.scss b/frontend/src/styles.scss index 2cb3ed419c..05f7c400ab 100644 --- a/frontend/src/styles.scss +++ b/frontend/src/styles.scss @@ -224,3 +224,21 @@ body { margin-left: 8px; margin-right: 8px; } + +.lightMode { + .ant-dropdown-menu { + border: 1px solid var(--bg-vanilla-300); + background: var(--bg-vanilla-100); + box-shadow: 4px 10px 16px 2px rgba(0, 0, 0, 0.2); + } + + .ant-dropdown-menu-item { + &:hover { + background-color: var(--bg-vanilla-300) !important; + + &.ant-dropdown-menu-item-danger { + background-color: var(--bg-cherry-500) !important; + } + } + } +} diff --git a/frontend/src/types/api/dashboard/getAll.ts b/frontend/src/types/api/dashboard/getAll.ts index bb302c152b..2111d3d57b 100644 --- a/frontend/src/types/api/dashboard/getAll.ts +++ b/frontend/src/types/api/dashboard/getAll.ts @@ -5,6 +5,9 @@ import { ReactNode } from 'react'; import { Layout } from 'react-grid-layout'; import { Query } from 'types/api/queryBuilder/queryBuilderData'; +import { IField } from '../logs/fields'; +import { BaseAutocompleteData } from '../queryBuilder/queryAutocompleteResponse'; + export type PayloadProps = Dashboard[]; export const VariableQueryTypeArr = ['QUERY', 'TEXTBOX', 'CUSTOM'] as const; @@ -76,6 +79,8 @@ export interface IBaseWidget { softMin: number | null; softMax: number | null; fillSpans?: boolean; + selectedLogFields: IField[] | null; + selectedTracesFields: BaseAutocompleteData[] | null; } export interface Widgets extends IBaseWidget { query: Query; diff --git a/frontend/src/types/api/pat/types.ts b/frontend/src/types/api/pat/types.ts new file mode 100644 index 0000000000..b00a6b327f --- /dev/null +++ b/frontend/src/types/api/pat/types.ts @@ -0,0 +1,53 @@ +export interface User { + createdAt?: number; + email?: string; + id: string; + name?: string; + notFound?: boolean; + profilePictureURL?: string; +} + +export interface APIKeyProps { + name: string; + expiresAt: number; + role: string; + token: string; + id: string; + createdAt: number; + createdByUser?: User; + updatedAt?: number; + updatedByUser?: User; + lastUsed?: number; +} + +export interface CreateAPIKeyProps { + name: string; + expiresInDays: number; + role: string; +} + +export interface AllAPIKeyProps { + status: string; + data: APIKeyProps[]; +} + +export interface CreateAPIKeyProp { + data: APIKeyProps; +} + +export interface DeleteAPIKeyPayloadProps { + status: string; +} + +export interface UpdateAPIKeyProps { + id: string; + data: { + name: string; + role: string; + }; +} + +export type PayloadProps = { + status: string; + data: string; +}; diff --git a/frontend/src/types/common/operations.types.ts b/frontend/src/types/common/operations.types.ts index b1a21ce385..58fd4533b9 100644 --- a/frontend/src/types/common/operations.types.ts +++ b/frontend/src/types/common/operations.types.ts @@ -12,6 +12,7 @@ import { SelectOption } from './select'; type UseQueryOperationsParams = Pick & Pick & { formula?: IBuilderFormula; + isListViewPanel?: boolean; }; export type HandleChangeQueryData = < diff --git a/frontend/src/utils/app.ts b/frontend/src/utils/app.ts index d0b859d108..0ab9e6fca7 100644 --- a/frontend/src/utils/app.ts +++ b/frontend/src/utils/app.ts @@ -15,8 +15,6 @@ export function extractDomain(email: string): string { export const isCloudUser = (): boolean => { const { hostname } = window.location; - return true; - return hostname?.endsWith('signoz.cloud'); }; diff --git a/frontend/src/utils/permission/index.ts b/frontend/src/utils/permission/index.ts index 5f6e59b14a..265ffe02a5 100644 --- a/frontend/src/utils/permission/index.ts +++ b/frontend/src/utils/permission/index.ts @@ -88,6 +88,7 @@ export const routePermission: Record = { SOMETHING_WENT_WRONG: ['ADMIN', 'EDITOR', 'VIEWER'], LOGS_SAVE_VIEWS: ['ADMIN', 'EDITOR', 'VIEWER'], TRACES_SAVE_VIEWS: ['ADMIN', 'EDITOR', 'VIEWER'], + API_KEYS: ['ADMIN'], LOGS_BASE: [], OLD_LOGS_EXPLORER: [], SHORTCUTS: ['ADMIN', 'EDITOR', 'VIEWER'], diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json index 7603747f46..e22372bac7 100644 --- a/frontend/tsconfig.json +++ b/frontend/tsconfig.json @@ -23,6 +23,11 @@ "isolatedModules": true, "noEmit": true, "baseUrl": "./src", + "paths": { + "@constants/*": [ + "/container/OnboardingContainer/constants/*" + ] + }, "downlevelIteration": true, "plugins": [ { diff --git a/frontend/yarn.lock b/frontend/yarn.lock index 80ef34ec02..8e1c80fad2 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -138,6 +138,14 @@ "@babel/highlight" "^7.22.13" chalk "^2.4.2" +"@babel/code-frame@^7.23.5": + version "7.23.5" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.23.5.tgz#9009b69a8c602293476ad598ff53e4562e15c244" + integrity sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA== + dependencies: + "@babel/highlight" "^7.23.4" + chalk "^2.4.2" + "@babel/compat-data@^7.17.7", "@babel/compat-data@^7.20.5", "@babel/compat-data@^7.21.4": version "7.21.4" resolved "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.21.4.tgz" @@ -148,6 +156,11 @@ resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.22.9.tgz#71cdb00a1ce3a329ce4cbec3a44f9fef35669730" integrity sha512-5UamI7xkUcJ3i9qVDS+KFDEK8/7oJ55/sJMB1Ge7IEapr7KfdfV/HErR+koZwOfd+SgtFKOKRhRakdg++DcJpQ== +"@babel/compat-data@^7.23.5": + version "7.23.5" + resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.23.5.tgz#ffb878728bb6bdcb6f4510aa51b1be9afb8cfd98" + integrity sha512-uU27kfDRlhfKl+w1U6vp16IuvSLtjAxdArVXPa9BvLkrr7CYIsxH5adpHObeAGY/41+syctUWOZ140a2Rvkgjw== + "@babel/core@^7.1.0", "@babel/core@^7.12.3", "@babel/core@^7.16.0", "@babel/core@^7.7.2", "@babel/core@^7.7.5", "@babel/core@^7.8.0": version "7.21.4" resolved "https://registry.npmjs.org/@babel/core/-/core-7.21.4.tgz" @@ -190,6 +203,27 @@ json5 "^2.2.3" semver "^6.3.1" +"@babel/core@^7.19.6": + version "7.23.9" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.23.9.tgz#b028820718000f267870822fec434820e9b1e4d1" + integrity sha512-5q0175NOjddqpvvzU+kDiSOAk4PfdO6FvwCWoQ6RO7rTzEe8vlo+4HVfcnAREhD4npMs0e9uZypjTwzZPCf/cw== + dependencies: + "@ampproject/remapping" "^2.2.0" + "@babel/code-frame" "^7.23.5" + "@babel/generator" "^7.23.6" + "@babel/helper-compilation-targets" "^7.23.6" + "@babel/helper-module-transforms" "^7.23.3" + "@babel/helpers" "^7.23.9" + "@babel/parser" "^7.23.9" + "@babel/template" "^7.23.9" + "@babel/traverse" "^7.23.9" + "@babel/types" "^7.23.9" + convert-source-map "^2.0.0" + debug "^4.1.0" + gensync "^1.0.0-beta.2" + json5 "^2.2.3" + semver "^6.3.1" + "@babel/core@^7.22.11": version "7.22.15" resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.22.15.tgz#15d4fd03f478a459015a4b94cfbb3bd42c48d2f4" @@ -251,6 +285,16 @@ "@jridgewell/trace-mapping" "^0.3.17" jsesc "^2.5.1" +"@babel/generator@^7.23.6": + version "7.23.6" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.23.6.tgz#9e1fca4811c77a10580d17d26b57b036133f3c2e" + integrity sha512-qrSfCYxYQB5owCmGLbl8XRpX1ytXlpueOb0N0UmQwA073KZxejgQTzAmJezxvpwQD9uGtK2shHdi55QT+MbjIw== + dependencies: + "@babel/types" "^7.23.6" + "@jridgewell/gen-mapping" "^0.3.2" + "@jridgewell/trace-mapping" "^0.3.17" + jsesc "^2.5.1" + "@babel/helper-annotate-as-pure@^7.15.4", "@babel/helper-annotate-as-pure@^7.16.0", "@babel/helper-annotate-as-pure@^7.18.6": version "7.18.6" resolved "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.18.6.tgz" @@ -313,6 +357,17 @@ lru-cache "^5.1.1" semver "^6.3.1" +"@babel/helper-compilation-targets@^7.23.6": + version "7.23.6" + resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.23.6.tgz#4d79069b16cbcf1461289eccfbbd81501ae39991" + integrity sha512-9JB548GZoQVmzrFgp8o7KxdgkTGm6xs9DW0o/Pim72UDjzr5ObUQ6ZzYPqA+g9OTS2bBQoctLJrky0RDCAWRgQ== + dependencies: + "@babel/compat-data" "^7.23.5" + "@babel/helper-validator-option" "^7.23.5" + browserslist "^4.22.2" + lru-cache "^5.1.1" + semver "^6.3.1" + "@babel/helper-create-class-features-plugin@^7.18.6", "@babel/helper-create-class-features-plugin@^7.21.0": version "7.21.4" resolved "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.21.4.tgz" @@ -513,6 +568,17 @@ "@babel/helper-split-export-declaration" "^7.22.6" "@babel/helper-validator-identifier" "^7.22.5" +"@babel/helper-module-transforms@^7.23.3": + version "7.23.3" + resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.23.3.tgz#d7d12c3c5d30af5b3c0fcab2a6d5217773e2d0f1" + integrity sha512-7bBs4ED9OmswdfDzpz4MpWgSrV7FXlc3zIagvLFjS5H+Mk7Snr21vQ6QwrsoCGMfNC4e4LQPdoULEt4ykz0SRQ== + dependencies: + "@babel/helper-environment-visitor" "^7.22.20" + "@babel/helper-module-imports" "^7.22.15" + "@babel/helper-simple-access" "^7.22.5" + "@babel/helper-split-export-declaration" "^7.22.6" + "@babel/helper-validator-identifier" "^7.22.20" + "@babel/helper-optimise-call-expression@^7.18.6": version "7.18.6" resolved "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.18.6.tgz" @@ -629,6 +695,11 @@ resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz#533f36457a25814cf1df6488523ad547d784a99f" integrity sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw== +"@babel/helper-string-parser@^7.23.4": + version "7.23.4" + resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.23.4.tgz#9478c707febcbbe1ddb38a3d91a2e054ae622d83" + integrity sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ== + "@babel/helper-validator-identifier@^7.18.6", "@babel/helper-validator-identifier@^7.19.1": version "7.19.1" resolved "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz" @@ -664,6 +735,11 @@ resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.22.5.tgz#de52000a15a177413c8234fa3a8af4ee8102d0ac" integrity sha512-R3oB6xlIVKUnxNUxbmgq7pKjxpru24zlimpE8WK47fACIlM0II/Hm1RS8IaOI7NgCr6LNS+jl5l75m20npAziw== +"@babel/helper-validator-option@^7.23.5": + version "7.23.5" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.23.5.tgz#907a3fbd4523426285365d1206c423c4c5520307" + integrity sha512-85ttAOMLsr53VgXkTbkx8oA6YTfT4q7/HzXSLEYmjcSTJPMPQtvq1BD79Byep5xMUYbGRzEpDsjUf3dyp54IKw== + "@babel/helper-wrap-function@^7.18.9": version "7.20.5" resolved "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.20.5.tgz" @@ -710,6 +786,15 @@ "@babel/traverse" "^7.22.15" "@babel/types" "^7.22.15" +"@babel/helpers@^7.23.9": + version "7.23.9" + resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.23.9.tgz#c3e20bbe7f7a7e10cb9b178384b4affdf5995c7d" + integrity sha512-87ICKgU5t5SzOT7sBMfCOZQ2rHjRU+Pcb9BoILMYz600W6DkVRLFBPwQ18gwUVvggqXivaUakpnxWQGbpywbBQ== + dependencies: + "@babel/template" "^7.23.9" + "@babel/traverse" "^7.23.9" + "@babel/types" "^7.23.9" + "@babel/highlight@^7.10.4", "@babel/highlight@^7.18.6": version "7.18.6" resolved "https://registry.npmjs.org/@babel/highlight/-/highlight-7.18.6.tgz" @@ -728,6 +813,15 @@ chalk "^2.4.2" js-tokens "^4.0.0" +"@babel/highlight@^7.23.4": + version "7.23.4" + resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.23.4.tgz#edaadf4d8232e1a961432db785091207ead0621b" + integrity sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A== + dependencies: + "@babel/helper-validator-identifier" "^7.22.20" + chalk "^2.4.2" + js-tokens "^4.0.0" + "@babel/parser@^7.1.0", "@babel/parser@^7.14.7", "@babel/parser@^7.20.7", "@babel/parser@^7.21.4", "@babel/parser@^7.7.0": version "7.21.4" resolved "https://registry.npmjs.org/@babel/parser/-/parser-7.21.4.tgz" @@ -753,6 +847,11 @@ resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.23.6.tgz#ba1c9e512bda72a47e285ae42aff9d2a635a9e3b" integrity sha512-Z2uID7YJ7oNvAI20O9X0bblw7Qqs8Q2hFy0R9tAfnfLkp5MW0UH9eUvnDSnFwKZ0AvgS1ucqR4KzvVHgnke1VQ== +"@babel/parser@^7.23.9": + version "7.23.9" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.23.9.tgz#7b903b6149b0f8fa7ad564af646c4c38a77fc44b" + integrity sha512-9tcKgqKbs3xGJ+NtKF2ndOBBLVwPjl1SHxPQkd36r3Dlirw3xWUeGaTbqr7uGZcTaxkVNwc+03SVP7aCdWrTlA== + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@^7.18.6": version "7.18.6" resolved "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.18.6.tgz" @@ -2122,6 +2221,15 @@ "@babel/parser" "^7.22.5" "@babel/types" "^7.22.5" +"@babel/template@^7.23.9": + version "7.23.9" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.23.9.tgz#f881d0487cba2828d3259dcb9ef5005a9731011a" + integrity sha512-+xrD2BWLpvHKNmX2QbpdpsBaWnRxahMwJjO+KZk2JOElj5nSmKezyS1B4u+QbHMTX69t4ukm6hh9lsYQ7GHCKA== + dependencies: + "@babel/code-frame" "^7.23.5" + "@babel/parser" "^7.23.9" + "@babel/types" "^7.23.9" + "@babel/traverse@^7.20.5", "@babel/traverse@^7.20.7", "@babel/traverse@^7.21.0", "@babel/traverse@^7.21.2", "@babel/traverse@^7.21.4", "@babel/traverse@^7.22.11", "@babel/traverse@^7.22.15", "@babel/traverse@^7.4.5", "@babel/traverse@^7.7.0", "@babel/traverse@^7.7.2": version "7.23.2" resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.23.2.tgz#329c7a06735e144a506bdb2cad0268b7f46f4ad8" @@ -2138,6 +2246,22 @@ debug "^4.1.0" globals "^11.1.0" +"@babel/traverse@^7.23.9": + version "7.23.9" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.23.9.tgz#2f9d6aead6b564669394c5ce0f9302bb65b9d950" + integrity sha512-I/4UJ9vs90OkBtY6iiiTORVMyIhJ4kAVmsKo9KFc8UOxMeUfi2hvtIBsET5u9GizXE6/GFSuKCTNfgCswuEjRg== + dependencies: + "@babel/code-frame" "^7.23.5" + "@babel/generator" "^7.23.6" + "@babel/helper-environment-visitor" "^7.22.20" + "@babel/helper-function-name" "^7.23.0" + "@babel/helper-hoist-variables" "^7.22.5" + "@babel/helper-split-export-declaration" "^7.22.6" + "@babel/parser" "^7.23.9" + "@babel/types" "^7.23.9" + debug "^4.3.1" + globals "^11.1.0" + "@babel/types@^7.0.0", "@babel/types@^7.18.6", "@babel/types@^7.18.9", "@babel/types@^7.20.0", "@babel/types@^7.20.2", "@babel/types@^7.20.5", "@babel/types@^7.20.7", "@babel/types@^7.21.0", "@babel/types@^7.21.2", "@babel/types@^7.21.4", "@babel/types@^7.3.0", "@babel/types@^7.3.3", "@babel/types@^7.4.4", "@babel/types@^7.7.0": version "7.21.4" resolved "https://registry.npmjs.org/@babel/types/-/types-7.21.4.tgz" @@ -2174,6 +2298,15 @@ "@babel/helper-validator-identifier" "^7.22.20" to-fast-properties "^2.0.0" +"@babel/types@^7.23.6", "@babel/types@^7.23.9": + version "7.23.9" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.23.9.tgz#1dd7b59a9a2b5c87f8b41e52770b5ecbf492e002" + integrity sha512-dQjSq/7HaSjRM43FFGnv5keM2HsxpmyV1PfaSVm0nzzjwwTmjOe6J4bC8e3+pTEIgHaHj+1ZlLThRJ2auc/w1Q== + dependencies: + "@babel/helper-string-parser" "^7.23.4" + "@babel/helper-validator-identifier" "^7.22.20" + to-fast-properties "^2.0.0" + "@bcoe/v8-coverage@^0.2.3": version "0.2.3" resolved "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz" @@ -3056,6 +3189,11 @@ resolved "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.21.tgz" integrity sha512-a5Sab1C4/icpTZVzZc5Ghpz88yQtGOyNqYXcZgOssB2uuAr+wF/MvN6bgtW32q7HHrvBki+BsZ0OuNv6EV3K9g== +"@polka/url@^1.0.0-next.24": + version "1.0.0-next.24" + resolved "https://registry.yarnpkg.com/@polka/url/-/url-1.0.0-next.24.tgz#58601079e11784d20f82d0585865bb42305c4df3" + integrity sha512-2LuNTFBIO0m7kKIQvvPHN6UE63VjpmL9rnEEaOOaiSPbZK+zUOYIzBAWcED+3XYzhYsd/0mD57VdxAEqqV52CQ== + "@radix-ui/primitive@1.0.1": version "1.0.1" resolved "https://registry.yarnpkg.com/@radix-ui/primitive/-/primitive-1.0.1.tgz#e46f9958b35d10e9f6dc71c497305c22e3e55dbd" @@ -3449,6 +3587,11 @@ resolved "https://registry.yarnpkg.com/@sinclair/typebox/-/typebox-0.27.8.tgz#6667fac16c436b5434a387a34dedb013198f6e6e" integrity sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA== +"@sindresorhus/is@^0.14.0": + version "0.14.0" + resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-0.14.0.tgz#9fb3a3cf3132328151f353de4632e01e52102bea" + integrity sha512-9NET910DNaIPngYnLLPeg+Ogzqsi9uM4mSboU5y6p8S5DzMTVEsJZrawi+BoDNUVBa2DhJqQYUFvMDfgU062LQ== + "@sinonjs/commons@^1.7.0": version "1.8.6" resolved "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.6.tgz" @@ -3463,6 +3606,96 @@ dependencies: "@sinonjs/commons" "^1.7.0" +"@svgr/babel-plugin-add-jsx-attribute@^6.5.1": + version "6.5.1" + resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-add-jsx-attribute/-/babel-plugin-add-jsx-attribute-6.5.1.tgz#74a5d648bd0347bda99d82409d87b8ca80b9a1ba" + integrity sha512-9PYGcXrAxitycIjRmZB+Q0JaN07GZIWaTBIGQzfaZv+qr1n8X1XUEJ5rZ/vx6OVD9RRYlrNnXWExQXcmZeD/BQ== + +"@svgr/babel-plugin-remove-jsx-attribute@*": + version "8.0.0" + resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-remove-jsx-attribute/-/babel-plugin-remove-jsx-attribute-8.0.0.tgz#69177f7937233caca3a1afb051906698f2f59186" + integrity sha512-BcCkm/STipKvbCl6b7QFrMh/vx00vIP63k2eM66MfHJzPr6O2U0jYEViXkHJWqXqQYjdeA9cuCl5KWmlwjDvbA== + +"@svgr/babel-plugin-remove-jsx-empty-expression@*": + version "8.0.0" + resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-remove-jsx-empty-expression/-/babel-plugin-remove-jsx-empty-expression-8.0.0.tgz#c2c48104cfd7dcd557f373b70a56e9e3bdae1d44" + integrity sha512-5BcGCBfBxB5+XSDSWnhTThfI9jcO5f0Ai2V24gZpG+wXF14BzwxxdDb4g6trdOux0rhibGs385BeFMSmxtS3uA== + +"@svgr/babel-plugin-replace-jsx-attribute-value@^6.5.1": + version "6.5.1" + resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-replace-jsx-attribute-value/-/babel-plugin-replace-jsx-attribute-value-6.5.1.tgz#fb9d22ea26d2bc5e0a44b763d4c46d5d3f596c60" + integrity sha512-8DPaVVE3fd5JKuIC29dqyMB54sA6mfgki2H2+swh+zNJoynC8pMPzOkidqHOSc6Wj032fhl8Z0TVn1GiPpAiJg== + +"@svgr/babel-plugin-svg-dynamic-title@^6.5.1": + version "6.5.1" + resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-svg-dynamic-title/-/babel-plugin-svg-dynamic-title-6.5.1.tgz#01b2024a2b53ffaa5efceaa0bf3e1d5a4c520ce4" + integrity sha512-FwOEi0Il72iAzlkaHrlemVurgSQRDFbk0OC8dSvD5fSBPHltNh7JtLsxmZUhjYBZo2PpcU/RJvvi6Q0l7O7ogw== + +"@svgr/babel-plugin-svg-em-dimensions@^6.5.1": + version "6.5.1" + resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-svg-em-dimensions/-/babel-plugin-svg-em-dimensions-6.5.1.tgz#dd3fa9f5b24eb4f93bcf121c3d40ff5facecb217" + integrity sha512-gWGsiwjb4tw+ITOJ86ndY/DZZ6cuXMNE/SjcDRg+HLuCmwpcjOktwRF9WgAiycTqJD/QXqL2f8IzE2Rzh7aVXA== + +"@svgr/babel-plugin-transform-react-native-svg@^6.5.1": + version "6.5.1" + resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-transform-react-native-svg/-/babel-plugin-transform-react-native-svg-6.5.1.tgz#1d8e945a03df65b601551097d8f5e34351d3d305" + integrity sha512-2jT3nTayyYP7kI6aGutkyfJ7UMGtuguD72OjeGLwVNyfPRBD8zQthlvL+fAbAKk5n9ZNcvFkp/b1lZ7VsYqVJg== + +"@svgr/babel-plugin-transform-svg-component@^6.5.1": + version "6.5.1" + resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-transform-svg-component/-/babel-plugin-transform-svg-component-6.5.1.tgz#48620b9e590e25ff95a80f811544218d27f8a250" + integrity sha512-a1p6LF5Jt33O3rZoVRBqdxL350oge54iZWHNI6LJB5tQ7EelvD/Mb1mfBiZNAan0dt4i3VArkFRjA4iObuNykQ== + +"@svgr/babel-preset@^6.5.1": + version "6.5.1" + resolved "https://registry.yarnpkg.com/@svgr/babel-preset/-/babel-preset-6.5.1.tgz#b90de7979c8843c5c580c7e2ec71f024b49eb828" + integrity sha512-6127fvO/FF2oi5EzSQOAjo1LE3OtNVh11R+/8FXa+mHx1ptAaS4cknIjnUA7e6j6fwGGJ17NzaTJFUwOV2zwCw== + dependencies: + "@svgr/babel-plugin-add-jsx-attribute" "^6.5.1" + "@svgr/babel-plugin-remove-jsx-attribute" "*" + "@svgr/babel-plugin-remove-jsx-empty-expression" "*" + "@svgr/babel-plugin-replace-jsx-attribute-value" "^6.5.1" + "@svgr/babel-plugin-svg-dynamic-title" "^6.5.1" + "@svgr/babel-plugin-svg-em-dimensions" "^6.5.1" + "@svgr/babel-plugin-transform-react-native-svg" "^6.5.1" + "@svgr/babel-plugin-transform-svg-component" "^6.5.1" + +"@svgr/core@^6.2.1": + version "6.5.1" + resolved "https://registry.yarnpkg.com/@svgr/core/-/core-6.5.1.tgz#d3e8aa9dbe3fbd747f9ee4282c1c77a27410488a" + integrity sha512-/xdLSWxK5QkqG524ONSjvg3V/FkNyCv538OIBdQqPNaAta3AsXj/Bd2FbvR87yMbXO2hFSWiAe/Q6IkVPDw+mw== + dependencies: + "@babel/core" "^7.19.6" + "@svgr/babel-preset" "^6.5.1" + "@svgr/plugin-jsx" "^6.5.1" + camelcase "^6.2.0" + cosmiconfig "^7.0.1" + +"@svgr/hast-util-to-babel-ast@^6.5.1": + version "6.5.1" + resolved "https://registry.yarnpkg.com/@svgr/hast-util-to-babel-ast/-/hast-util-to-babel-ast-6.5.1.tgz#81800bd09b5bcdb968bf6ee7c863d2288fdb80d2" + integrity sha512-1hnUxxjd83EAxbL4a0JDJoD3Dao3hmjvyvyEV8PzWmLK3B9m9NPlW7GKjFyoWE8nM7HnXzPcmmSyOW8yOddSXw== + dependencies: + "@babel/types" "^7.20.0" + entities "^4.4.0" + +"@svgr/plugin-jsx@^6.5.1": + version "6.5.1" + resolved "https://registry.yarnpkg.com/@svgr/plugin-jsx/-/plugin-jsx-6.5.1.tgz#0e30d1878e771ca753c94e69581c7971542a7072" + integrity sha512-+UdQxI3jgtSjCykNSlEMuy1jSRQlGC7pqBCPvkG/2dATdWo082zHTTK3uhnAju2/6XpE6B5mZ3z4Z8Ns01S8Gw== + dependencies: + "@babel/core" "^7.19.6" + "@svgr/babel-preset" "^6.5.1" + "@svgr/hast-util-to-babel-ast" "^6.5.1" + svg-parser "^2.0.4" + +"@szmarczak/http-timer@^1.1.2": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@szmarczak/http-timer/-/http-timer-1.1.2.tgz#b1665e2c461a2cd92f4c1bbf50d5454de0d4b421" + integrity sha512-XIB2XbzHTN6ieIjfIMV9hlVcfPU26s2vafYWQcZHWXHOxiaRZYEDKEwdl129Zyg50+foYV2jCgtrqSA6qNuNSA== + dependencies: + defer-to-connect "^1.0.1" + "@testing-library/dom@^8.5.0": version "8.20.0" resolved "https://registry.npmjs.org/@testing-library/dom/-/dom-8.20.0.tgz" @@ -3989,6 +4222,13 @@ dependencies: "@types/react" "*" +"@types/react-beautiful-dnd@13.1.8": + version "13.1.8" + resolved "https://registry.yarnpkg.com/@types/react-beautiful-dnd/-/react-beautiful-dnd-13.1.8.tgz#f52d3ea07e1e19159d6c3c4a48c8da3d855e60b4" + integrity sha512-E3TyFsro9pQuK4r8S/OL6G99eq7p8v29sX0PM7oT8Z+PJfZvSQTx4zTQbUJ+QZXioAF0e7TGBEcA1XhYhCweyQ== + dependencies: + "@types/react" "*" + "@types/react-dom@18.0.10", "@types/react-dom@^18.0.0": version "18.0.10" resolved "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.0.10.tgz" @@ -4773,6 +5013,13 @@ an-array@^1.0.0: resolved "https://registry.npmjs.org/an-array/-/an-array-1.0.0.tgz" integrity sha512-M175GYI7RmsYu24Ok383yZQa3eveDfNnmhTe3OQ3bm70bEovz2gWenH+ST/n32M8lrwLWk74hcPds5CDRPe2wg== +ansi-align@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/ansi-align/-/ansi-align-3.0.1.tgz#0cdf12e111ace773a86e9a1fad1225c43cb19a59" + integrity sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w== + dependencies: + string-width "^4.1.0" + ansi-colors@^4.1.1: version "4.1.3" resolved "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz" @@ -5654,6 +5901,20 @@ boolbase@^1.0.0: resolved "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz" integrity sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww== +boxen@^5.0.0: + version "5.1.2" + resolved "https://registry.yarnpkg.com/boxen/-/boxen-5.1.2.tgz#788cb686fc83c1f486dfa8a40c68fc2b831d2b50" + integrity sha512-9gYgQKXx+1nP8mP7CzFyaUARhg7D3n1dF/FnErWmu9l6JvGpNUN278h0aSb+QjoiKSWG+iZ3uHrcqk0qrY9RQQ== + dependencies: + ansi-align "^3.0.0" + camelcase "^6.2.0" + chalk "^4.1.0" + cli-boxes "^2.2.1" + string-width "^4.2.2" + type-fest "^0.20.2" + widest-line "^3.1.0" + wrap-ansi "^7.0.0" + brace-expansion@^1.1.7: version "1.1.11" resolved "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz" @@ -5715,6 +5976,16 @@ browserslist@^4.21.10, browserslist@^4.21.9: node-releases "^2.0.13" update-browserslist-db "^1.0.11" +browserslist@^4.22.2: + version "4.22.3" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.22.3.tgz#299d11b7e947a6b843981392721169e27d60c5a6" + integrity sha512-UAp55yfwNv0klWNapjs/ktHoguxuQNGnOzxYmfnXIS+8AsRDZkSDxg7R1AX3GKzn078SBI5dzwzj/Yx0Or0e3A== + dependencies: + caniuse-lite "^1.0.30001580" + electron-to-chromium "^1.4.648" + node-releases "^2.0.14" + update-browserslist-db "^1.0.13" + bs-logger@0.x: version "0.2.6" resolved "https://registry.npmjs.org/bs-logger/-/bs-logger-0.2.6.tgz" @@ -5770,6 +6041,19 @@ bytes@3.1.2: resolved "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz" integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg== +cacheable-request@^6.0.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/cacheable-request/-/cacheable-request-6.1.0.tgz#20ffb8bd162ba4be11e9567d823db651052ca912" + integrity sha512-Oj3cAGPCqOZX7Rz64Uny2GYAZNliQSqfbePrgAQ1wKAihYmCUnraBtJtKcGR4xz7wF+LoJC+ssFZvv5BgF9Igg== + dependencies: + clone-response "^1.0.2" + get-stream "^5.1.0" + http-cache-semantics "^4.0.0" + keyv "^3.0.0" + lowercase-keys "^2.0.0" + normalize-url "^4.1.0" + responselike "^1.0.2" + caching-transform@^4.0.0: version "4.0.0" resolved "https://registry.npmjs.org/caching-transform/-/caching-transform-4.0.0.tgz" @@ -5815,7 +6099,7 @@ camelcase@^5.0.0, camelcase@^5.3.1: resolved "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz" integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg== -camelcase@^6.1.0, camelcase@^6.2.0: +camelcase@^6.1.0, camelcase@^6.2.0, camelcase@^6.3.0: version "6.3.0" resolved "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz" integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA== @@ -5845,6 +6129,11 @@ caniuse-lite@^1.0.30001517: resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001525.tgz#d2e8fdec6116ffa36284ca2c33ef6d53612fe1c8" integrity sha512-/3z+wB4icFt3r0USMwxujAqRvaD/B7rvGTsKhbhSQErVrJvkZCLhgNLJxU8MevahQVH6hCU9FsHdNUFbiwmE7Q== +caniuse-lite@^1.0.30001580: + version "1.0.30001587" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001587.tgz#a0bce920155fa56a1885a69c74e1163fc34b4881" + integrity sha512-HMFNotUmLXn71BQxg8cijvqxnIAofforZOwGsxyXJ0qugTdspUF4sPSJ2vhgprHCB996tIDzEq1ubumPDV8ULA== + canvas-color-tracker@1: version "1.2.1" resolved "https://registry.npmjs.org/canvas-color-tracker/-/canvas-color-tracker-1.2.1.tgz" @@ -5875,7 +6164,7 @@ ccount@^2.0.0: resolved "https://registry.yarnpkg.com/ccount/-/ccount-2.0.1.tgz#17a3bf82302e0870d6da43a01311a8bc02a3ecf5" integrity sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg== -chalk@^2.0.0, chalk@^2.4.2: +chalk@^2.0.0, chalk@^2.4.1, chalk@^2.4.2: version "2.4.2" resolved "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz" integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== @@ -5892,7 +6181,7 @@ chalk@^3.0.0: ansi-styles "^4.1.0" supports-color "^7.1.0" -chalk@^4.0.0, chalk@^4.1.0, chalk@^4.1.1: +chalk@^4.0.0, chalk@^4.1.0, chalk@^4.1.1, chalk@^4.1.2: version "4.1.2" resolved "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz" integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== @@ -5998,6 +6287,11 @@ chrome-trace-event@^1.0.2: resolved "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz" integrity sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg== +ci-info@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-2.0.0.tgz#67a9e964be31a51e15e5010d58e6f12834002f46" + integrity sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ== + ci-info@^3.2.0: version "3.8.0" resolved "https://registry.npmjs.org/ci-info/-/ci-info-3.8.0.tgz" @@ -6025,6 +6319,11 @@ clean-stack@^2.0.0: resolved "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz" integrity sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A== +cli-boxes@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/cli-boxes/-/cli-boxes-2.2.1.tgz#ddd5035d25094fce220e9cab40a45840a440318f" + integrity sha512-y4coMcylgSCdVinjiDBuR8PCC2bLjyGTwEmPb9NHR/QaNU6EUOXcTY/s6VjGMD6ENSEaeQYHCY0GNGS5jfMwPw== + cli-cursor@^3.1.0: version "3.1.0" resolved "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz" @@ -6094,6 +6393,13 @@ clone-deep@^4.0.1: kind-of "^6.0.2" shallow-clone "^3.0.0" +clone-response@^1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/clone-response/-/clone-response-1.0.3.tgz#af2032aa47816399cf5f0a1d0db902f517abb8c3" + integrity sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA== + dependencies: + mimic-response "^1.0.0" + clone@^1.0.2: version "1.0.4" resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.4.tgz#da309cc263df15994c688ca902179ca3c7cd7c7e" @@ -6230,7 +6536,7 @@ commander@^8.3.0: resolved "https://registry.yarnpkg.com/commander/-/commander-8.3.0.tgz#4837ea1b2da67b9c616a67afbb0fafee567bca66" integrity sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww== -commander@^9.3.0: +commander@^9.2.0, commander@^9.3.0: version "9.5.0" resolved "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz" integrity sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ== @@ -6304,6 +6610,18 @@ concat-map@0.0.1: resolved "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz" integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== +configstore@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/configstore/-/configstore-5.0.1.tgz#d365021b5df4b98cdd187d6a3b0e3f6a7cc5ed96" + integrity sha512-aMKprgk5YhBNyH25hj8wGt2+D52Sw1DRRIzqBwLp2Ya9mFmY8KPvvtvmna8SxVR9JMZ4kzMD68N22vlaRpkeFA== + dependencies: + dot-prop "^5.2.0" + graceful-fs "^4.1.2" + make-dir "^3.0.0" + unique-string "^2.0.0" + write-file-atomic "^3.0.0" + xdg-basedir "^4.0.0" + confusing-browser-globals@^1.0.10: version "1.0.11" resolved "https://registry.npmjs.org/confusing-browser-globals/-/confusing-browser-globals-1.0.11.tgz" @@ -6314,6 +6632,16 @@ connect-history-api-fallback@^2.0.0: resolved "https://registry.npmjs.org/connect-history-api-fallback/-/connect-history-api-fallback-2.0.0.tgz" integrity sha512-U73+6lQFmfiNPrYbXqr6kZ1i1wiRqXnp2nhMsINseWXO8lDau0LGEffJ8kQi4EjLZympVgRdvqjAgiZ1tgzDDA== +connect@^3.7.0: + version "3.7.0" + resolved "https://registry.yarnpkg.com/connect/-/connect-3.7.0.tgz#5d49348910caa5e07a01800b030d0c35f20484f8" + integrity sha512-ZqRXc+tZukToSNmh5C2iWMSoV3X1YUcPbqEM4DkEG5tNQXrQUZCNVGGv3IuicnkMtPfGf3Xtp8WCXs295iQ1pQ== + dependencies: + debug "2.6.9" + finalhandler "1.1.2" + parseurl "~1.3.3" + utils-merge "1.0.1" + constant-case@^3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/constant-case/-/constant-case-3.0.4.tgz#3b84a9aeaf4cf31ec45e6bf5de91bdfb0589faf1" @@ -6448,7 +6776,7 @@ cosmiconfig-typescript-loader@^2.0.0: cosmiconfig "^7" ts-node "^10.8.1" -cosmiconfig@^7, cosmiconfig@^7.0.0: +cosmiconfig@^7, cosmiconfig@^7.0.0, cosmiconfig@^7.0.1: version "7.1.0" resolved "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz" integrity sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA== @@ -6488,7 +6816,7 @@ critters@^0.0.16: cross-env@^7.0.3: version "7.0.3" - resolved "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz" + resolved "https://registry.yarnpkg.com/cross-env/-/cross-env-7.0.3.tgz#865264b29677dc015ba8418918965dd232fc54cf" integrity sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw== dependencies: cross-spawn "^7.0.1" @@ -6500,6 +6828,17 @@ cross-fetch@3.1.5: dependencies: node-fetch "2.6.7" +cross-spawn@^6.0.5: + version "6.0.5" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4" + integrity sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ== + dependencies: + nice-try "^1.0.4" + path-key "^2.0.1" + semver "^5.5.0" + shebang-command "^1.2.0" + which "^1.2.9" + cross-spawn@^7.0.0, cross-spawn@^7.0.1, cross-spawn@^7.0.2, cross-spawn@^7.0.3: version "7.0.3" resolved "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz" @@ -6509,6 +6848,18 @@ cross-spawn@^7.0.0, cross-spawn@^7.0.1, cross-spawn@^7.0.2, cross-spawn@^7.0.3: shebang-command "^2.0.0" which "^2.0.1" +crypto-random-string@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/crypto-random-string/-/crypto-random-string-2.0.0.tgz#ef2a7a966ec11083388369baa02ebead229b30d5" + integrity sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA== + +css-box-model@^1.2.0: + version "1.2.1" + resolved "https://registry.yarnpkg.com/css-box-model/-/css-box-model-1.2.1.tgz#59951d3b81fd6b2074a62d49444415b0d2b4d7c1" + integrity sha512-a7Vr4Q/kd/aw96bnJG332W9V9LkJO69JRcaCYDUqjp6/z0w6VcZjgAcTbgFxEPfBgdnAwlh3iwu+hLopa+flJw== + dependencies: + tiny-invariant "^1.0.6" + css-color-keywords@^1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz" @@ -6985,6 +7336,11 @@ deep-equal@^2.0.5: which-collection "^1.0.1" which-typed-array "^1.1.9" +deep-extend@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac" + integrity sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA== + deep-is@^0.1.3, deep-is@~0.1.3: version "0.1.4" resolved "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz" @@ -7016,6 +7372,11 @@ defaults@^1.0.3: dependencies: clone "^1.0.2" +defer-to-connect@^1.0.1: + version "1.1.3" + resolved "https://registry.yarnpkg.com/defer-to-connect/-/defer-to-connect-1.1.3.tgz#331ae050c08dcf789f8c83a7b81f0ed94f4ac591" + integrity sha512-0ISdNousHvZT2EiFlZeZAHBUvSxmKswVCEf8hW7KWgG4a8MVEu/3Vb6uWYozkjylyCxe0JBIiRB1jV45S70WVQ== + define-lazy-prop@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz" @@ -7054,6 +7415,11 @@ destroy@1.2.0: resolved "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz" integrity sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg== +detect-file@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/detect-file/-/detect-file-1.0.0.tgz#f0d66d03672a825cb1b73bdb3fe62310c8e552b7" + integrity sha512-DtCOLG98P007x7wiiOmfI0fi3eIKyWiLTGJ2MDnVi/E04lWGbf+JzrRHMm0rgIIZJGtHpKpbVgLWHrv8xXpc3Q== + detect-newline@^3.0.0: version "3.1.0" resolved "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz" @@ -7228,7 +7594,7 @@ dot-case@^3.0.4: no-case "^3.0.4" tslib "^2.0.3" -dot-prop@^5.1.0: +dot-prop@^5.1.0, dot-prop@^5.2.0: version "5.3.0" resolved "https://registry.yarnpkg.com/dot-prop/-/dot-prop-5.3.0.tgz#90ccce708cd9cd82cc4dc8c3ddd9abdd55b20e88" integrity sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q== @@ -7250,6 +7616,11 @@ dtype@^2.0.0: resolved "https://registry.npmjs.org/dtype/-/dtype-2.0.0.tgz" integrity sha512-s2YVcLKdFGS0hpFqJaTwscsyt0E8nNFdmo73Ocd81xNPj4URI4rj6D60A+vFMIw7BXWlb4yRkEwfBqcZzPGiZg== +duplexer3@^0.1.4: + version "0.1.5" + resolved "https://registry.yarnpkg.com/duplexer3/-/duplexer3-0.1.5.tgz#0b5e4d7bad5de8901ea4440624c8e1d20099217e" + integrity sha512-1A8za6ws41LQgv9HrE/66jyC5yuSjQ3L/KOpFtoBilsAK2iA2wuS5rTt1OCzIvtS2V7nVmedsUU+DGRcjBmOYA== + duplexer@^0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/duplexer/-/duplexer-0.1.2.tgz#3abe43aef3835f8ae077d136ddce0f276b0400e6" @@ -7280,6 +7651,11 @@ electron-to-chromium@^1.4.477: resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.508.tgz#5641ff2f5ba11df4bd960fe6a2f9f70aa8b9af96" integrity sha512-FFa8QKjQK/A5QuFr2167myhMesGrhlOBD+3cYNxO9/S4XzHEXesyTD/1/xF644gC8buFPz3ca6G1LOQD0tZrrg== +electron-to-chromium@^1.4.648: + version "1.4.667" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.667.tgz#2767d998548e5eeeaf8bdaffd67b56796bfbed3d" + integrity sha512-66L3pLlWhTNVUhnmSA5+qDM3fwnXsM6KAqE36e2w4KN0g6pkEtlT5bs41FQtQwVwKnfhNBXiWRLPs30HSxd7Kw== + emitter-component@^1.1.1: version "1.1.1" resolved "https://registry.npmjs.org/emitter-component/-/emitter-component-1.1.1.tgz" @@ -7310,6 +7686,13 @@ encodeurl@~1.0.2: resolved "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz" integrity sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w== +end-of-stream@^1.1.0: + version "1.4.4" + resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0" + integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q== + dependencies: + once "^1.4.0" + enhanced-resolve@^5.13.0, enhanced-resolve@^5.7.0: version "5.13.0" resolved "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.13.0.tgz" @@ -7338,7 +7721,7 @@ entities@^2.0.0, entities@^2.2.0: resolved "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz" integrity sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A== -entities@^4.2.0, entities@^4.5.0: +entities@^4.2.0, entities@^4.4.0, entities@^4.5.0: version "4.5.0" resolved "https://registry.yarnpkg.com/entities/-/entities-4.5.0.tgz#5d268ea5e7113ec74c4d033b79ea5a35a488fb48" integrity sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw== @@ -7509,6 +7892,11 @@ escalade@^3.1.1: resolved "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz" integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw== +escape-goat@^2.0.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/escape-goat/-/escape-goat-2.1.1.tgz#1b2dc77003676c457ec760b2dc68edb648188675" + integrity sha512-8/uIhbG12Csjy2JEW7D9pHbreaVaS/OpN3ycnyvElTdwM5n6GY6W6e2IPemfvGZeUMqZ9A/3GqIZMgKnBhAw/Q== + escape-html@~1.0.3: version "1.0.3" resolved "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz" @@ -7951,6 +8339,13 @@ expand-tilde@^1.2.2: dependencies: os-homedir "^1.0.1" +expand-tilde@^2.0.0, expand-tilde@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/expand-tilde/-/expand-tilde-2.0.2.tgz#97e801aa052df02454de46b02bf621642cdc8502" + integrity sha512-A5EmesHW6rfnZ9ysHQjPdJRni0SRar0tjtG5MNtm9n5TUvsYU8oozprtRD4AqHxcZWWlVuAmQo2nWKfN9oyjTw== + dependencies: + homedir-polyfill "^1.0.1" + expect-playwright@^0.8.0: version "0.8.0" resolved "https://registry.npmjs.org/expect-playwright/-/expect-playwright-0.8.0.tgz" @@ -8146,6 +8541,19 @@ fill-range@^7.0.1: dependencies: to-regex-range "^5.0.1" +finalhandler@1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.1.2.tgz#b7e7d000ffd11938d0fdb053506f6ebabe9f587d" + integrity sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA== + dependencies: + debug "2.6.9" + encodeurl "~1.0.2" + escape-html "~1.0.3" + on-finished "~2.3.0" + parseurl "~1.3.3" + statuses "~1.5.0" + unpipe "~1.0.0" + finalhandler@1.2.0: version "1.2.0" resolved "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz" @@ -8184,6 +8592,14 @@ find-file-up@^0.1.2: fs-exists-sync "^0.1.0" resolve-dir "^0.1.0" +find-node-modules@^2.1.3: + version "2.1.3" + resolved "https://registry.yarnpkg.com/find-node-modules/-/find-node-modules-2.1.3.tgz#3c976cff2ca29ee94b4f9eafc613987fc4c0ee44" + integrity sha512-UC2I2+nx1ZuOBclWVNdcnbDR5dlrOdVb7xNjmT/lHE+LsgztWks3dG7boJ37yTS/venXw84B/mAW9uHVoC5QRg== + dependencies: + findup-sync "^4.0.0" + merge "^2.1.1" + find-pkg@^0.1.2: version "0.1.2" resolved "https://registry.npmjs.org/find-pkg/-/find-pkg-0.1.2.tgz" @@ -8224,6 +8640,16 @@ find-up@^6.3.0: locate-path "^7.1.0" path-exists "^5.0.0" +findup-sync@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/findup-sync/-/findup-sync-4.0.0.tgz#956c9cdde804052b881b428512905c4a5f2cdef0" + integrity sha512-6jvvn/12IC4quLBL1KNokxC7wWTvYncaVUYSoxWw7YykPLuRrnv4qdHcSOywOI5RpkOVGeQRtWM8/q+G6W6qfQ== + dependencies: + detect-file "^1.0.0" + is-glob "^4.0.0" + micromatch "^4.0.2" + resolve-dir "^1.0.1" + flat-cache@^3.0.4: version "3.0.4" resolved "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz" @@ -8452,6 +8878,20 @@ get-package-type@^0.1.0: resolved "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz" integrity sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q== +get-stream@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-4.1.0.tgz#c1b255575f3dc21d59bfc79cd3d2b46b1c3a54b5" + integrity sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w== + dependencies: + pump "^3.0.0" + +get-stream@^5.1.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-5.2.0.tgz#4966a1795ee5ace65e706c4b7beb71257d6e22d3" + integrity sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA== + dependencies: + pump "^3.0.0" + get-stream@^6.0.0: version "6.0.1" resolved "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz" @@ -8517,6 +8957,13 @@ global-dirs@^0.1.1: dependencies: ini "^1.3.4" +global-dirs@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/global-dirs/-/global-dirs-3.0.1.tgz#0c488971f066baceda21447aecb1a8b911d22485" + integrity sha512-NBcGGFbBA9s1VzD41QXDG+3++t9Mn5t1FpLdhESY6oKY4gYTFpX4wO3sqGUa0Srjtbfj3szX0RnemmrVRUdULA== + dependencies: + ini "2.0.0" + global-modules@^0.2.3: version "0.2.3" resolved "https://registry.npmjs.org/global-modules/-/global-modules-0.2.3.tgz" @@ -8525,6 +8972,15 @@ global-modules@^0.2.3: global-prefix "^0.1.4" is-windows "^0.2.0" +global-modules@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/global-modules/-/global-modules-1.0.0.tgz#6d770f0eb523ac78164d72b5e71a8877265cc3ea" + integrity sha512-sKzpEkf11GpOFuw0Zzjzmt4B4UZwjOcG757PPvrfhxcLFbq0wpsgpOqxpxtxFiCG4DtG93M6XRVbF2oGdev7bg== + dependencies: + global-prefix "^1.0.1" + is-windows "^1.0.1" + resolve-dir "^1.0.0" + global-prefix@^0.1.4: version "0.1.5" resolved "https://registry.npmjs.org/global-prefix/-/global-prefix-0.1.5.tgz" @@ -8535,6 +8991,17 @@ global-prefix@^0.1.4: is-windows "^0.2.0" which "^1.2.12" +global-prefix@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/global-prefix/-/global-prefix-1.0.2.tgz#dbf743c6c14992593c655568cb66ed32c0122ebe" + integrity sha512-5lsx1NUDHtSjfg0eHlmYvZKv8/nVqX4ckFbM+FrGcQ+04KWcWFo9P5MxPZYSzUvyzmdTbI7Eix8Q4IbELDqzKg== + dependencies: + expand-tilde "^2.0.2" + homedir-polyfill "^1.0.1" + ini "^1.3.4" + is-windows "^1.0.1" + which "^1.2.14" + global@^4.3.0, global@~4.4.0: version "4.4.0" resolved "https://registry.npmjs.org/global/-/global-4.4.0.tgz" @@ -8581,6 +9048,23 @@ gopd@^1.0.1: dependencies: get-intrinsic "^1.1.3" +got@^9.6.0: + version "9.6.0" + resolved "https://registry.yarnpkg.com/got/-/got-9.6.0.tgz#edf45e7d67f99545705de1f7bbeeeb121765ed85" + integrity sha512-R7eWptXuGYxwijs0eV+v3o6+XH1IqVK8dJOEecQfTmkncw9AV4dcw/Dhxi8MdlqPthxxpZyizMzyg8RTmEsG+Q== + dependencies: + "@sindresorhus/is" "^0.14.0" + "@szmarczak/http-timer" "^1.1.2" + cacheable-request "^6.0.0" + decompress-response "^3.3.0" + duplexer3 "^0.1.4" + get-stream "^4.1.0" + lowercase-keys "^1.0.1" + mimic-response "^1.0.1" + p-cancelable "^1.0.0" + to-readable-stream "^1.0.0" + url-parse-lax "^3.0.0" + graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.4, graceful-fs@^4.2.6, graceful-fs@^4.2.9: version "4.2.11" resolved "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz" @@ -8647,6 +9131,11 @@ has-tostringtag@^1.0.0: dependencies: has-symbols "^1.0.2" +has-yarn@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/has-yarn/-/has-yarn-2.1.0.tgz#137e11354a7b5bf11aa5cb649cf0c6f3ff2b2e77" + integrity sha512-UqBRqi4ju7T+TqGNdqAO0PaSVGsDGJUBQvk9eUWNGRY1CFGDzYhLWoM7JQEemnlvVcv/YEmc2wNW8BC24EnUsw== + has@^1.0.3: version "1.0.3" resolved "https://registry.npmjs.org/has/-/has-1.0.3.tgz" @@ -8878,7 +9367,7 @@ hoist-non-react-statics@^3.0.0, hoist-non-react-statics@^3.1.0, hoist-non-react- dependencies: react-is "^16.7.0" -homedir-polyfill@^1.0.0: +homedir-polyfill@^1.0.0, homedir-polyfill@^1.0.1: version "1.0.3" resolved "https://registry.npmjs.org/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz" integrity sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA== @@ -8970,6 +9459,11 @@ htmlparser2@^6.1.0: domutils "^2.5.2" entities "^2.0.0" +http-cache-semantics@^4.0.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz#abe02fcb2985460bf0323be664436ec3476a6d5a" + integrity sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ== + http-deceiver@^1.2.7: version "1.2.7" resolved "https://registry.npmjs.org/http-deceiver/-/http-deceiver-1.2.7.tgz" @@ -9131,6 +9625,11 @@ import-fresh@^3.0.0, import-fresh@^3.2.1: parent-module "^1.0.0" resolve-from "^4.0.0" +import-lazy@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/import-lazy/-/import-lazy-2.1.0.tgz#05698e3d45c88e8d7e9d92cb0584e77f096f3e43" + integrity sha512-m7ZEHgtw69qOGw+jwxXkHlrlIPdTGkyh66zXZ1ajZbxkDBNjSY/LGbmjc7h0s2ELsUDTAhFr55TrPSSqJGPG0A== + import-lazy@~4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/import-lazy/-/import-lazy-4.0.0.tgz#e8eb627483a0a43da3c03f3e35548be5cb0cc153" @@ -9177,7 +9676,12 @@ inherits@2.0.3: resolved "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz" integrity sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw== -ini@^1.3.4: +ini@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ini/-/ini-2.0.0.tgz#e5fd556ecdd5726be978fa1001862eacb0a94bc5" + integrity sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA== + +ini@^1.3.4, ini@~1.3.0: version "1.3.8" resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c" integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew== @@ -9342,6 +9846,13 @@ is-callable@^1.1.3, is-callable@^1.1.4, is-callable@^1.2.7: resolved "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz" integrity sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA== +is-ci@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/is-ci/-/is-ci-2.0.0.tgz#6bc6334181810e04b5c22b3d589fdca55026404c" + integrity sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w== + dependencies: + ci-info "^2.0.0" + is-ci@^3.0.1: version "3.0.1" resolved "https://registry.npmjs.org/is-ci/-/is-ci-3.0.1.tgz" @@ -9441,6 +9952,14 @@ is-hexadecimal@^2.0.0: resolved "https://registry.yarnpkg.com/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz#86b5bf668fca307498d319dfc03289d781a90027" integrity sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg== +is-installed-globally@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/is-installed-globally/-/is-installed-globally-0.4.0.tgz#9a0fd407949c30f86eb6959ef1b7994ed0b7b520" + integrity sha512-iwGqO3J21aaSkC7jWnHP/difazwS7SFeIqxv6wEtLU8Y5KlzFTjyqcSIT0d8s4+dDhKytsk9PJZ2BkS5eZwQRQ== + dependencies: + global-dirs "^3.0.0" + is-path-inside "^3.0.2" + is-interactive@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-interactive/-/is-interactive-1.0.0.tgz#cea6e6ae5c870a7b0a0004070b7b587e0252912e" @@ -9461,6 +9980,11 @@ is-node-process@^1.2.0: resolved "https://registry.yarnpkg.com/is-node-process/-/is-node-process-1.2.0.tgz#ea02a1b90ddb3934a19aea414e88edef7e11d134" integrity sha512-Vg4o6/fqPxIjtxgUH5QLJhwZ7gW5diGCVlXpuUfELC62CuxM1iHcRe51f2W1FDy04Ai4KJkagKjx3XaqyfRKXw== +is-npm@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/is-npm/-/is-npm-5.0.0.tgz#43e8d65cc56e1b67f8d47262cf667099193f45a8" + integrity sha512-WW/rQLOazUq+ST/bCAVBp/2oMERWLsR7OrKyt052dNDk4DHcDE0/7QSXITlmi+VBcV13DfIbysG3tZJm5RfdBA== + is-number-object@^1.0.4: version "1.0.7" resolved "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz" @@ -9483,6 +10007,11 @@ is-obj@^2.0.0: resolved "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz" integrity sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w== +is-path-inside@^3.0.2: + version "3.0.3" + resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-3.0.3.tgz#d231362e53a07ff2b0e0ea7fed049161ffd16283" + integrity sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ== + is-plain-obj@^1.1.0: version "1.1.0" resolved "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz" @@ -9621,7 +10150,7 @@ is-windows@^0.2.0: resolved "https://registry.npmjs.org/is-windows/-/is-windows-0.2.0.tgz" integrity sha512-n67eJYmXbniZB7RF4I/FTjK1s6RPOCTxhYrVYLRaCt3lF0mpWZPKr3T2LSZAqyjQsxR2qMmGYXXzK0YWwcPM1Q== -is-windows@^1.0.2: +is-windows@^1.0.1, is-windows@^1.0.2: version "1.0.2" resolved "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz" integrity sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA== @@ -9633,6 +10162,11 @@ is-wsl@^2.2.0: dependencies: is-docker "^2.0.0" +is-yarn-global@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/is-yarn-global/-/is-yarn-global-0.3.0.tgz#d502d3382590ea3004893746754c89139973e232" + integrity sha512-VjSeb/lHmkoyd8ryPVIKvOCn4D1koMqY+vqyjjUfc3xyKtP4dYOxM44sZrnqQSzSds3xyOrUTLTC9LVCVgLngw== + isarray@0.0.1: version "0.0.1" resolved "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz" @@ -10034,6 +10568,27 @@ jest-pnp-resolver@^1.2.2: resolved "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz" integrity sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w== +jest-preview@0.3.1: + version "0.3.1" + resolved "https://registry.yarnpkg.com/jest-preview/-/jest-preview-0.3.1.tgz#5445ba977b06cafb30c9d8489b9cb549f258ccaa" + integrity sha512-gRR4shnXFSh8tdNaIncJC98d1zXD7w7LA52HQC0bu0DsPb+FXVEg+NQh9GTbO+n6/SCgcZNQAVt4MeCfsIkBPA== + dependencies: + "@svgr/core" "^6.2.1" + camelcase "^6.3.0" + chalk "^4.1.2" + chokidar "^3.5.3" + commander "^9.2.0" + connect "^3.7.0" + find-node-modules "^2.1.3" + open "^8.4.0" + postcss-import "^14.1.0" + postcss-load-config "^4.0.1" + sirv "^2.0.2" + slash "^3.0.0" + string-hash "^1.1.3" + update-notifier "^5.1.0" + ws "^8.5.0" + jest-process-manager@^0.3.1: version "0.3.1" resolved "https://registry.npmjs.org/jest-process-manager/-/jest-process-manager-0.3.1.tgz" @@ -10364,6 +10919,16 @@ jsesc@~0.5.0: resolved "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz" integrity sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA== +json-buffer@3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.0.tgz#5b1f397afc75d677bde8bcfc0e47e1f9a3d9a898" + integrity sha512-CuUqjv0FUZIdXkHPI8MezCnFCdaTAacej1TZYulLoAg1h/PhwkdXFN4V/gzY4g+fMBCOV2xF+rp7t2XD2ns/NQ== + +json-parse-better-errors@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz#bb867cfb3450e69107c131d1c514bab3dc8bcaa9" + integrity sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw== + json-parse-even-better-errors@^2.3.0, json-parse-even-better-errors@^2.3.1: version "2.3.1" resolved "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz" @@ -10459,6 +11024,13 @@ kapsule@1, kapsule@^1.14: dependencies: lodash-es "4" +keyv@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/keyv/-/keyv-3.1.0.tgz#ecc228486f69991e49e9476485a5be1e8fc5c4d9" + integrity sha512-9ykJ/46SN/9KPM/sichzQ7OvXyGDYKGTaDlKMGCAlg2UK8KRy4jb0d8sFc+0Tt0YYnThq8X2RZgCg74RPxgcVA== + dependencies: + json-buffer "3.0.0" + kind-of@^6.0.2, kind-of@^6.0.3: version "6.0.3" resolved "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz" @@ -10496,6 +11068,13 @@ language-tags@=1.0.5: dependencies: language-subtag-registry "~0.3.2" +latest-version@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/latest-version/-/latest-version-5.1.0.tgz#119dfe908fe38d15dfa43ecd13fa12ec8832face" + integrity sha512-weT+r0kTkRQdCdYCNtkMwWXQTMEswKrFBkm4ckQOMVhhqhIMI1UT2hMj+1iigIhgSZm5gTmrRXBNoGUgaTY1xA== + dependencies: + package-json "^6.3.0" + launch-editor@^2.6.0: version "2.6.0" resolved "https://registry.npmjs.org/launch-editor/-/launch-editor-2.6.0.tgz" @@ -10597,6 +11176,11 @@ lilconfig@^2.0.5, lilconfig@^2.1.0: resolved "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz" integrity sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ== +lilconfig@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/lilconfig/-/lilconfig-3.0.0.tgz#f8067feb033b5b74dab4602a5f5029420be749bc" + integrity sha512-K2U4W2Ff5ibV7j7ydLr+zLAkIg5JJ4lPn1Ltsdt+Tz/IjQ8buJ55pZAxoP34lqIiwtF9iAvtLv3JGv7CAyAg+g== + lines-and-columns@^1.1.6: version "1.2.4" resolved "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz" @@ -10650,6 +11234,16 @@ load-bmfont@^1.2.3: xhr "^2.0.1" xtend "^4.0.0" +load-json-file@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-4.0.0.tgz#2f5f45ab91e33216234fd53adab668eb4ec0993b" + integrity sha512-Kx8hMakjX03tiGTLAIdJ+lL0htKnXjEZN6hk/tozf/WOuYGdZBJrZ+rCJRbVCugsjB3jMLn9746NsQIf5VjBMw== + dependencies: + graceful-fs "^4.1.2" + parse-json "^4.0.0" + pify "^3.0.0" + strip-bom "^3.0.0" + loader-runner@^4.2.0: version "4.3.0" resolved "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz" @@ -10787,6 +11381,16 @@ lower-case@^2.0.2: dependencies: tslib "^2.0.3" +lowercase-keys@^1.0.0, lowercase-keys@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-1.0.1.tgz#6f9e30b47084d971a7c820ff15a6c5167b74c26f" + integrity sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA== + +lowercase-keys@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-2.0.0.tgz#2603e78b7b4b0006cbca2fbcc8a3202558ac9479" + integrity sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA== + lowlight@^1.17.0: version "1.20.0" resolved "https://registry.yarnpkg.com/lowlight/-/lowlight-1.20.0.tgz#ddb197d33462ad0d93bf19d17b6c301aa3941888" @@ -10809,10 +11413,10 @@ lru-cache@^6.0.0: dependencies: yallist "^4.0.0" -lucide-react@0.288.0: - version "0.288.0" - resolved "https://registry.yarnpkg.com/lucide-react/-/lucide-react-0.288.0.tgz#cc9fda209fe4ec6e572efca38f7d3e3cde7422eb" - integrity sha512-ikhb/9LOkq9orPoLV9lLC4UYyoXQycBhIgH7H59ahOkk0mkcAqkD52m84RXedE/qVqZHW8rEJquInT4xGmsNqw== +lucide-react@0.321.0: + version "0.321.0" + resolved "https://registry.yarnpkg.com/lucide-react/-/lucide-react-0.321.0.tgz#05a2600e0a6551c117fb4e7b2676b1286389d949" + integrity sha512-Fi9VahIna6642U+2nAGSjnXwUBV3WyfFFPQq4yi3w30jtqxDLfSyiYCtCYCYQZ2KWNZc1MDI+rcsa0t+ChdYpw== lz-string@^1.4.4: version "1.5.0" @@ -11107,6 +11711,16 @@ memfs@^3.4.3: dependencies: fs-monkey "^1.0.3" +memoize-one@^5.1.1: + version "5.2.1" + resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-5.2.1.tgz#8337aa3c4335581839ec01c3d594090cebe8f00e" + integrity sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q== + +memorystream@^0.3.1: + version "0.3.1" + resolved "https://registry.yarnpkg.com/memorystream/-/memorystream-0.3.1.tgz#86d7090b30ce455d63fbae12dda51a47ddcaf9b2" + integrity sha512-S3UwM3yj5mtUSEfP41UZmt/0SCoVYUcU1rkXv+BQ5Ig8ndL4sPoJNBUJERafdPb5jjHJGuMgytgKvKIf58XNBw== + meow@^8.0.0: version "8.1.2" resolved "https://registry.npmjs.org/meow/-/meow-8.1.2.tgz" @@ -11139,6 +11753,11 @@ merge2@^1.3.0, merge2@^1.4.1: resolved "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz" integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== +merge@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/merge/-/merge-2.1.1.tgz#59ef4bf7e0b3e879186436e8481c06a6c162ca98" + integrity sha512-jz+Cfrg9GWOZbQAnDQ4hlVnQky+341Yk5ru8bZSe6sIDTCIg8n9i/u7hSQGSVOF3C7lH6mGtqjkiT9G4wFLL0w== + methods@~1.1.2: version "1.1.2" resolved "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz" @@ -11546,7 +12165,7 @@ mimic-fn@^2.1.0: resolved "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz" integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== -mimic-response@^1.0.0: +mimic-response@^1.0.0, mimic-response@^1.0.1: version "1.0.1" resolved "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz" integrity sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ== @@ -11639,6 +12258,11 @@ mrmime@^1.0.0: resolved "https://registry.npmjs.org/mrmime/-/mrmime-1.0.1.tgz" integrity sha512-hzzEagAgDyoU1Q6yg5uI+AorQgdvMCur3FcKf7NhMKWsaYg+RnbTyHRa/9IlLF9rf455MOCtcqqrQQ83pPP7Uw== +mrmime@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/mrmime/-/mrmime-2.0.0.tgz#151082a6e06e59a9a39b46b3e14d5cfe92b3abb4" + integrity sha512-eu38+hdgojoyq63s+yTpN4XMBdt5l8HhMhc4VKLO9KM5caLIBvUm4thi7fFaxyTmCKeNnXZ5pAlBwCUnhA09uw== + ms@2.1.2: version "2.1.2" resolved "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz" @@ -11793,6 +12417,11 @@ nice-color-palettes@^1.0.1: new-array "^1.0.0" xhr-request "^1.0.1" +nice-try@^1.0.4: + version "1.0.5" + resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366" + integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ== + no-case@^3.0.4: version "3.0.4" resolved "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz" @@ -11837,12 +12466,17 @@ node-releases@^2.0.13: resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.13.tgz#d5ed1627c23e3461e819b02e57b75e4899b1c81d" integrity sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ== +node-releases@^2.0.14: + version "2.0.14" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.14.tgz#2ffb053bceb8b2be8495ece1ab6ce600c4461b0b" + integrity sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw== + node-releases@^2.0.8: version "2.0.10" resolved "https://registry.npmjs.org/node-releases/-/node-releases-2.0.10.tgz" integrity sha512-5GFldHPXVG/YZmFzJvKK2zDSzPKhEp0+ZR5SVaoSag9fsL5YgHbUHDfnG5494ISANDcK4KwPXAx2xqVEydmd7w== -normalize-package-data@^2.5.0: +normalize-package-data@^2.3.2, normalize-package-data@^2.5.0: version "2.5.0" resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.5.0.tgz#e66db1838b200c1dfc233225d12cb36520e234a8" integrity sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA== @@ -11872,6 +12506,11 @@ normalize-range@^0.1.2: resolved "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz" integrity sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA== +normalize-url@^4.1.0: + version "4.5.1" + resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-4.5.1.tgz#0dd90cf1288ee1d1313b87081c9a5932ee48518a" + integrity sha512-9UZCFRHQdNrfTpGg8+1INIg93B6zE0aXMVFkw1WFwvO4SlZywU6aLg5Of0Ap/PgcbSw4LNxvMWXMeugwMCX0AA== + nosleep.js@^0.7.0: version "0.7.0" resolved "https://registry.npmjs.org/nosleep.js/-/nosleep.js-0.7.0.tgz" @@ -11882,6 +12521,21 @@ not@^0.1.0: resolved "https://registry.yarnpkg.com/not/-/not-0.1.0.tgz#c9691c1746c55dcfbe54cbd8bd4ff041bc2b519d" integrity sha512-5PDmaAsVfnWUgTUbJ3ERwn7u79Z0dYxN9ErxCpVJJqe2RK0PJ3z+iFUxuqjwtlDDegXvtWoxD/3Fzxox7tFGWA== +npm-run-all@latest: + version "4.1.5" + resolved "https://registry.yarnpkg.com/npm-run-all/-/npm-run-all-4.1.5.tgz#04476202a15ee0e2e214080861bff12a51d98fba" + integrity sha512-Oo82gJDAVcaMdi3nuoKFavkIHBRVqQ1qvMb+9LHk/cF4P6B2m8aP04hGf7oL6wZ9BuGwX1onlLhpuoofSyoQDQ== + dependencies: + ansi-styles "^3.2.1" + chalk "^2.4.1" + cross-spawn "^6.0.5" + memorystream "^0.3.1" + minimatch "^3.0.4" + pidtree "^0.3.0" + read-pkg "^3.0.0" + shell-quote "^1.6.1" + string.prototype.padend "^3.0.0" + npm-run-path@^4.0.1: version "4.0.1" resolved "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz" @@ -12053,12 +12707,19 @@ on-finished@2.4.1: dependencies: ee-first "1.1.1" +on-finished@~2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947" + integrity sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww== + dependencies: + ee-first "1.1.1" + on-headers@~1.0.2: version "1.0.2" resolved "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz" integrity sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA== -once@^1.3.0, once@^1.3.1: +once@^1.3.0, once@^1.3.1, once@^1.4.0: version "1.4.0" resolved "https://registry.npmjs.org/once/-/once-1.4.0.tgz" integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w== @@ -12079,7 +12740,7 @@ onetime@^5.1.0, onetime@^5.1.2: dependencies: mimic-fn "^2.1.0" -open@^8.0.9: +open@^8.0.9, open@^8.4.0: version "8.4.2" resolved "https://registry.npmjs.org/open/-/open-8.4.2.tgz" integrity sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ== @@ -12147,6 +12808,11 @@ outvariant@^1.2.1, outvariant@^1.4.0: resolved "https://registry.yarnpkg.com/outvariant/-/outvariant-1.4.0.tgz#e742e4bda77692da3eca698ef5bfac62d9fba06e" integrity sha512-AlWY719RF02ujitly7Kk/0QlV+pXGFDHrHf9O2OKqyqgBieaPOIeuSkL8sRK6j2WK+/ZAURq2kZsY0d8JapUiw== +p-cancelable@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-1.1.0.tgz#d078d15a3af409220c886f1d9a0ca2e441ab26cc" + integrity sha512-s73XxOZ4zpt1edZYZzvhqFa6uvQc1vwUa0K0BdtIZgQMAJj9IbebH+JkgKZc9h+B05PKHLOTl4ajG1BmNrVZlw== + p-limit@^2.2.0: version "2.3.0" resolved "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz" @@ -12226,6 +12892,16 @@ package-hash@^4.0.0: lodash.flattendeep "^4.4.0" release-zalgo "^1.0.0" +package-json@^6.3.0: + version "6.5.0" + resolved "https://registry.yarnpkg.com/package-json/-/package-json-6.5.0.tgz#6feedaca35e75725876d0b0e64974697fed145b0" + integrity sha512-k3bdm2n25tkyxcjSKzB5x8kfVxlMdgsbPr0GkZcwHsLpba6cBjqCt1KlcChKEvxHIcTB1FVMuwoijZ26xex5MQ== + dependencies: + got "^9.6.0" + registry-auth-token "^4.0.0" + registry-url "^5.0.0" + semver "^6.2.0" + pako@^2.0.4: version "2.1.0" resolved "https://registry.npmjs.org/pako/-/pako-2.1.0.tgz" @@ -12310,6 +12986,14 @@ parse-headers@^2.0.0, parse-headers@^2.0.2: resolved "https://registry.npmjs.org/parse-headers/-/parse-headers-2.0.5.tgz" integrity sha512-ft3iAoLOB/MlwbNXgzy43SWGP6sQki2jQvAyBg/zDFAgr9bfNWZIUj42Kw2eJIl8kEi4PbgE6U1Zau/HwI75HA== +parse-json@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-4.0.0.tgz#be35f5425be1f7f6c747184f98a788cb99477ee0" + integrity sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw== + dependencies: + error-ex "^1.3.1" + json-parse-better-errors "^1.0.1" + parse-json@^5.0.0, parse-json@^5.2.0: version "5.2.0" resolved "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz" @@ -12388,6 +13072,11 @@ path-is-absolute@^1.0.0: resolved "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz" integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg== +path-key@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40" + integrity sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw== + path-key@^3.0.0, path-key@^3.1.0: version "3.1.1" resolved "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz" @@ -12415,6 +13104,13 @@ path-to-regexp@^6.2.0: resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-6.2.1.tgz#d54934d6798eb9e5ef14e7af7962c945906918e5" integrity sha512-JLyh7xT1kizaEvcaXOQwOc2/Yhw6KZOvPf1S8401UyLk86CU79LN3vl7ztXGm/pZ+YjoyAJ4rxmHwbkBXJX+yw== +path-type@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/path-type/-/path-type-3.0.0.tgz#cef31dc8e0a1a3bb0d105c0cd97cf3bf47f4e36f" + integrity sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg== + dependencies: + pify "^3.0.0" + path-type@^4.0.0: version "4.0.0" resolved "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz" @@ -12457,11 +13153,26 @@ picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.3, picomatch@^2.3.0, picomatc resolved "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz" integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== +pidtree@^0.3.0: + version "0.3.1" + resolved "https://registry.yarnpkg.com/pidtree/-/pidtree-0.3.1.tgz#ef09ac2cc0533df1f3250ccf2c4d366b0d12114a" + integrity sha512-qQbW94hLHEqCg7nhby4yRC7G2+jYHY4Rguc2bjw7Uug4GIJuu1tvf2uHaZv5Q8zdt+WKJ6qK1FOI6amaWUo5FA== + pidtree@^0.5.0: version "0.5.0" resolved "https://registry.npmjs.org/pidtree/-/pidtree-0.5.0.tgz" integrity sha512-9nxspIM7OpZuhBxPg73Zvyq7j1QMPMPsGKTqRc2XOaFQauDvoNz9fM1Wdkjmeo7l9GXOZiRs97sPkuayl39wjA== +pify@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" + integrity sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog== + +pify@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/pify/-/pify-3.0.0.tgz#e5a4acd2c101fdf3d9a4d07f0dbc4db49dd28176" + integrity sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg== + pify@^4.0.1: version "4.0.1" resolved "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz" @@ -12561,6 +13272,15 @@ postcss-discard-overridden@^6.0.0: resolved "https://registry.yarnpkg.com/postcss-discard-overridden/-/postcss-discard-overridden-6.0.0.tgz#49c5262db14e975e349692d9024442de7cd8e234" integrity sha512-4VELwssYXDFigPYAZ8vL4yX4mUepF/oCBeeIT4OXsJPYOtvJumyz9WflmJWTfDwCUcpDR+z0zvCWBXgTx35SVw== +postcss-import@^14.1.0: + version "14.1.0" + resolved "https://registry.yarnpkg.com/postcss-import/-/postcss-import-14.1.0.tgz#a7333ffe32f0b8795303ee9e40215dac922781f0" + integrity sha512-flwI+Vgm4SElObFVPpTIT7SU7R3qk2L7PyduMcokiaVKuWv9d/U+Gm/QAd8NDLuykTWTkcrjOeD2Pp1rMeBTGw== + dependencies: + postcss-value-parser "^4.0.0" + read-cache "^1.0.0" + resolve "^1.1.7" + postcss-load-config@^3.1.4: version "3.1.4" resolved "https://registry.yarnpkg.com/postcss-load-config/-/postcss-load-config-3.1.4.tgz#1ab2571faf84bb078877e1d07905eabe9ebda855" @@ -12569,6 +13289,14 @@ postcss-load-config@^3.1.4: lilconfig "^2.0.5" yaml "^1.10.2" +postcss-load-config@^4.0.1: + version "4.0.2" + resolved "https://registry.yarnpkg.com/postcss-load-config/-/postcss-load-config-4.0.2.tgz#7159dcf626118d33e299f485d6afe4aff7c4a3e3" + integrity sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ== + dependencies: + lilconfig "^3.0.0" + yaml "^2.3.4" + postcss-merge-longhand@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/postcss-merge-longhand/-/postcss-merge-longhand-6.0.0.tgz#6f627b27db939bce316eaa97e22400267e798d69" @@ -12763,7 +13491,7 @@ postcss-unique-selectors@^6.0.0: dependencies: postcss-selector-parser "^6.0.5" -postcss-value-parser@^4.0.2, postcss-value-parser@^4.1.0, postcss-value-parser@^4.2.0: +postcss-value-parser@^4.0.0, postcss-value-parser@^4.0.2, postcss-value-parser@^4.1.0, postcss-value-parser@^4.2.0: version "4.2.0" resolved "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz" integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ== @@ -12813,6 +13541,11 @@ prelude-ls@~1.1.2: resolved "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz" integrity sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w== +prepend-http@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-2.0.0.tgz#e92434bfa5ea8c19f41cdfd401d741a3c819d897" + integrity sha512-ravE6m9Atw9Z/jjttRUZ+clIXogdghyZAuWJ3qEzjT+jI/dL1ifAqhZeC5VHzQp1MSt1+jxKkFNemj/iO7tVUA== + present@0.0.6: version "0.0.6" resolved "https://registry.npmjs.org/present/-/present-0.0.6.tgz" @@ -12955,11 +13688,26 @@ psl@^1.1.33: resolved "https://registry.yarnpkg.com/psl/-/psl-1.9.0.tgz#d0df2a137f00794565fcaf3b2c00cd09f8d5a5a7" integrity sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag== +pump@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64" + integrity sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww== + dependencies: + end-of-stream "^1.1.0" + once "^1.3.1" + punycode@^2.1.0, punycode@^2.1.1: version "2.3.0" resolved "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz" integrity sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA== +pupa@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/pupa/-/pupa-2.1.1.tgz#f5e8fd4afc2c5d97828faa523549ed8744a20d62" + integrity sha512-l1jNAspIBSFqbT+y+5FosojNpVpF94nlI+wDUpqP9enwOTfHx9f0gh5nB96vl+6yTpsJsypeNrwfzPrKuHB41A== + dependencies: + escape-goat "^2.0.0" + q@^1.5.1: version "1.5.1" resolved "https://registry.yarnpkg.com/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7" @@ -13020,6 +13768,11 @@ quickselect@^2.0.0: resolved "https://registry.npmjs.org/quickselect/-/quickselect-2.0.0.tgz" integrity sha512-RKJ22hX8mHe3Y6wH/N3wCM6BWtjaxIyyUIkpHOvfFnxdI4yD4tBXEBKSbriGujF6jnSVkJrffuo6vxACiSSxIw== +raf-schd@^4.0.2: + version "4.0.3" + resolved "https://registry.yarnpkg.com/raf-schd/-/raf-schd-4.0.3.tgz#5d6c34ef46f8b2a0e880a8fcdb743efc5bfdbc1a" + integrity sha512-tQkJl2GRWh83ui2DiPTJz9wEiMN20syf+5oKfB03yYP7ioZcJwsIK8FjrtLwH1m7C7e+Tt2yYBlrOpdT+dyeIQ== + randombytes@^2.1.0: version "2.1.0" resolved "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz" @@ -13433,6 +14186,16 @@ rc-virtual-list@^3.11.1, rc-virtual-list@^3.5.1, rc-virtual-list@^3.5.2: rc-resize-observer "^1.0.0" rc-util "^5.36.0" +rc@1.2.8, rc@^1.2.8: + version "1.2.8" + resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed" + integrity sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw== + dependencies: + deep-extend "^0.6.0" + ini "~1.3.0" + minimist "^1.2.0" + strip-json-comments "~2.0.1" + react-addons-update@15.6.3: version "15.6.3" resolved "https://registry.yarnpkg.com/react-addons-update/-/react-addons-update-15.6.3.tgz#c449c309154024d04087b206d0400e020547b313" @@ -13440,6 +14203,19 @@ react-addons-update@15.6.3: dependencies: object-assign "^4.1.0" +react-beautiful-dnd@13.1.1: + version "13.1.1" + resolved "https://registry.yarnpkg.com/react-beautiful-dnd/-/react-beautiful-dnd-13.1.1.tgz#b0f3087a5840920abf8bb2325f1ffa46d8c4d0a2" + integrity sha512-0Lvs4tq2VcrEjEgDXHjT98r+63drkKEgqyxdA7qD3mvKwga6a5SscbdLPO2IExotU1jW8L0Ksdl0Cj2AF67nPQ== + dependencies: + "@babel/runtime" "^7.9.2" + css-box-model "^1.2.0" + memoize-one "^5.1.1" + raf-schd "^4.0.2" + react-redux "^7.2.0" + redux "^4.0.4" + use-memo-one "^1.1.1" + react-dnd-html5-backend@16.0.1: version "16.0.1" resolved "https://registry.yarnpkg.com/react-dnd-html5-backend/-/react-dnd-html5-backend-16.0.1.tgz#87faef15845d512a23b3c08d29ecfd34871688b6" @@ -13622,7 +14398,7 @@ react-query@3.39.3: broadcast-channel "^3.4.1" match-sorter "^6.0.2" -react-redux@^7.2.2: +react-redux@^7.2.0, react-redux@^7.2.2: version "7.2.9" resolved "https://registry.npmjs.org/react-redux/-/react-redux-7.2.9.tgz" integrity sha512-Gx4L3uM182jEEayZfRbI/G11ZpYdNAnBs70lFVMNdHJI76XYtR+7m0MN+eAs7UHBPhWXcnFPaS+9owSCJQHNpQ== @@ -13726,6 +14502,13 @@ react@18.2.0: dependencies: loose-envify "^1.1.0" +read-cache@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/read-cache/-/read-cache-1.0.0.tgz#e664ef31161166c9751cdbe8dbcf86b5fb58f774" + integrity sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA== + dependencies: + pify "^2.3.0" + read-pkg-up@^7.0.1: version "7.0.1" resolved "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-7.0.1.tgz" @@ -13735,6 +14518,15 @@ read-pkg-up@^7.0.1: read-pkg "^5.2.0" type-fest "^0.8.1" +read-pkg@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-3.0.0.tgz#9cbc686978fee65d16c00e2b19c237fcf6e38389" + integrity sha512-BLq/cCO9two+lBgiTYNqD6GdtK8s4NpaWrl6/rCO9w0TUS8oJl7cmToOZfRYllKTISY6nt1U7jQ53brmKqY6BA== + dependencies: + load-json-file "^4.0.0" + normalize-package-data "^2.3.2" + path-type "^3.0.0" + read-pkg@^5.2.0: version "5.2.0" resolved "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz" @@ -13801,7 +14593,7 @@ redux-thunk@^2.3.0: resolved "https://registry.npmjs.org/redux-thunk/-/redux-thunk-2.4.2.tgz" integrity sha512-+P3TjtnP0k/FEjcBL5FZpoovtvrTNT/UXd4/sluaSyrURlSlhLSzEdfsTBW7WsKB6yPvgd7q/iZPICFjW4o57Q== -redux@^4.0.0, redux@^4.0.5, redux@^4.2.0: +redux@^4.0.0, redux@^4.0.4, redux@^4.0.5, redux@^4.2.0: version "4.2.1" resolved "https://registry.yarnpkg.com/redux/-/redux-4.2.1.tgz#c08f4306826c49b5e9dc901dee0452ea8fce6197" integrity sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w== @@ -13894,6 +14686,20 @@ regexpu-core@^5.3.1: unicode-match-property-ecmascript "^2.0.0" unicode-match-property-value-ecmascript "^2.1.0" +registry-auth-token@^4.0.0: + version "4.2.2" + resolved "https://registry.yarnpkg.com/registry-auth-token/-/registry-auth-token-4.2.2.tgz#f02d49c3668884612ca031419491a13539e21fac" + integrity sha512-PC5ZysNb42zpFME6D/XlIgtNGdTl8bBOCw90xQLVMpzuuubJKYDWFAEuUNc+Cn8Z8724tg2SDhDRrkVEsqfDMg== + dependencies: + rc "1.2.8" + +registry-url@^5.0.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/registry-url/-/registry-url-5.1.0.tgz#e98334b50d5434b81136b44ec638d9c2009c5009" + integrity sha512-8acYXXTI0AkQv6RAOjE3vOaIXZkT9wo4LOFbBKYQEEnnMNBpKqdUrI6S4NT0KPIo/WVvJ5tE/X5LF/TQUf0ekw== + dependencies: + rc "^1.2.8" + regjsparser@^0.9.1: version "0.9.1" resolved "https://registry.npmjs.org/regjsparser/-/regjsparser-0.9.1.tgz" @@ -14125,6 +14931,14 @@ resolve-dir@^0.1.0: expand-tilde "^1.2.2" global-modules "^0.2.3" +resolve-dir@^1.0.0, resolve-dir@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/resolve-dir/-/resolve-dir-1.0.1.tgz#79a40644c362be82f26effe739c9bb5382046f43" + integrity sha512-R7uiTjECzvOsWSfdM0QKFNBVFcK27aHOUwdvK53BcW8zqnGdYp0Fbj82cy54+2A4P2tFM22J5kRfe1R+lM/1yg== + dependencies: + expand-tilde "^2.0.0" + global-modules "^1.0.0" + resolve-from@5.0.0, resolve-from@^5.0.0: version "5.0.0" resolved "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz" @@ -14159,6 +14973,15 @@ resolve.exports@^1.1.0: resolved "https://registry.npmjs.org/resolve.exports/-/resolve.exports-1.1.1.tgz" integrity sha512-/NtpHNDN7jWhAaQ9BvBUYZ6YTXsRBgfqWFWP7BZBaoMJO/I3G5OFzvTuWNlZC3aPjins1F+TNrLKsGbH4rfsRQ== +resolve@^1.1.7, resolve@~1.22.1: + version "1.22.8" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.8.tgz#b6c87a9f2aa06dfab52e3d70ac8cde321fa5a48d" + integrity sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw== + dependencies: + is-core-module "^2.13.0" + path-parse "^1.0.7" + supports-preserve-symlinks-flag "^1.0.0" + resolve@^1.10.0, resolve@^1.10.1, resolve@^1.12.0, resolve@^1.14.2, resolve@^1.19.0, resolve@^1.20.0, resolve@^1.22.1, resolve@^1.9.0: version "1.22.2" resolved "https://registry.npmjs.org/resolve/-/resolve-1.22.2.tgz" @@ -14185,14 +15008,12 @@ resolve@~1.19.0: is-core-module "^2.1.0" path-parse "^1.0.6" -resolve@~1.22.1: - version "1.22.8" - resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.8.tgz#b6c87a9f2aa06dfab52e3d70ac8cde321fa5a48d" - integrity sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw== +responselike@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/responselike/-/responselike-1.0.2.tgz#918720ef3b631c5642be068f15ade5a46f4ba1e7" + integrity sha512-/Fpe5guzJk1gPqdJLJR5u7eG/gNY4nImjbRDaVWVMRhne55TCmj2i9Q+54PBRfatRC8v/rIiv9BN0pMd9OV5EQ== dependencies: - is-core-module "^2.13.0" - path-parse "^1.0.7" - supports-preserve-symlinks-flag "^1.0.0" + lowercase-keys "^1.0.0" restore-cursor@^3.1.0: version "3.1.0" @@ -14416,7 +15237,14 @@ selfsigned@^2.1.1: dependencies: node-forge "^1" -"semver@2 || 3 || 4 || 5", semver@7.3.7, semver@7.5.4, semver@7.x, semver@^5.6.0, semver@^6.0.0, semver@^6.1.0, semver@^6.1.1, semver@^6.1.2, semver@^6.3.0, semver@^6.3.1, semver@^7.2.1, semver@^7.3.2, semver@^7.3.4, semver@^7.3.5, semver@^7.3.7, semver@^7.5.4, semver@~7.5.4: +semver-diff@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/semver-diff/-/semver-diff-3.1.1.tgz#05f77ce59f325e00e2706afd67bb506ddb1ca32b" + integrity sha512-GX0Ix/CJcHyB8c4ykpHGIAvLyOwOobtM/8d+TQkAd81/bEjgPHrfba41Vpesr7jX/t8Uh+R3EX9eAS5be+jQYg== + dependencies: + semver "^6.3.0" + +"semver@2 || 3 || 4 || 5", semver@7.3.7, semver@7.5.4, semver@7.x, semver@^5.5.0, semver@^5.6.0, semver@^6.0.0, semver@^6.1.0, semver@^6.1.1, semver@^6.1.2, semver@^6.2.0, semver@^6.3.0, semver@^6.3.1, semver@^7.2.1, semver@^7.3.2, semver@^7.3.4, semver@^7.3.5, semver@^7.3.7, semver@^7.5.4, semver@~7.5.4: version "7.5.4" resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.4.tgz#483986ec4ed38e1c6c48c34894a9182dbff68a6e" integrity sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA== @@ -14530,6 +15358,13 @@ shallowequal@^1.1.0: resolved "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz" integrity sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ== +shebang-command@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea" + integrity sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg== + dependencies: + shebang-regex "^1.0.0" + shebang-command@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz" @@ -14537,12 +15372,17 @@ shebang-command@^2.0.0: dependencies: shebang-regex "^3.0.0" +shebang-regex@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3" + integrity sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ== + shebang-regex@^3.0.0: version "3.0.0" resolved "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz" integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== -shell-quote@^1.7.3: +shell-quote@^1.6.1, shell-quote@^1.7.3: version "1.8.1" resolved "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.1.tgz" integrity sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA== @@ -14591,6 +15431,15 @@ sirv@^1.0.7: mrmime "^1.0.0" totalist "^1.0.0" +sirv@^2.0.2: + version "2.0.4" + resolved "https://registry.yarnpkg.com/sirv/-/sirv-2.0.4.tgz#5dd9a725c578e34e449f332703eb2a74e46a29b0" + integrity sha512-94Bdh3cC2PKrbgSOUqTiGPWVZeSiXfKOVZNJniWoqrWrRkB1CJzBU3NEbiTsPcYy1lDsANA/THzS+9WBiy5nfQ== + dependencies: + "@polka/url" "^1.0.0-next.24" + mrmime "^2.0.0" + totalist "^3.0.0" + sisteransi@^1.0.5: version "1.0.5" resolved "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz" @@ -14839,7 +15688,7 @@ statuses@2.0.1: resolved "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz" integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ== -"statuses@>= 1.4.0 < 2": +"statuses@>= 1.4.0 < 2", statuses@~1.5.0: version "1.5.0" resolved "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz" integrity sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA== @@ -14890,6 +15739,11 @@ string-convert@^0.2.0: resolved "https://registry.npmjs.org/string-convert/-/string-convert-0.2.1.tgz" integrity sha512-u/1tdPl4yQnPBjnVrmdLo9gtuLvELKsAoRapekWggdiQNvvvum+jYF329d84NAa660KQw7pB2n36KrIKVoXa3A== +string-hash@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/string-hash/-/string-hash-1.1.3.tgz#e8aafc0ac1855b4666929ed7dd1275df5d6c811b" + integrity sha512-kJUvRUFK49aub+a7T1nNE66EJbZBMnBgoC1UbCZ5n6bsZKBRga4KgBRTMn/pFkeCZSYtNeSyMxPDM0AXWELk2A== + string-length@^4.0.1: version "4.0.2" resolved "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz" @@ -14898,7 +15752,7 @@ string-length@^4.0.1: char-regex "^1.0.2" strip-ansi "^6.0.0" -string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: +string-width@^4.0.0, string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.2, string-width@^4.2.3: version "4.2.3" resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -14930,6 +15784,15 @@ string.prototype.matchall@^4.0.8: regexp.prototype.flags "^1.4.3" side-channel "^1.0.4" +string.prototype.padend@^3.0.0: + version "3.1.5" + resolved "https://registry.yarnpkg.com/string.prototype.padend/-/string.prototype.padend-3.1.5.tgz#311ef3a4e3c557dd999cdf88fbdde223f2ac0f95" + integrity sha512-DOB27b/2UTTD+4myKUFh+/fXWcu/UDyASIXfg+7VzoCNNGOfWvoyU/x5pvVHr++ztyt/oSYI1BcWBBG/hmlNjA== + dependencies: + call-bind "^1.0.2" + define-properties "^1.2.0" + es-abstract "^1.22.1" + string.prototype.trim@^1.2.7: version "1.2.7" resolved "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.7.tgz" @@ -15020,6 +15883,11 @@ strip-json-comments@^3.1.0, strip-json-comments@^3.1.1, strip-json-comments@~3.1 resolved "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz" integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== +strip-json-comments@~2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" + integrity sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ== + style-dictionary@3.8.0: version "3.8.0" resolved "https://registry.yarnpkg.com/style-dictionary/-/style-dictionary-3.8.0.tgz#7cb8d64360c53431f768d44def665f61e971a73e" @@ -15139,6 +16007,11 @@ supports-preserve-symlinks-flag@^1.0.0: resolved "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz" integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== +svg-parser@^2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/svg-parser/-/svg-parser-2.0.4.tgz#fdc2e29e13951736140b76cb122c8ee6630eb6b5" + integrity sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ== + svgo@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/svgo/-/svgo-3.0.2.tgz#5e99eeea42c68ee0dc46aa16da093838c262fe0a" @@ -15326,7 +16199,7 @@ timestamp-nano@^1.0.0: resolved "https://registry.npmjs.org/timestamp-nano/-/timestamp-nano-1.0.1.tgz" integrity sha512-4oGOVZWTu5sl89PtCDnhQBSt7/vL1zVEwAfxH1p49JhTosxzVQWYBYFRFZ8nJmo0G6f824iyP/44BFAwIoKvIA== -tiny-invariant@^1.0.2: +tiny-invariant@^1.0.2, tiny-invariant@^1.0.6: version "1.3.1" resolved "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.1.tgz" integrity sha512-AD5ih2NlSssTCwsMznbvwMZpJ1cbhkGd2uueNxzv2jDlEeZdU04JQfRnggJQ8DrcVBGjAsCKwFBbDlVNtEMlzw== @@ -15358,6 +16231,11 @@ to-fast-properties@^2.0.0: resolved "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz" integrity sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog== +to-readable-stream@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/to-readable-stream/-/to-readable-stream-1.0.0.tgz#ce0aa0c2f3df6adf852efb404a783e77c0475771" + integrity sha512-Iq25XBt6zD5npPhlLVXGFN3/gyR2/qODcKNNyTMd4vbm39HUaOiAM4PMq0eMVC/Tkxz+Zjdsc55g9yyz+Yq00Q== + to-regex-range@^5.0.1: version "5.0.1" resolved "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz" @@ -15380,6 +16258,11 @@ totalist@^1.0.0: resolved "https://registry.npmjs.org/totalist/-/totalist-1.1.0.tgz" integrity sha512-gduQwd1rOdDMGxFG1gEvhV88Oirdo2p+KjoYFU7k2g+i7n6AFFbDQ5kMPUsW0pNbfQsB/cwXvT1i4Bue0s9g5g== +totalist@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/totalist/-/totalist-3.0.1.tgz#ba3a3d600c915b1a97872348f79c127475f6acf8" + integrity sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ== + tough-cookie@^4.0.0: version "4.1.3" resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-4.1.3.tgz#97b9adb0728b42280aa3d814b6b999b2ff0318bf" @@ -15691,6 +16574,13 @@ unified@^10.0.0, unified@^10.1.2, unified@~10.1.1: trough "^2.0.0" vfile "^5.0.0" +unique-string@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/unique-string/-/unique-string-2.0.0.tgz#39c6451f81afb2749de2b233e3f7c5e8843bd89d" + integrity sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg== + dependencies: + crypto-random-string "^2.0.0" + unist-util-filter@^4.0.0: version "4.0.1" resolved "https://registry.yarnpkg.com/unist-util-filter/-/unist-util-filter-4.0.1.tgz#fd885dd48adaad345de5f5dc706ec4ff44a8d074" @@ -15794,6 +16684,34 @@ update-browserslist-db@^1.0.10, update-browserslist-db@^1.0.11: escalade "^3.1.1" picocolors "^1.0.0" +update-browserslist-db@^1.0.13: + version "1.0.13" + resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz#3c5e4f5c083661bd38ef64b6328c26ed6c8248c4" + integrity sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg== + dependencies: + escalade "^3.1.1" + picocolors "^1.0.0" + +update-notifier@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/update-notifier/-/update-notifier-5.1.0.tgz#4ab0d7c7f36a231dd7316cf7729313f0214d9ad9" + integrity sha512-ItnICHbeMh9GqUy31hFPrD1kcuZ3rpxDZbf4KUDavXwS0bW5m7SLbDQpGX3UYr072cbrF5hFUs3r5tUsPwjfHw== + dependencies: + boxen "^5.0.0" + chalk "^4.1.0" + configstore "^5.0.1" + has-yarn "^2.1.0" + import-lazy "^2.1.0" + is-ci "^2.0.0" + is-installed-globally "^0.4.0" + is-npm "^5.0.0" + is-yarn-global "^0.3.0" + latest-version "^5.1.0" + pupa "^2.1.1" + semver "^7.3.4" + semver-diff "^3.1.1" + xdg-basedir "^4.0.0" + uplot@1.6.24: version "1.6.24" resolved "https://registry.yarnpkg.com/uplot/-/uplot-1.6.24.tgz#dfa213fa7da92763261920ea972ed1a5f9f6af12" @@ -15825,6 +16743,13 @@ uri-js@^4.2.2: dependencies: punycode "^2.1.0" +url-parse-lax@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/url-parse-lax/-/url-parse-lax-3.0.0.tgz#16b5cafc07dbe3676c1b1999177823d6503acb0c" + integrity sha512-NjFKA0DidqPa5ciFcSrXnAltTtzz84ogy+NebPvfEgAck0+TNg4UJ4IN+fB7zRZfbgUf0syOo9MDxFkDSMuFaQ== + dependencies: + prepend-http "^2.0.0" + url-parse@^1.5.3: version "1.5.10" resolved "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz" @@ -15843,6 +16768,11 @@ use-isomorphic-layout-effect@^1.1.2: resolved "https://registry.npmjs.org/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.1.2.tgz" integrity sha512-49L8yCO3iGT/ZF9QttjwLF/ZD9Iwto5LnH5LmEdk/6cFmXddqi2ulF0edxTwjj+7mqvpVVGQWvbXZdn32wRSHA== +use-memo-one@^1.1.1: + version "1.1.3" + resolved "https://registry.yarnpkg.com/use-memo-one/-/use-memo-one-1.1.3.tgz#2fd2e43a2169eabc7496960ace8c79efef975e99" + integrity sha512-g66/K7ZQGYrI6dy8GLpVcMsBp4s17xNkYJVSMvTEevGy3nDxHOfE6z8BVE22+5G5x7t3+bhzrlTDB7ObrEE0cQ== + use-sync-external-store@^1.0.0: version "1.2.0" resolved "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz" @@ -16404,7 +17334,7 @@ which-typed-array@^1.1.9: has-tostringtag "^1.0.0" is-typed-array "^1.1.10" -which@^1.2.12: +which@^1.2.12, which@^1.2.14, which@^1.2.9: version "1.3.1" resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ== @@ -16418,6 +17348,13 @@ which@^2.0.1: dependencies: isexe "^2.0.0" +widest-line@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/widest-line/-/widest-line-3.1.0.tgz#8292333bbf66cb45ff0de1603b136b7ae1496eca" + integrity sha512-NsmoXalsWVDMGupxZ5R08ka9flZjjiLvHVAWYOKtiKM8ujtZWr9cRffak+uSE48+Ob8ObalXpwyeUiyDD6QFgg== + dependencies: + string-width "^4.0.0" + wildcard@^2.0.0: version "2.0.1" resolved "https://registry.npmjs.org/wildcard/-/wildcard-2.0.1.tgz" @@ -16484,6 +17421,16 @@ ws@^8.13.0: resolved "https://registry.npmjs.org/ws/-/ws-8.13.0.tgz" integrity sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA== +ws@^8.5.0: + version "8.16.0" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.16.0.tgz#d1cd774f36fbc07165066a60e40323eab6446fd4" + integrity sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ== + +xdg-basedir@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/xdg-basedir/-/xdg-basedir-4.0.0.tgz#4bc8d9984403696225ef83a1573cbbcb4e79db13" + integrity sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q== + xhr-request@^1.0.1: version "1.1.0" resolved "https://registry.npmjs.org/xhr-request/-/xhr-request-1.1.0.tgz" @@ -16583,6 +17530,11 @@ yaml@^1.10.0, yaml@^1.10.2: resolved "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz" integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg== +yaml@^2.3.4: + version "2.3.4" + resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.3.4.tgz#53fc1d514be80aabf386dc6001eb29bf3b7523b2" + integrity sha512-8aAvwVUSHpfEqTQ4w/KMlf3HcRdt50E5ODIQJBw1fQ5RL34xabzxtUlzTXVqc4rkZsPbvrXKWnABCD7kWSmocA== + yargs-parser@20.x, yargs-parser@^20.2.2, yargs-parser@^20.2.3: version "20.2.9" resolved "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz" diff --git a/go.mod b/go.mod index ec4d7506ff..4b24c57239 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.21.3 require ( github.com/ClickHouse/clickhouse-go/v2 v2.15.0 github.com/SigNoz/govaluate v0.0.0-20240203125216-988004ccc7fd - github.com/SigNoz/signoz-otel-collector v0.88.12 + github.com/SigNoz/signoz-otel-collector v0.88.13 github.com/SigNoz/zap_otlp/zap_otlp_encoder v0.0.0-20230822164844-1b861a431974 github.com/SigNoz/zap_otlp/zap_otlp_sync v0.0.0-20230822164844-1b861a431974 github.com/antonmedv/expr v1.15.3 diff --git a/go.sum b/go.sum index ae300b0f17..ced65a3169 100644 --- a/go.sum +++ b/go.sum @@ -94,14 +94,12 @@ github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migc github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= github.com/OneOfOne/xxhash v1.2.2 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= -github.com/SigNoz/govaluate v0.0.0-20220522085550-d19c08c206cb h1:bneLSKPf9YUSFmafKx32bynV6QrzViL/s+ZDvQxH1E4= -github.com/SigNoz/govaluate v0.0.0-20220522085550-d19c08c206cb/go.mod h1:JznGDNg9x1cujDKa22RaQOimOvvEfy3nxzDGd8XDgmA= github.com/SigNoz/govaluate v0.0.0-20240203125216-988004ccc7fd h1:Bk43AsDYe0fhkbj57eGXx8H3ZJ4zhmQXBnrW523ktj8= github.com/SigNoz/govaluate v0.0.0-20240203125216-988004ccc7fd/go.mod h1:nxRcH/OEdM8QxzH37xkGzomr1O0JpYBRS6pwjsWW6Pc= github.com/SigNoz/prometheus v1.9.78 h1:bB3yuDrRzi/Mv00kWayR9DZbyjTuGfendSqISyDcXiY= github.com/SigNoz/prometheus v1.9.78/go.mod h1:MffmFu2qFILQrOHehx3D0XjYtaZMVfI+Ppeiv98x4Ww= -github.com/SigNoz/signoz-otel-collector v0.88.12 h1:UwkVi1o2NY9gRgCLBtWVKr+UDxb4FaTs63Sb20qgf8w= -github.com/SigNoz/signoz-otel-collector v0.88.12/go.mod h1:RH9OEjni6tkh9RgN/meSPxv3kykjcFscqMwJgbUAXmo= +github.com/SigNoz/signoz-otel-collector v0.88.13 h1:VAVXokL28Hqxo6xyzlCrFS1na/bd1cgqFAVOe1lJjUE= +github.com/SigNoz/signoz-otel-collector v0.88.13/go.mod h1:RH9OEjni6tkh9RgN/meSPxv3kykjcFscqMwJgbUAXmo= github.com/SigNoz/zap_otlp v0.1.0 h1:T7rRcFN87GavY8lDGZj0Z3Xv6OhJA6Pj3I9dNPmqvRc= github.com/SigNoz/zap_otlp v0.1.0/go.mod h1:lcHvbDbRgvDnPxo9lDlaL1JK2PyOyouP/C3ynnYIvyo= github.com/SigNoz/zap_otlp/zap_otlp_encoder v0.0.0-20230822164844-1b861a431974 h1:PKVgdf83Yw+lZJbFtNGBgqXiXNf3+kOXW2qZ7Ms7OaY= diff --git a/pkg/query-service/app/clickhouseReader/reader.go b/pkg/query-service/app/clickhouseReader/reader.go index 4eef78acf0..1834aa0ff0 100644 --- a/pkg/query-service/app/clickhouseReader/reader.go +++ b/pkg/query-service/app/clickhouseReader/reader.go @@ -3273,17 +3273,27 @@ func (r *ClickHouseReader) GetTotalSpans(ctx context.Context) (uint64, error) { return totalSpans, nil } -func (r *ClickHouseReader) GetSpansInLastHeartBeatInterval(ctx context.Context) (uint64, error) { +func (r *ClickHouseReader) GetSpansInLastHeartBeatInterval(ctx context.Context, interval time.Duration) (uint64, error) { var spansInLastHeartBeatInterval uint64 - queryStr := fmt.Sprintf("SELECT count() from %s.%s where timestamp > toUnixTimestamp(now()-toIntervalMinute(%d));", signozTraceDBName, signozSpansTable, 30) + queryStr := fmt.Sprintf("SELECT count() from %s.%s where timestamp > toUnixTimestamp(now()-toIntervalMinute(%d));", signozTraceDBName, signozSpansTable, int(interval.Minutes())) r.db.QueryRow(ctx, queryStr).Scan(&spansInLastHeartBeatInterval) return spansInLastHeartBeatInterval, nil } +func (r *ClickHouseReader) GetTotalLogs(ctx context.Context) (uint64, error) { + + var totalLogs uint64 + + queryStr := fmt.Sprintf("SELECT count() from %s.%s;", r.logsDB, r.logsTable) + r.db.QueryRow(ctx, queryStr).Scan(&totalLogs) + + return totalLogs, nil +} + func (r *ClickHouseReader) FetchTemporality(ctx context.Context, metricNames []string) (map[string]map[v3.Temporality]bool, error) { metricNameToTemporality := make(map[string]map[v3.Temporality]bool) @@ -3312,9 +3322,7 @@ func (r *ClickHouseReader) FetchTemporality(ctx context.Context, metricNames []s func (r *ClickHouseReader) GetTimeSeriesInfo(ctx context.Context) (map[string]interface{}, error) { - queryStr := fmt.Sprintf("SELECT count() as count from %s.%s group by metric_name order by count desc;", signozMetricDBName, signozTSTableName) - - // r.db.Select(ctx, &tsByMetricName, queryStr) + queryStr := fmt.Sprintf("SELECT count() as count from %s.%s where metric_name not like 'signoz_%%' group by metric_name order by count desc;", signozMetricDBName, signozTSTableName) rows, _ := r.db.Query(ctx, queryStr) @@ -3343,11 +3351,21 @@ func (r *ClickHouseReader) GetTimeSeriesInfo(ctx context.Context) (map[string]in return timeSeriesData, nil } -func (r *ClickHouseReader) GetSamplesInfoInLastHeartBeatInterval(ctx context.Context) (uint64, error) { +func (r *ClickHouseReader) GetSamplesInfoInLastHeartBeatInterval(ctx context.Context, interval time.Duration) (uint64, error) { var totalSamples uint64 - queryStr := fmt.Sprintf("select count() from %s.%s where timestamp_ms > toUnixTimestamp(now()-toIntervalMinute(%d))*1000;", signozMetricDBName, signozSampleTableName, 30) + queryStr := fmt.Sprintf("select count() from %s.%s where metric_name not like 'signoz_%%' and timestamp_ms > toUnixTimestamp(now()-toIntervalMinute(%d))*1000;", signozMetricDBName, signozSampleTableName, int(interval.Minutes())) + + r.db.QueryRow(ctx, queryStr).Scan(&totalSamples) + + return totalSamples, nil +} + +func (r *ClickHouseReader) GetTotalSamples(ctx context.Context) (uint64, error) { + var totalSamples uint64 + + queryStr := fmt.Sprintf("select count() from %s.%s where metric_name not like 'signoz_%%';", signozMetricDBName, signozSampleTableName) r.db.QueryRow(ctx, queryStr).Scan(&totalSamples) @@ -3367,23 +3385,23 @@ func (r *ClickHouseReader) GetDistributedInfoInLastHeartBeatInterval(ctx context return nil, nil } -func (r *ClickHouseReader) GetLogsInfoInLastHeartBeatInterval(ctx context.Context) (uint64, error) { +func (r *ClickHouseReader) GetLogsInfoInLastHeartBeatInterval(ctx context.Context, interval time.Duration) (uint64, error) { var totalLogLines uint64 - queryStr := fmt.Sprintf("select count() from %s.%s where timestamp > toUnixTimestamp(now()-toIntervalMinute(%d))*1000000000;", r.logsDB, r.logsTable, 30) + queryStr := fmt.Sprintf("select count() from %s.%s where timestamp > toUnixTimestamp(now()-toIntervalMinute(%d))*1000000000;", r.logsDB, r.logsTable, int(interval.Minutes())) err := r.db.QueryRow(ctx, queryStr).Scan(&totalLogLines) return totalLogLines, err } -func (r *ClickHouseReader) GetTagsInfoInLastHeartBeatInterval(ctx context.Context) (*model.TagsInfo, error) { +func (r *ClickHouseReader) GetTagsInfoInLastHeartBeatInterval(ctx context.Context, interval time.Duration) (*model.TagsInfo, error) { queryStr := fmt.Sprintf(`select serviceName, stringTagMap['deployment.environment'] as env, stringTagMap['telemetry.sdk.language'] as language from %s.%s where timestamp > toUnixTimestamp(now()-toIntervalMinute(%d)) - group by serviceName, env, language;`, r.TraceDB, r.indexTable, 1) + group by serviceName, env, language;`, r.TraceDB, r.indexTable, int(interval.Minutes())) tagTelemetryDataList := []model.TagTelemetryData{} err := r.db.Select(ctx, &tagTelemetryDataList, queryStr) @@ -3396,6 +3414,7 @@ func (r *ClickHouseReader) GetTagsInfoInLastHeartBeatInterval(ctx context.Contex tagsInfo := model.TagsInfo{ Languages: make(map[string]interface{}), + Services: make(map[string]interface{}), } for _, tagTelemetryData := range tagTelemetryDataList { @@ -3409,6 +3428,9 @@ func (r *ClickHouseReader) GetTagsInfoInLastHeartBeatInterval(ctx context.Contex if len(tagTelemetryData.Language) != 0 { tagsInfo.Languages[tagTelemetryData.Language] = struct{}{} } + if len(tagTelemetryData.ServiceName) != 0 { + tagsInfo.Services[tagTelemetryData.ServiceName] = struct{}{} + } } @@ -3665,7 +3687,7 @@ func (r *ClickHouseReader) UpdateLogField(ctx context.Context, field *model.Upda return &model.ApiError{Err: err, Typ: model.ErrorInternal} } - for _, table := range []string{r.logsLocalTable, r.logsTable} { + for _, table := range []string{r.logsTable, r.logsLocalTable} { // drop materialized column from logs table query := "ALTER TABLE %s.%s ON CLUSTER %s DROP COLUMN IF EXISTS %s " err := r.db.Exec(ctx, fmt.Sprintf(query, diff --git a/pkg/query-service/app/metrics/v4/delta/time_series_test.go b/pkg/query-service/app/metrics/v4/delta/time_series_test.go index 0af2c91154..29c2340ec4 100644 --- a/pkg/query-service/app/metrics/v4/delta/time_series_test.go +++ b/pkg/query-service/app/metrics/v4/delta/time_series_test.go @@ -212,6 +212,40 @@ func TestPrepareTimeseriesQuery(t *testing.T) { end: 1701796780000, expectedQueryContains: "SELECT service_name, toStartOfInterval(toDateTime(intDiv(unix_milli, 1000)), INTERVAL 60 SECOND) as ts, sum(value)/60 as value FROM signoz_metrics.distributed_samples_v4 INNER JOIN (SELECT DISTINCT JSONExtractString(labels, 'service_name') as service_name, fingerprint FROM signoz_metrics.time_series_v4 WHERE metric_name = 'http_requests' AND temporality = 'Delta' AND unix_milli >= 1701792000000 AND unix_milli < 1701796780000 AND like(JSONExtractString(labels, 'service_name'), '%payment_service%')) as filtered_time_series USING fingerprint WHERE metric_name = 'http_requests' AND unix_milli >= 1701794980000 AND unix_milli < 1701796780000 GROUP BY GROUPING SETS ( (service_name, ts), (service_name) ) ORDER BY service_name ASC, ts ASC", }, + { + name: "test time aggregation = rate, space aggregation percentile99, type = ExponentialHistogram", + builderQuery: &v3.BuilderQuery{ + QueryName: "A", + StepInterval: 60, + DataSource: v3.DataSourceMetrics, + AggregateAttribute: v3.AttributeKey{ + Key: "signoz_latency", + DataType: v3.AttributeKeyDataTypeFloat64, + Type: v3.AttributeKeyType(v3.MetricTypeExponentialHistogram), + IsColumn: true, + IsJSON: false, + }, + Temporality: v3.Delta, + Filters: &v3.FilterSet{ + Operator: "AND", + Items: []v3.FilterItem{}, + }, + GroupBy: []v3.AttributeKey{ + { + Key: "service_name", + DataType: v3.AttributeKeyDataTypeString, + Type: v3.AttributeKeyTypeTag, + }, + }, + Expression: "A", + Disabled: false, + TimeAggregation: v3.TimeAggregationRate, + SpaceAggregation: v3.SpaceAggregationPercentile99, + }, + start: 1701794980000, + end: 1701796780000, + expectedQueryContains: "SELECT service_name, toStartOfInterval(toDateTime(intDiv(unix_milli, 1000)), INTERVAL 60 SECOND) as ts, quantilesDDMerge(0.01, 0.990000)(sketch)[1] as value FROM signoz_metrics.distributed_exp_hist INNER JOIN (SELECT DISTINCT JSONExtractString(labels, 'service_name') as service_name, fingerprint FROM signoz_metrics.time_series_v4 WHERE metric_name = 'signoz_latency' AND temporality = 'Delta' AND unix_milli >= 1701792000000 AND unix_milli < 1701796780000) as filtered_time_series USING fingerprint WHERE metric_name = 'signoz_latency' AND unix_milli >= 1701794980000 AND unix_milli < 1701796780000 GROUP BY GROUPING SETS ( (service_name, ts), (service_name) ) ORDER BY service_name ASC, ts ASC", + }, } for _, testCase := range testCases { diff --git a/pkg/query-service/app/metrics/v4/delta/timeseries.go b/pkg/query-service/app/metrics/v4/delta/timeseries.go index 03781dfcd1..365b09c56d 100644 --- a/pkg/query-service/app/metrics/v4/delta/timeseries.go +++ b/pkg/query-service/app/metrics/v4/delta/timeseries.go @@ -9,6 +9,11 @@ import ( "go.signoz.io/signoz/pkg/query-service/utils" ) +// TODO(srikanthccv): support multiple quantiles; see https://github.com/SigNoz/signoz/issues/4016#issuecomment-1838583305 +var ( + sketchFmt = "quantilesDDMerge(0.01, %f)(sketch)[1]" +) + // prepareTimeAggregationSubQuery builds the sub-query to be used for temporal aggregation func prepareTimeAggregationSubQuery(start, end, step int64, mq *v3.BuilderQuery) (string, error) { @@ -84,12 +89,16 @@ func prepareQueryOptimized(start, end, step int64, mq *v3.BuilderQuery) (string, samplesTableFilter := fmt.Sprintf("metric_name = %s AND unix_milli >= %d AND unix_milli < %d", utils.ClickHouseFormattedValue(mq.AggregateAttribute.Key), start, end) + var tableName string = constants.SIGNOZ_SAMPLES_V4_TABLENAME + if mq.AggregateAttribute.Type == v3.AttributeKeyType(v3.MetricTypeExponentialHistogram) { + tableName = "distributed_exp_hist" + } // Select the aggregate value for interval queryTmpl := "SELECT %s" + " toStartOfInterval(toDateTime(intDiv(unix_milli, 1000)), INTERVAL %d SECOND) as ts," + " %s as value" + - " FROM " + constants.SIGNOZ_METRIC_DBNAME + "." + constants.SIGNOZ_SAMPLES_V4_TABLENAME + + " FROM " + constants.SIGNOZ_METRIC_DBNAME + "." + tableName + " INNER JOIN" + " (%s) as filtered_time_series" + " USING fingerprint" + @@ -110,6 +119,13 @@ func prepareQueryOptimized(start, end, step int64, mq *v3.BuilderQuery) (string, case v3.SpaceAggregationMax: op := "max(value)" query = fmt.Sprintf(queryTmpl, selectLabels, step, op, timeSeriesSubQuery, groupBy, orderBy) + case v3.SpaceAggregationPercentile50, + v3.SpaceAggregationPercentile75, + v3.SpaceAggregationPercentile90, + v3.SpaceAggregationPercentile95, + v3.SpaceAggregationPercentile99: + op := fmt.Sprintf(sketchFmt, v3.GetPercentileFromOperator(mq.SpaceAggregation)) + query = fmt.Sprintf(queryTmpl, selectLabels, step, op, timeSeriesSubQuery, groupBy, orderBy) } return query, nil } @@ -178,6 +194,9 @@ func PrepareMetricQueryDeltaTimeSeries(start, end, step int64, mq *v3.BuilderQue // 4. time aggregation = max and space aggregation = max // - max of maxs is same as max of all values // +// 5. special case exphist, there is no need for per series/fingerprint aggregation +// we can directly use the quantilesDDMerge function +// // all of this is true only for delta metrics func canShortCircuit(mq *v3.BuilderQuery) bool { if (mq.TimeAggregation == v3.TimeAggregationRate || mq.TimeAggregation == v3.TimeAggregationIncrease) && mq.SpaceAggregation == v3.SpaceAggregationSum { @@ -192,5 +211,8 @@ func canShortCircuit(mq *v3.BuilderQuery) bool { if mq.TimeAggregation == v3.TimeAggregationMax && mq.SpaceAggregation == v3.SpaceAggregationMax { return true } + if mq.AggregateAttribute.Type == v3.AttributeKeyType(v3.MetricTypeExponentialHistogram) && v3.IsPercentileOperator(mq.SpaceAggregation) { + return true + } return false } diff --git a/pkg/query-service/app/metrics/v4/helpers/clauses.go b/pkg/query-service/app/metrics/v4/helpers/clauses.go index 8714df51da..06f4b13cea 100644 --- a/pkg/query-service/app/metrics/v4/helpers/clauses.go +++ b/pkg/query-service/app/metrics/v4/helpers/clauses.go @@ -37,17 +37,6 @@ func GroupByAttributeKeyTags(tags ...v3.AttributeKey) string { return strings.Join(groupTags, ", ") } -func GroupByAttributeKeyTagsWithoutLe(tags ...v3.AttributeKey) string { - groupTags := []string{} - for _, tag := range tags { - if tag.Key != "le" { - groupTags = append(groupTags, tag.Key) - } - } - groupTags = append(groupTags, "ts") - return strings.Join(groupTags, ", ") -} - // OrderByAttributeKeyTags returns a string of comma separated tags for order by clause // if the order is not specified, it defaults to ASC func OrderByAttributeKeyTags(items []v3.OrderBy, tags []v3.AttributeKey) string { @@ -71,29 +60,6 @@ func OrderByAttributeKeyTags(items []v3.OrderBy, tags []v3.AttributeKey) string return strings.Join(orderBy, ", ") } -func OrderByAttributeKeyTagsWithoutLe(items []v3.OrderBy, tags []v3.AttributeKey) string { - var orderBy []string - for _, tag := range tags { - if tag.Key != "le" { - found := false - for _, item := range items { - if item.ColumnName == tag.Key { - found = true - orderBy = append(orderBy, fmt.Sprintf("%s %s", item.ColumnName, item.Order)) - break - } - } - if !found { - orderBy = append(orderBy, fmt.Sprintf("%s ASC", tag.Key)) - } - } - } - - orderBy = append(orderBy, "ts ASC") - - return strings.Join(orderBy, ", ") -} - func SelectLabelsAny(tags []v3.AttributeKey) string { var selectLabelsAny []string for _, tag := range tags { diff --git a/pkg/query-service/interfaces/interface.go b/pkg/query-service/interfaces/interface.go index e15b1db67e..9d0d65c39c 100644 --- a/pkg/query-service/interfaces/interface.go +++ b/pkg/query-service/interfaces/interface.go @@ -2,6 +2,7 @@ package interfaces import ( "context" + "time" "github.com/ClickHouse/clickhouse-go/v2" "github.com/prometheus/prometheus/promql" @@ -74,11 +75,13 @@ type Reader interface { GetDashboardsInfo(ctx context.Context) (*model.DashboardsInfo, error) GetAlertsInfo(ctx context.Context) (*model.AlertsInfo, error) GetTotalSpans(ctx context.Context) (uint64, error) - GetSpansInLastHeartBeatInterval(ctx context.Context) (uint64, error) + GetTotalLogs(ctx context.Context) (uint64, error) + GetTotalSamples(ctx context.Context) (uint64, error) + GetSpansInLastHeartBeatInterval(ctx context.Context, interval time.Duration) (uint64, error) GetTimeSeriesInfo(ctx context.Context) (map[string]interface{}, error) - GetSamplesInfoInLastHeartBeatInterval(ctx context.Context) (uint64, error) - GetLogsInfoInLastHeartBeatInterval(ctx context.Context) (uint64, error) - GetTagsInfoInLastHeartBeatInterval(ctx context.Context) (*model.TagsInfo, error) + GetSamplesInfoInLastHeartBeatInterval(ctx context.Context, interval time.Duration) (uint64, error) + GetLogsInfoInLastHeartBeatInterval(ctx context.Context, interval time.Duration) (uint64, error) + GetTagsInfoInLastHeartBeatInterval(ctx context.Context, interval time.Duration) (*model.TagsInfo, error) GetDistributedInfoInLastHeartBeatInterval(ctx context.Context) (map[string]interface{}, error) // Logs GetLogFields(ctx context.Context) (*model.GetFieldsResponse, *model.ApiError) diff --git a/pkg/query-service/model/response.go b/pkg/query-service/model/response.go index 6d5e65d732..ae99473720 100644 --- a/pkg/query-service/model/response.go +++ b/pkg/query-service/model/response.go @@ -613,6 +613,7 @@ type DashboardVar struct { type TagsInfo struct { Languages map[string]interface{} `json:"languages"` Env string `json:"env"` + Services map[string]interface{} `json:"services"` } type AlertsInfo struct { diff --git a/pkg/query-service/model/v3/v3.go b/pkg/query-service/model/v3/v3.go index 010b0b41c1..c01660d6e7 100644 --- a/pkg/query-service/model/v3/v3.go +++ b/pkg/query-service/model/v3/v3.go @@ -489,6 +489,17 @@ func (t TimeAggregation) IsRateOperator() bool { } } +type MetricType string + +const ( + MetricTypeUnspecified MetricType = "" + MetricTypeSum MetricType = "Sum" + MetricTypeGauge MetricType = "Gauge" + MetricTypeHistogram MetricType = "Histogram" + MetricTypeSummary MetricType = "Summary" + MetricTypeExponentialHistogram MetricType = "ExponentialHistogram" +) + type SpaceAggregation string const ( diff --git a/pkg/query-service/rules/alerting.go b/pkg/query-service/rules/alerting.go index ad82470e83..623d5dea21 100644 --- a/pkg/query-service/rules/alerting.go +++ b/pkg/query-service/rules/alerting.go @@ -225,7 +225,7 @@ func prepareRuleGeneratorURL(ruleId string, source string) string { } // check if source is a valid url - _, err := url.Parse(source) + parsedSource, err := url.Parse(source) if err != nil { return "" } @@ -239,5 +239,12 @@ func prepareRuleGeneratorURL(ruleId string, source string) string { return ruleURL } - return source + // The source contains the encoded query, start and end time + // and other parameters. We don't want to include them in the generator URL + // mainly to keep the URL short and lower the alert body contents + // The generator URL with /alerts/edit?ruleId= is enough + if parsedSource.Port() != "" { + return fmt.Sprintf("%s://%s:%s/alerts/edit?ruleId=%s", parsedSource.Scheme, parsedSource.Hostname(), parsedSource.Port(), ruleId) + } + return fmt.Sprintf("%s://%s/alerts/edit?ruleId=%s", parsedSource.Scheme, parsedSource.Hostname(), ruleId) } diff --git a/pkg/query-service/rules/resultTypes.go b/pkg/query-service/rules/resultTypes.go index de2095eea9..78474526bd 100644 --- a/pkg/query-service/rules/resultTypes.go +++ b/pkg/query-service/rules/resultTypes.go @@ -16,6 +16,10 @@ type Sample struct { Point Metric labels.Labels + + // Label keys as-is from the result query. + // The original labels are used to prepare the related{logs, traces} link in alert notification + MetricOrig labels.Labels } func (s Sample) String() string { diff --git a/pkg/query-service/rules/thresholdRule.go b/pkg/query-service/rules/thresholdRule.go index 3c6cf2537b..9687038a40 100644 --- a/pkg/query-service/rules/thresholdRule.go +++ b/pkg/query-service/rules/thresholdRule.go @@ -437,7 +437,13 @@ func (r *ThresholdRule) runChQuery(ctx context.Context, db clickhouse.Conn, quer } sample := Sample{} + // Why do we maintain two labels sets? Alertmanager requires + // label keys to follow the model https://prometheus.io/docs/concepts/data_model/#metric-names-and-labels + // However, our traces and logs explorers support label keys with dot and other namespace characters + // Using normalized label keys results in invalid filter criteria. + // The original labels are used to prepare the related{logs, traces} link in alert notification lbls := labels.NewBuilder(labels.Labels{}) + lblsOrig := labels.NewBuilder(labels.Labels{}) for i, v := range vars { @@ -446,6 +452,7 @@ func (r *ThresholdRule) runChQuery(ctx context.Context, db clickhouse.Conn, quer switch v := v.(type) { case *string: lbls.Set(colName, *v) + lblsOrig.Set(columnNames[i], *v) case *time.Time: timval := *v @@ -453,6 +460,7 @@ func (r *ThresholdRule) runChQuery(ctx context.Context, db clickhouse.Conn, quer sample.Point.T = timval.Unix() } else { lbls.Set(colName, timval.Format("2006-01-02 15:04:05")) + lblsOrig.Set(columnNames[i], timval.Format("2006-01-02 15:04:05")) } case *float64: @@ -460,6 +468,7 @@ func (r *ThresholdRule) runChQuery(ctx context.Context, db clickhouse.Conn, quer sample.Point.V = *v } else { lbls.Set(colName, fmt.Sprintf("%f", *v)) + lblsOrig.Set(columnNames[i], fmt.Sprintf("%f", *v)) } case **float64: // ch seems to return this type when column is derived from @@ -470,6 +479,7 @@ func (r *ThresholdRule) runChQuery(ctx context.Context, db clickhouse.Conn, quer sample.Point.V = *floatVal } else { lbls.Set(colName, fmt.Sprintf("%f", *floatVal)) + lblsOrig.Set(columnNames[i], fmt.Sprintf("%f", *floatVal)) } } case *float32: @@ -478,18 +488,21 @@ func (r *ThresholdRule) runChQuery(ctx context.Context, db clickhouse.Conn, quer sample.Point.V = float64(float32Val) } else { lbls.Set(colName, fmt.Sprintf("%f", float32Val)) + lblsOrig.Set(columnNames[i], fmt.Sprintf("%f", float32Val)) } case *uint8, *uint64, *uint16, *uint32: if _, ok := constants.ReservedColumnTargetAliases[colName]; ok { sample.Point.V = float64(reflect.ValueOf(v).Elem().Uint()) } else { lbls.Set(colName, fmt.Sprintf("%v", reflect.ValueOf(v).Elem().Uint())) + lblsOrig.Set(columnNames[i], fmt.Sprintf("%v", reflect.ValueOf(v).Elem().Uint())) } case *int8, *int16, *int32, *int64: if _, ok := constants.ReservedColumnTargetAliases[colName]; ok { sample.Point.V = float64(reflect.ValueOf(v).Elem().Int()) } else { lbls.Set(colName, fmt.Sprintf("%v", reflect.ValueOf(v).Elem().Int())) + lblsOrig.Set(columnNames[i], fmt.Sprintf("%v", reflect.ValueOf(v).Elem().Int())) } default: zap.S().Errorf("ruleId:", r.ID(), "\t error: invalid var found in query result", v, columnNames[i]) @@ -503,6 +516,7 @@ func (r *ThresholdRule) runChQuery(ctx context.Context, db clickhouse.Conn, quer // capture lables in result sample.Metric = lbls.Labels() + sample.MetricOrig = lblsOrig.Labels() labelHash := lbls.Labels().Hash() @@ -750,7 +764,7 @@ func (r *ThresholdRule) prepareLinksToLogs(ts time.Time, lbls labels.Labels) str } data, _ := json.Marshal(urlData) - compositeQuery := url.QueryEscape(url.QueryEscape(string(data))) + compositeQuery := url.QueryEscape(string(data)) optionsData, _ := json.Marshal(options) urlEncodedOptions := url.QueryEscape(string(optionsData)) @@ -813,8 +827,7 @@ func (r *ThresholdRule) prepareLinksToTraces(ts time.Time, lbls labels.Labels) s } data, _ := json.Marshal(urlData) - // We need to double encode the composite query to remain compatible with the UI - compositeQuery := url.QueryEscape(url.QueryEscape(string(data))) + compositeQuery := url.QueryEscape(string(data)) optionsData, _ := json.Marshal(options) urlEncodedOptions := url.QueryEscape(string(optionsData)) @@ -828,9 +841,9 @@ func (r *ThresholdRule) hostFromSource() string { return "" } if parsedUrl.Port() != "" { - return fmt.Sprintf("%s:%s", parsedUrl.Hostname(), parsedUrl.Port()) + return fmt.Sprintf("%s://%s:%s", parsedUrl.Scheme, parsedUrl.Hostname(), parsedUrl.Port()) } - return parsedUrl.Hostname() + return fmt.Sprintf("%s://%s", parsedUrl.Scheme, parsedUrl.Hostname()) } func (r *ThresholdRule) prepareClickhouseQueries(ts time.Time) (map[string]string, error) { @@ -1054,23 +1067,26 @@ func (r *ThresholdRule) Eval(ctx context.Context, ts time.Time, queriers *Querie lb.Set(labels.AlertRuleIdLabel, r.ID()) lb.Set(labels.RuleSourceLabel, r.GeneratorURL()) - if r.typ == "TRACES_BASED_ALERT" { - link := r.prepareLinksToTraces(ts, smpl.Metric) - if link != "" && r.hostFromSource() != "" { - lb.Set("RelatedTraces", fmt.Sprintf("%s/traces-explorer?%s", r.hostFromSource(), link)) - } - } else if r.typ == "LOGS_BASED_ALERT" { - link := r.prepareLinksToLogs(ts, smpl.Metric) - if link != "" && r.hostFromSource() != "" { - lb.Set("RelatedLogs", fmt.Sprintf("%s/logs/logs-explorer?%s", r.hostFromSource(), link)) - } - } - annotations := make(labels.Labels, 0, len(r.annotations)) for _, a := range r.annotations { annotations = append(annotations, labels.Label{Name: normalizeLabelName(a.Name), Value: expand(a.Value)}) } + // Links with timestamps should go in annotations since labels + // is used alert grouping, and we want to group alerts with the same + // label set, but different timestamps, together. + if r.typ == "TRACES_BASED_ALERT" { + link := r.prepareLinksToTraces(ts, smpl.MetricOrig) + if link != "" && r.hostFromSource() != "" { + annotations = append(annotations, labels.Label{Name: "related_traces", Value: fmt.Sprintf("%s/traces-explorer?%s", r.hostFromSource(), link)}) + } + } else if r.typ == "LOGS_BASED_ALERT" { + link := r.prepareLinksToLogs(ts, smpl.MetricOrig) + if link != "" && r.hostFromSource() != "" { + annotations = append(annotations, labels.Label{Name: "related_logs", Value: fmt.Sprintf("%s/logs/logs-explorer?%s", r.hostFromSource(), link)}) + } + } + lbs := lb.Labels() h := lbs.Hash() resultFPs[h] = struct{}{} diff --git a/pkg/query-service/telemetry/telemetry.go b/pkg/query-service/telemetry/telemetry.go index 55b9a8a72b..ea93d75a0a 100644 --- a/pkg/query-service/telemetry/telemetry.go +++ b/pkg/query-service/telemetry/telemetry.go @@ -35,6 +35,7 @@ const ( TELEMETRY_LICENSE_ACT_FAILED = "License Activation Failed" TELEMETRY_EVENT_ENVIRONMENT = "Environment" TELEMETRY_EVENT_LANGUAGE = "Language" + TELEMETRY_EVENT_SERVICE = "ServiceName" TELEMETRY_EVENT_LOGS_FILTERS = "Logs Filters" TELEMETRY_EVENT_DISTRIBUTED = "Distributed" TELEMETRY_EVENT_QUERY_RANGE_V3 = "Query Range V3 Metadata" @@ -51,6 +52,7 @@ var SAAS_EVENTS_LIST = map[string]struct{}{ TELEMETRY_EVENT_ACTIVE_USER: {}, TELEMETRY_EVENT_HEART_BEAT: {}, TELEMETRY_EVENT_LANGUAGE: {}, + TELEMETRY_EVENT_SERVICE: {}, TELEMETRY_EVENT_ENVIRONMENT: {}, TELEMETRY_EVENT_USER_INVITATION_SENT: {}, TELEMETRY_EVENT_USER_INVITATION_ACCEPTED: {}, @@ -152,6 +154,7 @@ type Telemetry struct { maxRandInt int rateLimits map[string]int8 activeUser map[string]int8 + patTokenUser bool countUsers int8 mutex sync.RWMutex } @@ -200,11 +203,17 @@ func createTelemetry() { select { case <-activeUserTicker.C: if telemetry.activeUser["logs"] != 0 { - getLogsInfoInLastHeartBeatInterval, err := telemetry.reader.GetLogsInfoInLastHeartBeatInterval(context.Background()) + getLogsInfoInLastHeartBeatInterval, err := telemetry.reader.GetLogsInfoInLastHeartBeatInterval(context.Background(), ACTIVE_USER_DURATION) if err != nil && getLogsInfoInLastHeartBeatInterval == 0 { telemetry.activeUser["logs"] = 0 } } + if telemetry.activeUser["metrics"] != 0 { + getSamplesInfoInLastHeartBeatInterval, err := telemetry.reader.GetSamplesInfoInLastHeartBeatInterval(context.Background(), ACTIVE_USER_DURATION) + if err != nil && getSamplesInfoInLastHeartBeatInterval == 0 { + telemetry.activeUser["metrics"] = 0 + } + } if (telemetry.activeUser["traces"] != 0) || (telemetry.activeUser["metrics"] != 0) || (telemetry.activeUser["logs"] != 0) { telemetry.activeUser["any"] = 1 } @@ -213,7 +222,7 @@ func createTelemetry() { case <-ticker.C: - tagsInfo, _ := telemetry.reader.GetTagsInfoInLastHeartBeatInterval(context.Background()) + tagsInfo, _ := telemetry.reader.GetTagsInfoInLastHeartBeatInterval(context.Background(), HEART_BEAT_DURATION) if len(tagsInfo.Env) != 0 { telemetry.SendEvent(TELEMETRY_EVENT_ENVIRONMENT, map[string]interface{}{"value": tagsInfo.Env}, "") @@ -223,12 +232,18 @@ func createTelemetry() { telemetry.SendEvent(TELEMETRY_EVENT_LANGUAGE, map[string]interface{}{"language": language}, "") } + for service, _ := range tagsInfo.Services { + telemetry.SendEvent(TELEMETRY_EVENT_SERVICE, map[string]interface{}{"serviceName": service}, "") + } + totalSpans, _ := telemetry.reader.GetTotalSpans(context.Background()) - spansInLastHeartBeatInterval, _ := telemetry.reader.GetSpansInLastHeartBeatInterval(context.Background()) - getSamplesInfoInLastHeartBeatInterval, _ := telemetry.reader.GetSamplesInfoInLastHeartBeatInterval(context.Background()) + totalLogs, _ := telemetry.reader.GetTotalLogs(context.Background()) + spansInLastHeartBeatInterval, _ := telemetry.reader.GetSpansInLastHeartBeatInterval(context.Background(), HEART_BEAT_DURATION) + getSamplesInfoInLastHeartBeatInterval, _ := telemetry.reader.GetSamplesInfoInLastHeartBeatInterval(context.Background(), HEART_BEAT_DURATION) + totalSamples, _ := telemetry.reader.GetTotalSamples(context.Background()) tsInfo, _ := telemetry.reader.GetTimeSeriesInfo(context.Background()) - getLogsInfoInLastHeartBeatInterval, _ := telemetry.reader.GetLogsInfoInLastHeartBeatInterval(context.Background()) + getLogsInfoInLastHeartBeatInterval, _ := telemetry.reader.GetLogsInfoInLastHeartBeatInterval(context.Background(), HEART_BEAT_DURATION) traceTTL, _ := telemetry.reader.GetTTL(context.Background(), &model.GetTTLParams{Type: constants.TraceTTL}) metricsTTL, _ := telemetry.reader.GetTTL(context.Background(), &model.GetTTLParams{Type: constants.MetricsTTL}) @@ -237,13 +252,17 @@ func createTelemetry() { data := map[string]interface{}{ "totalSpans": totalSpans, "spansInLastHeartBeatInterval": spansInLastHeartBeatInterval, + "totalSamples": totalSamples, "getSamplesInfoInLastHeartBeatInterval": getSamplesInfoInLastHeartBeatInterval, + "totalLogs": totalLogs, "getLogsInfoInLastHeartBeatInterval": getLogsInfoInLastHeartBeatInterval, "countUsers": telemetry.countUsers, "metricsTTLStatus": metricsTTL.Status, "tracesTTLStatus": traceTTL.Status, "logsTTLStatus": logsTTL.Status, + "patUser": telemetry.patTokenUser, } + telemetry.patTokenUser = false for key, value := range tsInfo { data[key] = value } @@ -266,7 +285,7 @@ func createTelemetry() { "tracesBasedAlerts": alertsInfo.TracesBasedAlerts, } // send event only if there are dashboards or alerts - if dashboardsInfo.TotalDashboards > 0 || alertsInfo.TotalAlerts > 0 { + if dashboardsInfo.TotalDashboards > 0 || alertsInfo.TotalAlerts > 0 { telemetry.SendEvent(TELEMETRY_EVENT_DASHBOARDS_ALERTS, dashboardsAlertsData, "") } } else { @@ -346,6 +365,10 @@ func (a *Telemetry) SetUserEmail(email string) { a.userEmail = email } +func (a *Telemetry) SetPatTokenUser() { + a.patTokenUser = true +} + func (a *Telemetry) GetUserEmail() string { return a.userEmail } diff --git a/pkg/query-service/tests/test-deploy/docker-compose.yaml b/pkg/query-service/tests/test-deploy/docker-compose.yaml index 7c9b50199f..d88945b312 100644 --- a/pkg/query-service/tests/test-deploy/docker-compose.yaml +++ b/pkg/query-service/tests/test-deploy/docker-compose.yaml @@ -192,7 +192,7 @@ services: <<: *db-depend otel-collector-migrator: - image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.88.12} + image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.88.13} container_name: otel-migrator command: - "--dsn=tcp://clickhouse:9000" @@ -205,7 +205,7 @@ services: # condition: service_healthy otel-collector: - image: signoz/signoz-otel-collector:0.88.12 + image: signoz/signoz-otel-collector:0.88.13 container_name: signoz-otel-collector command: [